@trackunit/iris-app-playwright 0.1.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/README.md +230 -0
- package/generators.json +12 -0
- package/package.json +43 -0
- package/src/fixtures/baseTest.d.ts +9 -0
- package/src/fixtures/baseTest.js +12 -0
- package/src/fixtures/describe.d.ts +11 -0
- package/src/fixtures/describe.js +23 -0
- package/src/fixtures/featureFlags.fixture.d.ts +19 -0
- package/src/fixtures/featureFlags.fixture.js +76 -0
- package/src/fixtures/harRecording.fixture.d.ts +12 -0
- package/src/fixtures/harRecording.fixture.js +35 -0
- package/src/fixtures/hydrateStorageState.d.ts +36 -0
- package/src/fixtures/hydrateStorageState.js +43 -0
- package/src/fixtures/irisApp.fixture.d.ts +15 -0
- package/src/fixtures/irisApp.fixture.js +13 -0
- package/src/fixtures/localDevMode.fixture.d.ts +5 -0
- package/src/fixtures/localDevMode.fixture.js +17 -0
- package/src/fixtures/login.fixture.d.ts +24 -0
- package/src/fixtures/login.fixture.js +95 -0
- package/src/fixtures/loginCacheKey.d.ts +25 -0
- package/src/fixtures/loginCacheKey.js +29 -0
- package/src/fixtures/resolveCredentials.d.ts +14 -0
- package/src/fixtures/resolveCredentials.js +49 -0
- package/src/generators/playwright-configuration/files/root/playwright/fixtures/auth.json__tmpl__ +4 -0
- package/src/generators/playwright-configuration/files/root/playwright/support/fixtures.ts__tmpl__ +1 -0
- package/src/generators/playwright-configuration/files/root/playwright/tests/app.spec.ts__tmpl__ +16 -0
- package/src/generators/playwright-configuration/files/root/playwright/tsconfig.json__tmpl__ +15 -0
- package/src/generators/playwright-configuration/files/root/playwright.config.ts__tmpl__ +13 -0
- package/src/generators/playwright-configuration/generator.d.ts +18 -0
- package/src/generators/playwright-configuration/generator.js +77 -0
- package/src/generators/playwright-configuration/schema.d.ts +3 -0
- package/src/generators/playwright-configuration/schema.json +18 -0
- package/src/index.d.ts +15 -0
- package/src/index.js +14 -0
- package/src/plugins/createLogFile.d.ts +24 -0
- package/src/plugins/createLogFile.js +51 -0
- package/src/plugins/defaultPlaywrightConfig.d.ts +68 -0
- package/src/plugins/defaultPlaywrightConfig.js +150 -0
- package/src/plugins/logsReporter.d.ts +55 -0
- package/src/plugins/logsReporter.js +119 -0
- package/src/plugins/nxPreset.d.ts +9 -0
- package/src/plugins/nxPreset.js +17 -0
- package/src/plugins/redactHar.d.ts +17 -0
- package/src/plugins/redactHar.js +138 -0
- package/src/plugins/redactSensitive.d.ts +31 -0
- package/src/plugins/redactSensitive.js +72 -0
- package/src/plugins/redactTrace.d.ts +12 -0
- package/src/plugins/redactTrace.js +48 -0
- package/src/plugins/setupPlugins.d.ts +11 -0
- package/src/plugins/setupPlugins.js +68 -0
- package/src/plugins/writeFileWithPrettier.d.ts +17 -0
- package/src/plugins/writeFileWithPrettier.js +37 -0
- package/src/support.d.ts +2 -0
- package/src/support.js +6 -0
- package/src/utils/Codeowner.d.ts +26 -0
- package/src/utils/Codeowner.js +89 -0
- package/src/utils/fileNameBuilder.d.ts +18 -0
- package/src/utils/fileNameBuilder.js +32 -0
- package/src/utils/fileUpdater.d.ts +16 -0
- package/src/utils/fileUpdater.js +38 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.defaultPlaywrightConfig = void 0;
|
|
4
|
+
exports.findWorkspaceRoot = findWorkspaceRoot;
|
|
5
|
+
const tslib_1 = require("tslib");
|
|
6
|
+
const crypto_1 = tslib_1.__importDefault(require("crypto"));
|
|
7
|
+
const fs_1 = require("fs");
|
|
8
|
+
const path_1 = tslib_1.__importDefault(require("path"));
|
|
9
|
+
const Codeowner_1 = require("../utils/Codeowner");
|
|
10
|
+
/**
|
|
11
|
+
* Finds the NX workspace root by walking up the directory tree looking for nx.json or workspace.json.
|
|
12
|
+
*
|
|
13
|
+
* @param startDir - Starting directory for the search (defaults to current working directory)
|
|
14
|
+
* @returns {string} Absolute path to the workspace root
|
|
15
|
+
* @throws Error if workspace root cannot be found
|
|
16
|
+
*/
|
|
17
|
+
function findWorkspaceRoot(startDir = process.cwd()) {
|
|
18
|
+
let currentDir = startDir;
|
|
19
|
+
while (currentDir !== path_1.default.dirname(currentDir)) {
|
|
20
|
+
if ((0, fs_1.existsSync)(path_1.default.join(currentDir, "nx.json")) || (0, fs_1.existsSync)(path_1.default.join(currentDir, "workspace.json"))) {
|
|
21
|
+
return currentDir;
|
|
22
|
+
}
|
|
23
|
+
currentDir = path_1.default.dirname(currentDir);
|
|
24
|
+
}
|
|
25
|
+
throw new Error("Could not find NX workspace root (nx.json or workspace.json not found)");
|
|
26
|
+
}
|
|
27
|
+
// Memoized at module scope so multiple `defaultPlaywrightConfig()` calls within
|
|
28
|
+
// a single process (Playwright loads the config twice — once for discovery, once
|
|
29
|
+
// for the run) share the same ids. Otherwise the junit reporter would write to
|
|
30
|
+
// two different filenames and `metadata.runId` would diverge between the two
|
|
31
|
+
// loads.
|
|
32
|
+
const runId = crypto_1.default.randomUUID();
|
|
33
|
+
const junitFileSuffix = crypto_1.default.randomUUID();
|
|
34
|
+
/**
|
|
35
|
+
* Default cap when running against a shared, rate-limited environment (dev,
|
|
36
|
+
* stage, prod, feature branches). Picked deliberately: it stays under Okta's
|
|
37
|
+
* `/api/v1/authn` burst threshold (typically ~5/s) for the initial parallel
|
|
38
|
+
* login fan-out, while still letting most suites finish in roughly half the
|
|
39
|
+
* wall-clock of a fully serial run.
|
|
40
|
+
*/
|
|
41
|
+
const SHARED_ENV_WORKER_CAP = 2;
|
|
42
|
+
/**
|
|
43
|
+
* Recognises whether a `BASE_URL` value points at a developer's own machine
|
|
44
|
+
* (Vite dev server, in-cluster preview, etc.) vs a shared, rate-limited
|
|
45
|
+
* environment. We don't try to enumerate every dev pattern — just the few
|
|
46
|
+
* that genuinely don't share an Okta tenant with other workers running
|
|
47
|
+
* elsewhere. Anything else (including unset) is treated conservatively.
|
|
48
|
+
*/
|
|
49
|
+
const isSharedEnvironmentBaseURL = (baseURL) => {
|
|
50
|
+
if (baseURL === undefined || baseURL === "") {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
// Node's URL parser wraps IPv6 hostnames in brackets (e.g. `"[::1]"`).
|
|
55
|
+
// Strip them so the IPv6 loopback check below works regardless.
|
|
56
|
+
const hostname = new URL(baseURL).hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
57
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (hostname.endsWith(".local") || hostname.endsWith(".localhost")) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Malformed URLs slip through as "not shared" — Playwright will fail
|
|
67
|
+
// loudly on its own URL parsing later, and we'd rather not cap workers
|
|
68
|
+
// based on a value we don't understand.
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Creates default Playwright configuration for E2E testing.
|
|
74
|
+
* Returns a partial `PlaywrightTestConfig` that projects can spread and extend.
|
|
75
|
+
*
|
|
76
|
+
* @param options - Optional configuration overrides
|
|
77
|
+
* @returns {Partial<PlaywrightTestConfig>} Playwright test configuration
|
|
78
|
+
*/
|
|
79
|
+
const defaultPlaywrightConfig = (options = {}) => {
|
|
80
|
+
const { configDir, nxRoot: providedNxRoot, outputDirOverride, projectConfig = {}, behaviorConfig = {}, pluginConfig = {}, } = options;
|
|
81
|
+
const callerDir = configDir ?? process.cwd();
|
|
82
|
+
const nxRoot = providedNxRoot ?? findWorkspaceRoot(callerDir);
|
|
83
|
+
const relativePath = path_1.default.relative(nxRoot, callerDir);
|
|
84
|
+
const dotsToNxRoot = relativePath
|
|
85
|
+
.split(path_1.default.sep)
|
|
86
|
+
.map(_ => "..")
|
|
87
|
+
.join("/");
|
|
88
|
+
// Playwright owns its own output-dir env var, decoupled from the
|
|
89
|
+
// Cypress-owned NX_E2E_OUTPUT_DIR. On CI this points at
|
|
90
|
+
// `./dist/playwright/executor-N` so Playwright artifacts land in a
|
|
91
|
+
// first-class `dist/playwright` tree (never inside `dist/cypress`).
|
|
92
|
+
const envOutputDirOverride = process.env.NX_PLAYWRIGHT_OUTPUT_DIR || outputDirOverride;
|
|
93
|
+
const buildOutputPath = (subPath) => {
|
|
94
|
+
if (envOutputDirOverride) {
|
|
95
|
+
return path_1.default.join(dotsToNxRoot, envOutputDirOverride, subPath);
|
|
96
|
+
}
|
|
97
|
+
return `${dotsToNxRoot}/dist/playwright/${relativePath}/${subPath}`;
|
|
98
|
+
};
|
|
99
|
+
const codeowner = (0, Codeowner_1.toShortTeamName)((0, Codeowner_1.getCodeowner)(callerDir, nxRoot));
|
|
100
|
+
// Only set workers / fullyParallel when we have a concrete value. Spreading
|
|
101
|
+
// `undefined` would still create the key on the returned config, which
|
|
102
|
+
// Playwright treats as "the user said `undefined`" — surprising semantics
|
|
103
|
+
// we'd rather avoid by leaving the key absent altogether.
|
|
104
|
+
//
|
|
105
|
+
// `workers` resolution order:
|
|
106
|
+
// 1. Explicit `behaviorConfig.workers` always wins.
|
|
107
|
+
// 2. Otherwise, when running against a shared environment (BASE_URL set
|
|
108
|
+
// and not pointing at localhost) we default to `SHARED_ENV_WORKER_CAP`.
|
|
109
|
+
// The `login` fixture's per-worker cache collapses subsequent Okta
|
|
110
|
+
// calls within a worker, but the first call on each worker still races
|
|
111
|
+
// at suite startup — and Okta's `/api/v1/authn` returns `E0000047`
|
|
112
|
+
// ("rate limit") once you fan out past a handful of parallel logins.
|
|
113
|
+
// Two workers stays comfortably under that threshold while preserving
|
|
114
|
+
// ~2x wall-clock speedup over fully serial execution.
|
|
115
|
+
// 3. Local/feature-branch runs (`BASE_URL` unset or localhost) keep
|
|
116
|
+
// Playwright's default — those environments aren't rate-limited.
|
|
117
|
+
const baseURL = process.env.NX_FEATURE_BRANCH_BASE_URL ?? process.env.BASE_URL;
|
|
118
|
+
const optional = {};
|
|
119
|
+
if (behaviorConfig.workers !== undefined) {
|
|
120
|
+
optional.workers = behaviorConfig.workers;
|
|
121
|
+
}
|
|
122
|
+
else if (isSharedEnvironmentBaseURL(baseURL)) {
|
|
123
|
+
optional.workers = SHARED_ENV_WORKER_CAP;
|
|
124
|
+
}
|
|
125
|
+
if (behaviorConfig.fullyParallel !== undefined) {
|
|
126
|
+
optional.fullyParallel = behaviorConfig.fullyParallel;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
timeout: behaviorConfig.timeout ?? 30000,
|
|
130
|
+
retries: behaviorConfig.retries ?? 3,
|
|
131
|
+
use: {
|
|
132
|
+
baseURL: process.env.NX_FEATURE_BRANCH_BASE_URL ?? process.env.BASE_URL ?? undefined,
|
|
133
|
+
navigationTimeout: behaviorConfig.navigationTimeout ?? 35000,
|
|
134
|
+
actionTimeout: behaviorConfig.actionTimeout ?? 20000,
|
|
135
|
+
screenshot: "only-on-failure",
|
|
136
|
+
video: "retain-on-failure",
|
|
137
|
+
trace: "retain-on-failure",
|
|
138
|
+
},
|
|
139
|
+
outputDir: pluginConfig.outputPath ?? buildOutputPath("test-results"),
|
|
140
|
+
reporter: [["junit", { outputFile: buildOutputPath(`results-${junitFileSuffix}.xml`) }]],
|
|
141
|
+
testDir: projectConfig.testDir ?? "./src/e2e",
|
|
142
|
+
metadata: {
|
|
143
|
+
codeowner,
|
|
144
|
+
runId,
|
|
145
|
+
},
|
|
146
|
+
...optional,
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
exports.defaultPlaywrightConfig = defaultPlaywrightConfig;
|
|
150
|
+
//# sourceMappingURL=defaultPlaywrightConfig.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from "@playwright/test/reporter";
|
|
2
|
+
import type { Formatter } from "./writeFileWithPrettier";
|
|
3
|
+
export interface LogsReporterOptions {
|
|
4
|
+
/** Directory where log files are written on test failure */
|
|
5
|
+
logsFolder: string;
|
|
6
|
+
/** Directory where HAR files are moved on test failure (temp HAR deleted on pass) */
|
|
7
|
+
harsFolder?: string;
|
|
8
|
+
/** Workspace root used by createLogFile to locate .prettierrc */
|
|
9
|
+
nxRoot: string;
|
|
10
|
+
/** Prettier formatter instance */
|
|
11
|
+
formatter: Formatter;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Custom Playwright reporter that writes redacted log files on test failure
|
|
15
|
+
* and manages HAR file cleanup (retain on failure, delete on pass).
|
|
16
|
+
*/
|
|
17
|
+
export declare class LogsReporter implements Reporter {
|
|
18
|
+
private readonly logsFolder;
|
|
19
|
+
private readonly harsFolder;
|
|
20
|
+
private readonly nxRoot;
|
|
21
|
+
private readonly formatter;
|
|
22
|
+
/**
|
|
23
|
+
* Promises of in-flight log file writes from `onTestEnd`. `createLogFile`
|
|
24
|
+
* formats with Prettier (async) and writes via `writeFileSync`; Playwright
|
|
25
|
+
* may finish the run before those formats settle, so we track them here and
|
|
26
|
+
* await them in `onEnd` to surface formatter errors and ensure files exist
|
|
27
|
+
* by the time the process exits.
|
|
28
|
+
*/
|
|
29
|
+
private readonly pendingWrites;
|
|
30
|
+
/** @param options - Reporter configuration options */
|
|
31
|
+
constructor(options: LogsReporterOptions);
|
|
32
|
+
/** Creates the logs folder before the run begins. */
|
|
33
|
+
onBegin(_config: FullConfig, _suite: Suite): void;
|
|
34
|
+
/** Writes a redacted log file on test failure and manages HAR file retention. */
|
|
35
|
+
onTestEnd(test: TestCase, result: TestResult): void;
|
|
36
|
+
/**
|
|
37
|
+
* Awaits in-flight log writes started in `onTestEnd` before the reporter
|
|
38
|
+
* signals completion. Without this, Playwright may exit before Prettier
|
|
39
|
+
* settles, leaving partial or missing log files and swallowing formatter
|
|
40
|
+
* errors.
|
|
41
|
+
*/
|
|
42
|
+
onEnd(_result: FullResult): Promise<void>;
|
|
43
|
+
/** Returns false so Playwright adds its own terminal reporter alongside this one. */
|
|
44
|
+
printsToStdio(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Returns the HAR attachment path for *this specific result*. Looking at
|
|
47
|
+
* `test.results.flatMap(r => r.attachments)` would mix attachments across
|
|
48
|
+
* retries — on a retry, the first attempt's HAR could be retained instead
|
|
49
|
+
* of (or alongside) the current attempt's HAR. Per-result scoping keeps
|
|
50
|
+
* each attempt's artifacts paired with its own log file.
|
|
51
|
+
*/
|
|
52
|
+
private getResultHarPath;
|
|
53
|
+
private getResultTracePath;
|
|
54
|
+
}
|
|
55
|
+
export default LogsReporter;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LogsReporter = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const fileNameBuilder_1 = require("../utils/fileNameBuilder");
|
|
8
|
+
const createLogFile_1 = require("./createLogFile");
|
|
9
|
+
const redactHar_1 = require("./redactHar");
|
|
10
|
+
const redactSensitive_1 = require("./redactSensitive");
|
|
11
|
+
const redactTrace_1 = require("./redactTrace");
|
|
12
|
+
/**
|
|
13
|
+
* Custom Playwright reporter that writes redacted log files on test failure
|
|
14
|
+
* and manages HAR file cleanup (retain on failure, delete on pass).
|
|
15
|
+
*/
|
|
16
|
+
class LogsReporter {
|
|
17
|
+
/** @param options - Reporter configuration options */
|
|
18
|
+
constructor(options) {
|
|
19
|
+
/**
|
|
20
|
+
* Promises of in-flight log file writes from `onTestEnd`. `createLogFile`
|
|
21
|
+
* formats with Prettier (async) and writes via `writeFileSync`; Playwright
|
|
22
|
+
* may finish the run before those formats settle, so we track them here and
|
|
23
|
+
* await them in `onEnd` to surface formatter errors and ensure files exist
|
|
24
|
+
* by the time the process exits.
|
|
25
|
+
*/
|
|
26
|
+
this.pendingWrites = [];
|
|
27
|
+
this.logsFolder = options.logsFolder;
|
|
28
|
+
this.harsFolder = options.harsFolder;
|
|
29
|
+
this.nxRoot = options.nxRoot;
|
|
30
|
+
this.formatter = options.formatter;
|
|
31
|
+
}
|
|
32
|
+
/** Creates the logs folder before the run begins. */
|
|
33
|
+
onBegin(_config, _suite) {
|
|
34
|
+
if (!(0, fs_1.existsSync)(this.logsFolder)) {
|
|
35
|
+
(0, fs_1.mkdirSync)(this.logsFolder, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Writes a redacted log file on test failure and manages HAR file retention. */
|
|
39
|
+
onTestEnd(test, result) {
|
|
40
|
+
const titlePath = test.titlePath().filter(t => t.length > 0);
|
|
41
|
+
const describeTitle = titlePath.slice(0, -1).join(" > ");
|
|
42
|
+
const testTitle = test.title;
|
|
43
|
+
const fullTitle = describeTitle ? `${describeTitle} - ${testTitle}` : testTitle;
|
|
44
|
+
if (result.status === "failed" || result.status === "timedOut") {
|
|
45
|
+
const fileName = (0, fileNameBuilder_1.fileNameBuilder)(fullTitle, "failed", result.retry);
|
|
46
|
+
const logs = result.stdout.concat(result.stderr).map(entry => {
|
|
47
|
+
const message = typeof entry === "string" ? entry : entry.toString();
|
|
48
|
+
return {
|
|
49
|
+
type: "stdout",
|
|
50
|
+
message: (0, redactSensitive_1.redactSensitive)(message),
|
|
51
|
+
severity: result.status === "failed" ? "error" : "warning",
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
this.pendingWrites.push((0, createLogFile_1.createLogFile)(this.nxRoot, this.logsFolder, fileName, logs, this.formatter));
|
|
55
|
+
if (this.harsFolder !== undefined) {
|
|
56
|
+
const tempHarPath = this.getResultHarPath(result);
|
|
57
|
+
if (tempHarPath !== undefined && (0, fs_1.existsSync)(tempHarPath)) {
|
|
58
|
+
if (!(0, fs_1.existsSync)(this.harsFolder)) {
|
|
59
|
+
(0, fs_1.mkdirSync)(this.harsFolder, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
const harFileName = `${(0, fileNameBuilder_1.fileNameBuilder)(fullTitle, "failed", result.retry)}.har`;
|
|
62
|
+
// HARs include Authorization, Cookie, and request/response bodies for
|
|
63
|
+
// authenticated traffic. Redact before moving to the shared artifacts folder.
|
|
64
|
+
(0, redactHar_1.redactHarFile)(tempHarPath, path.join(this.harsFolder, harFileName));
|
|
65
|
+
(0, fs_1.rmSync)(tempHarPath);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Defense in depth for trace artifacts: `login()`'s slow path does
|
|
69
|
+
// `page.goto("/auth/manager-classic#session_token=...")`, and Playwright
|
|
70
|
+
// records the full `goto` URL as the action argument. With
|
|
71
|
+
// `trace: "retain-on-failure"` from `defaultPlaywrightConfig`, a live
|
|
72
|
+
// Okta session token would otherwise persist in the retained trace zip
|
|
73
|
+
// alongside the test artifacts. The fragment is client-side only — it
|
|
74
|
+
// never reaches the HTTP server and HAR/log redaction already covers
|
|
75
|
+
// everything that does — so this pass scrubs the remaining surface.
|
|
76
|
+
const tracePath = this.getResultTracePath(result);
|
|
77
|
+
if (tracePath !== undefined && (0, fs_1.existsSync)(tracePath)) {
|
|
78
|
+
this.pendingWrites.push((0, redactTrace_1.redactTraceFile)(tracePath));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else if (this.harsFolder !== undefined) {
|
|
82
|
+
const tempHarPath = this.getResultHarPath(result);
|
|
83
|
+
if (tempHarPath !== undefined && (0, fs_1.existsSync)(tempHarPath)) {
|
|
84
|
+
(0, fs_1.rmSync)(tempHarPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Awaits in-flight log writes started in `onTestEnd` before the reporter
|
|
90
|
+
* signals completion. Without this, Playwright may exit before Prettier
|
|
91
|
+
* settles, leaving partial or missing log files and swallowing formatter
|
|
92
|
+
* errors.
|
|
93
|
+
*/
|
|
94
|
+
async onEnd(_result) {
|
|
95
|
+
await Promise.all(this.pendingWrites);
|
|
96
|
+
}
|
|
97
|
+
/** Returns false so Playwright adds its own terminal reporter alongside this one. */
|
|
98
|
+
printsToStdio() {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Returns the HAR attachment path for *this specific result*. Looking at
|
|
103
|
+
* `test.results.flatMap(r => r.attachments)` would mix attachments across
|
|
104
|
+
* retries — on a retry, the first attempt's HAR could be retained instead
|
|
105
|
+
* of (or alongside) the current attempt's HAR. Per-result scoping keeps
|
|
106
|
+
* each attempt's artifacts paired with its own log file.
|
|
107
|
+
*/
|
|
108
|
+
getResultHarPath(result) {
|
|
109
|
+
const attachment = result.attachments.find(a => a.name === "har" && a.path !== undefined);
|
|
110
|
+
return attachment?.path;
|
|
111
|
+
}
|
|
112
|
+
getResultTracePath(result) {
|
|
113
|
+
const attachment = result.attachments.find(a => a.name === "trace" && a.path !== undefined);
|
|
114
|
+
return attachment?.path;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
exports.LogsReporter = LogsReporter;
|
|
118
|
+
exports.default = LogsReporter;
|
|
119
|
+
//# sourceMappingURL=logsReporter.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { nxE2EPreset } from "@nx/playwright/preset";
|
|
2
|
+
/**
|
|
3
|
+
* Creates the standard NX E2E preset configuration for Playwright.
|
|
4
|
+
* Wraps the `@nx/playwright` preset with Trackunit defaults.
|
|
5
|
+
*
|
|
6
|
+
* @param filename - Pass `__filename` from the calling `playwright.config.ts`
|
|
7
|
+
* @returns {ReturnType<typeof nxE2EPreset>} NX Playwright preset configuration
|
|
8
|
+
*/
|
|
9
|
+
export declare function createNxPreset(filename: string): ReturnType<typeof nxE2EPreset>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createNxPreset = createNxPreset;
|
|
4
|
+
const preset_1 = require("@nx/playwright/preset");
|
|
5
|
+
/**
|
|
6
|
+
* Creates the standard NX E2E preset configuration for Playwright.
|
|
7
|
+
* Wraps the `@nx/playwright` preset with Trackunit defaults.
|
|
8
|
+
*
|
|
9
|
+
* @param filename - Pass `__filename` from the calling `playwright.config.ts`
|
|
10
|
+
* @returns {ReturnType<typeof nxE2EPreset>} NX Playwright preset configuration
|
|
11
|
+
*/
|
|
12
|
+
function createNxPreset(filename) {
|
|
13
|
+
return (0, preset_1.nxE2EPreset)(filename, {
|
|
14
|
+
testDir: "./src",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=nxPreset.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strips sensitive headers, cookies, query parameters, and bodies from a HAR document.
|
|
3
|
+
* Returns a new object — the input is not mutated.
|
|
4
|
+
*
|
|
5
|
+
* Redacts:
|
|
6
|
+
* - Headers matching SENSITIVE_HEADER_NAMES (Authorization, Cookie, Set-Cookie, etc.)
|
|
7
|
+
* - All cookie values (request & response)
|
|
8
|
+
* - Query-string parameters with sensitive names
|
|
9
|
+
* - Form params with sensitive names
|
|
10
|
+
* - JSON-like text in request/response bodies via redactSensitive
|
|
11
|
+
*/
|
|
12
|
+
export declare const redactHar: (har: unknown) => unknown;
|
|
13
|
+
/**
|
|
14
|
+
* Reads a HAR file, redacts sensitive content, and writes it to a new path.
|
|
15
|
+
* The source file is unchanged. Returns the bytes written.
|
|
16
|
+
*/
|
|
17
|
+
export declare const redactHarFile: (sourcePath: string, destPath: string) => void;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.redactHarFile = exports.redactHar = void 0;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const redactSensitive_1 = require("./redactSensitive");
|
|
6
|
+
const SENSITIVE_HEADER_NAMES = new Set([...redactSensitive_1.SENSITIVE_KEYS, "authorization", "cookie", "set-cookie", "x-api-key", "x-auth-token", "proxy-authorization"].map(n => n.toLowerCase()));
|
|
7
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
const redactHeaderArray = (headers) => {
|
|
9
|
+
if (!Array.isArray(headers))
|
|
10
|
+
return headers;
|
|
11
|
+
return headers.map((h) => {
|
|
12
|
+
if (!isRecord(h))
|
|
13
|
+
return h;
|
|
14
|
+
const name = typeof h.name === "string" ? h.name : "";
|
|
15
|
+
if (SENSITIVE_HEADER_NAMES.has(name.toLowerCase())) {
|
|
16
|
+
return { ...h, value: "***" };
|
|
17
|
+
}
|
|
18
|
+
return h;
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
const redactCookieArray = (cookies) => {
|
|
22
|
+
if (!Array.isArray(cookies))
|
|
23
|
+
return cookies;
|
|
24
|
+
return cookies.map((c) => (isRecord(c) ? { ...c, value: "***" } : c));
|
|
25
|
+
};
|
|
26
|
+
const redactQueryString = (qs) => {
|
|
27
|
+
if (!Array.isArray(qs))
|
|
28
|
+
return qs;
|
|
29
|
+
return qs.map(entry => {
|
|
30
|
+
if (!isRecord(entry))
|
|
31
|
+
return entry;
|
|
32
|
+
const name = typeof entry.name === "string" ? entry.name : "";
|
|
33
|
+
if (SENSITIVE_HEADER_NAMES.has(name.toLowerCase())) {
|
|
34
|
+
return { ...entry, value: "***" };
|
|
35
|
+
}
|
|
36
|
+
return entry;
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
const redactStringField = (value) => (typeof value === "string" ? (0, redactSensitive_1.redactSensitive)(value) : value);
|
|
40
|
+
const redactPostData = (postData) => {
|
|
41
|
+
if (!isRecord(postData))
|
|
42
|
+
return postData;
|
|
43
|
+
const next = { ...postData };
|
|
44
|
+
if (typeof postData.text === "string") {
|
|
45
|
+
next.text = (0, redactSensitive_1.redactSensitive)(postData.text);
|
|
46
|
+
}
|
|
47
|
+
if (Array.isArray(postData.params)) {
|
|
48
|
+
next.params = postData.params.map(p => {
|
|
49
|
+
if (!isRecord(p))
|
|
50
|
+
return p;
|
|
51
|
+
const name = typeof p.name === "string" ? p.name : "";
|
|
52
|
+
if (SENSITIVE_HEADER_NAMES.has(name.toLowerCase())) {
|
|
53
|
+
return { ...p, value: "***" };
|
|
54
|
+
}
|
|
55
|
+
return p;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return next;
|
|
59
|
+
};
|
|
60
|
+
const redactContent = (content) => {
|
|
61
|
+
if (!isRecord(content))
|
|
62
|
+
return content;
|
|
63
|
+
if (typeof content.text === "string") {
|
|
64
|
+
return { ...content, text: (0, redactSensitive_1.redactSensitive)(content.text) };
|
|
65
|
+
}
|
|
66
|
+
return content;
|
|
67
|
+
};
|
|
68
|
+
const redactRequest = (request) => {
|
|
69
|
+
if (!isRecord(request))
|
|
70
|
+
return request;
|
|
71
|
+
const next = { ...request };
|
|
72
|
+
next.url = redactStringField(request.url);
|
|
73
|
+
next.headers = redactHeaderArray(request.headers);
|
|
74
|
+
next.cookies = redactCookieArray(request.cookies);
|
|
75
|
+
next.queryString = redactQueryString(request.queryString);
|
|
76
|
+
next.postData = redactPostData(request.postData);
|
|
77
|
+
return next;
|
|
78
|
+
};
|
|
79
|
+
const redactResponse = (response) => {
|
|
80
|
+
if (!isRecord(response))
|
|
81
|
+
return response;
|
|
82
|
+
const next = { ...response };
|
|
83
|
+
next.url = redactStringField(response.url);
|
|
84
|
+
next.headers = redactHeaderArray(response.headers);
|
|
85
|
+
next.cookies = redactCookieArray(response.cookies);
|
|
86
|
+
next.content = redactContent(response.content);
|
|
87
|
+
return next;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Strips sensitive headers, cookies, query parameters, and bodies from a HAR document.
|
|
91
|
+
* Returns a new object — the input is not mutated.
|
|
92
|
+
*
|
|
93
|
+
* Redacts:
|
|
94
|
+
* - Headers matching SENSITIVE_HEADER_NAMES (Authorization, Cookie, Set-Cookie, etc.)
|
|
95
|
+
* - All cookie values (request & response)
|
|
96
|
+
* - Query-string parameters with sensitive names
|
|
97
|
+
* - Form params with sensitive names
|
|
98
|
+
* - JSON-like text in request/response bodies via redactSensitive
|
|
99
|
+
*/
|
|
100
|
+
const redactHar = (har) => {
|
|
101
|
+
if (!isRecord(har))
|
|
102
|
+
return har;
|
|
103
|
+
const log = har.log;
|
|
104
|
+
if (!isRecord(log))
|
|
105
|
+
return har;
|
|
106
|
+
const entries = log.entries;
|
|
107
|
+
if (!Array.isArray(entries))
|
|
108
|
+
return har;
|
|
109
|
+
const redactedEntries = entries.map(entry => {
|
|
110
|
+
if (!isRecord(entry))
|
|
111
|
+
return entry;
|
|
112
|
+
return {
|
|
113
|
+
...entry,
|
|
114
|
+
request: redactRequest(entry.request),
|
|
115
|
+
response: redactResponse(entry.response),
|
|
116
|
+
};
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
...har,
|
|
120
|
+
log: {
|
|
121
|
+
...log,
|
|
122
|
+
entries: redactedEntries,
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
exports.redactHar = redactHar;
|
|
127
|
+
/**
|
|
128
|
+
* Reads a HAR file, redacts sensitive content, and writes it to a new path.
|
|
129
|
+
* The source file is unchanged. Returns the bytes written.
|
|
130
|
+
*/
|
|
131
|
+
const redactHarFile = (sourcePath, destPath) => {
|
|
132
|
+
const raw = (0, fs_1.readFileSync)(sourcePath, "utf-8");
|
|
133
|
+
const parsed = JSON.parse(raw);
|
|
134
|
+
const redacted = (0, exports.redactHar)(parsed);
|
|
135
|
+
(0, fs_1.writeFileSync)(destPath, JSON.stringify(redacted));
|
|
136
|
+
};
|
|
137
|
+
exports.redactHarFile = redactHarFile;
|
|
138
|
+
//# sourceMappingURL=redactHar.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keys whose values must be redacted when they appear as JSON properties
|
|
3
|
+
* (`"key": "value"`) or URL-encoded params (`key=value`). Both camelCase and
|
|
4
|
+
* snake_case variants are listed because Okta uses `session_token` in its
|
|
5
|
+
* redirect URL while the rest of the codebase uses `sessionToken`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SENSITIVE_KEYS: string[];
|
|
8
|
+
/**
|
|
9
|
+
* HTTP header names whose values must be redacted when they appear in
|
|
10
|
+
* `Header-Name: value` form (e.g. log lines printed by HTTP clients, request
|
|
11
|
+
* dumps in test output). Header redaction is line-scoped because header
|
|
12
|
+
* values can contain `=` and `:` characters that would otherwise confuse the
|
|
13
|
+
* URL/JSON patterns.
|
|
14
|
+
*/
|
|
15
|
+
export declare const SENSITIVE_HEADERS: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Redacts sensitive key-value pairs from a string in JSON, escaped-JSON,
|
|
18
|
+
* URL-encoded, and `Header: value` formats.
|
|
19
|
+
*
|
|
20
|
+
* Replaces:
|
|
21
|
+
* - `"key": "<anything>"` → `"key": "***"` (case-insensitive)
|
|
22
|
+
* - `\"key\": \"<anything>\"` → `\"key\": \"***\"` (case-insensitive — JSON
|
|
23
|
+
* string embedded inside another JSON string, e.g. a request body field in
|
|
24
|
+
* a Playwright trace entry)
|
|
25
|
+
* - `key=<anything>` → `key=***` (URL-encoded, value terminates at `&`/whitespace/`"`)
|
|
26
|
+
* - `Header-Name: <rest-of-line>` → `Header-Name: ***` (case-insensitive, line-scoped)
|
|
27
|
+
*
|
|
28
|
+
* @param message - The string to redact sensitive values from
|
|
29
|
+
* @returns {string} The string with sensitive values replaced by `***`
|
|
30
|
+
*/
|
|
31
|
+
export declare const redactSensitive: (message: string) => string;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.redactSensitive = exports.SENSITIVE_HEADERS = exports.SENSITIVE_KEYS = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Keys whose values must be redacted when they appear as JSON properties
|
|
6
|
+
* (`"key": "value"`) or URL-encoded params (`key=value`). Both camelCase and
|
|
7
|
+
* snake_case variants are listed because Okta uses `session_token` in its
|
|
8
|
+
* redirect URL while the rest of the codebase uses `sessionToken`.
|
|
9
|
+
*/
|
|
10
|
+
exports.SENSITIVE_KEYS = [
|
|
11
|
+
"password",
|
|
12
|
+
"sessionToken",
|
|
13
|
+
"session_token",
|
|
14
|
+
"accessToken",
|
|
15
|
+
"access_token",
|
|
16
|
+
"refreshToken",
|
|
17
|
+
"refresh_token",
|
|
18
|
+
"idToken",
|
|
19
|
+
"id_token",
|
|
20
|
+
"authorization",
|
|
21
|
+
"apiKey",
|
|
22
|
+
"api_key",
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* HTTP header names whose values must be redacted when they appear in
|
|
26
|
+
* `Header-Name: value` form (e.g. log lines printed by HTTP clients, request
|
|
27
|
+
* dumps in test output). Header redaction is line-scoped because header
|
|
28
|
+
* values can contain `=` and `:` characters that would otherwise confuse the
|
|
29
|
+
* URL/JSON patterns.
|
|
30
|
+
*/
|
|
31
|
+
exports.SENSITIVE_HEADERS = [
|
|
32
|
+
"authorization",
|
|
33
|
+
"cookie",
|
|
34
|
+
"set-cookie",
|
|
35
|
+
"x-api-key",
|
|
36
|
+
"x-auth-token",
|
|
37
|
+
"proxy-authorization",
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Redacts sensitive key-value pairs from a string in JSON, escaped-JSON,
|
|
41
|
+
* URL-encoded, and `Header: value` formats.
|
|
42
|
+
*
|
|
43
|
+
* Replaces:
|
|
44
|
+
* - `"key": "<anything>"` → `"key": "***"` (case-insensitive)
|
|
45
|
+
* - `\"key\": \"<anything>\"` → `\"key\": \"***\"` (case-insensitive — JSON
|
|
46
|
+
* string embedded inside another JSON string, e.g. a request body field in
|
|
47
|
+
* a Playwright trace entry)
|
|
48
|
+
* - `key=<anything>` → `key=***` (URL-encoded, value terminates at `&`/whitespace/`"`)
|
|
49
|
+
* - `Header-Name: <rest-of-line>` → `Header-Name: ***` (case-insensitive, line-scoped)
|
|
50
|
+
*
|
|
51
|
+
* @param message - The string to redact sensitive values from
|
|
52
|
+
* @returns {string} The string with sensitive values replaced by `***`
|
|
53
|
+
*/
|
|
54
|
+
const redactSensitive = (message) => {
|
|
55
|
+
let redacted = message;
|
|
56
|
+
for (const key of exports.SENSITIVE_KEYS) {
|
|
57
|
+
// Direct JSON: "key": "value"
|
|
58
|
+
redacted = redacted.replace(new RegExp(`("${key}"\\s*:\\s*)"[^"]*"`, "gi"), `$1"***"`);
|
|
59
|
+
// Escaped JSON (one level): \"key\": \"value\" — appears when a JSON
|
|
60
|
+
// object is serialized as a string field of another JSON object, which
|
|
61
|
+
// is exactly what Playwright traces store for request/response bodies.
|
|
62
|
+
redacted = redacted.replace(new RegExp(`(\\\\"${key}\\\\"\\s*:\\s*)\\\\"[^"\\\\]*\\\\"`, "gi"), `$1\\"***\\"`);
|
|
63
|
+
// URL-encoded: key=value (value terminates at &, whitespace, or quote)
|
|
64
|
+
redacted = redacted.replace(new RegExp(`(${key}=)[^&\\s"]+`, "gi"), "$1***");
|
|
65
|
+
}
|
|
66
|
+
for (const header of exports.SENSITIVE_HEADERS) {
|
|
67
|
+
redacted = redacted.replace(new RegExp(`(^|[\\r\\n])(${header}\\s*:\\s*)[^\\r\\n]*`, "gim"), "$1$2***");
|
|
68
|
+
}
|
|
69
|
+
return redacted;
|
|
70
|
+
};
|
|
71
|
+
exports.redactSensitive = redactSensitive;
|
|
72
|
+
//# sourceMappingURL=redactSensitive.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrites a Playwright trace.zip in place with sensitive values stripped from
|
|
3
|
+
* the action/network metadata. The `login()` fixture issues a `page.goto`
|
|
4
|
+
* against `/auth/manager-classic#session_token=...`; Playwright records the
|
|
5
|
+
* full URL as the action argument, so without this pass a retained trace zip
|
|
6
|
+
* would carry a live Okta session token alongside the test artifacts.
|
|
7
|
+
*
|
|
8
|
+
* The fragment is client-side only — it never reaches the HTTP server, and
|
|
9
|
+
* HAR/log redaction already covers everything that does — so this pass is the
|
|
10
|
+
* defense-in-depth layer that scrubs the remaining surface.
|
|
11
|
+
*/
|
|
12
|
+
export declare const redactTraceFile: (filePath: string) => Promise<void>;
|