codeceptjs 4.0.0-beta.3 → 4.0.0-beta.5
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 +134 -119
- package/bin/codecept.js +12 -2
- package/bin/test-server.js +53 -0
- package/docs/webapi/clearCookie.mustache +1 -1
- package/lib/actor.js +66 -102
- package/lib/ai.js +130 -121
- package/lib/assert/empty.js +3 -5
- package/lib/assert/equal.js +4 -7
- package/lib/assert/include.js +4 -6
- package/lib/assert/throws.js +2 -4
- package/lib/assert/truth.js +2 -2
- package/lib/codecept.js +141 -86
- package/lib/command/check.js +201 -0
- package/lib/command/configMigrate.js +2 -4
- package/lib/command/definitions.js +8 -26
- package/lib/command/dryRun.js +30 -35
- package/lib/command/generate.js +10 -14
- package/lib/command/gherkin/snippets.js +75 -73
- package/lib/command/gherkin/steps.js +1 -1
- package/lib/command/info.js +42 -8
- package/lib/command/init.js +13 -12
- package/lib/command/interactive.js +10 -2
- package/lib/command/list.js +1 -1
- package/lib/command/run-multiple/chunk.js +48 -45
- package/lib/command/run-multiple.js +12 -35
- package/lib/command/run-workers.js +21 -58
- package/lib/command/utils.js +5 -6
- package/lib/command/workers/runTests.js +263 -222
- package/lib/container.js +386 -238
- package/lib/data/context.js +10 -13
- package/lib/data/dataScenarioConfig.js +8 -8
- package/lib/data/dataTableArgument.js +6 -6
- package/lib/data/table.js +5 -11
- package/lib/effects.js +223 -0
- package/lib/element/WebElement.js +327 -0
- package/lib/els.js +158 -0
- package/lib/event.js +21 -17
- package/lib/heal.js +88 -80
- package/lib/helper/AI.js +2 -1
- package/lib/helper/ApiDataFactory.js +4 -7
- package/lib/helper/Appium.js +50 -57
- package/lib/helper/FileSystem.js +3 -3
- package/lib/helper/GraphQLDataFactory.js +4 -4
- package/lib/helper/JSONResponse.js +75 -37
- package/lib/helper/Mochawesome.js +31 -9
- package/lib/helper/Nightmare.js +37 -58
- package/lib/helper/Playwright.js +267 -272
- package/lib/helper/Protractor.js +56 -87
- package/lib/helper/Puppeteer.js +247 -264
- package/lib/helper/REST.js +29 -17
- package/lib/helper/TestCafe.js +22 -47
- package/lib/helper/WebDriver.js +157 -368
- package/lib/helper/extras/PlaywrightPropEngine.js +2 -2
- package/lib/helper/extras/Popup.js +22 -22
- package/lib/helper/network/utils.js +1 -1
- package/lib/helper/testcafe/testcafe-utils.js +27 -28
- package/lib/listener/emptyRun.js +55 -0
- package/lib/listener/exit.js +7 -10
- package/lib/listener/{retry.js → globalRetry.js} +5 -5
- package/lib/listener/globalTimeout.js +165 -0
- package/lib/listener/helpers.js +15 -15
- package/lib/listener/mocha.js +1 -1
- package/lib/listener/result.js +12 -0
- package/lib/listener/retryEnhancer.js +85 -0
- package/lib/listener/steps.js +32 -18
- package/lib/listener/store.js +20 -0
- package/lib/locator.js +1 -1
- package/lib/mocha/asyncWrapper.js +231 -0
- package/lib/{interfaces → mocha}/bdd.js +3 -3
- package/lib/mocha/cli.js +308 -0
- package/lib/mocha/factory.js +104 -0
- package/lib/{interfaces → mocha}/featureConfig.js +32 -12
- package/lib/{interfaces → mocha}/gherkin.js +26 -28
- package/lib/mocha/hooks.js +112 -0
- package/lib/mocha/index.js +12 -0
- package/lib/mocha/inject.js +29 -0
- package/lib/{interfaces → mocha}/scenarioConfig.js +31 -7
- package/lib/mocha/suite.js +82 -0
- package/lib/mocha/test.js +181 -0
- package/lib/mocha/types.d.ts +42 -0
- package/lib/mocha/ui.js +232 -0
- package/lib/output.js +93 -65
- package/lib/pause.js +160 -138
- package/lib/plugin/analyze.js +396 -0
- package/lib/plugin/auth.js +435 -0
- package/lib/plugin/autoDelay.js +8 -8
- package/lib/plugin/autoLogin.js +3 -338
- package/lib/plugin/commentStep.js +6 -1
- package/lib/plugin/coverage.js +10 -22
- package/lib/plugin/customLocator.js +3 -3
- package/lib/plugin/customReporter.js +52 -0
- package/lib/plugin/eachElement.js +1 -1
- package/lib/plugin/fakerTransform.js +1 -1
- package/lib/plugin/heal.js +36 -9
- package/lib/plugin/htmlReporter.js +1947 -0
- package/lib/plugin/pageInfo.js +140 -0
- package/lib/plugin/retryFailedStep.js +17 -18
- package/lib/plugin/retryTo.js +2 -113
- package/lib/plugin/screenshotOnFail.js +17 -58
- package/lib/plugin/selenoid.js +15 -35
- package/lib/plugin/standardActingHelpers.js +4 -1
- package/lib/plugin/stepByStepReport.js +56 -17
- package/lib/plugin/stepTimeout.js +5 -12
- package/lib/plugin/subtitles.js +4 -4
- package/lib/plugin/tryTo.js +3 -102
- package/lib/plugin/wdio.js +8 -10
- package/lib/recorder.js +155 -124
- package/lib/rerun.js +43 -42
- package/lib/result.js +161 -0
- package/lib/secret.js +1 -2
- 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 +21 -332
- package/lib/steps.js +50 -0
- package/lib/store.js +37 -5
- package/lib/template/heal.js +2 -11
- package/lib/test-server.js +323 -0
- package/lib/timeout.js +66 -0
- package/lib/utils.js +351 -218
- package/lib/within.js +75 -55
- package/lib/workerStorage.js +2 -1
- package/lib/workers.js +386 -277
- package/package.json +81 -75
- package/translations/de-DE.js +5 -3
- package/translations/fr-FR.js +5 -4
- 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/index.d.ts +197 -187
- package/typings/promiseBasedTypes.d.ts +53 -903
- package/typings/types.d.ts +372 -1042
- package/lib/cli.js +0 -257
- package/lib/helper/ExpectHelper.js +0 -391
- package/lib/helper/MockServer.js +0 -221
- package/lib/helper/SoftExpectHelper.js +0 -381
- package/lib/listener/artifacts.js +0 -19
- package/lib/listener/timeout.js +0 -109
- package/lib/mochaFactory.js +0 -113
- package/lib/plugin/debugErrors.js +0 -67
- package/lib/scenario.js +0 -224
- package/lib/ui.js +0 -236
package/lib/heal.js
CHANGED
|
@@ -1,122 +1,125 @@
|
|
|
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')
|
|
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
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @class
|
|
10
10
|
*/
|
|
11
11
|
class Heal {
|
|
12
12
|
constructor() {
|
|
13
|
-
this.recipes = {}
|
|
14
|
-
this.fixes = []
|
|
15
|
-
this.prepareFns = []
|
|
16
|
-
this.contextName = null
|
|
17
|
-
this.numHealed = 0
|
|
13
|
+
this.recipes = {}
|
|
14
|
+
this.fixes = []
|
|
15
|
+
this.prepareFns = []
|
|
16
|
+
this.contextName = null
|
|
17
|
+
this.numHealed = 0
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
clear() {
|
|
21
|
-
this.recipes = {}
|
|
22
|
-
this.fixes = []
|
|
23
|
-
this.prepareFns = []
|
|
24
|
-
this.contextName = null
|
|
25
|
-
this.numHealed = 0
|
|
21
|
+
this.recipes = {}
|
|
22
|
+
this.fixes = []
|
|
23
|
+
this.prepareFns = []
|
|
24
|
+
this.contextName = null
|
|
25
|
+
this.numHealed = 0
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
addRecipe(name, opts = {}) {
|
|
29
|
-
if (!opts.priority) opts.priority = 0
|
|
29
|
+
if (!opts.priority) opts.priority = 0
|
|
30
30
|
|
|
31
|
-
if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`)
|
|
31
|
+
if (!opts.fn) throw new Error(`Recipe ${name} should have a function 'fn' to execute`)
|
|
32
32
|
|
|
33
|
-
this.recipes[name] = opts
|
|
33
|
+
this.recipes[name] = opts
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
connectToEvents() {
|
|
37
|
-
event.dispatcher.on(event.suite.before,
|
|
38
|
-
this.contextName = suite.title
|
|
39
|
-
})
|
|
37
|
+
event.dispatcher.on(event.suite.before, suite => {
|
|
38
|
+
this.contextName = suite.title
|
|
39
|
+
})
|
|
40
40
|
|
|
41
|
-
event.dispatcher.on(event.test.started,
|
|
42
|
-
this.contextName = test.fullTitle()
|
|
43
|
-
})
|
|
41
|
+
event.dispatcher.on(event.test.started, test => {
|
|
42
|
+
this.contextName = test.fullTitle()
|
|
43
|
+
})
|
|
44
44
|
|
|
45
45
|
event.dispatcher.on(event.test.finished, () => {
|
|
46
|
-
this.contextName = null
|
|
47
|
-
})
|
|
46
|
+
this.contextName = null
|
|
47
|
+
})
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
hasCorrespondingRecipes(step) {
|
|
51
|
-
return matchRecipes(this.recipes, this.contextName)
|
|
52
|
-
.filter(r => !r.steps || r.steps.includes(step.name))
|
|
53
|
-
.length > 0;
|
|
51
|
+
return matchRecipes(this.recipes, this.contextName).filter(r => !r.steps || r.steps.includes(step.name)).length > 0
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
async getCodeSuggestions(context) {
|
|
57
|
-
const suggestions = []
|
|
58
|
-
const recipes = matchRecipes(this.recipes, this.contextName)
|
|
55
|
+
const suggestions = []
|
|
56
|
+
const recipes = matchRecipes(this.recipes, this.contextName)
|
|
59
57
|
|
|
60
|
-
debug('Recipes', recipes)
|
|
58
|
+
debug('Recipes', recipes)
|
|
61
59
|
|
|
62
|
-
const currentOutputLevel = output.level()
|
|
63
|
-
output.level(0)
|
|
60
|
+
const currentOutputLevel = output.level()
|
|
61
|
+
output.level(0)
|
|
64
62
|
|
|
65
|
-
for (const [property, prepareFn] of Object.entries(
|
|
66
|
-
|
|
63
|
+
for (const [property, prepareFn] of Object.entries(
|
|
64
|
+
recipes
|
|
65
|
+
.map(r => r.prepare)
|
|
66
|
+
.filter(p => !!p)
|
|
67
|
+
.reduce((acc, obj) => ({ ...acc, ...obj }), {}),
|
|
68
|
+
)) {
|
|
69
|
+
if (!prepareFn) continue
|
|
67
70
|
|
|
68
|
-
if (context[property]) continue
|
|
69
|
-
context[property] = await prepareFn(Container.support())
|
|
71
|
+
if (context[property]) continue
|
|
72
|
+
context[property] = await prepareFn(Container.support())
|
|
70
73
|
}
|
|
71
74
|
|
|
72
|
-
output.level(currentOutputLevel)
|
|
75
|
+
output.level(currentOutputLevel)
|
|
73
76
|
|
|
74
77
|
for (const recipe of recipes) {
|
|
75
|
-
let snippets = await recipe.fn(context)
|
|
76
|
-
if (!Array.isArray(snippets)) snippets = [snippets]
|
|
78
|
+
let snippets = await recipe.fn(context)
|
|
79
|
+
if (!Array.isArray(snippets)) snippets = [snippets]
|
|
77
80
|
|
|
78
81
|
suggestions.push({
|
|
79
82
|
name: recipe.name,
|
|
80
83
|
snippets,
|
|
81
|
-
})
|
|
84
|
+
})
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
return suggestions.filter(s => !isBlank(s.snippets))
|
|
87
|
+
return suggestions.filter(s => !isBlank(s.snippets))
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
async healStep(failedStep, error, failureContext = {}) {
|
|
88
|
-
output.debug(`Trying to heal ${failedStep.toCode()} step`)
|
|
91
|
+
output.debug(`Trying to heal ${failedStep.toCode()} step`)
|
|
89
92
|
|
|
90
93
|
Object.assign(failureContext, {
|
|
91
94
|
error,
|
|
92
95
|
step: failedStep,
|
|
93
96
|
prevSteps: failureContext?.test?.steps?.slice(0, -1) || [],
|
|
94
|
-
})
|
|
97
|
+
})
|
|
95
98
|
|
|
96
|
-
const suggestions = await this.getCodeSuggestions(failureContext)
|
|
99
|
+
const suggestions = await this.getCodeSuggestions(failureContext)
|
|
97
100
|
|
|
98
101
|
if (suggestions.length === 0) {
|
|
99
|
-
debug('No healing suggestions found')
|
|
100
|
-
throw error
|
|
102
|
+
debug('No healing suggestions found')
|
|
103
|
+
throw error
|
|
101
104
|
}
|
|
102
105
|
|
|
103
|
-
output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`)
|
|
106
|
+
output.debug(`Received ${suggestions.length} suggestion${suggestions.length === 1 ? '' : 's'}`)
|
|
104
107
|
|
|
105
|
-
debug(suggestions)
|
|
108
|
+
debug(suggestions)
|
|
106
109
|
|
|
107
110
|
for (const suggestion of suggestions) {
|
|
108
111
|
for (const codeSnippet of suggestion.snippets) {
|
|
109
112
|
try {
|
|
110
|
-
debug('Executing', codeSnippet)
|
|
111
|
-
recorder.catch(
|
|
112
|
-
debug(e)
|
|
113
|
-
})
|
|
113
|
+
debug('Executing', codeSnippet)
|
|
114
|
+
recorder.catch(e => {
|
|
115
|
+
debug(e)
|
|
116
|
+
})
|
|
114
117
|
|
|
115
118
|
if (typeof codeSnippet === 'string') {
|
|
116
|
-
const I = Container.support('I')
|
|
117
|
-
await eval(codeSnippet)
|
|
119
|
+
const I = Container.support('I')
|
|
120
|
+
await eval(codeSnippet)
|
|
118
121
|
} else if (typeof codeSnippet === 'function') {
|
|
119
|
-
await codeSnippet(Container.support())
|
|
122
|
+
await codeSnippet(Container.support())
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
this.fixes.push({
|
|
@@ -124,49 +127,54 @@ class Heal {
|
|
|
124
127
|
test: failureContext?.test,
|
|
125
128
|
step: failedStep,
|
|
126
129
|
snippet: codeSnippet,
|
|
127
|
-
})
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
if (failureContext?.test) {
|
|
133
|
+
const test = failureContext.test
|
|
134
|
+
let note = `This test was healed by '${suggestion.name}'`
|
|
135
|
+
note += `\n\nReplace the failed code:\n\n`
|
|
136
|
+
note += colors.red(`- ${failedStep.toCode()}\n`)
|
|
137
|
+
note += colors.green(`+ ${codeSnippet}\n`)
|
|
138
|
+
test.addNote('heal', note)
|
|
139
|
+
test.meta.healed = true
|
|
140
|
+
}
|
|
128
141
|
|
|
129
|
-
recorder.add('healed', () => output.print(colors.bold.green(` Code healed successfully by ${suggestion.name}`), colors.gray('(no errors thrown)')))
|
|
130
|
-
this.numHealed
|
|
142
|
+
recorder.add('healed', () => output.print(colors.bold.green(` Code healed successfully by ${suggestion.name}`), colors.gray('(no errors thrown)')))
|
|
143
|
+
this.numHealed++
|
|
131
144
|
// recorder.session.restore();
|
|
132
|
-
return
|
|
145
|
+
return
|
|
133
146
|
} catch (err) {
|
|
134
|
-
debug('Failed to execute code', err)
|
|
135
|
-
recorder.ignoreErr(err)
|
|
136
|
-
recorder.catchWithoutStop(err)
|
|
137
|
-
await recorder.promise()
|
|
147
|
+
debug('Failed to execute code', err)
|
|
148
|
+
recorder.ignoreErr(err) // healing did not help
|
|
149
|
+
recorder.catchWithoutStop(err)
|
|
150
|
+
await recorder.promise() // wait for all promises to resolve
|
|
138
151
|
}
|
|
139
152
|
}
|
|
140
153
|
}
|
|
141
|
-
output.debug(`Couldn't heal the code for ${failedStep.toCode()}`)
|
|
142
|
-
recorder.throw(error)
|
|
154
|
+
output.debug(`Couldn't heal the code for ${failedStep.toCode()}`)
|
|
155
|
+
recorder.throw(error)
|
|
143
156
|
}
|
|
144
157
|
|
|
145
158
|
static setDefaultHealers() {
|
|
146
|
-
require('./template/heal')
|
|
159
|
+
require('./template/heal')
|
|
147
160
|
}
|
|
148
161
|
}
|
|
149
162
|
|
|
150
|
-
const heal = new Heal()
|
|
163
|
+
const heal = new Heal()
|
|
151
164
|
|
|
152
|
-
module.exports = heal
|
|
165
|
+
module.exports = heal
|
|
153
166
|
|
|
154
167
|
function matchRecipes(recipes, contextName) {
|
|
155
168
|
return Object.entries(recipes)
|
|
156
169
|
.filter(([, recipe]) => !contextName || !recipe.grep || new RegExp(recipe.grep).test(contextName))
|
|
157
170
|
.sort(([, a], [, b]) => a.priority - b.priority)
|
|
158
171
|
.map(([name, recipe]) => {
|
|
159
|
-
recipe.name = name
|
|
160
|
-
return recipe
|
|
172
|
+
recipe.name = name
|
|
173
|
+
return recipe
|
|
161
174
|
})
|
|
162
|
-
.filter(r => !!r.fn)
|
|
175
|
+
.filter(r => !!r.fn)
|
|
163
176
|
}
|
|
164
177
|
|
|
165
178
|
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
|
-
);
|
|
179
|
+
return value == null || (Array.isArray(value) && value.length === 0) || (typeof value === 'object' && Object.keys(value).length === 0) || (typeof value === 'string' && value.trim() === '')
|
|
172
180
|
}
|
package/lib/helper/AI.js
CHANGED
|
@@ -3,13 +3,14 @@ const ora = require('ora-classic')
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const ai = require('../ai')
|
|
6
|
-
const standardActingHelpers = require('../plugin/standardActingHelpers')
|
|
7
6
|
const Container = require('../container')
|
|
8
7
|
const { splitByChunks, minifyHtml } = require('../html')
|
|
9
8
|
const { beautify } = require('../utils')
|
|
10
9
|
const output = require('../output')
|
|
11
10
|
const { registerVariable } = require('../pause')
|
|
12
11
|
|
|
12
|
+
const standardActingHelpers = Container.STANDARD_ACTING_HELPERS
|
|
13
|
+
|
|
13
14
|
const gtpRole = {
|
|
14
15
|
user: 'user',
|
|
15
16
|
}
|
|
@@ -51,7 +51,7 @@ const REST = require('./REST')
|
|
|
51
51
|
*
|
|
52
52
|
* module.exports = new Factory()
|
|
53
53
|
* // no need to set id, it will be set by REST API
|
|
54
|
-
* .attr('author', () => faker.
|
|
54
|
+
* .attr('author', () => faker.person.findName())
|
|
55
55
|
* .attr('title', () => faker.lorem.sentence())
|
|
56
56
|
* .attr('body', () => faker.lorem.paragraph());
|
|
57
57
|
* ```
|
|
@@ -217,7 +217,7 @@ class ApiDataFactory extends Helper {
|
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
this.created = {}
|
|
220
|
-
Object.keys(this.factories).forEach(
|
|
220
|
+
Object.keys(this.factories).forEach(f => (this.created[f] = []))
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
static _checkRequirements() {
|
|
@@ -357,7 +357,7 @@ Current file error: ${err.message}`)
|
|
|
357
357
|
|
|
358
358
|
request.baseURL = this.config.endpoint
|
|
359
359
|
|
|
360
|
-
return this.restHelper._executeRequest(request).then(
|
|
360
|
+
return this.restHelper._executeRequest(request).then(resp => {
|
|
361
361
|
const id = this._fetchId(resp.data, factory)
|
|
362
362
|
this.created[factory].push(id)
|
|
363
363
|
this.debugSection('Created', `Id: ${id}`)
|
|
@@ -391,10 +391,7 @@ Current file error: ${err.message}`)
|
|
|
391
391
|
request.baseURL = this.config.endpoint
|
|
392
392
|
|
|
393
393
|
if (request.url.match(/^undefined/)) {
|
|
394
|
-
return this.debugSection(
|
|
395
|
-
'Please configure the delete request in your ApiDataFactory helper',
|
|
396
|
-
"delete: () => ({ method: 'DELETE', url: '/api/users' })",
|
|
397
|
-
)
|
|
394
|
+
return this.debugSection('Please configure the delete request in your ApiDataFactory helper', "delete: () => ({ method: 'DELETE', url: '/api/users' })")
|
|
398
395
|
}
|
|
399
396
|
|
|
400
397
|
return this.restHelper._executeRequest(request).then(() => {
|
package/lib/helper/Appium.js
CHANGED
|
@@ -44,7 +44,7 @@ const vendorPrefix = {
|
|
|
44
44
|
*
|
|
45
45
|
* This helper should be configured in codecept.conf.ts or codecept.conf.js
|
|
46
46
|
*
|
|
47
|
-
* * `appiumV2`: set this to
|
|
47
|
+
* * `appiumV2`: by default is true, set this to false if you want to run tests with AppiumV1. See more how to setup [here](https://codecept.io/mobile/#setting-up)
|
|
48
48
|
* * `app`: Application path. Local path or remote URL to an .ipa or .apk file, or a .zip containing one of these. Alias to desiredCapabilities.appPackage
|
|
49
49
|
* * `host`: (default: 'localhost') Appium host
|
|
50
50
|
* * `port`: (default: '4723') Appium port
|
|
@@ -124,7 +124,7 @@ const vendorPrefix = {
|
|
|
124
124
|
* {
|
|
125
125
|
* helpers: {
|
|
126
126
|
* Appium: {
|
|
127
|
-
* appiumV2: true,
|
|
127
|
+
* appiumV2: true, // By default is true, set to false if you want to run against Appium v1
|
|
128
128
|
* host: "hub-cloud.browserstack.com",
|
|
129
129
|
* port: 4444,
|
|
130
130
|
* user: process.env.BROWSERSTACK_USER,
|
|
@@ -178,16 +178,12 @@ class Appium extends Webdriver {
|
|
|
178
178
|
super(config)
|
|
179
179
|
|
|
180
180
|
this.isRunning = false
|
|
181
|
-
|
|
182
|
-
this.appiumV2 = true
|
|
183
|
-
}
|
|
181
|
+
this.appiumV2 = config.appiumV2 || true
|
|
184
182
|
this.axios = axios.create()
|
|
185
183
|
|
|
186
184
|
webdriverio = require('webdriverio')
|
|
187
185
|
if (!config.appiumV2) {
|
|
188
|
-
console.log(
|
|
189
|
-
'The Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. Please migrating to Appium 2.x by adding appiumV2: true to your config.',
|
|
190
|
-
)
|
|
186
|
+
console.log('The Appium core team does not maintain Appium 1.x anymore since the 1st of January 2022. Appium 2.x is used by default.')
|
|
191
187
|
console.log('More info: https://bit.ly/appium-v2-migration')
|
|
192
188
|
console.log('This Appium 1.x support will be removed in next major release.')
|
|
193
189
|
}
|
|
@@ -234,20 +230,14 @@ class Appium extends Webdriver {
|
|
|
234
230
|
|
|
235
231
|
config.baseUrl = config.url || config.baseUrl
|
|
236
232
|
if (config.desiredCapabilities && Object.keys(config.desiredCapabilities).length) {
|
|
237
|
-
config.capabilities =
|
|
238
|
-
this.appiumV2 === true ? this._convertAppiumV2Caps(config.desiredCapabilities) : config.desiredCapabilities
|
|
233
|
+
config.capabilities = this.appiumV2 === true ? this._convertAppiumV2Caps(config.desiredCapabilities) : config.desiredCapabilities
|
|
239
234
|
}
|
|
240
235
|
|
|
241
236
|
if (this.appiumV2) {
|
|
242
|
-
config.capabilities[`${vendorPrefix.appium}:deviceName`] =
|
|
243
|
-
|
|
244
|
-
config.capabilities[`${vendorPrefix.appium}:
|
|
245
|
-
|
|
246
|
-
config.capabilities[`${vendorPrefix.appium}:app`] =
|
|
247
|
-
config[`${vendorPrefix.appium}:app`] || config.capabilities[`${vendorPrefix.appium}:app`]
|
|
248
|
-
config.capabilities[`${vendorPrefix.appium}:tunnelIdentifier`] =
|
|
249
|
-
config[`${vendorPrefix.appium}:tunnelIdentifier`] ||
|
|
250
|
-
config.capabilities[`${vendorPrefix.appium}:tunnelIdentifier`] // Adding the code to connect to sauce labs via sauce tunnel
|
|
237
|
+
config.capabilities[`${vendorPrefix.appium}:deviceName`] = config[`${vendorPrefix.appium}:device`] || config.capabilities[`${vendorPrefix.appium}:deviceName`]
|
|
238
|
+
config.capabilities[`${vendorPrefix.appium}:browserName`] = config[`${vendorPrefix.appium}:browser`] || config.capabilities[`${vendorPrefix.appium}:browserName`]
|
|
239
|
+
config.capabilities[`${vendorPrefix.appium}:app`] = config[`${vendorPrefix.appium}:app`] || config.capabilities[`${vendorPrefix.appium}:app`]
|
|
240
|
+
config.capabilities[`${vendorPrefix.appium}:tunnelIdentifier`] = config[`${vendorPrefix.appium}:tunnelIdentifier`] || config.capabilities[`${vendorPrefix.appium}:tunnelIdentifier`] // Adding the code to connect to sauce labs via sauce tunnel
|
|
251
241
|
} else {
|
|
252
242
|
config.capabilities.deviceName = config.device || config.capabilities.deviceName
|
|
253
243
|
config.capabilities.browserName = config.browser || config.capabilities.browserName
|
|
@@ -393,8 +383,10 @@ class Appium extends Webdriver {
|
|
|
393
383
|
|
|
394
384
|
_buildAppiumEndpoint() {
|
|
395
385
|
const { protocol, port, hostname, path } = this.browser.options
|
|
386
|
+
// Ensure path does NOT end with a slash to prevent double slashes
|
|
387
|
+
const normalizedPath = path.replace(/\/$/, '')
|
|
396
388
|
// Build path to Appium REST API endpoint
|
|
397
|
-
return `${protocol}://${hostname}:${port}${
|
|
389
|
+
return `${protocol}://${hostname}:${port}${normalizedPath}/session/${this.browser.sessionId}`
|
|
398
390
|
}
|
|
399
391
|
|
|
400
392
|
/**
|
|
@@ -491,17 +483,15 @@ class Appium extends Webdriver {
|
|
|
491
483
|
* });
|
|
492
484
|
* ```
|
|
493
485
|
*
|
|
494
|
-
* @param {*} fn
|
|
495
486
|
*/
|
|
496
|
-
|
|
497
|
-
async runInWeb(
|
|
487
|
+
|
|
488
|
+
async runInWeb() {
|
|
498
489
|
if (!this.isWeb) return
|
|
499
490
|
recorder.session.start('Web-only actions')
|
|
500
491
|
|
|
501
492
|
recorder.add('restore from Web session', () => recorder.session.restore(), true)
|
|
502
493
|
return recorder.promise()
|
|
503
494
|
}
|
|
504
|
-
/* eslint-enable */
|
|
505
495
|
|
|
506
496
|
_runWithCaps(caps, fn) {
|
|
507
497
|
if (typeof caps === 'object') {
|
|
@@ -612,7 +602,7 @@ class Appium extends Webdriver {
|
|
|
612
602
|
|
|
613
603
|
return this.axios({
|
|
614
604
|
method: 'post',
|
|
615
|
-
url: `${this._buildAppiumEndpoint()}/
|
|
605
|
+
url: `${this._buildAppiumEndpoint()}/appium/device/remove_app`,
|
|
616
606
|
data: { appId, bundleId },
|
|
617
607
|
})
|
|
618
608
|
}
|
|
@@ -629,7 +619,7 @@ class Appium extends Webdriver {
|
|
|
629
619
|
onlyForApps.call(this)
|
|
630
620
|
return this.axios({
|
|
631
621
|
method: 'post',
|
|
632
|
-
url: `${this._buildAppiumEndpoint()}/
|
|
622
|
+
url: `${this._buildAppiumEndpoint()}/appium/app/reset`,
|
|
633
623
|
})
|
|
634
624
|
}
|
|
635
625
|
|
|
@@ -703,7 +693,7 @@ class Appium extends Webdriver {
|
|
|
703
693
|
|
|
704
694
|
const res = await this.axios({
|
|
705
695
|
method: 'get',
|
|
706
|
-
url: `${this._buildAppiumEndpoint()}/
|
|
696
|
+
url: `${this._buildAppiumEndpoint()}/orientation`,
|
|
707
697
|
})
|
|
708
698
|
|
|
709
699
|
const currentOrientation = res.data.value
|
|
@@ -727,7 +717,7 @@ class Appium extends Webdriver {
|
|
|
727
717
|
|
|
728
718
|
return this.axios({
|
|
729
719
|
method: 'post',
|
|
730
|
-
url: `${this._buildAppiumEndpoint()}/
|
|
720
|
+
url: `${this._buildAppiumEndpoint()}/orientation`,
|
|
731
721
|
data: { orientation },
|
|
732
722
|
})
|
|
733
723
|
}
|
|
@@ -966,21 +956,19 @@ class Appium extends Webdriver {
|
|
|
966
956
|
* ```js
|
|
967
957
|
* // taps outside to hide keyboard per default
|
|
968
958
|
* I.hideDeviceKeyboard();
|
|
969
|
-
* I.hideDeviceKeyboard('tapOutside');
|
|
970
|
-
*
|
|
971
|
-
* // or by pressing key
|
|
972
|
-
* I.hideDeviceKeyboard('pressKey', 'Done');
|
|
973
959
|
* ```
|
|
974
960
|
*
|
|
975
961
|
* Appium: support Android and iOS
|
|
976
962
|
*
|
|
977
|
-
* @param {'tapOutside' | 'pressKey'} [strategy] Desired strategy to close keyboard (‘tapOutside’ or ‘pressKey’)
|
|
978
|
-
* @param {string} [key] Optional key
|
|
979
963
|
*/
|
|
980
|
-
async hideDeviceKeyboard(
|
|
964
|
+
async hideDeviceKeyboard() {
|
|
981
965
|
onlyForApps.call(this)
|
|
982
|
-
|
|
983
|
-
return this.
|
|
966
|
+
|
|
967
|
+
return this.axios({
|
|
968
|
+
method: 'post',
|
|
969
|
+
url: `${this._buildAppiumEndpoint()}/appium/device/hide_keyboard`,
|
|
970
|
+
data: {},
|
|
971
|
+
})
|
|
984
972
|
}
|
|
985
973
|
|
|
986
974
|
/**
|
|
@@ -1056,7 +1044,13 @@ class Appium extends Webdriver {
|
|
|
1056
1044
|
* @param {*} locator
|
|
1057
1045
|
*/
|
|
1058
1046
|
async tap(locator) {
|
|
1059
|
-
|
|
1047
|
+
const { elementId } = await this.browser.$(parseLocator.call(this, locator))
|
|
1048
|
+
|
|
1049
|
+
return this.axios({
|
|
1050
|
+
method: 'post',
|
|
1051
|
+
url: `${this._buildAppiumEndpoint()}/element/${elementId}/click`,
|
|
1052
|
+
data: {},
|
|
1053
|
+
})
|
|
1060
1054
|
}
|
|
1061
1055
|
|
|
1062
1056
|
/**
|
|
@@ -1077,7 +1071,7 @@ class Appium extends Webdriver {
|
|
|
1077
1071
|
*
|
|
1078
1072
|
* Appium: support Android and iOS
|
|
1079
1073
|
*/
|
|
1080
|
-
|
|
1074
|
+
|
|
1081
1075
|
async swipe(locator, xoffset, yoffset, speed = 1000) {
|
|
1082
1076
|
onlyForApps.call(this)
|
|
1083
1077
|
const res = await this.browser.$(parseLocator.call(this, locator))
|
|
@@ -1087,7 +1081,6 @@ class Appium extends Webdriver {
|
|
|
1087
1081
|
y: (await res.getLocation()).y + yoffset,
|
|
1088
1082
|
})
|
|
1089
1083
|
}
|
|
1090
|
-
/* eslint-enable */
|
|
1091
1084
|
|
|
1092
1085
|
/**
|
|
1093
1086
|
* Perform a swipe on the screen.
|
|
@@ -1307,14 +1300,14 @@ class Appium extends Webdriver {
|
|
|
1307
1300
|
}
|
|
1308
1301
|
return browser
|
|
1309
1302
|
.$$(parseLocator.call(this, searchableLocator))
|
|
1310
|
-
.then(
|
|
1311
|
-
.then(
|
|
1303
|
+
.then(els => els.length && els[0].isDisplayed())
|
|
1304
|
+
.then(res => {
|
|
1312
1305
|
if (res) {
|
|
1313
1306
|
return true
|
|
1314
1307
|
}
|
|
1315
1308
|
return this[direction](scrollLocator, offset, speed)
|
|
1316
1309
|
.getSource()
|
|
1317
|
-
.then(
|
|
1310
|
+
.then(source => {
|
|
1318
1311
|
if (source === currentSource) {
|
|
1319
1312
|
err = true
|
|
1320
1313
|
} else {
|
|
@@ -1327,12 +1320,9 @@ class Appium extends Webdriver {
|
|
|
1327
1320
|
timeout * 1000,
|
|
1328
1321
|
errorMsg,
|
|
1329
1322
|
)
|
|
1330
|
-
.catch(
|
|
1323
|
+
.catch(e => {
|
|
1331
1324
|
if (e.message.indexOf('timeout') && e.type !== 'NoSuchElement') {
|
|
1332
|
-
throw new AssertionFailedError(
|
|
1333
|
-
{ customMessage: `Scroll to the end and element ${searchableLocator} was not found` },
|
|
1334
|
-
'',
|
|
1335
|
-
)
|
|
1325
|
+
throw new AssertionFailedError({ customMessage: `Scroll to the end and element ${searchableLocator} was not found` }, '')
|
|
1336
1326
|
} else {
|
|
1337
1327
|
throw e
|
|
1338
1328
|
}
|
|
@@ -1389,8 +1379,8 @@ class Appium extends Webdriver {
|
|
|
1389
1379
|
*/
|
|
1390
1380
|
async pullFile(path, dest) {
|
|
1391
1381
|
onlyForApps.call(this)
|
|
1392
|
-
return this.browser.pullFile(path).then(
|
|
1393
|
-
fs.writeFile(dest, Buffer.from(res, 'base64'),
|
|
1382
|
+
return this.browser.pullFile(path).then(res =>
|
|
1383
|
+
fs.writeFile(dest, Buffer.from(res, 'base64'), err => {
|
|
1394
1384
|
if (err) {
|
|
1395
1385
|
return false
|
|
1396
1386
|
}
|
|
@@ -1507,7 +1497,14 @@ class Appium extends Webdriver {
|
|
|
1507
1497
|
*/
|
|
1508
1498
|
async click(locator, context) {
|
|
1509
1499
|
if (this.isWeb) return super.click(locator, context)
|
|
1510
|
-
|
|
1500
|
+
|
|
1501
|
+
const { elementId } = await this.browser.$(parseLocator.call(this, locator), parseLocator.call(this, context))
|
|
1502
|
+
|
|
1503
|
+
return this.axios({
|
|
1504
|
+
method: 'post',
|
|
1505
|
+
url: `${this._buildAppiumEndpoint()}/element/${elementId}/click`,
|
|
1506
|
+
data: {},
|
|
1507
|
+
})
|
|
1511
1508
|
}
|
|
1512
1509
|
|
|
1513
1510
|
/**
|
|
@@ -1762,12 +1759,8 @@ function parseLocator(locator) {
|
|
|
1762
1759
|
}
|
|
1763
1760
|
|
|
1764
1761
|
locator = new Locator(locator, 'xpath')
|
|
1765
|
-
if (locator.type === 'css' && !this.isWeb)
|
|
1766
|
-
|
|
1767
|
-
'Unable to use css locators in apps. Locator strategies for this request: xpath, id, class name or accessibility id',
|
|
1768
|
-
)
|
|
1769
|
-
if (locator.type === 'name' && !this.isWeb)
|
|
1770
|
-
throw new Error("Can't locate element by name in Native context. Use either ID, class name or accessibility id")
|
|
1762
|
+
if (locator.type === 'css' && !this.isWeb) throw new Error('Unable to use css locators in apps. Locator strategies for this request: xpath, id, class name or accessibility id')
|
|
1763
|
+
if (locator.type === 'name' && !this.isWeb) throw new Error("Can't locate element by name in Native context. Use either ID, class name or accessibility id")
|
|
1771
1764
|
if (locator.type === 'id' && !this.isWeb && this.platform === 'android') return `//*[@resource-id='${locator.value}']`
|
|
1772
1765
|
return locator.simplify()
|
|
1773
1766
|
}
|
package/lib/helper/FileSystem.js
CHANGED
|
@@ -105,7 +105,7 @@ class FileSystem extends Helper {
|
|
|
105
105
|
*/
|
|
106
106
|
seeFileNameMatching(text) {
|
|
107
107
|
assert.ok(
|
|
108
|
-
this.grabFileNames().some(
|
|
108
|
+
this.grabFileNames().some(file => file.includes(text)),
|
|
109
109
|
`File name which contains ${text} not found in ${this.dir}`,
|
|
110
110
|
)
|
|
111
111
|
}
|
|
@@ -175,7 +175,7 @@ class FileSystem extends Helper {
|
|
|
175
175
|
* ```
|
|
176
176
|
*/
|
|
177
177
|
grabFileNames() {
|
|
178
|
-
return fs.readdirSync(this.dir).filter(
|
|
178
|
+
return fs.readdirSync(this.dir).filter(item => !fs.lstatSync(path.join(this.dir, item)).isDirectory())
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
@@ -216,7 +216,7 @@ function isFileExists(file, timeout) {
|
|
|
216
216
|
}
|
|
217
217
|
})
|
|
218
218
|
|
|
219
|
-
fs.access(file, fs.constants.R_OK,
|
|
219
|
+
fs.access(file, fs.constants.R_OK, err => {
|
|
220
220
|
if (!err) {
|
|
221
221
|
clearTimeout(timer)
|
|
222
222
|
watcher.close()
|
|
@@ -55,7 +55,7 @@ const GraphQL = require('./GraphQL')
|
|
|
55
55
|
* input: { ...buildObj },
|
|
56
56
|
* }))
|
|
57
57
|
* // 'attr'-id can be left out depending on the GraphQl resolvers
|
|
58
|
-
* .attr('name', () => faker.
|
|
58
|
+
* .attr('name', () => faker.person.findName())
|
|
59
59
|
* .attr('email', () => faker.interact.email())
|
|
60
60
|
* ```
|
|
61
61
|
* For more options see [rosie documentation](https://github.com/rosiejs/rosie).
|
|
@@ -170,7 +170,7 @@ class GraphQLDataFactory extends Helper {
|
|
|
170
170
|
this.factories = this.config.factories
|
|
171
171
|
|
|
172
172
|
this.created = {}
|
|
173
|
-
Object.keys(this.factories).forEach(
|
|
173
|
+
Object.keys(this.factories).forEach(f => (this.created[f] = []))
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
static _checkRequirements() {
|
|
@@ -278,7 +278,7 @@ class GraphQLDataFactory extends Helper {
|
|
|
278
278
|
*/
|
|
279
279
|
_requestCreate(operation, variables) {
|
|
280
280
|
const { query } = this.factories[operation]
|
|
281
|
-
return this.graphqlHelper.sendMutation(query, variables).then(
|
|
281
|
+
return this.graphqlHelper.sendMutation(query, variables).then(response => {
|
|
282
282
|
const data = response.data.data[operation]
|
|
283
283
|
this.created[operation].push(data)
|
|
284
284
|
this.debugSection('Created', `record: ${data}`)
|
|
@@ -297,7 +297,7 @@ class GraphQLDataFactory extends Helper {
|
|
|
297
297
|
const deleteOperation = this.factories[operation].revert(data)
|
|
298
298
|
const { query, variables } = deleteOperation
|
|
299
299
|
|
|
300
|
-
return this.graphqlHelper.sendMutation(query, variables).then(
|
|
300
|
+
return this.graphqlHelper.sendMutation(query, variables).then(response => {
|
|
301
301
|
const idx = this.created[operation].indexOf(data)
|
|
302
302
|
this.debugSection('Deleted', `record: ${response.data.data}`)
|
|
303
303
|
this.created[operation].splice(idx, 1)
|