@anzusystems/common-admin 2.0.0-rc.10 → 2.0.0-rc.11

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
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "./package.json": "./package.json"
42
42
  },
43
- "version": "2.0.0-rc.10",
43
+ "version": "2.0.0-rc.11",
44
44
  "license": "Apache-2.0",
45
45
  "repository": {
46
46
  "type": "git",
@@ -80,7 +80,7 @@
80
80
  "release:cleanup": "npx -y --allow-git=all github:anzusystems/release-tools#main cleanup"
81
81
  },
82
82
  "devDependencies": {
83
- "@anzusystems/common-admin": "2.0.0-rc.10",
83
+ "@anzusystems/common-admin": "2.0.0-rc.11",
84
84
  "@intlify/unplugin-vue-i18n": "^11.2.8",
85
85
  "@mdi/font": "^7.4.47",
86
86
  "@sentry/vite-plugin": "11.11.0",
@@ -109,13 +109,13 @@
109
109
  "cropperjs": "2.3.1",
110
110
  "dayjs": "1.11.23",
111
111
  "eslint": "^10.14.0",
112
- "eslint-plugin-oxlint": "1.90.0",
112
+ "eslint-plugin-oxlint": "1.92.0",
113
113
  "eslint-plugin-pinia": "^0.4.2",
114
114
  "eslint-plugin-vue": "10.12.0",
115
115
  "eslint-plugin-vuetify": "^2.7.3",
116
116
  "npm-run-all2": "^9.0.4",
117
- "oxfmt": "^0.76.0",
118
- "oxlint": "1.90.0",
117
+ "oxfmt": "^0.77.0",
118
+ "oxlint": "1.92.0",
119
119
  "pinia": "4.1.0",
120
120
  "playwright": "^1.65.0",
121
121
  "postcss": "^8.5.32",
@@ -156,11 +156,11 @@
156
156
  "axios": "^1.21.0",
157
157
  "dayjs": "^1.11.23",
158
158
  "eslint": "^10.14.0",
159
- "eslint-plugin-oxlint": "^1.90.0",
159
+ "eslint-plugin-oxlint": "^1.92.0",
160
160
  "eslint-plugin-pinia": "^0.4.2",
161
161
  "eslint-plugin-vue": "^10.12.0",
162
162
  "eslint-plugin-vuetify": "^2.7.3",
163
- "oxlint": "^1.90.0",
163
+ "oxlint": "^1.92.0",
164
164
  "pinia": "^4.1.0",
165
165
  "playwright": "^1.65.0",
166
166
  "socket.io-client": "^4.8.4",
@@ -0,0 +1,46 @@
1
+ /** The environments an admin's e2e suite runs against. */
2
+ export type E2eEnv = 'local' | 'devel' | 'staging'
3
+
4
+ export interface LoadE2eEnvOptions {
5
+ /** The e2e project's folder, holding `.env.<env>` (usually `__dirname` of `playwright.config.ts`). */
6
+ dir: string
7
+ /** The env when `E2E_ENV` is not set, e.g. `bin/test` without `-e`. */
8
+ defaultEnv: E2eEnv
9
+ /**
10
+ * A file read first, under `.env.<env>`, which overrides it, e.g. `.env.dist` when it holds the local stack's
11
+ * values. Without it, `.env.<env>` is the only file, and variables already set in the shell win.
12
+ */
13
+ base?: string
14
+ }
15
+
16
+ export type LoadE2eEnv = (options: LoadE2eEnvOptions) => E2eEnv
17
+ export type E2eEnvOf = () => E2eEnv
18
+ export type E2eEnvLabel = () => string
19
+ export type IsLocalStack = () => boolean
20
+
21
+ export declare const E2E_ENVS: readonly E2eEnv[]
22
+
23
+ /**
24
+ * Sets `E2E_ENV` (to `defaultEnv` when unset) and loads `<dir>/.env.<env>` into `process.env`. Throws on an env
25
+ * other than `local`, `devel` and `staging`, and when the file of `devel` or `staging` is missing; for `local` a
26
+ * missing file is only a warning. Returns the env.
27
+ */
28
+ export declare const loadE2eEnv: LoadE2eEnv
29
+
30
+ /** The env `loadE2eEnv` set. Throws when it has not run. */
31
+ export declare const e2eEnv: E2eEnvOf
32
+
33
+ /** The env as the suites' titles show it: `LOCAL`, `DEV` or `STG`. */
34
+ export declare const e2eEnvLabel: E2eEnvLabel
35
+
36
+ /** Whether the suite runs against the local Docker stack. */
37
+ export declare const isLocalStack: IsLocalStack
38
+
39
+ declare global {
40
+ namespace NodeJS {
41
+ interface ProcessEnv {
42
+ /** Set by `loadE2eEnv` in `playwright.config.ts`, so a comparison with another name fails the type check. */
43
+ E2E_ENV: E2eEnv
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,78 @@
1
+ // @ts-check
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import { parseEnv } from 'node:util'
5
+
6
+ /**
7
+ * Which environment an admin's e2e suite runs against: one variable, `E2E_ENV`, with the same three names in
8
+ * every admin. `bin/test -e <env>` sets it; left unset, the admin's `playwright.config.ts` picks its default.
9
+ * The env file is read with Node's own parser (`util.parseEnv`), so the package needs no dotenv.
10
+ *
11
+ * @typedef {import('./env.mjs').E2eEnv} E2eEnv
12
+ */
13
+
14
+ /** @type {readonly E2eEnv[]} */
15
+ export const E2E_ENVS = Object.freeze(['local', 'devel', 'staging'])
16
+
17
+ /** @type {Readonly<Record<E2eEnv, string>>} */
18
+ const LABELS = Object.freeze({ local: 'LOCAL', devel: 'DEV', staging: 'STG' })
19
+
20
+ /**
21
+ * @param {unknown} value
22
+ * @returns {value is E2eEnv}
23
+ */
24
+ const isE2eEnv = (value) => typeof value === 'string' && /** @type {readonly string[]} */ (E2E_ENVS).includes(value)
25
+
26
+ /** @param {unknown} value @param {string} source */
27
+ const invalid = (value, source) => new Error(`${source} must be one of ${E2E_ENVS.join(', ')}; got "${String(value)}".`)
28
+
29
+ /**
30
+ * Sets what the file holds in `process.env`. Without `override`, a variable already set (by the shell or an
31
+ * earlier file) is kept, as dotenv does.
32
+ *
33
+ * @param {string} file
34
+ * @param {boolean} override
35
+ */
36
+ const applyEnvFile = (file, override) => {
37
+ const values = parseEnv(fs.readFileSync(file, 'utf8'))
38
+ for (const [key, value] of Object.entries(values)) {
39
+ if (override || process.env[key] === undefined) process.env[key] = value
40
+ }
41
+ }
42
+
43
+ /** @type {import('./env.mjs').LoadE2eEnv} */
44
+ export function loadE2eEnv({ dir, defaultEnv, base }) {
45
+ if (!isE2eEnv(defaultEnv)) throw invalid(defaultEnv, 'The default env')
46
+ const env = process.env.E2E_ENV || defaultEnv
47
+ if (!isE2eEnv(env)) throw invalid(env, 'E2E_ENV')
48
+
49
+ const envFile = path.resolve(dir, `.env.${env}`)
50
+ const exists = fs.existsSync(envFile)
51
+ // A deployed environment run with the wrong (or no) credentials and URLs would report another environment's
52
+ // results under its name. The local stack runs with the defaults of `.env.dist` or the app's own.
53
+ if (!exists && env !== 'local') {
54
+ throw new Error(`Environment file not found: ${envFile}. Copy .env.dist to .env.${env} and fill it in.`)
55
+ }
56
+ process.env.E2E_ENV = env
57
+ if (base) applyEnvFile(path.resolve(dir, base), false)
58
+ if (exists) applyEnvFile(envFile, Boolean(base))
59
+ else if (!base) console.warn(`Environment file not found: ${envFile}. Copy .env.dist to .env.${env} and fill it in.`)
60
+ return env
61
+ }
62
+
63
+ /** @type {import('./env.mjs').E2eEnvOf} */
64
+ export function e2eEnv() {
65
+ const env = process.env.E2E_ENV
66
+ if (!isE2eEnv(env)) {
67
+ throw env
68
+ ? invalid(env, 'E2E_ENV')
69
+ : new Error('E2E_ENV is not set: playwright.config.ts loads it (loadE2eEnv) before any test file.')
70
+ }
71
+ return env
72
+ }
73
+
74
+ /** @type {import('./env.mjs').E2eEnvLabel} */
75
+ export const e2eEnvLabel = () => LABELS[e2eEnv()]
76
+
77
+ /** @type {import('./env.mjs').IsLocalStack} */
78
+ export const isLocalStack = () => e2eEnv() === 'local'
@@ -1,3 +1,5 @@
1
+ export type { E2eEnv, LoadE2eEnvOptions } from './env.mjs'
2
+ export { E2E_ENVS, e2eEnv, e2eEnvLabel, isLocalStack, loadE2eEnv } from './env.mjs'
1
3
  export type {
2
4
  RouteWalkIgnore,
3
5
  RouteWalkOptions,
@@ -6,3 +8,4 @@ export type {
6
8
  RouteWalkTimings,
7
9
  } from './types.mjs'
8
10
  export { DEFAULT_TIMINGS, defineRouteWalk, defineRouteWalkBoot, readDeclaredRoutes } from './routeWalk.mjs'
11
+ export { defineRouteWalkConfig } from './routeWalkConfig.mjs'
package/src/e2e/index.mjs CHANGED
@@ -1 +1,3 @@
1
+ export { E2E_ENVS, e2eEnv, e2eEnvLabel, isLocalStack, loadE2eEnv } from './env.mjs'
1
2
  export { DEFAULT_TIMINGS, defineRouteWalk, defineRouteWalkBoot, readDeclaredRoutes } from './routeWalk.mjs'
3
+ export { defineRouteWalkConfig } from './routeWalkConfig.mjs'
@@ -503,7 +503,8 @@ export function defineRouteWalk({ test: base, expect }, options) {
503
503
  const skip = options.skip ?? {}
504
504
  const noRecord = options.noRecord ?? {}
505
505
  const resolve = options.resolve ?? {}
506
- const ignore = options.ignore ?? []
506
+ // `E2E_ENV` as loadE2eEnv set it; the walk runs without it too (its own tests), as on a deployed environment.
507
+ const ignore = [...(options.ignore ?? []), ...(process.env.E2E_ENV === 'local' ? (options.localIgnore ?? []) : [])]
507
508
  const timings = withDefaults(options.timings)
508
509
 
509
510
  // One logged-in page per worker: logging in for each of hundreds of routes would double the run.
@@ -0,0 +1,16 @@
1
+ import type { PlaywrightTestConfig } from 'playwright/test'
2
+
3
+ export type DefineRouteWalkConfig = (
4
+ base: PlaywrightTestConfig,
5
+ overrides?: PlaywrightTestConfig
6
+ ) => PlaywrightTestConfig
7
+
8
+ /**
9
+ * The route walk's Playwright config over the admin's `playwright.config.ts`: `tests/routes`, 4 workers, no
10
+ * retries, reports in `.report-routes` and `.results-routes` (the suite's `.report` and `.results` are left
11
+ * alone), the boot check before the routes, and, for an http `BASE_URL`, Chromium told to treat that origin as
12
+ * secure. `overrides` go over all of it but the projects, e.g. a `globalSetup` or `globalTimeout` of its own.
13
+ *
14
+ * `export default defineRouteWalkConfig(base)` in `playwright.routes.config.ts`, next to `playwright.config.ts`.
15
+ */
16
+ export declare const defineRouteWalkConfig: DefineRouteWalkConfig
@@ -0,0 +1,52 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * The Playwright config of the route walk (`tests/routes`, run by `bin/test --routes`), made from the admin's own
5
+ * `playwright.config.ts`: the same env, URL and browser, with what the walk needs over it.
6
+ */
7
+
8
+ /** @type {import('./routeWalkConfig.mjs').DefineRouteWalkConfig} */
9
+ export function defineRouteWalkConfig(base, overrides = {}) {
10
+ const { use: ownUse, ...own } = overrides
11
+ // A local stack serves the app over plain http, where the browser withholds what needs a secure context
12
+ // (`crypto.randomUUID` and the like). Deployed, the app is served over https.
13
+ const baseURL = ownUse?.baseURL ?? base.use?.baseURL ?? process.env.BASE_URL
14
+ const origin = baseURL ? new URL(baseURL).origin : ''
15
+ const use = { ...base.use, ...ownUse }
16
+ // A config that already passes the flag (admin-dam's names admin-cms's origin too) keeps its own: Chromium reads
17
+ // only one of them.
18
+ const hasOwnFlag = (use.launchOptions?.args ?? []).some((arg) =>
19
+ arg.startsWith('--unsafely-treat-insecure-origin-as-secure=')
20
+ )
21
+ const secureContextArgs =
22
+ origin.startsWith('http:') && !hasOwnFlag ? [`--unsafely-treat-insecure-origin-as-secure=${origin}`] : []
23
+
24
+ return {
25
+ ...base,
26
+ // Relative paths are resolved against the config file's folder.
27
+ testDir: 'tests/routes',
28
+ // The default run leaves the walk out (`testIgnore`, or projects with folders of their own).
29
+ testIgnore: [],
30
+ // A route is only opened, nothing is clicked or typed, so the routes are spread over workers.
31
+ workers: 4,
32
+ retries: 0,
33
+ // Folders of its own: a run empties its outputDir, and the walk would wipe the last suite run's traces.
34
+ reporter: [['list'], ['html', { outputFolder: '.report-routes', open: 'never' }]],
35
+ outputDir: '.results-routes',
36
+ ...own,
37
+ use: {
38
+ ...use,
39
+ // The headless shell Playwright uses by default ignores the flag; full Chromium in headless mode honours it.
40
+ ...(secureContextArgs.length ? { channel: 'chromium' } : {}),
41
+ launchOptions: {
42
+ ...use.launchOptions,
43
+ args: [...(use.launchOptions?.args ?? []), ...secureContextArgs],
44
+ },
45
+ },
46
+ projects: [
47
+ { name: 'boot', testMatch: /boot\.spec\.ts$/ },
48
+ // An app that does not boot stops the walk here instead of failing each route after its timeout.
49
+ { name: 'routes', testMatch: /routes\.spec\.ts$/, dependencies: ['boot'] },
50
+ ],
51
+ }
52
+ }
@@ -83,6 +83,11 @@ export interface RouteWalkOptions {
83
83
  redirects?: Record<string, RegExp>
84
84
  /** Problems that do not fail a route. */
85
85
  ignore?: RouteWalkIgnore[]
86
+ /**
87
+ * Problems that do not fail a route on the local Docker stack only (`E2E_ENV=local`): what the stack does not
88
+ * run, e.g. a mailer or an analytics service. On devel and staging they are failures.
89
+ */
90
+ localIgnore?: RouteWalkIgnore[]
86
91
  /** The rows of a listing, clicked to find a record when no link to one is found. Default: Vuetify's data table. */
87
92
  rowSelector?: string
88
93
  /**