chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
.env
test-results
+156
View File
@@ -0,0 +1,156 @@
# Examples E2E (Playwright) — Notes for Agents & Future Devs
This folder contains an end-to-end (e2e) smoke test harness for the repositorys `examples/`.
The goals are:
- Provide a consistent way to smoke-test examples locally and in CI.
- Support multiple example “shapes” (Next-only vs hybrid examples that also have an agent).
- Keep tests lightweight and stable (avoid flakiness, avoid requiring real API keys).
## How the harness works
### Selecting which example to run
This suite intentionally runs **one example at a time**.
- The active example is selected via the `EXAMPLE` environment variable.
- If `EXAMPLE` is not set, it defaults to `form-filling`.
The Playwright config (`playwright.config.ts`) uses `EXAMPLE` to:
- Set the `webServer.cwd` to the chosen example directory (`examples/v1/${EXAMPLE}`).
- Choose the `webServer.command` used to start the app.
### Why each spec has `const EXAMPLE = process.env.EXAMPLE ?? "form-filling";`
Each spec file is **gated** so that when you run the suite for one example:
- The matching spec runs.
- All other specs skip.
This makes it easy to run a CI matrix (one job per example) while keeping all tests in one folder.
## Example types
### Next.js-only examples
These can be started with:
- `pnpm dev`
### Hybrid examples (UI + agent)
Some examples have a Python agent that can be run alongside the UI.
For e2e smoke tests we typically only need the UI to boot, so the Playwright config treats these as “hybrid”:
- `travel`
- `research-canvas`
For hybrid examples the `webServer.command` is:
- `pnpm dev:ui`
This avoids starting the Python agent during UI-only smoke tests.
## Local setup
### Install Playwright harness deps
From `examples/e2e`:
- `pnpm install`
- `pnpm exec playwright install --with-deps chromium`
### Install the examples deps
Each example has its own `package.json`.
Install deps in the example directory you want to test, e.g.:
- `cd examples/v1/travel && pnpm install`
Notes:
- If an example has a `postinstall` that requires non-Node tooling (e.g. Python `uv`), you may want to run `pnpm install --ignore-scripts` for CI-like behavior.
### Run a single example
From `examples/e2e`:
- `EXAMPLE=form-filling pnpm test`
- `EXAMPLE=travel pnpm test`
- `EXAMPLE=research-canvas pnpm test`
- `EXAMPLE=chat-with-your-data pnpm test`
- `EXAMPLE=state-machine pnpm test`
When `EXAMPLE` is set, you should see `1 passed` and the other example specs `skipped`.
## Test layout
- Tests live under `tests/v1.x/`.
- Each example gets a single smoke spec (minimal assertions).
Examples:
- `tests/v1.x/form-filling.spec.ts`
- `tests/v1.x/travel.spec.ts`
## Writing smoke tests (guidelines)
Keep smoke tests:
- **Stable**: prefer `getByRole` selectors and obvious headings/buttons.
- **Cheap**: do not rely on LLM outputs.
- **Non-invasive**: avoid sending chat messages or triggering expensive background work.
Patterns used:
- Gate the spec:
- `test.skip(EXAMPLE !== "<example>", ...)`
- Prefer:
- `await expect(page).toHaveTitle(/.../)`
- `await expect(page.getByRole("heading", { name: "..." })).toBeVisible()`
If an example auto-opens Copilot UI / triggers calls, prefer adding a query param to disable it (e.g. `travel` uses `/?copilotOpen=false`).
## CI (GitHub Actions)
Workflow:
- `.github/workflows/test_e2e-legacy-v1.yml`
It runs a matrix of:
- `form-filling`
- `travel`
- `research-canvas`
- `chat-with-your-data`
- `state-machine`
Key CI behaviors:
- Installs `examples/e2e` deps and Playwright Chromium.
- Installs the selected examples deps.
- Uses `pnpm install --frozen-lockfile` for deterministic installs.
- For `research-canvas`, installs with `--ignore-scripts` to avoid requiring Python tooling just to run UI smoke tests.
Artifacts:
- Always uploads Playwright output (`test-results` and `playwright-report`) for debugging.
## Common issues / debugging
- "`next: command not found`": the selected examples `node_modules` are missing; run `pnpm install` in that example directory.
- "module not found" for a transitive dep (e.g. `shiki`): add it explicitly to the examples dependencies and reinstall.
- Next.js dev warnings about cross-origin (`allowedDevOrigins`): currently treated as warnings; tests can still pass.
## Adding a new example
1. Ensure the example can be started via `pnpm dev` (Next-only) or `pnpm dev:ui` (hybrid).
2. Add a new spec under `tests/v1.x/<example>.spec.ts`.
3. Run locally:
- `EXAMPLE=<example> pnpm test`
4. Add the example name to the CI matrix in:
- `.github/workflows/test_e2e-legacy-v1.yml`
+15
View File
@@ -0,0 +1,15 @@
{
"name": "examples-e2e",
"version": "0.0.0",
"private": true,
"scripts": {
"test": "playwright test",
"test:headed": "playwright test --headed",
"test:ui": "playwright test --ui"
},
"devDependencies": {
"@playwright/test": "^1.50.0",
"@types/node": "^22",
"typescript": "^5"
}
}
+53
View File
@@ -0,0 +1,53 @@
import { defineConfig, devices } from "@playwright/test";
import path from "path";
const EXAMPLE = process.env.EXAMPLE ?? "form-filling";
const PORT = Number(process.env.PORT ?? "3000");
const HYBRID_EXAMPLES = new Set(["travel", "research-canvas"]);
const webServerCommand = HYBRID_EXAMPLES.has(EXAMPLE)
? "pnpm dev:ui"
: "pnpm dev";
const exampleDir = path.resolve(__dirname, "../v1", EXAMPLE);
export default defineConfig({
testDir: "./tests",
timeout: 60_000,
expect: {
timeout: 10_000,
},
use: {
baseURL: `http://127.0.0.1:${PORT}`,
trace: "retain-on-failure",
video: "retain-on-failure",
},
webServer: {
command: webServerCommand,
url: `http://127.0.0.1:${PORT}`,
cwd: exampleDir,
reuseExistingServer: !process.env.CI,
timeout: 180_000,
env: {
...process.env,
PORT: String(PORT),
NEXT_TELEMETRY_DISABLED: "1",
OPENAI_API_KEY: process.env.OPENAI_API_KEY ?? "test",
NEXT_PUBLIC_CPK_PUBLIC_API_KEY:
process.env.NEXT_PUBLIC_CPK_PUBLIC_API_KEY ?? "",
NEXT_PUBLIC_COPILOT_PUBLIC_API_KEY:
process.env.NEXT_PUBLIC_COPILOT_PUBLIC_API_KEY ?? "",
LANGSMITH_API_KEY: process.env.LANGSMITH_API_KEY ?? "",
REMOTE_ACTION_URL:
process.env.REMOTE_ACTION_URL ?? "http://127.0.0.1:8000/copilotkit",
},
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
reporter: process.env.CI ? "github" : "list",
outputDir: "test-results",
});
+95
View File
@@ -0,0 +1,95 @@
lockfileVersion: "9.0"
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
"@playwright/test":
specifier: ^1.50.0
version: 1.57.0
"@types/node":
specifier: ^22
version: 22.19.3
typescript:
specifier: ^5
version: 5.9.3
packages:
"@playwright/test@1.57.0":
resolution:
{
integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==,
}
engines: { node: ">=18" }
hasBin: true
"@types/node@22.19.3":
resolution:
{
integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==,
}
fsevents@2.3.2:
resolution:
{
integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==,
}
engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 }
os: [darwin]
playwright-core@1.57.0:
resolution:
{
integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==,
}
engines: { node: ">=18" }
hasBin: true
playwright@1.57.0:
resolution:
{
integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==,
}
engines: { node: ">=18" }
hasBin: true
typescript@5.9.3:
resolution:
{
integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==,
}
engines: { node: ">=14.17" }
hasBin: true
undici-types@6.21.0:
resolution:
{
integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==,
}
snapshots:
"@playwright/test@1.57.0":
dependencies:
playwright: 1.57.0
"@types/node@22.19.3":
dependencies:
undici-types: 6.21.0
fsevents@2.3.2:
optional: true
playwright-core@1.57.0: {}
playwright@1.57.0:
dependencies:
playwright-core: 1.57.0
optionalDependencies:
fsevents: 2.3.2
typescript@5.9.3: {}
undici-types@6.21.0: {}
View File
@@ -0,0 +1,78 @@
/**
* Regression test for the chat window flex layout broken in 1.55.
*
* Root cause: the drag-and-drop wrapper div introduced with attachments had
* no CSS class when not dragging, so no flex rule applied and the chat body
* collapsed to content height instead of filling the window.
*/
import { test, expect } from "@playwright/test";
const EXAMPLE = process.env.EXAMPLE ?? "form-filling";
test.describe("chat window layout", () => {
test.skip(EXAMPLE !== "form-filling", `EXAMPLE=${EXAMPLE}`);
test.beforeEach(async ({ page }) => {
await page.goto("/");
// form-filling uses defaultOpen — window is already open after hydration
await page.locator(".copilotKitWindow.open").waitFor({ timeout: 10_000 });
});
test("chat body fills the window", async ({ page }) => {
const chatBody = page.locator(".copilotKitChatBody");
const window = page.locator(".copilotKitWindow");
await expect(chatBody).toHaveCSS("flex", "1 1 0%");
const bodyBox = await chatBody.boundingBox();
const windowBox = await window.boundingBox();
expect(bodyBox).not.toBeNull();
expect(windowBox).not.toBeNull();
expect(bodyBox!.height).toBeGreaterThan(windowBox!.height * 0.8);
});
test("messages area fills available space", async ({ page }) => {
const messages = page.locator(".copilotKitMessages");
const window = page.locator(".copilotKitWindow");
const messagesBox = await messages.boundingBox();
const windowBox = await window.boundingBox();
expect(messagesBox).not.toBeNull();
expect(windowBox).not.toBeNull();
expect(messagesBox!.height).toBeGreaterThan(windowBox!.height * 0.5);
expect(messagesBox!.height).toBeLessThanOrEqual(windowBox!.height);
});
test("input is pinned to the lower half of the window", async ({ page }) => {
const input = page.locator(".copilotKitInput");
const window = page.locator(".copilotKitWindow");
const inputBox = await input.boundingBox();
const windowBox = await window.boundingBox();
expect(inputBox).not.toBeNull();
expect(windowBox).not.toBeNull();
const inputMidY = inputBox!.y + inputBox!.height / 2;
const windowMidY = windowBox!.y + windowBox!.height / 2;
expect(inputMidY).toBeGreaterThan(windowMidY);
});
test("drag state does not break layout", async ({ page }) => {
const chatBody = page.locator(".copilotKitChatBody");
const messages = page.locator(".copilotKitMessages");
const window = page.locator(".copilotKitWindow");
await page.evaluate(() => {
const body = document.querySelector(".copilotKitChatBody");
body?.dispatchEvent(new DragEvent("dragenter", { bubbles: true }));
});
await expect(chatBody).toHaveCSS("flex", "1 1 0%");
const messagesBox = await messages.boundingBox();
const windowBox = await window.boundingBox();
expect(messagesBox).not.toBeNull();
expect(windowBox).not.toBeNull();
expect(messagesBox!.height).toBeGreaterThan(windowBox!.height * 0.5);
});
});
@@ -0,0 +1,15 @@
import { test, expect } from "@playwright/test";
const EXAMPLE = process.env.EXAMPLE ?? "form-filling";
test.describe("chat-with-your-data", () => {
test.skip(EXAMPLE !== "chat-with-your-data", `EXAMPLE=${EXAMPLE}`);
test("loads", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveTitle(/Chat with your data/i);
await expect(
page.getByRole("heading", { name: "Data Dashboard" }),
).toBeVisible();
});
});
@@ -0,0 +1,118 @@
/**
* Regression test for #2920 — .dark CSS selectors must be scoped to CopilotKit
* elements, not leak into the host application.
*
* Before the fix (main), selectors like `.dark, html.dark, body.dark` applied
* `color: white` and `color: rgb(69, 69, 69)` directly to the .dark element
* itself, cascading into every child — including host app content.
*
* After the fix (#3850), selectors are scoped:
* `.dark .copilotKitDevConsole .copilotKitDebugMenuTriggerButton`
* `.dark .poweredBy`
* so only CopilotKit elements receive the dark mode overrides.
*/
import { test, expect } from "@playwright/test";
const EXAMPLE = process.env.EXAMPLE ?? "form-filling";
test.describe("dark mode CSS scoping (#2920)", () => {
// This test relies on an app that uses .dark class (e.g. form-filling)
test.skip(EXAMPLE !== "form-filling", `EXAMPLE=${EXAMPLE}`);
test.beforeEach(async ({ page }) => {
await page.goto("/");
await page.locator(".copilotKitWindow.open").waitFor({ timeout: 10_000 });
});
test("enabling .dark class does not leak CopilotKit styles to host elements", async ({
page,
}) => {
// Grab a host-app element that should never be styled by CopilotKit CSS.
// In form-filling the <body> itself or a top-level container works.
const body = page.locator("body");
// Toggle dark mode by adding .dark to <html>
await page.evaluate(() => document.documentElement.classList.add("dark"));
// Wait a tick for styles to recompute
await page.waitForTimeout(100);
const colorAfter = await body.evaluate(
(el) => window.getComputedStyle(el).color,
);
// The bug on main: .dark { color: rgb(69, 69, 69) !important } would
// override the body's color. With the fix, the body color should be
// determined solely by the host app's own .dark styles, NOT by CopilotKit.
//
// We verify the body color is NOT rgb(69, 69, 69) — the specific value
// that the broken CopilotKit input.css `.dark` rule forced.
expect(colorAfter).not.toBe("rgb(69, 69, 69)");
});
test("CopilotKit poweredBy gets correct dark mode color", async ({
page,
}) => {
// Toggle dark mode
await page.evaluate(() => document.documentElement.classList.add("dark"));
await page.waitForTimeout(100);
const poweredBy = page.locator(".poweredBy");
// With the fix, .dark .poweredBy { color: rgb(69, 69, 69) !important }
// should apply to the poweredBy element specifically.
const count = await poweredBy.count();
test.skip(count === 0, "No .poweredBy element found — skipping");
const color = await poweredBy
.first()
.evaluate((el) => window.getComputedStyle(el).color);
expect(color).toBe("rgb(69, 69, 69)");
});
test("screenshot: dark mode side-by-side comparison", async ({ page }) => {
// Light mode screenshot
await page.evaluate(() =>
document.documentElement.classList.remove("dark"),
);
await page.waitForTimeout(200);
const lightScreenshot = await page.screenshot({ fullPage: true });
expect(lightScreenshot).toBeTruthy();
// Dark mode screenshot
await page.evaluate(() => document.documentElement.classList.add("dark"));
await page.waitForTimeout(200);
const darkScreenshot = await page.screenshot({ fullPage: true });
expect(darkScreenshot).toBeTruthy();
// Visual regression: dark mode screenshot should differ from light mode
// (if they're identical, dark mode styles aren't applying at all)
expect(Buffer.compare(lightScreenshot, darkScreenshot)).not.toBe(0);
});
test("dark mode styles only target CopilotKit elements", async ({ page }) => {
// Add a marker element outside CopilotKit to detect style leaks
await page.evaluate(() => {
const marker = document.createElement("div");
marker.id = "leak-detector";
marker.textContent = "Leak detector";
document.body.prepend(marker);
});
// Enable dark mode
await page.evaluate(() => document.documentElement.classList.add("dark"));
await page.waitForTimeout(100);
const markerColorDark = await page
.locator("#leak-detector")
.evaluate((el) => window.getComputedStyle(el).color);
// The leak detector element should NOT have its color changed by
// CopilotKit's .dark CSS rules. If it does, that's a style leak.
//
// Note: The host app's own .dark { --foreground: ... } will change colors
// via CSS variables, which is fine. What we're checking is that
// CopilotKit doesn't force rgb(69, 69, 69) or white via its own rules.
expect(markerColorDark).not.toBe("rgb(69, 69, 69)");
});
});
@@ -0,0 +1,20 @@
import { test, expect } from "@playwright/test";
const EXAMPLE = process.env.EXAMPLE ?? "form-filling";
test.describe("form-filling", () => {
test.skip(EXAMPLE !== "form-filling", `EXAMPLE=${EXAMPLE}`);
test("loads", async ({ page }) => {
await page.goto("/");
await expect(
page.getByRole("heading", { name: "Security Incident Report" }),
).toBeVisible();
await expect(
page
.getByRole("contentinfo")
.filter({ hasText: /Powered by CopilotKit/i })
.first(),
).toBeVisible();
});
});
@@ -0,0 +1,14 @@
import { test, expect } from "@playwright/test";
const EXAMPLE = process.env.EXAMPLE ?? "form-filling";
test.describe("research-canvas", () => {
test.skip(EXAMPLE !== "research-canvas", `EXAMPLE=${EXAMPLE}`);
test("loads", async ({ page }) => {
await page.goto("/");
await expect(
page.getByRole("heading", { name: "Research Helper" }),
).toBeVisible();
});
});
@@ -0,0 +1,15 @@
import { test, expect } from "@playwright/test";
const EXAMPLE = process.env.EXAMPLE ?? "form-filling";
test.describe("state-machine", () => {
test.skip(EXAMPLE !== "state-machine", `EXAMPLE=${EXAMPLE}`);
test("loads", async ({ page }) => {
await page.goto("/");
await expect(page.getByRole("button", { name: "Orders" })).toBeVisible();
await expect(
page.getByRole("button", { name: "State Visualizer" }),
).toBeVisible();
});
});
+12
View File
@@ -0,0 +1,12 @@
import { test, expect } from "@playwright/test";
const EXAMPLE = process.env.EXAMPLE ?? "form-filling";
test.describe("travel", () => {
test.skip(EXAMPLE !== "travel", `EXAMPLE=${EXAMPLE}`);
test("loads", async ({ page }) => {
await page.goto("/?copilotOpen=false");
await expect(page).toHaveTitle(/CopilotKit Travel/i);
});
});
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "Node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["**/*.ts"]
}