// Proves the caption tracks actually load and follow the member's chosen language. // A .vtt served with the wrong Content-Type is silently ignored by the browser, so // checking the markup is not enough: this reads the parsed cues back out of the player. // // Run: LOCAL=http://127.0.0.1:3399 node qa/captions-e2e.mjs import { pathToFileURL } from 'node:url'; const PW = 'D:/Projects/MarketingAgent/qa-tester/node_modules/playwright'; const { chromium } = (await import(pathToFileURL(PW + '/index.js').href)).default; const B = process.env.LOCAL || 'http://127.0.0.1:3399'; const ok = [], bad = []; const t = (n, c, extra) => { (c ? ok : bad).push(n + (c || !extra ? '' : ' -> ' + extra)); }; const browser = await chromium.launch(); const VTT = '/captions/rmc-team-overview-f8856dc743'; // 1. the files are served as real VTT, not octet-stream (the silent killer) for (const lang of ['en', 'it']) { const r = await fetch(B + VTT + '.' + lang + '.vtt'); const ct = r.headers.get('content-type') || ''; const body = await r.text(); t('serves ' + lang + '.vtt as text/vtt', r.ok && /text\/vtt/.test(ct), r.status + ' ' + ct); t(lang + '.vtt starts with WEBVTT', body.startsWith('WEBVTT'), body.slice(0, 20)); t(lang + '.vtt has cues with timings', (body.match(/-->/g) || []).length > 50, String((body.match(/-->/g) || []).length)); } const load = async (lang) => { const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }); if (lang) await ctx.addInitScript(l => { try { localStorage.setItem('rmc.lang', l); } catch (e) {} }, lang); const p = await ctx.newPage(); await p.goto(B + '/training', { waitUntil: 'domcontentloaded' }); await p.waitForTimeout(2500); return { ctx, p }; }; const readTracks = p => p.evaluate(() => { const v = document.querySelector('video[src*="rmc-team-overview"]'); if (!v) return null; return Array.from(v.textTracks).map(tr => ({ lang: tr.language, mode: tr.mode, cues: tr.cues ? tr.cues.length : 0, first: tr.cues && tr.cues[0] ? tr.cues[0].text : '' })); }); // 2. an English member: tracks available, nothing forced on screen let { ctx, p } = await load(null); let tracks = await readTracks(p); t('the overview player has caption tracks', !!tracks && tracks.length === 2, JSON.stringify(tracks)); t('no captions forced on for an English member', !!tracks && tracks.every(x => x.mode !== 'showing'), JSON.stringify(tracks && tracks.map(x => x.mode))); await ctx.close(); // 3. a member who chose Italian: the Italian track is showing, with real parsed cues ({ ctx, p } = await load('it')); // the browser only parses a track once it is enabled, so read after the matcher runs await p.waitForTimeout(1500); tracks = await readTracks(p); const it = (tracks || []).find(x => x.lang === 'it'); const en = (tracks || []).find(x => x.lang === 'en'); t('Italian track is the one showing', !!it && it.mode === 'showing', JSON.stringify(tracks && tracks.map(x => x.lang + ':' + x.mode))); t('English track is not also showing', !!en && en.mode !== 'showing', en && en.mode); t('the Italian cues actually parsed', !!it && it.cues > 50, it && String(it.cues)); t('and they are Italian, not English passthrough', !!it && /parte|reclutano|squadra|team/i.test(it.first) && !/^Most team builds fail/.test(it.first), it && it.first); await ctx.close(); console.log('PASS ' + ok.length); for (const b of bad) console.log('FAIL ' + b); await browser.close(); process.exit(bad.length ? 1 : 0);