From 05530ce531876ffb62c5ee8ec8aa0b8cefc821eb Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Thu, 1 Dec 2022 19:33:44 -0600 Subject: [PATCH] chore(12): merge develop into release/12.0.0 (#24927) Co-authored-by: Lachlan Miller Co-authored-by: Mark Noonan Co-authored-by: Matt Henkes Co-authored-by: Mike Plummer Co-authored-by: Zach Bloomquist Co-authored-by: Chris Breiding Co-authored-by: Zachary Williams Co-authored-by: Feng Yu --- .circleci/workflows.yml | 12 +- .eslintrc.js | 6 + CONTRIBUTING.md | 28 +- cli/lib/util.js | 5 + cli/test/lib/util_spec.js | 5 + cli/types/cypress.d.ts | 63 +++- cli/types/tests/cypress-tests.ts | 26 ++ packages/app/cypress/e2e/run-all-specs.cy.ts | 16 +- .../app/src/composables/useRunAllSpecs.ts | 69 ---- packages/app/src/pages/Specs/Runner.vue | 9 +- .../AdjustRunnerStyleDuringScreenshot.vue | 2 +- packages/app/src/runner/useRunnerStyle.ts | 3 +- .../app/src/specs/InlineRunAllSpecs.cy.tsx | 16 +- packages/app/src/specs/InlineRunAllSpecs.vue | 19 +- packages/app/src/specs/InlineSpecList.cy.tsx | 38 ++- packages/app/src/specs/InlineSpecList.vue | 5 + .../app/src/specs/InlineSpecListHeader.cy.tsx | 52 ++- .../app/src/specs/InlineSpecListHeader.vue | 71 ++-- packages/app/src/specs/InlineSpecListTree.vue | 19 +- packages/app/src/specs/SpecsList.cy.tsx | 8 + packages/app/src/specs/SpecsList.vue | 32 +- packages/app/src/specs/SpecsRunAllSpecs.vue | 2 +- packages/app/src/store/run-all-specs-store.ts | 97 ++++++ .../data-context/src/codegen/spec-options.ts | 19 +- .../test/unit/codegen/spec-options.spec.ts | 31 ++ .../e2e/commands/sessions/manager.cy.ts | 106 ++---- .../e2e/commands/sessions/origins.cy.ts | 87 +++++ .../e2e/commands/sessions/storage.cy.ts | 37 ++ .../driver/cypress/e2e/commands/storage.cy.ts | 321 ++++++++++++++++++ .../e2e/origin/commands/local_storage.cy.ts | 52 --- .../e2e/e2e/origin/commands/misc.cy.ts | 2 +- .../e2e/e2e/origin/commands/storage.cy.ts | 120 +++++++ .../cypress/e2e/e2e/origin/cypress_api.cy.ts | 2 +- .../set-storage-on-multiple-origins.html | 9 + .../driver/cypress/fixtures/set-storage.html | 10 + packages/driver/src/cy/commands/index.ts | 4 +- .../driver/src/cy/commands/local_storage.ts | 51 --- .../src/cy/commands/sessions/manager.ts | 184 +--------- .../src/cy/commands/sessions/origins.ts | 28 ++ .../src/cy/commands/sessions/storage.ts | 158 +++++++++ packages/driver/src/cy/commands/storage.ts | 108 ++++++ packages/driver/src/cypress/error_messages.ts | 2 +- packages/driver/types/internal-types.d.ts | 10 + .../frontend-shared/src/locales/en-US.json | 2 +- .../frontend-shared/src/utils/isRunMode.ts | 1 + packages/types/src/constants.ts | 2 +- packages/ui-components/.eslintignore | 6 - .../src/snapshot-require.ts | 2 - system-tests/__snapshots__/domain_spec.js | 107 ------ system-tests/__snapshots__/session_spec.ts.js | 17 +- .../typescript_spec_support_spec.ts.js | 1 - .../projects/e2e/cypress/e2e/domain.cy.js | 19 -- .../projects/e2e/cypress/e2e/domain_2.cy.js | 19 -- .../projects/run-all-specs/cypress.config.js | 1 + .../run-all-specs/folder-c/spec-a.cy.js | 3 + .../run-all-specs/folder-c/spec-b.cy.js | 3 + .../cypress/e2e/session/session.cy.js | 96 ------ system-tests/test/domain_spec.js | 24 -- 58 files changed, 1394 insertions(+), 853 deletions(-) delete mode 100644 packages/app/src/composables/useRunAllSpecs.ts create mode 100644 packages/app/src/store/run-all-specs-store.ts create mode 100644 packages/driver/cypress/e2e/commands/sessions/origins.cy.ts create mode 100644 packages/driver/cypress/e2e/commands/sessions/storage.cy.ts create mode 100644 packages/driver/cypress/e2e/commands/storage.cy.ts delete mode 100644 packages/driver/cypress/e2e/e2e/origin/commands/local_storage.cy.ts create mode 100644 packages/driver/cypress/e2e/e2e/origin/commands/storage.cy.ts create mode 100644 packages/driver/cypress/fixtures/set-storage-on-multiple-origins.html create mode 100644 packages/driver/cypress/fixtures/set-storage.html delete mode 100644 packages/driver/src/cy/commands/local_storage.ts create mode 100644 packages/driver/src/cy/commands/sessions/origins.ts create mode 100644 packages/driver/src/cy/commands/sessions/storage.ts create mode 100644 packages/driver/src/cy/commands/storage.ts create mode 100644 packages/frontend-shared/src/utils/isRunMode.ts delete mode 100644 packages/ui-components/.eslintignore delete mode 100644 system-tests/__snapshots__/domain_spec.js delete mode 100644 system-tests/projects/e2e/cypress/e2e/domain.cy.js delete mode 100644 system-tests/projects/e2e/cypress/e2e/domain_2.cy.js create mode 100644 system-tests/projects/run-all-specs/folder-c/spec-a.cy.js create mode 100644 system-tests/projects/run-all-specs/folder-c/spec-b.cy.js delete mode 100644 system-tests/test/domain_spec.js diff --git a/.circleci/workflows.yml b/.circleci/workflows.yml index 4d4a618f18..62843db8bf 100644 --- a/.circleci/workflows.yml +++ b/.circleci/workflows.yml @@ -613,8 +613,9 @@ commands: - install-webkit-deps - run: name: Run system tests + environment: + CYPRESS_COMMERCIAL_RECOMMENDATIONS: '0' command: | - CYPRESS_COMMERCIAL_RECOMMENDATIONS=0 ALL_SPECS=`circleci tests glob "/root/cypress/system-tests/test/*spec*"` SPECS= for file in $ALL_SPECS; do @@ -641,8 +642,9 @@ commands: - restore_cached_system_tests_deps - run: name: Run system tests + environment: + CYPRESS_COMMERCIAL_RECOMMENDATIONS: '0' command: | - CYPRESS_COMMERCIAL_RECOMMENDATIONS=0 ALL_SPECS=`circleci tests glob "$HOME/cypress/system-tests/test-binary/*spec*"` SPECS=`echo $ALL_SPECS | xargs -n 1 | circleci tests split --split-by=timings` echo SPECS=$SPECS @@ -1325,7 +1327,7 @@ jobs: condition: equal: [ *darwin-arm64-executor, << parameters.executor >> ] steps: - - run: rm -f /tmp/cypress/junit/* + - run: rm -f /tmp/cypress/junit/* - unless: condition: or: @@ -1514,7 +1516,9 @@ jobs: steps: - restore_cached_workspace - run: - command: CYPRESS_COMMERCIAL_RECOMMENDATIONS=0 yarn workspace @tooling/system-tests test:ci "test/non_root*spec*" --browser electron + environment: + CYPRESS_COMMERCIAL_RECOMMENDATIONS: '0' + command: yarn workspace @tooling/system-tests test:ci "test/non_root*spec*" --browser electron - verify-mocha-results - store_test_results: path: /tmp/cypress diff --git a/.eslintrc.js b/.eslintrc.js index a5e6e3ba3e..33bb01de53 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -34,6 +34,12 @@ module.exports = { 'plugin:@cypress/dev/tests', ], parser: '@typescript-eslint/parser', + ignorePatterns: [ + // cli types are checked by dtslint + 'cli/types/**', + // these fixtures are supposed to fail linting + 'npm/eslint-plugin-dev/test/fixtures/**', + ], overrides: [ { files: [ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8b15d616c6..0745ab1042 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -112,9 +112,9 @@ video | Problems with video recordings | [open](https://github.com/cypress-io/cy ## Writing Documentation - + Cypress documentation lives in a separate repository with its own dependencies and build tools. -See [Documentation Contributing Guideline](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md). +See [Documentation Contributing Guidelines](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md). ## Writing code @@ -164,7 +164,7 @@ Here is a list of the core packages in this repository with a short description, | [ts](./packages/ts) | `@packages/ts` | A centralized version of typescript. | | [types](./packages/types) | `@packages/types` | The shared internal Cypress types. | | [v8-snapshot-require](./packages/v8-snapshot-require) | `@packages/v8-snapshot-requie` | Tool to load a snapshot for Electron applications that was created by `@tooling/v8-snapshot`. | - | [web-config](./packages/web-config) | `@packages/ui-components` | The web-related configuration. | + | [web-config](./packages/web-config) | `@packages/web-config` | The web-related configuration. | Private packages involved in development of the app live within the [`tooling`](./tooling) directory and are in the `@tooling/` namespace. They are discrete modules with different responsibilities, but each is necessary for development of the Cypress app and is not necessarily useful outside of the Cypress app. @@ -358,7 +358,7 @@ This is to ensure that links do not go dead in older versions of Cypress when th ### Tests -For most packages there are typically unit and integration tests. +For most packages there are typically unit and integration tests. For UI packages there are E2E and component tests. Please refer to each packages' `README.md` which documents how to run tests. It is not feasible to try to run all of the tests together. We run our entire test fleet across over a dozen containers in CI. @@ -488,22 +488,28 @@ We do not continuously deploy the Cypress binary, so `develop` contains all of t - When opening a PR for a specific issue already open, please name the branch you are working on using the convention `issue-[issue number]`. For example, if your PR fixes Issue #803, name your branch `issue-803`. If the PR is a larger issue, you can add more context like `issue-803-new-scrollable-area`. If there's not an associated open issue, **[create an issue](https://github.com/cypress-io/cypress/issues/new/choose)**. - PRs can be opened before all the work is finished. In fact we encourage this! Please create a [Draft Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests) if your PR is not ready for review. [Mark the PR as **Ready for Review**](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request#marking-a-pull-request-as-ready-for-review) when you're ready for a Cypress team member to review the PR. - Prefix the title of the Pull Request using [semantic-release](https://github.com/semantic-release/semantic-release)'s format as defined [here](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type). For example, if your PR is fixing a bug, you should prefix the PR title with `fix:`. -- Fill out the [Pull Request Template](./.github/PULL_REQUEST_TEMPLATE.md) completely within the body of the PR. If you feel some areas are not relevant add `N/A` as opposed to deleting those sections. PR's will not be reviewed if this template is not filled in. +- Fill out the [Pull Request Template](./.github/PULL_REQUEST_TEMPLATE.md) completely within the body of the PR. If you feel some areas are not relevant add `N/A` as opposed to deleting those sections. PRs will not be reviewed if this template is not filled in. - If the PR is a user facing change and you're a Cypress team member that has logged into [ZenHub](https://www.zenhub.com/) and downloaded the [ZenHub for GitHub extension](https://www.zenhub.com/extension), set the release the PR is intended to ship in from the sidebar of the PR. Follow semantic versioning to select the intended release. This is used to generate the changelog for the release. If you don't tag a PR for release, it won't be mentioned in the changelog. ![Select release for PR](https://user-images.githubusercontent.com/1271364/135139641-657015d6-2dca-42d4-a4fb-16478f61d63f.png) - Please check the "Allow edits from maintainers" checkbox when submitting your PR. This will make it easier for the maintainers to make minor adjustments, to help with tests or any other changes we may need. ![Allow edits from maintainers checkbox](https://user-images.githubusercontent.com/1271181/31393427-b3105d44-ada9-11e7-80f2-0dac51e3919e.png) - All Pull Requests require a minimum of **two** approvals. - After the PR is approved, the original contributor can merge the PR (if the original contributor has access). -- When you merge a PR into `develop`, select [**Squash and merge**](https://docs.github.com/en/github/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-pull-request-commits). This will squash all commits into a single commit. *The only exception to squashing is when converting files to another language and there is a clear commit history needed to maintain from the file conversion.* +- When you merge a PR into `develop`, select [**Squash and merge**](https://docs.github.com/en/github/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-pull-request-commits). This will squash all commits into a single commit. + +*The only exceptions to squashing are:* + +1. When converting files to another language and there is a clear commit history needed to maintain from the file conversion. +2. When merging a `release/*` branch to `develop`. Individual PRs were already squashed when they were merged to the release branch, and we want that history intact on develop. ### Write Some Tests -If you are adding a new feature or fixing a regression, ensure you add tests for it. Broadly speaking, there are three categories of tests you might consider: +If you are adding a new feature or fixing a regression, ensure you add tests for it. Broadly speaking, there are four categories of tests you might consider: -1. Unit test. Those are inside of `test/unit`, if the package has them. These are the fastest and cheapest to execute. -2. E2E/Integration tests. Those are inside `cypress/e2e`, if the package has them. These are between Unit Tests and System Tests when it comes to speed of execution. -3. System Tests. Those go in the [`system-tests`](https://github.com/cypress-io/cypress/tree/develop/system-tests) directory. The README explains how they work. These are the slowest to run, so you generally only want to add a system-test if it's absolutely required (but don't let that discourage you; they are also the most realistic way to test Cypress). +1. Unit tests. Those are inside of `test/unit`, if the package has them. These are the fastest and cheapest to execute. +2. Component Tests. These are co-located with components in the `src` directory of UI-related packages. These test individual UI components in isolation. They can exhaustively test all expected variations of a component and are faster than E2E tests. +3. E2E/Integration tests. Those are inside `cypress/e2e`, if the package has them. These are between Unit Tests and System Tests when it comes to speed of execution. +4. System tests. Those go in the [`system-tests`](https://github.com/cypress-io/cypress/tree/develop/system-tests) directory. The README explains how they work. These are the slowest to run, so you generally only want to add a system-test if it's absolutely required (but don't let that discourage you; they are also the most realistic way to test Cypress). When choosing what's most appropriate, consider: @@ -511,6 +517,8 @@ When choosing what's most appropriate, consider: - ease of debugging - resilience to refactoring +It is also worth considering when a failure will be noticed. A unit or component test is likely to be run while the related code is being modified and provides very fast feedback. E2E tests and System Tests are more likely to only fail in CI since they are slower to run. + ### Dependencies We use [RenovateBot](https://renovatebot.com/) to automatically upgrade our dependencies. The bot uses the settings in [renovate.json](renovate.json) to maintain our [Update Dependencies](https://github.com/cypress-io/cypress/issues/3777) issue and open PRs. You can manually select a package to open a PR from our [Update Dependencies](https://github.com/cypress-io/cypress/issues/3777) issue. diff --git a/cli/lib/util.js b/cli/lib/util.js index 6c42b3f587..1a5e548990 100644 --- a/cli/lib/util.js +++ b/cli/lib/util.js @@ -533,6 +533,7 @@ const util = { la(is.unemptyString(varName), 'expected environment variable name, not', varName) const configVarName = `npm_config_${varName}` + const configVarNameLower = configVarName.toLowerCase() const packageConfigVarName = `npm_package_config_${varName}` let result @@ -545,6 +546,10 @@ const util = { debug(`Using ${varName} from npm config`) result = process.env[configVarName] + } else if (process.env.hasOwnProperty(configVarNameLower)) { + debug(`Using ${varName.toLowerCase()} from npm config`) + + result = process.env[configVarNameLower] } else if (process.env.hasOwnProperty(packageConfigVarName)) { debug(`Using ${varName} from package.json config`) diff --git a/cli/test/lib/util_spec.js b/cli/test/lib/util_spec.js index 7a347e1129..008f4719e2 100644 --- a/cli/test/lib/util_spec.js +++ b/cli/test/lib/util_spec.js @@ -543,6 +543,11 @@ describe('util', () => { expect(util.getEnv('CYPRESS_FOO')).to.eql('') }) + it('npm config set should work', () => { + process.env.npm_config_cypress_foo_foo = 'bazz' + expect(util.getEnv('CYPRESS_FOO_FOO')).to.eql('bazz') + }) + it('throws on non-string name', () => { expect(() => { util.getEnv() diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 8672772956..697ae721ca 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -197,6 +197,39 @@ declare namespace Cypress { clear: (keys?: string[]) => void } + // TODO: raise minimum required TypeScript version to 3.7 + // and make this recursive + // https://github.com/cypress-io/cypress/issues/24875 + type Storable = + | string + | number + | boolean + | null + | StorableObject + | StorableArray + + interface StorableObject { + [key: string]: Storable + } + + interface StorableArray extends Array { } + + type StorableRecord = Record + + interface OriginStorage { + origin: string + value: StorableRecord + } + + interface Storages { + localStorage: OriginStorage[] + sessionStorage: OriginStorage[] + } + + interface StorageByOrigin { + [key: string]: StorableRecord + } + type IsBrowserMatcher = BrowserName | Partial | Array> interface ViewportPosition extends WindowPosition { @@ -898,7 +931,35 @@ declare namespace Cypress { clearCookies(options?: CookieOptions): Chainable /** - * Clear data in local storage. + * Get local storage for all origins. + * + * @see https://on.cypress.io/getalllocalstorage + */ + getAllLocalStorage(options?: Partial): Chainable + + /** + * Clear local storage for all origins. + * + * @see https://on.cypress.io/clearalllocalstorage + */ + clearAllLocalStorage(options?: Partial): Chainable + + /** + * Get session storage for all origins. + * + * @see https://on.cypress.io/getallsessionstorage + */ + getAllSessionStorage(options?: Partial): Chainable + + /** + * Clear session storage for all origins. + * + * @see https://on.cypress.io/clearallsessionstorage + */ + clearAllSessionStorage(options?: Partial): Chainable + + /** + * Clear data in local storage for the current origin. * Cypress automatically runs this command before each test to prevent state from being * shared across tests. You shouldn't need to use this command unless you're using it * to clear localStorage inside a single test. Yields `localStorage` object. diff --git a/cli/types/tests/cypress-tests.ts b/cli/types/tests/cypress-tests.ts index 7b7f17fdd0..5e27593d6a 100644 --- a/cli/types/tests/cypress-tests.ts +++ b/cli/types/tests/cypress-tests.ts @@ -1084,3 +1084,29 @@ namespace CypressClearCookiesTests { cy.clearCookies({ timeout: '10' }) // $ExpectError cy.clearCookies({ domain: false }) // $ExpectError } + +namespace CypressLocalStorageTests { + cy.getAllLocalStorage().then((result) => { + result // $ExpectType StorageByOrigin + }) + cy.getAllLocalStorage({ log: false }) + cy.getAllLocalStorage({ log: 'true' }) // $ExpectError + + cy.clearAllLocalStorage().then((result) => { + result // $ExpectType null + }) + cy.clearAllLocalStorage({ log: false }) + cy.clearAllLocalStorage({ log: 'true' }) // $ExpectError + + cy.getAllSessionStorage().then((result) => { + result // $ExpectType StorageByOrigin + }) + cy.getAllSessionStorage({ log: false }) + cy.getAllSessionStorage({ log: 'true' }) // $ExpectError + + cy.clearAllSessionStorage().then((result) => { + result // $ExpectType null + }) + cy.clearAllSessionStorage({ log: false }) + cy.clearAllSessionStorage({ log: 'true' }) // $ExpectError +} diff --git a/packages/app/cypress/e2e/run-all-specs.cy.ts b/packages/app/cypress/e2e/run-all-specs.cy.ts index 9650db6c8e..56617b9baa 100644 --- a/packages/app/cypress/e2e/run-all-specs.cy.ts +++ b/packages/app/cypress/e2e/run-all-specs.cy.ts @@ -6,9 +6,15 @@ describe('run-all-specs', () => { spec2: { relative: 'cypress/e2e/folder-a/spec-b.cy.js', name: 'runs folder-a/spec-b' }, spec3: { relative: 'cypress/e2e/folder-b/spec-a.cy.js', name: 'runs folder-b/spec-a' }, spec4: { relative: 'cypress/e2e/folder-b/spec-b.cy.js', name: 'runs folder-b/spec-b' }, + spec5: { relative: 'folder-c/spec-a.cy.js', name: 'runs folder-c/spec-a' }, + spec6: { relative: 'folder-c/spec-b.cy.js', name: 'runs folder-c/spec-b' }, } const clickRunAllSpecs = (directory: string) => { + if (directory === 'all') { + return cy.findByTestId('run-all-specs-for-all').click() + } + const command = cy.get('[data-cy=spec-item-directory]').contains(directory) return command.realHover().then(() => { @@ -30,7 +36,7 @@ describe('run-all-specs', () => { // Verify "Run All Specs" with sub-directory const subDirectorySpecs = [ALL_SPECS.spec1, ALL_SPECS.spec2] - cy.get('[data-cy=sidebar-link-specs-page]').click() + cy.findByTestId('sidebar-link-specs-page').click() clickRunAllSpecs('folder-a') @@ -87,16 +93,16 @@ describe('run-all-specs', () => { // Verify "Run All Specs" live-reload cy.get('[data-cy=sidebar-link-specs-page]').click() cy.findByLabelText('Search specs').clear() - cy.get('[data-cy=spec-list-file]').should('have.length', 4) + cy.get('[data-cy=spec-list-file]').should('have.length', 6) - clickRunAllSpecs('cypress/e2e') + clickRunAllSpecs('all') cy.withCtx((ctx, { specs, runAllSpecsKey }) => { expect(ctx.actions.project.launchProject).to.have.been.calledWith('e2e', undefined, runAllSpecsKey) expect(ctx.project.runAllSpecs).to.include.members(specs.map((spec) => spec.relative)) }, { specs: Object.values(ALL_SPECS), runAllSpecsKey: RUN_ALL_SPECS_KEY }) - cy.waitForSpecToFinish({ passCount: 4 }) + cy.waitForSpecToFinish({ passCount: 6 }) for (const spec of Object.values(ALL_SPECS)) { cy.get('.runnable-title').contains(spec.name) @@ -111,6 +117,6 @@ describe('run-all-specs', () => { await ctx.actions.file.writeFileInProject(spec.relative, newContent) }, { spec: ALL_SPECS.spec1 }) - cy.waitForSpecToFinish({ passCount: 3, failCount: 1 }) + cy.waitForSpecToFinish({ passCount: 5, failCount: 1 }) }) }) diff --git a/packages/app/src/composables/useRunAllSpecs.ts b/packages/app/src/composables/useRunAllSpecs.ts deleted file mode 100644 index 270a4c91b2..0000000000 --- a/packages/app/src/composables/useRunAllSpecs.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { RUN_ALL_SPECS_KEY } from '@packages/types/src' -import { gql, useMutation, useQuery } from '@urql/vue' -import { computed, ComputedRef } from 'vue' -import { useRouter } from 'vue-router' -import { RunAllSpecsDocument, RunAllSpecs_ConfigDocument } from '../generated/graphql' -import { getSeparator, SpecTreeNode, UseCollapsibleTreeNode } from '../specs/tree/useCollapsibleTree' - -type ResolvedConfig = { value: any, from: 'string', field: string }[] - -gql` -query RunAllSpecs_Config { - currentProject { - id - config - currentTestingType - } -} -` - -gql` -mutation RunAllSpecs ($specPath: String!, $runAllSpecs: [String!]!) { - setRunAllSpecs(runAllSpecs: $runAllSpecs) - launchOpenProject(specPath: $specPath) { - id - } -} -` - -const isRunMode = window.__CYPRESS_MODE__ === 'run' && window.top === window - -export function useRunAllSpecs (list: ComputedRef<{tree: UseCollapsibleTreeNode[]}>) { - const separator = getSeparator() - const router = useRouter() - const query = useQuery({ query: RunAllSpecs_ConfigDocument, pause: isRunMode }) - const setRunAllSpecsMutation = useMutation(RunAllSpecsDocument) - - return { - runAllSpecs: async (runAllSpecs: string[]) => { - await setRunAllSpecsMutation.executeMutation({ runAllSpecs, specPath: RUN_ALL_SPECS_KEY }) - - // Won't execute unless we are testing since the browser gets killed. In testing, - // we can stub `launchProject` to verify the functionality is working - router.push({ path: '/specs/runner', query: { file: RUN_ALL_SPECS_KEY } }) - }, - isRunAllSpecsAllowed: computed(() => { - const isE2E = query.data.value?.currentProject?.currentTestingType === 'e2e' - - const config: ResolvedConfig = query.data.value?.currentProject?.config || [] - const hasExperiment = config.find(({ field, value }) => field === 'experimentalRunAllSpecs' && value === true) - - return Boolean(isE2E && hasExperiment) - }), - directoryChildren: computed(() => { - return list.value.tree.reduce>((acc, node) => { - if (!node.isLeaf) { - acc[node.id] = [] - } else { - Object.keys(acc).forEach((dir) => { - if (node.id.startsWith(dir) && node.id.replace(dir, '').startsWith(separator)) { - acc[dir].push(node.id) - } - }) - } - - return acc - }, {}) - }), - } -} diff --git a/packages/app/src/pages/Specs/Runner.vue b/packages/app/src/pages/Specs/Runner.vue index eff3f05e9a..d69283908b 100644 --- a/packages/app/src/pages/Specs/Runner.vue +++ b/packages/app/src/pages/Specs/Runner.vue @@ -31,6 +31,7 @@ import SpecRunnerContainerOpenMode from '../../runner/SpecRunnerContainerOpenMod import SpecRunnerContainerRunMode from '../../runner/SpecRunnerContainerRunMode.vue' import { useEventManager } from '../../runner/useEventManager' import { useSpecStore } from '../../store' +import { isRunMode } from '@packages/frontend-shared/src/utils/isRunMode' gql` query SpecPageContainer { @@ -59,18 +60,14 @@ subscription Runner_ConfigChange { } ` -const isRunMode = window.__CYPRESS_MODE__ === 'run' - // subscriptions are used to trigger live updates without // reloading the page. // this is only useful in open mode - in run mode, we don't // use GraphQL, so we pause the // subscriptions so they never execute. -const shouldPauseSubscriptions = isRunMode && window.top === window - useSubscription({ query: SpecPageContainer_SpecsChangeDocument, - pause: shouldPauseSubscriptions, + pause: isRunMode, }) // in run mode, we are not using GraphQL or urql @@ -80,7 +77,7 @@ useSubscription({ // requests, which is what we want. const query = useQuery({ query: SpecPageContainerDocument, - pause: shouldPauseSubscriptions, + pause: isRunMode, }) let initialLoad = true diff --git a/packages/app/src/runner/screenshot/AdjustRunnerStyleDuringScreenshot.vue b/packages/app/src/runner/screenshot/AdjustRunnerStyleDuringScreenshot.vue index 2940f5ece3..7a7c469ec5 100644 --- a/packages/app/src/runner/screenshot/AdjustRunnerStyleDuringScreenshot.vue +++ b/packages/app/src/runner/screenshot/AdjustRunnerStyleDuringScreenshot.vue @@ -8,12 +8,12 @@ diff --git a/packages/app/src/specs/InlineSpecListHeader.cy.tsx b/packages/app/src/specs/InlineSpecListHeader.cy.tsx index d4949f2ad5..7964b59307 100644 --- a/packages/app/src/specs/InlineSpecListHeader.cy.tsx +++ b/packages/app/src/specs/InlineSpecListHeader.cy.tsx @@ -1,29 +1,32 @@ import InlineSpecListHeader from './InlineSpecListHeader.vue' import { ref } from 'vue' import { defaultMessages } from '@cy/i18n' +import { defineStore } from 'pinia' describe('InlineSpecListHeader', () => { - const mountWithResultCount = (resultCount = 0) => { + const mountWithProps = (props: {resultCount?: number, isRunAllSpecsAllowed?: boolean} = {}) => { const specFilterModel = ref('') - const onNewSpec = cy.spy().as('new-spec') - cy.wrap(specFilterModel).as('specFilterModel') - - const methods = { + const propsWithDefaults = { + resultCount: props.resultCount ?? 0, + isRunAllSpecsAllowed: props.isRunAllSpecsAllowed ?? false, 'onUpdate:specFilterModel': (val: string) => { specFilterModel.value = val }, - onNewSpec, + onNewSpec: cy.spy().as('new-spec'), + onRunAllSpecs: cy.spy().as('run-all-specs'), } + cy.wrap(specFilterModel).as('specFilterModel') + cy.mount(() => (
- +
)) } it('should allow search', () => { - mountWithResultCount(0) + mountWithProps({ resultCount: 0 }) const searchString = 'my/component.cy.tsx' cy.findByLabelText(defaultMessages.specPage.searchPlaceholder) @@ -33,7 +36,7 @@ describe('InlineSpecListHeader', () => { }) it('should emit add spec', () => { - mountWithResultCount(0) + mountWithProps({ resultCount: 0 }) cy.findAllByLabelText(defaultMessages.specPage.newSpecButton) .click() .get('@new-spec') @@ -41,7 +44,7 @@ describe('InlineSpecListHeader', () => { }) it('clears search field when clear button is clicked', () => { - mountWithResultCount(0) + mountWithProps({ resultCount: 0 }) cy.findByTestId('clear-search-button') .should('not.exist') @@ -56,14 +59,37 @@ describe('InlineSpecListHeader', () => { }) it('exposes the result count correctly to assistive tech', () => { - mountWithResultCount(0) + mountWithProps({ resultCount: 0 }) cy.contains('No matches') .should('have.class', 'sr-only') .and('have.attr', 'aria-live', 'polite') - mountWithResultCount(1) + mountWithProps({ resultCount: 1 }) cy.contains('1 match').should('have.class', 'sr-only') - mountWithResultCount(100) + mountWithProps({ resultCount: 100 }) cy.contains('100 matches').should('have.class', 'sr-only') }) + + it('renders "Run All Specs" button with flag and emits on click', () => { + const EXPECTED_SPEC_COUNT = 2 + + // make a small store to simulate some specs existing + // without touching any of the gql used by the real store + const useRunAllSpecsStore = defineStore('runAllSpecs', { + state: () => ({ allSpecsRef: ref(Array(EXPECTED_SPEC_COUNT)) }), + }) + + useRunAllSpecsStore() + + mountWithProps({ isRunAllSpecsAllowed: true }) + cy.percySnapshot() + + cy.get('[data-cy=run-all-specs-for-all]').as('run-all-btn').realHover() + cy.contains(`Run ${EXPECTED_SPEC_COUNT} specs`).should('be.visible') + + cy.percySnapshot('with tooltip') + + cy.get('@run-all-btn').click() + cy.get('@run-all-specs').should('have.been.called') + }) }) diff --git a/packages/app/src/specs/InlineSpecListHeader.vue b/packages/app/src/specs/InlineSpecListHeader.vue index 919f1895b1..23b27fcadd 100644 --- a/packages/app/src/specs/InlineSpecListHeader.vue +++ b/packages/app/src/specs/InlineSpecListHeader.vue @@ -1,14 +1,12 @@ @@ -207,7 +215,7 @@ import { useSpecFilter } from '../composables/useSpecFilter' import { useRequestAccess } from '../composables/useRequestAccess' import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store' import SpecsRunAllSpecs from './SpecsRunAllSpecs.vue' -import { useRunAllSpecs } from '../composables/useRunAllSpecs' +import { useRunAllSpecsStore } from '../store/run-all-specs-store' const { openLoginConnectModal } = useLoginConnectStore() @@ -427,11 +435,11 @@ const { refetchFailedCloudData } = useCloudSpecData( props.gql.currentProject?.specs as SpecsListFragment[] || [], ) -const { runAllSpecs, isRunAllSpecsAllowed, directoryChildren } = useRunAllSpecs(collapsible) +const runAllSpecsStore = useRunAllSpecsStore() -function onRunAllSpecs (rowId: string) { - runAllSpecs(directoryChildren.value[rowId]) -} +watch(collapsible, () => { + runAllSpecsStore.setRunAllSpecsData(collapsible.value.tree) +}, { immediate: true }) diff --git a/packages/app/src/specs/SpecsRunAllSpecs.vue b/packages/app/src/specs/SpecsRunAllSpecs.vue index 1d1aa5b364..d01ec6a598 100644 --- a/packages/app/src/specs/SpecsRunAllSpecs.vue +++ b/packages/app/src/specs/SpecsRunAllSpecs.vue @@ -18,7 +18,7 @@ class="font-normal text-sm" data-cy="run-all-specs-text" > - {{ t('specPage.runAllSpecs', specNumber) }} + {{ t('specPage.runSelectedSpecs', specNumber) }} diff --git a/packages/app/src/store/run-all-specs-store.ts b/packages/app/src/store/run-all-specs-store.ts new file mode 100644 index 0000000000..066360cdf4 --- /dev/null +++ b/packages/app/src/store/run-all-specs-store.ts @@ -0,0 +1,97 @@ +import { RUN_ALL_SPECS_KEY } from '@packages/types/src' +import { gql, useMutation, useQuery } from '@urql/vue' +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' +import { useRouter } from 'vue-router' +import { RunAllSpecsDataDocument, RunAllSpecsDocument } from '../generated/graphql' +import { getSeparator, SpecTreeNode, UseCollapsibleTreeNode } from '../specs/tree/useCollapsibleTree' +import { isRunMode } from '@packages/frontend-shared/src/utils/isRunMode' + +type ResolvedConfig = Array<{ value: any, from: string, field: string }> + +gql` +query RunAllSpecsData { + currentProject { + id + config + currentTestingType + } +} +` + +gql` +mutation RunAllSpecs ($specPath: String!, $runAllSpecs: [String!]!) { + setRunAllSpecs(runAllSpecs: $runAllSpecs) + launchOpenProject(specPath: $specPath) { + id + } +} +` + +// TODO: This is a "setup store" - see https://pinia.vuejs.org/core-concepts/#setup-stores +// Can we make it an "options store" like the others? https://pinia.vuejs.org/core-concepts/#option-stores +export const useRunAllSpecsStore = defineStore('runAllSpecs', () => { + const allSpecsRef = ref([]) + const directoryChildrenRef = ref>({}) + + const separator = getSeparator() + const router = useRouter() + const query = useQuery({ query: RunAllSpecsDataDocument, pause: isRunMode }) + const setRunAllSpecsMutation = useMutation(RunAllSpecsDocument) + + async function runSpecs (runAllSpecs: string[]) { + await setRunAllSpecsMutation.executeMutation({ runAllSpecs, specPath: RUN_ALL_SPECS_KEY }) + + // Won't execute unless we are testing since the browser gets killed. In testing, + // we can stub `launchProject` to verify the functionality is working + router.push({ path: '/specs/runner', query: { file: RUN_ALL_SPECS_KEY } }) + } + + async function runAllSpecs () { + await runSpecs(allSpecsRef.value) + } + + async function runSelectedSpecs (dir: string) { + await runSpecs(directoryChildrenRef.value[dir]) + } + + function setRunAllSpecsData (tree: UseCollapsibleTreeNode[]) { + const allSpecs: string[] = [] + const directoryChildren: Record = {} + + for (const { id, isLeaf } of tree) { + if (!isLeaf) { + directoryChildren[id] = [] + } else { + allSpecs.push(id) + + Object.keys(directoryChildren).forEach((dir) => { + if (id.startsWith(dir) && id.replace(dir, '').startsWith(separator)) { + directoryChildren[dir].push(id) + } + }) + } + } + + allSpecsRef.value = allSpecs + directoryChildrenRef.value = directoryChildren + } + + const isRunAllSpecsAllowed = computed(() => { + const isE2E = query.data.value?.currentProject?.currentTestingType === 'e2e' + + const config: ResolvedConfig = query.data.value?.currentProject?.config || [] + const hasExperiment = config.some(({ field, value }) => field === 'experimentalRunAllSpecs' && value === true) + + return (isE2E && hasExperiment) + }) + + return { + isRunAllSpecsAllowed, + directoryChildren: directoryChildrenRef, + runAllSpecs, + allSpecsRef, + runSelectedSpecs, + setRunAllSpecsData, + } +}) diff --git a/packages/data-context/src/codegen/spec-options.ts b/packages/data-context/src/codegen/spec-options.ts index d0f0d14a85..3332b45f13 100644 --- a/packages/data-context/src/codegen/spec-options.ts +++ b/packages/data-context/src/codegen/spec-options.ts @@ -2,6 +2,7 @@ import type { ParsedPath } from 'path' import type { CodeGenType } from '@packages/graphql/src/gen/nxs.gen' import type { WizardFrontendFramework } from '@packages/scaffold-config' import fs from 'fs-extra' +import { upperFirst } from 'lodash' import path from 'path' import { getDefaultSpecFileName } from '../sources/migration/utils' import { toPosix } from '../util' @@ -81,7 +82,7 @@ export class SpecOptions { } private async getFrameworkComponentOptions () { - const componentName = this.parsedPath.name + const componentName = this.buildComponentNameFromFilename(this.parsedPath.name) const extension = await this.getVueExtension() @@ -138,11 +139,25 @@ export class SpecOptions { return foundSpecExtension || '' } + private buildComponentNameFromFilename (fileNameWithoutExt: string): string { + const sanitizedName = fileNameWithoutExt + // Remove any characters from the filename that aren't allowed within a JS variable name (but leave periods and hyphens) + .replaceAll(/[^a-z_\d$.-]/gi, '') + // Remove any groupings of multiple periods (eg, '...all') but leave single periods alone + .replaceAll(/[.]{2,}/g, '') + + // Convert period- and hyphen-delimited portions to PascalCase + // eg, 'test.page.ts' => 'TestPage', 'about.component.vue' => 'AboutComponent' + return sanitizedName.split(/[-.]/g) + .map(upperFirst) + .join('') + } + private buildComponentSpecFilename (specExt: string, filePath?: ParsedPath) { const { dir, base, ext } = filePath || this.parsedPath const cyWithExt = this.getSpecExtension(filePath) + ext - const name = base.slice(0, base.indexOf('.')) + const name = base.slice(0, -cyWithExt.length) const finalExtension = filePath ? cyWithExt : specExt diff --git a/packages/data-context/test/unit/codegen/spec-options.spec.ts b/packages/data-context/test/unit/codegen/spec-options.spec.ts index 15fac55bc7..053b4d3f96 100644 --- a/packages/data-context/test/unit/codegen/spec-options.spec.ts +++ b/packages/data-context/test/unit/codegen/spec-options.spec.ts @@ -242,6 +242,37 @@ describe('spec-options', () => { expect(result.codeGenType).to.eq('e2e') expect(result.fileName).to.eq(`TestName.foo.bar-copy-2.js`) }) + + context('file name contains special characters', async () => { + [ + { condition: 'braces', fileName: '[...MyComponent].vue', expectedFileName: '[...MyComponent].cy.js', expectedComponentName: 'MyComponent' }, + { condition: 'hyphens', fileName: 'my-component.vue', expectedFileName: 'my-component.cy.js', expectedComponentName: 'MyComponent' }, + { condition: 'parentheses', fileName: 'My(Component).js', expectedFileName: 'My(Component).cy.js', expectedComponentName: 'MyComponent' }, + { condition: 'period-separated', fileName: 'my.component.js', expectedFileName: 'my.component.cy.js', expectedComponentName: 'MyComponent' }, + { condition: 'dollar', fileName: '$MyComponent.js', expectedFileName: '$MyComponent.cy.js', expectedComponentName: '$MyComponent' }, + { condition: 'underscores', fileName: 'My_Component.js', expectedFileName: 'My_Component.cy.js', expectedComponentName: 'My_Component' }, + { condition: 'mixed period- and hypen-delimited', fileName: 'about-us.component.js', expectedFileName: 'about-us.component.cy.js', expectedComponentName: 'AboutUsComponent' }, + ].forEach(({ condition, fileName, expectedFileName, expectedComponentName }) => { + it(`generates options for ${condition}`, async () => { + const testSpecOptions = new SpecOptions({ + currentProject: 'path/to/myProject', + codeGenPath: `${tmpPath}/${fileName}`, + codeGenType: 'component', + isDefaultSpecPattern: true, + specPattern: [defaultSpecPattern.component], + framework: WIZARD_FRAMEWORKS[1], + }) + + await fs.outputFile(`${tmpPath}/${fileName}`, '// foo') + + const result = await testSpecOptions.getCodeGenOptions() + + expect(result.codeGenType).to.eq('component') + expect(result.fileName).to.eq(expectedFileName) + expect(result['componentName']).to.eq(expectedComponentName) + }) + }) + }) }) }) }) diff --git a/packages/driver/cypress/e2e/commands/sessions/manager.cy.ts b/packages/driver/cypress/e2e/commands/sessions/manager.cy.ts index 872fc7ed0f..68a9412e5a 100644 --- a/packages/driver/cypress/e2e/commands/sessions/manager.cy.ts +++ b/packages/driver/cypress/e2e/commands/sessions/manager.cy.ts @@ -3,12 +3,10 @@ const $Cypress = require('../../../../src/cypress').default describe('src/cy/commands/sessions/manager.ts', () => { let CypressInstance - let baseUrl beforeEach(function () { // @ts-ignore CypressInstance = new $Cypress() - baseUrl = Cypress.config('baseUrl') }) it('creates SessionsManager instance', () => { @@ -168,70 +166,6 @@ describe('src/cy/commands/sessions/manager.ts', () => { }) }) - describe('.mapOrigins()', () => { - it('maps when requesting all origins', async () => { - const sessionsManager = new SessionsManager(CypressInstance, cy) - - const allOrigins = ['https://example.com', baseUrl, 'http://foobar.com', 'http://foobar.com'] - const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins').resolves(allOrigins) - - const origins = await sessionsManager.mapOrigins('*') - - expect(origins).to.deep.eq(['https://example.com', baseUrl, 'http://foobar.com']) - expect(sessionsSpy).to.be.calledOnce - }) - - it('maps when requesting the current origin', async () => { - const sessionsManager = new SessionsManager(CypressInstance, cy) - const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins') - const origins = await sessionsManager.mapOrigins('currentOrigin') - - expect(origins).to.deep.eq([baseUrl]) - expect(sessionsSpy).not.to.be.called - }) - - it('maps when requesting a specific origin', async () => { - const sessionsManager = new SessionsManager(CypressInstance, cy) - const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins') - const origins = await sessionsManager.mapOrigins('https://example.com/random_page?1') - - expect(origins).to.deep.eq(['https://example.com']) - expect(sessionsSpy).not.to.be.called - }) - - it('maps when requesting a list of origins', async () => { - const sessionsManager = new SessionsManager(CypressInstance, cy) - - const allOrigins = ['https://example.com', baseUrl, 'http://foobar.com', 'http://foobar.com'] - const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins').resolves(allOrigins) - - const origins = await sessionsManager.mapOrigins(['*', 'https://other.com']) - - expect(origins).to.deep.eq(['https://example.com', baseUrl, 'http://foobar.com', 'https://other.com']) - expect(sessionsSpy).to.be.calledOnce - }) - }) - - // TODO: - describe('._setStorageOnOrigins()', () => {}) - - it('.getAllHtmlOrigins()', async () => { - const storedOrigins = { - 'https://example.com': {}, - 'https://foobar.com': {}, - } - - storedOrigins[`${baseUrl}`] = {} - const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:rendered:html:origins').resolves(storedOrigins) - const sessionsManager = new SessionsManager(CypressInstance, cy) - - const origins = await sessionsManager.getAllHtmlOrigins() - - expect(cypressSpy).have.been.calledOnce - expect(origins).to.have.lengthOf(3) - expect(origins).to.deep.eq(['https://example.com', 'https://foobar.com', baseUrl]) - }) - describe('.sessions', () => { it('sessions.defineSession()', () => { const sessionsManager = new SessionsManager(CypressInstance, cy) @@ -273,17 +207,27 @@ describe('src/cy/commands/sessions/manager.ts', () => { expect(window.localStorage).of.have.lengthOf(1) expect(window.sessionStorage).of.have.lengthOf(1) + const specWindow = {} + CypressInstance.log = cy.stub() + CypressInstance.state = cy.stub() + CypressInstance.state.withArgs('specWindow').returns(specWindow) + + const storedOrigins = {} + + cy.stub(CypressInstance, 'backend') + .callThrough() + .withArgs('get:rendered:html:origins') + .resolves(storedOrigins) + const sessionsManager = new SessionsManager(CypressInstance, { state: () => true, }) - const clearStorageSpy = cy.stub(sessionsManager.sessions, 'clearStorage') const clearCookiesSpy = cy.stub(sessionsManager.sessions, 'clearCookies') await sessionsManager.sessions.clearCurrentSessionData() - expect(clearStorageSpy).to.be.calledOnce expect(clearCookiesSpy).to.be.calledOnce expect(window.localStorage).of.have.lengthOf(0) expect(window.sessionStorage).of.have.lengthOf(0) @@ -292,8 +236,8 @@ describe('src/cy/commands/sessions/manager.ts', () => { it('does not log message when setting up tests', async () => { // Unable to cleanly mock localStorage or sessionStorage on Firefox, - // so add dummy values and ensure they are cleared as expected. - // https://bugzilla.mozilla.org/show_bug.cgi?id=1141698 + // so add dummy values and ensure they are cleared as expected. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1141698 window.localStorage.foo = 'bar' window.sessionStorage.jazzy = 'music' @@ -301,16 +245,14 @@ describe('src/cy/commands/sessions/manager.ts', () => { expect(window.sessionStorage).of.have.lengthOf(1) CypressInstance.log = cy.stub() - const sessionsManager = new SessionsManager(CypressInstance, { + const sessionsManager = new SessionsManager(Cypress, { state: () => false, }) - const clearStorageSpy = cy.stub(sessionsManager.sessions, 'clearStorage') const clearCookiesSpy = cy.stub(sessionsManager.sessions, 'clearCookies') await sessionsManager.sessions.clearCurrentSessionData() - expect(clearStorageSpy).to.be.calledOnce expect(clearCookiesSpy).to.be.calledOnce expect(window.localStorage).of.have.lengthOf(0) expect(window.sessionStorage).of.have.lengthOf(0) @@ -373,19 +315,18 @@ describe('src/cy/commands/sessions/manager.ts', () => { expect(cypressSpy).to.be.calledOnceWith('clear:cookies', cookies) }) - it('sessions.getCurrentSessionData', async () => { - const sessionsManager = new SessionsManager(CypressInstance, () => {}) - const getStorageSpy = cy.stub(sessionsManager.sessions, 'getStorage').resolves({ localStorage: [] }) + it('sessions.getCurrentSessionData()', async () => { + const sessionsManager = new SessionsManager(Cypress, () => {}) const cookiesSpy = cy.stub(sessionsManager.sessions, 'getCookies').resolves([{ id: 'cookie' }]) const sessData = await sessionsManager.sessions.getCurrentSessionData() - expect(sessData).to.deep.eq({ + expect(sessData).to.deep.equal({ localStorage: [], + sessionStorage: [], cookies: [{ id: 'cookie' }], }) - expect(getStorageSpy).to.be.calledOnce expect(cookiesSpy).to.be.calledOnce }) @@ -398,14 +339,5 @@ describe('src/cy/commands/sessions/manager.ts', () => { expect(cypressSpy).to.be.calledOnceWith('get:session', 'session_1') }) - - // TODO: - describe('sessions.getStorage', () => {}) - - // TODO: - describe('sessions.clearStorage', () => {}) - - // TODO: - describe('sessions.setStorage', () => {}) }) }) diff --git a/packages/driver/cypress/e2e/commands/sessions/origins.cy.ts b/packages/driver/cypress/e2e/commands/sessions/origins.cy.ts new file mode 100644 index 0000000000..bf64908f1c --- /dev/null +++ b/packages/driver/cypress/e2e/commands/sessions/origins.cy.ts @@ -0,0 +1,87 @@ +import { getAllHtmlOrigins, mapOrigins } from '../../../../src/cy/commands/sessions/origins' +const $Cypress = require('../../../../src/cypress').default + +describe('src/cy/commands/sessions/origins', () => { + let CypressInstance + let baseUrl + + beforeEach(function () { + // @ts-ignore + CypressInstance = new $Cypress() + baseUrl = Cypress.config('baseUrl') + }) + + describe('mapOrigins()', () => { + it('returns unique origins when requesting all origins', async () => { + const storedOrigins = { + [baseUrl]: {}, + 'https://example.com': {}, + 'http://foobar.com': {}, + } + + cy.stub(CypressInstance, 'backend') + .callThrough() + .withArgs('get:rendered:html:origins') + .resolves(storedOrigins) + + const origins = await mapOrigins(CypressInstance, '*') + + expect(origins).to.deep.equal([baseUrl, 'https://example.com', 'http://foobar.com']) + }) + + it('returns current origin when requesting the current origin', async () => { + cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:rendered:html:origins') + + const origins = await mapOrigins(CypressInstance, 'currentOrigin') + + expect(origins).to.deep.equal([baseUrl]) + expect(CypressInstance.backend).not.to.be.calledWith('get:rendered:html:origins') + }) + + it('returns specific origin when requesitng a specific origin', async () => { + cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:rendered:html:origins') + + const origins = await mapOrigins(CypressInstance, 'https://example.com/random_page?1') + + expect(origins).to.deep.equal(['https://example.com']) + expect(CypressInstance.backend).not.to.be.calledWith('get:rendered:html:origins') + }) + + it('return origins when requesting a list of origins', async () => { + const storedOrigins = { + [baseUrl]: {}, + 'https://example.com': {}, + 'http://foobar.com': {}, + 'https://other.com': {}, + } + + cy.stub(CypressInstance, 'backend') + .callThrough() + .withArgs('get:rendered:html:origins') + .resolves(storedOrigins) + + const origins = await mapOrigins(CypressInstance, ['*', 'https://other.com']) + + expect(origins).to.deep.equal([baseUrl, 'https://example.com', 'http://foobar.com', 'https://other.com']) + }) + }) + + describe('getAllHtmlOrigins()', () => { + it('returns rendered html origins from backend', async () => { + const storedOrigins = { + [baseUrl]: {}, + 'https://example.com': {}, + 'http://foobar.com': {}, + } + + cy.stub(CypressInstance, 'backend') + .callThrough() + .withArgs('get:rendered:html:origins') + .resolves(storedOrigins) + + const origins = await getAllHtmlOrigins(CypressInstance) + + expect(origins).to.deep.equal([baseUrl, 'https://example.com', 'http://foobar.com']) + }) + }) +}) diff --git a/packages/driver/cypress/e2e/commands/sessions/storage.cy.ts b/packages/driver/cypress/e2e/commands/sessions/storage.cy.ts new file mode 100644 index 0000000000..47439fce97 --- /dev/null +++ b/packages/driver/cypress/e2e/commands/sessions/storage.cy.ts @@ -0,0 +1,37 @@ +import { getStorage, setStorage } from '../../../../src/cy/commands/sessions/storage' + +describe('src/cy/commands/sessions/storage', () => { + describe('setStorage()', () => { + it('returns unique origins when requesting all origins', () => { + cy.visit('http://localhost:3500/fixtures/generic.html') + .then(() => { + localStorage.key1 = 'val1' + + return setStorage(Cypress, { localStorage: [{ value: { key2: 'val2' } }] }) + }) + .then(() => { + expect(window.localStorage.key2).equal('val2') + }) + .then(() => { + return setStorage(Cypress, { + localStorage: [ + // set localStorage on different origin + { origin: 'http://www.foobar.com:3500', value: { key2: 'val' }, clear: true }, + // set localStorage on current origin + { value: { key3: 'val' }, clear: true }, + ], + }) + }) + .then(() => getStorage(Cypress, { origin: ['current_url', 'http://www.foobar.com:3500'] })) + .then((result) => { + expect(result).deep.equal({ + localStorage: [ + { origin: 'http://localhost:3500', value: { key3: 'val' } }, + { origin: 'http://www.foobar.com:3500', value: { key2: 'val' } }, + ], + sessionStorage: [], + }) + }) + }) + }) +}) diff --git a/packages/driver/cypress/e2e/commands/storage.cy.ts b/packages/driver/cypress/e2e/commands/storage.cy.ts new file mode 100644 index 0000000000..c5dcc47a11 --- /dev/null +++ b/packages/driver/cypress/e2e/commands/storage.cy.ts @@ -0,0 +1,321 @@ +import { assertLogLength } from '../../support/utils' + +describe('src/cy/commands/storage', () => { + let logs: Cypress.Log[] + + beforeEach(() => { + logs = [] + + cy.on('log:added', (attrs, log: Cypress.Log) => { + logs.push(log) + }) + }) + + context('#getAllLocalStorage', () => { + beforeEach(() => { + cy.visit('/fixtures/set-storage-on-multiple-origins.html') + }) + + it('gets local storage from all origins', () => { + cy.getAllLocalStorage().should('deep.equal', { + 'http://localhost:3500': { + key1: 'value1', + key2: 'value2', + }, + 'http://www.foobar.com:3500': { + key3: 'value3', + key4: 'value4', + }, + 'http://other.foobar.com:3500': { + key5: 'value5', + key6: 'value6', + }, + 'http://barbaz.com:3500': { + key7: 'value7', + key8: 'value8', + }, + }) + }) + + it('logs once', () => { + cy.getAllLocalStorage().then(() => { + assertLogLength(logs, 2) + expect(logs[0].get('name')).to.eq('visit') + expect(logs[1].get('name')).to.eq('getAllLocalStorage') + }) + }) + + it('does not log when log: false', () => { + cy.getAllLocalStorage({ log: false }).then(() => { + assertLogLength(logs, 1) + expect(logs[0].get('name')).to.eq('visit') + }) + }) + + it('consoleProps includes the storage yielded', () => { + cy.getAllLocalStorage().then(() => { + const consoleProps = logs[1].get('consoleProps')() + + expect(consoleProps).to.deep.equal({ + Command: 'getAllLocalStorage', + Yielded: { + 'http://localhost:3500': { + key1: 'value1', + key2: 'value2', + }, + 'http://www.foobar.com:3500': { + key3: 'value3', + key4: 'value4', + }, + 'http://other.foobar.com:3500': { + key5: 'value5', + key6: 'value6', + }, + 'http://barbaz.com:3500': { + key7: 'value7', + key8: 'value8', + }, + }, + }) + }) + }) + }) + + context('#clearAllLocalStorage', () => { + beforeEach(() => { + cy.visit('/fixtures/set-storage-on-multiple-origins.html') + }) + + it('clears local storage for all origins', () => { + cy.clearAllLocalStorage() + cy.getAllLocalStorage().should('deep.equal', {}) + }) + + it('logs once', () => { + cy.clearAllLocalStorage().then(() => { + assertLogLength(logs, 2) + expect(logs[0].get('name')).to.eq('visit') + expect(logs[1].get('name')).to.eq('clearAllLocalStorage') + }) + }) + + it('does not log when log: false', () => { + cy.clearAllLocalStorage({ log: false }).then(() => { + assertLogLength(logs, 1) + expect(logs[0].get('name')).to.eq('visit') + }) + }) + }) + + context('#getAllSessionStorage', () => { + beforeEach(() => { + cy.visit('/fixtures/set-storage-on-multiple-origins.html') + }) + + it('gets local storage from all origins', () => { + cy.getAllSessionStorage().should('deep.equal', { + 'http://localhost:3500': { + key11: 'value11', + key12: 'value12', + }, + 'http://www.foobar.com:3500': { + key13: 'value13', + key14: 'value14', + }, + 'http://other.foobar.com:3500': { + key15: 'value15', + key16: 'value16', + }, + 'http://barbaz.com:3500': { + key17: 'value17', + key18: 'value18', + }, + }) + }) + + it('logs once', () => { + cy.getAllSessionStorage().then(() => { + assertLogLength(logs, 2) + expect(logs[0].get('name')).to.eq('visit') + expect(logs[1].get('name')).to.eq('getAllSessionStorage') + }) + }) + + it('does not log when log: false', () => { + cy.getAllSessionStorage({ log: false }).then(() => { + assertLogLength(logs, 1) + expect(logs[0].get('name')).to.eq('visit') + }) + }) + + it('consoleProps includes the storage yielded', () => { + cy.getAllSessionStorage().then(() => { + const consoleProps = logs[1].get('consoleProps')() + + expect(consoleProps).to.deep.equal({ + Command: 'getAllSessionStorage', + Yielded: { + 'http://localhost:3500': { + key11: 'value11', + key12: 'value12', + }, + 'http://www.foobar.com:3500': { + key13: 'value13', + key14: 'value14', + }, + 'http://other.foobar.com:3500': { + key15: 'value15', + key16: 'value16', + }, + 'http://barbaz.com:3500': { + key17: 'value17', + key18: 'value18', + }, + }, + }) + }) + }) + }) + + context('#clearAllSessionStorage', () => { + beforeEach(() => { + cy.visit('/fixtures/set-storage-on-multiple-origins.html') + }) + + it('clears session storage for all origins', () => { + cy.clearAllSessionStorage() + cy.getAllSessionStorage().should('deep.equal', {}) + }) + + it('logs once', () => { + cy.clearAllSessionStorage().then(() => { + assertLogLength(logs, 2) + expect(logs[0].get('name')).to.eq('visit') + expect(logs[1].get('name')).to.eq('clearAllSessionStorage') + }) + }) + + it('does not log when log: false', () => { + cy.clearAllSessionStorage({ log: false }).then(() => { + assertLogLength(logs, 1) + expect(logs[0].get('name')).to.eq('visit') + }) + }) + }) + + context('#clearLocalStorage', () => { + it('passes keys onto Cypress.LocalStorage.clear', () => { + const clear = cy.spy(Cypress.LocalStorage, 'clear') + + cy.clearLocalStorage('foo').then(() => { + expect(clear).to.be.calledWith('foo') + }) + }) + + it('sets the storages', () => { + const { + localStorage, + } = window + const remoteStorage = cy.state('window').localStorage + + const setStorages = cy.spy(Cypress.LocalStorage as InternalCypress.LocalStorage, 'setStorages') + + cy.clearLocalStorage().then(() => { + expect(setStorages).to.be.calledWith(localStorage, remoteStorage) + }) + }) + + it('unsets the storages', () => { + const unsetStorages = cy.spy(Cypress.LocalStorage as InternalCypress.LocalStorage, 'unsetStorages') + + cy.clearLocalStorage().then(() => { + expect(unsetStorages).to.be.called + }) + }) + + it('sets subject to remote localStorage', () => { + const ls = cy.state('window').localStorage + + cy.clearLocalStorage().then((remote) => { + expect(remote).to.eq(ls) + }) + }) + + describe('test:before:run', () => { + it('clears localStorage before each test run', () => { + const clear = cy.spy(Cypress.LocalStorage, 'clear') + + Cypress.emit('test:before:run', {}) + expect(clear).not.to.be.called + }) + }) + + describe('errors', () => { + it('throws when being passed a non string or regexp', (done) => { + cy.on('fail', (err) => { + expect(err.message).to.include('`cy.clearLocalStorage()` must be called with either a string or regular expression.') + expect(err.docsUrl).to.include('https://on.cypress.io/clearlocalstorage') + + done() + }) + + // @ts-expect-error + cy.clearLocalStorage(1) + }) + }) + + describe('.log', () => { + beforeEach(function () { + cy.on('log:added', (attrs, log) => { + this.lastLog = log + }) + + return null + }) + + it('ends immediately', () => { + cy.clearLocalStorage().then(function () { + const { lastLog } = this + + expect(lastLog.get('ended')).to.be.true + expect(lastLog.get('state')).to.eq('passed') + }) + }) + + it('snapshots immediately', () => { + cy.clearLocalStorage().then(function () { + const { lastLog } = this + + expect(lastLog.get('snapshots').length).to.eq(1) + expect(lastLog.get('snapshots')[0]).to.be.an('object') + }) + }) + }) + + describe('without log', () => { + beforeEach(function () { + cy.on('log:added', (attrs, log) => { + this.lastLog = log + }) + + return null + }) + + it('log is disabled', () => { + cy.clearLocalStorage('foo', { log: false }).then(function () { + const { lastLog } = this + + expect(lastLog).to.be.undefined + }) + }) + + it('log is disabled without key', () => { + cy.clearLocalStorage({ log: false }).then(function () { + const { lastLog } = this + + expect(lastLog).to.be.undefined + }) + }) + }) + }) +}) diff --git a/packages/driver/cypress/e2e/e2e/origin/commands/local_storage.cy.ts b/packages/driver/cypress/e2e/e2e/origin/commands/local_storage.cy.ts deleted file mode 100644 index 9b5137ea27..0000000000 --- a/packages/driver/cypress/e2e/e2e/origin/commands/local_storage.cy.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { findCrossOriginLogs } from '../../../../support/utils' - -context('cy.origin local storage', { browser: '!webkit' }, () => { - beforeEach(() => { - cy.visit('/fixtures/primary-origin.html') - cy.get('a[data-cy="cross-origin-secondary-link"]').click() - }) - - it('.clearLocalStorage()', () => { - cy.origin('http://www.foobar.com:3500', () => { - cy.window().then((win) => { - win.localStorage.setItem('foo', 'bar') - expect(win.localStorage.getItem('foo')).to.equal('bar') - }) - - cy.clearLocalStorage().should((localStorage) => { - expect(localStorage.length).to.equal(0) - expect(localStorage.getItem('foo')).to.be.null - }) - }) - }) - - context('#consoleProps', () => { - let logs: Map - - beforeEach(() => { - logs = new Map() - - cy.on('log:changed', (attrs, log) => { - logs.set(attrs.id, log) - }) - }) - - it('.clearLocalStorage()', () => { - cy.origin('http://www.foobar.com:3500', () => { - cy.window().then((win) => { - win.localStorage.setItem('foo', 'bar') - expect(win.localStorage.getItem('foo')).to.equal('bar') - }) - - cy.clearLocalStorage() - }) - - cy.shouldWithTimeout(() => { - const { consoleProps } = findCrossOriginLogs('clearLocalStorage', logs, 'foobar.com') - - expect(consoleProps.Command).to.equal('clearLocalStorage') - expect(consoleProps.Yielded).to.be.null - }) - }) - }) -}) diff --git a/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts b/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts index 1f104cc70c..6de00c9e4a 100644 --- a/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/commands/misc.cy.ts @@ -219,7 +219,7 @@ it('verifies number of cy commands', () => { 'focused', 'get', 'contains', 'shadow', 'within', 'request', 'session', 'screenshot', 'task', 'find', 'filter', 'not', 'children', 'eq', 'closest', 'first', 'last', 'next', 'nextAll', 'nextUntil', 'parent', 'parents', 'parentsUntil', 'prev', 'prevAll', 'prevUntil', 'siblings', 'wait', 'title', 'window', 'document', 'viewport', 'server', 'route', 'intercept', 'origin', - 'mount', 'as', 'root', + 'mount', 'as', 'root', 'getAllLocalStorage', 'clearAllLocalStorage', 'getAllSessionStorage', 'clearAllSessionStorage', ] const addedCommands = Cypress._.difference(actualCommands, expectedCommands) const removedCommands = Cypress._.difference(expectedCommands, actualCommands) diff --git a/packages/driver/cypress/e2e/e2e/origin/commands/storage.cy.ts b/packages/driver/cypress/e2e/e2e/origin/commands/storage.cy.ts new file mode 100644 index 0000000000..21671bec9e --- /dev/null +++ b/packages/driver/cypress/e2e/e2e/origin/commands/storage.cy.ts @@ -0,0 +1,120 @@ +import { findCrossOriginLogs } from '../../../../support/utils' + +context('cy.origin storage', { browser: '!webkit' }, () => { + beforeEach(() => { + cy.visit('/fixtures/primary-origin.html') + cy.get('a[data-cy="cross-origin-secondary-link"]').click() + }) + + it('.getAllLocalStorage', () => { + cy.origin('http://www.foobar.com:3500', () => { + cy.visit('/fixtures/set-storage-on-multiple-origins.html') + + cy.getAllLocalStorage().should('deep.equal', { + 'http://localhost:3500': { + key1: 'value1', + key2: 'value2', + }, + 'http://www.foobar.com:3500': { + key3: 'value3', + key4: 'value4', + }, + 'http://other.foobar.com:3500': { + key5: 'value5', + key6: 'value6', + }, + 'http://barbaz.com:3500': { + key7: 'value7', + key8: 'value8', + }, + }) + }) + }) + + it('.clearAllLocalStorage', () => { + cy.origin('http://www.foobar.com:3500', () => { + cy.visit('/fixtures/set-storage-on-multiple-origins.html') + + cy.clearAllLocalStorage() + cy.getAllLocalStorage().should('deep.equal', {}) + }) + }) + + it('.getAllSessionStorage', () => { + cy.origin('http://www.foobar.com:3500', () => { + cy.visit('/fixtures/set-storage-on-multiple-origins.html') + + cy.getAllSessionStorage().should('deep.equal', { + 'http://localhost:3500': { + key11: 'value11', + key12: 'value12', + }, + 'http://www.foobar.com:3500': { + key13: 'value13', + key14: 'value14', + }, + 'http://other.foobar.com:3500': { + key15: 'value15', + key16: 'value16', + }, + 'http://barbaz.com:3500': { + key17: 'value17', + key18: 'value18', + }, + }) + }) + }) + + it('.clearAllSessionStorage', () => { + cy.origin('http://www.foobar.com:3500', () => { + cy.visit('/fixtures/set-storage-on-multiple-origins.html') + + cy.clearAllSessionStorage() + cy.getAllSessionStorage().should('deep.equal', {}) + }) + }) + + it('.clearLocalStorage()', () => { + cy.origin('http://www.foobar.com:3500', () => { + cy.window().then((win) => { + win.localStorage.setItem('foo', 'bar') + expect(win.localStorage.getItem('foo')).to.equal('bar') + }) + + cy.clearLocalStorage().should((localStorage) => { + expect(localStorage.length).to.equal(0) + expect(localStorage.getItem('foo')).to.be.null + }) + }) + }) + + context('#consoleProps', () => { + let logs: Map + + beforeEach(() => { + logs = new Map() + + cy.on('log:changed', (attrs, log) => { + logs.set(attrs.id, log) + }) + }) + + it('.clearLocalStorage()', () => { + cy.origin('http://www.foobar.com:3500', () => { + cy.window().then((win) => { + win.localStorage.setItem('foo', 'bar') + expect(win.localStorage.getItem('foo')).to.equal('bar') + }) + + cy.clearLocalStorage() + }) + + cy.shouldWithTimeout(() => { + const { consoleProps } = findCrossOriginLogs('clearLocalStorage', logs, 'foobar.com') + + expect(consoleProps.Command).to.equal('clearLocalStorage') + expect(consoleProps.Yielded).to.be.null + }) + }) + }) +}) diff --git a/packages/driver/cypress/e2e/e2e/origin/cypress_api.cy.ts b/packages/driver/cypress/e2e/e2e/origin/cypress_api.cy.ts index 4dfca63506..c0fbc950d4 100644 --- a/packages/driver/cypress/e2e/e2e/origin/cypress_api.cy.ts +++ b/packages/driver/cypress/e2e/e2e/origin/cypress_api.cy.ts @@ -206,7 +206,7 @@ describe('cy.origin Cypress API', { browser: '!webkit' }, () => { context('not supported', () => { it('throws an error when a user attempts to call Cypress.session.clearAllSavedSessions() inside of cy.origin', (done) => { cy.on('fail', (err) => { - expect(err.message).to.equal('`Cypress.session.*` methods are not supported in the `cy.switchToDomain()` callback. Consider using them outside of the callback instead.') + expect(err.message).to.equal('`Cypress.session.*` methods are not supported in the `cy.origin()` callback. Consider using them outside of the callback instead.') expect(err.docsUrl).to.equal('https://on.cypress.io/session-api') done() }) diff --git a/packages/driver/cypress/fixtures/set-storage-on-multiple-origins.html b/packages/driver/cypress/fixtures/set-storage-on-multiple-origins.html new file mode 100644 index 0000000000..d455d5ff67 --- /dev/null +++ b/packages/driver/cypress/fixtures/set-storage-on-multiple-origins.html @@ -0,0 +1,9 @@ +

Set storage on multiple origins

+ diff --git a/packages/driver/cypress/fixtures/set-storage.html b/packages/driver/cypress/fixtures/set-storage.html new file mode 100644 index 0000000000..a5a97c8b83 --- /dev/null +++ b/packages/driver/cypress/fixtures/set-storage.html @@ -0,0 +1,10 @@ +

Set storage

+ diff --git a/packages/driver/src/cy/commands/index.ts b/packages/driver/src/cy/commands/index.ts index 82199f4c37..8131a16485 100644 --- a/packages/driver/src/cy/commands/index.ts +++ b/packages/driver/src/cy/commands/index.ts @@ -22,7 +22,7 @@ import * as Files from './files' import * as Fixtures from './fixtures' -import LocalStorage from './local_storage' +import Storage from './storage' import * as Location from './location' @@ -65,7 +65,7 @@ export const allCommands = { Exec, Files, Fixtures, - LocalStorage, + Storage, Location, Misc, Origin, diff --git a/packages/driver/src/cy/commands/local_storage.ts b/packages/driver/src/cy/commands/local_storage.ts deleted file mode 100644 index 8dacdfde28..0000000000 --- a/packages/driver/src/cy/commands/local_storage.ts +++ /dev/null @@ -1,51 +0,0 @@ -import _ from 'lodash' - -import $errUtils from '../../cypress/error_utils' -import $LocalStorage from '../../cypress/local_storage' - -const clearLocalStorage = (state, keys) => { - const local = window.localStorage - const remote = state('window').localStorage - - // set our localStorage and the remote localStorage - $LocalStorage.setStorages(local, remote) - - // clear the keys - $LocalStorage.clear(keys) - - // and then unset the references - $LocalStorage.unsetStorages() - - // return the remote localStorage object - return remote -} - -export default (Commands, Cypress, cy, state) => { - Commands.addAll({ - clearLocalStorage (keys, options: Partial = {}) { - if (_.isPlainObject(keys)) { - options = keys - keys = null - } - - _.defaults(options, { log: true }) - - // bail if we have keys and we're not a string and we're not a regexp - if (keys && !_.isString(keys) && !_.isRegExp(keys)) { - $errUtils.throwErrByPath('clearLocalStorage.invalid_argument') - } - - const remote = clearLocalStorage(state, keys) - - if (options.log) { - Cypress.log({ - snapshot: true, - end: true, - }) - } - - // return the remote local storage object - return remote - }, - }) -} diff --git a/packages/driver/src/cy/commands/sessions/manager.ts b/packages/driver/src/cy/commands/sessions/manager.ts index 91b814f3ec..164d69a330 100644 --- a/packages/driver/src/cy/commands/sessions/manager.ts +++ b/packages/driver/src/cy/commands/sessions/manager.ts @@ -1,11 +1,8 @@ -import _ from 'lodash' -import { $Location } from '../../../cypress/location' import type { ServerSessionData } from '@packages/types' -import { - getCurrentOriginStorage, - setPostMessageLocalStorage, - getPostMessageLocalStorage, -} from './utils' +import _ from 'lodash' + +import { getAllHtmlOrigins } from './origins' +import { clearStorage, getStorage, setStorage } from './storage' type ActiveSessions = Cypress.Commands.Session.ActiveSessions type SessionData = Cypress.Commands.Session.SessionData @@ -63,66 +60,6 @@ export default class SessionsManager { this.cy.state('activeSessions', clearedSessions) } - mapOrigins = async (origins: string | Array): Promise> => { - const getOrigins = this.Cypress.Promise.map( - ([] as string[]).concat(origins), async (v) => { - if (v === '*') { - return await this.getAllHtmlOrigins() - } - - if (v === 'currentOrigin') { - return $Location.create(window.location.href).origin - } - - return $Location.create(v).origin - }, - ) - - return _.uniq(_.flatten(await getOrigins)) - } - - _setStorageOnOrigins = async (originOptions) => { - const specWindow = this.cy.state('specWindow') - - const currentOrigin = $Location.create(window.location.href).origin - - const currentOriginIndex = _.findIndex(originOptions, { origin: currentOrigin }) - - if (currentOriginIndex !== -1) { - const opts = originOptions.splice(currentOriginIndex, 1)[0] - - if (!_.isEmpty(opts.localStorage)) { - if (opts.localStorage.clear) { - window.localStorage.clear() - } - - _.each(opts.localStorage.value, (val, key) => localStorage.setItem(key, val)) - } - - if (opts.sessionStorage) { - if (opts.sessionStorage.clear) { - window.sessionStorage.clear() - } - - _.each(opts.sessionStorage.value, (val, key) => sessionStorage.setItem(key, val)) - } - } - - if (_.isEmpty(originOptions)) { - return - } - - await setPostMessageLocalStorage(specWindow, originOptions) - } - - getAllHtmlOrigins = async () => { - const currentOrigin = $Location.create(window.location.href).origin - const storedOrigins = await this.Cypress.backend('get:rendered:html:origins') - const origins = [..._.keys(storedOrigins), currentOrigin] - - return _.uniq(origins) - } - // this the public api exposed to consumers as Cypress.session sessions = { defineSession: (options = {} as any): SessionData => { @@ -156,7 +93,7 @@ export default class SessionsManager { window.sessionStorage.clear() await Promise.all([ - this.sessions.clearStorage(), + clearStorage(this.Cypress), this.sessions.clearCookies(), ]) }, @@ -170,7 +107,7 @@ export default class SessionsManager { }, setSessionData: async (data) => { - const allHtmlOrigins = await this.getAllHtmlOrigins() + const allHtmlOrigins = await getAllHtmlOrigins(this.Cypress) let _localStorage = data.localStorage || [] let _sessionStorage = data.sessionStorage || [] @@ -186,7 +123,7 @@ export default class SessionsManager { }) await Promise.all([ - this.sessions.setStorage({ localStorage: _localStorage, sessionStorage: _sessionStorage }), + setStorage(this.Cypress, { localStorage: _localStorage, sessionStorage: _sessionStorage }), this.sessions.setCookies(data.cookies), ]) }, @@ -205,7 +142,7 @@ export default class SessionsManager { getCurrentSessionData: async () => { const [storage, cookies] = await Promise.all([ - this.sessions.getStorage({ origin: '*' }), + getStorage(this.Cypress, { origin: '*' }), this.sessions.getCookies(), ]) @@ -218,110 +155,5 @@ export default class SessionsManager { getSession: (id: string): Promise => { return this.Cypress.backend('get:session', id) }, - - /** - * 1) if we only need currentOrigin localStorage, access sync - * 2) if cross-origin http, we need to load in iframe from our proxy that will intercept all http reqs at /__cypress/automation/* - * and postMessage() the localStorage value to us - * 3) if cross-origin https, since we pass-thru https connections in the proxy, we need to - * send a message telling our proxy server to intercept the next req to the https domain, - * then follow 2) - */ - getStorage: async (options = {}) => { - const specWindow = this.cy.state('specWindow') - - if (!_.isObject(options)) { - throw new Error('getStorage() takes an object') - } - - const opts = _.defaults({}, options, { - origin: 'currentOrigin', - }) - - const currentOrigin = $Location.create(window.location.href).origin - - const origins: Array = await this.mapOrigins(opts.origin) - - const results = { - localStorage: [] as any[], - sessionStorage: [] as any[], - } - - function pushValue (origin, value) { - if (!_.isEmpty(value.localStorage)) { - results.localStorage.push({ origin, value: value.localStorage }) - } - - if (!_.isEmpty(value.sessionStorage)) { - results.sessionStorage.push({ origin, value: value.sessionStorage }) - } - } - - const currentOriginIndex = origins.indexOf(currentOrigin) - - if (currentOriginIndex !== -1) { - origins.splice(currentOriginIndex, 1) - const currentOriginStorage = getCurrentOriginStorage() - - pushValue(currentOrigin, currentOriginStorage) - } - - if (_.isEmpty(origins)) { - return results - } - - if (currentOrigin.startsWith('https:')) { - _.remove(origins, (v) => v.startsWith('http:')) - } - - const postMessageResults = await getPostMessageLocalStorage(specWindow, origins) - - postMessageResults.forEach((val) => { - pushValue(val[0], val[1]) - }) - - return results - }, - - clearStorage: async () => { - const origins = await this.getAllHtmlOrigins() - - const originOptions = origins.map((v) => ({ origin: v, clear: true })) - - await this.sessions.setStorage({ - localStorage: originOptions, - sessionStorage: originOptions, - }) - }, - - setStorage: async (options: any, clearAll = false) => { - const currentOrigin = $Location.create(window.location.href).origin as string - - const mapToCurrentOrigin = (v) => ({ ...v, origin: (v.origin && v.origin !== 'currentOrigin') ? $Location.create(v.origin).origin : currentOrigin }) - - const mappedLocalStorage = _.map(options.localStorage, (v) => { - const mapped = { origin: v.origin, localStorage: _.pick(v, 'value', 'clear') } - - if (clearAll) { - mapped.localStorage.clear = true - } - - return mapped - }).map(mapToCurrentOrigin) - - const mappedSessionStorage = _.map(options.sessionStorage, (v) => { - const mapped = { origin: v.origin, sessionStorage: _.pick(v, 'value', 'clear') } - - if (clearAll) { - mapped.sessionStorage.clear = true - } - - return mapped - }).map(mapToCurrentOrigin) - - const storageOptions = _.map(_.groupBy(mappedLocalStorage.concat(mappedSessionStorage), 'origin'), (v) => _.merge({}, ...v)) - - await this._setStorageOnOrigins(storageOptions) - }, } } diff --git a/packages/driver/src/cy/commands/sessions/origins.ts b/packages/driver/src/cy/commands/sessions/origins.ts new file mode 100644 index 0000000000..56d30f655c --- /dev/null +++ b/packages/driver/src/cy/commands/sessions/origins.ts @@ -0,0 +1,28 @@ +import Bluebird from 'bluebird' +import { $Location } from '../../../cypress/location' + +export async function mapOrigins (Cypress: Cypress.Cypress, origins: string | string[]): Promise { + const getOrigins = Bluebird.map( + ([] as string[]).concat(origins), async (origin) => { + if (origin === '*') { + return await getAllHtmlOrigins(Cypress) + } + + if (origin === 'currentOrigin') { + return window.location.origin + } + + return $Location.create(origin).origin + }, + ) + + return _.uniq(_.flatten(await getOrigins)) +} + +export async function getAllHtmlOrigins (Cypress: Cypress.Cypress) { + const currentOrigin = window.location.origin + const storedOrigins = await Cypress.backend('get:rendered:html:origins') + const origins = [..._.keys(storedOrigins), currentOrigin] + + return _.uniq(origins) +} diff --git a/packages/driver/src/cy/commands/sessions/storage.ts b/packages/driver/src/cy/commands/sessions/storage.ts new file mode 100644 index 0000000000..a8fd92a081 --- /dev/null +++ b/packages/driver/src/cy/commands/sessions/storage.ts @@ -0,0 +1,158 @@ +import _ from 'lodash' + +import { $Location } from '../../../cypress/location' +import { getAllHtmlOrigins, mapOrigins } from './origins' +import { getCurrentOriginStorage, getPostMessageLocalStorage, setPostMessageLocalStorage } from './utils' + +export type StorageType = 'localStorage' | 'sessionStorage' + +interface GetStorageOptions { + origin?: '*' | 'currentOrigin' | string | string[] +} + +interface OriginStorageOptions { + clear?: boolean + origin?: string | string[] + value?: any +} + +interface SetStoragesOptions { + localStorage?: OriginStorageOptions[] + sessionStorage?: OriginStorageOptions[] +} +/** + * 1) if we only need currentOrigin localStorage, access sync + * 2) if cross-origin http, we need to load in iframe from our proxy that will intercept all http reqs at /__cypress/automation/* + * and postMessage() the localStorage value to us + * 3) if cross-origin https, since we pass-thru https connections in the proxy, we need to + * send a message telling our proxy server to intercept the next req to the https domain, + * then follow 2) + */ +export async function getStorage (Cypress: Cypress.Cypress, options: GetStorageOptions = {}): Promise { + const specWindow = Cypress.state('specWindow') + + if (!_.isObject(options)) { + throw new Error('getStorage() takes an object') + } + + const opts = _.defaults({}, options, { + origin: 'currentOrigin', + }) + + const currentOrigin = window.location.origin + const origins: Array = await mapOrigins(Cypress, opts.origin) + const results = { + localStorage: [] as Cypress.OriginStorage[], + sessionStorage: [] as Cypress.OriginStorage[], + } + + function pushValue (origin, value) { + if (!_.isEmpty(value.localStorage)) { + results.localStorage.push({ origin, value: value.localStorage }) + } + + if (!_.isEmpty(value.sessionStorage)) { + results.sessionStorage.push({ origin, value: value.sessionStorage }) + } + } + + const currentOriginIndex = origins.indexOf(currentOrigin) + + if (currentOriginIndex !== -1) { + origins.splice(currentOriginIndex, 1) + const currentOriginStorage = getCurrentOriginStorage() + + pushValue(currentOrigin, currentOriginStorage) + } + + if (_.isEmpty(origins)) { + return results + } + + if (currentOrigin.startsWith('https:')) { + _.remove(origins, (v) => v.startsWith('http:')) + } + + const postMessageResults = await getPostMessageLocalStorage(specWindow, origins) + + postMessageResults.forEach((val) => { + pushValue(val[0], val[1]) + }) + + return results +} + +export async function clearStorage (Cypress: Cypress.Cypress, type?: StorageType) { + const origins = await getAllHtmlOrigins(Cypress) + const originOptions = origins.map((origin) => ({ origin, clear: true })) + const options: SetStoragesOptions = {} + + if (!type || type === 'localStorage') { + options.localStorage = originOptions + } + + if (!type || type === 'sessionStorage') { + options.sessionStorage = originOptions + } + + await setStorage(Cypress, options) +} + +async function setStorageOnOrigins (Cypress: Cypress.Cypress, originOptions) { + const specWindow = Cypress.state('specWindow') + + const currentOrigin = window.location.origin + + const currentOriginIndex = _.findIndex(originOptions, { origin: currentOrigin }) + + if (currentOriginIndex !== -1) { + const opts = originOptions.splice(currentOriginIndex, 1)[0] + + if (!_.isEmpty(opts.localStorage)) { + if (opts.localStorage.clear) { + window.localStorage.clear() + } + + _.each(opts.localStorage.value, (val, key) => localStorage.setItem(key, val)) + } + + if (opts.sessionStorage) { + if (opts.sessionStorage.clear) { + window.sessionStorage.clear() + } + + _.each(opts.sessionStorage.value, (val, key) => sessionStorage.setItem(key, val)) + } + } + + if (_.isEmpty(originOptions)) { + return + } + + await setPostMessageLocalStorage(specWindow, originOptions) +} + +export async function setStorage (Cypress: Cypress.Cypress, options: SetStoragesOptions) { + const currentOrigin = window.location.origin + + function mapToCurrentOrigin (v) { + return { + ...v, + origin: (v.origin && v.origin !== 'currentOrigin') + ? $Location.create(v.origin).origin + : currentOrigin, + } + } + + const mappedLocalStorage = _.map(options.localStorage, (v) => { + return mapToCurrentOrigin({ origin: v.origin, localStorage: _.pick(v, 'value', 'clear') }) + }) + + const mappedSessionStorage = _.map(options.sessionStorage, (v) => { + return mapToCurrentOrigin({ origin: v.origin, sessionStorage: _.pick(v, 'value', 'clear') }) + }) + + const storageOptions = _.map(_.groupBy(mappedLocalStorage.concat(mappedSessionStorage), 'origin'), (v) => _.merge({}, ...v)) + + await setStorageOnOrigins(Cypress, storageOptions) +} diff --git a/packages/driver/src/cy/commands/storage.ts b/packages/driver/src/cy/commands/storage.ts new file mode 100644 index 0000000000..629b1b387e --- /dev/null +++ b/packages/driver/src/cy/commands/storage.ts @@ -0,0 +1,108 @@ +import _ from 'lodash' + +import $errUtils from '../../cypress/error_utils' +import $LocalStorage from '../../cypress/local_storage' +import { clearStorage, getStorage, StorageType } from './sessions/storage' + +type Options = Partial + +const clearLocalStorage = (state, keys) => { + const local = window.localStorage + const remote = state('window').localStorage + + // set our localStorage and the remote localStorage + $LocalStorage.setStorages(local, remote) + + // clear the keys + $LocalStorage.clear(keys) + + // and then unset the references + $LocalStorage.unsetStorages() + + // return the remote localStorage object + return remote +} + +const getAllStorage = async (type: StorageType, Cypress: InternalCypress.Cypress, userOptions: Options = {}) => { + const options: Options = { + log: true, + ...userOptions, + } + + let storageByOrigin: Cypress.StorageByOrigin = {} + + if (options.log) { + Cypress.log({ + consoleProps () { + const obj = {} + + if (Object.keys(storageByOrigin).length) { + obj['Yielded'] = storageByOrigin + } + + return obj + }, + }) + } + + const storages = await getStorage(Cypress, { origin: '*' }) + + storageByOrigin = storages[type].reduce((memo, storage) => { + memo[storage.origin] = storage.value + + return memo + }, {} as Cypress.StorageByOrigin) + + return storageByOrigin +} + +const clearAllStorage = async (type: StorageType, Cypress: InternalCypress.Cypress, userOptions: Options = {}) => { + const options: Options = { + log: true, + ...userOptions, + } + + if (options.log) { + Cypress.log({}) + } + + await clearStorage(Cypress, type) + + return null +} + +export default (Commands, Cypress: InternalCypress.Cypress, cy, state, config) => { + Commands.addAll({ + getAllLocalStorage: getAllStorage.bind(null, 'localStorage', Cypress), + getAllSessionStorage: getAllStorage.bind(null, 'sessionStorage', Cypress), + + clearAllLocalStorage: clearAllStorage.bind(null, 'localStorage', Cypress), + clearAllSessionStorage: clearAllStorage.bind(null, 'sessionStorage', Cypress), + + clearLocalStorage (keys, options: Options = {}) { + if (_.isPlainObject(keys)) { + options = keys + keys = null + } + + _.defaults(options, { log: true }) + + // bail if we have keys and we're not a string and we're not a regexp + if (keys && !_.isString(keys) && !_.isRegExp(keys)) { + $errUtils.throwErrByPath('clearLocalStorage.invalid_argument') + } + + const remote = clearLocalStorage(state, keys) + + if (options.log) { + Cypress.log({ + snapshot: true, + end: true, + }) + } + + // return the remote local storage object + return remote + }, + }) +} diff --git a/packages/driver/src/cypress/error_messages.ts b/packages/driver/src/cypress/error_messages.ts index aef83c2def..6f0cdef2fd 100644 --- a/packages/driver/src/cypress/error_messages.ts +++ b/packages/driver/src/cypress/error_messages.ts @@ -1277,7 +1277,7 @@ export default { docsUrl: 'https://on.cypress.io/github-issue/20721', }, Cypress_session: { - message: `\`Cypress.session.*\` methods are not supported in the ${cmd('switchToDomain')} callback. Consider using them outside of the callback instead.`, + message: `\`Cypress.session.*\` methods are not supported in the ${cmd('origin')} callback. Consider using them outside of the callback instead.`, docsUrl: 'https://on.cypress.io/session-api', }, }, diff --git a/packages/driver/types/internal-types.d.ts b/packages/driver/types/internal-types.d.ts index e255819140..e5cc171c17 100644 --- a/packages/driver/types/internal-types.d.ts +++ b/packages/driver/types/internal-types.d.ts @@ -63,6 +63,16 @@ declare namespace Cypress { interface Backend { (task: 'cross:origin:cookies:received'): Promise + (task: 'get:rendered:html:origins'): Promise + } +} + +declare namespace InternalCypress { + interface Cypress extends Cypress.Cypress, NodeEventEmitter {} + + interface LocalStorage extends Cypress.LocalStorage { + setStorages: (local, remote) => LocalStorage + unsetStorages: () => LocalStorage } } diff --git a/packages/frontend-shared/src/locales/en-US.json b/packages/frontend-shared/src/locales/en-US.json index 169477b994..14f0447c26 100644 --- a/packages/frontend-shared/src/locales/en-US.json +++ b/packages/frontend-shared/src/locales/en-US.json @@ -218,7 +218,7 @@ "content": "Record a run to see your test results in Cypress Cloud. You can then optimize your test suite, debug failing and flaky tests, and integrate with your favorite tools." } }, - "runAllSpecs": "Run {n} spec | Run {n} specs" + "runSelectedSpecs": "Run {n} spec | Run {n} specs" }, "noResults": { "defaultMessage": "No results matched your search:", diff --git a/packages/frontend-shared/src/utils/isRunMode.ts b/packages/frontend-shared/src/utils/isRunMode.ts new file mode 100644 index 0000000000..5a44dc30dc --- /dev/null +++ b/packages/frontend-shared/src/utils/isRunMode.ts @@ -0,0 +1 @@ +export const isRunMode = window.__CYPRESS_MODE__ === 'run' && window.top === window diff --git a/packages/types/src/constants.ts b/packages/types/src/constants.ts index 01bfa3ec12..0f85e77edb 100644 --- a/packages/types/src/constants.ts +++ b/packages/types/src/constants.ts @@ -27,7 +27,7 @@ export const PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm'] as const // for a new major version of Cypress export const MAJOR_VERSION_FOR_CONTENT = '11' -export const RUN_ALL_SPECS_KEY = '__all' +export const RUN_ALL_SPECS_KEY = '__all' as const export const RUN_ALL_SPECS: SpecFile = { name: 'All E2E Specs', diff --git a/packages/ui-components/.eslintignore b/packages/ui-components/.eslintignore deleted file mode 100644 index 2255e970fb..0000000000 --- a/packages/ui-components/.eslintignore +++ /dev/null @@ -1,6 +0,0 @@ -**/dist -**/*.d.ts -**/package-lock.json -**/tsconfig.json -**/cypress/fixtures -**/__snapshots__ diff --git a/packages/v8-snapshot-require/src/snapshot-require.ts b/packages/v8-snapshot-require/src/snapshot-require.ts index c428425d5e..f9c173f637 100644 --- a/packages/v8-snapshot-require/src/snapshot-require.ts +++ b/packages/v8-snapshot-require/src/snapshot-require.ts @@ -261,8 +261,6 @@ export function snapshotRequire ( return path.resolve(projectBaseDir, p) } catch (err) { logError(err) - // eslint-disable-next-line no-debugger - debugger } return diff --git a/system-tests/__snapshots__/domain_spec.js b/system-tests/__snapshots__/domain_spec.js deleted file mode 100644 index 49a7e3f1a2..0000000000 --- a/system-tests/__snapshots__/domain_spec.js +++ /dev/null @@ -1,107 +0,0 @@ -exports['e2e domain / passes'] = ` - -==================================================================================================== - - (Run Starting) - - ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Cypress: 1.2.3 │ - │ Browser: FooBrowser 88 │ - │ Specs: 2 found (domain.cy.js, domain_2.cy.js) │ - │ Searched: cypress/e2e/domain* │ - └────────────────────────────────────────────────────────────────────────────────────────────────┘ - - -──────────────────────────────────────────────────────────────────────────────────────────────────── - - Running: domain.cy.js (1 of 2) - - - localhost - ✓ can visit - - com.au - ✓ can visit - - herokuapp.com - ✓ can visit - - - 3 passing - - - (Results) - - ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Tests: 3 │ - │ Passing: 3 │ - │ Failing: 0 │ - │ Pending: 0 │ - │ Skipped: 0 │ - │ Screenshots: 0 │ - │ Video: true │ - │ Duration: X seconds │ - │ Spec Ran: domain.cy.js │ - └────────────────────────────────────────────────────────────────────────────────────────────────┘ - - - (Video) - - - Started processing: Compressing to 32 CRF - - Finished processing: /XXX/XXX/XXX/cypress/videos/domain.cy.js.mp4 (X second) - - -──────────────────────────────────────────────────────────────────────────────────────────────────── - - Running: domain_2.cy.js (2 of 2) - - - localhost - ✓ can visit - - com.au - ✓ can visit - - herokuapp.com - ✓ can visit - - - 3 passing - - - (Results) - - ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Tests: 3 │ - │ Passing: 3 │ - │ Failing: 0 │ - │ Pending: 0 │ - │ Skipped: 0 │ - │ Screenshots: 0 │ - │ Video: true │ - │ Duration: X seconds │ - │ Spec Ran: domain_2.cy.js │ - └────────────────────────────────────────────────────────────────────────────────────────────────┘ - - - (Video) - - - Started processing: Compressing to 32 CRF - - Finished processing: /XXX/XXX/XXX/cypress/videos/domain_2.cy.js.mp4 (X second) - - -==================================================================================================== - - (Run Finished) - - - Spec Tests Passing Failing Pending Skipped - ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ ✔ domain.cy.js XX:XX 3 3 - - - │ - ├────────────────────────────────────────────────────────────────────────────────────────────────┤ - │ ✔ domain_2.cy.js XX:XX 3 3 - - - │ - └────────────────────────────────────────────────────────────────────────────────────────────────┘ - ✔ All specs passed! XX:XX 6 6 - - - - - -` diff --git a/system-tests/__snapshots__/session_spec.ts.js b/system-tests/__snapshots__/session_spec.ts.js index 77b602d936..2c869ae2bb 100644 --- a/system-tests/__snapshots__/session_spec.ts.js +++ b/system-tests/__snapshots__/session_spec.ts.js @@ -17,13 +17,6 @@ exports['e2e sessions / session tests'] = ` Running: session.cy.js (1 of 1) - cross origin automations - ✓ get storage - ✓ get storage w/ sessionStorage - ✓ set storage - ✓ get localStorage from all origins - ✓ only gets localStorage from origins visited in test - with a blank session ✓ t1 ✓ t2 @@ -99,15 +92,15 @@ exports['e2e sessions / session tests'] = ` ✓ clears only secure context data - 2/2 - 40 passing + 35 passing 1 pending (Results) ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ Tests: 41 │ - │ Passing: 40 │ + │ Tests: 36 │ + │ Passing: 35 │ │ Failing: 0 │ │ Pending: 1 │ │ Skipped: 0 │ @@ -125,9 +118,9 @@ exports['e2e sessions / session tests'] = ` Spec Tests Passing Failing Pending Skipped ┌────────────────────────────────────────────────────────────────────────────────────────────────┐ - │ ✔ session.cy.js XX:XX 41 40 - 1 - │ + │ ✔ session.cy.js XX:XX 36 35 - 1 - │ └────────────────────────────────────────────────────────────────────────────────────────────────┘ - ✔ All specs passed! XX:XX 41 40 - 1 - + ✔ All specs passed! XX:XX 36 35 - 1 - ` diff --git a/system-tests/__snapshots__/typescript_spec_support_spec.ts.js b/system-tests/__snapshots__/typescript_spec_support_spec.ts.js index 61db89152d..24c288b424 100644 --- a/system-tests/__snapshots__/typescript_spec_support_spec.ts.js +++ b/system-tests/__snapshots__/typescript_spec_support_spec.ts.js @@ -90,7 +90,6 @@ Error: Webpack Compilation Error ./cypress/e2e/typescript_syntax_error.cy.tsXX:XX Module parse failed: Unexpected token (3:19) File was processed with these loaders: - * relative/path/to/webpack-preprocessor/dist/lib/cross-origin-callback-loader.js * relative/path/to/webpack-batteries-included-preprocessor/node_modules/ts-loader/index.js You may need an additional loader to handle the result of these loaders. | // The code below is ignored by eslint diff --git a/system-tests/projects/e2e/cypress/e2e/domain.cy.js b/system-tests/projects/e2e/cypress/e2e/domain.cy.js deleted file mode 100644 index 1914f1cf3e..0000000000 --- a/system-tests/projects/e2e/cypress/e2e/domain.cy.js +++ /dev/null @@ -1,19 +0,0 @@ -/* eslint-disable no-undef */ -describe('localhost', () => { - it('can visit', () => { - cy.visit('http://app.localhost:4848') - }) -}) - -describe('com.au', () => { - it('can visit', () => { - cy.visit('http://foo.bar.baz.com.au:4848') - }) -}) - -describe('herokuapp.com', () => { - it('can visit', () => { - cy.visit('https://cypress-example.herokuapp.com') - cy.contains('Getting Started with Node on Heroku') - }) -}) diff --git a/system-tests/projects/e2e/cypress/e2e/domain_2.cy.js b/system-tests/projects/e2e/cypress/e2e/domain_2.cy.js deleted file mode 100644 index 1914f1cf3e..0000000000 --- a/system-tests/projects/e2e/cypress/e2e/domain_2.cy.js +++ /dev/null @@ -1,19 +0,0 @@ -/* eslint-disable no-undef */ -describe('localhost', () => { - it('can visit', () => { - cy.visit('http://app.localhost:4848') - }) -}) - -describe('com.au', () => { - it('can visit', () => { - cy.visit('http://foo.bar.baz.com.au:4848') - }) -}) - -describe('herokuapp.com', () => { - it('can visit', () => { - cy.visit('https://cypress-example.herokuapp.com') - cy.contains('Getting Started with Node on Heroku') - }) -}) diff --git a/system-tests/projects/run-all-specs/cypress.config.js b/system-tests/projects/run-all-specs/cypress.config.js index 02b9ce581d..2f7b4231b5 100644 --- a/system-tests/projects/run-all-specs/cypress.config.js +++ b/system-tests/projects/run-all-specs/cypress.config.js @@ -4,5 +4,6 @@ module.exports = defineConfig({ e2e: { experimentalRunAllSpecs: true, supportFile: false, + specPattern: '**/*.cy.js', }, }) diff --git a/system-tests/projects/run-all-specs/folder-c/spec-a.cy.js b/system-tests/projects/run-all-specs/folder-c/spec-a.cy.js new file mode 100644 index 0000000000..339da8a4bb --- /dev/null +++ b/system-tests/projects/run-all-specs/folder-c/spec-a.cy.js @@ -0,0 +1,3 @@ +it('runs folder-c/spec-a', () => { + expect(true).eq(true) +}) diff --git a/system-tests/projects/run-all-specs/folder-c/spec-b.cy.js b/system-tests/projects/run-all-specs/folder-c/spec-b.cy.js new file mode 100644 index 0000000000..870145bf8c --- /dev/null +++ b/system-tests/projects/run-all-specs/folder-c/spec-b.cy.js @@ -0,0 +1,3 @@ +it('runs folder-c/spec-b', () => { + expect(true).eq(true) +}) diff --git a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session.cy.js b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session.cy.js index 00ce35aa92..1f43a4dea4 100644 --- a/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session.cy.js +++ b/system-tests/projects/session-and-origin-e2e-specs/cypress/e2e/session/session.cy.js @@ -38,102 +38,6 @@ const sessionUser = (name = 'user0', cacheAcrossSpecs = false) => { }) } -describe('cross origin automations', function () { - it('get storage', () => { - cy.visit('https://localhost:4466/cross_origin_iframe/foo') - .then(() => { - localStorage.key1 = 'val1' - }) - - .then(() => Cypress.session.getStorage({ origin: ['https://127.0.0.1:44665', 'current_origin'] })) - .then((result) => { - expect(result).deep.eq({ - localStorage: [ - { origin: 'https://localhost:4466', value: { key1: 'val1' } }, - { origin: 'https://127.0.0.1:44665', value: { name: 'foo' } }, - ], - sessionStorage: [], - }) - }) - }) - - it('get storage w/ sessionStorage', () => { - cy.visit('https://localhost:4466/cross_origin_iframe/foo') - .then(() => { - localStorage.key1 = 'val' - sessionStorage.key1 = 'val' - }) - - .then(() => Cypress.session.getStorage({ origin: ['https://127.0.0.1:44665', 'current_origin'] })) - .then((result) => { - expect(result).deep.eq({ - localStorage: [ - { origin: 'https://localhost:4466', value: { key1: 'val' } }, - { origin: 'https://127.0.0.1:44665', value: { name: 'foo' } }, - ], - sessionStorage: [ - { origin: 'https://localhost:4466', value: { key1: 'val' } }, - ], - }) - }) - }) - - it('set storage', () => { - cy.visit('https://localhost:4466/cross_origin_iframe/foo') - .then(() => { - localStorage.key1 = 'val1' - }) - .then(() => Cypress.session.setStorage({ localStorage: [{ value: { key2: 'val2' } }] })) - .then(() => { - expect(window.localStorage.key2).eq('val2') - }) - .then(() => { - return Cypress.session.setStorage({ - localStorage: [ - // set localStorage on different origin - { origin: 'https://127.0.0.1:44665', value: { key2: 'val' }, clear: true }, - // set localStorage on current origin - { value: { key3: 'val' }, clear: true }, - ], - }) - }) - .then(() => Cypress.session.getStorage({ origin: ['current_url', 'https://127.0.0.1:44665'] })) - .then((result) => { - expect(result).deep.eq({ - localStorage: [ - { origin: 'https://localhost:4466', value: { key3: 'val' } }, - { origin: 'https://127.0.0.1:44665', value: { key2: 'val' } }, - ], - sessionStorage: [], - }) - }) - }) - - it('get localStorage from all origins', () => { - cy.visit('https://localhost:4466/cross_origin_iframe/foo') - .then(() => { - localStorage.key1 = 'val1' - }) - - .then(() => Cypress.session.getStorage({ origin: '*' })) - .then((result) => { - expect(result.localStorage).deep.eq([{ origin: 'https://localhost:4466', value: { key1: 'val1' } }, { origin: 'https://127.0.0.1:44665', value: { name: 'foo' } }]) - }) - }) - - it('only gets localStorage from origins visited in test', () => { - cy.visit('https://localhost:4466/form') - .then(() => { - localStorage.key1 = 'val1' - }) - - .then(() => Cypress.session.getStorage({ origin: '*' })) - .then((result) => { - expect(result.localStorage).deep.eq([{ origin: 'https://localhost:4466', value: { key1: 'val1' } }]) - }) - }) -}) - describe('with a blank session', () => { beforeEach(() => { cy.session('sess1', diff --git a/system-tests/test/domain_spec.js b/system-tests/test/domain_spec.js deleted file mode 100644 index bc2d303c79..0000000000 --- a/system-tests/test/domain_spec.js +++ /dev/null @@ -1,24 +0,0 @@ -const systemTests = require('../lib/system-tests').default - -const hosts = { - 'app.localhost': '127.0.0.1', - 'foo.bar.baz.com.au': '127.0.0.1', -} - -describe('e2e domain', () => { - systemTests.setup({ - servers: { - port: 4848, - static: true, - }, - }) - - systemTests.it('passes', { - spec: 'domain*', - snapshot: true, - video: false, - config: { - hosts, - }, - }) -})