Skip to content

Commit 18052d5

Browse files
authored
Merge pull request #484 from webdriverio/fix/481-service-assert-target
Name a node:assert target from value provenance in the WDIO service
2 parents 04beaa0 + 22c733f commit 18052d5

31 files changed

Lines changed: 559 additions & 83 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@wdio/devtools-service': patch
3+
'@wdio/selenium-devtools': patch
4+
'@wdio/nightwatch-devtools': patch
5+
---
6+
7+
A `node:assert` row only names an element read in the same test (or Cucumber scenario). An assertion in a later test, such as `assert.ok(true)`, no longer boxes an element an earlier test read the same value from. With the WebdriverIO service, a `browser.getTitle()` issued while one of the service's own title reads is still pending keeps its row in the Actions list.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@wdio/devtools-service': patch
3+
---
4+
5+
A `node:assert` row names the element whose read produced the asserted value, and none for a page-level value: `assert.notEqual(await browser.getTitle(), 'Secure Area')` no longer boxes the last element the test resolved. The test's own `browser.getTitle()` is now a `Page.getTitle` row in the Actions list, as in the other adapters.

‎CLAUDE.md‎

Lines changed: 3 additions & 4 deletions
Large diffs are not rendered by default.

‎packages/conformance/src/allowlist.ts‎

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import {
33
CONDITION_WAIT_READS,
44
HELPER_WAIT_READ,
55
KNOWN_DEBT,
6-
PAGE_TITLE_TEST,
76
RULES
87
} from './constants.js'
98
import { anyOf, hunk } from './gap-matchers.js'
@@ -17,9 +16,8 @@ import type {
1716
Violation
1817
} from './types.js'
1918

20-
const [WAITS, PAGE_TITLE, MISSING] = [
19+
const [WAITS, MISSING] = [
2120
'waits on what the page shows',
22-
PAGE_TITLE_TEST,
2321
'times out waiting for a missing element'
2422
].map(scenarioIndex)
2523

@@ -63,41 +61,12 @@ export const KNOWN_GAPS: KnownGap[] = [
6361
timingDependent: true
6462
},
6563
...BIDI_MODES.flatMap((bidi) => [
66-
...serviceGaps(bidi),
6764
...nightwatchGaps(bidi),
6865
...seleniumGaps(bidi),
6966
...pythonGaps(bidi)
7067
])
7168
]
7269

73-
function serviceGaps(bidi: BidiMode): KnownGap[] {
74-
const adapter = 'devtools-service'
75-
const reason = KNOWN_DEBT.serviceAssertTarget
76-
return [
77-
{
78-
adapter,
79-
bidi,
80-
rules: [RULES.pageAssertNamesNoElement],
81-
only: (message) =>
82-
message === 'assert.notEqual on the page title names h2',
83-
reason
84-
},
85-
{
86-
adapter,
87-
bidi,
88-
rules: [RULES.agreeOnActions],
89-
only: hunk(
90-
PAGE_TITLE,
91-
'Element.getText h2',
92-
adapter,
93-
'Assert.notEqual h2',
94-
'Page.getTitle, Assert.notEqual'
95-
),
96-
reason
97-
}
98-
]
99-
}
100-
10170
/** Nightwatch's scenario gaps in one mode, each an adapter bug with its Known debt entry. */
10271
function nightwatchGaps(bidi: BidiMode): KnownGap[] {
10372
const adapter = 'nightwatch-devtools'

‎packages/conformance/src/constants.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,6 @@ const SELENIUM_WAIT_DEBT =
203203

204204
/** Why each scenario gap exists: the limitation, and the Known debt entry that describes it. */
205205
export const KNOWN_DEBT = {
206-
serviceAssertTarget: `The WebdriverIO service stamps a node:assert row with the last element it resolved and hides the user's own getTitle, so an assertion on the page title boxes the heading read before it. ${debt('The WDIO service boxes the last element it resolved for an assertion on the page title', '#481')}`,
207206
conditionWait: `A passing condition wait shows as its reads in Selenium and Python, which have no row for the wait itself, as one Browser.waitForFunction row in the WebdriverIO service, which hides the reads inside it, and as both in Nightwatch. ${debt('A condition wait shows its reads in some adapters and one wait row in others', '#464')}`,
208207
seleniumWait: `A successful driver.wait() has no row of its own: its polls are findElements rows, which the trace vocabulary leaves unmapped. ${debt(SELENIUM_WAIT_DEBT, '#464')}`,
209208
seleniumFailedWait: `A timed-out driver.wait() is a wait row named after its condition, mapped to Browser.waitForFunction with no target, where the other adapters show Element.waitForExist on the selector. ${debt(SELENIUM_WAIT_DEBT, '#464')}`,

‎packages/core/src/read-value-locators.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ export function rememberReadValue(
5959
}
6060
}
6161

62+
/** Called at each test start: an earlier test's read would otherwise box its element for a later `assert.ok(true)`. */
63+
export function clearReadValues(): void {
64+
selectorByReadValue.clear()
65+
}
66+
6267
/** The element locator this value was read from — undefined when an element read
6368
* wasn't its most recent producer. */
6469
export function selectorForReadValue(value: unknown): string | undefined {

‎packages/core/tests/read-value-locators.test.ts‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, it, expect } from 'vitest'
22
import {
3+
clearReadValues,
34
rememberReadValue,
45
resolveAssertTargetFromArgs,
56
selectorForReadValue
@@ -144,3 +145,18 @@ describe('resolveAssertTargetFromArgs', () => {
144145
).toBe('#handle')
145146
})
146147
})
148+
149+
describe('clearReadValues', () => {
150+
it('forgets every producer, so a later assertion on the same value names nothing', () => {
151+
rememberReadValue(true, '#remember-me')
152+
rememberReadValue('Example Domain', undefined)
153+
clearReadValues()
154+
expect(selectorForReadValue(true)).toBeUndefined()
155+
expect(resolveAssertTargetFromArgs([true])).toBeUndefined()
156+
// The no-element sentinel goes too: a cleared value no longer ends the lookup.
157+
rememberReadValue('expected-after-clear', '#fresh')
158+
expect(
159+
resolveAssertTargetFromArgs(['Example Domain', 'expected-after-clear'])
160+
).toBe('#fresh')
161+
})
162+
})

