From 43c5477c6b232087c608a0c8437a65c1c7038443 Mon Sep 17 00:00:00 2001 From: Emily Rohrbough Date: Tue, 28 Jun 2022 13:26:37 -0500 Subject: [PATCH 01/10] chore: merge develop into master v10.3.0 (#22567) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: cypress-bot[bot] <2f0651858c6e38e0+cypress-bot[bot]@users.noreply.github.com> Co-authored-by: Matt Henkes Co-authored-by: Zachary Williams Co-authored-by: Bill Glesias Co-authored-by: Sam Tsai Co-authored-by: Emily Rohrbough Co-authored-by: Renovate Bot Co-authored-by: Lachlan Miller Co-authored-by: Muaz Othman Co-authored-by: Tim Griesser Co-authored-by: Mike Plummer Co-authored-by: Peter Stakoun Co-authored-by: Mike Plummer Co-authored-by: Ryan Manuel Co-authored-by: Kukhyeon Heo Co-authored-by: Ishan Madhusanka Co-authored-by: Blue F Co-authored-by: Zach Bloomquist Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Ely Lucas Co-authored-by: Barthélémy Ledoux Co-authored-by: Tyler Biethman Co-authored-by: Adam Stone Co-authored-by: Chris Breiding Co-authored-by: Dylan Schlabach <26153156+DJSdev@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .vscode/cspell.json | 16 +- .vscode/extensions.json | 2 +- README.md | 8 +- assets/cypress-logo-dark.png | Bin 0 -> 17723 bytes assets/cypress-logo-light.png | Bin 0 -> 17670 bytes browser-versions.json | 4 +- circle.yml | 57 +- cli/types/cypress.d.ts | 2 +- cli/types/tests/cypress-tests.ts | 1 + graphql-codegen.yml | 3 + npm/angular/package.json | 2 +- npm/cypress-schematic/sandbox12/package.json | 2 +- npm/webpack-dev-server/test/.mocharc.js | 2 +- npm/webpack-preprocessor/package.json | 2 +- package.json | 7 +- packages/app/cypress.config.ts | 3 +- .../e2e/cypress-in-cypress-component.cy.ts | 18 +- .../cypress/e2e/cypress-in-cypress-e2e.cy.ts | 18 +- .../app/cypress/e2e/reporter_header.cy.ts | 4 +- packages/app/cypress/e2e/runs.cy.ts | 9 +- .../app/cypress/e2e/sidebar_navigation.cy.ts | 3 +- packages/app/cypress/e2e/specs.cy.ts | 2 +- packages/app/cypress/e2e/specs_list_e2e.cy.ts | 14 +- .../cypress/e2e/specs_list_latest_runs.cy.ts | 633 +++++ .../authChange-subscription.cy.ts | 6 +- .../createCloudOrgModal-subscription.cy.ts | 5 +- .../specChange-subscription.cy.ts | 72 +- .../app/cypress/e2e/support/execute-spec.ts | 2 +- packages/app/package.json | 2 - packages/app/src/pages/Specs/Index.vue | 63 +- packages/app/src/runner/event-manager.ts | 2 +- packages/app/src/runs/CloudConnectButton.vue | 1 + packages/app/src/runs/RunCard.vue | 2 +- packages/app/src/runs/RunResults.cy.tsx | 4 +- packages/app/src/runs/RunResults.vue | 59 +- packages/app/src/runs/RunsError.spec.tsx | 24 +- packages/app/src/runs/RunsErrorRenderer.vue | 22 +- packages/app/src/settings/SettingsCard.vue | 5 +- .../app/src/settings/SettingsContainer.cy.tsx | 17 +- packages/app/src/specs/AverageDuration.cy.tsx | 116 + packages/app/src/specs/AverageDuration.vue | 43 + .../app/src/specs/InlineSpecListHeader.cy.tsx | 19 +- .../app/src/specs/InlineSpecListHeader.vue | 21 +- .../app/src/specs/LastUpdatedHeader.cy.tsx | 35 + packages/app/src/specs/LastUpdatedHeader.vue | 69 + packages/app/src/specs/RunStatusDots.cy.tsx | 128 + packages/app/src/specs/RunStatusDots.vue | 206 ++ .../specs/SpecHeaderCloudDataTooltip.cy.tsx | 225 ++ .../src/specs/SpecHeaderCloudDataTooltip.vue | 244 ++ packages/app/src/specs/SpecListGitInfo.cy.tsx | 81 + packages/app/src/specs/SpecListGitInfo.vue | 30 +- packages/app/src/specs/SpecRunSummary.cy.tsx | 213 ++ packages/app/src/specs/SpecRunSummary.vue | 184 ++ packages/app/src/specs/SpecsList.cy.tsx | 108 +- packages/app/src/specs/SpecsList.vue | 322 ++- packages/app/src/specs/SpecsListHeader.cy.tsx | 48 +- packages/app/src/specs/SpecsListHeader.vue | 16 +- packages/app/src/specs/SpecsListRowItem.vue | 30 +- packages/app/windi.config.ts | 7 +- .../config/__snapshots__/index.spec.ts.js | 1 + packages/config/src/options.ts | 6 + packages/data-context/src/DataContext.ts | 38 + .../data-context/src/actions/AuthActions.ts | 15 +- .../src/actions/DataEmitterActions.ts | 52 +- .../src/actions/MigrationActions.ts | 20 +- .../src/actions/ProjectActions.ts | 9 +- .../data-context/src/data/ProjectConfigIpc.ts | 17 +- .../src/data/ProjectConfigManager.ts | 4 + .../src/data/ProjectLifecycleManager.ts | 4 + .../data-context/src/data/coreDataShape.ts | 3 +- .../src/sources/CloudDataSource.ts | 104 +- .../src/sources/GraphQLDataSource.ts | 27 +- .../src/sources/ProjectDataSource.ts | 19 + .../src/sources/RemotePollingDataSource.ts | 101 + .../src/sources/RemoteRequestDataSource.ts | 463 ++++ packages/data-context/src/sources/index.ts | 2 + .../src/sources/migration/codegen.ts | 69 +- .../src/util/DocumentNodeBuilder.ts | 92 +- .../data-context/src/util/urqlCacheKeys.ts | 7 +- .../test/unit/data/ProjectConfigIpc.spec.ts | 32 + .../unit/data/ProjectConfigManager.spec.ts | 42 + .../unit/data/ProjectLifecycleManager.spec.ts | 18 + .../test/unit/sources/CloudDataSource.spec.ts | 153 +- .../sources/RemoteRequestDataSource.spec.ts | 58 + .../unit/sources/fixtures/graphqlFixtures.ts | 44 + .../unit/util/DocumentNodeBuilder.spec.ts | 17 +- packages/driver/cross-origin-testing.md | 6 +- packages/driver/cypress.config.ts | 1 + .../cypress/e2e/commands/actions/click.cy.js | 4 +- .../cypress/e2e/commands/navigation.cy.js | 6 +- .../cypress/e2e/cypress/command_queue.cy.ts | 13 +- .../cypress/e2e/e2e/origin/cookie_login.cy.ts | 646 ++++- .../cypress/fixtures/auth/cookie-login.html | 64 +- .../cypress/fixtures/primary-origin.html | 43 +- packages/driver/cypress/plugins/server.js | 69 +- packages/driver/cypress/support/utils.js | 4 +- packages/driver/src/cy/actionability.ts | 9 +- packages/driver/src/cy/chai.ts | 6 +- .../driver/src/cy/commands/actions/click.ts | 9 +- packages/driver/src/cy/commands/xhr.ts | 5 + packages/driver/src/cy/focused.ts | 5 +- packages/driver/src/cy/jquery.ts | 2 +- packages/driver/src/cy/keyboard.ts | 20 +- packages/driver/src/cy/mouse.ts | 19 +- packages/driver/src/cy/overrides.ts | 8 +- packages/driver/src/cy/snapshots.ts | 9 +- packages/driver/src/cy/stability.ts | 17 +- packages/driver/src/cy/xhrs.ts | 7 +- packages/driver/src/cypress.ts | 3 + packages/driver/src/cypress/command_queue.ts | 31 +- packages/driver/src/cypress/cy.ts | 49 +- packages/driver/src/cypress/stack_utils.ts | 6 +- packages/driver/src/cypress/state.ts | 27 +- packages/driver/src/dom/elements/find.ts | 2 +- packages/driver/types/window.d.ts | 13 +- packages/extension/app/background.js | 1 + packages/frontend-shared/.windicss/colors.ts | 195 -- .../.windicss/icon-color-plugins.ts | 141 -- .../frontend-shared/.windicss/safelist.ts | 27 - .../frontend-shared/.windicss/shortcuts.ts | 26 - .../cypress/e2e/e2ePluginSetup.ts | 5 +- .../mock-graphql/clientTestUrqlClient.ts | 4 +- .../support/mock-graphql/fakeCloudSpecRun.ts | 77 + .../support/mock-graphql/mountFragment.ts | 21 +- .../support/mock-graphql/stubgql-Mutation.ts | 22 +- packages/frontend-shared/package.json | 5 +- .../script/generate-shiki-theme-ts.ts | 2 +- .../src/assets/icons/cancelled-solid_x16.svg | 3 + .../src/assets/icons/dot-solid_x4.svg | 3 + .../src/assets/icons/errored-solid_x16.svg | 3 + .../src/assets/icons/failed-solid_x16.svg | 4 + .../src/assets/icons/passed-solid_x16.svg | 4 + .../assets/icons/placeholder-solid_x16.svg | 3 + .../src/assets/icons/queued-solid_x16.svg | 5 + .../src/assets/icons/running-outline_x16.svg | 4 + .../frontend-shared/src/components/Input.vue | 21 +- .../src/components/ResultCounts.cy.tsx | 52 + .../src/components/ResultCounts.vue | 68 + .../src/components/Tooltip.cy.tsx | 45 +- .../src/components/Tooltip.vue | 96 +- .../src/composables/useCollapsibleTree.ts | 27 +- .../src/gql-components/Auth.vue | 10 +- .../src/gql-components/HeaderBarContent.vue | 6 + .../gql-components/topnav/LoginModal.cy.tsx | 14 +- .../src/gql-components/topnav/LoginModal.vue | 13 +- .../frontend-shared/src/graphql/urqlClient.ts | 71 +- .../src/graphql/urqlDetailedDebugExchange.ts | 74 + .../src/graphql/urqlGlobalSubscriptions.ts | 3 + .../frontend-shared/src/locales/en-US.json | 56 +- .../src/utils/getUrlWithParams.ts | 8 +- .../frontend-shared/src/utils/getUtmSource.ts | 7 + packages/frontend-shared/src/utils/time.ts | 23 + packages/frontend-shared/vite.config.ts | 16 +- packages/frontend-shared/windi.config.ts | 57 +- packages/graphql/.eslintrc.json | 41 +- packages/graphql/package.json | 3 + packages/graphql/schemas/cloud.graphql | 254 ++ packages/graphql/schemas/schema.graphql | 344 ++- packages/graphql/src/makeGraphQLServer.ts | 8 + packages/graphql/src/plugins/index.ts | 1 + .../src/plugins/nexusRemoteFieldPlugin.ts | 137 ++ .../src/plugins/nexusSlowGuardPlugin.ts | 2 +- packages/graphql/src/schema.ts | 16 +- .../schemaTypes/interfaceTypes/gql-Node.ts | 22 + .../interfaceTypes/gql-ProjectLike.ts | 28 +- .../interfaceTypes/gql-RemoteFetchable.ts | 68 + .../src/schemaTypes/interfaceTypes/index.ts | 2 + .../schemaTypes/objectTypes/gql-Mutation.ts | 15 +- .../src/schemaTypes/objectTypes/gql-Spec.ts | 20 + .../objectTypes/gql-Subscription.ts | 36 +- .../src/stitching/remoteSchemaWrapped.ts | 38 +- .../graphql/src/utils/graphqlTypeUtils.ts | 14 + packages/graphql/src/utils/index.ts | 4 + packages/graphql/test/stubCloudTypes.ts | 2 + packages/launchpad/README.md | 2 +- packages/launchpad/cypress.config.ts | 3 +- .../launchpad/cypress/e2e/global-mode.cy.ts | 3 +- .../launchpad/cypress/e2e/migration.cy.ts | 25 +- .../launchpad/cypress/e2e/open-mode.cy.ts | 11 +- packages/launchpad/package.json | 1 - packages/launchpad/windi.config.ts | 7 +- packages/proxy/lib/http/index.ts | 42 +- packages/proxy/lib/http/request-middleware.ts | 31 +- .../proxy/lib/http/response-middleware.ts | 140 +- packages/proxy/lib/http/util/cookies.ts | 160 ++ packages/proxy/lib/http/util/prerequests.ts | 151 +- .../test/unit/http/request-middleware.spec.ts | 52 + .../unit/http/response-middleware.spec.ts | 355 +-- .../test/unit/http/util/prerequests.spec.ts | 32 +- packages/server/lib/automation/automation.ts | 15 +- packages/server/lib/automation/cookies.ts | 38 +- .../server/lib/browsers/cdp_automation.ts | 14 + packages/server/lib/browsers/utils.ts | 3 +- packages/server/lib/cache.js | 3 + packages/server/lib/cookie-jar.ts | 67 + packages/server/lib/gui/auth.ts | 16 +- packages/server/lib/makeDataContext.ts | 19 +- packages/server/lib/open_project.ts | 4 +- packages/server/lib/plugins/index.js | 41 - packages/server/lib/plugins/index.ts | 28 + packages/server/lib/project-base.ts | 4 + packages/server/lib/saved_state.ts | 7 +- packages/server/lib/server-base.ts | 20 +- packages/server/lib/socket-base.ts | 4 + packages/server/lib/util/process_profiler.ts | 8 +- packages/server/test/integration/cli_spec.js | 19 +- .../server/test/integration/cypress_spec.js | 90 +- .../test/unit/browsers/cdp_automation_spec.ts | 92 +- .../server/test/unit/browsers/firefox_spec.ts | 7 - packages/server/test/unit/config_spec.js | 44 +- packages/server/test/unit/gui/auth_spec.js | 4 +- packages/server/test/unit/gui/events_spec.js | 191 -- .../test/unit/modes/interactive_spec.js | 3 +- .../server/test/unit/open_project_spec.js | 6 +- packages/server/test/unit/saved_state_spec.js | 2 +- packages/types/src/preferences.ts | 2 + scripts/gulp/gulpfile.ts | 4 +- scripts/gulp/tasks/gulpE2ETestScaffold.ts | 2 +- scripts/gulp/tasks/gulpWebpack.ts | 8 + scripts/run-webpack.js | 11 +- system-tests/__snapshots__/config_spec.js | 9 + .../cypress/e2e/z001.spec.js | 9 + .../cypress/e2e/z002.spec.js | 9 + .../cypress/e2e/z003.spec.js | 9 + .../cypress/e2e/z004.spec.js | 9 + .../cypress/e2e/z005.spec.js | 9 + .../cypress/e2e/z006.spec.js | 9 + .../cypress/e2e/z007.spec.js | 9 + .../cypress/e2e/z008.spec.js | 9 + .../cypress/e2e/z009.spec.js | 9 + .../invalid-env-file/cypress.config.js | 3 + .../invalid-env-file/cypress.env.json | 3 + .../README.md | 0 .../customConfig.json | 0 .../cypress/integration/foo.spec.js | 0 .../cypress/plugins/index.js | 0 .../cypress/support/index.ts | 0 .../expected-cypress.config.js | 0 .../README.md | 35 + .../cypress.json | 3 + .../cypress/integration/foo.spec.js | 0 .../cypress/plugins/index.js | 1 + .../cypress/support/index.js | 0 .../expected-cypress.config.js | 11 + .../simple with spaces/cypress.config.js | 8 + .../projects/simple with spaces/package.json | 3 + .../cypress/support/component-index.html | 12 + .../.vscode/extensions.json | 2 +- system-tests/test/config_spec.js | 10 + yarn.lock | 2071 +++++++++-------- 250 files changed, 9099 insertions(+), 2984 deletions(-) create mode 100644 assets/cypress-logo-dark.png create mode 100644 assets/cypress-logo-light.png create mode 100644 packages/app/cypress/e2e/specs_list_latest_runs.cy.ts create mode 100644 packages/app/src/specs/AverageDuration.cy.tsx create mode 100644 packages/app/src/specs/AverageDuration.vue create mode 100644 packages/app/src/specs/LastUpdatedHeader.cy.tsx create mode 100644 packages/app/src/specs/LastUpdatedHeader.vue create mode 100644 packages/app/src/specs/RunStatusDots.cy.tsx create mode 100644 packages/app/src/specs/RunStatusDots.vue create mode 100644 packages/app/src/specs/SpecHeaderCloudDataTooltip.cy.tsx create mode 100644 packages/app/src/specs/SpecHeaderCloudDataTooltip.vue create mode 100644 packages/app/src/specs/SpecListGitInfo.cy.tsx create mode 100644 packages/app/src/specs/SpecRunSummary.cy.tsx create mode 100644 packages/app/src/specs/SpecRunSummary.vue create mode 100644 packages/data-context/src/sources/RemotePollingDataSource.ts create mode 100644 packages/data-context/src/sources/RemoteRequestDataSource.ts create mode 100644 packages/data-context/test/unit/data/ProjectConfigIpc.spec.ts create mode 100644 packages/data-context/test/unit/data/ProjectConfigManager.spec.ts create mode 100644 packages/data-context/test/unit/sources/RemoteRequestDataSource.spec.ts create mode 100644 packages/data-context/test/unit/sources/fixtures/graphqlFixtures.ts delete mode 100644 packages/frontend-shared/.windicss/colors.ts delete mode 100644 packages/frontend-shared/.windicss/icon-color-plugins.ts delete mode 100644 packages/frontend-shared/.windicss/safelist.ts delete mode 100644 packages/frontend-shared/.windicss/shortcuts.ts create mode 100644 packages/frontend-shared/cypress/support/mock-graphql/fakeCloudSpecRun.ts create mode 100644 packages/frontend-shared/src/assets/icons/cancelled-solid_x16.svg create mode 100644 packages/frontend-shared/src/assets/icons/dot-solid_x4.svg create mode 100644 packages/frontend-shared/src/assets/icons/errored-solid_x16.svg create mode 100644 packages/frontend-shared/src/assets/icons/failed-solid_x16.svg create mode 100644 packages/frontend-shared/src/assets/icons/passed-solid_x16.svg create mode 100644 packages/frontend-shared/src/assets/icons/placeholder-solid_x16.svg create mode 100644 packages/frontend-shared/src/assets/icons/queued-solid_x16.svg create mode 100644 packages/frontend-shared/src/assets/icons/running-outline_x16.svg create mode 100644 packages/frontend-shared/src/components/ResultCounts.cy.tsx create mode 100644 packages/frontend-shared/src/components/ResultCounts.vue create mode 100644 packages/frontend-shared/src/graphql/urqlDetailedDebugExchange.ts create mode 100644 packages/frontend-shared/src/utils/getUtmSource.ts create mode 100644 packages/graphql/src/plugins/nexusRemoteFieldPlugin.ts create mode 100644 packages/graphql/src/schemaTypes/interfaceTypes/gql-Node.ts create mode 100644 packages/graphql/src/schemaTypes/interfaceTypes/gql-RemoteFetchable.ts create mode 100644 packages/graphql/src/utils/graphqlTypeUtils.ts create mode 100644 packages/graphql/src/utils/index.ts create mode 100644 packages/proxy/lib/http/util/cookies.ts create mode 100644 packages/server/lib/cookie-jar.ts delete mode 100644 packages/server/lib/plugins/index.js create mode 100644 packages/server/lib/plugins/index.ts delete mode 100644 packages/server/test/unit/gui/events_spec.js create mode 100644 system-tests/projects/cypress-in-cypress/cypress/e2e/z001.spec.js create mode 100644 system-tests/projects/cypress-in-cypress/cypress/e2e/z002.spec.js create mode 100644 system-tests/projects/cypress-in-cypress/cypress/e2e/z003.spec.js create mode 100644 system-tests/projects/cypress-in-cypress/cypress/e2e/z004.spec.js create mode 100644 system-tests/projects/cypress-in-cypress/cypress/e2e/z005.spec.js create mode 100644 system-tests/projects/cypress-in-cypress/cypress/e2e/z006.spec.js create mode 100644 system-tests/projects/cypress-in-cypress/cypress/e2e/z007.spec.js create mode 100644 system-tests/projects/cypress-in-cypress/cypress/e2e/z008.spec.js create mode 100644 system-tests/projects/cypress-in-cypress/cypress/e2e/z009.spec.js create mode 100644 system-tests/projects/invalid-env-file/cypress.config.js create mode 100644 system-tests/projects/invalid-env-file/cypress.env.json rename system-tests/projects/{migration-custom-config-file-root-level => migration-custom-config-file-root-level spaces}/README.md (100%) rename system-tests/projects/{migration-custom-config-file-root-level => migration-custom-config-file-root-level spaces}/customConfig.json (100%) rename system-tests/projects/{migration-custom-config-file-root-level => migration-custom-config-file-root-level spaces}/cypress/integration/foo.spec.js (100%) rename system-tests/projects/{migration-custom-config-file-root-level => migration-custom-config-file-root-level spaces}/cypress/plugins/index.js (100%) rename system-tests/projects/{migration-custom-config-file-root-level => migration-custom-config-file-root-level spaces}/cypress/support/index.ts (100%) rename system-tests/projects/{migration-custom-config-file-root-level => migration-custom-config-file-root-level spaces}/expected-cypress.config.js (100%) create mode 100644 system-tests/projects/migration-e2e-plugins-implicit-index-js/README.md create mode 100644 system-tests/projects/migration-e2e-plugins-implicit-index-js/cypress.json create mode 100644 system-tests/projects/migration-e2e-plugins-implicit-index-js/cypress/integration/foo.spec.js create mode 100644 system-tests/projects/migration-e2e-plugins-implicit-index-js/cypress/plugins/index.js create mode 100644 system-tests/projects/migration-e2e-plugins-implicit-index-js/cypress/support/index.js create mode 100644 system-tests/projects/migration-e2e-plugins-implicit-index-js/expected-cypress.config.js create mode 100644 system-tests/projects/simple with spaces/cypress.config.js create mode 100644 system-tests/projects/simple with spaces/package.json create mode 100644 system-tests/projects/todos/cypress/support/component-index.html diff --git a/.vscode/cspell.json b/.vscode/cspell.json index 0d635abffbad..5649488074b9 100644 --- a/.vscode/cspell.json +++ b/.vscode/cspell.json @@ -6,27 +6,41 @@ "words": [ "Chainable", "composables", + "dedup", "ERRORED", "execa", + "Fetchable", + "Fetchables", "forcedefault", + "getenv", + "graphcache", + "headlessui", "Iconify", + "intlify", "Lachlan", + "loggedin", "msapplication", "NOTESTS", "OVERLIMIT", + "overscan", "Pinia", "pnpm", "pseudoclass", "revparse", "Screenshotting", + "semibold", "shiki", + "speclist", "testid", "TIMEDOUT", + "titleize", + "topnav", "unconfigured", "unplugin", "unrunnable", "unstaged", "urql", + "viewports", "vite", "vitejs", "vueuse", @@ -34,4 +48,4 @@ ], "ignoreWords": [], "import": [] -} +} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 18f46a05a56a..845b2fc55678 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -28,7 +28,7 @@ // Name: Volar // Description: Language server for Vue. Required for any syntax highlighting in Vue files. - "vue.volar", + "Vue.volar", // Name: Code Spell Checker // Description: Add spell-checking help to your code. diff --git a/README.md b/README.md index 5ed74849fdc5..d10a70893421 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@

- + + + + + Cypress Logo + +

Documentation | diff --git a/assets/cypress-logo-dark.png b/assets/cypress-logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..97ced299171f97df744b0f3cb7ab8fbf867bc7ca GIT binary patch literal 17723 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 F2>_5i8V&#e literal 0 HcmV?d00001 diff --git a/browser-versions.json b/browser-versions.json index 423d9c28e419..1ba5d0e62917 100644 --- a/browser-versions.json +++ b/browser-versions.json @@ -1,4 +1,4 @@ { - "chrome:beta": "103.0.5060.42", - "chrome:stable": "102.0.5005.115" + "chrome:beta": "103.0.5060.53", + "chrome:stable": "103.0.5060.53" } diff --git a/circle.yml b/circle.yml index ede5ccf5b0fc..fa0dfbe418e0 100644 --- a/circle.yml +++ b/circle.yml @@ -528,14 +528,45 @@ commands: browser: <> - run: command: | - cmd=$([[ <> == 'true' ]] && echo 'yarn percy exec --parallel -- --') || true - DEBUG=<> \ - CYPRESS_KONFIG_ENV=production \ - CYPRESS_RECORD_KEY=${TEST_LAUNCHPAD_RECORD_KEY:-$MAIN_RECORD_KEY} \ - PERCY_PARALLEL_NONCE=$CIRCLE_SHA1 \ - PERCY_ENABLE=${PERCY_TOKEN:-0} \ - PERCY_PARALLEL_TOTAL=-1 \ - $cmd yarn workspace @packages/<> cypress:run:<> --browser <> --record --parallel --group <>-<> + echo Current working directory is $PWD + echo Total containers $CIRCLE_NODE_TOTAL + + if [[ -v MAIN_RECORD_KEY ]]; then + # internal PR + cmd=$([[ <> == 'true' ]] && echo 'yarn percy exec --parallel -- --') || true + DEBUG=<> \ + CYPRESS_KONFIG_ENV=production \ + CYPRESS_RECORD_KEY=${TEST_LAUNCHPAD_RECORD_KEY:-$MAIN_RECORD_KEY} \ + PERCY_PARALLEL_NONCE=$CIRCLE_SHA1 \ + PERCY_ENABLE=${PERCY_TOKEN:-0} \ + PERCY_PARALLEL_TOTAL=-1 \ + $cmd yarn workspace @packages/<> cypress:run:<> --browser <> --record --parallel --group <>-<> + else + # external PR + + # To make `circleci tests` work correctly, we need to step into the package folder. + cd packages/<> + + GLOB="cypress/e2e/**/*cy.*" + + if [[ <> == 'ct' ]]; then + # component tests are located side by side with the source codes. + GLOB="src/**/*cy.*" + fi + + TESTFILES=$(circleci tests glob "$GLOB" | circleci tests split --total=$CIRCLE_NODE_TOTAL) + echo "Test files for this machine are $TESTFILES" + + # To run the `yarn` command, we need to walk out of the package folder. + cd ../.. + + DEBUG=<> \ + CYPRESS_KONFIG_ENV=production \ + PERCY_PARALLEL_NONCE=$CIRCLE_SHA1 \ + PERCY_ENABLE=${PERCY_TOKEN:-0} \ + PERCY_PARALLEL_TOTAL=-1 \ + yarn workspace @packages/<> cypress:run:<> --browser <> --spec $TESTFILES + fi - run: command: | if [[ <> == 'app' && <> == 'true' && -d "packages/app/cypress/screenshots/runner/screenshot/screenshot.cy.tsx/percy" ]]; then @@ -2359,22 +2390,22 @@ linux-x64-workflow: &linux-x64-workflow requires: - build - run-frontend-shared-component-tests-chrome: - context: [test-runner:launchpad-tests, test-runner:percy] + context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] percy: true requires: - build - run-launchpad-integration-tests-chrome: - context: [test-runner:launchpad-tests, test-runner:percy] + context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] percy: true requires: - build - run-launchpad-component-tests-chrome: - context: [test-runner:launchpad-tests, test-runner:percy] + context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] percy: true requires: - build - run-app-integration-tests-chrome: - context: [test-runner:launchpad-tests, test-runner:percy] + context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] percy: true requires: - build @@ -2387,7 +2418,7 @@ linux-x64-workflow: &linux-x64-workflow requires: - system-tests-node-modules-install - run-app-component-tests-chrome: - context: [test-runner:launchpad-tests, test-runner:percy] + context: [test-runner:cypress-record-key, test-runner:launchpad-tests, test-runner:percy] percy: true requires: - build diff --git a/cli/types/cypress.d.ts b/cli/types/cypress.d.ts index 86040383ba81..334db2f7cb69 100644 --- a/cli/types/cypress.d.ts +++ b/cli/types/cypress.d.ts @@ -3003,7 +3003,7 @@ declare namespace Cypress { xhrUrl: string } - interface TestConfigOverrides extends Partial> { + interface TestConfigOverrides extends Partial>, Partial> { browser?: IsBrowserMatcher | IsBrowserMatcher[] keystrokeDelay?: number } diff --git a/cli/types/tests/cypress-tests.ts b/cli/types/tests/cypress-tests.ts index f28e09c95893..2f7202713699 100644 --- a/cli/types/tests/cypress-tests.ts +++ b/cli/types/tests/cypress-tests.ts @@ -33,6 +33,7 @@ namespace CypressConfigTests { Cypress.config().baseUrl // $ExpectType string | null // setters + Cypress.config('baseUrl', '.') // $ExpectType void Cypress.config({ e2e: { baseUrl: '.' }}) // $ExpectType void Cypress.config({ e2e: { baseUrl: null }}) // $ExpectType void Cypress.config({ e2e: { baseUrl: '.', }}) // $ExpectType void diff --git a/graphql-codegen.yml b/graphql-codegen.yml index 111c3dc6c3c1..c540da7d5694 100644 --- a/graphql-codegen.yml +++ b/graphql-codegen.yml @@ -67,11 +67,14 @@ generates: - 'packages/frontend-shared/script/codegen-type-map.js' './packages/graphql/src/gen/cloud-source-types.gen.ts': + config: + useTypeImports: true schema: 'packages/graphql/schemas/cloud.graphql' plugins: - add: content: '/* eslint-disable */' - 'typescript' + - 'typescript-resolvers' ### # Generates types for us to infer the correct keys for graphcache diff --git a/npm/angular/package.json b/npm/angular/package.json index 82c7e10455bd..711a7a3e93a4 100644 --- a/npm/angular/package.json +++ b/npm/angular/package.json @@ -55,7 +55,7 @@ "raw-loader": "1.0.0", "renderkid": "2.0.5", "rxjs": "~6.6.0", - "semantic-release": "17.4.2", + "semantic-release": "19.0.3", "to-string-loader": "1.1.6", "ts-loader": "8.1.0", "ts-node": "^10.2.1", diff --git a/npm/cypress-schematic/sandbox12/package.json b/npm/cypress-schematic/sandbox12/package.json index 6d4647bd8147..e2ede6c5d19b 100644 --- a/npm/cypress-schematic/sandbox12/package.json +++ b/npm/cypress-schematic/sandbox12/package.json @@ -28,7 +28,7 @@ "@angular/compiler-cli": "~13.1.3", "@types/jasmine": "~3.6.0", "@types/node": "^12.11.1", - "jasmine-core": "~3.7.0", + "jasmine-core": "~3.8.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.0.3", diff --git a/npm/webpack-dev-server/test/.mocharc.js b/npm/webpack-dev-server/test/.mocharc.js index b41c643ee9ec..8b0298fc8597 100644 --- a/npm/webpack-dev-server/test/.mocharc.js +++ b/npm/webpack-dev-server/test/.mocharc.js @@ -1,4 +1,4 @@ module.exports = { spec: 'test/**/*.spec.ts', - timeout: 10000, + timeout: 15000, } diff --git a/npm/webpack-preprocessor/package.json b/npm/webpack-preprocessor/package.json index 465ad7db54a5..390f12d8f08b 100644 --- a/npm/webpack-preprocessor/package.json +++ b/npm/webpack-preprocessor/package.json @@ -54,7 +54,7 @@ "react-dom": "16.13.1", "react-scripts": "3.2", "rimraf": "3.0.2", - "semantic-release": "17.2.3", + "semantic-release": "19.0.3", "sinon": "^9.0.0", "sinon-chai": "^3.5.0", "snap-shot-it": "7.9.2", diff --git a/package.json b/package.json index f41dc4328b5d..e820142fb6e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cypress", - "version": "10.2.0", + "version": "10.3.0", "description": "Cypress.io end to end testing tool", "private": true, "scripts": { @@ -83,6 +83,7 @@ "@graphql-codegen/typed-document-node": "2.2.8", "@graphql-codegen/typescript": "2.4.2", "@graphql-codegen/typescript-operations": "2.2.3", + "@graphql-codegen/typescript-resolvers": "^2.6.4", "@graphql-codegen/typescript-urql-graphcache": "2.2.3", "@graphql-tools/delegate": "8.2.1", "@graphql-tools/utils": "8.2.3", @@ -196,7 +197,7 @@ "print-arch": "1.0.0", "proxyquire": "2.1.3", "rimraf": "3.0.2", - "semantic-release": "17.2.3", + "semantic-release": "19.0.3", "semantic-release-monorepo": "7.0.3", "semver": "7.3.2", "shelljs": "0.8.5", @@ -276,4 +277,4 @@ "sharp": "0.29.3", "vue-template-compiler": "2.6.12" } -} +} \ No newline at end of file diff --git a/packages/app/cypress.config.ts b/packages/app/cypress.config.ts index 772965e18456..7c42687ab789 100644 --- a/packages/app/cypress.config.ts +++ b/packages/app/cypress.config.ts @@ -3,9 +3,10 @@ import getenv from 'getenv' import { initGitRepoForTestProject, resetGitRepoForTestProject } from './cypress/tasks/git' const CYPRESS_INTERNAL_CLOUD_ENV = getenv('CYPRESS_INTERNAL_CLOUD_ENV', process.env.CYPRESS_INTERNAL_ENV || 'development') +const CYPRESS_INTERNAL_DEV_PROJECT_ID = getenv('CYPRESS_INTERNAL_DEV_PROJECT_ID', process.env.CYPRESS_INTERNAL_DEV_PROJECT_ID || 'sehy69') export default defineConfig({ - projectId: CYPRESS_INTERNAL_CLOUD_ENV === 'staging' ? 'ypt4pf' : 'sehy69', + projectId: CYPRESS_INTERNAL_CLOUD_ENV === 'staging' ? 'ypt4pf' : CYPRESS_INTERNAL_DEV_PROJECT_ID, retries: { runMode: 2, openMode: 0, diff --git a/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts b/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts index d1b480379a04..90d5af4b00d9 100644 --- a/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts +++ b/packages/app/cypress/e2e/cypress-in-cypress-component.cy.ts @@ -80,13 +80,13 @@ describe('Cypress In Cypress CT', { viewportWidth: 1500, defaultCommandTimeout: it('redirects to the specs list with error if a spec is not found', () => { cy.visitApp() - const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage + const { title, intro, explainer } = defaultMessages.specPage.noSpecError const badFilePath = 'src/DoesNotExist.spec.js' cy.visitApp(`/specs/runner?file=${badFilePath}`) - cy.contains(noSpecErrorTitle).should('be.visible') - cy.contains(noSpecErrorIntro).should('be.visible') - cy.contains(noSpecErrorExplainer).should('be.visible') + cy.contains(title).should('be.visible') + cy.contains(intro).should('be.visible') + cy.contains(explainer).should('be.visible') cy.contains(getPathForPlatform(badFilePath)).should('be.visible') cy.location() .its('href') @@ -94,11 +94,11 @@ describe('Cypress In Cypress CT', { viewportWidth: 1500, defaultCommandTimeout: // should clear after reload cy.reload() - cy.contains(noSpecErrorTitle).should('not.exist') + cy.contains(title).should('not.exist') }) it('redirects to the specs list with error if an open spec is not found when specs list updates', () => { - const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage + const { title, intro, explainer } = defaultMessages.specPage.noSpecError const goodFilePath = 'src/TestComponent.spec.jsx' @@ -109,9 +109,9 @@ describe('Cypress In Cypress CT', { viewportWidth: 1500, defaultCommandTimeout: cy.withCtx((ctx, o) => { ctx.actions.project.setSpecs(ctx.project.specs.filter((spec) => !spec.absolute.includes(o.path))) }, { path: goodFilePath }).then(() => { - cy.contains(noSpecErrorTitle).should('be.visible') - cy.contains(noSpecErrorIntro).should('be.visible') - cy.contains(noSpecErrorExplainer).should('be.visible') + cy.contains(title).should('be.visible') + cy.contains(intro).should('be.visible') + cy.contains(explainer).should('be.visible') cy.contains(getPathForPlatform(goodFilePath)).should('be.visible') cy.location() .its('href') diff --git a/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts b/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts index 7da4e7badfcd..c657da01ae69 100644 --- a/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts +++ b/packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts @@ -98,13 +98,13 @@ describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout: }) it('redirects to the specs list with error if a spec is not found when navigating', () => { - const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage + const { title, intro, explainer } = defaultMessages.specPage.noSpecError const badFilePath = 'cypress/e2e/does-not-exist.spec.js' cy.visitApp(`/specs/runner?file=${badFilePath}`) - cy.contains(noSpecErrorTitle).should('be.visible') - cy.contains(noSpecErrorIntro).should('be.visible') - cy.contains(noSpecErrorExplainer).should('be.visible') + cy.contains(title).should('be.visible') + cy.contains(intro).should('be.visible') + cy.contains(explainer).should('be.visible') cy.contains(getPathForPlatform(badFilePath)).should('be.visible') cy.location() .its('href') @@ -114,11 +114,11 @@ describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout: // should clear after reload cy.reload() - cy.contains(noSpecErrorTitle).should('not.exist') + cy.contains(title).should('not.exist') }) it('redirects to the specs list with error if an open spec is not found when specs list updates', () => { - const { noSpecErrorTitle, noSpecErrorIntro, noSpecErrorExplainer } = defaultMessages.specPage + const { title, intro, explainer } = defaultMessages.specPage.noSpecError const goodFilePath = 'cypress/e2e/dom-content.spec.js' @@ -129,9 +129,9 @@ describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout: cy.withCtx((ctx, o) => { ctx.actions.project.setSpecs(ctx.project.specs.filter((spec) => !spec.absolute.includes(o.path))) }, { path: goodFilePath }).then(() => { - cy.contains(noSpecErrorTitle).should('be.visible') - cy.contains(noSpecErrorIntro).should('be.visible') - cy.contains(noSpecErrorExplainer).should('be.visible') + cy.contains(title).should('be.visible') + cy.contains(intro).should('be.visible') + cy.contains(explainer).should('be.visible') cy.contains(getPathForPlatform(goodFilePath)).should('be.visible') cy.location() .its('href') diff --git a/packages/app/cypress/e2e/reporter_header.cy.ts b/packages/app/cypress/e2e/reporter_header.cy.ts index d4e5abc05ad1..3f5186584463 100644 --- a/packages/app/cypress/e2e/reporter_header.cy.ts +++ b/packages/app/cypress/e2e/reporter_header.cy.ts @@ -13,7 +13,7 @@ describe('Reporter Header', () => { cy.get('body').type('f') cy.get('[data-selected-spec="true"]').should('contain', 'dom-content').should('have.length', '1') - cy.get('[data-selected-spec="false"]').should('have.length', '18') + cy.get('[data-selected-spec="false"]').should('have.length', '27') }) it('filters the list of specs when searching for specs', () => { @@ -26,7 +26,7 @@ describe('Reporter Header', () => { cy.get('input').clear() - cy.get('[data-cy="spec-file-item"]').should('have.length', '3') + cy.get('[data-cy="spec-file-item"]').should('have.length', 3) cy.get('input').type('asdf', { force: true }) diff --git a/packages/app/cypress/e2e/runs.cy.ts b/packages/app/cypress/e2e/runs.cy.ts index ff6b60ae315f..09ca8bb91bee 100644 --- a/packages/app/cypress/e2e/runs.cy.ts +++ b/packages/app/cypress/e2e/runs.cy.ts @@ -10,7 +10,7 @@ function scaffoldTestingTypeAndVisitRunsPage (testingType: 'e2e' | 'component') // make sure there are no runs found for the project ID cy.remoteGraphQLIntercept(async (obj) => { - if (obj.result.data?.cloudProjectBySlug) { + if (obj.result.data?.cloudProjectBySlug?.runs?.nodes) { obj.result.data.cloudProjectBySlug.runs.nodes = [] } @@ -136,7 +136,7 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { cy.visitApp() cy.remoteGraphQLIntercept(async (obj) => { - if ((obj.operationName === 'CheckCloudOrganizations_cloudViewerChange_cloudViewer' || obj.operationName === 'Runs_cloudViewer')) { + if ((obj.operationName === 'CheckCloudOrganizations_cloudViewerChange_cloudViewer' || obj.operationName === 'Runs_cloudViewer' || obj.operationName === 'SpecsPageContainer_cloudViewer')) { if (obj.result.data?.cloudViewer?.organizations?.nodes) { obj.result.data.cloudViewer.organizations.nodes = [] } @@ -597,8 +597,6 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { let cloudData: any cy.loginUser() - cy.visitApp() - cy.remoteGraphQLIntercept((obj) => { if (obj.operationName === 'Runs_currentProject_cloudProject_cloudProjectBySlug') { cloudData = obj.result @@ -610,7 +608,10 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { return obj.result }) + cy.visitApp() + cy.findByTestId('sidebar-link-runs-page').click() + cy.contains('h2', 'Cannot connect to the Cypress Dashboard') cy.percySnapshot() diff --git a/packages/app/cypress/e2e/sidebar_navigation.cy.ts b/packages/app/cypress/e2e/sidebar_navigation.cy.ts index 27e07abe718e..5fbd5cdd1b36 100644 --- a/packages/app/cypress/e2e/sidebar_navigation.cy.ts +++ b/packages/app/cypress/e2e/sidebar_navigation.cy.ts @@ -221,7 +221,8 @@ describe('Sidebar Navigation', () => { it('has a menu item labeled "Runs" which takes you to the Runs page', () => { cy.get('[data-cy="app-header-bar"]').findByText('Runs').should('not.exist') - cy.findByText('Runs').should('be.visible').click() + + cy.findByTestId('sidebar-link-runs-page').should('have.text', 'Runs').should('be.visible').click() cy.get('[data-cy="app-header-bar"]').findByText('Runs').should('be.visible') cy.get('.router-link-active').findByText('Runs').should('be.visible') }) diff --git a/packages/app/cypress/e2e/specs.cy.ts b/packages/app/cypress/e2e/specs.cy.ts index 1716658ce2e4..d71a2e703e12 100644 --- a/packages/app/cypress/e2e/specs.cy.ts +++ b/packages/app/cypress/e2e/specs.cy.ts @@ -95,7 +95,7 @@ describe('App: Specs', () => { const expectedScaffoldPathsForPlatform = expectedScaffoldPaths.map(getPathForPlatform) - it('scaffolds example files when card is clicked', () => { + it('scaffolds example files when card is clicked', { viewportHeight: 1200 }, () => { cy.get('@ScaffoldCard').click() cy.findByRole('dialog', { diff --git a/packages/app/cypress/e2e/specs_list_e2e.cy.ts b/packages/app/cypress/e2e/specs_list_e2e.cy.ts index 0ef87875ac69..2ec0cc25a7c7 100644 --- a/packages/app/cypress/e2e/specs_list_e2e.cy.ts +++ b/packages/app/cypress/e2e/specs_list_e2e.cy.ts @@ -112,19 +112,19 @@ describe('App: Spec List (E2E)', () => { describe('typing the filter', function () { it('displays only matching spec', function () { - cy.get('button').contains('14 Matches') + cy.get('button').contains('23 Matches') cy.findByLabelText('Search Specs').type('content') cy.get('[data-cy="spec-item"]') .should('have.length', 2) .and('contain', 'dom-content.spec.js') - cy.get('button').contains('2 of 14 Matches') + cy.get('button').contains('2 of 23 Matches') cy.findByLabelText('Search Specs').clear().type('asdf') cy.get('[data-cy="spec-item"]') .should('have.length', 0) - cy.get('button').contains('0 of 14 Matches') + cy.get('button').contains('0 of 23 Matches') }) it('only shows matching folders', () => { @@ -176,7 +176,7 @@ describe('App: Spec List (E2E)', () => { .should('have.value', '') cy.get('[data-cy="spec-item"]') - .should('have.length', 14) + .should('have.length', 23) }) it('clears the filter if the user presses ESC key', function () { @@ -187,8 +187,7 @@ describe('App: Spec List (E2E)', () => { cy.findByLabelText('Search Specs') .should('have.value', '') - cy.get('[data-cy="spec-item"]') - .should('have.length', 14) + cy.get('button').contains('23 Matches') }) it('shows empty message if no results', function () { @@ -203,8 +202,7 @@ describe('App: Spec List (E2E)', () => { cy.findByText('Clear Search').click() cy.focused().should('have.id', 'spec-filter') - cy.get('[data-cy="spec-item"]') - .should('have.length', 14) + cy.get('button').contains('23 Matches') }) //TODO: https://cypress-io.atlassian.net/browse/UNIFY-1588 diff --git a/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts b/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts new file mode 100644 index 000000000000..28a76b60039b --- /dev/null +++ b/packages/app/cypress/e2e/specs_list_latest_runs.cy.ts @@ -0,0 +1,633 @@ +import defaultMessages from '@packages/frontend-shared/src/locales/en-US.json' +import type { CloudRunStatus } from '@packages/frontend-shared/src/generated/graphql' + +function specRowSelector (specFileName: string) { + return `[data-cy-row="${specFileName}"]` +} + +function dotSelector (specFileName: string, dotNumber: 0 | 1 | 2 |'latest') { + return `${specRowSelector(specFileName)} [data-cy="run-status-dot-${dotNumber}"]` +} + +function dotsSkeletonSelector (specFileName: string) { + return `${specRowSelector(specFileName)} [data-cy="run-status-dots-loading"]` +} + +function averageDurationSelector (specFileName: string) { + return `${specRowSelector(specFileName)} [data-cy="average-duration"]` +} + +function specShouldShow (specFileName: string, runDotsClasses: string[], latestRunStatus: CloudRunStatus|'PLACEHOLDER') { + const latestStatusSpinning = latestRunStatus === 'RUNNING' + + type dotIndex = Parameters[1]; + const indexes: dotIndex[] = [0, 1, 2] + + indexes.forEach((i) => { + return cy.get(dotSelector(specFileName, i)).should('have.class', `icon-light-${runDotsClasses.length > i ? runDotsClasses[i] : 'gray-300'}`) + }) + + cy.get(dotSelector(specFileName, 'latest')) + .should(`${latestStatusSpinning ? '' : 'not.'}have.class`, 'animate-spin') + .and('have.attr', 'data-cy-run-status', latestRunStatus) + + // TODO: add link verification + // if (latestRunStatus !== 'PLACEHOLDER') { + // cy.get(`${specRowSelector(specFileName)} [data-cy="run-status-dots"]`).validateExternalLink('https://google.com') + // } +} + +function simulateRunData () { + cy.remoteGraphQLIntercept(async (obj) => { + const fakeRuns = (statuses: string[], idPrefix: string) => { + return statuses.map((s, idx) => { + return { + __typename: 'CloudSpecRun', + id: `SpecRun_${idPrefix}_${idx}`, + status: s, + createdAt: new Date('2022-05-08T03:17:00').toISOString(), + completedAt: new Date('2022-05-08T05:17:00').toISOString(), + runNumber: 432, + groupCount: 2, + specDuration: { + min: 143003, // 2:23 + max: 159120, // 3:40 + __typename: 'SpecDataAggregate', + }, + testsFailed: { + min: 1, + max: 2, + __typename: 'SpecDataAggregate', + }, + testsPassed: { + min: 22, + max: 23, + __typename: 'SpecDataAggregate', + }, + testsSkipped: { + min: null, + max: null, + __typename: 'SpecDataAggregate', + }, + testsPending: { + min: 1, + max: 2, + __typename: 'SpecDataAggregate', + }, + url: 'https://google.com', + } + }) + } + + if (obj.result.data && 'cloudSpecByPath' in obj.result.data) { + // simulate network latency to allow for caching to register + await new Promise((r) => setTimeout(r, 20)) + + const statuses = obj.variables.specPath?.includes('accounts_list.spec.js') ? + ['PASSED', 'FAILED', 'CANCELLED', 'ERRORED'] : + obj.variables.specPath?.includes('app.spec.js') ? + [] : + ['RUNNING', 'PASSED'] + + const runs = fakeRuns(statuses, obj.variables.specPath) + const averageDuration = obj.variables.specPath?.includes('accounts_list.spec.js') ? + 12000 : // 0:12 + 123000 // 2:03 + + obj.result.data.cloudSpecByPath = { + __typename: 'CloudProjectSpec', + retrievedAt: new Date().toISOString(), + id: `id${obj.variables.specPath}`, + specRuns: { + __typename: 'CloudSpecRunConnection', + nodes: runs, + }, + averageDuration, + } + } + + return obj.result + }) +} + +function allVisibleSpecsShouldBePlaceholders () { + cy.findAllByTestId('run-status-dot-0').should('have.class', 'icon-light-gray-300') + cy.findAllByTestId('run-status-dot-1').should('have.class', 'icon-light-gray-300') + cy.findAllByTestId('run-status-dot-2').should('have.class', 'icon-light-gray-300') + cy.findAllByTestId('run-status-dot-latest') + .should('not.have.class', 'animate-spin') + .and('have.attr', 'data-cy-run-status', 'PLACEHOLDER') + + cy.get('.spec-list-container').scrollTo('bottom') + cy.get('.spec-list-container').scrollTo('bottom') +} + +describe('App/Cloud Integration - Latest runs and Average duration', { viewportWidth: 1200, viewportHeight: 900 }, () => { + beforeEach(() => { + cy.scaffoldProject('cypress-in-cypress') + cy.openProject('cypress-in-cypress') + cy.startAppServer() + + cy.withCtx((ctx, o) => { + o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value('fakeBranch') + }) + }) + + context('when no runs are recorded', () => { + beforeEach(() => { + cy.loginUser() + + cy.remoteGraphQLIntercept(async (obj) => { + if (obj.result.data && 'cloudSpecByPath' in obj.result.data) { + obj.result.data.cloudSpecByPath = { + __typename: 'CloudProjectSpecNotFound', + retrievedAt: new Date().toISOString(), + id: `id${obj.variables.specPath}`, + specRuns: { + __typename: 'CloudSpecRunConnection', + nodes: [], + }, + averageDuration: null, + } + } + + return obj.result + }) + + cy.visitApp() + cy.findByTestId('sidebar-link-specs-page').click() + }) + + it('shows placeholders for all visible specs', () => { + allVisibleSpecsShouldBePlaceholders() + }) + }) + + context('when logged out', () => { + beforeEach(() => { + cy.visitApp() + cy.findByTestId('sidebar-link-specs-page').click() + }) + + it('shows placeholders for all visible specs', () => { + allVisibleSpecsShouldBePlaceholders() + }) + + it('shows correct tooltips with log in buttons', () => { + cy.findByTestId('latest-runs-header').trigger('mouseenter') + cy.get('.v-popper__popper--shown') + .should('contain', 'Connect to the Cypress Dashboard to see the status of your latest runs') + .find('button') + .should('have.text', 'Log in to the Dashboard') + .click() + + cy.findByRole('dialog', { name: 'Log in to Cypress' }).within(() => { + cy.get('button').contains('Log In') + cy.get('[aria-label="Close"]').click() + }) + + cy.findByTestId('latest-runs-header').trigger('mouseleave') + + cy.findByTestId('average-duration-header').trigger('mouseenter') + cy.get('.v-popper__popper--shown') + .should('contain', 'Connect to the Cypress Dashboard to see the average spec durations of your latest runs') + .find('button') + .should('have.text', 'Log in to the Dashboard') + .click() + + cy.findByRole('dialog', { name: 'Log in to Cypress' }).within(() => { + cy.get('button').contains('Log In') + cy.get('[aria-label="Close"]').click() + }) + + cy.findByTestId('average-duration-header').trigger('mouseleave') + }) + }) + + context('when project disconnected', () => { + beforeEach(() => { + cy.loginUser() + cy.withCtx((ctx, o) => { + o.sinon.stub(ctx.project, 'projectId').resolves(null) + }) + + cy.visitApp() + cy.findByTestId('sidebar-link-specs-page').click() + }) + + it('shows placeholders for all visible specs', () => { + allVisibleSpecsShouldBePlaceholders() + }) + + it('shows correct tooltips with log in buttons', () => { + cy.findByTestId('latest-runs-header').trigger('mouseenter') + cy.get('.v-popper__popper--shown') + .should('contain', 'Connect to the Cypress Dashboard to see the status of your latest runs') + .find('button') + .should('have.text', 'Connect your project') + .click() + + cy.findByRole('dialog', { name: 'Create project' }).within(() => { + cy.get('[aria-label="Close"]').click({ force: true }) + }) + + cy.findByTestId('latest-runs-header').trigger('mouseleave') + + cy.findByTestId('average-duration-header').trigger('mouseenter') + cy.get('.v-popper__popper--shown') + .should('contain', 'Connect to the Cypress Dashboard to see the average spec durations of your latest runs') + .find('button') + .should('have.text', 'Connect your project') + .click() + + cy.findByRole('dialog', { name: 'Create project' }).within(() => { + cy.get('[aria-label="Close"]').click({ force: true }) + }) + + cy.findByTestId('average-duration-header').trigger('mouseleave') + }) + }) + + context('when not using a branch', () => { + beforeEach(() => { + cy.withCtx((ctx, o) => { + o.sinon.stub(ctx.lifecycleManager.git!, 'currentBranch').value(undefined) + }) + + cy.loginUser() + + cy.visitApp() + cy.findByTestId('sidebar-link-specs-page').click() + }) + + it('shows placeholders for all visible specs', () => { + allVisibleSpecsShouldBePlaceholders() + }) + }) + + context('when runs are recorded', () => { + beforeEach(() => { + cy.loginUser() + + simulateRunData() + + cy.visitApp() + cy.findByTestId('sidebar-link-specs-page').click() + }) + + it('shows correct tooltips', () => { + cy.findByTestId('latest-runs-header').trigger('mouseenter') + cy.get('.v-popper__popper--shown') + .should('contain', 'The status of your latest runs in the Cypress Dashboard') + .find('button') + .should('not.exist') + + cy.findByTestId('latest-runs-header').trigger('mouseleave') + + cy.findByTestId('average-duration-header').trigger('mouseenter') + cy.get('.v-popper__popper--shown') + .should('contain', 'The average spec durations of your latest runs in the Cypress Dashboard') + .find('button') + .should('not.exist') + + cy.findByTestId('average-duration-header').trigger('mouseleave') + }) + + it('shows accurate latest runs and average duration data', () => { + specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED') + cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12') + + // all should use placeholder + specShouldShow('app.spec.js', [], 'PLACEHOLDER') + cy.get(averageDurationSelector('app.spec.js')).contains('2:03') + // shouldn't have a tooltip + cy.get(dotSelector('app.spec.js', 'latest')).trigger('mouseenter') + cy.get('.v-popper__popper--shown').should('not.exist') + cy.get(dotSelector('app.spec.js', 'latest')).trigger('mouseleave') + + // oldest 2 status dots will use placeholder + specShouldShow('accounts_new.spec.js', ['gray-300', 'gray-300', 'jade-400'], 'RUNNING') + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseenter') + cy.get('.v-popper__popper--shown').should('exist') + // TODO: verify the contents of the tooltip + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseleave') + cy.get(averageDurationSelector('accounts_new.spec.js')).contains('2:03') + }) + + it('lazily loads data for off-screen specs', () => { + // make sure the virtualized list didn't load z008.spec.js + cy.get(specRowSelector('z008.spec.js')).should('not.exist') + + cy.get('.spec-list-container').scrollTo('bottom') + // scrolling down should load z008.spec.js with loading status + cy.get(dotsSkeletonSelector('z008.spec.js')).should('exist') + + // then z008.spec.js should show proper data + specShouldShow('z008.spec.js', ['gray-300', 'gray-300', 'jade-400'], 'RUNNING') + cy.get(averageDurationSelector('z008.spec.js')).contains('2:03') + }) + + describe('preserving tree expansion state', () => { + it('should preserve state when row data is updated without additions/deletions', () => { + // Collapse a directory + cy.get('button[data-cy="row-directory-depth-1"]').first() + .should('have.attr', 'aria-expanded', 'true') + .click() + .should('have.attr', 'aria-expanded', 'false') + + // Trigger cloud specs list change by scrolling + cy.get('.spec-list-container') + .scrollTo('bottom', { duration: 500 }) + .wait(100) + .scrollTo('top', { duration: 500 }) + + // Directory should still be collapsed + cy.get('button[data-cy="row-directory-depth-1"]').first() + .should('have.attr', 'aria-expanded', 'false') + }) + + it('should expand all directories when search is performed', () => { + // Collapse a directory + cy.get('button[data-cy="row-directory-depth-0"]').first() + .should('have.attr', 'aria-expanded', 'true') + .click() + .should('have.attr', 'aria-expanded', 'false') + .then((dir) => { + // Perform a search/filter operation + cy.findByLabelText('Search Specs').type(dir.text()[0]) + }) + + // Previously-collapsed directory should automatically expand + cy.get('button[data-cy="row-directory-depth-0"]').first() + .should('have.attr', 'aria-expanded', 'true') + }) + }) + + it('should retain data after app navigation', () => { + // App/Cloud Integration data should load and render to start + specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED') + cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12') + + // Move to Settings page and wait for render + cy.get('a[href="#/settings"]').click() + cy.location('hash').should('include', '/settings') + cy.findByText('Project Settings').should('be.visible') + + // Move back to Specs page and wait for render + cy.get('a[href="#/specs"]').click() + cy.location('hash').should('include', '/specs') + cy.findByText('E2E specs').should('be.visible') + + // App/Cloud Integration data should still be loaded and rendered + specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED') + cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12') + }) + }) + + context('polling indicates new data', () => { + beforeEach(() => { + cy.loginUser() + + cy.remoteGraphQLIntercept(async (obj, testState) => { + const fakeRuns = (statuses: string[], idPrefix: string) => { + return statuses.map((s, idx) => { + return { + __typename: 'CloudSpecRun', + id: `SpecRun_${idPrefix}_${idx}`, + status: s, + createdAt: new Date('2022-05-08T03:17:00').toISOString(), + completedAt: new Date('2022-05-08T05:17:00').toISOString(), + runNumber: 432, + groupCount: 2, + specDuration: { + min: 143003, // 2:23 + max: 159120, // 3:40 + __typename: 'SpecDataAggregate', + }, + testsFailed: { + min: 1, + max: 2, + __typename: 'SpecDataAggregate', + }, + testsPassed: { + min: 22, + max: 23, + __typename: 'SpecDataAggregate', + }, + testsSkipped: { + min: null, + max: null, + __typename: 'SpecDataAggregate', + }, + testsPending: { + min: 1, + max: 2, + __typename: 'SpecDataAggregate', + }, + url: 'https://google.com', + } + }) + } + + const pollingCounter = testState.pollingCounter ?? 0 + + if (obj.result.data && 'cloudSpecByPath' in obj.result.data) { + // simulate network latency to allow for caching to register + await new Promise((r) => setTimeout(r, 20)) + + const statuses = pollingCounter < 2 ? ['PASSED', 'FAILED', 'CANCELLED', 'ERRORED'] : ['FAILED', 'PASSED', 'FAILED', 'CANCELLED', 'ERRORED'] + const runs = fakeRuns(statuses, obj.variables.specPath) + const averageDuration = pollingCounter < 2 ? 12000 : 13000 + + obj.result.data.cloudSpecByPath = { + __typename: 'CloudProjectSpec', + retrievedAt: new Date().toISOString(), + id: `id${obj.variables.specPath}`, + specRuns: { + __typename: 'CloudSpecRunConnection', + nodes: runs, + }, + averageDuration, + } + } else if (obj.result.data && 'cloudLatestRunUpdateSpecData' in obj.result.data) { + const mostRecentUpdate = pollingCounter > 1 ? new Date().toISOString() : new Date('2022-06-10').toISOString() + // initial polling interval is set to every second to avoid long wait times + const pollingInterval = pollingCounter > 1 ? 30 : 1 + + obj.result.data.cloudLatestRunUpdateSpecData = { + __typename: 'CloudLatestRunUpdateSpecData', + mostRecentUpdate, + pollingInterval, + } + + testState.pollingCounter = pollingCounter + 1 + } + + return obj.result + }) + + cy.visitApp() + cy.findByTestId('sidebar-link-specs-page').click() + }) + + it('refreshes view to reflect new data', () => { + specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED') + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseenter') + cy.get('.v-popper__popper--shown').should('exist') + // TODO: verify the contents of the tooltip + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseleave') + cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12') + + cy.wait(1200) + + // new results should be shown + specShouldShow('accounts_list.spec.js', ['gray-300', 'red-400', 'jade-400'], 'FAILED') + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseenter') + cy.get('.v-popper__popper--shown').should('exist') + // TODO: verify the contents of the tooltip + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseleave') + cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:13') + }) + }) + + context('polling indicates no new data', () => { + beforeEach(() => { + cy.loginUser() + + cy.remoteGraphQLIntercept(async (obj, testState) => { + const fakeRuns = (statuses: string[], idPrefix: string) => { + return statuses.map((s, idx) => { + return { + __typename: 'CloudSpecRun', + id: `SpecRun_${idPrefix}_${idx}`, + status: s, + createdAt: new Date('2022-05-08T03:17:00').toISOString(), + completedAt: new Date('2022-05-08T05:17:00').toISOString(), + runNumber: 432, + groupCount: 2, + specDuration: { + min: 143003, // 2:23 + max: 159120, // 3:40 + __typename: 'SpecDataAggregate', + }, + testsFailed: { + min: 1, + max: 2, + __typename: 'SpecDataAggregate', + }, + testsPassed: { + min: 22, + max: 23, + __typename: 'SpecDataAggregate', + }, + testsSkipped: { + min: null, + max: null, + __typename: 'SpecDataAggregate', + }, + testsPending: { + min: 1, + max: 2, + __typename: 'SpecDataAggregate', + }, + url: 'https://google.com', + } + }) + } + + const pollingCounter = testState.pollingCounter ?? 0 + + if (obj.result.data && 'cloudSpecByPath' in obj.result.data) { + // simulate network latency to allow for caching to register + await new Promise((r) => setTimeout(r, 20)) + + const statuses = pollingCounter < 2 ? ['PASSED', 'FAILED', 'CANCELLED', 'ERRORED'] : ['FAILED', 'PASSED', 'FAILED', 'CANCELLED', 'ERRORED'] + const runs = fakeRuns(statuses, obj.variables.specPath) + const averageDuration = pollingCounter < 2 ? 12000 : 13000 + + obj.result.data.cloudSpecByPath = { + __typename: 'CloudProjectSpec', + retrievedAt: new Date().toISOString(), + id: `id${obj.variables.specPath}`, + specRuns: { + __typename: 'CloudSpecRunConnection', + nodes: runs, + }, + averageDuration, + } + } else if (obj.result.data && 'cloudLatestRunUpdateSpecData' in obj.result.data) { + const mostRecentUpdate = new Date('2022-06-10').toISOString() + // initial polling interval is set to every second to avoid long wait times + const pollingInterval = pollingCounter > 1 ? 30 : 1 + + obj.result.data.cloudLatestRunUpdateSpecData = { + __typename: 'CloudLatestRunUpdateSpecData', + mostRecentUpdate, + pollingInterval, + } + + testState.pollingCounter = pollingCounter + 1 + } + + return obj.result + }) + + cy.visitApp() + cy.findByTestId('sidebar-link-specs-page').click() + }) + + it('shows the same data after polling', () => { + specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED') + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseenter') + cy.get('.v-popper__popper--shown').should('exist') + // TODO: verify the contents of the tooltip + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseleave') + cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12') + + cy.wait(1200) + + // new results should be shown + specShouldShow('accounts_list.spec.js', ['orange-400', 'gray-300', 'red-400'], 'PASSED') + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseenter') + cy.get('.v-popper__popper--shown').should('exist') + // TODO: verify the contents of the tooltip + cy.get(dotSelector('accounts_new.spec.js', 'latest')).trigger('mouseleave') + cy.get(averageDurationSelector('accounts_list.spec.js')).contains('0:12') + }) + }) +}) + +describe('App/Cloud Integration - Latest runs and Average duration', { viewportWidth: 1200 }, () => { + context('when offline', () => { + beforeEach(() => { + cy.scaffoldProject('cypress-in-cypress') + cy.goOffline() + cy.wait(300) + cy.openProject('cypress-in-cypress') + cy.startAppServer() + + cy.loginUser() + + simulateRunData() + cy.visitApp() + + cy.findByTestId('sidebar-link-specs-page').click() + + // after navigating to a new page, the browser needs to go offline again + cy.goOffline() + }) + + it('shows placeholders for all visible specs', () => { + allVisibleSpecsShouldBePlaceholders() + }) + + it('shows offline alert then hides it after coming online', () => { + cy.findByTestId('offline-alert') + .should('contain.text', defaultMessages.specPage.offlineWarning.title) + .and('contain.text', defaultMessages.specPage.offlineWarning.explainer) + + cy.goOnline() + cy.findByTestId('offline-alert').should('not.exist') + }) + }) +}) diff --git a/packages/app/cypress/e2e/subscriptions/authChange-subscription.cy.ts b/packages/app/cypress/e2e/subscriptions/authChange-subscription.cy.ts index 6ea15de793d6..acbfa8274f87 100644 --- a/packages/app/cypress/e2e/subscriptions/authChange-subscription.cy.ts +++ b/packages/app/cypress/e2e/subscriptions/authChange-subscription.cy.ts @@ -33,7 +33,7 @@ describe('authChange subscription', () => { cy.contains('Log In') cy.wait(500) cy.withCtx(async (ctx) => { - await ctx.actions.auth.login() + await ctx.actions.auth.login('testing', 'testing') }) cy.contains('Test User') @@ -62,7 +62,7 @@ describe('authChange subscription', () => { cy.contains('Log In') cy.wait(500) cy.withCtx(async (ctx) => { - await ctx.actions.auth.login() + await ctx.actions.auth.login('testing', 'testing') }) cy.contains('Test User') @@ -90,7 +90,7 @@ describe('authChange subscription', () => { cy.contains('Log In') cy.wait(500) cy.withCtx(async (ctx) => { - await ctx.actions.auth.login() + await ctx.actions.auth.login('testing', 'testing') }) cy.contains('Test User') diff --git a/packages/app/cypress/e2e/subscriptions/createCloudOrgModal-subscription.cy.ts b/packages/app/cypress/e2e/subscriptions/createCloudOrgModal-subscription.cy.ts index f6a6cee32c24..d667d7e15685 100644 --- a/packages/app/cypress/e2e/subscriptions/createCloudOrgModal-subscription.cy.ts +++ b/packages/app/cypress/e2e/subscriptions/createCloudOrgModal-subscription.cy.ts @@ -15,11 +15,10 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { cy.startAppServer('component') cy.loginUser() - cy.visitApp() // Simulate no orgs cy.remoteGraphQLIntercept(async (obj) => { - if ((obj.operationName === 'CheckCloudOrganizations_cloudViewerChange_cloudViewer' || obj.operationName === 'Runs_cloudViewer')) { + if ((obj.operationName === 'CheckCloudOrganizations_cloudViewerChange_cloudViewer' || obj.operationName === 'Runs_cloudViewer' || obj.operationName === 'SpecsPageContainer_cloudViewer')) { if (obj.result.data?.cloudViewer?.organizations?.nodes) { obj.result.data.cloudViewer.organizations.nodes = [] } @@ -28,6 +27,8 @@ describe('App: Runs', { viewportWidth: 1200 }, () => { return obj.result }) + cy.visitApp() + cy.findByTestId('sidebar-link-runs-page').click() cy.findByText(defaultMessages.runs.connect.buttonProject).click() diff --git a/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts b/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts index 95e5a37f3b61..9cd761419f6d 100644 --- a/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts +++ b/packages/app/cypress/e2e/subscriptions/specChange-subscription.cy.ts @@ -12,7 +12,8 @@ describe('specChange subscription', () => { describe('specs list', () => { it('responds to specChange event for an added file', () => { cy.get('[data-cy="spec-item-link"]') - .should('have.length', 14) + // cannot assert a length since this is a virtualized list + // .should('have.length', 14) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -23,7 +24,8 @@ describe('specChange subscription', () => { }, { path: getPathForPlatform('cypress/e2e/new-file.spec.js') }) cy.get('[data-cy="spec-item-link"]') - .should('have.length', 15) + // cannot assert a length since this is a virtualized list + // .should('have.length', 15) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -33,7 +35,8 @@ describe('specChange subscription', () => { it('responds to specChange event for a removed file', () => { cy.get('[data-cy="spec-item-link"]') - .should('have.length', 14) + // cannot assert a length since this is a virtualized list + // .should('have.length', 14) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -44,10 +47,12 @@ describe('specChange subscription', () => { }, { path: getPathForPlatform('cypress/e2e/dom-list.spec.js') }) cy.get('[data-cy="spec-item-link"]') - .should('have.length', 13) + // cannot assert a length since this is a virtualized list + // .should('have.length', 13) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') + .should('not.contain', 'dom-list.spec.js') }) it('handles adding the first file', () => { @@ -69,6 +74,15 @@ describe('specChange subscription', () => { getPathForPlatform('cypress/e2e/accounts/accounts_new.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin_users_list.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin.user/foo_list.spec.js'), + getPathForPlatform('cypress/e2e/z001.spec.js'), + getPathForPlatform('cypress/e2e/z002.spec.js'), + getPathForPlatform('cypress/e2e/z003.spec.js'), + getPathForPlatform('cypress/e2e/z004.spec.js'), + getPathForPlatform('cypress/e2e/z005.spec.js'), + getPathForPlatform('cypress/e2e/z006.spec.js'), + getPathForPlatform('cypress/e2e/z007.spec.js'), + getPathForPlatform('cypress/e2e/z008.spec.js'), + getPathForPlatform('cypress/e2e/z009.spec.js'), ], }) @@ -102,6 +116,15 @@ describe('specChange subscription', () => { getPathForPlatform('cypress/e2e/accounts/accounts_new.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin_users_list.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin.user/foo_list.spec.js'), + getPathForPlatform('cypress/e2e/z001.spec.js'), + getPathForPlatform('cypress/e2e/z002.spec.js'), + getPathForPlatform('cypress/e2e/z003.spec.js'), + getPathForPlatform('cypress/e2e/z004.spec.js'), + getPathForPlatform('cypress/e2e/z005.spec.js'), + getPathForPlatform('cypress/e2e/z006.spec.js'), + getPathForPlatform('cypress/e2e/z007.spec.js'), + getPathForPlatform('cypress/e2e/z008.spec.js'), + getPathForPlatform('cypress/e2e/z009.spec.js'), ], }) @@ -119,7 +142,8 @@ describe('specChange subscription', () => { it('responds to a cypress.config.js file change', () => { cy.get('[data-cy="spec-item-link"]') - .should('have.length', 14) + // cannot assert a length since this is a virtualized list + // .should('have.length', 14) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -161,7 +185,7 @@ e2e: { cy.get('body').type('f') cy.get('[data-cy="spec-file-item"]') - .should('have.length', 14) + .should('have.length', 23) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -172,7 +196,7 @@ e2e: { }, { path: getPathForPlatform('cypress/e2e/new-file.spec.js') }) cy.get('[data-cy="spec-file-item"]') - .should('have.length', 15) + .should('have.length', 24) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -187,7 +211,7 @@ e2e: { cy.get('body').type('f') cy.get('[data-cy="spec-file-item"]') - .should('have.length', 14) + .should('have.length', 23) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -198,7 +222,7 @@ e2e: { }, { path: getPathForPlatform('cypress/e2e/dom-list.spec.js') }) cy.get('[data-cy="spec-file-item"]') - .should('have.length', 13) + .should('have.length', 22) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -226,6 +250,15 @@ e2e: { getPathForPlatform('cypress/e2e/accounts/accounts_new.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin_users_list.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin.user/foo_list.spec.js'), + getPathForPlatform('cypress/e2e/z001.spec.js'), + getPathForPlatform('cypress/e2e/z002.spec.js'), + getPathForPlatform('cypress/e2e/z003.spec.js'), + getPathForPlatform('cypress/e2e/z004.spec.js'), + getPathForPlatform('cypress/e2e/z005.spec.js'), + getPathForPlatform('cypress/e2e/z006.spec.js'), + getPathForPlatform('cypress/e2e/z007.spec.js'), + getPathForPlatform('cypress/e2e/z008.spec.js'), + getPathForPlatform('cypress/e2e/z009.spec.js'), ], }) @@ -248,7 +281,7 @@ e2e: { cy.get('body').type('f') cy.get('[data-cy="spec-file-item"]') - .should('have.length', 14) + .should('have.length', 23) .should('contain', 'blank-contents.spec.js') .should('contain', 'dom-container.spec.js') .should('contain', 'dom-content.spec.js') @@ -290,14 +323,14 @@ e2e: { cy.get('[data-cy="spec-pattern"]').contains('cypress/e2e/**/*.spec.{js,ts}') cy.get('[data-cy="file-match-indicator"]') - .should('contain', '14 Matches') + .should('contain', '23 Matches') cy.withCtx(async (ctx, o) => { await ctx.actions.file.writeFileInProject(o.path, '') }, { path: getPathForPlatform('cypress/e2e/new-file.spec.js') }) cy.get('[data-cy="file-match-indicator"]') - .should('contain', '15 Matches') + .should('contain', '24 Matches') }) it('responds to specChange event for a removed file', () => { @@ -307,14 +340,14 @@ e2e: { cy.get('[data-cy="spec-pattern"]').contains('cypress/e2e/**/*.spec.{js,ts}') cy.get('[data-cy="file-match-indicator"]') - .should('contain', '14 Matches') + .should('contain', '23 Matches') cy.withCtx(async (ctx, o) => { await ctx.actions.file.removeFileInProject(o.path) }, { path: getPathForPlatform('cypress/e2e/dom-list.spec.js') }) cy.get('[data-cy="file-match-indicator"]') - .should('contain', '13 Matches') + .should('contain', '22 Matches') }) it('handles removing the last file', () => { @@ -340,6 +373,15 @@ e2e: { getPathForPlatform('cypress/e2e/accounts/accounts_new.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin_users_list.spec.js'), getPathForPlatform('cypress/e2e/admin_users/admin.user/foo_list.spec.js'), + getPathForPlatform('cypress/e2e/z001.spec.js'), + getPathForPlatform('cypress/e2e/z002.spec.js'), + getPathForPlatform('cypress/e2e/z003.spec.js'), + getPathForPlatform('cypress/e2e/z004.spec.js'), + getPathForPlatform('cypress/e2e/z005.spec.js'), + getPathForPlatform('cypress/e2e/z006.spec.js'), + getPathForPlatform('cypress/e2e/z007.spec.js'), + getPathForPlatform('cypress/e2e/z008.spec.js'), + getPathForPlatform('cypress/e2e/z009.spec.js'), ], }) @@ -361,7 +403,7 @@ e2e: { cy.get('[data-cy="spec-pattern"]').contains('cypress/e2e/**/*.spec.{js,ts}') cy.get('[data-cy="file-match-indicator"]') - .should('contain', '14 Matches') + .should('contain', '23 Matches') cy.withCtx(async (ctx) => { await ctx.actions.file.writeFileInProject('cypress.config.js', diff --git a/packages/app/cypress/e2e/support/execute-spec.ts b/packages/app/cypress/e2e/support/execute-spec.ts index e27b85a0512b..5478bbf37c6a 100644 --- a/packages/app/cypress/e2e/support/execute-spec.ts +++ b/packages/app/cypress/e2e/support/execute-spec.ts @@ -9,7 +9,7 @@ declare global { * 3. Waits (with a timeout of 30s) for the Rerun all tests button to be present. This ensures all tests have completed * */ - waitForSpecToFinish() + waitForSpecToFinish(): void } } } diff --git a/packages/app/package.json b/packages/app/package.json index 2f064685b791..99d9eebea668 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -34,7 +34,6 @@ "@vitejs/plugin-vue": "2.2.4", "@vitejs/plugin-vue-jsx": "1.3.8", "@vueuse/core": "7.2.2", - "@windicss/plugin-interaction-variants": "1.0.0", "ansi-to-html": "0.6.14", "bluebird": "3.5.3", "classnames": "2.3.1", @@ -69,7 +68,6 @@ "vue-i18n": "9.2.0-beta.7", "vue-router": "4", "vue-tsc": "^0.3.0", - "windicss": "3.1.4", "wonka": "^4.0.15" }, "files": [ diff --git a/packages/app/src/pages/Specs/Index.vue b/packages/app/src/pages/Specs/Index.vue index bc39deb8e615..eb6945a9fb6f 100644 --- a/packages/app/src/pages/Specs/Index.vue +++ b/packages/app/src/pages/Specs/Index.vue @@ -11,6 +11,7 @@ import { computed, ref } from 'vue' -import { gql, useQuery, useSubscription } from '@urql/vue' +import { gql, SubscriptionHandlerArg, useQuery, useSubscription } from '@urql/vue' import { useI18n } from '@cy/i18n' import SpecsList from '../../specs/SpecsList.vue' import NoSpecsPage from '../../specs/NoSpecsPage.vue' import CreateSpecModal from '../../specs/CreateSpecModal.vue' -import { SpecsPageContainerDocument, SpecsPageContainer_SpecsChangeDocument } from '../../generated/graphql' +import { SpecsPageContainerDocument, SpecsPageContainer_SpecsChangeDocument, SpecsPageContainer_SpecListPollingDocument, SpecsPageContainer_BranchInfoDocument } from '../../generated/graphql' const { t } = useI18n() gql` -query SpecsPageContainer { +query SpecsPageContainer_BranchInfo { + currentProject { + id + branch + projectId + } +} +` + +gql` +query SpecsPageContainer($fromBranch: String!, $hasBranch: Boolean!) { ...Specs_SpecsList ...NoSpecsPage ...CreateSpecModal @@ -47,7 +58,7 @@ query SpecsPageContainer { ` gql` -subscription SpecsPageContainer_specsChange { +subscription SpecsPageContainer_specsChange($fromBranch: String!, $hasBranch: Boolean!) { specsChange { id specs { @@ -58,9 +69,48 @@ subscription SpecsPageContainer_specsChange { } ` -useSubscription({ query: SpecsPageContainer_SpecsChangeDocument }) +gql` +subscription SpecsPageContainer_specListPolling($fromBranch: String, $projectId: String) { + startPollingForSpecs(branchName: $fromBranch, projectId: $projectId) +} +` -const query = useQuery({ query: SpecsPageContainerDocument }) +const branchInfo = useQuery({ query: SpecsPageContainer_BranchInfoDocument }) + +const variables = computed(() => { + const fromBranch = branchInfo.data.value?.currentProject?.branch ?? '' + const hasBranch = Boolean(fromBranch) + + return { fromBranch, hasBranch } +}) + +const pollingVariables = computed(() => { + const fromBranch = branchInfo.data.value?.currentProject?.branch ?? null + const projectId = branchInfo.data.value?.currentProject?.projectId ?? null + + return { fromBranch, projectId } +}) + +useSubscription({ + query: SpecsPageContainer_SpecsChangeDocument, + variables, +}) + +const mostRecentUpdate = ref(null) + +const updateMostRecentUpdate: SubscriptionHandlerArg = (_, reportedUpdate) => { + mostRecentUpdate.value = reportedUpdate?.startPollingForSpecs ?? null +} + +useSubscription({ + query: SpecsPageContainer_SpecListPollingDocument, + variables: pollingVariables, +}, updateMostRecentUpdate) + +const query = useQuery({ + query: SpecsPageContainerDocument, + variables, +}) const isDefaultSpecPattern = computed(() => !!query.data.value?.currentProject?.isDefaultSpecPattern) @@ -83,7 +133,6 @@ const closeCreateSpecModal = () => { modalIsShown.value = false generator.value = null } - diff --git a/packages/app/src/runner/event-manager.ts b/packages/app/src/runner/event-manager.ts index b43db785e39a..5b8be0ed6ae1 100644 --- a/packages/app/src/runner/event-manager.ts +++ b/packages/app/src/runner/event-manager.ts @@ -39,7 +39,7 @@ const driverToSocketEvents = 'backend:request automation:request mocha recorder: const driverTestEvents = 'test:before:run:async test:after:run'.split(' ') const driverToLocalEvents = 'viewport:changed config stop url:changed page:loading visit:failed visit:blank cypress:in:cypress:runner:event'.split(' ') const socketRerunEvents = 'runner:restart watched:file:changed'.split(' ') -const socketToDriverEvents = 'net:stubbing:event request:event script:error'.split(' ') +const socketToDriverEvents = 'net:stubbing:event request:event script:error cross:origin:automation:cookies'.split(' ') const localToReporterEvents = 'reporter:log:add reporter:log:state:changed reporter:log:remove'.split(' ') /** diff --git a/packages/app/src/runs/CloudConnectButton.vue b/packages/app/src/runs/CloudConnectButton.vue index bc2e681d3645..fa0d25d620c6 100644 --- a/packages/app/src/runs/CloudConnectButton.vue +++ b/packages/app/src/runs/CloudConnectButton.vue @@ -10,6 +10,7 @@ diff --git a/packages/app/src/runs/RunResults.cy.tsx b/packages/app/src/runs/RunResults.cy.tsx index f374dc3f9f57..efd23fc9146d 100644 --- a/packages/app/src/runs/RunResults.cy.tsx +++ b/packages/app/src/runs/RunResults.cy.tsx @@ -14,8 +14,8 @@ describe('', { viewportHeight: 150, viewportWidth: 250 }, () => { result[key] = res[key] }) }, - render (gql) { - return + render (props) { + return }, }) diff --git a/packages/app/src/runs/RunResults.vue b/packages/app/src/runs/RunResults.vue index e8b8ec861cb3..fe79447f28f5 100644 --- a/packages/app/src/runs/RunResults.vue +++ b/packages/app/src/runs/RunResults.vue @@ -4,36 +4,19 @@ v-if="props.gql.totalFlakyTests" class="rounded-md font-semibold bg-warning-100 text-sm py-2px px-4px text-warning-600 whitespace-nowrap" >{{ props.gql.totalFlakyTests }} Flaky -

-
- - {{ result.name }} - {{ result.value }} -
-
+ diff --git a/packages/app/src/runs/RunsError.spec.tsx b/packages/app/src/runs/RunsError.spec.tsx index f168bcee2e65..0b3261528c0b 100644 --- a/packages/app/src/runs/RunsError.spec.tsx +++ b/packages/app/src/runs/RunsError.spec.tsx @@ -6,17 +6,19 @@ describe('', () => { cy.mount({ name: 'RunsError', render () { - return (
- - The request timed out when trying to retrieve the recorded runs from the Cypress Dashboard.
- Please refresh the page to try again and visit our Status Page if this behavior continues. -
-
) + return ( +
+ + The request timed out when trying to retrieve the recorded runs from the Cypress Dashboard.
+ Please refresh the page to try again and visit our Status Page if this behavior continues. +
+
+ ) }, }) }) diff --git a/packages/app/src/runs/RunsErrorRenderer.vue b/packages/app/src/runs/RunsErrorRenderer.vue index 8ab117956f16..d9278287d8a8 100644 --- a/packages/app/src/runs/RunsErrorRenderer.vue +++ b/packages/app/src/runs/RunsErrorRenderer.vue @@ -38,7 +38,7 @@