Skip to content

Commit 30dc579

Browse files
authored
fix(browser): keep querying elements even if locator is created with elementLocator, add pubic @vitest/browser/utils (#6296)
1 parent 73abf30 commit 30dc579

File tree

19 files changed

+566
-53
lines changed

19 files changed

+566
-53
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,15 @@ jobs:
4040
strategy:
4141
matrix:
4242
os: [ubuntu-latest]
43-
node_version: [18, 20]
43+
# Reset back to 20 after https://github.com/nodejs/node/issues/53648
44+
# (The issues is closed, but the error persist even after 20.14)
45+
node_version: [18, 20.14]
4446
# node_version: [18, 20, 22] 22 when LTS is close enough
4547
include:
4648
- os: macos-14
47-
node_version: 20
49+
node_version: 20.14
4850
- os: windows-latest
49-
node_version: 20
51+
node_version: 20.14
5052
fail-fast: false
5153

5254
steps:

packages/browser/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
"types": "./dist/locators/index.d.ts",
4545
"default": "./dist/locators/index.js"
4646
},
47+
"./utils": {
48+
"types": "./utils.d.ts",
49+
"default": "./dist/utils.js"
50+
},
4751
"./*": "./*"
4852
},
4953
"main": "./dist/index.js",

packages/browser/rollup.config.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export default () =>
6666
'locators/webdriverio': './src/client/tester/locators/webdriverio.ts',
6767
'locators/preview': './src/client/tester/locators/preview.ts',
6868
'locators/index': './src/client/tester/locators/index.ts',
69+
'utils': './src/client/tester/public-utils.ts',
6970
},
7071
output: {
7172
dir: 'dist',
@@ -129,9 +130,11 @@ export default () =>
129130
],
130131
},
131132
{
132-
input: './src/client/tester/locators/index.ts',
133+
input: {
134+
'locators/index': './src/client/tester/locators/index.ts',
135+
},
133136
output: {
134-
file: 'dist/locators/index.d.ts',
137+
dir: 'dist',
135138
format: 'esm',
136139
},
137140
external,

packages/browser/src/client/tester/locators/index.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type { BrowserRPC } from '@vitest/browser/client'
1212
import {
1313
Ivya,
1414
type ParsedSelector,
15-
asLocator,
1615
getByAltTextSelector,
1716
getByLabelSelector,
1817
getByPlaceholderSelector,
@@ -24,6 +23,7 @@ import {
2423
import type { WorkerGlobalState } from 'vitest'
2524
import type { BrowserRunnerState } from '../../utils'
2625
import { getBrowserState, getWorkerState } from '../../utils'
26+
import { getElementError } from '../public-utils'
2727

2828
// we prefer using playwright locators because they are more powerful and support Shadow DOM
2929
export const selectorEngine = Ivya.create({
@@ -45,8 +45,8 @@ export abstract class Locator {
4545
public abstract selector: string
4646

4747
private _parsedSelector: ParsedSelector | undefined
48+
protected _container?: Element | undefined
4849
protected _pwSelector?: string | undefined
49-
protected _forceElement?: Element | undefined
5050

5151
public click(options: UserEventClickOptions = {}): Promise<void> {
5252
return this.triggerCommand<void>('__vitest_click', this.selector, options)
@@ -143,25 +143,19 @@ export abstract class Locator {
143143
}
144144

145145
public query(): Element | null {
146-
if (this._forceElement) {
147-
return this._forceElement
148-
}
149146
const parsedSelector = this._parsedSelector || (this._parsedSelector = selectorEngine.parseSelector(this._pwSelector || this.selector))
150147
return selectorEngine.querySelector(parsedSelector, document.documentElement, true)
151148
}
152149

153150
public element(): Element {
154151
const element = this.query()
155152
if (!element) {
156-
throw new Error(`element not found: ${asLocator('javascript', this._pwSelector || this.selector)}`)
153+
throw getElementError(this._pwSelector || this.selector, this._container || document.documentElement)
157154
}
158155
return element
159156
}
160157

161158
public elements(): Element[] {
162-
if (this._forceElement) {
163-
return [this._forceElement]
164-
}
165159
const parsedSelector = this._parsedSelector || (this._parsedSelector = selectorEngine.parseSelector(this._pwSelector || this.selector))
166160
return selectorEngine.querySelectorAll(parsedSelector, document.documentElement)
167161
}

packages/browser/src/client/tester/locators/playwright.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,26 @@ page.extend({
3434
},
3535

3636
elementLocator(element: Element) {
37-
return new PlaywrightLocator(selectorEngine.generateSelectorSimple(element), element)
37+
return new PlaywrightLocator(
38+
selectorEngine.generateSelectorSimple(element),
39+
element,
40+
)
3841
},
3942
})
4043

4144
class PlaywrightLocator extends Locator {
42-
constructor(public selector: string, protected _forceElement?: Element) {
45+
constructor(public selector: string, protected _container?: Element) {
4346
super()
4447
}
4548

4649
protected locator(selector: string) {
47-
return new PlaywrightLocator(`${this.selector} >> ${selector}`)
50+
return new PlaywrightLocator(`${this.selector} >> ${selector}`, this._container)
4851
}
4952

5053
protected elementLocator(element: Element) {
51-
return new PlaywrightLocator(selectorEngine.generateSelectorSimple(element), element)
54+
return new PlaywrightLocator(
55+
selectorEngine.generateSelectorSimple(element),
56+
element,
57+
)
5258
}
5359
}

packages/browser/src/client/tester/locators/preview.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
getByTitleSelector,
1111
} from 'ivya'
1212
import { convertElementToCssSelector } from '../../utils'
13+
import { getElementError } from '../public-utils'
1314
import { Locator, selectorEngine } from './index'
1415

1516
page.extend({
@@ -36,19 +37,22 @@ page.extend({
3637
},
3738

3839
elementLocator(element: Element) {
39-
return new PreviewLocator(selectorEngine.generateSelectorSimple(element), element)
40+
return new PreviewLocator(
41+
selectorEngine.generateSelectorSimple(element),
42+
element,
43+
)
4044
},
4145
})
4246

4347
class PreviewLocator extends Locator {
44-
constructor(protected _pwSelector: string, protected _forceElement?: Element) {
48+
constructor(protected _pwSelector: string, protected _container?: Element) {
4549
super()
4650
}
4751

4852
override get selector() {
4953
const selectors = this.elements().map(element => convertElementToCssSelector(element))
5054
if (!selectors.length) {
51-
throw new Error(`element not found: ${this._pwSelector}`)
55+
throw getElementError(this._pwSelector, this._container || document.documentElement)
5256
}
5357
return selectors.join(', ')
5458
}
@@ -100,10 +104,13 @@ class PreviewLocator extends Locator {
100104
}
101105

102106
protected locator(selector: string) {
103-
return new PreviewLocator(`${this._pwSelector} >> ${selector}`)
107+
return new PreviewLocator(`${this._pwSelector} >> ${selector}`, this._container)
104108
}
105109

106110
protected elementLocator(element: Element) {
107-
return new PreviewLocator(selectorEngine.generateSelectorSimple(element), element)
111+
return new PreviewLocator(
112+
selectorEngine.generateSelectorSimple(element),
113+
element,
114+
)
108115
}
109116
}

packages/browser/src/client/tester/locators/webdriverio.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
getByTitleSelector,
1010
} from 'ivya'
1111
import { convertElementToCssSelector } from '../../utils'
12+
import { getElementError } from '../public-utils'
1213
import { Locator, selectorEngine } from './index'
1314

1415
page.extend({
@@ -35,19 +36,19 @@ page.extend({
3536
},
3637

3738
elementLocator(element: Element) {
38-
return new WebdriverIOLocator(selectorEngine.generateSelectorSimple(element), element)
39+
return new WebdriverIOLocator(selectorEngine.generateSelectorSimple(element))
3940
},
4041
})
4142

4243
class WebdriverIOLocator extends Locator {
43-
constructor(protected _pwSelector: string, protected _forceElement?: Element) {
44+
constructor(protected _pwSelector: string, protected _container?: Element) {
4445
super()
4546
}
4647

4748
override get selector() {
4849
const selectors = this.elements().map(element => convertElementToCssSelector(element))
4950
if (!selectors.length) {
50-
throw new Error(`element not found: ${this._pwSelector}`)
51+
throw getElementError(this._pwSelector, this._container || document.documentElement)
5152
}
5253
return selectors.join(', ')
5354
}
@@ -58,7 +59,7 @@ class WebdriverIOLocator extends Locator {
5859
}
5960

6061
protected locator(selector: string) {
61-
return new WebdriverIOLocator(`${this._pwSelector} >> ${selector}`)
62+
return new WebdriverIOLocator(`${this._pwSelector} >> ${selector}`, this._container)
6263
}
6364

6465
protected elementLocator(element: Element) {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { type Locator, type LocatorSelectors, page } from '@vitest/browser/context'
2+
import { type StringifyOptions, stringify } from 'vitest/utils'
3+
import { asLocator } from 'ivya'
4+
5+
export function getElementLocatorSelectors(element: Element): LocatorSelectors {
6+
const locator = page.elementLocator(element)
7+
return {
8+
getByAltText: (altText, options) => locator.getByAltText(altText, options),
9+
getByLabelText: (labelText, options) => locator.getByLabelText(labelText, options),
10+
getByPlaceholder: (placeholderText, options) => locator.getByPlaceholder(placeholderText, options),
11+
getByRole: (role, options) => locator.getByRole(role, options),
12+
getByTestId: testId => locator.getByTestId(testId),
13+
getByText: (text, options) => locator.getByText(text, options),
14+
getByTitle: (title, options) => locator.getByTitle(title, options),
15+
}
16+
}
17+
18+
type PrettyDOMOptions = Omit<StringifyOptions, 'maxLength'>
19+
20+
export function debug(
21+
el?: Element | Locator | null | (Element | Locator)[],
22+
maxLength?: number,
23+
options?: PrettyDOMOptions,
24+
): void {
25+
if (Array.isArray(el)) {
26+
// eslint-disable-next-line no-console
27+
el.forEach(e => console.log(prettyDOM(e, maxLength, options)))
28+
}
29+
else {
30+
// eslint-disable-next-line no-console
31+
console.log(prettyDOM(el, maxLength, options))
32+
}
33+
}
34+
35+
export function prettyDOM(
36+
dom?: Element | Locator | undefined | null,
37+
maxLength: number = Number(import.meta.env.DEBUG_PRINT_LIMIT ?? 7000),
38+
prettyFormatOptions: PrettyDOMOptions = {},
39+
): string {
40+
if (maxLength === 0) {
41+
return ''
42+
}
43+
44+
if (!dom) {
45+
dom = document.body
46+
}
47+
48+
if ('element' in dom && 'all' in dom) {
49+
dom = dom.element()
50+
}
51+
52+
const type = typeof dom
53+
if (type !== 'object' || !dom.outerHTML) {
54+
const typeName = type === 'object' ? dom.constructor.name : type
55+
throw new TypeError(`Expecting a valid DOM element, but got ${typeName}.`)
56+
}
57+
58+
const pretty = stringify(dom, Number.POSITIVE_INFINITY, {
59+
maxLength,
60+
highlight: true,
61+
...prettyFormatOptions,
62+
})
63+
return dom.outerHTML.length > maxLength
64+
? `${pretty.slice(0, maxLength)}...`
65+
: pretty
66+
}
67+
68+
export function getElementError(selector: string, container: Element): Error {
69+
const error = new Error(`Cannot find element with locator: ${asLocator('javascript', selector)}\n\n${prettyDOM(container)}`)
70+
error.name = 'VitestBrowserElementError'
71+
return error
72+
}

packages/browser/src/client/tester/tester.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
{__VITEST_INTERNAL_SCRIPTS__}
2222
{__VITEST_SCRIPTS__}
2323
</head>
24-
<body data-vitest-body>
24+
<body>
2525
<script type="module" src="./tester.ts"></script>
2626
{__VITEST_APPEND__}
2727
</body>

packages/browser/src/client/tester/tester.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { SpyModule, collectTests, setupCommonEnv, startTests } from 'vitest/browser'
2+
import { page } from '@vitest/browser/context'
23
import { channel, client, onCancel } from '@vitest/browser/client'
34
import { getBrowserState, getConfig, getWorkerState } from '../utils'
45
import { setupDialogsSpy } from './dialog'
@@ -8,6 +9,8 @@ import { browserHashMap, initiateRunner } from './runner'
89
import { VitestBrowserClientMocker } from './mocker'
910
import { setupExpectDom } from './expect-element'
1011

12+
const cleanupSymbol = Symbol.for('vitest:component-cleanup')
13+
1114
const url = new URL(location.href)
1215
const reloadStart = url.searchParams.get('__reloadStart')
1316

@@ -123,6 +126,18 @@ async function executeTests(method: 'run' | 'collect', files: string[]) {
123126
}
124127
}
125128
finally {
129+
try {
130+
if (cleanupSymbol in page) {
131+
(page[cleanupSymbol] as any)()
132+
}
133+
}
134+
catch (error: any) {
135+
await client.rpc.onUnhandledError({
136+
name: error.name,
137+
message: error.message,
138+
stack: String(error.stack),
139+
}, 'Cleanup Error')
140+
}
126141
state.environmentTeardownRun = true
127142
debug('finished running tests')
128143
done(files)

0 commit comments

Comments
 (0)