diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 1f3175ecbfd..6202d0dbfbd 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,8 +1,12 @@ -## 15.0.1 +## 15.1.0 _Released 08/26/2025 (PENDING)_ +**Features:** + +- Expanded `cy.press()` to support more key types. Addresses [#31051](https://github.com/cypress-io/cypress/issues/31051) and [#31488](https://github.com/cypress-io/cypress/issues/31488). Addressed in [#31496](https://github.com/cypress-io/cypress/pull/31496). + **Bugfixes:** - Fixed an issue where the open Studio button would incorrectly show for component tests. Addressed in [#32315](https://github.com/cypress-io/cypress/pull/32315). diff --git a/cli/types/cypress-automation.d.ts b/cli/types/cypress-automation.d.ts new file mode 100644 index 00000000000..e636551de50 --- /dev/null +++ b/cli/types/cypress-automation.d.ts @@ -0,0 +1,18 @@ +declare namespace Cypress { + type SupportedNamedKey = 'ArrowDown' | + 'ArrowLeft' | + 'ArrowRight' | + 'ArrowUp' | + 'End' | + 'Home' | + 'PageDown' | + 'PageUp' | + 'Space' | + 'Enter' | + 'Tab' | + 'Backspace' | + 'Delete' | + 'Insert' + + type SupportedKey = SupportedNamedKey | string | number +} diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 35a5d517460..11133b937fd 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -2,7 +2,7 @@ /// /// /// - +/// declare namespace Cypress { type FileContents = string | any[] | object type HistoryDirection = 'back' | 'forward' @@ -684,7 +684,20 @@ declare namespace Cypress { Keyboard: { defaults(options: Partial): void Keys: { + DOWN: 'ArrowDown', + LEFT: 'ArrowLeft', + RIGHT: 'ArrowRight', + UP: 'ArrowUp', + END: 'End', + HOME: 'Home', + PAGEDOWN: 'PageDown', + PAGEUP: 'PageUp', + ENTER: 'Enter', TAB: 'Tab', + BACKSPACE: 'Backspace', + SPACE: 'Space', + DELETE: 'Delete', + INSERT: 'Insert', }, } @@ -1765,7 +1778,7 @@ declare namespace Cypress { * cy.press(Cypress.Keyboard.Keys.TAB) // dispatches a keydown and press event to the browser, followed by a keyup event. * @see https://on.cypress.io/press */ - press(key: typeof Cypress.Keyboard.Keys[keyof typeof Cypress.Keyboard.Keys], options?: Partial): Chainable + press(key: SupportedKey, options?: Partial): Chainable /** * Get the immediately preceding sibling of each element in a set of the elements. diff --git a/package.json b/package.json index 473fe5073a7..2245458e7a6 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "stop-only": "npx stop-only --skip .cy,.publish,.projects,node_modules,dist,dist-test,fixtures,lib,bower_components,src,__snapshots__,patches --exclude cypress-tests.ts,*only.cy.js", "stop-only-all": "yarn stop-only --folder packages", "pretest": "yarn ensure-deps", - "test": "yarn lerna exec yarn test --scope=cypress --scope=@packages/{config,data-context,driver,electron,errors,extension,https-proxy,launcher,net-stubbing,network,packherd-require,proxy,rewriter,scaffold-config,socket,v8-snapshot-require,telemetry,stderr-filtering} --scope=@tooling/{electron-mksnapshot,v8-snapshot}", + "test": "yarn lerna exec yarn test --scope=cypress --scope=@packages/{config,data-context,driver,electron,errors,extension,https-proxy,launcher,net-stubbing,network,packherd-require,proxy,rewriter,scaffold-config,socket,v8-snapshot-require,telemetry,stderr-filtering,types} --scope=@tooling/{electron-mksnapshot,v8-snapshot}", "test-debug": "lerna exec yarn test-debug --ignore=@packages/{driver,root,static,web-config}", "test-integration": "lerna exec yarn test-integration --ignore=@packages/{driver,root,static,web-config}", "test-mocha": "mocha --reporter spec scripts/spec.js", diff --git a/packages/driver/cypress/e2e/commands/actions/press.cy.ts b/packages/driver/cypress/e2e/commands/actions/press.cy.ts index 1fcf76f5257..e9131b33e00 100644 --- a/packages/driver/cypress/e2e/commands/actions/press.cy.ts +++ b/packages/driver/cypress/e2e/commands/actions/press.cy.ts @@ -1,21 +1,49 @@ describe('src/cy/commands/actions/press', () => { - it('dispatches the tab keypress to the AUT', () => { - // Non-BiDi firefox is not supported - if (Cypress.browser.family === 'firefox' && Cypress.browserMajorVersion() < 135) { - return - } - - // TODO: Webkit is not supported. https://github.com/cypress-io/cypress/issues/31054 - if (Cypress.isBrowser('webkit')) { - return - } + // TODO: Webkit is not supported. https://github.com/cypress-io/cypress/issues/31054 + if (Cypress.isBrowser('webkit')) { + return + } + beforeEach(() => { cy.visit('/fixtures/input_events.html') + }) - cy.press(Cypress.Keyboard.Keys.TAB) - - cy.get('#keydown').should('have.value', 'Tab') + it('fires the click event on the button when the named key is sent', () => { + cy.get('#button').focus() + cy.get('#button').should('be.focused') + cy.press(Cypress.Keyboard.Keys.SPACE) + cy.get('#checkbox').should('be.checked') + }) - cy.get('#keyup').should('have.value', 'Tab') + it('fires the click event on the button when a space is sent', () => { + cy.get('#button').focus() + cy.get('#button').should('be.focused') + cy.press(' ') + cy.get('#checkbox').should('be.checked') }) + + const testKeyDownUp = (key) => { + it(`dispatches ${key} keypress to the AUT`, () => { + cy.press(key) + // spacebar is a special case - it's both a named key and a single character, + // but when we dispatch the named key (via codepoint in bidi, via `Space` in CDP) + // we get the space character, not the name of the key. + cy.get('#keydown').should('have.value', key === 'Space' ? ' ' : key) + }) + } + + Object.values(Cypress.Keyboard.Keys).forEach(testKeyDownUp) + + // sets truncated for speed + + // // Numbers + ;['0', '1'].forEach(testKeyDownUp) + + ;[0, 1].forEach(testKeyDownUp) + + // // Letters + ;['a', 'z'].forEach(testKeyDownUp) + + // // Special characters + ;['!', ' ', '€', 'é'].forEach(testKeyDownUp) }) diff --git a/packages/driver/cypress/fixtures/input_events.html b/packages/driver/cypress/fixtures/input_events.html index 2a23c656452..fead454bdc7 100644 --- a/packages/driver/cypress/fixtures/input_events.html +++ b/packages/driver/cypress/fixtures/input_events.html @@ -3,15 +3,29 @@ Input Event Monitor - + + + + + + + + diff --git a/packages/driver/cypress/tsconfig.json b/packages/driver/cypress/tsconfig.json index ee98b718d4d..bded9f172ae 100644 --- a/packages/driver/cypress/tsconfig.json +++ b/packages/driver/cypress/tsconfig.json @@ -9,7 +9,8 @@ // We are setting sourceMap=true in order to have full codeFrame support within the driver package. // We need to do this because we are using the standalone @cypress/webpack-preprocessor, which doesn't // default the sourceMap option for us - "sourceMap": true + "sourceMap": true, + "moduleResolution": "node" }, "include": ["**/*.ts"] } \ No newline at end of file diff --git a/packages/driver/src/cy/commands/actions/press.ts b/packages/driver/src/cy/commands/actions/press.ts index 7c4110f35de..99c27a708f9 100644 --- a/packages/driver/src/cy/commands/actions/press.ts +++ b/packages/driver/src/cy/commands/actions/press.ts @@ -1,17 +1,16 @@ import type { $Cy } from '../../../cypress/cy' import type { StateFunc } from '../../../cypress/state' -import type { KeyPressSupportedKeys, AutomationCommands } from '@packages/types' +import { isSupportedKey, SupportedKey, AutomationCommands } from '@packages/types' import { defaults } from 'lodash' -import { isSupportedKey } from '@packages/server/lib/automation/commands/key_press' import $errUtils from '../../../cypress/error_utils' import $utils from '../../../cypress/utils' export interface PressCommand { - (key: KeyPressSupportedKeys, userOptions?: Partial & Partial): void + (key: SupportedKey | string, userOptions?: Partial & Partial): void } export default function (Commands: Cypress.Commands, Cypress: Cypress.Cypress, cy: $Cy, state: StateFunc, config: any) { - async function pressCommand (key: KeyPressSupportedKeys, userOptions?: Partial & Partial) { + async function pressCommand (key: SupportedKey | string | number, userOptions?: Partial & Partial) { const options: Cypress.Loggable & Partial = defaults({}, userOptions, { log: true, }) @@ -50,17 +49,6 @@ export default function (Commands: Cypress.Commands, Cypress: Cypress.Cypress, c return null } - if (Cypress.browser.name === 'firefox' && Number(Cypress.browser.majorVersion) < 135) { - $errUtils.throwErrByPath('press.unsupported_browser_version', { - onFail: log, - args: { - browser: Cypress.browser.name, - version: Cypress.browser.majorVersion, - minimumVersion: 135, - }, - }) - } - try { const command: 'key:press' = 'key:press' const args: AutomationCommands[typeof command]['dataType'] = { diff --git a/packages/driver/src/cy/keyboard.ts b/packages/driver/src/cy/keyboard.ts index 3256d9f951c..29532e048e8 100644 --- a/packages/driver/src/cy/keyboard.ts +++ b/packages/driver/src/cy/keyboard.ts @@ -14,7 +14,6 @@ import $utils from '../cypress/utils' import $window from '../dom/window' import type { Log } from '../cypress/log' import type { StateFunc } from '../cypress/state' -import type { KeyPressSupportedKeys } from '@packages/types' const debug = Debug('cypress:driver:keyboard') @@ -1398,8 +1397,21 @@ const defaults = (props: Partial) => { return getConfig() } -const Keys: Record = { +const Keys: Record = { + DOWN: 'ArrowDown', + LEFT: 'ArrowLeft', + RIGHT: 'ArrowRight', + UP: 'ArrowUp', + END: 'End', + HOME: 'Home', + PAGEDOWN: 'PageDown', + PAGEUP: 'PageUp', + ENTER: 'Enter', TAB: 'Tab', + BACKSPACE: 'Backspace', + SPACE: 'Space', + DELETE: 'Delete', + INSERT: 'Insert', } export default { diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index b65b62eff60..094434b6bfb 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -1311,15 +1311,24 @@ export default { }, press: { - invalid_key: stripIndent`\ - \`{{key}}\` is not supported by ${cmd('press')}. See \`Cypress.Keyboard.Keys\` for keys that are supported. - `, - unsupported_browser_version: stripIndent`\ - ${cmd('press')} is not supported in {{browser}} version {{version}}. Upgrade to version {{minimumVersion}} to use \`cy.press()\`. - `, - unsupported_browser: stripIndent`\ + invalid_key: { + message: stripIndent`\ + \`{{key}}\` is not supported by ${cmd('press')}. + `, + docsUrl: 'https://on.cypress.io/press', + }, + unsupported_browser_version: { + message: stripIndent`\ + ${cmd('press')} is not supported in {{browser}} version {{version}}. Upgrade to version {{minimumVersion}} to use \`cy.press()\`. + `, + docsUrl: 'https://on.cypress.io/press', + }, + unsupported_browser: { + message: stripIndent`\ ${cmd('press')} is not supported in {{family}} browsers. - `, + `, + docsUrl: 'https://on.cypress.io/press', + }, }, proxy: { diff --git a/packages/driver/test/unit/cy/commands/actions/press.spec.ts b/packages/driver/test/unit/cy/commands/actions/press.spec.ts index 228afe24d7f..549e543d6d0 100644 --- a/packages/driver/test/unit/cy/commands/actions/press.spec.ts +++ b/packages/driver/test/unit/cy/commands/actions/press.spec.ts @@ -2,7 +2,7 @@ * @vitest-environment jsdom */ import { vi, describe, it, expect, beforeEach, Mock, MockedObject } from 'vitest' -import type { KeyPressSupportedKeys } from '@packages/types' + import addCommand, { PressCommand } from '../../../../../src/cy/commands/actions/press' import type { $Cy } from '../../../../../src/cypress/cy' import type { StateFunc } from '../../../../../src/cypress/state' @@ -25,7 +25,6 @@ vi.mock('../../../../../src/cypress/error_utils', async () => { }) describe('cy/commands/actions/press', () => { - let log: Mock let automation: Mock let press: PressCommand let Cypress: MockedObject @@ -36,13 +35,11 @@ describe('cy/commands/actions/press', () => { let logReturnValue: Cypress.Log beforeEach(() => { - log = vi.fn() automation = vi.fn() Cypress = { - // The overloads for `log` don't get applied correctly here - // @ts-expect-error - log, + // @ts-expect-error - has a type conflict between Driver and CLI d.ts + log: vi.fn(), automation, // @ts-expect-error browser: { @@ -95,14 +92,15 @@ describe('cy/commands/actions/press', () => { }) describe('with a valid key', () => { - const key: KeyPressSupportedKeys = Keyboard.Keys.TAB - - it('dispatches a key:press automation command', async () => { - await press(key) - expect(automation).toHaveBeenCalledWith('key:press', { key }) - }) + for (const key of Object.values(Keyboard.Keys)) { + it(`dispatches a key:press automation command for key: ${key}`, async () => { + await press(key) + expect(automation).toHaveBeenCalledWith('key:press', { key }) + }) + } describe('with options', () => { + const key = 'Tab' let options: Cypress.Loggable & Cypress.Timeoutable beforeEach(() => { @@ -152,23 +150,6 @@ describe('cy/commands/actions/press', () => { }) }) - describe('when in firefox below 135', () => { - it('throws an unsupported browser version error', async () => { - Cypress.browser.name = 'firefox' - Cypress.browser.majorVersion = '134' - await expect(press('Tab')).rejects.toThrow('`cy.press()` is not supported in firefox version 134. Upgrade to version 135 to use `cy.press()`.') - - expect($errUtils.throwErrByPath).toHaveBeenCalledWith('press.unsupported_browser_version', { - onFail: logReturnValue, - args: { - browser: Cypress.browser.name, - version: Cypress.browser.majorVersion, - minimumVersion: 135, - }, - }) - }) - }) - describe('when automation throws', () => { it('throws via $errUtils, passing in the results from Cypress.log', async () => { const thrown = new Error('Some error') diff --git a/packages/server/lib/automation/commands/key_press.ts b/packages/server/lib/automation/commands/key_press.ts index 66a2511b783..6b749acc5e0 100644 --- a/packages/server/lib/automation/commands/key_press.ts +++ b/packages/server/lib/automation/commands/key_press.ts @@ -1,5 +1,5 @@ import type { Protocol } from 'devtools-protocol' -import type { KeyPressParams, KeyPressSupportedKeys } from '@packages/types' +import { NamedKeys, SupportedKey, SupportedNamedKey, toSupportedKey, isSupportedKey, SpaceKey } from '@packages/types' import type { SendDebuggerCommand } from '../../browsers/cdp_automation' import type { Client } from 'webdriver' import Debug from 'debug' @@ -9,40 +9,49 @@ import { AUT_FRAME_NAME_IDENTIFIER } from '../helpers/aut_identifier' const debug = Debug('cypress:server:automation:command:keypress') -interface KeyCodeLookup extends Record {} +// This type is not exported from webdriver, but we need it to type the .map call in the bidi implementation +type InputKeySourceAction = Parameters[0]['actions'][number] extends infer ActionParams + ? ActionParams extends { type: 'key', actions: infer Actions } + ? Actions extends Array ? Action : never + : never + : never -const invalidKeyErrorKind = 'InvalidKeyError' - -export class InvalidKeyError extends Error { - kind = invalidKeyErrorKind - constructor (key: string) { - super(`${key} is not supported by 'cy.press()'.`) +function getKeyParams (key: SupportedKey): { text?: string, key: string, code?: string } { + if (!isSupportedKey(key)) { + throw new Error(`Invalid key: ${key}`) } - static isInvalidKeyError (e: any): e is InvalidKeyError { - return e.kind === invalidKeyErrorKind + + if (key === SpaceKey) { + return { + text: ' ', + key: ' ', + } } -} -export function isSupportedKey (key: string): key is KeyPressSupportedKeys { - return CDP_KEYCODE[key] && BIDI_VALUE[key] -} + const isNamedKey = NamedKeys.includes(key) + + if (isNamedKey) { + return { + key, + code: key, + } + } -export const CDP_KEYCODE: KeyCodeLookup = { - 'Tab': 'U+000009', + return { + key, + text: key, + } } export async function cdpKeyPress ( - { key }: KeyPressParams, send: SendDebuggerCommand, + inKey: SupportedKey, + send: SendDebuggerCommand, contexts: Map, frameTree: Protocol.Page.FrameTree, ): Promise { - debug('cdp keypress', { key }) - if (!CDP_KEYCODE[key]) { - throw new InvalidKeyError(key) - } - - const keyIdentifier = CDP_KEYCODE[key] + const key = toSupportedKey(inKey) + debug('cdp keypress', { key, length: [...key].length }) const autFrame = frameTree.childFrames?.find(({ frame }) => { return frame.name?.includes(AUT_FRAME_NAME_IDENTIFIER) }) @@ -60,29 +69,33 @@ export async function cdpKeyPress ( } try { - await send('Input.dispatchKeyEvent', { - type: 'keyDown', - key, - code: key, - keyIdentifier, - }) - - await send('Input.dispatchKeyEvent', { - type: 'keyUp', - key, - code: key, - keyIdentifier, - }) + // Named keys must be dispatched as full strings, + // single-character keys must be dispatched as single characters, + // multi-codepoint characters must be dispatched as individual codepoints + const chars = NamedKeys.includes(key) ? [key] : [...key] + + for (const char of chars) { + const params = getKeyParams(toSupportedKey(char)) + + debug('dispatching keydown', params) + + await send('Input.dispatchKeyEvent', { + type: 'keyDown', + ...params, + }) + + debug('dispatching keyup', params) + await send('Input.dispatchKeyEvent', { + type: 'keyUp', + ...params, + }) + } } catch (e) { debug(e) throw e } } -export const BIDI_VALUE: KeyCodeLookup = { - 'Tab': '\uE004', -} - async function getActiveWindow (client: Client) { try { return await client.getWindowHandle() @@ -91,16 +104,33 @@ async function getActiveWindow (client: Client) { } } -export async function bidiKeyPress ({ key }: KeyPressParams, client: Client, autContext: string, idSuffix?: string): Promise { - const value = BIDI_VALUE[key] - - if (!value) { - throw new InvalidKeyError(key) - } +// While other browsers support the named keys, BiDi does not. +// We need to override the codepoints for the named keys to work. +export const BidiOverrideCodepoints: Record = { + 'ArrowDown': '\uE015', + 'ArrowLeft': '\uE012', + 'ArrowRight': '\uE014', + 'ArrowUp': '\uE013', + 'End': '\uE010', + 'Home': '\uE011', + 'PageDown': '\uE00F', + 'PageUp': '\uE00E', + 'Enter': '\uE007', + 'Tab': '\uE004', + 'Backspace': '\uE003', + 'Delete': '\uE017', + 'Insert': '\uE016', + 'Space': '\uE00D', +} +// any is fine to be used here because the key must be typeguarded before it can be used as a supported key +export async function bidiKeyPress (inKey: any, client: Client, autContext: string, idSuffix?: string): Promise { const activeWindow = await getActiveWindow(client) const { contexts: [{ context: topLevelContext }] } = await client.browsingContextGetTree({}) + debug('bidi keypress', { inKey, activeWindow, topLevelContext }) + const key = toSupportedKey(BidiOverrideCodepoints[inKey] ?? inKey) + // TODO: refactor for Cy15 https://github.com/cypress-io/cypress/issues/31480 if (activeWindow !== topLevelContext) { debug('Primary window is not currently active; attempting to activate') @@ -136,17 +166,30 @@ export async function bidiKeyPress ({ key }: KeyPressParams, client: Client, aut } try { + const chars = NamedKeys.includes(inKey) ? [key] : [...key] + + const actions = chars.map((value): InputKeySourceAction[] => { + return [ + { type: 'keyDown', value }, + { type: 'keyUp', value }, + ] + }) + .reduce((arr, el) => [...arr, ...el], []) + + debug('preparing to perform InputKeySourceActions:', { actions }) + await client.inputPerformActions({ context: autContext, actions: [{ type: 'key', - id: `${autContext}-${key}-${idSuffix || Date.now()}`, - actions: [ - { type: 'keyDown', value }, - { type: 'keyUp', value }, - ], + id: `${autContext}-${inKey}-${idSuffix || Date.now()}`, + actions, }], }) + + await client.inputReleaseActions({ + context: autContext, + }) } catch (e) { debug(e) const err = new Error(`Unable to perform key press command for '${key}' key: ${e?.message || 'Unknown Error Occurred'}. DEBUG namespace cypress:server:automation:command:keypress for more information.`) diff --git a/packages/server/lib/browsers/bidi_automation.ts b/packages/server/lib/browsers/bidi_automation.ts index 28fb1d0720f..1d1364b1d4d 100644 --- a/packages/server/lib/browsers/bidi_automation.ts +++ b/packages/server/lib/browsers/bidi_automation.ts @@ -9,7 +9,7 @@ import { AutomationNotImplemented } from '../automation/automation_not_implement import type Protocol from 'devtools-protocol' import type { Automation } from '../automation' import type { BrowserPreRequest, BrowserResponseReceived, ResourceType } from '@packages/proxy' -import type { AutomationMiddleware, AutomationCommands } from '@packages/types' +import { AutomationMiddleware, AutomationCommands, toSupportedKey } from '@packages/types' import type { Client as WebDriverClient } from 'webdriver' import type { NetworkBeforeRequestSentParameters, @@ -681,7 +681,8 @@ export class BidiAutomation { return case 'key:press': if (this.autContextId) { - await bidiKeyPress(data, this.webDriverClient, this.autContextId, this.topLevelContextId) + debug(`key:press %s`, data.key) + await bidiKeyPress(toSupportedKey(data.key), this.webDriverClient, this.autContextId, this.topLevelContextId) } else { throw new Error('Cannot emit key press: no AUT context initialized') } diff --git a/packages/server/lib/browsers/cdp_automation.ts b/packages/server/lib/browsers/cdp_automation.ts index 8f5189be81b..b97aec21461 100644 --- a/packages/server/lib/browsers/cdp_automation.ts +++ b/packages/server/lib/browsers/cdp_automation.ts @@ -16,6 +16,9 @@ import { cookieMatches, CyCookie, CyCookieFilter } from '../automation/util' import { DEFAULT_NETWORK_ENABLE_OPTIONS, CriClient } from './cri-client' import { AUT_FRAME_NAME_IDENTIFIER } from '../automation/helpers/aut_identifier' import { cdpKeyPress } from '../automation/commands/key_press' + +import { toSupportedKey } from '@packages/types' + import { cdpGetUrl } from '../automation/commands/get_url' import { cdpReloadFrame } from '../automation/commands/reload_frame' import { cdpNavigateHistory } from '../automation/commands/navigate_history' @@ -649,7 +652,7 @@ export class CdpAutomation implements CDPClient, AutomationMiddleware { case 'collect:garbage': return this.sendDebuggerCommandFn('HeapProfiler.collectGarbage') case 'key:press': - return cdpKeyPress(data, this.sendDebuggerCommandFn, this.executionContexts, (await this.send('Page.getFrameTree')).frameTree) + return cdpKeyPress(toSupportedKey(data.key), this.sendDebuggerCommandFn, this.executionContexts, (await this.send('Page.getFrameTree')).frameTree) case 'get:aut:url': return cdpGetUrl(this.sendDebuggerCommandFn, this.executionContexts, await this._getAutFrame()) case 'reload:aut:frame': diff --git a/packages/server/test/spec_helper.js b/packages/server/test/spec_helper.js index b2019b8e8a5..32332b4bed8 100644 --- a/packages/server/test/spec_helper.js +++ b/packages/server/test/spec_helper.js @@ -14,6 +14,7 @@ global.expect = chai.expect global.mockery = require('mockery') global.proxyquire = require('proxyquire') global.sinon = require('sinon') + const _ = require('lodash') const Promise = require('bluebird') const cache = require('../lib/cache').cache diff --git a/packages/server/test/unit/automation/commands/key_press.spec.ts b/packages/server/test/unit/automation/commands/key_press.spec.ts index 3b71235d9bd..d880f3f3703 100644 --- a/packages/server/test/unit/automation/commands/key_press.spec.ts +++ b/packages/server/test/unit/automation/commands/key_press.spec.ts @@ -1,8 +1,8 @@ import type Sinon from 'sinon' import type { expect as Expect } from 'chai' -import type { KeyPressSupportedKeys } from '@packages/types' +import { SupportedKey, NamedKeys, toSupportedKey, SpaceKey } from '@packages/types' import type { SendDebuggerCommand } from '../../../../lib/browsers/cdp_automation' -import { cdpKeyPress, bidiKeyPress, BIDI_VALUE, CDP_KEYCODE } from '../../../../lib/automation/commands/key_press' +import { cdpKeyPress, bidiKeyPress, BidiOverrideCodepoints } from '../../../../lib/automation/commands/key_press' import { Client as WebdriverClient } from 'webdriver' import type { Protocol } from 'devtools-protocol' const { expect, sinon }: { expect: typeof Expect, sinon: Sinon.SinonSandbox } = require('../../../spec_helper') @@ -16,6 +16,12 @@ type ClientReturn = WebdriverClient[T] extends never describe('key:press automation command', () => { + const tab: SupportedKey = toSupportedKey('Tab') + + function stubClientMethod (method: T) { + return sinon.stub, ClientReturn>() + } + describe('cdp', () => { let sendFn: Sinon.SinonStub, ReturnType> const topFrameId = 'abc' @@ -78,7 +84,7 @@ describe('key:press automation command', () => { }) it('focuses the frame and sends keydown and keyup', async () => { - await cdpKeyPress({ key: 'Tab' }, sendFn, executionContexts, frameTree) + await cdpKeyPress(tab, sendFn, executionContexts, frameTree) expect(sendFn).to.have.been.calledWith('Runtime.evaluate', { expression: 'window.focus()', contextId: autExecutionContext.id, @@ -86,16 +92,14 @@ describe('key:press automation command', () => { expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', { type: 'keyDown', - keyIdentifier: CDP_KEYCODE.Tab, - key: 'Tab', code: 'Tab', + key: 'Tab', }) expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', { type: 'keyUp', - keyIdentifier: CDP_KEYCODE.Tab, - key: 'Tab', code: 'Tab', + key: 'Tab', }) }) @@ -123,7 +127,7 @@ describe('key:press automation command', () => { let thrown: any = undefined try { - await cdpKeyPress({ key: 'Tab' }, sendFn, executionContexts, frameTree) + await cdpKeyPress(tab, sendFn, executionContexts, frameTree) } catch (e) { thrown = e } @@ -148,31 +152,109 @@ describe('key:press automation command', () => { }).resolves(topActiveElement) }) - it('dispaches a keydown followed by a keyup event to the provided send fn with the tab keycode', async () => { - await cdpKeyPress({ key: 'Tab' }, sendFn, executionContexts, frameTree) + it('dispatches a keydown followed by a keyup event to the provided send fn with the tab keycode', async () => { + await cdpKeyPress(tab, sendFn, executionContexts, frameTree) expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', { type: 'keyDown', - keyIdentifier: CDP_KEYCODE.Tab, key: 'Tab', code: 'Tab', }) expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', { type: 'keyUp', - keyIdentifier: CDP_KEYCODE.Tab, key: 'Tab', code: 'Tab', }) }) - }) - describe('when supplied an invalid key', () => { - it('errors', async () => { - // typescript would keep this from happening, but it hasn't yet - // been checked for correctness since being received by automation - // @ts-expect-error - await expect(cdpKeyPress({ key: 'foo' }, sendFn, executionContexts, frameTree)).to.be.rejectedWith('foo is not supported by \'cy.press()\'.') + describe('when supplied a valid named key', () => { + for (const key of NamedKeys.filter((k) => k !== SpaceKey)) { + it(`dispatches a keydown followed by a keyup event to the provided send fn with the ${key} keycode`, async () => { + await cdpKeyPress(key as SupportedKey, sendFn, executionContexts, frameTree) + + expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', { + type: 'keyDown', + key, + code: key, + }) + + expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', { + type: 'keyUp', + key, + code: key, + }) + }) + } + + it(`dispatches ' ' as text and key, with no code, when the named Space key is pressed`, async () => { + await cdpKeyPress(toSupportedKey(SpaceKey), sendFn, executionContexts, frameTree) + + expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', { + type: 'keyDown', + key: ' ', + text: ' ', + }) + + expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', { + type: 'keyUp', + key: ' ', + text: ' ', + }) + }) + }) + + describe('when supplied a valid character key', () => { + const key: SupportedKey = 'a' as SupportedKey + + it('adds text to the keydown event data', async () => { + await cdpKeyPress(key, sendFn, executionContexts, frameTree) + + expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', { + type: 'keyDown', + key, + text: key, + }) + }) + }) + + describe('when supplied a utf8 key', () => { + const codeOne = 'e' + const codeTwo = '́' + const value = 'é' + let key: SupportedKey + + beforeEach(() => { + key = toSupportedKey(value) + }) + + it('dispatches a keydown followed by a keyup event to the provided send fn with the a keycode', async () => { + await cdpKeyPress(key, sendFn, executionContexts, frameTree) + + expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', { + type: 'keyDown', + key: codeOne, + text: codeOne, + }) + + expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', { + type: 'keyUp', + key: codeOne, + text: codeOne, + }) + + expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', { + type: 'keyDown', + key: codeTwo, + text: codeTwo, + }) + + expect(sendFn).to.have.been.calledWith('Input.dispatchKeyEvent', { + type: 'keyUp', + key: codeTwo, + text: codeTwo, + }) + }) }) }) }) @@ -180,7 +262,7 @@ describe('key:press automation command', () => { describe('bidi', () => { let client: Sinon.SinonStubbedInstance let autContext: string - let key: KeyPressSupportedKeys + let key: SupportedKey const iframeElement = { 'element-6066-11e4-a52e-4f735466cecf': 'uuid-1', } @@ -190,23 +272,22 @@ describe('key:press automation command', () => { const topLevelContext = 'b7173d71-c76c-41ec-beff-25a72f7cae13' beforeEach(() => { - // can't create a sinon stubbed instance because webdriver doesn't export the constructor. Because it's known that - // bidiKeypress only invokes inputPerformActions, and inputPerformActions is properly typed, this is okay. - // @ts-expect-error + const stubbedClientMethods: (keyof WebdriverClient)[] = ['inputPerformActions', 'inputReleaseActions', 'getActiveElement', 'findElement', 'scriptEvaluate', 'getWindowHandle', 'switchToWindow', 'browsingContextGetTree'] + + // @ts-expect-error - webdriver doesn't export the constructor client = { - inputPerformActions: sinon.stub, ClientReturn<'inputPerformActions'>>(), - getActiveElement: sinon.stub, ClientReturn<'getActiveElement'>>(), - findElement: sinon.stub, ClientReturn<'findElement'>>(), - scriptEvaluate: sinon.stub, ClientReturn<'scriptEvaluate'>>(), - getWindowHandle: sinon.stub, ClientReturn<'getWindowHandle'>>(), - switchToWindow: sinon.stub, ClientReturn<'switchToWindow'>>().resolves(), - browsingContextGetTree: sinon.stub, ClientReturn<'browsingContextGetTree'>>(), + ...stubbedClientMethods.reduce((acc, method) => { + acc[method] = stubClientMethod(method) + + return acc + }, {} as Record, ClientReturn>>), } autContext = 'someContextId' - key = 'Tab' + key = toSupportedKey('Tab') + client.switchToWindow.resolves() client.inputPerformActions.resolves() client.browsingContextGetTree.resolves({ contexts: [ @@ -215,6 +296,8 @@ describe('key:press automation command', () => { children: [], url: 'someUrl', userContext: 'userContext', + clientWindow: 'clientWindow', + originalOpener: 'originalOpener', }, ], }) @@ -223,30 +306,36 @@ describe('key:press automation command', () => { describe('when the aut iframe is not in focus', () => { beforeEach(() => { client.getWindowHandle.resolves(topLevelContext) - client.findElement.withArgs('css selector ', 'iframe.aut-iframe').resolves(iframeElement) + client.findElement.withArgs('css selector', 'iframe.aut-iframe').resolves(iframeElement) // @ts-expect-error - webdriver types show this returning a string, but it actually returns an ElementReference, same as findElement client.getActiveElement.resolves(otherElement) }) - it('focuses the frame before dispatching keydown and keyup', async () => { - await bidiKeyPress({ key }, client as WebdriverClient, autContext, 'idSuffix') + it('focuses the frame before dispatching keydown and keyup, and then releases the input actions', async () => { + await bidiKeyPress(key, client, autContext, 'idSuffix') expect(client.scriptEvaluate).to.have.been.calledWith({ expression: 'window.focus()', target: { context: autContext }, awaitPromise: false, }) + const expectedValue = BidiOverrideCodepoints[key] ?? key + expect(client.inputPerformActions.firstCall.args[0]).to.deep.equal({ context: autContext, actions: [{ type: 'key', id: 'someContextId-Tab-idSuffix', actions: [ - { type: 'keyDown', value: BIDI_VALUE[key] }, - { type: 'keyUp', value: BIDI_VALUE[key] }, + { type: 'keyDown', value: expectedValue }, + { type: 'keyUp', value: expectedValue }, ], }], }) + + expect(client.inputReleaseActions).to.have.been.calledWith({ + context: autContext, + }) }) }) @@ -256,7 +345,7 @@ describe('key:press automation command', () => { }) it('activates the top level context window', async () => { - await bidiKeyPress({ key }, client as WebdriverClient, autContext, 'idSuffix') + await bidiKeyPress(key, client, autContext, 'idSuffix') expect(client.switchToWindow).to.have.been.calledWith(topLevelContext) }) }) @@ -267,7 +356,7 @@ describe('key:press automation command', () => { }) it('does not activate the top level context window', async () => { - await bidiKeyPress({ key }, client as WebdriverClient, autContext, 'idSuffix') + await bidiKeyPress(key, client, autContext, 'idSuffix') expect(client.switchToWindow).not.to.have.been.called }) }) @@ -278,28 +367,68 @@ describe('key:press automation command', () => { }) it('activates the top level context window', async () => { - await bidiKeyPress({ key }, client as WebdriverClient, autContext, 'idSuffix') + await bidiKeyPress(key, client, autContext, 'idSuffix') expect(client.switchToWindow).to.have.been.calledWith(topLevelContext) }) }) - it('calls client.inputPerformActions with a keydown and keyup action', async () => { - client.getWindowHandle.resolves(topLevelContext) - client.findElement.withArgs('css selector ', 'iframe.aut-iframe').resolves(iframeElement) - // @ts-expect-error - webdriver types show this returning a string, but it actually returns an ElementReference, same as findElement - client.getActiveElement.resolves(iframeElement) - await bidiKeyPress({ key }, client as WebdriverClient, autContext, 'idSuffix') - - expect(client.inputPerformActions.firstCall.args[0]).to.deep.equal({ - context: autContext, - actions: [{ - type: 'key', - id: 'someContextId-Tab-idSuffix', - actions: [ - { type: 'keyDown', value: BIDI_VALUE[key] }, - { type: 'keyUp', value: BIDI_VALUE[key] }, - ], - }], + describe('when supplied an overridden codepoint', () => { + beforeEach(() => { + client.findElement.withArgs('css selector', 'iframe.aut-iframe').resolves(iframeElement) + // @ts-expect-error - webdriver types show this returning a string, but it actually returns an ElementReference, same as findElement + client.getActiveElement.resolves(iframeElement) + }) + + for (const [key, value] of Object.entries(BidiOverrideCodepoints) as [SupportedKey, string][]) { + // special handling to render the source unicode instead of the rendered unicode + it(`dispatches a keydown and keyup action with the value '\\u${value.charCodeAt(0).toString(16).toUpperCase()}' for key '${key}'`, async () => { + await bidiKeyPress(key, client, autContext, 'idSuffix') + + expect(client.inputPerformActions.firstCall.args[0]).to.deep.equal({ + context: autContext, + actions: [{ + type: 'key', + id: `someContextId-${key}-idSuffix`, + actions: [ + { type: 'keyDown', value }, + { type: 'keyUp', value }, // in some browsers, F6 will cause the frame to lose focus, so the keyup will not be triggered + ], + }], + }) + + expect(client.inputReleaseActions).to.have.been.calledWith({ + context: autContext, + }) + }) + } + }) + + describe('when supplied a multi-codepointutf8 key', () => { + const codeOne = 'e' + const codeTwo = '́' + const value = 'é' + let key: SupportedKey + + beforeEach(() => { + key = toSupportedKey(value) + }) + + it('dispatches one keydown followed by a keyup event for each codepoint', async () => { + await bidiKeyPress(key, client, autContext, 'idSuffix') + + expect(client.inputPerformActions.firstCall.args[0]).to.deep.equal({ + context: autContext, + actions: [{ + type: 'key', + id: `someContextId-${key}-idSuffix`, + actions: [ + { type: 'keyDown', value: codeOne }, + { type: 'keyUp', value: codeOne }, + { type: 'keyDown', value: codeTwo }, + { type: 'keyUp', value: codeTwo }, + ], + }], + }) }) }) }) diff --git a/packages/types/package.json b/packages/types/package.json index 27f6d59d23d..ee3a5a00231 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -8,7 +8,8 @@ "build-prod": "tsc || echo 'built, with type errors'", "check-ts": "tsc --noEmit", "clean": "rimraf src/*.js src/**/*.js", - "lint": "eslint --ext .js,.jsx,.ts,.tsx,.json, ." + "lint": "eslint --ext .js,.jsx,.ts,.tsx,.json, .", + "test": "vitest run" }, "dependencies": { "semver": "^7.7.1" @@ -20,7 +21,8 @@ "devtools-protocol": "0.0.1459876", "express": "4.21.0", "socket.io": "4.0.1", - "typescript": "~5.4.5" + "typescript": "~5.4.5", + "vitest": "2.1.8" }, "files": [ "src/*" diff --git a/packages/types/src/automation.ts b/packages/types/src/automation.ts index 77c7e050de1..2def55be531 100644 --- a/packages/types/src/automation.ts +++ b/packages/types/src/automation.ts @@ -1 +1,103 @@ +/// + export type AutomationElementId = `${string}-string` + +const invalidKeyErrorKind = 'InvalidKeyError' + +export type SupportedNamedKey = Cypress.SupportedNamedKey + +export const SpaceKey = 'Space' + +/** + * Array of all supported named keys that can be used with cy.press(). + * These are special keys that have specific meanings beyond single characters. + */ +export const NamedKeys: SupportedNamedKey[] = [ + 'ArrowDown', + 'ArrowLeft', + 'ArrowRight', + 'ArrowUp', + 'End', + 'Home', + 'PageDown', + 'PageUp', + 'Enter', + 'Tab', + 'Backspace', + SpaceKey, + 'Delete', + 'Insert', +] + +// utility type to enable the SupportedKey union type +enum SupportedKeyType {} + +/** + * Union type representing all keys supported by cy.press(). + * Includes single-character strings (including unicode characters with multiple code points) + * and named utility keys. + * Must be cast to via `toSupportedKey` or guarded with `isSupportedKey` + * to ensure it is a valid key. + */ +export type SupportedKey = SupportedKeyType & string + +function isSingleDigitNumber (key: number | string): boolean { + return typeof key === 'number' && key === Math.floor(key) && key >= 0 && key <= 9 +} + +/** + * Type guard that checks if a string is a supported key for cy.press(). + * @param key The string to check + * @returns True if the key is supported (single character or named key) + */ +export function isSupportedKey (key: string | number): key is SupportedKey { + if (isSingleDigitNumber(key)) { + return isSupportedKey(String(key)) + } + + if (!(typeof key === 'string')) { + return false + } + + // Normalize the string to combine combining characters + const normalizedKey = key.normalize('NFC') + + return ( + // Check if it's a single grapheme cluster (user-perceived character) + // This handles multi-codepoint characters like emoji with modifiers + [...normalizedKey].length === 1 || + NamedKeys.includes(key as SupportedNamedKey) + ) +} + +/** + * Error thrown when an unsupported key is used with cy.press(). + * Provides information about which keys are supported. + */ +export class InvalidKeyError extends Error { + kind = invalidKeyErrorKind + constructor (key: string) { + super(`${key} is not supported by 'cy.press()'. Single-character keys are supported, as well as a selection of utility keys: ${NamedKeys.join(', ')}`) + } + static isInvalidKeyError (e: any): e is InvalidKeyError { + return e.kind === invalidKeyErrorKind + } +} + +/** + * Converts a string to a SupportedKey, throwing an error if invalid. + * @param key The string key to validate and convert + * @returns The validated SupportedKey + * @throws InvalidKeyError when the key is not supported + */ +export function toSupportedKey (key: string | number): SupportedKey { + if (typeof key === 'number' && key >= 0 && key <= 9) { + return toSupportedKey(String(key)) + } + + if (isSupportedKey(key)) { + return key + } + + throw new InvalidKeyError(String(key)) +} diff --git a/packages/types/src/server.ts b/packages/types/src/server.ts index 3bcdbb39d0d..796b47827d3 100644 --- a/packages/types/src/server.ts +++ b/packages/types/src/server.ts @@ -4,6 +4,7 @@ import type { PlatformName } from './platform' import type { RunModeVideoApi } from './video' import type { ProtocolManagerShape } from './protocol' import type Protocol from 'devtools-protocol' +import type { SupportedKey } from './automation' export type OpenProjectLaunchOpts = { projectRoot: string @@ -57,16 +58,12 @@ export interface LaunchArgs { } type NullableMiddlewareHook = ((message: unknown, data: unknown) => void) | null - -export type KeyPressSupportedKeys = 'Tab' - interface CommandSignature

