diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 1bfec196443..b6a5dba93b9 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -12,6 +12,7 @@ _Released 07/15/2025 (PENDING)_ - `@cypress/webpack-dev-server` no longer supports `webpack-dev-server` version 4. Addresses [#31605](https://github.com/cypress-io/cypress/issues/31605). If you still need to use `webpack-dev-server` version 4, please see our [migration guide](https://docs.cypress.io/app/references/migration-guide#Migrating-to-Cypress-150). - In order to better align with best practices, `@cypress/webpack-batteries-included-preprocessor` no longer includes certain browser built-ins that were automatically provided by Webpack 4. The removed built-ins are `assert`, `constants`, `crypto`, `domain`, `events`, `http`, `https`, `punycode`, `querystring`, `string_decoder`, `sys`, `timers`, `tty`, `url`, `util`, `vm`, and `zlib`. However, we know that certain built-ins are popular, given that many users have files that are shared between their Cypress tests and node context. Because of this, `@cypress/webpack-batteries-included-preprocessor` will ship with built-in support for `buffer`, `path`, `process`, `os`, and `stream`. If there is a built-in that isn't supported be default and you need to add support, please refer to the Webpack [resolve.fallback](https://webpack.js.org/configuration/resolve/#resolvefallback) documentation and the [`@cypress/webpack-batteries-included-preprocessor` README](../npm/webpack-batteries-included-preprocessor/README.md). Addresses [#31039](https://github.com/cypress-io/cypress/issues/31039). - The application under test's `pagehide` event in Chromium browsers will no longer trigger Cypress's `window:unload` event. Addressed in [#31853](https://github.com/cypress-io/cypress/pull/31853). +- The `Cypress.SelectorPlayground` API has been renamed to `Cypress.ElementSelector`. This API was renamed to accommodate its use for defining `selectorPriority` in Cypress Studio and our future [`cy.prompt` release](https://on.cypress.io/cy-prompt-early-access?utm_source=docs&utm_medium=app-changelog&utm_content=cy-prompt-release). Addresses [#31801](https://github.com/cypress-io/cypress/issues/31801). Addressed in [#31889](https://github.com/cypress-io/cypress/pull/31889). - **Component Testing breaking changes:** - Removed support for Angular 17. The minimum supported version is now `18.0.0`. Addresses [#31303](https://github.com/cypress-io/cypress/issues/31303). - `@cypress/angular` now requires a minimum of `zone.js` `0.14.0`. Addresses [#31582](https://github.com/cypress-io/cypress/issues/31582). @@ -20,6 +21,7 @@ _Released 07/15/2025 (PENDING)_ **Features:** - [`cy.url()`](https://docs.cypress.io/api/commands/url), [`cy.hash()`](https://docs.cypress.io/api/commands/hash), [`cy.go()`](https://docs.cypress.io/api/commands/go), [`cy.reload()`](https://docs.cypress.io/api/commands/reload), [`cy.title()`](https://docs.cypress.io/api/commands/title), and [`cy.location()`](https://docs.cypress.io/api/commands/location) now use the automation client (CDP for Chromium browsers and WebDriver BiDi for Firefox) to return the appropriate values from the commands to the user instead of the window object. This is to avoid cross origin issues with [`cy.origin()`](https://docs.cypress.io/api/commands/origin) so these commands can be invoked anywhere inside a Cypress test without having to worry about origin access issues. Experimental Webkit still will use the window object to retrieve these values. Also, [`cy.window()`](https://docs.cypress.io/api/commands/window) will always return the current window object, regardless of origin restrictions. Not every property from the window object will be accessible depending on the origin context. Addresses [#31196](https://github.com/cypress-io/cypress/issues/31196). +- Selectors accepted in the `selectorPriority` of the `SelectorPlayground` (renamed to `ElementSelector`) API have been expanded to accept `name` and `attributes:*`. Additionally, the default selector priority used by Cypress now includes `name`. Addresses [#31801](https://github.com/cypress-io/cypress/issues/30309) and [#6876](https://github.com/cypress-io/cypress/issues/6876). Addressed in [#31889](https://github.com/cypress-io/cypress/pull/31889). - [`tsx`](https://tsx.is/) is now used in all cases to run the Cypress config, replacing [ts-node](https://github.com/TypeStrong/ts-node) for TypeScript and Node for commonjs/ESM. This should allow for more interoperability for users who are using any variant of ES Modules. Addresses [#8090](https://github.com/cypress-io/cypress/issues/8090), [#15724](https://github.com/cypress-io/cypress/issues/15724), [#21805](https://github.com/cypress-io/cypress/issues/21805), [#22273](https://github.com/cypress-io/cypress/issues/22273), [#22747](https://github.com/cypress-io/cypress/issues/22747), [#23141](https://github.com/cypress-io/cypress/issues/23141), [#25958](https://github.com/cypress-io/cypress/issues/25958), [#25959](https://github.com/cypress-io/cypress/issues/25959), [#26606](https://github.com/cypress-io/cypress/issues/26606), [#27359](https://github.com/cypress-io/cypress/issues/27359), [#27450](https://github.com/cypress-io/cypress/issues/27450), [#28442](https://github.com/cypress-io/cypress/issues/28442), [#30318](https://github.com/cypress-io/cypress/issues/30318), [#30718](https://github.com/cypress-io/cypress/issues/30718), [#30907](https://github.com/cypress-io/cypress/issues/30907), [#30915](https://github.com/cypress-io/cypress/issues/30915), [#30925](https://github.com/cypress-io/cypress/issues/30925), [#30954](https://github.com/cypress-io/cypress/issues/30954) and [#31185](https://github.com/cypress-io/cypress/issues/31185). **Misc:** diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 11c8df64e6c..833f2a24d00 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -696,10 +696,21 @@ declare namespace Cypress { } /** - * @see https://on.cypress.io/selector-playground-api + * Element selector logic used for generating selectors for elements + * in the Selector Playground and Cypress Studio. + * @see https://on.cypress.io/element-selector-api + */ + ElementSelector: { + defaults(options: Partial): void + getSelector($el: JQuery): JQuery.Selector + } + + /** + * @deprecated Use ElementSelector instead + * @see https://on.cypress.io/element-selector-api */ SelectorPlayground: { - defaults(options: Partial): void + defaults(options: Partial): void getSelector($el: JQuery): JQuery.Selector } @@ -2971,7 +2982,7 @@ declare namespace Cypress { */ requestTimeout: number /** - * Time, in milliseconds, to wait until a response in a [cy.request()](https://on.cypress.io/request), [cy.wait()](https://on.cypress.io/wait), [cy.fixture()](https://on.cypress.io/fixture), [cy.getCookie()](https://on.cypress.io/getcookie), [cy.getCookies()](https://on.cypress.io/getcookies), [cy.setCookie()](https://on.cypress.io/setcookie), [cy.clearCookie()](https://on.cypress.io/clearcookie), [cy.clearCookies()](https://on.cypress.io/clearcookies), and [cy.screenshot()](https://on.cypress.io/screenshot) commands + * Time, in milliseconds, to wait for a response in a [cy.request()](https://on.cypress.io/request), [cy.wait()](https://on.cypress.io/wait), [cy.fixture()](https://on.cypress.io/fixture), [cy.getCookie()](https://on.cypress.io/getcookie), [cy.getCookies()](https://on.cypress.io/getcookies), [cy.setCookie()](https://on.cypress.io/setcookie), [cy.clearCookie()](https://on.cypress.io/clearcookie), [cy.clearCookies()](https://on.cypress.io/clearcookies), and [cy.screenshot()](https://on.cypress.io/screenshot) commands * @default 30000 */ responseTimeout: number @@ -3714,9 +3725,18 @@ declare namespace Cypress { screenshotOnRunFailure: boolean } - interface SelectorPlaygroundDefaultsOptions { - selectorPriority: string[] - onElement: ($el: JQuery) => string | null | undefined + type SelectorPriority = + | `attribute:${string}` + | 'attributes' + | 'class' + | `data-${string}` + | 'id' + | 'name' + | 'nth-child' + | 'tag' + + interface ElementSelectorDefaultsOptions { + selectorPriority?: SelectorPriority[] } interface ScrollToOptions extends Loggable, Timeoutable { diff --git a/packages/app/src/runner/aut-iframe.ts b/packages/app/src/runner/aut-iframe.ts index 9681dea289c..5548802fa5c 100644 --- a/packages/app/src/runner/aut-iframe.ts +++ b/packages/app/src/runner/aut-iframe.ts @@ -385,7 +385,7 @@ export class AutIframe { const Cypress = this.eventManager.getCypress() - const selector = Cypress.SelectorPlayground.getSelector($el) + const selector = Cypress.ElementSelector.getSelector($el) const selectorPlaygroundStore = useSelectorPlaygroundStore() this._addOrUpdateSelectorPlaygroundHighlight({ diff --git a/packages/app/src/store/studio-store.ts b/packages/app/src/store/studio-store.ts index fb57c0f7047..3f90c33c759 100644 --- a/packages/app/src/store/studio-store.ts +++ b/packages/app/src/store/studio-store.ts @@ -383,7 +383,7 @@ export const useStudioStore = defineStore('studioRecorder', { if (name === 'click' && this._matchPreviousMouseEvent($el)) { selector = this._previousMouseEvent?.selector } else { - selector = getCypress().SelectorPlayground.getSelector($el) + selector = getCypress().ElementSelector.getSelector($el) } this._clearPreviousMouseEvent() @@ -440,7 +440,7 @@ export const useStudioStore = defineStore('studioRecorder', { _addAssertion ($el: HTMLElement | JQuery, ...args: AssertionArgs) { const id = this._getId() - const selector = getCypress().SelectorPlayground.getSelector($el) + const selector = getCypress().ElementSelector.getSelector($el) const log: StudioLog = { id, @@ -666,7 +666,7 @@ export const useStudioStore = defineStore('studioRecorder', { if (!this._matchPreviousMouseEvent(target)) { this._previousMouseEvent = { element: target, - selector: getCypress().SelectorPlayground.getSelector(window.UnifiedRunner.CypressJQuery(target)), + selector: getCypress().ElementSelector.getSelector(window.UnifiedRunner.CypressJQuery(target)), } } }, diff --git a/packages/driver/cypress/e2e/cypress/element_selector.cy.ts b/packages/driver/cypress/e2e/cypress/element_selector.cy.ts new file mode 100644 index 00000000000..b0bf92b0741 --- /dev/null +++ b/packages/driver/cypress/e2e/cypress/element_selector.cy.ts @@ -0,0 +1,150 @@ +/// +import type { ElementSelectorAPI } from '../../../src/cypress/element_selector' +import { DEFAULT_SELECTOR_PRIORITIES } from '../../../src/cypress/element_selector' +const { $: $cypress } = Cypress.$Cypress +const ElementSelector = Cypress.ElementSelector as ElementSelectorAPI + +const SELECTOR_DEFAULTS: Cypress.SelectorPriority[] = [...DEFAULT_SELECTOR_PRIORITIES] + +describe('src/cypress/element_selector', () => { + beforeEach(() => { + ElementSelector.reset() + }) + + it('has defaults', () => { + expect(ElementSelector.getSelectorPriority()).to.deep.eq(SELECTOR_DEFAULTS) + }) + + context('.defaults', () => { + it('is noop if not called with selectorPriority', () => { + ElementSelector.defaults({}) + expect(ElementSelector.getSelectorPriority()).to.deep.eq(SELECTOR_DEFAULTS) + }) + + it('sets element:selector:priority if selectorPriority specified', () => { + const selectorPriority: Cypress.SelectorPriority[] = [ + 'data-1', + 'data-2', + 'id', + 'class', + 'tag', + 'attributes', + 'nth-child', + 'name', + 'attribute:aria-label', + 'attribute:aria-labelledby', + ] + + ElementSelector.defaults({ + selectorPriority, + }) + + expect(ElementSelector.getSelectorPriority()).to.eql(selectorPriority) + }) + + it('throws if not passed an object', () => { + const fn = () => { + ElementSelector.defaults(undefined as any) + } + + expect(fn).to.throw() + .with.property('message') + .and.include('`Cypress.ElementSelector.defaults()` must be called with an object. You passed: ') + + expect(fn).to.throw() + .with.property('docsUrl') + .and.include('https://on.cypress.io/element-selector-api') + }) + + it('throws if selectorPriority is not an array', () => { + const fn = () => { + ElementSelector.defaults({ selectorPriority: 'foo' as any }) + } + + expect(fn).to.throw() + .with.property('message') + .and.include('`Cypress.ElementSelector.defaults()` called with invalid `selectorPriority` property. It must be an array. You passed: `foo`') + + expect(fn).to.throw() + .with.property('docsUrl') + .and.include('https://on.cypress.io/element-selector-api') + }) + + it('throws if selectorPriority contains an unsupported priority', () => { + const fn = () => { + ElementSelector.defaults({ + selectorPriority: [ + 'id', + // @ts-expect-error - invalid priority + 'attr', + ], + }) + } + + expect(fn).to.throw() + .with.property('message') + .and.include('`Cypress.ElementSelector.defaults()` called with invalid `selectorPriority` property. It must be one of: `data-*`, `attribute:*`, `id`, `class`, `tag`, `name`,`attributes`, or `nth-child`. You passed: `attr`') + }) + + it('throws if selectorPriority has an unsupported priority that contains a substring of a valid priority', () => { + const fn = () => { + ElementSelector.defaults({ + selectorPriority: [ + // @ts-expect-error - invalid priority + 'idIsNotValid', + ], + }) + } + + expect(fn).to.throw() + .with.property('message') + .and.include('`Cypress.ElementSelector.defaults()` called with invalid `selectorPriority` property. It must be one of: `data-*`, `attribute:*`, `id`, `class`, `tag`, `name`,`attributes`, or `nth-child`. You passed: `idIsNotValid`') + }) + }) + + context('.getSelector', () => { + it('uses defaults.selectorPriority', () => { + const $div = $cypress('
') + + Cypress.$('body').append($div) + + expect(ElementSelector.getSelector($div)).to.eq('[data-cy="main button 123"]') + + ElementSelector.defaults({ + selectorPriority: ['data-foo'], + }) + + expect(ElementSelector.getSelector($div)).to.eq('[data-foo="bar"]') + }) + }) + + describe('Cypress.SelectorPlayground (renamed)', () => { + it('throws error when calling defaults()', () => { + const fn = () => { + Cypress.SelectorPlayground.defaults({}) + } + + expect(fn).to.throw() + .with.property('message') + .and.include('`Cypress.SelectorPlayground.defaults()` has been renamed to `Cypress.ElementSelector.defaults()`') + + expect(fn).to.throw() + .with.property('message') + .and.include('Please update your code to use `Cypress.ElementSelector` instead') + }) + + it('throws error when calling getSelector()', () => { + const fn = () => { + Cypress.SelectorPlayground.getSelector($cypress('body')) + } + + expect(fn).to.throw() + .with.property('message') + .and.include('`Cypress.SelectorPlayground.getSelector()` has been renamed to `Cypress.ElementSelector.getSelector()`') + + expect(fn).to.throw() + .with.property('message') + .and.include('Please update your code to use `Cypress.ElementSelector` instead') + }) + }) +}) diff --git a/packages/driver/cypress/e2e/cypress/selector_playground.cy.js b/packages/driver/cypress/e2e/cypress/selector_playground.cy.js deleted file mode 100644 index ff938873647..00000000000 --- a/packages/driver/cypress/e2e/cypress/selector_playground.cy.js +++ /dev/null @@ -1,154 +0,0 @@ -const { $ } = window.Cypress.$Cypress -const SelectorPlayground = Cypress.SelectorPlayground - -const SELECTOR_DEFAULTS = [ - 'data-cy', 'data-test', 'data-testid', 'data-qa', 'id', 'class', 'tag', 'attributes', 'nth-child', -] - -describe('src/cypress/selector_playground', () => { - beforeEach(() => { - SelectorPlayground.reset() - }) - - it('has defaults', () => { - expect(SelectorPlayground.getSelectorPriority()).to.deep.eq(SELECTOR_DEFAULTS) - expect(SelectorPlayground.getOnElement()).to.be.null - }) - - context('.defaults', () => { - it('is noop if not called with selectorPriority or onElement', () => { - SelectorPlayground.defaults({}) - expect(SelectorPlayground.getSelectorPriority()).to.deep.eq(SELECTOR_DEFAULTS) - expect(SelectorPlayground.getOnElement()).to.be.null - }) - - it('sets selector:playground:priority if selectorPriority specified', () => { - const selectorPriority = [ - 'data-1', - 'data-2', - 'id', - 'class', - 'tag', - 'attributes', - 'nth-child', - ] - - SelectorPlayground.defaults({ - selectorPriority, - }) - - expect(SelectorPlayground.getSelectorPriority()).to.eql(selectorPriority) - }) - - it('throws if selectorPriority contains an unsupported priority', () => { - const fn = () => { - SelectorPlayground.defaults({ - selectorPriority: [ - 'id', - 'name', - ], - }) - } - - expect(fn).to.throw() - .with.property('message') - .and.include('`Cypress.SelectorPlayground.defaults()` called with invalid `selectorPriority` property. It must be one of: `data-*`, `id`, `class`, `tag`, `attributes`, `nth-child`. You passed: `name`') - }) - - it('throws if selectorPriority has an unsupported priority that contains a substring of a valid priority', () => { - const fn = () => { - SelectorPlayground.defaults({ - selectorPriority: [ - 'idIsNotValid', - ], - }) - } - - expect(fn).to.throw() - .with.property('message') - .and.include('`Cypress.SelectorPlayground.defaults()` called with invalid `selectorPriority` property. It must be one of: `data-*`, `id`, `class`, `tag`, `attributes`, `nth-child`. You passed: `idIsNotValid`') - }) - - it('sets selector:playground:on:element if onElement specified', () => { - const onElement = () => {} - - SelectorPlayground.defaults({ onElement }) - - expect(SelectorPlayground.getOnElement()).to.equal(onElement) - }) - - it('throws if not passed an object', () => { - const fn = () => { - SelectorPlayground.defaults() - } - - expect(fn).to.throw() - .with.property('message') - .and.include('`Cypress.SelectorPlayground.defaults()` must be called with an object. You passed: ') - - expect(fn).to.throw() - .with.property('docsUrl') - .and.include('https://on.cypress.io/selector-playground-api') - }) - - it('throws if selectorPriority is not an array', () => { - const fn = () => { - SelectorPlayground.defaults({ selectorPriority: 'foo' }) - } - - expect(fn).to.throw() - .with.property('message') - .and.include('`Cypress.SelectorPlayground.defaults()` called with invalid `selectorPriority` property. It must be an array. You passed: `foo`') - - expect(fn).to.throw() - .with.property('docsUrl') - .and.include('https://on.cypress.io/selector-playground-api') - }) - - it('throws if onElement is not a function', () => { - const fn = () => { - SelectorPlayground.defaults({ onElement: 'foo' }) - } - - expect(fn).to.throw() - .with.property('message') - .and.include('`Cypress.SelectorPlayground.defaults()` called with invalid `onElement` property. It must be a function. You passed: `foo`') - - expect(fn).to.throw() - .with.property('docsUrl') - .and.include('https://on.cypress.io/selector-playground-api') - }) - }) - - context('.getSelector', () => { - it('uses defaults.selectorPriority', () => { - const $div = $('
') - - Cypress.$('body').append($div) - - expect(SelectorPlayground.getSelector($div)).to.eq('[data-cy="main button 123"]') - - SelectorPlayground.defaults({ - selectorPriority: ['data-foo'], - }) - - expect(SelectorPlayground.getSelector($div)).to.eq('[data-foo="bar"]') - - SelectorPlayground.defaults({ - onElement ($el) { - return 'quux' - }, - }) - - expect(SelectorPlayground.getSelector($div)).to.eq('quux') - - SelectorPlayground.defaults({ - onElement ($el) { - return null - }, - }) - - expect(SelectorPlayground.getSelector($div)).to.eq('[data-foo="bar"]') - }) - }) -}) diff --git a/packages/driver/package.json b/packages/driver/package.json index 8a6b08746e2..34170fa0d0b 100644 --- a/packages/driver/package.json +++ b/packages/driver/package.json @@ -19,7 +19,7 @@ "devDependencies": { "@babel/code-frame": "7.24.7", "@cypress/sinon-chai": "2.9.1", - "@cypress/unique-selector": "0.0.5", + "@cypress/unique-selector": "2.1.1", "@cypress/webpack-dev-server": "0.0.0-development", "@cypress/webpack-preprocessor": "0.0.0-development", "@packages/config": "0.0.0-development", diff --git a/packages/driver/src/cypress.ts b/packages/driver/src/cypress.ts index c8657904fae..e8086ad1ffc 100644 --- a/packages/driver/src/cypress.ts +++ b/packages/driver/src/cypress.ts @@ -24,7 +24,7 @@ import $Mocha from './cypress/mocha' import { create as createMouse } from './cy/mouse' import $Runner from './cypress/runner' import $Screenshot from './cypress/screenshot' -import $SelectorPlayground from './cypress/selector_playground' +import $ElementSelector from './cypress/element_selector' import $Server from './cypress/server' import $SetterGetter from './cypress/setter_getter' import { validateConfig } from './util/config' @@ -151,7 +151,19 @@ class $Cypress { Runner = $Runner Server = $Server Screenshot = $Screenshot - SelectorPlayground = $SelectorPlayground + ElementSelector = $ElementSelector + SelectorPlayground = { + defaults (options: any) { + $errUtils.throwErrByPath('selector_playground.renamed', { + args: { method: 'defaults' }, + }) + }, + getSelector ($el: any) { + $errUtils.throwErrByPath('selector_playground.renamed', { + args: { method: 'getSelector' }, + }) + }, + } utils = $utils _ = _ Blob = blobUtil diff --git a/packages/driver/src/cypress/element_selector.ts b/packages/driver/src/cypress/element_selector.ts new file mode 100644 index 00000000000..7e63da231b5 --- /dev/null +++ b/packages/driver/src/cypress/element_selector.ts @@ -0,0 +1,92 @@ +/// +import _ from 'lodash' +import uniqueSelector from '@cypress/unique-selector' + +import $utils from './utils' +import $errUtils from './error_utils' + +const VALID_SELECTOR_PRIORITY_REGEX = /^(data\-.+|id|class|tag|attributes|nth\-child|attribute:(.+)|name)$/ + +export const DEFAULT_SELECTOR_PRIORITIES = [ + 'data-cy', + 'data-test', + 'data-testid', + 'data-qa', + 'name', + 'id', + 'class', + 'tag', + 'attributes', + 'nth-child', +] as const + +export type Defaults = { + selectorPriority: Cypress.SelectorPriority[] +} + +export type ElementSelectorDefaultsOptions = { + selectorPriority?: Cypress.SelectorPriority[] +} + +export interface ElementSelectorAPI { + reset(): void + getSelectorPriority(): Cypress.SelectorPriority[] + getSelector($el: any): string + defaults(options: ElementSelectorDefaultsOptions): void +} + +const reset = (): Defaults => { + return { + selectorPriority: [...DEFAULT_SELECTOR_PRIORITIES], + } +} + +let defaults = reset() + +const ElementSelector: ElementSelectorAPI = { + reset () { + defaults = reset() + }, + + getSelectorPriority () { + return defaults.selectorPriority + }, + + getSelector ($el: any) { + return uniqueSelector($el.get(0), { + selectorTypes: defaults.selectorPriority, + }) + }, + + defaults (props: ElementSelectorDefaultsOptions) { + if (!_.isPlainObject(props)) { + $errUtils.throwErrByPath('element_selector.defaults_invalid_arg', { + args: { arg: $utils.stringify(props) }, + }) + } + + const { selectorPriority } = props + + if (selectorPriority) { + if (!_.isArray(selectorPriority)) { + $errUtils.throwErrByPath('element_selector.defaults_invalid_priority_type', { + args: { arg: $utils.stringify(selectorPriority) }, + }) + } + + // Validate that the priority is one of: + // "data-*", "id", "class", "tag", "attributes", "nth-child" , "attribute:*", "name" + selectorPriority.forEach((priority) => { + if (!VALID_SELECTOR_PRIORITY_REGEX.test(priority)) { + $errUtils.throwErrByPath('element_selector.defaults_invalid_selector_priority', { + args: { arg: priority }, + }) + } + }) + + defaults.selectorPriority = selectorPriority + } + }, +} + +export default ElementSelector diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index a82d7a41908..f8a86d3e885 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -1718,22 +1718,27 @@ export default { }, }, - selector_playground: { + element_selector: { defaults_invalid_arg: { - message: '`Cypress.SelectorPlayground.defaults()` must be called with an object. You passed: `{{arg}}`', - docsUrl: 'https://on.cypress.io/selector-playground-api', + message: '`Cypress.ElementSelector.defaults()` must be called with an object. You passed: `{{arg}}`', + docsUrl: 'https://on.cypress.io/element-selector-api', }, defaults_invalid_priority_type: { - message: '`Cypress.SelectorPlayground.defaults()` called with invalid `selectorPriority` property. It must be an array. You passed: `{{arg}}`', - docsUrl: 'https://on.cypress.io/selector-playground-api', + message: '`Cypress.ElementSelector.defaults()` called with invalid `selectorPriority` property. It must be an array. You passed: `{{arg}}`', + docsUrl: 'https://on.cypress.io/element-selector-api', }, - defaults_invalid_priority: { - message: '`Cypress.SelectorPlayground.defaults()` called with invalid `selectorPriority` property. It must be one of: `data-*`, `id`, `class`, `tag`, `attributes`, `nth-child`. You passed: `{{arg}}`. Consider using the `onElement` property if a specific selector is desired.', - docsUrl: 'https://on.cypress.io/selector-playground-api', + defaults_invalid_selector_priority: { + message: '`Cypress.ElementSelector.defaults()` called with invalid `selectorPriority` property. It must be one of: `data-*`, `attribute:*`, `id`, `class`, `tag`, `name`,`attributes`, or `nth-child`. You passed: `{{arg}}`.', + docsUrl: 'https://on.cypress.io/element-selector-api', }, - defaults_invalid_on_element: { - message: '`Cypress.SelectorPlayground.defaults()` called with invalid `onElement` property. It must be a function. You passed: `{{arg}}`', - docsUrl: 'https://on.cypress.io/selector-playground-api', + }, + + selector_playground: { + renamed: ({ method }: { method: string }) => { + return { + message: `\`Cypress.SelectorPlayground.${method}()\` has been renamed to \`Cypress.ElementSelector.${method}()\`. Please update your code to use \`Cypress.ElementSelector\` instead.`, + docsUrl: 'https://on.cypress.io/element-selector-api', + } }, }, diff --git a/packages/driver/src/cypress/selector_playground.ts b/packages/driver/src/cypress/selector_playground.ts deleted file mode 100644 index 1b776c8801b..00000000000 --- a/packages/driver/src/cypress/selector_playground.ts +++ /dev/null @@ -1,92 +0,0 @@ -import _ from 'lodash' -import uniqueSelector from '@cypress/unique-selector' - -import $utils from './utils' -import $errUtils from './error_utils' - -const SELECTOR_PRIORITIES = 'data-cy data-test data-testid data-qa id class tag attributes nth-child'.split(' ') - -type Defaults = { - onElement: Cypress.SelectorPlaygroundDefaultsOptions['onElement'] | null - selectorPriority: Cypress.SelectorPlaygroundDefaultsOptions['selectorPriority'] -} - -const reset = (): Defaults => { - return { - onElement: null, - selectorPriority: SELECTOR_PRIORITIES, - } -} - -let defaults = reset() - -export default { - reset () { - defaults = reset() - }, - - getSelectorPriority () { - return defaults.selectorPriority - }, - - getOnElement () { - return defaults.onElement - }, - - getSelector ($el) { - // if we have a callback, and it returned truthy - const selector = defaults.onElement && defaults.onElement($el) - - if (selector) { - // and it returned a string - if (_.isString(selector)) { - // use this! - return selector - } - } - - // else use uniqueSelector with the priorities - return uniqueSelector($el.get(0), { - selectorTypes: defaults.selectorPriority, - }) - }, - - defaults (props) { - if (!_.isPlainObject(props)) { - $errUtils.throwErrByPath('selector_playground.defaults_invalid_arg', { - args: { arg: $utils.stringify(props) }, - }) - } - - const { selectorPriority, onElement } = props - - if (selectorPriority) { - if (!_.isArray(selectorPriority)) { - $errUtils.throwErrByPath('selector_playground.defaults_invalid_priority_type', { - args: { arg: $utils.stringify(selectorPriority) }, - }) - } - // Validate that the priority is one of: "data-*", "id", "class", "tag", "attributes", "nth-child" - - selectorPriority.forEach((priority) => { - if (!/^(data\-.*|id|class|tag|attributes|nth\-child)$/.test(priority)) { - $errUtils.throwErrByPath('selector_playground.defaults_invalid_priority', { - args: { arg: priority }, - }) - } - }) - - defaults.selectorPriority = selectorPriority - } - - if (onElement) { - if (!_.isFunction(onElement)) { - $errUtils.throwErrByPath('selector_playground.defaults_invalid_on_element', { - args: { arg: $utils.stringify(onElement) }, - }) - } - - defaults.onElement = onElement - } - }, -} diff --git a/packages/driver/types/internal-types.d.ts b/packages/driver/types/internal-types.d.ts index bc632521e2b..afd797528a9 100644 --- a/packages/driver/types/internal-types.d.ts +++ b/packages/driver/types/internal-types.d.ts @@ -61,6 +61,9 @@ declare namespace Cypress { Location: { create: (url: string) => ({ domain: string, superDomain: string }) } + $Cypress: { + $: JQueryStatic + } } interface CypressUtils { diff --git a/yarn.lock b/yarn.lock index 52a77448f19..59d20676af9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2878,10 +2878,10 @@ resolved "https://registry.yarnpkg.com/@cypress/snapbuild-windows-64/-/snapbuild-windows-64-1.0.4.tgz#37854c0d1262b98913d3b6aa99f5343724e80d2b" integrity sha512-sXqQ2+6xTVZVvKnK0WTNkczDUFRXeE9Cmv0nhfVYXpFNVhhLcUJm748haTMzeBtLh40o9BzvN/sFqkQwoCsI9w== -"@cypress/unique-selector@0.0.5": - version "0.0.5" - resolved "https://registry.yarnpkg.com/@cypress/unique-selector/-/unique-selector-0.0.5.tgz#1ce7889dd9ffe47e8b39d31da3a6b7e9a0c9c93d" - integrity sha512-WS1M1arFAcl2v+A4OHT4ZPAXQgoQuVwM4quyZZQUmJsshXZqbTFyT918WQujC6yikOddZgiVGk46CIBGoCMnow== +"@cypress/unique-selector@2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@cypress/unique-selector/-/unique-selector-2.1.1.tgz#e5afa80705eb48735f0ecbb5d148cded071d27bf" + integrity sha512-uMUiPfwWv/eG/ubSzvIdc/7C+RKG+r0QH7/qot61oEdYvBEabaZEQuugxXGzU5t2tbjpepU+FNWXBreTRo5MBQ== dependencies: css.escape "^1.5.1"