Files
Jyotisha/frontend/tests/mobile-viewport-scroll-lock-20260917.test.ts
T
jesse-ux 1b508d5e9a
Independent Staging Quality Gate / validate (push) Successful in 11m21s
Independent Staging Quality Gate / publish (push) Successful in 10m46s
fix(web): iPhone 键盘收起后顶栏复位,积分块与盘面芯片同高
2026-09-17 13:13:39 +08:00

125 lines
4.5 KiB
TypeScript

import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { cssDeclarations } from "./css-contract-test-support.ts";
import {
installViewportScrollLock,
isKeyboardOpen,
shouldResetWindowScroll,
type ViewportScrollLockHost,
} from "../src/lib/viewport-scroll-lock.ts";
const layout = readFileSync(new URL("../src/app/layout.tsx", import.meta.url), "utf8");
const component = readFileSync(new URL("../src/components/viewport-scroll-lock.tsx", import.meta.url), "utf8");
const lock = readFileSync(new URL("../src/lib/viewport-scroll-lock.ts", import.meta.url), "utf8");
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
test("ViewportScrollLock mounts beside StaleClientRecovery and uses the shared installer", () => {
assert.match(layout, /import \{ ViewportScrollLock \} from "@\/components\/viewport-scroll-lock"/);
assert.match(layout, /<StaleClientRecovery \/>\s*<ViewportScrollLock \/>/);
assert.match(component, /installViewportScrollLock/);
assert.match(lock, /history\.scrollRestoration = "manual"/);
assert.match(lock, /addEventListener\("resize"/);
assert.match(lock, /addEventListener\("scroll"/);
assert.match(lock, /addEventListener\("pageshow"/);
assert.match(lock, /addEventListener\("orientationchange"/);
assert.match(lock, /addEventListener\("focusout"/);
assert.match(lock, /KEYBOARD_OPEN_GAP_PX = 100/);
assert.doesNotMatch(lock, /setInterval/);
});
test("keyboard-open window offsets are left alone", () => {
assert.equal(isKeyboardOpen(680, 800), true);
assert.equal(isKeyboardOpen(799, 800), false);
assert.equal(shouldResetWindowScroll({
scrollY: 260,
innerHeight: 800,
visualViewportHeight: 680,
}), false);
assert.equal(shouldResetWindowScroll({
scrollY: 260,
innerHeight: 800,
visualViewportHeight: 800,
}), true);
assert.equal(shouldResetWindowScroll({
scrollY: 0,
innerHeight: 800,
visualViewportHeight: 800,
}), false);
});
test("visualViewport resize resets window scroll once the keyboard has closed", () => {
const calls: Array<[number, number]> = [];
const viewportListeners = new Map<string, () => void>();
const host: ViewportScrollLockHost = {
innerHeight: 800,
scrollY: 260,
scrollTo(x, y) {
host.scrollY = y;
calls.push([x, y]);
},
history: { scrollRestoration: "auto" },
visualViewport: {
height: 800,
addEventListener(type, listener) {
viewportListeners.set(type, listener);
},
removeEventListener(type) {
viewportListeners.delete(type);
},
},
matchMedia: () => ({ matches: true }),
addEventListener() {},
removeEventListener() {},
document: {
addEventListener() {},
removeEventListener() {},
},
};
const uninstall = installViewportScrollLock(host);
assert.equal(host.history.scrollRestoration, "manual");
assert.deepEqual(calls, [[0, 0]]);
host.scrollY = 180;
host.visualViewport!.height = 680;
viewportListeners.get("resize")?.();
assert.equal(calls.length, 1);
host.visualViewport!.height = 800;
viewportListeners.get("resize")?.();
assert.deepEqual(calls.at(-1), [0, 0]);
assert.equal(calls.length, 2);
uninstall();
});
test("header chips share height, type size, and radius, and drop the 64px min-width", () => {
const shared = cssDeclarations(
".chat-header-actions .credit-button,\n.chat-header-rectification .rectification-board-peek",
styles,
);
assert.match(shared, /height:\s*40px/);
assert.match(shared, /min-height:\s*40px/);
assert.match(shared, /font-size:\s*var\(--type-body-sm\)/);
assert.match(shared, /border-radius:\s*var\(--radius-md\)/);
assert.match(shared, /font-variant-numeric:\s*tabular-nums/);
assert.doesNotMatch(shared, /min-width:/);
const credit = cssDeclarations(".credit-button", styles);
assert.doesNotMatch(credit, /min-width:\s*64px/);
const hit = cssDeclarations(
".chat-header-actions .credit-button::before,\n.chat-header-rectification .rectification-board-peek::before",
styles,
);
assert.match(hit, /inset:\s*-2px/);
});
test("compact chat header is 52px plus the safe-area inset", () => {
assert.match(
styles,
/\.chat-panel \{ grid-template-rows: calc\(52px \+ env\(safe-area-inset-top\)\) minmax\(0, 1fr\) auto; \}/,
);
assert.match(
styles,
/\.chat-panel\.is-rectification \{ grid-template-rows: calc\(52px \+ env\(safe-area-inset-top\)\) minmax\(0, 1fr\); \}/,
);
assert.match(styles, /\.chat-panel \{[^}]*grid-template-rows:\s*46px minmax\(0,\s*1fr\) auto/);
});