codeceptjs 4.0.0-rc.9 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -10
- package/bin/codecept.js +15 -2
- package/bin/codeceptq.js +49 -0
- package/bin/mcp-server.js +751 -172
- package/docs/advanced.md +201 -0
- package/docs/agents.md +181 -0
- package/docs/ai.md +489 -0
- package/docs/aitrace.md +266 -0
- package/docs/api.md +332 -0
- package/docs/architecture.md +235 -0
- package/docs/assertions.md +415 -0
- package/docs/auth.md +318 -0
- package/docs/basics.md +424 -0
- package/docs/bdd.md +539 -0
- package/docs/best.md +240 -0
- package/docs/bootstrap.md +132 -0
- package/docs/commands.md +352 -0
- package/docs/community-helpers.md +63 -0
- package/docs/configuration.md +185 -0
- package/docs/continuous-integration.md +431 -0
- package/docs/custom-helpers.md +297 -0
- package/docs/data.md +448 -0
- package/docs/debugging.md +332 -0
- package/docs/detox.md +235 -0
- package/docs/docker.md +107 -0
- package/docs/effects.md +179 -0
- package/docs/element-based-testing.md +295 -0
- package/docs/element-selection.md +125 -0
- package/docs/els.md +328 -0
- package/docs/environment-variables.md +131 -0
- package/docs/examples.md +160 -0
- package/docs/heal.md +213 -0
- package/docs/helpers/ApiDataFactory.md +267 -0
- package/docs/helpers/Appium.md +1419 -0
- package/docs/helpers/Detox.md +665 -0
- package/docs/helpers/ExpectHelper.md +275 -0
- package/docs/helpers/FileSystem.md +152 -0
- package/docs/helpers/GraphQL.md +152 -0
- package/docs/helpers/GraphQLDataFactory.md +226 -0
- package/docs/helpers/JSONResponse.md +255 -0
- package/docs/helpers/MockRequest.md +377 -0
- package/docs/helpers/Playwright.md +2970 -0
- package/docs/helpers/Puppeteer-firefox.md +86 -0
- package/docs/helpers/Puppeteer.md +2583 -0
- package/docs/helpers/REST.md +289 -0
- package/docs/helpers/WebDriver.md +2639 -0
- package/docs/hooks.md +148 -0
- package/docs/index.md +111 -0
- package/docs/installation.md +121 -0
- package/docs/internal-test-server.md +89 -0
- package/docs/locators.md +355 -0
- package/docs/mcp.md +485 -0
- package/docs/migrate-from-cypress.md +98 -0
- package/docs/migrate-from-java.md +108 -0
- package/docs/migrate-from-protractor.md +101 -0
- package/docs/migrate-from-testcafe.md +99 -0
- package/docs/migration-4.md +743 -0
- package/docs/mobile.md +338 -0
- package/docs/pageobjects.md +399 -0
- package/docs/parallel.md +187 -0
- package/docs/playwright.md +714 -0
- package/docs/plugins/aiTrace.md +49 -0
- package/docs/plugins/analyze.md +66 -0
- package/docs/plugins/auth.md +241 -0
- package/docs/plugins/autoDelay.md +48 -0
- package/docs/plugins/browser.md +41 -0
- package/docs/plugins/coverage.md +39 -0
- package/docs/plugins/customLocator.md +119 -0
- package/docs/plugins/customReporter.md +16 -0
- package/docs/plugins/expose.md +75 -0
- package/docs/plugins/heal.md +44 -0
- package/docs/plugins/junitReporter.md +51 -0
- package/docs/plugins/pageInfo.md +34 -0
- package/docs/plugins/pause.md +43 -0
- package/docs/plugins/pauseOnFail.md +18 -0
- package/docs/plugins/retryFailedStep.md +75 -0
- package/docs/plugins/screencast.md +55 -0
- package/docs/plugins/screenshot.md +58 -0
- package/docs/plugins/screenshotOnFail.md +18 -0
- package/docs/plugins/stepTimeout.md +65 -0
- package/docs/plugins.md +87 -0
- package/docs/puppeteer.md +314 -0
- package/docs/quickstart.md +120 -0
- package/docs/reports.md +198 -0
- package/docs/retry.md +311 -0
- package/docs/secrets.md +150 -0
- package/docs/sessions.md +80 -0
- package/docs/shadow.md +68 -0
- package/docs/store.md +94 -0
- package/docs/test-structure.md +275 -0
- package/docs/timeouts.md +183 -0
- package/docs/translation.md +247 -0
- package/docs/tutorial.md +323 -0
- package/docs/typescript.md +159 -0
- package/docs/web-element.md +251 -0
- package/docs/webdriver.md +641 -0
- package/docs/within.md +55 -0
- package/lib/actor.js +1 -36
- package/lib/ai.js +3 -2
- package/lib/aria.js +260 -0
- package/lib/assertions.js +18 -0
- package/lib/codecept.js +7 -7
- package/lib/command/check.js +2 -1
- package/lib/command/dryRun.js +24 -5
- package/lib/command/generate.js +2 -0
- package/lib/command/gherkin/snippets.js +5 -4
- package/lib/command/init.js +248 -266
- package/lib/command/list.js +150 -10
- package/lib/command/query.js +218 -0
- package/lib/command/run-multiple.js +3 -2
- package/lib/command/run-workers.js +1 -14
- package/lib/command/run.js +3 -17
- package/lib/command/utils.js +14 -0
- package/lib/command/workers/runTests.js +11 -15
- package/lib/config.js +77 -4
- package/lib/container.js +97 -15
- package/lib/effects.js +17 -0
- package/lib/element/WebElement.js +194 -2
- package/lib/els.js +12 -6
- package/lib/globals.js +32 -19
- package/lib/heal.js +7 -4
- package/lib/helper/ApiDataFactory.js +2 -1
- package/lib/helper/FileSystem.js +3 -2
- package/lib/helper/GraphQLDataFactory.js +2 -1
- package/lib/helper/Playwright.js +63 -70
- package/lib/helper/Puppeteer.js +20 -109
- package/lib/helper/WebDriver.js +13 -30
- package/lib/helper/errors/NonFocusedType.js +8 -0
- package/lib/helper/extras/Download.js +45 -0
- package/lib/helper/extras/PlaywrightLocator.js +10 -0
- package/lib/helper/extras/elementSelection.js +10 -3
- package/lib/helper/extras/focusCheck.js +43 -0
- package/lib/helper/extras/richTextEditor.js +178 -0
- package/lib/history.js +3 -2
- package/lib/html.js +90 -16
- package/lib/index.js +9 -1
- package/lib/listener/config.js +6 -4
- package/lib/listener/emptyRun.js +2 -1
- package/lib/listener/helpers.js +4 -1
- package/lib/listener/mocha.js +2 -1
- package/lib/listener/pageobjects.js +43 -0
- package/lib/listener/result.js +3 -2
- package/lib/locator.js +126 -16
- package/lib/mocha/cli.js +4 -2
- package/lib/mocha/factory.js +7 -2
- package/lib/mocha/inject.js +1 -1
- package/lib/mocha/scenarioConfig.js +2 -1
- package/lib/mocha/ui.js +5 -6
- package/lib/parser.js +2 -2
- package/lib/pause.js +38 -4
- package/lib/plugin/aiTrace.js +96 -103
- package/lib/plugin/analyze.js +9 -9
- package/lib/plugin/auth.js +3 -3
- package/lib/plugin/browser.js +77 -0
- package/lib/plugin/expose.js +159 -0
- package/lib/plugin/heal.js +47 -3
- package/lib/plugin/junitReporter.js +303 -0
- package/lib/plugin/pageInfo.js +54 -52
- package/lib/plugin/pause.js +131 -0
- package/lib/plugin/pauseOnFail.js +11 -33
- package/lib/plugin/retryFailedStep.js +15 -13
- package/lib/plugin/screencast.js +289 -0
- package/lib/plugin/screenshot.js +558 -0
- package/lib/plugin/screenshotOnFail.js +9 -170
- package/lib/plugin/stepTimeout.js +3 -2
- package/lib/recorder.js +1 -1
- package/lib/rerun.js +2 -1
- package/lib/result.js +2 -1
- package/lib/step/base.js +10 -9
- package/lib/step/comment.js +2 -2
- package/lib/step/config.js +7 -0
- package/lib/step/helper.js +4 -4
- package/lib/step/meta.js +3 -3
- package/lib/step/record.js +5 -5
- package/lib/store.js +72 -3
- package/lib/translation.js +2 -1
- package/lib/utils/mask_data.js +2 -1
- package/lib/utils/pluginParser.js +151 -0
- package/lib/utils/trace.js +297 -0
- package/lib/utils.js +29 -3
- package/lib/workers.js +14 -22
- package/package.json +17 -14
- package/typings/index.d.ts +0 -5
- package/docs/webapi/amOnPage.mustache +0 -11
- package/docs/webapi/appendField.mustache +0 -16
- package/docs/webapi/attachFile.mustache +0 -24
- package/docs/webapi/blur.mustache +0 -18
- package/docs/webapi/checkOption.mustache +0 -13
- package/docs/webapi/clearCookie.mustache +0 -9
- package/docs/webapi/clearField.mustache +0 -14
- package/docs/webapi/click.mustache +0 -29
- package/docs/webapi/clickLink.mustache +0 -8
- package/docs/webapi/closeCurrentTab.mustache +0 -7
- package/docs/webapi/closeOtherTabs.mustache +0 -8
- package/docs/webapi/dontSee.mustache +0 -11
- package/docs/webapi/dontSeeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/dontSeeCookie.mustache +0 -8
- package/docs/webapi/dontSeeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/dontSeeCurrentUrlEquals.mustache +0 -10
- package/docs/webapi/dontSeeElement.mustache +0 -12
- package/docs/webapi/dontSeeElementInDOM.mustache +0 -8
- package/docs/webapi/dontSeeInCurrentUrl.mustache +0 -4
- package/docs/webapi/dontSeeInField.mustache +0 -16
- package/docs/webapi/dontSeeInSource.mustache +0 -8
- package/docs/webapi/dontSeeInTitle.mustache +0 -8
- package/docs/webapi/dontSeeTraffic.mustache +0 -13
- package/docs/webapi/doubleClick.mustache +0 -13
- package/docs/webapi/downloadFile.mustache +0 -12
- package/docs/webapi/dragAndDrop.mustache +0 -9
- package/docs/webapi/dragSlider.mustache +0 -11
- package/docs/webapi/executeAsyncScript.mustache +0 -24
- package/docs/webapi/executeScript.mustache +0 -26
- package/docs/webapi/fillField.mustache +0 -21
- package/docs/webapi/flushNetworkTraffics.mustache +0 -5
- package/docs/webapi/focus.mustache +0 -13
- package/docs/webapi/forceClick.mustache +0 -28
- package/docs/webapi/forceRightClick.mustache +0 -18
- package/docs/webapi/grabAllWindowHandles.mustache +0 -7
- package/docs/webapi/grabAttributeFrom.mustache +0 -10
- package/docs/webapi/grabAttributeFromAll.mustache +0 -9
- package/docs/webapi/grabBrowserLogs.mustache +0 -9
- package/docs/webapi/grabCookie.mustache +0 -11
- package/docs/webapi/grabCssPropertyFrom.mustache +0 -11
- package/docs/webapi/grabCssPropertyFromAll.mustache +0 -10
- package/docs/webapi/grabCurrentUrl.mustache +0 -9
- package/docs/webapi/grabCurrentWindowHandle.mustache +0 -6
- package/docs/webapi/grabDataFromPerformanceTiming.mustache +0 -20
- package/docs/webapi/grabElementBoundingRect.mustache +0 -20
- package/docs/webapi/grabGeoLocation.mustache +0 -8
- package/docs/webapi/grabHTMLFrom.mustache +0 -10
- package/docs/webapi/grabHTMLFromAll.mustache +0 -9
- package/docs/webapi/grabNumberOfOpenTabs.mustache +0 -8
- package/docs/webapi/grabNumberOfVisibleElements.mustache +0 -9
- package/docs/webapi/grabPageScrollPosition.mustache +0 -8
- package/docs/webapi/grabPopupText.mustache +0 -5
- package/docs/webapi/grabRecordedNetworkTraffics.mustache +0 -10
- package/docs/webapi/grabSource.mustache +0 -8
- package/docs/webapi/grabTextFrom.mustache +0 -10
- package/docs/webapi/grabTextFromAll.mustache +0 -9
- package/docs/webapi/grabTitle.mustache +0 -8
- package/docs/webapi/grabValueFrom.mustache +0 -9
- package/docs/webapi/grabValueFromAll.mustache +0 -8
- package/docs/webapi/grabWebElement.mustache +0 -9
- package/docs/webapi/grabWebElements.mustache +0 -9
- package/docs/webapi/moveCursorTo.mustache +0 -16
- package/docs/webapi/openNewTab.mustache +0 -7
- package/docs/webapi/pressKey.mustache +0 -12
- package/docs/webapi/pressKeyDown.mustache +0 -12
- package/docs/webapi/pressKeyUp.mustache +0 -12
- package/docs/webapi/pressKeyWithKeyNormalization.mustache +0 -60
- package/docs/webapi/refreshPage.mustache +0 -6
- package/docs/webapi/resizeWindow.mustache +0 -6
- package/docs/webapi/rightClick.mustache +0 -14
- package/docs/webapi/saveElementScreenshot.mustache +0 -10
- package/docs/webapi/saveScreenshot.mustache +0 -12
- package/docs/webapi/say.mustache +0 -10
- package/docs/webapi/scrollIntoView.mustache +0 -11
- package/docs/webapi/scrollPageToBottom.mustache +0 -6
- package/docs/webapi/scrollPageToTop.mustache +0 -6
- package/docs/webapi/scrollTo.mustache +0 -12
- package/docs/webapi/see.mustache +0 -11
- package/docs/webapi/seeAttributesOnElements.mustache +0 -9
- package/docs/webapi/seeCheckboxIsChecked.mustache +0 -10
- package/docs/webapi/seeCookie.mustache +0 -8
- package/docs/webapi/seeCssPropertiesOnElements.mustache +0 -9
- package/docs/webapi/seeCurrentPathEquals.mustache +0 -10
- package/docs/webapi/seeCurrentUrlEquals.mustache +0 -11
- package/docs/webapi/seeElement.mustache +0 -12
- package/docs/webapi/seeElementInDOM.mustache +0 -8
- package/docs/webapi/seeInCurrentUrl.mustache +0 -8
- package/docs/webapi/seeInField.mustache +0 -17
- package/docs/webapi/seeInPopup.mustache +0 -8
- package/docs/webapi/seeInSource.mustache +0 -7
- package/docs/webapi/seeInTitle.mustache +0 -8
- package/docs/webapi/seeNumberOfElements.mustache +0 -11
- package/docs/webapi/seeNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/seeTextEquals.mustache +0 -9
- package/docs/webapi/seeTitleEquals.mustache +0 -8
- package/docs/webapi/seeTraffic.mustache +0 -36
- package/docs/webapi/selectOption.mustache +0 -26
- package/docs/webapi/setCookie.mustache +0 -16
- package/docs/webapi/setGeoLocation.mustache +0 -12
- package/docs/webapi/startRecordingTraffic.mustache +0 -8
- package/docs/webapi/startRecordingWebSocketMessages.mustache +0 -8
- package/docs/webapi/stopRecordingTraffic.mustache +0 -5
- package/docs/webapi/stopRecordingWebSocketMessages.mustache +0 -7
- package/docs/webapi/switchTo.mustache +0 -9
- package/docs/webapi/switchToNextTab.mustache +0 -10
- package/docs/webapi/switchToPreviousTab.mustache +0 -10
- package/docs/webapi/type.mustache +0 -21
- package/docs/webapi/uncheckOption.mustache +0 -13
- package/docs/webapi/wait.mustache +0 -8
- package/docs/webapi/waitForClickable.mustache +0 -11
- package/docs/webapi/waitForCookie.mustache +0 -9
- package/docs/webapi/waitForDetached.mustache +0 -10
- package/docs/webapi/waitForDisabled.mustache +0 -6
- package/docs/webapi/waitForElement.mustache +0 -11
- package/docs/webapi/waitForEnabled.mustache +0 -6
- package/docs/webapi/waitForFunction.mustache +0 -17
- package/docs/webapi/waitForInvisible.mustache +0 -10
- package/docs/webapi/waitForNumberOfTabs.mustache +0 -9
- package/docs/webapi/waitForText.mustache +0 -13
- package/docs/webapi/waitForValue.mustache +0 -10
- package/docs/webapi/waitForVisible.mustache +0 -10
- package/docs/webapi/waitInUrl.mustache +0 -9
- package/docs/webapi/waitNumberOfVisibleElements.mustache +0 -10
- package/docs/webapi/waitToHide.mustache +0 -10
- package/docs/webapi/waitUrlEquals.mustache +0 -10
- package/lib/helper/AI.js +0 -214
- package/lib/helper/Mochawesome.js +0 -96
- package/lib/helper/extras/PlaywrightReactVueLocator.js +0 -52
- package/lib/helper/extras/React.js +0 -65
- package/lib/plugin/stepByStepReport.js +0 -431
- package/lib/plugin/subtitles.js +0 -89
package/lib/plugin/aiTrace.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
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
|
|
|
5
|
+
import store from '../store.js'
|
|
7
6
|
import Container from '../container.js'
|
|
8
7
|
import recorder from '../recorder.js'
|
|
9
8
|
import event from '../event.js'
|
|
10
9
|
import output from '../output.js'
|
|
11
10
|
import { deleteDir, clearString } from '../utils.js'
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
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'
|
|
15
18
|
|
|
16
19
|
const defaultConfig = {
|
|
20
|
+
on: 'step',
|
|
17
21
|
deleteSuccessful: false,
|
|
18
22
|
fullPageScreenshots: false,
|
|
19
|
-
output:
|
|
23
|
+
output: store.outputDir,
|
|
20
24
|
captureHTML: true,
|
|
21
25
|
captureARIA: true,
|
|
22
26
|
captureBrowserLogs: true,
|
|
@@ -52,20 +56,26 @@ const defaultConfig = {
|
|
|
52
56
|
* * `captureHTTP`: capture HTTP requests (requires `trace` or `recordHar` enabled in helper config). Default: true.
|
|
53
57
|
* * `captureDebugOutput`: capture CodeceptJS debug output. Default: true.
|
|
54
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=...`
|
|
55
68
|
*
|
|
56
69
|
* @param {*} config
|
|
57
70
|
*/
|
|
58
|
-
export default function (config) {
|
|
59
|
-
const
|
|
60
|
-
|
|
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
|
|
61
75
|
|
|
62
76
|
config = Object.assign(defaultConfig, config)
|
|
63
77
|
|
|
64
|
-
|
|
65
|
-
if (Object.keys(helpers).indexOf(helperName) > -1) {
|
|
66
|
-
helper = helpers[helperName]
|
|
67
|
-
}
|
|
68
|
-
}
|
|
78
|
+
const helper = pickActingHelper(Container.helpers())
|
|
69
79
|
|
|
70
80
|
if (!helper) {
|
|
71
81
|
output.warn('aiTrace plugin: No supported helper found (Playwright, Puppeteer, WebDriver). Plugin disabled.')
|
|
@@ -82,9 +92,10 @@ export default function (config) {
|
|
|
82
92
|
let testStartTime
|
|
83
93
|
let currentUrl = null
|
|
84
94
|
let testFailed = false
|
|
95
|
+
let pendingArtifactCapture = null
|
|
85
96
|
let firstFailedStepSaved = false
|
|
86
97
|
|
|
87
|
-
const reportDir = config.output ? path.resolve(
|
|
98
|
+
const reportDir = config.output ? path.resolve(store.codeceptDir, config.output) : defaultConfig.output
|
|
88
99
|
|
|
89
100
|
if (config.captureDebugOutput) {
|
|
90
101
|
const originalDebug = output.debug
|
|
@@ -105,13 +116,7 @@ export default function (config) {
|
|
|
105
116
|
} catch (err) {
|
|
106
117
|
title = test.title
|
|
107
118
|
}
|
|
108
|
-
|
|
109
|
-
const uniqueHash = crypto
|
|
110
|
-
.createHash('sha256')
|
|
111
|
-
.update(test.file + test.title)
|
|
112
|
-
.digest('hex')
|
|
113
|
-
.slice(0, 8)
|
|
114
|
-
dir = path.join(reportDir, `trace_${testTitle}_${uniqueHash}`)
|
|
119
|
+
dir = traceDirFor(test.file, title, reportDir)
|
|
115
120
|
mkdirp.sync(dir)
|
|
116
121
|
deleteDir(dir)
|
|
117
122
|
mkdirp.sync(dir)
|
|
@@ -125,6 +130,7 @@ export default function (config) {
|
|
|
125
130
|
currentUrl = null
|
|
126
131
|
testFailed = false
|
|
127
132
|
firstFailedStepSaved = false
|
|
133
|
+
pendingArtifactCapture = null
|
|
128
134
|
})
|
|
129
135
|
|
|
130
136
|
event.dispatcher.on(event.step.after, step => {
|
|
@@ -140,13 +146,30 @@ export default function (config) {
|
|
|
140
146
|
output.debug(`aiTrace: Skipping failed step "${step.toString()}" - already handled by step.failed event`)
|
|
141
147
|
return
|
|
142
148
|
}
|
|
143
|
-
|
|
149
|
+
|
|
150
|
+
// on= filtering
|
|
151
|
+
if (trigger.on === 'fail') return // failed steps handled by step.failed
|
|
152
|
+
if (trigger.on === 'file' && !matchStepFile(step, trigger.path, trigger.line)) return
|
|
153
|
+
if (trigger.on === 'url') {
|
|
154
|
+
recorder.add('aiTrace:url check', async () => {
|
|
155
|
+
try {
|
|
156
|
+
if (!helper.grabCurrentUrl) return
|
|
157
|
+
const url = await helper.grabCurrentUrl()
|
|
158
|
+
if (!matchUrl(url, trigger.pattern)) return
|
|
159
|
+
await persistStep(step)
|
|
160
|
+
} catch (err) {
|
|
161
|
+
output.debug(`aiTrace: Error in url-mode step persistence: ${err.message}`)
|
|
162
|
+
}
|
|
163
|
+
}, true)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
recorder.add(`aiTrace step persistence: ${step.toString()}`, () => persistStep(step).catch(err => {
|
|
144
168
|
output.debug(`aiTrace: Error saving step: ${err.message}`)
|
|
145
|
-
})
|
|
146
|
-
recorder.add(`wait aiTrace step persistence: ${step.toString()}`, () => stepPersistPromise, true)
|
|
169
|
+
}), true)
|
|
147
170
|
})
|
|
148
171
|
|
|
149
|
-
event.dispatcher.on(event.step.failed,
|
|
172
|
+
event.dispatcher.on(event.step.failed, step => {
|
|
150
173
|
if (!currentTest) return
|
|
151
174
|
if (step.status === 'queued' && testFailed) {
|
|
152
175
|
output.debug(`aiTrace: Skipping queued failed step "${step.toString()}" - testFailed: ${testFailed}`)
|
|
@@ -166,15 +189,13 @@ export default function (config) {
|
|
|
166
189
|
}
|
|
167
190
|
existingStep.status = 'failed'
|
|
168
191
|
|
|
169
|
-
|
|
170
|
-
await captureArtifactsForStep(step, existingStep, existingStep.prefix)
|
|
171
|
-
} catch (err) {
|
|
192
|
+
pendingArtifactCapture = captureArtifactsForStep(step, existingStep, existingStep.prefix).catch(err => {
|
|
172
193
|
output.debug(`aiTrace: Error updating failed step: ${err.message}`)
|
|
173
|
-
}
|
|
194
|
+
})
|
|
174
195
|
} else {
|
|
175
196
|
if (stepNum === -1) return
|
|
176
197
|
if (isStepIgnored(step)) return
|
|
177
|
-
if (step.metaStep && step.metaStep.
|
|
198
|
+
if (step.metaStep && step.metaStep.title === 'BeforeSuite') return
|
|
178
199
|
|
|
179
200
|
const stepPrefix = generateStepPrefix(step, stepNum)
|
|
180
201
|
stepNum++
|
|
@@ -196,11 +217,9 @@ export default function (config) {
|
|
|
196
217
|
steps.push(stepData)
|
|
197
218
|
firstFailedStepSaved = true
|
|
198
219
|
|
|
199
|
-
|
|
200
|
-
await captureArtifactsForStep(step, stepData, stepPrefix)
|
|
201
|
-
} catch (err) {
|
|
220
|
+
pendingArtifactCapture = captureArtifactsForStep(step, stepData, stepPrefix).catch(err => {
|
|
202
221
|
output.debug(`aiTrace: Error capturing failed step artifacts: ${err.message}`)
|
|
203
|
-
}
|
|
222
|
+
})
|
|
204
223
|
}
|
|
205
224
|
})
|
|
206
225
|
|
|
@@ -216,13 +235,19 @@ export default function (config) {
|
|
|
216
235
|
if (hookName === 'BeforeSuite' || hookName === 'AfterSuite') {
|
|
217
236
|
return
|
|
218
237
|
}
|
|
219
|
-
persist
|
|
238
|
+
recorder.add('aiTrace:persist failed', async () => {
|
|
239
|
+
if (pendingArtifactCapture) {
|
|
240
|
+
await pendingArtifactCapture
|
|
241
|
+
pendingArtifactCapture = null
|
|
242
|
+
}
|
|
243
|
+
persist(test, 'failed')
|
|
244
|
+
}, true)
|
|
220
245
|
})
|
|
221
246
|
|
|
222
247
|
async function persistStep(step) {
|
|
223
248
|
if (stepNum === -1) return
|
|
224
249
|
if (isStepIgnored(step)) return
|
|
225
|
-
if (step.metaStep && step.metaStep.
|
|
250
|
+
if (step.metaStep && step.metaStep.title === 'BeforeSuite') return
|
|
226
251
|
|
|
227
252
|
const stepKey = step.toString()
|
|
228
253
|
|
|
@@ -281,6 +306,7 @@ export default function (config) {
|
|
|
281
306
|
output.debug(`aiTrace: Browser unavailable, partial artifact capture: ${err.message}`)
|
|
282
307
|
}
|
|
283
308
|
|
|
309
|
+
let preExistingScreenshot = false
|
|
284
310
|
if (step.artifacts?.screenshot) {
|
|
285
311
|
const screenshotPath = path.isAbsolute(step.artifacts.screenshot)
|
|
286
312
|
? step.artifacts.screenshot
|
|
@@ -288,6 +314,7 @@ export default function (config) {
|
|
|
288
314
|
const screenshotFile = path.basename(screenshotPath)
|
|
289
315
|
stepData.artifacts.screenshot = screenshotFile
|
|
290
316
|
step.artifacts.screenshot = screenshotPath
|
|
317
|
+
preExistingScreenshot = true
|
|
291
318
|
|
|
292
319
|
if (!fs.existsSync(screenshotPath)) {
|
|
293
320
|
try {
|
|
@@ -296,58 +323,31 @@ export default function (config) {
|
|
|
296
323
|
output.debug(`aiTrace: Could not save screenshot: ${err.message}`)
|
|
297
324
|
}
|
|
298
325
|
}
|
|
299
|
-
} else {
|
|
300
|
-
try {
|
|
301
|
-
const screenshotFile = `${stepPrefix}_screenshot.png`
|
|
302
|
-
const screenshotPath = path.join(dir, screenshotFile)
|
|
303
|
-
await helper.saveScreenshot(screenshotPath, config.fullPageScreenshots)
|
|
304
|
-
|
|
305
|
-
stepData.artifacts.screenshot = screenshotFile
|
|
306
|
-
step.artifacts.screenshot = screenshotPath
|
|
307
|
-
} catch (err) {
|
|
308
|
-
output.debug(`aiTrace: Could not save screenshot: ${err.message}`)
|
|
309
|
-
}
|
|
310
326
|
}
|
|
311
327
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
output.debug(`aiTrace: Could not capture HTML: ${err.message}`)
|
|
322
|
-
}
|
|
323
|
-
} else {
|
|
324
|
-
stepData.artifacts.html = step.artifacts.html
|
|
325
|
-
}
|
|
326
|
-
}
|
|
328
|
+
const captured = await captureSnapshot(helper, {
|
|
329
|
+
dir,
|
|
330
|
+
prefix: stepPrefix,
|
|
331
|
+
fullPage: config.fullPageScreenshots,
|
|
332
|
+
captureHTML: config.captureHTML && browserAvailable,
|
|
333
|
+
captureARIA: config.captureARIA && browserAvailable,
|
|
334
|
+
captureBrowserLogs: config.captureBrowserLogs && browserAvailable,
|
|
335
|
+
captureStorage: false,
|
|
336
|
+
})
|
|
327
337
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const aria = await helper.grabAriaSnapshot()
|
|
332
|
-
const ariaFile = `${stepPrefix}_aria.txt`
|
|
333
|
-
fs.writeFileSync(path.join(dir, ariaFile), aria)
|
|
334
|
-
stepData.artifacts.aria = ariaFile
|
|
335
|
-
} catch (err) {
|
|
336
|
-
output.debug(`aiTrace: Could not capture ARIA snapshot: ${err.message}`)
|
|
337
|
-
}
|
|
338
|
+
if (!preExistingScreenshot && captured.screenshot) {
|
|
339
|
+
stepData.artifacts.screenshot = captured.screenshot
|
|
340
|
+
step.artifacts.screenshot = path.join(dir, captured.screenshot)
|
|
338
341
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
} catch (err) {
|
|
349
|
-
output.debug(`aiTrace: Could not capture browser logs: ${err.message}`)
|
|
350
|
-
}
|
|
342
|
+
if (step.artifacts?.html) {
|
|
343
|
+
stepData.artifacts.html = step.artifacts.html
|
|
344
|
+
} else if (captured.html) {
|
|
345
|
+
stepData.artifacts.html = captured.html
|
|
346
|
+
}
|
|
347
|
+
if (captured.aria) stepData.artifacts.aria = captured.aria
|
|
348
|
+
if (captured.console) {
|
|
349
|
+
stepData.artifacts.console = captured.console
|
|
350
|
+
stepData.meta.consoleCount = captured.consoleCount
|
|
351
351
|
}
|
|
352
352
|
} catch (err) {
|
|
353
353
|
output.plugin(`aiTrace: Can't save step artifacts: ${err}`)
|
|
@@ -360,6 +360,12 @@ export default function (config) {
|
|
|
360
360
|
return
|
|
361
361
|
}
|
|
362
362
|
|
|
363
|
+
// on=test: only render the last step in markdown; artifacts of earlier steps
|
|
364
|
+
// remain on disk unreferenced.
|
|
365
|
+
if (trigger.on === 'test') {
|
|
366
|
+
steps = steps.slice(-1)
|
|
367
|
+
}
|
|
368
|
+
|
|
363
369
|
const testDuration = ((Date.now() - testStartTime) / 1000).toFixed(2)
|
|
364
370
|
|
|
365
371
|
let markdown = `file: ${test.file || 'unknown'}\n`
|
|
@@ -404,22 +410,8 @@ export default function (config) {
|
|
|
404
410
|
})
|
|
405
411
|
}
|
|
406
412
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
if (stepData.artifacts.aria) {
|
|
412
|
-
markdown += ` > [ARIA Snapshot](./${stepData.artifacts.aria})\n`
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if (stepData.artifacts.screenshot) {
|
|
416
|
-
markdown += ` > [Screenshot](./${stepData.artifacts.screenshot})\n`
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (stepData.artifacts.console) {
|
|
420
|
-
const count = stepData.meta.consoleCount || 0
|
|
421
|
-
markdown += ` > [Browser Logs](./${stepData.artifacts.console}) (${count} entries)\n`
|
|
422
|
-
}
|
|
413
|
+
const links = artifactLinks(stepData.artifacts, { consoleCount: stepData.meta.consoleCount })
|
|
414
|
+
if (links) markdown += links + '\n'
|
|
423
415
|
|
|
424
416
|
if (config.captureHTTP) {
|
|
425
417
|
if (test.artifacts && test.artifacts.har) {
|
|
@@ -437,7 +429,7 @@ export default function (config) {
|
|
|
437
429
|
const traceFile = path.join(dir, 'trace.md')
|
|
438
430
|
fs.writeFileSync(traceFile, markdown)
|
|
439
431
|
|
|
440
|
-
output.print(
|
|
432
|
+
output.print(`Trace Saved: file://${traceFile}`)
|
|
441
433
|
|
|
442
434
|
if (!test.artifacts) test.artifacts = {}
|
|
443
435
|
test.artifacts.aiTrace = traceFile
|
|
@@ -445,8 +437,9 @@ export default function (config) {
|
|
|
445
437
|
|
|
446
438
|
function isStepIgnored(step) {
|
|
447
439
|
if (!config.ignoreSteps) return false
|
|
440
|
+
if (!step.title) return false
|
|
448
441
|
for (const pattern of config.ignoreSteps || []) {
|
|
449
|
-
if (step.
|
|
442
|
+
if (step.title.match(pattern)) return true
|
|
450
443
|
}
|
|
451
444
|
return false
|
|
452
445
|
}
|
package/lib/plugin/analyze.js
CHANGED
|
@@ -12,6 +12,7 @@ const ai = aiModule.default || aiModule
|
|
|
12
12
|
import colors from 'chalk'
|
|
13
13
|
import ora from 'ora'
|
|
14
14
|
import event from '../event.js'
|
|
15
|
+
import recorder from '../recorder.js'
|
|
15
16
|
|
|
16
17
|
import output from '../output.js'
|
|
17
18
|
|
|
@@ -154,10 +155,9 @@ const defaultConfig = {
|
|
|
154
155
|
if (config.vision && test.artifacts.screenshot) {
|
|
155
156
|
debug('Adding screenshot to prompt')
|
|
156
157
|
messages[0].content.push({
|
|
157
|
-
type: '
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
},
|
|
158
|
+
type: 'image',
|
|
159
|
+
image: base64EncodeFile(test.artifacts.screenshot),
|
|
160
|
+
mediaType: 'image/png',
|
|
161
161
|
})
|
|
162
162
|
}
|
|
163
163
|
|
|
@@ -227,14 +227,14 @@ export default function (config = {}) {
|
|
|
227
227
|
console.log('Enabled AI analysis')
|
|
228
228
|
})
|
|
229
229
|
|
|
230
|
-
event.dispatcher.on(event.all.result,
|
|
230
|
+
event.dispatcher.on(event.all.result, result => {
|
|
231
231
|
if (!isMainThread) return // run only on main thread
|
|
232
232
|
if (!ai.isEnabled) {
|
|
233
233
|
console.log('AI is disabled, no analysis will be performed. Run tests with --ai flag to enable it.')
|
|
234
234
|
return
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
printReport(result)
|
|
237
|
+
recorder.add('analyze:print-ai-report', () => printReport(result), true)
|
|
238
238
|
})
|
|
239
239
|
|
|
240
240
|
event.dispatcher.on(event.workers.result, async result => {
|
|
@@ -248,7 +248,7 @@ export default function (config = {}) {
|
|
|
248
248
|
return
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
printReport(result)
|
|
251
|
+
await printReport(result)
|
|
252
252
|
})
|
|
253
253
|
|
|
254
254
|
async function printReport(result) {
|
|
@@ -294,7 +294,7 @@ export default function (config = {}) {
|
|
|
294
294
|
console.error('Error analyzing failed tests', err)
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
-
if (!Object.keys(
|
|
297
|
+
if (!Object.keys(Container.plugins()).includes('pageInfo')) {
|
|
298
298
|
console.log('To improve analysis, enable pageInfo plugin to get more context for failed tests.')
|
|
299
299
|
}
|
|
300
300
|
}
|
|
@@ -353,7 +353,7 @@ function serializeError(error) {
|
|
|
353
353
|
errorMessage +=
|
|
354
354
|
'\n' +
|
|
355
355
|
error.stack
|
|
356
|
-
.replace(
|
|
356
|
+
.replace(store.codeceptDir || '', '.')
|
|
357
357
|
.split('\n')
|
|
358
358
|
.map(line => line.replace(ansiRegExp(), ''))
|
|
359
359
|
.slice(0, 5)
|
package/lib/plugin/auth.js
CHANGED
|
@@ -321,7 +321,7 @@ export default function (config) {
|
|
|
321
321
|
}
|
|
322
322
|
if (config.saveToFile) {
|
|
323
323
|
output.debug(`Saved user session into file for ${name}`)
|
|
324
|
-
fs.writeFileSync(path.join(
|
|
324
|
+
fs.writeFileSync(path.join(store.outputDir, `${name}_session.json`), JSON.stringify(cookies))
|
|
325
325
|
}
|
|
326
326
|
store[`${name}_session`] = cookies
|
|
327
327
|
}
|
|
@@ -377,7 +377,7 @@ export default function (config) {
|
|
|
377
377
|
}
|
|
378
378
|
|
|
379
379
|
if (!config.saveToFile) return
|
|
380
|
-
const cookieFile = path.join(
|
|
380
|
+
const cookieFile = path.join(store.outputDir, `${name}_session.json`)
|
|
381
381
|
|
|
382
382
|
if (!fileExists(cookieFile)) {
|
|
383
383
|
return
|
|
@@ -412,7 +412,7 @@ export default function (config) {
|
|
|
412
412
|
|
|
413
413
|
function loadCookiesFromFile(config) {
|
|
414
414
|
for (const name in config.users) {
|
|
415
|
-
const fileName = path.join(
|
|
415
|
+
const fileName = path.join(store.outputDir, `${name}_session.json`)
|
|
416
416
|
if (!fileExists(fileName)) continue
|
|
417
417
|
const data = fs.readFileSync(fileName).toString()
|
|
418
418
|
try {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import output from '../output.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Overrides browser helper config from the command line. Works for all browser helpers
|
|
5
|
+
* (Playwright, Puppeteer, WebDriver, Appium) without touching `codecept.conf`.
|
|
6
|
+
*
|
|
7
|
+
* Enable it via `-p` option with one or more colon-chained args:
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* npx codeceptjs run -p browser:show
|
|
11
|
+
* npx codeceptjs run -p browser:hide
|
|
12
|
+
* npx codeceptjs run -p browser:browser=firefox
|
|
13
|
+
* npx codeceptjs run -p browser:windowSize=1024x768:video=false
|
|
14
|
+
* npx codeceptjs run -p browser:hide:browser=webkit:windowSize=800x600
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* #### Args
|
|
18
|
+
*
|
|
19
|
+
* * **show** — force visible browser
|
|
20
|
+
* * **hide** — force headless (also injects `--headless` into WebDriver chrome/firefox capability args)
|
|
21
|
+
* * **`<key>=<value>`** — set `helpers.<eachBrowserHelper>.<key> = <value>`. Three keys
|
|
22
|
+
* get per-helper translation via `setBrowserConfig`:
|
|
23
|
+
* * `browser=<name>` — Puppeteer receives `product`, Playwright receives `browser`
|
|
24
|
+
* * `windowSize=WxH` — also adds `--window-size=W,H` chromium/chrome args
|
|
25
|
+
* * `show=true|false` — toggles `show` on Playwright/Puppeteer; injects/strips
|
|
26
|
+
* `--headless` in WebDriver chrome/firefox capability args
|
|
27
|
+
*
|
|
28
|
+
* Values stay as strings. `true` / `false` are coerced to booleans.
|
|
29
|
+
*
|
|
30
|
+
* Requires `@codeceptjs/configure` to be installed; if missing, the plugin
|
|
31
|
+
* logs a hint and skips the override.
|
|
32
|
+
*/
|
|
33
|
+
export default async function (config = {}) {
|
|
34
|
+
const { _args, enabled, ...rest } = config
|
|
35
|
+
const opts = { ...rest, ...parseArgs(_args || []) }
|
|
36
|
+
if (Object.keys(opts).length === 0) return
|
|
37
|
+
|
|
38
|
+
const configure = await tryImportConfigure()
|
|
39
|
+
if (!configure) return
|
|
40
|
+
|
|
41
|
+
configure.setBrowserConfig(opts)
|
|
42
|
+
output.debug(`browser plugin: applied ${formatOpts(opts)}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function tryImportConfigure() {
|
|
46
|
+
try {
|
|
47
|
+
return await import('@codeceptjs/configure')
|
|
48
|
+
} catch (err) {
|
|
49
|
+
output.error("browser plugin: '@codeceptjs/configure' is not installed; CLI overrides are skipped. Run `npm i @codeceptjs/configure` to enable.")
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseArgs(args) {
|
|
55
|
+
return args.filter(Boolean).reduce((acc, arg) => Object.assign(acc, parseArg(arg)), {})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseArg(arg) {
|
|
59
|
+
if (arg === 'show') return { show: true }
|
|
60
|
+
if (arg === 'hide') return { show: false }
|
|
61
|
+
if (arg.includes('=')) {
|
|
62
|
+
const [key, ...rest] = arg.split('=')
|
|
63
|
+
return { [key]: parseValue(rest.join('=')) }
|
|
64
|
+
}
|
|
65
|
+
output.error(`browser plugin: unknown arg "${arg}"`)
|
|
66
|
+
return {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseValue(v) {
|
|
70
|
+
if (v === 'true') return true
|
|
71
|
+
if (v === 'false') return false
|
|
72
|
+
return v
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatOpts(opts) {
|
|
76
|
+
return Object.entries(opts).map(([k, v]) => `${k}=${v}`).join(', ')
|
|
77
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import Container from '../container.js'
|
|
2
|
+
|
|
3
|
+
const RESERVED_NAMES = new Set(['I', 'test', 'suite'])
|
|
4
|
+
const SHORTHAND_PROPERTIES = new Set(['page', 'browser', 'browserContext', 'context'])
|
|
5
|
+
|
|
6
|
+
const defaultConfig = {
|
|
7
|
+
inject: {},
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Exposes properties from helper instances as injectable test arguments.
|
|
12
|
+
* Use it to access the underlying Playwright/Puppeteer `page`, the wdio `browser` client,
|
|
13
|
+
* or any other helper internal directly from a Scenario:
|
|
14
|
+
*
|
|
15
|
+
* ```js
|
|
16
|
+
* Scenario('listen for requests', async ({ I, page, browser }) => {
|
|
17
|
+
* page.on('request', r => console.log(r.url()))
|
|
18
|
+
* await page.evaluate(() => 1 + 1)
|
|
19
|
+
* I.amOnPage('/')
|
|
20
|
+
* })
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The injected value is a live proxy: every property access reads the *current*
|
|
24
|
+
* helper property, so mid-test reassignments (popups, `switchToNextTab`,
|
|
25
|
+
* `openNewTab`) are reflected automatically. Calls are not wrapped as
|
|
26
|
+
* CodeceptJS steps — `await page.evaluate(...)` runs as native Playwright.
|
|
27
|
+
*
|
|
28
|
+
* #### Configuration
|
|
29
|
+
*
|
|
30
|
+
* `inject` maps an injection name to a `HelperName.propertyName` string. A
|
|
31
|
+
* value with no dot is shorthand for "first configured browser helper that
|
|
32
|
+
* exposes this property" (allowed properties: `page`, `browser`,
|
|
33
|
+
* `browserContext`, `context`).
|
|
34
|
+
*
|
|
35
|
+
* ```js
|
|
36
|
+
* plugins: {
|
|
37
|
+
* expose: {
|
|
38
|
+
* enabled: true,
|
|
39
|
+
* inject: {
|
|
40
|
+
* page: 'Playwright.page',
|
|
41
|
+
* browser: 'Playwright.browser',
|
|
42
|
+
* browserContext: 'Playwright.browserContext',
|
|
43
|
+
* frame: 'Playwright.context', // current frame set by switchTo
|
|
44
|
+
* wdio: 'WebDriver.browser',
|
|
45
|
+
* }
|
|
46
|
+
* }
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*
|
|
50
|
+
* Shorthand:
|
|
51
|
+
*
|
|
52
|
+
* ```js
|
|
53
|
+
* plugins: {
|
|
54
|
+
* expose: {
|
|
55
|
+
* enabled: true,
|
|
56
|
+
* inject: {
|
|
57
|
+
* page: 'page', // resolves to Playwright.page or Puppeteer.page
|
|
58
|
+
* }
|
|
59
|
+
* }
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*
|
|
63
|
+
* #### Caveats
|
|
64
|
+
*
|
|
65
|
+
* - The injected value is a `Proxy`, not the actual `Page`/`Browser` instance,
|
|
66
|
+
* so `page instanceof Page` is `false`. Use duck typing instead.
|
|
67
|
+
* - Cached method references lose the live binding. Call `page.click(...)`,
|
|
68
|
+
* not `const click = page.click; click(...)`.
|
|
69
|
+
* - In dry-run mode the underlying helper property is `undefined`; accessing
|
|
70
|
+
* any property on the proxy returns `undefined` rather than throwing.
|
|
71
|
+
*/
|
|
72
|
+
export default function (config = {}) {
|
|
73
|
+
config = { ...defaultConfig, ...config }
|
|
74
|
+
|
|
75
|
+
const mappings = parseMappings(config.inject)
|
|
76
|
+
|
|
77
|
+
const support = {}
|
|
78
|
+
for (const [name, { helperName, property }] of Object.entries(mappings)) {
|
|
79
|
+
support[name] = makeLiveProxy(helperName, property)
|
|
80
|
+
}
|
|
81
|
+
Container.append({ support })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseMappings(inject) {
|
|
85
|
+
const out = {}
|
|
86
|
+
for (const [name, value] of Object.entries(inject || {})) {
|
|
87
|
+
if (RESERVED_NAMES.has(name)) {
|
|
88
|
+
throw new Error(`expose plugin: inject name '${name}' is reserved`)
|
|
89
|
+
}
|
|
90
|
+
if (typeof value !== 'string' || !value) {
|
|
91
|
+
throw new Error(`expose plugin: inject value for '${name}' must be a non-empty string`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let helperName
|
|
95
|
+
let property
|
|
96
|
+
|
|
97
|
+
if (value.includes('.')) {
|
|
98
|
+
const dot = value.indexOf('.')
|
|
99
|
+
helperName = value.slice(0, dot)
|
|
100
|
+
property = value.slice(dot + 1)
|
|
101
|
+
if (!helperName || !property) {
|
|
102
|
+
throw new Error(`expose plugin: invalid inject value '${value}' for '${name}' (expected 'HelperName.propertyName')`)
|
|
103
|
+
}
|
|
104
|
+
if (!Container.helpers(helperName)) {
|
|
105
|
+
throw new Error(`expose plugin: helper '${helperName}' is not configured (needed for inject '${name}')`)
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
property = value
|
|
109
|
+
if (!SHORTHAND_PROPERTIES.has(property)) {
|
|
110
|
+
throw new Error(`expose plugin: shorthand '${property}' is not a known helper property for '${name}' (use 'HelperName.${property}' instead)`)
|
|
111
|
+
}
|
|
112
|
+
helperName = Container.STANDARD_ACTING_HELPERS.find(h => Container.helpers(h))
|
|
113
|
+
if (!helperName) {
|
|
114
|
+
throw new Error(`expose plugin: no standard browser helper configured (needed for inject '${name}')`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
out[name] = { helperName, property }
|
|
119
|
+
}
|
|
120
|
+
return out
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function makeLiveProxy(helperName, property) {
|
|
124
|
+
const resolve = () => Container.helpers(helperName)?.[property]
|
|
125
|
+
return new Proxy(function () {}, {
|
|
126
|
+
get(_, prop) {
|
|
127
|
+
const target = resolve()
|
|
128
|
+
if (target == null) return undefined
|
|
129
|
+
const value = target[prop]
|
|
130
|
+
if (typeof value === 'function') return value.bind(target)
|
|
131
|
+
return value
|
|
132
|
+
},
|
|
133
|
+
has(_, prop) {
|
|
134
|
+
const target = resolve()
|
|
135
|
+
return target != null && prop in target
|
|
136
|
+
},
|
|
137
|
+
apply(_, thisArg, args) {
|
|
138
|
+
const target = resolve()
|
|
139
|
+
return target?.apply(thisArg, args)
|
|
140
|
+
},
|
|
141
|
+
set(_, prop, value) {
|
|
142
|
+
const target = resolve()
|
|
143
|
+
if (target != null) target[prop] = value
|
|
144
|
+
return true
|
|
145
|
+
},
|
|
146
|
+
getPrototypeOf() {
|
|
147
|
+
const target = resolve()
|
|
148
|
+
return target != null ? Object.getPrototypeOf(target) : null
|
|
149
|
+
},
|
|
150
|
+
ownKeys() {
|
|
151
|
+
const target = resolve()
|
|
152
|
+
return target != null ? Reflect.ownKeys(target) : []
|
|
153
|
+
},
|
|
154
|
+
getOwnPropertyDescriptor(_, prop) {
|
|
155
|
+
const target = resolve()
|
|
156
|
+
return target != null ? Object.getOwnPropertyDescriptor(target, prop) : undefined
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
}
|