waybetter/packages/runtime/tests/styles.test.ts
CRT c404dd3dd0 runtime: styles for created roots are constructed sheets (CSP-proof)
- styles.ts: adoptStyles(root, css) adopts a constructed stylesheet into a shadow root or document
  (not governed by a page's CSP, parsed once, stays with the root when its host is moved);
  <style> fallback where constructable sheets are missing or refused.
- UI kit: the kit's shadow root and each dialog's `css` use it (dialog CSS goes with the dialog).
- Plugin API: api.adoptCSS(root, css) and scope.adoptCSS (removed on dispose); injectCSS keeps its
  <style> and, when a CSP leaves it without a sheet, applies the same rules as an adopted sheet.
- tests/styles.test.ts.
2026-09-29 04:09:14 +07:00

175 lines
7.2 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { adoptStyles } from '../src/styles.ts';
import { createCorePluginApi, type PluginApiSite } from '../src/pluginApi.ts';
/** A constructable sheet as far as the helper uses one. */
class FakeSheet {
static refuse = false;
css = '';
replaceSync(css: string) {
if (FakeSheet.refuse) throw new Error('refused');
this.css = css;
}
}
type FakeStyle = { tag: string; textContent: string; parent: FakeParent | null; sheet?: unknown; remove(): void };
type FakeParent = { children: FakeStyle[]; appendChild(node: FakeStyle): FakeStyle };
function parent(): FakeParent {
const node: FakeParent = {
children: [],
appendChild(child) { child.parent = node; node.children.push(child); return child; },
};
return node;
}
function fakeDocument(options: { constructable?: boolean; adopted?: boolean } = {}) {
const win: Record<string, unknown> = {};
if (options.constructable !== false) win.CSSStyleSheet = FakeSheet;
const head = parent();
const doc: Record<string, unknown> = {
nodeType: 9,
defaultView: win,
head,
documentElement: parent(),
createElement(tag: string): FakeStyle {
const node: FakeStyle = { tag, textContent: '', parent: null, remove() { if (node.parent) node.parent.children = node.parent.children.filter(c => c !== node); node.parent = null; } };
return node;
},
};
if (options.adopted !== false) doc.adoptedStyleSheets = [];
win.document = doc;
return { win, doc, head };
}
function fakeShadowRoot(doc: Record<string, unknown>, adopted = true) {
const root = parent() as FakeParent & Record<string, unknown>;
root.nodeType = 11;
root.ownerDocument = doc;
if (adopted) root.adoptedStyleSheets = [];
return root;
}
test('a shadow root adopts a constructed sheet: no <style> element, update and remove work', () => {
FakeSheet.refuse = false;
const { doc } = fakeDocument();
const root = fakeShadowRoot(doc);
const earlier = new FakeSheet();
root.adoptedStyleSheets = [earlier];
const styles = adoptStyles(root as unknown as ShadowRoot, '.a { color: red; }');
assert.equal(styles.adopted, true);
assert.equal(root.children.length, 0, 'no <style> element the page CSP could refuse');
const adopted = root.adoptedStyleSheets as FakeSheet[];
assert.equal(adopted.length, 2);
assert.equal(adopted[0], earlier, 'sheets already adopted stay first');
assert.equal(adopted[1].css, '.a { color: red; }');
assert.equal(styles.sheet, adopted[1] as unknown as CSSStyleSheet);
styles.update('.b {}');
assert.equal(adopted[1].css, '.b {}');
styles.remove();
assert.deepEqual(root.adoptedStyleSheets, [earlier]);
styles.remove();
assert.deepEqual(root.adoptedStyleSheets, [earlier], 'remove is idempotent');
});
test('without constructable sheets, or when the engine refuses one, a <style> element is the fallback', () => {
FakeSheet.refuse = false;
const noCtor = fakeDocument({ constructable: false });
const root = fakeShadowRoot(noCtor.doc);
const styles = adoptStyles(root as unknown as ShadowRoot, '.x {}');
assert.equal(styles.adopted, false);
assert.equal(root.children.length, 1);
assert.equal(root.children[0].textContent, '.x {}');
styles.update('.y {}');
assert.equal(root.children[0].textContent, '.y {}');
styles.remove();
assert.equal(root.children.length, 0);
const noAdopt = fakeDocument();
const legacy = fakeShadowRoot(noAdopt.doc, false);
assert.equal(adoptStyles(legacy as unknown as ShadowRoot, '.z {}').adopted, false);
assert.equal(legacy.children.length, 1);
FakeSheet.refuse = true;
const refused = fakeDocument();
const root2 = fakeShadowRoot(refused.doc);
assert.equal(adoptStyles(root2 as unknown as ShadowRoot, '.w {}').adopted, false);
assert.equal(root2.children.length, 1);
FakeSheet.refuse = false;
});
test('a document root: adopted sheet, or a <style> in <head> as the fallback', () => {
FakeSheet.refuse = false;
const { doc, head } = fakeDocument();
const styles = adoptStyles(doc as unknown as Document, '.d {}');
assert.equal(styles.adopted, true);
assert.equal((doc.adoptedStyleSheets as FakeSheet[]).length, 1);
assert.equal(head.children.length, 0);
const plain = fakeDocument({ adopted: false });
adoptStyles(plain.doc as unknown as Document, '.e {}');
assert.equal(plain.head.children.length, 1);
});
const SITE: PluginApiSite = {
name: 'Demo', product: 'wayBetter for Test', storagePrefix: 'demo-plugin:', styleDataKey: 'demoPlugin',
toastId: 'demo-toast', toastBackground: '#111', itemSelector: 'article.message', chordKeys: ['g'],
};
function pluginEnv(cspBlocksInline: boolean) {
FakeSheet.refuse = false;
const { win, doc, head } = fakeDocument();
const createElement = doc.createElement as (tag: string) => FakeStyle;
Object.assign(doc, {
addEventListener() {}, removeEventListener() {}, activeElement: null, querySelectorAll: () => [],
createElement(tag: string) {
const node = createElement(tag) as FakeStyle & { dataset: Record<string, string>; isConnected: boolean };
node.dataset = {};
// A real <style>: a sheet once connected, none when the CSP refuses inline styles.
Object.defineProperty(node, 'sheet', { get: () => (node.parent && !cspBlocksInline ? {} : null) });
Object.defineProperty(node, 'isConnected', { get: () => !!node.parent });
return node;
},
});
Object.assign(win, {
location: { href: 'https://site.test/home' }, localStorage: null,
addEventListener() {}, removeEventListener() {},
requestAnimationFrame: (fn: () => void) => { fn(); return 1; }, cancelAnimationFrame: () => {},
MutationObserver: class { observe() {} disconnect() {} },
});
const api = createCorePluginApi(win as unknown as Window & typeof globalThis, SITE);
return { api, doc, head };
}
test('injectCSS: an inline <style> the page CSP refuses gets its rules as an adopted sheet instead', () => {
const { api, doc, head } = pluginEnv(true);
api.injectCSS('demo', '.a {}');
assert.equal(head.children.length, 1, 'the <style> element is still there (the CSP may change)');
const adopted = doc.adoptedStyleSheets as FakeSheet[];
assert.equal(adopted.length, 1);
assert.equal(adopted[0].css, '.a {}');
api.injectCSS('demo', '.b {}');
assert.equal(adopted.length, 1, 'updated in place, not stacked');
assert.equal((doc.adoptedStyleSheets as FakeSheet[])[0].css, '.b {}');
api.clearCSS('demo');
assert.equal((doc.adoptedStyleSheets as FakeSheet[]).length, 0);
assert.equal(head.children.length, 0);
});
test('injectCSS: an allowed <style> needs no adopted copy', () => {
const { api, doc, head } = pluginEnv(false);
api.injectCSS('demo', '.a {}');
assert.equal(head.children.length, 1);
assert.equal((doc.adoptedStyleSheets as FakeSheet[]).length, 0);
});
test('scope.adoptCSS takes the sheet off the root on dispose', () => {
const { api, doc } = pluginEnv(false);
const root = fakeShadowRoot(doc);
const scope = api.scope();
const styles = scope.adoptCSS(root as unknown as ShadowRoot, '.card {}');
assert.equal(styles.adopted, true);
assert.equal((root.adoptedStyleSheets as FakeSheet[]).length, 1);
scope.dispose();
assert.equal((root.adoptedStyleSheets as FakeSheet[]).length, 0);
});