@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 +7 -7
- package/src/e2e/env.d.mts +46 -0
- package/src/e2e/env.mjs +78 -0
- package/src/e2e/index.d.mts +3 -0
- package/src/e2e/index.mjs +2 -0
- package/src/e2e/routeWalk.mjs +2 -1
- package/src/e2e/routeWalkConfig.d.mts +16 -0
- package/src/e2e/routeWalkConfig.mjs +52 -0
- package/src/e2e/types.d.mts +5 -0
package/package.json
CHANGED
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"./package.json": "./package.json"
|
|
42
42
|
},
|
|
43
|
-
"version": "2.0.0-rc.
|
|
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.
|
|
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.
|
|
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.
|
|
118
|
-
"oxlint": "1.
|
|
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.
|
|
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.
|
|
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
|
+
}
|
package/src/e2e/env.mjs
ADDED
|
@@ -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'
|
package/src/e2e/index.d.mts
CHANGED
|
@@ -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
package/src/e2e/routeWalk.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/e2e/types.d.mts
CHANGED
|
@@ -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
|
/**
|