aboutsummaryrefslogtreecommitdiffstats
path: root/tools/media-timeline-test.js
blob: c8f1fd9b91054846f3ea77cd9abed5a4e74c2e84 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import assert from 'node:assert/strict';
import { mediaMoment, momentTags, readMoment, waveformFromTags, waveformPeaks } from '../src/media-timeline.js';
import { buildZapRequest } from '../src/lnurl.js';
import puppeteer from './puppeteer.js';
const url = 'https://example.com/song.mp3';
const moment = mediaMoment(url, 65.9);
assert.deepEqual(moment, { url, seconds: 65 });
for (const value of [-1, Infinity, NaN]) assert.equal(mediaMoment(url, value), null);
assert.equal(mediaMoment('javascript:alert(1)', 0), null);
assert.deepEqual(readMoment({ tags: momentTags(moment) }), moment);
assert.equal(readMoment({ tags: [['media', url, '']] }), null);
const request = buildZapRequest({ amountMsat: 21000, relays: [], recipientPk: 'p', eventId: 'e', moment, signFn: e => e });
assert.equal(request.tags.find(t => t[0] === 'e')[1], 'e');
assert.deepEqual(readMoment({ kind: 9735, tags: [['description', JSON.stringify(request)]] }), moment);
assert.equal(readMoment(buildZapRequest({ amountMsat: 21000, relays: [], recipientPk: 'p', eventId: 'e', signFn: e => e })), null);
assert.deepEqual(waveformFromTags({ tags: [['imeta', 'url ' + url, 'waveform 1 5 2']] }, url), [1, 5, 2]);
assert.equal(waveformFromTags({ tags: [['imeta', 'url ' + url, 'waveform -1 2']] }, url), null);
assert.deepEqual(waveformPeaks({ numberOfChannels: 2, getChannelData: n => n ? [0, 0, .8, 0] : [.2, -.5, .1, 0] }, 2), [.5, .8]);
const wav = new Uint8Array(44 + 16000);
const dv = new DataView(wav.buffer);
const word = (at, text) => wav.set(new TextEncoder().encode(text), at);
word(0, 'RIFF'); dv.setUint32(4, wav.length - 8, true); word(8, 'WAVE'); word(12, 'fmt ');
dv.setUint32(16, 16, true); dv.setUint16(20, 1, true); dv.setUint16(22, 1, true); dv.setUint32(24, 8000, true); dv.setUint32(28, 16000, true); dv.setUint16(32, 2, true); dv.setUint16(34, 16, true); word(36, 'data'); dv.setUint32(40, 16000, true);
for (let i = 0; i < 8000; i++) dv.setInt16(44 + i * 2, Math.sin(i * .3) * 20000, true);
const server = Bun.serve({ port: 0, fetch(req) {
  const path = new URL(req.url).pathname;
  if (path === '/wave.wav') return new Response(wav, { headers: { 'content-type': 'audio/wav' } });
  if (path === '/large.wav') return new Response('', { headers: { 'content-length': String(21 * 1024 * 1024) } });
  if (path === '/module.js') return new Response(Bun.file('src/media-timeline.js'), { headers: { 'content-type': 'text/javascript' } });
  if (path === '/style.css') return new Response(Bun.file('src/style.css'));
  return new Response('<!doctype html><link rel="stylesheet" href="/style.css"><body>', { headers: { 'content-type': 'text/html' } });
} });
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
try {
  const page = await browser.newPage();
  await page.goto(server.url.href);
  const decoded = await page.evaluate(async () => {
    const { loadWaveform } = await import('/module.js');
    const peaks = await loadWaveform('/wave.wav');
    return { count: peaks?.length, nonzero: peaks?.every(x => x > .5), fallback: await loadWaveform('/large.wav') };
  });
  assert.equal(decoded.count, 96); assert.equal(decoded.nonzero, true); assert.equal(decoded.fallback, null);
  const result = await page.evaluate(async () => {
    const { mediaTimeline } = await import('/module.js');
    const h = (tag, attrs = {}, ...children) => {
      const el = document.createElement(tag);
      for (const [k, v] of Object.entries(attrs)) { if (k.startsWith('on')) el.addEventListener(k.slice(2).toLowerCase(), v); else if (v === true) el.setAttribute(k, ''); else if (v !== false) el.setAttribute(k, v); }
      el.append(...children); return el;
    };
    const player = h('audio');
    Object.defineProperty(player, 'duration', { value: 120 });
    let at = 0;
    Object.defineProperty(player, 'currentTime', { get: () => at, set: x => { at = x; } });
    const url = 'https://example.com/song.mp3';
    const ev = { id: 'post', tags: [['imeta', 'url ' + url, 'waveform 1 10 4 8']] };
    const calls = [], items = [{ id: 'a', moment: { seconds: 30 }, icon: '♡', text: 'Nice!' }];
    const box = mediaTimeline(player, url, { h, event: () => ev, moments: () => items, action: (...a) => calls.push(a.slice(0, 3)), label: x => ({ mediaAtTime: 'at this time' }[x] || x) });
    document.body.append(player, box);
    player.dispatchEvent(new Event('timeupdate'));
    const wave = box.querySelectorAll('.media-wave i').length;
    const range = box.querySelector('input'); range.value = 500; range.dispatchEvent(new Event('input'));
    const seek = at;
    for (const b of box.querySelectorAll('.media-moment-actions button')) b.click();
    box.querySelector('.media-mark').click();
    const marker = at;
    items.push({ id: 'b', moment: { seconds: 70 }, icon: '⚡', text: '' });
    player.dispatchEvent(new Event('mediaactivity'));
    window.fixture = box;
    return { wave, seek, marker, calls, markers: box.querySelectorAll('.media-mark').length, aria: range.getAttribute('aria-valuetext') };
  });
  assert.equal(result.wave, 4);
  assert.equal(result.seek, 60);
  assert.equal(result.marker, 30);
  assert.equal(result.markers, 2);
  assert.deepEqual(result.calls.map(c => [c[0], c[1].id, c[2].seconds]), [['comment', 'post', 60], ['reaction', 'post', 60], ['zap', 'post', 60]]);
  await page.setViewport({ width: 360, height: 740 });
  assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth), true);
  await page.screenshot({ path: '/tmp/media-timeline-mobile.png' });
  console.log('✓ Timestamp validation, Lightning receipt round-trip, untimed zap isolation, waveform peaks, browser seeking, all three actions, marker updates and mobile layout');
} finally { await browser.close(); server.stop(); }