Skip to content

Commit

Permalink
fix(browser): keep querying elements even if locator is created with …
Browse files Browse the repository at this point in the history
…elementLocator, add pubic @vitest/browser/utils (#6296)
  • Loading branch information
sheremet-va authored Aug 7, 2024
1 parent 73abf30 commit 30dc579
Show file tree
Hide file tree
Showing 19 changed files with 566 additions and 53 deletions.
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

0 comments on commit 30dc579

Please sign in to comment.