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/heal.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const debug = require('debug')('codeceptjs:heal');
|
|
2
|
+
const colors = require('chalk');
|
|
3
|
+
const Container = require('./container');
|
|
4
|
+
const recorder = require('./recorder');
|
|
5
|
+
const output = require('./output');
|
|
6
|
+
const event = require('./event');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @class
|
|
10
|
+
*/
|
|
11
|
+
class Heal {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.recipes = {};
|
|
14
|
+
this.fixes = [];
|
|
15
|
+
this.prepareFns = [];
|
|
16
|
+
this.contextName = null;
|
|
17
|
+
this.numHealed = 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
clear() {
|
|
21
|
+
this.recipes = {};
|
|
22
|
+
this.fixes = [];
|
|
23
|
+
this.prepareFns = [];
|
|
24
|
+
this.contextName = null;
|
|
25
|
+
this.numHealed = 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
addRecipe(name, opts = {}) {
|
|
29
|
+
if (!opts.priority) opts.priority = 0;
|
|
30
|
+
|
|
31
|
+
if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`);
|
|
32
|
+
|
|
33
|
+
this.recipes[name] = opts;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
connectToEvents() {
|
|
37
|
+
event.dispatcher.on(event.suite.before, (suite) => {
|
|
38
|
+
this.contextName = suite.title;
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
event.dispatcher.on(event.test.started, (test) => {
|
|
42
|
+
this.contextName = test.fullTitle();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
event.dispatcher.on(event.test.finished, () => {
|
|
46
|
+
this.contextName = null;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
hasCorrespondingRecipes(step) {
|
|
51
|
+
return matchRecipes(this.recipes, this.contextName)
|
|
52
|
+
.filter(r => !r.steps || r.steps.includes(step.name))
|
|
53
|
+
.length > 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async getCodeSuggestions(context) {
|
|
57
|
+
const suggestions = [];
|
|
58
|
+
const recipes = matchRecipes(this.recipes, this.contextName);
|
|
59
|
+
|
|
60
|
+
debug('Recipes', recipes);
|
|
61
|
+
|
|
62
|
+
const currentOutputLevel = output.level();
|
|
63
|
+
output.level(0);
|
|
64
|
+
|
|
65
|
+
for (const [property, prepareFn] of Object.entries(recipes.map(r => r.prepare).filter(p => !!p).reduce((acc, obj) => ({ ...acc, ...obj }), {}))) {
|
|
66
|
+
if (!prepareFn) continue;
|
|
67
|
+
|
|
68
|
+
if (context[property]) continue;
|
|
69
|
+
context[property] = await prepareFn(Container.support());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
output.level(currentOutputLevel);
|
|
73
|
+
|
|
74
|
+
for (const recipe of recipes) {
|
|
75
|
+
let snippets = await recipe.fn(context);
|
|
76
|
+
if (!Array.isArray(snippets)) snippets = [snippets];
|
|
77
|
+
|
|
78
|
+
suggestions.push({
|
|
79
|
+
name: recipe.name,
|
|
80
|
+
snippets,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return suggestions.filter(s => !isBlank(s.snippets));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async healStep(failedStep, error, failureContext = {}) {
|
|
88
|
+
output.debug(`Trying to heal ${failedStep.toCode()} step`);
|
|
89
|
+
|
|
90
|
+
Object.assign(failureContext, {
|
|
91
|
+
error,
|
|
92
|
+
step: failedStep,
|
|
93
|
+
prevSteps: failureContext?.test?.steps?.slice(0, -1) || [],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const suggestions = await this.getCodeSuggestions(failureContext);
|
|
97
|
+
|
|
98
|
+
if (suggestions.length === 0) {
|
|
99
|
+
debug('No healing suggestions found');
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`);
|
|
104
|
+
|
|
105
|
+
debug(suggestions);
|
|
106
|
+
|
|
107
|
+
for (const suggestion of suggestions) {
|
|
108
|
+
for (const codeSnippet of suggestion.snippets) {
|
|
109
|
+
try {
|
|
110
|
+
debug('Executing', codeSnippet);
|
|
111
|
+
recorder.catch((e) => {
|
|
112
|
+
debug(e);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
if (typeof codeSnippet === 'string') {
|
|
116
|
+
const I = Container.support('I'); // eslint-disable-line
|
|
117
|
+
await eval(codeSnippet); // eslint-disable-line
|
|
118
|
+
} else if (typeof codeSnippet === 'function') {
|
|
119
|
+
await codeSnippet(Container.support());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.fixes.push({
|
|
123
|
+
recipe: suggestion.name,
|
|
124
|
+
test: failureContext?.test,
|
|
125
|
+
step: failedStep,
|
|
126
|
+
snippet: codeSnippet,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
recorder.add('healed', () => output.print(colors.bold.green(` Code healed successfully by ${suggestion.name}`), colors.gray('(no errors thrown)')));
|
|
130
|
+
this.numHealed++;
|
|
131
|
+
// recorder.session.restore();
|
|
132
|
+
return;
|
|
133
|
+
} catch (err) {
|
|
134
|
+
debug('Failed to execute code', err);
|
|
135
|
+
recorder.ignoreErr(err); // healing did not help
|
|
136
|
+
recorder.catchWithoutStop(err);
|
|
137
|
+
await recorder.promise(); // wait for all promises to resolve
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
output.debug(`Couldn't heal the code for ${failedStep.toCode()}`);
|
|
142
|
+
recorder.throw(error);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
static setDefaultHealers() {
|
|
146
|
+
require('./template/heal');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const heal = new Heal();
|
|
151
|
+
|
|
152
|
+
module.exports = heal;
|
|
153
|
+
|
|
154
|
+
function matchRecipes(recipes, contextName) {
|
|
155
|
+
return Object.entries(recipes)
|
|
156
|
+
.filter(([, recipe]) => !contextName || !recipe.grep || new RegExp(recipe.grep).test(contextName))
|
|
157
|
+
.sort(([, a], [, b]) => b.priority - a.priority)
|
|
158
|
+
.map(([name, recipe]) => {
|
|
159
|
+
recipe.name = name;
|
|
160
|
+
return recipe;
|
|
161
|
+
})
|
|
162
|
+
.filter(r => !!r.fn);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isBlank(value) {
|
|
166
|
+
return (
|
|
167
|
+
value == null
|
|
168
|
+
|| (Array.isArray(value) && value.length === 0)
|
|
169
|
+
|| (typeof value === 'object' && Object.keys(value).length === 0)
|
|
170
|
+
|| (typeof value === 'string' && value.trim() === '')
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
const Helper = require('@codeceptjs/helper');
|
|
2
|
-
const
|
|
2
|
+
const ai = require('../ai');
|
|
3
3
|
const standardActingHelpers = require('../plugin/standardActingHelpers');
|
|
4
4
|
const Container = require('../container');
|
|
5
5
|
const { splitByChunks, minifyHtml } = require('../html');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* AI Helper for CodeceptJS.
|
|
9
9
|
*
|
|
10
|
-
* This helper class provides integration with the
|
|
10
|
+
* This helper class provides integration with the AI GPT-3.5 or 4 language model for generating responses to questions or prompts within the context of web pages. It allows you to interact with the GPT-3.5 model to obtain intelligent responses based on HTML fragments or general prompts.
|
|
11
11
|
* This helper should be enabled with any web helpers like Playwright or Puppeteer or WebDrvier to ensure the HTML context is available.
|
|
12
12
|
*
|
|
13
13
|
* ## Configuration
|
|
14
14
|
*
|
|
15
15
|
* This helper should be configured in codecept.json or codecept.conf.js
|
|
16
16
|
*
|
|
17
|
-
* * `chunkSize`: (optional, default: 80000) - The maximum number of characters to send to the
|
|
17
|
+
* * `chunkSize`: (optional, default: 80000) - The maximum number of characters to send to the AI API at once. We split HTML fragments by 8000 chars to not exceed token limit. Increase this value if you use GPT-4.
|
|
18
18
|
*/
|
|
19
|
-
class
|
|
19
|
+
class AI extends Helper {
|
|
20
20
|
constructor(config) {
|
|
21
21
|
super(config);
|
|
22
|
-
this.aiAssistant =
|
|
22
|
+
this.aiAssistant = ai;
|
|
23
23
|
|
|
24
24
|
this.options = {
|
|
25
25
|
chunkSize: 80000,
|
|
@@ -39,7 +39,7 @@ class OpenAI extends Helper {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Asks the
|
|
42
|
+
* Asks the AI GPT language model a question based on the provided prompt within the context of the current page's HTML.
|
|
43
43
|
*
|
|
44
44
|
* ```js
|
|
45
45
|
* I.askGptOnPage('what does this page do?');
|
|
@@ -77,7 +77,7 @@ class OpenAI extends Helper {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
* Asks the
|
|
80
|
+
* Asks the AI a question based on the provided prompt within the context of a specific HTML fragment on the current page.
|
|
81
81
|
*
|
|
82
82
|
* ```js
|
|
83
83
|
* I.askGptOnPageFragment('describe features of this screen', '.screen');
|
|
@@ -113,9 +113,7 @@ class OpenAI extends Helper {
|
|
|
113
113
|
{ role: 'user', content: prompt },
|
|
114
114
|
];
|
|
115
115
|
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
const response = completion?.data?.choices[0]?.message?.content;
|
|
116
|
+
const response = await this.aiAssistant.createCompletion(messages);
|
|
119
117
|
|
|
120
118
|
console.log(response);
|
|
121
119
|
|
|
@@ -123,4 +121,4 @@ class OpenAI extends Helper {
|
|
|
123
121
|
}
|
|
124
122
|
}
|
|
125
123
|
|
|
126
|
-
module.exports =
|
|
124
|
+
module.exports = AI;
|
package/lib/helper/Playwright.js
CHANGED
|
@@ -33,7 +33,7 @@ const ElementNotFound = require('./errors/ElementNotFound');
|
|
|
33
33
|
const RemoteBrowserConnectionRefused = require('./errors/RemoteBrowserConnectionRefused');
|
|
34
34
|
const Popup = require('./extras/Popup');
|
|
35
35
|
const Console = require('./extras/Console');
|
|
36
|
-
const { findReact, findVue } = require('./extras/PlaywrightReactVueLocator');
|
|
36
|
+
const { findReact, findVue, findByPlaywrightLocator } = require('./extras/PlaywrightReactVueLocator');
|
|
37
37
|
|
|
38
38
|
let playwright;
|
|
39
39
|
let perfTiming;
|
|
@@ -50,8 +50,9 @@ const { createValueEngine, createDisabledEngine } = require('./extras/Playwright
|
|
|
50
50
|
const {
|
|
51
51
|
seeElementError, dontSeeElementError, dontSeeElementInDOMError, seeElementInDOMError,
|
|
52
52
|
} = require('./errors/ElementAssertion');
|
|
53
|
-
const {
|
|
54
|
-
|
|
53
|
+
const {
|
|
54
|
+
dontSeeTraffic, seeTraffic, grabRecordedNetworkTraffics, stopRecordingTraffic, flushNetworkTraffics,
|
|
55
|
+
} = require('./network/actions');
|
|
55
56
|
|
|
56
57
|
const pathSeparator = path.sep;
|
|
57
58
|
|
|
@@ -100,6 +101,7 @@ const pathSeparator = path.sep;
|
|
|
100
101
|
* @prop {boolean} [bypassCSP] - bypass Content Security Policy or CSP
|
|
101
102
|
* @prop {boolean} [highlightElement] - highlight the interacting elements. Default: false. Note: only activate under verbose mode (--verbose).
|
|
102
103
|
* @prop {object} [recordHar] - record HAR and will be saved to `output/har`. See more of [HAR options](https://playwright.dev/docs/api/class-browser#browser-new-context-option-record-har).
|
|
104
|
+
* @prop {string} [testIdAttribute=data-testid] - locate elements based on the testIdAttribute. See more of [locate by test id](https://playwright.dev/docs/locators#locate-by-test-id).
|
|
103
105
|
*/
|
|
104
106
|
const config = {};
|
|
105
107
|
|
|
@@ -379,6 +381,7 @@ class Playwright extends Helper {
|
|
|
379
381
|
highlightElement: false,
|
|
380
382
|
};
|
|
381
383
|
|
|
384
|
+
process.env.testIdAttribute = 'data-testid';
|
|
382
385
|
config = Object.assign(defaults, config);
|
|
383
386
|
|
|
384
387
|
if (availableBrowsers.indexOf(config.browser) < 0) {
|
|
@@ -464,6 +467,7 @@ class Playwright extends Helper {
|
|
|
464
467
|
try {
|
|
465
468
|
await playwright.selectors.register('__value', createValueEngine);
|
|
466
469
|
await playwright.selectors.register('__disabled', createDisabledEngine);
|
|
470
|
+
if (process.env.testIdAttribute) await playwright.selectors.setTestIdAttribute(process.env.testIdAttribute);
|
|
467
471
|
} catch (e) {
|
|
468
472
|
console.warn(e);
|
|
469
473
|
}
|
|
@@ -2468,7 +2472,7 @@ class Playwright extends Helper {
|
|
|
2468
2472
|
async waitNumberOfVisibleElements(locator, num, sec) {
|
|
2469
2473
|
const waitTimeout = sec ? sec * 1000 : this.options.waitForTimeout;
|
|
2470
2474
|
locator = new Locator(locator, 'css');
|
|
2471
|
-
|
|
2475
|
+
|
|
2472
2476
|
let waiter;
|
|
2473
2477
|
const context = await this._getContext();
|
|
2474
2478
|
if (locator.isCSS()) {
|
|
@@ -2957,7 +2961,7 @@ class Playwright extends Helper {
|
|
|
2957
2961
|
* This method allows intercepting and mocking requests & responses. [Learn more about it](https://playwright.dev/docs/network#handle-requests)
|
|
2958
2962
|
*
|
|
2959
2963
|
* @param {string|RegExp} [url] URL, regex or pattern for to match URL
|
|
2960
|
-
* @param {function} [handler] a function to process
|
|
2964
|
+
* @param {function} [handler] a function to process request
|
|
2961
2965
|
*/
|
|
2962
2966
|
async mockRoute(url, handler) {
|
|
2963
2967
|
return this.browserContext.route(...arguments);
|
|
@@ -2973,7 +2977,7 @@ class Playwright extends Helper {
|
|
|
2973
2977
|
* If no handler is passed, all mock requests for the rote are disabled.
|
|
2974
2978
|
*
|
|
2975
2979
|
* @param {string|RegExp} [url] URL, regex or pattern for to match URL
|
|
2976
|
-
* @param {function} [handler] a function to process
|
|
2980
|
+
* @param {function} [handler] a function to process request
|
|
2977
2981
|
*/
|
|
2978
2982
|
async stopMockingRoute(url, handler) {
|
|
2979
2983
|
return this.browserContext.unroute(...arguments);
|
|
@@ -3007,37 +3011,6 @@ class Playwright extends Helper {
|
|
|
3007
3011
|
});
|
|
3008
3012
|
}
|
|
3009
3013
|
|
|
3010
|
-
/**
|
|
3011
|
-
* {{> grabRecordedNetworkTraffics }}
|
|
3012
|
-
*/
|
|
3013
|
-
async grabRecordedNetworkTraffics() {
|
|
3014
|
-
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
3015
|
-
throw new Error('Failure in test automation. You use "I.grabRecordedNetworkTraffics", but "I.startRecordingTraffic" was never called before.');
|
|
3016
|
-
}
|
|
3017
|
-
|
|
3018
|
-
const promises = this.requests.map(async (request) => {
|
|
3019
|
-
const resp = await request.response;
|
|
3020
|
-
let body;
|
|
3021
|
-
try {
|
|
3022
|
-
// There's no 'body' for some requests (redirect etc...)
|
|
3023
|
-
body = JSON.parse((await resp.body()).toString());
|
|
3024
|
-
} catch (e) {
|
|
3025
|
-
// only interested in JSON, not HTML responses.
|
|
3026
|
-
}
|
|
3027
|
-
|
|
3028
|
-
return {
|
|
3029
|
-
url: resp.url(),
|
|
3030
|
-
response: {
|
|
3031
|
-
status: resp.status(),
|
|
3032
|
-
statusText: resp.statusText(),
|
|
3033
|
-
body,
|
|
3034
|
-
},
|
|
3035
|
-
};
|
|
3036
|
-
});
|
|
3037
|
-
|
|
3038
|
-
return Promise.all(promises);
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
3014
|
/**
|
|
3042
3015
|
* Blocks traffic of a given URL or a list of URLs.
|
|
3043
3016
|
*
|
|
@@ -3117,67 +3090,19 @@ class Playwright extends Helper {
|
|
|
3117
3090
|
}
|
|
3118
3091
|
|
|
3119
3092
|
/**
|
|
3093
|
+
*
|
|
3120
3094
|
* {{> flushNetworkTraffics }}
|
|
3121
3095
|
*/
|
|
3122
3096
|
flushNetworkTraffics() {
|
|
3123
|
-
this
|
|
3097
|
+
flushNetworkTraffics.call(this);
|
|
3124
3098
|
}
|
|
3125
3099
|
|
|
3126
3100
|
/**
|
|
3101
|
+
*
|
|
3127
3102
|
* {{> stopRecordingTraffic }}
|
|
3128
3103
|
*/
|
|
3129
3104
|
stopRecordingTraffic() {
|
|
3130
|
-
|
|
3131
|
-
this.recording = false;
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
/**
|
|
3135
|
-
* {{> seeTraffic }}
|
|
3136
|
-
*/
|
|
3137
|
-
async seeTraffic({
|
|
3138
|
-
name, url, parameters, requestPostData, timeout = 10,
|
|
3139
|
-
}) {
|
|
3140
|
-
if (!name) {
|
|
3141
|
-
throw new Error('Missing required key "name" in object given to "I.seeTraffic".');
|
|
3142
|
-
}
|
|
3143
|
-
|
|
3144
|
-
if (!url) {
|
|
3145
|
-
throw new Error('Missing required key "url" in object given to "I.seeTraffic".');
|
|
3146
|
-
}
|
|
3147
|
-
|
|
3148
|
-
if (!this.recording || !this.recordedAtLeastOnce) {
|
|
3149
|
-
throw new Error('Failure in test automation. You use "I.seeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
3150
|
-
}
|
|
3151
|
-
|
|
3152
|
-
for (let i = 0; i <= timeout * 2; i++) {
|
|
3153
|
-
const found = this._isInTraffic(url, parameters);
|
|
3154
|
-
if (found) {
|
|
3155
|
-
return true;
|
|
3156
|
-
}
|
|
3157
|
-
await new Promise((done) => {
|
|
3158
|
-
setTimeout(done, 1000);
|
|
3159
|
-
});
|
|
3160
|
-
}
|
|
3161
|
-
|
|
3162
|
-
// check request post data
|
|
3163
|
-
if (requestPostData && this._isInTraffic(url)) {
|
|
3164
|
-
const advancedTestResults = createAdvancedTestResults(url, requestPostData, this.requests);
|
|
3165
|
-
|
|
3166
|
-
assert.equal(advancedTestResults, true, `Traffic named "${name}" found correct URL ${url}, BUT the post data did not match:\n ${advancedTestResults}`);
|
|
3167
|
-
} else if (parameters && this._isInTraffic(url)) {
|
|
3168
|
-
const advancedTestResults = createAdvancedTestResults(url, parameters, this.requests);
|
|
3169
|
-
|
|
3170
|
-
assert.fail(
|
|
3171
|
-
`Traffic named "${name}" found correct URL ${url}, BUT the query parameters did not match:\n`
|
|
3172
|
-
+ `${advancedTestResults}`,
|
|
3173
|
-
);
|
|
3174
|
-
} else {
|
|
3175
|
-
assert.fail(
|
|
3176
|
-
`Traffic named "${name}" not found in recorded traffic within ${timeout} seconds.\n`
|
|
3177
|
-
+ `Expected url: ${url}.\n`
|
|
3178
|
-
+ `Recorded traffic:\n${this._getTrafficDump()}`,
|
|
3179
|
-
);
|
|
3180
|
-
}
|
|
3105
|
+
stopRecordingTraffic.call(this);
|
|
3181
3106
|
}
|
|
3182
3107
|
|
|
3183
3108
|
/**
|
|
@@ -3214,83 +3139,34 @@ class Playwright extends Helper {
|
|
|
3214
3139
|
}
|
|
3215
3140
|
|
|
3216
3141
|
/**
|
|
3217
|
-
* {{> dontSeeTraffic }}
|
|
3218
3142
|
*
|
|
3143
|
+
* {{> grabRecordedNetworkTraffics }}
|
|
3219
3144
|
*/
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
throw new Error('Failure in test automation. You use "I.dontSeeTraffic", but "I.startRecordingTraffic" was never called before.');
|
|
3223
|
-
}
|
|
3224
|
-
|
|
3225
|
-
if (!name) {
|
|
3226
|
-
throw new Error('Missing required key "name" in object given to "I.dontSeeTraffic".');
|
|
3227
|
-
}
|
|
3228
|
-
|
|
3229
|
-
if (!url) {
|
|
3230
|
-
throw new Error('Missing required key "url" in object given to "I.dontSeeTraffic".');
|
|
3231
|
-
}
|
|
3232
|
-
|
|
3233
|
-
if (this._isInTraffic(url)) {
|
|
3234
|
-
assert.fail(`Traffic with name "${name}" (URL: "${url}') found, but was not expected to be found.`);
|
|
3235
|
-
}
|
|
3145
|
+
async grabRecordedNetworkTraffics() {
|
|
3146
|
+
return grabRecordedNetworkTraffics.call(this);
|
|
3236
3147
|
}
|
|
3237
3148
|
|
|
3238
3149
|
/**
|
|
3239
|
-
* Checks if URL with parameters is part of network traffic. Returns true or false. Internal method for this helper.
|
|
3240
3150
|
*
|
|
3241
|
-
*
|
|
3242
|
-
* @param [parameters] Parameters that this URL needs to contain
|
|
3243
|
-
* @return {boolean} Whether or not URL with parameters is part of network traffic.
|
|
3244
|
-
* @private
|
|
3151
|
+
* {{> seeTraffic }}
|
|
3245
3152
|
*/
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
return; // We already found traffic. Continue with next request
|
|
3251
|
-
}
|
|
3252
|
-
|
|
3253
|
-
if (!request.url.match(new RegExp(url))) {
|
|
3254
|
-
return; // url not found in this request. continue with next request
|
|
3255
|
-
}
|
|
3256
|
-
|
|
3257
|
-
// URL has matched. Now we check the parameters
|
|
3258
|
-
|
|
3259
|
-
if (parameters) {
|
|
3260
|
-
const advancedReport = allParameterValuePairsMatchExtreme(extractQueryObjects(request.url), parameters);
|
|
3261
|
-
if (advancedReport === true) {
|
|
3262
|
-
isInTraffic = true;
|
|
3263
|
-
}
|
|
3264
|
-
} else {
|
|
3265
|
-
isInTraffic = true;
|
|
3266
|
-
}
|
|
3267
|
-
});
|
|
3268
|
-
|
|
3269
|
-
return isInTraffic;
|
|
3153
|
+
async seeTraffic({
|
|
3154
|
+
name, url, parameters, requestPostData, timeout = 10,
|
|
3155
|
+
}) {
|
|
3156
|
+
await seeTraffic.call(this, ...arguments);
|
|
3270
3157
|
}
|
|
3271
3158
|
|
|
3272
3159
|
/**
|
|
3273
|
-
* Returns all URLs of all network requests recorded so far during execution of test scenario.
|
|
3274
3160
|
*
|
|
3275
|
-
*
|
|
3276
|
-
*
|
|
3161
|
+
* {{> dontSeeTraffic }}
|
|
3162
|
+
*
|
|
3277
3163
|
*/
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
this.requests.forEach((request) => {
|
|
3281
|
-
dumpedTraffic += `${request.method} - ${request.url}\n`;
|
|
3282
|
-
});
|
|
3283
|
-
return dumpedTraffic;
|
|
3164
|
+
dontSeeTraffic({ name, url }) {
|
|
3165
|
+
dontSeeTraffic.call(this, ...arguments);
|
|
3284
3166
|
}
|
|
3285
3167
|
|
|
3286
3168
|
/**
|
|
3287
|
-
*
|
|
3288
|
-
* This also resets recorded websocket messages.
|
|
3289
|
-
*
|
|
3290
|
-
* ```js
|
|
3291
|
-
* await I.startRecordingWebSocketMessages();
|
|
3292
|
-
* ```
|
|
3293
|
-
*
|
|
3169
|
+
* {{> startRecordingWebSocketMessages }}
|
|
3294
3170
|
*/
|
|
3295
3171
|
async startRecordingWebSocketMessages() {
|
|
3296
3172
|
this.flushWebSocketMessages();
|
|
@@ -3324,11 +3200,7 @@ class Playwright extends Helper {
|
|
|
3324
3200
|
}
|
|
3325
3201
|
|
|
3326
3202
|
/**
|
|
3327
|
-
*
|
|
3328
|
-
*
|
|
3329
|
-
* ```js
|
|
3330
|
-
* await I.stopRecordingWebSocketMessages();
|
|
3331
|
-
* ```
|
|
3203
|
+
* {{> stopRecordingWebSocketMessages }}
|
|
3332
3204
|
*/
|
|
3333
3205
|
async stopRecordingWebSocketMessages() {
|
|
3334
3206
|
await this.cdpSession.send('Network.disable');
|
|
@@ -3455,6 +3327,7 @@ function buildLocatorString(locator) {
|
|
|
3455
3327
|
async function findElements(matcher, locator) {
|
|
3456
3328
|
if (locator.react) return findReact(matcher, locator);
|
|
3457
3329
|
if (locator.vue) return findVue(matcher, locator);
|
|
3330
|
+
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator);
|
|
3458
3331
|
locator = new Locator(locator, 'css');
|
|
3459
3332
|
|
|
3460
3333
|
return matcher.locator(buildLocatorString(locator)).all();
|
|
@@ -3462,6 +3335,8 @@ async function findElements(matcher, locator) {
|
|
|
3462
3335
|
|
|
3463
3336
|
async function findElement(matcher, locator) {
|
|
3464
3337
|
if (locator.react) return findReact(matcher, locator);
|
|
3338
|
+
if (locator.vue) return findVue(matcher, locator);
|
|
3339
|
+
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator);
|
|
3465
3340
|
locator = new Locator(locator, 'css');
|
|
3466
3341
|
|
|
3467
3342
|
return matcher.locator(buildLocatorString(locator)).first();
|
|
@@ -3517,6 +3392,7 @@ async function proceedClick(locator, context = null, options = {}) {
|
|
|
3517
3392
|
async function findClickable(matcher, locator) {
|
|
3518
3393
|
if (locator.react) return findReact(matcher, locator);
|
|
3519
3394
|
if (locator.vue) return findVue(matcher, locator);
|
|
3395
|
+
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator);
|
|
3520
3396
|
|
|
3521
3397
|
locator = new Locator(locator);
|
|
3522
3398
|
if (!locator.isFuzzy()) return findElements.call(this, matcher, locator);
|