tests/engine/language.test.mjs (3933 bytes)
1 /* The languages SubRead is checked against: English, Portuguese, Spanish, 2 * Russian and Japanese. The text of each comes from the golden fixtures, so 3 * the same public-domain excerpts serve the guess of the language and the 4 * alignment. 5 * 6 * node --test tests/engine 7 */ 8 import test from 'node:test'; 9 import assert from 'node:assert/strict'; 10 import { readdirSync, readFileSync } from 'node:fs'; 11 import { fileURLToPath } from 'node:url'; 12 import path from 'node:path'; 13 14 import { LANGUAGES } from '../../frontend/engine/languages.js'; 15 import { detectLanguage } from '../../frontend/engine/book.js'; 16 import { language, align, UNMATCHED } from '../../frontend/engine/align.js'; 17 18 export const CHECKED = ['en', 'pt', 'es', 'ru', 'ja']; 19 20 const here = path.dirname(fileURLToPath(import.meta.url)); 21 const fixtures = readdirSync(path.join(here, 'golden')).filter((f) => f.endsWith('.json')).sort() 22 .map((f) => JSON.parse(readFileSync(path.join(here, 'golden', f), 'utf8'))); 23 24 /** The first fixture of each language. */ 25 export const byLanguage = new Map(CHECKED.map((code) => [code, fixtures.find((c) => c.language === code)])); 26 27 test('each checked language has a golden fixture', () => { 28 for (const code of CHECKED) assert.ok(byLanguage.get(code), `no golden fixture for ${code}`); 29 }); 30 31 test('each checked language is offered to the visitor', () => { 32 const offered = new Set(LANGUAGES.map((l) => l.code)); 33 for (const code of CHECKED) assert.ok(offered.has(code), `${code} is not in LANGUAGES`); 34 }); 35 36 test('the language of a book is guessed from its text', () => { 37 for (const [code, c] of byLanguage) assert.equal(detectLanguage(c.paragraphs), code, `guess for ${code}`); 38 }); 39 40 test('a page of a book is enough to guess the language', () => { 41 for (const [code, c] of byLanguage) { 42 const page = c.paragraphs.slice(0, 3); 43 assert.equal(detectLanguage(page), code, `guess for ${code} from ${page.join('').length} characters`); 44 } 45 }); 46 47 test('cleaning folds case in every script and keeps the letters', () => { 48 assert.equal(language('ru').clean('Ёлка И Её'), 'ёлка и её'); 49 assert.equal(language('pt').clean('Não É Coração'), 'não é coração'); 50 assert.equal(language('es').clean('¿Qué? ¡Ñandú!'), '¿qué? ¡ñandú!'); 51 assert.equal(language('en').clean('The Yellow Wallpaper'), 'the yellow wallpaper'); 52 // Only Japanese drops punctuation and spacing, and folds the particles the recogniser confuses. 53 assert.equal(language('ja').clean('吾輩は 猫である。'), '吾輩わ猫でわる。'); 54 }); 55 56 test('a transcript with no words in common with the book gives unmatched cues, not a crash', () => { 57 for (const [code, c] of byLanguage) { 58 const other = byLanguage.get(code === 'ja' ? 'en' : 'ja'); 59 const cues = align(other.transcript.slice(0, 20), c.paragraphs.slice(0, 5), language(code)); 60 assert.equal(cues.length, 20, code); 61 for (const cue of cues) assert.ok(typeof cue.text === 'string' && cue.text.length > 0, code); 62 } 63 }); 64 65 test('the whole pipeline keeps the words of the book, in order, for each language', () => { 66 for (const [code, c] of byLanguage) { 67 const cues = align(c.transcript, c.paragraphs, language(code)); 68 const matched = cues.filter((cue) => !cue.text.startsWith(UNMATCHED)); 69 assert.ok(matched.length / cues.length >= 0.95, `${code}: ${matched.length}/${cues.length} matched`); 70 // Every matched cue is a slice of the book, and the slices come in reading order. 71 const squeeze = (s) => s.replace(/\s+/g, ''); 72 const book = squeeze(c.paragraphs.join('')); 73 let at = 0; 74 for (const cue of matched) { 75 const found = book.indexOf(squeeze(cue.text), at); 76 assert.ok(found >= 0, `${code}: cue not in the book after position ${at}: ${cue.text}`); 77 at = found; 78 } 79 for (let i = 1; i < cues.length; i++) assert.ok(cues[i].start >= cues[i - 1].start, `${code}: cue ${i} starts before cue ${i - 1}`); 80 } 81 });