diff options
Diffstat (limited to 'tools/live-stream-test.js')
| -rw-r--r-- | tools/live-stream-test.js | 48 |
1 files changed, 40 insertions, 8 deletions
diff --git a/tools/live-stream-test.js b/tools/live-stream-test.js index 0deb48c..ef76043 100644 --- a/tools/live-stream-test.js +++ b/tools/live-stream-test.js @@ -55,9 +55,17 @@ const pastPage = finalizeEvent({ kind: 30311, created_at: now - 50000, content: ['d', 'stream-p'], ['title', 'Zelda on Rumble'], ['status', 'ended'], ['streaming', STREAM], ['recording', 'https://rumble.com/v7gt2va-zelda.html'], ['ends', String(now - 50000)], ['p', HOST.pk, '', 'Host'], ] }, SERVICE.sk); +// who's watching (NIP-53 presence): the fan, just now; someone two hours ago is gone +const here = finalizeEvent({ kind: 10312, created_at: now - 60, content: '', tags: [['a', A, '', 'root']] }, FAN.sk); +const longGone = finalizeEvent({ kind: 10312, created_at: now - 7200, content: '', tags: [['a', A, '', 'root']] }, STALE.sk); const reaction = finalizeEvent({kind:7,created_at:now-10,content:'👍',tags:[['e',chat.id],['p',FAN.pk],['k','1311']]},HOST.sk); const messageZap = finalizeEvent({kind:9737,created_at:now-9,content:'nice comment',tags:[['e',chat.id],['p',FAN.pk],['P',HOST.pk],['amount','5'],['network','regtest']]},HOST.sk); -const FIXTURES = [live, stale, past, pastPage, chat, zap, reaction, messageZap]; +// and months of older ones, past the first ask's month: the grid pages back through them +const OLD = Array.from({ length: 130 }, (_, i) => finalizeEvent({ kind: 30311, created_at: now - 31 * 86400 - i * 3600, content: '', tags: [ + ['d', 'old-' + i], ['title', 'Old stream ' + i], ['status', 'ended'], ['streaming', STREAM], ['recording', STREAM], + ['ends', String(now - 31 * 86400 - i * 3600)], ['p', HOST.pk, '', 'Host'], +] }, SERVICE.sk)); +const FIXTURES = [live, stale, past, pastPage, chat, zap, reaction, messageZap, here, longGone, ...OLD]; const ended = finalizeEvent({ ...live, id: undefined, sig: undefined, created_at: now - 5, tags: live.tags.map((x) => (x[0] === 'status' ? ['status', 'ended'] : x)) }, SERVICE.sk); // the site as built (the split build: hls.js arrives as its own chunk) @@ -81,7 +89,7 @@ const relayStub = (fx) => { // a new event, to every open subscription it matches (a relay's live feed) window.__push = (e) => { for (const [ws, id, fs] of subs) if (ws.readyState === 1 && fs.some((f) => match(f, e))) ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EVENT', id, e]) })); }; const match = (f, e) => (!f.kinds || f.kinds.includes(e.kind)) && (!f.authors || f.authors.includes(e.pubkey)) - && (!f.since || e.created_at >= f.since) + && (!f.since || e.created_at >= f.since) && (!f.until || e.created_at <= f.until) && Object.keys(f).filter((k) => k[0] === '#').every((k) => e.tags.some((x) => x[0] === k.slice(1) && f[k].includes(x[1]))); const Real = window.WebSocket; window.WebSocket = function (...a) { @@ -92,8 +100,9 @@ const relayStub = (fx) => { const m = JSON.parse(data); if (m[0] === 'REQ' && m[2]) { window.__reqs.push([a[0], ...m.slice(2)]); - subs.push([ws, m[1], m.slice(2).map((f) => ({ ...f, since: undefined, limit: undefined }))]); - const hits = [...fx, ...window.__published].filter((e) => m.slice(2).some((f) => match(f, e))); + subs.push([ws, m[1], m.slice(2).map((f) => ({ ...f, since: undefined, until: undefined, limit: undefined }))]); + // each filter its newest `limit`, as a relay answers + const hits = [...new Set(m.slice(2).flatMap((f) => [...fx, ...window.__published].filter((e) => match(f, e)).sort((x, y) => y.created_at - x.created_at).slice(0, f.limit || Infinity)))]; if (hits.length) setTimeout(() => { for (const e of hits) ws.dispatchEvent(new MessageEvent('message', { data: JSON.stringify(['EVENT', m[1], e]) })); }, 0); } if (m[0] === 'EVENT' && m[1] && !window.__published.some(e => e.id === m[1].id)) { window.__published.push(m[1]); setTimeout(() => window.__push(m[1]), 0); } @@ -168,6 +177,20 @@ try { check('...titled, marked LIVE, with how many watch and who', tile.length && /LIVE/.test(tile[0]) && /12 watching/.test(tile[0]) && /Building coinos live/.test(tile[0]), JSON.stringify(tile)); check('past streams follow, the recording kept, its end time shown', await waitFor(() => [...document.querySelectorAll('.live-past .live-tile')].some((t) => /Yesterday on coinos/.test(t.innerText) && /RECORDING/.test(t.innerText)), 8000) && await page.evaluate(() => /Past streams/.test(document.body.innerText))); + console.log('\n[past streams scroll on]'); + const pastCount = () => page.evaluate(() => document.querySelectorAll('.live-past .live-tile').length); + const first = await pastCount(); + check('a page of them at first, not all', first > 0 && first <= 30, first + ' tiles'); + let last = first; + for (let i = 0; i < 120 && !(await page.evaluate(() => document.body.innerText.includes('Old stream 129'))); i++) { // (this relay never says EOSE: each page waits out its 5 s) + await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight)); + await sleep(250); + last = await pastCount(); + } + check('scrolling down brings older ones, from the relays, back to the oldest', await page.evaluate(() => document.body.innerText.includes('Old stream 129')), last + ' tiles'); + check('...asked for page by page (until the oldest so far)', await page.evaluate(() => window.__reqs.filter((r) => r.slice(1).some((f) => f.kinds && f.kinds.includes(30311) && f.until)).length >= 2)); + check('...all of them there, once each', last === 132 && await page.evaluate(() => new Set([...document.querySelectorAll('.live-past .live-tile')].map((t) => t.dataset.key)).size === 132), last + ' tiles'); + await page.evaluate(() => window.scrollTo(0, 0)); check('...and a stream unrefreshed for two hours is not there', tile.length === 1 && !(await page.evaluate(() => document.body.innerText.includes('Forgotten stream')))); await page.evaluate(() => document.querySelector('.media-mode[data-mode="all"]').click()); check('a post linking the stream shows it as a card', await waitFor(() => !!document.querySelector('.notes-feed .live-card'), 15000)); @@ -183,8 +206,14 @@ try { await page.evaluate(() => document.querySelector('.live-grid:not(.live-past) .live-tile').click()); check('a tap opens it, the address bar naming the stream', await waitFor(() => !!document.querySelector('.live-page'), 5000) && await waitFor((n) => location.pathname === '/' + n, 5000, NADDR), await page.evaluate(() => location.pathname)); check('the stream plays', await waitFor(playing, 20000), JSON.stringify(await page.evaluate(() => { const v = document.querySelector('video.live-video'); return v && { t: v.currentTime, paused: v.paused, err: v.error && v.error.code, src: v.currentSrc.slice(0, 60) }; }))); - check('...from its playlist, natively in Chrome', await page.evaluate((s) => document.querySelector('video.live-video').currentSrc === s, STREAM)); + // a stream that's on plays through hls.js (kept near the live edge), not Chrome's own player + check('...through hls.js, kept near the live edge', await page.evaluate(() => document.querySelector('video.live-video').currentSrc.startsWith('blob:'))); check('the chat shows what was said', await waitText('hello from the chat', 8000)); + const who = () => page.evaluate(() => { const x = document.querySelector('.live-page .live-watching'); return x && { text: x.textContent, title: x.getAttribute('title') || '' }; }); + check('"watching" names who, on hover: the fan and this viewer, not someone long gone', await waitFor(() => (document.querySelector('.live-page .live-watching')?.getAttribute('title') || '').split(', ').length === 2, 8000), JSON.stringify(await who())); + check('...this viewer said they\'re here (a 10312 for the stream)', await page.evaluate((a) => window.__published.some((e) => e.kind === 10312 && e.tags.some((t) => t[0] === 'a' && t[1] === a)), A)); + await page.evaluate(() => document.querySelector('.live-page .live-watching').click()); + check('...a tap shows their faces', await waitFor(() => document.querySelectorAll('.live-page .live-who .live-who-p').length === 2, 3000)); check('...and the zap, its amount and words', await waitText('zapped 21 sats', 5000) && await waitText('great stream', 1000)); const rowSel = '.live-message[data-key="' + chat.id + '"]'; check('comments use the group-chat bubble and shared composer', await page.$(rowSel + ' .chat-bubble') && await page.$('.live-page coinos-text#msg-draft')); @@ -232,21 +261,24 @@ try { console.log('\n[leaving]'); await page.goBack(); check('Back leaves the stream\'s page', await waitFor(() => !document.querySelector('.live-page'), 5000)); + check('...and takes back this viewer\'s presence (an empty 10312)', await waitFor(() => window.__published.some((e) => e.kind === 10312 && !e.tags.some((t) => t[0] === 'a')), 3000)); check('...and nothing plays on behind it', await page.evaluate(() => [...document.querySelectorAll('video')].every((v) => v.paused || !v.isConnected))); await page.evaluate(() => document.querySelector('.media-mode[data-mode="all"]').click()); await waitFor(() => !!document.querySelector('.notes-feed .live-card'), 5000); await page.evaluate(() => document.querySelector('.notes-feed .live-card').click()); check('the card in the post opens the stream too', await waitFor(() => !!document.querySelector('.live-page'), 5000) && await waitFor(playing, 20000)); await page.evaluate(() => { const a = [...document.querySelectorAll('.live-page a.quote-name')].find((x) => /Stream Host/.test(x.textContent)); a.click(); }); - check('the host\'s profile lists their past streams', await waitFor(() => [...document.querySelectorAll('.live-past-row .live-tile')].some((t) => /Yesterday on coinos/.test(t.innerText)), 10000)); - await page.evaluate(() => [...document.querySelectorAll('.live-past-row .live-tile')].find((t) => /Zelda on Rumble/.test(t.innerText)).click()); + check('the host\'s profile has a Streams tab', await waitFor(() => !!document.querySelector('.prof-tabs [data-tab="streams"]'), 10000) && !(await page.$('.live-past'))); + await page.evaluate(() => document.querySelector('.prof-tabs [data-tab="streams"]').click()); + check('...listing their past streams', await waitFor(() => [...document.querySelectorAll('.live-past .live-tile')].some((t) => /Yesterday on coinos/.test(t.innerText)), 10000)); + await page.evaluate(() => [...document.querySelectorAll('.live-past .live-tile')].find((t) => /Zelda on Rumble/.test(t.innerText)).click()); check('a recording kept as a web page: ended, and a link to watch it there, no player', await waitFor(() => !!document.querySelector('.live-page a.live-rec-link[href="https://rumble.com/v7gt2va-zelda.html"]'), 5000) && await page.evaluate(() => /This stream has ended/.test(document.body.innerText) && !document.querySelector('.live-page video.live-video'))); await page.goBack(); await waitFor(() => !document.querySelector('.live-page'), 5000); check('the host\'s profile says they\'re on', await waitFor(() => !!document.querySelector('.live-banner'), 10000) && await waitText('Building coinos live', 2000)); check('...the player stopped when the page went', await page.evaluate(() => [...document.querySelectorAll('video')].every((v) => v.paused || !v.isConnected))); - await page.evaluate(() => [...document.querySelectorAll('.live-past-row .live-tile')].find((t) => /Yesterday on coinos/.test(t.innerText)).click()); + await page.evaluate(() => [...document.querySelectorAll('.live-past .live-tile')].find((t) => /Yesterday on coinos/.test(t.innerText)).click()); check('a past stream opens on its recording, playing', await waitFor(() => !!document.querySelector('.live-page'), 5000) && await waitFor(playing, 20000) && await page.evaluate(() => !document.body.innerText.includes('This stream has ended'))); check('no page errors', !page.errs.length, page.errs.join(' | ')); |
