WebexLinkFixer/tests/smoke.mjs

145 lines
5.8 KiB
JavaScript

// Smoke test for the Empty Link Fixer content script.
//
// Runs the actual config.js + content.js against jsdom pages and verifies that
// empty anchors are filled — statically, on dynamically added content, emptied
// text, late href attributes and inside (open) shadow DOM — while links that
// have text or media content are left alone.
//
// Usage: node tests/smoke.mjs (needs `npm install --no-save jsdom`)
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import assert from 'node:assert/strict';
import { JSDOM } from 'jsdom';
const dir = fileURLToPath(new URL('..', import.meta.url));
const configJs = readFileSync(`${dir}config.js`, 'utf8');
const contentJs = readFileSync(`${dir}content.js`, 'utf8');
const tick = (ms = 40) => new Promise((r) => setTimeout(r, ms));
function boot(html, { url = 'https://example.com/', hosts = ['example.com'] } = {}) {
const dom = new JSDOM(html, {
url,
runScripts: 'outside-only',
pretendToBeVisual: true,
});
const { window } = dom;
window.eval(configJs);
window.eval(`globalThis.LINK_FIX_CONFIG = ${JSON.stringify({
hosts,
includeSubdomains: true,
rescanIntervalMs: 0, // disable the safety-net timer during tests
debug: false,
})};`);
window.eval(contentJs);
return dom;
}
const $ = (dom, sel) => dom.window.document.querySelector(sel);
async function run() {
// ---------------------------------------------------------------- static
{
const dom = boot(`
<a id="e1" href="https://example.com/a"></a>
<a id="e2" href="/docs">Already has text</a>
<a id="e3" href="/icon"><img src="x.png" alt="icon"></a>
<a id="e4" href="/svg"><svg viewBox="0 0 10 10"><circle cx="5" cy="5" r="5"/></svg></a>
<a id="e5" href="/ws"> </a>
<a id="e6" href="#"></a>
<a id="e7" href="javascript:void(0)"></a>
<a id="e8" href="/hidden" style="display:none"></a>
`);
await tick();
assert.equal($(dom, '#e1').textContent, 'https://example.com/a', 'empty anchor filled with href');
assert.equal($(dom, '#e2').textContent.trim(), 'Already has text', 'text anchor untouched');
assert.equal($(dom, '#e3').textContent, '', 'image anchor untouched');
assert.ok($(dom, '#e3').querySelector('img'), 'image preserved inside anchor');
assert.equal($(dom, '#e4').textContent, '', 'svg anchor untouched');
assert.ok($(dom, '#e4').querySelector('svg'), 'svg preserved inside anchor');
assert.equal($(dom, '#e5').textContent.trim(), '/ws', 'whitespace-only anchor filled');
assert.equal($(dom, '#e6').textContent, '', 'placeholder href="#" not filled');
assert.equal($(dom, '#e7').textContent, '', 'javascript: href not filled');
assert.equal($(dom, '#e8').textContent.trim(), '/hidden', 'empty anchor filled regardless of visibility');
dom.window.close();
}
// ------------------------------------------------- dynamically inserted
{
const dom = boot('<div id="root"></div>');
await tick();
dom.window.document.querySelector('#root').innerHTML =
'<a href="/dyn"></a><a href="/kept">keep me</a>';
await tick();
assert.equal($(dom, 'a[href="/dyn"]').textContent.trim(), '/dyn', 'dynamically added empty anchor filled');
assert.equal($(dom, 'a[href="/kept"]').textContent.trim(), 'keep me', 'dynamically added text anchor untouched');
dom.window.close();
}
// ------------------------------------------------- text emptied in place
{
const dom = boot('<a id="t1" href="/t1">content here</a>');
await tick();
// Simulate a framework emptying the text node in place (characterData).
dom.window.document.querySelector('#t1').firstChild.nodeValue = '';
await tick();
assert.equal($(dom, '#t1').textContent.trim(), '/t1', 'anchor re-filled after its text node was emptied');
dom.window.close();
}
// ------------------------------------------------- children replaced
{
const dom = boot('<a id="t2" href="/t2">content here</a>');
await tick();
// Simulate a re-render that wipes the inner content entirely.
dom.window.document.querySelector('#t2').textContent = '';
await tick();
assert.equal($(dom, '#t2').textContent.trim(), '/t2', 'anchor re-filled after children were removed');
dom.window.close();
}
// ------------------------------------------------- href added later
{
const dom = boot('<a id="t3"></a>');
await tick();
dom.window.document.querySelector('#t3').setAttribute('href', '/later');
await tick();
assert.equal($(dom, '#t3').textContent.trim(), '/later', 'anchor filled when href appears dynamically');
dom.window.close();
}
// ------------------------------------------------- open shadow DOM
{
const dom = boot('<div id="host"></div>');
const { document } = dom.window;
await tick();
const host = document.querySelector('#host');
const root = host.attachShadow({ mode: 'open' });
root.innerHTML = '<a href="/shadow"></a><a href="/shadow2">shadow text</a>';
document.body.appendChild(host); // host enters the DOM after init
await tick();
assert.equal(root.querySelector('a[href="/shadow"]').textContent.trim(), '/shadow', 'anchor inside open shadow root filled');
assert.equal(root.querySelector('a[href="/shadow2"]').textContent.trim(), 'shadow text', 'text anchor in shadow root untouched');
dom.window.close();
}
// ------------------------------------------------- different site
{
const dom = boot('<a id="off" href="https://other.com/x"></a>', {
url: 'https://somewhere-else.com/',
hosts: ['example.com'],
});
await tick();
assert.equal($(dom, '#off').textContent, '', 'nothing happens on a non-configured site');
dom.window.close();
}
console.log('All smoke tests passed ✔');
}
run().catch((err) => {
console.error(err);
process.exit(1);
});