Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(browser): keep querying elements even if locator is created with elementLocator, add pubic @vitest/browser/utils #6296

Merged
merged 12 commits into from
Aug 7, 2024
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
node_version: [18, 20]
# Reset back to 20 after https://github.com/nodejs/node/issues/53648
# (The issues is closed, but the error persist even after 20.14)
node_version: [18, 20.14]
# node_version: [18, 20, 22] 22 when LTS is close enough
include:
- os: macos-14
node_version: 20
node_version: 20.14
- os: windows-latest
node_version: 20
node_version: 20.14
fail-fast: false

steps:
Expand Down
4 changes: 4 additions & 0 deletions packages/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@
"types": "./dist/locators/index.d.ts",
"default": "./dist/locators/index.js"
},
"./utils": {
"types": "./utils.d.ts",
"default": "./dist/utils.js"
},
"./*": "./*"
},
"main": "./dist/index.js",
Expand Down
7 changes: 5 additions & 2 deletions packages/browser/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export default () =>
'locators/webdriverio': './src/client/tester/locators/webdriverio.ts',
'locators/preview': './src/client/tester/locators/preview.ts',
'locators/index': './src/client/tester/locators/index.ts',
'utils': './src/client/tester/public-utils.ts',
},
output: {
dir: 'dist',
Expand Down Expand Up @@ -129,9 +130,11 @@ export default () =>
],
},
{
input: './src/client/tester/locators/index.ts',
input: {
'locators/index': './src/client/tester/locators/index.ts',
},
output: {
file: 'dist/locators/index.d.ts',
dir: 'dist',
format: 'esm',
},
external,
Expand Down
12 changes: 3 additions & 9 deletions packages/browser/src/client/tester/locators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type { BrowserRPC } from '@vitest/browser/client'
import {
Ivya,
type ParsedSelector,
asLocator,
getByAltTextSelector,
getByLabelSelector,
getByPlaceholderSelector,
Expand All @@ -24,6 +23,7 @@ import {
import type { WorkerGlobalState } from 'vitest'
import type { BrowserRunnerState } from '../../utils'
import { getBrowserState, getWorkerState } from '../../utils'
import { getElementError } from '../public-utils'

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

private _parsedSelector: ParsedSelector | undefined
protected _container?: Element | undefined
protected _pwSelector?: string | undefined
protected _forceElement?: Element | undefined

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

public query(): Element | null {
if (this._forceElement) {
return this._forceElement
}
const parsedSelector = this._parsedSelector || (this._parsedSelector = selectorEngine.parseSelector(this._pwSelector || this.selector))
return selectorEngine.querySelector(parsedSelector, document.documentElement, true)
}

public element(): Element {
const element = this.query()
if (!element) {
throw new Error(`element not found: ${asLocator('javascript', this._pwSelector || this.selector)}`)
throw getElementError(this._pwSelector || this.selector, this._container || document.documentElement)
}
return element
}

public elements(): Element[] {
if (this._forceElement) {
return [this._forceElement]
}
const parsedSelector = this._parsedSelector || (this._parsedSelector = selectorEngine.parseSelector(this._pwSelector || this.selector))
return selectorEngine.querySelectorAll(parsedSelector, document.documentElement)
}
Expand Down
14 changes: 10 additions & 4 deletions packages/browser/src/client/tester/locators/playwright.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,26 @@ page.extend({
},

elementLocator(element: Element) {
return new PlaywrightLocator(selectorEngine.generateSelectorSimple(element), element)
return new PlaywrightLocator(
selectorEngine.generateSelectorSimple(element),
element,
)
},
})

class PlaywrightLocator extends Locator {
constructor(public selector: string, protected _forceElement?: Element) {
constructor(public selector: string, protected _container?: Element) {
super()
}

protected locator(selector: string) {
return new PlaywrightLocator(`${this.selector} >> ${selector}`)
return new PlaywrightLocator(`${this.selector} >> ${selector}`, this._container)
}

protected elementLocator(element: Element) {
return new PlaywrightLocator(selectorEngine.generateSelectorSimple(element), element)
return new PlaywrightLocator(
selectorEngine.generateSelectorSimple(element),
element,
)
}
}
17 changes: 12 additions & 5 deletions packages/browser/src/client/tester/locators/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getByTitleSelector,
} from 'ivya'
import { convertElementToCssSelector } from '../../utils'
import { getElementError } from '../public-utils'
import { Locator, selectorEngine } from './index'

page.extend({
Expand All @@ -36,19 +37,22 @@ page.extend({
},

elementLocator(element: Element) {
return new PreviewLocator(selectorEngine.generateSelectorSimple(element), element)
return new PreviewLocator(
selectorEngine.generateSelectorSimple(element),
element,
)
},
})

class PreviewLocator extends Locator {
constructor(protected _pwSelector: string, protected _forceElement?: Element) {
constructor(protected _pwSelector: string, protected _container?: Element) {
super()
}

override get selector() {
const selectors = this.elements().map(element => convertElementToCssSelector(element))
if (!selectors.length) {
throw new Error(`element not found: ${this._pwSelector}`)
throw getElementError(this._pwSelector, this._container || document.documentElement)
}
return selectors.join(', ')
}
Expand Down Expand Up @@ -100,10 +104,13 @@ class PreviewLocator extends Locator {
}

protected locator(selector: string) {
return new PreviewLocator(`${this._pwSelector} >> ${selector}`)
return new PreviewLocator(`${this._pwSelector} >> ${selector}`, this._container)
}

protected elementLocator(element: Element) {
return new PreviewLocator(selectorEngine.generateSelectorSimple(element), element)
return new PreviewLocator(
selectorEngine.generateSelectorSimple(element),
element,
)
}
}
9 changes: 5 additions & 4 deletions packages/browser/src/client/tester/locators/webdriverio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getByTitleSelector,
} from 'ivya'
import { convertElementToCssSelector } from '../../utils'
import { getElementError } from '../public-utils'
import { Locator, selectorEngine } from './index'

page.extend({
Expand All @@ -35,19 +36,19 @@ page.extend({
},

elementLocator(element: Element) {
return new WebdriverIOLocator(selectorEngine.generateSelectorSimple(element), element)
return new WebdriverIOLocator(selectorEngine.generateSelectorSimple(element))
},
})

class WebdriverIOLocator extends Locator {
constructor(protected _pwSelector: string, protected _forceElement?: Element) {
constructor(protected _pwSelector: string, protected _container?: Element) {
super()
}

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

protected locator(selector: string) {
return new WebdriverIOLocator(`${this._pwSelector} >> ${selector}`)
return new WebdriverIOLocator(`${this._pwSelector} >> ${selector}`, this._container)
}

protected elementLocator(element: Element) {
Expand Down
72 changes: 72 additions & 0 deletions packages/browser/src/client/tester/public-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { type Locator, type LocatorSelectors, page } from '@vitest/browser/context'
import { type StringifyOptions, stringify } from 'vitest/utils'
import { asLocator } from 'ivya'

export function getElementLocatorSelectors(element: Element): LocatorSelectors {
const locator = page.elementLocator(element)
return {
getByAltText: (altText, options) => locator.getByAltText(altText, options),
getByLabelText: (labelText, options) => locator.getByLabelText(labelText, options),
getByPlaceholder: (placeholderText, options) => locator.getByPlaceholder(placeholderText, options),
getByRole: (role, options) => locator.getByRole(role, options),
getByTestId: testId => locator.getByTestId(testId),
getByText: (text, options) => locator.getByText(text, options),
getByTitle: (title, options) => locator.getByTitle(title, options),
}
}

type PrettyDOMOptions = Omit<StringifyOptions, 'maxLength'>

export function debug(
el?: Element | Locator | null | (Element | Locator)[],
maxLength?: number,
options?: PrettyDOMOptions,
): void {
if (Array.isArray(el)) {
// eslint-disable-next-line no-console
el.forEach(e => console.log(prettyDOM(e, maxLength, options)))
}
else {
// eslint-disable-next-line no-console
console.log(prettyDOM(el, maxLength, options))
}
}

export function prettyDOM(
dom?: Element | Locator | undefined | null,
maxLength: number = Number(import.meta.env.DEBUG_PRINT_LIMIT ?? 7000),
prettyFormatOptions: PrettyDOMOptions = {},
): string {
if (maxLength === 0) {
return ''
}

if (!dom) {
dom = document.body
}

if ('element' in dom && 'all' in dom) {
dom = dom.element()
}

const type = typeof dom
if (type !== 'object' || !dom.outerHTML) {
const typeName = type === 'object' ? dom.constructor.name : type
throw new TypeError(`Expecting a valid DOM element, but got ${typeName}.`)
}

const pretty = stringify(dom, Number.POSITIVE_INFINITY, {
maxLength,
highlight: true,
...prettyFormatOptions,
})
return dom.outerHTML.length > maxLength
? `${pretty.slice(0, maxLength)}...`
: pretty
}

export function getElementError(selector: string, container: Element): Error {
const error = new Error(`Cannot find element with locator: ${asLocator('javascript', selector)}\n\n${prettyDOM(container)}`)
error.name = 'VitestBrowserElementError'
return error
}
2 changes: 1 addition & 1 deletion packages/browser/src/client/tester/tester.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
{__VITEST_INTERNAL_SCRIPTS__}
{__VITEST_SCRIPTS__}
</head>
<body data-vitest-body>
<body>
<script type="module" src="./tester.ts"></script>
{__VITEST_APPEND__}
</body>
Expand Down
15 changes: 15 additions & 0 deletions packages/browser/src/client/tester/tester.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SpyModule, collectTests, setupCommonEnv, startTests } from 'vitest/browser'
import { page } from '@vitest/browser/context'
import { channel, client, onCancel } from '@vitest/browser/client'
import { getBrowserState, getConfig, getWorkerState } from '../utils'
import { setupDialogsSpy } from './dialog'
Expand All @@ -8,6 +9,8 @@ import { browserHashMap, initiateRunner } from './runner'
import { VitestBrowserClientMocker } from './mocker'
import { setupExpectDom } from './expect-element'

const cleanupSymbol = Symbol.for('vitest:component-cleanup')

const url = new URL(location.href)
const reloadStart = url.searchParams.get('__reloadStart')

Expand Down Expand Up @@ -123,6 +126,18 @@ async function executeTests(method: 'run' | 'collect', files: string[]) {
}
}
finally {
try {
if (cleanupSymbol in page) {
(page[cleanupSymbol] as any)()
}
}
catch (error: any) {
await client.rpc.onUnhandledError({
name: error.name,
message: error.message,
stack: String(error.stack),
}, 'Cleanup Error')
}
state.environmentTeardownRun = true
debug('finished running tests')
done(files)
Expand Down
21 changes: 21 additions & 0 deletions packages/browser/utils.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// should be in sync with tester/public-utils.ts
// we cannot bundle it because vitest depend on the @vitest/browser and vise versa
// fortunately, the file is quite small

import { LocatorSelectors } from '@vitest/browser/context'
import { StringifyOptions } from 'vitest/utils'

type PrettyDOMOptions = Omit<StringifyOptions, 'maxLength'>

export declare function getElementLocatorSelectors(element: Element): LocatorSelectors
export declare function debug(
el?: Element | Locator | null | (Element | Locator)[],
maxLength?: number,
options?: PrettyDOMOptions,
): void
export declare function prettyDOM(
dom?: Element | Locator | undefined | null,
maxLength?: number,
prettyFormatOptions?: PrettyDOMOptions,
): string
export declare function getElementError(selector: string, container?: Element): Error
6 changes: 5 additions & 1 deletion packages/utils/src/display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,14 @@ const PLUGINS = [
AsymmetricMatcher,
]

export interface StringifyOptions extends PrettyFormatOptions {
maxLength?: number
}

export function stringify(
object: unknown,
maxDepth = 10,
{ maxLength, ...options }: PrettyFormatOptions & { maxLength?: number } = {},
{ maxLength, ...options }: StringifyOptions = {},
): string {
const MAX_LENGTH = maxLength ?? 10000
let result
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export {
inspect,
objDisplay,
} from './display'
export type { StringifyOptions } from './display'
export {
positionToOffset,
offsetToLineNumber,
Expand Down
Loading
Loading