codeceptjs 4.0.0-rc.2 → 4.0.0-rc.21
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 +39 -27
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +1189 -0
- package/docs/advanced.md +201 -0
- package/docs/agents.md +181 -0
- package/docs/ai.md +537 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -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 +230 -0
- package/docs/continuous-integration.md +497 -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 +136 -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 +161 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1405 -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/Mochawesome.md +8 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/MockServer.md +212 -0
- package/docs/helpers/Playwright.md +2969 -0
- package/docs/helpers/Polly.md +44 -0
- package/docs/helpers/Protractor.md +1769 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2690 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/SoftExpectHelper.md +352 -0
- package/docs/helpers/WebDriver.md +2682 -0
- package/docs/hooks.md +339 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +83 -0
- package/docs/internal-api.md +265 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migration-4.md +556 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +585 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins.md +866 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/react.md +70 -0
- package/docs/reports.md +483 -0
- package/docs/retry.md +274 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +271 -0
- package/docs/typescript.md +374 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +708 -0
- package/docs/within.md +55 -0
- package/lib/ai.js +3 -2
- package/lib/aria.js +260 -0
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +27 -24
- 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 -269
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +2 -0
- package/lib/command/run-workers.js +2 -14
- package/lib/command/run.js +3 -17
- package/lib/command/utils.js +14 -0
- package/lib/command/workers/runTests.js +10 -10
- package/lib/config.js +77 -4
- package/lib/container.js +114 -17
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +246 -2
- package/lib/els.js +12 -6
- package/lib/globals.js +32 -19
- package/lib/heal.js +6 -3
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/Appium.js +8 -8
- package/lib/helper/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +233 -162
- package/lib/helper/Puppeteer.js +208 -76
- package/lib/helper/WebDriver.js +173 -68
- package/lib/helper/errors/MultipleElementsFound.js +27 -110
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
- package/lib/helper/extras/elementSelection.js +58 -0
- package/lib/helper/extras/focusCheck.js +43 -0
- package/lib/helper/extras/richTextEditor.js +178 -0
- package/lib/helper/scripts/dropFile.js +11 -0
- package/lib/history.js +3 -2
- package/lib/html.js +103 -16
- package/lib/index.js +9 -1
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/globalRetry.js +32 -6
- 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 -3
- package/lib/mocha/cli.js +14 -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 +456 -0
- package/lib/plugin/analyze.js +6 -5
- 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/pageInfo.js +54 -52
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +10 -34
- package/lib/plugin/retryFailedStep.js +32 -22
- package/lib/plugin/screencast.js +289 -0
- package/lib/plugin/screenshot.js +563 -0
- package/lib/plugin/screenshotOnFail.js +8 -171
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +3 -2
- package/lib/step/config.js +15 -2
- package/lib/step/record.js +2 -2
- 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 +77 -3
- package/lib/workers.js +63 -25
- package/package.json +19 -13
- package/typings/index.d.ts +19 -5
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -11
- package/docs/webapi/attachFile.mustache +0 -12
- 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 -9
- 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 -8
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -11
- 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 -16
- 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 -12
- 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 -8
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -12
- 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 -21
- 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/listener/enhancedGlobalRetry.js +0 -110
- package/lib/plugin/enhancedRetryFailedStep.js +0 -99
- package/lib/plugin/htmlReporter.js +0 -3648
- package/lib/plugin/stepByStepReport.js +0 -427
- package/lib/plugin/subtitles.js +0 -89
- package/lib/retryCoordinator.js +0 -207
- package/typings/promiseBasedTypes.d.ts +0 -9469
- package/typings/types.d.ts +0 -11402
|
@@ -1,14 +1,24 @@
|
|
|
1
|
+
import debugModule from 'debug'
|
|
1
2
|
import event from '../event.js'
|
|
2
|
-
|
|
3
3
|
import recorder from '../recorder.js'
|
|
4
|
-
|
|
5
4
|
import store from '../store.js'
|
|
6
5
|
|
|
6
|
+
const debug = debugModule('codeceptjs:retryFailedStep')
|
|
7
|
+
|
|
7
8
|
const defaultConfig = {
|
|
8
9
|
retries: 3,
|
|
9
10
|
defaultIgnoredSteps: ['amOnPage', 'wait*', 'send*', 'execute*', 'run*', 'have*'],
|
|
10
11
|
factor: 1.5,
|
|
11
12
|
ignoredSteps: [],
|
|
13
|
+
deferToScenarioRetries: true,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const RETRY_PRIORITIES = {
|
|
17
|
+
MANUAL_STEP: 100,
|
|
18
|
+
STEP_PLUGIN: 50,
|
|
19
|
+
SCENARIO_CONFIG: 30,
|
|
20
|
+
FEATURE_CONFIG: 20,
|
|
21
|
+
HOOK_CONFIG: 10,
|
|
12
22
|
}
|
|
13
23
|
|
|
14
24
|
/**
|
|
@@ -49,6 +59,7 @@ const defaultConfig = {
|
|
|
49
59
|
* * `ignoredSteps` - an array for custom steps to ignore on retry. Use it to append custom steps to ignored list.
|
|
50
60
|
* You can use step names or step prefixes ending with `*`. As such, `wait*` will match all steps starting with `wait`.
|
|
51
61
|
* To append your own steps to ignore list - copy and paste a default steps list. Regexp values are accepted as well.
|
|
62
|
+
* * `deferToScenarioRetries` - when enabled (default), step retries are automatically disabled if scenario retries are configured to avoid excessive total retries.
|
|
52
63
|
*
|
|
53
64
|
* #### Example
|
|
54
65
|
*
|
|
@@ -88,73 +99,72 @@ export default function (config) {
|
|
|
88
99
|
if (!enableRetry) return
|
|
89
100
|
if (store.debugMode) return false
|
|
90
101
|
if (!store.autoRetries) return false
|
|
91
|
-
// Don't retry terminal errors (e.g., frame detachment errors)
|
|
92
102
|
if (err && err.isTerminal) return false
|
|
93
|
-
// Don't retry navigation errors that are known to be terminal
|
|
94
103
|
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
|
|
95
104
|
if (customWhen) return customWhen(err)
|
|
96
105
|
return true
|
|
97
106
|
}
|
|
98
107
|
config.when = when
|
|
99
108
|
|
|
100
|
-
// Ensure retry options are available before any steps run
|
|
101
109
|
if (!recorder.retries.find(r => r === config)) {
|
|
102
110
|
recorder.retries.push(config)
|
|
103
111
|
}
|
|
104
112
|
|
|
105
113
|
event.dispatcher.on(event.step.started, step => {
|
|
106
|
-
// if a step is ignored - return
|
|
107
114
|
for (const ignored of config.ignoredSteps) {
|
|
108
115
|
if (step.name === ignored) return
|
|
109
116
|
if (ignored instanceof RegExp) {
|
|
110
117
|
if (step.name.match(ignored)) return
|
|
111
118
|
} else if (ignored.indexOf('*') && step.name.startsWith(ignored.slice(0, -1))) return
|
|
112
119
|
}
|
|
113
|
-
enableRetry = true
|
|
120
|
+
enableRetry = true
|
|
114
121
|
})
|
|
115
122
|
|
|
116
|
-
// Disable retry only after a successful step; keep it enabled for failure so retry logic can act
|
|
117
123
|
event.dispatcher.on(event.step.passed, () => {
|
|
118
124
|
enableRetry = false
|
|
119
125
|
})
|
|
120
126
|
|
|
121
127
|
event.dispatcher.on(event.test.before, test => {
|
|
122
|
-
// pass disableRetryFailedStep is a preferred way to disable retries
|
|
123
|
-
// test.disableRetryFailedStep is used for backward compatibility
|
|
124
128
|
if (!test.opts) test.opts = {}
|
|
125
129
|
if (test.opts.disableRetryFailedStep || test.disableRetryFailedStep) {
|
|
126
130
|
store.autoRetries = false
|
|
127
|
-
return
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const scenarioRetries = typeof test.retries === 'function' ? test.retries() : -1
|
|
135
|
+
const stepRetryPriority = RETRY_PRIORITIES.STEP_PLUGIN
|
|
136
|
+
const scenarioPriority = test.opts.retryPriority || 0
|
|
137
|
+
|
|
138
|
+
if (scenarioRetries > 0 && config.deferToScenarioRetries !== false) {
|
|
139
|
+
store.autoRetries = false
|
|
140
|
+
return
|
|
128
141
|
}
|
|
129
142
|
|
|
130
|
-
// Don't apply plugin retry logic if there are already manual retries configured
|
|
131
|
-
// Check if any retry configs exist that aren't from this plugin
|
|
132
143
|
const hasManualRetries = recorder.retries.some(retry => retry !== config)
|
|
133
144
|
if (hasManualRetries) {
|
|
134
145
|
store.autoRetries = false
|
|
135
146
|
return
|
|
136
147
|
}
|
|
137
148
|
|
|
138
|
-
// this option is used to set the retries inside _before() block of helpers
|
|
139
149
|
store.autoRetries = true
|
|
140
150
|
test.opts.conditionalRetries = config.retries
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
console.log('[retryFailedStep] applying retries =', config.retries, 'for test', test.title)
|
|
145
|
-
}
|
|
151
|
+
test.opts.stepRetryPriority = stepRetryPriority
|
|
152
|
+
|
|
153
|
+
debug('applying retries = %d for test %s', config.retries, test.title)
|
|
146
154
|
recorder.retry(config)
|
|
147
155
|
})
|
|
148
156
|
|
|
149
|
-
// Fallback for environments where event.test.before wasn't emitted (runner scenarios)
|
|
150
157
|
event.dispatcher.on(event.test.started, test => {
|
|
151
158
|
if (test.opts?.disableRetryFailedStep || test.disableRetryFailedStep) return
|
|
152
159
|
|
|
153
|
-
// Don't apply plugin retry logic if there are already manual retries configured
|
|
154
|
-
// Check if any retry configs exist that aren't from this plugin
|
|
155
160
|
const hasManualRetries = recorder.retries.some(retry => retry !== config)
|
|
156
161
|
if (hasManualRetries) return
|
|
157
162
|
|
|
163
|
+
const scenarioRetries = typeof test.retries === 'function' ? test.retries() : -1
|
|
164
|
+
if (scenarioRetries > 0 && config.deferToScenarioRetries !== false) {
|
|
165
|
+
return
|
|
166
|
+
}
|
|
167
|
+
|
|
158
168
|
if (!store.autoRetries) {
|
|
159
169
|
store.autoRetries = true
|
|
160
170
|
test.opts.conditionalRetries = test.opts.conditionalRetries || config.retries
|
|
@@ -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.name}(${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
|
+
}
|