From 166b69414c5e347ef825c121330b0b561a4caa3b Mon Sep 17 00:00:00 2001 From: Adam Stone-Lord Date: Sun, 18 Dec 2022 21:42:14 -0500 Subject: [PATCH] feat: create from React component (#25168) * feat: server logic for create from React component (#24881) Co-authored-by: Ryan Manuel Co-authored-by: Lachlan Miller * fix: add default export detection (#24954) Co-authored-by: astone123 * update cache * update yarn.lock to fix builds * fix: compilation with webpack preprocessor * feat: create from React component UI (#24982) * feat: WIP server logic for create from React component * feat: add more tests; error handling * feat: WIP create from React UI * feat: PR feedback [run CI] * feat: try committing snapshot cache changes [run ci] * feat: try re-generating snapshot [run ci] * fix build * regenerate cache on darwin * update caches * Revert "feat: try re-generating snapshot [run ci]" This reverts commit d763e1f7a43862b5ad9cf9cd67c3c9f1c69124cb. * fix typing error * types * fix test * chore: try using react-docgen@6.0.0-alpha.3 * update test * regen linux snapshot * update snapshots for darwin * re-gen linux snapshot * yarn install * update snapshots * update snapshot metadata * update snapshots due to babel deps changing slightly * make react docgen a dep * update tests * revert * snapshots again?? * revert * update * update * try change snapshot * change snap * update snap * feat: remove unnecessary ts-ignore * feat: add more test cases * feat: create CodegenActions; other minor refactors * feat: continue UI work * feat: ignore config and Cypress-related files * feat: PR feedback * update Vue component link * merge in default export work * consolidate graphql queries * other misc feedback * use network-only policy to fetch files; include cypress/ dir for code gen candidates; fix type error * add basic e2e test * fix app integration tests * refactor and fix app component and webpack dev server tests * add error state; fix unit tests [skip ci] * simplify generator show logic [skip ci] * more testing * fix types * style updates [skip ci] * fix error state [skip ci] * fix list padding [skip ci] * use slots (#25079) * add more tests; fix unit tests * fix types * fix test describe * add percy snapshots for new list * update trouble rendering banner link [skip ci] * use collapsible component * use button for component list items * fix tests * build binaries * revert changes to circle config * remove eslintignore and extra loading div [skip ci] because we know it will fail * revert changes to framework glob patterns [skip ci] Co-authored-by: Ryan Manuel Co-authored-by: Lachlan Miller * feat: pass parser options to allow parsing of tsx files (#25145) * fix create from component e2e test * build binaries [run ci] * fix component tests [run ci] * regen windows snapshot Co-authored-by: Ryan Manuel Co-authored-by: Lachlan Miller Co-authored-by: Zachary Williams --- .circleci/workflows.yml | 10 +- .../cypress/e2e/react.cy.ts | 2 + .../__snapshots__/compilation.spec.js | 2 +- .../unit/cross-origin-callback-loader.spec.ts | 11 - .../cypress/e2e/create-from-component.cy.ts | 192 +- packages/app/cypress/e2e/specs.cy.ts | 22 +- packages/app/src/paths.ts | 4 + .../src/runner/SpecRunnerHeaderOpenMode.vue | 4 +- packages/app/src/specs/CreateSpecModal.cy.tsx | 280 +- packages/app/src/specs/CreateSpecModal.vue | 13 +- packages/app/src/specs/InlineSpecListTree.vue | 5 +- packages/app/src/specs/SpecsList.vue | 3 +- .../src/specs/generators/EmptyGenerator.vue | 3 +- .../generators/ExpandableFileChooser.cy.tsx | 233 + .../generators/ExpandableFileChooser.vue | 157 + .../generators/ExpandableFileList.cy.tsx | 84 + .../specs/generators/ExpandableFileList.vue | 80 + .../src/specs/generators/FileChooser.cy.tsx | 11 +- .../app/src/specs/generators/FileChooser.vue | 13 +- .../component/ComponentGenerator.tsx | 14 - .../component/ReactComponentGenerator.tsx | 12 + .../ReactComponentGeneratorStepOne.vue | 188 + .../component/ReactComponentList.cy.tsx | 59 + .../component/ReactComponentList.vue | 87 + .../component/VueComponentGenerator.tsx | 12 + ...e.vue => VueComponentGeneratorStepOne.vue} | 50 +- .../src/specs/generators/component/index.ts | 4 +- packages/app/src/specs/generators/index.ts | 5 +- packages/app/src/specs/generators/types.ts | 3 +- .../destructure-require-ts/output.ts | 1 - packages/data-context/package.json | 2 + packages/data-context/src/DataActions.ts | 6 + .../src/actions/CodegenActions.ts | 234 + .../src/actions/ProjectActions.ts | 112 +- packages/data-context/src/actions/index.ts | 1 + .../data-context/src/codegen/spec-options.ts | 69 +- .../data-context/src/codegen/templates.ts | 1 + .../react-component/react-component.ejs | 13 + .../templates/vue-component/vue-component.ejs | 2 +- .../src/sources/ProjectDataSource.ts | 20 +- .../src/sources/migration/codegen.ts | 1 + .../src/sources/migration/utils.ts | 4 +- .../test/unit/actions/CodegenActions.spec.ts | 135 + .../test/unit/actions/project/LoginForm.tsx | 21 + .../project/counter-arrow-function.jsx | 7 + .../unit/actions/project/counter-class.jsx | 20 + .../unit/actions/project/counter-default.tsx | 11 + .../actions/project/counter-functional.jsx | 7 + .../test/unit/actions/project/counter-hoc.jsx | 11 + .../counter-mixed-multiple-components.tsx | 11 + .../project/counter-multiple-components.jsx | 11 + .../project/counter-separate-exports.jsx | 17 + .../test/unit/actions/project/counter.tsx | 9 + .../actions/project/default-anonymous.jsx | 1 + .../unit/actions/project/default-class.jsx | 7 + .../unit/actions/project/default-function.jsx | 3 + .../actions/project/default-specifier.jsx | 3 + .../test/unit/actions/project/empty.jsx | 0 .../unit/actions/project/export-alias.jsx | 3 + .../test/unit/codegen/code-generator.spec.ts | 101 +- .../test/unit/codegen/spec-options.spec.ts | 66 +- .../support/mock-graphql/stubgql-Mutation.ts | 21 + .../src/assets/icons/puzzle-piece_x16.svg | 3 + .../frontend-shared/src/locales/en-US.json | 2 + packages/graphql/schemas/schema.graphql | 32 +- .../enumTypes/gql-FileExtensionEnum.ts | 2 +- .../objectTypes/gql-CurrentProject.ts | 6 + .../schemaTypes/objectTypes/gql-Mutation.ts | 18 +- .../gql-ReactComponentDescriptor.ts | 15 + .../objectTypes/gql-ReactComponentResponse.ts | 17 + .../src/schemaTypes/objectTypes/index.ts | 2 + .../__snapshots__/spec_isolation_spec.js | 6824 ++++++++--------- .../cypress-custom-spec-pattern.config.ts | 9 + .../projects/no-specs/cypress.config.ts | 1 - .../no-specs/cypress/support/commands.js | 25 + .../no-specs/cypress/support/component.js | 27 + .../projects/no-specs/src/Invalid.jsx | 4 + system-tests/test/testConfigOverrides_spec.ts | 2 +- .../cache/darwin/snapshot-meta.json | 630 +- .../cache/linux/snapshot-meta.json | 735 +- .../cache/win32/snapshot-meta.json | 729 +- tooling/v8-snapshot/package.json | 26 +- .../v8-snapshot/src/setup/force-no-rewrite.ts | 1 + yarn.lock | 493 +- 84 files changed, 7568 insertions(+), 4529 deletions(-) create mode 100644 packages/app/src/specs/generators/ExpandableFileChooser.cy.tsx create mode 100644 packages/app/src/specs/generators/ExpandableFileChooser.vue create mode 100644 packages/app/src/specs/generators/ExpandableFileList.cy.tsx create mode 100644 packages/app/src/specs/generators/ExpandableFileList.vue delete mode 100644 packages/app/src/specs/generators/component/ComponentGenerator.tsx create mode 100644 packages/app/src/specs/generators/component/ReactComponentGenerator.tsx create mode 100644 packages/app/src/specs/generators/component/ReactComponentGeneratorStepOne.vue create mode 100644 packages/app/src/specs/generators/component/ReactComponentList.cy.tsx create mode 100644 packages/app/src/specs/generators/component/ReactComponentList.vue create mode 100644 packages/app/src/specs/generators/component/VueComponentGenerator.tsx rename packages/app/src/specs/generators/component/{ComponentGeneratorStepOne.vue => VueComponentGeneratorStepOne.vue} (79%) create mode 100644 packages/data-context/src/actions/CodegenActions.ts create mode 100644 packages/data-context/src/codegen/templates/react-component/react-component.ejs create mode 100644 packages/data-context/test/unit/actions/CodegenActions.spec.ts create mode 100644 packages/data-context/test/unit/actions/project/LoginForm.tsx create mode 100644 packages/data-context/test/unit/actions/project/counter-arrow-function.jsx create mode 100644 packages/data-context/test/unit/actions/project/counter-class.jsx create mode 100644 packages/data-context/test/unit/actions/project/counter-default.tsx create mode 100644 packages/data-context/test/unit/actions/project/counter-functional.jsx create mode 100644 packages/data-context/test/unit/actions/project/counter-hoc.jsx create mode 100644 packages/data-context/test/unit/actions/project/counter-mixed-multiple-components.tsx create mode 100644 packages/data-context/test/unit/actions/project/counter-multiple-components.jsx create mode 100644 packages/data-context/test/unit/actions/project/counter-separate-exports.jsx create mode 100644 packages/data-context/test/unit/actions/project/counter.tsx create mode 100644 packages/data-context/test/unit/actions/project/default-anonymous.jsx create mode 100644 packages/data-context/test/unit/actions/project/default-class.jsx create mode 100644 packages/data-context/test/unit/actions/project/default-function.jsx create mode 100644 packages/data-context/test/unit/actions/project/default-specifier.jsx create mode 100644 packages/data-context/test/unit/actions/project/empty.jsx create mode 100644 packages/data-context/test/unit/actions/project/export-alias.jsx create mode 100644 packages/frontend-shared/src/assets/icons/puzzle-piece_x16.svg create mode 100644 packages/graphql/src/schemaTypes/objectTypes/gql-ReactComponentDescriptor.ts create mode 100644 packages/graphql/src/schemaTypes/objectTypes/gql-ReactComponentResponse.ts create mode 100644 system-tests/projects/no-specs/cypress-custom-spec-pattern.config.ts create mode 100644 system-tests/projects/no-specs/cypress/support/commands.js create mode 100644 system-tests/projects/no-specs/cypress/support/component.js create mode 100644 system-tests/projects/no-specs/src/Invalid.jsx diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index f78ea29459e6..ddccc7091b80 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -28,7 +28,7 @@ mainBuildFilters: &mainBuildFilters only: - develop - /^release\/\d+\.\d+\.\d+$/ - - 'matth/fix/electron-video-performance' + - 'feature/create-from-react-component' # 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 @@ -37,7 +37,7 @@ macWorkflowFilters: &darwin-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ 'matth/fix/electron-video-performance', << pipeline.git.branch >> ] + - equal: [ 'feature/create-from-react-component', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -45,7 +45,7 @@ linuxArm64WorkflowFilters: &linux-arm64-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ 'matth/fix/electron-video-performance', << pipeline.git.branch >> ] + - equal: [ 'feature/create-from-react-component', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -63,7 +63,7 @@ windowsWorkflowFilters: &windows-workflow-filters when: or: - equal: [ develop, << pipeline.git.branch >> ] - - equal: [ 'matth/fix/electron-video-performance', << pipeline.git.branch >> ] + - equal: [ 'feature/create-from-react-component', << pipeline.git.branch >> ] - matches: pattern: /^release\/\d+\.\d+\.\d+$/ value: << pipeline.git.branch >> @@ -129,7 +129,7 @@ commands: - run: name: Check current branch to persist artifacts command: | - if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "matth/fix/electron-video-performance" ]]; then + if [[ "$CIRCLE_BRANCH" != "develop" && "$CIRCLE_BRANCH" != "release/"* && "$CIRCLE_BRANCH" != "feature/create-from-react-component" ]]; then echo "Not uploading artifacts or posting install comment for this branch." circleci-agent step halt fi diff --git a/npm/webpack-dev-server/cypress/e2e/react.cy.ts b/npm/webpack-dev-server/cypress/e2e/react.cy.ts index 310d8bfe66fc..d3def96214ca 100644 --- a/npm/webpack-dev-server/cypress/e2e/react.cy.ts +++ b/npm/webpack-dev-server/cypress/e2e/react.cy.ts @@ -129,6 +129,8 @@ for (const project of WEBPACK_REACT) { // 4. recreate spec, with same name as removed spec cy.findByTestId('new-spec-button').click() + cy.findByRole('button', { name: 'Create new empty spec' }).should('be.visible').click() + cy.findByRole('dialog').within(() => { cy.get('input').clear().type('src/App.cy.jsx') cy.contains('button', 'Create spec').click() diff --git a/npm/webpack-preprocessor/__snapshots__/compilation.spec.js b/npm/webpack-preprocessor/__snapshots__/compilation.spec.js index 20c347c230c1..9d8868f8b77c 100644 --- a/npm/webpack-preprocessor/__snapshots__/compilation.spec.js +++ b/npm/webpack-preprocessor/__snapshots__/compilation.spec.js @@ -1,6 +1,6 @@ exports['webpack preprocessor - e2e correctly preprocesses the file 1'] = ` it("is a test",(function(){expect(1).to.equal(1),expect(2).to.equal(2),expect(Math.min.apply(Math,[3,4])).to.equal(3)})); -//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXhhbXBsZV9zcGVjX291dHB1dC5qcyIsIm1hcHBpbmdzIjoiQUFBQUEsR0FBRyxhQUFhLFdBR2RDLE9BRmdCLEdBRU5DLEdBQUdDLE1BQU0sR0FDbkJGLE9BSG1CLEdBR1RDLEdBQUdDLE1BQU0sR0FDbkJGLE9BQU9HLEtBQUtDLElBQUwsTUFBQUQsS0FBWSxDQUFDLEVBQUcsS0FBS0YsR0FBR0MsTUFBTSIsInNvdXJjZXMiOlsid2VicGFjazovL0BjeXByZXNzL3dlYnBhY2stcHJlcHJvY2Vzc29yLy4vdGVzdC9fdGVzdC1vdXRwdXQvZXhhbXBsZV9zcGVjLmpzIl0sInNvdXJjZXNDb250ZW50IjpbIml0KCdpcyBhIHRlc3QnLCAoKSA9PiB7XG4gIGNvbnN0IFthLCBiXSA9IFsxLCAyXVxuXG4gIGV4cGVjdChhKS50by5lcXVhbCgxKVxuICBleHBlY3QoYikudG8uZXF1YWwoMilcbiAgZXhwZWN0KE1hdGgubWluKC4uLlszLCA0XSkpLnRvLmVxdWFsKDMpXG59KVxuIl0sIm5hbWVzIjpbIml0IiwiZXhwZWN0IiwidG8iLCJlcXVhbCIsIk1hdGgiLCJtaW4iXSwic291cmNlUm9vdCI6IiJ9 +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZXhhbXBsZV9zcGVjX291dHB1dC5qcyIsIm1hcHBpbmdzIjoiQUFBQUEsR0FBRyxhQUFhLFdBR2RDLE9BRmdCLEdBRU5DLEdBQUdDLE1BQU0sR0FDbkJGLE9BSG1CLEdBR1RDLEdBQUdDLE1BQU0sR0FDbkJGLE9BQU9HLEtBQUtDLElBQUcsTUFBUkQsS0FBWSxDQUFDLEVBQUcsS0FBS0YsR0FBR0MsTUFBTSIsInNvdXJjZXMiOlsid2VicGFjazovL0BjeXByZXNzL3dlYnBhY2stcHJlcHJvY2Vzc29yLy4vdGVzdC9fdGVzdC1vdXRwdXQvZXhhbXBsZV9zcGVjLmpzIl0sInNvdXJjZXNDb250ZW50IjpbIml0KCdpcyBhIHRlc3QnLCAoKSA9PiB7XG4gIGNvbnN0IFthLCBiXSA9IFsxLCAyXVxuXG4gIGV4cGVjdChhKS50by5lcXVhbCgxKVxuICBleHBlY3QoYikudG8uZXF1YWwoMilcbiAgZXhwZWN0KE1hdGgubWluKC4uLlszLCA0XSkpLnRvLmVxdWFsKDMpXG59KVxuIl0sIm5hbWVzIjpbIml0IiwiZXhwZWN0IiwidG8iLCJlcXVhbCIsIk1hdGgiLCJtaW4iXSwic291cmNlUm9vdCI6IiJ9 ` exports['webpack preprocessor - e2e has less verbose syntax error 1'] = ` diff --git a/npm/webpack-preprocessor/test/unit/cross-origin-callback-loader.spec.ts b/npm/webpack-preprocessor/test/unit/cross-origin-callback-loader.spec.ts index 0e390956bc5f..06a334b48223 100644 --- a/npm/webpack-preprocessor/test/unit/cross-origin-callback-loader.spec.ts +++ b/npm/webpack-preprocessor/test/unit/cross-origin-callback-loader.spec.ts @@ -220,7 +220,6 @@ describe('./lib/cross-origin-callback-loader', () => { expectAddFileSource(store).to.equal(stripIndent` __cypressCrossOriginCallback = () => { const utils = require('../support/utils'); - utils.foo(); }`) }) @@ -230,9 +229,7 @@ describe('./lib/cross-origin-callback-loader', () => { `it('test', () => { cy.origin('http://www.foobar.com:3500', () => { require('../support/commands') - const utils = require('../support/utils') - const _ = require('lodash') }) })`, @@ -241,9 +238,7 @@ describe('./lib/cross-origin-callback-loader', () => { expectAddFileSource(store).to.equal(stripIndent` __cypressCrossOriginCallback = () => { require('../support/commands'); - const utils = require('../support/utils'); - const _ = require('lodash'); }`) }) @@ -270,9 +265,7 @@ describe('./lib/cross-origin-callback-loader', () => { `it('test', () => { cy.origin('http://www.foobar.com:3500', () => { const someVar = 'someValue' - const result = require('./fn')(someVar) - expect(result).to.equal('mutated someVar') }) })`, @@ -281,9 +274,7 @@ describe('./lib/cross-origin-callback-loader', () => { expectAddFileSource(store).to.equal(stripIndent` __cypressCrossOriginCallback = () => { const someVar = 'someValue'; - const result = require('./fn')(someVar); - expect(result).to.equal('mutated someVar'); }`) }) @@ -293,7 +284,6 @@ describe('./lib/cross-origin-callback-loader', () => { `it('test', () => { cy.origin('http://www.foobar.com:3500', { args: { foo: 'foo'}}, ({ foo }) => { const result = require('./fn')(foo) - expect(result).to.equal('mutated someVar') }) })`, @@ -304,7 +294,6 @@ describe('./lib/cross-origin-callback-loader', () => { foo }) => { const result = require('./fn')(foo); - expect(result).to.equal('mutated someVar'); }`) }) diff --git a/packages/app/cypress/e2e/create-from-component.cy.ts b/packages/app/cypress/e2e/create-from-component.cy.ts index 5771917cb9b3..162508b36a9b 100644 --- a/packages/app/cypress/e2e/create-from-component.cy.ts +++ b/packages/app/cypress/e2e/create-from-component.cy.ts @@ -1,10 +1,10 @@ import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json' import { getPathForPlatform } from '../../src/paths' -function validateCreateFromComponentCard (beforeEachFn: () => void, expectedSpecPath: string) { +function validateCreateFromVueComponentCard (beforeEachFn: () => void, expectedSpecPath: string) { beforeEach(beforeEachFn) - it('Shows create from component card for Vue projects with default spec patterns', () => { + it('Shows create from component card for Vue projects', () => { cy.get('@ComponentCard') .within(() => { cy.findByRole('button', { @@ -82,31 +82,175 @@ function validateCreateFromComponentCard (beforeEachFn: () => void, expectedSpec .should('have.attr', 'href', `#/specs/runner?file=${expectedSpecPath}`).click() }) - cy.findByText('', { timeout: 10000 }).should('be.visible') + cy.waitForSpecToFinish({ passCount: 1 }) + }) +} + +function validateCreateFromReactComponentCard (beforeEachFn: () => void, expectedSpecPath: string) { + beforeEach(beforeEachFn) + + it('Shows create from component card for React projects', () => { + cy.get('@ComponentCard') + .within(() => { + cy.findByRole('button', { + name: 'Create from component', + }).should('be.visible') + .and('not.be.disabled') + }) + }) + + it('Can be closed with the x button', () => { + cy.get('@ComponentCard').click() + + cy.findByRole('button', { name: 'Close' }).as('DialogCloseButton') + + cy.get('@DialogCloseButton').click() + cy.findByRole('dialog', { + name: 'Choose a component', + }).should('not.exist') + }) + + it('Lists files in the project', () => { + cy.get('@ComponentCard').click() + + cy.findByText('5 matches').should('be.visible') + + cy.findByText('App').should('be.visible') + cy.findByText('index').should('be.visible') + }) + + it('Allows for the user to search through their components', () => { + cy.get('@ComponentCard').click() + + cy.findByText('*.{js,jsx,tsx}').should('be.visible') + cy.findByText('5 matches').should('be.visible') + cy.findByLabelText('file-name-input').type('App') + + cy.findByText('App').should('be.visible') + cy.findByText('1 of 5 matches').should('be.visible') + cy.findByText('index').should('not.exist') + cy.findByText('component').should('not.exist') + }) + + it('shows \'No components found\' if there are no exported components', () => { + cy.get('@ComponentCard').click() + + cy.findByText('index').should('be.visible').click() + + cy.findByTestId('react-component-row').should('not.exist') + cy.contains('No components found').should('be.visible') + }) + + it('shows \'Unable to parse file\' if there was an error parsing the file', () => { + cy.get('@ComponentCard').click() + + // This component has a syntax error so we will fail to parse it + cy.findByText('Invalid').should('be.visible').click() + + cy.findByTestId('react-component-row').should('not.exist') + cy.contains('Unable to parse file').should('be.visible') + }) + + it('shows success modal when component spec is created', () => { + cy.get('@ComponentCard').click() + + // Expand the row + cy.findByText('App').should('be.visible').click() + + // Click on 'app' component + cy.findByTestId('react-component-row').should('contain', 'App').click() + + cy.findByRole('dialog', { + name: defaultMessages.createSpec.successPage.header, + }).as('SuccessDialog').within(() => { + cy.contains(getPathForPlatform(expectedSpecPath)).should('be.visible') + cy.findByRole('button', { name: 'Close' }).should('be.visible') + + cy.findByRole('link', { name: 'Okay, run the spec' }) + .should('have.attr', 'href', `#/specs/runner?file=${expectedSpecPath}`) + + cy.findByRole('button', { name: 'Create another spec' }).click() + }) + + // 'Create from component' card appears again when the user selects "create another spec" + cy.findByText('Create from component').should('be.visible') + }) + + it('runs generated spec', () => { + cy.get('@ComponentCard').click() + + // Expand the row + cy.findByText('App').should('be.visible').click() + + // Click on 'app' component + cy.findByTestId('react-component-row').should('contain', 'App').click() + + cy.findByRole('dialog', { + name: defaultMessages.createSpec.successPage.header, + }).as('SuccessDialog').within(() => { + cy.contains(getPathForPlatform(expectedSpecPath)).should('be.visible') + cy.findByRole('button', { name: 'Close' }).should('be.visible') + + // There appears to be a race condition here where sometimes we try to run the spec + // before the file has been written to. Waiting here for 1 second resolves the issue. + cy.wait(2000) + + cy.findByRole('link', { name: 'Okay, run the spec' }) + .should('have.attr', 'href', `#/specs/runner?file=${expectedSpecPath}`).click() + }) + + cy.waitForSpecToFinish({ passCount: 1 }) }) } describe('Create from component card', () => { - context('project with default spec pattern', () => { - validateCreateFromComponentCard(() => { - cy.scaffoldProject('no-specs-vue-2') - cy.openProject('no-specs-vue-2') - cy.startAppServer('component') - cy.visitApp() - - cy.findAllByTestId('card').eq(0).as('ComponentCard') - }, 'src/components/HelloWorld.cy.js') - }) - - context('project with custom spec pattern', () => { - validateCreateFromComponentCard(() => { - cy.scaffoldProject('no-specs-vue-2') - cy.openProject('no-specs-vue-2', ['--config-file', 'cypress-custom-spec-pattern.config.js']) - cy.startAppServer('component') - cy.visitApp() - - cy.findByText('New spec').click() - cy.findAllByTestId('card').eq(0).as('ComponentCard') - }, 'src/specs-folder/HelloWorld.cy.js') + context('Vue', () => { + context('project with default spec pattern', () => { + validateCreateFromVueComponentCard(() => { + cy.scaffoldProject('no-specs-vue-2') + cy.openProject('no-specs-vue-2') + cy.startAppServer('component') + cy.visitApp() + + cy.findAllByTestId('card').eq(0).as('ComponentCard') + }, 'src/components/HelloWorld.cy.js') + }) + + context('project with custom spec pattern', () => { + validateCreateFromVueComponentCard(() => { + cy.scaffoldProject('no-specs-vue-2') + cy.openProject('no-specs-vue-2', ['--config-file', 'cypress-custom-spec-pattern.config.js']) + cy.startAppServer('component') + cy.visitApp() + + cy.findByText('New spec').click() + cy.findAllByTestId('card').eq(0).as('ComponentCard') + }, 'src/specs-folder/HelloWorld.cy.js') + }) + }) + + context('React', () => { + context('project with default spec pattern', () => { + validateCreateFromReactComponentCard(() => { + cy.scaffoldProject('no-specs') + cy.openProject('no-specs') + cy.startAppServer('component') + cy.visitApp() + + cy.findAllByTestId('card').eq(0).as('ComponentCard') + }, 'src/App.cy.jsx') + }) + + context('project with custom spec pattern', () => { + validateCreateFromReactComponentCard(() => { + cy.scaffoldProject('no-specs') + cy.openProject('no-specs', ['--config-file', 'cypress-custom-spec-pattern.config.ts']) + cy.startAppServer('component') + cy.visitApp() + + cy.findByText('New spec').click() + cy.findAllByTestId('card').eq(0).as('ComponentCard') + }, 'src/specs-folder/App.cy.jsx') + }) }) }) diff --git a/packages/app/cypress/e2e/specs.cy.ts b/packages/app/cypress/e2e/specs.cy.ts index 4d1838408ba9..6fbcce550edc 100644 --- a/packages/app/cypress/e2e/specs.cy.ts +++ b/packages/app/cypress/e2e/specs.cy.ts @@ -524,6 +524,12 @@ describe('App: Specs', () => { }) }) + function selectEmptySpecCard () { + cy.findAllByTestId('card').should('have.length', 2) + cy.findByRole('button', { name: 'Create from component' }).should('be.visible') + cy.findByRole('button', { name: 'Create new empty spec' }).should('be.visible').click() + } + describe('Testing Type: Component', { viewportHeight: 768, viewportWidth: 1024, @@ -535,7 +541,7 @@ describe('App: Specs', () => { cy.startAppServer('component') cy.visitApp() - cy.findAllByTestId('card').eq(0).as('EmptyCard') + cy.findAllByTestId('card').eq(1).as('EmptyCard') }) it('shows create new empty spec card', () => { @@ -589,7 +595,9 @@ describe('App: Specs', () => { // 'Create a new spec' dialog presents with options when user indicates they want to create // another spec. - cy.findByRole('dialog', { name: 'Enter the path for your new spec' }).should('be.visible') + cy.findAllByTestId('card').should('have.length', 2) + cy.findByRole('button', { name: 'Create new empty spec' }).should('be.visible') + cy.findByRole('button', { name: 'Create from component' }).should('be.visible') }) it('navigates to spec runner when selected', () => { @@ -628,7 +636,7 @@ describe('App: Specs', () => { }) cy.contains('Review the docs') - .should('have.attr', 'href', 'https://on.cypress.io/mount') + .should('have.attr', 'href', 'https://on.cypress.io/styling-components') cy.log('should not contain the link if you navigate away and back') cy.get('body').type('f') @@ -709,13 +717,17 @@ describe('App: Specs', () => { it('shows new spec button to start creation workflow', () => { cy.findByRole('button', { name: 'New spec', exact: false }).click() + selectEmptySpecCard() + cy.findByRole('dialog', { name: 'Enter the path for your new spec' }).should('be.visible') }) it('shows create first spec page with create empty option and goes back if it is cancel', () => { cy.findByRole('button', { name: 'New spec', exact: false }).click() - cy.contains('Cancel').click() + selectEmptySpecCard() + + cy.contains('Back').click() cy.findByRole('dialog', { name: 'Enter the path for your new spec' }).should('not.exist') }) @@ -738,6 +750,8 @@ describe('App: Specs', () => { cy.findByRole('button', { name: 'New spec' }).click() + selectEmptySpecCard() + cy.findByRole('dialog', { name: 'Enter the path for your new spec', }).within(() => { diff --git a/packages/app/src/paths.ts b/packages/app/src/paths.ts index d01d302b78b5..1c889ca8fbf1 100644 --- a/packages/app/src/paths.ts +++ b/packages/app/src/paths.ts @@ -18,3 +18,7 @@ export function getPathForPlatform (posixPath?: string) { return posixPath } + +export function posixify (path: string): string { + return path.replace(/\\/g, '/') +} diff --git a/packages/app/src/runner/SpecRunnerHeaderOpenMode.vue b/packages/app/src/runner/SpecRunnerHeaderOpenMode.vue index af42d0e06746..80c458927f3d 100644 --- a/packages/app/src/runner/SpecRunnerHeaderOpenMode.vue +++ b/packages/app/src/runner/SpecRunnerHeaderOpenMode.vue @@ -153,8 +153,8 @@ dismissible > - diff --git a/packages/app/src/specs/generators/ExpandableFileList.cy.tsx b/packages/app/src/specs/generators/ExpandableFileList.cy.tsx new file mode 100644 index 000000000000..01c3435b9109 --- /dev/null +++ b/packages/app/src/specs/generators/ExpandableFileList.cy.tsx @@ -0,0 +1,84 @@ +import ExpandableFileList from './ExpandableFileList.vue' +import data from '../../../cypress/fixtures/FileList.json' +import { ref, Ref } from 'vue' +import type { FileListItemFragment } from '../../generated/graphql-test' + +const difficultFile = { + baseName: '[...all].vue', + fileExtension: '.vue', +} + +const noResultsSlot = () =>
No Results
+const noResultsSelector = '[data-testid=no-results]' +const fileRowSelector = '[data-cy=file-list-row]' + +const allFiles = data as FileListItemFragment[] + +allFiles[1] = { ...allFiles[1], ...difficultFile } +describe('', { viewportHeight: 500, viewportWidth: 400 }, () => { + describe('with files', () => { + const files = allFiles + + beforeEach(() => { + const selectItemStub = cy.stub() + + cy.mount(() => (
+ +
)) + }) + + it('renders all of the files passed in', () => { + cy.get(fileRowSelector) + .should('have.length', 10) + }) + + it('expands rows when they are clicked', () => { + cy.mount(() => (
+ This is the expanded content
}} + files={files} /> + )) + + cy.contains('This is the expanded content').should('not.exist') + + cy.get(fileRowSelector) + .first() + .click() + + cy.contains('This is the expanded content').should('be.visible') + }) + + it('correctly formats a difficult file', () => { + cy.get('body').contains('[...all]') + cy.percySnapshot() + }) + }) + + describe('without files', () => { + it('shows the no results slot', () => { + const files: Ref = ref([]) + let idx = 0 + + cy.mount(() => (
+ + + + +
)) + .get(noResultsSelector).should('be.visible') + + cy.percySnapshot() + + cy.get('[data-testid=add-file]') + .click() + .get(noResultsSelector).should('not.exist') + }) + }) +}) diff --git a/packages/app/src/specs/generators/ExpandableFileList.vue b/packages/app/src/specs/generators/ExpandableFileList.vue new file mode 100644 index 000000000000..44ef10f09f91 --- /dev/null +++ b/packages/app/src/specs/generators/ExpandableFileList.vue @@ -0,0 +1,80 @@ + + + diff --git a/packages/app/src/specs/generators/FileChooser.cy.tsx b/packages/app/src/specs/generators/FileChooser.cy.tsx index 98f36a501b28..7711eb6629eb 100644 --- a/packages/app/src/specs/generators/FileChooser.cy.tsx +++ b/packages/app/src/specs/generators/FileChooser.cy.tsx @@ -3,10 +3,11 @@ import FileChooser from './FileChooser.vue' import { ref } from 'vue' import { defaultMessages } from '@cy/i18n' import data from '../../../cypress/fixtures/FileChooser.json' +import type { FileParts } from '@packages/data-context/src/gen/graphcache-config.gen' /*---------- Fixtures ----------*/ const numFiles = data.length -const allFiles = data +const allFiles = data as unknown as FileParts[] const extensionPattern = '*.jsx' const existentExtensionPattern = '*.tsx' const nonExistentFileName = 'non existent file' @@ -243,12 +244,16 @@ describe('', () => { }) it('fires a selectFile event when a file is clicked on', () => { - const onSelectFileSpy = cy.spy().as('onSelectFileSpy') + const onSelectFileStub = cy.stub() cy.mount(() => ( )) + + cy.findAllByTestId('file-list-row').first().click().then(() => { + expect(onSelectFileStub).to.be.calledOnce + }) }) }) diff --git a/packages/app/src/specs/generators/FileChooser.vue b/packages/app/src/specs/generators/FileChooser.vue index 7c250a5b1d39..ec3a55d812d6 100644 --- a/packages/app/src/specs/generators/FileChooser.vue +++ b/packages/app/src/specs/generators/FileChooser.vue @@ -73,9 +73,10 @@ import CreateSpecModalBody from './CreateSpecModalBody.vue' import FileList from './FileList.vue' import FileMatch from '../../components/FileMatch.vue' import { gql } from '@urql/core' +import type { FileParts } from '@packages/data-context/src/gen/graphcache-config.gen' const props = withDefaults(defineProps<{ - files: any[] + files: FileParts[] extensionPattern: string loading?: boolean }>(), { @@ -148,9 +149,13 @@ const noResults = computed(() => { return { search: filePathSearch.value || debouncedExtensionPattern.value, message: filePathSearch.value ? t('noResults.defaultMessage') : t('components.fileSearch.noMatchesForExtension'), - clear: filePathSearch.value ? - () => filePathSearch.value = '' : - () => localExtensionPattern.value = initialExtensionPattern, + clear: () => { + if (filePathSearch.value) { + filePathSearch.value = '' + } else { + localExtensionPattern.value = initialExtensionPattern + } + }, } }) diff --git a/packages/app/src/specs/generators/component/ComponentGenerator.tsx b/packages/app/src/specs/generators/component/ComponentGenerator.tsx deleted file mode 100644 index 1937264008b9..000000000000 --- a/packages/app/src/specs/generators/component/ComponentGenerator.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { filters } from '../GeneratorsCommon' -import ComponentGeneratorStepOne from './ComponentGeneratorStepOne.vue' -import type { SpecGenerator } from '../types' -import ComponentGeneratorCard from './ComponentGeneratorCard.vue' - -export const ComponentGenerator: SpecGenerator = { - card: ComponentGeneratorCard, - entry: ComponentGeneratorStepOne, - show: (currentProject) => { - return currentProject?.codeGenGlobs?.component === '*.vue' - }, - matches: filters.matchesCT, - id: 'component', -} diff --git a/packages/app/src/specs/generators/component/ReactComponentGenerator.tsx b/packages/app/src/specs/generators/component/ReactComponentGenerator.tsx new file mode 100644 index 000000000000..37345ff7add4 --- /dev/null +++ b/packages/app/src/specs/generators/component/ReactComponentGenerator.tsx @@ -0,0 +1,12 @@ +import { filters } from '../GeneratorsCommon' +import ReactComponentGeneratorStepOne from './ReactComponentGeneratorStepOne.vue' +import type { SpecGenerator } from '../types' +import ComponentGeneratorCard from './ComponentGeneratorCard.vue' + +export const ReactComponentGenerator: SpecGenerator = { + card: ComponentGeneratorCard, + entry: ReactComponentGeneratorStepOne, + show: (currentProject) => currentProject?.codeGenFramework === 'react', + matches: filters.matchesCT, + id: 'reactComponent', +} diff --git a/packages/app/src/specs/generators/component/ReactComponentGeneratorStepOne.vue b/packages/app/src/specs/generators/component/ReactComponentGeneratorStepOne.vue new file mode 100644 index 000000000000..b14a9ab862be --- /dev/null +++ b/packages/app/src/specs/generators/component/ReactComponentGeneratorStepOne.vue @@ -0,0 +1,188 @@ + + diff --git a/packages/app/src/specs/generators/component/ReactComponentList.cy.tsx b/packages/app/src/specs/generators/component/ReactComponentList.cy.tsx new file mode 100644 index 000000000000..5f3ad5b79ad3 --- /dev/null +++ b/packages/app/src/specs/generators/component/ReactComponentList.cy.tsx @@ -0,0 +1,59 @@ +import { ComponentList_GetReactComponentsFromFileDocument } from '../../../generated/graphql-test' +import ReactComponentList from './ReactComponentList.vue' + +describe('ReactComponentList', () => { + const mockFile = { + absolute: '/path/to/my/component', + id: 'fileId', + relative: '../path/to/my/component', + fileName: 'Component.js', + fileExtension: '.tsx', baseName: 'Component', + } + + it('renders empty state if no components are returned', () => { + cy.stubMutationResolver(ComponentList_GetReactComponentsFromFileDocument, (defineResult) => { + return defineResult({ getReactComponentsFromFile: { components: [], errored: false } }) + }) + + cy.mount() + + cy.contains('No components found').should('be.visible') + + cy.percySnapshot() + }) + + it('renders error state if errored is true', () => { + cy.stubMutationResolver(ComponentList_GetReactComponentsFromFileDocument, (defineResult) => { + return defineResult({ getReactComponentsFromFile: { components: [], errored: true } }) + }) + + cy.mount() + + cy.contains('Unable to parse file').should('be.visible') + + cy.percySnapshot() + }) + + it('fetches and displays a list of components', () => { + cy.mount() + + cy.contains('FooBar').should('be.visible') + cy.contains('BarFoo').should('be.visible') + cy.contains('FooBarBaz').should('be.visible') + + cy.percySnapshot() + }) + + it('calls selectItem on click', () => { + const onSelectItemStub = cy.stub() + + cy.mount() + + cy.contains('FooBar').should('be.visible').click().then(() => { + expect(onSelectItemStub).to.be.calledOnceWith({ file: mockFile, item: { exportName: 'FooBar', isDefault: false } }) + }) + + cy.contains('BarFoo').should('be.visible') + cy.contains('FooBarBaz').should('be.visible') + }) +}) diff --git a/packages/app/src/specs/generators/component/ReactComponentList.vue b/packages/app/src/specs/generators/component/ReactComponentList.vue new file mode 100644 index 000000000000..217b9054c353 --- /dev/null +++ b/packages/app/src/specs/generators/component/ReactComponentList.vue @@ -0,0 +1,87 @@ + + + diff --git a/packages/app/src/specs/generators/component/VueComponentGenerator.tsx b/packages/app/src/specs/generators/component/VueComponentGenerator.tsx new file mode 100644 index 000000000000..a56ee121fd84 --- /dev/null +++ b/packages/app/src/specs/generators/component/VueComponentGenerator.tsx @@ -0,0 +1,12 @@ +import { filters } from '../GeneratorsCommon' +import VueComponentGeneratorStepOne from './VueComponentGeneratorStepOne.vue' +import type { SpecGenerator } from '../types' +import ComponentGeneratorCard from './ComponentGeneratorCard.vue' + +export const VueComponentGenerator: SpecGenerator = { + card: ComponentGeneratorCard, + entry: VueComponentGeneratorStepOne, + show: (currentProject) => currentProject?.codeGenFramework === 'vue', + matches: filters.matchesCT, + id: 'vueComponent', +} diff --git a/packages/app/src/specs/generators/component/ComponentGeneratorStepOne.vue b/packages/app/src/specs/generators/component/VueComponentGeneratorStepOne.vue similarity index 79% rename from packages/app/src/specs/generators/component/ComponentGeneratorStepOne.vue rename to packages/app/src/specs/generators/component/VueComponentGeneratorStepOne.vue index 84ea5f54e2c3..0ad6a91ccc17 100644 --- a/packages/app/src/specs/generators/component/ComponentGeneratorStepOne.vue +++ b/packages/app/src/specs/generators/component/VueComponentGeneratorStepOne.vue @@ -40,20 +40,15 @@ v-if="result" class="flex gap-16px items-center" > - - - + {{ t('createSpec.successPage.runSpecButton') }} +