Skip to content

Commit 2bd2cc7

Browse files
authored
Add webview testing infrastructure and pnpm catalog (#770)
- Add pnpm catalog with strict mode for shared dependency versions - Add webview unit tests for shared utilities - Upgrade vite 6→7 and @vitejs/plugin-react-swc 3→4 - Add typed message API and ErrorBoundary component
1 parent 5e4909f commit 2bd2cc7

24 files changed

+1114
-426
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
/.vscode-test/
55
/.nyc_output/
66
/coverage/
7+
/.claude/
78
*.vsix
89
flake.lock
910
pnpm-debug.log

CLAUDE.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,30 @@ Comments explain what code does or why it exists:
4747
## Build and Test Commands
4848

4949
- Build: `pnpm build`
50-
- Watch mode: `pnpm watch`
50+
- Watch mode: `pnpm watch` (or `pnpm watch:all`)
5151
- Package: `pnpm package`
5252
- Format: `pnpm fmt`
5353
- Format check: `pnpm fmt:check`
5454
- Lint: `pnpm lint`
5555
- Lint with auto-fix: `pnpm lint:fix`
56-
- Run all tests: `pnpm test`
57-
- Unit tests: `pnpm test:ci`
56+
- All unit tests: `pnpm test` (or `pnpm test:all`)
57+
- Extension tests: `pnpm test:extension`
58+
- Webview tests: `pnpm test:webview`
59+
- CI mode: `pnpm test:ci`
5860
- Integration tests: `pnpm test:integration`
59-
- Run specific unit test: `pnpm test:ci ./test/unit/filename.test.ts`
60-
- Run specific integration test: `pnpm test:integration ./test/integration/filename.test.ts`
61+
- Run specific extension test: `pnpm test:extension ./test/unit/filename.test.ts`
62+
- Run specific webview test: `pnpm test:webview ./test/webview/filename.test.ts`
63+
64+
## Test File Organization
65+
66+
```text
67+
test/
68+
├── unit/ # Extension unit tests (mirrors src/ structure)
69+
├── webview/ # Webview unit tests (by package name)
70+
├── integration/ # VS Code integration tests (uses Mocha, not Vitest)
71+
├── utils/ # Test utilities that are also tested
72+
└── mocks/ # Shared test mocks
73+
```
6174

6275
## Code Style
6376

@@ -69,5 +82,7 @@ Comments explain what code does or why it exists:
6982
- Prefix unused variables with underscore (e.g., `_unused`)
7083
- Error handling: wrap and type errors appropriately
7184
- Use async/await for promises, avoid explicit Promise construction where possible
72-
- Unit test files must be named `*.test.ts` and use Vitest, they should be placed in `./test/unit/<path in src>`
85+
- Unit test files must be named `*.test.ts` and use Vitest
86+
- Extension tests go in `./test/unit/<path in src>`
87+
- Webview tests go in `./test/webview/<package name>/`
7388
- Never disable ESLint rules without user approval

CONTRIBUTING.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,18 +121,42 @@ The link format is `vscode://coder.coder-remote/open?${query}`. For example:
121121
code --open-url 'vscode://coder.coder-remote/open?url=dev.coder.com&owner=my-username&workspace=my-ws&agent=my-agent'
122122
```
123123

124-
There are unit tests using `vitest` with mocked VS Code APIs:
124+
### Unit Tests
125+
126+
The project uses Vitest with separate test configurations for extension and webview code:
125127

126128
```bash
127-
pnpm test:ci
129+
pnpm test:extension # Extension tests (runs in Electron with mocked VS Code APIs)
130+
pnpm test:webview # Webview tests (runs in jsdom)
131+
pnpm test:all # Both extension and webview tests
132+
pnpm test:ci # CI mode (same as test:all with CI=true)
133+
```
134+
135+
Test files are organized by type:
136+
137+
```text
138+
test/
139+
├── unit/ # Extension unit tests
140+
├── webview/ # Webview unit tests (jsdom environment)
141+
├── integration/ # Integration tests (real VS Code)
142+
└── mocks/ # Shared test mocks
128143
```
129144

130-
There are also integration tests that run inside a real VS Code instance:
145+
### Integration Tests
146+
147+
Integration tests run inside a real VS Code instance:
131148

132149
```bash
133150
pnpm test:integration
134151
```
135152

153+
**Limitations:**
154+
155+
- Must use Mocha (VS Code test runner requirement), not Vitest
156+
- Cannot run while another VS Code instance is open (they share state)
157+
- Requires closing VS Code or running in a clean environment
158+
- Test files in `test/integration/` are compiled to `out/` before running
159+
136160
## Development
137161

138162
> [!IMPORTANT]

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export default defineConfig(
123123

124124
// Test files - use test tsconfig and relax some rules
125125
{
126-
files: ["test/**/*.ts", "**/*.test.ts", "**/*.spec.ts"],
126+
files: ["test/**/*.{ts,tsx}", "**/*.test.{ts,tsx}", "**/*.spec.{ts,tsx}"],
127127
settings: {
128128
"import-x/resolver-next": [
129129
createTypeScriptImportResolver({ project: "test/tsconfig.json" }),

package.json

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727
"lint:fix": "pnpm lint --fix",
2828
"package": "vsce package --no-dependencies",
2929
"package:prerelease": "vsce package --pre-release --no-dependencies",
30-
"pretest": "tsc -p test --noEmit && pnpm fmt:check && pnpm lint",
31-
"test": "ELECTRON_RUN_AS_NODE=1 electron node_modules/vitest/vitest.mjs",
32-
"test:ci": "CI=true pnpm test",
30+
"test": "CI=true pnpm test:extension && CI=true pnpm test:webview",
31+
"test:ci": "pnpm test",
32+
"test:extension": "ELECTRON_RUN_AS_NODE=1 electron node_modules/vitest/vitest.mjs --project extension",
3333
"test:integration": "tsc -p test --outDir out && node esbuild.mjs && vscode-test",
34+
"test:webview": "vitest --project webview",
3435
"vscode:prepublish": "pnpm build:production",
36+
"watch": "pnpm watch:all",
3537
"watch:all": "concurrently -n extension,webviews \"pnpm watch:extension\" \"pnpm watch:webviews\"",
3638
"watch:extension": "node esbuild.mjs --watch",
3739
"watch:webviews": "pnpm -r --filter \"./packages/*\" --parallel dev"
@@ -463,17 +465,20 @@
463465
"devDependencies": {
464466
"@eslint/js": "^9.39.2",
465467
"@eslint/markdown": "^7.5.1",
468+
"@testing-library/react": "^16.3.2",
466469
"@tsconfig/node20": "^20.1.8",
467470
"@types/mocha": "^10.0.10",
468471
"@types/node": "^20",
469472
"@types/proper-lockfile": "^4.1.4",
473+
"@types/react": "catalog:",
474+
"@types/react-dom": "catalog:",
470475
"@types/semver": "^7.7.1",
471476
"@types/ua-parser-js": "0.7.39",
472477
"@types/vscode": "^1.95.0",
473478
"@types/ws": "^8.18.1",
474479
"@typescript-eslint/eslint-plugin": "^8.53.1",
475480
"@typescript-eslint/parser": "^8.53.1",
476-
"@vitejs/plugin-react-swc": "^3.8.0",
481+
"@vitejs/plugin-react-swc": "catalog:",
477482
"@vitest/coverage-v8": "^4.0.16",
478483
"@vscode/test-cli": "^0.0.12",
479484
"@vscode/test-electron": "^2.5.2",
@@ -492,13 +497,16 @@
492497
"eslint-plugin-react": "^7.37.0",
493498
"eslint-plugin-react-hooks": "^5.0.0",
494499
"globals": "^17.0.0",
500+
"jsdom": "^27.4.0",
495501
"jsonc-eslint-parser": "^2.4.2",
496502
"memfs": "^4.56.10",
497503
"prettier": "^3.7.4",
498-
"typescript": "^5.9.3",
504+
"react": "catalog:",
505+
"react-dom": "catalog:",
506+
"typescript": "catalog:",
499507
"typescript-eslint": "^8.53.1",
500508
"utf-8-validate": "^6.0.6",
501-
"vite": "^6.0.0",
509+
"vite": "catalog:",
502510
"vitest": "^4.0.16"
503511
},
504512
"extensionPack": [

packages/tasks/package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@
1010
},
1111
"dependencies": {
1212
"@repo/webview-shared": "workspace:*",
13-
"@vscode-elements/react-elements": "^2.4.0",
14-
"react": "^19.0.0",
15-
"react-dom": "^19.0.0"
13+
"@vscode-elements/react-elements": "catalog:",
14+
"react": "catalog:",
15+
"react-dom": "catalog:"
1616
},
1717
"devDependencies": {
18-
"@types/react": "^19.0.0",
19-
"@types/react-dom": "^19.0.0",
20-
"@vitejs/plugin-react-swc": "^3.8.0",
21-
"typescript": "^5.7.3",
22-
"vite": "^6.0.0"
18+
"@types/react": "catalog:",
19+
"@types/react-dom": "catalog:",
20+
"@vitejs/plugin-react-swc": "catalog:",
21+
"typescript": "catalog:",
22+
"vite": "catalog:"
2323
}
2424
}

packages/tasks/src/App.tsx

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
1-
import { postMessage, useMessage } from "@repo/webview-shared/react";
1+
import { logger } from "@repo/webview-shared/logger";
2+
import { useMessage } from "@repo/webview-shared/react";
23
import {
34
VscodeButton,
45
VscodeProgressRing,
56
} from "@vscode-elements/react-elements";
6-
import { useCallback, useEffect, useState } from "react";
7+
import { useEffect, useState } from "react";
78

8-
import type { WebviewMessage } from "@repo/webview-shared";
9+
import { sendReady, sendRefresh } from "./messages";
10+
11+
import type { TasksExtensionMessage } from "@repo/webview-shared";
912

1013
export default function App() {
1114
const [ready, setReady] = useState(false);
1215

13-
const handleMessage = useCallback((message: WebviewMessage) => {
16+
useMessage<TasksExtensionMessage>((message) => {
1417
switch (message.type) {
1518
case "init":
1619
setReady(true);
1720
break;
21+
case "error":
22+
logger.error("Tasks panel error:", message.data);
23+
break;
1824
}
19-
}, []);
20-
21-
useMessage(handleMessage);
25+
});
2226

2327
useEffect(() => {
24-
postMessage({ type: "ready" });
28+
sendReady();
2529
}, []);
2630

2731
if (!ready) {
@@ -31,9 +35,7 @@ export default function App() {
3135
return (
3236
<div>
3337
<h2>Coder Tasks</h2>
34-
<VscodeButton onClick={() => postMessage({ type: "refresh" })}>
35-
Refresh
36-
</VscodeButton>
38+
<VscodeButton onClick={sendRefresh}>Refresh</VscodeButton>
3739
</div>
3840
);
3941
}

packages/tasks/src/index.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1+
import { ErrorBoundary } from "@repo/webview-shared/react";
12
import { StrictMode } from "react";
23
import { createRoot } from "react-dom/client";
34

45
import App from "./App";
56
import "./index.css";
67

78
const root = document.getElementById("root");
8-
if (root) {
9-
createRoot(root).render(
10-
<StrictMode>
11-
<App />
12-
</StrictMode>,
9+
if (!root) {
10+
throw new Error(
11+
"Failed to find root element. The webview HTML must contain an element with id='root'.",
1312
);
1413
}
14+
15+
createRoot(root).render(
16+
<StrictMode>
17+
<ErrorBoundary>
18+
<App />
19+
</ErrorBoundary>
20+
</StrictMode>,
21+
);

packages/tasks/src/messages.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { postMessage } from "@repo/webview-shared/api";
2+
3+
import type { TasksWebviewMessage } from "@repo/webview-shared";
4+
5+
function sendMessage(message: TasksWebviewMessage): void {
6+
postMessage(message);
7+
}
8+
9+
/** Signal to the extension that the webview is ready */
10+
export function sendReady(): void {
11+
sendMessage({ type: "ready" });
12+
}
13+
14+
/** Request task refresh from the extension */
15+
export function sendRefresh(): void {
16+
sendMessage({ type: "refresh" });
17+
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,6 @@
11
// Types exposed to the extension (react/ subpath is excluded).
2-
export type { WebviewMessage } from "./src/index";
2+
export type {
3+
TasksExtensionMessage,
4+
TasksWebviewMessage,
5+
WebviewMessage,
6+
} from "./src/index";

0 commit comments

Comments
 (0)