Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ selenium-video-*.webm
nightwatch-video-*.webm
packages/nightwatch-devtools/nightwatch-video-*.webm

# trace.zip output (mode: 'trace')
# trace output (mode: 'trace')
trace-*.zip
examples/**/trace-*/

# vitest --coverage output
coverage/
Expand Down
3 changes: 2 additions & 1 deletion examples/wdio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"scripts": {
"wdio": "wdio run ./wdio.conf.ts",
"mobile": "wdio run ./wdio.mobile.conf.ts"
"mobile": "wdio run ./wdio.mobile.conf.ts",
"trace": "wdio run ./wdio.trace.conf.ts"
}
}
64 changes: 64 additions & 0 deletions examples/wdio/wdio.trace.conf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import path from 'node:path'

const __dirname = path.resolve(path.dirname(new URL(import.meta.url).pathname))

export const config: WebdriverIO.Config = {
runner: 'local',
tsConfigPath: './tsconfig.json',

specs: ['./features/login.feature'],

maxInstances: 1,

capabilities: [
{
browserName: 'chrome',
'goog:chromeOptions': {
args: [
'--headless',
'--disable-gpu',
'--remote-allow-origins=*',
'--window-size=1600,1200'
]
}
}
],

logLevel: 'warn',

baseUrl: 'http://localhost',

waitforTimeout: 10000,
connectionRetryTimeout: 120000,
connectionRetryCount: 3,

services: [
[
'devtools',
{
mode: 'trace' as const
// traceFormat: 'ndjson-directory'
}
]
],

framework: 'cucumber',

reporters: ['spec'],

cucumberOpts: {
require: [
path.resolve(__dirname, 'features', 'step-definitions', 'steps.ts')
],
backtrace: false,
requireModule: [],
dryRun: false,
failFast: false,
snippets: true,
source: true,
strict: false,
tagExpression: '',
timeout: 60000,
ignoreUndefinedDefinitions: false
}
}
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
},
"types": "./src/index.ts",
"scripts": {
"lint": "eslint ."
"lint": "eslint . --fix"
},
"license": "MIT",
"devDependencies": {
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/action-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,13 @@ export function formatActionTitle(
if (firstArg === undefined) {
return `${action.class}.${action.method}()`
}
const label = (
const label =
typeof firstArg === 'object' ? JSON.stringify(firstArg) : String(firstArg)
).slice(0, 80)
return `${action.class}.${action.method}("${label}")`
}

/**
* Methods where the first positional argument should render as value= in the
* transcript line (e.g. setValue, selectByVisibleText).
*/
export const FILL_METHODS = new Set(['fill', 'selectOption'])
73 changes: 64 additions & 9 deletions packages/core/src/action-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,43 @@
// (timeouts, fallbacks, snapshot serialization) lives in one place.

import { accessibilityTreeScript, elementsScript } from './element-scripts.js'
import { serializeWebSnapshot } from './element-snapshot.js'
import {
serializeWebSnapshot,
serializeMobileSnapshot
} from './element-snapshot.js'
import type { AccessibilityNode, BrowserElementInfo } from './element-types.js'
import { xmlToJSON } from './locators/xml-parsing.js'
import {
generateAllElementLocators,
getDefaultFilters
} from './locators/index.js'
import { SNAPSHOT_PROBE_TIMEOUT_MS, withTimeout } from './with-timeout.js'
import type { ActionSnapshot } from '@wdio/devtools-shared'

export type ScriptRunner = (scriptSrc: string) => Promise<unknown>

export interface CaptureActionSnapshotInput {
command: string
runScript: ScriptRunner
/** Browser script runner — omit on native mobile where Appium can't execute JS. */
runScript?: ScriptRunner
takeScreenshot?: () => Promise<string | null | undefined>
getUrl?: () => Promise<string | undefined>
getTitle?: () => Promise<string | undefined>
/** Page-source XML fetcher for native mobile — used instead of runScript. */
getPageSource?: () => Promise<string | undefined>
/** Platform identifier for mobile snapshot formatting ('android' | 'ios'). */
platform?: 'android' | 'ios'
}

async function runWith<T>(
runScript: ScriptRunner,
runScript: ScriptRunner | undefined,
scriptSrc: string,
fallback: T
): Promise<T> {
if (!runScript) {
return fallback
}

return withTimeout(
runScript(scriptSrc).then((r) => r as T),
SNAPSHOT_PROBE_TIMEOUT_MS,
Expand All @@ -35,10 +52,15 @@ export async function captureActionSnapshot(
): Promise<ActionSnapshot | null> {
try {
const timestamp = Date.now()
const [shot, url, title, tree, elements] = await Promise.all([
input.takeScreenshot?.().catch(() => null) ?? Promise.resolve(null),
input.getUrl?.().catch(() => undefined) ?? Promise.resolve(undefined),
input.getTitle?.().catch(() => undefined) ?? Promise.resolve(undefined),
const isNativeMobile = !input.runScript && !!input.getPageSource

const [shot, url, title, pageSource, tree, elements] = await Promise.all([
input.takeScreenshot?.().catch(() => null),
input.getUrl?.().catch(() => undefined),
input.getTitle?.().catch(() => undefined),
isNativeMobile
? input.getPageSource?.().catch(() => undefined)
: undefined,
runWith<AccessibilityNode[]>(
input.runScript,
accessibilityTreeScript(true),
Expand All @@ -50,14 +72,47 @@ export async function captureActionSnapshot(
[]
)
])
const snapshotText = serializeWebSnapshot(tree, { url, title })

let snapshotText: string
let finalElements: unknown[] = elements

if (isNativeMobile && pageSource) {
const platform = input.platform ?? 'android'
const jsonTree = xmlToJSON(pageSource)
if (jsonTree) {
jsonTree.attributes._sourceXML = pageSource
snapshotText = serializeMobileSnapshot(jsonTree, {
platform,
sourceXML: pageSource
})
} else {
snapshotText = `[${platform}]`
}
// Generate mobile element locators from the page source XML.
try {
const viewport = { width: 9999, height: 9999 }
const filters = getDefaultFilters(platform, false)
const locators = generateAllElementLocators(pageSource, {
platform,
viewportSize: viewport,
filters,
inViewportOnly: false
})
finalElements = locators
} catch {
// Non-fatal — snapshot text is the primary deliverable.
}
} else {
snapshotText = serializeWebSnapshot(tree, { url, title })
}

return {
timestamp,
command: input.command,
url,
title,
screenshot: shot ?? undefined,
elements,
elements: finalElements,
snapshotText
}
} catch {
Expand Down
Loading
Loading