diff --git a/.github/workflows/update-browser-versions.yml b/.github/workflows/update-browser-versions.yml index 0d73441e469b..974281fc06e2 100644 --- a/.github/workflows/update-browser-versions.yml +++ b/.github/workflows/update-browser-versions.yml @@ -108,7 +108,7 @@ jobs: uses: actions/github-script@v4 with: script: | - const { createPullRequest } = require('./scripts/github-actions/update-browser-versions.js') + const { createPullRequest } = require('./scripts/github-actions/create-pull-request.js') await createPullRequest({ context, @@ -116,4 +116,5 @@ jobs: baseBranch: '${{ env.BASE_BRANCH }}', branchName: '${{ steps.check-branch.outputs.branch_name }}', description: '${{ steps.get-versions.outputs.description }}', + body: 'This PR was auto-generated to update the version(s) of Chrome for driver tests', }) diff --git a/.github/workflows/update_v8_snapshot_cache.yml b/.github/workflows/update_v8_snapshot_cache.yml index e0b0666a007a..11150a178c1c 100644 --- a/.github/workflows/update_v8_snapshot_cache.yml +++ b/.github/workflows/update_v8_snapshot_cache.yml @@ -1,10 +1,137 @@ name: Update V8 Snapshot Cache -on: [workflow_dispatch] +on: + schedule: + # Run every Wednesday at 00:00 UTC + - cron: '0 0 * * 3' + push: + branches: + - ryanm/feature/v8-snapshots-auto-pr + - develop + - 'release/**' + workflow_dispatch: + inputs: + branch: + description: 'Branch to update' + required: true + default: 'develop' + generate_from_scratch: + description: 'Generate from scratch' + type: boolean + default: false + commit_directly_to_branch: + description: 'Commit directly to branch' + type: boolean + default: false concurrency: - group: ${{ github.ref }} + group: ${{ inputs.branch || github.ref }} cancel-in-progress: true jobs: update-v8-snapshot-cache: + strategy: + max-parallel: 1 + matrix: + platform: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.platform }} + env: + CYPRESS_BOT_APP_ID: ${{ secrets.CYPRESS_BOT_APP_ID }} + BASE_BRANCH: ${{ inputs.branch || github.ref_name }} + GENERATE_FROM_SCRATCH: ${{ inputs.generate_from_scratch == true || github.event_name == 'schedule' }} steps: - - name: Dummy step - run: echo "Dummy step" + - name: Determine snapshot files - Windows + if: ${{ matrix.platform == 'windows-latest' }} + run: echo "SNAPSHOT_FILES='tooling\v8-snapshot\cache\win32\snapshot-meta.json'" >> $GITHUB_ENV + shell: bash + - name: Determine snapshot files - Linux + if: ${{ matrix.platform == 'ubuntu-latest' }} + run: echo "SNAPSHOT_FILES='tooling/v8-snapshot/cache/linux/snapshot-meta.json'" >> $GITHUB_ENV + - name: Determine snapshot files - Mac + if: ${{ matrix.platform == 'macos-latest' }} + run: echo "SNAPSHOT_FILES='tooling/v8-snapshot/cache/darwin/snapshot-meta.json'" >> $GITHUB_ENV + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + ref: ${{ env.BASE_BRANCH }} + - name: Set committer info + ## attribute the commit to cypress-bot: https://github.community/t/logging-into-git-as-a-github-app/115916 + run: | + git config --local user.email "${{ env.CYPRESS_BOT_APP_ID }}+cypress-bot[bot]@users.noreply.github.com" + git config --local user.name "cypress-bot[bot]" + - name: Set up Node.js + uses: actions/setup-node@v2 + with: + node-version: 16 + cache: 'yarn' + - name: Run yarn + run: yarn + - name: Run build + run: yarn build + - name: Generate prod snapshot from scratch + if: ${{ env.GENERATE_FROM_SCRATCH == 'true' }} + run: yarn cross-env V8_SNAPSHOT_FROM_SCRATCH=1 V8_UPDATE_METAFILE=1 yarn build-v8-snapshot-prod + - name: Generate prod snapshot iteratively + if: ${{ env.GENERATE_FROM_SCRATCH != 'true' }} + run: yarn cross-env V8_UPDATE_METAFILE=1 yarn build-v8-snapshot-prod + - name: Check for v8 snapshot cache changes + id: check-for-v8-snapshot-cache-changes + run: | + echo "has_changes=$(test "$(git status --porcelain -- ${{ env.SNAPSHOT_FILES }})" && echo 'true')" >> $GITHUB_OUTPUT + shell: bash + - name: Determine branch name - commit directly to branch + if: ${{ inputs.commit_directly_to_branch == true }} + run: | + echo "BRANCH_NAME=${{ env.BASE_BRANCH }}" >> $GITHUB_ENV + echo "BRANCH_EXISTS=true" >> $GITHUB_ENV + shell: bash + - name: Determine branch name - commit to separate branch + if: ${{ inputs.commit_directly_to_branch != true }} + run: | + echo "BRANCH_NAME=update-v8-snapshot-cache-on-${{ env.BASE_BRANCH }}" >> $GITHUB_ENV + echo "BRANCH_EXISTS=$(git show-ref --verify --quiet refs/remotes/origin/update-v8-snapshot-cache-on-${{ env.BASE_BRANCH }} && echo 'true')" >> $GITHUB_ENV + shell: bash + - name: Check need for PR or branch update + id: check-need-for-pr + run: | + echo "needs_pr=${{ steps.check-for-v8-snapshot-cache-changes.outputs.has_changes == 'true' && env.BRANCH_EXISTS != 'true' }}" >> $GITHUB_OUTPUT + echo "needs_branch_update=${{ steps.check-for-v8-snapshot-cache-changes.outputs.has_changes == 'true' && env.BRANCH_EXISTS == 'true' }}" >> $GITHUB_OUTPUT + shell: bash + ## Update available and a branch/PR already exists + - name: Checkout existing branch + if: ${{ steps.check-need-for-pr.outputs.needs_branch_update == 'true' }} + run: | + git stash push -- ${{ env.SNAPSHOT_FILES }} + git reset --hard + git checkout ${{ env.BRANCH_NAME }} + git pull origin ${{ env.BRANCH_NAME }} + git merge --squash -Xtheirs stash + ## Update available and a PR doesn't already exist + - name: Checkout new branch + if: ${{ steps.check-need-for-pr.outputs.needs_pr == 'true' }} + run: git checkout -b ${{ env.BRANCH_NAME }} ${{ env.BASE_BRANCH }} + ## Commit changes if present + - name: Commit the changes + if: ${{ steps.check-for-v8-snapshot-cache-changes.outputs.has_changes == 'true' }} + run: | + git diff-index --quiet HEAD || git commit -am "chore: updating v8 snapshot cache" + ## Push branch + - name: Push branch to remote + if: ${{ steps.check-for-v8-snapshot-cache-changes.outputs.has_changes == 'true' }} + run: git push origin ${{ env.BRANCH_NAME }} + # PR needs to be created + - name: Create Pull Request + if: ${{ steps.check-need-for-pr.outputs.needs_pr == 'true' }} + uses: actions/github-script@v4 + with: + script: | + const { createPullRequest } = require('./scripts/github-actions/create-pull-request.js') + + await createPullRequest({ + context, + github, + baseBranch: '${{ env.BASE_BRANCH }}', + branchName: '${{ env.BRANCH_NAME }}', + description: 'Update v8 snapshot cache', + body: 'This PR was automatically generated by the [update-v8-snapshot-cache](https://github.com/cypress-io/cypress/actions/workflows/update_v8_snapshot_cache.yml) github action.', + reviewers: ['ryanthemanuel'] + }) diff --git a/.gitignore b/.gitignore index ce0f3b177ba0..65d1854e6057 100644 --- a/.gitignore +++ b/.gitignore @@ -389,3 +389,11 @@ globbed_node_modules # Snapshot Binaries snapshot_blob.bin v8_context_snapshot.x86_64.bin + +# Legacy snapshot cache files +tooling/v8-snapshot/cache/dev-darwin +tooling/v8-snapshot/cache/dev-linux +tooling/v8-snapshot/cache/dev-win32 +tooling/v8-snapshot/cache/prod-darwin +tooling/v8-snapshot/cache/prod-linux +tooling/v8-snapshot/cache/prod-win32 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0745ab104242..7822e0295eb4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -435,17 +435,9 @@ During the process of snapshot generation, metadata is created/updated in `tooli **Generation** -If you run into errors while generating the v8 snapshot, you can occasionally identify the problem dependency via the output. You can try to remove that dependency from the cache and see if regenerating succeeds. If it does, likely it was moved to a more restrictive section (e.g. healthy to deferred/no-rewrite or deferred to norewrite). If all else fails, you can try running the following (but keep in mind this may take a while): +If the `build-v8-snapshot-prod` command is taking a long time to run on Circle CI, the snapshot cache probably needs to be updated. Run the [Update V8 Snapshot Cache](https://github.com/cypress-io/cypress/actions/workflows/update_v8_snapshot_cache.yml) github action against your branch to generate the snapshots for you on all platforms. You can choose to commit directly to your branch or alternatively issue a PR to your branch. -``` -V8_SNAPSHOT_FROM_SCRATCH=1 yarn build-v8-snapshot-dev -``` - -or - -``` -V8_SNAPSHOT_FROM_SCRATCH=1 yarn build-v8-snapshot-prod -``` +![Update V8 SnapshotCache](https://user-images.githubusercontent.com/4873279/206541239-1afb1d29-4d66-4593-92a7-5a5961a12137.png) **Runtime** @@ -456,20 +448,6 @@ If you're experiencing issues during runtime, you can try and narrow down where * If the problem occurs with both `yarn build-v8-snapshot-prod` and `yarn build-v8-snapshot-dev` but does not occur when using the `DISABLE_SNAPSHOT_REQUIRE` environment variable, then that means there's a problem with a node module dependency. Chances are that a file is not being flagged properly (e.g. healthy when it should be deferred or norewrite). * If the problem still occurs when using the `DISABLE_SNAPSHOT_REQUIRE` environment variable, then that means the problem is not snapshot related. -**Build Length** - -If the `build-v8-snapshot-prod` command is taking a long time to run on Circle CI, the snapshot cache probably needs to be updated. Run these commands on a windows, linux, and mac and commit the updates to the snapshot cache to git: - -``` -yarn build-v8-snapshot-dev -``` - -or - -``` -yarn build-v8-snapshot-prod -``` - ## Committing Code ### Branches diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 91d80c342334..5291f6488358 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -1589,19 +1589,19 @@ declare namespace Cypress { * * @see https://on.cypress.io/nextuntil */ - nextUntil(selector: K, options?: Partial): Chainable> + nextUntil(selector: K, filter?: string, options?: Partial): Chainable> /** * Get all following siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. * * @see https://on.cypress.io/nextuntil */ - nextUntil(options?: Partial): Chainable> + nextUntil(selector: string, filter?: string, options?: Partial): Chainable> /** * Get all following siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. * * @see https://on.cypress.io/nextuntil */ - nextUntil(selector: string, options?: Partial): Chainable> + nextUntil(element: E | JQuery, filter?: string, options?: Partial): Chainable> /** * Filter DOM element(s) from a set of DOM elements. Opposite of `.filter()` @@ -1774,21 +1774,21 @@ declare namespace Cypress { * Get all previous siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. * > The querying behavior of this command matches exactly how [.prevUntil()](http://api.jquery.com/prevUntil) works in jQuery. * - * @see https://on.cypress.io/prevall + * @see https://on.cypress.io/prevuntil */ prevUntil(selector: K, filter?: string, options?: Partial): Chainable> /** * Get all previous siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. * > The querying behavior of this command matches exactly how [.prevUntil()](http://api.jquery.com/prevUntil) works in jQuery. * - * @see https://on.cypress.io/prevall + * @see https://on.cypress.io/prevuntil */ prevUntil(selector: string, filter?: string, options?: Partial): Chainable> /** * Get all previous siblings of each DOM element in a set of matched DOM elements up to, but not including, the element provided. * > The querying behavior of this command matches exactly how [.prevUntil()](http://api.jquery.com/prevUntil) works in jQuery. * - * @see https://on.cypress.io/prevall + * @see https://on.cypress.io/prevuntil */ prevUntil(element: E | JQuery, filter?: string, options?: Partial): Chainable> diff --git a/cli/types/tests/cypress-tests.ts b/cli/types/tests/cypress-tests.ts index 3b9f5f9f4df2..d31223d59680 100644 --- a/cli/types/tests/cypress-tests.ts +++ b/cli/types/tests/cypress-tests.ts @@ -1136,3 +1136,29 @@ namespace CypressLocalStorageTests { cy.clearAllSessionStorage({ log: false }) cy.clearAllSessionStorage({ log: 'true' }) // $ExpectError } + +namespace CypressTraversalTests { + cy.wrap({}).prevUntil('a') // $ExpectType Chainable> + cy.wrap({}).prevUntil('#myItem') // $ExpectType Chainable> + cy.wrap({}).prevUntil('span', 'a') // $ExpectType Chainable> + cy.wrap({}).prevUntil('#myItem', 'a') // $ExpectType Chainable> + cy.wrap({}).prevUntil('div', 'a', { log: false, timeout: 100 }) // $ExpectType Chainable> + cy.wrap({}).prevUntil('#myItem', 'a', { log: false, timeout: 100 }) // $ExpectType Chainable> + cy.wrap({}).prevUntil('#myItem', 'a', { log: 'true' }) // $ExpectError + + cy.wrap({}).nextUntil('a') // $ExpectType Chainable> + cy.wrap({}).nextUntil('#myItem') // $ExpectType Chainable> + cy.wrap({}).nextUntil('span', 'a') // $ExpectType Chainable> + cy.wrap({}).nextUntil('#myItem', 'a') // $ExpectType Chainable> + cy.wrap({}).nextUntil('div', 'a', { log: false, timeout: 100 }) // $ExpectType Chainable> + cy.wrap({}).nextUntil('#myItem', 'a', { log: false, timeout: 100 }) // $ExpectType Chainable> + cy.wrap({}).nextUntil('#myItem', 'a', { log: 'true' }) // $ExpectError + + cy.wrap({}).parentsUntil('a') // $ExpectType Chainable> + cy.wrap({}).parentsUntil('#myItem') // $ExpectType Chainable> + cy.wrap({}).parentsUntil('span', 'a') // $ExpectType Chainable> + cy.wrap({}).parentsUntil('#myItem', 'a') // $ExpectType Chainable> + cy.wrap({}).parentsUntil('div', 'a', { log: false, timeout: 100 }) // $ExpectType Chainable> + cy.wrap({}).parentsUntil('#myItem', 'a', { log: false, timeout: 100 }) // $ExpectType Chainable> + cy.wrap({}).parentsUntil('#myItem', 'a', { log: 'true' }) // $ExpectError +} diff --git a/npm/grep/CHANGELOG.md b/npm/grep/CHANGELOG.md index ea29de2e18e7..200c73ca841f 100644 --- a/npm/grep/CHANGELOG.md +++ b/npm/grep/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@cypress/grep-v3.1.3](https://github.com/cypress-io/cypress/compare/@cypress/grep-v3.1.2...@cypress/grep-v3.1.3) (2022-12-14) + + +### Bug Fixes + +* **grep:** @cypress/grep types ([#24844](https://github.com/cypress-io/cypress/issues/24844)) ([55058e7](https://github.com/cypress-io/cypress/commit/55058e7783420d0946bd19eeb72a08ccf3f3a86e)) + # [@cypress/grep-v3.1.2](https://github.com/cypress-io/cypress/compare/@cypress/grep-v3.1.1...@cypress/grep-v3.1.2) (2022-12-09) diff --git a/npm/grep/README.md b/npm/grep/README.md index 7114b854bed9..7d586849d0e4 100644 --- a/npm/grep/README.md +++ b/npm/grep/README.md @@ -79,7 +79,7 @@ yarn add -D @cypress/grep ### Support file -**required:** load this module from the [support file](https://on.cypress.io/writing-and-organizing-tests#Support-file) or at the top of the spec file if not using the support file. You improve the registration function and then call it: +**required:** load this module from the [support file](https://on.cypress.io/writing-and-organizing-tests#Support-file) or at the top of the spec file if not using the support file. You import the registration function and then call it: ```js // cypress/support/index.js @@ -89,6 +89,16 @@ const registerCypressGrep = require('@cypress/grep') registerCypressGrep() // if you want to use the "import" keyword +// note: `./index.d.ts` currently extends the global Cypress types and +// does not define `registerCypressGrep` so the import path is directly +// pointed to the `support.js` file +import registerCypressGrep from '@cypress/grep/src/support' +registerCypressGrep() + + +// "import" with `@ts-ignore` +// @see error 2306 https://github.com/microsoft/TypeScript/blob/3fcd1b51a1e6b16d007b368229af03455c7d5794/src/compiler/diagnosticMessages.json#L1635 +// @ts-ignore import registerCypressGrep from '@cypress/grep' registerCypressGrep() ``` @@ -207,7 +217,7 @@ $ npx cypress run --env grep="-hello world" $ npx cypress run --env grep="hello; -world" ``` -**Note:** Inverted title filter is not compativle with the `grepFilterSpecs` option +**Note:** Inverted title filter is not compatible with the `grepFilterSpecs` option ## Filter with tags @@ -277,7 +287,7 @@ If you want to run all tests with tag `@slow` but without tag `@smoke`: --env grepTags=@slow+-@smoke ``` -**Note:** Inverted tag filter is not compativle with the `grepFilterSpecs` option +**Note:** Inverted tag filter is not compatible with the `grepFilterSpecs` option ### NOT tags @@ -417,7 +427,7 @@ This package comes with [src/index.d.ts](./src/index.d.ts) definition file that ```js // cypress/integration/my-spec.js -/// +/// ``` If you have `tsconfig.json` file, add this library to the types list @@ -427,7 +437,7 @@ If you have `tsconfig.json` file, add this library to the types list "compilerOptions": { "target": "es5", "lib": ["es5", "dom"], - "types": ["cypress", "cypress-grep"] + "types": ["cypress", "@cypress/grep"] }, "include": ["**/*.ts"] } diff --git a/npm/grep/package.json b/npm/grep/package.json index cd7056aad67a..375ae84c6ec7 100644 --- a/npm/grep/package.json +++ b/npm/grep/package.json @@ -2,7 +2,7 @@ "name": "@cypress/grep", "version": "0.0.0-development", "description": "Filter tests using substring", - "main": "src/support", + "main": "src/support.js", "scripts": { "cy:run": "node ../../scripts/cypress.js run --config specPattern='**/unit.js'", "cy:open": "node ../../scripts/cypress.js open --e2e -b electron --config specPattern='**/unit.js'" diff --git a/npm/grep/src/index.d.ts b/npm/grep/src/index.d.ts index 864e27420ab7..8410b981d849 100644 --- a/npm/grep/src/index.d.ts +++ b/npm/grep/src/index.d.ts @@ -1,6 +1,17 @@ /// declare namespace Cypress { + interface SuiteConfigOverrides { + /** + * List of tags for this suite + * @example a single tag + * describe('block with config tag', { tags: '@smoke' }, () => {}) + * @example multiple tags + * describe('block with config tag', { tags: ['@smoke', '@slow'] }, () => {}) + */ + tags?: string | string[] + } + // specify additional properties in the TestConfig object // in our case we will add "tags" property interface TestConfigOverrides { @@ -17,4 +28,4 @@ declare namespace Cypress { interface Cypress { grep?: (grep?: string, tags?: string, burn?: string) => void } -} \ No newline at end of file +} diff --git a/npm/mount-utils/package.json b/npm/mount-utils/package.json index 2f04a31b9be7..b02e60e3c909 100644 --- a/npm/mount-utils/package.json +++ b/npm/mount-utils/package.json @@ -15,7 +15,7 @@ "devDependencies": { "@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-node-resolve": "^11.1.1", - "rollup": "^2.38.5", + "rollup": "3.7.3", "rollup-plugin-dts": "^4.2.3", "rollup-plugin-typescript2": "^0.29.0", "typescript": "^4.7.4" diff --git a/npm/react18/package.json b/npm/react18/package.json index 7268e0e2bb8c..2ffc31d894f3 100644 --- a/npm/react18/package.json +++ b/npm/react18/package.json @@ -20,7 +20,7 @@ "cypress": "0.0.0-development", "react": "^16", "react-dom": "^16", - "rollup": "^2.38.5", + "rollup": "3.7.3", "rollup-plugin-typescript2": "^0.29.0", "typescript": "^4.7.4" }, diff --git a/npm/webpack-dev-server/CHANGELOG.md b/npm/webpack-dev-server/CHANGELOG.md index 58e7d0ac5e6c..46ce0869ce18 100644 --- a/npm/webpack-dev-server/CHANGELOG.md +++ b/npm/webpack-dev-server/CHANGELOG.md @@ -1,3 +1,10 @@ +# [@cypress/webpack-dev-server-v3.1.2](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v3.1.1...@cypress/webpack-dev-server-v3.1.2) (2022-12-16) + + +### Bug Fixes + +* use srcRoot for angular build context ([#25090](https://github.com/cypress-io/cypress/issues/25090)) ([7c36118](https://github.com/cypress-io/cypress/commit/7c361187e31c3681f415da8a9073221d807987f6)) + # [@cypress/webpack-dev-server-v3.1.1](https://github.com/cypress-io/cypress/compare/@cypress/webpack-dev-server-v3.1.0...@cypress/webpack-dev-server-v3.1.1) (2022-12-08) 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-dev-server/src/helpers/angularHandler.ts b/npm/webpack-dev-server/src/helpers/angularHandler.ts index 58a5dcb28e5f..1135be85d496 100644 --- a/npm/webpack-dev-server/src/helpers/angularHandler.ts +++ b/npm/webpack-dev-server/src/helpers/angularHandler.ts @@ -1,10 +1,13 @@ import * as fs from 'fs-extra' import { tmpdir } from 'os' import * as path from 'path' -import type { Configuration } from 'webpack' +import type { Configuration, RuleSetRule } from 'webpack' import type { PresetHandlerResult, WebpackDevServerConfig } from '../devServer' import { dynamicAbsoluteImport, dynamicImport } from '../dynamic-import' import { sourceDefaultWebpackDependencies } from './sourceRelativeWebpackModules' +import debugLib from 'debug' + +const debug = debugLib('cypress:webpack-dev-server:angularHandler') export type BuildOptions = Record @@ -217,7 +220,7 @@ function createFakeContext (projectRoot: string, defaultProjectConfig: Cypress.A getProjectMetadata: () => { return { root: defaultProjectConfig.root, - sourceRoot: defaultProjectConfig.root, + sourceRoot: defaultProjectConfig.sourceRoot, projectType: 'application', } }, @@ -250,7 +253,33 @@ async function getAngularCliWebpackConfig (devServerConfig: AngularWebpackDevSer const { config } = await generateBrowserWebpackConfigFromContext( buildOptions, context, - (wco: any) => [getCommonConfig(wco), getStylesConfig(wco)], + (wco: any) => { + const stylesConfig = getStylesConfig(wco) + + // We modify resolve-url-loader and set `root` to be `projectRoot` + `sourceRoot` to ensure + // imports in scss, sass, etc are correctly resolved. + // https://github.com/cypress-io/cypress/issues/24272 + stylesConfig.module.rules.forEach((rule: RuleSetRule) => { + rule.rules?.forEach((ruleSet) => { + if (!Array.isArray(ruleSet.use)) { + return + } + + ruleSet.use.map((loader) => { + if (typeof loader !== 'object' || typeof loader.options !== 'object' || !loader.loader?.includes('resolve-url-loader')) { + return + } + + const root = path.join(devServerConfig.cypressConfig.projectRoot, projectConfig.sourceRoot) + + debug('Adding root %s to resolve-url-loader options', root) + loader.options.root = path.join(devServerConfig.cypressConfig.projectRoot, projectConfig.sourceRoot) + }) + }) + }) + + return [getCommonConfig(wco), stylesConfig] + }, ) delete config.entry.main 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/package.json b/package.json index 00b362be108e..d0c91f375c2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cypress", - "version": "12.0.2", + "version": "12.1.0", "description": "Cypress is a next generation front end testing tool built for the modern web", "private": true, "scripts": { diff --git a/packages/app/cypress.config.ts b/packages/app/cypress.config.ts index 12a2dc92ed83..152cf4c3e857 100644 --- a/packages/app/cypress.config.ts +++ b/packages/app/cypress.config.ts @@ -38,7 +38,6 @@ export default defineConfig({ delete process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF_PARENT_PROJECT process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF = 'true' process.env.CYPRESS_INTERNAL_VITE_OPEN_MODE_TESTING = 'true' - // process.env.DEBUG = '*' const { e2ePluginSetup } = require('@packages/frontend-shared/cypress/e2e/e2ePluginSetup') on('task', { 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/runner/reporter.errors.cy.ts b/packages/app/cypress/e2e/runner/reporter.errors.cy.ts index 1d3125047929..a1d2797a7079 100644 --- a/packages/app/cypress/e2e/runner/reporter.errors.cy.ts +++ b/packages/app/cypress/e2e/runner/reporter.errors.cy.ts @@ -180,6 +180,18 @@ describe('errors ui', { ], }) + verify('spec unhandled rejection with string content', { + uncaught: true, + column: 20, + originalMessage: 'Unhandled promise rejection with string content from the spec', + message: [ + 'The following error originated from your test code', + 'It was caused by an unhandled promise rejection', + ], + stackRegex: /.*/, + hasCodeFrame: false, + }) + verify('spec unhandled rejection with done', { uncaught: true, column: 20, diff --git a/packages/app/cypress/e2e/runner/support/snapshot-reporter.ts b/packages/app/cypress/e2e/runner/support/snapshot-reporter.ts index 4aa85c3c83f1..dc4e91125ae4 100644 --- a/packages/app/cypress/e2e/runner/support/snapshot-reporter.ts +++ b/packages/app/cypress/e2e/runner/support/snapshot-reporter.ts @@ -13,6 +13,14 @@ export const snapshotReporter = () => { '[data-cy=reporter-panel]': ($el) => { $el.attr('style', 'width: 450px !important') }, + '[data-cy=reporter-running-icon]': ($el) => { + // remove 'fa-spin' class so that the icon is not animated + $el.attr('class', '') + }, + '.command-progress': ($el) => { + // don't display command progress bar in snapshot + $el.attr('style', 'display: none !important') + }, }, }) } 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/package.json b/packages/app/package.json index 5c4babc749b1..665a026a2a86 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -63,7 +63,7 @@ "rollup-plugin-polyfill-node": "^0.7.0", "unplugin-icons": "0.13.2", "unplugin-vue-components": "^0.15.2", - "vite": "3.1.0", + "vite": "4.0.1", "vite-plugin-components": "0.11.3", "vite-plugin-pages": "0.18.1", "vite-plugin-vue-layouts": "0.6.0", 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') }} +