const { test, expect } = require('@playwright/test'); const { openClassic } = require('./helpers/classic-fixture'); for (const layout of ['classic', 'glass-stage']) for (const width of [390, 1440]) { test(`${layout} settings navigation at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 900 }); await openClassic(page); await page.evaluate(layout => { data.settings.layout = layout; applyAppearance(); view = { type: 'settings' }; render(); }, layout); await expect(page.locator('.settings-section-row').first()).toBeVisible(); await expect(page.locator('#setQuality')).toHaveCount(1); if (width === 390) await expect(page.locator('#setQuality')).toBeHidden(); await page.locator('.settings-search input').fill('default quality'); await page.locator('.settings-results button').first().click(); await expect(page.locator('#setQuality')).toBeVisible(); await page.locator('#setQuality').selectOption('720p'); expect(await page.evaluate(() => data.settings.quality)).toBe('720p'); if (width === 390) { await page.goBack(); await expect(page.locator('.settings-overview')).toBeVisible(); await expect(page.locator('#setQuality')).toBeHidden(); } await page.locator('.settings-section-row[data-section="appearance"]').click(); await expect(page.locator('#setTheme')).toBeVisible(); await page.locator('#setTheme').selectOption('light'); expect(await page.evaluate(() => data.settings.theme)).toBe('light'); await expect(page.locator('#setQuality')).toHaveValue('720p'); expect(await page.evaluate(() => sessionStorage.getItem('ytpSettingsSection'))).toBe('appearance'); const overflow = await page.locator('.settings-browser').evaluate(el => el.scrollWidth > el.clientWidth + 1); expect(overflow).toBe(false); }); } test('floating lyrics move the real view and restore it on close', async ({ page }) => { await page.addInitScript(() => { window.documentPictureInPicture = { requestWindow: async () => window.open('about:blank', 'lyrics-test', 'width=420,height=500') }; }); await openClassic(page); const popupPromise = page.waitForEvent('popup'); await page.locator('#stageLyricsAuto').evaluate(el => el.nextElementSibling.click()); const popup = await popupPromise; await expect(popup.locator('#stageLyrics')).toBeVisible(); await expect(popup.getByRole('button', { name: 'Play / Pause', exact: true })).toBeVisible(); await expect(page.locator('#stageLyrics')).toHaveCount(0); await popup.close(); await expect(page.locator('#stageLyrics')).toHaveCount(1); }); test('OBS lower third has a transparent background and renders two cues', async ({ page }) => { await page.route('**/api/**', route => route.fulfill({ contentType: 'application/json', body: '{"ok":false}' })); await page.goto('/?overlay=lowerthird&code=123456'); await page.evaluate(() => LowerThird.paint([{ kind: 'line', text: 'Current lyric' }, { kind: 'line', text: 'Next lyric' }], 0)); await expect(page.locator('.obs-current')).toHaveText('Current lyric'); await expect(page.locator('.obs-next')).toHaveText('Next lyric'); expect(await page.evaluate(() => getComputedStyle(document.body).backgroundColor)).toBe('rgba(0, 0, 0, 0)'); await expect(page.locator('#searchInput')).toBeHidden(); }); test('setlist import confirms library matches and retains key tags', async ({ page }) => { await openClassic(page, { populated: true }); await page.evaluate(() => SetlistImport.open()); await page.getByLabel('Setlist text', { exact: true }).fill('1. A quiet morning — live studio session [E]'); await page.getByRole('button', { name: 'Find matches', exact: true }).click(); await expect(page.getByLabel('Match A quiet morning — live studio session', { exact: true })).toHaveValue('preview0000'); await page.getByRole('button', { name: 'Create setlist', exact: true }).click(); expect(await page.evaluate(() => data.playlists.at(-1).videos[0].tags)).toContain('Key E'); }); for (const layout of ['classic', 'glass-stage']) for (const width of [390, 1440]) test(`${layout} Car mode at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 900 }); await openClassic(page); await page.evaluate(layout => { data.settings.layout = layout; applyAppearance(); CarMode.open(); }, layout); await expect(page.getByRole('dialog', { name: 'Car mode' })).toBeVisible(); expect(await page.locator('.car-controls button').first().evaluate(el => el.getBoundingClientRect().height)).toBeGreaterThanOrEqual(100); expect(await page.locator('.app').evaluate(el => el.inert)).toBe(true); await page.getByRole('button', { name: 'Exit Car mode' }).click(); await expect(page.locator('.car-mode')).toHaveCount(0); expect(await page.locator('.app').evaluate(el => el.inert)).toBe(false); }); for (const layout of ['classic', 'glass-stage']) for (const width of [390, 1440]) test(`${layout} ten-band EQ at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 900 }); await openClassic(page, { settings: { eq: { preset: 'custom', g: [6,2,-2,4,0] } } }); await page.evaluate(layout => { data.settings.layout = layout; applyAppearance(); EQ.open(); }, layout); await expect(page.locator('.eq-band input')).toHaveCount(10); expect(await page.evaluate(() => data.settings.eq.g.length)).toBe(10); await page.getByRole('button', { name: 'Vocal clarity', exact: true }).click(); expect(await page.evaluate(() => data.settings.eq.g[7])).toBe(4); expect(await page.locator('.eq-panel').evaluate(el => el.scrollWidth <= el.clientWidth + 1)).toBe(true); }); for (const layout of ['classic', 'glass-stage']) test(`${layout} secondary lyrics in stage and Service mode`, async ({ page }) => { await openClassic(page); await page.route('**/api/notes/0gfX0dFLaBc', route => route.fulfill({ contentType: 'application/json', body: JSON.stringify({ ok:true, lyrics:{rev:1,data:{offset:0,lines:[{t:0,text:'主你的恩典',secondary:'Your grace is enough',phonetic:'zhǔ nǐ de ēndiǎn',kind:'line'}]}}, chapters:null }) })); await page.evaluate(async layout => { data.settings.layout = layout; applyAppearance(); current = {meta:{id:'0gfX0dFLaBc',title:'Fixture'}}; await Notes.load(current.meta); Notes.stageLyrics.set(true); }, layout); await expect(page.locator('#stageLyricsList .lyric-secondary').first()).toHaveText('Your grace is enough'); await page.evaluate(() => { $('serviceMode').classList.remove('hidden'); data.settings.serviceLyrics = true; Notes.serviceRefresh(); }); await expect(page.locator('#serviceLyricsList .lyric-phonetic')).toHaveText('zhǔ nǐ de ēndiǎn'); expect(await page.locator('#serviceLyricsList .lyric-secondary').first().evaluate(el => parseFloat(getComputedStyle(el).fontSize) < parseFloat(getComputedStyle(el.parentElement).fontSize))).toBe(true); }); for (const layout of ['classic', 'glass-stage']) for (const width of [390,1440]) test(`${layout} piano settings stay lazy at ${width}px`, async ({ page }) => { const models=[];page.on('request',req=>{if(/esm\.sh|jsdelivr.*basic-pitch/.test(req.url()))models.push(req.url());}); await page.setViewportSize({width,height:900});await openClassic(page);await page.evaluate(layout=>{data.settings.layout=layout;applyAppearance();current={meta:{id:'0gfX0dFLaBc',title:'Piano fixture',duration:200}};view={type:'settings'};render();},layout); await page.locator('[data-section="piano"]').click();await expect(page.getByRole('button',{name:'Transcribe in browser',exact:true})).toBeDisabled(); await page.locator('#settings-section-piano input[type="checkbox"]').check();await expect(page.getByRole('button',{name:'Transcribe in browser',exact:true})).toBeEnabled(); await page.getByRole('button',{name:'Add current 8 s to practice',exact:true}).click();await expect(page.locator('.piano-practice button').first()).toHaveText('Loop 0.0–8.0 s'); expect(models).toEqual([]);expect(await page.locator('#settings-section-piano').evaluate(el=>el.scrollWidth<=el.clientWidth+1)).toBe(true); });