@norskvideo/ctl-dev-kit 0.2.78 → 0.2.79

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.
@@ -13,6 +13,7 @@
13
13
  // directory below is not a free choice.
14
14
  import type { GeneratedFile, ShapeContext } from "../create-product.ts";
15
15
  import { EXAMPLE_ID } from "./components.ts";
16
+ import { TEST_SETUP_TS, UI_TEST_DEV_DEPENDENCIES, VITEST_CONFIG_TS } from "./ui-tests.ts";
16
17
 
17
18
  /** The tar ships workflow.yml at its root, so Studio knows the workflow by this name. */
18
19
  export const WORKFLOW_NAME = "workflow";
@@ -39,6 +40,7 @@ function packageJson(ctx: ShapeContext): string {
39
40
  dev: "bunx vite build --watch",
40
41
  clean: `rm -rf ../dashboards/${WORKFLOW_NAME}`,
41
42
  typecheck: "bunx tsc --noEmit",
43
+ test: "vitest run",
42
44
  },
43
45
  dependencies: {
44
46
  react: "^19.0.0",
@@ -51,6 +53,7 @@ function packageJson(ctx: ShapeContext): string {
51
53
  "@vitejs/plugin-react": "^6.0.1",
52
54
  typescript: "^5.7.2",
53
55
  vite: "^8.0.0",
56
+ ...UI_TEST_DEV_DEPENDENCIES,
54
57
  },
55
58
  },
56
59
  null,
@@ -209,6 +212,38 @@ export default function App({ env }: { env: Env }) {
209
212
  }
