首次提交:脑晴测 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:
242
brain-h5/tests/mobile-runtime.spec.ts
Normal file
242
brain-h5/tests/mobile-runtime.spec.ts
Normal 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();
|
||||
});
|
||||
82
brain-h5/tests/runtime-fixture.css
Normal file
82
brain-h5/tests/runtime-fixture.css
Normal file
@@ -0,0 +1,82 @@
|
||||
.fixture-screen {
|
||||
background: #f6f6f6;
|
||||
}
|
||||
|
||||
.fixture-content {
|
||||
min-height: 1200px;
|
||||
padding: 76px 24px 120px;
|
||||
}
|
||||
|
||||
.fixture-content h1 {
|
||||
margin: 0 0 20px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.sheet-trigger {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.fixture-carousel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fixture-carousel-track {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.carousel-card {
|
||||
width: 180px;
|
||||
height: 132px;
|
||||
flex: 0 0 auto;
|
||||
border: 0;
|
||||
border-radius: 18px;
|
||||
background: #d9e7ff;
|
||||
}
|
||||
|
||||
[data-testid="tap-count"] {
|
||||
display: block;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.fixture-tall-content {
|
||||
min-height: 900px;
|
||||
padding-top: 160px;
|
||||
}
|
||||
|
||||
.keyboard-footer {
|
||||
height: 84px;
|
||||
padding: 14px 20px;
|
||||
background: white;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.keyboard-footer input {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid #bbb;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.flow-fixture-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
align-items: center;
|
||||
height: var(--flow-header-content-height);
|
||||
padding: 0 18px;
|
||||
background: white;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.flow-fixture-header button {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.flow-fixture-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
height: 64px;
|
||||
padding: 10px 18px;
|
||||
background: white;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
12
brain-h5/tests/runtime-fixture.html
Normal file
12
brain-h5/tests/runtime-fixture.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Mobile runtime interaction fixture</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/tests/runtime-fixture.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
152
brain-h5/tests/runtime-fixture.tsx
Normal file
152
brain-h5/tests/runtime-fixture.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { StrictMode, useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import {
|
||||
BottomSheet,
|
||||
Carousel,
|
||||
FlowStack,
|
||||
KeyboardInput,
|
||||
MobileRuntime,
|
||||
MobileScroll,
|
||||
type FlowScreen,
|
||||
} from "../src/mobile";
|
||||
import "../src/styles.css";
|
||||
import "./runtime-fixture.css";
|
||||
|
||||
function CarouselFixture() {
|
||||
const [tapCount, setTapCount] = useState(0);
|
||||
const [sheetOpen, setSheetOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<MobileRuntime>
|
||||
<MobileScroll className="fixture-screen">
|
||||
<main className="fixture-content">
|
||||
<h1>Runtime fixture</h1>
|
||||
<button className="sheet-trigger" type="button" onClick={() => setSheetOpen(true)}>
|
||||
Open sheet
|
||||
</button>
|
||||
<Carousel
|
||||
ariaLabel="Featured cards"
|
||||
className="fixture-carousel"
|
||||
contentClassName="fixture-carousel-track"
|
||||
>
|
||||
{Array.from({ length: 7 }, (_, index) => (
|
||||
<button
|
||||
className="carousel-card"
|
||||
type="button"
|
||||
onClick={() => setTapCount((count) => count + 1)}
|
||||
key={index}
|
||||
>
|
||||
Card {index + 1}
|
||||
</button>
|
||||
))}
|
||||
</Carousel>
|
||||
<output data-testid="tap-count">{tapCount}</output>
|
||||
<div className="fixture-tall-content">Scrollable parent content</div>
|
||||
</main>
|
||||
</MobileScroll>
|
||||
<BottomSheet
|
||||
open={sheetOpen}
|
||||
onOpenChange={setSheetOpen}
|
||||
title="Animated sheet"
|
||||
description="Exit motion regression fixture."
|
||||
>
|
||||
<p>Sheet content</p>
|
||||
</BottomSheet>
|
||||
</MobileRuntime>
|
||||
);
|
||||
}
|
||||
|
||||
function KeyboardFixture() {
|
||||
const screen: FlowScreen = {
|
||||
id: "keyboard",
|
||||
footerHeight: 84,
|
||||
footer: () => (
|
||||
<div className="keyboard-footer">
|
||||
<KeyboardInput aria-label="Message" placeholder="Message" />
|
||||
</div>
|
||||
),
|
||||
render: () => (
|
||||
<MobileScroll className="fixture-screen">
|
||||
<main className="fixture-content fixture-tall-content">Keyboard fixture</main>
|
||||
</MobileScroll>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<MobileRuntime>
|
||||
<FlowStack initial={screen} />
|
||||
</MobileRuntime>
|
||||
);
|
||||
}
|
||||
|
||||
function stackedScreen(level: number): FlowScreen {
|
||||
return {
|
||||
id: `flow-level-${level}`,
|
||||
headerHeight: 56,
|
||||
header: (flow) => (
|
||||
<div className="flow-fixture-header">
|
||||
<button type="button" onClick={flow.pop}>‹ Back</button>
|
||||
<strong>Level {level}</strong>
|
||||
<span />
|
||||
</div>
|
||||
),
|
||||
footerHeight: 64,
|
||||
footer: (flow) => (
|
||||
<div className="flow-fixture-footer">
|
||||
<button type="button" onClick={flow.pop}>Done</button>
|
||||
</div>
|
||||
),
|
||||
render: (flow) => (
|
||||
<MobileScroll className="fixture-screen">
|
||||
<main className="fixture-content">
|
||||
<h1>{level === 2 ? "Screen stacking works" : `Nested view level ${level}`}</h1>
|
||||
{level < 4 ? (
|
||||
<button type="button" onClick={() => flow.push(stackedScreen(level + 1))}>
|
||||
Push level {level + 1}
|
||||
</button>
|
||||
) : null}
|
||||
</main>
|
||||
</MobileScroll>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function FlowFixture() {
|
||||
const screen: FlowScreen = {
|
||||
id: "flow-root",
|
||||
footerHeight: 84,
|
||||
footer: () => (
|
||||
<div className="keyboard-footer">
|
||||
<KeyboardInput aria-label="Flow message" placeholder="Message" />
|
||||
</div>
|
||||
),
|
||||
render: (flow) => (
|
||||
<MobileScroll className="fixture-screen">
|
||||
<main className="fixture-content">
|
||||
<h1>Flow root</h1>
|
||||
<button type="button" onClick={() => flow.push(stackedScreen(2))}>
|
||||
Push level 2
|
||||
</button>
|
||||
</main>
|
||||
</MobileScroll>
|
||||
),
|
||||
};
|
||||
|
||||
return (
|
||||
<MobileRuntime>
|
||||
<FlowStack initial={screen} />
|
||||
</MobileRuntime>
|
||||
);
|
||||
}
|
||||
|
||||
const fixture = new URLSearchParams(window.location.search).get("fixture");
|
||||
const fixtureElement =
|
||||
fixture === "keyboard"
|
||||
? <KeyboardFixture />
|
||||
: fixture === "flow"
|
||||
? <FlowFixture />
|
||||
: <CarouselFixture />;
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>{fixtureElement}</StrictMode>,
|
||||
);
|
||||
68
brain-h5/tests/sites-worker.test.mjs
Normal file
68
brain-h5/tests/sites-worker.test.mjs
Normal file
@@ -0,0 +1,68 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { access } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import worker from "../worker/index.js";
|
||||
|
||||
test("serves existing static assets without a fallback", async () => {
|
||||
const calls = [];
|
||||
const response = await worker.fetch(new Request("https://example.test/assets/app.js"), {
|
||||
ASSETS: {
|
||||
fetch: async (request) => {
|
||||
calls.push(new URL(request.url).pathname);
|
||||
return new Response("asset", { status: 200 });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(calls, ["/assets/app.js"]);
|
||||
});
|
||||
|
||||
test("falls back to index.html for an unknown app route", async () => {
|
||||
const calls = [];
|
||||
const response = await worker.fetch(
|
||||
new Request("https://example.test/flow/step-two?source=share", {
|
||||
headers: { accept: "text/html" },
|
||||
}),
|
||||
{
|
||||
ASSETS: {
|
||||
fetch: async (request) => {
|
||||
const url = new URL(request.url);
|
||||
calls.push(url.pathname + url.search);
|
||||
return new Response(url.pathname === "/index.html" ? "app" : "missing", {
|
||||
status: url.pathname === "/index.html" ? 200 : 404,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
assert.deepEqual(calls, ["/flow/step-two?source=share", "/index.html"]);
|
||||
});
|
||||
|
||||
test("does not turn missing API or write requests into the app shell", async () => {
|
||||
for (const request of [
|
||||
new Request("https://example.test/api/missing", { headers: { accept: "application/json" } }),
|
||||
new Request("https://example.test/flow", { method: "POST", headers: { accept: "text/html" } }),
|
||||
]) {
|
||||
let calls = 0;
|
||||
const response = await worker.fetch(request, {
|
||||
ASSETS: {
|
||||
fetch: async () => {
|
||||
calls += 1;
|
||||
return new Response("missing", { status: 404 });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(response.status, 404);
|
||||
assert.equal(calls, 1);
|
||||
}
|
||||
});
|
||||
|
||||
test("emits the files required by Sites packaging", async () => {
|
||||
await access(new URL("../dist/client/index.html", import.meta.url));
|
||||
await access(new URL("../dist/server/index.js", import.meta.url));
|
||||
await access(new URL("../dist/.openai/hosting.json", import.meta.url));
|
||||
});
|
||||
Reference in New Issue
Block a user