codeceptjs 3.7.0-beta.9 → 3.7.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 +1 -1
- package/lib/codecept.js +14 -12
- package/lib/command/check.js +33 -9
- package/lib/command/definitions.js +1 -1
- package/lib/command/gherkin/snippets.js +69 -69
- package/lib/command/interactive.js +1 -1
- package/lib/command/run-multiple/chunk.js +48 -45
- package/lib/container.js +14 -7
- package/lib/effects.js +7 -2
- package/lib/event.js +2 -0
- package/lib/helper/AI.js +2 -1
- package/lib/helper/Playwright.js +1 -1
- package/lib/helper/Puppeteer.js +1 -1
- package/lib/helper/extras/Popup.js +22 -22
- package/lib/mocha/asyncWrapper.js +3 -1
- package/lib/mocha/gherkin.js +1 -1
- package/lib/mocha/inject.js +5 -0
- package/lib/mocha/test.js +5 -2
- package/lib/plugin/analyze.js +50 -3
- package/lib/plugin/auth.js +435 -0
- package/lib/plugin/autoDelay.js +2 -2
- package/lib/plugin/autoLogin.js +3 -337
- package/lib/plugin/pageInfo.js +1 -1
- package/lib/plugin/retryFailedStep.js +13 -14
- package/lib/plugin/retryTo.js +6 -17
- package/lib/plugin/screenshotOnFail.js +4 -5
- package/lib/plugin/standardActingHelpers.js +4 -1
- package/lib/plugin/stepByStepReport.js +1 -1
- package/lib/plugin/tryTo.js +6 -15
- package/lib/recorder.js +1 -0
- package/lib/step/base.js +15 -4
- package/lib/step/comment.js +10 -0
- package/lib/store.js +29 -5
- package/lib/utils.js +1 -1
- package/lib/within.js +2 -0
- package/package.json +18 -18
- package/translations/de-DE.js +4 -3
- package/translations/fr-FR.js +4 -3
- package/translations/index.js +1 -0
- package/translations/it-IT.js +4 -3
- package/translations/ja-JP.js +4 -3
- package/translations/nl-NL.js +76 -0
- package/translations/pl-PL.js +4 -3
- package/translations/pt-BR.js +4 -3
- package/translations/ru-RU.js +4 -3
- package/translations/utils.js +9 -0
- package/translations/zh-CN.js +4 -3
- package/translations/zh-TW.js +4 -3
- package/typings/promiseBasedTypes.d.ts +0 -652
- package/typings/types.d.ts +99 -655
|
@@ -2,66 +2,66 @@
|
|
|
2
2
|
* Class to handle the interaction with the Dialog (Popup) Class from Puppeteer
|
|
3
3
|
*/
|
|
4
4
|
class Popup {
|
|
5
|
-
constructor(popup, defaultAction) {
|
|
6
|
-
this._popup = popup
|
|
7
|
-
this._actionType = ''
|
|
8
|
-
this._defaultAction = defaultAction
|
|
5
|
+
constructor(popup = null, defaultAction = '') {
|
|
6
|
+
this._popup = popup
|
|
7
|
+
this._actionType = ''
|
|
8
|
+
this._defaultAction = defaultAction
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
_assertValidActionType(action) {
|
|
12
12
|
if (['accept', 'cancel'].indexOf(action) === -1) {
|
|
13
|
-
throw new Error('Invalid Popup action type. Only "accept" or "cancel" actions are accepted')
|
|
13
|
+
throw new Error('Invalid Popup action type. Only "accept" or "cancel" actions are accepted')
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
set defaultAction(action) {
|
|
18
|
-
this._assertValidActionType(action)
|
|
19
|
-
this._defaultAction = action
|
|
18
|
+
this._assertValidActionType(action)
|
|
19
|
+
this._defaultAction = action
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
get defaultAction() {
|
|
23
|
-
return this._defaultAction
|
|
23
|
+
return this._defaultAction
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
get popup() {
|
|
27
|
-
return this._popup
|
|
27
|
+
return this._popup
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
set popup(popup) {
|
|
31
31
|
if (this._popup) {
|
|
32
|
-
|
|
32
|
+
return
|
|
33
33
|
}
|
|
34
|
-
this._popup = popup
|
|
34
|
+
this._popup = popup
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
get actionType() {
|
|
38
|
-
return this._actionType
|
|
38
|
+
return this._actionType
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
set actionType(action) {
|
|
42
|
-
this._assertValidActionType(action)
|
|
43
|
-
this._actionType = action
|
|
42
|
+
this._assertValidActionType(action)
|
|
43
|
+
this._actionType = action
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
clear() {
|
|
47
|
-
this._popup = null
|
|
48
|
-
this._actionType = ''
|
|
47
|
+
this._popup = null
|
|
48
|
+
this._actionType = ''
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
assertPopupVisible() {
|
|
52
52
|
if (!this._popup) {
|
|
53
|
-
throw new Error('There is no Popup visible')
|
|
53
|
+
throw new Error('There is no Popup visible')
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
assertPopupActionType(type) {
|
|
58
|
-
this.assertPopupVisible()
|
|
59
|
-
const expectedAction = this._actionType || this._defaultAction
|
|
58
|
+
this.assertPopupVisible()
|
|
59
|
+
const expectedAction = this._actionType || this._defaultAction
|
|
60
60
|
if (expectedAction !== type) {
|
|
61
|
-
throw new Error(`Popup action does not fit the expected action type. Expected popup action to be '${expectedAction}' not '${type}`)
|
|
61
|
+
throw new Error(`Popup action does not fit the expected action type. Expected popup action to be '${expectedAction}' not '${type}`)
|
|
62
62
|
}
|
|
63
|
-
this.clear()
|
|
63
|
+
this.clear()
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
module.exports = Popup
|
|
67
|
+
module.exports = Popup
|
|
@@ -145,11 +145,13 @@ module.exports.injected = function (fn, suite, hookName) {
|
|
|
145
145
|
const opts = suite.opts || {}
|
|
146
146
|
const retries = opts[`retry${ucfirst(hookName)}`] || 0
|
|
147
147
|
|
|
148
|
+
const currentTest = hookName === 'before' || hookName === 'after' ? suite?.ctx?.currentTest : null
|
|
149
|
+
|
|
148
150
|
promiseRetry(
|
|
149
151
|
async (retry, number) => {
|
|
150
152
|
try {
|
|
151
153
|
recorder.startUnlessRunning()
|
|
152
|
-
await fn.call(this, getInjectedArguments(fn))
|
|
154
|
+
await fn.call(this, { ...getInjectedArguments(fn), suite, test: currentTest })
|
|
153
155
|
await recorder.promise().catch(err => retry(err))
|
|
154
156
|
} catch (err) {
|
|
155
157
|
retry(err)
|
package/lib/mocha/gherkin.js
CHANGED
|
@@ -107,7 +107,7 @@ module.exports = (text, file) => {
|
|
|
107
107
|
)
|
|
108
108
|
continue
|
|
109
109
|
}
|
|
110
|
-
if (child.scenario && (currentLanguage ? child.scenario.keyword
|
|
110
|
+
if (child.scenario && (currentLanguage ? currentLanguage.contexts.ScenarioOutline.includes(child.scenario.keyword) : child.scenario.keyword === 'Scenario Outline')) {
|
|
111
111
|
for (const examples of child.scenario.examples) {
|
|
112
112
|
const fields = examples.tableHeader.cells.map(c => c.value)
|
|
113
113
|
for (const example of examples.tableBody) {
|
package/lib/mocha/inject.js
CHANGED
|
@@ -5,6 +5,7 @@ const getInjectedArguments = (fn, test) => {
|
|
|
5
5
|
const testArgs = {}
|
|
6
6
|
const params = parser.getParams(fn) || []
|
|
7
7
|
const objects = container.support()
|
|
8
|
+
|
|
8
9
|
for (const key of params) {
|
|
9
10
|
testArgs[key] = {}
|
|
10
11
|
if (test && test.inject && test.inject[key]) {
|
|
@@ -18,6 +19,10 @@ const getInjectedArguments = (fn, test) => {
|
|
|
18
19
|
testArgs[key] = container.support(key)
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
if (test) {
|
|
23
|
+
testArgs.suite = test?.parent
|
|
24
|
+
testArgs.test = test
|
|
25
|
+
}
|
|
21
26
|
return testArgs
|
|
22
27
|
}
|
|
23
28
|
|
package/lib/mocha/test.js
CHANGED
|
@@ -133,8 +133,10 @@ function cloneTest(test) {
|
|
|
133
133
|
return deserializeTest(serializeTest(test))
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
function testToFileName(test) {
|
|
137
|
-
let fileName =
|
|
136
|
+
function testToFileName(test, suffix = '') {
|
|
137
|
+
let fileName = test.title
|
|
138
|
+
|
|
139
|
+
if (suffix) fileName = `${fileName}_${suffix}`
|
|
138
140
|
// remove tags with empty string (disable for now)
|
|
139
141
|
// fileName = fileName.replace(/\@\w+/g, '')
|
|
140
142
|
fileName = fileName.slice(0, 100)
|
|
@@ -146,6 +148,7 @@ function testToFileName(test) {
|
|
|
146
148
|
// if (test.parent && test.parent.title) {
|
|
147
149
|
// fileName = `${clearString(test.parent.title)}_${fileName}`
|
|
148
150
|
// }
|
|
151
|
+
fileName = clearString(fileName).slice(0, 100)
|
|
149
152
|
return fileName
|
|
150
153
|
}
|
|
151
154
|
|
package/lib/plugin/analyze.js
CHANGED
|
@@ -60,7 +60,7 @@ const defaultConfig = {
|
|
|
60
60
|
|
|
61
61
|
If there is no groups of tests, say: "No patterns found"
|
|
62
62
|
Preserve error messages but cut them if they are too long.
|
|
63
|
-
Respond clearly and directly, without introductory words or phrases like
|
|
63
|
+
Respond clearly and directly, without introductory words or phrases like 'Of course,' 'Here is the answer,' etc.
|
|
64
64
|
Do not list more than 3 errors in the group.
|
|
65
65
|
If you identify that all tests in the group have the same tag, add this tag to the group report, otherwise ignore TAG section.
|
|
66
66
|
If you identify that all tests in the group have the same suite, add this suite to the group report, otherwise ignore SUITE section.
|
|
@@ -160,9 +160,56 @@ const defaultConfig = {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
/**
|
|
163
|
+
* CodeceptJS Analyze Plugin - Uses AI to analyze test failures and provide insights
|
|
163
164
|
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
165
|
+
* This plugin analyzes failed tests using AI to provide detailed explanations and group similar failures.
|
|
166
|
+
* When enabled with --ai flag, it generates reports after test execution.
|
|
167
|
+
*
|
|
168
|
+
* #### Usage
|
|
169
|
+
*
|
|
170
|
+
* ```js
|
|
171
|
+
* // in codecept.conf.js
|
|
172
|
+
* exports.config = {
|
|
173
|
+
* plugins: {
|
|
174
|
+
* analyze: {
|
|
175
|
+
* enabled: true,
|
|
176
|
+
* clusterize: 5,
|
|
177
|
+
* analyze: 2,
|
|
178
|
+
* vision: false
|
|
179
|
+
* }
|
|
180
|
+
* }
|
|
181
|
+
* }
|
|
182
|
+
* ```
|
|
183
|
+
*
|
|
184
|
+
* #### Configuration
|
|
185
|
+
*
|
|
186
|
+
* * `clusterize` (number) - minimum number of failures to trigger clustering analysis. Default: 5
|
|
187
|
+
* * `analyze` (number) - maximum number of individual test failures to analyze in detail. Default: 2
|
|
188
|
+
* * `vision` (boolean) - enables visual analysis of test screenshots. Default: false
|
|
189
|
+
* * `categories` (array) - list of failure categories for classification. Defaults to:
|
|
190
|
+
* - Browser connection error / browser crash
|
|
191
|
+
* - Network errors (server error, timeout, etc)
|
|
192
|
+
* - HTML / page elements (not found, not visible, etc)
|
|
193
|
+
* - Navigation errors (404, etc)
|
|
194
|
+
* - Code errors (syntax error, JS errors, etc)
|
|
195
|
+
* - Library & framework errors
|
|
196
|
+
* - Data errors (password incorrect, invalid format, etc)
|
|
197
|
+
* - Assertion failures
|
|
198
|
+
* - Other errors
|
|
199
|
+
* * `prompts` (object) - customize AI prompts for analysis
|
|
200
|
+
* - `clusterize` - prompt for clustering analysis
|
|
201
|
+
* - `analyze` - prompt for individual test analysis
|
|
202
|
+
*
|
|
203
|
+
* #### Features
|
|
204
|
+
*
|
|
205
|
+
* * Groups similar failures when number of failures >= clusterize value
|
|
206
|
+
* * Provides detailed analysis of individual failures
|
|
207
|
+
* * Analyzes screenshots if vision=true and screenshots are available
|
|
208
|
+
* * Classifies failures into predefined categories
|
|
209
|
+
* * Suggests possible causes and solutions
|
|
210
|
+
*
|
|
211
|
+
* @param {Object} config - Plugin configuration
|
|
212
|
+
* @returns {void}
|
|
166
213
|
*/
|
|
167
214
|
module.exports = function (config = {}) {
|
|
168
215
|
config = Object.assign(defaultConfig, config)
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { fileExists } = require('../utils')
|
|
4
|
+
const CommentStep = require('../step/comment')
|
|
5
|
+
const Section = require('../step/section')
|
|
6
|
+
const container = require('../container')
|
|
7
|
+
const store = require('../store')
|
|
8
|
+
const event = require('../event')
|
|
9
|
+
const recorder = require('../recorder')
|
|
10
|
+
const { debug } = require('../output')
|
|
11
|
+
const { isAsyncFunction } = require('../utils')
|
|
12
|
+
|
|
13
|
+
const defaultUser = {
|
|
14
|
+
fetch: I => I.grabCookie(),
|
|
15
|
+
check: () => {},
|
|
16
|
+
restore: (I, cookies) => {
|
|
17
|
+
I.amOnPage('/') // open a page
|
|
18
|
+
I.setCookie(cookies)
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const defaultConfig = {
|
|
23
|
+
saveToFile: false,
|
|
24
|
+
inject: 'login',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Logs user in for the first test and reuses session for next tests.
|
|
29
|
+
* Works by saving cookies into memory or file.
|
|
30
|
+
* If a session expires automatically logs in again.
|
|
31
|
+
*
|
|
32
|
+
* > For better development experience cookies can be saved into file, so a session can be reused while writing tests.
|
|
33
|
+
*
|
|
34
|
+
* #### Usage
|
|
35
|
+
*
|
|
36
|
+
* 1. Enable this plugin and configure as described below
|
|
37
|
+
* 2. Define user session names (example: `user`, `editor`, `admin`, etc).
|
|
38
|
+
* 3. Define how users are logged in and how to check that user is logged in
|
|
39
|
+
* 4. Use `login` object inside your tests to log in:
|
|
40
|
+
*
|
|
41
|
+
* ```js
|
|
42
|
+
* // inside a test file
|
|
43
|
+
* // use login to inject auto-login function
|
|
44
|
+
* Feature('Login');
|
|
45
|
+
*
|
|
46
|
+
* Before(({ login }) => {
|
|
47
|
+
* login('user'); // login using user session
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* // Alternatively log in for one scenario.
|
|
51
|
+
* Scenario('log me in', ( { I, login } ) => {
|
|
52
|
+
* login('admin');
|
|
53
|
+
* I.see('I am logged in');
|
|
54
|
+
* });
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* #### Configuration
|
|
58
|
+
*
|
|
59
|
+
* * `saveToFile` (default: false) - save cookies to file. Allows to reuse session between execution.
|
|
60
|
+
* * `inject` (default: `login`) - name of the login function to use
|
|
61
|
+
* * `users` - an array containing different session names and functions to:
|
|
62
|
+
* * `login` - sign in into the system
|
|
63
|
+
* * `check` - check that user is logged in
|
|
64
|
+
* * `fetch` - to get current cookies (by default `I.grabCookie()`)
|
|
65
|
+
* * `restore` - to set cookies (by default `I.amOnPage('/'); I.setCookie(cookie)`)
|
|
66
|
+
*
|
|
67
|
+
* #### How It Works
|
|
68
|
+
*
|
|
69
|
+
* 1. `restore` method is executed. It should open a page and set credentials.
|
|
70
|
+
* 2. `check` method is executed. It should reload a page (so cookies are applied) and check that this page belongs to logged-in user. When you pass the second args `session`, you could perform the validation using passed session.
|
|
71
|
+
* 3. If `restore` and `check` were not successful, `login` is executed
|
|
72
|
+
* 4. `login` should fill in login form
|
|
73
|
+
* 5. After successful login, `fetch` is executed to save cookies into memory or file.
|
|
74
|
+
*
|
|
75
|
+
* #### Example: Simple login
|
|
76
|
+
*
|
|
77
|
+
* ```js
|
|
78
|
+
* auth: {
|
|
79
|
+
* enabled: true,
|
|
80
|
+
* saveToFile: true,
|
|
81
|
+
* inject: 'login',
|
|
82
|
+
* users: {
|
|
83
|
+
* admin: {
|
|
84
|
+
* // loginAdmin function is defined in `steps_file.js`
|
|
85
|
+
* login: (I) => I.loginAdmin(),
|
|
86
|
+
* // if we see `Admin` on page, we assume we are logged in
|
|
87
|
+
* check: (I) => {
|
|
88
|
+
* I.amOnPage('/');
|
|
89
|
+
* I.see('Admin');
|
|
90
|
+
* }
|
|
91
|
+
* }
|
|
92
|
+
* }
|
|
93
|
+
* }
|
|
94
|
+
* ```
|
|
95
|
+
*
|
|
96
|
+
* #### Example: Multiple users
|
|
97
|
+
*
|
|
98
|
+
* ```js
|
|
99
|
+
* auth: {
|
|
100
|
+
* enabled: true,
|
|
101
|
+
* saveToFile: true,
|
|
102
|
+
* inject: 'loginAs', // use `loginAs` instead of login
|
|
103
|
+
* users: {
|
|
104
|
+
* user: {
|
|
105
|
+
* login: (I) => {
|
|
106
|
+
* I.amOnPage('/login');
|
|
107
|
+
* I.fillField('email', 'user@site.com');
|
|
108
|
+
* I.fillField('password', '123456');
|
|
109
|
+
* I.click('Login');
|
|
110
|
+
* },
|
|
111
|
+
* check: (I) => {
|
|
112
|
+
* I.amOnPage('/');
|
|
113
|
+
* I.see('User', '.navbar');
|
|
114
|
+
* },
|
|
115
|
+
* },
|
|
116
|
+
* admin: {
|
|
117
|
+
* login: (I) => {
|
|
118
|
+
* I.amOnPage('/login');
|
|
119
|
+
* I.fillField('email', 'admin@site.com');
|
|
120
|
+
* I.fillField('password', '123456');
|
|
121
|
+
* I.click('Login');
|
|
122
|
+
* },
|
|
123
|
+
* check: (I) => {
|
|
124
|
+
* I.amOnPage('/');
|
|
125
|
+
* I.see('Admin', '.navbar');
|
|
126
|
+
* },
|
|
127
|
+
* },
|
|
128
|
+
* }
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* #### Example: Keep cookies between tests
|
|
133
|
+
*
|
|
134
|
+
* If you decide to keep cookies between tests you don't need to save/retrieve cookies between tests.
|
|
135
|
+
* But you need to login once work until session expires.
|
|
136
|
+
* For this case, disable `fetch` and `restore` methods.
|
|
137
|
+
*
|
|
138
|
+
* ```js
|
|
139
|
+
* helpers: {
|
|
140
|
+
* WebDriver: {
|
|
141
|
+
* // config goes here
|
|
142
|
+
* keepCookies: true; // keep cookies for all tests
|
|
143
|
+
* }
|
|
144
|
+
* },
|
|
145
|
+
* plugins: {
|
|
146
|
+
* auth: {
|
|
147
|
+
* users: {
|
|
148
|
+
* admin: {
|
|
149
|
+
* login: (I) => {
|
|
150
|
+
* I.amOnPage('/login');
|
|
151
|
+
* I.fillField('email', 'admin@site.com');
|
|
152
|
+
* I.fillField('password', '123456');
|
|
153
|
+
* I.click('Login');
|
|
154
|
+
* },
|
|
155
|
+
* check: (I) => {
|
|
156
|
+
* I.amOnPage('/dashboard');
|
|
157
|
+
* I.see('Admin', '.navbar');
|
|
158
|
+
* },
|
|
159
|
+
* fetch: () => {}, // empty function
|
|
160
|
+
* restore: () => {}, // empty funciton
|
|
161
|
+
* }
|
|
162
|
+
* }
|
|
163
|
+
* }
|
|
164
|
+
* }
|
|
165
|
+
* ```
|
|
166
|
+
*
|
|
167
|
+
* #### Example: Getting sessions from local storage
|
|
168
|
+
*
|
|
169
|
+
* If your session is stored in local storage instead of cookies you still can obtain sessions.
|
|
170
|
+
*
|
|
171
|
+
* ```js
|
|
172
|
+
* plugins: {
|
|
173
|
+
* auth: {
|
|
174
|
+
* admin: {
|
|
175
|
+
* login: (I) => I.loginAsAdmin(),
|
|
176
|
+
* check: (I) => I.see('Admin', '.navbar'),
|
|
177
|
+
* fetch: (I) => {
|
|
178
|
+
* return I.executeScript(() => localStorage.getItem('session_id'));
|
|
179
|
+
* },
|
|
180
|
+
* restore: (I, session) => {
|
|
181
|
+
* I.amOnPage('/');
|
|
182
|
+
* I.executeScript((session) => localStorage.setItem('session_id', session), session);
|
|
183
|
+
* },
|
|
184
|
+
* }
|
|
185
|
+
* }
|
|
186
|
+
* }
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* #### Tips: Using async function in the auth
|
|
190
|
+
*
|
|
191
|
+
* If you use async functions in the auth plugin, login function should be used with `await` keyword.
|
|
192
|
+
*
|
|
193
|
+
* ```js
|
|
194
|
+
* auth: {
|
|
195
|
+
* enabled: true,
|
|
196
|
+
* saveToFile: true,
|
|
197
|
+
* inject: 'login',
|
|
198
|
+
* users: {
|
|
199
|
+
* admin: {
|
|
200
|
+
* login: async (I) => { // If you use async function in the auth plugin
|
|
201
|
+
* const phrase = await I.grabTextFrom('#phrase')
|
|
202
|
+
* I.fillField('username', 'admin'),
|
|
203
|
+
* I.fillField('password', 'password')
|
|
204
|
+
* I.fillField('phrase', phrase)
|
|
205
|
+
* },
|
|
206
|
+
* check: (I) => {
|
|
207
|
+
* I.amOnPage('/');
|
|
208
|
+
* I.see('Admin');
|
|
209
|
+
* },
|
|
210
|
+
* }
|
|
211
|
+
* }
|
|
212
|
+
* }
|
|
213
|
+
* ```
|
|
214
|
+
*
|
|
215
|
+
* ```js
|
|
216
|
+
* Scenario('login', async ( {I, login} ) => {
|
|
217
|
+
* await login('admin') // you should use `await`
|
|
218
|
+
* })
|
|
219
|
+
* ```
|
|
220
|
+
*
|
|
221
|
+
* #### Tips: Using session to validate user
|
|
222
|
+
*
|
|
223
|
+
* Instead of asserting on page elements for the current user in `check`, you can use the `session` you saved in `fetch`
|
|
224
|
+
*
|
|
225
|
+
* ```js
|
|
226
|
+
* auth: {
|
|
227
|
+
* enabled: true,
|
|
228
|
+
* saveToFile: true,
|
|
229
|
+
* inject: 'login',
|
|
230
|
+
* users: {
|
|
231
|
+
* admin: {
|
|
232
|
+
* login: async (I) => { // If you use async function in the auth plugin
|
|
233
|
+
* const phrase = await I.grabTextFrom('#phrase')
|
|
234
|
+
* I.fillField('username', 'admin'),
|
|
235
|
+
* I.fillField('password', 'password')
|
|
236
|
+
* I.fillField('phrase', phrase)
|
|
237
|
+
* },
|
|
238
|
+
* check: (I, session) => {
|
|
239
|
+
* // Throwing an error in `check` will make CodeceptJS perform the login step for the user
|
|
240
|
+
* if (session.profile.email !== the.email.you.expect@some-mail.com) {
|
|
241
|
+
* throw new Error ('Wrong user signed in');
|
|
242
|
+
* }
|
|
243
|
+
* },
|
|
244
|
+
* }
|
|
245
|
+
* }
|
|
246
|
+
* }
|
|
247
|
+
* ```
|
|
248
|
+
*
|
|
249
|
+
* ```js
|
|
250
|
+
* Scenario('login', async ( {I, login} ) => {
|
|
251
|
+
* await login('admin') // you should use `await`
|
|
252
|
+
* })
|
|
253
|
+
*
|
|
254
|
+
*
|
|
255
|
+
*/
|
|
256
|
+
module.exports = function (config) {
|
|
257
|
+
config = Object.assign(defaultConfig, config)
|
|
258
|
+
Object.keys(config.users).map(
|
|
259
|
+
u =>
|
|
260
|
+
(config.users[u] = {
|
|
261
|
+
...defaultUser,
|
|
262
|
+
...config.users[u],
|
|
263
|
+
}),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if (config.saveToFile) {
|
|
267
|
+
// loading from file
|
|
268
|
+
loadCookiesFromFile(config)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const loginFunction = async name => {
|
|
272
|
+
const I = container.support('I')
|
|
273
|
+
const userSession = config.users[name]
|
|
274
|
+
|
|
275
|
+
if (!userSession) {
|
|
276
|
+
throw new Error(`User '${name}' was not configured for authorization in auth plugin. Add it to the plugin config`)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const test = store.currentTest
|
|
280
|
+
|
|
281
|
+
// we are in BeforeSuite hook
|
|
282
|
+
if (!test) {
|
|
283
|
+
enableAuthBeforeEachTest(name)
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const section = new Section(`I am logged in as ${name}`)
|
|
288
|
+
|
|
289
|
+
if (config.saveToFile && !store[`${name}_session`]) {
|
|
290
|
+
loadCookiesFromFile(config)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (isPlaywrightSession() && test?.opts?.cookies) {
|
|
294
|
+
if (test.opts.user == name) {
|
|
295
|
+
debug(`Cookies already loaded for ${name}`)
|
|
296
|
+
|
|
297
|
+
alreadyLoggedIn(name)
|
|
298
|
+
return
|
|
299
|
+
} else {
|
|
300
|
+
debug(`Cookies already loaded for ${test.opts.user}, but not for ${name}`)
|
|
301
|
+
await I.deleteCookie()
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
section.start()
|
|
306
|
+
|
|
307
|
+
const cookies = store[`${name}_session`]
|
|
308
|
+
const shouldAwait = isAsyncFunction(userSession.login) || isAsyncFunction(userSession.restore) || isAsyncFunction(userSession.check)
|
|
309
|
+
|
|
310
|
+
const loginAndSave = async () => {
|
|
311
|
+
if (shouldAwait) {
|
|
312
|
+
await userSession.login(I)
|
|
313
|
+
} else {
|
|
314
|
+
userSession.login(I)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
section.end()
|
|
318
|
+
const cookies = await userSession.fetch(I)
|
|
319
|
+
if (!cookies) {
|
|
320
|
+
debug("Cannot save user session with empty cookies from auto login's fetch method")
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
if (config.saveToFile) {
|
|
324
|
+
debug(`Saved user session into file for ${name}`)
|
|
325
|
+
fs.writeFileSync(path.join(global.output_dir, `${name}_session.json`), JSON.stringify(cookies))
|
|
326
|
+
}
|
|
327
|
+
store[`${name}_session`] = cookies
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!cookies) return loginAndSave()
|
|
331
|
+
|
|
332
|
+
recorder.session.start('check login')
|
|
333
|
+
if (shouldAwait) {
|
|
334
|
+
await userSession.restore(I, cookies)
|
|
335
|
+
await userSession.check(I, cookies)
|
|
336
|
+
} else {
|
|
337
|
+
userSession.restore(I, cookies)
|
|
338
|
+
userSession.check(I, cookies)
|
|
339
|
+
}
|
|
340
|
+
section.end()
|
|
341
|
+
recorder.session.catch(err => {
|
|
342
|
+
debug(`Failed auto login for ${name} due to ${err}`)
|
|
343
|
+
debug('Logging in again')
|
|
344
|
+
recorder.session.start('auto login')
|
|
345
|
+
return loginAndSave()
|
|
346
|
+
.then(() => {
|
|
347
|
+
recorder.add(() => recorder.session.restore('auto login'))
|
|
348
|
+
recorder.catch(() => debug('continue'))
|
|
349
|
+
})
|
|
350
|
+
.catch(err => {
|
|
351
|
+
recorder.session.restore('auto login')
|
|
352
|
+
recorder.session.restore('check login')
|
|
353
|
+
section.end()
|
|
354
|
+
recorder.throw(err)
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
recorder.add(() => {
|
|
358
|
+
recorder.session.restore('check login')
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
return recorder.promise()
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function enableAuthBeforeEachTest(name) {
|
|
365
|
+
const suite = store.currentSuite
|
|
366
|
+
if (!suite) return
|
|
367
|
+
|
|
368
|
+
debug(`enabling auth as ${name} for each test of suite ${suite.title}`)
|
|
369
|
+
|
|
370
|
+
// we are setting test opts so they can be picked up by Playwright if it starts browser for this test
|
|
371
|
+
suite.eachTest(test => {
|
|
372
|
+
// preload from store
|
|
373
|
+
if (store[`${name}_session`]) {
|
|
374
|
+
test.opts.cookies = store[`${name}_session`]
|
|
375
|
+
test.opts.user = name
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!config.saveToFile) return
|
|
380
|
+
const cookieFile = path.join(global.output_dir, `${name}_session.json`)
|
|
381
|
+
|
|
382
|
+
if (!fileExists(cookieFile)) {
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const context = fs.readFileSync(cookieFile).toString()
|
|
387
|
+
test.opts.cookies = JSON.parse(context)
|
|
388
|
+
test.opts.user = name
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
function runLoginFunctionForTest(test) {
|
|
392
|
+
if (!suite.tests.includes(test)) return
|
|
393
|
+
// let's call this function to ensure that authorization happened
|
|
394
|
+
// if no cookies, it will login and save them
|
|
395
|
+
loginFunction(name)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// we are in BeforeSuite hook
|
|
399
|
+
event.dispatcher.on(event.test.started, runLoginFunctionForTest)
|
|
400
|
+
event.dispatcher.on(event.suite.after, () => {
|
|
401
|
+
event.dispatcher.off(event.test.started, runLoginFunctionForTest)
|
|
402
|
+
})
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// adding this to DI container
|
|
406
|
+
const support = {}
|
|
407
|
+
support[config.inject] = loginFunction
|
|
408
|
+
container.append({ support })
|
|
409
|
+
|
|
410
|
+
return loginFunction
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function loadCookiesFromFile(config) {
|
|
414
|
+
for (const name in config.users) {
|
|
415
|
+
const fileName = path.join(global.output_dir, `${name}_session.json`)
|
|
416
|
+
if (!fileExists(fileName)) continue
|
|
417
|
+
const data = fs.readFileSync(fileName).toString()
|
|
418
|
+
try {
|
|
419
|
+
store[`${name}_session`] = JSON.parse(data)
|
|
420
|
+
} catch (err) {
|
|
421
|
+
throw new Error(`Could not load session from ${fileName}\n${err}`)
|
|
422
|
+
}
|
|
423
|
+
debug(`Loaded user session for ${name}`)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function isPlaywrightSession() {
|
|
428
|
+
return !!container.helpers('Playwright')
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function alreadyLoggedIn(name) {
|
|
432
|
+
const step = new CommentStep('am logged in as')
|
|
433
|
+
step.actor = 'I'
|
|
434
|
+
return step.addToRecorder([name])
|
|
435
|
+
}
|
package/lib/plugin/autoDelay.js
CHANGED
|
@@ -2,8 +2,8 @@ const Container = require('../container')
|
|
|
2
2
|
const store = require('../store')
|
|
3
3
|
const recorder = require('../recorder')
|
|
4
4
|
const event = require('../event')
|
|
5
|
-
const log = require('../output')
|
|
6
|
-
const
|
|
5
|
+
const { log } = require('../output')
|
|
6
|
+
const standardActingHelpers = Container.STANDARD_ACTING_HELPERS
|
|
7
7
|
|
|
8
8
|
const methodsToDelay = ['click', 'fillField', 'checkOption', 'pressKey', 'doubleClick', 'rightClick']
|
|
9
9
|
|