@capillarytech/cap-ui-utils 3.1.1 → 3.1.2
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/constants/common.js +22 -0
- package/e2e/index.js +65 -0
- package/e2e/pages/common/base.page.js +13 -0
- package/e2e/pages/common/constant.js +5 -0
- package/e2e/pages/common/login.page.js +95 -0
- package/e2e/services/{lockService.ts → lockService.js} +63 -67
- package/e2e/utils/{antdVersionUtil.ts → antdVersionUtil.js} +24 -32
- package/e2e/utils/{automationBypassUtil.ts → automationBypassUtil.js} +11 -8
- package/e2e/utils/{debugModeUtil.ts → debugModeUtil.js} +9 -7
- package/e2e/utils/deletionRegistry.js +16 -0
- package/e2e/utils/{elementUtil.ts → elementUtil.js} +224 -296
- package/e2e/utils/expectUtil.js +40 -0
- package/e2e/utils/featureFlagUtil.js +25 -0
- package/e2e/utils/garudaDropdownUtil.js +34 -0
- package/e2e/utils/htmlEditorUtil.js +59 -0
- package/e2e/utils/logCollectorUtil.js +65 -0
- package/e2e/utils/{mockResponse.ts → mockResponse.js} +5 -2
- package/e2e/utils/{reportUploader.ts → reportUploader.js} +69 -66
- package/e2e/utils/{requestRecorderUtil.ts → requestRecorderUtil.js} +69 -97
- package/e2e/utils/{screenshotRecorderUtil.ts → screenshotRecorderUtil.js} +61 -82
- package/e2e/utils/setupMock.js +18 -0
- package/e2e/utils/unmatchedBracesUtil.js +87 -0
- package/e2e/utils/uploaders/fileServiceUploader.js +113 -0
- package/e2e/utils/uploaders/uploader.js +2 -0
- package/e2e/utils/virtualListUtil.js +74 -0
- package/package.json +1 -1
- package/e2e/constants/common.ts +0 -23
- package/e2e/index.ts +0 -30
- package/e2e/pages/common/base.page.ts +0 -11
- package/e2e/pages/common/constant.ts +0 -2
- package/e2e/pages/common/login.page.ts +0 -95
- package/e2e/utils/deletionRegistry.ts +0 -15
- package/e2e/utils/expectUtil.ts +0 -19
- package/e2e/utils/featureFlagUtil.ts +0 -30
- package/e2e/utils/garudaDropdownUtil.ts +0 -60
- package/e2e/utils/htmlEditorUtil.ts +0 -56
- package/e2e/utils/logCollectorUtil.ts +0 -59
- package/e2e/utils/setupMock.ts +0 -29
- package/e2e/utils/unmatchedBracesUtil.ts +0 -101
- package/e2e/utils/uploaders/fileServiceUploader.ts +0 -84
- package/e2e/utils/uploaders/uploader.ts +0 -20
- package/e2e/utils/virtualListUtil.ts +0 -115
|
@@ -1,133 +1,107 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* Off by default; enabled only when FULL_DEBUG_MODE=true. When off, every
|
|
10
|
-
* method is a no-op so there is zero overhead.
|
|
11
|
-
*
|
|
12
|
-
* One JSON file is written per test, containing an array of all the
|
|
13
|
-
* POST / PATCH / DELETE requests captured while that test was running.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
interface InitiatorFrame {
|
|
17
|
-
functionName: string;
|
|
18
|
-
url: string;
|
|
19
|
-
line: number;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface RequestInitiator {
|
|
23
|
-
// 'script' (a JS handler fired it), 'parser', 'preflight', 'other', ...
|
|
24
|
-
type: string;
|
|
25
|
-
// Top JS call frames that triggered the request — i.e. which handler fired it.
|
|
26
|
-
stack: InitiatorFrame[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface RecordedRequest {
|
|
30
|
-
requestId: string;
|
|
31
|
-
method: string;
|
|
32
|
-
url: string;
|
|
33
|
-
timestamp: string;
|
|
34
|
-
postData: string | null;
|
|
35
|
-
headers: Record<string, string>;
|
|
36
|
-
// Which app code initiated the request (from CDP initiator).
|
|
37
|
-
initiator: RequestInitiator | null;
|
|
38
|
-
}
|
|
39
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const debugModeUtil_1 = __importDefault(require("./debugModeUtil"));
|
|
40
9
|
const RECORDED_METHODS = ['POST', 'PATCH', 'DELETE', 'PUT'];
|
|
41
10
|
// How many call frames to keep — enough to identify the handler without bloat.
|
|
42
11
|
const MAX_INITIATOR_FRAMES = 3;
|
|
43
|
-
|
|
44
12
|
class RequestRecorder {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
13
|
+
constructor() {
|
|
14
|
+
// Off by default; record only when capture is enabled for this module
|
|
15
|
+
// (FULL_DEBUG_MODE=true and, if DEBUG_MODULES is set, module is listed).
|
|
16
|
+
this.enabled = debugModeUtil_1.default.isCaptureEnabled();
|
|
17
|
+
this.outputDir = path_1.default.join(process.cwd(), 'reports', 'debug', 'request-payloads');
|
|
18
|
+
this.currentTestTitle = '';
|
|
19
|
+
this.buffer = [];
|
|
20
|
+
this.seenRequestIds = new Set();
|
|
21
|
+
}
|
|
22
|
+
isEnabled() {
|
|
54
23
|
return this.enabled;
|
|
55
24
|
}
|
|
56
|
-
|
|
57
25
|
/** Reset the buffer and remember the test we are currently recording for. */
|
|
58
|
-
startTest(testTitle
|
|
59
|
-
if (!this.enabled)
|
|
26
|
+
startTest(testTitle) {
|
|
27
|
+
if (!this.enabled)
|
|
28
|
+
return;
|
|
60
29
|
this.currentTestTitle = testTitle || 'unknown_test';
|
|
61
30
|
this.buffer = [];
|
|
62
31
|
this.seenRequestIds.clear();
|
|
63
32
|
}
|
|
64
|
-
|
|
65
33
|
/**
|
|
66
34
|
* Capture a single request from a CDP Network.requestWillBeSent event.
|
|
67
35
|
* Only POST / PATCH / DELETE requests matching baseUrl are recorded.
|
|
68
36
|
* Large request bodies are fetched via Network.getRequestPostData when
|
|
69
37
|
* the event payload does not inline them.
|
|
70
38
|
*/
|
|
71
|
-
async record(params
|
|
72
|
-
|
|
39
|
+
async record(params, baseUrl) {
|
|
40
|
+
var _a, _b, _c;
|
|
41
|
+
if (!this.enabled)
|
|
42
|
+
return;
|
|
73
43
|
try {
|
|
74
|
-
const request = params
|
|
75
|
-
if (!request)
|
|
76
|
-
|
|
44
|
+
const request = params === null || params === void 0 ? void 0 : params.request;
|
|
45
|
+
if (!request)
|
|
46
|
+
return;
|
|
77
47
|
const method = (request.method || '').toUpperCase();
|
|
78
|
-
if (!RECORDED_METHODS.includes(method))
|
|
79
|
-
|
|
80
|
-
|
|
48
|
+
if (!RECORDED_METHODS.includes(method))
|
|
49
|
+
return;
|
|
50
|
+
if (baseUrl && !String(request.url).startsWith(baseUrl))
|
|
51
|
+
return;
|
|
81
52
|
// CDP can emit requestWillBeSent more than once per request; dedupe by id.
|
|
82
53
|
const requestId = String(params.requestId);
|
|
83
|
-
if (this.seenRequestIds.has(requestId))
|
|
54
|
+
if (this.seenRequestIds.has(requestId))
|
|
55
|
+
return;
|
|
84
56
|
this.seenRequestIds.add(requestId);
|
|
85
|
-
|
|
86
|
-
const entry: RecordedRequest = {
|
|
57
|
+
const entry = {
|
|
87
58
|
requestId,
|
|
88
59
|
method,
|
|
89
60
|
url: request.url,
|
|
90
61
|
timestamp: new Date().toISOString(),
|
|
91
|
-
postData: request.postData
|
|
92
|
-
headers: request.headers
|
|
93
|
-
initiator: this.extractInitiator(params
|
|
62
|
+
postData: (_a = request.postData) !== null && _a !== void 0 ? _a : null,
|
|
63
|
+
headers: (_b = request.headers) !== null && _b !== void 0 ? _b : {},
|
|
64
|
+
initiator: this.extractInitiator(params === null || params === void 0 ? void 0 : params.initiator),
|
|
94
65
|
};
|
|
95
66
|
// Preserve ordering: push synchronously, backfill body if needed.
|
|
96
67
|
this.buffer.push(entry);
|
|
97
|
-
|
|
98
68
|
if (entry.postData == null && request.hasPostData) {
|
|
99
69
|
try {
|
|
100
|
-
const res
|
|
70
|
+
const res = await browser.cdp('Network', 'getRequestPostData', {
|
|
101
71
|
requestId: params.requestId,
|
|
102
72
|
});
|
|
103
|
-
entry.postData = res
|
|
104
|
-
}
|
|
73
|
+
entry.postData = (_c = res === null || res === void 0 ? void 0 : res.postData) !== null && _c !== void 0 ? _c : null;
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
105
76
|
console.log(`[requestRecorder] could not fetch post data: ${err}`);
|
|
106
77
|
}
|
|
107
78
|
}
|
|
108
|
-
}
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
109
81
|
console.log(`[requestRecorder] failed to record request: ${err}`);
|
|
110
82
|
}
|
|
111
83
|
}
|
|
112
|
-
|
|
113
84
|
/**
|
|
114
85
|
* Reduce a CDP initiator to a compact, comparable shape: the request type
|
|
115
86
|
* and the top few JS call frames (the handler that fired the request).
|
|
116
87
|
* CDP line/column numbers are 0-based, so +1 for human-readable lines.
|
|
117
88
|
*/
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
89
|
+
extractInitiator(initiator) {
|
|
90
|
+
var _a;
|
|
91
|
+
if (!initiator)
|
|
92
|
+
return null;
|
|
93
|
+
const frames = [];
|
|
94
|
+
const callFrames = (_a = initiator === null || initiator === void 0 ? void 0 : initiator.stack) === null || _a === void 0 ? void 0 : _a.callFrames;
|
|
122
95
|
if (Array.isArray(callFrames)) {
|
|
123
96
|
for (const f of callFrames.slice(0, MAX_INITIATOR_FRAMES)) {
|
|
124
97
|
frames.push({
|
|
125
|
-
functionName: f
|
|
126
|
-
url: f
|
|
127
|
-
line: typeof f
|
|
98
|
+
functionName: (f === null || f === void 0 ? void 0 : f.functionName) || '(anonymous)',
|
|
99
|
+
url: (f === null || f === void 0 ? void 0 : f.url) || '',
|
|
100
|
+
line: typeof (f === null || f === void 0 ? void 0 : f.lineNumber) === 'number' ? f.lineNumber + 1 : -1,
|
|
128
101
|
});
|
|
129
102
|
}
|
|
130
|
-
}
|
|
103
|
+
}
|
|
104
|
+
else if (initiator.url) {
|
|
131
105
|
// parser-initiated requests carry url/lineNumber directly.
|
|
132
106
|
frames.push({
|
|
133
107
|
functionName: '',
|
|
@@ -137,24 +111,22 @@ class RequestRecorder {
|
|
|
137
111
|
}
|
|
138
112
|
return { type: initiator.type || 'other', stack: frames };
|
|
139
113
|
}
|
|
140
|
-
|
|
141
114
|
/** Write the buffered requests for the current test to a JSON file. */
|
|
142
|
-
flushTest()
|
|
143
|
-
if (!this.enabled)
|
|
115
|
+
flushTest() {
|
|
116
|
+
if (!this.enabled)
|
|
117
|
+
return;
|
|
144
118
|
try {
|
|
145
|
-
if (this.buffer.length === 0)
|
|
146
|
-
|
|
119
|
+
if (this.buffer.length === 0)
|
|
120
|
+
return;
|
|
147
121
|
const moduleName = process.env.module || 'unknown_module';
|
|
148
|
-
const dir =
|
|
149
|
-
|
|
150
|
-
|
|
122
|
+
const dir = path_1.default.join(this.outputDir, moduleName);
|
|
123
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
151
124
|
const safeTitle = this.currentTestTitle
|
|
152
125
|
.replace(/[^a-zA-Z0-9_-]+/g, '_')
|
|
153
126
|
.slice(0, 120);
|
|
154
127
|
// Stable filename (no timestamp) so the same test maps to the same
|
|
155
128
|
// path across runs/zips — enables zip-to-zip payload diffing.
|
|
156
129
|
const fileName = `${safeTitle}.json`;
|
|
157
|
-
|
|
158
130
|
const payload = {
|
|
159
131
|
test: this.currentTestTitle,
|
|
160
132
|
module: moduleName,
|
|
@@ -162,17 +134,17 @@ class RequestRecorder {
|
|
|
162
134
|
requestCount: this.buffer.length,
|
|
163
135
|
requests: this.buffer,
|
|
164
136
|
};
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
137
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, fileName), JSON.stringify(payload, null, 2));
|
|
138
|
+
console.log(`[requestRecorder] saved ${this.buffer.length} request(s) to ${path_1.default.join(dir, fileName)}`);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
169
141
|
console.log(`[requestRecorder] failed to write payload file: ${err}`);
|
|
170
|
-
}
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
171
144
|
this.buffer = [];
|
|
172
145
|
this.seenRequestIds.clear();
|
|
173
146
|
this.currentTestTitle = '';
|
|
174
147
|
}
|
|
175
148
|
}
|
|
176
149
|
}
|
|
177
|
-
|
|
178
|
-
export default new RequestRecorder();
|
|
150
|
+
exports.default = new RequestRecorder();
|
|
@@ -1,80 +1,60 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* Off by default; enabled only when FULL_DEBUG_MODE=true. When off, every
|
|
10
|
-
* method is a no-op so there is zero overhead.
|
|
11
|
-
*
|
|
12
|
-
* Driven by the WDIO afterCommand hook. Only click commands trigger a capture,
|
|
13
|
-
* so other commands (typing, lookups, navigation) don't spam the report.
|
|
14
|
-
*
|
|
15
|
-
* Also writes a steps.json per test mapping each click (step index) to its
|
|
16
|
-
* screenshot file and the wall-clock time it fired. The compare tool uses the
|
|
17
|
-
* click times to attribute recorded request payloads to the click that
|
|
18
|
-
* triggered them (request.timestamp falls between consecutive click times).
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
interface RecordedStep {
|
|
22
|
-
step: number;
|
|
23
|
-
file: string;
|
|
24
|
-
command: string;
|
|
25
|
-
clickTime: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const debugModeUtil_1 = __importDefault(require("./debugModeUtil"));
|
|
28
9
|
// WDIO command names that represent a click worth screenshotting.
|
|
29
10
|
const INTERACTION_COMMANDS = new Set([
|
|
30
11
|
'click',
|
|
31
12
|
'doubleClick',
|
|
32
13
|
]);
|
|
33
|
-
|
|
34
14
|
// afterCommand fires before the post-click content has rendered; wait this long
|
|
35
15
|
// so the screenshot reflects the settled page. Tunable via env.
|
|
36
16
|
const SETTLE_MS = Number(process.env.FULL_DEBUG_SCREENSHOT_DELAY_MS) || 500;
|
|
37
|
-
|
|
38
17
|
// Hard cap on a single CDP screenshot. The capture can hang indefinitely when it
|
|
39
18
|
// fires while the page is mid-navigation (e.g. during login), which would block
|
|
40
19
|
// the awaiting afterCommand hook and stall the whole run until the Mocha hook
|
|
41
20
|
// timeout. Bounding it guarantees screenshots can never block automations.
|
|
42
21
|
const CAPTURE_TIMEOUT_MS = Number(process.env.FULL_DEBUG_SCREENSHOT_TIMEOUT_MS) || 8000;
|
|
43
|
-
|
|
44
22
|
class ScreenshotRecorder {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
23
|
+
constructor() {
|
|
24
|
+
this.enabled = debugModeUtil_1.default.isCaptureEnabled();
|
|
25
|
+
this.outputDir = path_1.default.join(process.cwd(), 'reports', 'debug', 'screenshots');
|
|
26
|
+
this.currentTestTitle = '';
|
|
27
|
+
this.counter = 0;
|
|
28
|
+
// Last saved image; used to drop byte-identical duplicate captures.
|
|
29
|
+
this.lastImage = null;
|
|
30
|
+
// Guard against re-entrancy: our own cdp/screenshot calls also fire afterCommand.
|
|
31
|
+
this.capturing = false;
|
|
32
|
+
// Per-test record of clicks → screenshot file + time, for request correlation.
|
|
33
|
+
this.steps = [];
|
|
34
|
+
}
|
|
35
|
+
isEnabled() {
|
|
57
36
|
return this.enabled;
|
|
58
37
|
}
|
|
59
|
-
|
|
60
38
|
/** Reset the per-test counter and remember the current test. */
|
|
61
|
-
startTest(testTitle
|
|
62
|
-
if (!this.enabled)
|
|
39
|
+
startTest(testTitle) {
|
|
40
|
+
if (!this.enabled)
|
|
41
|
+
return;
|
|
63
42
|
this.currentTestTitle = testTitle || 'unknown_test';
|
|
64
43
|
this.counter = 0;
|
|
65
44
|
this.lastImage = null;
|
|
66
45
|
this.steps = [];
|
|
67
46
|
}
|
|
68
|
-
|
|
69
47
|
/** Capture a full-page screenshot if commandName is a click. */
|
|
70
|
-
async captureAfter(commandName
|
|
71
|
-
if (!this.enabled || this.capturing)
|
|
72
|
-
|
|
48
|
+
async captureAfter(commandName) {
|
|
49
|
+
if (!this.enabled || this.capturing)
|
|
50
|
+
return;
|
|
51
|
+
if (!INTERACTION_COMMANDS.has(commandName))
|
|
52
|
+
return;
|
|
73
53
|
// No active test means we're in the login/setup phase (the root before-all
|
|
74
54
|
// hook). Capturing here is pointless and, worse, the CDP screenshot can hang
|
|
75
55
|
// mid-navigation and block that hook until its Mocha timeout. Skip it.
|
|
76
|
-
if (!this.currentTestTitle)
|
|
77
|
-
|
|
56
|
+
if (!this.currentTestTitle)
|
|
57
|
+
return;
|
|
78
58
|
this.capturing = true;
|
|
79
59
|
// Stamp the click time before the settle pause so it lines up with when
|
|
80
60
|
// the click actually fired (and thus with the requests it triggered).
|
|
@@ -82,20 +62,18 @@ class ScreenshotRecorder {
|
|
|
82
62
|
try {
|
|
83
63
|
// Let the post-click content render before capturing.
|
|
84
64
|
await browser.pause(SETTLE_MS);
|
|
85
|
-
|
|
86
65
|
// captureBeyondViewport => full scrollable page, not just the viewport.
|
|
87
66
|
// Bound the CDP call: it can hang indefinitely if it fires while the page
|
|
88
67
|
// is mid-navigation, which would block the awaiting afterCommand hook.
|
|
89
|
-
const capturePromise
|
|
68
|
+
const capturePromise = browser.cdp('Page', 'captureScreenshot', {
|
|
90
69
|
format: 'png',
|
|
91
70
|
captureBeyondViewport: true,
|
|
92
71
|
});
|
|
93
72
|
// If the timeout wins, the CDP promise may still settle (or reject) later;
|
|
94
73
|
// swallow it so a late rejection never surfaces as an unhandled rejection.
|
|
95
|
-
capturePromise.catch(() => {});
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const res: any = await Promise.race([
|
|
74
|
+
capturePromise.catch(() => { });
|
|
75
|
+
let timeoutHandle;
|
|
76
|
+
const res = await Promise.race([
|
|
99
77
|
capturePromise,
|
|
100
78
|
new Promise((resolve) => {
|
|
101
79
|
timeoutHandle = setTimeout(() => resolve(null), CAPTURE_TIMEOUT_MS);
|
|
@@ -104,71 +82,72 @@ class ScreenshotRecorder {
|
|
|
104
82
|
// Clear the fallback timer once the race settles so a pending timeout
|
|
105
83
|
// can't keep the Node event loop alive (delaying process exit by up to
|
|
106
84
|
// CAPTURE_TIMEOUT_MS) when the CDP call won.
|
|
107
|
-
if (timeoutHandle)
|
|
108
|
-
|
|
109
|
-
|
|
85
|
+
if (timeoutHandle)
|
|
86
|
+
clearTimeout(timeoutHandle);
|
|
87
|
+
if (!(res === null || res === void 0 ? void 0 : res.data))
|
|
88
|
+
return;
|
|
110
89
|
const image = Buffer.from(res.data, 'base64');
|
|
111
90
|
// afterCommand fires twice per click; once settled both frames are
|
|
112
91
|
// identical, so skip the byte-identical repeat.
|
|
113
|
-
if (this.lastImage && this.lastImage.equals(image))
|
|
92
|
+
if (this.lastImage && this.lastImage.equals(image))
|
|
93
|
+
return;
|
|
114
94
|
this.lastImage = image;
|
|
115
|
-
|
|
116
95
|
const moduleName = process.env.module || 'unknown_module';
|
|
117
96
|
const safeTitle = (this.currentTestTitle || '_pre-test')
|
|
118
97
|
.replace(/[^a-zA-Z0-9_-]+/g, '_')
|
|
119
98
|
.slice(0, 120);
|
|
120
|
-
const dir =
|
|
121
|
-
|
|
122
|
-
|
|
99
|
+
const dir = path_1.default.join(this.outputDir, moduleName, safeTitle);
|
|
100
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
123
101
|
this.counter += 1;
|
|
124
102
|
const seq = String(this.counter).padStart(3, '0');
|
|
125
103
|
const fileName = `${seq}_${commandName}.png`;
|
|
126
|
-
|
|
127
|
-
|
|
104
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, fileName), image);
|
|
128
105
|
this.steps.push({
|
|
129
106
|
step: this.counter,
|
|
130
107
|
file: fileName,
|
|
131
108
|
command: commandName,
|
|
132
109
|
clickTime,
|
|
133
110
|
});
|
|
134
|
-
}
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
135
113
|
console.log(`[screenshotRecorder] failed to capture after ${commandName}: ${err}`);
|
|
136
|
-
}
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
137
116
|
this.capturing = false;
|
|
138
117
|
}
|
|
139
118
|
}
|
|
140
|
-
|
|
141
119
|
/**
|
|
142
120
|
* Write the per-test steps.json alongside this test's screenshots, mapping
|
|
143
121
|
* each click step to its screenshot file and the time it fired. Used by the
|
|
144
122
|
* compare tool to attribute request payloads to the click that triggered
|
|
145
123
|
* them. No-op when nothing was captured.
|
|
146
124
|
*/
|
|
147
|
-
flushTest()
|
|
148
|
-
if (!this.enabled)
|
|
125
|
+
flushTest() {
|
|
126
|
+
if (!this.enabled)
|
|
127
|
+
return;
|
|
149
128
|
try {
|
|
150
|
-
if (this.steps.length === 0)
|
|
151
|
-
|
|
129
|
+
if (this.steps.length === 0)
|
|
130
|
+
return;
|
|
152
131
|
const moduleName = process.env.module || 'unknown_module';
|
|
153
132
|
const safeTitle = (this.currentTestTitle || '_pre-test')
|
|
154
133
|
.replace(/[^a-zA-Z0-9_-]+/g, '_')
|
|
155
134
|
.slice(0, 120);
|
|
156
|
-
const dir =
|
|
157
|
-
|
|
158
|
-
|
|
135
|
+
const dir = path_1.default.join(this.outputDir, moduleName, safeTitle);
|
|
136
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
159
137
|
const payload = {
|
|
160
138
|
test: this.currentTestTitle,
|
|
161
139
|
module: moduleName,
|
|
162
140
|
stepCount: this.steps.length,
|
|
163
141
|
steps: this.steps,
|
|
164
142
|
};
|
|
165
|
-
|
|
166
|
-
}
|
|
143
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'steps.json'), JSON.stringify(payload, null, 2));
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
167
146
|
console.log(`[screenshotRecorder] failed to write steps.json: ${err}`);
|
|
168
|
-
}
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
169
149
|
this.steps = [];
|
|
170
150
|
}
|
|
171
151
|
}
|
|
172
152
|
}
|
|
173
|
-
|
|
174
|
-
export default new ScreenshotRecorder();
|
|
153
|
+
exports.default = new ScreenshotRecorder();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const mockResponse_1 = require("./mockResponse");
|
|
4
|
+
class setupMock {
|
|
5
|
+
async mockBeamerAPIForPopup() {
|
|
6
|
+
const beamerReq1 = await browser.mock("https://backend.getbeamer.com/initialize" + "**", {
|
|
7
|
+
method: "get",
|
|
8
|
+
});
|
|
9
|
+
beamerReq1.respond(mockResponse_1.beamerApiResp);
|
|
10
|
+
const beamerReq2 = await browser.mock("https://backend.getbeamer.com/numberFeatures" + "**", {
|
|
11
|
+
method: "get",
|
|
12
|
+
});
|
|
13
|
+
beamerReq2.respond([{}], {
|
|
14
|
+
statusCode: 404,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.default = new setupMock();
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.injectAndRestore = exports.focusEmailEditorAddNewLineAtEnd = exports.componentWithLabelErrorSpan = exports.invalidLabelErrorSpan = exports.invalidLabelErrorFooterContainer = exports.INVALID_LABEL_ERROR_TEXT = exports.UNSUPPORTED_TAG = exports.UNMATCHED_BRACES_SNIPPET = void 0;
|
|
7
|
+
const elementUtil_1 = __importDefault(require("./elementUtil"));
|
|
8
|
+
/** Invalid snippet used to trigger unmatched-braces validation across Engage and Adiona flows. */
|
|
9
|
+
exports.UNMATCHED_BRACES_SNIPPET = "{{ my_standard_tag";
|
|
10
|
+
/** Unsupported/unrecognised tag used to trigger the send-for-approval server-side validation error. */
|
|
11
|
+
exports.UNSUPPORTED_TAG = "{{my_invalid_standard_tag}}";
|
|
12
|
+
/** Full footer / inline error copy for standard-tag validation (SMS, MPush, Web Push, etc.). */
|
|
13
|
+
exports.INVALID_LABEL_ERROR_TEXT = "Invalid label, please close all curly braces";
|
|
14
|
+
/**
|
|
15
|
+
* Footer error container for "Invalid label…" type errors (SMS, Journey SMS).
|
|
16
|
+
* XPath: //div[contains(@class,'error-container')]//div[text()='<message>']
|
|
17
|
+
*/
|
|
18
|
+
function invalidLabelErrorFooterContainer(errorMessage) {
|
|
19
|
+
return $(`//div[contains(@class,'error-container')]//div[text()='${errorMessage}']`);
|
|
20
|
+
}
|
|
21
|
+
exports.invalidLabelErrorFooterContainer = invalidLabelErrorFooterContainer;
|
|
22
|
+
/**
|
|
23
|
+
* Inline error span for "Invalid label…" (Viber, Zalo, Journey Zalo).
|
|
24
|
+
* XPath: //span[contains(@class,'error-message') and contains(.,'Invalid label')]
|
|
25
|
+
*/
|
|
26
|
+
function invalidLabelErrorSpan() {
|
|
27
|
+
return $("//span[contains(@class,'error-message') and contains(.,'Invalid label')]");
|
|
28
|
+
}
|
|
29
|
+
exports.invalidLabelErrorSpan = invalidLabelErrorSpan;
|
|
30
|
+
/**
|
|
31
|
+
* Validation error span below a component-with-label input (MPush Title / Message).
|
|
32
|
+
* XPath: //div[contains(@class,'component-with-label-label') and normalize-space()='<label>']
|
|
33
|
+
* /ancestor::div[contains(@class,'component-with-label')]//span[contains(@class,'error-message')]
|
|
34
|
+
* Use for channels where the error appears inline under the input rather than in the footer.
|
|
35
|
+
*/
|
|
36
|
+
function componentWithLabelErrorSpan(label) {
|
|
37
|
+
return $(`//div[contains(@class,'component-with-label-label') and normalize-space()='${label}']/ancestor::div[contains(@class,'component-with-label')]//span[contains(@class,'error-message')]`);
|
|
38
|
+
}
|
|
39
|
+
exports.componentWithLabelErrorSpan = componentWithLabelErrorSpan;
|
|
40
|
+
/**
|
|
41
|
+
* CodeMirror email editor: click the last cm-line at a stable left-edge offset to acquire focus,
|
|
42
|
+
* wait for the editor to be focused, then press End + Enter to position the cursor on a new last line.
|
|
43
|
+
* Call this before typing a snippet into the editor.
|
|
44
|
+
*
|
|
45
|
+
* @param codeMirrorEditor The CodeMirror content-editable div (aria-placeholder contains "Write your HTML email code here").
|
|
46
|
+
* @param codeMirrorLastLine The last `.cm-line` div inside that editor.
|
|
47
|
+
*/
|
|
48
|
+
async function focusEmailEditorAddNewLineAtEnd(codeMirrorEditor, codeMirrorLastLine) {
|
|
49
|
+
await codeMirrorEditor.waitForDisplayed({ timeout: 90000 });
|
|
50
|
+
await codeMirrorLastLine.waitForDisplayed({ timeout: 10000 });
|
|
51
|
+
const size = await codeMirrorLastLine.getSize();
|
|
52
|
+
const xOffset = Math.round(2 - size.width / 2);
|
|
53
|
+
await codeMirrorLastLine.click({ x: xOffset, y: 0 });
|
|
54
|
+
await browser.waitUntil(async () => {
|
|
55
|
+
const focused = await browser.execute(() => {
|
|
56
|
+
const codeMirrorEditorEl = document.querySelector('div[aria-placeholder*="Write your HTML email code here"]');
|
|
57
|
+
if (!codeMirrorEditorEl)
|
|
58
|
+
return false;
|
|
59
|
+
const active = document.activeElement;
|
|
60
|
+
const editorFocused = active === codeMirrorEditorEl || (active && codeMirrorEditorEl.contains(active));
|
|
61
|
+
const cmEditor = codeMirrorEditorEl.closest(".cm-editor");
|
|
62
|
+
const hasCmFocused = (cmEditor && cmEditor.classList.contains("cm-focused")) ||
|
|
63
|
+
(active && active.classList.contains("cm-focused"));
|
|
64
|
+
return !!(editorFocused || hasCmFocused);
|
|
65
|
+
});
|
|
66
|
+
return focused === true;
|
|
67
|
+
}, {
|
|
68
|
+
timeout: 10000,
|
|
69
|
+
timeoutMsg: "CodeMirror editor did not receive focus after clicking last line",
|
|
70
|
+
interval: 100,
|
|
71
|
+
});
|
|
72
|
+
await browser.keys(["End"]);
|
|
73
|
+
await browser.keys(["Enter"]);
|
|
74
|
+
}
|
|
75
|
+
exports.focusEmailEditorAddNewLineAtEnd = focusEmailEditorAddNewLineAtEnd;
|
|
76
|
+
/**
|
|
77
|
+
* Inject unmatched-braces snippet, try submit (if clickable), assert via callback, then restore field.
|
|
78
|
+
* Not used for Email (CodeMirror debounce / waitUntil sequencing is bespoke).
|
|
79
|
+
*/
|
|
80
|
+
async function injectAndRestore(targetInput, submitButton, originalValue, assertFn) {
|
|
81
|
+
await elementUtil_1.default.enterText(targetInput, exports.UNMATCHED_BRACES_SNIPPET);
|
|
82
|
+
await elementUtil_1.default.clickIfClickable(submitButton);
|
|
83
|
+
await assertFn();
|
|
84
|
+
await elementUtil_1.default.clearByBackspace(exports.UNMATCHED_BRACES_SNIPPET, targetInput);
|
|
85
|
+
await elementUtil_1.default.typeText(targetInput, originalValue);
|
|
86
|
+
}
|
|
87
|
+
exports.injectAndRestore = injectAndRestore;
|