codeceptjs 4.0.0-rc.17 → 4.0.0-rc.19
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/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +733 -196
- package/docs/advanced.md +201 -0
- package/docs/agents.md +159 -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/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/aria.js +260 -0
- package/lib/command/dryRun.js +23 -3
- package/lib/command/init.js +247 -266
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/config.js +77 -4
- package/lib/container.js +34 -2
- package/lib/element/WebElement.js +37 -0
- package/lib/globals.js +11 -10
- package/lib/helper/Playwright.js +5 -6
- package/lib/helper/extras/PlaywrightReactVueLocator.js +45 -36
- package/lib/html.js +90 -16
- package/lib/index.js +9 -1
- package/lib/locator.js +2 -2
- package/lib/mocha/factory.js +5 -1
- package/lib/mocha/inject.js +1 -1
- package/lib/parser.js +2 -2
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +72 -84
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +44 -1
- package/lib/plugin/pageInfo.js +51 -48
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +10 -34
- package/lib/plugin/screencast.js +287 -0
- package/lib/plugin/screenshot.js +563 -0
- package/lib/plugin/screenshotOnFail.js +8 -170
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils.js +25 -0
- package/lib/workers.js +1 -15
- package/package.json +12 -10
- 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/plugin/pauseOn.js +0 -167
- package/lib/plugin/stepByStepReport.js +0 -432
- package/lib/plugin/subtitles.js +0 -89
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
import crypto from 'crypto'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { mkdirp } from 'mkdirp'
|
|
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 { fileExists, deleteDir, template } from '../utils.js'
|
|
13
|
+
import Codeceptjs from '../index.js'
|
|
14
|
+
import { testToFileName } from '../mocha/test.js'
|
|
15
|
+
import {
|
|
16
|
+
parsePluginArgs,
|
|
17
|
+
resolveTrigger,
|
|
18
|
+
matchStepFile,
|
|
19
|
+
matchUrl,
|
|
20
|
+
getBrowserHelper,
|
|
21
|
+
} from '../utils/pluginParser.js'
|
|
22
|
+
|
|
23
|
+
const defaultConfig = {
|
|
24
|
+
on: 'fail',
|
|
25
|
+
slides: false,
|
|
26
|
+
uniqueScreenshotNames: false,
|
|
27
|
+
disableScreenshots: false,
|
|
28
|
+
fullPageScreenshots: false,
|
|
29
|
+
animateSlides: true,
|
|
30
|
+
deleteSuccessful: true,
|
|
31
|
+
ignoreSteps: [],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Saves screenshots from the browser at points triggered by `on=`.
|
|
36
|
+
*
|
|
37
|
+
* Replaces the legacy `screenshotOnFail` plugin. Default `on=fail` preserves the
|
|
38
|
+
* old behavior (screenshot when a test fails). Pass `slides=true` (with `on=step`)
|
|
39
|
+
* to generate a step-by-step slideshow report — replaces the legacy
|
|
40
|
+
* `stepByStepReport` plugin.
|
|
41
|
+
*
|
|
42
|
+
* #### Configuration
|
|
43
|
+
*
|
|
44
|
+
* ```js
|
|
45
|
+
* plugins: {
|
|
46
|
+
* screenshot: {
|
|
47
|
+
* enabled: true,
|
|
48
|
+
* on: 'fail',
|
|
49
|
+
* }
|
|
50
|
+
* }
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* #### `on=` modes
|
|
54
|
+
*
|
|
55
|
+
* * **fail** — screenshot when a test fails (default)
|
|
56
|
+
* * **test** — screenshot at the end of every test
|
|
57
|
+
* * **step** — screenshot after every step
|
|
58
|
+
* * **file** — screenshot for steps in `path=...[;line=...]`
|
|
59
|
+
* * **url** — screenshot when the current browser URL matches `pattern=...`
|
|
60
|
+
*
|
|
61
|
+
* Other config options:
|
|
62
|
+
*
|
|
63
|
+
* * `uniqueScreenshotNames`: use unique names for screenshot. Default: false.
|
|
64
|
+
* * `fullPageScreenshots`: make full page screenshots. Default: false.
|
|
65
|
+
* * `disableScreenshots`: legacy switch to skip the plugin entirely.
|
|
66
|
+
* * `slides`: generate a step-by-step slideshow report (requires `on=step`). Default: false.
|
|
67
|
+
* * `deleteSuccessful`: when `slides=true`, drop slideshow directories of passing tests. Default: true.
|
|
68
|
+
* * `animateSlides`: when `slides=true`, animate transitions between slides. Default: true.
|
|
69
|
+
* * `ignoreSteps`: when `slides=true`, RegExps of step names to skip in the slideshow.
|
|
70
|
+
*
|
|
71
|
+
* CLI examples:
|
|
72
|
+
*
|
|
73
|
+
* ```
|
|
74
|
+
* npx codeceptjs run -p screenshot
|
|
75
|
+
* npx codeceptjs run -p screenshot:on=step
|
|
76
|
+
* npx codeceptjs run -p screenshot:on=step;slides=true
|
|
77
|
+
* npx codeceptjs run -p screenshot:on=file:path=tests/login_test.js
|
|
78
|
+
* npx codeceptjs run -p screenshot:on=url:pattern=/users/*
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export default function (config = {}) {
|
|
82
|
+
const helper = getBrowserHelper()
|
|
83
|
+
if (!helper) return
|
|
84
|
+
|
|
85
|
+
const cliArgs = parsePluginArgs(config._args)
|
|
86
|
+
const trigger = resolveTrigger(cliArgs, config, { on: defaultConfig.on }, { name: 'screenshot' })
|
|
87
|
+
if (!trigger) return
|
|
88
|
+
|
|
89
|
+
const helpers = Container.helpers()
|
|
90
|
+
const options = Object.assign({}, defaultConfig, helper.options, config)
|
|
91
|
+
options.slides = cliArgs.slides ?? config.slides ?? defaultConfig.slides
|
|
92
|
+
|
|
93
|
+
if (helpers.Mochawesome?.config) {
|
|
94
|
+
options.uniqueScreenshotNames = helpers.Mochawesome.config.uniqueScreenshotNames
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (Codeceptjs.container.mocha()) {
|
|
98
|
+
options.reportDir = Codeceptjs.container.mocha()?.options?.reporterOptions
|
|
99
|
+
&& Codeceptjs.container.mocha()?.options?.reporterOptions?.reportDir
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (options.disableScreenshots) return
|
|
103
|
+
|
|
104
|
+
if (options.slides) {
|
|
105
|
+
return wireSlides(options, trigger)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
switch (trigger.on) {
|
|
109
|
+
case 'fail':
|
|
110
|
+
return wireOnFail(options)
|
|
111
|
+
case 'test':
|
|
112
|
+
return wireOnTest(options)
|
|
113
|
+
case 'step':
|
|
114
|
+
return wireOnStep(options, () => true)
|
|
115
|
+
case 'file':
|
|
116
|
+
return wireOnStep(options, step => matchStepFile(step, trigger.path, trigger.line))
|
|
117
|
+
case 'url':
|
|
118
|
+
return wireOnUrl(options, trigger.pattern)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function wireOnFail(options) {
|
|
123
|
+
let currentTest = null
|
|
124
|
+
event.dispatcher.on(event.test.before, test => {
|
|
125
|
+
currentTest = test
|
|
126
|
+
})
|
|
127
|
+
event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
|
|
128
|
+
if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') return
|
|
129
|
+
const t = test || currentTest
|
|
130
|
+
if (!t) return
|
|
131
|
+
scheduleScreenshot(t, suffix(t, options, 'failed'), options)
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function wireOnTest(options) {
|
|
136
|
+
event.dispatcher.on(event.test.after, test => {
|
|
137
|
+
if (!test) return
|
|
138
|
+
scheduleScreenshot(test, suffix(test, options, 'test'), options)
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function wireOnStep(options, filter) {
|
|
143
|
+
let currentTest = null
|
|
144
|
+
let stepCount = 0
|
|
145
|
+
event.dispatcher.on(event.test.before, test => {
|
|
146
|
+
currentTest = test
|
|
147
|
+
stepCount = 0
|
|
148
|
+
})
|
|
149
|
+
event.dispatcher.on(event.step.after, step => {
|
|
150
|
+
if (!currentTest) return
|
|
151
|
+
if (!filter(step)) return
|
|
152
|
+
stepCount++
|
|
153
|
+
const name = `${testToFileName(currentTest, { suffix: '', unique: options.uniqueScreenshotNames })}.step_${stepCount}.png`
|
|
154
|
+
scheduleScreenshot(currentTest, name, options)
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function wireOnUrl(options, pattern) {
|
|
159
|
+
let currentTest = null
|
|
160
|
+
let stepCount = 0
|
|
161
|
+
event.dispatcher.on(event.test.before, test => {
|
|
162
|
+
currentTest = test
|
|
163
|
+
stepCount = 0
|
|
164
|
+
})
|
|
165
|
+
event.dispatcher.on(event.step.after, () => {
|
|
166
|
+
if (!currentTest) return
|
|
167
|
+
const helper = getBrowserHelper()
|
|
168
|
+
if (!helper) return
|
|
169
|
+
recorder.add('screenshot:url check', async () => {
|
|
170
|
+
try {
|
|
171
|
+
const url = await helper.grabCurrentUrl()
|
|
172
|
+
if (!matchUrl(url, pattern)) return
|
|
173
|
+
stepCount++
|
|
174
|
+
const name = `${testToFileName(currentTest, { suffix: '', unique: options.uniqueScreenshotNames })}.url_${stepCount}.png`
|
|
175
|
+
await takeScreenshot(currentTest, name, options)
|
|
176
|
+
} catch (err) {
|
|
177
|
+
// page may not be ready
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function suffix(test, options, kind) {
|
|
184
|
+
const base = testToFileName(test, { suffix: '', unique: options.uniqueScreenshotNames })
|
|
185
|
+
return `${base}.${kind}.png`
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function scheduleScreenshot(test, fileName, options) {
|
|
189
|
+
recorder.add(
|
|
190
|
+
'screenshot capture',
|
|
191
|
+
async () => takeScreenshot(test, fileName, options),
|
|
192
|
+
true,
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function takeScreenshot(test, fileName, options) {
|
|
197
|
+
const quietMode = !store.outputDir
|
|
198
|
+
if (!quietMode) {
|
|
199
|
+
output.plugin('screenshot', `Saving screenshot ${fileName}`)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const helper = getBrowserHelper()
|
|
203
|
+
if (!helper || typeof helper.saveScreenshot !== 'function') return
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
if (options.reportDir) {
|
|
207
|
+
fileName = path.join(options.reportDir, fileName)
|
|
208
|
+
const mochaReportDir = path.resolve(process.cwd(), options.reportDir)
|
|
209
|
+
if (!fileExists(mochaReportDir)) fs.mkdirSync(mochaReportDir)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (helper.page && helper.page.isClosed && helper.page.isClosed()) {
|
|
213
|
+
throw new Error('Browser page has been closed')
|
|
214
|
+
}
|
|
215
|
+
if (helper.browser && helper.browser.isConnected && !helper.browser.isConnected()) {
|
|
216
|
+
throw new Error('Browser has been disconnected')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const screenshotPromise = helper.saveScreenshot(fileName, options.fullPageScreenshots)
|
|
220
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
221
|
+
setTimeout(() => reject(new Error('Screenshot timeout after 5 seconds')), 5000)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
await Promise.race([screenshotPromise, timeoutPromise])
|
|
225
|
+
|
|
226
|
+
if (!test.artifacts) test.artifacts = {}
|
|
227
|
+
const baseOutputDir = store.outputDir || null
|
|
228
|
+
if (baseOutputDir) {
|
|
229
|
+
test.artifacts.screenshot = path.join(baseOutputDir, fileName)
|
|
230
|
+
const mocha = Container.mocha()
|
|
231
|
+
const junit = mocha?.options?.reporterOptions?.['mocha-junit-reporter']
|
|
232
|
+
if (junit?.options?.attachments) {
|
|
233
|
+
test.attachments = [path.join(baseOutputDir, fileName)]
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
test.artifacts.screenshot = fileName
|
|
237
|
+
}
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (!quietMode) {
|
|
240
|
+
output.plugin('screenshot', `Failed to save screenshot: ${err.message}`)
|
|
241
|
+
}
|
|
242
|
+
if (
|
|
243
|
+
err
|
|
244
|
+
&& ((err.message
|
|
245
|
+
&& (err.message.includes('Target page, context or browser has been closed')
|
|
246
|
+
|| err.message.includes('Browser page has been closed')
|
|
247
|
+
|| err.message.includes('Browser has been disconnected')
|
|
248
|
+
|| err.message.includes('was terminated due to')
|
|
249
|
+
|| err.message.includes('no such window: target window already closed')
|
|
250
|
+
|| err.message.includes('Screenshot timeout after')))
|
|
251
|
+
|| (err.type && err.type === 'RuntimeError'))
|
|
252
|
+
) {
|
|
253
|
+
output.log(`Can't make screenshot, ${err.message}`)
|
|
254
|
+
helper.isRunning = false
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function wireSlides(options, trigger) {
|
|
260
|
+
const reportDir = options.output
|
|
261
|
+
? path.resolve(store.codeceptDir, options.output)
|
|
262
|
+
: (store.outputDir || './_output')
|
|
263
|
+
|
|
264
|
+
const stepFilter = makeStepFilter(trigger, options)
|
|
265
|
+
const recordedTests = {}
|
|
266
|
+
|
|
267
|
+
let dir
|
|
268
|
+
let stepNum
|
|
269
|
+
let slides = {}
|
|
270
|
+
let savedStep = null
|
|
271
|
+
let currentTest = null
|
|
272
|
+
let scenarioFailed = false
|
|
273
|
+
|
|
274
|
+
event.dispatcher.on(event.suite.before, () => {
|
|
275
|
+
stepNum = -1
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
event.dispatcher.on(event.test.before, test => {
|
|
279
|
+
const hash = crypto.createHash('sha256').update(test.file + test.title).digest('hex')
|
|
280
|
+
dir = path.join(reportDir, `record_${hash}`)
|
|
281
|
+
mkdirp.sync(dir)
|
|
282
|
+
stepNum = 0
|
|
283
|
+
slides = {}
|
|
284
|
+
savedStep = null
|
|
285
|
+
currentTest = test
|
|
286
|
+
scenarioFailed = false
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
event.dispatcher.on(event.step.failed, step => {
|
|
290
|
+
recorder.add('slides: failed step', async () => persistStep(step), true)
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
event.dispatcher.on(event.step.after, step => {
|
|
294
|
+
recorder.add('slides: step', async () => persistStep(step), true)
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
event.dispatcher.on(event.test.passed, test => {
|
|
298
|
+
if (options.deleteSuccessful) {
|
|
299
|
+
deleteDir(dir)
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
persist(test)
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
event.dispatcher.on(event.test.failed, (test, _err, hookName) => {
|
|
306
|
+
if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') return
|
|
307
|
+
persist(test)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
event.dispatcher.on(event.all.result, () => {
|
|
311
|
+
if (Object.keys(recordedTests).length === 0) return
|
|
312
|
+
writeIndex(reportDir, recordedTests)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
if (event.workers && event.workers.result) {
|
|
316
|
+
event.dispatcher.on(event.workers.result, async () => {
|
|
317
|
+
await recorder.add(() => {
|
|
318
|
+
const tests = scanRecordDirs(reportDir)
|
|
319
|
+
if (Object.keys(tests).length) writeIndex(reportDir, tests)
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function persistStep(step) {
|
|
325
|
+
if (stepNum === -1) return
|
|
326
|
+
if (savedStep === step) return
|
|
327
|
+
if (scenarioFailed) return
|
|
328
|
+
if (step.metaStep && step.metaStep.name === 'BeforeSuite') return
|
|
329
|
+
if (!currentTest) return
|
|
330
|
+
if (!stepFilter(step)) return
|
|
331
|
+
if (isStepIgnored(step, options.ignoreSteps)) return
|
|
332
|
+
|
|
333
|
+
const fileName = `${String(stepNum).padStart(4, '0')}.png`
|
|
334
|
+
if (step.status === 'failed') scenarioFailed = true
|
|
335
|
+
stepNum++
|
|
336
|
+
slides[fileName] = step
|
|
337
|
+
|
|
338
|
+
const helper = getBrowserHelper()
|
|
339
|
+
if (!helper || typeof helper.saveScreenshot !== 'function') return
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const screenshotPath = path.join(dir, fileName)
|
|
343
|
+
await helper.saveScreenshot(screenshotPath, options.fullPageScreenshots)
|
|
344
|
+
step.artifacts = step.artifacts || {}
|
|
345
|
+
step.artifacts.screenshot = screenshotPath
|
|
346
|
+
|
|
347
|
+
currentTest.artifacts = currentTest.artifacts || {}
|
|
348
|
+
currentTest.artifacts.screenshots = currentTest.artifacts.screenshots || []
|
|
349
|
+
currentTest.artifacts.screenshots.push(screenshotPath)
|
|
350
|
+
} catch (err) {
|
|
351
|
+
output.plugin('screenshot', `Can't save step screenshot: ${err.message}`)
|
|
352
|
+
} finally {
|
|
353
|
+
savedStep = step
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function persist(test) {
|
|
358
|
+
if (!Object.keys(slides).length) return
|
|
359
|
+
|
|
360
|
+
const slideHtml = Object.keys(slides)
|
|
361
|
+
.sort()
|
|
362
|
+
.map((fileName, idx) => {
|
|
363
|
+
const step = slides[fileName]
|
|
364
|
+
const caption = step.toString().replace(/\[\d{2}m/g, '')
|
|
365
|
+
const failed = step.status === 'failed' ? ' is-failed' : ''
|
|
366
|
+
return template(SLIDE_TEMPLATE, {
|
|
367
|
+
image: fileName,
|
|
368
|
+
caption,
|
|
369
|
+
index: idx + 1,
|
|
370
|
+
activeClass: idx === 0 ? ' is-active' : '',
|
|
371
|
+
failed,
|
|
372
|
+
})
|
|
373
|
+
})
|
|
374
|
+
.join('')
|
|
375
|
+
|
|
376
|
+
const dotHtml = Object.keys(slides)
|
|
377
|
+
.map((_, idx) => `<button type="button" class="slides__dot${idx === 0 ? ' is-active' : ''}" data-slide="${idx}" aria-label="Step ${idx + 1}"></button>`)
|
|
378
|
+
.join('')
|
|
379
|
+
|
|
380
|
+
const html = template(SLIDESHOW_TEMPLATE, {
|
|
381
|
+
title: test.title,
|
|
382
|
+
feature: (test.parent && test.parent.title) || '',
|
|
383
|
+
slides: slideHtml,
|
|
384
|
+
dots: dotHtml,
|
|
385
|
+
animate: options.animateSlides ? 'true' : 'false',
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
const indexFile = path.join(dir, 'index.html')
|
|
389
|
+
fs.writeFileSync(indexFile, html)
|
|
390
|
+
recordedTests[`${(test.parent && test.parent.title) || ''}: ${test.title}`] = path.relative(reportDir, indexFile)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function makeStepFilter(trigger, options) {
|
|
395
|
+
if (trigger.on === 'file' && trigger.path) {
|
|
396
|
+
return step => matchStepFile(step, trigger.path, trigger.line)
|
|
397
|
+
}
|
|
398
|
+
if (trigger.on === 'fail') {
|
|
399
|
+
return step => step.status === 'failed'
|
|
400
|
+
}
|
|
401
|
+
return () => true
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function isStepIgnored(step, patterns) {
|
|
405
|
+
if (!patterns || !patterns.length) return false
|
|
406
|
+
for (const pattern of patterns) {
|
|
407
|
+
if (step.name && step.name.match(pattern)) return true
|
|
408
|
+
}
|
|
409
|
+
return false
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function scanRecordDirs(reportDir) {
|
|
413
|
+
const out = {}
|
|
414
|
+
try {
|
|
415
|
+
for (const item of fs.readdirSync(reportDir, { withFileTypes: true })) {
|
|
416
|
+
if (!item.isDirectory() || !item.name.startsWith('record_')) continue
|
|
417
|
+
const indexFile = path.join(reportDir, item.name, 'index.html')
|
|
418
|
+
if (!fs.existsSync(indexFile)) continue
|
|
419
|
+
const html = fs.readFileSync(indexFile, 'utf-8')
|
|
420
|
+
const titleMatch = html.match(/<title>([^<]*)<\/title>/)
|
|
421
|
+
const label = titleMatch ? titleMatch[1].replace(/^Slides — /, '') : item.name
|
|
422
|
+
out[label] = `${item.name}/index.html`
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
// ignore
|
|
426
|
+
}
|
|
427
|
+
return out
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function writeIndex(reportDir, recordedTests) {
|
|
431
|
+
const items = Object.entries(recordedTests)
|
|
432
|
+
.map(([name, href]) => `<li><a href="${href}">${escapeHtml(name)}</a></li>`)
|
|
433
|
+
.join('\n')
|
|
434
|
+
|
|
435
|
+
const html = template(INDEX_TEMPLATE, {
|
|
436
|
+
time: new Date().toString(),
|
|
437
|
+
records: items,
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
const indexPath = path.join(reportDir, 'records.html')
|
|
441
|
+
fs.writeFileSync(indexPath, html)
|
|
442
|
+
output.print(`Step-by-step preview: file://${indexPath}`)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function escapeHtml(s) {
|
|
446
|
+
return String(s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]))
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const SLIDE_TEMPLATE = `
|
|
450
|
+
<figure class="slides__slide{{activeClass}}{{failed}}" data-index="{{index}}">
|
|
451
|
+
<img class="slides__image" src="{{image}}" alt="">
|
|
452
|
+
<figcaption class="slides__caption">
|
|
453
|
+
<span class="slides__step">{{index}}</span>
|
|
454
|
+
<span class="slides__text">{{caption}}</span>
|
|
455
|
+
</figcaption>
|
|
456
|
+
</figure>
|
|
457
|
+
`
|
|
458
|
+
|
|
459
|
+
const SLIDESHOW_TEMPLATE = `<!DOCTYPE html>
|
|
460
|
+
<html lang="en">
|
|
461
|
+
<head>
|
|
462
|
+
<meta charset="utf-8">
|
|
463
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
464
|
+
<title>Slides — {{feature}}: {{title}}</title>
|
|
465
|
+
<style>
|
|
466
|
+
:root { color-scheme: dark; --bg: #0b0d10; --panel: #14181d; --fg: #e7ecef; --muted: #8a96a0; --accent: #ff5b00; --error: #c0392b; }
|
|
467
|
+
* { box-sizing: border-box; }
|
|
468
|
+
html, body { height: 100%; margin: 0; }
|
|
469
|
+
body { background: var(--bg); color: var(--fg); font: 14px/1.4 system-ui, -apple-system, "Segoe UI", Inter, sans-serif; display: flex; flex-direction: column; }
|
|
470
|
+
header { padding: 14px 20px; background: var(--panel); border-bottom: 1px solid #1f262d; display: flex; align-items: baseline; gap: 16px; }
|
|
471
|
+
header a { color: var(--muted); text-decoration: none; font-weight: 500; }
|
|
472
|
+
header a:hover { color: var(--fg); }
|
|
473
|
+
header .feature { color: var(--muted); }
|
|
474
|
+
header .test { font-weight: 600; }
|
|
475
|
+
.slides { flex: 1; position: relative; overflow: hidden; }
|
|
476
|
+
.slides__slide { position: absolute; inset: 0; margin: 0; display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; transition: opacity .25s ease; }
|
|
477
|
+
.slides[data-animate="false"] .slides__slide { transition: none; }
|
|
478
|
+
.slides__slide.is-active { opacity: 1; pointer-events: auto; }
|
|
479
|
+
.slides__image { max-width: 100%; max-height: 100%; object-fit: contain; box-shadow: 0 10px 40px rgba(0,0,0,.4); }
|
|
480
|
+
.slides__caption { position: absolute; left: 20px; right: 20px; bottom: 24px; padding: 12px 16px; background: rgba(20,24,29,.92); border: 1px solid #1f262d; border-radius: 6px; display: flex; gap: 12px; align-items: baseline; }
|
|
481
|
+
.slides__slide.is-failed .slides__caption { background: var(--error); border-color: var(--error); }
|
|
482
|
+
.slides__step { font-variant-numeric: tabular-nums; color: var(--muted); font-weight: 600; min-width: 2ch; }
|
|
483
|
+
.slides__slide.is-failed .slides__step { color: #ffd9d4; }
|
|
484
|
+
.slides__text { word-break: break-word; }
|
|
485
|
+
.nav { position: absolute; top: 0; bottom: 0; width: 25%; background: transparent; border: 0; cursor: pointer; color: transparent; }
|
|
486
|
+
.nav--prev { left: 0; }
|
|
487
|
+
.nav--next { right: 0; }
|
|
488
|
+
.dots { display: flex; gap: 6px; justify-content: center; padding: 12px; background: var(--panel); border-top: 1px solid #1f262d; flex-wrap: wrap; }
|
|
489
|
+
.slides__dot { width: 10px; height: 10px; border-radius: 50%; border: 0; background: #2a323a; cursor: pointer; padding: 0; }
|
|
490
|
+
.slides__dot.is-active { background: var(--accent); }
|
|
491
|
+
.slides__dot:hover { background: #3d4751; }
|
|
492
|
+
.slides__dot.is-active:hover { background: var(--accent); }
|
|
493
|
+
.hint { color: var(--muted); font-size: 12px; padding: 8px 20px; text-align: center; background: var(--panel); border-top: 1px solid #1f262d; }
|
|
494
|
+
</style>
|
|
495
|
+
</head>
|
|
496
|
+
<body>
|
|
497
|
+
<header>
|
|
498
|
+
<a href="../records.html">« back</a>
|
|
499
|
+
<span class="feature">{{feature}}</span>
|
|
500
|
+
<span class="test">{{title}}</span>
|
|
501
|
+
</header>
|
|
502
|
+
<div class="slides" data-animate="{{animate}}">
|
|
503
|
+
{{slides}}
|
|
504
|
+
<button class="nav nav--prev" type="button" aria-label="Previous">←</button>
|
|
505
|
+
<button class="nav nav--next" type="button" aria-label="Next">→</button>
|
|
506
|
+
</div>
|
|
507
|
+
<nav class="dots">{{dots}}</nav>
|
|
508
|
+
<p class="hint">Use ← / → to navigate, click sides of image, or use the dots below.</p>
|
|
509
|
+
<script>
|
|
510
|
+
(function () {
|
|
511
|
+
var slidesEl = document.querySelector('.slides');
|
|
512
|
+
var slides = Array.prototype.slice.call(slidesEl.querySelectorAll('.slides__slide'));
|
|
513
|
+
var dots = Array.prototype.slice.call(document.querySelectorAll('.slides__dot'));
|
|
514
|
+
var idx = 0;
|
|
515
|
+
function show(i) {
|
|
516
|
+
if (i < 0) i = slides.length - 1;
|
|
517
|
+
if (i >= slides.length) i = 0;
|
|
518
|
+
slides[idx].classList.remove('is-active');
|
|
519
|
+
dots[idx] && dots[idx].classList.remove('is-active');
|
|
520
|
+
idx = i;
|
|
521
|
+
slides[idx].classList.add('is-active');
|
|
522
|
+
dots[idx] && dots[idx].classList.add('is-active');
|
|
523
|
+
}
|
|
524
|
+
document.querySelector('.nav--prev').addEventListener('click', function () { show(idx - 1); });
|
|
525
|
+
document.querySelector('.nav--next').addEventListener('click', function () { show(idx + 1); });
|
|
526
|
+
dots.forEach(function (d, i) { d.addEventListener('click', function () { show(i); }); });
|
|
527
|
+
document.addEventListener('keydown', function (e) {
|
|
528
|
+
if (e.key === 'ArrowLeft') show(idx - 1);
|
|
529
|
+
if (e.key === 'ArrowRight') show(idx + 1);
|
|
530
|
+
});
|
|
531
|
+
})();
|
|
532
|
+
</script>
|
|
533
|
+
</body>
|
|
534
|
+
</html>
|
|
535
|
+
`
|
|
536
|
+
|
|
537
|
+
const INDEX_TEMPLATE = `<!DOCTYPE html>
|
|
538
|
+
<html lang="en">
|
|
539
|
+
<head>
|
|
540
|
+
<meta charset="utf-8">
|
|
541
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
542
|
+
<title>Step-by-step Reports</title>
|
|
543
|
+
<style>
|
|
544
|
+
:root { color-scheme: dark; --bg: #0b0d10; --panel: #14181d; --fg: #e7ecef; --muted: #8a96a0; --accent: #ff5b00; }
|
|
545
|
+
* { box-sizing: border-box; }
|
|
546
|
+
body { background: var(--bg); color: var(--fg); font: 14px/1.5 system-ui, -apple-system, "Segoe UI", Inter, sans-serif; max-width: 880px; margin: 0 auto; padding: 32px 24px; }
|
|
547
|
+
h1 { margin: 0 0 4px; font-size: 22px; font-weight: 600; }
|
|
548
|
+
.meta { color: var(--muted); margin-bottom: 24px; font-size: 13px; }
|
|
549
|
+
ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 4px; }
|
|
550
|
+
li { background: var(--panel); border: 1px solid #1f262d; border-radius: 6px; }
|
|
551
|
+
li a { display: block; padding: 12px 16px; color: var(--fg); text-decoration: none; }
|
|
552
|
+
li a:hover { background: #1c2229; border-color: var(--accent); }
|
|
553
|
+
</style>
|
|
554
|
+
</head>
|
|
555
|
+
<body>
|
|
556
|
+
<h1>Step-by-step Reports</h1>
|
|
557
|
+
<div class="meta">{{time}}</div>
|
|
558
|
+
<ul>
|
|
559
|
+
{{records}}
|
|
560
|
+
</ul>
|
|
561
|
+
</body>
|
|
562
|
+
</html>
|
|
563
|
+
`
|