{ dataType: P returnType: R } - export interface KeyPressParams { - key: KeyPressSupportedKeys + key: SupportedKey } export interface AutomationCommands { diff --git a/packages/types/test/automation.spec.ts b/packages/types/test/automation.spec.ts new file mode 100644 index 00000000000..8daca40370d --- /dev/null +++ b/packages/types/test/automation.spec.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from 'vitest' +import { isSupportedKey, toSupportedKey, NamedKeys } from '../src/automation' + +describe('automation', () => { + const supportedKeys = [...NamedKeys, 'a', 'b', 'c', 'd'] + const unsupportedKeys = [10, 'some random string', -1, null, undefined, true, false, Symbol('some symbol'), new Date(), new Error(), new Function(), new RegExp(/./), new Set(), new Map(), new WeakMap(), new WeakSet()] + + describe('toSupportedKey', () => { + describe('supported keys', () => { + for (const key of supportedKeys) { + it(`returns ${key} for ${key}`, () => { + expect(toSupportedKey(key)).toBe(key) + }) + } + + it('returns a string for a single digit number', () => { + expect(toSupportedKey(2)).toBe('2') + }) + }) + + describe('unsupported keys', () => { + for (const key of unsupportedKeys) { + it(`throws an error for ${String(key)}`, () => { + // @ts-expect-error key is not a string + expect(() => toSupportedKey(key)).toThrow() + }) + } + }) + }) + + describe('isSupportedKey', () => { + it('should return true for supported keys', () => { + supportedKeys.forEach((key) => { + expect(isSupportedKey(key)).toBe(true) + }) + }) + + it('returns false for unsupported keys', () => { + unsupportedKeys.forEach((key) => { + // @ts-expect-error key is not a string + expect(isSupportedKey(key)).toBe(false) + }) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 19ee10d23fc..96e83001e37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9088,6 +9088,16 @@ test-exclude "^7.0.1" tinyrainbow "^2.0.0" +"@vitest/expect@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.8.tgz#13fad0e8d5a0bf0feb675dcf1d1f1a36a1773bc1" + integrity sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw== + dependencies: + "@vitest/spy" "2.1.8" + "@vitest/utils" "2.1.8" + chai "^5.1.2" + tinyrainbow "^1.2.0" + "@vitest/expect@2.1.9": version "2.1.9" resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.9.tgz#b566ea20d58ea6578d8dc37040d6c1a47ebe5ff8" @@ -9109,6 +9119,15 @@ chai "^5.2.0" tinyrainbow "^2.0.0" +"@vitest/mocker@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.8.tgz#51dec42ac244e949d20009249e033e274e323f73" + integrity sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA== + dependencies: + "@vitest/spy" "2.1.8" + estree-walker "^3.0.3" + magic-string "^0.30.12" + "@vitest/mocker@2.1.9": version "2.1.9" resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.9.tgz#36243b27351ca8f4d0bbc4ef91594ffd2dc25ef5" @@ -9127,7 +9146,14 @@ estree-walker "^3.0.3" magic-string "^0.30.17" -"@vitest/pretty-format@2.1.9", "@vitest/pretty-format@^2.1.9": +"@vitest/pretty-format@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.8.tgz#88f47726e5d0cf4ba873d50c135b02e4395e2bca" + integrity sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ== + dependencies: + tinyrainbow "^1.2.0" + +"@vitest/pretty-format@2.1.9", "@vitest/pretty-format@^2.1.8", "@vitest/pretty-format@^2.1.9": version "2.1.9" resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.9.tgz#434ff2f7611689f9ce70cd7d567eceb883653fdf" integrity sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ== @@ -9141,6 +9167,14 @@ dependencies: tinyrainbow "^2.0.0" +"@vitest/runner@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.8.tgz#b0e2dd29ca49c25e9323ea2a45a5125d8729759f" + integrity sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg== + dependencies: + "@vitest/utils" "2.1.8" + pathe "^1.1.2" + "@vitest/runner@2.1.9": version "2.1.9" resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.9.tgz#cc18148d2d797fd1fd5908d1f1851d01459be2f6" @@ -9158,6 +9192,15 @@ pathe "^2.0.3" strip-literal "^3.0.0" +"@vitest/snapshot@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.8.tgz#d5dc204f4b95dc8b5e468b455dfc99000047d2de" + integrity sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg== + dependencies: + "@vitest/pretty-format" "2.1.8" + magic-string "^0.30.12" + pathe "^1.1.2" + "@vitest/snapshot@2.1.9": version "2.1.9" resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.9.tgz#24260b93f798afb102e2dcbd7e61c6dfa118df91" @@ -9176,6 +9219,13 @@ magic-string "^0.30.17" pathe "^2.0.3" +"@vitest/spy@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.8.tgz#bc41af3e1e6a41ae3b67e51f09724136b88fa447" + integrity sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg== + dependencies: + tinyspy "^3.0.2" + "@vitest/spy@2.1.9": version "2.1.9" resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.9.tgz#cb28538c5039d09818b8bfa8edb4043c94727c60" @@ -9190,6 +9240,15 @@ dependencies: tinyspy "^4.0.3" +"@vitest/utils@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.8.tgz#f8ef85525f3362ebd37fd25d268745108d6ae388" + integrity sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA== + dependencies: + "@vitest/pretty-format" "2.1.8" + loupe "^3.1.2" + tinyrainbow "^1.2.0" + "@vitest/utils@2.1.9": version "2.1.9" resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.9.tgz#4f2486de8a54acf7ecbf2c5c24ad7994a680a6c1" @@ -32060,6 +32119,17 @@ vinyl@^3.0.0: optionalDependencies: fsevents "~2.3.3" +vite-node@2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.8.tgz#9495ca17652f6f7f95ca7c4b568a235e0c8dbac5" + integrity sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg== + dependencies: + cac "^6.7.14" + debug "^4.3.7" + es-module-lexer "^1.5.4" + pathe "^1.1.2" + vite "^5.0.0" + vite-node@2.1.9: version "2.1.9" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.9.tgz#549710f76a643f1c39ef34bdb5493a944e4f895f" @@ -32191,6 +32261,32 @@ vite@^5.0.0: optionalDependencies: fsevents "~2.3.3" +vitest@2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.8.tgz#2e6a00bc24833574d535c96d6602fb64163092fa" + integrity sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ== + dependencies: + "@vitest/expect" "2.1.8" + "@vitest/mocker" "2.1.8" + "@vitest/pretty-format" "^2.1.8" + "@vitest/runner" "2.1.8" + "@vitest/snapshot" "2.1.8" + "@vitest/spy" "2.1.8" + "@vitest/utils" "2.1.8" + chai "^5.1.2" + debug "^4.3.7" + expect-type "^1.1.0" + magic-string "^0.30.12" + pathe "^1.1.2" + std-env "^3.8.0" + tinybench "^2.9.0" + tinyexec "^0.3.1" + tinypool "^1.0.1" + tinyrainbow "^1.2.0" + vite "^5.0.0" + vite-node "2.1.8" + why-is-node-running "^2.3.0" + vitest@2.1.9, vitest@^2.1.9: version "2.1.9" resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.9.tgz#7d01ffd07a553a51c87170b5e80fea3da7fb41e7"