首次提交:脑晴测 H5 项目

包含前端源码、预构建产物(dist/)、零依赖 Node.js 服务器(server.mjs)、
Cloudflare Worker 版本(worker/)、部署文档(DEPLOY.md)和交接文档。

技术总监可直接 clone 后运行 node server.mjs 启动,无需安装 React 或 build。
DEEPSEEK_API_KEY 通过环境变量注入,未纳入仓库。
This commit is contained in:
MingNian
2026-07-28 10:24:47 +08:00
commit b0fce840a3
152 changed files with 8536 additions and 0 deletions

View File

@@ -0,0 +1,242 @@
import { expect, test, type Locator, type Page } from "@playwright/test";
async function drag(page: Page, locator: Locator, deltaX: number, deltaY: number, steps = 8) {
const box = await locator.boundingBox();
if (!box) throw new Error("Drag target has no bounding box");
const startX = box.x + box.width / 2;
const startY = box.y + box.height / 2;
await page.mouse.move(startX, startY);
await page.mouse.down();
for (let step = 1; step <= steps; step += 1) {
await page.mouse.move(
startX + (deltaX * step) / steps,
startY + (deltaY * step) / steps,
);
await page.waitForTimeout(8);
}
await page.mouse.up();
}
test.beforeEach(async ({ page }) => {
await page.goto("/tests/runtime-fixture.html");
});
test("horizontal intent stays in Carousel and cannot create parent momentum", async ({ page }) => {
const carousel = page.locator(".fixture-carousel");
const card = page.locator(".carousel-card").nth(1);
const parent = page.getByTestId("mobile-scroll");
await expect(carousel).not.toHaveAttribute("data-scroll-drag", "ignore");
await drag(page, card, -130, 14, 5);
const afterRelease = await carousel.evaluate((element) => element.scrollLeft);
expect(afterRelease).toBeGreaterThan(40);
expect(await parent.evaluate((element) => element.scrollTop)).toBe(0);
await page.waitForTimeout(250);
expect(await parent.evaluate((element) => element.scrollTop)).toBe(0);
expect(await page.getByTestId("tap-count").textContent()).toBe("0");
});
test("vertical intent over a carousel is handed to MobileScroll in both directions", async ({ page }) => {
const card = page.locator(".carousel-card").nth(1);
const carousel = page.locator(".fixture-carousel");
const parent = page.getByTestId("mobile-scroll");
await drag(page, card, 4, -150);
expect(await parent.evaluate((element) => element.scrollTop)).toBeGreaterThan(60);
expect(await carousel.evaluate((element) => element.scrollLeft)).toBe(0);
await parent.evaluate((element) => {
element.scrollTop = 80;
});
await drag(page, card, -3, 110);
expect(await parent.evaluate((element) => element.scrollTop)).toBeLessThan(80);
});
test("tap activates a card but a completed drag does not", async ({ page }) => {
const firstCard = page.locator(".carousel-card").first();
await firstCard.click();
await expect(page.getByTestId("tap-count")).toHaveText("1");
await drag(page, firstCard, -100, 6);
await expect(page.getByTestId("tap-count")).toHaveText("1");
});
test("Carousel preserves momentum and edge rubber-banding", async ({ page }) => {
const carousel = page.locator(".fixture-carousel");
const card = page.locator(".carousel-card").nth(1);
await drag(page, card, -100, 5, 3);
const releasedOffset = await carousel.evaluate((element) => element.scrollLeft);
await page.waitForTimeout(120);
expect(await carousel.evaluate((element) => element.scrollLeft)).toBeGreaterThan(releasedOffset);
await carousel.evaluate((element) => {
element.scrollLeft = 0;
});
const box = await card.boundingBox();
if (!box) throw new Error("Card has no bounding box");
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 2 + 90, box.y + box.height / 2, { steps: 4 });
expect(Number(await carousel.getAttribute("data-overscroll"))).toBeGreaterThan(0);
await page.mouse.up();
await page.waitForTimeout(900);
expect(Math.abs(Number(await carousel.getAttribute("data-overscroll")))).toBeLessThan(1);
});
test("BottomSheet remains mounted while its default exit animation plays", async ({ page }) => {
await page.locator(".sheet-trigger").click();
await expect(page.getByTestId("bottom-sheet")).toBeVisible();
await page.getByTestId("sheet-overlay").click({ position: { x: 8, y: 8 } });
await expect(page.getByTestId("bottom-sheet")).toHaveCount(1);
await page.waitForTimeout(500);
await expect(page.getByTestId("bottom-sheet")).toHaveCount(0);
});
test("keyboard and its attached footer dismiss on the same transition", async ({ page }) => {
await page.goto("/tests/runtime-fixture.html?fixture=keyboard");
const input = page.getByLabel("Message");
const footer = page.getByTestId("flow-fixed-footer");
const keyboard = page.getByTestId("keyboard-dock");
await input.click();
await expect(keyboard).toHaveAttribute("data-visible", "true");
await drag(page, footer, 0, 120, 5);
await expect(keyboard).toHaveAttribute("data-visible", "false");
await page.waitForTimeout(100);
const progress = await page.evaluate(() => {
const footerElement = document.querySelector<HTMLElement>('[data-testid="flow-fixed-footer"]')!;
const keyboardElement = document.querySelector<HTMLElement>('[data-testid="keyboard-dock"]')!;
const fullHeight = Number.parseFloat(keyboardElement.style.height);
const footerRemaining = Number.parseFloat(getComputedStyle(footerElement).bottom);
const matrix = new DOMMatrixReadOnly(getComputedStyle(keyboardElement).transform);
return {
footer: footerRemaining / fullHeight,
keyboard: 1 - matrix.m42 / fullHeight,
};
});
expect(Math.abs(progress.footer - progress.keyboard)).toBeLessThan(0.18);
await page.waitForTimeout(300);
expect(await footer.evaluate((element) => getComputedStyle(element).bottom)).toBe("34px");
});
test("switching to Pixel keeps the composer above Android navigation", async ({ page }) => {
await page.goto("/tests/runtime-fixture.html?fixture=keyboard");
const input = page.getByLabel("Message");
await input.evaluate((element: HTMLInputElement) => {
element.value = "Draft message";
});
await page.getByTestId("device-picker").click();
await page.getByTestId("device-option-pixel-10").click();
const frame = page.getByTestId("phone-frame");
const screen = page.getByTestId("device-screen");
const statusIndicators = page.getByTestId("status-indicators");
const navigation = page.getByTestId("android-navigation-bar");
const footer = page.getByTestId("flow-fixed-footer");
await expect(frame).toHaveAttribute("data-device", "pixel-10");
await expect(screen).toHaveAttribute("data-device", "pixel-10");
await expect(page.locator(".phone-bezel")).toHaveAttribute(
"src",
"/assets/android/Pixel10.png",
);
await expect(statusIndicators).toHaveAttribute("data-platform", "android");
await expect(statusIndicators).toHaveAttribute(
"src",
"/assets/status/status-icons.svg",
);
await expect(navigation).toBeVisible();
await expect(page.getByTestId("home-indicator")).toHaveCount(0);
await expect(input).toHaveValue("Draft message");
await page.waitForTimeout(300);
const layout = await page.evaluate(() => {
const footerElement = document.querySelector<HTMLElement>(
'[data-testid="flow-fixed-footer"]',
)!;
const navigationElement = document.querySelector<HTMLElement>(
'[data-testid="android-navigation-bar"]',
)!;
const appViewportElement = document.querySelector<HTMLElement>(
'[data-testid="mobile-app-viewport"]',
)!;
return {
footerBottom: footerElement.getBoundingClientRect().bottom,
appViewportBottom: appViewportElement.getBoundingClientRect().bottom,
navigationTop: navigationElement.getBoundingClientRect().top,
navigationHeight: Number.parseFloat(getComputedStyle(navigationElement).height),
safeAreaBottom: Number.parseFloat(
getComputedStyle(document.querySelector<HTMLElement>('[data-testid="device-screen"]')!).getPropertyValue(
"--device-safe-area-bottom",
),
),
};
});
expect(layout.safeAreaBottom).toBe(layout.navigationHeight);
expect(Math.abs(layout.appViewportBottom - layout.navigationTop)).toBeLessThanOrEqual(1);
expect(Math.abs(layout.footerBottom - layout.navigationTop)).toBeLessThanOrEqual(1);
await input.click();
await expect(page.getByTestId("keyboard-dock")).toHaveAttribute("data-visible", "true");
await expect(navigation).toHaveCount(0);
await page.waitForTimeout(300);
const keyboardLayout = await page.evaluate(() => {
const screen = document.querySelector<HTMLElement>('[data-testid="device-screen"]')!;
const viewport = document.querySelector<HTMLElement>('[data-testid="mobile-app-viewport"]')!;
const scroll = document.querySelector<HTMLElement>('[data-testid="mobile-scroll"]')!;
const footerElement = document.querySelector<HTMLElement>('[data-testid="flow-fixed-footer"]')!;
const keyboard = document.querySelector<HTMLElement>('[data-testid="keyboard-dock"]')!;
return {
screenBottom: screen.getBoundingClientRect().bottom,
viewportBottom: viewport.getBoundingClientRect().bottom,
scrollBottom: scroll.getBoundingClientRect().bottom,
footerBottom: footerElement.getBoundingClientRect().bottom,
keyboardTop: keyboard.getBoundingClientRect().top,
keyboardBottom: keyboard.getBoundingClientRect().bottom,
};
});
expect(keyboardLayout.viewportBottom).toBeCloseTo(keyboardLayout.screenBottom, 0);
expect(Math.abs(keyboardLayout.keyboardBottom - keyboardLayout.screenBottom)).toBeLessThanOrEqual(1);
expect(Math.abs(keyboardLayout.scrollBottom - keyboardLayout.keyboardTop)).toBeLessThanOrEqual(1);
expect(Math.abs(keyboardLayout.footerBottom - keyboardLayout.keyboardTop)).toBeLessThanOrEqual(1);
});
test("FlowStack pushes and pops screens while dismissing the keyboard", async ({ page }) => {
await page.goto("/tests/runtime-fixture.html?fixture=flow");
await page.getByLabel("Flow message").click();
await expect(page.getByTestId("keyboard-dock")).toHaveAttribute("data-visible", "true");
await page.getByRole("button", { name: "Push level 2" }).click();
await expect(page.getByRole("heading", { name: "Screen stacking works" })).toBeVisible();
await expect(page.getByTestId("keyboard-dock")).toHaveAttribute("data-visible", "false");
const safeHeaderPlacement = await page.evaluate(() => {
const screen = document.querySelector<HTMLElement>('[data-testid="device-screen"]')!;
const toolbar = document.querySelector<HTMLElement>(".flow-fixture-header")!;
return toolbar.getBoundingClientRect().top - screen.getBoundingClientRect().top;
});
expect(safeHeaderPlacement).toBeGreaterThanOrEqual(54);
await page.getByRole("button", { name: "Push level 3" }).click();
await expect(page.getByRole("heading", { name: "Nested view level 3" })).toBeVisible();
await page.getByRole("button", { name: "Push level 4" }).click();
await expect(page.getByRole("heading", { name: "Nested view level 4" })).toBeVisible();
await page.getByRole("button", { name: "Done" }).click();
await expect(page.getByRole("heading", { name: "Nested view level 3" })).toBeVisible();
await page.getByRole("button", { name: " Back" }).click();
await expect(page.getByRole("heading", { name: "Screen stacking works" })).toBeVisible();
await page.getByRole("button", { name: "Done" }).click();
await expect(page.getByRole("heading", { name: "Flow root" })).toBeVisible();
});