diff --git a/apps/webclaw/e2e/table-block.spec.ts b/apps/webclaw/e2e/table-block.spec.ts new file mode 100644 index 0000000..6dd767f --- /dev/null +++ b/apps/webclaw/e2e/table-block.spec.ts @@ -0,0 +1,42 @@ +import { expect, test } from '@playwright/test' + +test('table block edit/import/insert and persistence', async ({ page }) => { + await page.goto('/dev/table-block') + await page.evaluate(() => { + window.localStorage.removeItem('chat-table-workbench-blocks-v1') + }) + await page.reload() + + await page.getByRole('button', { name: 'Add Table Block' }).click() + + await page.getByRole('button', { name: 'Empty' }).first().click() + const editor = page.locator('table input').first() + await editor.fill('alpha') + await editor.press('Enter') + + await expect(page.getByRole('button', { name: 'alpha' })).toBeVisible() + + await page.getByRole('button', { name: 'Import CSV' }).click() + await page.locator('textarea').first().fill('name,age\nAlice,30\nBob,29') + await page.getByRole('button', { name: 'Apply import' }).click() + + await expect( + page.getByRole('button', { name: 'name', exact: true }), + ).toBeVisible() + await expect( + page.getByRole('button', { name: 'Alice', exact: true }), + ).toBeVisible() + + await page.getByRole('button', { name: 'Insert to Prompt' }).click() + + await expect(page.getByTestId('prompt-target')).toContainText('| name | age |') + + await page.reload() + + await expect( + page.getByRole('button', { name: 'name', exact: true }), + ).toBeVisible() + await expect( + page.getByRole('button', { name: 'Alice', exact: true }), + ).toBeVisible() +}) diff --git a/apps/webclaw/package.json b/apps/webclaw/package.json index 74d3627..8a0f0d5 100644 --- a/apps/webclaw/package.json +++ b/apps/webclaw/package.json @@ -7,6 +7,7 @@ "build": "vite build", "preview": "vite preview", "test": "vitest run", + "test:e2e": "playwright test", "lint": "eslint", "format": "prettier", "check": "prettier --write . && eslint --fix" @@ -41,6 +42,7 @@ "zustand": "^5.0.11" }, "devDependencies": { + "@playwright/test": "^1.58.2", "@tanstack/eslint-config": "^0.3.0", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.2.0", diff --git a/apps/webclaw/playwright.config.ts b/apps/webclaw/playwright.config.ts new file mode 100644 index 0000000..f3989e1 --- /dev/null +++ b/apps/webclaw/playwright.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ + testDir: './e2e', + timeout: 45_000, + reporter: 'list', + use: { + baseURL: 'http://127.0.0.1:3200', + headless: true, + }, + webServer: { + command: 'pnpm build && pnpm exec vite preview --port 3200 --host 127.0.0.1', + url: 'http://127.0.0.1:3200', + reuseExistingServer: false, + timeout: 240_000, + }, +}) diff --git a/apps/webclaw/src/routeTree.gen.ts b/apps/webclaw/src/routeTree.gen.ts index 9fc7637..48e43bd 100644 --- a/apps/webclaw/src/routeTree.gen.ts +++ b/apps/webclaw/src/routeTree.gen.ts @@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as NewRouteImport } from './routes/new' import { Route as ConnectRouteImport } from './routes/connect' import { Route as IndexRouteImport } from './routes/index' +import { Route as DevTableBlockRouteImport } from './routes/dev.table-block' import { Route as ChatSessionKeyRouteImport } from './routes/chat/$sessionKey' import { Route as ApiStreamRouteImport } from './routes/api/stream' import { Route as ApiSessionsRouteImport } from './routes/api/sessions' @@ -35,6 +36,11 @@ const IndexRoute = IndexRouteImport.update({ path: '/', getParentRoute: () => rootRouteImport, } as any) +const DevTableBlockRoute = DevTableBlockRouteImport.update({ + id: '/dev/table-block', + path: '/dev/table-block', + getParentRoute: () => rootRouteImport, +} as any) const ChatSessionKeyRoute = ChatSessionKeyRouteImport.update({ id: '/chat/$sessionKey', path: '/chat/$sessionKey', @@ -82,6 +88,7 @@ export interface FileRoutesByFullPath { '/api/sessions': typeof ApiSessionsRoute '/api/stream': typeof ApiStreamRoute '/chat/$sessionKey': typeof ChatSessionKeyRoute + '/dev/table-block': typeof DevTableBlockRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -94,6 +101,7 @@ export interface FileRoutesByTo { '/api/sessions': typeof ApiSessionsRoute '/api/stream': typeof ApiStreamRoute '/chat/$sessionKey': typeof ChatSessionKeyRoute + '/dev/table-block': typeof DevTableBlockRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -107,6 +115,7 @@ export interface FileRoutesById { '/api/sessions': typeof ApiSessionsRoute '/api/stream': typeof ApiStreamRoute '/chat/$sessionKey': typeof ChatSessionKeyRoute + '/dev/table-block': typeof DevTableBlockRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -121,6 +130,7 @@ export interface FileRouteTypes { | '/api/sessions' | '/api/stream' | '/chat/$sessionKey' + | '/dev/table-block' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -133,6 +143,7 @@ export interface FileRouteTypes { | '/api/sessions' | '/api/stream' | '/chat/$sessionKey' + | '/dev/table-block' id: | '__root__' | '/' @@ -145,6 +156,7 @@ export interface FileRouteTypes { | '/api/sessions' | '/api/stream' | '/chat/$sessionKey' + | '/dev/table-block' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -158,6 +170,7 @@ export interface RootRouteChildren { ApiSessionsRoute: typeof ApiSessionsRoute ApiStreamRoute: typeof ApiStreamRoute ChatSessionKeyRoute: typeof ChatSessionKeyRoute + DevTableBlockRoute: typeof DevTableBlockRoute } declare module '@tanstack/react-router' { @@ -183,6 +196,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexRouteImport parentRoute: typeof rootRouteImport } + '/dev/table-block': { + id: '/dev/table-block' + path: '/dev/table-block' + fullPath: '/dev/table-block' + preLoaderRoute: typeof DevTableBlockRouteImport + parentRoute: typeof rootRouteImport + } '/chat/$sessionKey': { id: '/chat/$sessionKey' path: '/chat/$sessionKey' @@ -246,6 +266,7 @@ const rootRouteChildren: RootRouteChildren = { ApiSessionsRoute: ApiSessionsRoute, ApiStreamRoute: ApiStreamRoute, ChatSessionKeyRoute: ChatSessionKeyRoute, + DevTableBlockRoute: DevTableBlockRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/apps/webclaw/src/routes/dev.table-block.tsx b/apps/webclaw/src/routes/dev.table-block.tsx new file mode 100644 index 0000000..5611080 --- /dev/null +++ b/apps/webclaw/src/routes/dev.table-block.tsx @@ -0,0 +1,47 @@ +import { createFileRoute } from '@tanstack/react-router' +import { useState } from 'react' + +import { ChatWorkbench } from '@/screens/chat/components/chat-workbench' + +export const Route = createFileRoute('/dev/table-block')({ + component: RouteComponent, +}) + +function RouteComponent() { + const [promptValue, setPromptValue] = useState('') + + return ( +
+
+

Table Block E2E

+

+ Dev page for browser-level table block verification. +

+
+ + { + const trimmed = markdown.trim() + if (trimmed.length === 0) return + setPromptValue((prev) => + prev.trim().length > 0 ? `${prev.trimEnd()}\n\n${trimmed}` : trimmed, + ) + }} + /> + +
+ +