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
9 changes: 9 additions & 0 deletions .changeset/shared-accessible-name-script.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@wdio/elements': patch
'@wdio/devtools-backend': patch
'@wdio/devtools-service': patch
'@wdio/nightwatch-devtools': patch
'@wdio/selenium-devtools': patch
---

The accessibility-tree and interactable-element scripts now share one copy of the accessible-name steps they had in common. Names, locators and every export are unchanged.
2 changes: 1 addition & 1 deletion packages/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ import {
type BaselineGetQuery,
type BaselineSavedWsPayload,
type BaselineClearedWsPayload,
type RunnerRequestBody,
type SocketMessage,
type TracePlayerData
} from '@wdio/devtools-shared'
import type { RunnerRequestBody } from './types.js'

let server: FastifyInstance | undefined

Expand Down
3 changes: 2 additions & 1 deletion packages/backend/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import kill from 'tree-kill'
import logger from '@wdio/logger'
import { parse as shellParse, quote as shellQuote } from 'shell-quote'
import {
NIGHTWATCH_CONFIG_FILENAMES,
RERUN_SLOT,
REUSE_ENV,
RUNNER_ENV,
WDIO_CONFIG_FILENAMES,
parseInitialArgs,
type RunnerRequestBody
} from '@wdio/devtools-shared'
import { WDIO_CONFIG_FILENAMES, NIGHTWATCH_CONFIG_FILENAMES } from './types.js'
import {
escapeFilterRegex,
getFilterBuilder,
Expand Down
16 changes: 0 additions & 16 deletions packages/backend/src/types.ts

This file was deleted.

7 changes: 5 additions & 2 deletions packages/backend/tests/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import { spawn } from 'node:child_process'
import { EventEmitter } from 'node:events'
import fs from 'node:fs'
import path from 'node:path'
import { RERUN_SLOT, REUSE_ENV } from '@wdio/devtools-shared'
import {
RERUN_SLOT,
REUSE_ENV,
type RunnerRequestBody
} from '@wdio/devtools-shared'

import type * as BinResolver from '../src/bin-resolver.js'
import type { RunnerRequestBody } from '../src/types.js'

vi.mock('node:child_process')
vi.mock('tree-kill')
Expand Down
28 changes: 12 additions & 16 deletions packages/conformance/src/allowlist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,33 +15,29 @@ const [LOGIN, MISSING] = [
'times out waiting for a missing element'
].map(scenarioIndex)

/** What a document left before an appended collector anchors it costs, in either adapter without BiDi. */
const SCRIPT_INJECTION_RULES: RuleId[] = [
'every-document-anchored',
'row-replays-its-page',
'every-browser-request-listed',
'adapters-agree-on-pages',
'adapters-agree-on-snapshots',
'adapters-agree-on-requests'
]

export const KNOWN_GAPS: KnownGap[] = [
{
adapter: 'selenium-devtools',
bidi: 'off',
rules: [
'every-document-anchored',
'row-replays-its-page',
'every-browser-request-listed',
'adapters-agree-on-pages',
'adapters-agree-on-snapshots',
'adapters-agree-on-requests'
],
rules: SCRIPT_INJECTION_RULES,
reason:
'Without BiDi the collector is appended as a <script> after a readiness poll, so a document the test leaves within tens of ms is never anchored and its Resource Timing buffer dies undrained with it. CLAUDE.md Known debt: "Document-start injection is what removes the whole race class".',
timingDependent: true
},
{
adapter: 'nightwatch-devtools',
bidi: 'off',
rules: [
'every-document-anchored',
'row-replays-its-page',
'every-browser-request-listed',
'adapters-agree-on-pages',
'adapters-agree-on-snapshots',
'adapters-agree-on-requests'
],
rules: SCRIPT_INJECTION_RULES,
reason:
'Without BiDi the collector is appended as a <script> and the destination is anchored by polling for the old collector to disappear, so a document the test leaves within tens of ms is never anchored and its Resource Timing buffer dies undrained with it. CLAUDE.md Known debt: "Document-start injection is what removes the whole race class".',
timingDependent: true
Expand Down
9 changes: 7 additions & 2 deletions packages/core/src/locators/element-filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import {
IOS_LAYOUT_CONTAINERS
} from './constants.js'

/** Android's automation drivers are the UiAutomator family; anything else is read as iOS. */
export function isUiAutomatorDriver(automationName: string): boolean {
return automationName.toLowerCase().includes('uiautomator')
}

/**
* Check if element tag matches any in the list (handles partial matches)
*/
Expand Down Expand Up @@ -86,7 +91,7 @@ export function isInteractableElement(
_isNative: boolean,
automationName: string
): boolean {
const isAndroid = automationName.toLowerCase().includes('uiautomator')
const isAndroid = isUiAutomatorDriver(automationName)
const interactableTags = isAndroid
? ANDROID_INTERACTABLE_TAGS
: IOS_INTERACTABLE_TAGS
Expand Down Expand Up @@ -194,7 +199,7 @@ export function shouldIncludeElement(
}

if (visibleOnly) {
const isAndroid = automationName.toLowerCase().includes('uiautomator')
const isAndroid = isUiAutomatorDriver(automationName)
if (isAndroid && element.attributes?.displayed === 'false') {
return false
}
Expand Down
18 changes: 7 additions & 11 deletions packages/core/src/locators/locator-generation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
evaluateXPath,
isAttributeUnique
} from './xml-parsing.js'
import { isUiAutomatorDriver } from './element-filter.js'

/**
* Check if a string value is valid for use in a locator
Expand Down Expand Up @@ -422,12 +423,11 @@ function buildXPath(
function getSimpleSuggestedLocators(
element: JSONElement,
ctx: LocatorContext,
automationName: string,
isAndroid: boolean,
inUiAutomatorScope: boolean,
targetNode?: XMLNode
): [LocatorStrategy, string][] {
const results: [LocatorStrategy, string][] = []
const isAndroid = automationName.toLowerCase().includes('uiautomator')
const attrs = element.attributes

if (isAndroid) {
Expand Down Expand Up @@ -528,12 +528,11 @@ function getSimpleSuggestedLocators(
function getComplexSuggestedLocators(
element: JSONElement,
ctx: LocatorContext,
automationName: string,
isAndroid: boolean,
inUiAutomatorScope: boolean,
targetNode?: XMLNode
): [LocatorStrategy, string][] {
const results: [LocatorStrategy, string][] = []
const isAndroid = automationName.toLowerCase().includes('uiautomator')

if (isAndroid) {
if (inUiAutomatorScope) {
Expand Down Expand Up @@ -589,11 +588,8 @@ export function getSuggestedLocators(
ctx?: LocatorContext,
targetNode?: XMLNode
): [LocatorStrategy, string][] {
const locatorCtx = ctx ?? {
sourceXML,
parsedDOM: null,
isAndroid: automationName.toLowerCase().includes('uiautomator')
}
const isAndroid = isUiAutomatorDriver(automationName)
const locatorCtx = ctx ?? { sourceXML, parsedDOM: null, isAndroid }

const inUiAutomatorScope = locatorCtx.isAndroid
? isInUiAutomatorScope(element, locatorCtx.parsedDOM)
Expand All @@ -602,14 +598,14 @@ export function getSuggestedLocators(
const simpleLocators = getSimpleSuggestedLocators(
element,
locatorCtx,
automationName,
isAndroid,
inUiAutomatorScope,
targetNode
)
const complexLocators = getComplexSuggestedLocators(
element,
locatorCtx,
automationName,
isAndroid,
inUiAutomatorScope,
targetNode
)
Expand Down
50 changes: 50 additions & 0 deletions packages/core/tests/element-filter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, it, expect } from 'vitest'

import { isUiAutomatorDriver } from '../src/locators/element-filter.js'
import { getSuggestedLocators } from '../src/locators/locator-generation.js'
import type { JSONElement } from '../src/locators/types.js'

describe('isUiAutomatorDriver', () => {
it('reads every UiAutomator driver as Android, whatever its case', () => {
expect(isUiAutomatorDriver('UiAutomator2')).toBe(true)
expect(isUiAutomatorDriver('uiautomator')).toBe(true)
expect(isUiAutomatorDriver('Espresso-UiAutomator2')).toBe(true)
})

it('reads any other driver as iOS', () => {
expect(isUiAutomatorDriver('XCUITest')).toBe(false)
expect(isUiAutomatorDriver('Espresso')).toBe(false)
expect(isUiAutomatorDriver('')).toBe(false)
})
})

describe('getSuggestedLocators platform branch', () => {
const element: JSONElement = {
tagName: 'XCUIElementTypeButton',
attributes: { name: 'Submit', 'resource-id': 'app:id/submit' },
children: [],
path: '0'
}

it('builds Android strategies for a UiAutomator driver', () => {
const xml =
'<hierarchy><android.widget.Button resource-id="app:id/submit"/></hierarchy>'
const strategies = getSuggestedLocators(
{ ...element, tagName: 'android.widget.Button' },
xml,
'UiAutomator2'
).map(([strategy]) => strategy)

expect(strategies).toEqual(['id', 'xpath'])
})

it('builds iOS strategies for any other driver', () => {
const xml = '<AppiumAUT><XCUIElementTypeButton name="Submit"/></AppiumAUT>'
const strategies = getSuggestedLocators(element, xml, 'XCUITest').map(
([strategy]) => strategy
)

expect(strategies).toContain('accessibility-id')
expect(strategies).not.toContain('uiautomator')
})
})
9 changes: 0 additions & 9 deletions packages/nightwatch-devtools/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,6 @@ export { SPEC_FILE_RE as TEST_FILE_PATTERN } from '@wdio/devtools-shared'
/** The CLI flags that name the config file, left out of the selection Run all replays (the backend passes its own). */
export const NIGHTWATCH_CONFIG_FLAGS = ['--config', '-c']

/** Nightwatch config file names to search for, in priority order. */
export const CONFIG_FILENAMES = [
'nightwatch.conf.cjs',
'nightwatch.conf.js',
'nightwatch.conf.ts',
'nightwatch.conf.mjs',
'nightwatch.json'
] as const

/**
* Global key used to share the plugin instance with Cucumber hooks.
* Defined in its own leaf module so the Cucumber hooks can bundle it standalone
Expand Down
9 changes: 3 additions & 6 deletions packages/nightwatch-devtools/src/helpers/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,8 @@ import {
isUserCodeFrame,
normalizeFilePath
} from '@wdio/devtools-core'
import {
TEST_FILE_PATTERN,
CONFIG_FILENAMES,
NIGHTWATCH_CONFIG_FLAGS
} from '../constants.js'
import { NIGHTWATCH_CONFIG_FILENAMES } from '@wdio/devtools-shared'
import { TEST_FILE_PATTERN, NIGHTWATCH_CONFIG_FLAGS } from '../constants.js'
import type {
NightwatchTestCase,
TestFileMetadata,
Expand Down Expand Up @@ -452,7 +449,7 @@ export function resolveNightwatchConfig(): string | undefined {
let dir = process.cwd()
const root = path.parse(dir).root
while (dir && dir !== root) {
for (const file of CONFIG_FILENAMES) {
for (const file of NIGHTWATCH_CONFIG_FILENAMES) {
const candidate = path.join(dir, file)
if (fs.existsSync(candidate)) {
return candidate
Expand Down
Loading
Loading