@capillarytech/cap-ui-utils 3.2.0-beta.0 → 3.2.0-beta.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/e2e/playwright/debugRecorder.js +167 -0
- package/package.json +6 -16
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Playwright FULL_DEBUG_MODE recorders — phase-2 parity with the WDIO debug bundle.
|
|
4
|
+
*
|
|
5
|
+
* Reproduces, engine-natively, the two artifact sets the WDIO
|
|
6
|
+
* screenshotRecorderUtil + requestRecorderService produce, at the SAME paths so
|
|
7
|
+
* the zip-compare tooling is unchanged:
|
|
8
|
+
*
|
|
9
|
+
* reports/debug/screenshots/<module>/<test>/NNN_click.png (+ steps.json)
|
|
10
|
+
* reports/debug/request-payloads/<module>/<test>.json
|
|
11
|
+
*
|
|
12
|
+
* Gated by the shared debugMode util (FULL_DEBUG_MODE=true [+ optional
|
|
13
|
+
* DEBUG_MODULES]); a no-op otherwise, so zero overhead when off.
|
|
14
|
+
*
|
|
15
|
+
* import { installDebugRecorders } from '@capillarytech/cap-ui-utils/e2e/playwright/debugRecorder';
|
|
16
|
+
* const rec = await installDebugRecorders(page, { module, testTitle, baseURL });
|
|
17
|
+
* ... run test ...
|
|
18
|
+
* await rec.flush(); // in fixture teardown
|
|
19
|
+
*
|
|
20
|
+
* Notes on WDIO parity:
|
|
21
|
+
* - screenshots: WDIO captured after every `click` command; we hook real DOM
|
|
22
|
+
* clicks (capture phase) so programmatic PW clicks count too, then settle +
|
|
23
|
+
* full-page screenshot, dropping byte-identical duplicates (same as WDIO).
|
|
24
|
+
* - request payloads: WDIO recorded POST/PATCH/PUT/DELETE to the app host via CDP;
|
|
25
|
+
* we use page.on('request') (no CDP needed). Initiator JS stack isn't available
|
|
26
|
+
* via the PW request API, so `initiator` is null (only field that differs).
|
|
27
|
+
*/
|
|
28
|
+
const fs = require("fs");
|
|
29
|
+
const path = require("path");
|
|
30
|
+
const debugMode = require("../utils/debugModeUtil").default;
|
|
31
|
+
|
|
32
|
+
const RECORDED_METHODS = ["POST", "PATCH", "PUT", "DELETE"];
|
|
33
|
+
const SETTLE_MS = Number(process.env.FULL_DEBUG_SCREENSHOT_DELAY_MS) || 500;
|
|
34
|
+
const CAPTURE_TIMEOUT_MS = Number(process.env.FULL_DEBUG_SCREENSHOT_TIMEOUT_MS) || 8000;
|
|
35
|
+
|
|
36
|
+
function safeName(s) {
|
|
37
|
+
// Drop the leading suite tag (e.g. "@smoke ") so the folder matches the WDIO
|
|
38
|
+
// it()-title naming (smoke_garudaUI_analytics_KPI_visibility).
|
|
39
|
+
return String(s || "unknown_test").replace(/^@\w+\s+/, "").replace(/[^\w.-]+/g, "_");
|
|
40
|
+
}
|
|
41
|
+
function hostOf(u) {
|
|
42
|
+
try { return new URL(u).host; } catch (_) { return ""; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class PwDebugRecorder {
|
|
46
|
+
constructor(page, opts = {}) {
|
|
47
|
+
this.page = page;
|
|
48
|
+
this.enabled = debugMode.isCaptureEnabled();
|
|
49
|
+
this.module = opts.module || process.env.module || "unknown";
|
|
50
|
+
this.testTitle = safeName(opts.testTitle);
|
|
51
|
+
this.baseHost = hostOf(opts.baseURL || process.env.INTOUCH_URL || "");
|
|
52
|
+
this.requests = [];
|
|
53
|
+
this.steps = [];
|
|
54
|
+
this.counter = 0;
|
|
55
|
+
this.lastImage = null;
|
|
56
|
+
this.chain = Promise.resolve();
|
|
57
|
+
const root = path.join(process.cwd(), "reports", "debug");
|
|
58
|
+
this.ssDir = path.join(root, "screenshots", this.module, this.testTitle);
|
|
59
|
+
this.rpDir = path.join(root, "request-payloads", this.module);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async install() {
|
|
63
|
+
if (!this.enabled) return this;
|
|
64
|
+
|
|
65
|
+
// --- request payloads (POST/PATCH/PUT/DELETE to the app host) ---
|
|
66
|
+
this.page.on("request", (req) => {
|
|
67
|
+
try {
|
|
68
|
+
const method = req.method();
|
|
69
|
+
if (!RECORDED_METHODS.includes(method)) return;
|
|
70
|
+
const url = req.url();
|
|
71
|
+
if (this.baseHost && !url.includes(this.baseHost)) return;
|
|
72
|
+
this.requests.push({
|
|
73
|
+
method,
|
|
74
|
+
url,
|
|
75
|
+
timestamp: new Date().toISOString(),
|
|
76
|
+
postData: req.postData() || null,
|
|
77
|
+
headers: req.headers(),
|
|
78
|
+
resourceType: req.resourceType(),
|
|
79
|
+
initiator: null, // JS initiator stack not exposed by the PW request API
|
|
80
|
+
});
|
|
81
|
+
} catch (_) { /* never let recording break the run */ }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// --- screenshot after every click ---
|
|
85
|
+
await this.page.exposeFunction("__capOnClick", (info) => {
|
|
86
|
+
// Serialize captures so rapid clicks don't overlap screenshots.
|
|
87
|
+
this.chain = this.chain.then(() => this._capture(info)).catch(() => {});
|
|
88
|
+
});
|
|
89
|
+
const inject = () => {
|
|
90
|
+
// eslint-disable-next-line no-undef
|
|
91
|
+
if (window.__capClickHooked) return;
|
|
92
|
+
// eslint-disable-next-line no-undef
|
|
93
|
+
window.__capClickHooked = true;
|
|
94
|
+
// eslint-disable-next-line no-undef
|
|
95
|
+
document.addEventListener(
|
|
96
|
+
"click",
|
|
97
|
+
(e) => {
|
|
98
|
+
try {
|
|
99
|
+
const t = e.target;
|
|
100
|
+
// eslint-disable-next-line no-undef
|
|
101
|
+
window.__capOnClick &&
|
|
102
|
+
window.__capOnClick({
|
|
103
|
+
tag: t && t.tagName,
|
|
104
|
+
text: ((t && t.innerText) || "").trim().slice(0, 40),
|
|
105
|
+
});
|
|
106
|
+
} catch (_) { /* ignore */ }
|
|
107
|
+
},
|
|
108
|
+
true // capture phase — fires even if a handler stops propagation
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
await this.page.addInitScript(inject); // future navigations / SPA reloads
|
|
112
|
+
await this.page.evaluate(inject).catch(() => {}); // the already-loaded document
|
|
113
|
+
return this;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async _capture(info) {
|
|
117
|
+
const clickTime = new Date().toISOString();
|
|
118
|
+
await this.page.waitForTimeout(SETTLE_MS).catch(() => {}); // let the click settle (WDIO parity)
|
|
119
|
+
let buf;
|
|
120
|
+
try {
|
|
121
|
+
buf = await this.page.screenshot({ fullPage: true, timeout: CAPTURE_TIMEOUT_MS });
|
|
122
|
+
} catch (_) {
|
|
123
|
+
return; // a mid-navigation screenshot can fail; never block the run
|
|
124
|
+
}
|
|
125
|
+
if (this.lastImage && buf.equals(this.lastImage)) return; // drop byte-identical duplicate
|
|
126
|
+
this.lastImage = buf;
|
|
127
|
+
this.counter += 1;
|
|
128
|
+
const file = String(this.counter).padStart(3, "0") + "_click.png";
|
|
129
|
+
try {
|
|
130
|
+
fs.mkdirSync(this.ssDir, { recursive: true });
|
|
131
|
+
fs.writeFileSync(path.join(this.ssDir, file), buf);
|
|
132
|
+
this.steps.push({ step: this.counter, file, command: "click", clickTime, target: (info && info.tag) || null });
|
|
133
|
+
} catch (_) { /* ignore */ }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Drain pending screenshots, then write steps.json + the request-payloads file. */
|
|
137
|
+
async flush() {
|
|
138
|
+
if (!this.enabled) return;
|
|
139
|
+
await this.chain.catch(() => {});
|
|
140
|
+
try {
|
|
141
|
+
if (this.steps.length) {
|
|
142
|
+
fs.mkdirSync(this.ssDir, { recursive: true });
|
|
143
|
+
fs.writeFileSync(path.join(this.ssDir, "steps.json"), JSON.stringify(this.steps, null, 2));
|
|
144
|
+
}
|
|
145
|
+
} catch (_) { /* ignore */ }
|
|
146
|
+
try {
|
|
147
|
+
fs.mkdirSync(this.rpDir, { recursive: true });
|
|
148
|
+
const payload = {
|
|
149
|
+
test: this.testTitle,
|
|
150
|
+
module: this.module,
|
|
151
|
+
recordedAt: new Date().toISOString(),
|
|
152
|
+
requestCount: this.requests.length,
|
|
153
|
+
requests: this.requests,
|
|
154
|
+
};
|
|
155
|
+
fs.writeFileSync(path.join(this.rpDir, this.testTitle + ".json"), JSON.stringify(payload, null, 2));
|
|
156
|
+
} catch (_) { /* ignore */ }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function installDebugRecorders(page, opts = {}) {
|
|
161
|
+
const rec = new PwDebugRecorder(page, opts);
|
|
162
|
+
await rec.install();
|
|
163
|
+
return rec;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { installDebugRecorders, PwDebugRecorder };
|
|
167
|
+
module.exports.default = { installDebugRecorders };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capillarytech/cap-ui-utils",
|
|
3
|
-
"version": "3.2.0-beta.
|
|
3
|
+
"version": "3.2.0-beta.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -30,20 +30,10 @@
|
|
|
30
30
|
"nanoid": ">=3"
|
|
31
31
|
},
|
|
32
32
|
"peerDependenciesMeta": {
|
|
33
|
-
"webdriverio": {
|
|
34
|
-
|
|
35
|
-
},
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
},
|
|
39
|
-
"axios": {
|
|
40
|
-
"optional": true
|
|
41
|
-
},
|
|
42
|
-
"supertest": {
|
|
43
|
-
"optional": true
|
|
44
|
-
},
|
|
45
|
-
"nanoid": {
|
|
46
|
-
"optional": true
|
|
47
|
-
}
|
|
33
|
+
"webdriverio": { "optional": true },
|
|
34
|
+
"@rpii/wdio-commands": { "optional": true },
|
|
35
|
+
"axios": { "optional": true },
|
|
36
|
+
"supertest": { "optional": true },
|
|
37
|
+
"nanoid": { "optional": true }
|
|
48
38
|
}
|
|
49
39
|
}
|