// 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, and a // track that fails to parse shows zero cues with no error anywhere. So checking the // markup is not enough: this reads the parsed cues back out of the player itself. // // Run: LOCAL=http://127.0.0.1:3399 node qa/captions-e2e.mjs // LOCAL=https://rmcircle.team node qa/captions-e2e.mjs import { pathToFileURL } from 'node:url'; import fs from 'node:fs'; 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)); }; // the captioned set is whatever training.html actually wires up, so this suite follows // the page rather than a list that can drift out of date const html = fs.readFileSync('public/training.html', 'utf8'); const players = []; for (const m of html.matchAll(/]*src="([^"]+\.mp4)"[^>]*>([\s\S]*?)<\/video>/g)) { const langs = [...m[2].matchAll(/srclang="([a-z]+)"[^>]*src="([^"]+)"/g)].map(x => ({ lang: x[1], src: x[2] })); if (langs.length) players.push({ video: m[1], langs }); } t('training.html wires up captioned players', players.length > 0, String(players.length)); console.log('captioned players: ' + players.length); // 1. every referenced file is served as real VTT with real cues for (const p of players) { for (const { lang, src } of p.langs) { const r = await fetch(B + src); const ct = r.headers.get('content-type') || ''; const body = r.ok ? await r.text() : ''; const cues = (body.match(/-->/g) || []).length; t(src.split('/').pop() + ' served as text/vtt with cues', r.ok && /text\/vtt/.test(ct) && body.startsWith('WEBVTT') && cues > 10, r.status + ' ' + ct + ' cues=' + cues); } } const browser = await chromium.launch(); const load = async lang => { const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }); await ctx.addInitScript(l => { try { if (l) localStorage.setItem('rmc.lang', l); } catch (e) {} // the event flyer covers the page and is not what this suite tests const g = Storage.prototype.getItem; Storage.prototype.getItem = function (k) { if (/^rmc-promo-/.test(k)) return 'seen'; if (/^rmc-announce-/.test(k)) return 'done'; return g.call(this, k); }; }, lang); const page = await ctx.newPage(); await page.goto(B + '/training', { waitUntil: 'domcontentloaded' }); await page.waitForTimeout(3000); return { ctx, page }; }; const read = page => page.evaluate(() => Array.from(document.querySelectorAll('video')).map(v => ({ src: v.getAttribute('src'), tracks: 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 are available but nothing is forced over the picture let { ctx, page } = await load(null); let vids = await read(page); for (const p of players) { const v = vids.find(x => x.src === p.video); t('player has its tracks: ' + p.video.split('/').pop(), !!v && v.tracks.length === p.langs.length, v ? String(v.tracks.length) : 'player not found'); t('nothing forced on for an English member: ' + p.video.split('/').pop(), !!v && v.tracks.every(x => x.mode !== 'showing'), v && JSON.stringify(v.tracks.map(x => x.mode))); } await ctx.close(); // 3. each translated language: that track shows, parses, and is not English passthrough const ENGLISH_OPENERS = /^(Most team builds|Welcome|Hey|In this video|Alright|So )/i; for (const lang of ['it', 'fr', 'es']) { ({ ctx, page } = await load(lang)); await page.waitForTimeout(1500); vids = await read(page); for (const p of players) { if (!p.langs.some(l => l.lang === lang)) continue; const v = vids.find(x => x.src === p.video); const tr = v && v.tracks.find(x => x.lang === lang); const name = p.video.split('/').pop().slice(0, 30); t(lang + ': showing on ' + name, !!tr && tr.mode === 'showing', tr ? tr.mode : 'track missing'); t(lang + ': cues parsed on ' + name, !!tr && tr.cues > 10, tr ? String(tr.cues) : '-'); t(lang + ': not English passthrough on ' + name, !!tr && !ENGLISH_OPENERS.test(tr.first), tr && tr.first.slice(0, 50)); t(lang + ': English not also showing on ' + name, !!v && v.tracks.every(x => x.lang === lang || x.mode !== 'showing'), v && JSON.stringify(v.tracks.map(x => x.lang + ':' + x.mode))); } 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);