codeceptjs 4.0.0-beta.1 → 4.0.0-beta.11.esm-aria
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/README.md +133 -120
- package/bin/codecept.js +107 -96
- package/bin/test-server.js +64 -0
- package/docs/webapi/clearCookie.mustache +1 -1
- package/docs/webapi/click.mustache +5 -1
- package/lib/actor.js +71 -103
- package/lib/ai.js +159 -188
- package/lib/assert/empty.js +22 -24
- package/lib/assert/equal.js +30 -37
- package/lib/assert/error.js +14 -14
- package/lib/assert/include.js +43 -48
- package/lib/assert/throws.js +11 -11
- package/lib/assert/truth.js +22 -22
- package/lib/assert.js +20 -18
- package/lib/codecept.js +238 -162
- package/lib/colorUtils.js +50 -52
- package/lib/command/check.js +206 -0
- package/lib/command/configMigrate.js +56 -51
- package/lib/command/definitions.js +96 -109
- package/lib/command/dryRun.js +77 -79
- package/lib/command/generate.js +234 -194
- package/lib/command/gherkin/init.js +42 -33
- package/lib/command/gherkin/snippets.js +76 -74
- package/lib/command/gherkin/steps.js +20 -17
- package/lib/command/info.js +74 -38
- package/lib/command/init.js +300 -290
- package/lib/command/interactive.js +41 -32
- package/lib/command/list.js +28 -27
- package/lib/command/run-multiple/chunk.js +51 -48
- package/lib/command/run-multiple/collection.js +5 -5
- package/lib/command/run-multiple/run.js +5 -1
- package/lib/command/run-multiple.js +97 -97
- package/lib/command/run-rerun.js +19 -25
- package/lib/command/run-workers.js +68 -92
- package/lib/command/run.js +39 -27
- package/lib/command/utils.js +80 -64
- package/lib/command/workers/runTests.js +388 -226
- package/lib/config.js +124 -50
- package/lib/container.js +765 -260
- package/lib/data/context.js +60 -61
- package/lib/data/dataScenarioConfig.js +47 -47
- package/lib/data/dataTableArgument.js +32 -32
- package/lib/data/table.js +22 -22
- package/lib/effects.js +307 -0
- package/lib/element/WebElement.js +327 -0
- package/lib/els.js +160 -0
- package/lib/event.js +173 -163
- package/lib/globals.js +141 -0
- package/lib/heal.js +89 -85
- package/lib/helper/AI.js +131 -41
- package/lib/helper/ApiDataFactory.js +107 -75
- package/lib/helper/Appium.js +542 -404
- package/lib/helper/FileSystem.js +100 -79
- package/lib/helper/GraphQL.js +44 -43
- package/lib/helper/GraphQLDataFactory.js +52 -52
- package/lib/helper/JSONResponse.js +126 -88
- package/lib/helper/Mochawesome.js +54 -29
- package/lib/helper/Playwright.js +2547 -1316
- package/lib/helper/Puppeteer.js +1578 -1181
- package/lib/helper/REST.js +209 -68
- package/lib/helper/WebDriver.js +1482 -1342
- package/lib/helper/errors/ConnectionRefused.js +6 -6
- package/lib/helper/errors/ElementAssertion.js +11 -16
- package/lib/helper/errors/ElementNotFound.js +5 -9
- package/lib/helper/errors/RemoteBrowserConnectionRefused.js +5 -5
- package/lib/helper/extras/Console.js +11 -11
- package/lib/helper/extras/PlaywrightLocator.js +110 -0
- package/lib/helper/extras/PlaywrightPropEngine.js +18 -18
- package/lib/helper/extras/PlaywrightReactVueLocator.js +17 -8
- package/lib/helper/extras/PlaywrightRestartOpts.js +25 -11
- package/lib/helper/extras/Popup.js +22 -22
- package/lib/helper/extras/React.js +27 -28
- package/lib/helper/network/actions.js +36 -42
- package/lib/helper/network/utils.js +78 -84
- package/lib/helper/scripts/blurElement.js +5 -5
- package/lib/helper/scripts/focusElement.js +5 -5
- package/lib/helper/scripts/highlightElement.js +8 -8
- package/lib/helper/scripts/isElementClickable.js +34 -34
- package/lib/helper.js +2 -3
- package/lib/history.js +23 -19
- package/lib/hooks.js +8 -8
- package/lib/html.js +94 -104
- package/lib/index.js +38 -27
- package/lib/listener/config.js +30 -23
- package/lib/listener/emptyRun.js +54 -0
- package/lib/listener/enhancedGlobalRetry.js +110 -0
- package/lib/listener/exit.js +16 -18
- package/lib/listener/globalRetry.js +70 -0
- package/lib/listener/globalTimeout.js +181 -0
- package/lib/listener/helpers.js +76 -51
- package/lib/listener/mocha.js +10 -11
- package/lib/listener/result.js +11 -0
- package/lib/listener/retryEnhancer.js +85 -0
- package/lib/listener/steps.js +71 -59
- package/lib/listener/store.js +20 -0
- package/lib/locator.js +214 -197
- package/lib/mocha/asyncWrapper.js +274 -0
- package/lib/mocha/bdd.js +167 -0
- package/lib/mocha/cli.js +341 -0
- package/lib/mocha/factory.js +163 -0
- package/lib/mocha/featureConfig.js +89 -0
- package/lib/mocha/gherkin.js +231 -0
- package/lib/mocha/hooks.js +121 -0
- package/lib/mocha/index.js +21 -0
- package/lib/mocha/inject.js +46 -0
- package/lib/{interfaces → mocha}/scenarioConfig.js +58 -34
- package/lib/mocha/suite.js +89 -0
- package/lib/mocha/test.js +184 -0
- package/lib/mocha/types.d.ts +42 -0
- package/lib/mocha/ui.js +242 -0
- package/lib/output.js +141 -71
- package/lib/parser.js +47 -44
- package/lib/pause.js +173 -145
- package/lib/plugin/analyze.js +403 -0
- package/lib/plugin/{autoLogin.js → auth.js} +178 -79
- package/lib/plugin/autoDelay.js +36 -40
- package/lib/plugin/coverage.js +131 -78
- package/lib/plugin/customLocator.js +22 -21
- package/lib/plugin/customReporter.js +53 -0
- package/lib/plugin/enhancedRetryFailedStep.js +99 -0
- package/lib/plugin/heal.js +101 -110
- package/lib/plugin/htmlReporter.js +3648 -0
- package/lib/plugin/pageInfo.js +140 -0
- package/lib/plugin/pauseOnFail.js +12 -11
- package/lib/plugin/retryFailedStep.js +82 -47
- package/lib/plugin/screenshotOnFail.js +111 -92
- package/lib/plugin/stepByStepReport.js +159 -101
- package/lib/plugin/stepTimeout.js +20 -25
- package/lib/plugin/subtitles.js +38 -38
- package/lib/recorder.js +193 -130
- package/lib/rerun.js +94 -49
- package/lib/result.js +238 -0
- package/lib/retryCoordinator.js +207 -0
- package/lib/secret.js +20 -18
- package/lib/session.js +95 -89
- package/lib/step/base.js +239 -0
- package/lib/step/comment.js +10 -0
- package/lib/step/config.js +50 -0
- package/lib/step/func.js +46 -0
- package/lib/step/helper.js +50 -0
- package/lib/step/meta.js +99 -0
- package/lib/step/record.js +74 -0
- package/lib/step/retry.js +11 -0
- package/lib/step/section.js +55 -0
- package/lib/step.js +18 -329
- package/lib/steps.js +54 -0
- package/lib/store.js +38 -7
- package/lib/template/heal.js +3 -12
- package/lib/template/prompts/generatePageObject.js +31 -0
- package/lib/template/prompts/healStep.js +13 -0
- package/lib/template/prompts/writeStep.js +9 -0
- package/lib/test-server.js +334 -0
- package/lib/timeout.js +60 -0
- package/lib/transform.js +8 -8
- package/lib/translation.js +34 -21
- package/lib/utils/mask_data.js +47 -0
- package/lib/utils.js +411 -228
- package/lib/workerStorage.js +37 -34
- package/lib/workers.js +532 -296
- package/package.json +115 -95
- package/translations/de-DE.js +5 -3
- package/translations/fr-FR.js +5 -4
- package/translations/index.js +22 -12
- 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 +10 -0
- package/translations/zh-CN.js +4 -3
- package/translations/zh-TW.js +4 -3
- package/typings/index.d.ts +546 -185
- package/typings/promiseBasedTypes.d.ts +150 -879
- package/typings/types.d.ts +547 -996
- package/lib/cli.js +0 -249
- package/lib/dirname.js +0 -5
- package/lib/helper/Expect.js +0 -425
- package/lib/helper/ExpectHelper.js +0 -399
- package/lib/helper/MockServer.js +0 -223
- package/lib/helper/Nightmare.js +0 -1411
- package/lib/helper/Protractor.js +0 -1835
- package/lib/helper/SoftExpectHelper.js +0 -381
- package/lib/helper/TestCafe.js +0 -1410
- package/lib/helper/clientscripts/nightmare.js +0 -213
- package/lib/helper/testcafe/testControllerHolder.js +0 -42
- package/lib/helper/testcafe/testcafe-utils.js +0 -63
- package/lib/interfaces/bdd.js +0 -98
- package/lib/interfaces/featureConfig.js +0 -69
- package/lib/interfaces/gherkin.js +0 -195
- package/lib/listener/artifacts.js +0 -19
- package/lib/listener/retry.js +0 -68
- package/lib/listener/timeout.js +0 -109
- package/lib/mochaFactory.js +0 -110
- package/lib/plugin/allure.js +0 -15
- package/lib/plugin/commentStep.js +0 -136
- package/lib/plugin/debugErrors.js +0 -67
- package/lib/plugin/eachElement.js +0 -127
- package/lib/plugin/fakerTransform.js +0 -49
- package/lib/plugin/retryTo.js +0 -121
- package/lib/plugin/selenoid.js +0 -371
- package/lib/plugin/standardActingHelpers.js +0 -9
- package/lib/plugin/tryTo.js +0 -105
- package/lib/plugin/wdio.js +0 -246
- package/lib/scenario.js +0 -222
- package/lib/ui.js +0 -238
- package/lib/within.js +0 -70
package/lib/effects.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import recorder from './recorder.js'
|
|
2
|
+
import output from './output.js'
|
|
3
|
+
import store from './store.js'
|
|
4
|
+
import event from './event.js'
|
|
5
|
+
import container from './container.js'
|
|
6
|
+
import MetaStep from './step/meta.js'
|
|
7
|
+
import { isAsyncFunction } from './utils.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {CodeceptJS.LocatorOrString} context
|
|
11
|
+
* @param {Function} fn
|
|
12
|
+
* @return {Promise<*> | undefined}
|
|
13
|
+
*/
|
|
14
|
+
function within(context, fn) {
|
|
15
|
+
const helpers = store.dryRun ? {} : container.helpers()
|
|
16
|
+
const locator = typeof context === 'object' ? JSON.stringify(context) : context
|
|
17
|
+
|
|
18
|
+
return recorder.add(
|
|
19
|
+
'register within wrapper',
|
|
20
|
+
() => {
|
|
21
|
+
const metaStep = new WithinStep(locator, fn)
|
|
22
|
+
const defineMetaStep = step => (step.metaStep = metaStep)
|
|
23
|
+
recorder.session.start('within')
|
|
24
|
+
|
|
25
|
+
event.dispatcher.prependListener(event.step.before, defineMetaStep)
|
|
26
|
+
|
|
27
|
+
Object.keys(helpers).forEach(helper => {
|
|
28
|
+
if (helpers[helper]._withinBegin) recorder.add(`[${helper}] start within`, () => helpers[helper]._withinBegin(context))
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const finalize = () => {
|
|
32
|
+
event.dispatcher.removeListener(event.step.before, defineMetaStep)
|
|
33
|
+
recorder.add('Finalize session within session', () => {
|
|
34
|
+
output.stepShift = 1
|
|
35
|
+
recorder.session.restore('within')
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
const finishHelpers = () => {
|
|
39
|
+
Object.keys(helpers).forEach(helper => {
|
|
40
|
+
if (helpers[helper]._withinEnd) recorder.add(`[${helper}] finish within`, () => helpers[helper]._withinEnd())
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (isAsyncFunction(fn)) {
|
|
45
|
+
return fn()
|
|
46
|
+
.then(res => {
|
|
47
|
+
finishHelpers()
|
|
48
|
+
finalize()
|
|
49
|
+
return recorder.promise().then(() => res)
|
|
50
|
+
})
|
|
51
|
+
.catch(e => {
|
|
52
|
+
finalize()
|
|
53
|
+
recorder.throw(e)
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let res
|
|
58
|
+
try {
|
|
59
|
+
res = fn()
|
|
60
|
+
} catch (err) {
|
|
61
|
+
recorder.throw(err)
|
|
62
|
+
} finally {
|
|
63
|
+
finishHelpers()
|
|
64
|
+
recorder.catch(err => {
|
|
65
|
+
output.stepShift = 1
|
|
66
|
+
throw err
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
finalize()
|
|
70
|
+
return recorder.promise().then(() => res)
|
|
71
|
+
},
|
|
72
|
+
false,
|
|
73
|
+
false,
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
class WithinStep extends MetaStep {
|
|
78
|
+
constructor(locator, fn) {
|
|
79
|
+
super('Within')
|
|
80
|
+
this.args = [locator]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
toString() {
|
|
84
|
+
return `${this.prefix}Within ${this.humanizeArgs()}${this.suffix}`
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* A utility function for CodeceptJS tests that acts as a soft assertion.
|
|
90
|
+
* Executes a callback within a recorded session, ensuring errors are handled gracefully without failing the test immediately.
|
|
91
|
+
*
|
|
92
|
+
* @async
|
|
93
|
+
* @function hopeThat
|
|
94
|
+
* @param {Function} callback - The callback function containing the logic to validate.
|
|
95
|
+
* This function should perform the desired assertion or condition check.
|
|
96
|
+
* @returns {Promise<boolean|any>} A promise resolving to `true` if the assertion or condition was successful,
|
|
97
|
+
* or `false` if an error occurred.
|
|
98
|
+
*
|
|
99
|
+
* @description
|
|
100
|
+
* - Designed for use in CodeceptJS tests as a "soft assertion."
|
|
101
|
+
* Unlike standard assertions, it does not stop the test execution on failure.
|
|
102
|
+
* - Starts a new recorder session named 'hopeThat' and manages state restoration.
|
|
103
|
+
* - Logs errors and attaches them as notes to the test, enabling post-test reporting of soft assertion failures.
|
|
104
|
+
* - Resets the `store.hopeThat` flag after the execution, ensuring clean state for subsequent operations.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* const { hopeThat } = require('codeceptjs/effects')
|
|
108
|
+
* await hopeThat(() => {
|
|
109
|
+
* I.see('Welcome'); // Perform a soft assertion
|
|
110
|
+
* });
|
|
111
|
+
*
|
|
112
|
+
* @throws Will handle errors that occur during the callback execution. Errors are logged and attached as notes to the test.
|
|
113
|
+
*/
|
|
114
|
+
async function hopeThat(callback) {
|
|
115
|
+
if (store.dryRun) return
|
|
116
|
+
const sessionName = 'hopeThat'
|
|
117
|
+
|
|
118
|
+
let result = false
|
|
119
|
+
return recorder.add(
|
|
120
|
+
'hopeThat',
|
|
121
|
+
() => {
|
|
122
|
+
recorder.session.start(sessionName)
|
|
123
|
+
store.hopeThat = true
|
|
124
|
+
callback()
|
|
125
|
+
recorder.add(() => {
|
|
126
|
+
result = true
|
|
127
|
+
recorder.session.restore(sessionName)
|
|
128
|
+
return result
|
|
129
|
+
})
|
|
130
|
+
recorder.session.catch(err => {
|
|
131
|
+
result = false
|
|
132
|
+
const msg = err.inspect ? err.inspect() : err.toString()
|
|
133
|
+
output.debug(`Unsuccessful assertion > ${msg}`)
|
|
134
|
+
event.dispatcher.once(event.test.finished, test => {
|
|
135
|
+
if (!test.notes) test.notes = []
|
|
136
|
+
test.notes.push({ type: 'conditionalError', text: msg })
|
|
137
|
+
})
|
|
138
|
+
recorder.session.restore(sessionName)
|
|
139
|
+
return result
|
|
140
|
+
})
|
|
141
|
+
return recorder.add(
|
|
142
|
+
'result',
|
|
143
|
+
() => {
|
|
144
|
+
store.hopeThat = undefined
|
|
145
|
+
return result
|
|
146
|
+
},
|
|
147
|
+
true,
|
|
148
|
+
false,
|
|
149
|
+
)
|
|
150
|
+
},
|
|
151
|
+
false,
|
|
152
|
+
false,
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* A CodeceptJS utility function to retry a step or callback multiple times with a specified polling interval.
|
|
158
|
+
*
|
|
159
|
+
* @async
|
|
160
|
+
* @function retryTo
|
|
161
|
+
* @param {Function} callback - The function to execute, which will be retried upon failure.
|
|
162
|
+
* Receives the current retry count as an argument.
|
|
163
|
+
* @param {number} maxTries - The maximum number of attempts to retry the callback.
|
|
164
|
+
* @param {number} [pollInterval=200] - The delay (in milliseconds) between retry attempts.
|
|
165
|
+
* @returns {Promise<void|any>} A promise that resolves when the callback executes successfully, or rejects after reaching the maximum retries.
|
|
166
|
+
*
|
|
167
|
+
* @description
|
|
168
|
+
* - This function is designed for use in CodeceptJS tests to handle intermittent or flaky test steps.
|
|
169
|
+
* - Starts a new recorder session for each retry attempt, ensuring proper state management and error handling.
|
|
170
|
+
* - Logs errors and retries the callback until it either succeeds or the maximum number of attempts is reached.
|
|
171
|
+
* - Restores the session state after each attempt, whether successful or not.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* const { retryTo } = require('codeceptjs/effects')
|
|
175
|
+
* await retryTo((tries) => {
|
|
176
|
+
* if (tries < 3) {
|
|
177
|
+
* I.see('Non-existent element'); // Simulates a failure
|
|
178
|
+
* } else {
|
|
179
|
+
* I.see('Welcome'); // Succeeds on the 3rd attempt
|
|
180
|
+
* }
|
|
181
|
+
* }, 5, 300); // Retry up to 5 times, with a 300ms interval
|
|
182
|
+
*
|
|
183
|
+
* @throws Will reject with the last error encountered if the maximum retries are exceeded.
|
|
184
|
+
*/
|
|
185
|
+
async function retryTo(callback, maxTries, pollInterval = 200) {
|
|
186
|
+
const sessionName = 'retryTo'
|
|
187
|
+
|
|
188
|
+
return new Promise((done, reject) => {
|
|
189
|
+
let tries = 1
|
|
190
|
+
|
|
191
|
+
function handleRetryException(err) {
|
|
192
|
+
recorder.throw(err)
|
|
193
|
+
reject(err)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const tryBlock = async () => {
|
|
197
|
+
tries++
|
|
198
|
+
recorder.session.start(`${sessionName} ${tries}`)
|
|
199
|
+
try {
|
|
200
|
+
await callback(tries)
|
|
201
|
+
} catch (err) {
|
|
202
|
+
handleRetryException(err)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Call done if no errors
|
|
206
|
+
recorder.add(() => {
|
|
207
|
+
recorder.session.restore(`${sessionName} ${tries}`)
|
|
208
|
+
done(null)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// Catch errors and retry
|
|
212
|
+
recorder.session.catch(err => {
|
|
213
|
+
recorder.session.restore(`${sessionName} ${tries}`)
|
|
214
|
+
if (tries <= maxTries) {
|
|
215
|
+
output.debug(`Error ${err}... Retrying`)
|
|
216
|
+
recorder.add(`${sessionName} ${tries}`, () => setTimeout(tryBlock, pollInterval))
|
|
217
|
+
} else {
|
|
218
|
+
// if maxTries reached
|
|
219
|
+
handleRetryException(err)
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
recorder.add(sessionName, tryBlock).catch(err => {
|
|
225
|
+
console.error('An error occurred:', err)
|
|
226
|
+
done(null)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* A CodeceptJS utility function to attempt a step or callback without failing the test.
|
|
233
|
+
* If the step fails, the test continues execution without interruption, and the result is logged.
|
|
234
|
+
*
|
|
235
|
+
* @async
|
|
236
|
+
* @function tryTo
|
|
237
|
+
* @param {Function} callback - The function to execute, which may succeed or fail.
|
|
238
|
+
* This function contains the logic to be attempted.
|
|
239
|
+
* @returns {Promise<boolean|any>} A promise resolving to `true` if the step succeeds, or `false` if it fails.
|
|
240
|
+
*
|
|
241
|
+
* @description
|
|
242
|
+
* - Useful for scenarios where certain steps are optional or their failure should not interrupt the test flow.
|
|
243
|
+
* - Starts a new recorder session named 'tryTo' for isolation and error handling.
|
|
244
|
+
* - Captures errors during execution and logs them for debugging purposes.
|
|
245
|
+
* - Ensures the `store.tryTo` flag is reset after execution to maintain a clean state.
|
|
246
|
+
*
|
|
247
|
+
* @example
|
|
248
|
+
* const { tryTo } = require('codeceptjs/effects')
|
|
249
|
+
* const wasSuccessful = await tryTo(() => {
|
|
250
|
+
* I.see('Welcome'); // Attempt to find an element on the page
|
|
251
|
+
* });
|
|
252
|
+
*
|
|
253
|
+
* if (!wasSuccessful) {
|
|
254
|
+
* I.say('Optional step failed, but test continues.');
|
|
255
|
+
* }
|
|
256
|
+
*
|
|
257
|
+
* @throws Will handle errors internally, logging them and returning `false` as the result.
|
|
258
|
+
*/
|
|
259
|
+
async function tryTo(callback) {
|
|
260
|
+
if (store.dryRun) return
|
|
261
|
+
const sessionName = 'tryTo'
|
|
262
|
+
|
|
263
|
+
let result = false
|
|
264
|
+
let isAutoRetriesEnabled = store.autoRetries
|
|
265
|
+
return recorder.add(
|
|
266
|
+
sessionName,
|
|
267
|
+
() => {
|
|
268
|
+
recorder.session.start(sessionName)
|
|
269
|
+
isAutoRetriesEnabled = store.autoRetries
|
|
270
|
+
if (isAutoRetriesEnabled) output.debug('Auto retries disabled inside tryTo effect')
|
|
271
|
+
store.autoRetries = false
|
|
272
|
+
callback()
|
|
273
|
+
recorder.add(() => {
|
|
274
|
+
result = true
|
|
275
|
+
recorder.session.restore(sessionName)
|
|
276
|
+
return result
|
|
277
|
+
})
|
|
278
|
+
recorder.session.catch(err => {
|
|
279
|
+
result = false
|
|
280
|
+
const msg = err.inspect ? err.inspect() : err.toString()
|
|
281
|
+
output.debug(`Unsuccessful try > ${msg}`)
|
|
282
|
+
recorder.session.restore(sessionName)
|
|
283
|
+
return result
|
|
284
|
+
})
|
|
285
|
+
return recorder.add(
|
|
286
|
+
'result',
|
|
287
|
+
() => {
|
|
288
|
+
store.autoRetries = isAutoRetriesEnabled
|
|
289
|
+
return result
|
|
290
|
+
},
|
|
291
|
+
true,
|
|
292
|
+
false,
|
|
293
|
+
)
|
|
294
|
+
},
|
|
295
|
+
false,
|
|
296
|
+
false,
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export { hopeThat, retryTo, tryTo, within }
|
|
301
|
+
|
|
302
|
+
export default {
|
|
303
|
+
hopeThat,
|
|
304
|
+
retryTo,
|
|
305
|
+
tryTo,
|
|
306
|
+
within,
|
|
307
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import assert from 'assert'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unified WebElement class that wraps native element instances from different helpers
|
|
5
|
+
* and provides a consistent API across all supported helpers (Playwright, WebDriver, Puppeteer).
|
|
6
|
+
*/
|
|
7
|
+
class WebElement {
|
|
8
|
+
constructor(element, helper) {
|
|
9
|
+
this.element = element
|
|
10
|
+
this.helper = helper
|
|
11
|
+
this.helperType = this._detectHelperType(helper)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
_detectHelperType(helper) {
|
|
15
|
+
if (!helper) return 'unknown'
|
|
16
|
+
|
|
17
|
+
const className = helper.constructor.name
|
|
18
|
+
if (className === 'Playwright') return 'playwright'
|
|
19
|
+
if (className === 'WebDriver') return 'webdriver'
|
|
20
|
+
if (className === 'Puppeteer') return 'puppeteer'
|
|
21
|
+
|
|
22
|
+
return 'unknown'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the native element instance
|
|
27
|
+
* @returns {ElementHandle|WebElement|ElementHandle} Native element
|
|
28
|
+
*/
|
|
29
|
+
getNativeElement() {
|
|
30
|
+
return this.element
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the helper instance
|
|
35
|
+
* @returns {Helper} Helper instance
|
|
36
|
+
*/
|
|
37
|
+
getHelper() {
|
|
38
|
+
return this.helper
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get text content of the element
|
|
43
|
+
* @returns {Promise<string>} Element text content
|
|
44
|
+
*/
|
|
45
|
+
async getText() {
|
|
46
|
+
switch (this.helperType) {
|
|
47
|
+
case 'playwright':
|
|
48
|
+
return this.element.textContent()
|
|
49
|
+
case 'webdriver':
|
|
50
|
+
return this.element.getText()
|
|
51
|
+
case 'puppeteer':
|
|
52
|
+
return this.element.evaluate(el => el.textContent)
|
|
53
|
+
default:
|
|
54
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get attribute value of the element
|
|
60
|
+
* @param {string} name Attribute name
|
|
61
|
+
* @returns {Promise<string|null>} Attribute value
|
|
62
|
+
*/
|
|
63
|
+
async getAttribute(name) {
|
|
64
|
+
switch (this.helperType) {
|
|
65
|
+
case 'playwright':
|
|
66
|
+
return this.element.getAttribute(name)
|
|
67
|
+
case 'webdriver':
|
|
68
|
+
return this.element.getAttribute(name)
|
|
69
|
+
case 'puppeteer':
|
|
70
|
+
return this.element.evaluate((el, attrName) => el.getAttribute(attrName), name)
|
|
71
|
+
default:
|
|
72
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get property value of the element
|
|
78
|
+
* @param {string} name Property name
|
|
79
|
+
* @returns {Promise<any>} Property value
|
|
80
|
+
*/
|
|
81
|
+
async getProperty(name) {
|
|
82
|
+
switch (this.helperType) {
|
|
83
|
+
case 'playwright':
|
|
84
|
+
return this.element.evaluate((el, propName) => el[propName], name)
|
|
85
|
+
case 'webdriver':
|
|
86
|
+
return this.element.getProperty(name)
|
|
87
|
+
case 'puppeteer':
|
|
88
|
+
return this.element.evaluate((el, propName) => el[propName], name)
|
|
89
|
+
default:
|
|
90
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get innerHTML of the element
|
|
96
|
+
* @returns {Promise<string>} Element innerHTML
|
|
97
|
+
*/
|
|
98
|
+
async getInnerHTML() {
|
|
99
|
+
switch (this.helperType) {
|
|
100
|
+
case 'playwright':
|
|
101
|
+
return this.element.innerHTML()
|
|
102
|
+
case 'webdriver':
|
|
103
|
+
return this.element.getProperty('innerHTML')
|
|
104
|
+
case 'puppeteer':
|
|
105
|
+
return this.element.evaluate(el => el.innerHTML)
|
|
106
|
+
default:
|
|
107
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get value of the element (for input elements)
|
|
113
|
+
* @returns {Promise<string>} Element value
|
|
114
|
+
*/
|
|
115
|
+
async getValue() {
|
|
116
|
+
switch (this.helperType) {
|
|
117
|
+
case 'playwright':
|
|
118
|
+
return this.element.inputValue()
|
|
119
|
+
case 'webdriver':
|
|
120
|
+
return this.element.getValue()
|
|
121
|
+
case 'puppeteer':
|
|
122
|
+
return this.element.evaluate(el => el.value)
|
|
123
|
+
default:
|
|
124
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if element is visible
|
|
130
|
+
* @returns {Promise<boolean>} True if element is visible
|
|
131
|
+
*/
|
|
132
|
+
async isVisible() {
|
|
133
|
+
switch (this.helperType) {
|
|
134
|
+
case 'playwright':
|
|
135
|
+
return this.element.isVisible()
|
|
136
|
+
case 'webdriver':
|
|
137
|
+
return this.element.isDisplayed()
|
|
138
|
+
case 'puppeteer':
|
|
139
|
+
return this.element.evaluate(el => {
|
|
140
|
+
const style = window.getComputedStyle(el)
|
|
141
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'
|
|
142
|
+
})
|
|
143
|
+
default:
|
|
144
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if element is enabled
|
|
150
|
+
* @returns {Promise<boolean>} True if element is enabled
|
|
151
|
+
*/
|
|
152
|
+
async isEnabled() {
|
|
153
|
+
switch (this.helperType) {
|
|
154
|
+
case 'playwright':
|
|
155
|
+
return this.element.isEnabled()
|
|
156
|
+
case 'webdriver':
|
|
157
|
+
return this.element.isEnabled()
|
|
158
|
+
case 'puppeteer':
|
|
159
|
+
return this.element.evaluate(el => !el.disabled)
|
|
160
|
+
default:
|
|
161
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if element exists in DOM
|
|
167
|
+
* @returns {Promise<boolean>} True if element exists
|
|
168
|
+
*/
|
|
169
|
+
async exists() {
|
|
170
|
+
try {
|
|
171
|
+
switch (this.helperType) {
|
|
172
|
+
case 'playwright':
|
|
173
|
+
// For Playwright, if we have the element, it exists
|
|
174
|
+
return await this.element.evaluate(el => !!el)
|
|
175
|
+
case 'webdriver':
|
|
176
|
+
// For WebDriver, if we have the element, it exists
|
|
177
|
+
return true
|
|
178
|
+
case 'puppeteer':
|
|
179
|
+
// For Puppeteer, if we have the element, it exists
|
|
180
|
+
return await this.element.evaluate(el => !!el)
|
|
181
|
+
default:
|
|
182
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
return false
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get bounding box of the element
|
|
191
|
+
* @returns {Promise<Object>} Bounding box with x, y, width, height properties
|
|
192
|
+
*/
|
|
193
|
+
async getBoundingBox() {
|
|
194
|
+
switch (this.helperType) {
|
|
195
|
+
case 'playwright':
|
|
196
|
+
return this.element.boundingBox()
|
|
197
|
+
case 'webdriver':
|
|
198
|
+
const rect = await this.element.getRect()
|
|
199
|
+
return {
|
|
200
|
+
x: rect.x,
|
|
201
|
+
y: rect.y,
|
|
202
|
+
width: rect.width,
|
|
203
|
+
height: rect.height,
|
|
204
|
+
}
|
|
205
|
+
case 'puppeteer':
|
|
206
|
+
return this.element.boundingBox()
|
|
207
|
+
default:
|
|
208
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Click the element
|
|
214
|
+
* @param {Object} options Click options
|
|
215
|
+
* @returns {Promise<void>}
|
|
216
|
+
*/
|
|
217
|
+
async click(options = {}) {
|
|
218
|
+
switch (this.helperType) {
|
|
219
|
+
case 'playwright':
|
|
220
|
+
return this.element.click(options)
|
|
221
|
+
case 'webdriver':
|
|
222
|
+
return this.element.click()
|
|
223
|
+
case 'puppeteer':
|
|
224
|
+
return this.element.click(options)
|
|
225
|
+
default:
|
|
226
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Type text into the element
|
|
232
|
+
* @param {string} text Text to type
|
|
233
|
+
* @param {Object} options Type options
|
|
234
|
+
* @returns {Promise<void>}
|
|
235
|
+
*/
|
|
236
|
+
async type(text, options = {}) {
|
|
237
|
+
switch (this.helperType) {
|
|
238
|
+
case 'playwright':
|
|
239
|
+
return this.element.type(text, options)
|
|
240
|
+
case 'webdriver':
|
|
241
|
+
return this.element.setValue(text)
|
|
242
|
+
case 'puppeteer':
|
|
243
|
+
return this.element.type(text, options)
|
|
244
|
+
default:
|
|
245
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Find first child element matching the locator
|
|
251
|
+
* @param {string|Object} locator Element locator
|
|
252
|
+
* @returns {Promise<WebElement|null>} WebElement instance or null if not found
|
|
253
|
+
*/
|
|
254
|
+
async $(locator) {
|
|
255
|
+
let childElement
|
|
256
|
+
|
|
257
|
+
switch (this.helperType) {
|
|
258
|
+
case 'playwright':
|
|
259
|
+
childElement = await this.element.$(this._normalizeLocator(locator))
|
|
260
|
+
break
|
|
261
|
+
case 'webdriver':
|
|
262
|
+
try {
|
|
263
|
+
childElement = await this.element.$(this._normalizeLocator(locator))
|
|
264
|
+
} catch (e) {
|
|
265
|
+
return null
|
|
266
|
+
}
|
|
267
|
+
break
|
|
268
|
+
case 'puppeteer':
|
|
269
|
+
childElement = await this.element.$(this._normalizeLocator(locator))
|
|
270
|
+
break
|
|
271
|
+
default:
|
|
272
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return childElement ? new WebElement(childElement, this.helper) : null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Find all child elements matching the locator
|
|
280
|
+
* @param {string|Object} locator Element locator
|
|
281
|
+
* @returns {Promise<WebElement[]>} Array of WebElement instances
|
|
282
|
+
*/
|
|
283
|
+
async $$(locator) {
|
|
284
|
+
let childElements
|
|
285
|
+
|
|
286
|
+
switch (this.helperType) {
|
|
287
|
+
case 'playwright':
|
|
288
|
+
childElements = await this.element.$$(this._normalizeLocator(locator))
|
|
289
|
+
break
|
|
290
|
+
case 'webdriver':
|
|
291
|
+
childElements = await this.element.$$(this._normalizeLocator(locator))
|
|
292
|
+
break
|
|
293
|
+
case 'puppeteer':
|
|
294
|
+
childElements = await this.element.$$(this._normalizeLocator(locator))
|
|
295
|
+
break
|
|
296
|
+
default:
|
|
297
|
+
throw new Error(`Unsupported helper type: ${this.helperType}`)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return childElements.map(el => new WebElement(el, this.helper))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Normalize locator for element search
|
|
305
|
+
* @param {string|Object} locator Locator to normalize
|
|
306
|
+
* @returns {string} Normalized CSS selector
|
|
307
|
+
* @private
|
|
308
|
+
*/
|
|
309
|
+
_normalizeLocator(locator) {
|
|
310
|
+
if (typeof locator === 'string') {
|
|
311
|
+
return locator
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (typeof locator === 'object') {
|
|
315
|
+
// Handle CodeceptJS locator objects
|
|
316
|
+
if (locator.css) return locator.css
|
|
317
|
+
if (locator.xpath) return locator.xpath
|
|
318
|
+
if (locator.id) return `#${locator.id}`
|
|
319
|
+
if (locator.name) return `[name="${locator.name}"]`
|
|
320
|
+
if (locator.className) return `.${locator.className}`
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return locator.toString()
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export default WebElement
|