@cosmicdrift/kumiko-testing 0.364.0 → 0.365.1

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.364.0",
3
+ "version": "0.365.1",
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>",
@@ -79,10 +79,10 @@
79
79
  "kumiko-testing": "./bin/kumiko-testing.ts"
80
80
  },
81
81
  "dependencies": {
82
- "@cosmicdrift/kumiko-bundled-features": "0.364.0",
83
- "@cosmicdrift/kumiko-dev-server": "0.364.0",
84
- "@cosmicdrift/kumiko-framework": "0.364.0",
85
- "@cosmicdrift/kumiko-types": "0.364.0",
82
+ "@cosmicdrift/kumiko-bundled-features": "0.365.1",
83
+ "@cosmicdrift/kumiko-dev-server": "0.365.1",
84
+ "@cosmicdrift/kumiko-framework": "0.365.1",
85
+ "@cosmicdrift/kumiko-types": "0.365.1",
86
86
  "zod": "^4.4.3"
87
87
  },
88
88
  "peerDependencies": {
package/src/changes.json CHANGED
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.365.0",
4
+ "type": "fix",
5
+ "title": "E2E webServer env no longer overrides keys from .env.local or .env.<NODE_ENV>",
6
+ "detail": "Keys defined in the app's .env.local or .env.<NODE_ENV> (not only .env) are now left to the server's own dotenv loading instead of being overridden by the template service defaults."
7
+ },
8
+ {
9
+ "version": "0.365.0",
10
+ "type": "improvement",
11
+ "title": "extractMailLink and extractMailLinkToken in kumiko-testing/e2e",
12
+ "detail": "mailLinks, extractMailLink(mail, fragment) and extractMailLinkToken(mail, fragment, param) read href values from a captured HTML mail and decode HTML entities such as &#x3D; and &amp; once, so e2e specs no longer need their own mail helper."
13
+ },
2
14
  {
3
15
  "version": "0.357.0",
4
16
  "type": "improvement",
@@ -13,6 +13,8 @@ export const PLAYWRIGHT_DEMO_ENV = {
13
13
  export const CSRF_COOKIE_NAME = sessionCookieName(CSRF_COOKIE_BASE, { secure: false });
14
14
  export const CSRF_HEADER_NAME = "X-CSRF-Token";
15
15
 
16
+ // Duplicated from kumiko-framework (SEED_MODE_ENV): the Playwright config runs under Node
17
+ // and cannot import the framework barrel.
16
18
  export const SEED_ENABLE_ENV = "KUMIKO_TEST_SEED";
17
19
  export const SEED_TOKEN_ENV = "KUMIKO_TEST_SEED_TOKEN";
18
20
  export const SEED_TOKEN_HEADER = "x-kumiko-test-seed";
@@ -38,11 +38,17 @@ const OPT_IN_SERVICE_ENV_KEYS: readonly string[] = [
38
38
 
39
39
  // Bun never overrides a variable that is already set, so a default placed in
40
40
  // webServer.env would beat the same key in the app's `.env`. Keys the app's
41
- // `.env` defines are therefore left out and stay with the server's own loading.
41
+ // dotenv files define are therefore left out and stay with the server's own loading.
42
42
  function keysDefinedInAppDotenv(): ReadonlySet<string> {
43
- const dotenvPath = join(process.cwd(), ".env");
44
- if (!existsSync(dotenvPath)) return new Set();
45
- return new Set(Object.keys(parseEnv(readFileSync(dotenvPath, "utf-8"))));
43
+ // Same files Bun loads: .env, .env.<NODE_ENV>, .env.local. Only the key set matters, not precedence.
44
+ const nodeEnv = process.env["NODE_ENV"] ?? "development";
45
+ const keys = new Set<string>();
46
+ for (const fileName of [".env", `.env.${nodeEnv}`, ".env.local"]) {
47
+ const dotenvPath = join(process.cwd(), fileName);
48
+ if (!existsSync(dotenvPath)) continue;
49
+ for (const key of Object.keys(parseEnv(readFileSync(dotenvPath, "utf-8")))) keys.add(key);
50
+ }
51
+ return keys;
46
52
  }
47
53
 
48
54
  // Precedence per key: shell/CI environment, then the app's `.env`, then the local-dev default.
package/src/e2e/index.ts CHANGED
@@ -37,6 +37,12 @@ export {
37
37
  resolveE2eWorkers,
38
38
  } from "./define-app-e2e-config";
39
39
  export { type MailCaptureOptions, mailCapture } from "./mail-capture";
40
+ export {
41
+ decodeHtmlAttribute,
42
+ extractMailLink,
43
+ extractMailLinkToken,
44
+ mailLinks,
45
+ } from "./mail-links";
40
46
  export { pinEnglishLocale } from "./pin-english-locale";
41
47
  export { pollForRow, waitForProjection } from "./poll";
42
48
  export {
@@ -0,0 +1,50 @@
1
+ import type { CapturedMail } from "./seed-contract";
2
+
3
+ const NAMED_ENTITIES: Readonly<Record<string, string>> = {
4
+ amp: "&",
5
+ quot: '"',
6
+ apos: "'",
7
+ lt: "<",
8
+ gt: ">",
9
+ };
10
+
11
+ // The mail renderer entity-encodes attribute values (`=` arrives as `&#x3D;`).
12
+ // One pass, so an encoded "&amp;#x3D;" stays the literal text "&#x3D;".
13
+ export function decodeHtmlAttribute(value: string): string {
14
+ return value.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (entity, body: string) => {
15
+ if (body.startsWith("#x") || body.startsWith("#X")) {
16
+ return String.fromCodePoint(Number.parseInt(body.slice(2), 16));
17
+ }
18
+ if (body.startsWith("#")) return String.fromCodePoint(Number.parseInt(body.slice(1), 10));
19
+ return NAMED_ENTITIES[body.toLowerCase()] ?? entity;
20
+ });
21
+ }
22
+
23
+ export function mailLinks(mail: Pick<CapturedMail, "html">): readonly string[] {
24
+ return [...mail.html.matchAll(/href="([^"]+)"/g)].map((match) =>
25
+ decodeHtmlAttribute(match[1] ?? ""),
26
+ );
27
+ }
28
+
29
+ export function extractMailLink(
30
+ mail: Pick<CapturedMail, "html" | "to">,
31
+ pathFragment: string,
32
+ ): string {
33
+ const link = mailLinks(mail).find((href) => href.includes(pathFragment));
34
+ if (link === undefined) {
35
+ throw new Error(`No link containing "${pathFragment}" found in mail to ${mail.to}`);
36
+ }
37
+ return link;
38
+ }
39
+
40
+ export function extractMailLinkToken(
41
+ mail: Pick<CapturedMail, "html" | "to">,
42
+ pathFragment: string,
43
+ param = "token",
44
+ ): string {
45
+ const token = new URL(extractMailLink(mail, pathFragment)).searchParams.get(param);
46
+ if (!token) {
47
+ throw new Error(`Link "${pathFragment}" in mail to ${mail.to} carries no "${param}" parameter`);
48
+ }
49
+ return token;
50
+ }