diff --git a/packages/browser/package.json b/packages/browser/package.json index c9ff22e069bf..a43aa0686ffe 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -97,6 +97,7 @@ "@wdio/protocols": "^8.38.0", "birpc": "0.2.17", "flatted": "^3.3.1", + "ivya": "^1.1.0", "pathe": "^1.1.2", "periscopic": "^4.0.2", "playwright": "^1.45.3", diff --git a/packages/browser/src/client/tester/context.ts b/packages/browser/src/client/tester/context.ts index 568dad4c6696..40d2ff0c5bbc 100644 --- a/packages/browser/src/client/tester/context.ts +++ b/packages/browser/src/client/tester/context.ts @@ -1,4 +1,4 @@ -import type { Task } from 'vitest' +import type { RunnerTask } from 'vitest' import type { BrowserRPC } from '@vitest/browser/client' import type { BrowserPage, @@ -46,10 +46,10 @@ function createUserEvent(): UserEvent { return convertToLocator(element).selectOptions(value) }, async type(element: Element | Locator, text: string, options: UserEventTypeOptions = {}) { - const css = convertToLocator(element).selector + const selector = convertToSelector(element) const { unreleased } = await triggerCommand<{ unreleased: string[] }>( '__vitest_type', - css, + selector, text, { ...options, unreleased: keyboard.unreleased }, ) @@ -195,6 +195,6 @@ function convertToSelector(elementOrLocator: Element | Locator): string { throw new Error('Expected element or locator to be an instance of Element or Locator.') } -function getTaskFullName(task: Task): string { +function getTaskFullName(task: RunnerTask): string { return task.suite ? `${getTaskFullName(task.suite)} ${task.name}` : task.name } diff --git a/packages/browser/src/client/tester/locators/index.ts b/packages/browser/src/client/tester/locators/index.ts index 9ad978f7b4c5..f15481d487f9 100644 --- a/packages/browser/src/client/tester/locators/index.ts +++ b/packages/browser/src/client/tester/locators/index.ts @@ -6,19 +6,39 @@ import type { UserEventDragAndDropOptions, UserEventFillOptions, } from '@vitest/browser/context' -import { page } from '@vitest/browser/context' +import { page, server } from '@vitest/browser/context' import type { BrowserRPC } from '@vitest/browser/client' +import { + Ivya, + type ParsedSelector, + asLocator, + getByAltTextSelector, + getByLabelSelector, + getByPlaceholderSelector, + getByRoleSelector, + getByTestIdSelector, + getByTextSelector, + getByTitleSelector, +} from 'ivya' import type { WorkerGlobalState } from 'vitest' import type { BrowserRunnerState } from '../../utils' import { getBrowserState, getWorkerState } from '../../utils' -import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from './playwright-selector/locatorUtils' -import type { ParsedSelector } from './playwright-selector/selectorParser' -import { parseSelector } from './playwright-selector/selectorParser' -import { PlaywrightSelector } from './playwright-selector/selector' -import { asLocator } from './playwright-selector/locatorGenerators' -// we prefer using playwright locators because they are more powerful and support Shdow DOM -export const selectorEngine = new PlaywrightSelector() +// we prefer using playwright locators because they are more powerful and support Shadow DOM +export const selectorEngine = Ivya.create({ + browser: ((name: string) => { + switch (name) { + case 'edge': + case 'chrome': + return 'chromium' + case 'safari': + return 'webkit' + default: + return name as 'webkit' | 'firefox' | 'chromium' + } + })(server.config.browser.name), + testIdAttribute: server.config.browser.locators.testIdAttribute, +}) export abstract class Locator { public abstract selector: string @@ -110,7 +130,7 @@ export abstract class Locator { } public getByTestId(testId: string | RegExp): Locator { - return this.locator(getByTestIdSelector(page.config.browser.locators.testIdAttribute, testId)) + return this.locator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId)) } public getByText(text: string | RegExp, options?: LocatorOptions): Locator { @@ -125,7 +145,7 @@ export abstract class Locator { if (this._forceElement) { return this._forceElement } - const parsedSelector = this._parsedSelector || (this._parsedSelector = parseSelector(this._pwSelector || this.selector)) + const parsedSelector = this._parsedSelector || (this._parsedSelector = selectorEngine.parseSelector(this._pwSelector || this.selector)) return selectorEngine.querySelector(parsedSelector, document.body, true) } @@ -141,7 +161,7 @@ export abstract class Locator { if (this._forceElement) { return [this._forceElement] } - const parsedSelector = this._parsedSelector || (this._parsedSelector = parseSelector(this._pwSelector || this.selector)) + const parsedSelector = this._parsedSelector || (this._parsedSelector = selectorEngine.parseSelector(this._pwSelector || this.selector)) return selectorEngine.querySelectorAll(parsedSelector, document.body) } diff --git a/packages/browser/src/client/tester/locators/playwright-selector/cssParser.ts b/packages/browser/src/client/tester/locators/playwright-selector/cssParser.ts deleted file mode 100644 index 15c762d05e07..000000000000 --- a/packages/browser/src/client/tester/locators/playwright-selector/cssParser.ts +++ /dev/null @@ -1,281 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// copied without changes from https://github.com/microsoft/playwright/blob/4554372e456154d7365b6902ef9f3e1e7de76e94/packages/playwright-core/src/utils/isomorphic/cssParser.ts - -import * as css from './cssTokenizer' - -export class InvalidSelectorError extends Error { -} - -export function isInvalidSelectorError(error: Error) { - return error instanceof InvalidSelectorError -} - -// Note: '>=' is used internally for text engine to preserve backwards compatibility. -type ClauseCombinator = '' | '>' | '+' | '~' | '>=' -// TODO: consider -// - key=value -// - operators like `=`, `|=`, `~=`, `*=`, `/` -// - ~=value -// - argument modes: "parse all", "parse commas", "just a string" -export type CSSFunctionArgument = CSSComplexSelector | number | string -export interface CSSFunction { name: string; args: CSSFunctionArgument[] } -export interface CSSSimpleSelector { css?: string; functions: CSSFunction[] } -export interface CSSComplexSelector { simples: { selector: CSSSimpleSelector; combinator: ClauseCombinator }[] } -export type CSSComplexSelectorList = CSSComplexSelector[] - -export function parseCSS(selector: string, customNames: Set): { selector: CSSComplexSelectorList; names: string[] } { - let tokens: css.CSSTokenInterface[] - try { - tokens = css.tokenize(selector) - if (!(tokens[tokens.length - 1] instanceof css.EOFToken)) { - tokens.push(new css.EOFToken()) - } - } - catch (e: any) { - const newMessage = `${e.message} while parsing selector "${selector}"` - const index = (e.stack || '').indexOf(e.message) - if (index !== -1) { - e.stack = e.stack.substring(0, index) + newMessage + e.stack.substring(index + e.message.length) - } - e.message = newMessage - throw e - } - const unsupportedToken = tokens.find((token) => { - return (token instanceof css.AtKeywordToken) - || (token instanceof css.BadStringToken) - || (token instanceof css.BadURLToken) - || (token instanceof css.ColumnToken) - || (token instanceof css.CDOToken) - || (token instanceof css.CDCToken) - || (token instanceof css.SemicolonToken) - // TODO: Consider using these for something, e.g. to escape complex strings. - // For example :xpath{ (//div/bar[@attr="foo"])[2]/baz } - // Or this way :xpath( {complex-xpath-goes-here("hello")} ) - || (token instanceof css.OpenCurlyToken) - || (token instanceof css.CloseCurlyToken) - // TODO: Consider treating these as strings? - || (token instanceof css.URLToken) - || (token instanceof css.PercentageToken) - }) - if (unsupportedToken) { - throw new InvalidSelectorError(`Unsupported token "${unsupportedToken.toSource()}" while parsing selector "${selector}"`) - } - - let pos = 0 - const names = new Set() - - function unexpected() { - return new InvalidSelectorError(`Unexpected token "${tokens[pos].toSource()}" while parsing selector "${selector}"`) - } - - function skipWhitespace() { - while (tokens[pos] instanceof css.WhitespaceToken) { - pos++ - } - } - - function isIdent(p = pos) { - return tokens[p] instanceof css.IdentToken - } - - function isString(p = pos) { - return tokens[p] instanceof css.StringToken - } - - function isNumber(p = pos) { - return tokens[p] instanceof css.NumberToken - } - - function isComma(p = pos) { - return tokens[p] instanceof css.CommaToken - } - - function isOpenParen(p = pos) { - return tokens[p] instanceof css.OpenParenToken - } - - function isCloseParen(p = pos) { - return tokens[p] instanceof css.CloseParenToken - } - - function isFunction(p = pos) { - return tokens[p] instanceof css.FunctionToken - } - - function isStar(p = pos) { - return (tokens[p] instanceof css.DelimToken) && tokens[p].value === '*' - } - - function isEOF(p = pos) { - return tokens[p] instanceof css.EOFToken - } - - function isClauseCombinator(p = pos) { - return (tokens[p] instanceof css.DelimToken) && (['>', '+', '~'].includes(tokens[p].value as string)) - } - - function isSelectorClauseEnd(p = pos) { - return isComma(p) || isCloseParen(p) || isEOF(p) || isClauseCombinator(p) || (tokens[p] instanceof css.WhitespaceToken) - } - - function consumeFunctionArguments(): CSSFunctionArgument[] { - const result = [consumeArgument()] - while (true) { - skipWhitespace() - if (!isComma()) { - break - } - pos++ - result.push(consumeArgument()) - } - return result - } - - function consumeArgument(): CSSFunctionArgument { - skipWhitespace() - if (isNumber()) { - return tokens[pos++].value! - } - if (isString()) { - return tokens[pos++].value! - } - return consumeComplexSelector() - } - - function consumeComplexSelector(): CSSComplexSelector { - const result: CSSComplexSelector = { simples: [] } - skipWhitespace() - if (isClauseCombinator()) { - // Put implicit ":scope" at the start. https://drafts.csswg.org/selectors-4/#relative - result.simples.push({ selector: { functions: [{ name: 'scope', args: [] }] }, combinator: '' }) - } - else { - result.simples.push({ selector: consumeSimpleSelector(), combinator: '' }) - } - while (true) { - skipWhitespace() - if (isClauseCombinator()) { - result.simples[result.simples.length - 1].combinator = tokens[pos++].value as ClauseCombinator - skipWhitespace() - } - else if (isSelectorClauseEnd()) { - break - } - result.simples.push({ combinator: '', selector: consumeSimpleSelector() }) - } - return result - } - - function consumeSimpleSelector(): CSSSimpleSelector { - let rawCSSString = '' - const functions: CSSFunction[] = [] - - while (!isSelectorClauseEnd()) { - if (isIdent() || isStar()) { - rawCSSString += tokens[pos++].toSource() - } - else if (tokens[pos] instanceof css.HashToken) { - rawCSSString += tokens[pos++].toSource() - } - else if ((tokens[pos] instanceof css.DelimToken) && tokens[pos].value === '.') { - pos++ - if (isIdent()) { - rawCSSString += `.${tokens[pos++].toSource()}` - } - else { throw unexpected() } - } - else if (tokens[pos] instanceof css.ColonToken) { - pos++ - if (isIdent()) { - if (!customNames.has((tokens[pos].value as string).toLowerCase())) { - rawCSSString += `:${tokens[pos++].toSource()}` - } - else { - const name = (tokens[pos++].value as string).toLowerCase() - functions.push({ name, args: [] }) - names.add(name) - } - } - else if (isFunction()) { - const name = (tokens[pos++].value as string).toLowerCase() - if (!customNames.has(name)) { - rawCSSString += `:${name}(${consumeBuiltinFunctionArguments()})` - } - else { - functions.push({ name, args: consumeFunctionArguments() }) - names.add(name) - } - skipWhitespace() - if (!isCloseParen()) { - throw unexpected() - } - pos++ - } - else { - throw unexpected() - } - } - else if (tokens[pos] instanceof css.OpenSquareToken) { - rawCSSString += '[' - pos++ - while (!(tokens[pos] instanceof css.CloseSquareToken) && !isEOF()) { - rawCSSString += tokens[pos++].toSource() - } - if (!(tokens[pos] instanceof css.CloseSquareToken)) { - throw unexpected() - } - rawCSSString += ']' - pos++ - } - else { - throw unexpected() - } - } - if (!rawCSSString && !functions.length) { - throw unexpected() - } - return { css: rawCSSString || undefined, functions } - } - - function consumeBuiltinFunctionArguments(): string { - let s = '' - let balance = 1 // First open paren is a part of a function token. - while (!isEOF()) { - if (isOpenParen() || isFunction()) { - balance++ - } - if (isCloseParen()) { - balance-- - } - if (!balance) { - break - } - s += tokens[pos++].toSource() - } - return s - } - - const result = consumeFunctionArguments() - if (!isEOF()) { - throw unexpected() - } - if (result.some(arg => typeof arg !== 'object' || !('simples' in arg))) { - throw new InvalidSelectorError(`Error while parsing selector "${selector}"`) - } - return { selector: result as CSSComplexSelector[], names: Array.from(names) } -} diff --git a/packages/browser/src/client/tester/locators/playwright-selector/cssTokenizer.ts b/packages/browser/src/client/tester/locators/playwright-selector/cssTokenizer.ts deleted file mode 100644 index d21953c15cff..000000000000 --- a/packages/browser/src/client/tester/locators/playwright-selector/cssTokenizer.ts +++ /dev/null @@ -1,1175 +0,0 @@ -/* eslint-disable ts/no-use-before-define */ -/* - * The code in this file is licensed under the CC0 license. - * http://creativecommons.org/publicdomain/zero/1.0/ - * It is free to use for any purpose. No attribution, permission, or reproduction of this license is required. - */ - -// Original at https://github.com/tabatkins/parse-css -// Changes: -// - JS is replaced with TS. -// - Universal Module Definition wrapper is removed. -// - Everything not related to tokenizing - below the first exports block - is removed. - -export interface CSSTokenInterface { - toSource: () => string - value: string | number | undefined -} - -const between = function (num: number, first: number, last: number) { - return num >= first && num <= last -} -function digit(code: number) { - return between(code, 0x30, 0x39) -} -function hexdigit(code: number) { - return digit(code) || between(code, 0x41, 0x46) || between(code, 0x61, 0x66) -} -function uppercaseletter(code: number) { - return between(code, 0x41, 0x5A) -} -function lowercaseletter(code: number) { - return between(code, 0x61, 0x7A) -} -function letter(code: number) { - return uppercaseletter(code) || lowercaseletter(code) -} -function nonascii(code: number) { - return code >= 0x80 -} -function namestartchar(code: number) { - return letter(code) || nonascii(code) || code === 0x5F -} -function namechar(code: number) { - return namestartchar(code) || digit(code) || code === 0x2D -} -function nonprintable(code: number) { - return between(code, 0, 8) || code === 0xB || between(code, 0xE, 0x1F) || code === 0x7F -} -function newline(code: number) { - return code === 0xA -} -function whitespace(code: number) { - return newline(code) || code === 9 || code === 0x20 -} - -const maximumallowedcodepoint = 0x10FFFF - -export class InvalidCharacterError extends Error { - constructor(message: string) { - super(message) - this.name = 'InvalidCharacterError' - } -} - -function preprocess(str: string): number[] { - // Turn a string into an array of code points, - // following the preprocessing cleanup rules. - const codepoints = [] - for (let i = 0; i < str.length; i++) { - let code = str.charCodeAt(i) - if (code === 0xD && str.charCodeAt(i + 1) === 0xA) { - code = 0xA - i++ - } - if (code === 0xD || code === 0xC) { - code = 0xA - } - if (code === 0x0) { - code = 0xFFFD - } - if (between(code, 0xD800, 0xDBFF) && between(str.charCodeAt(i + 1), 0xDC00, 0xDFFF)) { - // Decode a surrogate pair into an astral codepoint. - const lead = code - 0xD800 - const trail = str.charCodeAt(i + 1) - 0xDC00 - code = 2 ** 16 + lead * 2 ** 10 + trail - i++ - } - codepoints.push(code) - } - return codepoints -} - -function stringFromCode(code: number) { - if (code <= 0xFFFF) { - return String.fromCharCode(code) - } - // Otherwise, encode astral char as surrogate pair. - code -= 2 ** 16 - const lead = Math.floor(code / 2 ** 10) + 0xD800 - const trail = code % 2 ** 10 + 0xDC00 - return String.fromCharCode(lead) + String.fromCharCode(trail) -} - -export function tokenize(str1: string): CSSTokenInterface[] { - const str = preprocess(str1) - let i = -1 - const tokens: CSSTokenInterface[] = [] - let code: number - - // Line number information. - let line = 0 - let column = 0 - // The only use of lastLineLength is in reconsume(). - let lastLineLength = 0 - const incrLineno = function () { - line += 1 - lastLineLength = column - column = 0 - } - const locStart = { line, column } - - const codepoint = function (i: number): number { - if (i >= str.length) { - return -1 - } - - return str[i] - } - const next = function (num?: number) { - if (num === undefined) { - num = 1 - } - if (num > 3) { - // eslint-disable-next-line no-throw-literal - throw 'Spec Error: no more than three codepoints of lookahead.' - } - return codepoint(i + num) - } - const consume = function (num?: number): boolean { - if (num === undefined) { - num = 1 - } - i += num - code = codepoint(i) - if (newline(code)) { - incrLineno() - } - else { column += num } - // console.log('Consume '+i+' '+String.fromCharCode(code) + ' 0x' + code.toString(16)); - return true - } - const reconsume = function () { - i -= 1 - if (newline(code)) { - line -= 1 - column = lastLineLength - } - else { - column -= 1 - } - locStart.line = line - locStart.column = column - return true - } - const eof = function (codepoint?: number): boolean { - if (codepoint === undefined) { - codepoint = code - } - return codepoint === -1 - } - const donothing = function () { } - const parseerror = function () { - // Language bindings don't like writing to stdout! - // console.log('Parse error at index ' + i + ', processing codepoint 0x' + code.toString(16) + '.'); return true; - } - - const consumeAToken = function (): CSSTokenInterface { - consumeComments() - consume() - if (whitespace(code)) { - while (whitespace(next())) { - consume() - } - return new WhitespaceToken() - } - else if (code === 0x22) { - return consumeAStringToken() - } - else if (code === 0x23) { - if (namechar(next()) || areAValidEscape(next(1), next(2))) { - const token = new HashToken('') - if (wouldStartAnIdentifier(next(1), next(2), next(3))) { - token.type = 'id' - } - token.value = consumeAName() - return token - } - else { - return new DelimToken(code) - } - } - else if (code === 0x24) { - if (next() === 0x3D) { - consume() - return new SuffixMatchToken() - } - else { - return new DelimToken(code) - } - } - else if (code === 0x27) { - return consumeAStringToken() - } - else if (code === 0x28) { - return new OpenParenToken() - } - else if (code === 0x29) { - return new CloseParenToken() - } - else if (code === 0x2A) { - if (next() === 0x3D) { - consume() - return new SubstringMatchToken() - } - else { - return new DelimToken(code) - } - } - else if (code === 0x2B) { - if (startsWithANumber()) { - reconsume() - return consumeANumericToken() - } - else { - return new DelimToken(code) - } - } - else if (code === 0x2C) { - return new CommaToken() - } - else if (code === 0x2D) { - if (startsWithANumber()) { - reconsume() - return consumeANumericToken() - } - else if (next(1) === 0x2D && next(2) === 0x3E) { - consume(2) - return new CDCToken() - } - else if (startsWithAnIdentifier()) { - reconsume() - return consumeAnIdentlikeToken() - } - else { - return new DelimToken(code) - } - } - else if (code === 0x2E) { - if (startsWithANumber()) { - reconsume() - return consumeANumericToken() - } - else { - return new DelimToken(code) - } - } - else if (code === 0x3A) { - return new ColonToken() - } - else if (code === 0x3B) { - return new SemicolonToken() - } - else if (code === 0x3C) { - if (next(1) === 0x21 && next(2) === 0x2D && next(3) === 0x2D) { - consume(3) - return new CDOToken() - } - else { - return new DelimToken(code) - } - } - else if (code === 0x40) { - if (wouldStartAnIdentifier(next(1), next(2), next(3))) { - return new AtKeywordToken(consumeAName()) - } - else { - return new DelimToken(code) - } - } - else if (code === 0x5B) { - return new OpenSquareToken() - } - else if (code === 0x5C) { - if (startsWithAValidEscape()) { - reconsume() - return consumeAnIdentlikeToken() - } - else { - parseerror() - return new DelimToken(code) - } - } - else if (code === 0x5D) { - return new CloseSquareToken() - } - else if (code === 0x5E) { - if (next() === 0x3D) { - consume() - return new PrefixMatchToken() - } - else { - return new DelimToken(code) - } - } - else if (code === 0x7B) { - return new OpenCurlyToken() - } - else if (code === 0x7C) { - if (next() === 0x3D) { - consume() - return new DashMatchToken() - } - else if (next() === 0x7C) { - consume() - return new ColumnToken() - } - else { - return new DelimToken(code) - } - } - else if (code === 0x7D) { - return new CloseCurlyToken() - } - else if (code === 0x7E) { - if (next() === 0x3D) { - consume() - return new IncludeMatchToken() - } - else { - return new DelimToken(code) - } - } - else if (digit(code)) { - reconsume() - return consumeANumericToken() - } - else if (namestartchar(code)) { - reconsume() - return consumeAnIdentlikeToken() - } - else if (eof()) { - return new EOFToken() - } - else { - return new DelimToken(code) - } - } - - const consumeComments = function () { - while (next(1) === 0x2F && next(2) === 0x2A) { - consume(2) - while (true) { - consume() - if (code === 0x2A && next() === 0x2F) { - consume() - break - } - else if (eof()) { - parseerror() - return - } - } - } - } - - const consumeANumericToken = function () { - const num = consumeANumber() - if (wouldStartAnIdentifier(next(1), next(2), next(3))) { - const token = new DimensionToken() - token.value = num.value - token.repr = num.repr - token.type = num.type - token.unit = consumeAName() - return token - } - else if (next() === 0x25) { - consume() - const token = new PercentageToken() - token.value = num.value - token.repr = num.repr - return token - } - else { - const token = new NumberToken() - token.value = num.value - token.repr = num.repr - token.type = num.type - return token - } - } - - const consumeAnIdentlikeToken = function (): CSSTokenInterface { - const str = consumeAName() - if (str.toLowerCase() === 'url' && next() === 0x28) { - consume() - while (whitespace(next(1)) && whitespace(next(2))) { - consume() - } - if (next() === 0x22 || next() === 0x27) { - return new FunctionToken(str) - } - else if (whitespace(next()) && (next(2) === 0x22 || next(2) === 0x27)) { - return new FunctionToken(str) - } - else { - return consumeAURLToken() - } - } - else if (next() === 0x28) { - consume() - return new FunctionToken(str) - } - else { - return new IdentToken(str) - } - } - - const consumeAStringToken = function (endingCodePoint?: number): CSSParserToken { - if (endingCodePoint === undefined) { - endingCodePoint = code - } - let string = '' - while (consume()) { - if (code === endingCodePoint || eof()) { - return new StringToken(string) - } - else if (newline(code)) { - parseerror() - reconsume() - return new BadStringToken() - } - else if (code === 0x5C) { - if (eof(next())) { - donothing() - } - else if (newline(next())) { - consume() - } - else { string += stringFromCode(consumeEscape()) } - } - else { - string += stringFromCode(code) - } - } - throw new Error('Internal error') - } - - const consumeAURLToken = function (): CSSTokenInterface { - const token = new URLToken('') - while (whitespace(next())) { - consume() - } - if (eof(next())) { - return token - } - while (consume()) { - if (code === 0x29 || eof()) { - return token - } - else if (whitespace(code)) { - while (whitespace(next())) { - consume() - } - if (next() === 0x29 || eof(next())) { - consume() - return token - } - else { - consumeTheRemnantsOfABadURL() - return new BadURLToken() - } - } - else if (code === 0x22 || code === 0x27 || code === 0x28 || nonprintable(code)) { - parseerror() - consumeTheRemnantsOfABadURL() - return new BadURLToken() - } - else if (code === 0x5C) { - if (startsWithAValidEscape()) { - token.value += stringFromCode(consumeEscape()) - } - else { - parseerror() - consumeTheRemnantsOfABadURL() - return new BadURLToken() - } - } - else { - token.value += stringFromCode(code) - } - } - throw new Error('Internal error') - } - - const consumeEscape = function () { - // Assume the current character is the \ - // and the next code point is not a newline. - consume() - if (hexdigit(code)) { - // Consume 1-6 hex digits - const digits = [code] - for (let total = 0; total < 5; total++) { - if (hexdigit(next())) { - consume() - digits.push(code) - } - else { - break - } - } - if (whitespace(next())) { - consume() - } - let value = Number.parseInt(digits.map((x) => { - return String.fromCharCode(x) - }).join(''), 16) - if (value > maximumallowedcodepoint) { - value = 0xFFFD - } - return value - } - else if (eof()) { - return 0xFFFD - } - else { - return code - } - } - - const areAValidEscape = function (c1: number, c2: number) { - if (c1 !== 0x5C) { - return false - } - if (newline(c2)) { - return false - } - return true - } - const startsWithAValidEscape = function () { - return areAValidEscape(code, next()) - } - - const wouldStartAnIdentifier = function (c1: number, c2: number, c3: number) { - if (c1 === 0x2D) { - return namestartchar(c2) || c2 === 0x2D || areAValidEscape(c2, c3) - } - else if (namestartchar(c1)) { - return true - } - else if (c1 === 0x5C) { - return areAValidEscape(c1, c2) - } - else { - return false - } - } - const startsWithAnIdentifier = function () { - return wouldStartAnIdentifier(code, next(1), next(2)) - } - - const wouldStartANumber = function (c1: number, c2: number, c3: number) { - if (c1 === 0x2B || c1 === 0x2D) { - if (digit(c2)) { - return true - } - if (c2 === 0x2E && digit(c3)) { - return true - } - return false - } - else if (c1 === 0x2E) { - if (digit(c2)) { - return true - } - return false - } - else if (digit(c1)) { - return true - } - else { - return false - } - } - const startsWithANumber = function () { - return wouldStartANumber(code, next(1), next(2)) - } - - const consumeAName = function (): string { - let result = '' - while (consume()) { - if (namechar(code)) { - result += stringFromCode(code) - } - else if (startsWithAValidEscape()) { - result += stringFromCode(consumeEscape()) - } - else { - reconsume() - return result - } - } - throw new Error('Internal parse error') - } - - const consumeANumber = function () { - let repr = '' - let type = 'integer' - if (next() === 0x2B || next() === 0x2D) { - consume() - repr += stringFromCode(code) - } - while (digit(next())) { - consume() - repr += stringFromCode(code) - } - if (next(1) === 0x2E && digit(next(2))) { - consume() - repr += stringFromCode(code) - consume() - repr += stringFromCode(code) - type = 'number' - while (digit(next())) { - consume() - repr += stringFromCode(code) - } - } - const c1 = next(1) - const c2 = next(2) - const c3 = next(3) - if ((c1 === 0x45 || c1 === 0x65) && digit(c2)) { - consume() - repr += stringFromCode(code) - consume() - repr += stringFromCode(code) - type = 'number' - while (digit(next())) { - consume() - repr += stringFromCode(code) - } - } - else if ((c1 === 0x45 || c1 === 0x65) && (c2 === 0x2B || c2 === 0x2D) && digit(c3)) { - consume() - repr += stringFromCode(code) - consume() - repr += stringFromCode(code) - consume() - repr += stringFromCode(code) - type = 'number' - while (digit(next())) { - consume() - repr += stringFromCode(code) - } - } - const value = convertAStringToANumber(repr) - return { type, value, repr } - } - - const convertAStringToANumber = function (string: string): number { - // CSS's number rules are identical to JS, afaik. - return +string - } - - const consumeTheRemnantsOfABadURL = function () { - while (consume()) { - if (code === 0x29 || eof()) { - return - } - else if (startsWithAValidEscape()) { - consumeEscape() - donothing() - } - else { - donothing() - } - } - } - - let iterationCount = 0 - while (!eof(next())) { - tokens.push(consumeAToken()) - iterationCount++ - if (iterationCount > str.length * 2) { - throw new Error('I\'m infinite-looping!') - } - } - return tokens -} - -export class CSSParserToken implements CSSTokenInterface { - tokenType = '' - value: string | number | undefined - toJSON(): any { - return { token: this.tokenType } - } - - toString() { - return this.tokenType - } - - toSource() { - return `${this}` - } -} - -export class BadStringToken extends CSSParserToken { - override tokenType = 'BADSTRING' -} - -export class BadURLToken extends CSSParserToken { - override tokenType = 'BADURL' -} - -export class WhitespaceToken extends CSSParserToken { - override tokenType = 'WHITESPACE' - override toString() { - return 'WS' - } - - override toSource() { - return ' ' - } -} - -export class CDOToken extends CSSParserToken { - override tokenType = 'CDO' - override toSource() { - return '' - } -} - -export class ColonToken extends CSSParserToken { - override tokenType = ':' -} - -export class SemicolonToken extends CSSParserToken { - override tokenType = ';' -} - -export class CommaToken extends CSSParserToken { - override tokenType = ',' -} - -export class GroupingToken extends CSSParserToken { - override value = '' - mirror = '' -} - -export class OpenCurlyToken extends GroupingToken { - override tokenType = '{' - constructor() { - super() - this.value = '{' - this.mirror = '}' - } -} - -export class CloseCurlyToken extends GroupingToken { - override tokenType = '}' - constructor() { - super() - this.value = '}' - this.mirror = '{' - } -} - -export class OpenSquareToken extends GroupingToken { - override tokenType = '[' - constructor() { - super() - this.value = '[' - this.mirror = ']' - } -} - -export class CloseSquareToken extends GroupingToken { - override tokenType = ']' - constructor() { - super() - this.value = ']' - this.mirror = '[' - } -} - -export class OpenParenToken extends GroupingToken { - override tokenType = '(' - constructor() { - super() - this.value = '(' - this.mirror = ')' - } -} - -export class CloseParenToken extends GroupingToken { - override tokenType = ')' - constructor() { - super() - this.value = ')' - this.mirror = '(' - } -} - -export class IncludeMatchToken extends CSSParserToken { - override tokenType = '~=' -} - -export class DashMatchToken extends CSSParserToken { - override tokenType = '|=' -} - -export class PrefixMatchToken extends CSSParserToken { - override tokenType = '^=' -} - -export class SuffixMatchToken extends CSSParserToken { - override tokenType = '$=' -} - -export class SubstringMatchToken extends CSSParserToken { - override tokenType = '*=' -} - -export class ColumnToken extends CSSParserToken { - override tokenType = '||' -} - -export class EOFToken extends CSSParserToken { - override tokenType = 'EOF' - override toSource() { - return '' - } -} - -export class DelimToken extends CSSParserToken { - override tokenType = 'DELIM' - override value: string = '' - - constructor(code: number) { - super() - this.value = stringFromCode(code) - } - - override toString() { - return `DELIM(${this.value})` - } - - override toJSON() { - const json = this.constructor.prototype.constructor.prototype.toJSON.call(this) - json.value = this.value - return json - } - - override toSource() { - if (this.value === '\\') { - return '\\\n' - } - else { - return this.value - } - } -} - -export abstract class StringValuedToken extends CSSParserToken { - override value: string = '' - ASCIIMatch(str: string) { - return this.value.toLowerCase() === str.toLowerCase() - } - - override toJSON() { - const json = this.constructor.prototype.constructor.prototype.toJSON.call(this) - json.value = this.value - return json - } -} - -export class IdentToken extends StringValuedToken { - constructor(val: string) { - super() - this.value = val - } - - override tokenType = 'IDENT' - override toString() { - return `IDENT(${this.value})` - } - - override toSource() { - return escapeIdent(this.value) - } -} - -export class FunctionToken extends StringValuedToken { - override tokenType = 'FUNCTION' - mirror: string - constructor(val: string) { - super() - this.value = val - this.mirror = ')' - } - - override toString() { - return `FUNCTION(${this.value})` - } - - override toSource() { - return `${escapeIdent(this.value)}(` - } -} - -export class AtKeywordToken extends StringValuedToken { - override tokenType = 'AT-KEYWORD' - constructor(val: string) { - super() - this.value = val - } - - override toString() { - return `AT(${this.value})` - } - - override toSource() { - return `@${escapeIdent(this.value)}` - } -} - -export class HashToken extends StringValuedToken { - override tokenType = 'HASH' - type: string - constructor(val: string) { - super() - this.value = val - this.type = 'unrestricted' - } - - override toString() { - return `HASH(${this.value})` - } - - override toJSON() { - const json = this.constructor.prototype.constructor.prototype.toJSON.call(this) - json.value = this.value - json.type = this.type - return json - } - - override toSource() { - if (this.type === 'id') { - return `#${escapeIdent(this.value)}` - } - else { - return `#${escapeHash(this.value)}` - } - } -} - -export class StringToken extends StringValuedToken { - override tokenType = 'STRING' - constructor(val: string) { - super() - this.value = val - } - - override toString() { - return `"${escapeString(this.value)}"` - } -} - -export class URLToken extends StringValuedToken { - override tokenType = 'URL' - constructor(val: string) { - super() - this.value = val - } - - override toString() { - return `URL(${this.value})` - } - - override toSource() { - return `url("${escapeString(this.value)}")` - } -} - -export class NumberToken extends CSSParserToken { - override tokenType = 'NUMBER' - type: string - repr: string - - constructor() { - super() - this.type = 'integer' - this.repr = '' - } - - override toString() { - if (this.type === 'integer') { - return `INT(${this.value})` - } - return `NUMBER(${this.value})` - } - - override toJSON() { - const json = super.toJSON() - json.value = this.value - json.type = this.type - json.repr = this.repr - return json - } - - override toSource() { - return this.repr - } -} - -export class PercentageToken extends CSSParserToken { - override tokenType = 'PERCENTAGE' - repr: string - constructor() { - super() - this.repr = '' - } - - override toString() { - return `PERCENTAGE(${this.value})` - } - - override toJSON() { - const json = this.constructor.prototype.constructor.prototype.toJSON.call(this) - json.value = this.value - json.repr = this.repr - return json - } - - override toSource() { - return `${this.repr}%` - } -} - -export class DimensionToken extends CSSParserToken { - override tokenType = 'DIMENSION' - type: string - repr: string - unit: string - - constructor() { - super() - this.type = 'integer' - this.repr = '' - this.unit = '' - } - - override toString() { - return `DIM(${this.value},${this.unit})` - } - - override toJSON() { - const json = this.constructor.prototype.constructor.prototype.toJSON.call(this) - json.value = this.value - json.type = this.type - json.repr = this.repr - json.unit = this.unit - return json - } - - override toSource() { - const source = this.repr - let unit = escapeIdent(this.unit) - if (unit[0].toLowerCase() === 'e' && (unit[1] === '-' || between(unit.charCodeAt(1), 0x30, 0x39))) { - // Unit is ambiguous with scinot - // Remove the leading "e", replace with escape. - unit = `\\65 ${unit.slice(1, unit.length)}` - } - return source + unit - } -} - -function escapeIdent(string: string) { - string = `${string}` - let result = '' - const firstcode = string.charCodeAt(0) - for (let i = 0; i < string.length; i++) { - const code = string.charCodeAt(i) - if (code === 0x0) { - throw new InvalidCharacterError('Invalid character: the input contains U+0000.') - } - - if ( - between(code, 0x1, 0x1F) || code === 0x7F - || (i === 0 && between(code, 0x30, 0x39)) - || (i === 1 && between(code, 0x30, 0x39) && firstcode === 0x2D) - ) { - result += `\\${code.toString(16)} ` - } - else if ( - code >= 0x80 - || code === 0x2D - || code === 0x5F - || between(code, 0x30, 0x39) - || between(code, 0x41, 0x5A) - || between(code, 0x61, 0x7A) - ) { - result += string[i] - } - else { - result += `\\${string[i]}` - } - } - return result -} - -function escapeHash(string: string) { - // Escapes the contents of "unrestricted"-type hash tokens. - // Won't preserve the ID-ness of "id"-type hash tokens; - // use escapeIdent() for that. - string = `${string}` - let result = '' - for (let i = 0; i < string.length; i++) { - const code = string.charCodeAt(i) - if (code === 0x0) { - throw new InvalidCharacterError('Invalid character: the input contains U+0000.') - } - - if ( - code >= 0x80 - || code === 0x2D - || code === 0x5F - || between(code, 0x30, 0x39) - || between(code, 0x41, 0x5A) - || between(code, 0x61, 0x7A) - ) { - result += string[i] - } - else { - result += `\\${code.toString(16)} ` - } - } - return result -} - -function escapeString(string: string) { - string = `${string}` - let result = '' - for (let i = 0; i < string.length; i++) { - const code = string.charCodeAt(i) - - if (code === 0x0) { - throw new InvalidCharacterError('Invalid character: the input contains U+0000.') - } - - if (between(code, 0x1, 0x1F) || code === 0x7F) { - result += `\\${code.toString(16)} ` - } - else if (code === 0x22 || code === 0x5C) { - result += `\\${string[i]}` - } - else { result += string[i] } - } - return result -} diff --git a/packages/browser/src/client/tester/locators/playwright-selector/domUtils.ts b/packages/browser/src/client/tester/locators/playwright-selector/domUtils.ts deleted file mode 100644 index 9978815a6d4e..000000000000 --- a/packages/browser/src/client/tester/locators/playwright-selector/domUtils.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// copied without changes from https://github.com/microsoft/playwright/blob/4554372e456154d7365b6902ef9f3e1e7de76e94/packages/playwright-core/src/server/injected/domUtils.ts - -import { server } from '@vitest/browser/context' - -export function isInsideScope(scope: Node, element: Element | undefined): boolean { - while (element) { - if (scope.contains(element)) { - return true - } - element = enclosingShadowHost(element) - } - return false -} - -export function parentElementOrShadowHost(element: Element): Element | undefined { - if (element.parentElement) { - return element.parentElement - } - if (!element.parentNode) { - return - } - if (element.parentNode.nodeType === 11 /* Node.DOCUMENT_FRAGMENT_NODE */ && (element.parentNode as ShadowRoot).host) { - return (element.parentNode as ShadowRoot).host - } -} - -export function enclosingShadowRootOrDocument(element: Element): Document | ShadowRoot | undefined { - let node: Node = element - while (node.parentNode) { - node = node.parentNode - } - if (node.nodeType === 11 /* Node.DOCUMENT_FRAGMENT_NODE */ || node.nodeType === 9 /* Node.DOCUMENT_NODE */) { - return node as Document | ShadowRoot - } -} - -function enclosingShadowHost(element: Element): Element | undefined { - while (element.parentElement) { - element = element.parentElement - } - return parentElementOrShadowHost(element) -} - -// Assumption: if scope is provided, element must be inside scope's subtree. -export function closestCrossShadow(element: Element | undefined, css: string): Element | undefined { - while (element) { - const closest = element.closest(css) - if (closest) { - return closest - } - element = enclosingShadowHost(element) - } -} - -export function getElementComputedStyle(element: Element, pseudo?: string): CSSStyleDeclaration | undefined { - return element.ownerDocument && element.ownerDocument.defaultView ? element.ownerDocument.defaultView.getComputedStyle(element, pseudo) : undefined -} - -export function isElementStyleVisibilityVisible(element: Element, style?: CSSStyleDeclaration): boolean { - style = style ?? getElementComputedStyle(element) - if (!style) { - return true - } - // Element.checkVisibility checks for content-visibility and also looks at - // styles up the flat tree including user-agent ShadowRoots, such as the - // details element for example. - // All the browser implement it, but WebKit has a bug which prevents us from using it: - // https://bugs.webkit.org/show_bug.cgi?id=264733 - // @ts-expect-error explained above - if (Element.prototype.checkVisibility && server.browser !== 'webkit') { - if (!element.checkVisibility()) { - return false - } - } - else { - // Manual workaround for WebKit that does not have checkVisibility. - const detailsOrSummary = element.closest('details,summary') - if (detailsOrSummary !== element && detailsOrSummary?.nodeName === 'DETAILS' && !(detailsOrSummary as HTMLDetailsElement).open) { - return false - } - } - if (style.visibility !== 'visible') { - return false - } - return true -} - -export function isElementVisible(element: Element): boolean { - // Note: this logic should be similar to waitForDisplayedAtStablePosition() to avoid surprises. - const style = getElementComputedStyle(element) - if (!style) { - return true - } - if (style.display === 'contents') { - // display:contents is not rendered itself, but its child nodes are. - for (let child = element.firstChild; child; child = child.nextSibling) { - if (child.nodeType === 1 /* Node.ELEMENT_NODE */ && isElementVisible(child as Element)) { - return true - } - if (child.nodeType === 3 /* Node.TEXT_NODE */ && isVisibleTextNode(child as Text)) { - return true - } - } - return false - } - if (!isElementStyleVisibilityVisible(element, style)) { - return false - } - const rect = element.getBoundingClientRect() - return rect.width > 0 && rect.height > 0 -} - -export function isVisibleTextNode(node: Text) { - // https://stackoverflow.com/questions/1461059/is-there-an-equivalent-to-getboundingclientrect-for-text-nodes - const range = node.ownerDocument.createRange() - range.selectNode(node) - const rect = range.getBoundingClientRect() - return rect.width > 0 && rect.height > 0 -} - -export function elementSafeTagName(element: Element) { - // Named inputs, e.g. , will be exposed as fields on the parent
- // and override its properties. - if (element instanceof HTMLFormElement) { - return 'FORM' - } - // Elements from the svg namespace do not have uppercase tagName right away. - return element.tagName.toUpperCase() -} diff --git a/packages/browser/src/client/tester/locators/playwright-selector/layoutSelectorUtils.ts b/packages/browser/src/client/tester/locators/playwright-selector/layoutSelectorUtils.ts deleted file mode 100644 index 4f9602880dc9..000000000000 --- a/packages/browser/src/client/tester/locators/playwright-selector/layoutSelectorUtils.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// copied without changes from https://github.com/microsoft/playwright/blob/4554372e456154d7365b6902ef9f3e1e7de76e94/packages/playwright-core/src/utils/isomorphic/layoutSelectorUtils.ts - -function boxRightOf(box1: DOMRect, box2: DOMRect, maxDistance: number | undefined): number | undefined { - const distance = box1.left - box2.right - if (distance < 0 || (maxDistance !== undefined && distance > maxDistance)) { - return - } - return distance + Math.max(box2.bottom - box1.bottom, 0) + Math.max(box1.top - box2.top, 0) -} - -function boxLeftOf(box1: DOMRect, box2: DOMRect, maxDistance: number | undefined): number | undefined { - const distance = box2.left - box1.right - if (distance < 0 || (maxDistance !== undefined && distance > maxDistance)) { - return - } - return distance + Math.max(box2.bottom - box1.bottom, 0) + Math.max(box1.top - box2.top, 0) -} - -function boxAbove(box1: DOMRect, box2: DOMRect, maxDistance: number | undefined): number | undefined { - const distance = box2.top - box1.bottom - if (distance < 0 || (maxDistance !== undefined && distance > maxDistance)) { - return - } - return distance + Math.max(box1.left - box2.left, 0) + Math.max(box2.right - box1.right, 0) -} - -function boxBelow(box1: DOMRect, box2: DOMRect, maxDistance: number | undefined): number | undefined { - const distance = box1.top - box2.bottom - if (distance < 0 || (maxDistance !== undefined && distance > maxDistance)) { - return - } - return distance + Math.max(box1.left - box2.left, 0) + Math.max(box2.right - box1.right, 0) -} - -function boxNear(box1: DOMRect, box2: DOMRect, maxDistance: number | undefined): number | undefined { - const kThreshold = maxDistance === undefined ? 50 : maxDistance - let score = 0 - if (box1.left - box2.right >= 0) { - score += box1.left - box2.right - } - if (box2.left - box1.right >= 0) { - score += box2.left - box1.right - } - if (box2.top - box1.bottom >= 0) { - score += box2.top - box1.bottom - } - if (box1.top - box2.bottom >= 0) { - score += box1.top - box2.bottom - } - return score > kThreshold ? undefined : score -} - -export type LayoutSelectorName = 'left-of' | 'right-of' | 'above' | 'below' | 'near' -export const kLayoutSelectorNames: LayoutSelectorName[] = ['left-of', 'right-of', 'above', 'below', 'near'] - -export function layoutSelectorScore(name: LayoutSelectorName, element: Element, inner: Element[], maxDistance: number | undefined): number | undefined { - const box = element.getBoundingClientRect() - const scorer = { 'left-of': boxLeftOf, 'right-of': boxRightOf, 'above': boxAbove, 'below': boxBelow, 'near': boxNear }[name] - let bestScore: number | undefined - for (const e of inner) { - if (e === element) { - continue - } - const score = scorer(box, e.getBoundingClientRect(), maxDistance) - if (score === undefined) { - continue - } - if (bestScore === undefined || score < bestScore) { - bestScore = score - } - } - return bestScore -} diff --git a/packages/browser/src/client/tester/locators/playwright-selector/locatorGenerators.ts b/packages/browser/src/client/tester/locators/playwright-selector/locatorGenerators.ts deleted file mode 100644 index 9f37accdbf30..000000000000 --- a/packages/browser/src/client/tester/locators/playwright-selector/locatorGenerators.ts +++ /dev/null @@ -1,378 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// copied from https://github.com/microsoft/playwright/blob/4554372e456154d7365b6902ef9f3e1e7de76e94/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts -// removed support for all languages except javascript - -import { escapeWithQuotes, normalizeEscapedRegexQuotes } from './stringUtils' -import type { NestedSelectorBody, ParsedSelector } from './selectorParser' -import { parseAttributeSelector, parseSelector, stringifySelector } from './selectorParser' - -export type Language = 'javascript' -export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has-not-text' | 'has' | 'hasNot' | 'frame' | 'and' | 'or' | 'chain' -export type LocatorBase = 'page' | 'locator' | 'frame-locator' -export type Quote = '\'' | '"' | '`' - -interface LocatorOptions { - attrs?: { name: string; value: string | boolean | number }[] - exact?: boolean - name?: string | RegExp - hasText?: string | RegExp - hasNotText?: string | RegExp -} -export interface LocatorFactory { - generateLocator: (base: LocatorBase, kind: LocatorType, body: string | RegExp, options?: LocatorOptions) => string - chainLocators: (locators: string[]) => string -} - -export function asLocator(lang: Language, selector: string, isFrameLocator: boolean = false): string { - return asLocators(lang, selector, isFrameLocator)[0] -} - -function asLocators(lang: Language, selector: string, isFrameLocator: boolean = false, maxOutputSize = 20, preferredQuote?: Quote): string[] { - try { - // eslint-disable-next-line ts/no-use-before-define - return innerAsLocators(new generators[lang](preferredQuote), parseSelector(selector), isFrameLocator, maxOutputSize) - } - catch (e) { - // Tolerate invalid input. - return [selector] - } -} - -function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFrameLocator: boolean = false, maxOutputSize = 20): string[] { - const parts = [...parsed.parts] - // frameLocator('iframe').first is actually "iframe >> nth=0 >> internal:control=enter-frame" - // To make it easier to parse, we turn it into "iframe >> internal:control=enter-frame >> nth=0" - for (let index = 0; index < parts.length - 1; index++) { - if (parts[index].name === 'nth' && parts[index + 1].name === 'internal:control' && (parts[index + 1].body as string) === 'enter-frame') { - // Swap nth and enter-frame. - const [nth] = parts.splice(index, 1) - parts.splice(index + 1, 0, nth) - } - } - - const tokens: string[][] = [] - let nextBase: LocatorBase = isFrameLocator ? 'frame-locator' : 'page' - for (let index = 0; index < parts.length; index++) { - const part = parts[index] - const base = nextBase - nextBase = 'locator' - - if (part.name === 'nth') { - if (part.body === '0') { - tokens.push([factory.generateLocator(base, 'first', ''), factory.generateLocator(base, 'nth', '0')]) - } - else if (part.body === '-1') { - tokens.push([factory.generateLocator(base, 'last', ''), factory.generateLocator(base, 'nth', '-1')]) - } - else { - tokens.push([factory.generateLocator(base, 'nth', part.body as string)]) - } - continue - } - if (part.name === 'internal:text') { - const { exact, text } = detectExact(part.body as string) - tokens.push([factory.generateLocator(base, 'text', text, { exact })]) - continue - } - if (part.name === 'internal:has-text') { - const { exact, text } = detectExact(part.body as string) - // There is no locator equivalent for strict has-text, leave it as is. - if (!exact) { - tokens.push([factory.generateLocator(base, 'has-text', text, { exact })]) - continue - } - } - if (part.name === 'internal:has-not-text') { - const { exact, text } = detectExact(part.body as string) - // There is no locator equivalent for strict has-not-text, leave it as is. - if (!exact) { - tokens.push([factory.generateLocator(base, 'has-not-text', text, { exact })]) - continue - } - } - if (part.name === 'internal:has') { - const inners = innerAsLocators(factory, (part.body as NestedSelectorBody).parsed, false, maxOutputSize) - tokens.push(inners.map(inner => factory.generateLocator(base, 'has', inner))) - continue - } - if (part.name === 'internal:has-not') { - const inners = innerAsLocators(factory, (part.body as NestedSelectorBody).parsed, false, maxOutputSize) - tokens.push(inners.map(inner => factory.generateLocator(base, 'hasNot', inner))) - continue - } - if (part.name === 'internal:and') { - const inners = innerAsLocators(factory, (part.body as NestedSelectorBody).parsed, false, maxOutputSize) - tokens.push(inners.map(inner => factory.generateLocator(base, 'and', inner))) - continue - } - if (part.name === 'internal:or') { - const inners = innerAsLocators(factory, (part.body as NestedSelectorBody).parsed, false, maxOutputSize) - tokens.push(inners.map(inner => factory.generateLocator(base, 'or', inner))) - continue - } - if (part.name === 'internal:chain') { - const inners = innerAsLocators(factory, (part.body as NestedSelectorBody).parsed, false, maxOutputSize) - tokens.push(inners.map(inner => factory.generateLocator(base, 'chain', inner))) - continue - } - if (part.name === 'internal:label') { - const { exact, text } = detectExact(part.body as string) - tokens.push([factory.generateLocator(base, 'label', text, { exact })]) - continue - } - if (part.name === 'internal:role') { - const attrSelector = parseAttributeSelector(part.body as string, true) - const options: LocatorOptions = { attrs: [] } - for (const attr of attrSelector.attributes) { - if (attr.name === 'name') { - options.exact = attr.caseSensitive - options.name = attr.value - } - else { - if (attr.name === 'level' && typeof attr.value === 'string') { - attr.value = +attr.value - } - options.attrs!.push({ name: attr.name === 'include-hidden' ? 'includeHidden' : attr.name, value: attr.value }) - } - } - tokens.push([factory.generateLocator(base, 'role', attrSelector.name, options)]) - continue - } - if (part.name === 'internal:testid') { - const attrSelector = parseAttributeSelector(part.body as string, true) - const { value } = attrSelector.attributes[0] - tokens.push([factory.generateLocator(base, 'test-id', value)]) - continue - } - if (part.name === 'internal:attr') { - const attrSelector = parseAttributeSelector(part.body as string, true) - const { name, value, caseSensitive } = attrSelector.attributes[0] - const text = value as string | RegExp - const exact = !!caseSensitive - if (name === 'placeholder') { - tokens.push([factory.generateLocator(base, 'placeholder', text, { exact })]) - continue - } - if (name === 'alt') { - tokens.push([factory.generateLocator(base, 'alt', text, { exact })]) - continue - } - if (name === 'title') { - tokens.push([factory.generateLocator(base, 'title', text, { exact })]) - continue - } - } - - let locatorType: LocatorType = 'default' - - const nextPart = parts[index + 1] - if (nextPart && nextPart.name === 'internal:control' && (nextPart.body as string) === 'enter-frame') { - locatorType = 'frame' - nextBase = 'frame-locator' - index++ - } - - const selectorPart = stringifySelector({ parts: [part] }) - const locatorPart = factory.generateLocator(base, locatorType, selectorPart) - - if (locatorType === 'default' && nextPart && ['internal:has-text', 'internal:has-not-text'].includes(nextPart.name)) { - const { exact, text } = detectExact(nextPart.body as string) - // There is no locator equivalent for strict has-text and has-not-text, leave it as is. - if (!exact) { - const nextLocatorPart = factory.generateLocator('locator', nextPart.name === 'internal:has-text' ? 'has-text' : 'has-not-text', text, { exact }) - const options: LocatorOptions = {} - if (nextPart.name === 'internal:has-text') { - options.hasText = text - } - else { options.hasNotText = text } - const combinedPart = factory.generateLocator(base, 'default', selectorPart, options) - // Two options: - // - locator('div').filter({ hasText: 'foo' }) - // - locator('div', { hasText: 'foo' }) - tokens.push([factory.chainLocators([locatorPart, nextLocatorPart]), combinedPart]) - index++ - continue - } - } - - // Selectors can be prefixed with engine name, e.g. xpath=//foo - let locatorPartWithEngine: string | undefined - if (['xpath', 'css'].includes(part.name)) { - const selectorPart = stringifySelector({ parts: [part] }, /* forceEngineName */ true) - locatorPartWithEngine = factory.generateLocator(base, locatorType, selectorPart) - } - - tokens.push([locatorPart, locatorPartWithEngine].filter(Boolean) as string[]) - } - - return combineTokens(factory, tokens, maxOutputSize) -} - -function combineTokens(factory: LocatorFactory, tokens: string[][], maxOutputSize: number): string[] { - const currentTokens = tokens.map(() => '') - const result: string[] = [] - - const visit = (index: number) => { - if (index === tokens.length) { - result.push(factory.chainLocators(currentTokens)) - return currentTokens.length < maxOutputSize - } - for (const taken of tokens[index]) { - currentTokens[index] = taken - if (!visit(index + 1)) { - return false - } - } - return true - } - - visit(0) - return result -} - -function detectExact(text: string): { exact?: boolean; text: string | RegExp } { - let exact = false - const match = text.match(/^\/(.*)\/([igm]*)$/) - if (match) { - return { text: new RegExp(match[1], match[2]) } - } - if (text.endsWith('"')) { - text = JSON.parse(text) - exact = true - } - else if (text.endsWith('"s')) { - text = JSON.parse(text.substring(0, text.length - 1)) - exact = true - } - else if (text.endsWith('"i')) { - text = JSON.parse(text.substring(0, text.length - 1)) - exact = false - } - return { exact, text } -} - -export class JavaScriptLocatorFactory implements LocatorFactory { - constructor(private preferredQuote?: Quote) {} - - generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: LocatorOptions = {}): string { - switch (kind) { - case 'default': - if (options.hasText !== undefined) { - return `locator(${this.quote(body as string)}, { hasText: ${this.toHasText(options.hasText)} })` - } - if (options.hasNotText !== undefined) { - return `locator(${this.quote(body as string)}, { hasNotText: ${this.toHasText(options.hasNotText)} })` - } - return `locator(${this.quote(body as string)})` - case 'frame': - return `frameLocator(${this.quote(body as string)})` - case 'nth': - return `nth(${body})` - case 'first': - return `first()` - case 'last': - return `last()` - case 'role': { - const attrs: string[] = [] - if (isRegExp(options.name)) { - attrs.push(`name: ${this.regexToSourceString(options.name)}`) - } - else if (typeof options.name === 'string') { - attrs.push(`name: ${this.quote(options.name)}`) - if (options.exact) { - attrs.push(`exact: true`) - } - } - for (const { name, value } of options.attrs!) { - attrs.push(`${name}: ${typeof value === 'string' ? this.quote(value) : value}`) - } - const attrString = attrs.length ? `, { ${attrs.join(', ')} }` : '' - return `getByRole(${this.quote(body as string)}${attrString})` - } - case 'has-text': - return `filter({ hasText: ${this.toHasText(body)} })` - case 'has-not-text': - return `filter({ hasNotText: ${this.toHasText(body)} })` - case 'has': - return `filter({ has: ${body} })` - case 'hasNot': - return `filter({ hasNot: ${body} })` - case 'and': - return `and(${body})` - case 'or': - return `or(${body})` - case 'chain': - return `locator(${body})` - case 'test-id': - return `getByTestId(${this.toTestIdValue(body)})` - case 'text': - return this.toCallWithExact('getByText', body, !!options.exact) - case 'alt': - return this.toCallWithExact('getByAltText', body, !!options.exact) - case 'placeholder': - return this.toCallWithExact('getByPlaceholder', body, !!options.exact) - case 'label': - return this.toCallWithExact('getByLabel', body, !!options.exact) - case 'title': - return this.toCallWithExact('getByTitle', body, !!options.exact) - default: - throw new Error(`Unknown selector kind ${kind}`) - } - } - - chainLocators(locators: string[]): string { - return locators.join('.') - } - - private regexToSourceString(re: RegExp) { - return normalizeEscapedRegexQuotes(String(re)) - } - - private toCallWithExact(method: string, body: string | RegExp, exact?: boolean) { - if (isRegExp(body)) { - return `${method}(${this.regexToSourceString(body)})` - } - return exact ? `${method}(${this.quote(body)}, { exact: true })` : `${method}(${this.quote(body)})` - } - - private toHasText(body: string | RegExp) { - if (isRegExp(body)) { - return this.regexToSourceString(body) - } - return this.quote(body) - } - - private toTestIdValue(value: string | RegExp): string { - if (isRegExp(value)) { - return this.regexToSourceString(value) - } - return this.quote(value) - } - - private quote(text: string) { - return escapeWithQuotes(text, this.preferredQuote ?? '\'') - } -} - -const generators: Record LocatorFactory> = { - javascript: JavaScriptLocatorFactory, -} - -function isRegExp(obj: any): obj is RegExp { - return obj instanceof RegExp -} diff --git a/packages/browser/src/client/tester/locators/playwright-selector/locatorUtils.ts b/packages/browser/src/client/tester/locators/playwright-selector/locatorUtils.ts deleted file mode 100644 index 783a3d49d1d2..000000000000 --- a/packages/browser/src/client/tester/locators/playwright-selector/locatorUtils.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// copied without changes from https://github.com/microsoft/playwright/blob/4554372e456154d7365b6902ef9f3e1e7de76e94/packages/playwright-core/src/utils/isomorphic/locatorUtils.ts - -import { escapeForAttributeSelector, escapeForTextSelector } from './stringUtils' - -export interface ByRoleOptions { - checked?: boolean - disabled?: boolean - exact?: boolean - expanded?: boolean - includeHidden?: boolean - level?: number - name?: string | RegExp - pressed?: boolean - selected?: boolean -} - -function getByAttributeTextSelector(attrName: string, text: string | RegExp, options?: { exact?: boolean }): string { - return `internal:attr=[${attrName}=${escapeForAttributeSelector(text, options?.exact || false)}]` -} - -export function getByTestIdSelector(testIdAttributeName: string, testId: string | RegExp): string { - return `internal:testid=[${testIdAttributeName}=${escapeForAttributeSelector(testId, true)}]` -} - -export function getByLabelSelector(text: string | RegExp, options?: { exact?: boolean }): string { - return `internal:label=${escapeForTextSelector(text, !!options?.exact)}` -} - -export function getByAltTextSelector(text: string | RegExp, options?: { exact?: boolean }): string { - return getByAttributeTextSelector('alt', text, options) -} - -export function getByTitleSelector(text: string | RegExp, options?: { exact?: boolean }): string { - return getByAttributeTextSelector('title', text, options) -} - -export function getByPlaceholderSelector(text: string | RegExp, options?: { exact?: boolean }): string { - return getByAttributeTextSelector('placeholder', text, options) -} - -export function getByTextSelector(text: string | RegExp, options?: { exact?: boolean }): string { - return `internal:text=${escapeForTextSelector(text, !!options?.exact)}` -} - -export function getByRoleSelector(role: string, options: ByRoleOptions = {}): string { - const props: string[][] = [] - if (options.checked !== undefined) { - props.push(['checked', String(options.checked)]) - } - if (options.disabled !== undefined) { - props.push(['disabled', String(options.disabled)]) - } - if (options.selected !== undefined) { - props.push(['selected', String(options.selected)]) - } - if (options.expanded !== undefined) { - props.push(['expanded', String(options.expanded)]) - } - if (options.includeHidden !== undefined) { - props.push(['include-hidden', String(options.includeHidden)]) - } - if (options.level !== undefined) { - props.push(['level', String(options.level)]) - } - if (options.name !== undefined) { - props.push(['name', escapeForAttributeSelector(options.name, !!options.exact)]) - } - if (options.pressed !== undefined) { - props.push(['pressed', String(options.pressed)]) - } - return `internal:role=${role}${props.map(([n, v]) => `[${n}=${v}]`).join('')}` -} diff --git a/packages/browser/src/client/tester/locators/playwright-selector/roleSelectorEngine.ts b/packages/browser/src/client/tester/locators/playwright-selector/roleSelectorEngine.ts deleted file mode 100644 index 2a7c792fa8c2..000000000000 --- a/packages/browser/src/client/tester/locators/playwright-selector/roleSelectorEngine.ts +++ /dev/null @@ -1,219 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// copied without changes from https://github.com/microsoft/playwright/blob/4554372e456154d7365b6902ef9f3e1e7de76e94/packages/playwright-core/src/server/injected/roleSelectorEngine.ts - -import { type AttributeSelectorOperator, type AttributeSelectorPart, parseAttributeSelector } from './selectorParser' -import { normalizeWhiteSpace } from './stringUtils' -import type { SelectorEngine, SelectorRoot } from './selector' -import { matchesAttributePart } from './selectorUtils' -import { beginAriaCaches, endAriaCaches, getAriaChecked, getAriaDisabled, getAriaExpanded, getAriaLevel, getAriaPressed, getAriaRole, getAriaSelected, getElementAccessibleName, isElementHiddenForAria, kAriaCheckedRoles, kAriaExpandedRoles, kAriaLevelRoles, kAriaPressedRoles, kAriaSelectedRoles } from './roleUtils' - -interface RoleEngineOptions { - role: string - name?: string | RegExp - nameOp?: '=' | '*=' | '|=' | '^=' | '$=' | '~=' - exact?: boolean - checked?: boolean | 'mixed' - pressed?: boolean | 'mixed' - selected?: boolean - expanded?: boolean - level?: number - disabled?: boolean - includeHidden?: boolean -} - -const kSupportedAttributes = ['selected', 'checked', 'pressed', 'expanded', 'level', 'disabled', 'name', 'include-hidden'] -kSupportedAttributes.sort() - -function validateSupportedRole(attr: string, roles: string[], role: string) { - if (!roles.includes(role)) { - throw new Error(`"${attr}" attribute is only supported for roles: ${roles.slice().sort().map(role => `"${role}"`).join(', ')}`) - } -} - -function validateSupportedValues(attr: AttributeSelectorPart, values: any[]) { - if (attr.op !== '' && !values.includes(attr.value)) { - throw new Error(`"${attr.name}" must be one of ${values.map(v => JSON.stringify(v)).join(', ')}`) - } -} - -function validateSupportedOp(attr: AttributeSelectorPart, ops: AttributeSelectorOperator[]) { - if (!ops.includes(attr.op)) { - throw new Error(`"${attr.name}" does not support "${attr.op}" matcher`) - } -} - -function validateAttributes(attrs: AttributeSelectorPart[], role: string): RoleEngineOptions { - const options: RoleEngineOptions = { role } - for (const attr of attrs) { - switch (attr.name) { - case 'checked': { - validateSupportedRole(attr.name, kAriaCheckedRoles, role) - validateSupportedValues(attr, [true, false, 'mixed']) - validateSupportedOp(attr, ['', '=']) - options.checked = attr.op === '' ? true : attr.value - break - } - case 'pressed': { - validateSupportedRole(attr.name, kAriaPressedRoles, role) - validateSupportedValues(attr, [true, false, 'mixed']) - validateSupportedOp(attr, ['', '=']) - options.pressed = attr.op === '' ? true : attr.value - break - } - case 'selected': { - validateSupportedRole(attr.name, kAriaSelectedRoles, role) - validateSupportedValues(attr, [true, false]) - validateSupportedOp(attr, ['', '=']) - options.selected = attr.op === '' ? true : attr.value - break - } - case 'expanded': { - validateSupportedRole(attr.name, kAriaExpandedRoles, role) - validateSupportedValues(attr, [true, false]) - validateSupportedOp(attr, ['', '=']) - options.expanded = attr.op === '' ? true : attr.value - break - } - case 'level': { - validateSupportedRole(attr.name, kAriaLevelRoles, role) - // Level is a number, convert it from string. - if (typeof attr.value === 'string') { - attr.value = +attr.value - } - if (attr.op !== '=' || typeof attr.value !== 'number' || Number.isNaN(attr.value)) { - throw new Error(`"level" attribute must be compared to a number`) - } - options.level = attr.value - break - } - case 'disabled': { - validateSupportedValues(attr, [true, false]) - validateSupportedOp(attr, ['', '=']) - options.disabled = attr.op === '' ? true : attr.value - break - } - case 'name': { - if (attr.op === '') { - throw new Error(`"name" attribute must have a value`) - } - if (typeof attr.value !== 'string' && !(attr.value instanceof RegExp)) { - throw new TypeError(`"name" attribute must be a string or a regular expression`) - } - options.name = attr.value - options.nameOp = attr.op - options.exact = attr.caseSensitive - break - } - case 'include-hidden': { - validateSupportedValues(attr, [true, false]) - validateSupportedOp(attr, ['', '=']) - options.includeHidden = attr.op === '' ? true : attr.value - break - } - default: { - throw new Error(`Unknown attribute "${attr.name}", must be one of ${kSupportedAttributes.map(a => `"${a}"`).join(', ')}.`) - } - } - } - return options -} - -function queryRole(scope: SelectorRoot, options: RoleEngineOptions, internal: boolean): Element[] { - const result: Element[] = [] - const match = (element: Element) => { - if (getAriaRole(element) !== options.role) { - return - } - if (options.selected !== undefined && getAriaSelected(element) !== options.selected) { - return - } - if (options.checked !== undefined && getAriaChecked(element) !== options.checked) { - return - } - if (options.pressed !== undefined && getAriaPressed(element) !== options.pressed) { - return - } - if (options.expanded !== undefined && getAriaExpanded(element) !== options.expanded) { - return - } - if (options.level !== undefined && getAriaLevel(element) !== options.level) { - return - } - if (options.disabled !== undefined && getAriaDisabled(element) !== options.disabled) { - return - } - if (!options.includeHidden) { - const isHidden = isElementHiddenForAria(element) - if (isHidden) { - return - } - } - if (options.name !== undefined) { - // Always normalize whitespace in the accessible name. - const accessibleName = normalizeWhiteSpace(getElementAccessibleName(element, !!options.includeHidden)) - if (typeof options.name === 'string') { - options.name = normalizeWhiteSpace(options.name) - } - // internal:role assumes that [name="foo"i] also means substring. - if (internal && !options.exact && options.nameOp === '=') { - options.nameOp = '*=' - } - if (!matchesAttributePart(accessibleName, { name: '', jsonPath: [], op: options.nameOp || '=', value: options.name, caseSensitive: !!options.exact })) { - return - } - } - result.push(element) - } - - const query = (root: Element | ShadowRoot | Document) => { - const shadows: ShadowRoot[] = [] - if ((root as Element).shadowRoot) { - shadows.push((root as Element).shadowRoot!) - } - for (const element of root.querySelectorAll('*')) { - match(element) - if (element.shadowRoot) { - shadows.push(element.shadowRoot) - } - } - shadows.forEach(query) - } - - query(scope) - return result -} - -export function createRoleEngine(internal: boolean): SelectorEngine { - return { - queryAll: (scope: SelectorRoot, selector: string): Element[] => { - const parsed = parseAttributeSelector(selector, true) - const role = parsed.name.toLowerCase() - if (!role) { - throw new Error(`Role must not be empty`) - } - const options = validateAttributes(parsed.attributes, role) - beginAriaCaches() - try { - return queryRole(scope, options, internal) - } - finally { - endAriaCaches() - } - }, - } -} diff --git a/packages/browser/src/client/tester/locators/playwright-selector/roleUtils.ts b/packages/browser/src/client/tester/locators/playwright-selector/roleUtils.ts deleted file mode 100644 index 1c6d70a7a528..000000000000 --- a/packages/browser/src/client/tester/locators/playwright-selector/roleUtils.ts +++ /dev/null @@ -1,1115 +0,0 @@ -/* eslint-disable ts/no-use-before-define */ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { closestCrossShadow, elementSafeTagName, enclosingShadowRootOrDocument, getElementComputedStyle, isElementStyleVisibilityVisible, isVisibleTextNode, parentElementOrShadowHost } from './domUtils' - -function hasExplicitAccessibleName(e: Element) { - return e.hasAttribute('aria-label') || e.hasAttribute('aria-labelledby') -} - -// https://www.w3.org/TR/wai-aria-practices/examples/landmarks/HTML5.html -const kAncestorPreventingLandmark = 'article:not([role]), aside:not([role]), main:not([role]), nav:not([role]), section:not([role]), [role=article], [role=complementary], [role=main], [role=navigation], [role=region]' - -// https://www.w3.org/TR/wai-aria-1.2/#global_states -const kGlobalAriaAttributes = new Map | undefined>([ - ['aria-atomic', undefined], - ['aria-busy', undefined], - ['aria-controls', undefined], - ['aria-current', undefined], - ['aria-describedby', undefined], - ['aria-details', undefined], - // Global use deprecated in ARIA 1.2 - // ['aria-disabled', undefined], - ['aria-dropeffect', undefined], - // Global use deprecated in ARIA 1.2 - // ['aria-errormessage', undefined], - ['aria-flowto', undefined], - ['aria-grabbed', undefined], - // Global use deprecated in ARIA 1.2 - // ['aria-haspopup', undefined], - ['aria-hidden', undefined], - // Global use deprecated in ARIA 1.2 - // ['aria-invalid', undefined], - ['aria-keyshortcuts', undefined], - ['aria-label', new Set(['caption', 'code', 'deletion', 'emphasis', 'generic', 'insertion', 'paragraph', 'presentation', 'strong', 'subscript', 'superscript'])], - ['aria-labelledby', new Set(['caption', 'code', 'deletion', 'emphasis', 'generic', 'insertion', 'paragraph', 'presentation', 'strong', 'subscript', 'superscript'])], - ['aria-live', undefined], - ['aria-owns', undefined], - ['aria-relevant', undefined], - ['aria-roledescription', new Set(['generic'])], -]) - -function hasGlobalAriaAttribute(element: Element, forRole?: string | null) { - return [...kGlobalAriaAttributes].some(([attr, prohibited]) => { - return !prohibited?.has(forRole || '') && element.hasAttribute(attr) - }) -} - -function hasTabIndex(element: Element) { - return !Number.isNaN(Number(String(element.getAttribute('tabindex')))) -} - -function isFocusable(element: Element) { - // TODO: - // - "inert" attribute makes the whole substree not focusable - // - when dialog is open on the page - everything but the dialog is not focusable - return !isNativelyDisabled(element) && (isNativelyFocusable(element) || hasTabIndex(element)) -} - -function isNativelyFocusable(element: Element) { - const tagName = elementSafeTagName(element) - if (['BUTTON', 'DETAILS', 'SELECT', 'TEXTAREA'].includes(tagName)) { - return true - } - if (tagName === 'A' || tagName === 'AREA') { - return element.hasAttribute('href') - } - if (tagName === 'INPUT') { - return !(element as HTMLInputElement).hidden - } - return false -} - -// https://w3c.github.io/html-aam/#html-element-role-mappings -// https://www.w3.org/TR/html-aria/#docconformance -const kImplicitRoleByTagName: { [tagName: string]: (e: Element) => string | null } = { - A: (e: Element) => { - return e.hasAttribute('href') ? 'link' : null - }, - AREA: (e: Element) => { - return e.hasAttribute('href') ? 'link' : null - }, - ARTICLE: () => 'article', - ASIDE: () => 'complementary', - BLOCKQUOTE: () => 'blockquote', - BUTTON: () => 'button', - CAPTION: () => 'caption', - CODE: () => 'code', - DATALIST: () => 'listbox', - DD: () => 'definition', - DEL: () => 'deletion', - DETAILS: () => 'group', - DFN: () => 'term', - DIALOG: () => 'dialog', - DT: () => 'term', - EM: () => 'emphasis', - FIELDSET: () => 'group', - FIGURE: () => 'figure', - FOOTER: (e: Element) => closestCrossShadow(e, kAncestorPreventingLandmark) ? null : 'contentinfo', - FORM: (e: Element) => hasExplicitAccessibleName(e) ? 'form' : null, - H1: () => 'heading', - H2: () => 'heading', - H3: () => 'heading', - H4: () => 'heading', - H5: () => 'heading', - H6: () => 'heading', - HEADER: (e: Element) => closestCrossShadow(e, kAncestorPreventingLandmark) ? null : 'banner', - HR: () => 'separator', - HTML: () => 'document', - IMG: (e: Element) => (e.getAttribute('alt') === '') && !e.getAttribute('title') && !hasGlobalAriaAttribute(e) && !hasTabIndex(e) ? 'presentation' : 'img', - INPUT: (e: Element) => { - const type = (e as HTMLInputElement).type.toLowerCase() - if (type === 'search') { - return e.hasAttribute('list') ? 'combobox' : 'searchbox' - } - if (['email', 'tel', 'text', 'url', ''].includes(type)) { - // https://html.spec.whatwg.org/multipage/input.html#concept-input-list - const list = getIdRefs(e, e.getAttribute('list'))[0] - return (list && elementSafeTagName(list) === 'DATALIST') ? 'combobox' : 'textbox' - } - if (type === 'hidden') { - return '' - } - return { - button: 'button', - checkbox: 'checkbox', - image: 'button', - number: 'spinbutton', - radio: 'radio', - range: 'slider', - reset: 'button', - submit: 'button', - }[type] || 'textbox' - }, - INS: () => 'insertion', - LI: () => 'listitem', - MAIN: () => 'main', - MARK: () => 'mark', - MATH: () => 'math', - MENU: () => 'list', - METER: () => 'meter', - NAV: () => 'navigation', - OL: () => 'list', - OPTGROUP: () => 'group', - OPTION: () => 'option', - OUTPUT: () => 'status', - P: () => 'paragraph', - PROGRESS: () => 'progressbar', - SECTION: (e: Element) => hasExplicitAccessibleName(e) ? 'region' : null, - SELECT: (e: Element) => e.hasAttribute('multiple') || (e as HTMLSelectElement).size > 1 ? 'listbox' : 'combobox', - STRONG: () => 'strong', - SUB: () => 'subscript', - SUP: () => 'superscript', - // For we default to Chrome behavior: - // - Chrome reports 'img'. - // - Firefox reports 'diagram' that is not in official ARIA spec yet. - // - Safari reports 'no role', but still computes accessible name. - SVG: () => 'img', - TABLE: () => 'table', - TBODY: () => 'rowgroup', - TD: (e: Element) => { - const table = closestCrossShadow(e, 'table') - const role = table ? getExplicitAriaRole(table) : '' - return (role === 'grid' || role === 'treegrid') ? 'gridcell' : 'cell' - }, - TEXTAREA: () => 'textbox', - TFOOT: () => 'rowgroup', - TH: (e: Element) => { - if (e.getAttribute('scope') === 'col') { - return 'columnheader' - } - if (e.getAttribute('scope') === 'row') { - return 'rowheader' - } - const table = closestCrossShadow(e, 'table') - const role = table ? getExplicitAriaRole(table) : '' - return (role === 'grid' || role === 'treegrid') ? 'gridcell' : 'cell' - }, - THEAD: () => 'rowgroup', - TIME: () => 'time', - TR: () => 'row', - UL: () => 'list', -} - -const kPresentationInheritanceParents: { [tagName: string]: string[] } = { - DD: ['DL', 'DIV'], - DIV: ['DL'], - DT: ['DL', 'DIV'], - LI: ['OL', 'UL'], - TBODY: ['TABLE'], - TD: ['TR'], - TFOOT: ['TABLE'], - TH: ['TR'], - THEAD: ['TABLE'], - TR: ['THEAD', 'TBODY', 'TFOOT', 'TABLE'], -} - -function getImplicitAriaRole(element: Element): string | null { - const implicitRole = kImplicitRoleByTagName[elementSafeTagName(element)]?.(element) || '' - if (!implicitRole) { - return null - } - // Inherit presentation role when required. - // https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none - let ancestor: Element | null = element - while (ancestor) { - const parent = parentElementOrShadowHost(ancestor) - const parents = kPresentationInheritanceParents[elementSafeTagName(ancestor)] - if (!parents || !parent || !parents.includes(elementSafeTagName(parent))) { - break - } - const parentExplicitRole = getExplicitAriaRole(parent) - if ((parentExplicitRole === 'none' || parentExplicitRole === 'presentation') && !hasPresentationConflictResolution(parent, parentExplicitRole)) { - return parentExplicitRole - } - ancestor = parent - } - return implicitRole -} - -// https://www.w3.org/TR/wai-aria-1.2/#role_definitions -const allRoles = [ - 'alert', - 'alertdialog', - 'application', - 'article', - 'banner', - 'blockquote', - 'button', - 'caption', - 'cell', - 'checkbox', - 'code', - 'columnheader', - 'combobox', - 'command', - 'complementary', - 'composite', - 'contentinfo', - 'definition', - 'deletion', - 'dialog', - 'directory', - 'document', - 'emphasis', - 'feed', - 'figure', - 'form', - 'generic', - 'grid', - 'gridcell', - 'group', - 'heading', - 'img', - 'input', - 'insertion', - 'landmark', - 'link', - 'list', - 'listbox', - 'listitem', - 'log', - 'main', - 'marquee', - 'math', - 'meter', - 'menu', - 'menubar', - 'menuitem', - 'menuitemcheckbox', - 'menuitemradio', - 'navigation', - 'none', - 'note', - 'option', - 'paragraph', - 'presentation', - 'progressbar', - 'radio', - 'radiogroup', - 'range', - 'region', - 'roletype', - 'row', - 'rowgroup', - 'rowheader', - 'scrollbar', - 'search', - 'searchbox', - 'section', - 'sectionhead', - 'select', - 'separator', - 'slider', - 'spinbutton', - 'status', - 'strong', - 'structure', - 'subscript', - 'superscript', - 'switch', - 'tab', - 'table', - 'tablist', - 'tabpanel', - 'term', - 'textbox', - 'time', - 'timer', - 'toolbar', - 'tooltip', - 'tree', - 'treegrid', - 'treeitem', - 'widget', - 'window', -] -// https://www.w3.org/TR/wai-aria-1.2/#abstract_roles -const abstractRoles = ['command', 'composite', 'input', 'landmark', 'range', 'roletype', 'section', 'sectionhead', 'select', 'structure', 'widget', 'window'] -const validRoles = allRoles.filter(role => !abstractRoles.includes(role)) - -function getExplicitAriaRole(element: Element): string | null { - // https://www.w3.org/TR/wai-aria-1.2/#document-handling_author-errors_roles - const roles = (element.getAttribute('role') || '').split(' ').map(role => role.trim()) - return roles.find(role => validRoles.includes(role)) || null -} - -function hasPresentationConflictResolution(element: Element, role: string | null) { - // https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none - return hasGlobalAriaAttribute(element, role) || isFocusable(element) -} - -export function getAriaRole(element: Element): string | null { - const explicitRole = getExplicitAriaRole(element) - if (!explicitRole) { - return getImplicitAriaRole(element) - } - if (explicitRole === 'none' || explicitRole === 'presentation') { - const implicitRole = getImplicitAriaRole(element) - if (hasPresentationConflictResolution(element, implicitRole)) { - return implicitRole - } - } - return explicitRole -} - -function getAriaBoolean(attr: string | null) { - return attr === null ? undefined : attr.toLowerCase() === 'true' -} - -// https://www.w3.org/TR/wai-aria-1.2/#tree_exclusion, but including "none" and "presentation" roles -// Not implemented: -// `Any descendants of elements that have the characteristic "Children Presentational: True"` -// https://www.w3.org/TR/wai-aria-1.2/#aria-hidden -export function isElementHiddenForAria(element: Element): boolean { - if (['STYLE', 'SCRIPT', 'NOSCRIPT', 'TEMPLATE'].includes(elementSafeTagName(element))) { - return true - } - const style = getElementComputedStyle(element) - const isSlot = element.nodeName === 'SLOT' - if (style?.display === 'contents' && !isSlot) { - // display:contents is not rendered itself, but its child nodes are. - for (let child = element.firstChild; child; child = child.nextSibling) { - if (child.nodeType === 1 /* Node.ELEMENT_NODE */ && !isElementHiddenForAria(child as Element)) { - return false - } - if (child.nodeType === 3 /* Node.TEXT_NODE */ && isVisibleTextNode(child as Text)) { - return false - } - } - return true - } - // Note: , but all browsers actually support it. - const summary = element.getAttribute('summary') || '' - if (summary) { - return summary - } - // SPEC DIFFERENCE. - // Spec says "if the table element has a title attribute, then use that attribute". - // We ignore title to pass "name_from_content-manual.html". - } - - // https://w3c.github.io/html-aam/#area-element - if (tagName === 'AREA') { - options.visitedElements.add(element) - const alt = element.getAttribute('alt') || '' - if (trimFlatString(alt)) { - return alt - } - const title = element.getAttribute('title') || '' - return title - } - - // https://www.w3.org/TR/svg-aam-1.0/#mapping_additional_nd - if (tagName === 'SVG' || (element as SVGElement).ownerSVGElement) { - options.visitedElements.add(element) - for (let child = element.firstElementChild; child; child = child.nextElementSibling) { - if (elementSafeTagName(child) === 'TITLE' && (child as SVGElement).ownerSVGElement) { - return getTextAlternativeInternal(child, { - ...childOptions, - embeddedInLabelledBy: { element: child, hidden: isElementHiddenForAria(child) }, - }) - } - } - } - if ((element as SVGElement).ownerSVGElement && tagName === 'A') { - const title = element.getAttribute('xlink:title') || '' - if (trimFlatString(title)) { - options.visitedElements.add(element) - return title - } - } - } - - // See https://w3c.github.io/html-aam/#summary-element-accessible-name-computation for "summary"-specific check. - const shouldNameFromContentForSummary = tagName === 'SUMMARY' && !['presentation', 'none'].includes(role) - - // step 2f + step 2h. - if (allowsNameFromContent(role, options.embeddedInTargetElement === 'descendant') - || shouldNameFromContentForSummary - || !!options.embeddedInLabelledBy || !!options.embeddedInDescribedBy - || !!options.embeddedInLabel || !!options.embeddedInNativeTextAlternative) { - options.visitedElements.add(element) - const tokens: string[] = [] - const visit = (node: Node, skipSlotted: boolean) => { - if (skipSlotted && (node as Element | Text).assignedSlot) { - return - } - if (node.nodeType === 1 /* Node.ELEMENT_NODE */) { - const display = getElementComputedStyle(node as Element)?.display || 'inline' - let token = getTextAlternativeInternal(node as Element, childOptions) - // SPEC DIFFERENCE. - // Spec says "append the result to the accumulated text", assuming "with space". - // However, multiple tests insist that inline elements do not add a space. - // Additionally,
insists on a space anyway, see "name_file-label-inline-block-elements-manual.html" - if (display !== 'inline' || node.nodeName === 'BR') { - token = ` ${token} ` - } - tokens.push(token) - } - else if (node.nodeType === 3 /* Node.TEXT_NODE */) { - // step 2g. - tokens.push(node.textContent || '') - } - } - tokens.push(getPseudoContent(element, '::before')) - const assignedNodes = element.nodeName === 'SLOT' ? (element as HTMLSlotElement).assignedNodes() : [] - if (assignedNodes.length) { - for (const child of assignedNodes) { - visit(child, false) - } - } - else { - for (let child = element.firstChild; child; child = child.nextSibling) { - visit(child, true) - } - if (element.shadowRoot) { - for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) { - visit(child, true) - } - } - for (const owned of getIdRefs(element, element.getAttribute('aria-owns'))) { - visit(owned, true) - } - } - tokens.push(getPseudoContent(element, '::after')) - const accessibleName = tokens.join('') - // Spec says "Return the accumulated text if it is not the empty string". However, that is not really - // compatible with the real browser behavior and wpt tests, where an element with empty contents will fallback to the title. - // So we follow the spec everywhere except for the target element itself. This can probably be improved. - const maybeTrimmedAccessibleName = options.embeddedInTargetElement === 'self' ? trimFlatString(accessibleName) : accessibleName - if (maybeTrimmedAccessibleName) { - return accessibleName - } - } - - // step 2i. - if (!['presentation', 'none'].includes(role) || tagName === 'IFRAME') { - options.visitedElements.add(element) - const title = element.getAttribute('title') || '' - if (trimFlatString(title)) { - return title - } - } - - options.visitedElements.add(element) - return '' -} - -export const kAriaSelectedRoles = ['gridcell', 'option', 'row', 'tab', 'rowheader', 'columnheader', 'treeitem'] -export function getAriaSelected(element: Element): boolean { - // https://www.w3.org/TR/wai-aria-1.2/#aria-selected - // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings - if (elementSafeTagName(element) === 'OPTION') { - return (element as HTMLOptionElement).selected - } - if (kAriaSelectedRoles.includes(getAriaRole(element) || '')) { - return getAriaBoolean(element.getAttribute('aria-selected')) === true - } - return false -} - -export const kAriaCheckedRoles = ['checkbox', 'menuitemcheckbox', 'option', 'radio', 'switch', 'menuitemradio', 'treeitem'] -export function getAriaChecked(element: Element): boolean | 'mixed' { - const result = getChecked(element, true) - return result === 'error' ? false : result -} -function getChecked(element: Element, allowMixed: boolean): boolean | 'mixed' | 'error' { - const tagName = elementSafeTagName(element) - // https://www.w3.org/TR/wai-aria-1.2/#aria-checked - // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings - if (allowMixed && tagName === 'INPUT' && (element as HTMLInputElement).indeterminate) { - return 'mixed' - } - if (tagName === 'INPUT' && ['checkbox', 'radio'].includes((element as HTMLInputElement).type)) { - return (element as HTMLInputElement).checked - } - if (kAriaCheckedRoles.includes(getAriaRole(element) || '')) { - const checked = element.getAttribute('aria-checked') - if (checked === 'true') { - return true - } - if (allowMixed && checked === 'mixed') { - return 'mixed' - } - return false - } - return 'error' -} - -export const kAriaPressedRoles = ['button'] -export function getAriaPressed(element: Element): boolean | 'mixed' { - // https://www.w3.org/TR/wai-aria-1.2/#aria-pressed - if (kAriaPressedRoles.includes(getAriaRole(element) || '')) { - const pressed = element.getAttribute('aria-pressed') - if (pressed === 'true') { - return true - } - if (pressed === 'mixed') { - return 'mixed' - } - } - return false -} - -export const kAriaExpandedRoles = ['application', 'button', 'checkbox', 'combobox', 'gridcell', 'link', 'listbox', 'menuitem', 'row', 'rowheader', 'tab', 'treeitem', 'columnheader', 'menuitemcheckbox', 'menuitemradio', 'rowheader', 'switch'] -export function getAriaExpanded(element: Element): boolean | 'none' { - // https://www.w3.org/TR/wai-aria-1.2/#aria-expanded - // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings - if (elementSafeTagName(element) === 'DETAILS') { - return (element as HTMLDetailsElement).open - } - if (kAriaExpandedRoles.includes(getAriaRole(element) || '')) { - const expanded = element.getAttribute('aria-expanded') - if (expanded === null) { - return 'none' - } - if (expanded === 'true') { - return true - } - return false - } - return 'none' -} - -export const kAriaLevelRoles = ['heading', 'listitem', 'row', 'treeitem'] -export function getAriaLevel(element: Element): number { - // https://www.w3.org/TR/wai-aria-1.2/#aria-level - // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings - const native = { H1: 1, H2: 2, H3: 3, H4: 4, H5: 5, H6: 6 }[elementSafeTagName(element)] - if (native) { - return native - } - if (kAriaLevelRoles.includes(getAriaRole(element) || '')) { - const attr = element.getAttribute('aria-level') - const value = attr === null ? Number.NaN : Number(attr) - if (Number.isInteger(value) && value >= 1) { - return value - } - } - return 0 -} - -const kAriaDisabledRoles = ['application', 'button', 'composite', 'gridcell', 'group', 'input', 'link', 'menuitem', 'scrollbar', 'separator', 'tab', 'checkbox', 'columnheader', 'combobox', 'grid', 'listbox', 'menu', 'menubar', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'radiogroup', 'row', 'rowheader', 'searchbox', 'select', 'slider', 'spinbutton', 'switch', 'tablist', 'textbox', 'toolbar', 'tree', 'treegrid', 'treeitem'] -export function getAriaDisabled(element: Element): boolean { - // https://www.w3.org/TR/wai-aria-1.2/#aria-disabled - // Note that aria-disabled applies to all descendants, so we look up the hierarchy. - return isNativelyDisabled(element) || hasExplicitAriaDisabled(element) -} - -function isNativelyDisabled(element: Element) { - // https://www.w3.org/TR/html-aam-1.0/#html-attribute-state-and-property-mappings - const isNativeFormControl = ['BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'OPTION', 'OPTGROUP'].includes(element.tagName) - return isNativeFormControl && (element.hasAttribute('disabled') || belongsToDisabledFieldSet(element)) -} - -function belongsToDisabledFieldSet(element: Element | null): boolean { - if (!element) { - return false - } - if (elementSafeTagName(element) === 'FIELDSET' && element.hasAttribute('disabled')) { - return true - } - // fieldset does not work across shadow boundaries. - return belongsToDisabledFieldSet(element.parentElement) -} - -function hasExplicitAriaDisabled(element: Element | undefined): boolean { - if (!element) { - return false - } - if (kAriaDisabledRoles.includes(getAriaRole(element) || '')) { - const attribute = (element.getAttribute('aria-disabled') || '').toLowerCase() - if (attribute === 'true') { - return true - } - if (attribute === 'false') { - return false - } - } - // aria-disabled works across shadow boundaries. - return hasExplicitAriaDisabled(parentElementOrShadowHost(element)) -} - -function getAccessibleNameFromAssociatedLabels(labels: Iterable, options: AccessibleNameOptions) { - return [...labels].map(label => getTextAlternativeInternal(label, { - ...options, - embeddedInLabel: { element: label, hidden: isElementHiddenForAria(label) }, - embeddedInNativeTextAlternative: undefined, - embeddedInLabelledBy: undefined, - embeddedInDescribedBy: undefined, - embeddedInTargetElement: 'none', - })).filter(accessibleName => !!accessibleName).join(' ') -} - -let cacheAccessibleName!: Map | undefined -let cacheAccessibleNameHidden!: Map | undefined -let cacheAccessibleDescription!: Map | undefined -let cacheAccessibleDescriptionHidden!: Map | undefined -let cacheIsHidden!: Map | undefined -let cachePseudoContentBefore!: Map | undefined -let cachePseudoContentAfter!: Map | undefined -let cachesCounter = 0 - -export function beginAriaCaches() { - ++cachesCounter - cacheAccessibleName ??= new Map() - cacheAccessibleNameHidden ??= new Map() - cacheAccessibleDescription ??= new Map() - cacheAccessibleDescriptionHidden ??= new Map() - cacheIsHidden ??= new Map() - cachePseudoContentBefore ??= new Map() - cachePseudoContentAfter ??= new Map() -} - -export function endAriaCaches() { - if (!--cachesCounter) { - cacheAccessibleName = undefined - cacheAccessibleNameHidden = undefined - cacheAccessibleDescription = undefined - cacheAccessibleDescriptionHidden = undefined - cacheIsHidden = undefined - cachePseudoContentBefore = undefined - cachePseudoContentAfter = undefined - } -} diff --git a/packages/browser/src/client/tester/locators/playwright-selector/selector.ts b/packages/browser/src/client/tester/locators/playwright-selector/selector.ts deleted file mode 100644 index 128967e7a6d9..000000000000 --- a/packages/browser/src/client/tester/locators/playwright-selector/selector.ts +++ /dev/null @@ -1,505 +0,0 @@ -/* eslint-disable ts/no-use-before-define */ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// based on https://github.com/microsoft/playwright/blob/4554372e456154d7365b6902ef9f3e1e7de76e94/packages/playwright-core/src/server/injected/injectedScript.ts - -import { page, server } from '@vitest/browser/context' -import { createRoleEngine } from './roleSelectorEngine' -import type { NestedSelectorBody, ParsedSelector, ParsedSelectorPart } from './selectorParser' -import { parseAttributeSelector, parseSelector, stringifySelector, visitAllSelectorParts } from './selectorParser' -import { asLocator } from './locatorGenerators' -import type { GenerateSelectorOptions } from './selectorGenerator' -import { generateSelector } from './selectorGenerator' -import { normalizeWhiteSpace, trimStringWithEllipsis } from './stringUtils' -import type { LayoutSelectorName } from './layoutSelectorUtils' -import { kLayoutSelectorNames, layoutSelectorScore } from './layoutSelectorUtils' -import { SelectorEvaluatorImpl, sortInDOMOrder } from './selectorEvaluator' -import type { ElementText, TextMatcher } from './selectorUtils' -import { elementMatchesText, elementText, getElementLabels } from './selectorUtils' -import type { CSSComplexSelectorList } from './cssParser' -import { isElementVisible } from './domUtils' - -export class PlaywrightSelector { - _engines: Map - _evaluator: SelectorEvaluatorImpl - - constructor() { - this._evaluator = new SelectorEvaluatorImpl(new Map()) - this._engines = new Map() - this._engines.set('xpath', XPathEngine) - this._engines.set('xpath:light', XPathEngine) - this._engines.set('role', createRoleEngine(false)) - this._engines.set('text', this._createTextEngine(true, false)) - this._engines.set('text:light', this._createTextEngine(false, false)) - this._engines.set('id', this._createAttributeEngine('id', true)) - this._engines.set('id:light', this._createAttributeEngine('id', false)) - this._engines.set('data-testid', this._createAttributeEngine('data-testid', true)) - this._engines.set('data-testid:light', this._createAttributeEngine('data-testid', false)) - this._engines.set('data-test-id', this._createAttributeEngine('data-test-id', true)) - this._engines.set('data-test-id:light', this._createAttributeEngine('data-test-id', false)) - this._engines.set('data-test', this._createAttributeEngine('data-test', true)) - this._engines.set('data-test:light', this._createAttributeEngine('data-test', false)) - this._engines.set('css', this._createCSSEngine()) - this._engines.set('nth', { queryAll: () => [] }) - this._engines.set('visible', this._createVisibleEngine()) - this._engines.set('internal:control', { queryAll: () => [] }) - this._engines.set('internal:has', this._createHasEngine()) - this._engines.set('internal:has-not', this._createHasNotEngine()) - this._engines.set('internal:and', { queryAll: () => [] }) - this._engines.set('internal:or', { queryAll: () => [] }) - this._engines.set('internal:chain', this._createInternalChainEngine()) - this._engines.set('internal:label', this._createInternalLabelEngine()) - this._engines.set('internal:text', this._createTextEngine(true, true)) - this._engines.set('internal:has-text', this._createInternalHasTextEngine()) - this._engines.set('internal:has-not-text', this._createInternalHasNotTextEngine()) - this._engines.set('internal:attr', this._createNamedAttributeEngine()) - this._engines.set('internal:testid', this._createNamedAttributeEngine()) - this._engines.set('internal:role', createRoleEngine(true)) - } - - querySelector(selector: ParsedSelector, root: Node, strict: boolean): Element | null { - const result = this.querySelectorAll(selector, root) - if (strict && result.length > 1) { - throw this.strictModeViolationError(selector, result) - } - return result[0] || null - } - - strictModeViolationError(selector: ParsedSelector, matches: Element[]): Error { - const infos = matches.slice(0, 10).map(m => ({ - preview: this.previewNode(m), - selector: this.generateSelectorSimple(m), - })) - const lines = infos.map((info, i) => `\n ${i + 1}) ${info.preview} aka ${asLocator('javascript', info.selector)}`) - if (infos.length < matches.length) { - lines.push('\n ...') - } - return this.createStacklessError(`strict mode violation: ${asLocator('javascript', stringifySelector(selector))} resolved to ${matches.length} elements:${lines.join('')}\n`) - } - - generateSelectorSimple(targetElement: Element, options?: GenerateSelectorOptions): string { - return generateSelector(this, targetElement, { ...options, testIdAttributeName: page.config.browser.locators.testIdAttribute }) - } - - parseSelector(selector: string): ParsedSelector { - const result = parseSelector(selector) - visitAllSelectorParts(result, (part) => { - if (!this._engines.has(part.name)) { - throw this.createStacklessError(`Unknown engine "${part.name}" while parsing selector ${selector}`) - } - }) - return result - } - - previewNode(node: Node): string { - if (node.nodeType === Node.TEXT_NODE) { - return oneLine(`#text=${node.nodeValue || ''}`) - } - if (node.nodeType !== Node.ELEMENT_NODE) { - return oneLine(`<${node.nodeName.toLowerCase()} />`) - } - const element = node as Element - - const attrs: string[] = [] - for (let i = 0; i < element.attributes.length; i++) { - const { name, value } = element.attributes[i] - if (name === 'style') { - continue - } - if (!value && booleanAttributes.has(name)) { - attrs.push(` ${name}`) - } - else { attrs.push(` ${name}="${value}"`) } - } - attrs.sort((a, b) => a.length - b.length) - const attrText = trimStringWithEllipsis(attrs.join(''), 500) - if (autoClosingTags.has(element.nodeName)) { - return oneLine(`<${element.nodeName.toLowerCase()}${attrText}/>`) - } - - const children = element.childNodes - let onlyText = false - if (children.length <= 5) { - onlyText = true - for (let i = 0; i < children.length; i++) { - onlyText = onlyText && children[i].nodeType === Node.TEXT_NODE - } - } - const text = onlyText ? (element.textContent || '') : (children.length ? '\u2026' : '') - return oneLine(`<${element.nodeName.toLowerCase()}${attrText}>${trimStringWithEllipsis(text, 50)}`) - } - - querySelectorAll(selector: ParsedSelector, root: Node): Element[] { - if (selector.capture !== undefined) { - if (selector.parts.some(part => part.name === 'nth')) { - throw this.createStacklessError(`Can't query n-th element in a request with the capture.`) - } - const withHas: ParsedSelector = { parts: selector.parts.slice(0, selector.capture + 1) } - if (selector.capture < selector.parts.length - 1) { - const parsed: ParsedSelector = { parts: selector.parts.slice(selector.capture + 1) } - const has: ParsedSelectorPart = { name: 'internal:has', body: { parsed }, source: stringifySelector(parsed) } - withHas.parts.push(has) - } - return this.querySelectorAll(withHas, root) - } - - if (!(root as any).querySelectorAll) { - throw this.createStacklessError('Node is not queryable.') - } - - if (selector.capture !== undefined) { - // We should have handled the capture above. - throw this.createStacklessError('Internal error: there should not be a capture in the selector.') - } - - // Workaround so that ":scope" matches the ShadowRoot. - // This is, unfortunately, because an ElementHandle can point to any Node (including ShadowRoot/Document/etc), - // and not just to an Element, and we support various APIs on ElementHandle like "textContent()". - if (root.nodeType === 11 /* Node.DOCUMENT_FRAGMENT_NODE */ && selector.parts.length === 1 && selector.parts[0].name === 'css' && selector.parts[0].source === ':scope') { - return [root as Element] - } - - this._evaluator.begin() - try { - let roots = new Set([root as Element]) - for (const part of selector.parts) { - if (part.name === 'nth') { - roots = this._queryNth(roots, part) - } - else if (part.name === 'internal:and') { - const andElements = this.querySelectorAll((part.body as NestedSelectorBody).parsed, root) - roots = new Set(andElements.filter(e => roots.has(e))) - } - else if (part.name === 'internal:or') { - const orElements = this.querySelectorAll((part.body as NestedSelectorBody).parsed, root) - roots = new Set(sortInDOMOrder(new Set([...roots, ...orElements]))) - } - else if (kLayoutSelectorNames.includes(part.name as LayoutSelectorName)) { - roots = this._queryLayoutSelector(roots, part, root) - } - else { - const next = new Set() - for (const root of roots) { - const all = this._queryEngineAll(part, root) - for (const one of all) { - next.add(one) - } - } - roots = next - } - } - return [...roots] - } - finally { - this._evaluator.end() - } - } - - private _queryEngineAll(part: ParsedSelectorPart, root: SelectorRoot): Element[] { - const result = this._engines.get(part.name)!.queryAll(root, part.body) - for (const element of result) { - if (!('nodeName' in element)) { - throw this.createStacklessError(`Expected a Node but got ${Object.prototype.toString.call(element)}`) - } - } - return result - } - - private _queryNth(elements: Set, part: ParsedSelectorPart): Set { - const list = [...elements] - let nth = +part.body - if (nth === -1) { - nth = list.length - 1 - } - return new Set(list.slice(nth, nth + 1)) - } - - private _queryLayoutSelector(elements: Set, part: ParsedSelectorPart, originalRoot: Node): Set { - const name = part.name as LayoutSelectorName - const body = part.body as NestedSelectorBody - const result: { element: Element; score: number }[] = [] - const inner = this.querySelectorAll(body.parsed, originalRoot) - for (const element of elements) { - const score = layoutSelectorScore(name, element, inner, body.distance) - if (score !== undefined) { - result.push({ element, score }) - } - } - result.sort((a, b) => a.score - b.score) - return new Set(result.map(r => r.element)) - } - - createStacklessError(message: string): Error { - if (server.browser === 'firefox') { - const error = new Error(`Error: ${message}`) - // Firefox cannot delete the stack, so assign to an empty string. - error.stack = '' - return error - } - const error = new Error(message) - // Chromium/WebKit should delete the stack instead. - delete error.stack - return error - } - - private _createTextEngine(shadow: boolean, internal: boolean): SelectorEngine { - const queryAll = (root: SelectorRoot, selector: string): Element[] => { - const { matcher, kind } = createTextMatcher(selector, internal) - const result: Element[] = [] - let lastDidNotMatchSelf: Element | null = null - - const appendElement = (element: Element) => { - // TODO: replace contains() with something shadow-dom-aware? - if (kind === 'lax' && lastDidNotMatchSelf && lastDidNotMatchSelf.contains(element)) { - return false - } - const matches = elementMatchesText(this._evaluator._cacheText, element, matcher) - if (matches === 'none') { - lastDidNotMatchSelf = element - } - if (matches === 'self' || (matches === 'selfAndChildren' && kind === 'strict' && !internal)) { - result.push(element) - } - } - - if (root.nodeType === Node.ELEMENT_NODE) { - appendElement(root as Element) - } - const elements = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: shadow }, '*') - for (const element of elements) { - appendElement(element) - } - return result - } - return { queryAll } - } - - private _createAttributeEngine(attribute: string, shadow: boolean): SelectorEngine { - const toCSS = (selector: string): CSSComplexSelectorList => { - const css = `[${attribute}=${JSON.stringify(selector)}]` - return [{ simples: [{ selector: { css, functions: [] }, combinator: '' }] }] - } - return { - queryAll: (root: SelectorRoot, selector: string): Element[] => { - return this._evaluator.query({ scope: root as Document | Element, pierceShadow: shadow }, toCSS(selector)) - }, - } - } - - private _createCSSEngine(): SelectorEngine { - return { - queryAll: (root: SelectorRoot, body: any) => { - return this._evaluator.query({ scope: root as Document | Element, pierceShadow: true }, body) - }, - } - } - - private _createNamedAttributeEngine(): SelectorEngine { - const queryAll = (root: SelectorRoot, selector: string): Element[] => { - const parsed = parseAttributeSelector(selector, true) - if (parsed.name || parsed.attributes.length !== 1) { - throw new Error(`Malformed attribute selector: ${selector}`) - } - const { name, value, caseSensitive } = parsed.attributes[0] - const lowerCaseValue = caseSensitive ? null : value.toLowerCase() - let matcher: (s: string) => boolean - if (value instanceof RegExp) { - matcher = s => !!s.match(value) - } - else if (caseSensitive) { - matcher = s => s === value - } - else { - matcher = s => s.toLowerCase().includes(lowerCaseValue!) - } - const elements = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: true }, `[${name}]`) - return elements.filter(e => matcher(e.getAttribute(name)!)) - } - return { queryAll } - } - - private _createVisibleEngine(): SelectorEngine { - const queryAll = (root: SelectorRoot, body: string) => { - if (root.nodeType !== 1 /* Node.ELEMENT_NODE */) { - return [] - } - return isElementVisible(root as Element) === Boolean(body) ? [root as Element] : [] - } - return { queryAll } - } - - private _createHasEngine(): SelectorEngine { - const queryAll = (root: SelectorRoot, body: NestedSelectorBody) => { - if (root.nodeType !== 1 /* Node.ELEMENT_NODE */) { - return [] - } - const has = !!this.querySelector(body.parsed, root, false) - return has ? [root as Element] : [] - } - return { queryAll } - } - - private _createHasNotEngine(): SelectorEngine { - const queryAll = (root: SelectorRoot, body: NestedSelectorBody) => { - if (root.nodeType !== 1 /* Node.ELEMENT_NODE */) { - return [] - } - const has = !!this.querySelector(body.parsed, root, false) - return has ? [] : [root as Element] - } - return { queryAll } - } - - private _createInternalChainEngine(): SelectorEngine { - const queryAll = (root: SelectorRoot, body: NestedSelectorBody) => { - return this.querySelectorAll(body.parsed, root) - } - return { queryAll } - } - - private _createInternalLabelEngine(): SelectorEngine { - return { - queryAll: (root: SelectorRoot, selector: string): Element[] => { - const { matcher } = createTextMatcher(selector, true) - const allElements = this._evaluator._queryCSS({ scope: root as Document | Element, pierceShadow: true }, '*') - return allElements.filter((element) => { - return getElementLabels(this._evaluator._cacheText, element).some(label => matcher(label)) - }) - }, - } - } - - private _createInternalHasTextEngine(): SelectorEngine { - return { - queryAll: (root: SelectorRoot, selector: string): Element[] => { - if (root.nodeType !== 1 /* Node.ELEMENT_NODE */) { - return [] - } - const element = root as Element - const text = elementText(this._evaluator._cacheText, element) - const { matcher } = createTextMatcher(selector, true) - return matcher(text) ? [element] : [] - }, - } - } - - private _createInternalHasNotTextEngine(): SelectorEngine { - return { - queryAll: (root: SelectorRoot, selector: string): Element[] => { - if (root.nodeType !== 1 /* Node.ELEMENT_NODE */) { - return [] - } - const element = root as Element - const text = elementText(this._evaluator._cacheText, element) - const { matcher } = createTextMatcher(selector, true) - return matcher(text) ? [] : [element] - }, - } - } -} - -export type SelectorRoot = Element | ShadowRoot | Document - -export interface SelectorEngine { - queryAll: (root: SelectorRoot, selector: string | any) => Element[] -} - -function oneLine(s: string): string { - return s.replace(/\n/g, '↵').replace(/\t/g, '⇆') -} - -const booleanAttributes = new Set(['checked', 'selected', 'disabled', 'readonly', 'multiple']) -const autoClosingTags = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']) - -function cssUnquote(s: string): string { - // Trim quotes. - s = s.substring(1, s.length - 1) - if (!s.includes('\\')) { - return s - } - const r: string[] = [] - let i = 0 - while (i < s.length) { - if (s[i] === '\\' && i + 1 < s.length) { - i++ - } - r.push(s[i++]) - } - return r.join('') -} - -function createTextMatcher(selector: string, internal: boolean): { matcher: TextMatcher; kind: 'regex' | 'strict' | 'lax' } { - if (selector[0] === '/' && selector.lastIndexOf('/') > 0) { - const lastSlash = selector.lastIndexOf('/') - const re = new RegExp(selector.substring(1, lastSlash), selector.substring(lastSlash + 1)) - return { matcher: (elementText: ElementText) => re.test(elementText.full), kind: 'regex' } - } - const unquote = internal ? JSON.parse.bind(JSON) : cssUnquote - let strict = false - if (selector.length > 1 && selector[0] === '"' && selector[selector.length - 1] === '"') { - selector = unquote(selector) - strict = true - } - else if (internal && selector.length > 1 && selector[0] === '"' && selector[selector.length - 2] === '"' && selector[selector.length - 1] === 'i') { - selector = unquote(selector.substring(0, selector.length - 1)) - strict = false - } - else if (internal && selector.length > 1 && selector[0] === '"' && selector[selector.length - 2] === '"' && selector[selector.length - 1] === 's') { - selector = unquote(selector.substring(0, selector.length - 1)) - strict = true - } - else if (selector.length > 1 && selector[0] === '\'' && selector[selector.length - 1] === '\'') { - selector = unquote(selector) - strict = true - } - selector = normalizeWhiteSpace(selector) - if (strict) { - if (internal) { - return { kind: 'strict', matcher: (elementText: ElementText) => elementText.normalized === selector } - } - - const strictTextNodeMatcher = (elementText: ElementText) => { - if (!selector && !elementText.immediate.length) { - return true - } - return elementText.immediate.some(s => normalizeWhiteSpace(s) === selector) - } - return { matcher: strictTextNodeMatcher, kind: 'strict' } - } - selector = selector.toLowerCase() - return { kind: 'lax', matcher: (elementText: ElementText) => elementText.normalized.toLowerCase().includes(selector) } -} - -export const XPathEngine: SelectorEngine = { - queryAll(root: SelectorRoot, selector: string): Element[] { - if (selector.startsWith('/') && root.nodeType !== Node.DOCUMENT_NODE) { - selector = `.${selector}` - } - const result: Element[] = [] - const document = root.ownerDocument || root - if (!document) { - return result - } - const it = document.evaluate(selector, root, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE) - for (let node = it.iterateNext(); node; node = it.iterateNext()) { - if (node.nodeType === Node.ELEMENT_NODE) { - result.push(node as Element) - } - } - return result - }, -} diff --git a/packages/browser/src/client/tester/locators/playwright-selector/selectorEvaluator.ts b/packages/browser/src/client/tester/locators/playwright-selector/selectorEvaluator.ts deleted file mode 100644 index 03f6a3e89bf5..000000000000 --- a/packages/browser/src/client/tester/locators/playwright-selector/selectorEvaluator.ts +++ /dev/null @@ -1,666 +0,0 @@ -/* eslint-disable ts/no-use-before-define */ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// copied without changes from https://github.com/microsoft/playwright/blob/4554372e456154d7365b6902ef9f3e1e7de76e94/packages/playwright-core/src/server/injected/selectorEvaluator.ts - -import type { CSSComplexSelector, CSSComplexSelectorList, CSSFunctionArgument, CSSSimpleSelector } from './cssParser' -import { customCSSNames } from './selectorParser' -import { isElementVisible, parentElementOrShadowHost } from './domUtils' -import { type LayoutSelectorName, layoutSelectorScore } from './layoutSelectorUtils' -import { type ElementText, elementMatchesText, elementText, shouldSkipForTextMatching } from './selectorUtils' -import { normalizeWhiteSpace } from './stringUtils' - -interface QueryContext { - scope: Element | Document - pierceShadow: boolean - // When context expands to accommodate :scope matching, original scope is saved here. - originalScope?: Element | Document - // Place for more options, e.g. normalizing whitespace. -} -export type Selector = any // Opaque selector type. -export interface SelectorEvaluator { - query: (context: QueryContext, selector: Selector) => Element[] - matches: (element: Element, selector: Selector, context: QueryContext) => boolean -} -export interface SelectorEngine { - matches?: (element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator) => boolean - query?: (context: QueryContext, args: (string | number | Selector)[], evaluator: SelectorEvaluator) => Element[] -} - -type QueryCache = Map -export class SelectorEvaluatorImpl implements SelectorEvaluator { - private _engines = new Map() - private _cacheQueryCSS: QueryCache = new Map() - private _cacheMatches: QueryCache = new Map() - private _cacheQuery: QueryCache = new Map() - private _cacheMatchesSimple: QueryCache = new Map() - private _cacheMatchesParents: QueryCache = new Map() - private _cacheCallMatches: QueryCache = new Map() - private _cacheCallQuery: QueryCache = new Map() - private _cacheQuerySimple: QueryCache = new Map() - _cacheText = new Map() - private _scoreMap: Map | undefined - private _retainCacheCounter = 0 - - constructor(extraEngines: Map) { - for (const [name, engine] of extraEngines) { - this._engines.set(name, engine) - } - this._engines.set('not', notEngine) - this._engines.set('is', isEngine) - this._engines.set('where', isEngine) - this._engines.set('has', hasEngine) - this._engines.set('scope', scopeEngine) - this._engines.set('light', lightEngine) - this._engines.set('visible', visibleEngine) - this._engines.set('text', textEngine) - this._engines.set('text-is', textIsEngine) - this._engines.set('text-matches', textMatchesEngine) - this._engines.set('has-text', hasTextEngine) - this._engines.set('right-of', createLayoutEngine('right-of')) - this._engines.set('left-of', createLayoutEngine('left-of')) - this._engines.set('above', createLayoutEngine('above')) - this._engines.set('below', createLayoutEngine('below')) - this._engines.set('near', createLayoutEngine('near')) - this._engines.set('nth-match', nthMatchEngine) - - const allNames = [...this._engines.keys()] - allNames.sort() - const parserNames = [...customCSSNames] - parserNames.sort() - if (allNames.join('|') !== parserNames.join('|')) { - throw new Error(`Please keep customCSSNames in sync with evaluator engines: ${allNames.join('|')} vs ${parserNames.join('|')}`) - } - } - - begin() { - ++this._retainCacheCounter - } - - end() { - --this._retainCacheCounter - if (!this._retainCacheCounter) { - this._cacheQueryCSS.clear() - this._cacheMatches.clear() - this._cacheQuery.clear() - this._cacheMatchesSimple.clear() - this._cacheMatchesParents.clear() - this._cacheCallMatches.clear() - this._cacheCallQuery.clear() - this._cacheQuerySimple.clear() - this._cacheText.clear() - } - } - - private _cached(cache: QueryCache, main: any, rest: any[], cb: () => T): T { - if (!cache.has(main)) { - cache.set(main, []) - } - const entries = cache.get(main)! - const entry = entries.find(e => rest.every((value, index) => e.rest[index] === value)) - if (entry) { - return entry.result as T - } - const result = cb() - entries.push({ rest, result }) - return result - } - - private _checkSelector(s: Selector): CSSComplexSelector | CSSComplexSelectorList { - const wellFormed = typeof s === 'object' && s - && (Array.isArray(s) || (('simples' in s) && (s.simples.length))) - if (!wellFormed) { - throw new Error(`Malformed selector "${s}"`) - } - return s as CSSComplexSelector | CSSComplexSelectorList - } - - matches(element: Element, s: Selector, context: QueryContext): boolean { - const selector = this._checkSelector(s) - this.begin() - try { - return this._cached(this._cacheMatches, element, [selector, context.scope, context.pierceShadow, context.originalScope], () => { - if (Array.isArray(selector)) { - return this._matchesEngine(isEngine, element, selector, context) - } - if (this._hasScopeClause(selector)) { - context = this._expandContextForScopeMatching(context) - } - if (!this._matchesSimple(element, selector.simples[selector.simples.length - 1].selector, context)) { - return false - } - return this._matchesParents(element, selector, selector.simples.length - 2, context) - }) - } - finally { - this.end() - } - } - - query(context: QueryContext, s: any): Element[] { - const selector = this._checkSelector(s) - this.begin() - try { - return this._cached(this._cacheQuery, selector, [context.scope, context.pierceShadow, context.originalScope], () => { - if (Array.isArray(selector)) { - return this._queryEngine(isEngine, context, selector) - } - if (this._hasScopeClause(selector)) { - context = this._expandContextForScopeMatching(context) - } - - // query() recursively calls itself, so we set up a new map for this particular query() call. - const previousScoreMap = this._scoreMap - this._scoreMap = new Map() - let elements = this._querySimple(context, selector.simples[selector.simples.length - 1].selector) - elements = elements.filter(element => this._matchesParents(element, selector, selector.simples.length - 2, context)) - if (this._scoreMap.size) { - elements.sort((a, b) => { - const aScore = this._scoreMap!.get(a) - const bScore = this._scoreMap!.get(b) - if (aScore === bScore) { - return 0 - } - if (aScore === undefined) { - return 1 - } - if (bScore === undefined) { - return -1 - } - return aScore - bScore - }) - } - this._scoreMap = previousScoreMap - - return elements - }) - } - finally { - this.end() - } - } - - _markScore(element: Element, score: number) { - // HACK ALERT: temporary marks an element with a score, to be used - // for sorting at the end of the query(). - if (this._scoreMap) { - this._scoreMap.set(element, score) - } - } - - private _hasScopeClause(selector: CSSComplexSelector): boolean { - return selector.simples.some(simple => simple.selector.functions.some(f => f.name === 'scope')) - } - - private _expandContextForScopeMatching(context: QueryContext): QueryContext { - if (context.scope.nodeType !== 1 /* Node.ELEMENT_NODE */) { - return context - } - const scope = parentElementOrShadowHost(context.scope as Element) - if (!scope) { - return context - } - return { ...context, scope, originalScope: context.originalScope || context.scope } - } - - private _matchesSimple(element: Element, simple: CSSSimpleSelector, context: QueryContext): boolean { - return this._cached(this._cacheMatchesSimple, element, [simple, context.scope, context.pierceShadow, context.originalScope], () => { - if (element === context.scope) { - return false - } - if (simple.css && !this._matchesCSS(element, simple.css)) { - return false - } - for (const func of simple.functions) { - if (!this._matchesEngine(this._getEngine(func.name), element, func.args, context)) { - return false - } - } - return true - }) - } - - private _querySimple(context: QueryContext, simple: CSSSimpleSelector): Element[] { - if (!simple.functions.length) { - return this._queryCSS(context, simple.css || '*') - } - - return this._cached(this._cacheQuerySimple, simple, [context.scope, context.pierceShadow, context.originalScope], () => { - let css = simple.css - const funcs = simple.functions - if (css === '*' && funcs.length) { - css = undefined - } - - let elements: Element[] - let firstIndex = -1 - if (css !== undefined) { - elements = this._queryCSS(context, css) - } - else { - firstIndex = funcs.findIndex(func => this._getEngine(func.name).query !== undefined) - if (firstIndex === -1) { - firstIndex = 0 - } - elements = this._queryEngine(this._getEngine(funcs[firstIndex].name), context, funcs[firstIndex].args) - } - for (let i = 0; i < funcs.length; i++) { - if (i === firstIndex) { - continue - } - const engine = this._getEngine(funcs[i].name) - if (engine.matches !== undefined) { - elements = elements.filter(e => this._matchesEngine(engine, e, funcs[i].args, context)) - } - } - for (let i = 0; i < funcs.length; i++) { - if (i === firstIndex) { - continue - } - const engine = this._getEngine(funcs[i].name) - if (engine.matches === undefined) { - elements = elements.filter(e => this._matchesEngine(engine, e, funcs[i].args, context)) - } - } - return elements - }) - } - - private _matchesParents(element: Element, complex: CSSComplexSelector, index: number, context: QueryContext): boolean { - if (index < 0) { - return true - } - return this._cached(this._cacheMatchesParents, element, [complex, index, context.scope, context.pierceShadow, context.originalScope], () => { - const { selector: simple, combinator } = complex.simples[index] - if (combinator === '>') { - const parent = parentElementOrShadowHostInContext(element, context) - if (!parent || !this._matchesSimple(parent, simple, context)) { - return false - } - return this._matchesParents(parent, complex, index - 1, context) - } - if (combinator === '+') { - const previousSibling = previousSiblingInContext(element, context) - if (!previousSibling || !this._matchesSimple(previousSibling, simple, context)) { - return false - } - return this._matchesParents(previousSibling, complex, index - 1, context) - } - if (combinator === '') { - let parent = parentElementOrShadowHostInContext(element, context) - while (parent) { - if (this._matchesSimple(parent, simple, context)) { - if (this._matchesParents(parent, complex, index - 1, context)) { - return true - } - if (complex.simples[index - 1].combinator === '') { - break - } - } - parent = parentElementOrShadowHostInContext(parent, context) - } - return false - } - if (combinator === '~') { - let previousSibling = previousSiblingInContext(element, context) - while (previousSibling) { - if (this._matchesSimple(previousSibling, simple, context)) { - if (this._matchesParents(previousSibling, complex, index - 1, context)) { - return true - } - if (complex.simples[index - 1].combinator === '~') { - break - } - } - previousSibling = previousSiblingInContext(previousSibling, context) - } - return false - } - if (combinator === '>=') { - let parent: Element | undefined = element - while (parent) { - if (this._matchesSimple(parent, simple, context)) { - if (this._matchesParents(parent, complex, index - 1, context)) { - return true - } - if (complex.simples[index - 1].combinator === '') { - break - } - } - parent = parentElementOrShadowHostInContext(parent, context) - } - return false - } - throw new Error(`Unsupported combinator "${combinator}"`) - }) - } - - private _matchesEngine(engine: SelectorEngine, element: Element, args: CSSFunctionArgument[], context: QueryContext): boolean { - if (engine.matches) { - return this._callMatches(engine, element, args, context) - } - if (engine.query) { - return this._callQuery(engine, args, context).includes(element) - } - throw new Error(`Selector engine should implement "matches" or "query"`) - } - - private _queryEngine(engine: SelectorEngine, context: QueryContext, args: CSSFunctionArgument[]): Element[] { - if (engine.query) { - return this._callQuery(engine, args, context) - } - if (engine.matches) { - return this._queryCSS(context, '*').filter(element => this._callMatches(engine, element, args, context)) - } - throw new Error(`Selector engine should implement "matches" or "query"`) - } - - private _callMatches(engine: SelectorEngine, element: Element, args: CSSFunctionArgument[], context: QueryContext): boolean { - return this._cached(this._cacheCallMatches, element, [engine, context.scope, context.pierceShadow, context.originalScope, ...args], () => { - return engine.matches!(element, args, context, this) - }) - } - - private _callQuery(engine: SelectorEngine, args: CSSFunctionArgument[], context: QueryContext): Element[] { - return this._cached(this._cacheCallQuery, engine, [context.scope, context.pierceShadow, context.originalScope, ...args], () => { - return engine.query!(context, args, this) - }) - } - - private _matchesCSS(element: Element, css: string): boolean { - return element.matches(css) - } - - _queryCSS(context: QueryContext, css: string): Element[] { - return this._cached(this._cacheQueryCSS, css, [context.scope, context.pierceShadow, context.originalScope], () => { - let result: Element[] = [] - function query(root: Element | ShadowRoot | Document) { - result = result.concat([...root.querySelectorAll(css)]) - if (!context.pierceShadow) { - return - } - if ((root as Element).shadowRoot) { - query((root as Element).shadowRoot!) - } - for (const element of root.querySelectorAll('*')) { - if (element.shadowRoot) { - query(element.shadowRoot) - } - } - } - query(context.scope) - return result - }) - } - - private _getEngine(name: string): SelectorEngine { - const engine = this._engines.get(name) - if (!engine) { - throw new Error(`Unknown selector engine "${name}"`) - } - return engine - } -} - -const isEngine: SelectorEngine = { - matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean { - if (args.length === 0) { - throw new Error(`"is" engine expects non-empty selector list`) - } - return args.some(selector => evaluator.matches(element, selector, context)) - }, - - query(context: QueryContext, args: (string | number | Selector)[], evaluator: SelectorEvaluator): Element[] { - if (args.length === 0) { - throw new Error(`"is" engine expects non-empty selector list`) - } - let elements: Element[] = [] - for (const arg of args) { - elements = elements.concat(evaluator.query(context, arg)) - } - return args.length === 1 ? elements : sortInDOMOrder(elements) - }, -} - -const hasEngine: SelectorEngine = { - matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean { - if (args.length === 0) { - throw new Error(`"has" engine expects non-empty selector list`) - } - return evaluator.query({ ...context, scope: element }, args).length > 0 - }, - - // TODO: we can implement efficient "query" by matching "args" and returning - // all parents/descendants, just have to be careful with the ":scope" matching. -} - -const scopeEngine: SelectorEngine = { - matches(element: Element, args: (string | number | Selector)[], context: QueryContext, _evaluator: SelectorEvaluator): boolean { - if (args.length !== 0) { - throw new Error(`"scope" engine expects no arguments`) - } - const actualScope = context.originalScope || context.scope - if (actualScope.nodeType === 9 /* Node.DOCUMENT_NODE */) { - return element === (actualScope as Document).documentElement - } - return element === actualScope - }, - - query(context: QueryContext, args: (string | number | Selector)[], _evaluator: SelectorEvaluator): Element[] { - if (args.length !== 0) { - throw new Error(`"scope" engine expects no arguments`) - } - const actualScope = context.originalScope || context.scope - if (actualScope.nodeType === 9 /* Node.DOCUMENT_NODE */) { - const root = (actualScope as Document).documentElement - return root ? [root] : [] - } - if (actualScope.nodeType === 1 /* Node.ELEMENT_NODE */) { - return [actualScope as Element] - } - return [] - }, -} - -const notEngine: SelectorEngine = { - matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean { - if (args.length === 0) { - throw new Error(`"not" engine expects non-empty selector list`) - } - return !evaluator.matches(element, args, context) - }, -} - -const lightEngine: SelectorEngine = { - query(context: QueryContext, args: (string | number | Selector)[], evaluator: SelectorEvaluator): Element[] { - return evaluator.query({ ...context, pierceShadow: false }, args) - }, - - matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean { - return evaluator.matches(element, args, { ...context, pierceShadow: false }) - }, -} - -const visibleEngine: SelectorEngine = { - matches(element: Element, args: (string | number | Selector)[], _context: QueryContext, _evaluator: SelectorEvaluator): boolean { - if (args.length) { - throw new Error(`"visible" engine expects no arguments`) - } - return isElementVisible(element) - }, -} - -const textEngine: SelectorEngine = { - matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean { - if (args.length !== 1 || typeof args[0] !== 'string') { - throw new Error(`"text" engine expects a single string`) - } - const text = normalizeWhiteSpace(args[0]).toLowerCase() - const matcher = (elementText: ElementText) => elementText.normalized.toLowerCase().includes(text) - return elementMatchesText((evaluator as SelectorEvaluatorImpl)._cacheText, element, matcher) === 'self' - }, -} - -const textIsEngine: SelectorEngine = { - matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean { - if (args.length !== 1 || typeof args[0] !== 'string') { - throw new Error(`"text-is" engine expects a single string`) - } - const text = normalizeWhiteSpace(args[0]) - const matcher = (elementText: ElementText) => { - if (!text && !elementText.immediate.length) { - return true - } - return elementText.immediate.some(s => normalizeWhiteSpace(s) === text) - } - return elementMatchesText((evaluator as SelectorEvaluatorImpl)._cacheText, element, matcher) !== 'none' - }, -} - -const textMatchesEngine: SelectorEngine = { - matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean { - if (args.length === 0 || typeof args[0] !== 'string' || args.length > 2 || (args.length === 2 && typeof args[1] !== 'string')) { - throw new Error(`"text-matches" engine expects a regexp body and optional regexp flags`) - } - const re = new RegExp(args[0], args.length === 2 ? args[1] : undefined) - const matcher = (elementText: ElementText) => re.test(elementText.full) - return elementMatchesText((evaluator as SelectorEvaluatorImpl)._cacheText, element, matcher) === 'self' - }, -} - -const hasTextEngine: SelectorEngine = { - matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean { - if (args.length !== 1 || typeof args[0] !== 'string') { - throw new Error(`"has-text" engine expects a single string`) - } - if (shouldSkipForTextMatching(element)) { - return false - } - const text = normalizeWhiteSpace(args[0]).toLowerCase() - const matcher = (elementText: ElementText) => elementText.normalized.toLowerCase().includes(text) - return matcher(elementText((evaluator as SelectorEvaluatorImpl)._cacheText, element)) - }, -} - -function createLayoutEngine(name: LayoutSelectorName): SelectorEngine { - return { - matches(element: Element, args: (string | number | Selector)[], context: QueryContext, evaluator: SelectorEvaluator): boolean { - const maxDistance = args.length && typeof args[args.length - 1] === 'number' ? args[args.length - 1] : undefined - const queryArgs = maxDistance === undefined ? args : args.slice(0, args.length - 1) - if (args.length < 1 + (maxDistance === undefined ? 0 : 1)) { - throw new Error(`"${name}" engine expects a selector list and optional maximum distance in pixels`) - } - const inner = evaluator.query(context, queryArgs) - const score = layoutSelectorScore(name, element, inner, maxDistance) - if (score === undefined) { - return false - } - (evaluator as SelectorEvaluatorImpl)._markScore(element, score) - return true - }, - } -} - -const nthMatchEngine: SelectorEngine = { - query(context: QueryContext, args: (string | number | Selector)[], evaluator: SelectorEvaluator): Element[] { - let index = args[args.length - 1] - if (args.length < 2) { - throw new Error(`"nth-match" engine expects non-empty selector list and an index argument`) - } - if (typeof index !== 'number' || index < 1) { - throw new Error(`"nth-match" engine expects a one-based index as the last argument`) - } - const elements = isEngine.query!(context, args.slice(0, args.length - 1), evaluator) - index-- // one-based - return index < elements.length ? [elements[index]] : [] - }, -} - -function parentElementOrShadowHostInContext(element: Element, context: QueryContext): Element | undefined { - if (element === context.scope) { - return - } - if (!context.pierceShadow) { - return element.parentElement || undefined - } - return parentElementOrShadowHost(element) -} - -function previousSiblingInContext(element: Element, context: QueryContext): Element | undefined { - if (element === context.scope) { - return - } - return element.previousElementSibling || undefined -} - -export function sortInDOMOrder(elements: Iterable): Element[] { - interface SortEntry { children: Element[]; taken: boolean } - - const elementToEntry = new Map() - const roots: Element[] = [] - const result: Element[] = [] - - function append(element: Element): SortEntry { - let entry = elementToEntry.get(element) - if (entry) { - return entry - } - const parent = parentElementOrShadowHost(element) - if (parent) { - const parentEntry = append(parent) - parentEntry.children.push(element) - } - else { - roots.push(element) - } - entry = { children: [], taken: false } - elementToEntry.set(element, entry) - return entry - } - for (const e of elements) { - append(e).taken = true - } - - function visit(element: Element) { - const entry = elementToEntry.get(element)! - if (entry.taken) { - result.push(element) - } - if (entry.children.length > 1) { - const set = new Set(entry.children) - entry.children = [] - let child = element.firstElementChild - while (child && entry.children.length < set.size) { - if (set.has(child)) { - entry.children.push(child) - } - child = child.nextElementSibling - } - child = element.shadowRoot ? element.shadowRoot.firstElementChild : null - while (child && entry.children.length < set.size) { - if (set.has(child)) { - entry.children.push(child) - } - child = child.nextElementSibling - } - } - entry.children.forEach(visit) - } - roots.forEach(visit) - - return result -} diff --git a/packages/browser/src/client/tester/locators/playwright-selector/selectorGenerator.ts b/packages/browser/src/client/tester/locators/playwright-selector/selectorGenerator.ts deleted file mode 100644 index baa45062efff..000000000000 --- a/packages/browser/src/client/tester/locators/playwright-selector/selectorGenerator.ts +++ /dev/null @@ -1,549 +0,0 @@ -/* eslint-disable ts/no-use-before-define */ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// copied from https://github.com/microsoft/playwright/blob/4554372e456154d7365b6902ef9f3e1e7de76e94/packages/playwright-core/src/server/injected/selectorGenerator.ts -// with some modifications (removed options that we are not using) - -import { cssEscape, escapeForAttributeSelector, escapeForTextSelector, escapeRegExp, quoteCSSAttributeValue } from './stringUtils' -import { parentElementOrShadowHost } from './domUtils' -import type { PlaywrightSelector } from './selector' -import { beginAriaCaches, endAriaCaches, getAriaRole, getElementAccessibleName } from './roleUtils' -import { elementText, getElementLabels } from './selectorUtils' - -interface SelectorToken { - engine: string - selector: string - score: number // Lower is better. -} - -const cacheAllowText = new Map() -const cacheDisallowText = new Map() - -const kTextScoreRange = 10 -const kExactPenalty = kTextScoreRange / 2 - -const kTestIdScore = 1 // testIdAttributeName -const kOtherTestIdScore = 2 // other data-test* attributes - -const kIframeByAttributeScore = 10 - -const kBeginPenalizedScore = 50 -const kPlaceholderScore = 100 -const kLabelScore = 120 -const kRoleWithNameScore = 140 -const kAltTextScore = 160 -const kTextScore = 180 -const kTitleScore = 200 -const kTextScoreRegex = 250 -const kPlaceholderScoreExact = kPlaceholderScore + kExactPenalty -const kLabelScoreExact = kLabelScore + kExactPenalty -const kRoleWithNameScoreExact = kRoleWithNameScore + kExactPenalty -const kAltTextScoreExact = kAltTextScore + kExactPenalty -const kTextScoreExact = kTextScore + kExactPenalty -const kTitleScoreExact = kTitleScore + kExactPenalty -const kEndPenalizedScore = 300 - -const kCSSIdScore = 500 -const kRoleWithoutNameScore = 510 -const kCSSInputTypeNameScore = 520 -const kCSSTagNameScore = 530 -const kNthScore = 10000 -const kCSSFallbackScore = 10000000 - -export interface GenerateSelectorOptions { - testIdAttributeName: string -} - -export function generateSelector( - injectedScript: PlaywrightSelector, - targetElement: Element, - options: GenerateSelectorOptions, -): string { - injectedScript._evaluator.begin() - beginAriaCaches() - try { - const targetTokens = generateSelectorFor(injectedScript, targetElement, options) || cssFallback(injectedScript, targetElement, options) - return joinTokens(targetTokens) - } - finally { - cacheAllowText.clear() - cacheDisallowText.clear() - endAriaCaches() - injectedScript._evaluator.end() - } -} - -function filterRegexTokens(textCandidates: SelectorToken[][]): SelectorToken[][] { - // Filter out regex-based selectors for better performance. - return textCandidates.filter(c => c[0].selector[0] !== '/') -} - -type InternalOptions = GenerateSelectorOptions & { noText?: boolean; noCSSId?: boolean } - -function generateSelectorFor(injectedScript: PlaywrightSelector, targetElement: Element, options: InternalOptions): SelectorToken[] | null { - if (targetElement.ownerDocument.documentElement === targetElement) { - return [{ engine: 'css', selector: 'html', score: 1 }] - } - - const calculate = (element: Element, allowText: boolean): SelectorToken[] | null => { - const allowNthMatch = element === targetElement - - let textCandidates = allowText ? buildTextCandidates(injectedScript, element, element === targetElement) : [] - if (element !== targetElement) { - // Do not use regex for parent elements (for performance). - textCandidates = filterRegexTokens(textCandidates) - } - const noTextCandidates = buildNoTextCandidates(injectedScript, element, options) - .map(token => [token]) - - // First check all text and non-text candidates for the element. - let result = chooseFirstSelector(injectedScript, targetElement.ownerDocument, element, [...textCandidates, ...noTextCandidates], allowNthMatch) - - // Do not use regex for chained selectors (for performance). - textCandidates = filterRegexTokens(textCandidates) - - const checkWithText = (textCandidatesToUse: SelectorToken[][]) => { - // Use the deepest possible text selector - works pretty good and saves on compute time. - const allowParentText = allowText && !textCandidatesToUse.length - - const candidates = [...textCandidatesToUse, ...noTextCandidates].filter((c) => { - if (!result) { - return true - } - return combineScores(c) < combineScores(result) - }) - - // This is best theoretically possible candidate from the current parent. - // We use the fact that widening the scope to grand-parent makes any selector - // even less likely to match. - let bestPossibleInParent: SelectorToken[] | null = candidates[0] - if (!bestPossibleInParent) { - return - } - - for (let parent = parentElementOrShadowHost(element); parent; parent = parentElementOrShadowHost(parent)) { - const parentTokens = calculateCached(parent, allowParentText) - if (!parentTokens) { - continue - } - // Even the best selector won't be too good - skip this parent. - if (result && combineScores([...parentTokens, ...bestPossibleInParent]) >= combineScores(result)) { - continue - } - // Update the best candidate that finds "element" in the "parent". - bestPossibleInParent = chooseFirstSelector(injectedScript, parent, element, candidates, allowNthMatch) - if (!bestPossibleInParent) { - return - } - const combined = [...parentTokens, ...bestPossibleInParent] - if (!result || combineScores(combined) < combineScores(result)) { - result = combined - } - } - } - - checkWithText(textCandidates) - // Allow skipping text on the target element, and using text on one of the parents. - if (element === targetElement && textCandidates.length) { - checkWithText([]) - } - - return result - } - - const calculateCached = (element: Element, allowText: boolean): SelectorToken[] | null => { - const cache = allowText ? cacheAllowText : cacheDisallowText - let value = cache.get(element) - if (value === undefined) { - value = calculate(element, allowText) - cache.set(element, value) - } - return value - } - - return calculate(targetElement, !options.noText) -} - -function buildNoTextCandidates(injectedScript: PlaywrightSelector, element: Element, options: InternalOptions): SelectorToken[] { - const candidates: SelectorToken[] = [] - - // CSS selectors are applicable to elements via locator() and iframes via frameLocator(). - // eslint-disable-next-line no-lone-blocks - { - for (const attr of ['data-testid', 'data-test-id', 'data-test']) { - if (attr !== options.testIdAttributeName && element.getAttribute(attr)) { - candidates.push({ engine: 'css', selector: `[${attr}=${quoteCSSAttributeValue(element.getAttribute(attr)!)}]`, score: kOtherTestIdScore }) - } - } - - if (!options.noCSSId) { - const idAttr = element.getAttribute('id') - if (idAttr && !isGuidLike(idAttr)) { - candidates.push({ engine: 'css', selector: makeSelectorForId(idAttr), score: kCSSIdScore }) - } - } - - candidates.push({ engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: kCSSTagNameScore }) - } - - if (element.nodeName === 'IFRAME') { - for (const attribute of ['name', 'title']) { - if (element.getAttribute(attribute)) { - candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[${attribute}=${quoteCSSAttributeValue(element.getAttribute(attribute)!)}]`, score: kIframeByAttributeScore }) - } - } - - // Locate by testId via CSS selector. - if (element.getAttribute(options.testIdAttributeName)) { - candidates.push({ engine: 'css', selector: `[${options.testIdAttributeName}=${quoteCSSAttributeValue(element.getAttribute(options.testIdAttributeName)!)}]`, score: kTestIdScore }) - } - - penalizeScoreForLength([candidates]) - return candidates - } - - // Everything below is not applicable to iframes (getBy* methods). - if (element.getAttribute(options.testIdAttributeName)) { - candidates.push({ engine: 'internal:testid', selector: `[${options.testIdAttributeName}=${escapeForAttributeSelector(element.getAttribute(options.testIdAttributeName)!, true)}]`, score: kTestIdScore }) - } - - if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { - const input = element as HTMLInputElement | HTMLTextAreaElement - if (input.placeholder) { - candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(input.placeholder, true)}]`, score: kPlaceholderScoreExact }) - for (const alternative of suitableTextAlternatives(input.placeholder)) { - candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(alternative.text, false)}]`, score: kPlaceholderScore - alternative.scoreBouns }) - } - } - } - - const labels = getElementLabels(injectedScript._evaluator._cacheText, element) - for (const label of labels) { - const labelText = label.normalized - candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(labelText, true), score: kLabelScoreExact }) - for (const alternative of suitableTextAlternatives(labelText)) { - candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(alternative.text, false), score: kLabelScore - alternative.scoreBouns }) - } - } - - const ariaRole = getAriaRole(element) - if (ariaRole && !['none', 'presentation'].includes(ariaRole)) { - candidates.push({ engine: 'internal:role', selector: ariaRole, score: kRoleWithoutNameScore }) - } - - if (element.getAttribute('name') && ['BUTTON', 'FORM', 'FIELDSET', 'FRAME', 'IFRAME', 'INPUT', 'KEYGEN', 'OBJECT', 'OUTPUT', 'SELECT', 'TEXTAREA', 'MAP', 'META', 'PARAM'].includes(element.nodeName)) { - candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[name=${quoteCSSAttributeValue(element.getAttribute('name')!)}]`, score: kCSSInputTypeNameScore }) - } - - if (['INPUT', 'TEXTAREA'].includes(element.nodeName) && element.getAttribute('type') !== 'hidden') { - if (element.getAttribute('type')) { - candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[type=${quoteCSSAttributeValue(element.getAttribute('type')!)}]`, score: kCSSInputTypeNameScore }) - } - } - - if (['INPUT', 'TEXTAREA', 'SELECT'].includes(element.nodeName) && element.getAttribute('type') !== 'hidden') { - candidates.push({ engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: kCSSInputTypeNameScore + 1 }) - } - - penalizeScoreForLength([candidates]) - return candidates -} - -function buildTextCandidates(injectedScript: PlaywrightSelector, element: Element, isTargetNode: boolean): SelectorToken[][] { - if (element.nodeName === 'SELECT') { - return [] - } - const candidates: SelectorToken[][] = [] - - const title = element.getAttribute('title') - if (title) { - candidates.push([{ engine: 'internal:attr', selector: `[title=${escapeForAttributeSelector(title, true)}]`, score: kTitleScoreExact }]) - for (const alternative of suitableTextAlternatives(title)) { - candidates.push([{ engine: 'internal:attr', selector: `[title=${escapeForAttributeSelector(alternative.text, false)}]`, score: kTitleScore - alternative.scoreBouns }]) - } - } - - const alt = element.getAttribute('alt') - if (alt && ['APPLET', 'AREA', 'IMG', 'INPUT'].includes(element.nodeName)) { - candidates.push([{ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(alt, true)}]`, score: kAltTextScoreExact }]) - for (const alternative of suitableTextAlternatives(alt)) { - candidates.push([{ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(alternative.text, false)}]`, score: kAltTextScore - alternative.scoreBouns }]) - } - } - - const text = elementText(injectedScript._evaluator._cacheText, element).normalized - if (text) { - const alternatives = suitableTextAlternatives(text) - if (isTargetNode) { - if (text.length <= 80) { - candidates.push([{ engine: 'internal:text', selector: escapeForTextSelector(text, true), score: kTextScoreExact }]) - } - for (const alternative of alternatives) { - candidates.push([{ engine: 'internal:text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBouns }]) - } - } - const cssToken: SelectorToken = { engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: kCSSTagNameScore } - for (const alternative of alternatives) { - candidates.push([cssToken, { engine: 'internal:has-text', selector: escapeForTextSelector(alternative.text, false), score: kTextScore - alternative.scoreBouns }]) - } - if (text.length <= 80) { - const re = new RegExp(`^${escapeRegExp(text)}$`) - candidates.push([cssToken, { engine: 'internal:has-text', selector: escapeForTextSelector(re, false), score: kTextScoreRegex }]) - } - } - - const ariaRole = getAriaRole(element) - if (ariaRole && !['none', 'presentation'].includes(ariaRole)) { - const ariaName = getElementAccessibleName(element, false) - if (ariaName) { - candidates.push([{ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: kRoleWithNameScoreExact }]) - for (const alternative of suitableTextAlternatives(ariaName)) { - candidates.push([{ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(alternative.text, false)}]`, score: kRoleWithNameScore - alternative.scoreBouns }]) - } - } - } - - penalizeScoreForLength(candidates) - return candidates -} - -function makeSelectorForId(id: string) { - return /^[a-z][\w\-]+$/i.test(id) ? `#${id}` : `[id="${cssEscape(id)}"]` -} - -function cssFallback(injectedScript: PlaywrightSelector, targetElement: Element, options: InternalOptions): SelectorToken[] { - const root: Node = targetElement.ownerDocument - const tokens: string[] = [] - - function uniqueCSSSelector(prefix?: string): string | undefined { - const path = tokens.slice() - if (prefix) { - path.unshift(prefix) - } - const selector = path.join(' > ') - const parsedSelector = injectedScript.parseSelector(selector) - const node = injectedScript.querySelector(parsedSelector, root, false) - return node === targetElement ? selector : undefined - } - - function makeStrict(selector: string): SelectorToken[] { - const token = { engine: 'css', selector, score: kCSSFallbackScore } - const parsedSelector = injectedScript.parseSelector(selector) - const elements = injectedScript.querySelectorAll(parsedSelector, root) - if (elements.length === 1) { - return [token] - } - const nth = { engine: 'nth', selector: String(elements.indexOf(targetElement)), score: kNthScore } - return [token, nth] - } - - for (let element: Element | undefined = targetElement; element && element !== root; element = parentElementOrShadowHost(element)) { - const nodeName = element.nodeName.toLowerCase() - - let bestTokenForLevel: string = '' - - // Element ID is the strongest signal, use it. - if (element.id && !options.noCSSId) { - const token = makeSelectorForId(element.id) - const selector = uniqueCSSSelector(token) - if (selector) { - return makeStrict(selector) - } - bestTokenForLevel = token - } - - const parent = element.parentNode as (Element | ShadowRoot) - - // Combine class names until unique. - const classes = [...element.classList] - for (let i = 0; i < classes.length; ++i) { - const token = `.${cssEscape(classes.slice(0, i + 1).join('.'))}` - const selector = uniqueCSSSelector(token) - if (selector) { - return makeStrict(selector) - } - // Even if not unique, does this subset of classes uniquely identify node as a child? - if (!bestTokenForLevel && parent) { - const sameClassSiblings = parent.querySelectorAll(token) - if (sameClassSiblings.length === 1) { - bestTokenForLevel = token - } - } - } - - // Ordinal is the weakest signal. - if (parent) { - const siblings = [...parent.children] - const sameTagSiblings = siblings.filter(sibling => (sibling).nodeName.toLowerCase() === nodeName) - const token = sameTagSiblings.indexOf(element) === 0 ? cssEscape(nodeName) : `${cssEscape(nodeName)}:nth-child(${1 + siblings.indexOf(element)})` - const selector = uniqueCSSSelector(token) - if (selector) { - return makeStrict(selector) - } - if (!bestTokenForLevel) { - bestTokenForLevel = token - } - } - else if (!bestTokenForLevel) { - bestTokenForLevel = cssEscape(nodeName) - } - tokens.unshift(bestTokenForLevel) - } - return makeStrict(uniqueCSSSelector()!) -} - -function penalizeScoreForLength(groups: SelectorToken[][]) { - for (const group of groups) { - for (const token of group) { - if (token.score > kBeginPenalizedScore && token.score < kEndPenalizedScore) { - token.score += Math.min(kTextScoreRange, (token.selector.length / 10) | 0) - } - } - } -} - -function joinTokens(tokens: SelectorToken[]): string { - const parts = [] - let lastEngine = '' - for (const { engine, selector } of tokens) { - if (parts.length && (lastEngine !== 'css' || engine !== 'css' || selector.startsWith(':nth-match('))) { - parts.push('>>') - } - lastEngine = engine - if (engine === 'css') { - parts.push(selector) - } - else { parts.push(`${engine}=${selector}`) } - } - return parts.join(' ') -} - -function combineScores(tokens: SelectorToken[]): number { - let score = 0 - for (let i = 0; i < tokens.length; i++) { - score += tokens[i].score * (tokens.length - i) - } - return score -} - -function chooseFirstSelector(injectedScript: PlaywrightSelector, scope: Element | Document, targetElement: Element, selectors: SelectorToken[][], allowNthMatch: boolean): SelectorToken[] | null { - const joined = selectors.map(tokens => ({ tokens, score: combineScores(tokens) })) - joined.sort((a, b) => a.score - b.score) - - let bestWithIndex: SelectorToken[] | null = null - for (const { tokens } of joined) { - const parsedSelector = injectedScript.parseSelector(joinTokens(tokens)) - const result = injectedScript.querySelectorAll(parsedSelector, scope) - if (result[0] === targetElement && result.length === 1) { - // We are the only match - found the best selector. - return tokens - } - - // Otherwise, perhaps we can use nth=? - const index = result.indexOf(targetElement) - if (!allowNthMatch || bestWithIndex || index === -1 || result.length > 5) { - continue - } - - const nth: SelectorToken = { engine: 'nth', selector: String(index), score: kNthScore } - bestWithIndex = [...tokens, nth] - } - return bestWithIndex -} - -function isGuidLike(id: string): boolean { - let lastCharacterType: 'lower' | 'upper' | 'digit' | 'other' | undefined - let transitionCount = 0 - for (let i = 0; i < id.length; ++i) { - const c = id[i] - let characterType: 'lower' | 'upper' | 'digit' | 'other' - if (c === '-' || c === '_') { - continue - } - if (c >= 'a' && c <= 'z') { - characterType = 'lower' - } - else if (c >= 'A' && c <= 'Z') { - characterType = 'upper' - } - else if (c >= '0' && c <= '9') { - characterType = 'digit' - } - else { characterType = 'other' } - - if (characterType === 'lower' && lastCharacterType === 'upper') { - lastCharacterType = characterType - continue - } - - if (lastCharacterType && lastCharacterType !== characterType) { - ++transitionCount - } - lastCharacterType = characterType - } - return transitionCount >= id.length / 4 -} - -function trimWordBoundary(text: string, maxLength: number) { - if (text.length <= maxLength) { - return text - } - text = text.substring(0, maxLength) - // Find last word boundary in the text. - const match = text.match(/^(.*)\b(.+)$/) - if (!match) { - return '' - } - return match[1].trimEnd() -} - -function suitableTextAlternatives(text: string) { - let result: { text: string; scoreBouns: number }[] = [] - - { - const match = text.match(/^([\d.,]+)[^.,\w]/) - const leadingNumberLength = match ? match[1].length : 0 - if (leadingNumberLength) { - const alt = trimWordBoundary(text.substring(leadingNumberLength).trimStart(), 80) - result.push({ text: alt, scoreBouns: alt.length <= 30 ? 2 : 1 }) - } - } - - { - const match = text.match(/[^.,\w]([\d.,]+)$/) - const trailingNumberLength = match ? match[1].length : 0 - if (trailingNumberLength) { - const alt = trimWordBoundary(text.substring(0, text.length - trailingNumberLength).trimEnd(), 80) - result.push({ text: alt, scoreBouns: alt.length <= 30 ? 2 : 1 }) - } - } - - if (text.length <= 30) { - result.push({ text, scoreBouns: 0 }) - } - else { - result.push({ text: trimWordBoundary(text, 80), scoreBouns: 0 }) - result.push({ text: trimWordBoundary(text, 30), scoreBouns: 1 }) - } - - result = result.filter(r => r.text) - if (!result.length) { - result.push({ text: text.substring(0, 80), scoreBouns: 0 }) - } - - return result -} diff --git a/packages/browser/src/client/tester/locators/playwright-selector/selectorParser.ts b/packages/browser/src/client/tester/locators/playwright-selector/selectorParser.ts deleted file mode 100644 index dcb599d22933..000000000000 --- a/packages/browser/src/client/tester/locators/playwright-selector/selectorParser.ts +++ /dev/null @@ -1,496 +0,0 @@ -/* eslint-disable no-unmodified-loop-condition */ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// copied without changes from https://github.com/microsoft/playwright/blob/4554372e456154d7365b6902ef9f3e1e7de76e94/packages/playwright-core/src/server/injected/selectorParser.ts - -import type { CSSComplexSelectorList } from './cssParser' -import { InvalidSelectorError, parseCSS } from './cssParser' - -export { InvalidSelectorError, isInvalidSelectorError } from './cssParser' - -export interface NestedSelectorBody { parsed: ParsedSelector; distance?: number } -const kNestedSelectorNames = new Set(['internal:has', 'internal:has-not', 'internal:and', 'internal:or', 'internal:chain', 'left-of', 'right-of', 'above', 'below', 'near']) -const kNestedSelectorNamesWithDistance = new Set(['left-of', 'right-of', 'above', 'below', 'near']) - -export interface ParsedSelectorPart { - name: string - body: string | CSSComplexSelectorList | NestedSelectorBody - source: string -} - -export interface ParsedSelector { - parts: ParsedSelectorPart[] - capture?: number -} - -interface ParsedSelectorStrings { - parts: { name: string; body: string }[] - capture?: number -} - -export const customCSSNames = new Set(['not', 'is', 'where', 'has', 'scope', 'light', 'visible', 'text', 'text-matches', 'text-is', 'has-text', 'above', 'below', 'right-of', 'left-of', 'near', 'nth-match']) - -export function parseSelector(selector: string): ParsedSelector { - const parsedStrings = parseSelectorString(selector) - const parts: ParsedSelectorPart[] = [] - for (const part of parsedStrings.parts) { - if (part.name === 'css' || part.name === 'css:light') { - if (part.name === 'css:light') { - part.body = `:light(${part.body})` - } - const parsedCSS = parseCSS(part.body, customCSSNames) - parts.push({ - name: 'css', - body: parsedCSS.selector, - source: part.body, - }) - continue - } - if (kNestedSelectorNames.has(part.name)) { - let innerSelector: string - let distance: number | undefined - try { - const unescaped = JSON.parse(`[${part.body}]`) - if (!Array.isArray(unescaped) || unescaped.length < 1 || unescaped.length > 2 || typeof unescaped[0] !== 'string') { - throw new InvalidSelectorError(`Malformed selector: ${part.name}=${part.body}`) - } - innerSelector = unescaped[0] - if (unescaped.length === 2) { - if (typeof unescaped[1] !== 'number' || !kNestedSelectorNamesWithDistance.has(part.name)) { - throw new InvalidSelectorError(`Malformed selector: ${part.name}=${part.body}`) - } - distance = unescaped[1] - } - } - catch (e) { - throw new InvalidSelectorError(`Malformed selector: ${part.name}=${part.body}`) - } - const nested = { name: part.name, source: part.body, body: { parsed: parseSelector(innerSelector), distance } } - const lastFrame = [...nested.body.parsed.parts].reverse().find(part => part.name === 'internal:control' && part.body === 'enter-frame') - const lastFrameIndex = lastFrame ? nested.body.parsed.parts.indexOf(lastFrame) : -1 - // Allow nested selectors to start with the same frame selector. - if (lastFrameIndex !== -1 && selectorPartsEqual(nested.body.parsed.parts.slice(0, lastFrameIndex + 1), parts.slice(0, lastFrameIndex + 1))) { - nested.body.parsed.parts.splice(0, lastFrameIndex + 1) - } - parts.push(nested) - continue - } - parts.push({ ...part, source: part.body }) - } - if (kNestedSelectorNames.has(parts[0].name)) { - throw new InvalidSelectorError(`"${parts[0].name}" selector cannot be first`) - } - return { - capture: parsedStrings.capture, - parts, - } -} - -export function splitSelectorByFrame(selectorText: string): ParsedSelector[] { - const selector = parseSelector(selectorText) - const result: ParsedSelector[] = [] - let chunk: ParsedSelector = { - parts: [], - } - let chunkStartIndex = 0 - for (let i = 0; i < selector.parts.length; ++i) { - const part = selector.parts[i] - if (part.name === 'internal:control' && part.body === 'enter-frame') { - if (!chunk.parts.length) { - throw new InvalidSelectorError('Selector cannot start with entering frame, select the iframe first') - } - result.push(chunk) - chunk = { parts: [] } - chunkStartIndex = i + 1 - continue - } - if (selector.capture === i) { - chunk.capture = i - chunkStartIndex - } - chunk.parts.push(part) - } - if (!chunk.parts.length) { - throw new InvalidSelectorError(`Selector cannot end with entering frame, while parsing selector ${selectorText}`) - } - result.push(chunk) - if (typeof selector.capture === 'number' && typeof result[result.length - 1].capture !== 'number') { - throw new InvalidSelectorError(`Can not capture the selector before diving into the frame. Only use * after the last frame has been selected`) - } - return result -} - -function selectorPartsEqual(list1: ParsedSelectorPart[], list2: ParsedSelectorPart[]) { - return stringifySelector({ parts: list1 }) === stringifySelector({ parts: list2 }) -} - -export function stringifySelector(selector: string | ParsedSelector, forceEngineName?: boolean): string { - if (typeof selector === 'string') { - return selector - } - return selector.parts.map((p, i) => { - let includeEngine = true - if (!forceEngineName && i !== selector.capture) { - if (p.name === 'css') { - includeEngine = false - } - else if ((p.name === 'xpath' && p.source.startsWith('//')) || p.source.startsWith('..')) { - includeEngine = false - } - } - const prefix = includeEngine ? `${p.name}=` : '' - return `${i === selector.capture ? '*' : ''}${prefix}${p.source}` - }).join(' >> ') -} - -export function visitAllSelectorParts(selector: ParsedSelector, visitor: (part: ParsedSelectorPart, nested: boolean) => void) { - const visit = (selector: ParsedSelector, nested: boolean) => { - for (const part of selector.parts) { - visitor(part, nested) - if (kNestedSelectorNames.has(part.name)) { - visit((part.body as NestedSelectorBody).parsed, true) - } - } - } - visit(selector, false) -} - -function parseSelectorString(selector: string): ParsedSelectorStrings { - let index = 0 - let quote: string | undefined - let start = 0 - const result: ParsedSelectorStrings = { parts: [] } - const append = () => { - const part = selector.substring(start, index).trim() - const eqIndex = part.indexOf('=') - let name: string - let body: string - if (eqIndex !== -1 && part.substring(0, eqIndex).trim().match(/^[\w\-+:*]+$/)) { - name = part.substring(0, eqIndex).trim() - body = part.substring(eqIndex + 1) - } - else if (part.length > 1 && part[0] === '"' && part[part.length - 1] === '"') { - name = 'text' - body = part - } - else if (part.length > 1 && part[0] === '\'' && part[part.length - 1] === '\'') { - name = 'text' - body = part - } - else if (/^\(*\/\//.test(part) || part.startsWith('..')) { - // If selector starts with '//' or '//' prefixed with multiple opening - // parenthesis, consider xpath. @see https://github.com/microsoft/playwright/issues/817 - // If selector starts with '..', consider xpath as well. - name = 'xpath' - body = part - } - else { - name = 'css' - body = part - } - let capture = false - if (name[0] === '*') { - capture = true - name = name.substring(1) - } - result.parts.push({ name, body }) - if (capture) { - if (result.capture !== undefined) { - throw new InvalidSelectorError(`Only one of the selectors can capture using * modifier`) - } - result.capture = result.parts.length - 1 - } - } - - if (!selector.includes('>>')) { - index = selector.length - append() - return result - } - - const shouldIgnoreTextSelectorQuote = () => { - const prefix = selector.substring(start, index) - const match = prefix.match(/^\s*text\s*=(.*)$/) - // Must be a text selector with some text before the quote. - return !!match && !!match[1] - } - - while (index < selector.length) { - const c = selector[index] - if (c === '\\' && index + 1 < selector.length) { - index += 2 - } - else if (c === quote) { - quote = undefined - index++ - } - else if (!quote && (c === '"' || c === '\'' || c === '`') && !shouldIgnoreTextSelectorQuote()) { - quote = c - index++ - } - else if (!quote && c === '>' && selector[index + 1] === '>') { - append() - index += 2 - start = index - } - else { - index++ - } - } - append() - return result -} - -export type AttributeSelectorOperator = '' | '=' | '*=' | '|=' | '^=' | '$=' | '~=' -export interface AttributeSelectorPart { - name: string - jsonPath: string[] - op: AttributeSelectorOperator - value: any - caseSensitive: boolean -} - -export interface AttributeSelector { - name: string - attributes: AttributeSelectorPart[] -} - -export function parseAttributeSelector(selector: string, allowUnquotedStrings: boolean): AttributeSelector { - let wp = 0 - let EOL = selector.length === 0 - - const next = () => selector[wp] || '' - const eat1 = () => { - const result = next() - ++wp - EOL = wp >= selector.length - return result - } - - const syntaxError = (stage: string | undefined) => { - if (EOL) { - throw new InvalidSelectorError(`Unexpected end of selector while parsing selector \`${selector}\``) - } - throw new InvalidSelectorError(`Error while parsing selector \`${selector}\` - unexpected symbol "${next()}" at position ${wp}${stage ? ` during ${stage}` : ''}`) - } - - function skipSpaces() { - while (!EOL && /\s/.test(next())) { - eat1() - } - } - - function isCSSNameChar(char: string) { - // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram - return (char >= '\u0080') // non-ascii - || (char >= '\u0030' && char <= '\u0039') // digit - || (char >= '\u0041' && char <= '\u005A') // uppercase letter - || (char >= '\u0061' && char <= '\u007A') // lowercase letter - || (char >= '\u0030' && char <= '\u0039') // digit - || char === '\u005F' // "_" - || char === '\u002D' // "-" - } - - function readIdentifier() { - let result = '' - skipSpaces() - while (!EOL && isCSSNameChar(next())) { - result += eat1() - } - return result - } - - function readQuotedString(quote: string) { - let result = eat1() - if (result !== quote) { - syntaxError('parsing quoted string') - } - while (!EOL && next() !== quote) { - if (next() === '\\') { - eat1() - } - result += eat1() - } - if (next() !== quote) { - syntaxError('parsing quoted string') - } - result += eat1() - return result - } - - function readRegularExpression() { - if (eat1() !== '/') { - syntaxError('parsing regular expression') - } - let source = '' - let inClass = false - // https://262.ecma-international.org/11.0/#sec-literals-regular-expression-literals - while (!EOL) { - if (next() === '\\') { - source += eat1() - if (EOL) { - syntaxError('parsing regular expression') - } - } - else if (inClass && next() === ']') { - inClass = false - } - else if (!inClass && next() === '[') { - inClass = true - } - else if (!inClass && next() === '/') { - break - } - source += eat1() - } - if (eat1() !== '/') { - syntaxError('parsing regular expression') - } - let flags = '' - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions - while (!EOL && next().match(/[dgimsuy]/)) { - flags += eat1() - } - try { - return new RegExp(source, flags) - } - catch (e: any) { - throw new InvalidSelectorError(`Error while parsing selector \`${selector}\`: ${e.message}`) - } - } - - function readAttributeToken() { - let token = '' - skipSpaces() - if (next() === `'` || next() === `"`) { - token = readQuotedString(next()).slice(1, -1) - } - else { token = readIdentifier() } - if (!token) { - syntaxError('parsing property path') - } - return token - } - - function readOperator(): AttributeSelectorOperator { - skipSpaces() - let op = '' - if (!EOL) { - op += eat1() - } - if (!EOL && (op !== '=')) { - op += eat1() - } - if (!['=', '*=', '^=', '$=', '|=', '~='].includes(op)) { - syntaxError('parsing operator') - } - return (op as AttributeSelectorOperator) - } - - function readAttribute(): AttributeSelectorPart { - // skip leading [ - eat1() - - // read attribute name: - // foo.bar - // 'foo' . "ba zz" - const jsonPath = [] - jsonPath.push(readAttributeToken()) - skipSpaces() - while (next() === '.') { - eat1() - jsonPath.push(readAttributeToken()) - skipSpaces() - } - // check property is truthy: [enabled] - if (next() === ']') { - eat1() - return { name: jsonPath.join('.'), jsonPath, op: '', value: null, caseSensitive: false } - } - - const operator = readOperator() - - let value - let caseSensitive = true - skipSpaces() - if (next() === '/') { - if (operator !== '=') { - throw new InvalidSelectorError(`Error while parsing selector \`${selector}\` - cannot use ${operator} in attribute with regular expression`) - } - value = readRegularExpression() - } - else if (next() === `'` || next() === `"`) { - value = readQuotedString(next()).slice(1, -1) - skipSpaces() - if (next() === 'i' || next() === 'I') { - caseSensitive = false - eat1() - } - else if (next() === 's' || next() === 'S') { - caseSensitive = true - eat1() - } - } - else { - value = '' - while (!EOL && (isCSSNameChar(next()) || next() === '+' || next() === '.')) { - value += eat1() - } - if (value === 'true') { - value = true - } - else if (value === 'false') { - value = false - } - else { - if (!allowUnquotedStrings) { - value = +value - if (Number.isNaN(value)) { - syntaxError('parsing attribute value') - } - } - } - } - skipSpaces() - if (next() !== ']') { - syntaxError('parsing attribute value') - } - - eat1() - if (operator !== '=' && typeof value !== 'string') { - throw new InvalidSelectorError(`Error while parsing selector \`${selector}\` - cannot use ${operator} in attribute with non-string matching value - ${value}`) - } - return { name: jsonPath.join('.'), jsonPath, op: operator, value, caseSensitive } - } - - const result: AttributeSelector = { - name: '', - attributes: [], - } - result.name = readIdentifier() - skipSpaces() - while (next() === '[') { - result.attributes.push(readAttribute()) - skipSpaces() - } - if (!EOL) { - syntaxError(undefined) - } - if (!result.name && !result.attributes.length) { - throw new InvalidSelectorError(`Error while parsing selector \`${selector}\` - selector cannot be empty`) - } - return result -} diff --git a/packages/browser/src/client/tester/locators/playwright-selector/selectorUtils.ts b/packages/browser/src/client/tester/locators/playwright-selector/selectorUtils.ts deleted file mode 100644 index e537374eced1..000000000000 --- a/packages/browser/src/client/tester/locators/playwright-selector/selectorUtils.ts +++ /dev/null @@ -1,143 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// copied without changes from https://github.com/microsoft/playwright/blob/4554372e456154d7365b6902ef9f3e1e7de76e94/packages/playwright-core/src/server/injected/selectorUtils.ts - -import type { AttributeSelectorPart } from './selectorParser' -import { normalizeWhiteSpace } from './stringUtils' -import { getAriaLabelledByElements } from './roleUtils' - -export function matchesAttributePart(value: any, attr: AttributeSelectorPart) { - const objValue = typeof value === 'string' && !attr.caseSensitive ? value.toUpperCase() : value - const attrValue = typeof attr.value === 'string' && !attr.caseSensitive ? attr.value.toUpperCase() : attr.value - - if (attr.op === '') { - return !!objValue - } - if (attr.op === '=') { - if (attrValue instanceof RegExp) { - return typeof objValue === 'string' && !!objValue.match(attrValue) - } - return objValue === attrValue - } - if (typeof objValue !== 'string' || typeof attrValue !== 'string') { - return false - } - if (attr.op === '*=') { - return objValue.includes(attrValue) - } - if (attr.op === '^=') { - return objValue.startsWith(attrValue) - } - if (attr.op === '$=') { - return objValue.endsWith(attrValue) - } - if (attr.op === '|=') { - return objValue === attrValue || objValue.startsWith(`${attrValue}-`) - } - if (attr.op === '~=') { - return objValue.split(' ').includes(attrValue) - } - return false -} - -export function shouldSkipForTextMatching(element: Element | ShadowRoot) { - const document = element.ownerDocument - return element.nodeName === 'SCRIPT' || element.nodeName === 'NOSCRIPT' || element.nodeName === 'STYLE' || (document.head && document.head.contains(element)) -} - -export interface ElementText { full: string; normalized: string; immediate: string[] } -export type TextMatcher = (text: ElementText) => boolean - -export function elementText(cache: Map, root: Element | ShadowRoot): ElementText { - let value = cache.get(root) - if (value === undefined) { - value = { full: '', normalized: '', immediate: [] } - if (!shouldSkipForTextMatching(root)) { - let currentImmediate = '' - if ((root instanceof HTMLInputElement) && (root.type === 'submit' || root.type === 'button')) { - value = { full: root.value, normalized: normalizeWhiteSpace(root.value), immediate: [root.value] } - } - else { - for (let child = root.firstChild; child; child = child.nextSibling) { - if (child.nodeType === Node.TEXT_NODE) { - value.full += child.nodeValue || '' - currentImmediate += child.nodeValue || '' - } - else { - if (currentImmediate) { - value.immediate.push(currentImmediate) - } - currentImmediate = '' - if (child.nodeType === Node.ELEMENT_NODE) { - value.full += elementText(cache, child as Element).full - } - } - } - if (currentImmediate) { - value.immediate.push(currentImmediate) - } - if ((root as Element).shadowRoot) { - value.full += elementText(cache, (root as Element).shadowRoot!).full - } - if (value.full) { - value.normalized = normalizeWhiteSpace(value.full) - } - } - } - cache.set(root, value) - } - return value -} - -export function elementMatchesText(cache: Map, element: Element, matcher: TextMatcher): 'none' | 'self' | 'selfAndChildren' { - if (shouldSkipForTextMatching(element)) { - return 'none' - } - if (!matcher(elementText(cache, element))) { - return 'none' - } - for (let child = element.firstChild; child; child = child.nextSibling) { - if (child.nodeType === Node.ELEMENT_NODE && matcher(elementText(cache, child as Element))) { - return 'selfAndChildren' - } - } - if (element.shadowRoot && matcher(elementText(cache, element.shadowRoot))) { - return 'selfAndChildren' - } - return 'self' -} - -export function getElementLabels(textCache: Map, element: Element): ElementText[] { - const labels = getAriaLabelledByElements(element) - if (labels) { - return labels.map(label => elementText(textCache, label)) - } - const ariaLabel = element.getAttribute('aria-label') - if (ariaLabel !== null && !!ariaLabel.trim()) { - return [{ full: ariaLabel, normalized: normalizeWhiteSpace(ariaLabel), immediate: [ariaLabel] }] - } - - // https://html.spec.whatwg.org/multipage/forms.html#category-label - const isNonHiddenInput = element.nodeName === 'INPUT' && (element as HTMLInputElement).type !== 'hidden' - if (['BUTTON', 'METER', 'OUTPUT', 'PROGRESS', 'SELECT', 'TEXTAREA'].includes(element.nodeName) || isNonHiddenInput) { - const labels = (element as HTMLInputElement).labels - if (labels) { - return [...labels].map(label => elementText(textCache, label)) - } - } - return [] -} diff --git a/packages/browser/src/client/tester/locators/playwright-selector/stringUtils.ts b/packages/browser/src/client/tester/locators/playwright-selector/stringUtils.ts deleted file mode 100644 index ac35c50a7229..000000000000 --- a/packages/browser/src/client/tester/locators/playwright-selector/stringUtils.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// copied without changes from https://github.com/microsoft/playwright/blob/4554372e456154d7365b6902ef9f3e1e7de76e94/packages/playwright-core/src/utils/isomorphic/stringUtils.ts - -// NOTE: this function should not be used to escape any selectors. -export function escapeWithQuotes(text: string, char: string = '\'') { - const stringified = JSON.stringify(text) - const escapedText = stringified.substring(1, stringified.length - 1).replace(/\\"/g, '"') - if (char === '\'') { - return char + escapedText.replace(/'/g, '\\\'') + char - } - if (char === '"') { - return char + escapedText.replace(/"/g, '\\"') + char - } - if (char === '`') { - return char + escapedText.replace(/`/g, '`') + char - } - throw new Error('Invalid escape char') -} - -export function cssEscape(s: string): string { - let result = '' - for (let i = 0; i < s.length; i++) { - result += cssEscapeOne(s, i) - } - return result -} - -export function quoteCSSAttributeValue(text: string): string { - return `"${cssEscape(text).replace(/\\ /g, ' ')}"` -} - -function cssEscapeOne(s: string, i: number): string { - // https://drafts.csswg.org/cssom/#serialize-an-identifier - const c = s.charCodeAt(i) - if (c === 0x0000) { - return '\uFFFD' - } - if ((c >= 0x0001 && c <= 0x001F) - || (c >= 0x0030 && c <= 0x0039 && (i === 0 || (i === 1 && s.charCodeAt(0) === 0x002D)))) { - return `\\${c.toString(16)} ` - } - if (i === 0 && c === 0x002D && s.length === 1) { - return `\\${s.charAt(i)}` - } - if (c >= 0x0080 || c === 0x002D || c === 0x005F || (c >= 0x0030 && c <= 0x0039) - || (c >= 0x0041 && c <= 0x005A) || (c >= 0x0061 && c <= 0x007A)) { - return s.charAt(i) - } - return `\\${s.charAt(i)}` -} - -export function normalizeWhiteSpace(text: string): string { - const result = text.replace(/\u200B/g, '').trim().replace(/\s+/g, ' ') - return result -} - -export function normalizeEscapedRegexQuotes(source: string) { - // This function reverses the effect of escapeRegexForSelector below. - // Odd number of backslashes followed by the quote -> remove unneeded backslash. - return source.replace(/(^|[^\\])(\\\\)*\\(['"`])/g, '$1$2$3') -} - -function escapeRegexForSelector(re: RegExp): string { - // Unicode mode does not allow "identity character escapes", so we do not escape and - // hope that it does not contain quotes and/or >> signs. - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Character_escape - // TODO: rework RE usages in internal selectors away from literal representation to json, e.g. {source,flags}. - if (re.unicode || (re as any).unicodeSets) { - return String(re) - } - // Even number of backslashes followed by the quote -> insert a backslash. - return String(re).replace(/(^|[^\\])(\\\\)*(["'`])/g, '$1$2\\$3').replace(/>>/g, '\\>\\>') -} - -export function escapeForTextSelector(text: string | RegExp, exact: boolean): string { - if (typeof text !== 'string') { - return escapeRegexForSelector(text) - } - return `${JSON.stringify(text)}${exact ? 's' : 'i'}` -} - -export function escapeForAttributeSelector(value: string | RegExp, exact: boolean): string { - if (typeof value !== 'string') { - return escapeRegexForSelector(value) - } - // TODO: this should actually be - // cssEscape(value).replace(/\\ /g, ' ') - // However, our attribute selectors do not conform to CSS parsing spec, - // so we escape them differently. - return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"${exact ? 's' : 'i'}` -} - -function trimString(input: string, cap: number, suffix: string = ''): string { - if (input.length <= cap) { - return input - } - const chars = [...input] - if (chars.length > cap) { - return chars.slice(0, cap - suffix.length).join('') + suffix - } - return chars.join('') -} - -export function trimStringWithEllipsis(input: string, cap: number): string { - return trimString(input, cap, '\u2026') -} - -export function escapeRegExp(s: string) { - // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping - return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string -} diff --git a/packages/browser/src/client/tester/locators/playwright.ts b/packages/browser/src/client/tester/locators/playwright.ts index 47c356f04613..750f8552dd2a 100644 --- a/packages/browser/src/client/tester/locators/playwright.ts +++ b/packages/browser/src/client/tester/locators/playwright.ts @@ -1,4 +1,4 @@ -import { page } from '@vitest/browser/context' +import { page, server } from '@vitest/browser/context' import { getByAltTextSelector, getByLabelSelector, @@ -7,7 +7,7 @@ import { getByTestIdSelector, getByTextSelector, getByTitleSelector, -} from './playwright-selector/locatorUtils' +} from 'ivya' import { Locator, selectorEngine } from './index' page.extend({ @@ -18,7 +18,7 @@ page.extend({ return new PlaywrightLocator(getByRoleSelector(role, options)) }, getByTestId(testId) { - return new PlaywrightLocator(getByTestIdSelector(page.config.browser.locators.testIdAttribute, testId)) + return new PlaywrightLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId)) }, getByAltText(text, options) { return new PlaywrightLocator(getByAltTextSelector(text, options)) diff --git a/packages/browser/src/client/tester/locators/preview.ts b/packages/browser/src/client/tester/locators/preview.ts index c9b9f5cc407e..e23d4a195bcc 100644 --- a/packages/browser/src/client/tester/locators/preview.ts +++ b/packages/browser/src/client/tester/locators/preview.ts @@ -1,6 +1,5 @@ -import { page } from '@vitest/browser/context' +import { page, server } from '@vitest/browser/context' import { userEvent } from '@testing-library/user-event' -import { convertElementToCssSelector } from '../../utils' import { getByAltTextSelector, getByLabelSelector, @@ -9,7 +8,8 @@ import { getByTestIdSelector, getByTextSelector, getByTitleSelector, -} from './playwright-selector/locatorUtils' +} from 'ivya' +import { convertElementToCssSelector } from '../../utils' import { Locator, selectorEngine } from './index' page.extend({ @@ -20,7 +20,7 @@ page.extend({ return new PreviewLocator(getByRoleSelector(role, options)) }, getByTestId(testId) { - return new PreviewLocator(getByTestIdSelector(page.config.browser.locators.testIdAttribute, testId)) + return new PreviewLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId)) }, getByAltText(text, options) { return new PreviewLocator(getByAltTextSelector(text, options)) diff --git a/packages/browser/src/client/tester/locators/webdriverio.ts b/packages/browser/src/client/tester/locators/webdriverio.ts index e48bc7ef8ea7..d92b53649b88 100644 --- a/packages/browser/src/client/tester/locators/webdriverio.ts +++ b/packages/browser/src/client/tester/locators/webdriverio.ts @@ -1,5 +1,4 @@ -import { page } from '@vitest/browser/context' -import { convertElementToCssSelector } from '../../utils' +import { page, server } from '@vitest/browser/context' import { getByAltTextSelector, getByLabelSelector, @@ -8,7 +7,8 @@ import { getByTestIdSelector, getByTextSelector, getByTitleSelector, -} from './playwright-selector/locatorUtils' +} from 'ivya' +import { convertElementToCssSelector } from '../../utils' import { Locator, selectorEngine } from './index' page.extend({ @@ -19,7 +19,7 @@ page.extend({ return new WebdriverIOLocator(getByRoleSelector(role, options)) }, getByTestId(testId) { - return new WebdriverIOLocator(getByTestIdSelector(page.config.browser.locators.testIdAttribute, testId)) + return new WebdriverIOLocator(getByTestIdSelector(server.config.browser.locators.testIdAttribute, testId)) }, getByAltText(text, options) { return new WebdriverIOLocator(getByAltTextSelector(text, options)) diff --git a/packages/vitest/src/public/index.ts b/packages/vitest/src/public/index.ts index 3b0293034179..881bdbb533f5 100644 --- a/packages/vitest/src/public/index.ts +++ b/packages/vitest/src/public/index.ts @@ -136,7 +136,7 @@ export type Test = Test_ /** @deprecated use `RunnerCustomCase` instead */ export type Custom = Custom_ /** @deprecated use `RunnerTask` instead */ -export type Task = Task_ +export type RunnerTask = Task_ export type { RunMode, diff --git a/packages/vitest/src/runtime/config.ts b/packages/vitest/src/runtime/config.ts index 912b4bb271d8..0ecbf4cd1628 100644 --- a/packages/vitest/src/runtime/config.ts +++ b/packages/vitest/src/runtime/config.ts @@ -121,6 +121,9 @@ export interface SerializedConfig { width: number height: number } + locators: { + testIdAttribute: string + } screenshotFailures: boolean } standalone: boolean diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24b4485c1ce7..c756596138b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -170,7 +170,7 @@ importers: version: 0.20.1(@vite-pwa/assets-generator@0.2.4)(vite@5.3.3(@types/node@20.14.14)(terser@5.22.0))(workbox-build@7.1.0(@types/babel__core@7.20.5))(workbox-window@7.1.0) vitepress: specifier: ^1.3.1 - version: 1.3.1(@algolia/client-search@4.20.0)(@types/node@20.14.14)(@types/react@18.2.79)(postcss@8.4.40)(react-dom@18.0.0(react@18.2.0))(react@18.2.0)(search-insights@2.9.0)(terser@5.22.0)(typescript@5.5.4) + version: 1.3.1(@algolia/client-search@4.20.0)(@types/node@20.14.14)(@types/react@18.2.79)(postcss@8.4.40)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.9.0)(terser@5.22.0)(typescript@5.5.4) workbox-window: specifier: ^7.1.0 version: 7.1.0 @@ -281,7 +281,7 @@ importers: version: 6.4.2(@types/jest@29.0.0)(vitest@packages+vitest) '@testing-library/react': specifier: ^13.2.0 - version: 13.4.0(react-dom@18.0.0(react@18.2.0))(react@18.2.0) + version: 13.4.0(react-dom@18.3.1(react@18.2.0))(react@18.2.0) '@testing-library/user-event': specifier: ^14.5.2 version: 14.5.2(@testing-library/dom@10.4.0) @@ -378,7 +378,7 @@ importers: version: 6.4.2(@types/jest@29.0.0)(vitest@packages+vitest) '@testing-library/react': specifier: ^13.2.0 - version: 13.4.0(react-dom@18.0.0(react@18.2.0))(react@18.2.0) + version: 13.4.0(react-dom@18.3.1(react@18.2.0))(react@18.2.0) '@testing-library/user-event': specifier: ^14.5.2 version: 14.5.2(@testing-library/dom@10.4.0) @@ -461,6 +461,9 @@ importers: flatted: specifier: ^3.3.1 version: 3.3.1 + ivya: + specifier: ^1.1.0 + version: 1.1.0 pathe: specifier: ^1.1.2 version: 1.1.2 @@ -1059,7 +1062,7 @@ importers: devDependencies: '@testing-library/react': specifier: ^13.2.0 - version: 13.4.0(react-dom@18.3.1)(react@18.3.1) + version: 13.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@types/react': specifier: ^18.2.79 version: 18.2.79 @@ -1587,10 +1590,6 @@ packages: resolution: {integrity: sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.24.9': - resolution: {integrity: sha512-e701mcfApCJqMMueQI0Fb68Amflj83+dvAvHawoBpAz+GDjCIyGHzNwnefjsWJ3xiYAqqiQFoWbspGYBdb2/ng==} - engines: {node: '>=6.9.0'} - '@babel/compat-data@7.25.2': resolution: {integrity: sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==} engines: {node: '>=6.9.0'} @@ -1611,10 +1610,6 @@ packages: resolution: {integrity: sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==} engines: {node: '>=6.9.0'} - '@babel/core@7.24.9': - resolution: {integrity: sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==} - engines: {node: '>=6.9.0'} - '@babel/core@7.25.2': resolution: {integrity: sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==} engines: {node: '>=6.9.0'} @@ -1655,10 +1650,6 @@ packages: resolution: {integrity: sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.24.8': - resolution: {integrity: sha512-oU+UoqCHdp+nWVDkpldqIQL/i/bvAv53tRqLG/s+cOXxe66zOYLU7ar/Xs3LdmBihrUMEUhwu6dMZwbNOYDwvw==} - engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.25.2': resolution: {integrity: sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==} engines: {node: '>=6.9.0'} @@ -1725,12 +1716,6 @@ packages: peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-module-transforms@7.24.9': - resolution: {integrity: sha512-oYbh+rtFKj/HwBQkFlUzvcybzklmVdVV3UU+mN7n2t/q3yGHbuVdNxyFvSBO1tfvjyArpHNcWMAzsSPdyI46hw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - '@babel/helper-module-transforms@7.25.2': resolution: {integrity: sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==} engines: {node: '>=6.9.0'} @@ -1821,10 +1806,6 @@ packages: resolution: {integrity: sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.24.8': - resolution: {integrity: sha512-gV2265Nkcz7weJJfvDoAEVzC1e2OTDpkGbEsebse8koXUJUXPsCMi7sRo/+SPMuMZ9MtUPnGwITTnQnU5YjyaQ==} - engines: {node: '>=6.9.0'} - '@babel/helpers@7.25.0': resolution: {integrity: sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==} engines: {node: '>=6.9.0'} @@ -6450,6 +6431,9 @@ packages: resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} engines: {node: '>=8'} + ivya@1.1.0: + resolution: {integrity: sha512-M9JrukbVtwVOK+rbCka+H1TFO4ZMcdT40idmGOXGjFR+B+D16N6E2POIA1vmPLzyYBKA4nFUr8sq4ojGi5DWxQ==} + jackspeak@3.4.0: resolution: {integrity: sha512-JVYhQnN59LVPFCEcVa2C3CrEKYacvjRfqIQl+h8oi91aLYQVWRYbxjPcv1bUiUy/kLmQaANrYfNMCO3kuEDHfw==} engines: {node: '>=14'} @@ -6487,10 +6471,6 @@ packages: resolution: {integrity: sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==} hasBin: true - jiti@1.21.0: - resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} - hasBin: true - jiti@1.21.6: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true @@ -7619,10 +7599,10 @@ packages: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true - react-dom@18.0.0: - resolution: {integrity: sha512-XqX7uzmFo0pUceWFCt7Gff6IyIMzFUn7QMZrbrQfGxtaxXZIcGQzoNpRLE3fQLnS4XzLLPMZX2T9TRcSrasicw==} + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} peerDependencies: - react: ^18.0.0 + react: ^18.3.1 react-is@17.0.2: resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} @@ -7651,6 +7631,10 @@ packages: resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} engines: {node: '>=0.10.0'} + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} engines: {node: '>=8'} @@ -7875,8 +7859,8 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - scheduler@0.21.0: - resolution: {integrity: sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==} + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} scslre@0.3.0: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} @@ -9479,8 +9463,6 @@ snapshots: '@babel/compat-data@7.24.7': {} - '@babel/compat-data@7.24.9': {} - '@babel/compat-data@7.25.2': {} '@babel/core@7.23.3': @@ -9563,26 +9545,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/core@7.24.9': - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.24.7 - '@babel/generator': 7.24.10 - '@babel/helper-compilation-targets': 7.24.8 - '@babel/helper-module-transforms': 7.24.9(@babel/core@7.24.9) - '@babel/helpers': 7.24.8 - '@babel/parser': 7.24.8 - '@babel/template': 7.24.7 - '@babel/traverse': 7.24.8 - '@babel/types': 7.24.9 - convert-source-map: 2.0.0 - debug: 4.3.6 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/core@7.25.2': dependencies: '@ampproject/remapping': 2.3.0 @@ -9641,7 +9603,7 @@ snapshots: '@babel/helper-builder-binary-assignment-operator-visitor@7.22.15': dependencies: - '@babel/types': 7.24.9 + '@babel/types': 7.25.2 '@babel/helper-compilation-targets@7.23.6': dependencies: @@ -9659,14 +9621,6 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-compilation-targets@7.24.8': - dependencies: - '@babel/compat-data': 7.24.9 - '@babel/helper-validator-option': 7.24.8 - browserslist: 4.23.2 - lru-cache: 5.1.1 - semver: 6.3.1 - '@babel/helper-compilation-targets@7.25.2': dependencies: '@babel/compat-data': 7.25.2 @@ -9690,21 +9644,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-create-class-features-plugin@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-function-name': 7.24.7 - '@babel/helper-member-expression-to-functions': 7.24.7 - '@babel/helper-optimise-call-expression': 7.24.7 - '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.9) - '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - '@babel/helper-create-class-features-plugin@7.24.7(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -9720,17 +9659,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.24.9)': + '@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-annotate-as-pure': 7.24.7 regexpu-core: 5.3.2 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.24.9)': + '@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 - '@babel/helper-compilation-targets': 7.24.8 + '@babel/core': 7.25.2 + '@babel/helper-compilation-targets': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 debug: 4.3.6 lodash.debounce: 4.0.8 @@ -9738,10 +9677,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-define-polyfill-provider@0.6.1(@babel/core@7.24.9)': + '@babel/helper-define-polyfill-provider@0.6.1(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 - '@babel/helper-compilation-targets': 7.24.8 + '@babel/core': 7.25.2 + '@babel/helper-compilation-targets': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 debug: 4.3.6 lodash.debounce: 4.0.8 @@ -9828,17 +9767,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-module-imports': 7.24.7 - '@babel/helper-simple-access': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 - '@babel/helper-validator-identifier': 7.24.7 - transitivePeerDependencies: - - supports-color - '@babel/helper-module-transforms@7.24.7(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -9850,17 +9778,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.24.9(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-module-imports': 7.24.7 - '@babel/helper-simple-access': 7.24.7 - '@babel/helper-split-export-declaration': 7.24.7 - '@babel/helper-validator-identifier': 7.24.7 - transitivePeerDependencies: - - supports-color - '@babel/helper-module-transforms@7.25.2(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -9879,9 +9796,9 @@ snapshots: '@babel/helper-plugin-utils@7.24.7': {} - '@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.24.9)': + '@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-annotate-as-pure': 7.24.7 '@babel/helper-environment-visitor': 7.24.7 '@babel/helper-wrap-function': 7.22.20 @@ -9895,15 +9812,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-member-expression-to-functions': 7.24.7 - '@babel/helper-optimise-call-expression': 7.24.7 - transitivePeerDependencies: - - supports-color - '@babel/helper-replace-supers@7.24.7(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -9952,8 +9860,8 @@ snapshots: '@babel/helper-wrap-function@7.22.20': dependencies: '@babel/helper-function-name': 7.24.7 - '@babel/template': 7.24.7 - '@babel/types': 7.24.9 + '@babel/template': 7.25.0 + '@babel/types': 7.25.2 '@babel/helpers@7.24.5': dependencies: @@ -9968,11 +9876,6 @@ snapshots: '@babel/template': 7.24.7 '@babel/types': 7.24.7 - '@babel/helpers@7.24.8': - dependencies: - '@babel/template': 7.24.7 - '@babel/types': 7.24.9 - '@babel/helpers@7.25.0': dependencies: '@babel/template': 7.25.0 @@ -10005,67 +9908,67 @@ snapshots: dependencies: '@babel/types': 7.25.2 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.15(@babel/core@7.24.9)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.15(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.22.15(@babel/core@7.24.9)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.22.15(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - '@babel/plugin-transform-optional-chaining': 7.23.0(@babel/core@7.24.9) + '@babel/plugin-transform-optional-chaining': 7.23.0(@babel/core@7.25.2) transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.9)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.9)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.9)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.9)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.9)': + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.9)': + '@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-import-assertions@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-syntax-import-assertions@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-import-attributes@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-syntax-import-attributes@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.9)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.9)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 '@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.24.5)': @@ -10083,44 +9986,44 @@ snapshots: '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.9)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.9)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.9)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.9)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.9)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.9)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.9)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.9)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 '@babel/plugin-syntax-typescript@7.24.7(@babel/core@7.23.3)': @@ -10133,154 +10036,154 @@ snapshots: '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.9)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.9) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-arrow-functions@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-arrow-functions@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-async-generator-functions@7.23.2(@babel/core@7.24.9)': + '@babel/plugin-transform-async-generator-functions@7.23.2(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-environment-visitor': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.9) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.9) + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.25.2) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) - '@babel/plugin-transform-async-to-generator@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-async-to-generator@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-module-imports': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.9) + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.25.2) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-block-scoped-functions@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-block-scoping@7.23.0(@babel/core@7.24.9)': + '@babel/plugin-transform-block-scoping@7.23.0(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-class-properties@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-class-properties@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.9) + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.22.11(@babel/core@7.24.9)': + '@babel/plugin-transform-class-static-block@7.22.11(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.9) + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.9) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.22.15(@babel/core@7.24.9)': + '@babel/plugin-transform-classes@7.22.15(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-compilation-targets': 7.24.8 + '@babel/helper-compilation-targets': 7.25.2 '@babel/helper-environment-visitor': 7.24.7 '@babel/helper-function-name': 7.24.7 '@babel/helper-optimise-call-expression': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.9) + '@babel/helper-replace-supers': 7.24.7(@babel/core@7.25.2) '@babel/helper-split-export-declaration': 7.24.7 globals: 11.12.0 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-computed-properties@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/template': 7.24.7 + '@babel/template': 7.25.0 - '@babel/plugin-transform-destructuring@7.23.0(@babel/core@7.24.9)': + '@babel/plugin-transform-destructuring@7.23.0(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-dotall-regex@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-dotall-regex@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.9) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-duplicate-keys@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-duplicate-keys@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-dynamic-import@7.22.11(@babel/core@7.24.9)': + '@babel/plugin-transform-dynamic-import@7.22.11(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.9) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-transform-exponentiation-operator@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-exponentiation-operator@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-export-namespace-from@7.22.11(@babel/core@7.24.9)': + '@babel/plugin-transform-export-namespace-from@7.22.11(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.9) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-transform-for-of@7.22.15(@babel/core@7.24.9)': + '@babel/plugin-transform-for-of@7.22.15(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-function-name@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-function-name@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 - '@babel/helper-compilation-targets': 7.24.8 + '@babel/core': 7.25.2 + '@babel/helper-compilation-targets': 7.25.2 '@babel/helper-function-name': 7.24.7 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-json-strings@7.22.11(@babel/core@7.24.9)': + '@babel/plugin-transform-json-strings@7.22.11(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.9) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-transform-literals@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-literals@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-logical-assignment-operators@7.22.11(@babel/core@7.24.9)': + '@babel/plugin-transform-logical-assignment-operators@7.22.11(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.9) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) - '@babel/plugin-transform-member-expression-literals@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-member-expression-literals@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-modules-amd@7.23.0(@babel/core@7.24.9)': + '@babel/plugin-transform-modules-amd@7.23.0(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 - '@babel/helper-module-transforms': 7.24.9(@babel/core@7.24.9) + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.7 transitivePeerDependencies: - supports-color @@ -10294,15 +10197,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.24.7(@babel/core@7.24.9)': - dependencies: - '@babel/core': 7.24.9 - '@babel/helper-module-transforms': 7.24.7(@babel/core@7.24.9) - '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-simple-access': 7.24.7 - transitivePeerDependencies: - - supports-color - '@babel/plugin-transform-modules-commonjs@7.24.7(@babel/core@7.25.2)': dependencies: '@babel/core': 7.25.2 @@ -10312,105 +10206,105 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.23.0(@babel/core@7.24.9)': + '@babel/plugin-transform-modules-systemjs@7.23.0(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-hoist-variables': 7.24.7 - '@babel/helper-module-transforms': 7.24.9(@babel/core@7.24.9) + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.7 '@babel/helper-validator-identifier': 7.24.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-modules-umd@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 - '@babel/helper-module-transforms': 7.24.9(@babel/core@7.24.9) + '@babel/core': 7.25.2 + '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.9) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-new-target@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-new-target@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-nullish-coalescing-operator@7.22.11(@babel/core@7.24.9)': + '@babel/plugin-transform-nullish-coalescing-operator@7.22.11(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.9) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-transform-numeric-separator@7.22.11(@babel/core@7.24.9)': + '@babel/plugin-transform-numeric-separator@7.22.11(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.9) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) - '@babel/plugin-transform-object-rest-spread@7.22.15(@babel/core@7.24.9)': + '@babel/plugin-transform-object-rest-spread@7.22.15(@babel/core@7.25.2)': dependencies: - '@babel/compat-data': 7.24.9 - '@babel/core': 7.24.9 - '@babel/helper-compilation-targets': 7.24.8 + '@babel/compat-data': 7.25.2 + '@babel/core': 7.25.2 + '@babel/helper-compilation-targets': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.24.9) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.25.2) - '@babel/plugin-transform-object-super@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-object-super@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/helper-replace-supers': 7.24.7(@babel/core@7.24.9) + '@babel/helper-replace-supers': 7.24.7(@babel/core@7.25.2) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.22.11(@babel/core@7.24.9)': + '@babel/plugin-transform-optional-catch-binding@7.22.11(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.9) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) - '@babel/plugin-transform-optional-chaining@7.23.0(@babel/core@7.24.9)': + '@babel/plugin-transform-optional-chaining@7.23.0(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.9) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.22.15(@babel/core@7.24.9)': + '@babel/plugin-transform-parameters@7.22.15(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-private-methods@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-private-methods@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.9) + '@babel/core': 7.25.2 + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.22.11(@babel/core@7.24.9)': + '@babel/plugin-transform-private-property-in-object@7.22.11(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-annotate-as-pure': 7.24.7 - '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.24.9) + '@babel/helper-create-class-features-plugin': 7.24.7(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.9) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-property-literals@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 '@babel/plugin-transform-react-jsx-development@7.22.5(@babel/core@7.24.5)': @@ -10437,43 +10331,43 @@ snapshots: '@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.24.5) '@babel/types': 7.24.5 - '@babel/plugin-transform-regenerator@7.22.10(@babel/core@7.24.9)': + '@babel/plugin-transform-regenerator@7.22.10(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 regenerator-transform: 0.15.2 - '@babel/plugin-transform-reserved-words@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-reserved-words@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-shorthand-properties@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-shorthand-properties@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-spread@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-spread@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 '@babel/helper-skip-transparent-expression-wrappers': 7.24.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-sticky-regex@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-template-literals@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-template-literals@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-typeof-symbol@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-typeof-symbol@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 '@babel/plugin-transform-typescript@7.24.7(@babel/core@7.23.3)': @@ -10496,120 +10390,120 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.22.10(@babel/core@7.24.9)': + '@babel/plugin-transform-unicode-escapes@7.22.10(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-unicode-property-regex@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-unicode-property-regex@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.9) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-unicode-regex@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-unicode-regex@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.9) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.7 - '@babel/plugin-transform-unicode-sets-regex@7.22.5(@babel/core@7.24.9)': + '@babel/plugin-transform-unicode-sets-regex@7.22.5(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 - '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.9) + '@babel/core': 7.25.2 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.25.2) '@babel/helper-plugin-utils': 7.24.7 - '@babel/preset-env@7.23.2(@babel/core@7.24.9)': + '@babel/preset-env@7.23.2(@babel/core@7.25.2)': dependencies: - '@babel/compat-data': 7.24.9 - '@babel/core': 7.24.9 - '@babel/helper-compilation-targets': 7.24.8 + '@babel/compat-data': 7.25.2 + '@babel/core': 7.25.2 + '@babel/helper-compilation-targets': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 '@babel/helper-validator-option': 7.24.8 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.22.15(@babel/core@7.24.9) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.22.15(@babel/core@7.24.9) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.9) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.9) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.9) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.9) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-import-assertions': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-syntax-import-attributes': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.9) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.9) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.9) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.9) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.9) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.9) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.9) - '@babel/plugin-transform-arrow-functions': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-async-generator-functions': 7.23.2(@babel/core@7.24.9) - '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-block-scoped-functions': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-block-scoping': 7.23.0(@babel/core@7.24.9) - '@babel/plugin-transform-class-properties': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-class-static-block': 7.22.11(@babel/core@7.24.9) - '@babel/plugin-transform-classes': 7.22.15(@babel/core@7.24.9) - '@babel/plugin-transform-computed-properties': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-destructuring': 7.23.0(@babel/core@7.24.9) - '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-duplicate-keys': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-dynamic-import': 7.22.11(@babel/core@7.24.9) - '@babel/plugin-transform-exponentiation-operator': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-export-namespace-from': 7.22.11(@babel/core@7.24.9) - '@babel/plugin-transform-for-of': 7.22.15(@babel/core@7.24.9) - '@babel/plugin-transform-function-name': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-json-strings': 7.22.11(@babel/core@7.24.9) - '@babel/plugin-transform-literals': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-logical-assignment-operators': 7.22.11(@babel/core@7.24.9) - '@babel/plugin-transform-member-expression-literals': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-modules-amd': 7.23.0(@babel/core@7.24.9) - '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.24.9) - '@babel/plugin-transform-modules-systemjs': 7.23.0(@babel/core@7.24.9) - '@babel/plugin-transform-modules-umd': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-new-target': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-nullish-coalescing-operator': 7.22.11(@babel/core@7.24.9) - '@babel/plugin-transform-numeric-separator': 7.22.11(@babel/core@7.24.9) - '@babel/plugin-transform-object-rest-spread': 7.22.15(@babel/core@7.24.9) - '@babel/plugin-transform-object-super': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-optional-catch-binding': 7.22.11(@babel/core@7.24.9) - '@babel/plugin-transform-optional-chaining': 7.23.0(@babel/core@7.24.9) - '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.24.9) - '@babel/plugin-transform-private-methods': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-private-property-in-object': 7.22.11(@babel/core@7.24.9) - '@babel/plugin-transform-property-literals': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-regenerator': 7.22.10(@babel/core@7.24.9) - '@babel/plugin-transform-reserved-words': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-shorthand-properties': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-spread': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-sticky-regex': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-template-literals': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-typeof-symbol': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-unicode-escapes': 7.22.10(@babel/core@7.24.9) - '@babel/plugin-transform-unicode-property-regex': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-unicode-regex': 7.22.5(@babel/core@7.24.9) - '@babel/plugin-transform-unicode-sets-regex': 7.22.5(@babel/core@7.24.9) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.9) - '@babel/types': 7.24.9 - babel-plugin-polyfill-corejs2: 0.4.10(@babel/core@7.24.9) - babel-plugin-polyfill-corejs3: 0.8.5(@babel/core@7.24.9) - babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.24.9) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.22.15(@babel/core@7.25.2) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.22.15(@babel/core@7.25.2) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.25.2) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.25.2) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.25.2) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-import-assertions': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-syntax-import-attributes': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.25.2) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.25.2) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.25.2) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.25.2) + '@babel/plugin-transform-arrow-functions': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-async-generator-functions': 7.23.2(@babel/core@7.25.2) + '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-block-scoped-functions': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-block-scoping': 7.23.0(@babel/core@7.25.2) + '@babel/plugin-transform-class-properties': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-class-static-block': 7.22.11(@babel/core@7.25.2) + '@babel/plugin-transform-classes': 7.22.15(@babel/core@7.25.2) + '@babel/plugin-transform-computed-properties': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-destructuring': 7.23.0(@babel/core@7.25.2) + '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-duplicate-keys': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-dynamic-import': 7.22.11(@babel/core@7.25.2) + '@babel/plugin-transform-exponentiation-operator': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-export-namespace-from': 7.22.11(@babel/core@7.25.2) + '@babel/plugin-transform-for-of': 7.22.15(@babel/core@7.25.2) + '@babel/plugin-transform-function-name': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-json-strings': 7.22.11(@babel/core@7.25.2) + '@babel/plugin-transform-literals': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-logical-assignment-operators': 7.22.11(@babel/core@7.25.2) + '@babel/plugin-transform-member-expression-literals': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-modules-amd': 7.23.0(@babel/core@7.25.2) + '@babel/plugin-transform-modules-commonjs': 7.24.7(@babel/core@7.25.2) + '@babel/plugin-transform-modules-systemjs': 7.23.0(@babel/core@7.25.2) + '@babel/plugin-transform-modules-umd': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-new-target': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-nullish-coalescing-operator': 7.22.11(@babel/core@7.25.2) + '@babel/plugin-transform-numeric-separator': 7.22.11(@babel/core@7.25.2) + '@babel/plugin-transform-object-rest-spread': 7.22.15(@babel/core@7.25.2) + '@babel/plugin-transform-object-super': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-optional-catch-binding': 7.22.11(@babel/core@7.25.2) + '@babel/plugin-transform-optional-chaining': 7.23.0(@babel/core@7.25.2) + '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.25.2) + '@babel/plugin-transform-private-methods': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-private-property-in-object': 7.22.11(@babel/core@7.25.2) + '@babel/plugin-transform-property-literals': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-regenerator': 7.22.10(@babel/core@7.25.2) + '@babel/plugin-transform-reserved-words': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-shorthand-properties': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-spread': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-sticky-regex': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-template-literals': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-typeof-symbol': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-escapes': 7.22.10(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-property-regex': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-regex': 7.22.5(@babel/core@7.25.2) + '@babel/plugin-transform-unicode-sets-regex': 7.22.5(@babel/core@7.25.2) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.25.2) + '@babel/types': 7.25.2 + babel-plugin-polyfill-corejs2: 0.4.10(@babel/core@7.25.2) + babel-plugin-polyfill-corejs3: 0.8.5(@babel/core@7.25.2) + babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.25.2) core-js-compat: 3.37.1 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.9)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.25.2)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-plugin-utils': 7.24.7 - '@babel/types': 7.24.9 + '@babel/types': 7.25.2 esutils: 2.0.3 '@babel/preset-typescript@7.23.2(@babel/core@7.23.3)': @@ -10781,9 +10675,9 @@ snapshots: '@docsearch/css@3.6.0': {} - '@docsearch/js@3.6.0(@algolia/client-search@4.20.0)(@types/react@18.2.79)(react-dom@18.0.0(react@18.2.0))(react@18.2.0)(search-insights@2.9.0)': + '@docsearch/js@3.6.0(@algolia/client-search@4.20.0)(@types/react@18.2.79)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.9.0)': dependencies: - '@docsearch/react': 3.6.0(@algolia/client-search@4.20.0)(@types/react@18.2.79)(react-dom@18.0.0(react@18.2.0))(react@18.2.0)(search-insights@2.9.0) + '@docsearch/react': 3.6.0(@algolia/client-search@4.20.0)(@types/react@18.2.79)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.9.0) preact: 10.21.0 transitivePeerDependencies: - '@algolia/client-search' @@ -10792,7 +10686,7 @@ snapshots: - react-dom - search-insights - '@docsearch/react@3.6.0(@algolia/client-search@4.20.0)(@types/react@18.2.79)(react-dom@18.0.0(react@18.2.0))(react@18.2.0)(search-insights@2.9.0)': + '@docsearch/react@3.6.0(@algolia/client-search@4.20.0)(@types/react@18.2.79)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.9.0)': dependencies: '@algolia/autocomplete-core': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0)(search-insights@2.9.0) '@algolia/autocomplete-preset-algolia': 1.9.3(@algolia/client-search@4.20.0)(algoliasearch@4.20.0) @@ -10800,8 +10694,8 @@ snapshots: algoliasearch: 4.20.0 optionalDependencies: '@types/react': 18.2.79 - react: 18.2.0 - react-dom: 18.0.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) search-insights: 2.9.0 transitivePeerDependencies: - '@algolia/client-search' @@ -11338,9 +11232,9 @@ snapshots: '@remix-run/router@1.16.0': {} - '@rollup/plugin-babel@5.3.1(@babel/core@7.24.9)(@types/babel__core@7.20.5)(rollup@4.20.0)': + '@rollup/plugin-babel@5.3.1(@babel/core@7.25.2)(@types/babel__core@7.20.5)(rollup@4.20.0)': dependencies: - '@babel/core': 7.24.9 + '@babel/core': 7.25.2 '@babel/helper-module-imports': 7.24.7 '@rollup/pluginutils': 3.1.0(rollup@4.20.0) rollup: 4.20.0 @@ -11767,13 +11661,21 @@ snapshots: '@testing-library/dom': 8.19.0 preact: 10.21.0 - '@testing-library/react@13.4.0(react-dom@18.0.0(react@18.2.0))(react@18.2.0)': + '@testing-library/react@13.4.0(react-dom@18.3.1(react@18.2.0))(react@18.2.0)': dependencies: '@babel/runtime': 7.24.4 '@testing-library/dom': 8.19.0 '@types/react-dom': 18.2.14 react: 18.2.0 - react-dom: 18.0.0(react@18.2.0) + react-dom: 18.3.1(react@18.2.0) + + '@testing-library/react@13.4.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.24.4 + '@testing-library/dom': 8.19.0 + '@types/react-dom': 18.2.14 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) '@testing-library/user-event@14.5.2(@testing-library/dom@10.4.0)': dependencies: @@ -13002,27 +12904,27 @@ snapshots: html-entities: 2.3.3 validate-html-nesting: 1.2.2 - babel-plugin-polyfill-corejs2@0.4.10(@babel/core@7.24.9): + babel-plugin-polyfill-corejs2@0.4.10(@babel/core@7.25.2): dependencies: - '@babel/compat-data': 7.24.9 - '@babel/core': 7.24.9 - '@babel/helper-define-polyfill-provider': 0.6.1(@babel/core@7.24.9) + '@babel/compat-data': 7.25.2 + '@babel/core': 7.25.2 + '@babel/helper-define-polyfill-provider': 0.6.1(@babel/core@7.25.2) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.8.5(@babel/core@7.24.9): + babel-plugin-polyfill-corejs3@0.8.5(@babel/core@7.25.2): dependencies: - '@babel/core': 7.24.9 - '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.24.9) + '@babel/core': 7.25.2 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.25.2) core-js-compat: 3.37.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.24.9): + babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.25.2): dependencies: - '@babel/core': 7.24.9 - '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.24.9) + '@babel/core': 7.25.2 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.25.2) transitivePeerDependencies: - supports-color @@ -15064,7 +14966,7 @@ snapshots: debug: 4.3.6 esbuild: 0.23.0 jiti: 2.0.0-beta.2 - jiti-v1: jiti@1.21.0 + jiti-v1: jiti@1.21.6 pathe: 1.1.2 pkg-types: 1.1.3 tsx: 4.16.5 @@ -15298,6 +15200,8 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + ivya@1.1.0: {} + jackspeak@3.4.0: dependencies: '@isaacs/cliui': 8.0.2 @@ -15356,8 +15260,6 @@ snapshots: jiti@1.20.0: {} - jiti@1.21.0: {} - jiti@1.21.6: {} jiti@2.0.0-beta.2: {} @@ -16656,11 +16558,17 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - react-dom@18.0.0(react@18.2.0): + react-dom@18.3.1(react@18.2.0): dependencies: loose-envify: 1.4.0 react: 18.2.0 - scheduler: 0.21.0 + scheduler: 0.23.2 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 react-is@17.0.2: {} @@ -16684,6 +16592,10 @@ snapshots: dependencies: loose-envify: 1.4.0 + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + read-pkg-up@7.0.1: dependencies: find-up: 4.1.0 @@ -16952,7 +16864,7 @@ snapshots: dependencies: xmlchars: 2.2.0 - scheduler@0.21.0: + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -17959,9 +17871,9 @@ snapshots: punycode: 1.4.1 qs: 6.11.2 - use-sync-external-store@1.2.0(react@18.2.0): + use-sync-external-store@1.2.0(react@18.3.1): dependencies: - react: 18.2.0 + react: 18.3.1 userhome@1.0.0: {} @@ -18072,10 +17984,10 @@ snapshots: optionalDependencies: vite: 5.3.3(@types/node@20.14.14)(terser@5.22.0) - vitepress@1.3.1(@algolia/client-search@4.20.0)(@types/node@20.14.14)(@types/react@18.2.79)(postcss@8.4.40)(react-dom@18.0.0(react@18.2.0))(react@18.2.0)(search-insights@2.9.0)(terser@5.22.0)(typescript@5.5.4): + vitepress@1.3.1(@algolia/client-search@4.20.0)(@types/node@20.14.14)(@types/react@18.2.79)(postcss@8.4.40)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.9.0)(terser@5.22.0)(typescript@5.5.4): dependencies: '@docsearch/css': 3.6.0 - '@docsearch/js': 3.6.0(@algolia/client-search@4.20.0)(@types/react@18.2.79)(react-dom@18.0.0(react@18.2.0))(react@18.2.0)(search-insights@2.9.0) + '@docsearch/js': 3.6.0(@algolia/client-search@4.20.0)(@types/react@18.2.79)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.9.0) '@shikijs/core': 1.10.3 '@shikijs/transformers': 1.10.3 '@types/markdown-it': 14.1.1 @@ -18413,10 +18325,10 @@ snapshots: workbox-build@7.1.0(@types/babel__core@7.20.5): dependencies: '@apideck/better-ajv-errors': 0.3.6(ajv@8.12.0) - '@babel/core': 7.24.9 - '@babel/preset-env': 7.23.2(@babel/core@7.24.9) + '@babel/core': 7.25.2 + '@babel/preset-env': 7.23.2(@babel/core@7.25.2) '@babel/runtime': 7.24.4 - '@rollup/plugin-babel': 5.3.1(@babel/core@7.24.9)(@types/babel__core@7.20.5)(rollup@4.20.0) + '@rollup/plugin-babel': 5.3.1(@babel/core@7.25.2)(@types/babel__core@7.20.5)(rollup@4.20.0) '@rollup/plugin-node-resolve': 15.2.3(rollup@4.20.0) '@rollup/plugin-replace': 2.4.2(rollup@4.20.0) '@rollup/plugin-terser': 0.4.4(rollup@4.20.0) @@ -18607,11 +18519,11 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.1.0 - zustand@4.1.1(react@18.2.0): + zustand@4.1.1(react@18.3.1): dependencies: - use-sync-external-store: 1.2.0(react@18.2.0) + use-sync-external-store: 1.2.0(react@18.3.1) optionalDependencies: - react: 18.2.0 + react: 18.3.1 zwitch@2.0.4: {}