diff --git a/test/content-engine.js b/test/content-engine.js new file mode 100644 index 0000000..4cbf3fb --- /dev/null +++ b/test/content-engine.js @@ -0,0 +1,192 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const { JSDOM } = require("jsdom"); + +const source = fs.readFileSync(path.join(__dirname, "../src/content.js"), "utf8"); + +function createExtensionDom(body, overrides = {}) { + const settings = { + enabled: true, + fixDirection: true, + useFont: true, + fixInputs: true, + disabledSites: [], + ...overrides + }; + let onChanged; + const dom = new JSDOM(`${body}`, { + url: "https://chat.example/", + runScripts: "dangerously", + pretendToBeVisual: true + }); + dom.window.requestIdleCallback = (callback) => callback({ didTimeout: false, timeRemaining: () => 50 }); + dom.window.chrome = { + runtime: { + getURL: (file) => `chrome-extension://test/${file}`, + lastError: null + }, + storage: { + local: { get: (_defaults, callback) => callback(settings) }, + onChanged: { addListener: (callback) => { onChanged = callback; } } + } + }; + const script = dom.window.document.createElement("script"); + script.textContent = source; + dom.window.document.head.appendChild(script); + return { + dom, + changeSettings(patch) { + for (const [key, value] of Object.entries(patch)) settings[key] = value; + const changes = Object.fromEntries(Object.entries(patch).map(([key, value]) => [key, { newValue: value }])); + onChanged(changes, "local"); + } + }; +} + +function element(body, id = "target", overrides) { + const fixture = createExtensionDom(body, overrides); + return { fixture, target: fixture.dom.window.document.getElementById(id) }; +} + +const tests = []; +function test(name, fn) { tests.push({ name, fn }); } +function nextTurn() { return new Promise((resolve) => setTimeout(resolve, 0)); } + +test("real Persian text is corrected to RTL", () => { + const { target } = element('

این یک متن فارسی است

'); + assert.equal(target.getAttribute("dir"), "rtl"); +}); + +test("Arabic-only text is not treated as Persian", () => { + const { target } = element('

هذا نص عربي للاختبار

'); + assert.equal(target.hasAttribute("dir"), false); + assert.equal(target.hasAttribute("data-rtlx-font"), false); +}); + +test("English-leading Persian text uses auto direction", () => { + const { target } = element('

The answer is متن فارسی برای آزمایش.

'); + assert.equal(target.getAttribute("dir"), "auto"); + assert.equal(target.hasAttribute("data-rtlx-rtl"), false); +}); + +test("Latin digits inside Persian text remain byte-identical", () => { + const text = "نسخه 1234567890 برای آزمایش"; + const { target } = element(`

${text}

`); + assert.equal(target.textContent, text); +}); + +test("a sensible explicit RTL direction is preserved without ownership markers", () => { + const { target } = element('

این متن از قبل راست‌چین است

'); + assert.equal(target.getAttribute("dir"), "rtl"); + assert.equal(target.hasAttribute("data-rtlx-setdir"), false); +}); + +test("a sensible explicit auto direction is preserved without ownership markers", () => { + const { target } = element('

این متن جهت خودکار دارد

'); + assert.equal(target.getAttribute("dir"), "auto"); + assert.equal(target.hasAttribute("data-rtlx-setdir"), false); +}); + +test("a clearly wrong explicit LTR direction is corrected reversibly", () => { + const { target } = element('

این متن فارسی راست‌به‌چپ است

'); + assert.equal(target.getAttribute("dir"), "rtl"); + assert.equal(target.getAttribute("data-rtlx-dir"), "ltr"); +}); + +test("an inline author font is never replaced", () => { + const { target } = element('

این متن فونت دارد

'); + assert.equal(target.style.fontFamily, "Arial"); + assert.equal(target.hasAttribute("data-rtlx-font"), false); +}); + +test("an inherited author font is never replaced", () => { + const { target } = element('

این متن فونت ارث‌بری دارد

'); + assert.equal(target.style.fontFamily, ""); + assert.equal(target.hasAttribute("data-rtlx-font"), false); +}); + +test("a stylesheet font on the Persian element is never replaced", () => { + const { target } = element('

این متن فونت دارد

'); + assert.equal(target.style.fontFamily, ""); + assert.equal(target.hasAttribute("data-rtlx-font"), false); +}); + +test("an inherited stylesheet font is never replaced", () => { + const { target } = element('

این پاسخ هوش مصنوعی است

'); + assert.equal(target.style.fontFamily, ""); + assert.equal(target.hasAttribute("data-rtlx-font"), false); +}); + +test("unstyled Persian text receives Vazirmatn", () => { + const { target } = element('

این متن بدون فونت است

'); + assert.match(target.style.fontFamily, /Vazirmatn/i); +}); + +test("streamed Persian text is corrected and Latin digits are untouched", async () => { + const { fixture, target } = element('

Waiting

'); + target.textContent = "پاسخ زنده 1234567890"; + await nextTurn(); + assert.equal(target.getAttribute("dir"), "rtl"); + assert.equal(target.textContent, "پاسخ زنده 1234567890"); + fixture.dom.window.close(); +}); + +test("a streamed block that becomes English is restored", async () => { + const { fixture, target } = element('

پاسخ فارسی

'); + assert.equal(target.getAttribute("dir"), "rtl"); + target.textContent = "The final answer is English."; + await nextTurn(); + assert.equal(target.hasAttribute("dir"), false); + assert.equal(target.hasAttribute("data-rtlx-rtl"), false); + assert.equal(target.style.fontFamily, ""); + fixture.dom.window.close(); +}); + +test("disabling the extension restores owned direction and font", () => { + const { fixture, target } = element('

پاسخ فارسی

'); + fixture.changeSettings({ enabled: false }); + assert.equal(target.hasAttribute("dir"), false); + assert.equal(target.style.fontFamily, ""); +}); + +test("unstyled inputs receive Vazirmatn only after Persian is present", () => { + const { fixture, target } = element(''); + assert.equal(target.style.fontFamily, ""); + target.value = "متن فارسی"; + target.dispatchEvent(new fixture.dom.window.Event("input", { bubbles: true })); + assert.match(target.style.fontFamily, /Vazirmatn/i); +}); + +test("author-styled Persian inputs preserve their font", () => { + const { target } = element(''); + assert.equal(target.style.fontFamily, "Arial"); + assert.equal(target.hasAttribute("data-rtlx-font"), false); +}); + +test("input listeners do not reapply styling while the extension is disabled", () => { + const { fixture, target } = element(''); + fixture.changeSettings({ enabled: false }); + target.value = "متن فارسی"; + target.dispatchEvent(new fixture.dom.window.Event("input", { bubbles: true })); + assert.equal(target.style.fontFamily, ""); + assert.equal(target.hasAttribute("data-rtlx-font"), false); +}); + +(async () => { + let passed = 0; + for (const { name, fn } of tests) { + try { + await fn(); + passed += 1; + console.log(`PASS ${name}`); + } catch (error) { + console.error(`FAIL ${name}`); + console.error(error.message); + } + } + console.log(`\n${passed}/${tests.length} passed`); + process.exitCode = passed === tests.length ? 0 : 1; +})();