diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0085cba..a11aa399 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ concurrency: jobs: check: - name: Lint & Typecheck + name: Lint, Typecheck & Unit Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -37,3 +37,31 @@ jobs: - name: Unit Tests run: pnpm test + + e2e: + name: E2E Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Install Playwright browsers + run: pnpm exec playwright install chromium --with-deps + + - name: Run e2e tests + run: pnpm exec playwright test + + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: playwright-report + path: playwright-report/ + retention-days: 7 diff --git a/.gitignore b/.gitignore index 1e6849bd..3a41641d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,12 @@ CLAUDE.local.md # testing /coverage +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ + # next.js /.next/ /out/ diff --git a/components/stage/scene-sidebar.tsx b/components/stage/scene-sidebar.tsx index 4f50d732..4b75471a 100644 --- a/components/stage/scene-sidebar.tsx +++ b/components/stage/scene-sidebar.tsx @@ -137,7 +137,10 @@ export function SceneSidebar({ {/* Scenes List */} -
+
{scenes.map((scene, index) => { const isActive = currentSceneId === scene.id; const Icon = getSceneTypeIcon(scene.type); @@ -147,6 +150,7 @@ export function SceneSidebar({ return (
{ if (onSceneSelect) { onSceneSelect(scene.id); @@ -175,6 +179,7 @@ export function SceneSidebar({ {index + 1} ({ + mockApi: async ({ page }, use) => { + const mockApi = new MockApi(page); + // Always mock server-providers — called on every page load by root layout + await mockApi.mockServerProviders(); + await use(mockApi); + }, +}); + +export { expect } from '@playwright/test'; diff --git a/e2e/fixtures/mock-api.ts b/e2e/fixtures/mock-api.ts new file mode 100644 index 00000000..2291af9e --- /dev/null +++ b/e2e/fixtures/mock-api.ts @@ -0,0 +1,75 @@ +import type { Page } from '@playwright/test'; +import { mockOutlines } from './test-data/scene-outlines'; +import { mockSceneContentResponse } from './test-data/scene-content'; +import { createMockSceneActionsResponse } from './test-data/scene-actions'; + +/** + * Wraps Playwright's page.route() to mock OpenMAIC API endpoints. + * Supports both JSON and SSE (text/event-stream) responses. + */ +export class MockApi { + constructor(private page: Page) {} + + /** Mock the SSE outline streaming endpoint */ + async mockSceneOutlinesStream(outlines = mockOutlines) { + await this.page.route('**/api/generate/scene-outlines-stream', (route) => { + const events = outlines + .map( + (outline, i) => + `data: ${JSON.stringify({ type: 'outline', data: outline, index: i })}\n\n`, + ) + .join(''); + const done = `data: ${JSON.stringify({ type: 'done', outlines })}\n\n`; + + route.fulfill({ + status: 200, + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + Connection: 'keep-alive', + }, + body: events + done, + }); + }); + } + + /** Mock the scene content generation endpoint */ + async mockSceneContent(response = mockSceneContentResponse) { + await this.page.route('**/api/generate/scene-content', (route) => { + route.fulfill({ + status: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(response), + }); + }); + } + + /** Mock the scene actions generation endpoint */ + async mockSceneActions(stageId = 'test-stage') { + await this.page.route('**/api/generate/scene-actions', (route) => { + route.fulfill({ + status: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(createMockSceneActionsResponse(stageId)), + }); + }); + } + + /** Mock the server providers endpoint (returns empty — client-side config only) */ + async mockServerProviders() { + await this.page.route('**/api/server-providers', (route) => { + route.fulfill({ + status: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ providers: {} }), + }); + }); + } + + /** Set up API mocks for the generation flow. Note: server-providers is already mocked by the base fixture. */ + async setupGenerationMocks(stageId = 'test-stage') { + await this.mockSceneOutlinesStream(); + await this.mockSceneContent(); + await this.mockSceneActions(stageId); + } +} diff --git a/e2e/fixtures/test-data/scene-actions.ts b/e2e/fixtures/test-data/scene-actions.ts new file mode 100644 index 00000000..174d1c7c --- /dev/null +++ b/e2e/fixtures/test-data/scene-actions.ts @@ -0,0 +1,44 @@ +import { defaultTheme } from './scene-content'; + +/** Mock response for POST /api/generate/scene-actions */ +export function createMockSceneActionsResponse(stageId: string) { + return { + success: true, + scene: { + id: 'scene-0', + stageId, + type: 'slide', + title: '光合作用的基本概念', + order: 0, + content: { + type: 'slide', + canvas: { + id: 'slide-0', + viewportSize: 1000, + viewportRatio: 0.5625, + theme: defaultTheme, + elements: [ + { + type: 'text', + id: 'title-el', + content: '光合作用的基本概念', + left: 50, + top: 50, + width: 900, + height: 100, + }, + ], + }, + }, + actions: [ + { + id: 'action-0', + type: 'speech', + agent: 'teacher', + text: '今天我们来学习光合作用的基本概念。', + }, + ], + }, + previousSpeeches: [], + }; +} diff --git a/e2e/fixtures/test-data/scene-content.ts b/e2e/fixtures/test-data/scene-content.ts new file mode 100644 index 00000000..c94f339a --- /dev/null +++ b/e2e/fixtures/test-data/scene-content.ts @@ -0,0 +1,38 @@ +import type { SlideTheme } from '../../../lib/types/slides'; +import { mockOutlines } from './scene-outlines'; + +/** Default theme matching lib/types/slides.ts:SlideTheme */ +const defaultTheme: SlideTheme = { + backgroundColor: '#ffffff', + themeColors: ['#5b9bd5', '#ed7d31', '#a5a5a5', '#ffc000', '#4472c4'], + fontColor: '#333333', + fontName: 'Microsoft Yahei', +}; + +/** Mock response for POST /api/generate/scene-content */ +export const mockSceneContentResponse = { + success: true, + content: { + type: 'slide', + canvas: { + id: 'slide-0', + viewportSize: 1000, + viewportRatio: 0.5625, + theme: defaultTheme, + elements: [ + { + type: 'text', + id: 'title-el', + content: '光合作用的基本概念', + left: 50, + top: 50, + width: 900, + height: 100, + }, + ], + }, + }, + effectiveOutline: mockOutlines[0], +}; + +export { defaultTheme }; diff --git a/e2e/fixtures/test-data/scene-outlines.ts b/e2e/fixtures/test-data/scene-outlines.ts new file mode 100644 index 00000000..f870e8b7 --- /dev/null +++ b/e2e/fixtures/test-data/scene-outlines.ts @@ -0,0 +1,29 @@ +import type { SceneOutline } from '../../../lib/types/generation'; + +/** Mock SceneOutline data matching lib/types/generation.ts:SceneOutline */ +export const mockOutlines: SceneOutline[] = [ + { + id: 'outline-0', + type: 'slide' as const, + title: '光合作用的基本概念', + description: '介绍光合作用的定义和基本反应方程式', + keyPoints: ['光合作用的定义', '反应方程式', '能量转换'], + order: 0, + }, + { + id: 'outline-1', + type: 'slide' as const, + title: '光反应阶段', + description: '光反应中光能的吸收与水的分解', + keyPoints: ['光能吸收', '水的光解', 'ATP 与 NADPH 生成'], + order: 1, + }, + { + id: 'outline-2', + type: 'slide' as const, + title: '暗反应阶段', + description: '暗反应中碳固定与糖类合成', + keyPoints: ['CO₂ 固定', 'C₃ 还原', '糖类合成'], + order: 2, + }, +]; diff --git a/e2e/fixtures/test-data/settings.ts b/e2e/fixtures/test-data/settings.ts new file mode 100644 index 00000000..d3c0d077 --- /dev/null +++ b/e2e/fixtures/test-data/settings.ts @@ -0,0 +1,15 @@ +/** Default settings-storage value for e2e tests (Zustand persist v4 format) */ +export function createSettingsStorage(overrides: Record = {}) { + return JSON.stringify({ + state: { + modelId: 'gpt-4o', + providerId: 'openai', + agentMode: 'preset', + selectedAgentIds: [], + ttsEnabled: false, + autoConfigApplied: true, + ...overrides, + }, + version: 2, + }); +} diff --git a/e2e/pages/classroom.page.ts b/e2e/pages/classroom.page.ts new file mode 100644 index 00000000..2d87847d --- /dev/null +++ b/e2e/pages/classroom.page.ts @@ -0,0 +1,30 @@ +import type { Page, Locator } from '@playwright/test'; + +export class ClassroomPage { + readonly page: Page; + readonly loadingText: Locator; + readonly sidebarScenes: Locator; + + constructor(page: Page) { + this.page = page; + this.loadingText = page.getByText('Loading classroom...'); + this.sidebarScenes = page.locator('[data-testid="scene-item"]'); + } + + async goto(stageId: string) { + await this.page.goto(`/classroom/${stageId}`); + } + + async waitForLoaded() { + await this.loadingText.waitFor({ state: 'hidden', timeout: 15_000 }); + } + + async clickScene(index: number) { + await this.sidebarScenes.nth(index).click(); + } + + /** Get scene title — it's the second span (first is the number badge) */ + getSceneTitle(index: number) { + return this.sidebarScenes.nth(index).locator('[data-testid="scene-title"]'); + } +} diff --git a/e2e/pages/generation-preview.page.ts b/e2e/pages/generation-preview.page.ts new file mode 100644 index 00000000..00eb1de2 --- /dev/null +++ b/e2e/pages/generation-preview.page.ts @@ -0,0 +1,21 @@ +import type { Page, Locator } from '@playwright/test'; + +export class GenerationPreviewPage { + readonly page: Page; + readonly stepTitle: Locator; + readonly backButton: Locator; + + constructor(page: Page) { + this.page = page; + this.stepTitle = page.locator('h2'); + this.backButton = page.getByRole('button', { name: /back|返回/i }); + } + + async goto() { + await this.page.goto('/generation-preview'); + } + + async waitForRedirectToClassroom() { + await this.page.waitForURL(/\/classroom\//, { timeout: 30_000 }); + } +} diff --git a/e2e/pages/home.page.ts b/e2e/pages/home.page.ts new file mode 100644 index 00000000..35d5acb6 --- /dev/null +++ b/e2e/pages/home.page.ts @@ -0,0 +1,29 @@ +import type { Page, Locator } from '@playwright/test'; + +export class HomePage { + readonly page: Page; + readonly logo: Locator; + readonly textarea: Locator; + readonly enterButton: Locator; + + constructor(page: Page) { + this.page = page; + this.logo = page.locator('img[alt="OpenMAIC"]'); + this.textarea = page.locator('textarea'); + this.enterButton = page + .getByRole('button', { name: /enter/i }) + .or(page.locator('button:has-text("进入课堂")')); + } + + async goto() { + await this.page.goto('/'); + } + + async fillRequirement(text: string) { + await this.textarea.fill(text); + } + + async submit() { + await this.enterButton.click(); + } +} diff --git a/e2e/tests/classroom-interaction.spec.ts b/e2e/tests/classroom-interaction.spec.ts new file mode 100644 index 00000000..12f6c176 --- /dev/null +++ b/e2e/tests/classroom-interaction.spec.ts @@ -0,0 +1,148 @@ +import { test, expect } from '../fixtures/base'; +import { ClassroomPage } from '../pages/classroom.page'; +import { createSettingsStorage } from '../fixtures/test-data/settings'; +import { defaultTheme } from '../fixtures/test-data/scene-content'; + +const TEST_STAGE_ID = 'e2e-test-stage'; + +const SETTINGS_STORAGE = createSettingsStorage({ sidebarCollapsed: false }); + +/** Seed IndexedDB with stage + 3 scenes using raw IndexedDB API */ +async function seedDatabase(page: import('@playwright/test').Page) { + // Inject settings before navigating so it's available immediately on load + await page.addInitScript((settings) => { + localStorage.setItem('settings-storage', settings); + }, SETTINGS_STORAGE); + + // Navigate to home page first — this causes Dexie to open/create the DB at v8 + // with the correct schema. We wait for network idle to ensure Dexie is done. + await page.goto('/', { waitUntil: 'networkidle' }); + + // Now seed data by opening the DB at its current version (no upgrade). + // Opening without a version number returns the current version without triggering + // onupgradeneeded, so we can safely write to the already-initialized schema. + await page.evaluate( + ({ stageId, theme }) => { + return new Promise((resolve, reject) => { + // Open without specifying version — uses current DB version, no upgrade event + const request = indexedDB.open('MAIC-Database'); + + request.onsuccess = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + const tx = db.transaction(['stages', 'scenes', 'stageOutlines'], 'readwrite'); + const now = Date.now(); + + tx.objectStore('stages').put({ + id: stageId, + name: '光合作用', + description: '', + language: 'zh-CN', + style: 'professional', + createdAt: now, + updatedAt: now, + }); + + // Scene content uses SlideContent shape: { type: 'slide', canvas: Slide } + const makeSlideContent = (title: string, elId: string) => ({ + type: 'slide', + canvas: { + id: `slide-${elId}`, + viewportSize: 1000, + viewportRatio: 0.5625, + theme, + elements: [ + { + type: 'text', + id: `el-${elId}`, + content: title, + left: 50, + top: 50, + width: 900, + height: 100, + }, + ], + }, + }); + + const scenes = [ + { + id: 'scene-0', + stageId, + type: 'slide', + title: '基本概念', + order: 0, + content: makeSlideContent('基本概念', '0'), + createdAt: now, + updatedAt: now, + }, + { + id: 'scene-1', + stageId, + type: 'slide', + title: '光反应', + order: 1, + content: makeSlideContent('光反应', '1'), + createdAt: now, + updatedAt: now, + }, + { + id: 'scene-2', + stageId, + type: 'slide', + title: '暗反应', + order: 2, + content: makeSlideContent('暗反应', '2'), + createdAt: now, + updatedAt: now, + }, + ]; + for (const scene of scenes) { + tx.objectStore('scenes').put(scene); + } + + // Empty outlines = all scenes generated, no pending work + // StageOutlinesRecord requires createdAt + updatedAt + tx.objectStore('stageOutlines').put({ + stageId, + outlines: [], + createdAt: now, + updatedAt: now, + }); + + tx.oncomplete = () => { + db.close(); + resolve(); + }; + tx.onerror = () => reject(tx.error); + }; + + request.onerror = () => reject(request.error); + }); + }, + { stageId: TEST_STAGE_ID, theme: defaultTheme }, + ); +} + +test.describe('Classroom Interaction', () => { + test.beforeEach(async ({ page }) => { + await seedDatabase(page); + }); + + test('loads classroom and switches scenes', async ({ page }) => { + const classroom = new ClassroomPage(page); + await classroom.goto(TEST_STAGE_ID); + await classroom.waitForLoaded(); + + // Sidebar shows 3 scenes + await expect(classroom.sidebarScenes).toHaveCount(3, { timeout: 10_000 }); + + // First scene title visible + await expect(classroom.getSceneTitle(0)).toContainText('基本概念'); + + // Click second scene + await classroom.clickScene(1); + + // Verify second scene is now active — heading in the top bar shows the current scene name + await expect(page.getByRole('heading', { name: '光反应' })).toBeVisible(); + }); +}); diff --git a/e2e/tests/generation-flow.spec.ts b/e2e/tests/generation-flow.spec.ts new file mode 100644 index 00000000..4ef08c4c --- /dev/null +++ b/e2e/tests/generation-flow.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from '../fixtures/base'; +import { GenerationPreviewPage } from '../pages/generation-preview.page'; +import { createSettingsStorage } from '../fixtures/test-data/settings'; + +const SETTINGS_STORAGE = createSettingsStorage(); + +const GENERATION_SESSION = JSON.stringify({ + sessionId: 'e2e-test-session', + requirements: { + requirement: '讲解光合作用', + language: 'zh-CN', + }, + pdfText: '', + pdfImages: [], + imageStorageIds: [], + sceneOutlines: null, + currentStep: 'generating', +}); + +test.describe('Generation Flow', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript( + ({ settings, session }) => { + localStorage.setItem('settings-storage', settings); + sessionStorage.setItem('generationSession', session); + }, + { settings: SETTINGS_STORAGE, session: GENERATION_SESSION }, + ); + }); + + test('completes generation pipeline and redirects to classroom', async ({ page, mockApi }) => { + // Set up all API mocks + await mockApi.setupGenerationMocks(); + + const preview = new GenerationPreviewPage(page); + await preview.goto(); + + // Generation card with progress dots should be visible + await expect(preview.stepTitle).toBeVisible(); + + // Wait for auto-redirect to classroom + await preview.waitForRedirectToClassroom(); + expect(page.url()).toMatch(/\/classroom\//); + }); +}); diff --git a/e2e/tests/home-to-generation.spec.ts b/e2e/tests/home-to-generation.spec.ts new file mode 100644 index 00000000..b7000123 --- /dev/null +++ b/e2e/tests/home-to-generation.spec.ts @@ -0,0 +1,33 @@ +import { test, expect } from '../fixtures/base'; +import { HomePage } from '../pages/home.page'; +import { createSettingsStorage } from '../fixtures/test-data/settings'; + +// Inject settings with modelId so the "enter classroom" button works +const SETTINGS_STORAGE = createSettingsStorage(); + +test.describe('Home → Generation', () => { + test.beforeEach(async ({ page }) => { + await page.addInitScript((settings) => { + localStorage.setItem('settings-storage', settings); + }, SETTINGS_STORAGE); + }); + + test('home page loads with core UI elements and submits requirement', async ({ page }) => { + const home = new HomePage(page); + await home.goto(); + + // Core elements visible + await expect(home.logo).toBeVisible(); + await expect(home.textarea).toBeVisible(); + await expect(home.enterButton).toBeDisabled(); + + // Type requirement → button activates + await home.fillRequirement('讲解光合作用'); + await expect(home.enterButton).toBeEnabled(); + + // Submit → navigate to generation-preview + await home.submit(); + await page.waitForURL(/\/generation-preview/); + expect(page.url()).toContain('/generation-preview'); + }); +}); diff --git a/eslint.config.mjs b/eslint.config.mjs index f214ad7e..13322ac2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -18,6 +18,8 @@ const eslintConfig = defineConfig([ '.claude/**', '.superpowers/**', '.worktrees/**', + // Playwright e2e tests (not React code): + 'e2e/**', ]), { rules: { diff --git a/package.json b/package.json index 4d4e8f3e..55835ca1 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,9 @@ "lint": "eslint", "check": "prettier . --check", "format": "prettier . --write", - "test": "vitest run" + "test": "vitest run", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui" }, "dependencies": { "@ai-sdk/anthropic": "^3.0.23", @@ -97,6 +99,7 @@ "zustand": "^5.0.10" }, "devDependencies": { + "@playwright/test": "^1.58.2", "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-node-resolve": "^16.0.1", "@tailwindcss/postcss": "^4", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..e3adb5f6 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,28 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e/tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: process.env.CI ? 'html' : 'list', + use: { + baseURL: 'http://localhost:3002', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: process.env.CI ? 'pnpm build && pnpm start' : 'pnpm dev', + url: 'http://localhost:3002', + reuseExistingServer: !process.env.CI, + timeout: 120_000, + env: { PORT: '3002' }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c99478b7..fac3752e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ importers: version: 1.2.0(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@copilotkit/backend': specifier: ^0.37.0 - version: 0.37.0(axios@1.13.6)(ignore@5.3.2)(lodash@4.17.23)(ws@8.19.0) + version: 0.37.0(axios@1.13.6)(ignore@5.3.2)(lodash@4.17.23)(playwright@1.58.2)(ws@8.19.0) '@copilotkit/runtime': specifier: ^1.51.2 version: 1.53.0(@ag-ui/encoder@0.0.47)(@cfworker/json-schema@4.1.1)(@copilotkitnext/shared@1.53.0)(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.19.0)(zod@4.3.6)))(@langchain/langgraph-sdk@1.7.1(@langchain/core@1.1.31(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.19.0)(zod@4.3.6))))(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(ws@8.19.0) @@ -97,7 +97,7 @@ importers: version: 2.0.5 geist: specifier: ^1.7.0 - version: 1.7.0(next@16.1.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + version: 1.7.0(next@16.1.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) immer: specifier: ^11.1.3 version: 11.1.4 @@ -133,7 +133,7 @@ importers: version: 5.1.6 next: specifier: 16.1.2 - version: 16.1.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 16.1.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -243,6 +243,9 @@ importers: specifier: ^5.0.10 version: 5.0.11(@types/react@19.2.14)(immer@11.1.4)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) devDependencies: + '@playwright/test': + specifier: ^1.58.2 + version: 1.58.2 '@rollup/plugin-commonjs': specifier: ^28.0.1 version: 28.0.9(rollup@4.59.0) @@ -1966,6 +1969,11 @@ packages: '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + '@protobuf-ts/protoc@2.11.1': resolution: {integrity: sha512-mUZJaV0daGO6HUX90o/atzQ6A7bbN2RSuHtdwo8SSF2Qoe3zHwa4IHyCN1evftTeHfLmdz+45qo47sL+5P8nyg==} hasBin: true @@ -5159,6 +5167,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -7488,6 +7501,16 @@ packages: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + pluralize@7.0.0: resolution: {integrity: sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==} engines: {node: '>=4'} @@ -9685,14 +9708,14 @@ snapshots: '@cfworker/json-schema@4.1.1': {} - '@copilotkit/backend@0.37.0(axios@1.13.6)(ignore@5.3.2)(lodash@4.17.23)(ws@8.19.0)': + '@copilotkit/backend@0.37.0(axios@1.13.6)(ignore@5.3.2)(lodash@4.17.23)(playwright@1.58.2)(ws@8.19.0)': dependencies: '@copilotkit/shared': 0.37.0 '@google/generative-ai': 0.11.5 '@langchain/core': 0.1.63(openai@4.104.0(ws@8.19.0)(zod@4.3.6)) '@langchain/openai': 0.0.28(ws@8.19.0) js-tiktoken: 1.0.21 - langchain: 0.1.37(axios@1.13.6)(ignore@5.3.2)(lodash@4.17.23)(openai@4.104.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0) + langchain: 0.1.37(axios@1.13.6)(ignore@5.3.2)(lodash@4.17.23)(openai@4.104.0(ws@8.19.0)(zod@4.3.6))(playwright@1.58.2)(ws@8.19.0) openai: 4.104.0(ws@8.19.0)(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: @@ -10915,6 +10938,10 @@ snapshots: '@pinojs/redact@0.4.0': {} + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + '@protobuf-ts/protoc@2.11.1': {} '@radix-ui/number@1.1.1': {} @@ -14432,6 +14459,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -14454,9 +14484,9 @@ snapshots: fzf@0.5.2: {} - geist@1.7.0(next@16.1.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): + geist@1.7.0(next@16.1.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): dependencies: - next: 16.1.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + next: 16.1.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) generator-function@2.0.1: {} @@ -15709,7 +15739,7 @@ snapshots: kleur@4.1.5: {} - langchain@0.1.37(axios@1.13.6)(ignore@5.3.2)(lodash@4.17.23)(openai@4.104.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0): + langchain@0.1.37(axios@1.13.6)(ignore@5.3.2)(lodash@4.17.23)(openai@4.104.0(ws@8.19.0)(zod@4.3.6))(playwright@1.58.2)(ws@8.19.0): dependencies: '@anthropic-ai/sdk': 0.9.1 '@langchain/community': 0.0.57(lodash@4.17.23)(openai@4.104.0(ws@8.19.0)(zod@4.3.6))(ws@8.19.0) @@ -15732,6 +15762,7 @@ snapshots: optionalDependencies: axios: 1.13.6 ignore: 5.3.2 + playwright: 1.58.2 ws: 8.19.0 transitivePeerDependencies: - '@aws-crypto/sha256-js' @@ -16699,7 +16730,7 @@ snapshots: next-tick@1.1.0: {} - next@16.1.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@16.1.2(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@next/env': 16.1.2 '@swc/helpers': 0.5.15 @@ -16719,6 +16750,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 16.1.2 '@next/swc-win32-x64-msvc': 16.1.2 '@opentelemetry/api': 1.9.0 + '@playwright/test': 1.58.2 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' @@ -17136,6 +17168,14 @@ snapshots: dependencies: find-up: 4.1.0 + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + pluralize@7.0.0: {} possible-typed-array-names@1.1.0: {} diff --git a/tsconfig.json b/tsconfig.json index 9a86323e..b296749f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,5 +30,5 @@ ".next/dev/types/**/*.ts", "**/*.mts" ], - "exclude": ["node_modules", "dist", "packages/*/src", "openclaw"] + "exclude": ["node_modules", "dist", "packages/*/src", "openclaw", "e2e"] }