create-lego-one 2.0.10 → 2.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +179 -0
- package/dist/index.cjs.map +1 -1
- package/package.json +5 -3
- package/template/.cursor/rules/rules.mdc +639 -0
- package/template/.dockerignore +58 -0
- package/template/.env.example +18 -0
- package/template/.eslintignore +5 -0
- package/template/.eslintrc.js +28 -0
- package/template/.prettierignore +6 -0
- package/template/.prettierrc +11 -0
- package/template/CLAUDE.md +634 -0
- package/template/Dockerfile +67 -0
- package/template/PROMPT.md +457 -0
- package/template/README.md +325 -0
- package/template/docker-compose.yml +48 -0
- package/template/docker-entrypoint.sh +23 -0
- package/template/docs/checkpoints/.template.md +64 -0
- package/template/docs/checkpoints/framework/01-infrastructure-setup.md +132 -0
- package/template/docs/checkpoints/framework/02-pocketbase-setup.md +155 -0
- package/template/docs/checkpoints/framework/03-host-kernel.md +170 -0
- package/template/docs/checkpoints/framework/04-auth-system.md +163 -0
- package/template/docs/checkpoints/framework/phase-05-multitenancy-rbac.md +223 -0
- package/template/docs/checkpoints/framework/phase-06-ui-components.md +260 -0
- package/template/docs/checkpoints/framework/phase-07-communication-system.md +276 -0
- package/template/docs/checkpoints/framework/phase-08-plugin-system.md +91 -0
- package/template/docs/checkpoints/framework/phase-09-dashboard-plugin.md +111 -0
- package/template/docs/checkpoints/framework/phase-10-todo-plugin.md +169 -0
- package/template/docs/checkpoints/framework/phase-11-testing.md +264 -0
- package/template/docs/checkpoints/framework/phase-12-deployment.md +294 -0
- package/template/docs/checkpoints/framework/phase-13-documentation.md +312 -0
- package/template/docs/framework/plans/00-index.md +164 -0
- package/template/docs/framework/plans/01-infrastructure-setup.md +855 -0
- package/template/docs/framework/plans/02-pocketbase-setup.md +1374 -0
- package/template/docs/framework/plans/03-host-kernel.md +1518 -0
- package/template/docs/framework/plans/04-auth-system.md +1466 -0
- package/template/docs/framework/plans/05-multitenancy-rbac.md +1527 -0
- package/template/docs/framework/plans/06-ui-components.md +1478 -0
- package/template/docs/framework/plans/07-communication-system.md +1106 -0
- package/template/docs/framework/plans/08-plugin-system.md +1179 -0
- package/template/docs/framework/plans/09-dashboard-plugin.md +1137 -0
- package/template/docs/framework/plans/10-todo-plugin.md +1343 -0
- package/template/docs/framework/plans/11-testing.md +935 -0
- package/template/docs/framework/plans/12-deployment.md +896 -0
- package/template/docs/framework/prompts/0-boilerplate-modernjs.md +151 -0
- package/template/docs/framework/research/00-modernjs-audit.md +488 -0
- package/template/docs/framework/research/01-system-blueprint.md +721 -0
- package/template/docs/framework/research/02-data-migration-protocol.md +699 -0
- package/template/docs/framework/research/03-host-setup.md +714 -0
- package/template/docs/framework/research/04-plugin-architecture.md +645 -0
- package/template/docs/framework/research/05-slot-injection-pattern.md +671 -0
- package/template/docs/framework/research/06-cli-strategy.md +615 -0
- package/template/docs/framework/research/07-deployment.md +629 -0
- package/template/docs/framework/research/README.md +282 -0
- package/template/docs/framework/setup/00-index.md +210 -0
- package/template/docs/framework/setup/01-framework-structure.md +308 -0
- package/template/docs/framework/setup/02-development-workflow.md +405 -0
- package/template/docs/framework/setup/03-environment-setup.md +215 -0
- package/template/docs/framework/setup/04-kernel-architecture.md +499 -0
- package/template/docs/framework/setup/05-plugin-system.md +620 -0
- package/template/docs/framework/setup/06-communication-patterns.md +451 -0
- package/template/docs/framework/setup/07-plugin-development.md +582 -0
- package/template/docs/framework/setup/08-component-library.md +658 -0
- package/template/docs/framework/setup/09-data-integration.md +609 -0
- package/template/docs/framework/setup/10-auth-rbac.md +497 -0
- package/template/docs/framework/setup/11-hooks-api.md +393 -0
- package/template/docs/framework/setup/12-components-api.md +665 -0
- package/template/docs/framework/setup/13-deployment-guide.md +566 -0
- package/template/docs/framework/setup/README.md +548 -0
- package/template/host/e2e/auth.spec.ts +38 -0
- package/template/host/e2e/layout.spec.ts +38 -0
- package/template/host/modern.config.ts +19 -0
- package/template/host/package.json +71 -0
- package/template/host/playwright.config.ts +34 -0
- package/template/host/postcss.config.mjs +6 -0
- package/template/host/src/App.tsx +6 -0
- package/template/host/src/bootstrap.tsx +74 -0
- package/template/host/src/global.css +59 -0
- package/template/host/src/index.ts +2 -0
- package/template/host/src/kernel/__tests__/lib-utils.test.ts +32 -0
- package/template/host/src/kernel/__tests__/rbac-hooks.test.tsx +114 -0
- package/template/host/src/kernel/__tests__/rbac-utils.test.ts +108 -0
- package/template/host/src/kernel/auth/ProtectedRoute.tsx +41 -0
- package/template/host/src/kernel/auth/components/LoginForm.tsx +97 -0
- package/template/host/src/kernel/auth/components/LogoutButton.tsx +79 -0
- package/template/host/src/kernel/auth/hooks.ts +174 -0
- package/template/host/src/kernel/auth/index.ts +5 -0
- package/template/host/src/kernel/auth/schemas.ts +27 -0
- package/template/host/src/kernel/auth/service.ts +197 -0
- package/template/host/src/kernel/auth/types.ts +36 -0
- package/template/host/src/kernel/channels/ChannelBus.ts +181 -0
- package/template/host/src/kernel/channels/ChannelProvider.tsx +57 -0
- package/template/host/src/kernel/channels/events.ts +27 -0
- package/template/host/src/kernel/channels/hooks.ts +168 -0
- package/template/host/src/kernel/channels/index.ts +6 -0
- package/template/host/src/kernel/channels/integrations/ToastIntegration.tsx +60 -0
- package/template/host/src/kernel/channels/plugin-hooks.ts +72 -0
- package/template/host/src/kernel/channels/types.ts +112 -0
- package/template/host/src/kernel/components/__tests__/Badge.test.tsx +35 -0
- package/template/host/src/kernel/components/__tests__/Button.test.tsx +63 -0
- package/template/host/src/kernel/components/__tests__/Input.test.tsx +64 -0
- package/template/host/src/kernel/components/index.ts +32 -0
- package/template/host/src/kernel/components/ui/alert.tsx +58 -0
- package/template/host/src/kernel/components/ui/avatar.tsx +47 -0
- package/template/host/src/kernel/components/ui/badge.tsx +35 -0
- package/template/host/src/kernel/components/ui/button.tsx +50 -0
- package/template/host/src/kernel/components/ui/card.tsx +78 -0
- package/template/host/src/kernel/components/ui/dialog.tsx +116 -0
- package/template/host/src/kernel/components/ui/dropdown-menu.tsx +192 -0
- package/template/host/src/kernel/components/ui/index.ts +7 -0
- package/template/host/src/kernel/components/ui/input.tsx +24 -0
- package/template/host/src/kernel/components/ui/label.tsx +21 -0
- package/template/host/src/kernel/components/ui/popover.tsx +28 -0
- package/template/host/src/kernel/components/ui/progress.tsx +25 -0
- package/template/host/src/kernel/components/ui/scroll-area.tsx +45 -0
- package/template/host/src/kernel/components/ui/select.tsx +155 -0
- package/template/host/src/kernel/components/ui/separator.tsx +28 -0
- package/template/host/src/kernel/components/ui/skeleton.tsx +15 -0
- package/template/host/src/kernel/components/ui/switch.tsx +26 -0
- package/template/host/src/kernel/components/ui/table.tsx +116 -0
- package/template/host/src/kernel/components/ui/tabs.tsx +52 -0
- package/template/host/src/kernel/components/ui/toast.tsx +126 -0
- package/template/host/src/kernel/components/ui/toaster.tsx +34 -0
- package/template/host/src/kernel/components/ui/tooltip.tsx +27 -0
- package/template/host/src/kernel/components/ui/use-toast.ts +183 -0
- package/template/host/src/kernel/index.ts +48 -0
- package/template/host/src/kernel/lib/cn.ts +1 -0
- package/template/host/src/kernel/lib/utils.ts +36 -0
- package/template/host/src/kernel/plugins/Slot.tsx +41 -0
- package/template/host/src/kernel/plugins/SlotProvider.tsx +88 -0
- package/template/host/src/kernel/plugins/index.ts +23 -0
- package/template/host/src/kernel/plugins/loader.ts +122 -0
- package/template/host/src/kernel/plugins/schemas.ts +54 -0
- package/template/host/src/kernel/plugins/store.ts +185 -0
- package/template/host/src/kernel/plugins/types.ts +103 -0
- package/template/host/src/kernel/providers/PocketBaseProvider.tsx +70 -0
- package/template/host/src/kernel/providers/QueryProvider.tsx +28 -0
- package/template/host/src/kernel/providers/ThemeProvider.tsx +25 -0
- package/template/host/src/kernel/providers/index.ts +3 -0
- package/template/host/src/kernel/rbac/components/OrganizationSelector.tsx +69 -0
- package/template/host/src/kernel/rbac/components/PermissionGate.tsx +43 -0
- package/template/host/src/kernel/rbac/hooks.ts +379 -0
- package/template/host/src/kernel/rbac/index.ts +6 -0
- package/template/host/src/kernel/rbac/service.ts +504 -0
- package/template/host/src/kernel/rbac/types.ts +164 -0
- package/template/host/src/kernel/rbac/utils.ts +34 -0
- package/template/host/src/kernel/shared-state/bridge.ts +31 -0
- package/template/host/src/kernel/shared-state/index.ts +3 -0
- package/template/host/src/kernel/shared-state/store.ts +62 -0
- package/template/host/src/kernel/shared-state/types.ts +60 -0
- package/template/host/src/kernel/use-migrations.ts +72 -0
- package/template/host/src/layout/MobileMenu.tsx +61 -0
- package/template/host/src/layout/Shell.tsx +42 -0
- package/template/host/src/layout/Sidebar.tsx +178 -0
- package/template/host/src/layout/Topbar.tsx +50 -0
- package/template/host/src/layout/index.ts +4 -0
- package/template/host/src/lib/pocketbase/client.ts +38 -0
- package/template/host/src/lib/pocketbase/collections/audit_logs.ts +87 -0
- package/template/host/src/lib/pocketbase/collections/index.ts +19 -0
- package/template/host/src/lib/pocketbase/collections/organizations.ts +63 -0
- package/template/host/src/lib/pocketbase/collections/permissions.ts +57 -0
- package/template/host/src/lib/pocketbase/collections/roles.ts +55 -0
- package/template/host/src/lib/pocketbase/collections/todos.ts +74 -0
- package/template/host/src/lib/pocketbase/collections/user_roles.ts +57 -0
- package/template/host/src/lib/pocketbase/collections/users.ts +43 -0
- package/template/host/src/lib/pocketbase/index.ts +5 -0
- package/template/host/src/lib/pocketbase/migrations.ts +44 -0
- package/template/host/src/lib/pocketbase/seed/permissions.ts +8 -0
- package/template/host/src/lib/pocketbase/seed/roles.ts +22 -0
- package/template/host/src/lib/pocketbase/seed.ts +113 -0
- package/template/host/src/lib/pocketbase/types.ts +102 -0
- package/template/host/src/modern.runtime.ts +26 -0
- package/template/host/src/plugins.d.ts +9 -0
- package/template/host/src/providers/PocketBaseProvider.tsx +30 -0
- package/template/host/src/routes/_.tsx +6 -0
- package/template/host/src/routes/dashboard._.tsx +41 -0
- package/template/host/src/routes/index.tsx +93 -0
- package/template/host/src/routes/login.tsx +36 -0
- package/template/host/src/saas.config.ts +52 -0
- package/template/host/src/test/setup.ts +65 -0
- package/template/host/src/test/utils.tsx +69 -0
- package/template/host/src/test/vitest-globals.d.ts +19 -0
- package/template/host/src/vite-env.d.ts +16 -0
- package/template/host/tailwind.config.ts +77 -0
- package/template/host/tsconfig.json +19 -0
- package/template/host/vitest.config.ts +30 -0
- package/template/nginx.conf +72 -0
- package/template/package.json +44 -0
- package/template/packages/plugins/@lego/plugin-dashboard/modern.config.ts +19 -0
- package/template/packages/plugins/@lego/plugin-dashboard/package.json +35 -0
- package/template/packages/plugins/@lego/plugin-dashboard/postcss.config.mjs +6 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/App.tsx +27 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/ActivityFeed.tsx +63 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActionSlot.tsx +11 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/QuickActions.tsx +68 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/SidebarWidget.tsx +35 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/components/StatCard.tsx +47 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/global.css +24 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useChannelIntegration.ts +43 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useDashboardStats.ts +65 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/usePocketBase.ts +47 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/hooks/useRecentActivity.ts +55 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/lib/utils.ts +6 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/pages/DashboardPage.tsx +105 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.config.ts +121 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/plugin.ts +18 -0
- package/template/packages/plugins/@lego/plugin-dashboard/src/vite-env.d.ts +32 -0
- package/template/packages/plugins/@lego/plugin-dashboard/tailwind.config.ts +35 -0
- package/template/packages/plugins/@lego/plugin-dashboard/tsconfig.json +18 -0
- package/template/packages/plugins/@lego/plugin-todo/modern.config.ts +18 -0
- package/template/packages/plugins/@lego/plugin-todo/package.json +41 -0
- package/template/packages/plugins/@lego/plugin-todo/postcss.config.mjs +6 -0
- package/template/packages/plugins/@lego/plugin-todo/src/App.tsx +12 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/SidebarWidget.tsx +16 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoDialog.tsx +55 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoFilters.tsx +79 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoForm.tsx +94 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoItem.tsx +121 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/TodoList.tsx +41 -0
- package/template/packages/plugins/@lego/plugin-todo/src/components/index.ts +6 -0
- package/template/packages/plugins/@lego/plugin-todo/src/global.css +59 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useCreateTodo.ts +62 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useDeleteTodo.ts +46 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/usePocketBase.ts +38 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useTodos.ts +64 -0
- package/template/packages/plugins/@lego/plugin-todo/src/hooks/useUpdateTodo.ts +35 -0
- package/template/packages/plugins/@lego/plugin-todo/src/index.tsx +5 -0
- package/template/packages/plugins/@lego/plugin-todo/src/lib/utils.ts +20 -0
- package/template/packages/plugins/@lego/plugin-todo/src/pages/TodoPage.tsx +89 -0
- package/template/packages/plugins/@lego/plugin-todo/src/plugin.config.ts +104 -0
- package/template/packages/plugins/@lego/plugin-todo/src/plugin.ts +13 -0
- package/template/packages/plugins/@lego/plugin-todo/src/schemas.ts +37 -0
- package/template/packages/plugins/@lego/plugin-todo/src/types.ts +42 -0
- package/template/packages/plugins/@lego/plugin-todo/src/vite-env.d.ts +31 -0
- package/template/packages/plugins/@lego/plugin-todo/tailwind.config.ts +51 -0
- package/template/packages/plugins/@lego/plugin-todo/tsconfig.json +18 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/pocketbase/CHANGELOG.md +911 -0
- package/template/pocketbase/LICENSE.md +17 -0
- package/template/scripts/create-plugin.js +221 -0
- package/template/scripts/deploy.sh +56 -0
- package/template/tsconfig.base.json +26 -0
- package/template/tsconfig.json +8 -0
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
# Testing Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For AI Implementing This Plan:** This is document 11 of 13. Complete documents 01-10 first.
|
|
4
|
+
|
|
5
|
+
**Goal:** Implement comprehensive testing strategy with unit tests (Vitest), component tests (React Testing Library), and E2E tests (Playwright).
|
|
6
|
+
|
|
7
|
+
**Architecture:** Unit tests for utilities and hooks, component tests for UI components, E2E tests for critical user flows. Tests run in CI/CD pipeline.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Vitest, React Testing Library, Playwright, MSW (for API mocking), TypeScript
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Prerequisites
|
|
14
|
+
|
|
15
|
+
- ✅ Completed all documents 01-10
|
|
16
|
+
- ✅ Application is functional with all features implemented
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Task 1: Setup Testing Infrastructure
|
|
21
|
+
|
|
22
|
+
**Files:**
|
|
23
|
+
- Create: `vitest.workspace.ts`
|
|
24
|
+
- Create: `vitest.config.ts`
|
|
25
|
+
- Modify: `package.json` (root)
|
|
26
|
+
|
|
27
|
+
### Step 1: Create root testing dependencies
|
|
28
|
+
|
|
29
|
+
**File:** `package.json` (root)
|
|
30
|
+
|
|
31
|
+
Add to devDependencies:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@playwright/test": "^1.48.0",
|
|
37
|
+
"@testing-library/jest-dom": "^6.6.2",
|
|
38
|
+
"@testing-library/react": "^16.0.1",
|
|
39
|
+
"@testing-library/user-event": "^14.5.2",
|
|
40
|
+
"@vitejs/plugin-react": "^4.3.2",
|
|
41
|
+
"@vitest/ui": "^2.1.2",
|
|
42
|
+
"jsdom": "^25.0.1",
|
|
43
|
+
"msw": "^2.4.0",
|
|
44
|
+
"vitest": "^2.1.2",
|
|
45
|
+
"happy-dom": "^15.7.3"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Add test scripts:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"scripts": {
|
|
55
|
+
"test": "vitest",
|
|
56
|
+
"test:ui": "vitest --ui",
|
|
57
|
+
"test:run": "vitest run",
|
|
58
|
+
"test:coverage": "vitest run --coverage",
|
|
59
|
+
"test:e2e": "playwright test",
|
|
60
|
+
"test:e2e:ui": "playwright test --ui",
|
|
61
|
+
"test:e2e:debug": "playwright test --debug"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Step 2: Create Vitest workspace config
|
|
67
|
+
|
|
68
|
+
**File:** `vitest.workspace.ts`
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { defineWorkspace } from 'vitest/config';
|
|
72
|
+
|
|
73
|
+
export default defineWorkspace([
|
|
74
|
+
// Host app tests
|
|
75
|
+
{
|
|
76
|
+
test: {
|
|
77
|
+
name: 'host',
|
|
78
|
+
root: './host',
|
|
79
|
+
environment: 'jsdom',
|
|
80
|
+
setupFiles: ['./src/test/setup.ts'],
|
|
81
|
+
include: ['**/__tests__/**/*.test.{ts,tsx}'],
|
|
82
|
+
globals: true,
|
|
83
|
+
coverage: {
|
|
84
|
+
provider: 'v8',
|
|
85
|
+
reporter: ['text', 'json', 'html'],
|
|
86
|
+
exclude: ['node_modules/', 'dist/', '**/*.test.{ts,tsx}'],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
// Dashboard plugin tests
|
|
91
|
+
{
|
|
92
|
+
test: {
|
|
93
|
+
name: 'plugin-dashboard',
|
|
94
|
+
root: './packages/plugins/@lego/plugin-dashboard',
|
|
95
|
+
environment: 'jsdom',
|
|
96
|
+
setupFiles: ['./src/test/setup.ts'],
|
|
97
|
+
include: ['**/__tests__/**/*.test.{ts,tsx}'],
|
|
98
|
+
globals: true,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
// Todo plugin tests
|
|
102
|
+
{
|
|
103
|
+
test: {
|
|
104
|
+
name: 'plugin-todo',
|
|
105
|
+
root: './packages/plugins/@lego/plugin-todo',
|
|
106
|
+
environment: 'jsdom',
|
|
107
|
+
setupFiles: ['./src/test/setup.ts'],
|
|
108
|
+
include: ['**/__tests__/**/*.test.{ts,tsx}'],
|
|
109
|
+
globals: true,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Step 3: Create Vitest config for host
|
|
116
|
+
|
|
117
|
+
**File:** `host/vitest.config.ts`
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { defineConfig } from 'vitest/config';
|
|
121
|
+
import react from '@vitejs/plugin-react';
|
|
122
|
+
import { resolve } from 'path';
|
|
123
|
+
|
|
124
|
+
export default defineConfig({
|
|
125
|
+
plugins: [react()],
|
|
126
|
+
test: {
|
|
127
|
+
globals: true,
|
|
128
|
+
environment: 'jsdom',
|
|
129
|
+
setupFiles: ['./src/test/setup.ts'],
|
|
130
|
+
include: ['**/__tests__/**/*.test.{ts,tsx}'],
|
|
131
|
+
alias: {
|
|
132
|
+
'@': resolve(__dirname, './src'),
|
|
133
|
+
'@lego/kernel': resolve(__dirname, './src/kernel'),
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
resolve: {
|
|
137
|
+
alias: {
|
|
138
|
+
'@': resolve(__dirname, './src'),
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Step 4: Create test setup files
|
|
145
|
+
|
|
146
|
+
**File:** `host/src/test/setup.ts`
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { expect, afterEach, vi } from 'vitest';
|
|
150
|
+
import { cleanup } from '@testing-library/react';
|
|
151
|
+
import '@testing-library/jest-dom';
|
|
152
|
+
|
|
153
|
+
// Cleanup after each test
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
cleanup();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Mock PocketBase
|
|
159
|
+
vi.mock('pocketbase', () => ({
|
|
160
|
+
default: vi.fn().mockImplementation(() => ({
|
|
161
|
+
collection: vi.fn(),
|
|
162
|
+
authStore: {
|
|
163
|
+
isValid: false,
|
|
164
|
+
token: '',
|
|
165
|
+
model: null,
|
|
166
|
+
save: vi.fn(),
|
|
167
|
+
clear: vi.fn(),
|
|
168
|
+
onChange: vi.fn(),
|
|
169
|
+
},
|
|
170
|
+
})),
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
// Mock window bridge
|
|
174
|
+
global.__LEGO_KERNEL_STATE__ = {
|
|
175
|
+
useGlobalKernelState: vi.fn(),
|
|
176
|
+
};
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Task 2: Setup Playwright for E2E Tests
|
|
182
|
+
|
|
183
|
+
**Files:**
|
|
184
|
+
- Create: `playwright.config.ts`
|
|
185
|
+
- Create: `e2e/playwright.config.ts`
|
|
186
|
+
|
|
187
|
+
### Step 1: Create Playwright config
|
|
188
|
+
|
|
189
|
+
**File:** `playwright.config.ts`
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
193
|
+
|
|
194
|
+
export default defineConfig({
|
|
195
|
+
testDir: './e2e',
|
|
196
|
+
fullyParallel: true,
|
|
197
|
+
forbidOnly: !!process.env.CI,
|
|
198
|
+
retries: process.env.CI ? 2 : 0,
|
|
199
|
+
workers: process.env.CI ? 1 : undefined,
|
|
200
|
+
reporter: 'html',
|
|
201
|
+
use: {
|
|
202
|
+
baseURL: 'http://localhost:8080',
|
|
203
|
+
trace: 'on-first-retry',
|
|
204
|
+
screenshot: 'only-on-failure',
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
projects: [
|
|
208
|
+
{
|
|
209
|
+
name: 'chromium',
|
|
210
|
+
use: { ...devices['Desktop Chrome'] },
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: 'firefox',
|
|
214
|
+
use: { ...devices['Desktop Firefox'] },
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'webkit',
|
|
218
|
+
use: { ...devices['Desktop Safari'] },
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'Mobile Chrome',
|
|
222
|
+
use: { ...devices['Pixel 5'] },
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'Mobile Safari',
|
|
226
|
+
use: { ...devices['iPhone 12'] },
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
|
|
230
|
+
webServer: {
|
|
231
|
+
command: 'cd host && pnpm run dev',
|
|
232
|
+
url: 'http://localhost:8080',
|
|
233
|
+
reuseExistingServer: !process.env.CI,
|
|
234
|
+
timeout: 120 * 1000,
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Step 2: Create E2E test helpers
|
|
240
|
+
|
|
241
|
+
**File:** `e2e/helpers.ts`
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
import { Page, expect } from '@playwright/test';
|
|
245
|
+
|
|
246
|
+
export class DashboardPage {
|
|
247
|
+
constructor(private page: Page) {}
|
|
248
|
+
|
|
249
|
+
async goto() {
|
|
250
|
+
await this.page.goto('/dashboard');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async expectStatsVisible() {
|
|
254
|
+
await expect(this.page.getByText('Total Users')).toBeVisible();
|
|
255
|
+
await expect(this.page.getByText('Active Todos')).toBeVisible();
|
|
256
|
+
await expect(this.page.getByText('Completed Todos')).toBeVisible();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export class TodoPage {
|
|
261
|
+
constructor(private page: Page) {}
|
|
262
|
+
|
|
263
|
+
async goto() {
|
|
264
|
+
await this.page.goto('/todos');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async createTodo(title: string) {
|
|
268
|
+
await this.page.getByRole('button', { name: 'New Todo' }).click();
|
|
269
|
+
await this.page.getByLabel('Title').fill(title);
|
|
270
|
+
await this.page.getByRole('button', { name: 'Create Todo' }).click();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async expectTodoVisible(title: string) {
|
|
274
|
+
await expect(this.page.getByText(title)).toBeVisible();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async toggleTodo(title: string) {
|
|
278
|
+
const todo = this.page.getByText(title).locator('..').getByRole('button').first();
|
|
279
|
+
await todo.click();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async deleteTodo(title: string) {
|
|
283
|
+
const todo = this.page.getByText(title).locator('..');
|
|
284
|
+
await todo.getByRole('button', { name: /delete/i }).click();
|
|
285
|
+
await this.page.getByRole('button', { name: /confirm/i }).click();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export class AuthHelper {
|
|
290
|
+
constructor(private page: Page) {}
|
|
291
|
+
|
|
292
|
+
async login(email = 'admin@example.com', password = 'admin123') {
|
|
293
|
+
await this.page.goto('/login');
|
|
294
|
+
await this.page.getByLabel('Email').fill(email);
|
|
295
|
+
await this.page.getByLabel('Password').fill(password);
|
|
296
|
+
await this.page.getByRole('button', { name: 'Sign In' }).click();
|
|
297
|
+
await this.page.waitForURL('/dashboard');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async logout() {
|
|
301
|
+
await this.page.getByRole('button').filter({ hasText: /admin/i }).click();
|
|
302
|
+
await this.page.getByRole('menuitem', { name: /log out/i }).click();
|
|
303
|
+
await this.page.waitForURL('/login');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Task 3: Create Unit Tests
|
|
311
|
+
|
|
312
|
+
**Files:**
|
|
313
|
+
- Create: `host/src/kernel/lib/__tests__/utils.test.ts`
|
|
314
|
+
- Create: `host/src/kernel/shared-state/__tests__/store.test.ts`
|
|
315
|
+
|
|
316
|
+
### Step 1: Test utility functions
|
|
317
|
+
|
|
318
|
+
**File:** `host/src/kernel/lib/__tests__/utils.test.ts`
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import { describe, it, expect } from 'vitest';
|
|
322
|
+
import { cn, getInitials, formatDate, formatRelativeTime } from '../utils';
|
|
323
|
+
|
|
324
|
+
describe('cn (classnames utility)', () => {
|
|
325
|
+
it('should merge classnames correctly', () => {
|
|
326
|
+
expect(cn('foo', 'bar')).toBe('foo bar');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('should handle conditional classes', () => {
|
|
330
|
+
expect(cn('foo', false && 'bar', 'baz')).toBe('foo baz');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should handle tailwind merge conflicts', () => {
|
|
334
|
+
expect(cn('p-4', 'p-2')).toBe('p-2');
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe('getInitials', () => {
|
|
339
|
+
it('should extract initials from name', () => {
|
|
340
|
+
expect(getInitials('John Doe')).toBe('JD');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('should handle single word names', () => {
|
|
344
|
+
expect(getInitials('John')).toBe('J');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should handle multiple words', () => {
|
|
348
|
+
expect(getInitials('John Middle Doe')).toBe('JM');
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it('should limit to 2 characters', () => {
|
|
352
|
+
expect(getInitials('John Middle Last Doe')).toBe('JM');
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe('formatDate', () => {
|
|
357
|
+
it('should format date correctly', () => {
|
|
358
|
+
const date = new Date('2024-01-15');
|
|
359
|
+
const formatted = formatDate(date);
|
|
360
|
+
expect(formatted).toMatch(/Jan/);
|
|
361
|
+
expect(formatted).toMatch(/15/);
|
|
362
|
+
expect(formatted).toMatch(/2024/);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('formatRelativeTime', () => {
|
|
367
|
+
it('should return "just now" for very recent times', () => {
|
|
368
|
+
const now = new Date();
|
|
369
|
+
expect(formatRelativeTime(now)).toBe('just now');
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should return minutes ago', () => {
|
|
373
|
+
const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000);
|
|
374
|
+
expect(formatRelativeTime(twoMinutesAgo)).toBe('2m ago');
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should return hours ago', () => {
|
|
378
|
+
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);
|
|
379
|
+
expect(formatRelativeTime(twoHoursAgo)).toBe('2h ago');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should return days ago', () => {
|
|
383
|
+
const twoDaysAgo = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000);
|
|
384
|
+
expect(formatRelativeTime(twoDaysAgo)).toBe('2d ago');
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Step 2: Test shared state store
|
|
390
|
+
|
|
391
|
+
**File:** `host/src/kernel/shared-state/__tests__/store.test.ts`
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
395
|
+
import { act, renderHook } from '@testing-library/react';
|
|
396
|
+
import { useGlobalKernelState } from '../store';
|
|
397
|
+
|
|
398
|
+
describe('useGlobalKernelState', () => {
|
|
399
|
+
beforeEach(() => {
|
|
400
|
+
// Reset state before each test
|
|
401
|
+
const { result } = renderHook(() => useGlobalKernelState());
|
|
402
|
+
act(() => {
|
|
403
|
+
result.current.clearAuth();
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('should initialize with default state', () => {
|
|
408
|
+
const { result } = renderHook(() => useGlobalKernelState());
|
|
409
|
+
|
|
410
|
+
expect(result.current.user).toBeNull();
|
|
411
|
+
expect(result.current.isAuthenticated).toBe(false);
|
|
412
|
+
expect(result.current.theme).toBe('system');
|
|
413
|
+
expect(result.current.sidebarOpen).toBe(true);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('should set user', () => {
|
|
417
|
+
const { result } = renderHook(() => useGlobalKernelState());
|
|
418
|
+
|
|
419
|
+
act(() => {
|
|
420
|
+
result.current.setUser({
|
|
421
|
+
id: '123',
|
|
422
|
+
email: 'test@example.com',
|
|
423
|
+
name: 'Test User',
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
expect(result.current.user).toEqual({
|
|
428
|
+
id: '123',
|
|
429
|
+
email: 'test@example.com',
|
|
430
|
+
name: 'Test User',
|
|
431
|
+
});
|
|
432
|
+
expect(result.current.isAuthenticated).toBe(true);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should toggle sidebar', () => {
|
|
436
|
+
const { result } = renderHook(() => useGlobalKernelState());
|
|
437
|
+
|
|
438
|
+
expect(result.current.sidebarOpen).toBe(true);
|
|
439
|
+
|
|
440
|
+
act(() => {
|
|
441
|
+
result.current.toggleSidebar();
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
expect(result.current.sidebarOpen).toBe(false);
|
|
445
|
+
|
|
446
|
+
act(() => {
|
|
447
|
+
result.current.toggleSidebar();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
expect(result.current.sidebarOpen).toBe(true);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('should add and remove toasts', () => {
|
|
454
|
+
const { result } = renderHook(() => useGlobalKernelState());
|
|
455
|
+
|
|
456
|
+
act(() => {
|
|
457
|
+
result.current.addToast({
|
|
458
|
+
title: 'Test toast',
|
|
459
|
+
description: 'Test description',
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
expect(result.current.toasts).toHaveLength(1);
|
|
464
|
+
expect(result.current.toasts[0].title).toBe('Test toast');
|
|
465
|
+
|
|
466
|
+
act(() => {
|
|
467
|
+
result.current.removeToast(result.current.toasts[0].id);
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
expect(result.current.toasts).toHaveLength(0);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Task 4: Create Component Tests
|
|
478
|
+
|
|
479
|
+
**Files:**
|
|
480
|
+
- Create: `host/src/kernel/components/ui/__tests__/button.test.tsx`
|
|
481
|
+
- Create: `host/src/kernel/auth/__tests__/LoginForm.test.tsx`
|
|
482
|
+
|
|
483
|
+
### Step 1: Test button component
|
|
484
|
+
|
|
485
|
+
**File:** `host/src/kernel/components/ui/__tests__/button.test.tsx`
|
|
486
|
+
|
|
487
|
+
```typescript
|
|
488
|
+
import { describe, it, expect } from 'vitest';
|
|
489
|
+
import { render, screen } from '@testing-library/react';
|
|
490
|
+
import userEvent from '@testing-library/user-event';
|
|
491
|
+
import { Button } from '../button';
|
|
492
|
+
|
|
493
|
+
describe('Button', () => {
|
|
494
|
+
it('should render children', () => {
|
|
495
|
+
render(<Button>Click me</Button>);
|
|
496
|
+
expect(screen.getByRole('button')).toHaveTextContent('Click me');
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should call onClick when clicked', async () => {
|
|
500
|
+
const handleClick = vi.fn();
|
|
501
|
+
const user = userEvent.setup();
|
|
502
|
+
|
|
503
|
+
render(<Button onClick={handleClick}>Click me</Button>);
|
|
504
|
+
|
|
505
|
+
await user.click(screen.getByRole('button'));
|
|
506
|
+
|
|
507
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should be disabled when disabled prop is true', async () => {
|
|
511
|
+
const handleClick = vi.fn();
|
|
512
|
+
const user = userEvent.setup();
|
|
513
|
+
|
|
514
|
+
render(
|
|
515
|
+
<Button onClick={handleClick} disabled>
|
|
516
|
+
Click me
|
|
517
|
+
</Button>
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
await user.click(screen.getByRole('button'));
|
|
521
|
+
|
|
522
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('should apply variant classes', () => {
|
|
526
|
+
const { rerender } = render(<Button variant="destructive">Delete</Button>);
|
|
527
|
+
expect(screen.getByRole('button')).toHaveClass('bg-destructive');
|
|
528
|
+
|
|
529
|
+
rerender(<Button variant="outline">Cancel</Button>);
|
|
530
|
+
expect(screen.getByRole('button')).toHaveClass('border');
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
### Step 2: Test login form component
|
|
536
|
+
|
|
537
|
+
**File:** `host/src/kernel/auth/__tests__/LoginForm.test.tsx`
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
541
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
542
|
+
import userEvent from '@testing-library/user-event';
|
|
543
|
+
import { LoginForm } from '../components/LoginForm';
|
|
544
|
+
|
|
545
|
+
// Mock useAuth hook
|
|
546
|
+
vi.mock('../hooks', () => ({
|
|
547
|
+
useAuth: () => ({
|
|
548
|
+
login: mockLogin,
|
|
549
|
+
isLoading: false,
|
|
550
|
+
error: null,
|
|
551
|
+
}),
|
|
552
|
+
}));
|
|
553
|
+
|
|
554
|
+
const mockLogin = vi.fn();
|
|
555
|
+
|
|
556
|
+
// Mock navigate
|
|
557
|
+
vi.mock('@modern-js/runtime/router', () => ({
|
|
558
|
+
useNavigate: () => mockNavigate,
|
|
559
|
+
}));
|
|
560
|
+
|
|
561
|
+
const mockNavigate = vi.fn();
|
|
562
|
+
|
|
563
|
+
describe('LoginForm', () => {
|
|
564
|
+
beforeEach(() => {
|
|
565
|
+
vi.clearAllMocks();
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('should render form fields', () => {
|
|
569
|
+
render(<LoginForm />);
|
|
570
|
+
|
|
571
|
+
expect(screen.getByLabelText('Email')).toBeInTheDocument();
|
|
572
|
+
expect(screen.getByLabelText('Password')).toBeInTheDocument();
|
|
573
|
+
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument();
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it('should show validation errors for empty fields', async () => {
|
|
577
|
+
const user = userEvent.setup();
|
|
578
|
+
render(<LoginForm />);
|
|
579
|
+
|
|
580
|
+
const submitButton = screen.getByRole('button', { name: /sign in/i });
|
|
581
|
+
await user.click(submitButton);
|
|
582
|
+
|
|
583
|
+
expect(await screen.findByText('Invalid email address')).toBeInTheDocument();
|
|
584
|
+
expect(screen.getByText(/password must be at least/i)).toBeInTheDocument();
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it('should call login with form data', async () => {
|
|
588
|
+
const user = userEvent.setup();
|
|
589
|
+
render(<LoginForm />);
|
|
590
|
+
|
|
591
|
+
await user.type(screen.getByLabelText('Email'), 'admin@example.com');
|
|
592
|
+
await user.type(screen.getByLabelText('Password'), 'admin123');
|
|
593
|
+
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
594
|
+
|
|
595
|
+
await waitFor(() => {
|
|
596
|
+
expect(mockLogin).toHaveBeenCalledWith({
|
|
597
|
+
email: 'admin@example.com',
|
|
598
|
+
password: 'admin123',
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it('should navigate to dashboard on successful login', async () => {
|
|
604
|
+
const user = userEvent.setup();
|
|
605
|
+
mockLogin.mockResolvedValueOnce({});
|
|
606
|
+
|
|
607
|
+
render(<LoginForm />);
|
|
608
|
+
|
|
609
|
+
await user.type(screen.getByLabelText('Email'), 'admin@example.com');
|
|
610
|
+
await user.type(screen.getByLabelText('Password'), 'admin123');
|
|
611
|
+
await user.click(screen.getByRole('button', { name: /sign in/i }));
|
|
612
|
+
|
|
613
|
+
await waitFor(() => {
|
|
614
|
+
expect(mockNavigate).toHaveBeenCalledWith('/dashboard');
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
## Task 5: Create E2E Tests
|
|
623
|
+
|
|
624
|
+
**Files:**
|
|
625
|
+
- Create: `e2e/auth.spec.ts`
|
|
626
|
+
- Create: `e2e/dashboard.spec.ts`
|
|
627
|
+
- Create: `e2e/todo.spec.ts`
|
|
628
|
+
|
|
629
|
+
### Step 1: Test authentication flow
|
|
630
|
+
|
|
631
|
+
**File:** `e2e/auth.spec.ts`
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
import { test, expect } from '@playwright/test';
|
|
635
|
+
import { AuthHelper } from './helpers';
|
|
636
|
+
|
|
637
|
+
test.describe('Authentication', () => {
|
|
638
|
+
let auth: AuthHelper;
|
|
639
|
+
|
|
640
|
+
test.beforeEach(async ({ page }) => {
|
|
641
|
+
auth = new AuthHelper(page);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
test('should login with valid credentials', async ({ page }) => {
|
|
645
|
+
await auth.login();
|
|
646
|
+
|
|
647
|
+
await expect(page).toHaveURL('/dashboard');
|
|
648
|
+
await expect(page.getByText('Welcome to your organization dashboard')).toBeVisible();
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test('should show error with invalid credentials', async ({ page }) => {
|
|
652
|
+
await page.goto('/login');
|
|
653
|
+
await page.getByLabel('Email').fill('invalid@example.com');
|
|
654
|
+
await page.getByLabel('Password').fill('wrongpassword');
|
|
655
|
+
await page.getByRole('button', { name: 'Sign In' }).click();
|
|
656
|
+
|
|
657
|
+
await expect(page.getByText(/login failed/i)).toBeVisible();
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test('should logout and redirect to login', async ({ page }) => {
|
|
661
|
+
await auth.login();
|
|
662
|
+
await auth.logout();
|
|
663
|
+
|
|
664
|
+
await expect(page).toHaveURL('/login');
|
|
665
|
+
await expect(page.getByText('Sign In')).toBeVisible();
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
test('should protect authenticated routes', async ({ page }) => {
|
|
669
|
+
await page.goto('/dashboard');
|
|
670
|
+
|
|
671
|
+
await expect(page).toHaveURL('/login');
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
### Step 2: Test dashboard functionality
|
|
677
|
+
|
|
678
|
+
**File:** `e2e/dashboard.spec.ts`
|
|
679
|
+
|
|
680
|
+
```typescript
|
|
681
|
+
import { test, expect } from '@playwright/test';
|
|
682
|
+
import { AuthHelper } from './helpers';
|
|
683
|
+
|
|
684
|
+
test.describe('Dashboard', () => {
|
|
685
|
+
test.beforeEach(async ({ page }) => {
|
|
686
|
+
const auth = new AuthHelper(page);
|
|
687
|
+
await auth.login();
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
test('should display dashboard stats', async ({ page }) => {
|
|
691
|
+
await page.goto('/dashboard');
|
|
692
|
+
|
|
693
|
+
await expect(page.getByText('Total Users')).toBeVisible();
|
|
694
|
+
await expect(page.getByText('Active Todos')).toBeVisible();
|
|
695
|
+
await expect(page.getByText('Completed Todos')).toBeVisible();
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
test('should display recent activity', async ({ page }) => {
|
|
699
|
+
await page.goto('/dashboard');
|
|
700
|
+
|
|
701
|
+
await expect(page.getByText('Recent Activity')).toBeVisible();
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
test('should display quick actions', async ({ page }) => {
|
|
705
|
+
await page.goto('/dashboard');
|
|
706
|
+
|
|
707
|
+
await expect(page.getByText('Quick Actions')).toBeVisible();
|
|
708
|
+
await expect(page.getByText('New Todo')).toBeVisible();
|
|
709
|
+
await expect(page.getByText('Invite User')).toBeVisible();
|
|
710
|
+
});
|
|
711
|
+
});
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### Step 3: Test todo CRUD functionality
|
|
715
|
+
|
|
716
|
+
**File:** `e2e/todo.spec.ts`
|
|
717
|
+
|
|
718
|
+
```typescript
|
|
719
|
+
import { test, expect } from '@playwright/test';
|
|
720
|
+
import { AuthHelper, TodoPage } from './helpers';
|
|
721
|
+
|
|
722
|
+
test.describe('Todos', () => {
|
|
723
|
+
let auth: AuthHelper;
|
|
724
|
+
let todoPage: TodoPage;
|
|
725
|
+
|
|
726
|
+
test.beforeEach(async ({ page }) => {
|
|
727
|
+
auth = new AuthHelper(page);
|
|
728
|
+
todoPage = new TodoPage(page);
|
|
729
|
+
await auth.login();
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
test('should display todos page', async ({ page }) => {
|
|
733
|
+
await todoPage.goto();
|
|
734
|
+
|
|
735
|
+
await expect(page.getByText('Manage your tasks and stay organized')).toBeVisible();
|
|
736
|
+
await expect(page.getByRole('button', { name: 'New Todo' })).toBeVisible();
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
test('should create a new todo', async ({ page }) => {
|
|
740
|
+
await todoPage.goto();
|
|
741
|
+
await todoPage.createTodo('Test E2E Todo');
|
|
742
|
+
|
|
743
|
+
await todoPage.expectTodoVisible('Test E2E Todo');
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
test('should toggle todo completion', async ({ page }) => {
|
|
747
|
+
await todoPage.goto();
|
|
748
|
+
await todoPage.createTodo('Toggle Test');
|
|
749
|
+
|
|
750
|
+
const todoItem = page.getByText('Toggle Test').locator('..');
|
|
751
|
+
|
|
752
|
+
// Get initial state
|
|
753
|
+
const checkbox = todoItem.getByRole('button').first();
|
|
754
|
+
await expect(checkbox).toHaveAttribute('aria-checked', 'false');
|
|
755
|
+
|
|
756
|
+
// Toggle to complete
|
|
757
|
+
await todoPage.toggleTodo('Toggle Test');
|
|
758
|
+
await expect(todoItem).toHaveClass(/opacity-60/);
|
|
759
|
+
|
|
760
|
+
// Toggle back
|
|
761
|
+
await todoPage.toggleTodo('Toggle Test');
|
|
762
|
+
await expect(todoItem).not.toHaveClass(/opacity-60/);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
test('should delete a todo', async ({ page }) => {
|
|
766
|
+
await todoPage.goto();
|
|
767
|
+
await todoPage.createTodo('Delete Test');
|
|
768
|
+
|
|
769
|
+
await todoPage.expectTodoVisible('Delete Test');
|
|
770
|
+
|
|
771
|
+
await todoPage.deleteTodo('Delete Test');
|
|
772
|
+
|
|
773
|
+
await expect(page.getByText('Delete Test')).not.toBeVisible();
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
test('should filter todos by status', async ({ page }) => {
|
|
777
|
+
await todoPage.goto();
|
|
778
|
+
await todoPage.createTodo('Active Todo');
|
|
779
|
+
await todoPage.createTodo('Completed Todo');
|
|
780
|
+
|
|
781
|
+
// Mark one as completed
|
|
782
|
+
await todoPage.toggleTodo('Completed Todo');
|
|
783
|
+
|
|
784
|
+
// Filter to active only
|
|
785
|
+
await page.getByRole('combobox', { name: 'Status' }).click();
|
|
786
|
+
await page.getByRole('option', { name: 'Active' }).click();
|
|
787
|
+
|
|
788
|
+
await expect(page.getByText('Active Todo')).toBeVisible();
|
|
789
|
+
await expect(page.getByText('Completed Todo')).not.toBeVisible();
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
---
|
|
795
|
+
|
|
796
|
+
## Task 6: Create Test Scripts
|
|
797
|
+
|
|
798
|
+
**Files:**
|
|
799
|
+
- Modify: `package.json` (root)
|
|
800
|
+
|
|
801
|
+
### Step 1: Add test scripts to root package.json
|
|
802
|
+
|
|
803
|
+
**File:** `package.json`
|
|
804
|
+
|
|
805
|
+
```json
|
|
806
|
+
{
|
|
807
|
+
"scripts": {
|
|
808
|
+
"test": "vitest",
|
|
809
|
+
"test:ui": "vitest --ui",
|
|
810
|
+
"test:run": "vitest run",
|
|
811
|
+
"test:coverage": "vitest run --coverage",
|
|
812
|
+
"test:watch": "vitest watch",
|
|
813
|
+
"test:e2e": "playwright test",
|
|
814
|
+
"test:e2e:ui": "playwright test --ui",
|
|
815
|
+
"test:e2e:debug": "playwright test --debug",
|
|
816
|
+
"test:all": "pnpm run test:run && pnpm run test:e2e"
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
### Step 2: Update host package.json
|
|
822
|
+
|
|
823
|
+
**File:** `host/package.json`
|
|
824
|
+
|
|
825
|
+
```json
|
|
826
|
+
{
|
|
827
|
+
"scripts": {
|
|
828
|
+
"test": "vitest",
|
|
829
|
+
"test:ui": "vitest --ui",
|
|
830
|
+
"test:coverage": "vitest run --coverage"
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
## Verification
|
|
838
|
+
|
|
839
|
+
### Step 1: Install test dependencies
|
|
840
|
+
|
|
841
|
+
**Run:**
|
|
842
|
+
|
|
843
|
+
```bash
|
|
844
|
+
pnpm install
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
Expected: All test dependencies installed.
|
|
848
|
+
|
|
849
|
+
### Step 2: Run unit tests
|
|
850
|
+
|
|
851
|
+
**Run:**
|
|
852
|
+
|
|
853
|
+
```bash
|
|
854
|
+
pnpm test:run
|
|
855
|
+
```
|
|
856
|
+
|
|
857
|
+
Expected: Unit tests pass.
|
|
858
|
+
|
|
859
|
+
### Step 3: Run component tests with UI
|
|
860
|
+
|
|
861
|
+
**Run:**
|
|
862
|
+
|
|
863
|
+
```bash
|
|
864
|
+
pnpm test:ui
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
Expected: Vitest UI opens in browser.
|
|
868
|
+
|
|
869
|
+
### Step 4: Install Playwright browsers
|
|
870
|
+
|
|
871
|
+
**Run:**
|
|
872
|
+
|
|
873
|
+
```bash
|
|
874
|
+
npx playwright install
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
Expected: Playwright browsers downloaded.
|
|
878
|
+
|
|
879
|
+
### Step 5: Run E2E tests
|
|
880
|
+
|
|
881
|
+
**Run:**
|
|
882
|
+
|
|
883
|
+
```bash
|
|
884
|
+
pnpm test:e2e
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
Expected: E2E tests run and pass (requires dev server running).
|
|
888
|
+
|
|
889
|
+
---
|
|
890
|
+
|
|
891
|
+
## Summary
|
|
892
|
+
|
|
893
|
+
After completing this document, you will have:
|
|
894
|
+
|
|
895
|
+
1. ✅ Vitest configured for unit and component tests
|
|
896
|
+
2. ✅ Playwright configured for E2E tests
|
|
897
|
+
3. ✅ Unit tests for utilities and hooks
|
|
898
|
+
4. ✅ Component tests for UI components
|
|
899
|
+
5. ✅ E2E tests for critical user flows (auth, dashboard, todos)
|
|
900
|
+
6. ✅ Test helpers and fixtures
|
|
901
|
+
7. ✅ Coverage reporting configured
|
|
902
|
+
8. ✅ Test scripts in package.json
|
|
903
|
+
|
|
904
|
+
**Next:** `12-deployment.md` - Implement build configuration, deployment scripts, and CI/CD pipeline.
|
|
905
|
+
|
|
906
|
+
---
|
|
907
|
+
|
|
908
|
+
## Files Created
|
|
909
|
+
|
|
910
|
+
```
|
|
911
|
+
root/
|
|
912
|
+
├── vitest.workspace.ts
|
|
913
|
+
├── vitest.config.ts
|
|
914
|
+
├── playwright.config.ts
|
|
915
|
+
└── e2e/
|
|
916
|
+
├── auth.spec.ts
|
|
917
|
+
├── dashboard.spec.ts
|
|
918
|
+
├── todo.spec.ts
|
|
919
|
+
└── helpers.ts
|
|
920
|
+
|
|
921
|
+
host/
|
|
922
|
+
├── vitest.config.ts
|
|
923
|
+
└── src/
|
|
924
|
+
├── test/
|
|
925
|
+
│ └── setup.ts
|
|
926
|
+
└── kernel/
|
|
927
|
+
├── lib/__tests__/
|
|
928
|
+
│ └── utils.test.ts
|
|
929
|
+
├── shared-state/__tests__/
|
|
930
|
+
│ └── store.test.ts
|
|
931
|
+
├── components/ui/__tests__/
|
|
932
|
+
│ └── button.test.tsx
|
|
933
|
+
└── auth/__tests__/
|
|
934
|
+
└── LoginForm.test.tsx
|
|
935
|
+
```
|