Watch party refresh: Chat/Queue/Room tabs, host transport, gentle sync, heartbeat and latency-aware reconnect, in-place section updates

This commit is contained in:
Jonathan Sykes
2026-10-15 20:19:44 +08:00
parent 23097918e9
commit 088937d4de
12 changed files with 550 additions and 84 deletions

28
tests/fixtures/party-fixture.js vendored Normal file
View File

@@ -0,0 +1,28 @@
if (document.currentScript.src.includes('?boot')) {
window.YT.Party.boot();
document.getElementById('navPartyBtn').onclick = () => window.YT.Party.open();
} else {
Object.assign(window, {
WEB: true, $: id => document.getElementById(id),
data: { settings: {}, profile: null }, current: null, cachedIds: new Set(),
DirectMedia: { attach() {}, enabled: () => false, handle() {} },
Lazy: { load: () => Promise.resolve(), loaded: () => true },
Notes: { stageLyrics: { on: () => false, set() {} } },
API: { search: async () => ({ results: [{ id: 'demo-video1', title: 'Evening session' }] }) },
toast: text => { window.lastToast = text; }, escapeHtml: s => String(s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])),
fmtTime: String, updatePlayBtn() {}, advanceQueue() {},
TransportIcons: { svg: () => '' }, seekCount: 0, nextCount: 0,
});
window.els = { video: document.createElement('video'), audio: document.createElement('audio'), speed: { value: '1' }, audioOnlyToggle: document.createElement('input') };
window.Player = {
master: Object.assign(new EventTarget(), { currentTime: 0, paused: true, playbackRate: 1, readyState: 4, seeking: false, duration: 3600 }),
play() { this.master.paused = false; }, pause() { this.master.paused = true; },
toggle() { this.master.paused = !this.master.paused; },
seek(t) { this.master.currentTime = t; window.seekCount++; }, applySpeed() { this.master.playbackRate = Number(els.speed.value); },
};
els.video = Player.master;
window.playVideoAt = async (v, t = 0) => { window.current = { meta: v }; Player.master.currentTime = t; Player.master.paused = false; };
window.playNext = () => { window.nextCount++; };
window.playPrev = () => {};
els.audioOnlyToggle.addEventListener('change', () => { data.settings.audioOnly = els.audioOnlyToggle.checked; });
}

16
tests/fixtures/party-server.mjs vendored Normal file
View File

@@ -0,0 +1,16 @@
// Isolated integration harness: real party relay and frontend chunk; deterministic player.
import { createPartyHub } from '../../server/party.js';
import { join } from 'node:path';
const root = new URL('../../frontend/', import.meta.url).pathname;
const hub = createPartyHub();
const server = Bun.serve({ hostname: '127.0.0.1', port: Number(process.env.PARTY_TEST_PORT || 0),
async fetch(req, srv) {
const path = new URL(req.url).pathname;
if (path === '/ws/party') return hub.upgrade(req, srv, '127.0.0.1');
if (path === '/') return new Response(await Bun.file(new URL('party.html', import.meta.url)).text(), { headers: { 'Content-Type': 'text/html' } });
if (path === '/party-fixture.js') return new Response(Bun.file(new URL('party-fixture.js', import.meta.url)));
if (path.includes('..')) return new Response('', {status: 400});
return new Response(Bun.file(join(root, path)));
}, websocket: hub.websocket,
});
console.log(`http://127.0.0.1:${server.port}`);

4
tests/fixtures/party.html vendored Normal file
View File

@@ -0,0 +1,4 @@
<!doctype html><html lang="en" data-layout="classic"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Watch party integration fixture</title><link rel="stylesheet" href="/styles.css"><link rel="stylesheet" href="/theme-classic.css"></head><body>
<main style="padding:32px;max-width:600px"><h1>Watch party</h1><p>Integration preview · deterministic playback fixture</p><button id="navPartyBtn"><span class="nav-label">Watch party</span></button></main>
<aside id="partyPanel" class="party-panel hidden" aria-label="Watch party"><div class="pt-head"><span id="ptTitle">Watch party</span><span id="ptStatus" class="pt-status" role="status"></span><button id="ptMin" class="rv-icon" type="button" aria-label="Minimise">▁</button></div><div class="pt-body"></div></aside>
<script src="/party-fixture.js"></script><script src="/party-dj.js"></script><script src="/watch-party-client.js"></script><script src="/party-fixture.js?boot"></script></body></html>