| 1 | // Run: tsx src/__tests__/scroll-content-observer.test.ts |
| 2 | |
| 3 | import { JSDOM } from "jsdom"; |
| 4 | import { observeScrollContentSize } from "../lib/scrollContentObserver"; |
| 5 | |
| 6 | let passed = 0; |
| 7 | let failed = 0; |
| 8 | |
| 9 | function eq(actual: unknown, expected: unknown, label: string) { |
| 10 | if (actual === expected) { |
| 11 | process.stdout.write(` PASS ${label}\n`); |
| 12 | passed += 1; |
| 13 | } else { |
| 14 | process.stdout.write(` FAIL ${label}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}\n`); |
| 15 | failed += 1; |
| 16 | } |
| 17 | } |
| 18 | |
| 19 | console.log("\nscroll content observer"); |
| 20 | |
| 21 | const dom = new JSDOM("<!doctype html><html><body><div id=scroll><div id=first></div></div></body></html>"); |
| 22 | globalThis.window = dom.window as unknown as Window & typeof globalThis; |
| 23 | globalThis.document = dom.window.document; |
| 24 | |
| 25 | class FakeResizeObserver { |
| 26 | static current: FakeResizeObserver | null = null; |
| 27 | readonly observed = new Set<Element>(); |
| 28 | disconnected = false; |
| 29 | |
| 30 | constructor(private readonly callback: ResizeObserverCallback) { |
| 31 | FakeResizeObserver.current = this; |
| 32 | } |
| 33 | |
| 34 | observe(target: Element) { this.observed.add(target); } |
| 35 | unobserve(target: Element) { this.observed.delete(target); } |
| 36 | disconnect() { this.disconnected = true; this.observed.clear(); } |
| 37 | trigger() { this.callback([], this as unknown as ResizeObserver); } |
| 38 | } |
| 39 | |
| 40 | class FakeMutationObserver { |
| 41 | static current: FakeMutationObserver | null = null; |
| 42 | disconnected = false; |
| 43 | |
| 44 | constructor(private readonly callback: MutationCallback) { |
| 45 | FakeMutationObserver.current = this; |
| 46 | } |
| 47 | |
| 48 | observe() {} |
| 49 | disconnect() { this.disconnected = true; } |
| 50 | takeRecords(): MutationRecord[] { return []; } |
| 51 | trigger() { this.callback([], this as unknown as MutationObserver); } |
| 52 | } |
| 53 | |
| 54 | globalThis.ResizeObserver = FakeResizeObserver as unknown as typeof ResizeObserver; |
| 55 | globalThis.MutationObserver = FakeMutationObserver as unknown as typeof MutationObserver; |
| 56 | |
| 57 | const scroll = document.getElementById("scroll"); |
| 58 | const first = document.getElementById("first"); |
| 59 | if (!scroll || !first) throw new Error("missing test elements"); |
| 60 | |
| 61 | let changes = 0; |
| 62 | const stop = observeScrollContentSize(scroll, () => { changes += 1; }); |
| 63 | eq(FakeResizeObserver.current?.observed.has(first), true, "observes existing transcript children"); |
| 64 | |
| 65 | FakeResizeObserver.current?.trigger(); |
| 66 | eq(changes, 1, "content child resize refreshes scrollbar metrics"); |
| 67 | |
| 68 | const second = document.createElement("div"); |
| 69 | scroll.appendChild(second); |
| 70 | FakeMutationObserver.current?.trigger(); |
| 71 | eq(FakeResizeObserver.current?.observed.has(second), true, "observes children added after mount"); |
| 72 | eq(changes, 2, "child-list changes refresh scrollbar metrics"); |
| 73 | |
| 74 | first.remove(); |
| 75 | FakeMutationObserver.current?.trigger(); |
| 76 | eq(FakeResizeObserver.current?.observed.has(first), false, "stops observing removed transcript children"); |
| 77 | |
| 78 | stop(); |
| 79 | eq(FakeResizeObserver.current?.disconnected, true, "disconnects the resize observer on cleanup"); |
| 80 | eq(FakeMutationObserver.current?.disconnected, true, "disconnects the mutation observer on cleanup"); |
| 81 | |
| 82 | dom.window.close(); |
| 83 | |
| 84 | console.log(`\n${passed} passed, ${failed} failed, ${passed + failed} total`); |
| 85 | if (failed > 0) process.exit(1); |
| 86 |