@cosmicdrift/kumiko-testing 0.381.0 → 0.383.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-testing",
3
- "version": "0.381.0",
3
+ "version": "0.383.0",
4
4
  "description": "Test template for Kumiko apps: per-flow seedTenant, app test stack, canonical test preloads, bunfig generator and integration runner.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -99,9 +99,9 @@
99
99
  "kumiko-testing": "./bin/kumiko-testing.ts"
100
100
  },
101
101
  "dependencies": {
102
- "@cosmicdrift/kumiko-framework": "0.381.0",
103
- "@cosmicdrift/kumiko-guards": "0.381.0",
104
- "@cosmicdrift/kumiko-types": "0.381.0",
102
+ "@cosmicdrift/kumiko-framework": "0.383.0",
103
+ "@cosmicdrift/kumiko-guards": "0.383.0",
104
+ "@cosmicdrift/kumiko-types": "0.383.0",
105
105
  "zod": "^4.4.3"
106
106
  },
107
107
  "peerDependencies": {
@@ -125,8 +125,8 @@
125
125
  }
126
126
  },
127
127
  "devDependencies": {
128
- "@cosmicdrift/kumiko-bundled-features": "0.381.0",
129
- "@cosmicdrift/kumiko-dev-server": "0.381.0",
128
+ "@cosmicdrift/kumiko-bundled-features": "0.383.0",
129
+ "@cosmicdrift/kumiko-dev-server": "0.383.0",
130
130
  "@happy-dom/global-registrator": "^20.11.2",
131
131
  "@playwright/test": "^1.62.1",
132
132
  "@testing-library/dom": "^10.4.1",
package/src/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.382.0",
4
+ "type": "improvement",
5
+ "title": "withEnv and readSseFrames test helpers",
6
+ "detail": "withEnv restores process.env after a callback so parallel no-isolate runs do not leak variables; readSseFrames replaces hand-written SSE stream parsers in tests."
7
+ },
2
8
  {
3
9
  "version": "0.381.0",
4
10
  "type": "improvement",
@@ -431,6 +431,7 @@ function fieldToFixture(name: string, field: FieldDefinition): unknown {
431
431
  case "text": {
432
432
  if (field.format === "email") return `e2e-${name}@example.com`;
433
433
  if (field.format === "url") return "https://example.com";
434
+ if (field.format === "iban") return "DE89370400440532013000";
434
435
  return `e2e ${name}`;
435
436
  }
436
437
  case "longText":
@@ -62,8 +62,10 @@ export {
62
62
  sharedWidgetEntity,
63
63
  sharedWidgetTable,
64
64
  } from "./shared-entities.ts";
65
+ export { readSseFrames, type SseFrame } from "./sse-frames.ts";
65
66
  export { createTestStoredEvent } from "./stored-event-fixture.ts";
66
67
  export { sleep } from "./utils.ts";
67
68
  export { waitFor } from "./wait-for.ts";
69
+ export { withEnv } from "./with-env.ts";
68
70
  export { withoutAmbientTemporal } from "./without-ambient-temporal.ts";
69
71
  export { zipWithEntries } from "./zip-fixture.ts";
@@ -0,0 +1,82 @@
1
+ export type SseFrame = {
2
+ readonly event: string | undefined;
3
+ readonly data: string;
4
+ readonly id: string | undefined;
5
+ };
6
+
7
+ export type ReadSseFramesOptions = {
8
+ readonly until: (frame: SseFrame, all: readonly SseFrame[]) => boolean;
9
+ readonly skipPings?: boolean;
10
+ readonly timeoutMs?: number;
11
+ };
12
+
13
+ const DEFAULT_TIMEOUT_MS = 5000;
14
+
15
+ export async function readSseFrames(
16
+ response: Response,
17
+ options: ReadSseFramesOptions,
18
+ ): Promise<SseFrame[]> {
19
+ if (response.body === null) {
20
+ throw new Error("sse response has no body");
21
+ }
22
+ const skipPings = options.skipPings ?? true;
23
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
24
+ const reader = response.body.getReader();
25
+ const decoder = new TextDecoder();
26
+ const frames: SseFrame[] = [];
27
+ let buffer = "";
28
+ let timer: ReturnType<typeof setTimeout> | undefined;
29
+ const timedOut = new Promise<never>((_, reject) => {
30
+ timer = setTimeout(
31
+ () =>
32
+ reject(new Error(`sse read timed out after ${timeoutMs}ms: ${frames.length} frames read`)),
33
+ timeoutMs,
34
+ );
35
+ });
36
+
37
+ try {
38
+ for (;;) {
39
+ const chunk = await Promise.race([reader.read(), timedOut]);
40
+ if (chunk.done) {
41
+ throw new Error(`sse stream ended before condition: ${frames.length} frames read`);
42
+ }
43
+ buffer += decoder.decode(chunk.value, { stream: true });
44
+ let separator = buffer.indexOf("\n\n");
45
+ while (separator !== -1) {
46
+ const rawFrame = buffer.slice(0, separator);
47
+ buffer = buffer.slice(separator + 2);
48
+ const frame = parseFrame(rawFrame);
49
+ if (frame !== undefined && !(skipPings && frame.event === "ping")) {
50
+ frames.push(frame);
51
+ if (options.until(frame, frames)) {
52
+ return frames;
53
+ }
54
+ }
55
+ separator = buffer.indexOf("\n\n");
56
+ }
57
+ }
58
+ } finally {
59
+ clearTimeout(timer);
60
+ await reader.cancel().catch(() => undefined);
61
+ }
62
+ }
63
+
64
+ function parseFrame(rawFrame: string): SseFrame | undefined {
65
+ let event: string | undefined;
66
+ let id: string | undefined;
67
+ const dataLines: string[] = [];
68
+ for (const line of rawFrame.split("\n")) {
69
+ if (line === "" || line.startsWith(":")) continue;
70
+ const colon = line.indexOf(":");
71
+ const field = colon === -1 ? line : line.slice(0, colon);
72
+ const rawValue = colon === -1 ? "" : line.slice(colon + 1);
73
+ const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
74
+ if (field === "event") event = value;
75
+ else if (field === "id") id = value;
76
+ else if (field === "data") dataLines.push(value);
77
+ }
78
+ if (event === undefined && id === undefined && dataLines.length === 0) {
79
+ return undefined;
80
+ }
81
+ return { event, data: dataLines.join("\n"), id };
82
+ }
@@ -0,0 +1,28 @@
1
+ export async function withEnv<T>(
2
+ overrides: Readonly<Record<string, string | undefined>>,
3
+ run: () => T | Promise<T>,
4
+ ): Promise<T> {
5
+ const previous = new Map<string, string | undefined>();
6
+ for (const key of Object.keys(overrides)) {
7
+ previous.set(key, process.env[key]);
8
+ }
9
+ try {
10
+ for (const [key, value] of Object.entries(overrides)) {
11
+ applyEnvValue(key, value);
12
+ }
13
+ return await run();
14
+ } finally {
15
+ for (const [key, value] of previous) {
16
+ applyEnvValue(key, value);
17
+ }
18
+ }
19
+ }
20
+
21
+ // Assigning undefined would store the string "undefined", so absence must go through delete.
22
+ function applyEnvValue(key: string, value: string | undefined): void {
23
+ if (value === undefined) {
24
+ delete process.env[key];
25
+ } else {
26
+ process.env[key] = value;
27
+ }
28
+ }