@testivai/witness-webdriverio 0.4.5 → 0.5.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 +6 -0
- package/dist/element-map.d.ts +1 -1
- package/dist/element-map.js +2 -2
- package/dist/types.d.ts +42 -0
- package/dist/witness.js +186 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -131,6 +131,11 @@ Captures a viewport screenshot, the DOM and the element map, and writes them as
|
|
|
131
131
|
| `options.maxElements` | number | Cap on elements collected for the element map. Default: 3000. |
|
|
132
132
|
| `options.ignoreSelectors` | string[] | Elements hidden (`visibility: hidden`) for this capture; merged with the global `ignoreSelectors` from `.testivai/config.json`. |
|
|
133
133
|
| `options.stabilize` | boolean | Override capture stabilization (animations frozen, caret hidden, fonts awaited). Default: `true` (or the global `stabilize` setting). |
|
|
134
|
+
| `options.native` | boolean | Native app mode (experimental): detected by default in an Appium session on a native Android or iOS screen. `true` forces it, `false` keeps the web path. |
|
|
135
|
+
| `options.maskStatusBar` | boolean | Native mode only: mask the status bar, measured at capture time (Android: the window manager's bar, `mobile: getSystemBars`; iOS: `mobile: viewportRect`). Default: `true`. |
|
|
136
|
+
| `options.context` | string | Capture in another Appium context for this call, then switch back: `'webview'` (the app's web view) or an exact context name. For a hybrid app driven in the native context, the web view is captured as a page, with every layer. |
|
|
137
|
+
|
|
138
|
+
In an Appium session on a native app screen, `witness()` runs no page script (a native screen has no DOM): it takes the whole screen, masks the status bar, records the device, and keys the snapshot `<name>__<platform>-<device>-<osMajor>`. Pixel diff only. See [Native mobile apps](https://github.com/testivai/testivai-oss/blob/main/docs/guides/native-mobile.md).
|
|
134
139
|
|
|
135
140
|
### `TestivaiService`
|
|
136
141
|
|
|
@@ -147,6 +152,7 @@ Class registered as a WDIO service. Implements `onComplete`.
|
|
|
147
152
|
## Status
|
|
148
153
|
|
|
149
154
|
- **Local mode**: stable
|
|
155
|
+
- **Native app mode**: experimental (verified on an Android 14 emulator; iOS not verified)
|
|
150
156
|
|
|
151
157
|
## Links
|
|
152
158
|
|
package/dist/element-map.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface PageElementMap {
|
|
|
14
14
|
* run (so all lanes produce identical maps), plus the viewport it was taken
|
|
15
15
|
* in. WebdriverIO string scripts need an explicit `return`.
|
|
16
16
|
*/
|
|
17
|
-
export declare function elementMapScript(maxElements: number, ignoreSelectors: string[]): string;
|
|
17
|
+
export declare function elementMapScript(maxElements: number, ignoreSelectors: string[], idAttributes?: string[]): string;
|
|
18
18
|
export declare function isPageElementMap(value: unknown): value is PageElementMap;
|
|
19
19
|
/**
|
|
20
20
|
* WebDriver's Take Screenshot captures the viewport, while the collector
|
package/dist/element-map.js
CHANGED
|
@@ -9,8 +9,8 @@ const witness_1 = require("@testivai/witness");
|
|
|
9
9
|
* run (so all lanes produce identical maps), plus the viewport it was taken
|
|
10
10
|
* in. WebdriverIO string scripts need an explicit `return`.
|
|
11
11
|
*/
|
|
12
|
-
function elementMapScript(maxElements, ignoreSelectors) {
|
|
13
|
-
return (`return { elements: ${(0, witness_1.buildElementMapExpression)(maxElements, ignoreSelectors)}, ` +
|
|
12
|
+
function elementMapScript(maxElements, ignoreSelectors, idAttributes = []) {
|
|
13
|
+
return (`return { elements: ${(0, witness_1.buildElementMapExpression)(maxElements, ignoreSelectors, idAttributes)}, ` +
|
|
14
14
|
'scrollX: window.scrollX || 0, scrollY: window.scrollY || 0, ' +
|
|
15
15
|
'viewportWidth: window.innerWidth, viewportHeight: window.innerHeight, ' +
|
|
16
16
|
'dpr: window.devicePixelRatio || 1 };');
|
package/dist/types.d.ts
CHANGED
|
@@ -18,6 +18,22 @@ export interface WitnessBrowser {
|
|
|
18
18
|
* Type kept loose to accept WDIO 8/9 signature variants.
|
|
19
19
|
*/
|
|
20
20
|
execute<T = unknown>(script: string | ((...args: any[]) => T), ...args: any[]): Promise<T>;
|
|
21
|
+
/**
|
|
22
|
+
* The session's W3C capabilities (WebdriverIO's `browser.capabilities`).
|
|
23
|
+
* Optional: when present, the browser, its version and the driver are
|
|
24
|
+
* recorded as capture provenance in the capture's metadata.json.
|
|
25
|
+
*/
|
|
26
|
+
capabilities?: Record<string, unknown>;
|
|
27
|
+
/**
|
|
28
|
+
* Appium's current context (`NATIVE_APP`, `WEBVIEW_…`): WebdriverIO's
|
|
29
|
+
* `browser.getContext()`. Asked only in a mobile session that names no
|
|
30
|
+
* browser, to tell a native screen from a hybrid app's web view.
|
|
31
|
+
*/
|
|
32
|
+
getContext?(): Promise<unknown>;
|
|
33
|
+
/** Appium's contexts (WebdriverIO's `browser.getContexts()`), for `options.context`. */
|
|
34
|
+
getContexts?(): Promise<unknown[]>;
|
|
35
|
+
/** Switch Appium's context (WebdriverIO's `browser.switchContext()`), for `options.context`. */
|
|
36
|
+
switchContext?(context: string): Promise<unknown>;
|
|
21
37
|
}
|
|
22
38
|
/**
|
|
23
39
|
* Options for a single witness() call. All optional.
|
|
@@ -58,6 +74,32 @@ export interface WitnessOptions {
|
|
|
58
74
|
* via `stabilize` in `.testivai/config.json`; this per-call value wins).
|
|
59
75
|
*/
|
|
60
76
|
stabilize?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Native app mode (experimental). By default it is detected: an Appium
|
|
79
|
+
* session on Android or iOS that names no browser, in the `NATIVE_APP`
|
|
80
|
+
* context (or, when the context cannot be read, started with `app`,
|
|
81
|
+
* `appPackage` or `bundleId`). A native screen has no page, so no page
|
|
82
|
+
* script runs: the capture is the screenshot alone (pixel diff only), the
|
|
83
|
+
* status bar is masked, the environment comes from the capabilities, and
|
|
84
|
+
* the snapshot is keyed by device. `true` forces it, `false` turns it off.
|
|
85
|
+
*/
|
|
86
|
+
native?: boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Native mode only: mask the status bar, measured at capture time: on
|
|
89
|
+
* Android the bar's frame in the window manager (`mobile: getSystemBars`),
|
|
90
|
+
* on iOS `mobile: viewportRect`. Default: true.
|
|
91
|
+
*/
|
|
92
|
+
maskStatusBar?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Capture in another Appium context for this call, then switch back (also
|
|
95
|
+
* when the capture fails). `'webview'` is the app's web view: the one
|
|
96
|
+
* named after the app's package, else the first `WEBVIEW_…` context. Any
|
|
97
|
+
* other value is an exact context name (`'WEBVIEW_com.example.app'`,
|
|
98
|
+
* `'NATIVE_APP'`). For a hybrid app driven in the native context, this
|
|
99
|
+
* captures its web view as a page, with every layer. Needs WebView
|
|
100
|
+
* debugging enabled in the app's build.
|
|
101
|
+
*/
|
|
102
|
+
context?: 'webview' | (string & {});
|
|
61
103
|
}
|
|
62
104
|
/**
|
|
63
105
|
* Configuration passed to the WDIO service.
|
package/dist/witness.js
CHANGED
|
@@ -57,6 +57,8 @@ exports.witness = witness;
|
|
|
57
57
|
const fs = __importStar(require("fs"));
|
|
58
58
|
const path = __importStar(require("path"));
|
|
59
59
|
const witness_1 = require("@testivai/witness");
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
61
|
+
const ADAPTER_VERSION = require('../package.json').version;
|
|
60
62
|
const element_map_1 = require("./element-map");
|
|
61
63
|
/** id of the style element injected for the duration of a capture */
|
|
62
64
|
const STYLE_ID = '__testivai_capture_css__';
|
|
@@ -116,6 +118,99 @@ async function waitForFonts(browser) {
|
|
|
116
118
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
117
119
|
}
|
|
118
120
|
}
|
|
121
|
+
/** A variant as it goes into a snapshot name. */
|
|
122
|
+
const variantSegment = (variant) => variant.replace(/[^a-z0-9_-]+/gi, '_').toLowerCase();
|
|
123
|
+
/**
|
|
124
|
+
* Whether this capture is of a native app screen (experimental native mode).
|
|
125
|
+
* Only a mobile session that names no browser is asked for its context, so a
|
|
126
|
+
* browser session makes no extra call. NATIVE_APP is native; a web view
|
|
127
|
+
* (WEBVIEW_…, CHROMIUM) is a page. When the context cannot be read, the
|
|
128
|
+
* capabilities decide: started on an app (app, appPackage, bundleId).
|
|
129
|
+
*/
|
|
130
|
+
async function isNativeCapture(browser, forced) {
|
|
131
|
+
if (forced !== undefined)
|
|
132
|
+
return forced;
|
|
133
|
+
const caps = browser.capabilities;
|
|
134
|
+
if (!(0, witness_1.mobilePlatform)(caps) || (0, witness_1.namesBrowser)(caps))
|
|
135
|
+
return false;
|
|
136
|
+
if (typeof browser.getContext === 'function') {
|
|
137
|
+
try {
|
|
138
|
+
const context = await browser.getContext();
|
|
139
|
+
const id = typeof context === 'string' ? context : context?.id;
|
|
140
|
+
if (typeof id === 'string' && id.length > 0)
|
|
141
|
+
return id === 'NATIVE_APP';
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// the capabilities decide
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return (0, witness_1.isNativeAppCapabilities)(caps);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Native mode: a native screen has no page, so no page script runs (Appium
|
|
151
|
+
* rejects them all there, and WebdriverIO resends each failed one three
|
|
152
|
+
* times: ~1.5 s each, measured on an Android 14 emulator). The capture is the
|
|
153
|
+
* screenshot alone, with the status bar masked (`statusBarHeight`), the
|
|
154
|
+
* environment from the capabilities, and the name keyed by device unless the
|
|
155
|
+
* call gives a variant.
|
|
156
|
+
*/
|
|
157
|
+
async function witnessNative(browser, name, options) {
|
|
158
|
+
const localConfig = (0, witness_1.loadLocalConfig)(process.cwd());
|
|
159
|
+
const variant = options.variant ? variantSegment(options.variant) : (0, witness_1.nativeAppVariant)((0, witness_1.nativeAppDevice)(browser.capabilities));
|
|
160
|
+
if (variant)
|
|
161
|
+
name = `${name}__${variant}`;
|
|
162
|
+
const environment = (0, witness_1.appiumEnvironment)({
|
|
163
|
+
capabilities: browser.capabilities,
|
|
164
|
+
adapter: { name: '@testivai/witness-webdriverio', version: ADAPTER_VERSION, captureMethod: witness_1.CAPTURE_METHODS.APPIUM_SCREEN },
|
|
165
|
+
framework: { name: 'webdriverio', version: (0, witness_1.installedPackageVersion)('webdriverio') },
|
|
166
|
+
});
|
|
167
|
+
name = (0, witness_1.applyEnvironmentPolicyToName)(name, { browser: { name: environment.browser.name }, os: { platform: process.platform } }, localConfig.environmentPolicy);
|
|
168
|
+
const statusBar = options.maskStatusBar === false ? null : await statusBarHeight(browser);
|
|
169
|
+
const base64 = await browser.takeScreenshot();
|
|
170
|
+
if (typeof base64 !== 'string' || base64.length === 0) {
|
|
171
|
+
throw new Error(`testivai.witness("${name}"): browser.takeScreenshot() returned an empty value`);
|
|
172
|
+
}
|
|
173
|
+
const store = new witness_1.BaselineStore(process.cwd());
|
|
174
|
+
// Nothing from an earlier capture of this name: no DOM, element map or metadata.
|
|
175
|
+
for (const leftover of [store.getTempDomPath(name), store.getTempMetadataPath(name), path.join(store.getTempDir(), name, 'elements.json')]) {
|
|
176
|
+
fs.rmSync(leftover, { force: true });
|
|
177
|
+
}
|
|
178
|
+
store.writeTemp(name, Buffer.from(base64, 'base64'));
|
|
179
|
+
store.writeTempMetadata(name, { name, environment, ...(statusBar ? { masks: [{ top: statusBar }] } : {}) });
|
|
180
|
+
}
|
|
181
|
+
/** Sessions whose `mobile: getSystemBars` failed: not asked again, as WebdriverIO resends a failed command. */
|
|
182
|
+
const withoutSystemBars = new WeakSet();
|
|
183
|
+
/**
|
|
184
|
+
* The status bar's height in screenshot pixels for the mask, or null.
|
|
185
|
+
*
|
|
186
|
+
* On Android, the bar's window frame from the window manager at capture time
|
|
187
|
+
* (`mobile: getSystemBars`). UiAutomator2's own figure (`mobile: viewportRect`,
|
|
188
|
+
* then the capabilities) is read once, when the session starts, from a
|
|
189
|
+
* resource of its server app: on an Android 14 emulator it came back 63 px for
|
|
190
|
+
* a 136 px bar in one CI run, and it does not follow a rotation. It is the
|
|
191
|
+
* fallback, and the iOS source.
|
|
192
|
+
*/
|
|
193
|
+
async function statusBarHeight(browser) {
|
|
194
|
+
if ((0, witness_1.mobilePlatform)(browser.capabilities) === 'android' && !withoutSystemBars.has(browser)) {
|
|
195
|
+
try {
|
|
196
|
+
const fromWindowManager = (0, witness_1.statusBarFromSystemBars)(await browser.execute('mobile: getSystemBars'));
|
|
197
|
+
if (fromWindowManager !== null)
|
|
198
|
+
return fromWindowManager;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
withoutSystemBars.add(browser);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
const fromViewport = (0, witness_1.statusBarFromViewportRect)(await browser.execute('mobile: viewportRect'));
|
|
206
|
+
if (fromViewport !== null)
|
|
207
|
+
return fromViewport;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// older drivers: the capabilities' reading from the session start
|
|
211
|
+
}
|
|
212
|
+
return (0, witness_1.statusBarFromCapabilities)(browser.capabilities);
|
|
213
|
+
}
|
|
119
214
|
/**
|
|
120
215
|
* Capture a screenshot + DOM and write them as a temp snapshot.
|
|
121
216
|
*
|
|
@@ -132,16 +227,91 @@ async function witness(browser, name, options = {}) {
|
|
|
132
227
|
if (!browser || typeof browser.takeScreenshot !== 'function') {
|
|
133
228
|
throw new Error('testivai.witness: browser argument must expose takeScreenshot()');
|
|
134
229
|
}
|
|
230
|
+
if (options.context !== undefined) {
|
|
231
|
+
return inContext(browser, options.context, (native) => capture(browser, name, { ...options, native: options.native ?? native }));
|
|
232
|
+
}
|
|
233
|
+
return capture(browser, name, options);
|
|
234
|
+
}
|
|
235
|
+
/** A context as Appium names it: a string, or WebdriverIO's detailed `{ id }`. */
|
|
236
|
+
const contextName = (c) => typeof c === 'string' ? c : typeof c?.id === 'string' ? c.id : '';
|
|
237
|
+
/**
|
|
238
|
+
* Run a capture in another Appium context and switch back afterwards, also
|
|
239
|
+
* when it fails. `'webview'` is the web view named after the app (Android
|
|
240
|
+
* names it WEBVIEW_<package>), else the first one (iOS numbers them).
|
|
241
|
+
* Native mode follows the context: NATIVE_APP is native, any other is a page.
|
|
242
|
+
*/
|
|
243
|
+
async function inContext(browser, wanted, run) {
|
|
244
|
+
if (typeof browser.getContext !== 'function' || typeof browser.getContexts !== 'function' || typeof browser.switchContext !== 'function') {
|
|
245
|
+
throw new Error('testivai.witness: options.context needs an Appium session: browser.getContext(), getContexts() and switchContext()');
|
|
246
|
+
}
|
|
247
|
+
const contexts = (await browser.getContexts()).map(contextName).filter((c) => c.length > 0);
|
|
248
|
+
const current = contextName(await browser.getContext());
|
|
249
|
+
let target;
|
|
250
|
+
if (wanted === 'webview') {
|
|
251
|
+
const views = contexts.filter((c) => c.startsWith('WEBVIEW'));
|
|
252
|
+
const caps = (browser.capabilities ?? {});
|
|
253
|
+
const app = caps.appPackage ?? caps['appium:appPackage'] ?? caps.bundleId ?? caps['appium:bundleId'];
|
|
254
|
+
target = views.find((c) => typeof app === 'string' && c === `WEBVIEW_${app}`) ?? views[0];
|
|
255
|
+
if (!target) {
|
|
256
|
+
throw new Error(`testivai.witness: options.context 'webview': no web view context in this session (contexts: ${contexts.join(', ') || 'none'}). ` +
|
|
257
|
+
"The app's build needs WebView debugging enabled (Android WebView.setWebContentsDebuggingEnabled, iOS isInspectable), and the web view must be on screen.");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
target = wanted;
|
|
262
|
+
if (!contexts.includes(target)) {
|
|
263
|
+
throw new Error(`testivai.witness: options.context: no context "${wanted}" in this session (contexts: ${contexts.join(', ') || 'none'})`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const switched = target !== current && current.length > 0;
|
|
267
|
+
if (target !== current)
|
|
268
|
+
await browser.switchContext(target);
|
|
269
|
+
try {
|
|
270
|
+
await run(target === 'NATIVE_APP');
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
// Back where the test was; the capture error is the one that matters.
|
|
274
|
+
if (switched)
|
|
275
|
+
await browser.switchContext(current).catch(() => undefined);
|
|
276
|
+
throw err;
|
|
277
|
+
}
|
|
278
|
+
// A failed switch back leaves the session elsewhere: let it surface.
|
|
279
|
+
if (switched)
|
|
280
|
+
await browser.switchContext(current);
|
|
281
|
+
}
|
|
282
|
+
/** One capture in the session's current context: native mode or a page. */
|
|
283
|
+
async function capture(browser, name, options) {
|
|
284
|
+
if (await isNativeCapture(browser, options.native)) {
|
|
285
|
+
return witnessNative(browser, name, options);
|
|
286
|
+
}
|
|
135
287
|
// Multi-capability runs: fold the variant into the name so capabilities
|
|
136
288
|
// don't overwrite each other's baselines (same mechanism as the Playwright
|
|
137
289
|
// adapter's multi-project handling).
|
|
138
290
|
if (options.variant) {
|
|
139
|
-
name = `${name}__${options.variant
|
|
291
|
+
name = `${name}__${variantSegment(options.variant)}`;
|
|
292
|
+
}
|
|
293
|
+
const localConfig = (0, witness_1.loadLocalConfig)(process.cwd());
|
|
294
|
+
// Capture provenance: what the page is rendered with, measured before
|
|
295
|
+
// anything is injected, plus the session's capabilities. Best-effort.
|
|
296
|
+
let renderProbe = null;
|
|
297
|
+
if (typeof browser.execute === 'function') {
|
|
298
|
+
try {
|
|
299
|
+
renderProbe = await browser.execute(`return ${witness_1.RENDER_PROBE_EXPRESSION}`);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// no probe: no render settings recorded
|
|
303
|
+
}
|
|
140
304
|
}
|
|
305
|
+
const environment = (0, witness_1.webDriverEnvironment)({
|
|
306
|
+
capabilities: browser.capabilities,
|
|
307
|
+
probe: renderProbe,
|
|
308
|
+
adapter: { name: '@testivai/witness-webdriverio', version: ADAPTER_VERSION, captureMethod: witness_1.CAPTURE_METHODS.WEBDRIVER_VIEWPORT },
|
|
309
|
+
framework: { name: 'webdriverio', version: (0, witness_1.installedPackageVersion)('webdriverio') },
|
|
310
|
+
});
|
|
311
|
+
name = (0, witness_1.applyEnvironmentPolicyToName)(name, { browser: { name: environment?.browser.name ?? 'unknown' }, os: { platform: process.platform } }, localConfig.environmentPolicy);
|
|
141
312
|
// 0. Prepare the page: stabilization CSS (animations/caret/fonts — the
|
|
142
313
|
// flake killers) + ignoreSelectors from config.json and this call,
|
|
143
314
|
// injected as one style tag and removed after the capture.
|
|
144
|
-
const localConfig = (0, witness_1.loadLocalConfig)(process.cwd());
|
|
145
315
|
const stabilize = options.stabilize ?? localConfig.stabilize;
|
|
146
316
|
const ignoreSelectors = [
|
|
147
317
|
...new Set([...(localConfig.ignoreSelectors ?? []), ...(options.ignoreSelectors ?? [])]),
|
|
@@ -197,10 +367,13 @@ async function witness(browser, name, options = {}) {
|
|
|
197
367
|
// It powers the style check, region attribution and page-shift
|
|
198
368
|
// detection on the compare side. The screenshot is the viewport, so the
|
|
199
369
|
// map keeps only the elements inside it, in screenshot coordinates.
|
|
370
|
+
// Opt-in: paths anchored on attributes such as data-testid (config
|
|
371
|
+
// `elementIdAttributes`), recorded with the capture only when set.
|
|
372
|
+
const idAttributes = (0, witness_1.normalizeElementIdAttributes)(localConfig.elementIdAttributes);
|
|
200
373
|
let elementMap;
|
|
201
374
|
if (!options.skipElementMap && typeof browser.execute === 'function') {
|
|
202
375
|
try {
|
|
203
|
-
const result = await browser.execute((0, element_map_1.elementMapScript)(options.maxElements ?? witness_1.DEFAULT_MAX_ELEMENTS, ignoreSelectors));
|
|
376
|
+
const result = await browser.execute((0, element_map_1.elementMapScript)(options.maxElements ?? witness_1.DEFAULT_MAX_ELEMENTS, ignoreSelectors, idAttributes));
|
|
204
377
|
if ((0, element_map_1.isPageElementMap)(result)) {
|
|
205
378
|
const inView = (0, element_map_1.toViewportElementMap)(result);
|
|
206
379
|
if (inView.length > 0)
|
|
@@ -215,11 +388,21 @@ async function witness(browser, name, options = {}) {
|
|
|
215
388
|
// 4. Write to .testivai/temp/<name>/
|
|
216
389
|
const store = new witness_1.BaselineStore(process.cwd());
|
|
217
390
|
store.writeTemp(name, screenshot, dom);
|
|
391
|
+
if (environment) {
|
|
392
|
+
try {
|
|
393
|
+
store.writeTempMetadata(name, { name, environment });
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
// without it the compare cannot tell rendering drift from noise for this capture
|
|
397
|
+
}
|
|
398
|
+
}
|
|
218
399
|
if (elementMap) {
|
|
219
400
|
try {
|
|
220
401
|
const tempDir = path.join(process.cwd(), '.testivai', 'temp', name);
|
|
221
402
|
fs.mkdirSync(tempDir, { recursive: true });
|
|
222
403
|
fs.writeFileSync(path.join(tempDir, 'elements.json'), JSON.stringify(elementMap));
|
|
404
|
+
if (idAttributes.length > 0)
|
|
405
|
+
store.writeTempMetadata(name, { elementIdAttributes: idAttributes });
|
|
223
406
|
}
|
|
224
407
|
catch {
|
|
225
408
|
// A map we cannot persist is not worth failing a capture over.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testivai/witness-webdriverio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "WebdriverIO adapter for TestivAI Witness — local-first visual regression with pixel + DOM comparison",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@testivai/witness": "2.
|
|
57
|
+
"@testivai/witness": "2.6.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/jest": "^30.0.0",
|