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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy