From e3da6bac99377ca8c6a5421d49c6ab2293f0e246 Mon Sep 17 00:00:00 2001 From: "cypress-bot[bot]" <2f0651858c6e38e0+cypress-bot[bot]@users.noreply.github.com> Date: Fri, 28 Jan 2022 08:01:14 +0000 Subject: [PATCH 01/10] chore: Update Chrome (beta) to 98.0.4758.74 --- browser-versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser-versions.json b/browser-versions.json index c633c59f3519..eef229329ff3 100644 --- a/browser-versions.json +++ b/browser-versions.json @@ -1,4 +1,4 @@ { - "chrome:beta": "98.0.4758.66", + "chrome:beta": "98.0.4758.74", "chrome:stable": "97.0.4692.99" } From 805ed113457abd616424fb86412b10f2e5d9df22 Mon Sep 17 00:00:00 2001 From: Alejandro Estrada Date: Mon, 31 Jan 2022 10:48:38 -0500 Subject: [PATCH 02/10] fix: refactor set specs by specPattern (#19953) * fix: refactor set specs by specPattern * Update types Co-authored-by: Zachary Williams --- .../src/actions/ProjectActions.ts | 55 +++++++++++++++---- packages/server/lib/open_project.ts | 30 ++-------- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/packages/data-context/src/actions/ProjectActions.ts b/packages/data-context/src/actions/ProjectActions.ts index 184d23d30d7d..0a6bd43ab652 100644 --- a/packages/data-context/src/actions/ProjectActions.ts +++ b/packages/data-context/src/actions/ProjectActions.ts @@ -36,6 +36,14 @@ export interface ProjectApiShape { } } +type SetSpecsFoundBySpecPattern = { + path: string + testingType: Cypress.TestingType + specPattern?: Cypress.Config['specPattern'] + ignoreSpecPattern?: Cypress.Config['ignoreSpecPattern'] + additionalIgnorePattern?: string | string[] +} + export class ProjectActions { constructor (private ctx: DataContext) {} @@ -397,20 +405,18 @@ export class ProjectActions { const cfg = this.ctx.project.getConfig() - if (cfg) { - const toArray = (v: string | string[] | undefined) => Array.isArray(v) ? v : v ? [v] : undefined - + if (cfg && this.ctx.currentProject) { const testingType = (codeGenType === 'component' || codeGenType === 'story') ? 'component' : 'e2e' - const specPattern = toArray(cfg[testingType]?.specPattern) - const ignoreSpecPattern = toArray(cfg[testingType]?.ignoreSpecPattern) ?? [] - const additionalIgnore = toArray(testingType === 'component' ? cfg?.e2e?.specPattern : undefined) ?? [] - - if (this.ctx.currentProject && specPattern) { - const specs = await this.ctx.project.findSpecs(this.ctx.currentProject, testingType, specPattern, ignoreSpecPattern, additionalIgnore) - - this.ctx.project.setSpecs(specs) + const { specs } = await this.setSpecsFoundBySpecPattern({ + path: this.ctx.currentProject, + testingType, + specPattern: cfg[testingType]?.specPattern, + ignoreSpecPattern: cfg[testingType]?.ignoreSpecPattern, + additionalIgnorePattern: testingType === 'component' ? cfg?.e2e?.specPattern : undefined, + }) + if (specs) { if (testingType === 'component') { this.api.getDevServer().updateSpecs(specs) } @@ -424,6 +430,33 @@ export class ProjectActions { } } + async setSpecsFoundBySpecPattern ({ path, testingType, specPattern, ignoreSpecPattern, additionalIgnorePattern }: SetSpecsFoundBySpecPattern) { + const toArray = (val?: string | string[]) => val ? typeof val === 'string' ? [val] : val : undefined + + specPattern = toArray(specPattern) + + ignoreSpecPattern = toArray(ignoreSpecPattern) || [] + + // exclude all specs matching e2e if in component testing + additionalIgnorePattern = toArray(additionalIgnorePattern) || [] + + if (!specPattern) { + throw Error('could not find pattern to load specs') + } + + const specs = await this.ctx.project.findSpecs( + path, + testingType, + specPattern, + ignoreSpecPattern, + additionalIgnorePattern, + ) + + this.ctx.actions.project.setSpecs(specs) + + return { specs, specPattern, ignoreSpecPattern, additionalIgnorePattern } + } + async reconfigureProject () { // Initialize active project close first the current project await this.initializeActiveProject() diff --git a/packages/server/lib/open_project.ts b/packages/server/lib/open_project.ts index a8a083951975..512f60f23f48 100644 --- a/packages/server/lib/open_project.ts +++ b/packages/server/lib/open_project.ts @@ -263,34 +263,14 @@ export class OpenProject { try { const cfg = await this.projectBase.initializeConfig() - const toArray = (val?: string | string[]) => val ? typeof val === 'string' ? [val] : val : undefined - - let specPattern = options.spec || cfg[testingType].specPattern - - specPattern = toArray(specPattern) - - let ignoreSpecPattern = cfg[testingType].ignoreSpecPattern - - ignoreSpecPattern = toArray(ignoreSpecPattern) || [] - - // exclude all specs matching e2e if in component testing - let additionalIgnorePattern = testingType === 'component' ? cfg?.e2e?.specPattern : undefined - - additionalIgnorePattern = toArray(additionalIgnorePattern) || [] - - if (!specPattern) { - throw Error('could not find pattern to load specs') - } - - const specs = await this._ctx.project.findSpecs( + const { specPattern, ignoreSpecPattern, additionalIgnorePattern } = await this._ctx.actions.project.setSpecsFoundBySpecPattern({ path, testingType, - specPattern, - ignoreSpecPattern, - additionalIgnorePattern, - ) + specPattern: options.spec || cfg[testingType].specPattern, + ignoreSpecPattern: cfg[testingType].ignoreSpecPattern, + additionalIgnorePattern: testingType === 'component' ? cfg?.e2e?.specPattern : undefined, + }) - this._ctx.actions.project.setSpecs(specs) this._ctx.project.startSpecWatcher(path, testingType, specPattern, ignoreSpecPattern, additionalIgnorePattern) await this.projectBase.open() From 11179609a0c5979d9ffc45ed6386d7b78c2cc954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Barth=C3=A9l=C3=A9my=20Ledoux?= Date: Mon, 31 Jan 2022 10:38:06 -0600 Subject: [PATCH 03/10] feat: detect package manager in wizard (#19960) --- .vscode/cspell.json | 1 + .../src/data/ProjectLifecycleManager.ts | 23 ++++++++- .../data-context/src/data/coreDataShape.ts | 4 +- .../src/sources/migration/index.ts | 2 + .../cypress/e2e/support/e2eProjectDirs.ts | 3 ++ .../support/mock-graphql/stubgql-Project.ts | 1 + packages/graphql/schemas/schema.graphql | 7 +++ .../objectTypes/gql-CurrentProject.ts | 17 +++++-- .../launchpad/cypress/e2e/project-setup.cy.ts | 47 ++++++++++++++++++- .../launchpad/src/setup/ManualInstall.vue | 13 +++-- packages/types/src/constants.ts | 2 + system-tests/projects/pristine-npm/.gitignore | 1 + system-tests/projects/pristine-npm/app.js | 0 .../projects/pristine-npm/package-lock.json | 0 system-tests/projects/pristine-pnpm/app.js | 0 .../projects/pristine-pnpm/pnpm-lock.yaml | 0 system-tests/projects/pristine-yarn/app.js | 0 system-tests/projects/pristine-yarn/yarn.lock | 0 18 files changed, 109 insertions(+), 12 deletions(-) create mode 100644 system-tests/projects/pristine-npm/.gitignore create mode 100644 system-tests/projects/pristine-npm/app.js create mode 100644 system-tests/projects/pristine-npm/package-lock.json create mode 100644 system-tests/projects/pristine-pnpm/app.js create mode 100644 system-tests/projects/pristine-pnpm/pnpm-lock.yaml create mode 100644 system-tests/projects/pristine-yarn/app.js create mode 100644 system-tests/projects/pristine-yarn/yarn.lock diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 380b28e497ca..6382990a8816 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -14,6 +14,7 @@ "NOTESTS", "OVERLIMIT", "Pinia", + "pnpm", "Screenshotting", "shiki", "testid", diff --git a/packages/data-context/src/data/ProjectLifecycleManager.ts b/packages/data-context/src/data/ProjectLifecycleManager.ts index fac2f68feab1..b306cda65d56 100644 --- a/packages/data-context/src/data/ProjectLifecycleManager.ts +++ b/packages/data-context/src/data/ProjectLifecycleManager.ts @@ -20,7 +20,7 @@ import type { DataContext } from '..' import { LoadConfigReply, SetupNodeEventsReply, ProjectConfigIpc, IpcHandler } from './ProjectConfigIpc' import assert from 'assert' import type { AllModeOptions, FoundBrowser, FullConfig, TestingType } from '@packages/types' -import type { BaseErrorDataShape, CoreDataShape, WarningError } from '.' +import type { BaseErrorDataShape, WarningError } from '.' import { autoBindDebug } from '../util/autoBindDebug' const debug = debugLib(`cypress:lifecycle:ProjectLifecycleManager`) @@ -236,6 +236,22 @@ export class ProjectLifecycleManager { this._projectRoot = undefined } + getPackageManagerUsed (projectRoot: string) { + if (fs.existsSync(path.join(projectRoot, 'package-lock.json'))) { + return 'npm' + } + + if (fs.existsSync(path.join(projectRoot, 'yarn.lock'))) { + return 'yarn' + } + + if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))) { + return 'pnpm' + } + + return 'npm' + } + /** * When we set the current project, we need to cleanup the * previous project that might have existed. We use this as the @@ -253,9 +269,12 @@ export class ProjectLifecycleManager { this.legacyPluginGuard() Promise.resolve(this.ctx.browser.machineBrowsers()).catch(this.onLoadError) this.verifyProjectRoot(projectRoot) + const packageManagerUsed = this.getPackageManagerUsed(projectRoot) + this.resetInternalState() this.ctx.update((s) => { s.currentProject = projectRoot + s.packageManager = packageManagerUsed }) const { needsCypressJsonMigration } = this.refreshMetaState() @@ -300,7 +319,7 @@ export class ProjectLifecycleManager { * with the chosen testing type. */ setCurrentTestingType (testingType: TestingType | null) { - this.ctx.update((d: CoreDataShape) => { + this.ctx.update((d) => { d.currentTestingType = testingType }) diff --git a/packages/data-context/src/data/coreDataShape.ts b/packages/data-context/src/data/coreDataShape.ts index 07023481c48a..8f07f3fbd1ae 100644 --- a/packages/data-context/src/data/coreDataShape.ts +++ b/packages/data-context/src/data/coreDataShape.ts @@ -1,4 +1,4 @@ -import { BUNDLERS, FoundBrowser, Editor, Warning, AllowedState, AllModeOptions, TestingType } from '@packages/types' +import { BUNDLERS, FoundBrowser, Editor, Warning, AllowedState, AllModeOptions, TestingType, PACKAGE_MANAGERS } from '@packages/types' import type { NexusGenEnums, NexusGenObjects } from '@packages/graphql/src/gen/nxs.gen' import type { App, BrowserWindow } from 'electron' import type { ChildProcess } from 'child_process' @@ -110,6 +110,7 @@ export interface CoreDataShape { isAuthBrowserOpened: boolean scaffoldedFiles: NexusGenObjects['ScaffoldedFile'][] | null warnings: Warning[] + packageManager: typeof PACKAGE_MANAGERS[number] } /** @@ -161,5 +162,6 @@ export function makeCoreData (modeOptions: Partial = {}): CoreDa browserWindow: null, }, scaffoldedFiles: null, + packageManager: 'npm', } } diff --git a/packages/data-context/src/sources/migration/index.ts b/packages/data-context/src/sources/migration/index.ts index f45d2151a26a..d6ccf5c535f3 100644 --- a/packages/data-context/src/sources/migration/index.ts +++ b/packages/data-context/src/sources/migration/index.ts @@ -1,4 +1,6 @@ /* eslint-disable padding-line-between-statements */ // created by autobarrel, do not modify directly +export * from './autoRename' +export * from './regexps' export * from './shouldShowSteps' diff --git a/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts b/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts index b8814c27d6b1..f19501821537 100644 --- a/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts +++ b/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts @@ -73,9 +73,12 @@ export const e2eProjectDirs = [ 'plugins-async-error', 'plugins-root-async-error', 'pristine', + 'pristine-npm', + 'pristine-pnpm', 'pristine-with-ct-testing', 'pristine-with-e2e-testing', 'pristine-with-e2e-testing-and-storybook', + 'pristine-yarn', 'react-code-gen', 'read-only-project-root', 'record', diff --git a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Project.ts b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Project.ts index e97a2f713106..8788f92cbef7 100644 --- a/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Project.ts +++ b/packages/frontend-shared/cypress/support/mock-graphql/stubgql-Project.ts @@ -52,6 +52,7 @@ export const createTestCurrentProject = (title: string, currentProject: Partial< browsers: stubBrowsers, isDefaultSpecPattern: true, isBrowserOpen: false, + packageManager: 'yarn', ...currentProject, } } diff --git a/packages/graphql/schemas/schema.graphql b/packages/graphql/schemas/schema.graphql index 5229eda69694..e7a38509b8f6 100644 --- a/packages/graphql/schemas/schema.graphql +++ b/packages/graphql/schemas/schema.graphql @@ -385,6 +385,7 @@ type CurrentProject implements Node & ProjectLike { """Whether the project needs to be migrated before proceeding""" needsLegacyConfigMigration: Boolean + packageManager: PackageManagerEnum! """Cached preferences for this project""" preferences: ProjectPreferences @@ -818,6 +819,12 @@ interface Node { id: ID! } +enum PackageManagerEnum { + npm + pnpm + yarn +} + """ PageInfo cursor, as defined in https://facebook.github.io/relay/graphql/connections.htm#sec-undefined.PageInfo """ diff --git a/packages/graphql/src/schemaTypes/objectTypes/gql-CurrentProject.ts b/packages/graphql/src/schemaTypes/objectTypes/gql-CurrentProject.ts index 58a8cbee2e96..ee2fa2bf9710 100644 --- a/packages/graphql/src/schemaTypes/objectTypes/gql-CurrentProject.ts +++ b/packages/graphql/src/schemaTypes/objectTypes/gql-CurrentProject.ts @@ -1,4 +1,5 @@ -import { nonNull, objectType, stringArg } from 'nexus' +import { PACKAGE_MANAGERS } from '@packages/types' +import { enumType, nonNull, objectType, stringArg } from 'nexus' import path from 'path' import { BaseError } from '.' import { cloudProjectBySlug } from '../../stitching/remoteGraphQLCalls' @@ -10,6 +11,11 @@ import { ProjectPreferences } from './gql-ProjectPreferences' import { Spec } from './gql-Spec' import { Storybook } from './gql-Storybook' +export const PackageManagerEnum = enumType({ + name: 'PackageManagerEnum', + members: PACKAGE_MANAGERS, +}) + export const CurrentProject = objectType({ name: 'CurrentProject', description: 'The currently opened Cypress project, represented by a cypress.config.{ts|js} file', @@ -17,6 +23,11 @@ export const CurrentProject = objectType({ definition (t) { t.implements('ProjectLike') + t.nonNull.field('packageManager', { + type: PackageManagerEnum, + resolve: (source, args, ctx) => ctx.coreData.packageManager, + }) + t.field('errorLoadingConfigFile', { type: BaseError, description: 'If there is an error loading the config file, it is represented here', @@ -101,10 +112,6 @@ export const CurrentProject = objectType({ }, }) - // t.list.field('testingTypes', { - // type: TestingTypeInfo, - // }) - t.nonNull.list.nonNull.field('specs', { description: 'A list of specs for the currently open testing type of a project', type: Spec, diff --git a/packages/launchpad/cypress/e2e/project-setup.cy.ts b/packages/launchpad/cypress/e2e/project-setup.cy.ts index 57ac9247bfc7..3c7ea0020872 100644 --- a/packages/launchpad/cypress/e2e/project-setup.cy.ts +++ b/packages/launchpad/cypress/e2e/project-setup.cy.ts @@ -6,6 +6,9 @@ describe('Launchpad: Setup Project', () => { cy.scaffoldProject('pristine-with-ct-testing') // component configured cy.scaffoldProject('pristine-with-e2e-testing') // e2e configured cy.scaffoldProject('pristine-with-e2e-testing-and-storybook') // e2e configured + cy.scaffoldProject('pristine-npm') + cy.scaffoldProject('pristine-yarn') + cy.scaffoldProject('pristine-pnpm') }) const verifyWelcomePage = ({ e2eIsConfigured, ctIsConfigured }) => { @@ -469,6 +472,10 @@ describe('Launchpad: Setup Project', () => { it(testTitle, () => { cy.openProject(hasStorybookDep ? 'pristine-with-e2e-testing-and-storybook' : 'pristine-with-e2e-testing') + cy.withCtx((ctx) => { + ctx.actions.file.writeFileInProject('yarn.lock', '# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.') + }) + cy.visitLaunchpad() verifyWelcomePage({ e2eIsConfigured: true, ctIsConfigured: false }) @@ -538,7 +545,7 @@ describe('Launchpad: Setup Project', () => { cy.log('Go to next step and verify Install Dev Dependencies page') cy.contains('h1', 'Install Dev Dependencies') - let installCommand = `yarn add -D ${framework.package} ${bundler.package}` + let installCommand = `npm install -D ${framework.package} ${bundler.package}` if (hasStorybookDep) { installCommand += ` ${framework.storybookDep}` @@ -676,4 +683,42 @@ describe('Launchpad: Setup Project', () => { }) }) }) + + describe('Command for package managers', () => { + it('makes the right command for yarn', () => { + cy.openProject('pristine-yarn') + + cy.visitLaunchpad() + + cy.get('[data-cy-testingtype="component"]').click() + cy.get('[data-testid="select-framework"]').click() + cy.findByText('Create React App').click() + cy.findByText('Next Step').click() + cy.get('code').should('contain.text', 'yarn add -D ') + }) + + it('makes the right command for pnpm', () => { + cy.openProject('pristine-pnpm') + + cy.visitLaunchpad() + + cy.get('[data-cy-testingtype="component"]').click() + cy.get('[data-testid="select-framework"]').click() + cy.findByText('Create React App').click() + cy.findByText('Next Step').click() + cy.get('code').should('contain.text', 'pnpm install -D ') + }) + + it('makes the right command for npm', () => { + cy.openProject('pristine-npm') + + cy.visitLaunchpad() + + cy.get('[data-cy-testingtype="component"]').click() + cy.get('[data-testid="select-framework"]').click() + cy.findByText('Create React App').click() + cy.findByText('Next Step').click() + cy.get('code').should('contain.text', 'npm install -D ') + }) + }) }) diff --git a/packages/launchpad/src/setup/ManualInstall.vue b/packages/launchpad/src/setup/ManualInstall.vue index 532e137b9699..1d365dfe2812 100644 --- a/packages/launchpad/src/setup/ManualInstall.vue +++ b/packages/launchpad/src/setup/ManualInstall.vue @@ -9,7 +9,7 @@
  • () +const commands = { + 'npm': 'npm install -D ', + 'pnpm': 'pnpm install -D ', + 'yarn': 'yarn add -D ', +} + const installDependenciesCode = computed( () => { - return `yarn add -D ${ + return commands[props.gql.currentProject?.packageManager ?? 'npm'] + (props.gql.wizard.packagesToInstall ?? []) .map((pack) => `${pack.package}`) - .join(' ')}` + .join(' ') }, ) diff --git a/packages/types/src/constants.ts b/packages/types/src/constants.ts index d3b75b5a65db..45059a5c2210 100644 --- a/packages/types/src/constants.ts +++ b/packages/types/src/constants.ts @@ -126,3 +126,5 @@ export const PACKAGES_DESCRIPTIONS: Record = { '@storybook/testing-react': 'Testing utilities that allow you to reuse your stories in your unit tests', '@storybook/testing-vue3': 'Testing utilities that allow you to reuse your stories in your unit tests', } as const + +export const PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm'] as const diff --git a/system-tests/projects/pristine-npm/.gitignore b/system-tests/projects/pristine-npm/.gitignore new file mode 100644 index 000000000000..2fe28d55d55b --- /dev/null +++ b/system-tests/projects/pristine-npm/.gitignore @@ -0,0 +1 @@ +!package-lock.json \ No newline at end of file diff --git a/system-tests/projects/pristine-npm/app.js b/system-tests/projects/pristine-npm/app.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/system-tests/projects/pristine-npm/package-lock.json b/system-tests/projects/pristine-npm/package-lock.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/system-tests/projects/pristine-pnpm/app.js b/system-tests/projects/pristine-pnpm/app.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/system-tests/projects/pristine-pnpm/pnpm-lock.yaml b/system-tests/projects/pristine-pnpm/pnpm-lock.yaml new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/system-tests/projects/pristine-yarn/app.js b/system-tests/projects/pristine-yarn/app.js new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/system-tests/projects/pristine-yarn/yarn.lock b/system-tests/projects/pristine-yarn/yarn.lock new file mode 100644 index 000000000000..e69de29bb2d1 From a43f3a2cbbb3c18a1b76a0237efab30212711449 Mon Sep 17 00:00:00 2001 From: Kukhyeon Heo Date: Tue, 1 Feb 2022 02:04:39 +0900 Subject: [PATCH 04/10] fix: send click event with `cy.type('{enter}')`. (#19726) --- .../driver/cypress/fixtures/type-enter.html | 75 +++++++++++++++++++ .../integration/commands/actions/type_spec.js | 47 ++++++++++++ .../driver/src/cy/commands/actions/type.ts | 22 +++++- packages/driver/src/cy/keyboard.ts | 2 +- 4 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 packages/driver/cypress/fixtures/type-enter.html diff --git a/packages/driver/cypress/fixtures/type-enter.html b/packages/driver/cypress/fixtures/type-enter.html new file mode 100644 index 000000000000..421e2683d19d --- /dev/null +++ b/packages/driver/cypress/fixtures/type-enter.html @@ -0,0 +1,75 @@ + + + + type('{enter}') + + + + + + + + + + + + + +
    + + + + \ No newline at end of file diff --git a/packages/driver/cypress/integration/commands/actions/type_spec.js b/packages/driver/cypress/integration/commands/actions/type_spec.js index 138dde30501c..28ef1155efe2 100644 --- a/packages/driver/cypress/integration/commands/actions/type_spec.js +++ b/packages/driver/cypress/integration/commands/actions/type_spec.js @@ -557,6 +557,53 @@ describe('src/cy/commands/actions/type - #type', () => { }) }) + // https://github.com/cypress-io/cypress/issues/19541 + describe(`type('{enter}') and click event on button-like elements`, () => { + beforeEach(() => { + cy.visit('fixtures/type-enter.html') + }) + + describe('triggers', () => { + const targets = [ + 'button-tag', + 'input-button', + 'input-image', + 'input-reset', + 'input-submit', + ] + + targets.forEach((targetId) => { + it(`${targetId}`, () => { + cy.get(`#target-${targetId}`).focus() + cy.get(`#target-${targetId}`).type('{enter}') + + cy.get('li').eq(0).should('have.text', 'keydown') + cy.get('li').eq(1).should('have.text', 'keypress') + cy.get('li').eq(2).should('have.text', 'click') + cy.get('li').eq(3).should('have.text', 'keyup') + }) + }) + }) + + describe('does not trigger', () => { + const targets = [ + 'input-checkbox', + 'input-radio', + ] + + targets.forEach((targetId) => { + it(`${targetId}`, () => { + cy.get(`#target-${targetId}`).focus() + cy.get(`#target-${targetId}`).type('{enter}') + + cy.get('li').eq(0).should('have.text', 'keydown') + cy.get('li').eq(1).should('have.text', 'keypress') + cy.get('li').eq(2).should('have.text', 'keyup') + }) + }) + }) + }) + describe('tabindex', () => { beforeEach(function () { this.$div = cy.$$('#tabindex') diff --git a/packages/driver/src/cy/commands/actions/type.ts b/packages/driver/src/cy/commands/actions/type.ts index d6d18c9445de..4e3e8a3a0322 100644 --- a/packages/driver/src/cy/commands/actions/type.ts +++ b/packages/driver/src/cy/commands/actions/type.ts @@ -272,6 +272,11 @@ export default function (Commands, Cypress, cy, state, config) { const isContentEditable = $elements.isContentEditable(options.$el.get(0)) const isTextarea = $elements.isTextarea(options.$el.get(0)) + // click event is only fired on button, image, submit, reset elements. + // That's why we cannot use $elements.isButtonLike() here. + const type = (type) => $elements.isInputType(options.$el.get(0), type) + const sendClickEvent = type('button') || type('image') || type('submit') || type('reset') + return keyboard.type({ $el: options.$el, chars, @@ -347,22 +352,33 @@ export default function (Commands, Cypress, cy, state, config) { }) }, - onEnterPressed (id) { + onEnterPressed (el) { // dont dispatch change events or handle // submit event if we've pressed enter into // a textarea or contenteditable - if (isTextarea || isContentEditable) { return } + // https://github.com/cypress-io/cypress/issues/19541 + // Send click event on type('{enter}') + if (sendClickEvent) { + // Firefox sends a click event automatically. + if (!Cypress.isBrowser('firefox')) { + const ctor = $dom.getDocumentFromElement(el).defaultView?.PointerEvent + const event = new ctor('click') + + el.dispatchEvent(event) + } + } + // if our value has changed since our // element was activated we need to // fire a change event immediately const changeEvent = state('changeEvent') if (changeEvent) { - changeEvent(id) + changeEvent() } // handle submit event handler here diff --git a/packages/driver/src/cy/keyboard.ts b/packages/driver/src/cy/keyboard.ts index 5e28c5329f56..f03306b6fa21 100644 --- a/packages/driver/src/cy/keyboard.ts +++ b/packages/driver/src/cy/keyboard.ts @@ -582,7 +582,7 @@ const simulatedDefaultKeyMap: { [key: string]: SimulatedDefault } = { $selection.replaceSelectionContents(el, '\n') } - options.onEnterPressed && options.onEnterPressed() + options.onEnterPressed && options.onEnterPressed(el) }, Delete: (el, key) => { key.events.input = $selection.deleteRightOfCursor(el) From 815fe3d91325515a90bec3d24eb499d5be2cabfc Mon Sep 17 00:00:00 2001 From: Zach Bloomquist Date: Mon, 31 Jan 2022 17:52:14 +0000 Subject: [PATCH 05/10] fix: pass correct spec URL in `cypress run` on Windows (#19890) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Barthélémy Ledoux --- packages/server/lib/project_utils.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/server/lib/project_utils.ts b/packages/server/lib/project_utils.ts index 3bdbdd308549..0656165b26f9 100644 --- a/packages/server/lib/project_utils.ts +++ b/packages/server/lib/project_utils.ts @@ -7,10 +7,6 @@ import { escapeFilenameInUrl } from './util/escape_filename' const debug = Debug('cypress:server:project_utils') -const multipleForwardSlashesRe = /[^:\/\/](\/{2,})/g -const multipleForwardSlashesReplacer = (match: string) => match.replace('//', '/') -const backSlashesRe = /\\/g - // format is: http://localhost:/__/#/specs/runner?file= export const getSpecUrl = ({ spec, @@ -31,16 +27,14 @@ export const getSpecUrl = ({ } const relativeSpecPath = path.relative(projectRoot, path.resolve(projectRoot, spec.relative)) - .replace(backSlashesRe, '/') const escapedRelativePath = escapeFilenameInUrl(relativeSpecPath) - const normalized = `${browserUrl}/#/specs/runner?file=${escapedRelativePath}` - .replace(multipleForwardSlashesRe, multipleForwardSlashesReplacer) + const specUrl = `${browserUrl}#/specs/runner?file=${escapedRelativePath}` - debug('returning spec url %s', normalized) + debug('returning spec url %s', specUrl) - return normalized + return specUrl } export const checkSupportFile = async ({ From 99f24863a20f016a48e963997a0dd2982e977b78 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Mon, 31 Jan 2022 12:03:36 -0600 Subject: [PATCH 06/10] fix: move node 17 Check from Binary to CLI (#19977) --- circle.yml | 4 +- cli/lib/util.js | 31 +++++++-- cli/package.json | 1 + cli/test/lib/util_spec.js | 66 +++++++++++++++++++ packages/server/lib/plugins/index.js | 16 ----- .../server/test/unit/plugins/index_spec.js | 39 ----------- scripts/binary/bump.js | 10 +-- 7 files changed, 94 insertions(+), 73 deletions(-) diff --git a/circle.yml b/circle.yml index 4f8b02cd804a..2cbffc852f30 100644 --- a/circle.yml +++ b/circle.yml @@ -29,7 +29,7 @@ mainBuildFilters: &mainBuildFilters only: - develop - 10.0-release - - test-binary-downstream-windows + - node-17-maybe # usually we don't build Mac app - it takes a long time # but sometimes we want to really confirm we are doing the right thing @@ -1587,7 +1587,7 @@ jobs: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "test-binary-downstream-windows" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "node-17-maybe" ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi diff --git a/cli/lib/util.js b/cli/lib/util.js index 3028c5e0cfc1..36dbb788d915 100644 --- a/cli/lib/util.js +++ b/cli/lib/util.js @@ -18,10 +18,12 @@ const executable = require('executable') const { stripIndent } = require('common-tags') const supportsColor = require('supports-color') const isInstalledGlobally = require('is-installed-globally') -const pkg = require(path.join(__dirname, '..', 'package.json')) const logger = require('./logger') const debug = require('debug')('cypress:cli') const fs = require('./fs') +const semver = require('semver') + +const pkg = require(path.join(__dirname, '..', 'package.json')) const issuesUrl = 'https://github.com/cypress-io/cypress/issues' @@ -282,18 +284,33 @@ const util = { .mapValues((value) => { // stringify to 1 or 0 return value ? '1' : '0' }) - .extend(util.getOriginalNodeOptions(options)) + .extend(util.getOriginalNodeOptions()) .value() }, - getOriginalNodeOptions (options) { + getOriginalNodeOptions () { + const opts = {} + if (process.env.NODE_OPTIONS) { - return { - ORIGINAL_NODE_OPTIONS: process.env.NODE_OPTIONS, - } + opts.ORIGINAL_NODE_OPTIONS = process.env.NODE_OPTIONS + } + + // https://github.com/cypress-io/cypress/issues/18914 + // Node 17+ ships with OpenSSL 3 by default, so we may need the option + // --openssl-legacy-provider so that webpack@4 can use the legacy MD4 hash + // function. This option doesn't exist on Node <17 or when it is built + // against OpenSSL 1, so we have to detect Node's major version and check + // which version of OpenSSL it was built against before spawning the plugins + // process. + + // To be removed when the Cypress binary pulls in the @cypress/webpack-batteries-included-preprocessor + // version that has been updated to webpack >= 5.61, which no longer relies on + // Node's builtin crypto.hash function. + if (process.versions && semver.satisfies(process.versions.node, '>=17.0.0') && process.versions.openssl.startsWith('3.')) { + opts.ORIGINAL_NODE_OPTIONS = `${opts.ORIGINAL_NODE_OPTIONS || ''} --openssl-legacy-provider` } - return {} + return opts }, getForceTty () { diff --git a/cli/package.json b/cli/package.json index 1cbf5623efdf..2a9089f6c572 100644 --- a/cli/package.json +++ b/cli/package.json @@ -58,6 +58,7 @@ "pretty-bytes": "^5.6.0", "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", + "semver": "^7.3.2", "supports-color": "^8.1.1", "tmp": "~0.2.1", "untildify": "^4.0.0", diff --git a/cli/test/lib/util_spec.js b/cli/test/lib/util_spec.js index b325ccab31a0..8412933170f6 100644 --- a/cli/test/lib/util_spec.js +++ b/cli/test/lib/util_spec.js @@ -257,6 +257,7 @@ describe('util', () => { context('.getOriginalNodeOptions', () => { let restoreEnv + const sandbox = sinon.createSandbox() afterEach(() => { if (restoreEnv) { @@ -266,6 +267,9 @@ describe('util', () => { }) it('copy NODE_OPTIONS to ORIGINAL_NODE_OPTIONS', () => { + sandbox.stub(process.versions, 'node').value('v16.5.0') + sandbox.stub(process.versions, 'openssl').value('1.0.0') + restoreEnv = mockedEnv({ NODE_OPTIONS: '--require foo.js', }) @@ -274,6 +278,68 @@ describe('util', () => { ORIGINAL_NODE_OPTIONS: '--require foo.js', }) }) + + // https://github.com/cypress-io/cypress/issues/18914 + it('includes --openssl-legacy-provider in Node 17+ w/ OpenSSL 3', () => { + sandbox.stub(process.versions, 'node').value('v17.1.0') + sandbox.stub(process.versions, 'openssl').value('3.0.0-quic') + + restoreEnv = mockedEnv({ + NODE_OPTIONS: '--require foo.js', + }) + + let childOptions = util.getOriginalNodeOptions() + + expect(childOptions.ORIGINAL_NODE_OPTIONS).to.eq('--require foo.js --openssl-legacy-provider') + + restoreEnv() + restoreEnv = mockedEnv({}) + childOptions = util.getOriginalNodeOptions() + + expect(childOptions.ORIGINAL_NODE_OPTIONS).to.eq(' --openssl-legacy-provider') + }) + + // https://github.com/cypress-io/cypress/issues/19320 + it('does not include --openssl-legacy-provider in Node 17+ w/ OpenSSL 1', () => { + sandbox.stub(process.versions, 'node').value('v17.1.0') + sandbox.stub(process.versions, 'openssl').value('1.0.0') + + restoreEnv = mockedEnv({ + NODE_OPTIONS: '--require foo.js', + }) + + let childOptions = util.getOriginalNodeOptions() + + expect(childOptions.ORIGINAL_NODE_OPTIONS).to.eq('--require foo.js') + expect(childOptions.ORIGINAL_NODE_OPTIONS).not.to.contain('--openssl-legacy-provider') + + restoreEnv() + restoreEnv = mockedEnv({}) + childOptions = util.getOriginalNodeOptions() + + expect(childOptions.ORIGINAL_NODE_OPTIONS).to.be.undefined + }) + + // https://github.com/cypress-io/cypress/issues/18914 + it('does not include --openssl-legacy-provider in Node <=16', () => { + sandbox.stub(process.versions, 'node').value('v16.5.0') + sandbox.stub(process.versions, 'openssl').value('1.0.0') + + restoreEnv = mockedEnv({}) + + let childOptions = util.getOriginalNodeOptions() + + expect(childOptions.ORIGINAL_NODE_OPTIONS).to.be.undefined + + restoreEnv = mockedEnv({ + NODE_OPTIONS: '--require foo.js', + }) + + childOptions = util.getOriginalNodeOptions() + + expect(childOptions.ORIGINAL_NODE_OPTIONS).to.eq('--require foo.js') + expect(childOptions.ORIGINAL_NODE_OPTIONS).not.to.contain('--openssl-legacy-provider') + }) }) context('.exit', () => { diff --git a/packages/server/lib/plugins/index.js b/packages/server/lib/plugins/index.js index da0e05540945..c34ddad44c6f 100644 --- a/packages/server/lib/plugins/index.js +++ b/packages/server/lib/plugins/index.js @@ -8,7 +8,6 @@ const inspector = require('inspector') const errors = require('../errors') const util = require('./util') const pkg = require('@packages/root') -const semver = require('semver') let pluginsProcess = null let registeredEvents = {} @@ -52,21 +51,6 @@ const getChildOptions = (config) => { childOptions.execPath = config.resolvedNodePath } - // https://github.com/cypress-io/cypress/issues/18914 - // Node 17+ ships with OpenSSL 3 by default, so we may need the option - // --openssl-legacy-provider so that webpack@4 can use the legacy MD4 hash - // function. This option doesn't exist on Node <17 or when it is built - // against OpenSSL 1, so we have to detect Node's major version and check - // which version of OpenSSL it was built against before spawning the plugins - // process. - - // To be removed on update to webpack >= 5.61, which no longer relies on - // Node's builtin crypto.hash function. - if (semver.satisfies(config.resolvedNodeVersion, '>=17.0.0') && - !process.versions.openssl.startsWith('1.')) { - childOptions.env.NODE_OPTIONS += ' --openssl-legacy-provider' - } - if (inspector.url()) { childOptions.execArgv = _.chain(process.execArgv.slice(0)) .remove('--inspect-brk') diff --git a/packages/server/test/unit/plugins/index_spec.js b/packages/server/test/unit/plugins/index_spec.js index 81397112e8e1..8af2124a8a0e 100644 --- a/packages/server/test/unit/plugins/index_spec.js +++ b/packages/server/test/unit/plugins/index_spec.js @@ -64,45 +64,6 @@ describe('lib/plugins/index', () => { expect(childOptions.execPath).to.eq(undefined) }) - - // https://github.com/cypress-io/cypress/issues/18914 - it('includes --openssl-legacy-provider in Node 17+ w/ OpenSSL 3', () => { - const sandbox = sinon.createSandbox() - - sandbox.stub(process.versions, 'openssl').value('3.0.0-quic') - - const childOptions = plugins.getChildOptions({ - resolvedNodeVersion: 'v17.1.0', - }) - - expect(childOptions.env.NODE_OPTIONS).to.contain('--openssl-legacy-provider') - - sandbox.restore() - }) - - // https://github.com/cypress-io/cypress/issues/19320 - it('does not include --openssl-legacy-provider in Node 17+ w/ OpenSSL 1', () => { - const sandbox = sinon.createSandbox() - - sandbox.stub(process.versions, 'openssl').value('1.1.1m') - - const childOptions = plugins.getChildOptions({ - resolvedNodeVersion: 'v17.3.0', - }) - - expect(childOptions.env.NODE_OPTIONS).not.to.contain('--openssl-legacy-provider') - - sandbox.restore() - }) - - // https://github.com/cypress-io/cypress/issues/18914 - it('does not include --openssl-legacy-provider in Node <=16', () => { - const childOptions = plugins.getChildOptions({ - resolvedNodeVersion: 'v16.31.0', - }) - - expect(childOptions.env.NODE_OPTIONS).not.to.contain('--openssl-legacy-provider') - }) }) context('#init', () => { diff --git a/scripts/binary/bump.js b/scripts/binary/bump.js index d8a28866040b..b7cc4dd8dd40 100644 --- a/scripts/binary/bump.js +++ b/scripts/binary/bump.js @@ -268,15 +268,7 @@ Testing new Cypress version ${version} } // first try to commit to branch for next upcoming version - const specificBranchOptions = { - owner, - repo, - token: creds.githubToken, - message, - branch: version, - } - - return makeEmptyGithubCommit(specificBranchOptions) + return makeEmptyGithubCommit({ ...defaultOptions, branch: version }) .catch(() => { // maybe there is no branch for next version // try default branch From dbe903095f6337a011e3b5903af38bb673497f12 Mon Sep 17 00:00:00 2001 From: Tyler Biethman Date: Mon, 31 Jan 2022 12:06:07 -0600 Subject: [PATCH 07/10] fix(unify): Updating reporter to consistently use app-provided "Preferred Editor" dialog (#19933) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(unify): Updating reporter to use consistent preferred editor dialog Migrating helpers and initial tests to app from existing runner specs * Simplifying reporter tests around IDE launch * It'd help to open the stack trace * Scoping util hooks * Removing unnecessary spec project files * Removing socket emit assertions from system test validation * Condensing components further to better control tooltip presentation * Updating runnables usage Co-authored-by: Barthélémy Ledoux --- .../cypress/e2e/runner/reporter.errors.cy.ts | 90 ++++++++ .../e2e/runner/support/verify-helpers.ts | 205 ++++++++++++++++++ .../cypress/e2e/support/e2eProjectDirs.ts | 1 + packages/reporter/cypress/support/utils.ts | 124 +---------- packages/reporter/src/hooks/hooks.tsx | 13 +- .../reporter/src/lib/file-name-opener.tsx | 23 +- packages/reporter/src/lib/file-opener.tsx | 43 ---- .../reporter/src/lib/open-file-in-ide.tsx | 13 +- .../src/runnables/runnable-header.tsx | 19 +- packages/reporter/src/runnables/runnables.tsx | 12 +- .../projects/e2e/cypress/support/util.js | 29 +-- .../runner-e2e-specs/cypress.config.js | 5 + .../cypress/e2e/errors/assertions.cy.js | 13 ++ 13 files changed, 351 insertions(+), 239 deletions(-) create mode 100644 packages/app/cypress/e2e/runner/reporter.errors.cy.ts create mode 100644 packages/app/cypress/e2e/runner/support/verify-helpers.ts delete mode 100644 packages/reporter/src/lib/file-opener.tsx create mode 100644 system-tests/projects/runner-e2e-specs/cypress.config.js create mode 100644 system-tests/projects/runner-e2e-specs/cypress/e2e/errors/assertions.cy.js diff --git a/packages/app/cypress/e2e/runner/reporter.errors.cy.ts b/packages/app/cypress/e2e/runner/reporter.errors.cy.ts new file mode 100644 index 000000000000..30f070e042f4 --- /dev/null +++ b/packages/app/cypress/e2e/runner/reporter.errors.cy.ts @@ -0,0 +1,90 @@ +import { verify } from './support/verify-helpers' + +describe('errors ui', { + viewportHeight: 768, + viewportWidth: 1024, +}, () => { + describe('assertion failures', () => { + beforeEach(() => { + cy.scaffoldProject('runner-e2e-specs') + cy.openProject('runner-e2e-specs') + + // set preferred editor to bypass IDE selection dialog + cy.withCtx((ctx) => { + ctx.coreData.localSettings.availableEditors = [ + ...ctx.coreData.localSettings.availableEditors, + { + id: 'test-editor', + binary: '/usr/bin/test-editor', + name: 'Test editor', + }, + ] + + ctx.coreData.localSettings.preferences.preferredEditorBinary = 'test-editor' + }) + + cy.startAppServer() + cy.visitApp() + + cy.contains('[data-cy=spec-item]', 'assertions.cy.js').click() + + cy.location().should((location) => { + expect(location.hash).to.contain('assertions.cy.js') + }) + + // Wait for specs to complete + cy.findByLabelText('Stats').get('.failed', { timeout: 10000 }).should('have.text', 'Failed:3') + }) + + verify.it('with expect().', { + file: 'assertions.cy.js', + hasPreferredIde: true, + column: 25, + message: `expected 'actual' to equal 'expected'`, + codeFrameText: 'with expect().', + }) + + verify.it('with assert()', { + file: 'assertions.cy.js', + hasPreferredIde: true, + column: '(5|12)', // (chrome|firefox) + message: `should be true`, + codeFrameText: 'with assert()', + }) + + verify.it('with assert.()', { + file: 'assertions.cy.js', + hasPreferredIde: true, + column: 12, + message: `expected 'actual' to equal 'expected'`, + codeFrameText: 'with assert.()', + }) + }) + + describe('assertion failures - no preferred IDE', () => { + beforeEach(() => { + cy.scaffoldProject('runner-e2e-specs') + cy.openProject('runner-e2e-specs') + + cy.startAppServer() + cy.visitApp() + + cy.contains('[data-cy=spec-item]', 'assertions.cy.js').click() + + cy.location().should((location) => { + expect(location.hash).to.contain('assertions.cy.js') + }) + + // Wait for specs to complete + cy.findByLabelText('Stats').get('.failed', { timeout: 10000 }).should('have.text', 'Failed:3') + }) + + verify.it('with expect().', { + file: 'assertions.cy.js', + hasPreferredIde: false, + column: 25, + message: `expected 'actual' to equal 'expected'`, + codeFrameText: 'with expect().', + }) + }) +}) diff --git a/packages/app/cypress/e2e/runner/support/verify-helpers.ts b/packages/app/cypress/e2e/runner/support/verify-helpers.ts new file mode 100644 index 000000000000..dd0a742f9beb --- /dev/null +++ b/packages/app/cypress/e2e/runner/support/verify-helpers.ts @@ -0,0 +1,205 @@ +import _ from 'lodash' +import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json' + +// Assert that either the the dialog is presented or the mutation is emitted, depending on +// whether the test has a preferred IDE defined. +const verifyIdeOpen = ({ file, action, hasPreferredIde }) => { + if (hasPreferredIde) { + cy.intercept('mutation-OpenFileInIDE', { data: { 'openFileInIDE': true } }).as('OpenIDE') + + action() + + cy.wait('@OpenIDE').then(({ request }) => { + expect(request.body.variables.input.absolute).to.include(file) + }) + } else { + action() + + cy.contains(defaultMessages.globalPage.selectPreferredEditor).should('be.visible') + cy.findByRole('button', { name: defaultMessages.actions.close }).click() + } +} + +export const verifyFailure = (options) => { + const { + specTitle, + hasCodeFrame = true, + verifyOpenInIde = true, + hasPreferredIde, + column, + codeFrameText, + originalMessage, + message = [], + notInMessage = [], + command, + stack, + file, + uncaught = false, + uncaughtMessage, + } = options + let { regex, line } = options + + regex = regex || new RegExp(`${file}:${line || '\\d+'}:${column}`) + + cy.contains('.runnable-title', specTitle).closest('.runnable').as('Root') + + cy.get('@Root').within(() => { + cy.contains('View stack trace').click() + + const messageLines = [].concat(message) + + if (messageLines.length) { + cy.log('message contains expected lines and stack does not include message') + + _.each(messageLines, (msg) => { + cy.get('.runnable-err-message') + .should('include.text', msg) + + cy.get('.runnable-err-stack-trace') + .should('not.include.text', msg) + }) + } + + if (originalMessage) { + cy.get('.runnable-err-message') + .should('include.text', originalMessage) + } + + const notInMessageLines = [].concat(notInMessage) + + if (notInMessageLines.length) { + cy.log('message does not contain the specified lines') + + _.each(notInMessageLines, (msg) => { + cy.get('.runnable-err-message') + .should('not.include.text', msg) + }) + } + + cy.log('stack trace matches the specified pattern') + cy.get('.runnable-err-stack-trace') + .invoke('text') + .should('match', regex) + + if (stack) { + const stackLines = [].concat(stack) + + if (stackLines.length) { + cy.log('stack contains the expected lines') + } + + _.each(stackLines, (stackLine) => { + cy.get('.runnable-err-stack-trace') + .should('include.text', stackLine) + }) + } + + cy.get('.runnable-err-stack-trace') + .invoke('text') + .should('not.include', '__stackReplacementMarker') + .should((stackTrace) => { + // if this stack trace has the 'From Your Spec Code' addendum, + // it should only appear once + const match = stackTrace.match(/From Your Spec Code/g) + + if (match && match.length) { + expect(match.length, `'From Your Spec Code' should only be in the stack once, but found ${match.length} instances`).to.equal(1) + } + }) + }) + + if (verifyOpenInIde) { + verifyIdeOpen({ + file, + hasPreferredIde, + action: () => { + cy.get('@Root').contains('.runnable-err-stack-trace .runnable-err-file-path a', file) + .click('left') + }, + }) + } + + cy.get('@Root').within(() => { + if (command) { + cy.log('the error is attributed to the correct command') + cy + .get('.command-state-failed') + .first() + .find('.command-method') + .invoke('text') + .should('equal', command) + } + + if (uncaught) { + cy.log('uncaught error has an associated log for the original error') + cy.get('.command-name-uncaught-exception') + .should('have.length', 1) + .should('have.class', 'command-state-failed') + .find('.command-message-text') + .should('include.text', uncaughtMessage || originalMessage) + } else { + cy.log('"caught" error does not have an uncaught error log') + cy.get('.command-name-uncaught-exception').should('not.exist') + } + + if (!hasCodeFrame) return + + cy.log('code frame matches specified pattern') + cy + .get('.test-err-code-frame .runnable-err-file-path') + .invoke('text') + .should('match', regex) + + cy.get('.test-err-code-frame pre span').should('include.text', codeFrameText) + }) + + if (verifyOpenInIde) { + verifyIdeOpen({ + file, + hasPreferredIde, + action: () => { + cy.get('@Root').contains('.test-err-code-frame .runnable-err-file-path a', file) + .click() + }, + }) + } +} + +const createVerifyTest = (modifier?: string) => { + return (title: string, opts: any, props?: any) => { + if (!props) { + props = opts + opts = null + } + + props.specTitle ||= title + + const verifyFn = props.verifyFn || verifyFailure.bind(null, props) + + return (modifier ? it[modifier] : it)(title, verifyFn) + } +} + +export const verify = { + it: createVerifyTest(), +} + +verify.it['only'] = createVerifyTest('only') +verify.it['skip'] = createVerifyTest('skip') + +export const verifyInternalFailure = (props) => { + const { method, stackMethod } = props + + cy.get('.runnable-err-message') + .should('include.text', `thrown in ${method.replace(/\./g, '-')}`) + + cy.get('.runnable-err-stack-expander > .collapsible-header').click() + + cy.get('.runnable-err-stack-trace') + .should('include.text', stackMethod || method) + + // this is an internal cypress error and we can only show code frames + // from specs, so it should not show the code frame + cy.get('.test-err-code-frame') + .should('not.exist') +} diff --git a/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts b/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts index f19501821537..f7b7a0338fd0 100644 --- a/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts +++ b/packages/frontend-shared/cypress/e2e/support/e2eProjectDirs.ts @@ -85,6 +85,7 @@ export const e2eProjectDirs = [ 'remote-debugging-disconnect', 'remote-debugging-port-removed', 'retries-2', + 'runner-e2e-specs', 'same-fixtures-integration-folders', 'screen-size', 'selectFile', diff --git a/packages/reporter/cypress/support/utils.ts b/packages/reporter/cypress/support/utils.ts index 20a12d51fc47..e0e102c5f7c2 100644 --- a/packages/reporter/cypress/support/utils.ts +++ b/packages/reporter/cypress/support/utils.ts @@ -1,5 +1,4 @@ import { EventEmitter } from 'events' -import { Editor } from '@packages/ui-components' import CommandModel from './../../src/commands/command-model' const { _ } = Cypress @@ -18,131 +17,16 @@ interface HandlesFileOpeningProps { } export const itHandlesFileOpening = ({ getRunner, selector, file, stackTrace = false }: HandlesFileOpeningProps) => { - beforeEach(() => { - cy.stub(getRunner(), 'emit').callThrough() - }) - - describe('when user has already set opener and opens file', () => { - let editor: Partial - - beforeEach(() => { - editor = {} - - // @ts-ignore - getRunner().emit.withArgs('get:user:editor').yields({ - preferredOpener: editor, - }) + describe('it handles file opening', () => { + it('emits unified file open event', () => { + cy.stub(getRunner(), 'emit').callThrough() if (stackTrace) { cy.contains('View stack trace').click() } - }) - it('opens in preferred opener', () => { cy.get(selector).first().click().then(() => { - expect(getRunner().emit).to.be.calledWith('open:file', { - where: editor, - ...file, - }) - }) - }) - }) - - describe('when user has not already set opener and opens file', () => { - const availableEditors = [ - { id: 'computer', name: 'On Computer', isOther: false, binary: 'computer' }, - { id: 'atom', name: 'Atom', isOther: false, binary: 'atom' }, - { id: 'vim', name: 'Vim', isOther: false, binary: 'vim' }, - { id: 'sublime', name: 'Sublime Text', isOther: false, binary: 'sublime' }, - { id: 'vscode', name: 'Visual Studio Code', isOther: false, binary: 'vscode' }, - { id: 'other', name: 'Other', isOther: true, binary: '' }, - ] - - beforeEach(() => { - // @ts-ignore - getRunner().emit.withArgs('get:user:editor').yields({ availableEditors }) - // usual viewport of only reporter is a bit cramped for the modal - cy.viewport(600, 600) - - if (stackTrace) { - cy.contains('View stack trace').click() - } - - cy.get(selector).first().click() - }) - - it('opens modal with available editors', () => { - _.each(availableEditors, ({ name }) => { - cy.contains(name) - }) - - cy.contains('Other') - cy.contains('Set preference and open file') - }) - - // NOTE: this fails because mobx doesn't make the editors observable, so - // the changes to the path don't bubble up correctly. this only happens - // in the Cypress test and not when running the actual app - it.skip('updates "Other" path when typed into', () => { - cy.contains('Other').find('input[type="text"]').type('/absolute/path/to/foo.js') - .should('have.value', '/absolute/path/to/foo.js') - }) - - describe('when editor is not selected', () => { - it('disables submit button', () => { - cy.contains('Set preference and open file') - .should('have.class', 'is-disabled') - .click() - - cy.wrap(getRunner().emit).should('not.to.be.calledWith', 'set:user:editor') - cy.wrap(getRunner().emit).should('not.to.be.calledWith', 'open:file') - }) - - it('shows validation message when hovering over submit button', () => { - cy.get('.editor-picker-modal .submit').trigger('mouseover') - cy.get('.cy-tooltip').last().should('have.text', 'Please select a preference') - }) - }) - - describe('when Other is selected but path is not entered', () => { - beforeEach(() => { - cy.contains('Other').click() - }) - - it('disables submit button', () => { - cy.contains('Set preference and open file') - .should('have.class', 'is-disabled') - .click() - - cy.wrap(getRunner().emit).should('not.to.be.calledWith', 'set:user:editor') - cy.wrap(getRunner().emit).should('not.to.be.calledWith', 'open:file') - }) - - it('shows validation message when hovering over submit button', () => { - cy.get('.editor-picker-modal .submit').trigger('mouseover') - cy.get('.cy-tooltip').last().should('have.text', 'Please enter the path for the "Other" editor') - }) - }) - - describe('when editor is set', () => { - beforeEach(() => { - cy.contains('Visual Studio Code').click() - cy.contains('Set preference and open file').click() - }) - - it('closes modal', function () { - cy.contains('Set preference and open file').should('not.exist') - }) - - it('emits set:user:editor', () => { - expect(getRunner().emit).to.be.calledWith('set:user:editor', availableEditors[4]) - }) - - it('opens file in selected editor', () => { - expect(getRunner().emit).to.be.calledWith('open:file', { - where: availableEditors[4], - ...file, - }) + expect(getRunner().emit).to.be.calledWith('open:file:unified') }) }) }) diff --git a/packages/reporter/src/hooks/hooks.tsx b/packages/reporter/src/hooks/hooks.tsx index df31e1aa57f4..f7e5031f33b7 100644 --- a/packages/reporter/src/hooks/hooks.tsx +++ b/packages/reporter/src/hooks/hooks.tsx @@ -12,7 +12,6 @@ import HookModel, { HookName } from './hook-model' import ArrowRightIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/arrow-right_x16.svg' import OpenIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/technology-code-editor_x16.svg' import OpenFileInIDE from '../lib/open-file-in-ide' -import FileOpener from '../lib/file-opener' export interface HookHeaderProps { model: HookModel @@ -30,18 +29,10 @@ export interface HookOpenInIDEProps { } const HookOpenInIDE = ({ invocationDetails }: HookOpenInIDEProps) => { - if ('__vite__' in window) { - return ( - - Open in IDE - - ) - } - return ( - + Open in IDE - + ) } diff --git a/packages/reporter/src/lib/file-name-opener.tsx b/packages/reporter/src/lib/file-name-opener.tsx index e0e8dad5eb8f..17e60080cdc2 100644 --- a/packages/reporter/src/lib/file-name-opener.tsx +++ b/packages/reporter/src/lib/file-name-opener.tsx @@ -1,12 +1,11 @@ import { observer } from 'mobx-react' import React from 'react' +import { FileDetails } from '@packages/types' // @ts-ignore import Tooltip from '@cypress/react-tooltip' -import { FileDetails } from '@packages/types' - -import FileOpener from './file-opener' import TextIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/document-text_x16.svg' +import OpenFileInIDE from './open-file-in-ide' interface Props { fileDetails: FileDetails @@ -14,18 +13,24 @@ interface Props { hasIcon?: boolean } +/** + * Renders a link-style element that presents a tooltip on hover + * and opens the file in an external editor when selected. + */ const FileNameOpener = observer((props: Props) => { const { displayFile, originalFile, line, column } = props.fileDetails return ( - - {props.hasIcon && ( - - )} - {displayFile || originalFile}{!!line && `:${line}`}{!!column && `:${column}`} - + + e.preventDefault()}> + {props.hasIcon && ( + + )} + {displayFile || originalFile}{!!line && `:${line}`}{!!column && `:${column}`} + + ) diff --git a/packages/reporter/src/lib/file-opener.tsx b/packages/reporter/src/lib/file-opener.tsx deleted file mode 100644 index 4f1363afe376..000000000000 --- a/packages/reporter/src/lib/file-opener.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { observer } from 'mobx-react' -import React, { ReactNode } from 'react' -import type { FileDetails } from '@packages/types' -import { GetUserEditorResult, Editor, FileOpener as Opener } from '@packages/ui-components' - -import events from './events' - -interface Props { - fileDetails: FileDetails - children: ReactNode - className?: string -} - -const openFile = (where: Editor, { absoluteFile: file, line, column }: FileDetails) => { - events.emit('open:file', { - where, - file, - line, - column, - }) -} - -const getUserEditor = (callback: (result: GetUserEditorResult) => any) => { - events.emit('get:user:editor', callback) -} - -const setUserEditor = (editor: Editor) => { - events.emit('set:user:editor', editor) -} - -const FileOpener = observer(({ fileDetails, children, className }: Props) => ( - - {children} - -)) - -export default FileOpener diff --git a/packages/reporter/src/lib/open-file-in-ide.tsx b/packages/reporter/src/lib/open-file-in-ide.tsx index c916bdb1fce1..cccd0124f4eb 100644 --- a/packages/reporter/src/lib/open-file-in-ide.tsx +++ b/packages/reporter/src/lib/open-file-in-ide.tsx @@ -1,23 +1,20 @@ import { observer } from 'mobx-react' import React from 'react' -// @ts-ignore -import Tooltip from '@cypress/react-tooltip' import type { FileDetails } from '@packages/types' import events from './events' interface Props { fileDetails: FileDetails className?: string - hasIcon?: boolean } +// Catches click events that bubble from children and emits open file events to +// be handled by the app. const OpenFileInIDE: React.FC = observer((props) => { return ( - - events.emit('open:file:unified', props.fileDetails)}> - {props.children} - - + events.emit('open:file:unified', props.fileDetails)}> + {props.children} + ) }) diff --git a/packages/reporter/src/runnables/runnable-header.tsx b/packages/reporter/src/runnables/runnable-header.tsx index ec8d768f78d7..355c5b150dda 100644 --- a/packages/reporter/src/runnables/runnable-header.tsx +++ b/packages/reporter/src/runnables/runnable-header.tsx @@ -3,11 +3,9 @@ import React, { Component, ReactElement } from 'react' import { StatsStore } from '../header/stats-store' import { formatDuration, getFilenameParts } from '../lib/util' -import OpenFileInIDE from '../lib/open-file-in-ide' import FileNameOpener from '../lib/file-name-opener' -import TextIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/document-text_x16.svg' -const renderRunnableHeader = (children: ReactElement) =>
    {children}
    +const renderRunnableHeader = (children: ReactElement) =>
    {children}
    interface RunnableHeaderProps { spec: Cypress.Cypress['spec'] @@ -52,22 +50,11 @@ class RunnableHeader extends Component { relativeFile: relativeSpecPath, } - const openInIde = '__vite__' in window - ? ( - - e.preventDefault()}> - - {fileDetails.displayFile || fileDetails.originalFile} - - - ) - : - return renderRunnableHeader( <> - {openInIde} + {Boolean(statsStore.duration) && ( - {formatDuration(statsStore.duration)} + {formatDuration(statsStore.duration)} )} , ) diff --git a/packages/reporter/src/runnables/runnables.tsx b/packages/reporter/src/runnables/runnables.tsx index 8fff6084d542..a8885f88545f 100644 --- a/packages/reporter/src/runnables/runnables.tsx +++ b/packages/reporter/src/runnables/runnables.tsx @@ -11,7 +11,7 @@ import { RunnablesStore, RunnableArray } from './runnables-store' import statsStore, { StatsStore } from '../header/stats-store' import { Scroller } from '../lib/scroller' import { AppState } from '../lib/app-state' -import FileOpener from '../lib/file-opener' +import OpenFileInIDE from '../lib/open-file-in-ide' import OpenIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/technology-code-editor_x16.svg' import StudioIcon from '-!react-svg-loader!@packages/frontend-shared/src/assets/icons/object-magic-wand-dark-mode_x16.svg' @@ -49,15 +49,19 @@ const RunnablesEmptyState = ({ spec, eventManager = events }: RunnablesEmptyStat

    Cypress could not detect tests in this file.

    { !isAllSpecs && ( <> - -

    Open file in IDE

    -
    + { + event.preventDefault() + }}> +

    Open file in IDE

    +
    +

    Write a test using your preferred text editor.

    Create test with Cypress Studio

    Use an interactive tool to author a test right here.

    diff --git a/system-tests/projects/e2e/cypress/support/util.js b/system-tests/projects/e2e/cypress/support/util.js index e858ae9da5e5..eb1e6a0d38cd 100644 --- a/system-tests/projects/e2e/cypress/support/util.js +++ b/system-tests/projects/e2e/cypress/support/util.js @@ -28,20 +28,6 @@ export const verify = (ctx, options) => { const fileRegex = new RegExp(`${Cypress.spec.relative}:${line}:${column}`) it(`✓ VERIFY`, function () { - const runnerWs = window.top.ws - - cy.stub(window.top.ws, 'emit').callThrough().withArgs('get:user:editor') - .yields({ - preferredOpener: { - id: 'foo-editor', - name: 'Foo', - binary: 'foo-editor', - isOther: false, - }, - }) - - window.top.ws.emit.callThrough().withArgs('open:file') - cy.wrap(Cypress.$(window.top.document.body)) .find('.reporter') .contains(`FAIL - ${getTitle(ctx)}`) @@ -70,12 +56,6 @@ export const verify = (ctx, options) => { .should('not.include.text', '__stackReplacementMarker') cy.contains('.runnable-err-stack-trace .runnable-err-file-path', openInIdePath.relative) - .click() - .should(() => { - expect(runnerWs.emit).to.be.calledWithMatch('open:file', { - file: openInIdePath.absolute, - }) - }) cy .get('.test-err-code-frame .runnable-err-file-path') @@ -85,14 +65,7 @@ export const verify = (ctx, options) => { // code frames will show `fail(this,()=>` as the 1st line cy.get('.test-err-code-frame pre span').should('include.text', 'fail(this,()=>') - cy.contains('.test-err-code-frame .runnable-err-file-path span', openInIdePath.relative) - .click() - .should(() => { - expect(runnerWs.emit.withArgs('open:file')).to.be.calledTwice - expect(runnerWs.emit).to.be.calledWithMatch('open:file', { - file: openInIdePath.absolute, - }) - }) + cy.contains('.test-err-code-frame .runnable-err-file-path', openInIdePath.relative) }) }) } diff --git a/system-tests/projects/runner-e2e-specs/cypress.config.js b/system-tests/projects/runner-e2e-specs/cypress.config.js new file mode 100644 index 000000000000..d12d96d7e3e0 --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress.config.js @@ -0,0 +1,5 @@ +module.exports = { + e2e: { + supportFile: false, + }, +} diff --git a/system-tests/projects/runner-e2e-specs/cypress/e2e/errors/assertions.cy.js b/system-tests/projects/runner-e2e-specs/cypress/e2e/errors/assertions.cy.js new file mode 100644 index 000000000000..5c1549fae31a --- /dev/null +++ b/system-tests/projects/runner-e2e-specs/cypress/e2e/errors/assertions.cy.js @@ -0,0 +1,13 @@ +describe('assertion failures', () => { + it('with expect().', () => { + expect('actual').to.equal('expected') + }) + + it('with assert()', () => { + assert(false, 'should be true') + }) + + it('with assert.()', () => { + assert.equal('actual', 'expected') + }) +}) From 46b2a676a9a7a2c918d2a4a293dd80e41aac3428 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Mon, 31 Jan 2022 13:03:59 -0600 Subject: [PATCH 08/10] fix bad merge overrides- GOOD CATCH TYLER! --- npm/cypress-schematic/sandbox/package.json | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/npm/cypress-schematic/sandbox/package.json b/npm/cypress-schematic/sandbox/package.json index 97dd845d8b85..6d4647bd8147 100644 --- a/npm/cypress-schematic/sandbox/package.json +++ b/npm/cypress-schematic/sandbox/package.json @@ -10,22 +10,22 @@ "test": "ng test" }, "dependencies": { - "@angular/animations": "~12.0.0", - "@angular/common": "~12.0.0", - "@angular/compiler": "~12.0.0", - "@angular/core": "~12.0.0", - "@angular/forms": "~12.0.0", - "@angular/platform-browser": "~12.0.0", - "@angular/platform-browser-dynamic": "~12.0.0", - "@angular/router": "~12.0.0", + "@angular/animations": "~13.1.3", + "@angular/common": "~13.1.3", + "@angular/compiler": "~13.1.3", + "@angular/core": "~13.1.3", + "@angular/forms": "~13.1.3", + "@angular/platform-browser": "~13.1.3", + "@angular/platform-browser-dynamic": "~13.1.3", + "@angular/router": "~13.1.3", "rxjs": "~6.6.0", "tslib": "^2.1.0", "zone.js": "~0.11.4" }, "devDependencies": { - "@angular-devkit/build-angular": "~12.0.0", - "@angular/cli": "~12.0.0", - "@angular/compiler-cli": "~12.0.0", + "@angular-devkit/build-angular": "~13.1.4", + "@angular/cli": "~13.1.4", + "@angular/compiler-cli": "~13.1.3", "@types/jasmine": "~3.6.0", "@types/node": "^12.11.1", "jasmine-core": "~3.7.0", @@ -34,6 +34,6 @@ "karma-coverage": "~2.0.3", "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", - "typescript": "~4.2.3" + "typescript": "~4.5.5" } } From 807b3e38a3cff148d64c8cd4d8ea44cda7dc4845 Mon Sep 17 00:00:00 2001 From: Jess Date: Mon, 31 Jan 2022 16:20:49 -0500 Subject: [PATCH 09/10] feat: styling snapshots (#19972) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: adding styled snapshots * fixing type errors * fixing tests * test: make the content of tests visible Co-authored-by: Barthélémy Ledoux Co-authored-by: ElevateBart --- packages/app/cypress/support/fixtures.ts | 5 +- .../app/src/runner/SnapshotButtonGroup.vue | 5 - .../app/src/runner/SnapshotChangeState.vue | 40 ------- .../app/src/runner/SnapshotControls.cy.tsx | 101 +++++++++--------- packages/app/src/runner/SnapshotControls.vue | 96 +++++++++++++---- .../src/runner/SnapshotHighlightControls.vue | 65 +++++------ packages/app/src/runner/SnapshotMessage.vue | 39 ------- packages/app/src/runner/SnapshotToggle.cy.tsx | 52 +++++++++ packages/app/src/runner/SnapshotToggle.vue | 43 ++++++++ packages/app/src/runner/iframe-model.ts | 14 +-- packages/app/src/runner/index.ts | 3 + packages/app/src/runner/snapshot-store.ts | 32 ++---- .../frontend-shared/src/components/Switch.vue | 9 +- .../frontend-shared/src/locales/en-US.json | 11 ++ 14 files changed, 281 insertions(+), 234 deletions(-) delete mode 100644 packages/app/src/runner/SnapshotButtonGroup.vue delete mode 100644 packages/app/src/runner/SnapshotChangeState.vue delete mode 100644 packages/app/src/runner/SnapshotMessage.vue create mode 100644 packages/app/src/runner/SnapshotToggle.cy.tsx create mode 100644 packages/app/src/runner/SnapshotToggle.vue diff --git a/packages/app/cypress/support/fixtures.ts b/packages/app/cypress/support/fixtures.ts index d8effea942ba..1ce19d99fe26 100644 --- a/packages/app/cypress/support/fixtures.ts +++ b/packages/app/cypress/support/fixtures.ts @@ -10,12 +10,13 @@ export const autSnapshot: AutSnapshot = { y: 0, }, highlightAttr: '', - snapshots: [], + // @ts-ignore + snapshots: [{ name: 'Before' }, { name: 'After' }], htmlAttrs: {}, viewportHeight: 500, viewportWidth: 500, url: 'http://localhost:3000', body: { - get: () => null, + get: () => {}, }, } diff --git a/packages/app/src/runner/SnapshotButtonGroup.vue b/packages/app/src/runner/SnapshotButtonGroup.vue deleted file mode 100644 index 141e8635f131..000000000000 --- a/packages/app/src/runner/SnapshotButtonGroup.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/packages/app/src/runner/SnapshotChangeState.vue b/packages/app/src/runner/SnapshotChangeState.vue deleted file mode 100644 index 950392f79d04..000000000000 --- a/packages/app/src/runner/SnapshotChangeState.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - - - diff --git a/packages/app/src/runner/SnapshotControls.cy.tsx b/packages/app/src/runner/SnapshotControls.cy.tsx index 49c42ba1eef6..54047cb66719 100644 --- a/packages/app/src/runner/SnapshotControls.cy.tsx +++ b/packages/app/src/runner/SnapshotControls.cy.tsx @@ -3,13 +3,17 @@ import { autSnapshot } from '../../cypress/support/fixtures' import { useSnapshotStore } from './snapshot-store' import { createEventManager, createTestAutIframe } from '../../cypress/component/support/ctSupport' -// function createTestAutIframe () { -// return new class { -// removeHighlights () {} -// } -// } +const snapshotWithSnapshots = { ...autSnapshot } +const snapshotPinned = { ...autSnapshot, snapshots: [] } + +const snapshotControlsSelector = '[data-testid=snapshot-controls]' +const unpinButtonSelector = '[data-testid=unpin]' + +describe('SnapshotControls', { viewportHeight: 200, viewportWidth: 500 }, () => { + afterEach(() => { + cy.wait(100).percySnapshot() + }) -describe('SnapshotControls', () => { const mountSnapshotControls = ( eventManager = createEventManager(), autIframe = createTestAutIframe(), @@ -24,82 +28,73 @@ describe('SnapshotControls', () => { it('renders nothing when messageTitle is undefined', () => { mountSnapshotControls() - cy.get('[data-cy="snapshot-highlight-controls"]').should('not.exist') - cy.get('[data-cy="snapshot-message"]').should('not.exist') - cy.get('[data-cy="snapshot-change-state"]').should('not.exist') + cy.get(snapshotControlsSelector).should('not.exist') }) - it('renders snapshot title when one is pinned', () => { + it('renders the "pinned" snapshot title', () => { mountSnapshotControls() const snapshotStore = useSnapshotStore() - snapshotStore.pinSnapshot(autSnapshot) - cy.get('[data-cy="snapshot-message"]').contains('DOM Snapshot') - cy.get('[data-cy="snapshot-message"]').contains('(pinned)') + snapshotStore.pinSnapshot(snapshotPinned) + cy.get('body') + .findByText('Pinned') + .should('be.visible') }) - it('renders snapshot pinned status', () => { + it('pinned snapshots should not be dismissible', () => { mountSnapshotControls() const snapshotStore = useSnapshotStore() - snapshotStore.pinSnapshot(autSnapshot) - cy.get('[data-cy="snapshot-message"]').contains('DOM Snapshot') - cy.get('[data-cy="snapshot-message"]').contains('(pinned)') - .then(() => { - snapshotStore.unpinSnapshot() - cy.get('[data-cy="snapshot-message"]').should('not.contain', '(pinned)') - }) + snapshotStore.pinSnapshot(snapshotPinned) + cy.get('body') + .findByText('Pinned') + .should('be.visible') + .get(unpinButtonSelector) + .should('not.exist') }) it('clears snapshot message', () => { mountSnapshotControls() const snapshotStore = useSnapshotStore() - snapshotStore.pinSnapshot(autSnapshot) - cy.then(() => cy.get('[data-cy="snapshot-message"]').should('exist')) - .then(() => snapshotStore.clearMessage()) - .get('[data-cy="snapshot-message"]').should('not.exist') - }) - - it('shows snapshot with custom message', () => { - mountSnapshotControls() - const message = 'This is a custom message' - const snapshotStore = useSnapshotStore() - - snapshotStore.showSnapshot(message) - cy.get('[data-cy="snapshot-message"]').contains(message) + snapshotStore.pinSnapshot(snapshotWithSnapshots) + cy.get('body') + .findByText('Pinned') + .should('be.visible') + .get(unpinButtonSelector) + .click({ force: true }) + .get('body') + .findByText('Pinned') + .should('not.exist') }) it('does not show highlight controls if no element present on snapshot', () => { mountSnapshotControls() const snapshotStore = useSnapshotStore() - snapshotStore.pinSnapshot(autSnapshot) - cy.get('[data-cy="snapshot-highlight-controls"]').should('not.exist') + snapshotStore.pinSnapshot(snapshotWithSnapshots) + cy.get('body').findByText('Highlights').should('not.exist') }) it('toggles highlight controls if snapshot has an element', () => { const snapshotStore = useSnapshotStore() const eventManager = createEventManager() const autIframe = createTestAutIframe() - const removeHighlights = cy.stub(autIframe, 'removeHighlights') // we don't have an iframe-model since this is a CT test, but we can // simulate it by registering the same unpin:snapshot event it does. - eventManager.on('unpin:snapshot', () => snapshotStore.unpinSnapshot()) - snapshotStore.pinSnapshot({ ...autSnapshot, $el: 'some element' }) + eventManager.on('unpin:snapshot', () => snapshotStore.$reset()) + + // debugger + // console.log('snapshotWithSnapshots', snapshotWithSnapshots) + snapshotStore.pinSnapshot({ ...snapshotWithSnapshots, $el: document.body }) mountSnapshotControls(eventManager, autIframe) - cy.get('[data-cy="snapshot-highlight-controls"]').should('exist') - cy.get('[data-cy="toggle-snapshot-highlights"]').as('toggle') - cy.get('@toggle').should('have.attr', 'title', 'Hide highlights') - cy.get('@toggle').click().then(() => { - expect(removeHighlights).to.have.been.calledOnce - }) - - cy.get('@toggle').should('have.attr', 'title', 'Show highlights') - cy.get('[data-cy="unpin-snapshot"]').click() - cy.get('[data-cy="snapshot-highlight-controls"]').should('not.exist') + cy.get('body') + .findByText('Highlights') + .should('be.visible') + .findByLabelText('Toggle highlights') + .click({ force: true }) }) it('shows running test error', () => { @@ -107,7 +102,9 @@ describe('SnapshotControls', () => { const snapshotStore = useSnapshotStore() snapshotStore.setTestsRunningError() - cy.get('[data-cy="snapshot-message"]').contains('Cannot show Snapshot while tests are running') + cy.get('body') + .findByText('Cannot show Snapshot while tests are running') + .should('be.visible') }) it('shows snapshot missing error', () => { @@ -115,6 +112,8 @@ describe('SnapshotControls', () => { const snapshotStore = useSnapshotStore() snapshotStore.setMissingSnapshotMessage() - cy.get('[data-cy="snapshot-message"]').contains('The snapshot is missing. Displaying current state of the DOM.') + cy.get('body') + .findByText('The snapshot is missing. Displaying current state of the DOM.') + .should('be.visible') }) }) diff --git a/packages/app/src/runner/SnapshotControls.vue b/packages/app/src/runner/SnapshotControls.vue index 4dbb429520fe..a09e07858893 100644 --- a/packages/app/src/runner/SnapshotControls.vue +++ b/packages/app/src/runner/SnapshotControls.vue @@ -1,23 +1,43 @@ @@ -25,11 +45,10 @@ diff --git a/packages/app/src/runner/SnapshotHighlightControls.vue b/packages/app/src/runner/SnapshotHighlightControls.vue index e595ff9d3125..231b868f983b 100644 --- a/packages/app/src/runner/SnapshotHighlightControls.vue +++ b/packages/app/src/runner/SnapshotHighlightControls.vue @@ -1,46 +1,37 @@ - - diff --git a/packages/app/src/runner/SnapshotMessage.vue b/packages/app/src/runner/SnapshotMessage.vue deleted file mode 100644 index 749c6a5a4a77..000000000000 --- a/packages/app/src/runner/SnapshotMessage.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - diff --git a/packages/app/src/runner/SnapshotToggle.cy.tsx b/packages/app/src/runner/SnapshotToggle.cy.tsx new file mode 100644 index 000000000000..d0997833b638 --- /dev/null +++ b/packages/app/src/runner/SnapshotToggle.cy.tsx @@ -0,0 +1,52 @@ +import SnapshotToggle from './SnapshotToggle.vue' + +describe('', () => { + it('renders two segments', () => { + const messages = [{ text: '1', id: '1' }, { text: '2', id: '2' }] + + cy.mount(() => ()) + + cy.percySnapshot() + .get('body') + .findByText('2') + .click() + .parent() + .findByText('1') + .click() + .percySnapshot() + }) + + it('renders longer text', () => { + const messages = [{ text: 'Request', id: '1' }, { text: 'Response', id: '2' }] + + cy.mount(() => ()) + + cy.percySnapshot() + .get('body') + .findByText('Request') + .click() + .parent() + .findByText('Response') + .click() + .percySnapshot() + }) + + it('emits a select event with the active message', () => { + const messages = [{ text: 'Request', id: '1' }, { text: 'Response', id: '2' }] + + const onSelectSpy = cy.spy().as('onSelect') + + cy.mount(() => ()) + + cy.get('body') + .findByText('Request') + .click() + .get('@onSelect') + .should('have.been.calledWith', { message: messages[0], idx: 0 }) + .get('body') + .findByText('Response') + .click() + .get('@onSelect') + .should('have.been.calledWith', { message: messages[1], idx: 1 }) + }) +}) diff --git a/packages/app/src/runner/SnapshotToggle.vue b/packages/app/src/runner/SnapshotToggle.vue new file mode 100644 index 000000000000..947e080bfa5a --- /dev/null +++ b/packages/app/src/runner/SnapshotToggle.vue @@ -0,0 +1,43 @@ + + + diff --git a/packages/app/src/runner/iframe-model.ts b/packages/app/src/runner/iframe-model.ts index a4f4ff41111e..2e2e4fcb13f0 100644 --- a/packages/app/src/runner/iframe-model.ts +++ b/packages/app/src/runner/iframe-model.ts @@ -1,6 +1,7 @@ import { useSnapshotStore } from './snapshot-store' import { useAutStore } from '../store' import type { EventManager } from './event-manager' +import { defaultMessages } from '@cy/i18n' export interface AutSnapshot { id?: number @@ -73,7 +74,6 @@ export class IframeModel { } _beforeRun = () => { - const snapshotStore = useSnapshotStore() const autStore = useAutStore() autStore.setIsLoading(false) @@ -82,7 +82,6 @@ export class IframeModel { this.studio.selectorPlaygroundModel.setEnabled(false) this._reset() - snapshotStore.clearMessage() } _afterRun = () => { @@ -236,17 +235,14 @@ export class IframeModel { } _unpinSnapshot = () => { - const snapshotStore = useSnapshotStore() - - snapshotStore.unpinSnapshot() + useSnapshotStore().$reset() } _studioOpenError () { const snapshotStore = useSnapshotStore() snapshotStore.setMessage( - 'Cannot show Snapshot while creating commands in Studio', - 'warning', + defaultMessages.runner.snapshot.studioActiveError, ) } @@ -278,8 +274,6 @@ export class IframeModel { this.intervalId = undefined this.originalState = undefined - const snapshotStore = useSnapshotStore() - - snapshotStore.setSnapshotPinned(false) + useSnapshotStore().$reset() } } diff --git a/packages/app/src/runner/index.ts b/packages/app/src/runner/index.ts index a92b9ee78757..35b26688d817 100644 --- a/packages/app/src/runner/index.ts +++ b/packages/app/src/runner/index.ts @@ -26,6 +26,7 @@ import { EventManager } from './event-manager' import { client } from '@packages/socket/lib/browser' import { decodeBase64Unicode } from '@packages/frontend-shared/src/utils/base64' import type { AutomationElementId } from '@packages/types/src' +import { useSnapshotStore } from './snapshot-store' let _eventManager: EventManager | undefined @@ -162,6 +163,8 @@ function getSpecUrl (namespace: string, specSrc: string) { * or re-running the current spec. */ function teardownSpec () { + useSnapshotStore().$reset() + return getEventManager().teardown(getMobxRunnerStore()) } diff --git a/packages/app/src/runner/snapshot-store.ts b/packages/app/src/runner/snapshot-store.ts index a1bd16856ac1..ad5542f90d68 100644 --- a/packages/app/src/runner/snapshot-store.ts +++ b/packages/app/src/runner/snapshot-store.ts @@ -1,13 +1,10 @@ import { defineStore } from 'pinia' import type { AutSnapshot } from './iframe-model' import type { AutIframe } from './aut-iframe' - -export type SnapshotMessageType = 'info' | 'warning' | 'pinned' +import { defaultMessages } from '@cy/i18n' interface SnapshotStoreState { messageTitle?: string - messageDescription?: 'pinned' | string - messageType?: SnapshotMessageType snapshotProps?: AutSnapshot isSnapshotPinned: boolean snapshot?: { @@ -21,7 +18,6 @@ export const useSnapshotStore = defineStore({ state: (): SnapshotStoreState => { return { messageTitle: undefined, - messageDescription: undefined, isSnapshotPinned: false, snapshot: undefined, snapshotProps: undefined, @@ -33,9 +29,7 @@ export const useSnapshotStore = defineStore({ }, pinSnapshot (snapshotProps: AutSnapshot) { - this.messageTitle = 'DOM Snapshot' - this.messageDescription = 'pinned' - this.messageType = 'info' + this.messageTitle = defaultMessages.runner.snapshot.pinnedTitle this.isSnapshotPinned = true this.snapshotProps = snapshotProps this.snapshot = { @@ -46,20 +40,14 @@ export const useSnapshotStore = defineStore({ clearMessage () { this.messageTitle = undefined - this.messageDescription = undefined - this.messageType = undefined }, unpinSnapshot () { - this.isSnapshotPinned = false - this.messageTitle = 'DOM Snapshot' - this.messageDescription = undefined + this.$reset() }, - showSnapshot (messageDescription?: string) { - this.messageTitle = 'DOM Snapshot' - this.messageDescription = messageDescription - this.messageType = undefined + showSnapshot (messageDescription: string = defaultMessages.runner.snapshot.defaultTitle) { + this.messageTitle = messageDescription }, toggleHighlights (autIframe: AutIframe) { @@ -104,19 +92,15 @@ export const useSnapshotStore = defineStore({ }, setTestsRunningError () { - this.messageTitle = 'Cannot show Snapshot while tests are running' - this.messageType = 'warning' + this.messageTitle = defaultMessages.runner.snapshot.testsRunningError }, - setMessage (messageTitle: string, messageType: SnapshotMessageType) { + setMessage (messageTitle: string) { this.messageTitle = messageTitle - this.messageType = messageType }, setMissingSnapshotMessage () { - this.messageTitle = 'The snapshot is missing. Displaying current state of the DOM.' - this.messageDescription = undefined - this.messageType = 'warning' + this.messageTitle = defaultMessages.runner.snapshot.snapshotMissingError }, }, }) diff --git a/packages/frontend-shared/src/components/Switch.vue b/packages/frontend-shared/src/components/Switch.vue index f2cb274e7ca9..7d8f99e988d7 100644 --- a/packages/frontend-shared/src/components/Switch.vue +++ b/packages/frontend-shared/src/components/Switch.vue @@ -1,14 +1,16 @@