148 lines
9.8 KiB
JavaScript
148 lines
9.8 KiB
JavaScript
const { test, before, after } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { spawn } = require('node:child_process');
|
|
const { chromium } = require('playwright');
|
|
let server, browser, base;
|
|
before(async () => {
|
|
server = spawn('bun', ['tests/fixtures/party-server.mjs'], { cwd: require('node:path').resolve(__dirname, '..'), stdio: ['ignore','pipe','pipe'] });
|
|
base = await new Promise((resolve, reject) => { server.stdout.once('data', x => resolve(String(x).trim())); server.once('error', reject); server.once('exit', code => reject(new Error(`server exit ${code}`))); });
|
|
browser = await chromium.launch({ headless: true });
|
|
});
|
|
after(async () => { await browser?.close(); server?.kill(); });
|
|
async function host(viewport = { width: 1280, height: 900 }) {
|
|
const context = await browser.newContext({ viewport }); const page = await context.newPage();
|
|
const errors = []; page.on('pageerror', e => errors.push(e.message));
|
|
await page.goto(base); await page.click('#navPartyBtn');
|
|
await page.fill('.pt-name', 'Alex'); await page.click('[data-act=start]');
|
|
await page.waitForFunction(() => /^[A-Z0-9]{6}$/.test(document.querySelector('#ptRoomCode')?.textContent));
|
|
return { context, page, errors, code: await page.locator('#ptRoomCode').textContent() };
|
|
}
|
|
async function guest(code) {
|
|
const context = await browser.newContext(); const page = await context.newPage(); await page.goto(base);
|
|
await page.evaluate(code => YT.Party._join(code, 'Sam'), code);
|
|
await page.waitForFunction(() => YT.Party.state().members === 2);
|
|
return { context, page };
|
|
}
|
|
test('host transport stays available, drafts survive membership updates, chat and room controls work', async () => {
|
|
const h = await host();
|
|
await h.page.evaluate(() => { current = { meta: { id: 'demo', title: 'Evening session' } }; YT.Party.afterLoad(); });
|
|
await h.page.fill('.pt-msg', 'Draft that must survive');
|
|
const g = await guest(h.code);
|
|
await h.page.waitForFunction(() => YT.Party.state().members === 2);
|
|
assert.equal(await h.page.inputValue('.pt-msg'), 'Draft that must survive');
|
|
await h.page.click('[data-act=playback]');
|
|
assert.equal(await h.page.evaluate(() => Player.master.paused), false);
|
|
await h.page.click('[data-act=forward10]'); assert.equal(await h.page.evaluate(() => Player.master.currentTime), 10);
|
|
await h.page.click('[data-act=next]'); assert.equal(await h.page.evaluate(() => nextCount), 1);
|
|
await h.page.locator('.pt-send button').click();
|
|
await g.page.waitForFunction(() => document.querySelector('#ptChat').textContent.includes('Draft that must survive'));
|
|
await h.page.click('[data-tab=room]');
|
|
assert.equal(await h.page.locator('[data-act=playback]').isVisible(), true);
|
|
await h.page.check('[data-act=audio]'); assert.equal(await h.page.evaluate(() => data.settings.audioOnly), true);
|
|
await h.page.uncheck('[data-act=gentle]'); assert.equal(await h.page.evaluate(() => localStorage.getItem('ytpPartyGentle')), 'false');
|
|
assert.deepEqual(h.errors, []);
|
|
await g.context.close(); await h.context.close();
|
|
});
|
|
test('buffering guest does not repeatedly seek; ready playback recovers and cooldown prevents thrashing', async () => {
|
|
const h = await host();
|
|
await h.page.evaluate(() => { current = { meta: { id: 'demo', title: 'Evening session' } }; Player.master.paused = false; YT.Party.afterLoad(); });
|
|
const g = await guest(h.code);
|
|
await g.page.waitForFunction(() => current?.meta?.id === 'demo');
|
|
await g.page.evaluate(() => { Player.master.readyState = 2; Player.master.currentTime = 0; seekCount = 0; });
|
|
await h.page.evaluate(() => { Player.master.currentTime = 60; YT.Party.afterLoad(); });
|
|
await g.page.waitForFunction(() => document.querySelector('#ptStatus').textContent.includes('Buffering'));
|
|
assert.equal(await g.page.evaluate(() => seekCount), 0);
|
|
await g.page.evaluate(() => { Player.master.readyState = 4; });
|
|
await h.page.evaluate(() => YT.Party.afterLoad());
|
|
await g.page.waitForFunction(() => seekCount === 1);
|
|
await h.page.evaluate(() => { Player.master.currentTime = 90; YT.Party.afterLoad(); });
|
|
await g.page.waitForTimeout(150);
|
|
assert.equal(await g.page.evaluate(() => seekCount), 1);
|
|
await g.context.close(); await h.context.close();
|
|
});
|
|
test('membership preserves input node; mobile panel and host controls fit above navigation', async () => {
|
|
const h = await host({ width: 390, height: 844 });
|
|
await h.page.fill('.pt-msg', 'Still typing');
|
|
await h.page.evaluate(() => { window.draftNode = document.querySelector('.pt-msg'); });
|
|
const g = await guest(h.code);
|
|
assert.equal(await h.page.evaluate(() => draftNode === document.querySelector('.pt-msg')), true);
|
|
await h.page.click('[data-tab=queue]'); await h.page.click('[data-tab=room]');
|
|
const fit = await h.page.evaluate(() => {
|
|
const panel = document.querySelector('#partyPanel').getBoundingClientRect();
|
|
const controls = document.querySelector('.pt-transport').getBoundingClientRect();
|
|
return panel.left >= 0 && panel.right <= innerWidth && panel.bottom <= innerHeight - 96 && controls.bottom <= panel.bottom;
|
|
});
|
|
assert.equal(fit, true);
|
|
await h.page.setViewportSize({width:320,height:568}); await h.page.click('[data-tab=chat]');
|
|
assert.equal(await h.page.locator('.pt-send').evaluate(e => e.getBoundingClientRect().bottom <= document.querySelector('#partyPanel').getBoundingClientRect().bottom), true);
|
|
await h.page.click('#ptMin');
|
|
assert.equal(await h.page.locator('#ptMin').isVisible(), true);
|
|
assert.equal(await h.page.locator('#partyPanel').evaluate(e => e.getBoundingClientRect().bottom <= innerHeight - 96), true);
|
|
await h.page.click('#ptMin');
|
|
assert.deepEqual(h.errors, []);
|
|
await g.context.close(); await h.context.close();
|
|
});
|
|
test('queue votes preserve search, chat preserves history position, reconnect keeps drafts', async () => {
|
|
const h = await host(); const g = await guest(h.code);
|
|
await h.page.click('[data-tab=queue]');
|
|
await h.page.locator('.pt-dj input[type=checkbox]').check();
|
|
await h.page.locator('.party-dj-search input').fill('Evening');
|
|
await h.page.locator('.party-dj-search button').click();
|
|
await h.page.getByRole('button', {name:'Suggest: Evening session', exact:true}).click();
|
|
await h.page.waitForFunction(() => document.querySelector('.party-dj-row'));
|
|
assert.equal(await h.page.inputValue('.party-dj-search input'), 'Evening');
|
|
await h.page.click('[data-tab=chat]');
|
|
for (let i = 0; i < 12; i++) {
|
|
await g.page.fill('.pt-msg', `Message ${i}: This is a conversation to test reading earlier messages.`);
|
|
await g.page.locator('.pt-send button').click(); await g.page.waitForTimeout(410);
|
|
}
|
|
await h.page.evaluate(() => { document.querySelector('#ptChat').scrollTop = 0; });
|
|
await g.page.fill('.pt-msg', 'A new message while you read'); await g.page.locator('.pt-send button').click();
|
|
await h.page.waitForFunction(() => !document.querySelector('.pt-new').hidden);
|
|
assert.equal(await h.page.locator('#ptChat').evaluate(e => e.scrollTop), 0);
|
|
await h.page.click('[data-act=latest]');
|
|
assert.equal(await h.page.locator('.pt-new').isVisible(), false);
|
|
await h.page.fill('.pt-msg', 'Keep this offline draft'); await h.context.setOffline(true);
|
|
await h.page.locator('.pt-send button').click();
|
|
assert.equal(await h.page.inputValue('.pt-msg'), 'Keep this offline draft');
|
|
await h.context.setOffline(false);
|
|
await h.page.waitForFunction(() => document.querySelector('#ptStatus').textContent.includes('hosting'));
|
|
assert.equal(await h.page.inputValue('.pt-msg'), 'Keep this offline draft');
|
|
assert.deepEqual(h.errors, []);
|
|
await g.context.close(); await h.context.close();
|
|
});
|
|
test('heartbeat measures round trip and screenshots cover desktop and mobile host controls', async () => {
|
|
const h = await host();
|
|
await h.page.evaluate(() => { current = { meta: { id:'demo', title:'Evening session · acoustic favorites' } }; Player.master.paused = false; YT.Party.afterLoad(); });
|
|
const g = await guest(h.code);
|
|
await g.page.fill('.pt-msg', 'Ready when you are!'); await g.page.locator('.pt-send button').click();
|
|
await h.page.waitForFunction(() => document.querySelector('#ptChat').textContent.includes('Ready when you are'));
|
|
await h.page.click('[data-tab=room]');
|
|
await h.page.waitForFunction(() => document.querySelector('#ptLatency').textContent.includes('ms round trip'));
|
|
await h.page.click('[data-tab=chat]');
|
|
if (process.env.PARTY_SCREENSHOTS) {
|
|
const fs = require('node:fs'); fs.mkdirSync(process.env.PARTY_SCREENSHOTS, {recursive:true});
|
|
await h.page.screenshot({path: `${process.env.PARTY_SCREENSHOTS}/desktop.png`});
|
|
await h.page.setViewportSize({width:390,height:844});
|
|
await h.page.screenshot({path: `${process.env.PARTY_SCREENSHOTS}/mobile.png`});
|
|
await h.page.click('[data-tab=room]');
|
|
await h.page.screenshot({path: `${process.env.PARTY_SCREENSHOTS}/mobile-room.png`});
|
|
}
|
|
assert.deepEqual(h.errors, []);
|
|
await g.context.close(); await h.context.close();
|
|
});
|
|
|
|
test('independent guest reconnects chat without resuming host playback', async () => {
|
|
const h = await host(); const g = await guest(h.code);
|
|
await g.page.waitForTimeout(1600);
|
|
await g.page.click('.pt-msg');
|
|
await g.page.evaluate(() => { Player.master.paused = true; Player.master.dispatchEvent(new Event('pause')); });
|
|
assert.equal(await g.page.evaluate(() => YT.Party.state().detached), true);
|
|
await g.context.setOffline(true); await g.context.setOffline(false);
|
|
await g.page.waitForFunction(() => document.querySelector('#ptStatus').textContent === 'Watching independently');
|
|
assert.equal(await g.page.evaluate(() => YT.Party.state().detached), true);
|
|
await g.page.fill('.pt-msg', 'Still here, watching independently'); await g.page.locator('.pt-send button').click();
|
|
await h.page.waitForFunction(() => document.querySelector('#ptChat').textContent.includes('Still here, watching independently'));
|
|
await g.context.close(); await h.context.close();
|
|
});
|