Verify rendered native player artwork and expose missing manifest entry paths

This commit is contained in:
archipelago
2026-10-06 20:31:00 -04:00
parent 057150d377
commit a876dc3d0b
2 changed files with 32 additions and 2 deletions
+2 -1
View File
@@ -72,6 +72,7 @@ const { chromium, expect } = require(process.env.PLAYWRIGHT_MODULE || '../../neo
const frame = page.frameLocator('iframe[src*="7475"]');
const signIn = frame.getByRole('button', { name: 'Sign in with Nostr', exact: true });
await expect(signIn).toBeVisible({ timeout: 45000 });
proof.initialLaunchPath = await page.locator('iframe[src*="7475"]').evaluate(frame => new URL(frame.src).pathname);
await expect(frame.locator('input[type=password]')).toHaveCount(0);
// A fresh browser gets the real app introduction before authentication.
// Drive its visible controls instead of clicking through the overlay.
@@ -99,7 +100,7 @@ const { chromium, expect } = require(process.env.PLAYWRIGHT_MODULE || '../../neo
await expect(signIn).toHaveCount(0, { timeout: 15000 });
step = 'native playback controls';
if (process.env.QUALIFY_PLAYBACK === '1') await require('./v4v-native-playback.cjs')(page, appOrigin, proof);
console.log(JSON.stringify({ ...proof, result: 'PASS', scope: process.env.QUALIFY_PLAYBACK === '1' ? 'real managed V4V native login and bundled-song playback' : 'real managed V4V native authentication only; playback remains separately qualified' }));
console.log(JSON.stringify({ ...proof, result: proof.initialBrowse === false ? 'PARTIAL_PASS_BROWSE_OPEN' : 'PASS', scope: process.env.QUALIFY_PLAYBACK === '1' ? 'real managed V4V native login and bundled-song playback' : 'real managed V4V native authentication only; playback remains separately qualified' }));
} catch (error) {
const prefix = '/tmp/archy-v4v-native-failure-' + width;
fs.writeFileSync(prefix + '.private.txt', String(error.stack || error), { mode: 0o600 });
+30 -1
View File
@@ -4,6 +4,11 @@ const { expect } = require(process.env.PLAYWRIGHT_MODULE || '../../neode-ui/node
module.exports = async function qualifyPlayback(page, appOrigin, proof) {
const app = page.frames().find(frame => frame.url().startsWith(appOrigin + '/'));
if (!app) throw Error('Managed V4V iframe missing after login');
proof.initialBrowse = new URL(app.url()).pathname === '/browse';
if (process.env.QUALIFY_ARTWORK_ONLY !== '1') {
await expect.poll(() => new URL(app.url()).pathname).toBe('/browse');
proof.initialBrowse = true;
}
await app.waitForFunction(() => window.PulsewireLegacy?.getReactBridge?.(), undefined, { timeout: 30000 });
const later = app.getByRole('button', { name: 'Not now', exact: true });
if (await later.isVisible().catch(() => false)) await later.click();
@@ -56,7 +61,31 @@ module.exports = async function qualifyPlayback(page, appOrigin, proof) {
await expect(page.getByRole('button', { name: 'Shuffle', exact: true })).toHaveAttribute('aria-pressed', 'true');
expect(await app.evaluate(() => window.PulsewireLegacy.getReactBridge().getState().ui.shuffleEnabled)).toBe(true);
const artwork = await app.evaluate(() => window.PulsewireLegacy.getReactBridge().helpers.activePlayerRelease()?.cover || '');
if (artwork) await expect(bar.locator('img')).toHaveAttribute('src', new URL(artwork, appOrigin).href);
if (artwork) {
const background = bar.locator('img.player-artwork');
await expect(background).toHaveAttribute('src', new URL(artwork, appOrigin).href);
await expect.poll(() => background.evaluate(img => img.complete && img.naturalWidth > 0)).toBe(true);
const geometry = await background.evaluate(img => {
const image = img.getBoundingClientRect(); const bar = img.parentElement.getBoundingClientRect();
const style = getComputedStyle(img);
return { image: { x: image.x, y: image.y, width: image.width, height: image.height }, bar: { x: bar.x, y: bar.y, width: bar.width, height: bar.height }, opacity: Number(style.opacity), fit: style.objectFit };
});
expect(geometry.opacity).toBeGreaterThan(0); expect(geometry.fit).toBe('cover');
for (const axis of ['x', 'y', 'width', 'height']) expect(Math.abs(geometry.image[axis] - geometry.bar[axis])).toBeLessThanOrEqual(2);
proof.renderedArtwork = true;
}
const barBounds = await bar.boundingBox();
for (const name of ['Previous track', 'Next track', 'Open playing app']) {
const control = bar.getByRole('button', { name, exact: true });
await expect(control).toBeVisible(); const bounds = await control.boundingBox();
expect(bounds.x).toBeGreaterThanOrEqual(barBounds.x);
expect(bounds.x + bounds.width).toBeLessThanOrEqual(barBounds.x + barBounds.width);
expect(bounds.y).toBeGreaterThanOrEqual(barBounds.y);
expect(bounds.y + bounds.height).toBeLessThanOrEqual(barBounds.y + barBounds.height);
}
const screenshot = '/tmp/archy-v4v-live-player-' + proof.width + '.png';
await page.screenshot({ path: screenshot }); require('node:fs').chmodSync(screenshot, 0o600);
const beforeReopen = await app.evaluate(() => window.__qualificationMedia.find(audio => !audio.paused).currentTime);
await page.getByRole('button', { name: 'Open playing app', exact: true }).click();
if (!page.frames().includes(app)) throw Error('Reopening recreated the playback iframe');