Files
Netcatty/application/state/themeTransition.test.ts
2026-06-12 01:21:28 +08:00

130 lines
3.5 KiB
TypeScript

import assert from "node:assert/strict";
import test from "node:test";
import {
THEME_TRANSITION_ATTR,
THEME_TRANSITION_MS,
runThemeTransition,
} from "./themeTransition.ts";
function createRoot() {
const attributes = new Map<string, string>();
return {
attributes,
ownerDocument: { startViewTransition: undefined },
setAttribute: (name: string, value: string) => attributes.set(name, value),
removeAttribute: (name: string) => attributes.delete(name),
getAttribute: (name: string) => attributes.get(name) ?? null,
} as unknown as HTMLElement;
}
test("runThemeTransition applies tokens and clears fallback marker after duration", async () => {
const root = createRoot();
let applied = false;
runThemeTransition(() => {
applied = true;
}, root);
assert.equal(applied, true);
assert.equal(root.getAttribute(THEME_TRANSITION_ATTR), "true");
await new Promise((resolve) => setTimeout(resolve, THEME_TRANSITION_MS + 60));
assert.equal(root.getAttribute(THEME_TRANSITION_ATTR), null);
});
test("runThemeTransition cancels a pending fallback reset when invoked again", () => {
const root = createRoot();
let count = 0;
runThemeTransition(() => {
count += 1;
}, root);
runThemeTransition(() => {
count += 2;
}, root);
assert.equal(count, 3);
assert.equal(root.getAttribute(THEME_TRANSITION_ATTR), "true");
});
test("runThemeTransition uses view transition API when available", async () => {
const root = createRoot();
let applied = false;
let finished = false;
const doc = {
startViewTransition: (callback: () => void) => {
callback();
return {
finished: Promise.resolve().then(() => {
finished = true;
}),
skipTransition: () => {},
};
},
};
(root as { ownerDocument: typeof doc }).ownerDocument = doc;
runThemeTransition(() => {
applied = true;
}, root);
assert.equal(applied, true);
assert.equal(root.getAttribute(THEME_TRANSITION_ATTR), null);
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(finished, true);
});
test("runThemeTransition handles skipped view transitions", async () => {
const root = createRoot();
let applied = false;
let rejectFinished!: (reason: unknown) => void;
const doc = {
startViewTransition: (callback: () => void) => {
callback();
return {
finished: new Promise<void>((_, reject) => {
rejectFinished = reject;
}),
skipTransition: () => {},
};
},
};
(root as { ownerDocument: typeof doc }).ownerDocument = doc;
runThemeTransition(() => {
applied = true;
}, root);
rejectFinished(new DOMException("Transition was skipped", "AbortError"));
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(applied, true);
assert.equal(root.getAttribute(THEME_TRANSITION_ATTR), null);
});
test("runThemeTransition can apply without animation for heavy tab switches", () => {
const root = createRoot();
let applied = false;
let startViewTransitionCalled = false;
const doc = {
startViewTransition: (callback: () => void) => {
startViewTransitionCalled = true;
callback();
return {
finished: Promise.resolve(),
skipTransition: () => {},
};
},
};
(root as { ownerDocument: typeof doc }).ownerDocument = doc;
runThemeTransition(() => {
applied = true;
}, { root, mode: "instant" });
assert.equal(applied, true);
assert.equal(startViewTransitionCalled, false);
assert.equal(root.getAttribute(THEME_TRANSITION_ATTR), null);
});