codeceptjs 3.5.15 → 3.6.0
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/bin/codecept.js +68 -31
- package/docs/webapi/startRecordingWebSocketMessages.mustache +8 -0
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +7 -0
- package/lib/ai.js +152 -80
- package/lib/cli.js +1 -0
- package/lib/command/generate.js +34 -0
- package/lib/command/run-workers.js +3 -0
- package/lib/command/run.js +3 -0
- package/lib/container.js +2 -0
- package/lib/heal.js +172 -0
- package/lib/helper/{OpenAI.js → AI.js} +10 -12
- package/lib/helper/Playwright.js +32 -156
- package/lib/helper/Puppeteer.js +222 -3
- package/lib/helper/WebDriver.js +6 -144
- package/lib/helper/extras/PlaywrightReactVueLocator.js +6 -1
- package/lib/helper/network/actions.js +123 -0
- package/lib/helper/{networkTraffics → network}/utils.js +50 -0
- package/lib/index.js +3 -0
- package/lib/listener/steps.js +0 -2
- package/lib/locator.js +23 -1
- package/lib/plugin/heal.js +26 -117
- package/lib/recorder.js +11 -5
- package/lib/store.js +2 -0
- package/lib/template/heal.js +39 -0
- package/package.json +14 -15
- package/typings/index.d.ts +2 -2
- package/typings/promiseBasedTypes.d.ts +206 -25
- package/typings/types.d.ts +219 -26
package/lib/helper/Puppeteer.js
CHANGED
|
@@ -36,13 +36,14 @@ const ElementNotFound = require('./errors/ElementNotFound');
|
|
|
36
36
|
const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused');
|
|
37
37
|
const Popup = require('./extras/Popup');
|
|
38
38
|
const Console = require('./extras/Console');
|
|
39
|
-
const findReact = require('./extras/React');
|
|
40
39
|
const { highlightElement } = require('./scripts/highlightElement');
|
|
41
40
|
const { blurElement } = require('./scripts/blurElement');
|
|
42
|
-
const { focusElement } = require('./scripts/focusElement');
|
|
43
41
|
const {
|
|
44
42
|
dontSeeElementError, seeElementError, dontSeeElementInDOMError, seeElementInDOMError,
|
|
45
43
|
} = require('./errors/ElementAssertion');
|
|
44
|
+
const {
|
|
45
|
+
dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics,
|
|
46
|
+
} = require('./network/actions');
|
|
46
47
|
|
|
47
48
|
let puppeteer;
|
|
48
49
|
let perfTiming;
|
|
@@ -226,6 +227,17 @@ class Puppeteer extends Helper {
|
|
|
226
227
|
this.sessionPages = {};
|
|
227
228
|
this.activeSessionName = '';
|
|
228
229
|
|
|
230
|
+
// for network stuff
|
|
231
|
+
this.requests = [];
|
|
232
|
+
this.recording = false;
|
|
233
|
+
this.recordedAtLeastOnce = false;
|
|
234
|
+
|
|
235
|
+
// for websocket messages
|
|
236
|
+
this.webSocketMessages = [];
|
|
237
|
+
this.recordingWebSocketMessages = false;
|
|
238
|
+
this.recordedWebSocketMessagesAtLeastOnce = false;
|
|
239
|
+
this.cdpSession = null;
|
|
240
|
+
|
|
229
241
|
// override defaults with config
|
|
230
242
|
this._setConfig(config);
|
|
231
243
|
}
|
|
@@ -2078,7 +2090,6 @@ class Puppeteer extends Helper {
|
|
|
2078
2090
|
async waitNumberOfVisibleElements(locator, num, sec) {
|
|
2079
2091
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
2080
2092
|
locator = new Locator(locator, 'css');
|
|
2081
|
-
await this.context;
|
|
2082
2093
|
let waiter;
|
|
2083
2094
|
const context = await this._getContext();
|
|
2084
2095
|
if (locator.isCSS()) {
|
|
@@ -2459,6 +2470,214 @@ class Puppeteer extends Helper {
|
|
|
2459
2470
|
if (prop) return rect[prop];
|
|
2460
2471
|
return rect;
|
|
2461
2472
|
}
|
|
2473
|
+
|
|
2474
|
+
/**
|
|
2475
|
+
* Mocks network request using [`Request Interception`](https://pptr.dev/next/guides/request-interception)
|
|
2476
|
+
*
|
|
2477
|
+
* ```js
|
|
2478
|
+
* I.mockRoute(/(\.png$)|(\.jpg$)/, route => route.abort());
|
|
2479
|
+
* ```
|
|
2480
|
+
* This method allows intercepting and mocking requests & responses. [Learn more about it](https://pptr.dev/next/guides/request-interception)
|
|
2481
|
+
*
|
|
2482
|
+
* @param {string|RegExp} [url] URL, regex or pattern for to match URL
|
|
2483
|
+
* @param {function} [handler] a function to process request
|
|
2484
|
+
*/
|
|
2485
|
+
async mockRoute(url, handler) {
|
|
2486
|
+
await this.page.setRequestInterception(true);
|
|
2487
|
+
|
|
2488
|
+
this.page.on('request', interceptedRequest => {
|
|
2489
|
+
if (interceptedRequest.url().match(url)) {
|
|
2490
|
+
// @ts-ignore
|
|
2491
|
+
handler(interceptedRequest);
|
|
2492
|
+
} else {
|
|
2493
|
+
interceptedRequest.continue();
|
|
2494
|
+
}
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
/**
|
|
2499
|
+
* Stops network mocking created by `mockRoute`.
|
|
2500
|
+
*
|
|
2501
|
+
* ```js
|
|
2502
|
+
* I.stopMockingRoute(/(\.png$)|(\.jpg$)/);
|
|
2503
|
+
* ```
|
|
2504
|
+
*
|
|
2505
|
+
* @param {string|RegExp} [url] URL, regex or pattern for to match URL
|
|
2506
|
+
*/
|
|
2507
|
+
async stopMockingRoute(url) {
|
|
2508
|
+
await this.page.setRequestInterception(true);
|
|
2509
|
+
|
|
2510
|
+
this.page.off('request');
|
|
2511
|
+
|
|
2512
|
+
// Resume normal request handling for the given URL
|
|
2513
|
+
this.page.on('request', interceptedRequest => {
|
|
2514
|
+
if (interceptedRequest.url().includes(url)) {
|
|
2515
|
+
interceptedRequest.continue();
|
|
2516
|
+
} else {
|
|
2517
|
+
interceptedRequest.continue();
|
|
2518
|
+
}
|
|
2519
|
+
});
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
/**
|
|
2523
|
+
*
|
|
2524
|
+
* {{> flushNetworkTraffics }}
|
|
2525
|
+
*/
|
|
2526
|
+
flushNetworkTraffics() {
|
|
2527
|
+
flushNetworkTraffics.call(this);
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
/**
|
|
2531
|
+
*
|
|
2532
|
+
* {{> stopRecordingTraffic }}
|
|
2533
|
+
*/
|
|
2534
|
+
stopRecordingTraffic() {
|
|
2535
|
+
stopRecordingTraffic.call(this);
|
|
2536
|
+
}
|
|
2537
|
+
|
|
2538
|
+
/**
|
|
2539
|
+
* {{> startRecordingTraffic }}
|
|
2540
|
+
*
|
|
2541
|
+
*/
|
|
2542
|
+
async startRecordingTraffic() {
|
|
2543
|
+
this.flushNetworkTraffics();
|
|
2544
|
+
this.recording = true;
|
|
2545
|
+
this.recordedAtLeastOnce = true;
|
|
2546
|
+
|
|
2547
|
+
await this.page.setRequestInterception(true);
|
|
2548
|
+
|
|
2549
|
+
this.page.on('request', (request) => {
|
|
2550
|
+
const information = {
|
|
2551
|
+
url: request.url(),
|
|
2552
|
+
method: request.method(),
|
|
2553
|
+
requestHeaders: request.headers(),
|
|
2554
|
+
requestPostData: request.postData(),
|
|
2555
|
+
response: request.response(),
|
|
2556
|
+
};
|
|
2557
|
+
|
|
2558
|
+
this.debugSection('REQUEST: ', JSON.stringify(information));
|
|
2559
|
+
|
|
2560
|
+
if (typeof information.requestPostData === 'object') {
|
|
2561
|
+
information.requestPostData = JSON.parse(information.requestPostData);
|
|
2562
|
+
}
|
|
2563
|
+
request.continue();
|
|
2564
|
+
this.requests.push(information);
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
/**
|
|
2569
|
+
*
|
|
2570
|
+
* {{> grabRecordedNetworkTraffics }}
|
|
2571
|
+
*/
|
|
2572
|
+
async grabRecordedNetworkTraffics() {
|
|
2573
|
+
return grabRecordedNetworkTraffics.call(this);
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
/**
|
|
2577
|
+
*
|
|
2578
|
+
* {{> seeTraffic }}
|
|
2579
|
+
*/
|
|
2580
|
+
async seeTraffic({
|
|
2581
|
+
name, url, parameters, requestPostData, timeout = 10,
|
|
2582
|
+
}) {
|
|
2583
|
+
await seeTraffic.call(this, ...arguments);
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
/**
|
|
2587
|
+
*
|
|
2588
|
+
* {{> dontSeeTraffic }}
|
|
2589
|
+
*
|
|
2590
|
+
*/
|
|
2591
|
+
dontSeeTraffic({ name, url }) {
|
|
2592
|
+
dontSeeTraffic.call(this, ...arguments);
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
async getNewCDPSession() {
|
|
2596
|
+
const client = await this.page.target().createCDPSession();
|
|
2597
|
+
return client;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
/**
|
|
2601
|
+
* {{> startRecordingWebSocketMessages }}
|
|
2602
|
+
*/
|
|
2603
|
+
async startRecordingWebSocketMessages() {
|
|
2604
|
+
this.flushWebSocketMessages();
|
|
2605
|
+
this.recordingWebSocketMessages = true;
|
|
2606
|
+
this.recordedWebSocketMessagesAtLeastOnce = true;
|
|
2607
|
+
|
|
2608
|
+
this.cdpSession = await this.getNewCDPSession();
|
|
2609
|
+
await this.cdpSession.send('Network.enable');
|
|
2610
|
+
await this.cdpSession.send('Page.enable');
|
|
2611
|
+
|
|
2612
|
+
this.cdpSession.on(
|
|
2613
|
+
'Network.webSocketFrameReceived',
|
|
2614
|
+
(payload) => {
|
|
2615
|
+
this._logWebsocketMessages(this._getWebSocketLog('RECEIVED', payload));
|
|
2616
|
+
},
|
|
2617
|
+
);
|
|
2618
|
+
|
|
2619
|
+
this.cdpSession.on(
|
|
2620
|
+
'Network.webSocketFrameSent',
|
|
2621
|
+
(payload) => {
|
|
2622
|
+
this._logWebsocketMessages(this._getWebSocketLog('SENT', payload));
|
|
2623
|
+
},
|
|
2624
|
+
);
|
|
2625
|
+
|
|
2626
|
+
this.cdpSession.on(
|
|
2627
|
+
'Network.webSocketFrameError',
|
|
2628
|
+
(payload) => {
|
|
2629
|
+
this._logWebsocketMessages(this._getWebSocketLog('ERROR', payload));
|
|
2630
|
+
},
|
|
2631
|
+
);
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
/**
|
|
2635
|
+
* {{> stopRecordingWebSocketMessages }}
|
|
2636
|
+
*/
|
|
2637
|
+
async stopRecordingWebSocketMessages() {
|
|
2638
|
+
await this.cdpSession.send('Network.disable');
|
|
2639
|
+
await this.cdpSession.send('Page.disable');
|
|
2640
|
+
this.page.removeAllListeners('Network');
|
|
2641
|
+
this.recordingWebSocketMessages = false;
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
/**
|
|
2645
|
+
* Grab the recording WS messages
|
|
2646
|
+
*
|
|
2647
|
+
* @return { Array<any>|undefined }
|
|
2648
|
+
*
|
|
2649
|
+
*/
|
|
2650
|
+
grabWebSocketMessages() {
|
|
2651
|
+
if (!this.recordingWebSocketMessages) {
|
|
2652
|
+
if (!this.recordedWebSocketMessagesAtLeastOnce) {
|
|
2653
|
+
throw new Error('Failure in test automation. You use "I.grabWebSocketMessages", but "I.startRecordingWebSocketMessages" was never called before.');
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
return this.webSocketMessages;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
/**
|
|
2660
|
+
* Resets all recorded WS messages.
|
|
2661
|
+
*/
|
|
2662
|
+
flushWebSocketMessages() {
|
|
2663
|
+
this.webSocketMessages = [];
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
_getWebSocketMessage(payload) {
|
|
2667
|
+
if (payload.errorMessage) {
|
|
2668
|
+
return payload.errorMessage;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
return payload.response.payloadData;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
_getWebSocketLog(prefix, payload) {
|
|
2675
|
+
return `${prefix} ID: ${payload.requestId} TIMESTAMP: ${payload.timestamp} (${new Date().toISOString()})\n\n${this._getWebSocketMessage(payload)}\n\n`;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
_logWebsocketMessages(message) {
|
|
2679
|
+
this.webSocketMessages += message;
|
|
2680
|
+
}
|
|
2462
2681
|
}
|
|
2463
2682
|
|
|
2464
2683
|
module.exports = Puppeteer;
|
package/lib/helper/WebDriver.js
CHANGED
|
@@ -2,10 +2,8 @@ let webdriverio;
|
|
|
2
2
|
|
|
3
3
|
const assert = require('assert');
|
|
4
4
|
const path = require('path');
|
|
5
|
-
const fs = require('fs');
|
|
6
5
|
|
|
7
6
|
const Helper = require('@codeceptjs/helper');
|
|
8
|
-
const crypto = require('crypto');
|
|
9
7
|
const promiseRetry = require('promise-retry');
|
|
10
8
|
const stringIncludes = require('../assert/include').includes;
|
|
11
9
|
const { urlEquals, equals } = require('../assert/equal');
|
|
@@ -30,13 +28,14 @@ const ElementNotFound = require('./errors/ElementNotFound');
|
|
|
30
28
|
const ConnectionRefused = require('./errors/ConnectionRefused');
|
|
31
29
|
const Locator = require('../locator');
|
|
32
30
|
const { highlightElement } = require('./scripts/highlightElement');
|
|
33
|
-
const store = require('../store');
|
|
34
31
|
const { focusElement } = require('./scripts/focusElement');
|
|
35
32
|
const { blurElement } = require('./scripts/blurElement');
|
|
36
33
|
const {
|
|
37
34
|
dontSeeElementError, seeElementError, seeElementInDOMError, dontSeeElementInDOMError,
|
|
38
35
|
} = require('./errors/ElementAssertion');
|
|
39
|
-
const {
|
|
36
|
+
const {
|
|
37
|
+
dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics,
|
|
38
|
+
} = require('./network/actions');
|
|
40
39
|
|
|
41
40
|
const SHADOW = 'shadow';
|
|
42
41
|
const webRoot = 'body';
|
|
@@ -2758,42 +2757,7 @@ class WebDriver extends Helper {
|
|
|
2758
2757
|
console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration');
|
|
2759
2758
|
return;
|
|
2760
2759
|
}
|
|
2761
|
-
|
|
2762
|
-
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
|
|
2763
|
-
}
|
|
2764
|
-
|
|
2765
|
-
const promises = this.requests.map(async (request) => {
|
|
2766
|
-
const resp = await request.response;
|
|
2767
|
-
|
|
2768
|
-
if (!resp) {
|
|
2769
|
-
return {
|
|
2770
|
-
url: '',
|
|
2771
|
-
response: {
|
|
2772
|
-
status: '',
|
|
2773
|
-
statusText: '',
|
|
2774
|
-
body: '',
|
|
2775
|
-
},
|
|
2776
|
-
};
|
|
2777
|
-
}
|
|
2778
|
-
|
|
2779
|
-
let body;
|
|
2780
|
-
try {
|
|
2781
|
-
// There's no 'body' for some requests (redirect etc...)
|
|
2782
|
-
body = JSON.parse((await resp.body()).toString());
|
|
2783
|
-
} catch (e) {
|
|
2784
|
-
// only interested in JSON, not HTML responses.
|
|
2785
|
-
}
|
|
2786
|
-
|
|
2787
|
-
return {
|
|
2788
|
-
url: resp.url(),
|
|
2789
|
-
response: {
|
|
2790
|
-
status: resp.status(),
|
|
2791
|
-
statusText: resp.statusText(),
|
|
2792
|
-
body,
|
|
2793
|
-
},
|
|
2794
|
-
};
|
|
2795
|
-
});
|
|
2796
|
-
return Promise.all(promises);
|
|
2760
|
+
return grabRecordedNetworkTraffics.call(this);
|
|
2797
2761
|
}
|
|
2798
2762
|
|
|
2799
2763
|
/**
|
|
@@ -2809,47 +2773,7 @@ class WebDriver extends Helper {
|
|
|
2809
2773
|
console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration');
|
|
2810
2774
|
return;
|
|
2811
2775
|
}
|
|
2812
|
-
|
|
2813
|
-
throw new Error('Missing required key "name" in object given to "I.seeTraffic".');
|
|
2814
|
-
}
|
|
2815
|
-
|
|
2816
|
-
if (!url) {
|
|
2817
|
-
throw new Error('Missing required key "url" in object given to "I.seeTraffic".');
|
|
2818
|
-
}
|
|
2819
|
-
|
|
2820
|
-
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
2821
|
-
throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
2822
|
-
}
|
|
2823
|
-
|
|
2824
|
-
for (let i = 0; i <= timeout * 2; i++) {
|
|
2825
|
-
const found = this._isInTraffic(url, parameters);
|
|
2826
|
-
if (found) {
|
|
2827
|
-
return true;
|
|
2828
|
-
}
|
|
2829
|
-
await new Promise((done) => {
|
|
2830
|
-
setTimeout(done, 1000);
|
|
2831
|
-
});
|
|
2832
|
-
}
|
|
2833
|
-
|
|
2834
|
-
// check request post data
|
|
2835
|
-
if (requestPostData && this._isInTraffic(url)) {
|
|
2836
|
-
const advancedTestResults = createAdvancedTestResults(url, requestPostData, this.requests);
|
|
2837
|
-
|
|
2838
|
-
assert.equal(advancedTestResults, true, `Traffic named "${name}" found correct URL ${url}, BUT the post data did not match:\n ${advancedTestResults}`);
|
|
2839
|
-
} else if (parameters && this._isInTraffic(url)) {
|
|
2840
|
-
const advancedTestResults = createAdvancedTestResults(url, parameters, this.requests);
|
|
2841
|
-
|
|
2842
|
-
assert.fail(
|
|
2843
|
-
`Traffic named "${name}" found correct URL ${url}, BUT the query parameters did not match:\n`
|
|
2844
|
-
+ `${advancedTestResults}`,
|
|
2845
|
-
);
|
|
2846
|
-
} else {
|
|
2847
|
-
assert.fail(
|
|
2848
|
-
`Traffic named "${name}" not found in recorded traffic within ${timeout} seconds.\n`
|
|
2849
|
-
+ `Expected url: ${url}.\n`
|
|
2850
|
-
+ `Recorded traffic:\n${this._getTrafficDump()}`,
|
|
2851
|
-
);
|
|
2852
|
-
}
|
|
2776
|
+
await seeTraffic.call(this, ...arguments);
|
|
2853
2777
|
}
|
|
2854
2778
|
|
|
2855
2779
|
/**
|
|
@@ -2864,69 +2788,7 @@ class WebDriver extends Helper {
|
|
|
2864
2788
|
console.log('* Switch to devtools protocol to use this command by setting devtoolsProtocol: true in the configuration');
|
|
2865
2789
|
return;
|
|
2866
2790
|
}
|
|
2867
|
-
|
|
2868
|
-
throw new Error('Failure in test automation. You use "I.dontSeeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
2869
|
-
}
|
|
2870
|
-
|
|
2871
|
-
if (!name) {
|
|
2872
|
-
throw new Error('Missing required key "name" in object given to "I.dontSeeTraffic".');
|
|
2873
|
-
}
|
|
2874
|
-
|
|
2875
|
-
if (!url) {
|
|
2876
|
-
throw new Error('Missing required key "url" in object given to "I.dontSeeTraffic".');
|
|
2877
|
-
}
|
|
2878
|
-
|
|
2879
|
-
if (this._isInTraffic(url)) {
|
|
2880
|
-
assert.fail(`Traffic with name "${name}" (URL: "${url}') found, but was not expected to be found.`);
|
|
2881
|
-
}
|
|
2882
|
-
}
|
|
2883
|
-
|
|
2884
|
-
/**
|
|
2885
|
-
* Checks if URL with parameters is part of network traffic. Returns true or false. Internal method for this helper.
|
|
2886
|
-
*
|
|
2887
|
-
* @param url URL to look for.
|
|
2888
|
-
* @param [parameters] Parameters that this URL needs to contain
|
|
2889
|
-
* @return {boolean} Whether or not URL with parameters is part of network traffic.
|
|
2890
|
-
* @private
|
|
2891
|
-
*/
|
|
2892
|
-
_isInTraffic(url, parameters) {
|
|
2893
|
-
let isInTraffic = false;
|
|
2894
|
-
this.requests.forEach((request) => {
|
|
2895
|
-
if (isInTraffic) {
|
|
2896
|
-
return; // We already found traffic. Continue with next request
|
|
2897
|
-
}
|
|
2898
|
-
|
|
2899
|
-
if (!request.url.match(new RegExp(url))) {
|
|
2900
|
-
return; // url not found in this request. continue with next request
|
|
2901
|
-
}
|
|
2902
|
-
|
|
2903
|
-
// URL has matched. Now we check the parameters
|
|
2904
|
-
|
|
2905
|
-
if (parameters) {
|
|
2906
|
-
const advancedReport = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), parameters);
|
|
2907
|
-
if (advancedReport === true) {
|
|
2908
|
-
isInTraffic = true;
|
|
2909
|
-
}
|
|
2910
|
-
} else {
|
|
2911
|
-
isInTraffic = true;
|
|
2912
|
-
}
|
|
2913
|
-
});
|
|
2914
|
-
|
|
2915
|
-
return isInTraffic;
|
|
2916
|
-
}
|
|
2917
|
-
|
|
2918
|
-
/**
|
|
2919
|
-
* Returns all URLs of all network requests recorded so far during execution of test scenario.
|
|
2920
|
-
*
|
|
2921
|
-
* @return {string} List of URLs recorded as a string, separated by new lines after each URL
|
|
2922
|
-
* @private
|
|
2923
|
-
*/
|
|
2924
|
-
_getTrafficDump() {
|
|
2925
|
-
let dumpedTraffic = '';
|
|
2926
|
-
this.requests.forEach((request) => {
|
|
2927
|
-
dumpedTraffic += `${request.method} - ${request.url}\n`;
|
|
2928
|
-
});
|
|
2929
|
-
return dumpedTraffic;
|
|
2791
|
+
dontSeeTraffic.call(this, ...arguments);
|
|
2930
2792
|
}
|
|
2931
2793
|
}
|
|
2932
2794
|
|
|
@@ -20,6 +20,11 @@ async function findVue(matcher, locator) {
|
|
|
20
20
|
return matcher.locator(_locator).all();
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
async function findByPlaywrightLocator(matcher, locator) {
|
|
24
|
+
if (locator && locator.toString().includes(process.env.testIdAttribute)) return matcher.getByTestId(locator.pw.value.split('=')[1]);
|
|
25
|
+
return matcher.locator(locator.pw).all();
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
function propBuilder(props) {
|
|
24
29
|
let _props = '';
|
|
25
30
|
|
|
@@ -35,4 +40,4 @@ function propBuilder(props) {
|
|
|
35
40
|
return _props;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
|
-
module.exports = { findReact, findVue };
|
|
43
|
+
module.exports = { findReact, findVue, findByPlaywrightLocator };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const assert = require('assert');
|
|
2
|
+
const { isInTraffic, createAdvancedTestResults, getTrafficDump } = require('./utils');
|
|
3
|
+
|
|
4
|
+
function dontSeeTraffic({ name, url }) {
|
|
5
|
+
if (!this.recordedAtLeastOnce) {
|
|
6
|
+
throw new Error('Failure in test automation. You use "I.dontSeeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (!name) {
|
|
10
|
+
throw new Error('Missing required key "name" in object given to "I.dontSeeTraffic".');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!url) {
|
|
14
|
+
throw new Error('Missing required key "url" in object given to "I.dontSeeTraffic".');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (isInTraffic.call(this, url)) {
|
|
18
|
+
assert.fail(`Traffic with name "${name}" (URL: "${url}') found, but was not expected to be found.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function seeTraffic({
|
|
23
|
+
name, url, parameters, requestPostData, timeout = 10,
|
|
24
|
+
}) {
|
|
25
|
+
if (!name) {
|
|
26
|
+
throw new Error('Missing required key "name" in object given to "I.seeTraffic".');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!url) {
|
|
30
|
+
throw new Error('Missing required key "url" in object given to "I.seeTraffic".');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
34
|
+
throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i <= timeout * 2; i++) {
|
|
38
|
+
const found = isInTraffic.call(this, url, parameters);
|
|
39
|
+
if (found) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
await new Promise((done) => {
|
|
43
|
+
setTimeout(done, 1000);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// check request post data
|
|
48
|
+
if (requestPostData && isInTraffic.call(this, url)) {
|
|
49
|
+
const advancedTestResults = createAdvancedTestResults(url, requestPostData, this.requests);
|
|
50
|
+
|
|
51
|
+
assert.equal(advancedTestResults, true, `Traffic named "${name}" found correct URL ${url}, BUT the post data did not match:\n ${advancedTestResults}`);
|
|
52
|
+
} else if (parameters && isInTraffic.call(this, url)) {
|
|
53
|
+
const advancedTestResults = createAdvancedTestResults(url, parameters, this.requests);
|
|
54
|
+
|
|
55
|
+
assert.fail(
|
|
56
|
+
`Traffic named "${name}" found correct URL ${url}, BUT the query parameters did not match:\n`
|
|
57
|
+
+ `${advancedTestResults}`,
|
|
58
|
+
);
|
|
59
|
+
} else {
|
|
60
|
+
assert.fail(
|
|
61
|
+
`Traffic named "${name}" not found in recorded traffic within ${timeout} seconds.\n`
|
|
62
|
+
+ `Expected url: ${url}.\n`
|
|
63
|
+
+ `Recorded traffic:\n${getTrafficDump.call(this)}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function grabRecordedNetworkTraffics() {
|
|
69
|
+
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
70
|
+
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const promises = this.requests.map(async (request) => {
|
|
74
|
+
const resp = await request.response;
|
|
75
|
+
|
|
76
|
+
if (!resp) {
|
|
77
|
+
return {
|
|
78
|
+
url: '',
|
|
79
|
+
response: {
|
|
80
|
+
status: '',
|
|
81
|
+
statusText: '',
|
|
82
|
+
body: '',
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let body;
|
|
88
|
+
try {
|
|
89
|
+
// There's no 'body' for some requests (redirect etc...)
|
|
90
|
+
body = JSON.parse((await resp.body()).toString());
|
|
91
|
+
} catch (e) {
|
|
92
|
+
// only interested in JSON, not HTML responses.
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
url: resp.url(),
|
|
97
|
+
response: {
|
|
98
|
+
status: resp.status(),
|
|
99
|
+
statusText: resp.statusText(),
|
|
100
|
+
body,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
return Promise.all(promises);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function stopRecordingTraffic() {
|
|
108
|
+
// @ts-ignore
|
|
109
|
+
this.page.removeAllListeners('request');
|
|
110
|
+
this.recording = false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function flushNetworkTraffics() {
|
|
114
|
+
this.requests = [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = {
|
|
118
|
+
dontSeeTraffic,
|
|
119
|
+
seeTraffic,
|
|
120
|
+
grabRecordedNetworkTraffics,
|
|
121
|
+
stopRecordingTraffic,
|
|
122
|
+
flushNetworkTraffics,
|
|
123
|
+
};
|
|
@@ -129,9 +129,59 @@ const allRequestPostDataValuePairsMatchExtreme = (RequestPostDataObject, advance
|
|
|
129
129
|
return success ? true : littleReport;
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Returns all URLs of all network requests recorded so far during execution of test scenario.
|
|
134
|
+
*
|
|
135
|
+
* @return {string} List of URLs recorded as a string, separated by new lines after each URL
|
|
136
|
+
* @private
|
|
137
|
+
*/
|
|
138
|
+
function getTrafficDump() {
|
|
139
|
+
let dumpedTraffic = '';
|
|
140
|
+
this.requests.forEach((request) => {
|
|
141
|
+
dumpedTraffic += `${request.method} - ${request.url}\n`;
|
|
142
|
+
});
|
|
143
|
+
return dumpedTraffic;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Checks if URL with parameters is part of network traffic. Returns true or false. Internal method for this helper.
|
|
148
|
+
*
|
|
149
|
+
* @param url URL to look for.
|
|
150
|
+
* @param [parameters] Parameters that this URL needs to contain
|
|
151
|
+
* @return {boolean} Whether or not URL with parameters is part of network traffic.
|
|
152
|
+
* @private
|
|
153
|
+
*/
|
|
154
|
+
function isInTraffic(url, parameters) {
|
|
155
|
+
let isInTraffic = false;
|
|
156
|
+
this.requests.forEach((request) => {
|
|
157
|
+
if (isInTraffic) {
|
|
158
|
+
return; // We already found traffic. Continue with next request
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (!request.url.match(new RegExp(url))) {
|
|
162
|
+
return; // url not found in this request. continue with next request
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// URL has matched. Now we check the parameters
|
|
166
|
+
|
|
167
|
+
if (parameters) {
|
|
168
|
+
const advancedReport = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), parameters);
|
|
169
|
+
if (advancedReport === true) {
|
|
170
|
+
isInTraffic = true;
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
isInTraffic = true;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return isInTraffic;
|
|
178
|
+
}
|
|
179
|
+
|
|
132
180
|
module.exports = {
|
|
133
181
|
createAdvancedTestResults,
|
|
134
182
|
extractQueryObjects,
|
|
135
183
|
allParameterValuePairsMatchExtreme,
|
|
136
184
|
allRequestPostDataValuePairsMatchExtreme,
|
|
185
|
+
getTrafficDump,
|
|
186
|
+
isInTraffic,
|
|
137
187
|
};
|
package/lib/index.js
CHANGED
package/lib/listener/steps.js
CHANGED
|
@@ -67,7 +67,6 @@ module.exports = function () {
|
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
event.dispatcher.on(event.step.started, (step) => {
|
|
70
|
-
if (store.debugMode) return;
|
|
71
70
|
step.startedAt = +new Date();
|
|
72
71
|
step.test = currentTest;
|
|
73
72
|
if (currentHook && Array.isArray(currentHook.steps)) {
|
|
@@ -78,7 +77,6 @@ module.exports = function () {
|
|
|
78
77
|
});
|
|
79
78
|
|
|
80
79
|
event.dispatcher.on(event.step.finished, (step) => {
|
|
81
|
-
if (store.debugMode) return;
|
|
82
80
|
step.finishedAt = +new Date();
|
|
83
81
|
if (step.startedAt) step.duration = step.finishedAt - step.startedAt;
|
|
84
82
|
debug(`Step '${step}' finished; Duration: ${step.duration || 0}ms`);
|