feat(learner): module lists and folds open with motion
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a953e35178
commit
a83ac02a90
@@ -59,6 +59,10 @@ const click = (sel: string) => chrome.eval(`document.querySelector(${js(sel)}).c
|
||||
const text = (sel: string) => chrome.eval<string | null>(`document.querySelector(${js(sel)})?.textContent ?? null`);
|
||||
const rect = (sel: string) => chrome.eval<string>(`JSON.stringify(document.querySelector(${js(sel)}).getBoundingClientRect())`);
|
||||
|
||||
/** The animations and transitions running now, as "name@class::pseudo" (some pseudo-elements send no events). */
|
||||
const running = () => chrome.eval<string[]>(`document.getAnimations().map((a) => (a.transitionProperty || a.animationName) + "@" + (a.effect.target.getAttribute?.("class") || a.effect.target.tagName?.toLowerCase()) + (a.effect.pseudoElement || ""))`);
|
||||
const afterFrame = "new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)))";
|
||||
|
||||
/** Full page load of `path`; waits for `ready` to exist. */
|
||||
async function open(path: string, ready: string) {
|
||||
await chrome.goto(`${base}${path}`, 50);
|
||||
@@ -150,4 +154,39 @@ run("navigation motion (headless Chrome)", () => {
|
||||
await until(async () => (await chrome.eval<number>("scrollY")) === 900, "restored scroll", 2000);
|
||||
expect(await rect(".lr-side")).toBe(side);
|
||||
}, 30_000);
|
||||
|
||||
test("a module opened by the user rises in; lists do not animate on page load or lesson change", async () => {
|
||||
await open("/c/sample/m1-intro", ".lr-main .lr-lesson h1.title");
|
||||
await click('.lr-les[href="/c/sample/m1-code"]');
|
||||
await until(async () => (await text(".lr-main .lr-lesson h1.title")) === "Code along", "code lesson");
|
||||
await Bun.sleep(300);
|
||||
expect((await m()).anims.filter((a) => a.includes("lr-lessons"))).toEqual([]);
|
||||
await reset();
|
||||
const head = '[...document.querySelectorAll(".lr-mod-head")][1]';
|
||||
expect(await chrome.eval<string>(`${head}.getAttribute("aria-expanded")`)).toBe("false");
|
||||
await chrome.eval(`${head}.click(), true`);
|
||||
// Open at once: the list is there and focusable in the same task as the click.
|
||||
expect(await chrome.eval<number>(`${head}.parentElement.querySelectorAll(".lr-lessons a").length`)).toBe(2);
|
||||
await Bun.sleep(300);
|
||||
expect((await m()).anims).toContain("stc-enter@lr-lessons");
|
||||
}, 30_000);
|
||||
|
||||
test("folds open and close by animating their height", async () => {
|
||||
// Chrome animates ::details-content without listing it in getAnimations() or sending events, so sample its height.
|
||||
const sample = (open: boolean) => chrome.eval<number[]>(`(() => {
|
||||
const d = document.querySelector("details.fold.transcript");
|
||||
d.querySelector("summary").click();
|
||||
if (d.open !== ${open}) return [-1];
|
||||
const out = [], t0 = performance.now();
|
||||
return new Promise((r) => { const f = () => { out.push(parseFloat(getComputedStyle(d, "::details-content").height)); if (performance.now() - t0 < 400) requestAnimationFrame(f); else r(out); }; f(); });
|
||||
})()`);
|
||||
await open("/c/sample/m1-video", "details.fold.transcript");
|
||||
const opening = await sample(true);
|
||||
const full = opening.at(-1)!;
|
||||
expect(full).toBeGreaterThan(20);
|
||||
expect(opening.some((h) => h > 1 && h < full - 1)).toBe(true); // passed through in-between heights
|
||||
const closing = await sample(false);
|
||||
expect(closing.some((h) => h > 1 && h < full - 1)).toBe(true);
|
||||
expect(closing.at(-1)).toBe(0);
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user