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/template/heal.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { heal, ai } from 'codeceptjs'
|
|
2
2
|
|
|
3
3
|
heal.addRecipe('ai', {
|
|
4
4
|
priority: 10,
|
|
@@ -6,17 +6,8 @@ heal.addRecipe('ai', {
|
|
|
6
6
|
html: ({ I }) => I.grabHTMLFrom('body'),
|
|
7
7
|
},
|
|
8
8
|
suggest: true,
|
|
9
|
-
steps: [
|
|
10
|
-
|
|
11
|
-
'fillField',
|
|
12
|
-
'appendField',
|
|
13
|
-
'selectOption',
|
|
14
|
-
'attachFile',
|
|
15
|
-
'checkOption',
|
|
16
|
-
'uncheckOption',
|
|
17
|
-
'doubleClick',
|
|
18
|
-
],
|
|
19
|
-
fn: async (args) => {
|
|
9
|
+
steps: ['click', 'fillField', 'appendField', 'selectOption', 'attachFile', 'checkOption', 'uncheckOption', 'doubleClick'],
|
|
10
|
+
fn: async args => {
|
|
20
11
|
return ai.healFailedStep(args)
|
|
21
12
|
},
|
|
22
13
|
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export default (html, extraPrompt = '', rootLocator = null) => [
|
|
2
|
+
{
|
|
3
|
+
role: 'user',
|
|
4
|
+
content: `As a test automation engineer I am creating a Page Object for a web application using CodeceptJS.
|
|
5
|
+
Here is an sample page object:
|
|
6
|
+
|
|
7
|
+
const { I } = inject();
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
|
|
11
|
+
// setting locators
|
|
12
|
+
element1: '#selector',
|
|
13
|
+
element2: '.selector',
|
|
14
|
+
element3: locate().withText('text'),
|
|
15
|
+
|
|
16
|
+
// setting methods
|
|
17
|
+
doSomethingOnPage(params) {
|
|
18
|
+
// ...
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
I want to generate a Page Object for the page I provide.
|
|
23
|
+
Write JavaScript code in similar manner to list all locators on the page.
|
|
24
|
+
Use locators in order of preference: by text (use locate().withText()), label, CSS, XPath.
|
|
25
|
+
Avoid TailwindCSS, Bootstrap or React style formatting classes in locators.
|
|
26
|
+
Add methods to interact with page when needed.
|
|
27
|
+
${extraPrompt}
|
|
28
|
+
${rootLocator ? `All provided elements are inside '${rootLocator}'. Declare it as root variable and for every locator use locate(...).inside(root)` : ''}
|
|
29
|
+
Add only locators from this HTML: \n\n${html}`,
|
|
30
|
+
},
|
|
31
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export default (html, { step, error, prevSteps }) => {
|
|
2
|
+
return [
|
|
3
|
+
{
|
|
4
|
+
role: 'user',
|
|
5
|
+
content: `As a test automation engineer I am testing web application using CodeceptJS.
|
|
6
|
+
I want to heal a test that fails. Here is the list of executed steps: ${prevSteps.map(s => s.toString()).join(', ')}
|
|
7
|
+
Propose how to adjust ${step.toCode()} step to fix the test.
|
|
8
|
+
Use locators in order of preference: semantic locator by text, CSS, XPath. Use codeblocks marked with \`\`\`
|
|
9
|
+
Here is the error message: ${error.message}
|
|
10
|
+
Here is HTML code of a page where the failure has happened: \n\n${html}`,
|
|
11
|
+
},
|
|
12
|
+
]
|
|
13
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export default (html, input) => [
|
|
2
|
+
{
|
|
3
|
+
role: 'user',
|
|
4
|
+
content: `I am test engineer writing test in CodeceptJS
|
|
5
|
+
I have opened web page and I want to use CodeceptJS to ${input} on this page
|
|
6
|
+
Provide me valid CodeceptJS code to accomplish it
|
|
7
|
+
Use only locators from this HTML: \n\n${html}`,
|
|
8
|
+
},
|
|
9
|
+
]
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Internal API test server to replace json-server dependency
|
|
7
|
+
* Provides REST API endpoints for testing CodeceptJS helpers
|
|
8
|
+
*/
|
|
9
|
+
class TestServer {
|
|
10
|
+
constructor(config = {}) {
|
|
11
|
+
this.app = express()
|
|
12
|
+
this.server = null
|
|
13
|
+
this.port = config.port || 8010
|
|
14
|
+
this.host = config.host || 'localhost'
|
|
15
|
+
this.dbFile = config.dbFile || path.join(__dirname, '../test/data/rest/db.json')
|
|
16
|
+
this.readOnly = config.readOnly || false
|
|
17
|
+
this.lastModified = null
|
|
18
|
+
this.data = this.loadData()
|
|
19
|
+
|
|
20
|
+
this.setupMiddleware()
|
|
21
|
+
this.setupRoutes()
|
|
22
|
+
this.setupFileWatcher()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
loadData() {
|
|
26
|
+
try {
|
|
27
|
+
const content = fs.readFileSync(this.dbFile, 'utf8')
|
|
28
|
+
const data = JSON.parse(content)
|
|
29
|
+
// Update lastModified time when loading data
|
|
30
|
+
if (fs.existsSync(this.dbFile)) {
|
|
31
|
+
this.lastModified = fs.statSync(this.dbFile).mtime
|
|
32
|
+
}
|
|
33
|
+
console.log('[Data Load] Loaded data from file:', JSON.stringify(data))
|
|
34
|
+
return data
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.warn(`[Data Load] Could not load data file ${this.dbFile}:`, err.message)
|
|
37
|
+
console.log('[Data Load] Using fallback default data')
|
|
38
|
+
return {
|
|
39
|
+
posts: [{ id: 1, title: 'json-server', author: 'davert' }],
|
|
40
|
+
user: { name: 'john', password: '123456' },
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
reloadData() {
|
|
46
|
+
console.log('[Reload] Reloading data from file...')
|
|
47
|
+
this.data = this.loadData()
|
|
48
|
+
console.log('[Reload] Data reloaded successfully')
|
|
49
|
+
return this.data
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
saveData() {
|
|
53
|
+
if (this.readOnly) {
|
|
54
|
+
console.log('[Save] Skipping save - running in read-only mode')
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
fs.writeFileSync(this.dbFile, JSON.stringify(this.data, null, 2))
|
|
59
|
+
console.log('[Save] Data saved to file')
|
|
60
|
+
// Force update modification time to ensure auto-reload works
|
|
61
|
+
const now = new Date()
|
|
62
|
+
fs.utimesSync(this.dbFile, now, now)
|
|
63
|
+
this.lastModified = now
|
|
64
|
+
console.log('[Save] File modification time updated')
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.warn(`[Save] Could not save data file ${this.dbFile}:`, err.message)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
setupMiddleware() {
|
|
71
|
+
// Parse JSON bodies
|
|
72
|
+
this.app.use(express.json())
|
|
73
|
+
|
|
74
|
+
// Parse URL-encoded bodies
|
|
75
|
+
this.app.use(express.urlencoded({ extended: true }))
|
|
76
|
+
|
|
77
|
+
// CORS support
|
|
78
|
+
this.app.use((req, res, next) => {
|
|
79
|
+
res.header('Access-Control-Allow-Origin', '*')
|
|
80
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
|
|
81
|
+
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Test')
|
|
82
|
+
|
|
83
|
+
if (req.method === 'OPTIONS') {
|
|
84
|
+
res.status(200).end()
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
next()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
// Auto-reload middleware - check if file changed before each request
|
|
91
|
+
this.app.use((req, res, next) => {
|
|
92
|
+
try {
|
|
93
|
+
if (fs.existsSync(this.dbFile)) {
|
|
94
|
+
const stats = fs.statSync(this.dbFile)
|
|
95
|
+
if (!this.lastModified || stats.mtime > this.lastModified) {
|
|
96
|
+
console.log(`[Auto-reload] Database file changed (${this.dbFile}), reloading data...`)
|
|
97
|
+
console.log(`[Auto-reload] Old mtime: ${this.lastModified}, New mtime: ${stats.mtime}`)
|
|
98
|
+
this.reloadData()
|
|
99
|
+
this.lastModified = stats.mtime
|
|
100
|
+
console.log(`[Auto-reload] Data reloaded, user name is now: ${this.data.user?.name}`)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.warn('[Auto-reload] Error checking file modification time:', err.message)
|
|
105
|
+
}
|
|
106
|
+
next()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// Logging middleware
|
|
110
|
+
this.app.use((req, res, next) => {
|
|
111
|
+
console.log(`${req.method} ${req.path}`)
|
|
112
|
+
next()
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
setupRoutes() {
|
|
117
|
+
// Reload endpoint (for testing)
|
|
118
|
+
this.app.post('/_reload', (req, res) => {
|
|
119
|
+
this.reloadData()
|
|
120
|
+
res.json({ message: 'Data reloaded', data: this.data })
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// Headers endpoint (for header testing)
|
|
124
|
+
this.app.get('/headers', (req, res) => {
|
|
125
|
+
res.json(req.headers)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
this.app.post('/headers', (req, res) => {
|
|
129
|
+
res.json(req.headers)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// User endpoints
|
|
133
|
+
this.app.get('/user', (req, res) => {
|
|
134
|
+
console.log(`[GET /user] Serving user data: ${JSON.stringify(this.data.user)}`)
|
|
135
|
+
res.json(this.data.user)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
this.app.post('/user', (req, res) => {
|
|
139
|
+
this.data.user = { ...this.data.user, ...req.body }
|
|
140
|
+
this.saveData()
|
|
141
|
+
res.status(201).json(this.data.user)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
this.app.patch('/user', (req, res) => {
|
|
145
|
+
this.data.user = { ...this.data.user, ...req.body }
|
|
146
|
+
this.saveData()
|
|
147
|
+
res.json(this.data.user)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
this.app.put('/user', (req, res) => {
|
|
151
|
+
this.data.user = req.body
|
|
152
|
+
this.saveData()
|
|
153
|
+
res.json(this.data.user)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// Posts endpoints
|
|
157
|
+
this.app.get('/posts', (req, res) => {
|
|
158
|
+
res.json(this.data.posts)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
this.app.get('/posts/:id', (req, res) => {
|
|
162
|
+
const id = parseInt(req.params.id)
|
|
163
|
+
const post = this.data.posts.find(p => p.id === id)
|
|
164
|
+
|
|
165
|
+
if (!post) {
|
|
166
|
+
// Return empty object instead of 404 for json-server compatibility
|
|
167
|
+
return res.json({})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
res.json(post)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
this.app.post('/posts', (req, res) => {
|
|
174
|
+
const newId = Math.max(...this.data.posts.map(p => p.id || 0)) + 1
|
|
175
|
+
const newPost = { id: newId, ...req.body }
|
|
176
|
+
|
|
177
|
+
this.data.posts.push(newPost)
|
|
178
|
+
this.saveData()
|
|
179
|
+
res.status(201).json(newPost)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
this.app.put('/posts/:id', (req, res) => {
|
|
183
|
+
const id = parseInt(req.params.id)
|
|
184
|
+
const postIndex = this.data.posts.findIndex(p => p.id === id)
|
|
185
|
+
|
|
186
|
+
if (postIndex === -1) {
|
|
187
|
+
return res.status(404).json({ error: 'Post not found' })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.data.posts[postIndex] = { id, ...req.body }
|
|
191
|
+
this.saveData()
|
|
192
|
+
res.json(this.data.posts[postIndex])
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
this.app.patch('/posts/:id', (req, res) => {
|
|
196
|
+
const id = parseInt(req.params.id)
|
|
197
|
+
const postIndex = this.data.posts.findIndex(p => p.id === id)
|
|
198
|
+
|
|
199
|
+
if (postIndex === -1) {
|
|
200
|
+
return res.status(404).json({ error: 'Post not found' })
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.data.posts[postIndex] = { ...this.data.posts[postIndex], ...req.body }
|
|
204
|
+
this.saveData()
|
|
205
|
+
res.json(this.data.posts[postIndex])
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
this.app.delete('/posts/:id', (req, res) => {
|
|
209
|
+
const id = parseInt(req.params.id)
|
|
210
|
+
const postIndex = this.data.posts.findIndex(p => p.id === id)
|
|
211
|
+
|
|
212
|
+
if (postIndex === -1) {
|
|
213
|
+
return res.status(404).json({ error: 'Post not found' })
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const deletedPost = this.data.posts.splice(postIndex, 1)[0]
|
|
217
|
+
this.saveData()
|
|
218
|
+
res.json(deletedPost)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// File upload endpoint (basic implementation)
|
|
222
|
+
this.app.post('/upload', (req, res) => {
|
|
223
|
+
// Simple upload simulation - for more complex file uploads,
|
|
224
|
+
// multer would be needed but basic tests should work
|
|
225
|
+
res.json({
|
|
226
|
+
message: 'File upload endpoint available',
|
|
227
|
+
headers: req.headers,
|
|
228
|
+
body: req.body,
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
// Comments endpoints (for ApiDataFactory tests)
|
|
233
|
+
this.app.get('/comments', (req, res) => {
|
|
234
|
+
res.json(this.data.comments || [])
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
this.app.post('/comments', (req, res) => {
|
|
238
|
+
if (!this.data.comments) this.data.comments = []
|
|
239
|
+
const newId = Math.max(...this.data.comments.map(c => c.id || 0), 0) + 1
|
|
240
|
+
const newComment = { id: newId, ...req.body }
|
|
241
|
+
|
|
242
|
+
this.data.comments.push(newComment)
|
|
243
|
+
this.saveData()
|
|
244
|
+
res.status(201).json(newComment)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
this.app.delete('/comments/:id', (req, res) => {
|
|
248
|
+
if (!this.data.comments) this.data.comments = []
|
|
249
|
+
const id = parseInt(req.params.id)
|
|
250
|
+
const commentIndex = this.data.comments.findIndex(c => c.id === id)
|
|
251
|
+
|
|
252
|
+
if (commentIndex === -1) {
|
|
253
|
+
return res.status(404).json({ error: 'Comment not found' })
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const deletedComment = this.data.comments.splice(commentIndex, 1)[0]
|
|
257
|
+
this.saveData()
|
|
258
|
+
res.json(deletedComment)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// Generic catch-all for other endpoints
|
|
262
|
+
this.app.use((req, res) => {
|
|
263
|
+
res.status(404).json({ error: 'Endpoint not found' })
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
setupFileWatcher() {
|
|
268
|
+
if (fs.existsSync(this.dbFile)) {
|
|
269
|
+
fs.watchFile(this.dbFile, (current, previous) => {
|
|
270
|
+
if (current.mtime !== previous.mtime) {
|
|
271
|
+
console.log('Database file changed, reloading data...')
|
|
272
|
+
this.reloadData()
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
start() {
|
|
279
|
+
return new Promise((resolve, reject) => {
|
|
280
|
+
this.server = this.app.listen(this.port, this.host, err => {
|
|
281
|
+
if (err) {
|
|
282
|
+
reject(err)
|
|
283
|
+
} else {
|
|
284
|
+
console.log(`Test server running on http://${this.host}:${this.port}`)
|
|
285
|
+
resolve(this.server)
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
stop() {
|
|
292
|
+
return new Promise(resolve => {
|
|
293
|
+
if (this.server) {
|
|
294
|
+
this.server.close(() => {
|
|
295
|
+
console.log('Test server stopped')
|
|
296
|
+
resolve()
|
|
297
|
+
})
|
|
298
|
+
} else {
|
|
299
|
+
resolve()
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export default TestServer
|
|
306
|
+
|
|
307
|
+
// CLI usage - Import meta for ESM
|
|
308
|
+
import { fileURLToPath } from 'url'
|
|
309
|
+
import { dirname } from 'path'
|
|
310
|
+
|
|
311
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
312
|
+
const __dirname = dirname(__filename)
|
|
313
|
+
|
|
314
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
315
|
+
const config = {
|
|
316
|
+
port: process.env.PORT || 8010,
|
|
317
|
+
host: process.env.HOST || '0.0.0.0',
|
|
318
|
+
dbFile: process.argv[2] || path.join(__dirname, '../test/data/rest/db.json'),
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const server = new TestServer(config)
|
|
322
|
+
server.start().catch(console.error)
|
|
323
|
+
|
|
324
|
+
// Graceful shutdown
|
|
325
|
+
process.on('SIGINT', () => {
|
|
326
|
+
console.log('\nShutting down test server...')
|
|
327
|
+
server.stop().then(() => process.exit(0))
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
process.on('SIGTERM', () => {
|
|
331
|
+
console.log('\nShutting down test server...')
|
|
332
|
+
server.stop().then(() => process.exit(0))
|
|
333
|
+
})
|
|
334
|
+
}
|
package/lib/timeout.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const TIMEOUT_ORDER = {
|
|
2
|
+
/**
|
|
3
|
+
* timeouts set with order below zero only override timeouts of higher order if their value is smaller
|
|
4
|
+
*/
|
|
5
|
+
testOrSuite: -5,
|
|
6
|
+
/**
|
|
7
|
+
* 0-9 - designated for override of timeouts set from code, 5 is used by stepTimeout plugin when stepTimeout.config.overrideStepLimits=true
|
|
8
|
+
*/
|
|
9
|
+
stepTimeoutHard: 5,
|
|
10
|
+
/**
|
|
11
|
+
* 10-19 - designated for timeouts set from code, 15 is order of I.setTimeout(t) operation
|
|
12
|
+
*/
|
|
13
|
+
codeLimitTime: 15,
|
|
14
|
+
/**
|
|
15
|
+
* 20-29 - designated for timeout settings which could be overriden in tests code, 25 is used by stepTimeout plugin when stepTimeout.config.overrideStepLimits=false
|
|
16
|
+
*/
|
|
17
|
+
stepTimeoutSoft: 25,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getCurrentTimeout(timeouts) {
|
|
21
|
+
let totalTimeout
|
|
22
|
+
// iterate over all timeouts starting from highest values of order
|
|
23
|
+
new Map([...timeouts.entries()].sort().reverse()).forEach((timeout, order) => {
|
|
24
|
+
if (
|
|
25
|
+
timeout !== undefined &&
|
|
26
|
+
// when orders >= 0 - timeout value overrides those set with higher order elements
|
|
27
|
+
(order >= 0 ||
|
|
28
|
+
// when `order < 0 && totalTimeout === undefined` - timeout is used when nothing is set by elements with higher order
|
|
29
|
+
totalTimeout === undefined ||
|
|
30
|
+
// when `order < 0` - timeout overrides higher values of timeout or 'no timeout' (totalTimeout === 0) set by elements with higher order
|
|
31
|
+
(timeout > 0 && (timeout < totalTimeout || totalTimeout === 0)))
|
|
32
|
+
) {
|
|
33
|
+
totalTimeout = timeout
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
return totalTimeout
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class TimeoutError extends Error {
|
|
40
|
+
constructor(message) {
|
|
41
|
+
super(message)
|
|
42
|
+
this.name = 'TimeoutError'
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
class TestTimeoutError extends TimeoutError {
|
|
47
|
+
constructor(timeout) {
|
|
48
|
+
super(`Timeout ${timeout}s exceeded (with Before hook)`)
|
|
49
|
+
this.name = 'TestTimeoutError'
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class StepTimeoutError extends TimeoutError {
|
|
54
|
+
constructor(timeout, step) {
|
|
55
|
+
super(`Step ${step.toCode().trim()} timed out after ${timeout}s`)
|
|
56
|
+
this.name = 'StepTimeoutError'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { TIMEOUT_ORDER, getCurrentTimeout, TimeoutError, TestTimeoutError, StepTimeoutError }
|
package/lib/transform.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
const transformers = {
|
|
2
2
|
'gherkin.examples': [],
|
|
3
|
-
}
|
|
3
|
+
}
|
|
4
4
|
|
|
5
5
|
function transform(target, value) {
|
|
6
6
|
if (target in transformers) {
|
|
7
7
|
for (const transform of transformers[target]) {
|
|
8
|
-
value = transform(value)
|
|
8
|
+
value = transform(value)
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
|
-
return value
|
|
11
|
+
return value
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
transform.addTransformer = function (target, transformer) {
|
|
15
15
|
if (target in transformers) {
|
|
16
|
-
transformers[target].push(transformer)
|
|
16
|
+
transformers[target].push(transformer)
|
|
17
17
|
}
|
|
18
|
-
}
|
|
18
|
+
}
|
|
19
19
|
|
|
20
20
|
transform.addTransformerBeforeAll = function (target, transformer) {
|
|
21
21
|
if (target in transformers) {
|
|
22
|
-
transformers[target].unshift(transformer)
|
|
22
|
+
transformers[target].unshift(transformer)
|
|
23
23
|
}
|
|
24
|
-
}
|
|
24
|
+
}
|
|
25
25
|
|
|
26
|
-
export default transform
|
|
26
|
+
export default transform
|
package/lib/translation.js
CHANGED
|
@@ -1,56 +1,69 @@
|
|
|
1
|
-
import merge from 'lodash.merge'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
|
|
4
|
-
import importSync from 'import-sync';
|
|
5
|
-
import * as translations from '../translations/index.js';
|
|
1
|
+
import merge from 'lodash.merge'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { createRequire } from 'module'
|
|
6
4
|
|
|
7
5
|
const defaultVocabulary = {
|
|
8
6
|
I: 'I',
|
|
9
7
|
actions: {},
|
|
10
|
-
}
|
|
8
|
+
}
|
|
11
9
|
|
|
12
|
-
|
|
10
|
+
class Translation {
|
|
13
11
|
constructor(vocabulary, loaded) {
|
|
14
|
-
this.vocabulary = vocabulary
|
|
15
|
-
this.loaded = loaded !== false
|
|
12
|
+
this.vocabulary = vocabulary
|
|
13
|
+
this.loaded = loaded !== false
|
|
16
14
|
}
|
|
17
15
|
|
|
18
16
|
loadVocabulary(vocabularyFile) {
|
|
19
|
-
if (!vocabularyFile) return
|
|
20
|
-
const filePath = path.join(global.codecept_dir, vocabularyFile)
|
|
17
|
+
if (!vocabularyFile) return
|
|
18
|
+
const filePath = path.join(global.codecept_dir, vocabularyFile)
|
|
21
19
|
|
|
22
20
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
21
|
+
const require = createRequire(import.meta.url)
|
|
22
|
+
const vocabulary = require(filePath)
|
|
23
|
+
this.vocabulary = merge(this.vocabulary, vocabulary)
|
|
25
24
|
} catch (err) {
|
|
26
|
-
throw new Error(`Can't load vocabulary from ${filePath}; ${err}`)
|
|
25
|
+
throw new Error(`Can't load vocabulary from ${filePath}; ${err}`)
|
|
27
26
|
}
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
value(val) {
|
|
31
|
-
return this.vocabulary[val]
|
|
30
|
+
return this.vocabulary[val]
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
actionAliasFor(actualActionName) {
|
|
35
34
|
if (this.vocabulary.actions && this.vocabulary.actions[actualActionName]) {
|
|
36
|
-
return this.vocabulary.actions[actualActionName]
|
|
35
|
+
return this.vocabulary.actions[actualActionName]
|
|
37
36
|
}
|
|
38
|
-
return actualActionName
|
|
37
|
+
return actualActionName
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
get I() {
|
|
42
|
-
return this.vocabulary.I
|
|
41
|
+
return this.vocabulary.I
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static async getLangs() {
|
|
45
|
+
const translations = await import('../translations/index.js')
|
|
46
|
+
return translations.default
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
static get langs() {
|
|
46
|
-
|
|
50
|
+
// Synchronous fallback - may be empty initially
|
|
51
|
+
if (!this._cachedLangs) {
|
|
52
|
+
this.getLangs().then(langs => {
|
|
53
|
+
this._cachedLangs = langs
|
|
54
|
+
})
|
|
55
|
+
return {}
|
|
56
|
+
}
|
|
57
|
+
return this._cachedLangs
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
static createDefault() {
|
|
50
|
-
return new Translation(defaultVocabulary, true)
|
|
61
|
+
return new Translation(defaultVocabulary, true)
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
static createEmpty() {
|
|
54
|
-
return new Translation(defaultVocabulary, false)
|
|
65
|
+
return new Translation(defaultVocabulary, false)
|
|
55
66
|
}
|
|
56
67
|
}
|
|
68
|
+
|
|
69
|
+
export default Translation
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { maskSensitiveData } from 'invisi-data'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mask sensitive data utility for CodeceptJS
|
|
5
|
+
* Supports both boolean and object configuration formats
|
|
6
|
+
*
|
|
7
|
+
* @param {string} input - The string to mask
|
|
8
|
+
* @param {boolean|object} config - Masking configuration
|
|
9
|
+
* @returns {string} - Masked string
|
|
10
|
+
*/
|
|
11
|
+
export function maskData(input, config) {
|
|
12
|
+
if (!config) {
|
|
13
|
+
return input
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Handle boolean config (backward compatibility)
|
|
17
|
+
if (typeof config === 'boolean' && config === true) {
|
|
18
|
+
return maskSensitiveData(input)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Handle object config with custom patterns
|
|
22
|
+
if (typeof config === 'object' && config.enabled === true) {
|
|
23
|
+
const customPatterns = config.patterns || []
|
|
24
|
+
return maskSensitiveData(input, customPatterns)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return input
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if masking is enabled based on global configuration
|
|
32
|
+
*
|
|
33
|
+
* @returns {boolean|object} - Current masking configuration
|
|
34
|
+
*/
|
|
35
|
+
export function getMaskConfig() {
|
|
36
|
+
return global.maskSensitiveData || false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if data should be masked
|
|
41
|
+
*
|
|
42
|
+
* @returns {boolean} - True if masking is enabled
|
|
43
|
+
*/
|
|
44
|
+
export function shouldMaskData() {
|
|
45
|
+
const config = getMaskConfig()
|
|
46
|
+
return config === true || (typeof config === 'object' && config.enabled === true)
|
|
47
|
+
}
|