210
213
  `;
211
214
 
215
+ // A stand-in for the component's state socket: what a dashboard test drives
216
+ // instead of a running Studio.
217
+ const APP_TEST_TSX = `import { act, render, screen } from "@testing-library/react";
218
+ import { afterEach, expect, test, vi } from "vitest";
219
+ import App from "./App.tsx";
220
+
221
+ class FakeSocket {
222
+ static last: FakeSocket | undefined;
223
+ onmessage: ((event: { data: string }) => void) | null = null;
224
+ constructor(readonly url: string) {
225
+ FakeSocket.last = this;
226
+ }
227
+ close() {}
228
+ deliver(message: unknown) {
229
+ this.onmessage?.({ data: JSON.stringify(message) });
230
+ }
231
+ }
232
+
233
+ afterEach(() => {
234
+ vi.unstubAllGlobals();
235
+ });
236
+
237
+ test("shows the component's state as it arrives", () => {
238
+ vi.stubGlobal("WebSocket", FakeSocket);
239
+ render(<App env={{ apiBasePath: "/api", wsBasePath: "/ws" }} />);
240
+ expect(screen.getByText(/waiting for the component's first state/)).toBeInTheDocument();
241
+
242
+ act(() => FakeSocket.last?.deliver({ type: "state-update", state: { ticks: 3 } }));
243
+ expect(screen.getByText("3 ticks")).toBeInTheDocument();
244
+ });
245
+ `;
246
+
212
247
  const MAIN_TSX = `import React from "react";
213
248
  import ReactDOM from "react-dom/client";
214
249
  import App from "./App.tsx";
@@ -263,9 +298,12 @@ export function dashboardFiles(ctx: ShapeContext): GeneratedFile[] {
263
298
  { path: "dashboard/package.json", content: packageJson(ctx) },
264
299
  { path: "dashboard/tsconfig.json", content: TSCONFIG },
265
300
  { path: "dashboard/vite.config.ts", content: VITE_CONFIG_TS },
301
+ { path: "dashboard/vitest.config.ts", content: VITEST_CONFIG_TS },
266
302
  { path: "dashboard/index.html", content: indexHtml(ctx) },
267
303
  { path: "dashboard/src/main.tsx", content: MAIN_TSX },
268
304
  { path: "dashboard/src/App.tsx", content: APP_TSX },
305
+ { path: "dashboard/src/App.test.tsx", content: APP_TEST_TSX },
306
+ { path: "dashboard/src/test/setup.ts", content: TEST_SETUP_TS },
269
307
  { path: "dashboard/src/api.ts", content: API_TS },
270
308
  { path: "dashboard/src/index.css", content: INDEX_CSS },
271
309
  { path: "backend/src/lib/dashboards.ts", content: BACKEND_LIB_DASHBOARDS_TS },
@@ -13,6 +13,7 @@
13
13
  // three independent edits, and why the dev script builds dist before starting
14
14
  // the backend that serves it.
15
15
  import type { GeneratedFile, ShapeContext } from "../create-product.ts";
16
+ import { TEST_SETUP_TS, UI_TEST_DEV_DEPENDENCIES, VITEST_CONFIG_TS } from "./ui-tests.ts";
16
17
 
17
18
  export const CONFIG_SCREEN_URL = "/configure";
18
19
 
@@ -31,6 +32,7 @@ function packageJson(ctx: ShapeContext): string {
31
32
  build: "bunx vite build",
32
33
  clean: "rm -rf dist",
33
34
  typecheck: "bunx tsc --noEmit",
35
+ test: "vitest run",
34
36
  },
35
37
  dependencies: {
36
38
  [`${ctx.scope}/shared`]: "workspace:*",
@@ -44,6 +46,7 @@ function packageJson(ctx: ShapeContext): string {
44
46
  "@vitejs/plugin-react": "^6.0.1",
45
47
  typescript: "^5.7.2",
46
48
  vite: "^8.0.0",
49
+ ...UI_TEST_DEV_DEPENDENCIES,
47
50
  },
48
51
  },
49
52
  null,
@@ -141,6 +144,18 @@ export default function App() {
141
144
  `;
142
145
  }
143
146
 
147
+ function appTestTsx(ctx: ShapeContext): string {
148
+ return `import { render, screen } from "@testing-library/react";
149
+ import { expect, test } from "vitest";
150
+ import App from "./App.tsx";
151
+
152
+ test("names the product", () => {
153
+ render(<App />);
154
+ expect(screen.getByRole("heading", { name: "${ctx.name}" })).toBeInTheDocument();
155
+ });
156
+ `;
157
+ }
158
+
144
159
  const INDEX_CSS = `:root {
145
160
  color-scheme: light dark;
146
161
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
@@ -157,9 +172,12 @@ export function frontendFiles(ctx: ShapeContext): GeneratedFile[] {
157
172
  { path: "frontend/package.json", content: packageJson(ctx) },
158
173
  { path: "frontend/tsconfig.json", content: TSCONFIG },
159
174
  { path: "frontend/vite.config.ts", content: VITE_CONFIG_TS },
175
+ { path: "frontend/vitest.config.ts", content: VITEST_CONFIG_TS },
160
176
  { path: "frontend/index.html", content: indexHtml(ctx) },
161
177
  { path: "frontend/src/main.tsx", content: MAIN_TSX },
162
178
  { path: "frontend/src/App.tsx", content: appTsx(ctx) },
179
+ { path: "frontend/src/App.test.tsx", content: appTestTsx(ctx) },
180
+ { path: "frontend/src/test/setup.ts", content: TEST_SETUP_TS },
163
181
  { path: "frontend/src/index.css", content: INDEX_CSS },
164
182
  ];
165
183
  }
@@ -0,0 +1,44 @@
1
+ // The test runner the two screen workspaces (frontend/, dashboard/) share:
2
+ // vitest under jsdom with testing-library, run by the root `test:unit`, so the
3
+ // product's one test command covers the screens too.
4
+
5
+ export const UI_TEST_DEV_DEPENDENCIES = {
6
+ "@testing-library/jest-dom": "^6.9.1",
7
+ "@testing-library/react": "^16.3.2",
8
+ jsdom: "^29.0.1",
9
+ vitest: "^4.1.3",
10
+ };
11
+
12
+ // Like vite.config.ts, kept out of the tsconfig `include`: it imports the
13
+ // bundler.
14
+ export const VITEST_CONFIG_TS = `import { createRequire } from "node:module";
15
+ import { dirname } from "node:path";
16
+ import react from "@vitejs/plugin-react";
17
+ import { defineConfig } from "vitest/config";
18
+
19
+ // One React instance: alias to the pair react-dom loads, so testing-library
20
+ // cannot pull in a second copy (every hook then throws).
21
+ const require = createRequire(import.meta.url);
22
+
23
+ export default defineConfig({
24
+ plugins: [react()],
25
+ resolve: {
26
+ alias: { react: dirname(require.resolve("react")), "react-dom": dirname(require.resolve("react-dom")) },
27
+ dedupe: ["react", "react-dom"],
28
+ },
29
+ test: {
30
+ include: ["src/**/*.test.{ts,tsx}"],
31
+ environment: "jsdom",
32
+ setupFiles: ["./src/test/setup.ts"],
33
+ },
34
+ });
35
+ `;
36
+
37
+ // Without vitest globals, testing-library cannot register its own cleanup, so
38
+ // renders would pile up across tests in one document.
39
+ export const TEST_SETUP_TS = `import "@testing-library/jest-dom/vitest";
40
+ import { cleanup } from "@testing-library/react";
41
+ import { afterEach } from "vitest";
42
+
43
+ afterEach(cleanup);
44
+ `;
@@ -158,6 +158,9 @@ shared core.
158
158
 
159
159
  function claudeTail(ctx: ShapeContext): string {
160
160
  const workspaceNotes = featureNotes(ctx);
161
+ const screens = (["dashboard", "frontend"] as const).filter((f) => ctx.features.has(f));
162
+ const screenTests =
163
+ screens.length === 0 ? "" : `, then the vitest suites of ${screens.map((s) => `\`${s}/\``).join(" and ")} (jsdom)`;
161
164
  return `${workspaceNotes}## Getting norsk-ctl + the dev loop
162
165
 
163
166
  \`norsk-ctl\` (the daemon/CLI) is **not** an npm dep — it's the released binary,
@@ -285,7 +288,7 @@ refreshed \`~/.norsk-ctl/product-templates/<name>/compose.yml\` carries the pins
285
288
  Byte-snapshots (\`tests/unit/workflow-snapshot.test.ts\`) pin the emitted wire
286
289
  bytes; \`UPDATE_SNAPSHOTS=1\` refreshes deliberately.
287
290
 
288
- - \`bun run test\` / \`test:unit\` — shared/backend units + tests/unit scaffolds.
291
+ - \`bun run test\` / \`test:unit\` — shared/backend units + tests/unit scaffolds${screenTests}.
289
292
  - \`bun run test:image\` — image smoke test (needs Docker + \`bun run build:image\`):
290
293
  runs the built container and asserts the surface \`product add\` probes.
291
294
  - \`bun run check:drift\` — the shared-conventions gate; CI runs it plus
@@ -322,7 +325,11 @@ function rootPackageJson(ctx: ShapeContext): string {
322
325
  lint: "biome check .",
323
326
  "lint:fix": "biome check --write .",
324
327
  test: "bun run test:unit",
325
- "test:unit": `bun test shared/src/ backend/src/ ${hasComponents ? "components/src/ " : ""}tests/unit/`,
328
+ "test:unit": [
329
+ `bun test shared/src/ backend/src/ ${hasComponents ? "components/src/ " : ""}tests/unit/`,
330
+ ...(hasDashboard ? ["bun run --cwd dashboard test"] : []),
331
+ ...(hasFrontend ? ["bun run --cwd frontend test"] : []),
332
+ ].join(" && "),
326
333
  "test:image": "bun test tests/image/",
327
334
  "test:smoke": "NORSK_CTL_ENV=test bun test tests/smoke.spec.ts",
328
335
  "check:drift": "bun run node_modules/@norskvideo/ctl-dev-kit/conventions/check-drift.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@norskvideo/ctl-dev-kit",
3
- "version": "0.2.78",
3
+ "version": "0.2.79",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./create-product": "./create-product/create-product.ts",
@@ -44,16 +44,22 @@
44
44
  "@norskvideo/norsk-studio-builder": "1.30.0-2026-10-12-34c09bda",
45
45
  "@norskvideo/norsk-studio-built-ins": "1.30.0-2026-10-12-34c09bda",
46
46
  "@playwright/test": "^1.59.0",
47
+ "@testing-library/jest-dom": "^6.9.1",
48
+ "@testing-library/react": "^16.3.2",
47
49
  "@types/bun": "latest",
48
50
  "@types/cors": "^2.8.17",
49
51
  "@types/express": "5",
50
52
  "@types/node": "^22.10.0",
51
53
  "@types/react": "^19.0.10",
52
54
  "@types/react-dom": "^19.0.3",
55
+ "@vitejs/plugin-react": "^6.0.1",
53
56
  "cors": "^2.8.5",
54
57
  "express": "5",
58
+ "jsdom": "^29.0.1",
55
59
  "react": "^19.0.0",
56
60
  "react-dom": "^19.0.0",
61
+ "vite": "^8.0.0",
62
+ "vitest": "^4.1.3",
57
63
  "yaml": "^2.8.0",
58
64
  "zod": "4.4.3"
59
65
  },