hi
'); }); }); + +describe('purifyHtml -- Task 25: block-scoped (no CSS content) does not trigger a wrapper', () => { + const out = purifyHtml('hi
'); + expect(out).not.toContain('x
'; + const out1 = purifyHtml(code); + const out2 = purifyHtml(code); + expect(out1).toBe(out2); + const class1 = out1.match(/class="(whp-html-[0-9a-z]+)"/)![1]; + const class2 = out2.match(/class="(whp-html-[0-9a-z]+)"/)![1]; + expect(class1).toBe(class2); + }); + + test('pinned scope class for a known input -- guards against silent hash-function drift', () => { + // If this ever needs to change, it means the hash function itself + // changed -- which would silently churn every stored site's HTML on + // next save and desync already-published pages from a fresh Preview. + // That should be a loud, deliberate decision, not a side-effect of an + // unrelated refactor -- hence pinning the literal output here. + const code = ''; + expect(stableHash(code)).toBe('5fwbyn'); + const out = purifyHtml(code); + expect(out).toContain('class="whp-html-5fwbyn"'); + }); + + test('scope class is a pure function of `code` -- does not depend on Craft node id or call order', () => { + // purifyHtml's signature only ever takes the code string -- there is no + // node id parameter it could even reach for. This test documents that + // invariant so a future refactor threading a node id through here (as + // html-export.ts's renderNode already does for OTHER components, see + // its `scopeId` comment) doesn't silently get wired into this path too. + const codeA = 'x
'); + const scopeClass = out.match(/class="(whp-html-[0-9a-z]+)"/)![1]; + expect(out).toContain(`.${scopeClass} { --brand: red; }`); + expect(out).toContain(`.${scopeClass} { margin: 0; }`); + }); + + test('@import is stripped end-to-end (network-fetch/exfiltration channel)', () => { + const out = purifyHtml('B
'); + const classes = [...out.matchAll(/class="(whp-html-[0-9a-z]+)"/g)].map((m) => m[1]); + expect(classes.length).toBeGreaterThanOrEqual(1); + expect(new Set(classes).size).toBe(1); // same block -> same scope class everywhere + }); +}); + +describe('purifyHtml -- Task 25: security properties of the newly-allowed inside a CSS comment cannot break out into executable markup', () => { + const out = purifyHtml( + ' */ h1{color:red}hi
', + ); + expect(out).not.toContain('"}hi
', + ); + expect(out).not.toContain('y', + ); + expect(out).not.toMatch(/onclick/i); + expect(out).not.toContain('