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
package/lib/helper/Playwright.js
CHANGED
|
@@ -36,7 +36,7 @@ import MultipleElementsFound from './errors/MultipleElementsFound.js'
|
|
|
36
36
|
import RemoteBrowserConnectionRefused from './errors/RemoteBrowserConnectionRefused.js'
|
|
37
37
|
import Popup from './extras/Popup.js'
|
|
38
38
|
import Console from './extras/Console.js'
|
|
39
|
-
import { findReact,
|
|
39
|
+
import { findReact, findByPlaywrightLocator } from './extras/PlaywrightReactVueLocator.js'
|
|
40
40
|
import { dropFile } from './scripts/dropFile.js'
|
|
41
41
|
import WebElement from '../element/WebElement.js'
|
|
42
42
|
import { selectElement } from './extras/elementSelection.js'
|
|
@@ -2674,8 +2674,11 @@ class Playwright extends Helper {
|
|
|
2674
2674
|
* @returns {Promise<any>}
|
|
2675
2675
|
*/
|
|
2676
2676
|
async executeScript(fn, arg) {
|
|
2677
|
+
if (arg && typeof arg.getNativeElement === 'function') arg = arg.getNativeElement()
|
|
2678
|
+
if (arg && typeof arg.evaluate === 'function' && typeof arg.locator === 'function') {
|
|
2679
|
+
return arg.evaluate(fn)
|
|
2680
|
+
}
|
|
2677
2681
|
if (this.context && this.context.constructor.name === 'FrameLocator') {
|
|
2678
|
-
// switching to iframe context
|
|
2679
2682
|
return this.context.locator(':root').evaluate(fn, arg)
|
|
2680
2683
|
}
|
|
2681
2684
|
return this.page.evaluate.apply(this.page, [fn, arg])
|
|
@@ -4223,13 +4226,10 @@ async function findByRole(context, locator) {
|
|
|
4223
4226
|
}
|
|
4224
4227
|
|
|
4225
4228
|
async function findElements(matcher, locator) {
|
|
4226
|
-
// Check if locator is a Locator object with react/vue type, or a raw object with react/vue property
|
|
4227
4229
|
const isReactLocator = locator.type === 'react' || (locator.locator && locator.locator.react) || locator.react
|
|
4228
|
-
const isVueLocator = locator.type === 'vue' || (locator.locator && locator.locator.vue) || locator.vue
|
|
4229
4230
|
const isPwLocator = locator.type === 'pw' || (locator.locator && locator.locator.pw) || locator.pw
|
|
4230
4231
|
|
|
4231
4232
|
if (isReactLocator) return findReact(matcher, locator)
|
|
4232
|
-
if (isVueLocator) return findVue(matcher, locator)
|
|
4233
4233
|
if (isPwLocator) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4234
4234
|
|
|
4235
4235
|
// Handle role locators with text/exact options (e.g., {role: 'button', text: 'Submit', exact: true})
|
|
@@ -4245,7 +4245,6 @@ async function findElements(matcher, locator) {
|
|
|
4245
4245
|
|
|
4246
4246
|
async function findElement(matcher, locator) {
|
|
4247
4247
|
if (locator.react) return findReact(matcher, locator)
|
|
4248
|
-
if (locator.vue) return findVue(matcher, locator)
|
|
4249
4248
|
if (locator.pw) return findByPlaywrightLocator.call(this, matcher, locator)
|
|
4250
4249
|
|
|
4251
4250
|
locator = new Locator(locator, 'css')
|
|
@@ -1,52 +1,61 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import { fileURLToPath } from 'url'
|
|
3
|
+
|
|
4
|
+
let resqScript
|
|
5
|
+
|
|
1
6
|
async function findReact(matcher, locator) {
|
|
2
|
-
// Handle both Locator objects and raw locator objects
|
|
3
7
|
const reactLocator = locator.locator || locator
|
|
4
|
-
|
|
5
|
-
let props = '';
|
|
8
|
+
const page = typeof matcher.page === 'function' ? matcher.page() : matcher
|
|
6
9
|
|
|
7
|
-
if (
|
|
8
|
-
|
|
9
|
-
_locator += props;
|
|
10
|
+
if (!resqScript) {
|
|
11
|
+
resqScript = fs.readFileSync(fileURLToPath(import.meta.resolve('resq'))).toString()
|
|
10
12
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
await page.evaluate(resqScript)
|
|
14
|
+
await page.evaluate(() => window.resq.waitToLoadReact())
|
|
15
|
+
|
|
16
|
+
const arrayHandle = await page.evaluateHandle(
|
|
17
|
+
({ selector, props, state }) => {
|
|
18
|
+
let elements = window.resq.resq$$(selector)
|
|
19
|
+
if (Object.keys(props).length) elements = elements.byProps(props)
|
|
20
|
+
if (Object.keys(state).length) elements = elements.byState(state)
|
|
21
|
+
if (!elements.length) return []
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
23
|
+
let nodes = []
|
|
24
|
+
elements.forEach(element => {
|
|
25
|
+
let { node, isFragment } = element
|
|
26
|
+
if (!node) {
|
|
27
|
+
isFragment = true
|
|
28
|
+
node = element.children
|
|
29
|
+
}
|
|
30
|
+
if (isFragment) nodes = nodes.concat(node)
|
|
31
|
+
else nodes.push(node)
|
|
32
|
+
})
|
|
33
|
+
return [...nodes]
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
selector: reactLocator.react,
|
|
37
|
+
props: reactLocator.props || {},
|
|
38
|
+
state: reactLocator.state || {},
|
|
39
|
+
},
|
|
40
|
+
)
|
|
19
41
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
42
|
+
const properties = await arrayHandle.getProperties()
|
|
43
|
+
await arrayHandle.dispose()
|
|
44
|
+
const result = []
|
|
45
|
+
for (const property of properties.values()) {
|
|
46
|
+
const elementHandle = property.asElement()
|
|
47
|
+
if (elementHandle) result.push(elementHandle)
|
|
23
48
|
}
|
|
24
|
-
return
|
|
49
|
+
return result
|
|
25
50
|
}
|
|
26
51
|
|
|
27
52
|
async function findByPlaywrightLocator(matcher, locator) {
|
|
28
|
-
// Handle both Locator objects and raw locator objects
|
|
29
53
|
const pwLocator = locator.locator || locator
|
|
30
54
|
if (pwLocator && pwLocator.toString && pwLocator.toString().includes(process.env.testIdAttribute)) {
|
|
31
|
-
return matcher.getByTestId(pwLocator.pw.value.split('=')[1])
|
|
55
|
+
return matcher.getByTestId(pwLocator.pw.value.split('=')[1])
|
|
32
56
|
}
|
|
33
57
|
const pwValue = typeof pwLocator.pw === 'string' ? pwLocator.pw : pwLocator.pw
|
|
34
|
-
return matcher.locator(pwValue).all()
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function propBuilder(props) {
|
|
38
|
-
let _props = '';
|
|
39
|
-
|
|
40
|
-
for (const [key, value] of Object.entries(props)) {
|
|
41
|
-
if (typeof value === 'object') {
|
|
42
|
-
for (const [k, v] of Object.entries(value)) {
|
|
43
|
-
_props += `[${key}.${k} = "${v}"]`;
|
|
44
|
-
}
|
|
45
|
-
} else {
|
|
46
|
-
_props += `[${key} = "${value}"]`;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return _props;
|
|
58
|
+
return matcher.locator(pwValue).all()
|
|
50
59
|
}
|
|
51
60
|
|
|
52
|
-
export { findReact,
|
|
61
|
+
export { findReact, findByPlaywrightLocator }
|
package/lib/html.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { parse, serialize } from 'parse5'
|
|
2
2
|
import { minify } from 'html-minifier-terser'
|
|
3
|
+
import beautify from 'js-beautify'
|
|
4
|
+
|
|
5
|
+
const { html: html_beautify } = beautify
|
|
3
6
|
|
|
4
7
|
async function minifyHtml(html) {
|
|
5
8
|
return minify(html, {
|
|
@@ -14,6 +17,62 @@ async function minifyHtml(html) {
|
|
|
14
17
|
})
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
const TRASH_HTML_CLASSES = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/
|
|
21
|
+
|
|
22
|
+
function isTrashClass(className) {
|
|
23
|
+
if (!className) return true
|
|
24
|
+
if (/\d/.test(className)) return true
|
|
25
|
+
if (TRASH_HTML_CLASSES.test(className)) return true
|
|
26
|
+
if (/(:|__)/.test(className)) return true
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function filterClassValue(value) {
|
|
31
|
+
return (value || '')
|
|
32
|
+
.split(/\s+/)
|
|
33
|
+
.filter(c => c && !isTrashClass(c))
|
|
34
|
+
.join(' ')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const DROP_TAGS = new Set(['style', 'noscript'])
|
|
38
|
+
const DROP_ATTRS = new Set(['style'])
|
|
39
|
+
|
|
40
|
+
function cleanHtml(html) {
|
|
41
|
+
const document = parse(html)
|
|
42
|
+
|
|
43
|
+
function walk(node) {
|
|
44
|
+
if (!node) return false
|
|
45
|
+
|
|
46
|
+
if (DROP_TAGS.has(node.nodeName) || (node.nodeName === 'script' && !(node.attrs || []).some(a => a.name === 'src'))) {
|
|
47
|
+
const parent = node.parentNode
|
|
48
|
+
const idx = parent.childNodes.indexOf(node)
|
|
49
|
+
if (idx >= 0) parent.childNodes.splice(idx, 1)
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (node.attrs) {
|
|
54
|
+
node.attrs = node.attrs.filter(attr => {
|
|
55
|
+
if (DROP_ATTRS.has(attr.name)) return false
|
|
56
|
+
if (attr.name === 'class') {
|
|
57
|
+
attr.value = filterClassValue(attr.value)
|
|
58
|
+
if (!attr.value) return false
|
|
59
|
+
}
|
|
60
|
+
return true
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (node.childNodes) {
|
|
65
|
+
for (let i = node.childNodes.length - 1; i >= 0; i--) {
|
|
66
|
+
walk(node.childNodes[i])
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
walk(document)
|
|
73
|
+
return serialize(document)
|
|
74
|
+
}
|
|
75
|
+
|
|
17
76
|
const defaultHtmlOpts = {
|
|
18
77
|
interactiveElements: ['a', 'input', 'button', 'select', 'textarea', 'option'],
|
|
19
78
|
textElements: ['label', 'h1', 'h2'],
|
|
@@ -28,7 +87,6 @@ function removeNonInteractiveElements(html, opts = {}) {
|
|
|
28
87
|
// Parse the HTML into a document tree
|
|
29
88
|
const document = parse(html)
|
|
30
89
|
|
|
31
|
-
const trashHtmlClasses = /^(text-|color-|flex-|float-|v-|ember-|d-|border-)/
|
|
32
90
|
// Array to store interactive elements
|
|
33
91
|
const removeElements = ['path', 'script']
|
|
34
92
|
|
|
@@ -103,21 +161,10 @@ function removeNonInteractiveElements(html, opts = {}) {
|
|
|
103
161
|
if (node.attrs) {
|
|
104
162
|
// Filter and keep allowed attributes, accessibility attributes
|
|
105
163
|
node.attrs = node.attrs.filter(attr => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// Remove classes containing digits
|
|
109
|
-
attr.value = value
|
|
110
|
-
.split(' ')
|
|
111
|
-
// remove classes containing digits/
|
|
112
|
-
.filter(className => !/\d/.test(className))
|
|
113
|
-
// remove popular trash classes
|
|
114
|
-
.filter(className => !className.match(trashHtmlClasses))
|
|
115
|
-
// remove classes with : and __ in them
|
|
116
|
-
.filter(className => !className.match(/(:|__)/))
|
|
117
|
-
.join(' ')
|
|
164
|
+
if (attr.name === 'class') {
|
|
165
|
+
attr.value = filterClassValue(attr.value)
|
|
118
166
|
}
|
|
119
|
-
|
|
120
|
-
return allowedAttrs.includes(name)
|
|
167
|
+
return allowedAttrs.includes(attr.name)
|
|
121
168
|
})
|
|
122
169
|
}
|
|
123
170
|
|
|
@@ -258,4 +305,31 @@ function simplifyHtmlElement(html, maxLength = 300) {
|
|
|
258
305
|
return html
|
|
259
306
|
}
|
|
260
307
|
|
|
261
|
-
|
|
308
|
+
async function formatHtml(html) {
|
|
309
|
+
let processed = html
|
|
310
|
+
try {
|
|
311
|
+
processed = await minifyHtml(processed)
|
|
312
|
+
} catch (e) {
|
|
313
|
+
// keep raw html if minification fails
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
processed = cleanHtml(processed)
|
|
317
|
+
} catch (e) {
|
|
318
|
+
// keep minified html if cleaning fails
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
return html_beautify(processed, {
|
|
322
|
+
indent_size: 2,
|
|
323
|
+
wrap_line_length: 0,
|
|
324
|
+
preserve_newlines: false,
|
|
325
|
+
end_with_newline: false,
|
|
326
|
+
// Force every element onto its own line so line numbers in trace HTML
|
|
327
|
+
// map 1:1 to elements (consumed by codeceptq for AI/agent debugging).
|
|
328
|
+
inline: [],
|
|
329
|
+
})
|
|
330
|
+
} catch (e) {
|
|
331
|
+
return processed
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export { scanForErrorMessages, removeNonInteractiveElements, splitByChunks, minifyHtml, simplifyHtmlElement, formatHtml, cleanHtml, isTrashClass }
|
package/lib/index.js
CHANGED
|
@@ -23,6 +23,10 @@ import heal from './heal.js'
|
|
|
23
23
|
import ai from './ai.js'
|
|
24
24
|
import Workers from './workers.js'
|
|
25
25
|
import Secret, { secret } from './secret.js'
|
|
26
|
+
import session from './session.js'
|
|
27
|
+
|
|
28
|
+
const inject = (name) => container.support(name)
|
|
29
|
+
const locate = (query) => locator.build(query)
|
|
26
30
|
|
|
27
31
|
export default {
|
|
28
32
|
/** @type {typeof CodeceptJS.Codecept} */
|
|
@@ -67,7 +71,11 @@ export default {
|
|
|
67
71
|
Secret,
|
|
68
72
|
/** @type {typeof CodeceptJS.secret} */
|
|
69
73
|
secret,
|
|
74
|
+
|
|
75
|
+
session,
|
|
76
|
+
inject,
|
|
77
|
+
locate,
|
|
70
78
|
}
|
|
71
79
|
|
|
72
80
|
// Named exports for ESM compatibility
|
|
73
|
-
export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret }
|
|
81
|
+
export { codecept, output, container, event, recorder, config, actor, helper, pause, within, dataTable, dataTableArgument, store, locator, heal, ai, Workers, Secret, secret, session, inject, locate }
|
package/lib/locator.js
CHANGED
|
@@ -589,7 +589,7 @@ Locator.clickable = {
|
|
|
589
589
|
`.//button[./@name = ${literal}]`,
|
|
590
590
|
`.//*[@aria-label = ${literal}]`,
|
|
591
591
|
`.//*[@title = ${literal}]`,
|
|
592
|
-
`.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id
|
|
592
|
+
`.//*[@aria-labelledby][@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id]`,
|
|
593
593
|
`.//*[@role='button'][normalize-space(.)=${literal}]`,
|
|
594
594
|
`.//*[@role='tab' or @role='link' or @role='menuitem' or @role='menuitemcheckbox' or @role='menuitemradio' or @role='option' or @role='treeitem'][contains(normalize-space(string(.)), ${literal})]`,
|
|
595
595
|
]),
|
|
@@ -632,7 +632,7 @@ Locator.field = {
|
|
|
632
632
|
`.//label[contains(normalize-space(string(.)), ${literal})]//.//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]`,
|
|
633
633
|
`.//*[@aria-label = ${literal}]`,
|
|
634
634
|
`.//*[@title = ${literal}]`,
|
|
635
|
-
`.//*[@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id
|
|
635
|
+
`.//*[@aria-labelledby][@aria-labelledby = //*[@id][normalize-space(string(.)) = ${literal}]/@id]`,
|
|
636
636
|
]),
|
|
637
637
|
|
|
638
638
|
/**
|
package/lib/mocha/factory.js
CHANGED
|
@@ -17,7 +17,11 @@ let mocha
|
|
|
17
17
|
|
|
18
18
|
class MochaFactory {
|
|
19
19
|
static create(config, opts) {
|
|
20
|
-
|
|
20
|
+
const merged = Object.assign({}, config, opts)
|
|
21
|
+
mocha = new Mocha(merged)
|
|
22
|
+
if (merged.cleanReferencesAfterRun !== true) {
|
|
23
|
+
mocha.cleanReferencesAfterRun(false)
|
|
24
|
+
}
|
|
21
25
|
output.process(opts.child)
|
|
22
26
|
mocha.ui(scenarioUiFunction)
|
|
23
27
|
|
package/lib/mocha/inject.js
CHANGED
|
@@ -5,7 +5,7 @@ const getInjectedArguments = async (fn, test, suite) => {
|
|
|
5
5
|
const container = containerModule.default || containerModule
|
|
6
6
|
|
|
7
7
|
const testArgs = {}
|
|
8
|
-
const params = getParams(fn) || []
|
|
8
|
+
const params = getParams(fn, { warnOnLegacyFormat: true }) || []
|
|
9
9
|
const objects = container.support()
|
|
10
10
|
|
|
11
11
|
for (const key of params) {
|
package/lib/parser.js
CHANGED
|
@@ -14,11 +14,11 @@ export const getParamsToString = function (fn) {
|
|
|
14
14
|
return getParams(newFn).join(', ')
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
function getParams(fn) {
|
|
17
|
+
function getParams(fn, { warnOnLegacyFormat = false } = {}) {
|
|
18
18
|
if (fn.isSinonProxy) return []
|
|
19
19
|
try {
|
|
20
20
|
const reflected = parser.parse(fn)
|
|
21
|
-
if (reflected.args.length > 1 || reflected.args[0] === 'I') {
|
|
21
|
+
if (warnOnLegacyFormat && (reflected.args.length > 1 || reflected.args[0] === 'I')) {
|
|
22
22
|
output.error('Error: old CodeceptJS v2 format detected. Upgrade your project to the new format -> https://bit.ly/codecept3Up')
|
|
23
23
|
}
|
|
24
24
|
if (reflected.destructuredArgs.length > 0) reflected.args = [...reflected.destructuredArgs]
|
package/lib/pause.js
CHANGED
|
@@ -18,6 +18,8 @@ let nextStep
|
|
|
18
18
|
let finish
|
|
19
19
|
let next
|
|
20
20
|
let registeredVariables = {}
|
|
21
|
+
let externalHandler = null
|
|
22
|
+
|
|
21
23
|
/**
|
|
22
24
|
* Pauses test execution and starts interactive shell
|
|
23
25
|
* @param {Object<string, *>} [passedObject]
|
|
@@ -37,10 +39,10 @@ const pause = function (passedObject = {}) {
|
|
|
37
39
|
})
|
|
38
40
|
|
|
39
41
|
event.dispatcher.on(event.test.finished, () => {
|
|
40
|
-
finish()
|
|
42
|
+
if (typeof finish === 'function') finish()
|
|
41
43
|
recorder.session.restore('pause')
|
|
42
|
-
rl.close()
|
|
43
|
-
history.save()
|
|
44
|
+
if (rl) rl.close()
|
|
45
|
+
if (!externalHandler) history.save()
|
|
44
46
|
})
|
|
45
47
|
|
|
46
48
|
recorder.add('Start new session', () => pauseSession(passedObject))
|
|
@@ -49,6 +51,15 @@ const pause = function (passedObject = {}) {
|
|
|
49
51
|
function pauseSession(passedObject = {}) {
|
|
50
52
|
registeredVariables = passedObject
|
|
51
53
|
recorder.session.start('pause')
|
|
54
|
+
|
|
55
|
+
if (externalHandler) {
|
|
56
|
+
store.onPause = true
|
|
57
|
+
return externalHandler({ registeredVariables }).then(() => {
|
|
58
|
+
store.onPause = false
|
|
59
|
+
recorder.session.restore('pause')
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
52
63
|
if (!next) {
|
|
53
64
|
let vars = Object.keys(registeredVariables).join(', ')
|
|
54
65
|
if (vars) vars = `(vars: ${vars})`
|
|
@@ -234,5 +245,28 @@ function registerVariable(name, value) {
|
|
|
234
245
|
registeredVariables[name] = value
|
|
235
246
|
}
|
|
236
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Hook for external pause drivers (e.g. the MCP server). When set, pauseSession
|
|
250
|
+
* delegates to the handler instead of opening a readline REPL. The handler
|
|
251
|
+
* receives `{ registeredVariables }` and returns a Promise that resolves when
|
|
252
|
+
* the driver decides to continue (resume) or step.
|
|
253
|
+
*
|
|
254
|
+
* The driver controls step-vs-resume by mutating `next` via setNextStep before
|
|
255
|
+
* resolving its Promise.
|
|
256
|
+
*/
|
|
257
|
+
function setPauseHandler(handler) {
|
|
258
|
+
externalHandler = handler
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Trigger a one-shot pause from outside the test (e.g. the MCP server,
|
|
263
|
+
* pausing the test at a specific step index without modifying the test).
|
|
264
|
+
* Schedules pauseSession through the recorder so it slots between steps.
|
|
265
|
+
*/
|
|
266
|
+
function pauseNow(passedObject = {}) {
|
|
267
|
+
if (store.dryRun) return
|
|
268
|
+
recorder.add('Triggered pause', () => pauseSession(passedObject))
|
|
269
|
+
}
|
|
270
|
+
|
|
237
271
|
export default pause
|
|
238
|
-
export { registerVariable }
|
|
272
|
+
export { registerVariable, setPauseHandler, pauseNow }
|
package/lib/plugin/aiTrace.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import crypto from 'crypto'
|
|
2
1
|
import fs from 'fs'
|
|
3
2
|
import { mkdirp } from 'mkdirp'
|
|
4
3
|
import path from 'path'
|
|
5
|
-
import { fileURLToPath } from 'url'
|
|
6
4
|
|
|
7
5
|
import store from '../store.js'
|
|
8
6
|
import Container from '../container.js'
|
|
@@ -10,11 +8,16 @@ import recorder from '../recorder.js'
|
|
|
10
8
|
import event from '../event.js'
|
|
11
9
|
import output from '../output.js'
|
|
12
10
|
import { deleteDir, clearString } from '../utils.js'
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
import { captureSnapshot, pickActingHelper, traceDirFor, artifactLinks } from '../utils/trace.js'
|
|
12
|
+
import {
|
|
13
|
+
parsePluginArgs,
|
|
14
|
+
resolveTrigger,
|
|
15
|
+
matchStepFile,
|
|
16
|
+
matchUrl,
|
|
17
|
+
} from '../utils/pluginParser.js'
|
|
16
18
|
|
|
17
19
|
const defaultConfig = {
|
|
20
|
+
on: 'step',
|
|
18
21
|
deleteSuccessful: false,
|
|
19
22
|
fullPageScreenshots: false,
|
|
20
23
|
output: store.outputDir,
|
|
@@ -53,20 +56,26 @@ const defaultConfig = {
|
|
|
53
56
|
* * `captureHTTP`: capture HTTP requests (requires `trace` or `recordHar` enabled in helper config). Default: true.
|
|
54
57
|
* * `captureDebugOutput`: capture CodeceptJS debug output. Default: true.
|
|
55
58
|
* * `ignoreSteps`: steps to ignore in trace. Array of RegExps is expected.
|
|
59
|
+
* * `on`: trigger mode — `step` (default), `fail`, `test`, `file`, `url`.
|
|
60
|
+
*
|
|
61
|
+
* #### `on=` modes
|
|
62
|
+
*
|
|
63
|
+
* * **step** — persist every step (default)
|
|
64
|
+
* * **fail** — persist only the failed step
|
|
65
|
+
* * **test** — persist only the last step of each test
|
|
66
|
+
* * **file** — persist steps from `path=...[;line=...]`
|
|
67
|
+
* * **url** — persist when the current URL matches `pattern=...`
|
|
56
68
|
*
|
|
57
69
|
* @param {*} config
|
|
58
70
|
*/
|
|
59
|
-
export default function (config) {
|
|
60
|
-
const
|
|
61
|
-
|
|
71
|
+
export default function (config = {}) {
|
|
72
|
+
const cliArgs = parsePluginArgs(config._args)
|
|
73
|
+
const trigger = resolveTrigger(cliArgs, config, { on: defaultConfig.on }, { name: 'aiTrace' })
|
|
74
|
+
if (!trigger) return
|
|
62
75
|
|
|
63
76
|
config = Object.assign(defaultConfig, config)
|
|
64
77
|
|
|
65
|
-
|
|
66
|
-
if (Object.keys(helpers).indexOf(helperName) > -1) {
|
|
67
|
-
helper = helpers[helperName]
|
|
68
|
-
}
|
|
69
|
-
}
|
|
78
|
+
const helper = pickActingHelper(Container.helpers())
|
|
70
79
|
|
|
71
80
|
if (!helper) {
|
|
72
81
|
output.warn('aiTrace plugin: No supported helper found (Playwright, Puppeteer, WebDriver). Plugin disabled.')
|
|
@@ -106,13 +115,7 @@ export default function (config) {
|
|
|
106
115
|
} catch (err) {
|
|
107
116
|
title = test.title
|
|
108
117
|
}
|
|
109
|
-
|
|
110
|
-
const uniqueHash = crypto
|
|
111
|
-
.createHash('sha256')
|
|
112
|
-
.update(test.file + test.title)
|
|
113
|
-
.digest('hex')
|
|
114
|
-
.slice(0, 8)
|
|
115
|
-
dir = path.join(reportDir, `trace_${testTitle}_${uniqueHash}`)
|
|
118
|
+
dir = traceDirFor(test.file, title, reportDir)
|
|
116
119
|
mkdirp.sync(dir)
|
|
117
120
|
deleteDir(dir)
|
|
118
121
|
mkdirp.sync(dir)
|
|
@@ -141,6 +144,24 @@ export default function (config) {
|
|
|
141
144
|
output.debug(`aiTrace: Skipping failed step "${step.toString()}" - already handled by step.failed event`)
|
|
142
145
|
return
|
|
143
146
|
}
|
|
147
|
+
|
|
148
|
+
// on= filtering
|
|
149
|
+
if (trigger.on === 'fail') return // failed steps handled by step.failed
|
|
150
|
+
if (trigger.on === 'file' && !matchStepFile(step, trigger.path, trigger.line)) return
|
|
151
|
+
if (trigger.on === 'url') {
|
|
152
|
+
recorder.add('aiTrace:url check', async () => {
|
|
153
|
+
try {
|
|
154
|
+
if (!helper.grabCurrentUrl) return
|
|
155
|
+
const url = await helper.grabCurrentUrl()
|
|
156
|
+
if (!matchUrl(url, trigger.pattern)) return
|
|
157
|
+
await persistStep(step)
|
|
158
|
+
} catch (err) {
|
|
159
|
+
output.debug(`aiTrace: Error in url-mode step persistence: ${err.message}`)
|
|
160
|
+
}
|
|
161
|
+
}, true)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
144
165
|
const stepPersistPromise = persistStep(step).catch(err => {
|
|
145
166
|
output.debug(`aiTrace: Error saving step: ${err.message}`)
|
|
146
167
|
})
|
|
@@ -282,6 +303,7 @@ export default function (config) {
|
|
|
282
303
|
output.debug(`aiTrace: Browser unavailable, partial artifact capture: ${err.message}`)
|
|
283
304
|
}
|
|
284
305
|
|
|
306
|
+
let preExistingScreenshot = false
|
|
285
307
|
if (step.artifacts?.screenshot) {
|
|
286
308
|
const screenshotPath = path.isAbsolute(step.artifacts.screenshot)
|
|
287
309
|
? step.artifacts.screenshot
|
|
@@ -289,6 +311,7 @@ export default function (config) {
|
|
|
289
311
|
const screenshotFile = path.basename(screenshotPath)
|
|
290
312
|
stepData.artifacts.screenshot = screenshotFile
|
|
291
313
|
step.artifacts.screenshot = screenshotPath
|
|
314
|
+
preExistingScreenshot = true
|
|
292
315
|
|
|
293
316
|
if (!fs.existsSync(screenshotPath)) {
|
|
294
317
|
try {
|
|
@@ -297,58 +320,31 @@ export default function (config) {
|
|
|
297
320
|
output.debug(`aiTrace: Could not save screenshot: ${err.message}`)
|
|
298
321
|
}
|
|
299
322
|
}
|
|
300
|
-
} else {
|
|
301
|
-
try {
|
|
302
|
-
const screenshotFile = `${stepPrefix}_screenshot.png`
|
|
303
|
-
const screenshotPath = path.join(dir, screenshotFile)
|
|
304
|
-
await helper.saveScreenshot(screenshotPath, config.fullPageScreenshots)
|
|
305
|
-
|
|
306
|
-
stepData.artifacts.screenshot = screenshotFile
|
|
307
|
-
step.artifacts.screenshot = screenshotPath
|
|
308
|
-
} catch (err) {
|
|
309
|
-
output.debug(`aiTrace: Could not save screenshot: ${err.message}`)
|
|
310
|
-
}
|
|
311
323
|
}
|
|
312
324
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
output.debug(`aiTrace: Could not capture HTML: ${err.message}`)
|
|
323
|
-
}
|
|
324
|
-
} else {
|
|
325
|
-
stepData.artifacts.html = step.artifacts.html
|
|
326
|
-
}
|
|
327
|
-
}
|
|
325
|
+
const captured = await captureSnapshot(helper, {
|
|
326
|
+
dir,
|
|
327
|
+
prefix: stepPrefix,
|
|
328
|
+
fullPage: config.fullPageScreenshots,
|
|
329
|
+
captureHTML: config.captureHTML && browserAvailable,
|
|
330
|
+
captureARIA: config.captureARIA && browserAvailable,
|
|
331
|
+
captureBrowserLogs: config.captureBrowserLogs && browserAvailable,
|
|
332
|
+
captureStorage: false,
|
|
333
|
+
})
|
|
328
334
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const aria = await helper.grabAriaSnapshot()
|
|
333
|
-
const ariaFile = `${stepPrefix}_aria.txt`
|
|
334
|
-
fs.writeFileSync(path.join(dir, ariaFile), aria)
|
|
335
|
-
stepData.artifacts.aria = ariaFile
|
|
336
|
-
} catch (err) {
|
|
337
|
-
output.debug(`aiTrace: Could not capture ARIA snapshot: ${err.message}`)
|
|
338
|
-
}
|
|
335
|
+
if (!preExistingScreenshot && captured.screenshot) {
|
|
336
|
+
stepData.artifacts.screenshot = captured.screenshot
|
|
337
|
+
step.artifacts.screenshot = path.join(dir, captured.screenshot)
|
|
339
338
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
} catch (err) {
|
|
350
|
-
output.debug(`aiTrace: Could not capture browser logs: ${err.message}`)
|
|
351
|
-
}
|
|
339
|
+
if (step.artifacts?.html) {
|
|
340
|
+
stepData.artifacts.html = step.artifacts.html
|
|
341
|
+
} else if (captured.html) {
|
|
342
|
+
stepData.artifacts.html = captured.html
|
|
343
|
+
}
|
|
344
|
+
if (captured.aria) stepData.artifacts.aria = captured.aria
|
|
345
|
+
if (captured.console) {
|
|
346
|
+
stepData.artifacts.console = captured.console
|
|
347
|
+
stepData.meta.consoleCount = captured.consoleCount
|
|
352
348
|
}
|
|
353
349
|
} catch (err) {
|
|
354
350
|
output.plugin(`aiTrace: Can't save step artifacts: ${err}`)
|
|
@@ -361,6 +357,12 @@ export default function (config) {
|
|
|
361
357
|
return
|
|
362
358
|
}
|
|
363
359
|
|
|
360
|
+
// on=test: only render the last step in markdown; artifacts of earlier steps
|
|
361
|
+
// remain on disk unreferenced.
|
|
362
|
+
if (trigger.on === 'test') {
|
|
363
|
+
steps = steps.slice(-1)
|
|
364
|
+
}
|
|
365
|
+
|
|
364
366
|
const testDuration = ((Date.now() - testStartTime) / 1000).toFixed(2)
|
|
365
367
|
|
|
366
368
|
let markdown = `file: ${test.file || 'unknown'}\n`
|
|
@@ -405,22 +407,8 @@ export default function (config) {
|
|
|
405
407
|
})
|
|
406
408
|
}
|
|
407
409
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (stepData.artifacts.aria) {
|
|
413
|
-
markdown += ` > [ARIA Snapshot](./${stepData.artifacts.aria})\n`
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
if (stepData.artifacts.screenshot) {
|
|
417
|
-
markdown += ` > [Screenshot](./${stepData.artifacts.screenshot})\n`
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (stepData.artifacts.console) {
|
|
421
|
-
const count = stepData.meta.consoleCount || 0
|
|
422
|
-
markdown += ` > [Browser Logs](./${stepData.artifacts.console}) (${count} entries)\n`
|
|
423
|
-
}
|
|
410
|
+
const links = artifactLinks(stepData.artifacts, { consoleCount: stepData.meta.consoleCount })
|
|
411
|
+
if (links) markdown += links + '\n'
|
|
424
412
|
|
|
425
413
|
if (config.captureHTTP) {
|
|
426
414
|
if (test.artifacts && test.artifacts.har) {
|