‎packages/nightwatch-devtools/src/helpers/assertCapture.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,24 @@ const log = logger('@wdio/nightwatch-devtools:assertCapture')
1616
/**
1717
* Patch node:assert once per process. Getters are read at capture time — the
1818
* capturer is created lazily in session init and the test UID changes per test.
19+
* `scopeReadValues` runs before each lookup so an assert that precedes its
20+
* test's first command still cannot resolve against an earlier test's reads.
1921
*/
2022
export function wireAssertCapture(
2123
getCapturer: () => SessionCapturer | undefined,
22-
getTestUid: () => string | undefined
24+
getTestUid: () => string | undefined,
25+
scopeReadValues: () => void = () => {}
2326
): void {
2427
patchNodeAssert(
2528
(cmd) => captureAssert(getCapturer(), getTestUid(), cmd),
2629
(level, message) => log[level](message),
2730
// No handle resolver: Nightwatch commands take selector strings, so an
2831
// element object never reaches an assert — only the value's provenance,
2932
// recorded by the command path (BrowserProxy), can name the target.
30-
(args) => resolveAssertTargetFromArgs(args)
33+
(args) => {
34+
scopeReadValues()
35+
return resolveAssertTargetFromArgs(args)
36+
}
3137
)
3238
}
3339

‎packages/nightwatch-devtools/src/helpers/browserProxy.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { NativeAssertRecorder } from './nativeAssertRecorder.js'
1616
import {
1717
RetryTracker,
1818
beginInputDispatch,
19+
clearReadValues,
1920
rememberReadValue,
2021
toError
2122
} from '@wdio/devtools-core'
@@ -67,6 +68,9 @@ export class BrowserProxy {
6768
*/
6869
private browser: NightwatchBrowser | null = null
6970

71+
/** The test core's value registry currently holds reads for; see {@link scopeReadValuesToRowTest}. */
72+
private readValueTestUid: string | undefined
73+
7074
constructor(
7175
private readonly sessionCapturer: SessionCapturer,
7276
private testManager: TestManager,
@@ -104,6 +108,20 @@ export class BrowserProxy {
104108
return this.rowTest()?.uid
105109
}
106110

111+
/**
112+
* Forget the values an earlier test read once the running test changes. The
113+
* lifecycle reset below fires once per MODULE on the BDD interface, so this
114+
* per-`it` live read is what keeps one `it`'s reads from naming another's
115+
* assertion target. Called before every record and every lookup.
116+
*/
117+
scopeReadValuesToRowTest(): void {
118+
const uid = this.rowTestUid()
119+
if (uid !== this.readValueTestUid) {
120+
clearReadValues()
121+
this.readValueTestUid = uid
122+
}
123+
}
124+
107125
/** Per-command-scope dedup state only. Cucumber calls this per STEP, so it
108126
* must not touch the native-assert buffer — that buffer spans the whole
109127
* scenario and is drained once, at the scenario's pre-quit hook. */
@@ -118,6 +136,8 @@ export class BrowserProxy {
118136
resetTestTracking(): void {
119137
this.resetCommandTracking()
120138
this.assertRecorder.clear()
139+
clearReadValues()
140+
this.readValueTestUid = undefined
121141
}
122142

123143
/** Hand off this test's recorded native-assertion calls and clear the
@@ -437,6 +457,7 @@ export class BrowserProxy {
437457
callbackResult: unknown
438458
): unknown {
439459
const result = serializeCommandResult(callbackResult, methodName)
460+
this.scopeReadValuesToRowTest()
440461
rememberReadValue(result, commandTargetSelector(methodName, logArgs))
441462
return result
442463
}

‎packages/nightwatch-devtools/src/index.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,8 @@ class NightwatchDevToolsPlugin {
400400
// Rows are tagged per-`it` (BrowserProxy.rowTestUid); `#currentTestUid`
401401
// — the lifecycle test, which the BDD interface pins to the module's
402402
// first `it` — is only the fallback, and stays the artifact/slice key.
403-
() => this.browserProxy?.rowTestUid() ?? this.#currentTestUid()
403+
() => this.browserProxy?.rowTestUid() ?? this.#currentTestUid(),
404+
() => this.browserProxy?.scopeReadValuesToRowTest()
404405
)
405406
}
406407
}

0 commit comments

Comments
 (0)