codeceptjs 4.0.0-rc.9 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -10
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +751 -172
- package/docs/advanced.md +201 -0
- package/docs/agents.md +181 -0
- package/docs/ai.md +489 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -0
- package/docs/architecture.md +235 -0
- package/docs/assertions.md +415 -0
- package/docs/auth.md +318 -0
- package/docs/basics.md +424 -0
- package/docs/bdd.md +539 -0
- package/docs/best.md +240 -0
- package/docs/bootstrap.md +132 -0
- package/docs/commands.md +352 -0
- package/docs/community-helpers.md +63 -0
- package/docs/configuration.md +185 -0
- package/docs/continuous-integration.md +431 -0
- package/docs/custom-helpers.md +297 -0
- package/docs/data.md +448 -0
- package/docs/debugging.md +332 -0
- package/docs/detox.md +235 -0
- package/docs/docker.md +107 -0
- package/docs/effects.md +179 -0
- package/docs/element-based-testing.md +295 -0
- package/docs/element-selection.md +125 -0
- package/docs/els.md +328 -0
- package/docs/environment-variables.md +131 -0
- package/docs/examples.md +160 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1419 -0
- package/docs/helpers/Detox.md +665 -0
- package/docs/helpers/ExpectHelper.md +275 -0
- package/docs/helpers/FileSystem.md +152 -0
- package/docs/helpers/GraphQL.md +152 -0
- package/docs/helpers/GraphQLDataFactory.md +226 -0
- package/docs/helpers/JSONResponse.md +255 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/Playwright.md +2970 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2583 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/WebDriver.md +2639 -0
- package/docs/hooks.md +148 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +121 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migrate-from-cypress.md +98 -0
- package/docs/migrate-from-java.md +108 -0
- package/docs/migrate-from-protractor.md +101 -0
- package/docs/migrate-from-testcafe.md +99 -0
- package/docs/migration-4.md +743 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +187 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins/aiTrace.md +49 -0
- package/docs/plugins/analyze.md +66 -0
- package/docs/plugins/auth.md +241 -0
- package/docs/plugins/autoDelay.md +48 -0
- package/docs/plugins/browser.md +41 -0
- package/docs/plugins/coverage.md +39 -0
- package/docs/plugins/customLocator.md +119 -0
- package/docs/plugins/customReporter.md +16 -0
- package/docs/plugins/expose.md +75 -0
- package/docs/plugins/heal.md +44 -0
- package/docs/plugins/junitReporter.md +51 -0
- package/docs/plugins/pageInfo.md +34 -0
- package/docs/plugins/pause.md +43 -0
- package/docs/plugins/pauseOnFail.md +18 -0
- package/docs/plugins/retryFailedStep.md +75 -0
- package/docs/plugins/screencast.md +55 -0
- package/docs/plugins/screenshot.md +58 -0
- package/docs/plugins/screenshotOnFail.md +18 -0
- package/docs/plugins/stepTimeout.md +65 -0
- package/docs/plugins.md +87 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/reports.md +198 -0
- package/docs/retry.md +311 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/store.md +94 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +323 -0
- package/docs/typescript.md +159 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +641 -0
- package/docs/within.md +55 -0
- package/lib/actor.js +1 -36
- package/lib/ai.js +3 -2
- package/lib/aria.js +260 -0
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +7 -7
- package/lib/command/check.js +2 -1
- package/lib/command/dryRun.js +24 -5
- package/lib/command/generate.js +2 -0
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +248 -266
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +3 -2
- package/lib/command/run-workers.js +1 -14
- package/lib/command/run.js +3 -17
- package/lib/command/utils.js +14 -0
- package/lib/command/workers/runTests.js +11 -15
- package/lib/config.js +77 -4
- package/lib/container.js +97 -15
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +194 -2
- package/lib/els.js +12 -6
- package/lib/globals.js +32 -19
- package/lib/heal.js +7 -4
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +63 -70
- package/lib/helper/Puppeteer.js +20 -109
- package/lib/helper/WebDriver.js +13 -30
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightLocator.js +10 -0
- package/lib/helper/extras/elementSelection.js +10 -3
- package/lib/helper/extras/focusCheck.js +43 -0
- package/lib/helper/extras/richTextEditor.js +178 -0
- package/lib/history.js +3 -2
- package/lib/html.js +90 -16
- package/lib/index.js +9 -1
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/helpers.js +4 -1
- package/lib/listener/mocha.js +2 -1
- package/lib/listener/pageobjects.js +43 -0
- package/lib/listener/result.js +3 -2
- package/lib/locator.js +126 -16
- package/lib/mocha/cli.js +4 -2
- package/lib/mocha/factory.js +7 -2
- package/lib/mocha/inject.js +1 -1
- package/lib/mocha/scenarioConfig.js +2 -1
- package/lib/mocha/ui.js +5 -6
- package/lib/parser.js +2 -2
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +96 -103
- package/lib/plugin/analyze.js +9 -9
- package/lib/plugin/auth.js +3 -3
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +47 -3
- package/lib/plugin/junitReporter.js +303 -0
- package/lib/plugin/pageInfo.js +54 -52
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +11 -33
- package/lib/plugin/retryFailedStep.js +15 -13
- package/lib/plugin/screencast.js +289 -0
- package/lib/plugin/screenshot.js +558 -0
- package/lib/plugin/screenshotOnFail.js +9 -170
- package/lib/plugin/stepTimeout.js +3 -2
- package/lib/recorder.js +1 -1
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +10 -9
- package/lib/step/comment.js +2 -2
- package/lib/step/config.js +7 -0
- package/lib/step/helper.js +4 -4
- package/lib/step/meta.js +3 -3
- package/lib/step/record.js +5 -5
- package/lib/store.js +72 -3
- package/lib/translation.js +2 -1
- package/lib/utils/mask_data.js +2 -1
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils.js +29 -3
- package/lib/workers.js +14 -22
- package/package.json +17 -14
- package/typings/index.d.ts +0 -5
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -16
- package/docs/webapi/attachFile.mustache +0 -24
- package/docs/webapi/blur.mustache +0 -18
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -9
- package/docs/webapi/clearField.mustache +0 -14
- package/docs/webapi/click.mustache +0 -29
- package/docs/webapi/clickLink.mustache +0 -8
- package/docs/webapi/closeCurrentTab.mustache +0 -7
- package/docs/webapi/closeOtherTabs.mustache +0 -8
- package/docs/webapi/dontSee.mustache +0 -11
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/dontSeeCookie.mustache +0 -8
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -12
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -16
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/dontSeeTraffic.mustache +0 -13
- package/docs/webapi/doubleClick.mustache +0 -13
- package/docs/webapi/downloadFile.mustache +0 -12
- package/docs/webapi/dragAndDrop.mustache +0 -9
- package/docs/webapi/dragSlider.mustache +0 -11
- package/docs/webapi/executeAsyncScript.mustache +0 -24
- package/docs/webapi/executeScript.mustache +0 -26
- package/docs/webapi/fillField.mustache +0 -21
- package/docs/webapi/flushNetworkTraffics.mustache +0 -5
- package/docs/webapi/focus.mustache +0 -13
- package/docs/webapi/forceClick.mustache +0 -28
- package/docs/webapi/forceRightClick.mustache +0 -18
- package/docs/webapi/grabAllWindowHandles.mustache +0 -7
- package/docs/webapi/grabAttributeFrom.mustache +0 -10
- package/docs/webapi/grabAttributeFromAll.mustache +0 -9
- package/docs/webapi/grabBrowserLogs.mustache +0 -9
- package/docs/webapi/grabCookie.mustache +0 -11
- package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
- package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
- package/docs/webapi/grabCurrentUrl.mustache +0 -9
- package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
- package/docs/webapi/grabElementBoundingRect.mustache +0 -20
- package/docs/webapi/grabGeoLocation.mustache +0 -8
- package/docs/webapi/grabHTMLFrom.mustache +0 -10
- package/docs/webapi/grabHTMLFromAll.mustache +0 -9
- package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
- package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
- package/docs/webapi/grabPageScrollPosition.mustache +0 -8
- package/docs/webapi/grabPopupText.mustache +0 -5
- package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
- package/docs/webapi/grabSource.mustache +0 -8
- package/docs/webapi/grabTextFrom.mustache +0 -10
- package/docs/webapi/grabTextFromAll.mustache +0 -9
- package/docs/webapi/grabTitle.mustache +0 -8
- package/docs/webapi/grabValueFrom.mustache +0 -9
- package/docs/webapi/grabValueFromAll.mustache +0 -8
- package/docs/webapi/grabWebElement.mustache +0 -9
- package/docs/webapi/grabWebElements.mustache +0 -9
- package/docs/webapi/moveCursorTo.mustache +0 -16
- package/docs/webapi/openNewTab.mustache +0 -7
- package/docs/webapi/pressKey.mustache +0 -12
- package/docs/webapi/pressKeyDown.mustache +0 -12
- package/docs/webapi/pressKeyUp.mustache +0 -12
- package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
- package/docs/webapi/refreshPage.mustache +0 -6
- package/docs/webapi/resizeWindow.mustache +0 -6
- package/docs/webapi/rightClick.mustache +0 -14
- package/docs/webapi/saveElementScreenshot.mustache +0 -10
- package/docs/webapi/saveScreenshot.mustache +0 -12
- package/docs/webapi/say.mustache +0 -10
- package/docs/webapi/scrollIntoView.mustache +0 -11
- package/docs/webapi/scrollPageToBottom.mustache +0 -6
- package/docs/webapi/scrollPageToTop.mustache +0 -6
- package/docs/webapi/scrollTo.mustache +0 -12
- package/docs/webapi/see.mustache +0 -11
- package/docs/webapi/seeAttributesOnElements.mustache +0 -9
- package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/seeCookie.mustache +0 -8
- package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
- package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -12
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -17
- package/docs/webapi/seeInPopup.mustache +0 -8
- package/docs/webapi/seeInSource.mustache +0 -7
- package/docs/webapi/seeInTitle.mustache +0 -8
- package/docs/webapi/seeNumberOfElements.mustache +0 -11
- package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/seeTextEquals.mustache +0 -9
- package/docs/webapi/seeTitleEquals.mustache +0 -8
- package/docs/webapi/seeTraffic.mustache +0 -36
- package/docs/webapi/selectOption.mustache +0 -26
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/startRecordingTraffic.mustache +0 -8
- package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
- package/docs/webapi/stopRecordingTraffic.mustache +0 -5
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
- package/docs/webapi/switchTo.mustache +0 -9
- package/docs/webapi/switchToNextTab.mustache +0 -10
- package/docs/webapi/switchToPreviousTab.mustache +0 -10
- package/docs/webapi/type.mustache +0 -21
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForCookie.mustache +0 -9
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForDisabled.mustache +0 -6
- package/docs/webapi/waitForElement.mustache +0 -11
- package/docs/webapi/waitForEnabled.mustache +0 -6
- package/docs/webapi/waitForFunction.mustache +0 -17
- package/docs/webapi/waitForInvisible.mustache +0 -10
- package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
- package/docs/webapi/waitForText.mustache +0 -13
- package/docs/webapi/waitForValue.mustache +0 -10
- package/docs/webapi/waitForVisible.mustache +0 -10
- package/docs/webapi/waitInUrl.mustache +0 -9
- package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/waitToHide.mustache +0 -10
- package/docs/webapi/waitUrlEquals.mustache +0 -10
- package/lib/helper/AI.js +0 -214
- package/lib/helper/Mochawesome.js +0 -96
- package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
- package/lib/helper/extras/React.js +0 -65
- package/lib/plugin/stepByStepReport.js +0 -431
- package/lib/plugin/subtitles.js +0 -89
|
@@ -1,39 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import output from '../output.js'
|
|
2
|
+
import pause from './pause.js'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
let warned = false
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* Useful for debugging flaky tests on local environment.
|
|
9
|
-
* Add this plugin to config file:
|
|
10
|
-
*
|
|
11
|
-
* ```js
|
|
12
|
-
* plugins: {
|
|
13
|
-
* pauseOnFail: {},
|
|
14
|
-
* }
|
|
15
|
-
* ```
|
|
16
|
-
*
|
|
17
|
-
* Unlike other plugins, `pauseOnFail` is not recommended to be enabled by default.
|
|
18
|
-
* Enable it manually on each run via `-p` option:
|
|
19
|
-
*
|
|
20
|
-
* ```
|
|
21
|
-
* npx codeceptjs run -p pauseOnFail
|
|
22
|
-
* ```
|
|
7
|
+
* Starts an interactive pause when a test fails.
|
|
23
8
|
*
|
|
9
|
+
* **Deprecated:** use the `pause` plugin with `on: 'fail'`, which is the default behavior.
|
|
24
10
|
*/
|
|
25
|
-
export default function() {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
event.dispatcher.on(event.step.failed, () => {
|
|
33
|
-
failed = true
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
event.dispatcher.on(event.test.after, () => {
|
|
37
|
-
if (failed) pause()
|
|
38
|
-
})
|
|
11
|
+
export default function (config = {}) {
|
|
12
|
+
if (!warned) {
|
|
13
|
+
output.error('pauseOnFail is deprecated; use the `pause` plugin (default on=fail).')
|
|
14
|
+
warned = true
|
|
15
|
+
}
|
|
16
|
+
return pause({ ...config, on: 'fail' })
|
|
39
17
|
}
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
import debugModule from 'debug'
|
|
1
2
|
import event from '../event.js'
|
|
2
3
|
import recorder from '../recorder.js'
|
|
3
4
|
import store from '../store.js'
|
|
4
5
|
|
|
6
|
+
const debug = debugModule('codeceptjs:retryFailedStep')
|
|
7
|
+
|
|
5
8
|
const defaultConfig = {
|
|
6
9
|
retries: 3,
|
|
7
10
|
defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
|
|
11
|
+
minTimeout: 150,
|
|
12
|
+
maxTimeout: 10000,
|
|
8
13
|
factor: 1.5,
|
|
14
|
+
randomize: false,
|
|
9
15
|
ignoredSteps: [],
|
|
10
16
|
deferToScenarioRetries: true,
|
|
11
17
|
}
|
|
@@ -41,10 +47,9 @@ const RETRY_PRIORITIES = {
|
|
|
41
47
|
* #### Configuration:
|
|
42
48
|
*
|
|
43
49
|
* * `retries` - number of retries (by default 3),
|
|
44
|
-
* * `when` - function, when to perform a retry (accepts error as parameter)
|
|
45
50
|
* * `factor` - The exponential factor to use. Default is 1.5.
|
|
46
|
-
* * `minTimeout` - The number of milliseconds before starting the first retry. Default is
|
|
47
|
-
* * `maxTimeout` - The maximum number of milliseconds between two retries. Default is
|
|
51
|
+
* * `minTimeout` - The number of milliseconds before starting the first retry. Default is 150.
|
|
52
|
+
* * `maxTimeout` - The maximum number of milliseconds between two retries. Default is 10000.
|
|
48
53
|
* * `randomize` - Randomizes the timeouts by multiplying with a factor from 1 to 2. Default is false.
|
|
49
54
|
* * `defaultIgnoredSteps` - an array of steps to be ignored for retry. Includes:
|
|
50
55
|
* * `amOnPage`
|
|
@@ -74,7 +79,7 @@ const RETRY_PRIORITIES = {
|
|
|
74
79
|
*
|
|
75
80
|
* #### Disable Per Test
|
|
76
81
|
*
|
|
77
|
-
* This plugin can be disabled per test. In this case you will need to
|
|
82
|
+
* This plugin can be disabled per test. In this case you will need to add `step.retry()` to all flaky steps:
|
|
78
83
|
*
|
|
79
84
|
* Use scenario configuration to disable plugin for a test
|
|
80
85
|
*
|
|
@@ -86,9 +91,8 @@ const RETRY_PRIORITIES = {
|
|
|
86
91
|
*
|
|
87
92
|
*/
|
|
88
93
|
export default function (config) {
|
|
89
|
-
config = Object.assign(defaultConfig, config)
|
|
94
|
+
config = Object.assign({}, defaultConfig, config)
|
|
90
95
|
config.ignoredSteps = config.ignoredSteps.concat(config.defaultIgnoredSteps)
|
|
91
|
-
const customWhen = config.when
|
|
92
96
|
|
|
93
97
|
let enableRetry = false
|
|
94
98
|
|
|
@@ -98,7 +102,6 @@ export default function (config) {
|
|
|
98
102
|
if (!store.autoRetries) return false
|
|
99
103
|
if (err && err.isTerminal) return false
|
|
100
104
|
if (err && err.message && (err.message.includes('ERR_ABORTED') || err.message.includes('frame was detached') || err.message.includes('Target page, context or browser has been closed'))) return false
|
|
101
|
-
if (customWhen) return customWhen(err)
|
|
102
105
|
return true
|
|
103
106
|
}
|
|
104
107
|
config.when = when
|
|
@@ -108,11 +111,12 @@ export default function (config) {
|
|
|
108
111
|
}
|
|
109
112
|
|
|
110
113
|
event.dispatcher.on(event.step.started, step => {
|
|
114
|
+
if (!step.title) return
|
|
111
115
|
for (const ignored of config.ignoredSteps) {
|
|
112
|
-
if (step.
|
|
116
|
+
if (step.title === ignored) return
|
|
113
117
|
if (ignored instanceof RegExp) {
|
|
114
|
-
if (step.
|
|
115
|
-
} else if (ignored.indexOf('*') && step.
|
|
118
|
+
if (step.title.match(ignored)) return
|
|
119
|
+
} else if (ignored.indexOf('*') && step.title.startsWith(ignored.slice(0, -1))) return
|
|
116
120
|
}
|
|
117
121
|
enableRetry = true
|
|
118
122
|
})
|
|
@@ -147,9 +151,7 @@ export default function (config) {
|
|
|
147
151
|
test.opts.conditionalRetries = config.retries
|
|
148
152
|
test.opts.stepRetryPriority = stepRetryPriority
|
|
149
153
|
|
|
150
|
-
|
|
151
|
-
console.log('[retryFailedStep] applying retries =', config.retries, 'for test', test.title)
|
|
152
|
-
}
|
|
154
|
+
debug('applying retries = %d for test %s', config.retries, test.title)
|
|
153
155
|
recorder.retry(config)
|
|
154
156
|
})
|
|
155
157
|
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { mkdirp } from 'mkdirp'
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid'
|
|
5
|
+
|
|
6
|
+
import Container from '../container.js'
|
|
7
|
+
import recorder from '../recorder.js'
|
|
8
|
+
import event from '../event.js'
|
|
9
|
+
import output from '../output.js'
|
|
10
|
+
import store from '../store.js'
|
|
11
|
+
|
|
12
|
+
import { testToFileName } from '../mocha/test.js'
|
|
13
|
+
import { parsePluginArgs, resolveTrigger, getBrowserHelper } from '../utils/pluginParser.js'
|
|
14
|
+
|
|
15
|
+
const defaultConfig = {
|
|
16
|
+
on: 'fail',
|
|
17
|
+
captions: true,
|
|
18
|
+
subtitles: false,
|
|
19
|
+
video: true,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Records WebM video of tests using Playwright's screencast API.
|
|
24
|
+
*
|
|
25
|
+
* When `captions` is enabled, action annotations are burned into the video;
|
|
26
|
+
* when `subtitles` is enabled, a standalone `.srt` is also produced. Default
|
|
27
|
+
* `on=fail` keeps videos for failed tests only; `on=test` keeps every test's
|
|
28
|
+
* video.
|
|
29
|
+
*
|
|
30
|
+
* Note: enabling Playwright's helper-level `video: true` together with this
|
|
31
|
+
* plugin produces two independent recordings (`output/videos/*.webm` from the
|
|
32
|
+
* helper, `output/screencast/*.webm` from this plugin).
|
|
33
|
+
*
|
|
34
|
+
* #### Configuration
|
|
35
|
+
*
|
|
36
|
+
* ```js
|
|
37
|
+
* plugins: {
|
|
38
|
+
* screencast: {
|
|
39
|
+
* enabled: true,
|
|
40
|
+
* on: 'fail',
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* #### `on=` modes
|
|
46
|
+
*
|
|
47
|
+
* * **fail** — record while running; delete on pass, keep on fail (default)
|
|
48
|
+
* * **test** — record and keep every test's video
|
|
49
|
+
*
|
|
50
|
+
* Other config options:
|
|
51
|
+
*
|
|
52
|
+
* * `captions`: burn-in action overlays via `page.screencast.showActions()`. Default: true.
|
|
53
|
+
* * `subtitles`: also write a standalone `.srt` file alongside the video. Default: false.
|
|
54
|
+
* * `video`: record a video. With `video=false, subtitles=true`, only the `.srt` is produced. Default: true.
|
|
55
|
+
* * `size`: pass-through `{ width, height }` for `screencast.start`.
|
|
56
|
+
* * `quality`: pass-through 0–100 for `screencast.start`.
|
|
57
|
+
*
|
|
58
|
+
* CLI examples:
|
|
59
|
+
*
|
|
60
|
+
* ```
|
|
61
|
+
* npx codeceptjs run -p screencast
|
|
62
|
+
* npx codeceptjs run -p screencast:on=test
|
|
63
|
+
* npx codeceptjs run -p screencast:on=test;captions=false;subtitles=true
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export default function (config = {}) {
|
|
67
|
+
const helper = getBrowserHelper()
|
|
68
|
+
if (!helper) return
|
|
69
|
+
|
|
70
|
+
const cliArgs = parsePluginArgs(config._args)
|
|
71
|
+
const trigger = resolveTrigger(cliArgs, config, { on: defaultConfig.on }, {
|
|
72
|
+
name: 'screencast',
|
|
73
|
+
validModes: ['fail', 'test'],
|
|
74
|
+
})
|
|
75
|
+
if (!trigger) return
|
|
76
|
+
|
|
77
|
+
const options = Object.assign({}, defaultConfig, config)
|
|
78
|
+
options.captions = cliArgs.captions ?? config.captions ?? defaultConfig.captions
|
|
79
|
+
options.subtitles = cliArgs.subtitles ?? config.subtitles ?? defaultConfig.subtitles
|
|
80
|
+
options.video = cliArgs.video ?? config.video ?? defaultConfig.video
|
|
81
|
+
|
|
82
|
+
return wireScreencast(trigger.on, options)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function wireScreencast(mode, options) {
|
|
86
|
+
const state = {
|
|
87
|
+
test: null,
|
|
88
|
+
webmPath: null,
|
|
89
|
+
srtPath: null,
|
|
90
|
+
steps: null,
|
|
91
|
+
startedAt: null,
|
|
92
|
+
failed: false,
|
|
93
|
+
startQueued: false,
|
|
94
|
+
started: false,
|
|
95
|
+
warnedNoApi: false,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
event.dispatcher.on(event.test.before, test => {
|
|
99
|
+
state.test = test
|
|
100
|
+
state.failed = false
|
|
101
|
+
state.webmPath = null
|
|
102
|
+
state.srtPath = null
|
|
103
|
+
state.startQueued = false
|
|
104
|
+
state.started = false
|
|
105
|
+
state.steps = options.subtitles ? {} : null
|
|
106
|
+
state.startedAt = options.subtitles ? Date.now() : null
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
event.dispatcher.on(event.test.started, test => {
|
|
110
|
+
if (!options.video || state.startQueued) return
|
|
111
|
+
state.startQueued = true
|
|
112
|
+
recorder.add('screencast:start', async () => startScreencast(state.test, options, state), true)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
event.dispatcher.on(event.step.started, step => {
|
|
116
|
+
if (state.steps) {
|
|
117
|
+
const at = Date.now()
|
|
118
|
+
step.id = step.id || uuidv4()
|
|
119
|
+
state.steps[step.id] = {
|
|
120
|
+
start: formatTimestamp(at - state.startedAt),
|
|
121
|
+
startedAt: at,
|
|
122
|
+
title: stepTitle(step),
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
if (options.subtitles) {
|
|
128
|
+
event.dispatcher.on(event.step.finished, step => {
|
|
129
|
+
if (!state.steps || !step?.id || !state.steps[step.id]) return
|
|
130
|
+
state.steps[step.id].end = formatTimestamp(Date.now() - state.startedAt)
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
|
|
135
|
+
if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') return
|
|
136
|
+
state.failed = true
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
event.dispatcher.on(event.test.after, () => {
|
|
140
|
+
if (!state.test) return
|
|
141
|
+
recorder.add('screencast:stop', async () => finalizeScreencast({
|
|
142
|
+
test: state.test,
|
|
143
|
+
webmPath: state.webmPath,
|
|
144
|
+
srtPath: state.srtPath,
|
|
145
|
+
steps: state.steps,
|
|
146
|
+
failed: state.failed,
|
|
147
|
+
started: state.started,
|
|
148
|
+
options,
|
|
149
|
+
mode,
|
|
150
|
+
}), true)
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function startScreencast(test, options, state) {
|
|
155
|
+
const helper = getBrowserHelper()
|
|
156
|
+
if (!helper?.page?.screencast) {
|
|
157
|
+
if (!state.warnedNoApi) {
|
|
158
|
+
output.plugin('screencast', 'page.screencast not available — requires Playwright >= 1.59. Skipping.')
|
|
159
|
+
state.warnedNoApi = true
|
|
160
|
+
}
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const baseDir = path.join(store.outputDir || '_output', 'screencast')
|
|
165
|
+
mkdirp.sync(baseDir)
|
|
166
|
+
const baseName = testToFileName(test, { suffix: '', unique: true })
|
|
167
|
+
state.webmPath = path.join(baseDir, `${baseName}.webm`)
|
|
168
|
+
state.srtPath = path.join(baseDir, `${baseName}.srt`)
|
|
169
|
+
|
|
170
|
+
const startOpts = { path: state.webmPath }
|
|
171
|
+
if (options.size) startOpts.size = options.size
|
|
172
|
+
if (options.quality != null) startOpts.quality = options.quality
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
await helper.page.screencast.start(startOpts)
|
|
176
|
+
state.started = true
|
|
177
|
+
} catch (err) {
|
|
178
|
+
output.plugin('screencast', `Failed to start: ${err.message}`)
|
|
179
|
+
state.webmPath = null
|
|
180
|
+
state.srtPath = null
|
|
181
|
+
state.started = false
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (options.captions && typeof helper.page.screencast.showActions === 'function') {
|
|
186
|
+
try { await helper.page.screencast.showActions() }
|
|
187
|
+
catch (err) { output.plugin('screencast', `showActions failed: ${err.message}`) }
|
|
188
|
+
}
|
|
189
|
+
if (typeof helper.page.screencast.showChapter === 'function') {
|
|
190
|
+
try { await helper.page.screencast.showChapter(String(test.title || '')) }
|
|
191
|
+
catch (err) { output.plugin('screencast', `showChapter failed: ${err.message}`) }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function finalizeScreencast(snapshot) {
|
|
196
|
+
const { test, options, mode, steps } = snapshot
|
|
197
|
+
let { webmPath, srtPath } = snapshot
|
|
198
|
+
|
|
199
|
+
const helper = getBrowserHelper()
|
|
200
|
+
if (snapshot.started && helper?.page?.screencast) {
|
|
201
|
+
try {
|
|
202
|
+
await helper.page.screencast.stop()
|
|
203
|
+
} catch (err) {
|
|
204
|
+
output.plugin('screencast', `stop failed: ${err.message}`)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const shouldKeep = mode === 'test' || (mode === 'fail' && snapshot.failed)
|
|
209
|
+
|
|
210
|
+
if (options.video && webmPath) {
|
|
211
|
+
if (!shouldKeep) {
|
|
212
|
+
try { fs.unlinkSync(webmPath) } catch { /* file may not exist yet */ }
|
|
213
|
+
webmPath = null
|
|
214
|
+
} else {
|
|
215
|
+
ensureArtifactsObject(test)
|
|
216
|
+
test.artifacts.screencast = webmPath
|
|
217
|
+
attachJUnitArtifact(test, webmPath)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (options.subtitles && steps) {
|
|
222
|
+
if (options.video && !shouldKeep) {
|
|
223
|
+
try { srtPath && fs.unlinkSync(srtPath) } catch { /* nothing to delete */ }
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let target = srtPath
|
|
228
|
+
if (!options.video) {
|
|
229
|
+
if (test.artifacts && test.artifacts.video) {
|
|
230
|
+
const { dir, name } = path.parse(test.artifacts.video)
|
|
231
|
+
target = path.join(dir, `${name}.srt`)
|
|
232
|
+
} else {
|
|
233
|
+
const baseDir = path.join(store.outputDir || '_output', 'screencast')
|
|
234
|
+
mkdirp.sync(baseDir)
|
|
235
|
+
const baseName = testToFileName(test, { suffix: '', unique: true })
|
|
236
|
+
target = path.join(baseDir, `${baseName}.srt`)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!target) return
|
|
241
|
+
try {
|
|
242
|
+
await fs.promises.writeFile(target, buildSrt(steps))
|
|
243
|
+
ensureArtifactsObject(test)
|
|
244
|
+
test.artifacts.subtitle = target
|
|
245
|
+
} catch (err) {
|
|
246
|
+
output.plugin('screencast', `failed to write SRT: ${err.message}`)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function formatTimestamp(timestampInMs) {
|
|
252
|
+
const date = new Date(0, 0, 0, 0, 0, 0, timestampInMs)
|
|
253
|
+
const hours = date.getHours()
|
|
254
|
+
const minutes = date.getMinutes()
|
|
255
|
+
const seconds = date.getSeconds()
|
|
256
|
+
const ms = timestampInMs - (hours * 3600000 + minutes * 60000 + seconds * 1000)
|
|
257
|
+
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')},${ms.toString().padStart(3, '0')}`
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function stepTitle(step) {
|
|
261
|
+
let title = `${step.actor}.${step.title}(${step.args ? step.args.join(',') : ''})`
|
|
262
|
+
if (title.length > 100) title = `${title.substring(0, 100)}...`
|
|
263
|
+
return title
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function buildSrt(steps) {
|
|
267
|
+
const sorted = Object.values(steps).sort((a, b) => a.startedAt - b.startedAt)
|
|
268
|
+
let out = ''
|
|
269
|
+
let index = 1
|
|
270
|
+
for (const step of sorted) {
|
|
271
|
+
if (!step.end) continue
|
|
272
|
+
out += `${index}\n${step.start} --> ${step.end}\n${step.title}\n\n`
|
|
273
|
+
index++
|
|
274
|
+
}
|
|
275
|
+
return out
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function ensureArtifactsObject(test) {
|
|
279
|
+
if (!test.artifacts || Array.isArray(test.artifacts)) test.artifacts = {}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function attachJUnitArtifact(test, filePath) {
|
|
283
|
+
const mocha = Container.mocha?.()
|
|
284
|
+
const junit = mocha?.options?.reporterOptions?.['mocha-junit-reporter']
|
|
285
|
+
if (junit?.options?.attachments) {
|
|
286
|
+
test.attachments = test.attachments || []
|
|
287
|
+
test.attachments.push(filePath)
|
|
288
|
+
}
|
|
289
|
+
}
|