test/scripts/screenshots-layers.test.ts
Rama main · réplica de lectura del repositorio original.
import { expect } from "@std/expect";
import { afterEach, describe, it as test } from "@std/testing/bdd";
import { withWholePaintGroups } from "#scripts/screenshots/layers.ts";
import { addScreenshotStyle } from "#scripts/screenshots/style.ts";
import {
createDomInstaller,
createGlobalStash,
} from "#test-utils/happy-dom.ts";
type LinkEvent = "error" | "load";
interface FakeLink {
addEventListener: (event: LinkEvent, listener: () => void) => void;
dataset: Record<string, string>;
href: string;
rel: string;
remove: () => void;
removed: boolean;
send: (event: LinkEvent) => void;
}
interface FakeRoot {
appendChild: (link: FakeLink) => void;
links: FakeLink[];
querySelectorAll: (selector: string) => unknown[];
}
interface FakePaintStyle {
backgroundClip: string;
content: string;
display: string;
filter: string;
getPropertyValue: (name: string) => string;
maskImage: string;
mixBlendMode: string;
opacity: string;
visibility: string;
}
interface FakePaintElement {
attributes: Set<string>;
getRootNode: () => unknown;
hasAttribute: (name: string) => boolean;
matches: () => boolean;
parentElement: FakePaintElement | null;
pseudos: Record<string, FakePaintStyle>;
removeAttribute: (name: string) => void;
root: unknown;
setAttribute: (name: string) => void;
shadowRoot: FakePaintRoot | null;
style: FakePaintStyle;
}
interface PaintStyleOptions
extends Partial<Omit<FakePaintStyle, "getPropertyValue">> {
webkitBackgroundClip?: string;
webkitClipPath?: string;
webkitMaskImage?: string;
}
const makePaintStyle = (options: PaintStyleOptions = {}): FakePaintStyle => {
const {
webkitBackgroundClip = "border-box",
webkitClipPath = "none",
webkitMaskImage = "none",
...overrides
} = options;
return {
backgroundClip: "border-box",
content: '"paint"',
display: "block",
filter: "none",
getPropertyValue: (name) =>
name === "-webkit-background-clip"
? webkitBackgroundClip
: name === "-webkit-clip-path"
? webkitClipPath
: name === "-webkit-mask-image"
? webkitMaskImage
: "",
maskImage: "none",
mixBlendMode: "normal",
opacity: "1",
visibility: "visible",
...overrides,
};
};
class FakePaintRoot {
constructor(
readonly host: FakePaintElement,
readonly elements: FakePaintElement[],
readonly childNodes: { nodeType: number; textContent: string }[] = [],
) {}
querySelectorAll(selector: string): FakePaintElement[] {
return selector === "*" ? this.elements : [];
}
}
const makePaintElement = (
style: PaintStyleOptions = {},
matchesControl = false,
): FakePaintElement => {
const attributes = new Set<string>();
const element: FakePaintElement = {
attributes,
getRootNode: () => element.root,
hasAttribute: (name) => attributes.has(name),
matches: () => matchesControl,
parentElement: null,
pseudos: {
"::after": makePaintStyle({ content: '"after"', display: "none" }),
"::before": makePaintStyle({ content: "none" }),
},
removeAttribute: (name) => attributes.delete(name),
root: null,
setAttribute: (name) => attributes.add(name),
shadowRoot: null,
style: makePaintStyle(style),
};
return element;
};
const evaluationPage = {
evaluate: (fn: (argument: never) => unknown, argument: never) =>
Promise.resolve(fn(argument)),
} as never;
const makeLink = (): FakeLink => {
const listeners = new Map<LinkEvent, () => void>();
const link: FakeLink = {
addEventListener: (event, listener) => listeners.set(event, listener),
dataset: {},
href: "",
rel: "",
remove: () => {
link.removed = true;
},
removed: false,
send: (event) => listeners.get(event)?.(),
};
return link;
};
const makeRoot = (event: LinkEvent, children: FakeRoot[] = []): FakeRoot => {
const links: FakeLink[] = [];
return {
appendChild: (link) => {
links.push(link);
queueMicrotask(() => link.send(event));
},
links,
querySelectorAll: (selector) =>
selector === "*" ? children.map((shadowRoot) => ({ shadowRoot })) : links,
};
};
const setupPage = (
root: FakeRoot,
): {
globals: ReturnType<typeof createGlobalStash>;
page: never;
styleRemoved: () => boolean;
unrouted: () => boolean;
} => {
const globals = createGlobalStash();
globals.set("document", {
createElement: makeLink,
querySelectorAll: root.querySelectorAll,
});
let removedStyle = false;
let removedRoute = false;
return {
globals,
page: {
addStyleTag: () =>
Promise.resolve({
evaluate: (fn: (node: unknown) => unknown) =>
Promise.resolve(
fn({
parentNode: {
removeChild: () => {
removedStyle = true;
},
},
}),
),
}),
evaluate: (fn: (argument: never) => unknown, argument: never) =>
Promise.resolve(fn(argument)),
route: () => Promise.resolve(),
unroute: () => {
removedRoute = true;
return Promise.resolve();
},
url: () => "https://tickets.test/page",
} as never,
styleRemoved: () => removedStyle,
unrouted: () => removedRoute,
};
};
describe("screenshot layer styles", () => {
let pageGlobals: ReturnType<typeof createGlobalStash> | undefined;
afterEach(() => {
pageGlobals?.restore();
pageGlobals = undefined;
});
test("loads and removes styles in nested open shadow roots", async () => {
const nested = makeRoot("load");
const shadow = makeRoot("load", [nested]);
const documentRoot = makeRoot("load", [shadow]);
const state = setupPage(documentRoot);
pageGlobals = state.globals;
const remove = await addScreenshotStyle(
state.page,
"button { color: red; }",
);
expect(shadow.links).toHaveLength(1);
expect(nested.links).toHaveLength(1);
expect(shadow.links[0]).toEqual(
expect.objectContaining({ rel: "stylesheet", removed: false }),
);
await remove();
expect(shadow.links[0]?.removed).toBe(true);
expect(nested.links[0]?.removed).toBe(true);
expect(state.styleRemoved()).toBe(true);
expect(state.unrouted()).toBe(true);
});
test("cleans up when a shadow-root stylesheet fails to load", async () => {
const shadow = makeRoot("error");
const state = setupPage(makeRoot("load", [shadow]));
pageGlobals = state.globals;
await expect(
addScreenshotStyle(state.page, "button { color: red; }"),
).rejects.toThrow("Could not load screenshot style.");
expect(shadow.links[0]?.removed).toBe(true);
expect(state.styleRemoved()).toBe(true);
expect(state.unrouted()).toBe(true);
});
test("marks direct root text without changing the document tree", async () => {
const dom = createDomInstaller(["ShadowRoot", "getComputedStyle"]);
const window = dom.installDom("Words <p>Element</p> ");
const originalText = window.document.body.firstChild;
if (!originalText) throw new Error("Missing root text fixture.");
try {
await withWholePaintGroups(evaluationPage, () => {
expect(window.document.body.firstChild).toBe(originalText);
expect(
window.document.body.getAttribute("data-screenshot-whole-paint"),
).toBe("");
expect(window.document.body.lastChild?.nodeType).toBe(3);
return Promise.resolve();
});
expect(window.document.body.firstChild).toBe(originalText);
expect(
window.document.body.hasAttribute("data-screenshot-whole-paint"),
).toBe(false);
expect(window.document.body.textContent).toBe("Words Element ");
} finally {
await dom.cleanup();
}
});
test("marks paint groups and visible controls across composed trees", async () => {
const globals = createGlobalStash();
const whole = makePaintElement({ opacity: "0.5" });
const wholeChild = makePaintElement();
wholeChild.parentElement = whole;
const wholeGrandchild = makePaintElement();
wholeGrandchild.parentElement = wholeChild;
const text = makePaintElement({
webkitBackgroundClip: "border-box, text",
});
const textChild = makePaintElement();
textChild.parentElement = text;
const controlHost = makePaintElement({}, true);
controlHost.parentElement = wholeGrandchild;
const shadowControlChild = makePaintElement();
const shadowRoot = new FakePaintRoot(
controlHost,
[shadowControlChild],
[{ nodeType: 3, textContent: "Shadow words" }],
);
controlHost.shadowRoot = shadowRoot;
shadowControlChild.root = shadowRoot;
const hiddenControl = makePaintElement({ visibility: "hidden" }, true);
const generatedWhole = makePaintElement();
generatedWhole.pseudos["::before"] = makePaintStyle({
filter: "blur(1px)",
});
const generatedText = makePaintElement();
generatedText.pseudos["::before"] = makePaintStyle({
backgroundClip: "text",
});
const masked = makePaintElement({
webkitMaskImage: "linear-gradient(black, transparent)",
});
const clipped = makePaintElement({ webkitClipPath: "circle(50%)" });
const generatedImage = makePaintElement();
generatedImage.pseudos["::after"] = makePaintStyle({
content: 'url("icon.svg")',
});
const plain = makePaintElement();
const topLayerRoot = Object.assign(makePaintElement(), { childNodes: [] });
const topLevel = [
whole,
wholeChild,
wholeGrandchild,
text,
textChild,
controlHost,
hiddenControl,
generatedWhole,
generatedText,
masked,
clipped,
generatedImage,
plain,
topLayerRoot,
];
const root = { childNodes: [] };
const documentRoot = {
body: root,
documentElement: topLayerRoot,
querySelectorAll: (selector: string) =>
selector === "*"
? topLevel
: selector.includes("dialog:modal")
? [plain]
: [],
};
for (const element of topLevel) element.root = documentRoot;
globals.set("ShadowRoot", FakePaintRoot);
globals.set("document", documentRoot);
globals.set(
"getComputedStyle",
(element: FakePaintElement, pseudo?: string) =>
pseudo ? element.pseudos[pseudo] : element.style,
);
try {
await withWholePaintGroups(evaluationPage, () => {
expect([...whole.attributes]).toEqual(["data-screenshot-whole-paint"]);
expect([...wholeChild.attributes]).toEqual([]);
expect([...wholeGrandchild.attributes]).toEqual([]);
expect([...text.attributes]).toEqual(["data-screenshot-text-paint"]);
expect([...textChild.attributes]).toEqual([]);
expect(
controlHost.attributes.has("data-screenshot-visible-control"),
).toBe(true);
expect(
shadowControlChild.attributes.has("data-screenshot-visible-control"),
).toBe(true);
expect(
shadowControlChild.attributes.has("data-screenshot-whole-paint"),
).toBe(true);
expect(hiddenControl.attributes.size).toBe(0);
expect(
generatedWhole.attributes.has("data-screenshot-whole-paint"),
).toBe(true);
expect(generatedText.attributes.has("data-screenshot-text-paint")).toBe(
true,
);
expect(masked.attributes.has("data-screenshot-whole-paint")).toBe(true);
expect(clipped.attributes.has("data-screenshot-whole-paint")).toBe(
true,
);
expect(
generatedImage.attributes.has("data-screenshot-whole-paint"),
).toBe(true);
expect(plain.attributes.size).toBe(0);
expect(topLayerRoot.attributes.has("data-screenshot-whole-paint")).toBe(
true,
);
return Promise.resolve();
});
expect(topLevel.every((element) => element.attributes.size === 0)).toBe(
true,
);
expect(shadowControlChild.attributes.size).toBe(0);
} finally {
globals.restore();
}
});
});