diff --git a/tests/lifecycle/v4v-native-login.cjs b/tests/lifecycle/v4v-native-login.cjs index 5af60115..0ef3ef48 100644 --- a/tests/lifecycle/v4v-native-login.cjs +++ b/tests/lifecycle/v4v-native-login.cjs @@ -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 }); diff --git a/tests/lifecycle/v4v-native-playback.cjs b/tests/lifecycle/v4v-native-playback.cjs index 63678aa7..f2496b3d 100644 --- a/tests/lifecycle/v4v-native-playback.cjs +++ b/tests/lifecycle/v4v-native-playback.cjs @@ -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');