From d864bed4f4d5908ee9bc33b1967fa49d3b0458b2 Mon Sep 17 00:00:00 2001 From: Stefano Magni Date: Wed, 31 Aug 2022 11:03:22 +0200 Subject: [PATCH] console: Setup cypress on the Nx monorepo (close #5463) PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5675 GitOrigin-RevId: b320b6f5bb092d20a4de8e51f6711323dd2b0a05 --- .../cypress/e2e/data/relationships/spec.ts | 3 +- .../cypress/e2e/events/create-trigger/spec.ts | 4 +- console/cypress/helpers/constants.ts | 6 - frontend/apps/console-ce-e2e/.eslintrc.json | 21 +- frontend/apps/console-ce-e2e/README.md | 57 ++ .../apps/console-ce-e2e/cypress.config.ts | 33 +- .../src/e2e/_onboarding/spec.ts | 25 + .../src/e2e/_onboarding/test.ts | 39 ++ .../e2e/actions/query/queryAction.e2e.test.ts | 164 ++++++ .../utils/requests/logMetadataRequests.ts | 36 ++ .../utils/services/deleteAddNumbersAction.ts | 13 + .../query/utils/services/readMetadata.ts | 15 + .../testState/addNumbersActionMustNotExist.ts | 21 + .../console-ce-e2e/src/e2e/actions/test.ts | 59 ++ .../actionWithTransform.e2e.test.ts | 280 +++++++++ .../utils/requests/logMetadataRequests.ts | 36 ++ .../utils/services/deleteLoginAction.ts | 13 + .../utils/services/deleteV1LoginAction.ts | 13 + .../utils/services/readMetadata.ts | 15 + .../testState/loginActionMustNotExist.ts | 21 + .../src/e2e/api-explorer/graphql/spec.ts | 158 ++++++ .../src/e2e/api-explorer/graphql/test.ts | 44 ++ .../apps/console-ce-e2e/src/e2e/app.cy.ts | 13 - .../console-ce-e2e/src/e2e/data/404/test.ts | 28 + .../src/e2e/data/computed-fields/spec.ts | 152 +++++ .../src/e2e/data/computed-fields/test.ts | 45 ++ .../src/e2e/data/create-table/spec.ts | 246 ++++++++ .../src/e2e/data/create-table/test.ts | 48 ++ .../src/e2e/data/custom-functions/spec.ts | 125 ++++ .../src/e2e/data/custom-functions/test.ts | 43 ++ .../src/e2e/data/functions/spec.ts | 198 +++++++ .../src/e2e/data/functions/test.ts | 43 ++ .../src/e2e/data/insert-browse/spec.ts | 533 ++++++++++++++++++ .../src/e2e/data/insert-browse/test.ts | 68 +++ .../e2e/data/manage-database/common.spec.ts | 52 ++ .../manage-database.e2e.test.ts | 160 ++++++ .../e2e/data/manage-database/postgres.spec.ts | 198 +++++++ .../src/e2e/data/materialized-views/spec.ts | 424 ++++++++++++++ .../src/e2e/data/materialized-views/test.ts | 54 ++ .../src/e2e/data/migration-mode/spec.ts | 29 + .../src/e2e/data/migration-mode/test.ts | 27 + .../src/e2e/data/migration-mode/utils.ts | 37 ++ .../src/e2e/data/modify/spec.ts | 439 +++++++++++++++ .../src/e2e/data/modify/test.ts | 75 +++ .../src/e2e/data/permissions/spec.ts | 133 +++++ .../src/e2e/data/permissions/test.ts | 50 ++ .../src/e2e/data/permissions/utils.ts | 158 ++++++ .../src/e2e/data/raw-sql/spec.ts | 96 ++++ .../src/e2e/data/raw-sql/test.ts | 40 ++ .../src/e2e/data/relationships/spec.ts | 326 +++++++++++ .../src/e2e/data/relationships/test.ts | 46 ++ .../src/e2e/data/template-gallery/test.ts | 34 ++ .../console-ce-e2e/src/e2e/data/views/spec.ts | 415 ++++++++++++++ .../console-ce-e2e/src/e2e/data/views/test.ts | 57 ++ .../src/e2e/events/create-trigger/spec.ts | 409 ++++++++++++++ .../src/e2e/events/create-trigger/test.ts | 59 ++ .../src/e2e/events/one-off-trigger/spec.ts | 97 ++++ .../src/e2e/events/one-off-trigger/test.ts | 40 ++ .../create-remote-schema/spec.ts | 279 +++++++++ .../create-remote-schema/test.ts | 83 +++ .../e2e/remote-schemas/edit-schema/spec.ts | 102 ++++ .../e2e/remote-schemas/edit-schema/test.ts | 103 ++++ .../remote-schema-relationships/test.ts | 126 +++++ .../rs-to-db-relationships/test.ts | 92 +++ .../rs-to-rs-relationships/test.ts | 116 ++++ .../settings/metadata/insecure-domain/spec.ts | 69 +++ .../settings/metadata/insecure-domain/test.ts | 21 + .../src/e2e/settings/metadata/spec.ts | 45 ++ .../src/e2e/settings/metadata/test.ts | 16 + .../src/e2e/validators/validators.ts | 529 +++++++++++++++++ .../console-ce-e2e/src/fixtures/example.json | 3 +- frontend/apps/console-ce-e2e/src/global.d.ts | 25 + .../apps/console-ce-e2e/src/helpers/common.ts | 26 + .../console-ce-e2e/src/helpers/constants.ts | 1 + .../src/helpers/core/testMode.ts | 28 + .../console-ce-e2e/src/helpers/dataHelpers.ts | 264 +++++++++ .../src/helpers/eventHelpers.ts | 35 ++ .../console-ce-e2e/src/helpers/metadata.ts | 17 + .../src/helpers/remoteSchemaHelpers.ts | 38 ++ .../src/helpers/webhookTransformHelpers.ts | 82 +++ .../apps/console-ce-e2e/src/support/app.po.ts | 1 - .../src/support/clearConsoleTextarea.ts | 11 + .../console-ce-e2e/src/support/commands.ts | 27 +- .../contractIntercept/contractIntercept.ts | 168 ++++++ .../helpers/checkAndGetTestInfo.ts | 24 + .../helpers/generateDescribesTitle.ts | 27 + .../generateDescribesTitle.unit.test.ts | 88 +++ .../helpers/generateEmptyTestState.ts | 13 + .../helpers/generateTestTitle.ts | 19 + .../helpers/generateTestTitle.unit.test.ts | 57 ++ .../helpers/splitPathTask.unit.test.ts | 18 + .../helpers/throwIfCalledInTestHooks.ts | 25 + .../throwIfCalledInTestHooks.unit.test.ts | 55 ++ .../throwIfCalledInsideArrowFunction.ts | 19 + ...owIfCalledInsideArrowFunction.unit.test.ts | 15 + .../src/support/contractIntercept/index.ts | 1 + .../src/support/contractIntercept/types.ts | 67 +++ .../console-ce-e2e/src/support/index.d.ts | 73 +++ .../src/support/notifications.ts | 33 ++ .../console-ce-e2e/src/support/tasks/index.ts | 4 + .../src/support/tasks/joinPath.ts | 12 + .../src/support/tasks/mkdirSync.ts | 16 + .../src/support/tasks/splitPath.ts | 12 + .../src/support/tasks/writeFileSync.ts | 15 + .../src/support/visitEmptyPage.ts | 10 + frontend/apps/console-ce-e2e/tsconfig.json | 2 +- frontend/apps/console-pro-e2e/.eslintrc.json | 10 - .../apps/console-pro-e2e/cypress.config.ts | 6 - frontend/apps/console-pro-e2e/project.json | 29 - .../apps/console-pro-e2e/src/e2e/app.cy.ts | 13 - .../console-pro-e2e/src/fixtures/example.json | 4 - .../console-pro-e2e/src/support/app.po.ts | 1 - .../console-pro-e2e/src/support/commands.ts | 33 -- .../apps/console-pro-e2e/src/support/e2e.ts | 17 - frontend/apps/console-pro-e2e/tsconfig.json | 10 - frontend/package-lock.json | 197 +++---- frontend/package.json | 1 + frontend/workspace.json | 3 +- 118 files changed, 9034 insertions(+), 275 deletions(-) create mode 100644 frontend/apps/console-ce-e2e/README.md create mode 100644 frontend/apps/console-ce-e2e/src/e2e/_onboarding/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/_onboarding/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/actions/query/queryAction.e2e.test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/requests/logMetadataRequests.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/services/deleteAddNumbersAction.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/services/readMetadata.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/testState/addNumbersActionMustNotExist.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/actions/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/actionWithTransform.e2e.test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/requests/logMetadataRequests.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/services/deleteLoginAction.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/services/deleteV1LoginAction.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/services/readMetadata.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/testState/loginActionMustNotExist.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/api-explorer/graphql/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/api-explorer/graphql/test.ts delete mode 100644 frontend/apps/console-ce-e2e/src/e2e/app.cy.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/404/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/computed-fields/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/computed-fields/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/create-table/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/create-table/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/custom-functions/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/custom-functions/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/functions/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/functions/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/insert-browse/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/insert-browse/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/manage-database/common.spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/manage-database/manage-database.e2e.test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/manage-database/postgres.spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/materialized-views/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/materialized-views/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/migration-mode/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/migration-mode/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/migration-mode/utils.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/modify/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/modify/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/permissions/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/permissions/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/permissions/utils.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/raw-sql/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/raw-sql/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/relationships/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/relationships/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/template-gallery/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/views/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/data/views/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/events/create-trigger/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/events/create-trigger/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/events/one-off-trigger/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/events/one-off-trigger/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/remote-schemas/create-remote-schema/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/remote-schemas/create-remote-schema/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/remote-schemas/edit-schema/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/remote-schemas/edit-schema/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/remote-schemas/remote-schema-relationships/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/remote-schemas/rs-to-db-relationships/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/remote-schemas/rs-to-rs-relationships/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/settings/metadata/insecure-domain/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/settings/metadata/insecure-domain/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/settings/metadata/spec.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/settings/metadata/test.ts create mode 100644 frontend/apps/console-ce-e2e/src/e2e/validators/validators.ts create mode 100644 frontend/apps/console-ce-e2e/src/global.d.ts create mode 100644 frontend/apps/console-ce-e2e/src/helpers/common.ts create mode 100644 frontend/apps/console-ce-e2e/src/helpers/constants.ts create mode 100644 frontend/apps/console-ce-e2e/src/helpers/core/testMode.ts create mode 100644 frontend/apps/console-ce-e2e/src/helpers/dataHelpers.ts create mode 100644 frontend/apps/console-ce-e2e/src/helpers/eventHelpers.ts create mode 100644 frontend/apps/console-ce-e2e/src/helpers/metadata.ts create mode 100644 frontend/apps/console-ce-e2e/src/helpers/remoteSchemaHelpers.ts create mode 100644 frontend/apps/console-ce-e2e/src/helpers/webhookTransformHelpers.ts delete mode 100644 frontend/apps/console-ce-e2e/src/support/app.po.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/clearConsoleTextarea.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/contractIntercept/contractIntercept.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/checkAndGetTestInfo.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateDescribesTitle.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateDescribesTitle.unit.test.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateEmptyTestState.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateTestTitle.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateTestTitle.unit.test.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/splitPathTask.unit.test.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInTestHooks.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInTestHooks.unit.test.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInsideArrowFunction.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInsideArrowFunction.unit.test.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/contractIntercept/index.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/contractIntercept/types.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/index.d.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/notifications.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/tasks/index.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/tasks/joinPath.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/tasks/mkdirSync.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/tasks/splitPath.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/tasks/writeFileSync.ts create mode 100644 frontend/apps/console-ce-e2e/src/support/visitEmptyPage.ts delete mode 100644 frontend/apps/console-pro-e2e/.eslintrc.json delete mode 100644 frontend/apps/console-pro-e2e/cypress.config.ts delete mode 100644 frontend/apps/console-pro-e2e/project.json delete mode 100644 frontend/apps/console-pro-e2e/src/e2e/app.cy.ts delete mode 100644 frontend/apps/console-pro-e2e/src/fixtures/example.json delete mode 100644 frontend/apps/console-pro-e2e/src/support/app.po.ts delete mode 100644 frontend/apps/console-pro-e2e/src/support/commands.ts delete mode 100644 frontend/apps/console-pro-e2e/src/support/e2e.ts delete mode 100644 frontend/apps/console-pro-e2e/tsconfig.json diff --git a/console/cypress/e2e/data/relationships/spec.ts b/console/cypress/e2e/data/relationships/spec.ts index bc85b1366d3..c32addf2ae5 100644 --- a/console/cypress/e2e/data/relationships/spec.ts +++ b/console/cypress/e2e/data/relationships/spec.ts @@ -13,7 +13,8 @@ import { TableFields, } from '../../validators/validators'; import { setPromptValue } from '../../../helpers/common'; -import { AWAIT_SHORT } from '../../../helpers/constants'; + +const AWAIT_SHORT = 2000; const delRel = (table: string, relname: string) => { cy.get(getElementFromAlias(table)).click(); diff --git a/console/cypress/e2e/events/create-trigger/spec.ts b/console/cypress/e2e/events/create-trigger/spec.ts index 125cce6cf36..9be2734a3ca 100644 --- a/console/cypress/e2e/events/create-trigger/spec.ts +++ b/console/cypress/e2e/events/create-trigger/spec.ts @@ -32,7 +32,9 @@ import { clearPayloadTransformBody, clearRequestUrl, } from '../../../helpers/webhookTransformHelpers'; -import { AWAIT_LONG, AWAIT_SHORT } from '../../../helpers/constants'; + +const AWAIT_SHORT = 2000; +const AWAIT_LONG = 7000; const EVENT_REQUEST_BODY_TRANSFORM_TEXTAREA = 1; diff --git a/console/cypress/helpers/constants.ts b/console/cypress/helpers/constants.ts index 5395f78a1d0..993d3c70084 100644 --- a/console/cypress/helpers/constants.ts +++ b/console/cypress/helpers/constants.ts @@ -1,7 +1 @@ export const ADMIN_SECRET_HEADER_KEY = 'x-hasura-admin-secret'; - -// TODO cypress default timeout is 4000, we can remove this `AWAIT_SHORT` after verifying that this is followed by a test command that works with timeout -// https://docs.cypress.io/guides/references/configuration#Timeouts -export const AWAIT_SHORT = 2000; -export const AWAIT_MODERATE = 5000; -export const AWAIT_LONG = 7000; diff --git a/frontend/apps/console-ce-e2e/.eslintrc.json b/frontend/apps/console-ce-e2e/.eslintrc.json index 696cb8b1212..5a613091867 100644 --- a/frontend/apps/console-ce-e2e/.eslintrc.json +++ b/frontend/apps/console-ce-e2e/.eslintrc.json @@ -1,10 +1,27 @@ { - "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], + "extends": [ + "plugin:cypress/recommended", + "../../.eslintrc.json", + "plugin:chai-friendly/recommended" + ], "ignorePatterns": ["!**/*"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} + "rules": { + // TODO: restore it to "error". At the beginning of the Nx migration, we must keep it as is because + // of the huge number of cy.wait() around the legacy tests + "cypress/no-unnecessary-waiting": "warn", + + "no-underscore-dangle": "off", + "@typescript-eslint/no-unused-expressions": "off", + "no-plusplus": [ + "error", + { + "allowForLoopAfterthoughts": true + } + ] + } } ] } diff --git a/frontend/apps/console-ce-e2e/README.md b/frontend/apps/console-ce-e2e/README.md new file mode 100644 index 00000000000..cad00a317c7 --- /dev/null +++ b/frontend/apps/console-ce-e2e/README.md @@ -0,0 +1,57 @@ +# Test + +## Useful resources +- [Cypress Dashboard for the Console project](https://dashboard.cypress.io/projects/5yiuic) + +## Running all tests to generate coverage + +1. Set the `TEST_MODE` field in `cypress.json` to `cli` +2. Run the command `npm run test` from the `console` directory to run all the tests. + +## Running tests individually + +Tests are modularized into following modules: + +- API-Explorer +- Data + - Migration Mode + - Create Table + - Insert Browse + - Modify Table + - Table Relationships + - Table and View Permissions + - Views + +To run the tests for the modules individually (say for create table), + +- Go to the `cypress.json` and set the `env > TEST_MODE` variable to `ui`. + +```json +{ + "env": { + "TEST_MODE": "ui" + } +} +``` + +- Run the command `npm run cy:open` and click on `create-table > test.js` + +## Writing Tests + +- Read ups + + - If this is your first time with cypress, check out this getting started [guide](https://docs.cypress.io/guides/getting-started/writing-your-first-test.html) + + - Read cypress [best practices](https://docs.cypress.io/guides/references/best-practices.html) + +- File Structure + + The top-level directories in [console/cypress](../../console/cypress) are auto-generated by cypress except [helpers](../../console/cypress/helpers). To understand the use of each directory check out [Folder Structure](https://docs.cypress.io/guides/core-concepts/writing-and-organizing-tests.html#Folder-Structure) + + [helpers](../../console/cypress/helpers) directory is used for sharing reusable functions/constants across tests. Before adding a resubale function in this directory, consider if it will be better as a custom cypress command, if so, then add it to [Support](../../console/cypress/support) directory following this [guide](https://docs.cypress.io/api/cypress-api/custom-commands.html), preferrably to [command.ts](../../console/cypress/support/commands.ts) file. + +- Adding a Test + + Tests go to [integration](../../console/cypress/integration) directory, where there are folders corresponding to Components in [Services](../../console/src/components/Services) directory (The top-level routes on the console). + + Each of these folders contains different test folders, named after the particular feature they are testing. For example [create-table](../../console/cypress/integration/data/create-table) folder tests the functionality of creating a table from the console UI. diff --git a/frontend/apps/console-ce-e2e/cypress.config.ts b/frontend/apps/console-ce-e2e/cypress.config.ts index 22f7c84eb63..2604946d7ab 100644 --- a/frontend/apps/console-ce-e2e/cypress.config.ts +++ b/frontend/apps/console-ce-e2e/cypress.config.ts @@ -1,6 +1,37 @@ import { defineConfig } from 'cypress'; import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset'; +import * as customTasks from './src/support/tasks'; + +const nxConfig = nxE2EPreset(__dirname); + export default defineConfig({ - e2e: nxE2EPreset(__dirname), + env: { + TEST_MODE: 'parallel', + MIGRATE_URL: 'http://localhost:9693/apis/migrate', + }, + viewportWidth: 1280, + viewportHeight: 720, + chromeWebSecurity: false, + video: false, + projectId: '5yiuic', + numTestsKeptInMemory: 10, + e2e: { + ...nxConfig, + + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents(on, config) { + on('task', { + ...customTasks, + }); + + return config; + }, + baseUrl: 'http://localhost:3000', + specPattern: [ + 'src/e2e/**/*test.{js,jsx,ts,tsx}', + 'src/support/**/*unit.test.{js,ts}', + ], + }, }); diff --git a/frontend/apps/console-ce-e2e/src/e2e/_onboarding/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/_onboarding/spec.ts new file mode 100644 index 00000000000..24c42fe7f98 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/_onboarding/spec.ts @@ -0,0 +1,25 @@ +import { getElementFromAlias } from '../../helpers/dataHelpers'; + +export const viewOnboarding = () => { + // Click on create + cy.get(getElementFromAlias('onboarding-popup')) + .should('be.visible') + .should('contain.text', `Hi there, let's get started with Hasura!`); + // cy.get(getElementFromAlias('btn-hide-for-now')).click(); +}; +export const hideNow = () => { + // Click on create + cy.get(getElementFromAlias('btn-hide-for-now')).click(); + cy.get(getElementFromAlias('onboarding-popup')).should('not.exist'); +}; + +export const dontShowAgain = () => { + // Click on create + cy.reload(); + cy.get(getElementFromAlias('onboarding-popup')).should('be.visible'); + + cy.get(getElementFromAlias('btn-ob-dont-show-again')).click(); + cy.get(getElementFromAlias('onboarding-popup')).should('not.exist'); + cy.reload(); + cy.get(getElementFromAlias('onboarding-popup')).should('not.exist'); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/_onboarding/test.ts b/frontend/apps/console-ce-e2e/src/e2e/_onboarding/test.ts new file mode 100644 index 00000000000..0d3e45e4ef7 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/_onboarding/test.ts @@ -0,0 +1,39 @@ +import { viewOnboarding, hideNow, dontShowAgain } from './spec'; +import { testMode } from '../../helpers/common'; +import { setMetaData } from '../validators/validators'; +import { getIndexRoute } from '../../helpers/dataHelpers'; + +const setup = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Setup route', () => { + it('Visit the index route', () => { + cy.visit(getIndexRoute()); + setMetaData(); + }); + }); +}; + +export const runActionsTests = () => { + describe('onboarding', () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + it.skip('should show onboarding guide', viewOnboarding); + + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + it.skip('should hide when user click on Hide Now', hideNow); + + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + it.skip( + 'should hide forever when user click on Dont Show again', + dontShowAgain + ); + }); +}; + +if (testMode !== 'cli') { + setup(); + runActionsTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/actions/query/queryAction.e2e.test.ts b/frontend/apps/console-ce-e2e/src/e2e/actions/query/queryAction.e2e.test.ts new file mode 100644 index 00000000000..0125bf60146 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/actions/query/queryAction.e2e.test.ts @@ -0,0 +1,164 @@ +import { testMode } from '../../../helpers/common'; + +import { logMetadataRequests } from './utils/requests/logMetadataRequests'; +import { addNumbersActionMustNotExist } from './utils/testState/addNumbersActionMustNotExist'; + +// NOTE: This test suite does not include cases for relationships, headers and the codegen part + +if (testMode !== 'cli') { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Query Actions', () => { + before(() => { + addNumbersActionMustNotExist(); + logMetadataRequests(); + + cy.visit('/actions/manage/actions'); + }); + + after(() => { + // Cleanup after the whole test file run + + // Ensure the application is not there when manually deleting the created action to avoid any + // potential client-side error that makes the test fail + cy.visitEmptyPage(); + + // Delete the created action, if any + addNumbersActionMustNotExist(); + }); + + it('When the users create, edit, and delete a Query Action, everything should work', () => { + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + cy.log('**--- Step 1: Query Action creation**'); + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + + // -------------------- + cy.log('**--- Click on the Create button of the Actions panel**'); + cy.getBySel('data-create-actions').click(); + + // -------------------- + // Assign an alias to the most unclear selectors for future references + cy.get('textarea').eq(0).as('actionDefinitionTextarea'); + cy.get('textarea').eq(1).as('typeConfigurationTextarea'); + + // -------------------- + cy.log('**--- Type in the Action Definition textarea**'); + cy.get('@actionDefinitionTextarea') + .clearConsoleTextarea() + .type( + `type Query { + addNumbers (numbers: [Int]): AddResult + }`, + { force: true, delay: 0 } + ); + + // -------------------- + cy.log('**--- Type in the Type Configuration textarea**'); + cy.get('@typeConfigurationTextarea') + .clearConsoleTextarea() + .type( + `type AddResult { + sum: Int + }`, + { force: true, delay: 0 } + ); + + // -------------------- + cy.log('**--- Type in the Webhook Handler field**'); + cy.getBySel('action-create-handler-input') + .clearConsoleTextarea() + .type('https://hasura-actions-demo.glitch.me/addNumbers', { + delay: 0, + parseSpecialCharSequences: false, + }); + + // Due to the double server/cli mode behavior, we do not assert about the XHR request payload here + + // -------------------- + cy.log('**--- Click the Create button**'); + cy.getBySel('create-action-btn').click(); + + // Due to the double server/cli mode behavior, we do not assert about the XHR request payload here + + // -------------------- + cy.log('**--- Check if the success notification is visible**'); + cy.expectSuccessNotificationWithTitle('Created action successfully'); + + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + cy.log('**--- Step 2: Permission add and Handler change**'); + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + + // -------------------- + + cy.log('**--- Go the the action page**'); + cy.getBySel('actions-table-links').within(() => { + cy.getBySel('addNumbers').click(); + }); + + // -------------------- + cy.log('**--- Type in the Webhook Handler field**'); + cy.getBySel('action-create-handler-input') + .clearConsoleTextarea() + .type('http://host.docker.internal:3000', { + delay: 0, + // parseSpecialCharSequences: false, + }); + + // -------------------- + cy.log('**--- Click on the Save button**'); + cy.getBySel('save-modify-action-changes').click(); + + // -------------------- + cy.log('**--- Click the Permissions tab**'); + cy.getBySel('actions-permissions').click(); + + // -------------------- + cy.log('**--- Enter a new role**'); + cy.getBySel('role-textbox').type('manager'); + cy.getBySel('manager-Permission').click(); + + // -------------------- + cy.log('**--- Click Save Permissions**'); + cy.getBySel('save-permissions-for-action').click(); + + // -------------------- + cy.log('**--- Check if the success notification is visible**'); + cy.expectSuccessNotificationWithTitle('Permission saved successfully'); + + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + cy.log('**--- Step 3: Query Action delete**'); + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + + // -------------------- + cy.log('**--- Go the the action page**'); + cy.getBySel('actions-table-links').within(() => { + cy.getBySel('addNumbers').click(); + }); + + // -------------------- + cy.log('**--- Set the prompt value**'); + cy.window().then(win => cy.stub(win, 'prompt').returns('addNumbers')); + + cy.log('**--- Click the Delete button**'); + cy.getBySel('delete-action').click(); + + // Due to the double server/cli mode behavior, we do not assert about the XHR request payload here + + // -------------------- + cy.log('**--- Check if the success notification is visible**'); + cy.expectSuccessNotificationWithTitle('Action deleted successfully'); + }); + }); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/requests/logMetadataRequests.ts b/frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/requests/logMetadataRequests.ts new file mode 100644 index 00000000000..ba6012909cb --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/requests/logMetadataRequests.ts @@ -0,0 +1,36 @@ +interface SingleMetadataRequest { + type: string; + // There are a lot of other fields, but tracking them is not important for the purpose of this module +} + +interface BulkMetadataRequest { + type: 'bulk'; + args: SingleMetadataRequest[]; +} + +type MetadataRequest = SingleMetadataRequest | BulkMetadataRequest; + +/* + * Log all the requests outgoing to the Metadata endpoint. + * This is useful to have a glance of the requests that are going to the server. + */ +export function logMetadataRequests() { + cy.intercept('POST', 'http://localhost:8080/v1/metadata', req => { + const noArgs = !req.body.args; + + if (noArgs) return; + + const requestBody = req.body as MetadataRequest; + + if (requestBody.type === 'bulk') { + const request = requestBody as BulkMetadataRequest; + Cypress.log({ message: '*--- Bulk request*' }); + + request.args.forEach(arg => + Cypress.log({ message: `*--- Request: ${arg.type}*` }) + ); + } else { + Cypress.log({ message: `*--- Request: ${requestBody.type}*` }); + } + }); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/services/deleteAddNumbersAction.ts b/frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/services/deleteAddNumbersAction.ts new file mode 100644 index 00000000000..05e0104e7e7 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/services/deleteAddNumbersAction.ts @@ -0,0 +1,13 @@ +/** + * Delete the Action straight from the server. + */ +export function deleteAddNumbersAction() { + Cypress.log({ message: '**--- Action delete: start**' }); + + return cy + .request('POST', 'http://localhost:8080/v1/metadata', { + type: 'drop_action', + args: { name: 'addNumbers' }, + }) + .then(() => Cypress.log({ message: '**--- Action delete: end**' })); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/services/readMetadata.ts b/frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/services/readMetadata.ts new file mode 100644 index 00000000000..161e64927c0 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/services/readMetadata.ts @@ -0,0 +1,15 @@ +/** + * Read the Metadata straight from the server. + */ +export function readMetadata() { + Cypress.log({ message: '**--- Metadata read: start**' }); + + return cy + .request('POST', 'http://localhost:8080/v1/metadata', { + args: {}, + type: 'export_metadata', + }) + .then(_response => { + Cypress.log({ message: '**--- Metadata read: end**' }); + }); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/testState/addNumbersActionMustNotExist.ts b/frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/testState/addNumbersActionMustNotExist.ts new file mode 100644 index 00000000000..7b89a100667 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/actions/query/utils/testState/addNumbersActionMustNotExist.ts @@ -0,0 +1,21 @@ +import { readMetadata } from '../services/readMetadata'; +import { deleteAddNumbersAction } from '../services/deleteAddNumbersAction'; + +/** + * Ensure the Action does not exist. + */ +export function addNumbersActionMustNotExist() { + Cypress.log({ message: '**--- Action check: start**' }); + + readMetadata().then(response => { + const actionExists = !!response.body.actions?.find( + // TODO: properly type it + action => action.name === 'addNumbers' + ); + + if (actionExists) { + Cypress.log({ message: '**--- The Action must be deleted**' }); + deleteAddNumbersAction(); + } + }); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/actions/test.ts b/frontend/apps/console-ce-e2e/src/e2e/actions/test.ts new file mode 100644 index 00000000000..6a21de70acf --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/actions/test.ts @@ -0,0 +1,59 @@ +// const setup = () => { +// describe.skip('Setup route', () => { +// it('Visit the index route', () => { +// cy.visit('/actions/manage/actions'); +// // Get and set validation metadata +// setMetaData(); +// }); +// }); +// }; + +// TODO: what about the codegen part? Why is it not tested? + +// export const runActionsTests = () => { +// describe.skip('Actions', () => { +// The test has been moved to mutationAction.e2e.test +// it('Create Mutation Action', createMutationAction); + +// The test was commented before moving the other ones to mutationAction.e2e.test +// it('Verify Mutation Actions on GraphiQL', verifyMutation); + +// The test has been moved to mutationAction.e2e.test +// it('Modify Mutation Action', modifyMutationAction); + +// The test has been moved to mutationAction.e2e.test +// it('Delete Mutation Action', deleteMutationAction); + +// The test has been moved to queryAction.e2e.test.e2e.test +// it('Create Query Action', createQueryAction); + +// The test was commented before moving the other ones to queryAction.e2e.test +// it('Verify Query Actions on GraphiQL', verifyQuery); + +// The test has been moved to queryAction.e2e.test.e2e.test +// it('Modify Query Action', modifyQueryAction); + +// The test has been moved to queryAction.e2e.test.e2e.test +// it('Delete Query Action', deleteQueryAction); + +// The test has been moved to actionWithTransform.e2e.test.ts +// it('Create Action With Transform', createActionTransform); + +// The test has been moved to actionWithTransform.e2e.test.ts +// it('Update Action With Transform', modifyActionTransform); + +// The test has been moved to actionWithTransform.e2e.test.ts +// it('Delete Action With Transform', deleteActionTransform); + +// The test has been moved to v1ActionWithTransform.e2e.test.ts +// it( +// 'Create an action with V1 Transform and edit it through console, which will lead to the action being saved as V2', +// modifyV1ActionTransform +// ); +// }); +// }; + +// if (testMode !== 'cli') { +// setup(); +// runActionsTests(); +// } diff --git a/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/actionWithTransform.e2e.test.ts b/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/actionWithTransform.e2e.test.ts new file mode 100644 index 00000000000..e01d31ab106 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/actionWithTransform.e2e.test.ts @@ -0,0 +1,280 @@ +import { testMode } from '../../../helpers/common'; + +import { logMetadataRequests } from './utils/requests/logMetadataRequests'; +import { loginActionMustNotExist } from './utils/testState/loginActionMustNotExist'; + +if (testMode !== 'cli') { + describe('Actions with Transform', () => { + before(() => { + loginActionMustNotExist(); + logMetadataRequests(); + + cy.visit('/actions/manage/actions'); + }); + + after(() => { + // Delete the created action, if any + loginActionMustNotExist(); + }); + + it('When the users create, and delete a Action with Transform, everything should work', () => { + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + cy.log('**--- Step 1: Action with Transform creation**'); + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + + // -------------------- + cy.log('**--- Click on the Create button of the Actions panel**'); + cy.getBySel('data-create-actions').click(); + + // Assign an alias to the most unclear selectors for future references + cy.get('textarea').eq(0).as('actionDefinitionTextarea'); + cy.get('textarea').eq(1).as('typeConfigurationTextarea'); + + // -------------------- + cy.log('**--- Type in the Action Definition textarea**'); + cy.get('@actionDefinitionTextarea') + .clearConsoleTextarea() + .type( + `type Mutation { + login (username: String!, password: String!): LoginResponse + }`, + { force: true, delay: 0 } + ); + + // -------------------- + cy.log('**--- Type in the Type Configuration textarea**'); + cy.get('@typeConfigurationTextarea') + .clearConsoleTextarea() + .type( + `type LoginResponse { + accessToken: String! + }`, + { force: true, delay: 0 } + ); + + // -------------------- + cy.log('**--- Click the Add Request Options Transform button**'); + cy.contains('Add Request Options Transform').click(); + + cy.log('**------------------------------**'); + cy.log('**--- Step 1.1: Add URL**'); + cy.log('**------------------------------**'); + + cy.get('[data-cy="Change Request Options"]').within(() => { + // -------------------- + cy.log('**--- Choose POST**'); + cy.contains('POST').click(); + + // -------------------- + cy.log('**--- Type in the Request URL Template field**'); + cy.get('[placeholder="URL Template (Optional)..."]').type('/users'); + }); + + cy.log('**------------------------------**'); + cy.log('**--- Step 1.2: Add Env Var**'); + cy.log('**------------------------------**'); + + // -------------------- + cy.log('**--- Type in the Webhook Handler field**'); + cy.getBySel('action-create-handler-input') + .clearConsoleTextarea() + .type('{{MY_WEBHOOK}}', { + delay: 0, + parseSpecialCharSequences: false, + }); + + // -------------------- + cy.log('**--- Click the Show Sample Context button**'); + cy.contains('Show Sample Context').click(); + + // -------------------- + cy.log('**--- Type in the Env Variables Key field**'); + cy.getBySel('transform-env-vars-kv-key-0').type('MY_WEBHOOK', { + delay: 1, + }); + cy.log('**--- Type in the Env Variables Value field**'); + cy.getBySel('transform-env-vars-kv-value-0').type('https://handler.com', { + delay: 1, + }); + + // -------------------- + cy.get('[data-cy="Change Request Options"]').within(() => { + cy.log('**--- Check the Preview of the Request URL Template**'); + cy.getBySel('transform-requestUrl-preview').should( + 'have.value', + 'https://handler.com/users' + ); + }); + + cy.log('**------------------------------**'); + cy.log('**--- Step 1.3: Add path**'); + cy.log('**------------------------------**'); + + // -------------------- + cy.log('**--- Type in the Webhook Handler field**'); + cy.getBySel('action-create-handler-input') + .clearConsoleTextarea() + .type('https://hasura-actions-demo.glitch.me', { + delay: 0, + parseSpecialCharSequences: false, + }); + + cy.log('**------------------------------**'); + cy.log('**--- Step 1.4: Query Params add**'); + cy.log('**------------------------------**'); + + cy.get('[placeholder="URL Template (Optional)..."]') + .clearConsoleTextarea() + .type('/{{$body.action.name}}', { parseSpecialCharSequences: false }); + + // -------------------- + cy.log('**--- Type in the first Query Params Key field**'); + cy.getBySel('transform-query-params-kv-key-0').type('id'); + cy.log('**--- Type in the first Query Params Value field**'); + cy.getBySel('transform-query-params-kv-value-0').type('5'); + + // -------------------- + cy.log('**--- Type in the second Query Params Key field**'); + cy.getBySel('transform-query-params-kv-key-1').type('name'); + cy.log('**--- Type in the second Query Params Value field**'); + cy.getBySel('transform-query-params-kv-value-1').type( + '{{$body.action.name}}', + { + parseSpecialCharSequences: false, + delay: 0, + } + ); + + // -------------------- + cy.get('[data-cy="Change Request Options"]').within(() => { + cy.log('**--- Check the Preview of the Request URL Template**'); + cy.findByLabelText('Preview').should( + 'have.value', + 'https://hasura-actions-demo.glitch.me/login?name=login&id=5' + ); + }); + + cy.log('**------------------------------**'); + cy.log('**--- Step 1.5: Add Payload Transform**'); + cy.log('**------------------------------**'); + + // -------------------- + cy.log('**--- Click the Add Payload Transform button**'); + cy.contains('Add Payload Transform').click(); + + // -------------------- + cy.get('[data-cy="Change Payload"]').within(() => { + // Assign an alias to the most unclear selectors for future references + cy.get('textarea').eq(1).as('payloadTransformRequestBody'); + + cy.log('**--- Type in the Payload Transform Request Body textarea**'); + cy.get('@payloadTransformRequestBody') + .clearConsoleTextarea() + .type( + `{ + "userInfo": { + "name": {{$body.input.username}}, + "password": {{$body.input.password}}, + "type": {{$body.action.name}} + `, + // delay is set to 1 because setting it to 0 causes the test to fail because writes + // something like + // "name": {{$body.input.username}}name + // in the textarea (the closing "name" is a mistake) + { force: true, delay: 1, parseSpecialCharSequences: false } + ); + }); + + // -------------------- + cy.log('**--- Click the Create button**'); + cy.getBySel('create-action-btn').click(); + + // -------------------- + cy.log('**--- Check if the success notification is visible**'); + cy.expectSuccessNotificationWithTitle('Created action successfully'); + + // ------------------------------------------------------------------------- + // see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // The "Action change" part has been removed since it caused Cypress to crash + // TODO: identify the crashing reason + // ------------------------------------------------------------------------- + + // cy.log('**------------------------------**'); + // cy.log('**------------------------------**'); + // cy.log('**------------------------------**'); + // cy.log('**--- Step 2: Action change**'); + // cy.log('**------------------------------**'); + // cy.log('**------------------------------**'); + // cy.log('**------------------------------**'); + + // // -------------------- + // cy.log('**--- Wait all the requests to be settled**'); + // waitForPostCreationRequests(); + + // cy.get('[data-cy="Change Request Options"]').within(() => { + // // -------------------- + // cy.log('**--- Choose GET**'); + // cy.contains('GET').click(); + + // // -------------------- + // cy.log('**--- Type in the Request URL Template field**'); + + // cy.get('[placeholder="URL Template (Optional)..."]') + // .clearConsoleTextarea() + // .type('/{{$body.action.name}}/actions', { + // delay: 0, + // parseSpecialCharSequences: false, + // }); + + // // -------------------- + // cy.log('**--- Click on the first Remove Query Param button**'); + // cy.getBySel('transform-query-params-kv-remove-button-0').click(); + // }); + + // // -------------------- + // cy.log('**--- Click the Remove Payload Transform button**'); + // cy.contains('Remove Payload Transform').click(); + + // // -------------------- + // cy.log('**--- Click on the Save button**'); + // cy.getBySel('save-modify-action-changes').click(); + + // // -------------------- + // cy.log('**--- Check if the success notification is visible**'); + // cy.expectSuccessNotificationWithTitle('Action saved successfully'); + + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + cy.log('**--- Step 3: Action delete**'); + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + cy.log('**------------------------------**'); + + // -------------------- + cy.log('**--- Go the the action page**'); + cy.getBySel('actions-table-links').within(() => { + cy.getBySel('login').click(); + }); + + // -------------------- + cy.log('**--- Set the prompt value**'); + cy.window().then(win => cy.stub(win, 'prompt').returns('login')); + + cy.log('**--- Click the Delete button**'); + cy.getBySel('delete-action').click(); + + // -------------------- + cy.log('**--- Check the prompt has been called**'); + cy.window().its('prompt').should('be.called'); + + // -------------------- + cy.log('**--- Check if the success notification is visible**'); + cy.expectSuccessNotificationWithTitle('Action deleted successfully'); + }); + }); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/requests/logMetadataRequests.ts b/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/requests/logMetadataRequests.ts new file mode 100644 index 00000000000..ba6012909cb --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/requests/logMetadataRequests.ts @@ -0,0 +1,36 @@ +interface SingleMetadataRequest { + type: string; + // There are a lot of other fields, but tracking them is not important for the purpose of this module +} + +interface BulkMetadataRequest { + type: 'bulk'; + args: SingleMetadataRequest[]; +} + +type MetadataRequest = SingleMetadataRequest | BulkMetadataRequest; + +/* + * Log all the requests outgoing to the Metadata endpoint. + * This is useful to have a glance of the requests that are going to the server. + */ +export function logMetadataRequests() { + cy.intercept('POST', 'http://localhost:8080/v1/metadata', req => { + const noArgs = !req.body.args; + + if (noArgs) return; + + const requestBody = req.body as MetadataRequest; + + if (requestBody.type === 'bulk') { + const request = requestBody as BulkMetadataRequest; + Cypress.log({ message: '*--- Bulk request*' }); + + request.args.forEach(arg => + Cypress.log({ message: `*--- Request: ${arg.type}*` }) + ); + } else { + Cypress.log({ message: `*--- Request: ${requestBody.type}*` }); + } + }); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/services/deleteLoginAction.ts b/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/services/deleteLoginAction.ts new file mode 100644 index 00000000000..025a89365df --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/services/deleteLoginAction.ts @@ -0,0 +1,13 @@ +/** + * Delete the Action straight from the server. + */ +export function deleteLoginAction() { + Cypress.log({ message: '**--- Action delete: start**' }); + + return cy + .request('POST', 'http://localhost:8080/v1/metadata', { + type: 'drop_action', + args: { name: 'login' }, + }) + .then(() => Cypress.log({ message: '**--- Action delete: end**' })); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/services/deleteV1LoginAction.ts b/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/services/deleteV1LoginAction.ts new file mode 100644 index 00000000000..1a55fa63962 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/services/deleteV1LoginAction.ts @@ -0,0 +1,13 @@ +/** + * Delete the Action straight from the server. + */ +export function deleteV1LoginAction() { + Cypress.log({ message: '**--- Action delete: start**' }); + + return cy + .request('POST', 'http://localhost:8080/v1/metadata', { + type: 'drop_action', + args: { name: 'v1Login' }, + }) + .then(() => Cypress.log({ message: '**--- Action delete: end**' })); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/services/readMetadata.ts b/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/services/readMetadata.ts new file mode 100644 index 00000000000..161e64927c0 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/services/readMetadata.ts @@ -0,0 +1,15 @@ +/** + * Read the Metadata straight from the server. + */ +export function readMetadata() { + Cypress.log({ message: '**--- Metadata read: start**' }); + + return cy + .request('POST', 'http://localhost:8080/v1/metadata', { + args: {}, + type: 'export_metadata', + }) + .then(_response => { + Cypress.log({ message: '**--- Metadata read: end**' }); + }); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/testState/loginActionMustNotExist.ts b/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/testState/loginActionMustNotExist.ts new file mode 100644 index 00000000000..df95196c1f8 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/actions/withTransform/utils/testState/loginActionMustNotExist.ts @@ -0,0 +1,21 @@ +import { readMetadata } from '../services/readMetadata'; +import { deleteLoginAction } from '../services/deleteLoginAction'; + +/** + * Ensure the Action does not exist. + */ +export function loginActionMustNotExist() { + Cypress.log({ message: '**--- Action check: start**' }); + + readMetadata().then(response => { + const actionExists = !!response.body.actions?.find( + // TODO: properly type it + action => action.name === 'login' + ); + + if (actionExists) { + Cypress.log({ message: '**--- The Action must be deleted**' }); + deleteLoginAction(); + } + }); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/api-explorer/graphql/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/api-explorer/graphql/spec.ts new file mode 100644 index 00000000000..224b5ce5f72 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/api-explorer/graphql/spec.ts @@ -0,0 +1,158 @@ +import { + getElementFromAlias, + baseUrl, + tableColumnTypeSelector, + makeDataAPIOptions, + getIndexRoute, +} from '../../../helpers/dataHelpers'; +import { validateCT, ResultType } from '../../validators/validators'; +import { toggleOnMigrationMode } from '../../data/migration-mode/utils'; +import { setPromptValue } from '../../../helpers/common'; +// ***************** UTIL FUNCTIONS ************************** + +let adminSecret: string; +let dataApiUrl: string; + +export const createTestTable = () => { + cy.window().then(win => { + adminSecret = win.__env.adminSecret; + dataApiUrl = win.__env.dataApiUrl; + const { consoleMode } = win.__env; + if (consoleMode === 'cli') { + toggleOnMigrationMode(); + } + }); + + // Click on the create table button + cy.visit(getIndexRoute()); + cy.wait(15000); + cy.get(getElementFromAlias('data-create-table')).click(); + // Enter the table name + cy.get(getElementFromAlias('tableName')).type('users'); + // Set first column + cy.get(getElementFromAlias('column-0')).clear().type('id'); + tableColumnTypeSelector('col-type-0'); + cy.get(getElementFromAlias('data_test_column_type_value_serial')) + .first() + .click(); + cy.get(getElementFromAlias('column-1')).clear().type('name'); + tableColumnTypeSelector('col-type-1'); + cy.get(getElementFromAlias('data_test_column_type_value_text')) + .first() + .click(); + + // Set primary key + cy.get(getElementFromAlias('primary-key-select-0')).select('0'); + // Click on create + cy.get(getElementFromAlias('table-create')).click(); + cy.wait(10000); + // Check if the table got created and navigatied to modify table + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/users/modify` + ); + // Validate + validateCT('users', ResultType.SUCCESS); +}; + +export const insertValue = () => { + cy.get(getElementFromAlias('table-insert-rows')).click(); + // Insert a row + cy.get(getElementFromAlias('typed-input-1')).type('someName'); + cy.get(getElementFromAlias('insert-save-button')).click(); +}; + +export const openAPIExplorer = () => { + // Open API Explorer + cy.get(getElementFromAlias('api')).click(); + cy.wait(3000); +}; + +export const checkQuery = () => { + if (adminSecret) { + cy.get(getElementFromAlias('header-key-2')).type('someKey'); + cy.get(getElementFromAlias('header-value-2')).type('someValue'); + } else { + cy.get(getElementFromAlias('header-key-1')).type('someKey'); + cy.get(getElementFromAlias('header-value-1')).type('someValue'); + } + + cy.get('textarea') + .first() + .type('{enter}{uparrow}query{{}users{{}id}}', { force: true }); + cy.wait(1000); + cy.get('.execute-button').click(); + cy.get('.cm-property').contains('id'); + cy.get('.cm-number').contains('1'); +}; + +export const checkMutation = () => { + cy.get('textarea') + .first() + .type( + '{enter}{uparrow}#{leftarrow}{enter}{uparrow}mutation insert_user{{}insert_users(objects:[{{}name:"someName"}]){{}returning{{}id}}}', + { force: true } + ); + cy.wait(1000); + cy.get('.execute-button').click(); + cy.wait(5000); + cy.get('.cm-property').contains('id'); + cy.get('.cm-number').contains('2'); +}; + +export const checkSub = () => { + // Make a subscription + cy.get('textarea') + .first() + .type( + '{enter}{uparrow}#{leftarrow}{enter}{uparrow}subscription{{}users{{}name}}', + { force: true } + ); + cy.wait(1000); + cy.get('.execute-button').click(); + cy.wait(5000); + cy.get('.cm-property').contains('name'); + cy.get('.cm-string').contains('someName'); + // Update the user with id 1 + const reqBody = { + type: 'update', + args: { + table: { + name: 'users', + }, + where: { + id: '1', + }, + $set: { + name: 'someOtherName', + }, + }, + }; + // Make the request + const requestOptions = makeDataAPIOptions(dataApiUrl, adminSecret, reqBody); + cy.request(requestOptions).then(res => { + cy.log(JSON.stringify(res)); + cy.wait(3000); + cy.get('.cm-string').contains('someOtherName'); + }); +}; + +export const delTestTable = () => { + cy.get(getElementFromAlias('data-tab-link')).click(); + // Go to the modify section of the table + cy.get(getElementFromAlias('users')).click(); + cy.get(getElementFromAlias('table-modify')).click(); + setPromptValue('users'); + // Click on delete + cy.get(getElementFromAlias('delete-table')).click(); + // Confirm + cy.window().its('prompt').should('be.called'); + cy.wait(5000); + + // Temporarily disabled, until it's fixed on the main branch + // Match the URL + // cy.url().should('eq', `${baseUrl}/data/default/schema/public`); + + // Validate + validateCT('users', ResultType.FAILURE); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/api-explorer/graphql/test.ts b/frontend/apps/console-ce-e2e/src/e2e/api-explorer/graphql/test.ts new file mode 100644 index 00000000000..aa900d3f51a --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/api-explorer/graphql/test.ts @@ -0,0 +1,44 @@ +import { + openAPIExplorer, + checkQuery, + checkMutation, + createTestTable, + insertValue, + checkSub, + delTestTable, +} from './spec'; + +import { setMetaData } from '../../validators/validators'; +import { testMode } from '../../../helpers/common'; + +const setup = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Setup route', () => { + it('Visit the index route', () => { + // Visit the index route + cy.visit('/'); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runApiExplorerTests = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('API Explorer', () => { + it('Create test table', createTestTable); + it('Insert row into test table', insertValue); + it('Open API Explorer', openAPIExplorer); + it('Check query result', checkQuery); + it('Check mutation result', checkMutation); + it('Check subscription result', checkSub); + it('Delete test table', delTestTable); + }); +}; + +if (testMode !== 'cli') { + setup(); + runApiExplorerTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/app.cy.ts b/frontend/apps/console-ce-e2e/src/e2e/app.cy.ts deleted file mode 100644 index d1e0520abc6..00000000000 --- a/frontend/apps/console-ce-e2e/src/e2e/app.cy.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getGreeting } from '../support/app.po'; - -describe('console-ce', () => { - beforeEach(() => cy.visit('/')); - - it('should display welcome message', () => { - // Custom command example, see `../support/commands.ts` file - cy.login('my-email@something.com', 'myPassword'); - - // Function helper example, see `../support/app.po.ts` file - getGreeting().contains('Welcome console-ce'); - }); -}); diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/404/test.ts b/frontend/apps/console-ce-e2e/src/e2e/data/404/test.ts new file mode 100644 index 00000000000..a103044fe02 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/404/test.ts @@ -0,0 +1,28 @@ +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; +import { getIndexRoute } from '../../../helpers/dataHelpers'; + +const setup = () => { + describe('Setup route', () => { + it('Visit the index route', () => { + // Visit the index route + cy.visit(getIndexRoute()); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const run404Test = () => { + describe('404', () => { + it('Open random page', () => { + cy.visit('/someRandomPage'); + cy.get('h1').contains('404'); + }); + }); +}; + +if (testMode !== 'cli') { + setup(); + run404Test(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/computed-fields/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/data/computed-fields/spec.ts new file mode 100644 index 00000000000..71cfae24f0d --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/computed-fields/spec.ts @@ -0,0 +1,152 @@ +import { + baseUrl, + getElementFromAlias, + getElementFromClassName, +} from '../../../helpers/dataHelpers'; + +const statements = { + createTableSql: + 'CREATE TABLE a_test_test_author (id serial PRIMARY KEY, first_name text, last_name text);', + createCustomFuncSql: `CREATE OR REPLACE FUNCTION test_get_author_full_name(a_test_test_author_row a_test_test_author) + RETURNS TEXT AS $function$ + SELECT a_test_test_author_row.first_name || ' ' || a_test_test_author_row.last_name + $function$ + LANGUAGE sql STABLE;`, + insertData_a1: `INSERT INTO a_test_test_author(first_name, last_name) VALUES ('ruskin', 'bond');`, + insertData_a2: `INSERT INTO a_test_test_author(first_name, last_name) VALUES ('enid', 'blyton');`, + cleanUpSql: 'DROP TABLE a_test_test_author CASCADE;', + graphql: { + query: `{ + a_test_test_author { + full_name # this is the computed field`, + }, +}; + +export const openRawSQL = () => { + cy.get('a').contains('Data').click(); + cy.wait(3000); + cy.get(getElementFromAlias('sql-link')).click(); + cy.wait(3000); + cy.url().should('eq', `${baseUrl}/data/sql`); +}; + +const clearText = () => { + cy.get('textarea').type('{selectall}', { force: true }); + cy.get('textarea').trigger('keydown', { + keyCode: 46, + which: 46, + force: true, + }); + cy.wait(2000); +}; + +// helper to type into the SQL textarea on rawsql page +const typeStatement = ( + statement: string, + shouldClearText = false, + waitTimeUponType = 2000, + endWaitTime = 5000, + uncheckTrackCheckbox = false +) => { + if (shouldClearText) { + clearText(); + } + cy.get('textarea').type(statement, { force: true }); + cy.wait(waitTimeUponType); + if (uncheckTrackCheckbox) { + cy.get(getElementFromAlias('raw-sql-track-check')).uncheck(); + } + cy.get(getElementFromAlias('run-sql')).click(); + // FIXME: maybe necessary for CLI mode + // cy.get(getElementFromAlias('raw-sql-statement-timeout')).should('be.disabled'); + cy.wait(endWaitTime); +}; + +export const createTableAuthor = () => typeStatement(statements.createTableSql); + +export const createCustomFunction = () => + typeStatement(statements.createCustomFuncSql, true, 2000, 5000, true); + +export const insertAuthorsIntoTable = () => { + typeStatement(statements.insertData_a1, true); + typeStatement(statements.insertData_a2, true); + clearText(); +}; + +export const searchForTable = () => { + // ADD LATER: after search functionality is implemented + // cy.get(getElementFromAlias('search-tables')).type('a_test_test_author'); + // cy.get(getElementFromAlias('table-links')).should( + // 'contain', + // 'a_test_test_author' + // ); + cy.get(getElementFromAlias('a_test_test_author')).click(); +}; + +export const openModifySection = () => { + // open modify section + cy.get(getElementFromAlias('table-modify')).click(); + // click on computed field section + cy.get(getElementFromAlias('modify-table-edit-computed-field-0')).click(); + // type name + cy.get(getElementFromAlias('computed-field-name-input')).type('{selectall}', { + force: true, + }); + cy.get(getElementFromAlias('computed-field-name-input')).trigger('keydown', { + keyCode: 46, + which: 46, + force: true, + }); + cy.get(getElementFromAlias('computed-field-name-input')).type('full_name', { + force: true, + }); + cy.wait(2000); + // type & select function name + cy.get(getElementFromClassName('function-name-select__control')) + .children('div') + .click({ multiple: true }) + .find('input') + .focus() + .type('test_get_author_full_name', { force: true }) + .get(getElementFromClassName('function-name-select__menu')) + .first() + .click(); + // enter comment + cy.get( + getElementFromAlias('computed-field-comment-input') + ).type('this is a test comment', { force: true }); + // saving the computed field + cy.get(getElementFromAlias('modify-table-computed-field-0-save')).click(); + // verify that a computed field exists + cy.wait(5000); + cy.get(getElementFromAlias('computed-field-full_name')).contains('full_name'); + cy.wait(5000); +}; + +export const routeToGraphiql = () => { + cy.visit('/api/api-explorer'); + cy.wait(7000); + cy.url().should('eq', `${baseUrl}/api/api-explorer`); +}; + +export const verifyComputedFieldsResult = () => { + // type the query + cy.get('textarea') + .first() + .type(`{enter}{uparrow}${statements.graphql.query}`, { force: true }); + cy.wait(2000); + // execute the query + cy.get('.execute-button').click(); + // verify if full_name is present + cy.get('.cm-property').contains('full_name'); + cy.get('.cm-string').contains('ruskin bond'); + cy.wait(2000); +}; + +export const cleanUpSql = () => typeStatement(statements.cleanUpSql, true); + +export const routeToSQLPage = () => { + cy.visit('/data/sql'); + cy.wait(7000); + cy.url().should('eq', `${baseUrl}/data/sql`); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/computed-fields/test.ts b/frontend/apps/console-ce-e2e/src/e2e/data/computed-fields/test.ts new file mode 100644 index 00000000000..a2b77637dd6 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/computed-fields/test.ts @@ -0,0 +1,45 @@ +import { + openRawSQL, + createTableAuthor, + createCustomFunction, + insertAuthorsIntoTable, + searchForTable, + cleanUpSql, + openModifySection, + routeToGraphiql, + verifyComputedFieldsResult, + routeToSQLPage, +} from './spec'; +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; +import { getIndexRoute } from '../../../helpers/dataHelpers'; + +const setup = () => { + describe('Setup route', () => { + it('Visit the index route', () => { + cy.visit(getIndexRoute()); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runComputedFieldTests = () => { + describe('Computed Fields', () => { + it('Open Raw SQL page', openRawSQL); + it('Create test table', createTableAuthor); + it('Run SQL for custom function', createCustomFunction); + it('Insert authors into table', insertAuthorsIntoTable); + it('Search for table', searchForTable); + it('Open Modify page and add computed field', openModifySection); + it('Route to GraphiQL page', routeToGraphiql); + it('Check computed field results on GraphiQL', verifyComputedFieldsResult); + it('Route to Raw SQL page', routeToSQLPage); + it('Test cleanup', cleanUpSql); + }); +}; + +if (testMode !== 'cli') { + setup(); + runComputedFieldTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/create-table/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/data/create-table/spec.ts new file mode 100644 index 00000000000..1e621d975a9 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/create-table/spec.ts @@ -0,0 +1,246 @@ +import { + tableColumnTypeSelector, + getElementFromAlias, + getTableName, + getColName, + baseUrl, + getIndexRoute, +} from '../../../helpers/dataHelpers'; +import { + setMetaData, + validateCT, + ResultType, +} from '../../validators/validators'; +import { setPromptValue } from '../../../helpers/common'; + +const testName = 'ct'; + +export const checkCreateTableRoute = () => { + // Click on the create table button + cy.visit(getIndexRoute()); + cy.wait(15000); + cy.get(getElementFromAlias('data-create-table')).click(); + // Match the URL + cy.url().should('eq', `${baseUrl}/data/default/schema/public/table/add`); +}; + +export const failCTWithoutColumns = () => { + // Type table name + cy.get(getElementFromAlias('tableName')).type(getTableName(0, testName)); + // Click on create + cy.get(getElementFromAlias('table-create')).click(); + // Check if the route didn't change + cy.url().should('eq', `${baseUrl}/data/default/schema/public/table/add`); + // Validate + validateCT(getTableName(0, testName), ResultType.FAILURE); +}; + +export const failCTWithoutPK = () => { + // Set first column + cy.get(getElementFromAlias('column-0')).type(getColName(0)); + tableColumnTypeSelector('col-type-0'); + cy.get(getElementFromAlias('data_test_column_type_value_serial')) + .first() + .click(); + // Click on create + cy.get(getElementFromAlias('table-create')).click(); + // Check if the route didn't change + cy.url().should('eq', `${baseUrl}/data/default/schema/public/table/add`); + // Validate + validateCT(getTableName(0, testName), ResultType.FAILURE); +}; + +export const failCTDuplicateColumns = () => { + // Set second column + cy.get(getElementFromAlias('column-1')).type(getColName(0)); + tableColumnTypeSelector('col-type-1'); + cy.get(getElementFromAlias('data_test_column_type_value_serial')) + .first() + .click(); + // Set primary key + cy.get(getElementFromAlias('primary-key-select-0')).select('0'); + // Click on create + cy.get(getElementFromAlias('table-create')).click(); + // Check for an alert + cy.on('window:alert', str => { + expect( + str === `You have the following column names repeated: [${getColName(0)}]` + ).to.be.true; + }); + // Check if the route didn't change + cy.url().should('eq', `${baseUrl}/data/default/schema/public/table/add`); + // Validate + validateCT(getTableName(0, testName), ResultType.FAILURE); +}; + +export const failCTWrongDefaultValue = () => { + // Set second column + cy.get(getElementFromAlias('column-1')).clear().type(getColName(1)); + tableColumnTypeSelector('col-type-1'); + cy.get(getElementFromAlias('data_test_column_type_value_integer')) + .first() + .click(); + cy.get(getElementFromAlias('col-default-1')).type('qwerty'); + // Set primary key + cy.get(getElementFromAlias('primary-key-select-0')).select('0'); + // Click on create + cy.get(getElementFromAlias('table-create')).click(); + // Check if the route didn't change + cy.url().should('eq', `${baseUrl}/data/default/schema/public/table/add`); + // Validate + validateCT(getTableName(0, testName), ResultType.FAILURE); +}; + +export const passCT = () => { + cy.get(getElementFromAlias('frequently-used-columns')).first().should('exist'); + // Set second column + cy.get(getElementFromAlias('column-1')).clear().type(getColName(1)); + tableColumnTypeSelector('col-type-1'); + cy.get(getElementFromAlias('data_test_column_type_value_text')) + .first() + .click(); + cy.get(getElementFromAlias('col-default-1')).clear(); + // Set primary key + cy.get(getElementFromAlias('primary-key-select-0')).select('0'); + cy.get(getElementFromAlias('primary-key-select-1')).select('1'); + // Click on create + cy.get(getElementFromAlias('table-create')).click(); + cy.wait(10000); + // Check if the table got created and navigatied to modify table + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 0, + testName + )}/modify` + ); + cy.get(getElementFromAlias(getTableName(0, testName))); + // Validate + validateCT(getTableName(0, testName), ResultType.SUCCESS); +}; + +export const passCTWithFK = () => { + // go to create-table + cy.visit(getIndexRoute()); + cy.wait(5000); + cy.get(getElementFromAlias('data-create-table')).click(); + // cy.get(getElementFromAlias('table-create')).click(); + // Set tablename + cy.get(getElementFromAlias('tableName')) + .clear() + .type(getTableName(1, testName)); + // Set first column + cy.get(getElementFromAlias('column-0')).type(getColName(0)); + tableColumnTypeSelector('col-type-0'); + cy.get(getElementFromAlias('data_test_column_type_value_serial')) + .first() + .click(); + // Set second column + cy.get(getElementFromAlias('column-1')).type(getColName(1)); + tableColumnTypeSelector('col-type-1'); + cy.get(getElementFromAlias('data_test_column_type_value_text')) + .first() + .click(); + // Set third column + cy.get(getElementFromAlias('column-2')).type(getColName(2)); + tableColumnTypeSelector('col-type-2'); + cy.get(getElementFromAlias('data_test_column_type_value_text')) + .first() + .click(); + + // Set primary key + cy.get(getElementFromAlias('primary-key-select-0')).select('0'); + // Set foreign key + cy.get(getElementFromAlias('add-table-edit-fk-0')).click(); + cy.get(getElementFromAlias('foreign-key-ref-table-0')).select( + getTableName(0, testName) + ); + cy.get(getElementFromAlias('foreign-key-0-lcol-0')).select('0'); + cy.get(getElementFromAlias('foreign-key-0-rcol-0')).select(getColName(0)); + cy.get(getElementFromAlias('foreign-key-0-lcol-1')).select('1'); + cy.get(getElementFromAlias('foreign-key-0-rcol-1')).select(getColName(1)); + cy.get(getElementFromAlias('foreign-key-0-onUpdate-cascade')).check(); + cy.get(getElementFromAlias('foreign-key-0-onDelete-cascade')).check(); + + // set unique key 1 + cy.get(getElementFromAlias('add-table-edit-unique-key-0')).click(); + cy.get(getElementFromAlias('unique-key-0-column-0')).select('1'); + + // set unique key 2 + cy.get(getElementFromAlias('add-table-edit-unique-key-1')).click(); + cy.get(getElementFromAlias('unique-key-1-column-0')).select('1'); + cy.get(getElementFromAlias('unique-key-1-column-1')).select('2'); + cy.get(getElementFromAlias('unique-key-1-column-2')).select('0'); + cy.get(getElementFromAlias('remove-uk-1-column-1')).click(); + + // Click on create + cy.get(getElementFromAlias('table-create')).click(); + cy.wait(10000); + // Check if the table got created and navigatied to modify table + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 1, + testName + )}/modify` + ); + cy.get('div').contains( + `${getTableName(1, testName)}_${getColName(1)}_${getColName(0)}` + ); + cy.get(getElementFromAlias(getTableName(1, testName))); + // Validate + validateCT(getTableName(1, testName), ResultType.SUCCESS); +}; + +export const failCTDuplicateTable = () => { + // Visit data page + cy.visit(getIndexRoute()); + cy.wait(5000); + cy.get(getElementFromAlias('data-create-table')).click(); + // Type table name + cy.get(getElementFromAlias('tableName')).type(getTableName(0, testName)); + // Set column + cy.get(getElementFromAlias('column-0')).type(getColName(1)); + tableColumnTypeSelector('col-type-0'); + cy.get(getElementFromAlias('data_test_column_type_value_serial')) + .first() + .click(); + // Set primary key + cy.get(getElementFromAlias('primary-key-select-0')).select('0'); + // Click on create + cy.get(getElementFromAlias('table-create')).click(); + cy.wait(7000); +}; + +const deleteTable = (tableName: string) => { + cy.get(getElementFromAlias(tableName)).click(); + cy.get(getElementFromAlias('table-modify')).click(); + + setPromptValue(tableName); + + // Click on delete + cy.get(getElementFromAlias('delete-table')).click(); + // Confirm + cy.window().its('prompt').should('be.called'); + + cy.wait(5000); + validateCT(tableName, ResultType.FAILURE); +}; + +export const deleteCTTestTables = () => { + // Go to the modify section of the second table + const secondTableName = getTableName(1, testName); + deleteTable(secondTableName); + // Go to the modify section of the first table + const firstTableName = getTableName(0, testName); + deleteTable(firstTableName); + + // Match the URL + + // FIXME: Temporarily disabling this. + // cy.url().should('eq', `${baseUrl}/data/schema`); +}; + +export const setValidationMetaData = () => { + setMetaData(); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/create-table/test.ts b/frontend/apps/console-ce-e2e/src/e2e/data/create-table/test.ts new file mode 100644 index 00000000000..90f9912bc2b --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/create-table/test.ts @@ -0,0 +1,48 @@ +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; + +import { + checkCreateTableRoute, + failCTWithoutColumns, + failCTWithoutPK, + failCTDuplicateColumns, + failCTWrongDefaultValue, + passCT, + failCTDuplicateTable, + deleteCTTestTables, + passCTWithFK, +} from './spec'; +import { getIndexRoute } from '../../../helpers/dataHelpers'; + +const setup = () => { + describe('Setup route', () => { + it('Visit the index route', () => { + // Visit the index route + cy.visit(getIndexRoute()); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runCreateTableTests = () => { + describe('Create Table', () => { + it('Create table button opens the correct route', checkCreateTableRoute); + it('Fails to create table without columns', failCTWithoutColumns); + it('Fails to create table without primary key', failCTWithoutPK); + it('Fails to create with duplicate columns', failCTDuplicateColumns); + it('Fails to create with wrong default value', failCTWrongDefaultValue); + it('Successfuly creates table', passCT); + it( + 'Successfuly creates table with composite foreign and unique key', + passCTWithFK + ); + it('Fails to create duplicate table', failCTDuplicateTable); + it('Delete the test tables', deleteCTTestTables); + }); +}; + +if (testMode !== 'cli') { + setup(); + runCreateTableTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/custom-functions/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/data/custom-functions/spec.ts new file mode 100644 index 00000000000..e2c29e0877d --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/custom-functions/spec.ts @@ -0,0 +1,125 @@ +import { baseUrl, getElementFromAlias } from '../../../helpers/dataHelpers'; + +const statements = { + createTableSql: + 'CREATE TABLE a_test_test_article (id serial PRIMARY KEY, title text, content text);', + createCustomFuncSql: `CREATE FUNCTION a_test_test_search_articles(search text) +RETURNS SETOF a_test_test_article AS $function$ +SELECT * +FROM a_test_test_article +WHERE +title ilike ('%' || search || '%') +OR content ilike ('%' || search || '%') +$function$ LANGUAGE sql STABLE;`, + insertData_a1: `INSERT INTO a_test_test_article(title, content) VALUES ('hasura is awesome', 'I mean duh?!');`, + insertData_a2: `INSERT INTO a_test_test_article(title, content) VALUES ('cloud lauched', 'hasura <3 the cloud');`, + deleteFunction: 'DROP FUNCTION a_test_test_search_articles(search text);', + cleanUpSql: 'DROP TABLE a_test_test_article CASCADE;', + graphql: { + query: `{ + a_test_test_search_articles + (args: {{} search: "hasura" }) { + id + title + content + `, + }, +}; + +export const openRawSQL = () => { + cy.get('a').contains('Data').click(); + cy.wait(3000); + cy.get(getElementFromAlias('sql-link')).click(); + cy.wait(3000); + cy.url().should('eq', `${baseUrl}/data/sql`); +}; + +const clearText = () => { + cy.get('textarea').type('{selectall}', { force: true }); + cy.get('textarea').trigger('keydown', { + keyCode: 46, + which: 46, + force: true, + }); + cy.wait(2000); +}; + +// helper to type into the SQL textarea on rawsql page +const typeStatement = ( + statement: string, + shouldClearText = false, + waitTimeUponType = 2000, + endWaitTime = 5000, + unCheckTrackFunction = false +) => { + if (shouldClearText) { + clearText(); + } + cy.get('textarea').type(statement, { force: true }); + cy.wait(waitTimeUponType); + if (unCheckTrackFunction) { + cy.get(getElementFromAlias('raw-sql-track-check')).uncheck(); + } + cy.get(getElementFromAlias('run-sql')).click(); + // FIXME: maybe necessary for CLI mode + // cy.get(getElementFromAlias('raw-sql-statement-timeout')).should('be.disabled'); + cy.wait(endWaitTime); +}; + +export const createTableArticle = () => + typeStatement(statements.createTableSql); + +export const createCustomFunction = () => + typeStatement(statements.createCustomFuncSql, true, 2000, 5000, true); + +export const insertAuthorsIntoTable = () => { + typeStatement(statements.insertData_a1, true); + typeStatement(statements.insertData_a2, true); + clearText(); +}; + +export const trackCustomFn = () => { + cy.visit('/data/default/schema/public'); + cy.wait(7000); + cy.url().should('eq', `${baseUrl}/data/default/schema/public`); + + // Track Function + cy.get( + getElementFromAlias('add-track-function-a_test_test_search_articles') + ).click(); + cy.wait(5000); +}; + +export const routeToGraphiql = () => { + cy.visit('/api/api-explorer'); + cy.wait(7000); + cy.url().should('eq', `${baseUrl}/api/api-explorer`); +}; + +export const verifyCustomFnResult = () => { + // Type the query + cy.get('textarea') + .first() + .type(`{enter}{uparrow}${statements.graphql.query}`, { force: true }); + cy.wait(2000); + cy.get('.execute-button').click(); + // verify if article is present + + cy.get('.cm-property').contains('title'); + cy.get('.cm-property').contains('content'); + + cy.wait(2000); +}; + +export const cleanUpSql = () => { + typeStatement(statements.deleteFunction, true); + typeStatement(statements.cleanUpSql, true); + clearText(); + cy.wait(2000); +}; + +export const routeToSQLPage = () => { + cy.visit('/data/sql'); + cy.wait(7000); + cy.url().should('eq', `${baseUrl}/data/sql`); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/custom-functions/test.ts b/frontend/apps/console-ce-e2e/src/e2e/data/custom-functions/test.ts new file mode 100644 index 00000000000..1d5580e857a --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/custom-functions/test.ts @@ -0,0 +1,43 @@ +import { + openRawSQL, + createTableArticle, + createCustomFunction, + insertAuthorsIntoTable, + cleanUpSql, + trackCustomFn, + routeToGraphiql, + verifyCustomFnResult, + routeToSQLPage, +} from './spec'; +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; +import { getIndexRoute } from '../../../helpers/dataHelpers'; + +const setup = () => { + describe('Setup route', () => { + it('Visit the index route', () => { + cy.visit(getIndexRoute()); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runCustomFunctionTests = () => { + describe('Custom Functions', () => { + it('Open Raw SQL page', openRawSQL); + it('Create test table', createTableArticle); + it('Run SQL for custom function', createCustomFunction); + it('Insert articles into table', insertAuthorsIntoTable); + it('Track custom function', trackCustomFn); + it('Route to GraphiQL page', routeToGraphiql); + it('Check custom function results on GraphiQL', verifyCustomFnResult); + it('Route to Raw SQL page', routeToSQLPage); + it('Test cleanup', cleanUpSql); + }); +}; + +if (testMode !== 'cli') { + setup(); + runCustomFunctionTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/functions/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/data/functions/spec.ts new file mode 100644 index 00000000000..25cdb9a1b83 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/functions/spec.ts @@ -0,0 +1,198 @@ +import { + getElementFromAlias, + baseUrl, + getCustomFunctionName, + getSchema, + dropTable, + testCustomFunctionSQLWithSessArg, + getTrackFnPayload, + createFunctionTable, + trackCreateFunctionTable, + getCreateTestFunctionQuery, + getTrackTestFunctionQuery, + createSampleTable, + getTrackSampleTableQuery, + createVolatileFunction, + dropTableIfExists, +} from '../../../helpers/dataHelpers'; + +import { + dataRequest, + validateCFunc, + validateUntrackedFunc, + ResultType, + trackFunctionRequest, +} from '../../validators/validators'; +import { setPromptValue } from '../../../helpers/common'; + +export const createCustomFunctionSuccess = () => { + // Round about way to create a function + dataRequest(createFunctionTable(), ResultType.SUCCESS, 'query'); + cy.wait(5000); + dataRequest(trackCreateFunctionTable(), ResultType.SUCCESS, 'metadata'); + cy.wait(5000); + + dataRequest(getCreateTestFunctionQuery(1), ResultType.SUCCESS, 'query'); + cy.wait(5000); + dataRequest(getTrackTestFunctionQuery(1), ResultType.SUCCESS, 'metadata'); + cy.wait(5000); + + // Check if the track checkbox is clicked or not + validateCFunc(getCustomFunctionName(1), getSchema(), ResultType.SUCCESS); + cy.wait(5000); +}; + +export const unTrackFunction = () => { + cy.visit( + `data/default/schema/public/functions/${getCustomFunctionName(1)}/modify` + ); + cy.wait(5000); + cy.get(getElementFromAlias('custom-function-edit-untrack-btn')).click(); + cy.wait(5000); + validateUntrackedFunc( + getCustomFunctionName(1), + getSchema(), + ResultType.SUCCESS + ); + cy.wait(5000); +}; + +export const trackFunction = () => { + cy.get( + getElementFromAlias(`add-track-function-${getCustomFunctionName(1)}`) + ).should('exist'); + cy.get( + getElementFromAlias(`add-track-function-${getCustomFunctionName(1)}`) + ).click(); + cy.get(getElementFromAlias(`track-as-mutation`)).should('exist'); + cy.get(getElementFromAlias(`track-as-mutation`)).click(); + cy.wait(5000); + validateCFunc(getCustomFunctionName(1), getSchema(), ResultType.SUCCESS); + cy.wait(5000); +}; + +export const testSessVariable = () => { + const fN = 'customFunctionWithSessionArg'.toLowerCase(); + dataRequest( + dropTableIfExists({ name: 'text_result', schema: 'public' }), + ResultType.SUCCESS + ); + cy.wait(3000); + dataRequest(createSampleTable(), ResultType.SUCCESS, 'query'); + cy.wait(3000); + dataRequest(getTrackSampleTableQuery(), ResultType.SUCCESS, 'metadata'); + cy.wait(3000); + + dataRequest(testCustomFunctionSQLWithSessArg(fN), ResultType.SUCCESS); + cy.wait(3000); + + trackFunctionRequest(getTrackFnPayload(fN), ResultType.SUCCESS); + cy.wait(5000); + + cy.visit(`data/default/schema/public/functions/${fN}/modify`); + cy.get(getElementFromAlias(`${fN}-session-argument-btn`), { + timeout: 5000, + }).click(); + + // invalid data should fail + cy.get(getElementFromAlias(`${fN}-edit-sessvar-function-field`)) + .clear() + .type('invalid'); + cy.get(getElementFromAlias(`${fN}-session-argument-save`)).click(); + + cy.expectErrorNotificationWithTitle( + 'Updating Session argument variable failed' + ); + + cy.get(getElementFromAlias(`${fN}-session-argument-btn`), { + timeout: 1000, + }).click(); + cy.get(getElementFromAlias(`${fN}-edit-sessvar-function-field`)) + .clear() + .type('hasura_session'); + cy.get(getElementFromAlias(`${fN}-session-argument-save`)).click(); + cy.wait(3000); + cy.get(getElementFromAlias(fN)).should('be.visible'); + cy.visit(`data/default/schema/public/functions/${fN}/modify`); + cy.wait(3000); + cy.get(getElementFromAlias(`${fN}-session-argument`)).should( + 'contain', + 'hasura_session' + ); + dataRequest(dropTable('text_result', true), ResultType.SUCCESS); + cy.wait(3000); + cy.visit(`data/default/schema/public/`); +}; + +export const verifyPermissionTab = () => { + cy.get(getElementFromAlias('functions-data/default-permissions')).click(); + cy.wait(5000); + cy.get(getElementFromAlias('custom-function-permission-link')).should( + 'exist' + ); + cy.wait(5000); +}; + +export const deleteCustomFunction = () => { + cy.get(getElementFromAlias('functions-data/default-modify')).click(); + + setPromptValue(getCustomFunctionName(1)); + + cy.get(getElementFromAlias('custom-function-edit-delete-btn')).click(); + cy.window().its('prompt').should('be.called'); + cy.wait(5000); + cy.get(getElementFromAlias('delete-confirmation-error')).should('not.exist'); + cy.url().should('eq', `${baseUrl}/data/default/schema/public`); + cy.wait(5000); + + dataRequest(dropTable(), ResultType.SUCCESS); + cy.wait(5000); +}; + +export const trackVolatileFunction = () => { + const fN = 'customVolatileFunc'.toLowerCase(); + dataRequest( + dropTableIfExists({ name: 'text_result', schema: 'public' }), + ResultType.SUCCESS + ); + cy.wait(5000); + dataRequest(createSampleTable(), ResultType.SUCCESS); + cy.wait(5000); + dataRequest(getTrackSampleTableQuery(), ResultType.SUCCESS, 'metadata'); + dataRequest(createVolatileFunction(fN), ResultType.SUCCESS); + cy.wait(5000); + cy.visit(`data/default/schema/public`); + cy.get(getElementFromAlias(`add-track-function-${fN}`)).click(); + cy.get(getElementFromAlias('track-as-mutation')).click(); + cy.wait(2000); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/functions/${fN}/modify` + ); + dataRequest(dropTable('text_result', true), ResultType.SUCCESS); +}; + +export const trackVolatileFunctionAsQuery = () => { + const fN = 'customVolatileFunc'.toLowerCase(); + dataRequest( + dropTableIfExists({ name: 'text_result', schema: 'public' }), + ResultType.SUCCESS + ); + cy.wait(5000); + dataRequest(createSampleTable(), ResultType.SUCCESS); + cy.wait(5000); + dataRequest(getTrackSampleTableQuery(), ResultType.SUCCESS, 'metadata'); + dataRequest(createVolatileFunction(fN), ResultType.SUCCESS); + cy.wait(5000); + cy.visit(`data/default/schema/public`); + cy.get(getElementFromAlias(`add-track-function-${fN}`)).click(); + cy.get(getElementFromAlias('track-as-query')).click(); + cy.wait(2000); + cy.get(getElementFromAlias('track-as-query-confirm')).click(); + cy.wait(2000); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/functions/${fN}/modify` + ); + dataRequest(dropTable('text_result', true), ResultType.SUCCESS); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/functions/test.ts b/frontend/apps/console-ce-e2e/src/e2e/data/functions/test.ts new file mode 100644 index 00000000000..07e0ea67742 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/functions/test.ts @@ -0,0 +1,43 @@ +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; + +import { + createCustomFunctionSuccess, + deleteCustomFunction, + unTrackFunction, + trackFunction, + verifyPermissionTab, + trackVolatileFunction, + trackVolatileFunctionAsQuery, +} from './spec'; +import { getIndexRoute } from '../../../helpers/dataHelpers'; + +const setup = () => { + describe('Setup route', () => { + it('Visit the index route', () => { + // Visit the index route + cy.visit(getIndexRoute()); + cy.wait(5000); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runCreateCustomFunctionsTableTests = () => { + describe('Custom Function Tests', () => { + it('Create a custom function and track', createCustomFunctionSuccess); + it('Untrack custom function', unTrackFunction); + it('Track custom function', trackFunction); + it('Verify permission tab', verifyPermissionTab); + it('Delete custom function', deleteCustomFunction); + // TODO it('Test custom function with Session Argument', testSessVariable); + it('Tracks VOLATILE function as mutation', trackVolatileFunction); + it('Tracks VOLATILE function as query', trackVolatileFunctionAsQuery); + }); +}; + +if (testMode !== 'cli') { + setup(); + runCreateCustomFunctionsTableTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/insert-browse/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/data/insert-browse/spec.ts new file mode 100644 index 00000000000..4f6b7bd8591 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/insert-browse/spec.ts @@ -0,0 +1,533 @@ +import { + baseUrl, + getColName, + getTableName, + dataTypes, + getElementFromAlias, + typeDefaults, + tableColumnTypeSelector, + getIndexRoute, +} from '../../../helpers/dataHelpers'; + +import { + validateInsert, + setMetaData, + validateCT, + ResultType, +} from '../../validators/validators'; +import { setPromptValue } from '../../../helpers/common'; + +const numOfDataTypes = dataTypes.length; +const testName = 'ib'; + +//* ******************** Util functions ************************ + +const setColumns = () => { + for (let i = 0; i < numOfDataTypes; i += 1) { + // Type column name + cy.get(getElementFromAlias(`column-${i}`)).type(getColName(i)); + // Select column type + tableColumnTypeSelector(`col-type-${i}`); + cy.get(getElementFromAlias(`data_test_column_type_value_${dataTypes[i]}`)) + .first() + .click(); + + if (i === dataTypes.indexOf('text')) { + cy.get(getElementFromAlias(`unique-${i}`)).check({ force: true }); + } + // Set appropriate default if the type is not serial + if (i > 1) { + cy.get(getElementFromAlias(`col-default-${i}`)).type( + typeDefaults[dataTypes[i]] + ); + } + } +}; + +const clickSaveOrInsert = (firstIndex: number, currentIndex: number) => { + if (currentIndex === firstIndex) { + cy.get(getElementFromAlias('insert-save-button')).click(); + } else { + cy.get(getElementFromAlias('insert-save-button')).click(); + } + cy.wait(2000); +}; + +const checkQuerySuccess = () => { + // Expect only 4 rows i.e. expect fifth element to not exist + cy.get('[role=gridcell]').contains('filter-text'); + cy.get('[role=row]').eq(2).should('not.exist'); +}; + +const checkOrder = (order: string) => { + // Utility function to get right element + + if (order === 'asc') { + cy.get('[role=row]').each(($el, index) => { + if (index !== 0) { + cy.wrap($el) + .find('[role=gridcell]') + .first() + .next() + .next() + .contains(index); + } + }); + } else { + cy.get('[role=row]').each(($el, index) => { + if (index !== 0) { + cy.wrap($el) + .find('[role=gridcell]') + .first() + .next() + .next() + .contains(22 - index); + } + }); + } +}; + +//* ******************** Test functions *************************** + +export const passBICreateTable = () => { + cy.wait(7000); + // Click create table button + cy.get(getElementFromAlias('data-create-table')).click(); + // Type table name + cy.get(getElementFromAlias('tableName')).type(getTableName(0, testName)); + // Set columns with all fields + setColumns(); + // Set primary key + cy.get(getElementFromAlias('primary-key-select-0')).select('0'); + // Click on create + cy.get(getElementFromAlias('table-create')).click(); + cy.wait(7000); + validateCT(getTableName(0, testName), ResultType.SUCCESS); +}; + +export const passSearchTables = () => { + // Click add table button + cy.visit(getIndexRoute()); + cy.wait(5000); + cy.get(getElementFromAlias('data-create-table')).click(); + // Type table name + cy.get(getElementFromAlias('tableName')).type(getTableName(1, testName)); + // Type column name + cy.get(getElementFromAlias('column-0')).type(getColName(0)); + tableColumnTypeSelector('col-type-0'); + cy.get(getElementFromAlias('data_test_column_type_value_integer')) + .first() + .click(); + // Set primary key + cy.get(getElementFromAlias('primary-key-select-0')).select('0'); + // Click on create + cy.get(getElementFromAlias('table-create')).click(); + cy.wait(7000); + validateCT(getTableName(0, testName), ResultType.SUCCESS); + // cy.get(getElementFromAlias('search-tables')).type('0'); + // cy.get(getElementFromAlias('table-links')).should('not.contain', '1'); + // cy.get(getElementFromAlias('search-tables')).type('{home}{del}'); +}; + +export const checkInsertRoute = () => { + // Click on Insert tab + cy.get(getElementFromAlias(getTableName(0, testName))).click(); + cy.get(getElementFromAlias('table-insert-rows')).click(); + // Match URL + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 0, + testName + )}/insert` + ); +}; + +export const failBIWrongDataType = () => { + // Check if the table creation fails for wrong inputs of each data type + for (let i = 2; i < numOfDataTypes; i += 1) { + // Text and Boolean always succeed, so we check only for others + if (dataTypes[i] !== 'text' && dataTypes[i] !== 'boolean') { + const sureFailString = 'abcd1234'; + // Type a string that fails + cy.get(getElementFromAlias(`typed-input-${i}`)).type(sureFailString); + // Click the Save/Insert Again button. + clickSaveOrInsert(2, i); + cy.get(getElementFromAlias(`typed-input-${i}`)).clear(); + // Check the default radio of curret column + cy.get(getElementFromAlias(`typed-input-default-${i}`)).check(); + } + + validateInsert(getTableName(0, testName), 0); + } +}; + +export const passBIInsert20Rows = () => { + for (let i = 0; i < 20; i += 1) { + // Type a string in the text type fields of some rows (to be tested in Browse rows) + const textIndex = dataTypes.indexOf('text'); + // Click the Insert Again button. + if (i === 0) { + cy.get(getElementFromAlias(`typed-input-${textIndex}`)).type( + '{selectall}{del}' + ); + cy.get(getElementFromAlias(`typed-input-${textIndex}`)).type( + 'filter-text' + ); + cy.get(getElementFromAlias('insert-save-button')).click(); + } else { + cy.get(getElementFromAlias(`typed-input-${textIndex}`)).type( + '{selectall}{del}' + ); + cy.get(getElementFromAlias(`typed-input-${textIndex}`)) + .type('{selectall}{del}') + .type(Math.random().toString(36).substring(7)); + cy.get( + getElementFromAlias(`typed-input-default-${textIndex + 1}`) + ).check(); + cy.get(getElementFromAlias('insert-save-button')).click(); + cy.wait(300); + validateInsert(getTableName(0, testName), i + 1); + } + } + // Wait for insert notifications to disappear + cy.wait(7000); +}; + +export const checkBrowseRoute = () => { + // Click on Browse tab + cy.get(getElementFromAlias(getTableName(0, testName))).click(); + cy.get(getElementFromAlias('table-browse-rows')).click(); + cy.wait(2000); + // Match URL + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 0, + testName + )}/browse` + ); +}; + +export const passBI20RowsExist = () => { + // Check if the 20 inserted elements reflect in the UI + cy.get(getElementFromAlias('pagination-select')).select('20'); + // cy.get(getElementFromAlias('table-browse-rows')).contains('20'); +}; + +export const checkPagination = () => { + // Check if the current page is 1 + cy.get('.-pageJump > input').should('have.value', '1'); + // Check if the total number of pages is 3 + cy.get('.-totalPages').contains('3'); + // Check if the default value of rows displayed is 10 + cy.get('.-pageSizeOptions > select').should('have.value', '10'); + cy.get('.-next > button').click({ force: true }); + cy.wait(3000); + // Check if the page changed + cy.get( + '.rt-tbody > div:nth-child(1) > div > div:nth-child(3) > div' + ).contains('11'); + cy.get('.-pageJump > input').should('have.value', '2'); + cy.get('.-previous > button').click({ force: true }); + cy.wait(3000); + // Check if the page changed + cy.get('.-pageJump > input').should('have.value', '1'); + cy.get('.-pageSizeOptions > select').select('5 rows'); + cy.wait(3000); + // Check if the total number of pages changed + cy.get('.-totalPages').contains('5'); +}; + +export const passBISort = (order: string) => { + cy.wait(7000); + // Select column with type 'serial' + const serialIndex = dataTypes.indexOf('serial'); + cy.get(getElementFromAlias('sort-column-0')).select(getColName(serialIndex), { + force: true, + }); + // Select order as `descending` + cy.get(getElementFromAlias('sort-order-0')).select( + order === 'asc' ? 'Asc' : 'Desc', + { force: true } + ); + // Run query + cy.get(getElementFromAlias('run-query')).click(); + cy.wait(5000); + // Check order + checkOrder(order); + + // Clear filter + cy.get(getElementFromAlias('clear-sorts-0')).click({ force: true }); + // Run query + cy.get(getElementFromAlias('run-query')).click(); + cy.wait(5000); +}; + +export const passBIFilterQueryEq = () => { + // Select column with type "text" + const textIndex = dataTypes.indexOf('text'); + cy.get(getElementFromAlias('filter-column-0')).select(getColName(textIndex)); + // Select operator as `eq` + cy.get(getElementFromAlias('filter-op-0')).select('$eq'); + // Type value as "filter-text" + cy.get("input[placeholder='-- value --']").last().type('filter-text'); + // Run query + cy.get(getElementFromAlias('run-query')).click(); + cy.wait(2000); + // Check if the query was successful + checkQuerySuccess(); + + // Clear filter + cy.get(getElementFromAlias('clear-filter-0')).click(); + // Run query + cy.get(getElementFromAlias('run-query')).click(); + cy.wait(5000); +}; + +export const deleteBITestTable = () => { + cy.get(getElementFromAlias(getTableName(2, testName))).click(); + // Go to the modify section of the table + cy.get(getElementFromAlias('table-modify')).click(); + cy.wait(2000); + setPromptValue(getTableName(2, testName)); + // Click on delete + cy.get(getElementFromAlias('delete-table')).click(); + // Confirm + cy.window().its('prompt').should('be.called'); + cy.wait(7000); + // Match the URL + // FIXME: change this later + // cy.url().should('eq', `${baseUrl}/data/default/schema/public`); + validateCT(getTableName(2, testName), ResultType.FAILURE); + + cy.get(getElementFromAlias(getTableName(1, testName))).click(); + // Go to the modify section of the table + cy.get(getElementFromAlias('table-modify')).click(); + cy.wait(2000); + setPromptValue(getTableName(1, testName)); + // Click on delete + cy.get(getElementFromAlias('delete-table')).click(); + // Confirm + cy.window().its('prompt').should('be.called'); + cy.wait(7000); + // Match the URL + // FIXME: change this later + // cy.url().should('eq', `${baseUrl}/data/schema`); + validateCT(getTableName(1, testName), ResultType.FAILURE); + + cy.get(getElementFromAlias(getTableName(0, testName))).click(); + // Go to the modify section of the table + cy.get(getElementFromAlias('table-modify')).click(); + setPromptValue(getTableName(0, testName)); + cy.wait(2000); + // Click on delete + cy.get(getElementFromAlias('delete-table')).click(); + // Confirm + cy.window().its('prompt').should('be.called'); + cy.wait(7000); + + // Match the URL + // FIXME: change later + // cy.url().should('eq', `${baseUrl}/data/schema`); + validateCT(getTableName(0, testName), ResultType.FAILURE); +}; + +export const failBIUniqueKeys = () => { + // Type a string in the text type fields of some rows (to be tested in Browse rows) + const textIndex = dataTypes.indexOf('text'); + const floatIndex = dataTypes.indexOf('numeric'); + cy.get(getElementFromAlias(`typed-input-${floatIndex}`)).type(`${0.5555}`); + cy.get(getElementFromAlias(`typed-input-${textIndex}`)) + .clear() + .type('filter-text'); + + // Click the Insert Again button. + cy.get(getElementFromAlias(`typed-input-${textIndex}`)).type( + '{selectall}{del}' + ); + cy.get(getElementFromAlias(`typed-input-${textIndex}`)).type('name'); + + cy.get(getElementFromAlias('insert-save-button')).click(); + // Check default for next insert + + cy.get(getElementFromAlias(`typed-input-default-${textIndex}`)).check(); + + validateInsert(getTableName(0, testName), 21); + cy.wait(7000); + cy.get(getElementFromAlias(`typed-input-${textIndex}`)) + .clear() + .type('filter-text'); + // Click the Insert Again button. + cy.get(getElementFromAlias(`typed-input-${textIndex}`)).type( + '{selectall}{del}' + ); + cy.get(getElementFromAlias(`typed-input-${textIndex}`)).type('name'); + cy.get(getElementFromAlias('insert-save-button')).click(); + + cy.wait(7000); + validateInsert(getTableName(0, testName), 21); +}; +export const setValidationMetaData = () => { + setMetaData(); +}; + +export const passEditButton = () => { + cy.get(getElementFromAlias('table-browse-rows')).click(); + cy.wait(2000); + cy.get(getElementFromAlias('row-edit-button-0')).click(); + cy.wait(2000); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 0, + testName + )}/edit` + ); + const textIndex = dataTypes.indexOf('text'); + cy.get(getElementFromAlias(`typed-input-${textIndex}`)).type( + '{selectall}{del}' + ); + cy.get(getElementFromAlias(`typed-input-${textIndex}`)).type('new-text'); + cy.get(getElementFromAlias('edit-save-button')).click(); + cy.wait(7000); +}; + +export const passCloneButton = () => { + cy.get(getElementFromAlias('table-browse-rows')).click(); + cy.wait(5000); + cy.get(getElementFromAlias('row-clone-button-0')).click(); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 0, + testName + )}/insert` + ); + cy.get(getElementFromAlias('clear-button')).click(); + cy.get(getElementFromAlias('typed-input-0')).should('have.value', ''); +}; + +export const checkViewRelationship = () => { + cy.visit(getIndexRoute()); + cy.wait(5000); + cy.get(getElementFromAlias('data-create-table')).click(); + // Type table name + cy.get(getElementFromAlias('tableName')).type(getTableName(2, testName)); + cy.get(getElementFromAlias('column-0')).type('id'); + tableColumnTypeSelector('col-type-0'); + cy.get(getElementFromAlias('data_test_column_type_value_serial')) + .first() + .click(); + cy.get(getElementFromAlias('column-1')).type('someID'); + tableColumnTypeSelector('col-type-1'); + cy.get(getElementFromAlias('data_test_column_type_value_integer')) + .first() + .click(); + // Set primary key + cy.get(getElementFromAlias('primary-key-select-0')).select('0'); + // Click on create + cy.get(getElementFromAlias('table-create')).click(); + cy.wait(7000); + validateCT(getTableName(0, testName), ResultType.SUCCESS); + // Add foreign key + cy.get(getElementFromAlias('modify-table-edit-fk-0')).click(); + cy.get(getElementFromAlias('foreign-key-ref-table-0')).select( + getTableName(0, testName) + ); + cy.get(getElementFromAlias('foreign-key-0-lcol-0')).select('0'); + cy.get(getElementFromAlias('foreign-key-0-rcol-0')).select(getColName(0)); + cy.get(getElementFromAlias('modify-table-fk-0-save')).click(); + cy.wait(5000); + // Add relationship + cy.get(getElementFromAlias('table-relationships')).click(); + cy.get(getElementFromAlias('obj-rel-add-0')).click(); + cy.get(getElementFromAlias('suggested-rel-name')).clear().type('someRel'); + cy.get(getElementFromAlias('obj-rel-save-0')).click(); + cy.wait(2000); + // Insert a row + cy.get(getElementFromAlias('table-insert-rows')).click(); + cy.get(getElementFromAlias('typed-input-1')).type('1'); + cy.get(getElementFromAlias('insert-save-button')).click(); + cy.wait(1000); + cy.get(getElementFromAlias('table-browse-rows')).click(); + cy.wait(1000); + cy.get('.rt-table').within(() => { + cy.get('a').contains('View').click(); + cy.wait(1000); + }); + cy.get('a').contains('Close').first().click(); +}; + +export const passDeleteRow = () => { + cy.get(getElementFromAlias('table-browse-rows')).click(); + cy.wait(5000); + // cy.get(getElementFromAlias('table-browse-rows')).contains('21'); + cy.get(getElementFromAlias('row-delete-button-0')).click(); + cy.on('window:confirm', str => { + expect( + str.indexOf('This will permanently delete this row from this table') !== + -1 + ).to.be.true; + }); + // cy.get(getElementFromAlias('table-browse-rows')).contains('20'); + cy.wait(14000); +}; + +export const passBulkDeleteRows = () => { + cy.get(getElementFromAlias('table-browse-rows')).click(); + cy.wait(5000); + // cy.get(getElementFromAlias('table-browse-rows')).contains('20'); + cy.get(getElementFromAlias('row-checkbox-0')).click(); + cy.get(getElementFromAlias('row-checkbox-1')).click(); + cy.get(getElementFromAlias('bulk-delete')).click(); + cy.wait(1000); + cy.on('window:confirm', str => { + expect( + str.indexOf('This will permanently delete rows from this table') !== -1 + ).to.be.true; + }); + // cy.get(getElementFromAlias('table-browse-rows')).contains('18'); + cy.wait(14000); +}; + +export const passBulkDeleteAllRows = () => { + cy.get(getElementFromAlias('table-browse-rows')).click(); + cy.wait(5000); + // cy.get(getElementFromAlias('table-browse-rows')).contains('18'); + cy.get(getElementFromAlias('select-all-rows')).click(); + cy.get(getElementFromAlias('bulk-delete')).click(); + cy.wait(1000); + cy.on('window:confirm', str => { + expect( + str.indexOf('This will permanently delete rows from this table') !== -1 + ).to.be.true; + }); + // cy.get(getElementFromAlias('table-browse-rows')).contains('(8)'); + cy.wait(14000); +}; + +export const passArrayDataType = () => { + // create new column + cy.get(getElementFromAlias('table-modify')).click(); + cy.wait(1000); + cy.get(getElementFromAlias('modify-table-edit-add-new-column')).click(); + cy.get(getElementFromAlias('column-name')).type('array_column'); + cy.get(getElementFromAlias('col-type-0')) + .children('div') + .click() + .find('input') + .type('text[]', { force: true }); + cy.get(getElementFromAlias('modify-table-add-new-column-save')).click(); + + // insert new row + cy.get(getElementFromAlias('table-insert-rows')).click(); + cy.wait(5000); + cy.get(getElementFromAlias('typed-input-11')).type('["a", "b"]'); + cy.get(getElementFromAlias('insert-save-button')).click(); + + // go to browse rows and check if row was added + cy.get(getElementFromAlias('table-browse-rows')).click(); + cy.wait(5000); + // cy.get(getElementFromAlias('table-browse-rows')).contains('(9)'); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/insert-browse/test.ts b/frontend/apps/console-ce-e2e/src/e2e/data/insert-browse/test.ts new file mode 100644 index 00000000000..4e051985a1b --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/insert-browse/test.ts @@ -0,0 +1,68 @@ +import { testMode } from '../../../helpers/common'; + +import { + passBICreateTable, + deleteBITestTable, + checkInsertRoute, + failBIWrongDataType, + failBIUniqueKeys, + passBIInsert20Rows, + checkBrowseRoute, + passBISort, + passBIFilterQueryEq, + passEditButton, + passSearchTables, + passCloneButton, + checkViewRelationship, + passDeleteRow, + passBulkDeleteRows, + passBulkDeleteAllRows, + passArrayDataType, +} from './spec'; + +import { setMetaData } from '../../validators/validators'; +import { getIndexRoute } from '../../../helpers/dataHelpers'; + +const setup = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Setup route', () => { + it('Visit the index route', () => { + cy.visit(getIndexRoute()); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runInsertBrowseTests = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Table: Browse and Insert', () => { + it('Create a table with fields of all data types', passBICreateTable); + it('Search for tables', passSearchTables); + it('Check Insert Route', checkInsertRoute); + it('Fails when entered wrong data type', failBIWrongDataType); + it('Insert 20 rows', passBIInsert20Rows); + it('Fail for adding same data for Unique keys', failBIUniqueKeys); + it('Check browser rows route', checkBrowseRoute); + // it('20 Inserted rows reflect in browse rows', passBI20RowsExist); + // it('Check pagination in Browse Rows table', checkPagination); + it('Ascending sort works as expected', () => passBISort('asc')); + it('Descending sort works as expected', () => passBISort('desc')); + it('Filter query works as expected with $eq', passBIFilterQueryEq); + it('Check edit button', passEditButton); + it('Check for clone clear', passCloneButton); + it('Delete the row', passDeleteRow); + it('Bulk delete rows', passBulkDeleteRows); + it('Bulk delete all rows', passBulkDeleteAllRows); + it('Handle array data types', passArrayDataType); + it('Check view relationship', checkViewRelationship); + it('Delete test table', deleteBITestTable); + }); +}; + +if (testMode !== 'cli') { + setup(); + runInsertBrowseTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/manage-database/common.spec.ts b/frontend/apps/console-ce-e2e/src/e2e/data/manage-database/common.spec.ts new file mode 100644 index 00000000000..41b793d0819 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/manage-database/common.spec.ts @@ -0,0 +1,52 @@ +import { setPromptWithCb } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; + +export const navigateAndOpenConnectDatabasesForm = () => { + cy.location('pathname').then(currentPage => { + const alreadyOnThePage = currentPage.startsWith('/data/manage'); + if (!alreadyOnThePage) { + cy.log('**--- Navigate to Connect Databases Form**'); + + cy.log('**--- visit index route and set metadata**'); + cy.visit('/data/default/schema/public').then(setMetaData); + + cy.log('**--- Click on the manage database menu**'); + cy.findByRole('button', { name: 'Manage' }).click(); + cy.location('pathname').should('eq', '/data/manage'); + } + }); + + cy.log('**--- Click on the Connect Database button**'); + cy.findByRole('button', { name: 'Connect Database' }).click(); + cy.location('pathname').should('eq', '/data/manage/connect'); + + cy.get('form').within(() => { + cy.log('**--- Click on Connection Settings section**'); + cy.contains('Connection Settings').click(); + }); + cy.get('form').within(() => { + cy.log('**--- Click on GraphQL Field Customization section**'); + cy.contains('GraphQL Field Customization').click(); + }); +}; + +export const navigateToManageDatabases = () => { + cy.log('**--- Visit index route and set metadata**'); + cy.visit('/data/default/schema/public').then(setMetaData); + + cy.log('**--- Visit the manage database route**'); + cy.getBySel('sidebar-manage-database').click(); + cy.location('pathname').should('eq', '/data/manage'); +}; + +export const submitConnectDBForm = () => { + cy.log('**--- Click on the Connect Database button**'); + cy.getBySel('connect-database-btn').click(); +}; + +export const removeDBFromList = (dbName: string) => { + setPromptWithCb(dbName, () => { + cy.log('**--- Click on the Remove Database button**'); + cy.getBySel(dbName).find('button').contains('Remove').click(); + }); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/manage-database/manage-database.e2e.test.ts b/frontend/apps/console-ce-e2e/src/e2e/data/manage-database/manage-database.e2e.test.ts new file mode 100644 index 00000000000..7d59ef2c236 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/manage-database/manage-database.e2e.test.ts @@ -0,0 +1,160 @@ +import { baseUrl, testMode } from '../../../helpers/common'; +import { + navigateAndOpenConnectDatabasesForm, + navigateToManageDatabases, + removeDBFromList, + submitConnectDBForm, +} from './common.spec'; +import { + createDB, + fillDetailsForPgConnParamsForm, + fillDetailsForPgDbUrlForm, + fillDetailsForPgEnvVarForm, + removeDB, +} from './postgres.spec'; + +const connectPgDatabaseFormTests = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Add a database via connect form', () => { + describe('can successfully add', () => { + describe('a postgres database', () => { + it('using a connection string', () => { + cy.log('**------------------------------**'); + cy.log('**--- Add postgres DB via connection string**'); + cy.log('**------------------------------**'); + + navigateAndOpenConnectDatabasesForm(); + fillDetailsForPgDbUrlForm('postgres_db_with_url'); + submitConnectDBForm(); + + cy.log('**--- Notifies that DB is being added'); + cy.expectSuccessNotificationWithTitle('Adding data source...'); + + cy.log('**--- Redirects to Data Manager page'); + cy.getBySel('manage-database-section').within(() => { + cy.findByText('Data Manager'); + }); + cy.url().should('eq', `${baseUrl}/data/manage`); + + cy.log('**--- has postgres_db_with_url on manage page'); + cy.findByText('postgres_db_with_url'); + + cy.log('**--- has success notification displayed'); + cy.expectSuccessNotificationWithTitle( + 'Data source added successfully!' + ); + + cy.log('**--- Remove database**'); + removeDB('postgres_db_with_url'); + }); + + it('using connection parameters', () => { + cy.log('**------------------------------**'); + cy.log('**--- Add postgres DB via connection parameters**'); + cy.log('**------------------------------**'); + + navigateAndOpenConnectDatabasesForm(); + fillDetailsForPgConnParamsForm('postgres_db_with_conn_param'); + submitConnectDBForm(); + + cy.log('**--- notifies that db is being added'); + cy.expectSuccessNotificationWithTitle('Adding data source...'); + + cy.log('**--- redirects to Data Manager page'); + cy.getBySel('manage-database-section').within(() => { + cy.findByText('Data Manager'); + }); + cy.url().should('eq', `${baseUrl}/data/manage`); + + cy.log('**--- has postgres_db_with_conn_param on manage page'); + cy.findByText('postgres_db_with_conn_param'); + + cy.log('**--- has success notification displayed'); + cy.expectSuccessNotificationWithTitle( + 'Data source added successfully!' + ); + + cy.log('**--- Remove database**'); + removeDB('postgres_db_with_conn_param'); + }); + + it('using environment variables', () => { + cy.log('**------------------------------**'); + cy.log('**--- Add postgres DB via env vars**'); + cy.log('**------------------------------**'); + + navigateAndOpenConnectDatabasesForm(); + fillDetailsForPgEnvVarForm('postgres_db_with_env_var'); + submitConnectDBForm(); + + cy.log('**--- notifies that db is being added'); + cy.expectSuccessNotificationWithTitle('Adding data source...'); + + cy.log('**--- redirects to Data Manager page'); + cy.getBySel('manage-database-section').within(() => { + cy.findByText('Data Manager'); + }); + cy.url().should('eq', `${baseUrl}/data/manage`); + + cy.log('**--- has postgres_db_with_env_var on manage page'); + cy.findByText('postgres_db_with_env_var'); + + cy.log('**--- has success notification displayed'); + cy.expectSuccessNotificationWithTitle( + 'Data source added successfully!' + ); + + cy.log('**--- Remove database**'); + removeDB('postgres_db_with_env_var'); + }); + }); + }); + + describe('fails on submitting', () => { + describe('an empty form', () => { + it('submit with no inputs filled in', () => { + navigateAndOpenConnectDatabasesForm(); + cy.getBySel('connect-database-btn').click(); + cy.expectErrorNotification(); + }); + }); + + it('with a duplicate database name', () => { + navigateAndOpenConnectDatabasesForm(); + createDB('test'); + fillDetailsForPgDbUrlForm('test'); + submitConnectDBForm(); + + cy.log('**--- verify error notification'); + cy.expectErrorNotificationWithTitle('Adding data source failed'); + + cy.log('**--- Remove database**'); + removeDB('test'); + }); + }); + }); +}; + +const manageDatabasesPageTests = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Connected Databases list page', () => { + it('can successfully remove db', () => { + cy.log('**--- Create database**'); + createDB('db_for_removal'); + navigateToManageDatabases(); + + cy.log('**--- use the remove button'); + removeDBFromList('db_for_removal'); + + cy.log('**--- verify success notification'); + cy.expectSuccessNotificationWithTitle('Data source removed successfully'); + }); + }); +}; + +if (testMode !== 'cli') { + connectPgDatabaseFormTests(); + manageDatabasesPageTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/manage-database/postgres.spec.ts b/frontend/apps/console-ce-e2e/src/e2e/data/manage-database/postgres.spec.ts new file mode 100644 index 00000000000..11fa756d31b --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/manage-database/postgres.spec.ts @@ -0,0 +1,198 @@ +const config = { + host: 'postgres', + port: '5432', + dbName: 'postgres', + username: 'postgres', + password: 'postgrespassword', +}; + +const dbUrl = `postgres://${config.username}:${config.password}@${config.host}:${config.port}/${config.dbName}`; + +const graphqlCustomizationTest = () => { + cy.log('**--- Select a graphql naming convention**'); + cy.get('[data-test="GraphQL Field Customization"]').within(() => { + cy.get('[data-test="radio-select-hasura-default"]').should('be.visible'); + cy.get('[data-test="radio-select-graphql-default"]').should('be.visible'); + cy.get('label[for="radio-select-graphql-default"]').click(); + }); + + cy.log('**--- Fill Root Fields Customizations**'); + cy.get('form[aria-label="rootFields"]').within(() => { + cy.findByPlaceholderText('Namespace...').type('name_space'); + cy.findByPlaceholderText('prefix_').type('prefix_'); + cy.findByPlaceholderText('_suffix').type('_suffix'); + }); + + cy.log('**--- Fill Type Names Customizations**'); + cy.get('form[aria-label="typeNames"]').within(() => { + cy.findByPlaceholderText('prefix_').type('prefix_'); + cy.findByPlaceholderText('_suffix').type('_suffix'); + }); +}; + +export const fillDetailsForPgDbUrlForm = (dbName: string) => { + cy.log('**--- Fill Form using db url**'); + cy.getBySel('database-display-name').type(dbName); + cy.getBySel('database-type').select('postgres'); + cy.getBySel('database-url').type(dbUrl); + cy.getBySel('max-connections').type('50'); + cy.getBySel('idle-timeout').type('180'); + cy.getBySel('retries').type('1'); + graphqlCustomizationTest(); +}; + +export const fillDetailsForPgConnParamsForm = (dbName: string) => { + cy.log('**--- Fill Form using connection parameter**'); + cy.get("input[type='radio']").eq(2).click(); + cy.getBySel('database-display-name').type(dbName); + cy.getBySel('database-type').select('postgres'); + cy.getBySel('host').type(config.host); + cy.getBySel('port').type(config.port); + cy.getBySel('username').type(config.username); + cy.getBySel('password').type(config.password); + cy.getBySel('database-name').type(config.dbName); + graphqlCustomizationTest(); +}; + +export const fillDetailsForPgEnvVarForm = (dbName: string) => { + cy.log('**--- Fill Form using env vars**'); + cy.get("input[type='radio']").eq(0).click(); + cy.getBySel('database-display-name').type(dbName); + cy.getBySel('database-type').select('postgres'); + cy.getBySel('database-url-env').type('HASURA_GRAPHQL_DATABASE_URL'); + graphqlCustomizationTest(); +}; + +export const createDB = (dbName: string) => { + const postBody = { + type: 'pg_add_source', + args: { + name: dbName, + configuration: { + connection_info: { + database_url: dbUrl, + }, + }, + }, + }; + cy.request('POST', 'http://localhost:8080/v1/metadata', postBody).then( + response => { + expect(response.body).to.have.property('message', 'success'); // true + } + ); +}; + +export const removeDB = (dbName: string) => { + const postBody = { type: 'pg_drop_source', args: { name: dbName } }; + cy.request('POST', 'http://localhost:8080/v1/metadata', postBody).then( + response => { + expect(response.body).to.have.property('message', 'success'); // true + } + ); + cy.reload(); +}; + +const createTable = (tableName: string) => { + const postBody = { + type: 'run_sql', + args: { + source: 'default', + sql: `CREATE TABLE "public"."${tableName}" ("id" serial NOT NULL, "name" text NOT NULL, "countryCode" text DEFAULT 'IN', PRIMARY KEY ("id") );`, + cascade: false, + read_only: false, + }, + }; + cy.request('POST', 'http://localhost:8080/v2/query', postBody).then( + response => { + expect(response.body).to.have.property('result_type', 'CommandOk'); // true + } + ); +}; + +const trackTable = (tableName: string) => { + const postBody = { + type: 'pg_track_table', + args: { + table: { + name: tableName, + schema: 'public', + }, + source: 'default', + }, + }; + cy.request('POST', 'http://localhost:8080/v1/metadata', postBody).then( + response => { + expect(response.body).to.have.property('message', 'success'); // true + } + ); +}; + +const untrackTable = (tableName: string) => { + const postBody = { + type: 'pg_untrack_table', + args: { + table: { + schema: 'public', + name: tableName, + }, + source: 'default', + }, + }; + cy.request('POST', 'http://localhost:8080/v1/metadata', postBody).then( + response => { + expect(response.body).to.have.property('message', 'success'); // true + } + ); +}; + +const deleteTable = (tableName: string) => { + const postBody = { + type: 'run_sql', + args: { + source: 'default', + sql: `DROP table "public"."${tableName}";`, + cascade: false, + read_only: false, + }, + }; + cy.request('POST', 'http://localhost:8080/v2/query', postBody).then( + response => { + expect(response.body).to.have.property('result_type', 'CommandOk'); // true + } + ); +}; + +const createRemoteSchema = (remoteSchemaName: string) => { + const postBody = { + type: 'add_remote_schema', + args: { + name: remoteSchemaName, + definition: { + timeout_seconds: 60, + forward_client_headers: false, + headers: [], + url: 'https://countries.trevorblades.com/', + }, + comment: '', + }, + }; + cy.request('POST', 'http://localhost:8080/v1/metadata', postBody).then( + response => { + expect(response.body).to.have.property('message', 'success'); // true + } + ); +}; + +const deleteRemoteSchema = (remoteSchemaName: string) => { + const postBody = { + type: 'remove_remote_schema', + args: { + name: remoteSchemaName, + }, + }; + cy.request('POST', 'http://localhost:8080/v1/metadata', postBody).then( + response => { + expect(response.body).to.have.property('message', 'success'); // true + } + ); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/materialized-views/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/data/materialized-views/spec.ts new file mode 100644 index 00000000000..f42fc413338 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/materialized-views/spec.ts @@ -0,0 +1,424 @@ +import { + getElementFromAlias, + baseUrl, + tableColumnTypeSelector, + getIndexRoute, +} from '../../../helpers/dataHelpers'; + +import { + setMetaData, + validateCT, + createView, + validateColumn, + validateView, + ResultType, + TableFields, +} from '../../validators/validators'; +import { setPromptValue } from '../../../helpers/common'; + +const userId = 5555; + +export const createTable = (name: string, dict: TableFields) => { + cy.url().should('eq', `${baseUrl}/data/default/schema/public/table/add`); + cy.get(getElementFromAlias('tableName')).type(`${name}_table_vt`); + const keys = Object.keys(dict).map(k => k); + const values = Object.keys(dict).map(k => dict[k]); + for (let i = 0; i < keys.length; i += 1) { + cy.get(getElementFromAlias(`column-${i}`)).type(keys[i]); + tableColumnTypeSelector(`col-type-${i}`); + cy.get(getElementFromAlias(`data_test_column_type_value_${values[i]}`)) + .first() + .click(); + } + cy.get(getElementFromAlias('primary-key-select-0')).select('id'); + cy.get(getElementFromAlias('table-create')).click(); + cy.wait(7000); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${name}_table_vt/modify` + ); + + validateCT(`${name}_table_vt`, ResultType.SUCCESS); +}; + +export const passVCreateTables = () => { + cy.get(getElementFromAlias('data-create-table')).click(); + createTable('author', { id: 'integer', name: 'text' }); + cy.visit(getIndexRoute()); + cy.wait(5000); + cy.get(getElementFromAlias('data-create-table')).click(); + createTable('article', { + id: 'integer', + title: 'text', + Content: 'text', + author_id: 'integer', + rating: 'integer', + }); + cy.visit(getIndexRoute()); + cy.wait(5000); + cy.get(getElementFromAlias('data-create-table')).click(); + createTable('comment', { + id: 'integer', + user_id: 'integer', + article_id: 'integer', + comment: 'text', + }); +}; + +export const passVCreateMaterializedViews = () => { + createView(`CREATE MATERIALIZED VIEW author_average_rating_vt AS + SELECT author_table_vt.id, avg(article_table_vt.rating) + From author_table_vt, article_table_vt + WHERE author_table_vt.id = article_table_vt.author_id + GROUP BY author_table_vt.id`); +}; + +export const passTrackTable = () => { + cy.visit('/data/default/schema/public/'); + cy.wait(7000); + cy.get( + getElementFromAlias('add-track-table-author_average_rating_vt') + ).click(); + cy.wait(7000); + validateView('author_average_rating_vt', ResultType.SUCCESS); +}; + +export const passMaterializedViewRoute = () => { + cy.get(getElementFromAlias('author_average_rating_vt')).click(); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/views/author_average_rating_vt/browse` + ); +}; + +export const passVAddDataarticle = ( + data: (string | number)[], + index: number +) => { + // Click the Insert Again button. + cy.get('label') + .contains('id') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label').contains('id').next().find('input').last().type(`${data[0]}`); + cy.get('label') + .contains('title') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('title') + .next() + .find('input') + .last() + .type(`${data[1]}`); + cy.get('label') + .contains('Content') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('Content') + .next() + .find('input') + .last() + .type(`${data[2]}`); + cy.get('label') + .contains('author_id') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('author_id') + .next() + .find('input') + .last() + .type(`${data[3]}`); + cy.get('label') + .contains('rating') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('rating') + .next() + .find('input') + .last() + .type(`${data[4]}`); + if (index) { + cy.get(getElementFromAlias('insert-save-button')).click(); + } else { + cy.get(getElementFromAlias('insert-save-button')).click(); + } + + cy.wait(5000); +}; + +export const passVAddDataauthor = ( + data: (string | number)[], + index: number +) => { + cy.get('label') + .contains('id') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label').contains('id').next().find('input').last().type(`${data[0]}`); + cy.get('label') + .contains('name') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('name') + .next() + .find('input') + .last() + .type(`${data[1]}`); + if (index) { + cy.get(getElementFromAlias('insert-save-button')).click(); + } else { + cy.get(getElementFromAlias('insert-save-button')).click(); + } + cy.wait(5000); +}; + +export const passVAddDatacomment = ( + data: (string | number)[], + index: number +) => { + cy.get('label') + .contains('id') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label').contains('id').next().find('input').last().type(`${data[0]}`); + cy.get('label') + .contains('user_id') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('user_id') + .next() + .find('input') + .last() + .type(`${data[1]}`); + cy.get('label') + .contains('article_id') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('article_id') + .next() + .find('input') + .last() + .type(`${data[2]}`); + cy.get('label') + .contains('comment') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('comment') + .next() + .find('input') + .last() + .type(`${data[3]}`); + if (index) { + cy.get(getElementFromAlias('insert-save-button')).click(); + } else { + cy.get(getElementFromAlias('insert-save-button')).click(); + } + cy.wait(5000); +}; + +const checkQuerySuccess = () => { + // Expect only 4 rows i.e. expect fifth element to not exist + cy.get('[role=gridcell]').contains(userId); + cy.get('[role=row]').eq(2).should('not.exist'); +}; + +export const passVAddData = () => { + let data; + cy.get(getElementFromAlias('article_table_vt')).click(); + cy.get(getElementFromAlias('table-insert-rows')).click(); + data = [1, 'A', 'Sontent', userId, 4]; + passVAddDataarticle(data, 0); + data = [2, 'B', 'Sontenta', 2, 4]; + passVAddDataarticle(data, 1); + data = [3, 'C', 'Sontentb', userId, 4]; + passVAddDataarticle(data, 2); + cy.get(getElementFromAlias('author_table_vt')).click(); + cy.get(getElementFromAlias('table-insert-rows')).click(); + + data = [userId, 'A']; + passVAddDataauthor(data, 0); + data = [2, 'B']; + passVAddDataauthor(data, 1); + cy.get(getElementFromAlias('comment_table_vt')).click(); + cy.get(getElementFromAlias('table-insert-rows')).click(); + + data = [1, 1, 1, 'new comment']; + passVAddDatacomment(data, 0); + data = [2, 2, 2, 'new comment']; + passVAddDatacomment(data, 1); + data = [3, 1, 2, 'new comment']; + passVAddDatacomment(data, 2); +}; + +export const passVFilterQueryEq = () => { + // Select column with type `text` + cy.get('select') + .find('option') + .contains('-- column --') + .parent() + .first() + .select('id'); + // Type value as `filter-text` + cy.get("input[placeholder='-- value --']").last().type(`${userId}`); + // Run query + cy.get(getElementFromAlias('run-query')).click(); + cy.wait(5000); + // Check if the query was successful + checkQuerySuccess(); +}; + +const checkOrder = (order: string) => { + // Utility function to get right element + if (order === 'asc') { + cy.get('[role=row]').each(($el, index) => { + if (index === 1) { + cy.wrap($el).find('[role=gridcell]').first().next().next().contains(2); + } + if (index === 2) { + cy.wrap($el) + .find('[role=gridcell]') + .first() + .next() + .next() + .contains(userId); + } + }); + } else { + cy.get('[role=row]').each(($el, index) => { + if (index === 2) { + cy.wrap($el).find('[role=gridcell]').first().next().next().contains(2); + } + if (index === 1) { + cy.wrap($el) + .find('[role=gridcell]') + .first() + .next() + .next() + .contains(userId); + } + }); + } +}; + +export const passVAscendingSort = () => { + cy.wait(7000); + // cy.scrollTo('top'); + // Select column with type 'serial' + cy.get('select') + .find('option') + .contains('-- column --') + .parent() + .last() + .select('id'); + // Run query + cy.get(getElementFromAlias('run-query')).click(); + // Check order + checkOrder('asc'); +}; + +export const passModifyMaterializedView = () => { + cy.get(getElementFromAlias('table-modify')).click(); + cy.get('button').contains('Modify').last().click(); + cy.url().should('eq', `${baseUrl}/data/sql`); +}; + +export const passVAddManualObjRel = () => { + cy.get(getElementFromAlias('author_average_rating_vt')).click(); + cy.wait(2000); + cy.get(getElementFromAlias('table-relationships')).click(); + cy.wait(2000); + cy.get(getElementFromAlias('create-edit-manual-rel')).click(); + cy.get(getElementFromAlias('manual-relationship-type')).select('object'); + cy.get("input[placeholder='Enter relationship name']").type('author'); + cy.get(getElementFromAlias('manual-relationship-ref-schema')).select( + 'public' + ); + cy.get(getElementFromAlias('manual-relationship-ref-table')).select( + 'author_table_vt' + ); + cy.get(getElementFromAlias('manual-relationship-lcol-0')).select('id'); + cy.get(getElementFromAlias('manual-relationship-rcol-0')).select('id'); + cy.get(getElementFromAlias('create-manual-rel-save')).click(); + cy.wait(7000); + validateColumn( + 'author_average_rating_vt', + ['avg', { name: 'author', columns: ['name'] }], + ResultType.SUCCESS + ); +}; + +export const passVDeleteRelationships = () => { + cy.get(getElementFromAlias('author_average_rating_vt')).click(); + cy.get(getElementFromAlias('table-relationships')).click(); + cy.get(getElementFromAlias('relationship-toggle-editor-author')).click(); + cy.get(getElementFromAlias('relationship-remove-author')).click(); + cy.on('window:alert', str => { + return expect(str === 'Are you sure?').to.be.true; + }); + cy.wait(7000); + validateColumn( + 'author_average_rating_vt', + ['avg', { name: 'author', columns: ['name'] }], + ResultType.FAILURE + ); +}; + +export const passVDeleteMaterializedView = () => { + cy.get(getElementFromAlias('table-modify')).click(); + setPromptValue('author_average_rating_vt'); + cy.get(getElementFromAlias('delete-view')).click(); + cy.window().its('prompt').should('be.called'); + cy.wait(7000); + validateView('author_average_rating_vt', ResultType.FAILURE); +}; + +export const deleteTable = (name: string) => { + cy.get(getElementFromAlias(name)).click(); + cy.get(getElementFromAlias('table-modify')).click(); + setPromptValue(name); + cy.get(getElementFromAlias('delete-table')).click(); + cy.window().its('prompt').should('be.called'); + cy.wait(7000); + validateCT(name, ResultType.FAILURE); + cy.wait(7000); +}; + +export const passVDeleteTables = () => { + deleteTable('comment_table_vt'); + deleteTable('article_table_vt'); + deleteTable('author_table_vt'); +}; + +export const setValidationMetaData = () => { + setMetaData(); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/materialized-views/test.ts b/frontend/apps/console-ce-e2e/src/e2e/data/materialized-views/test.ts new file mode 100644 index 00000000000..71b8d0d74b8 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/materialized-views/test.ts @@ -0,0 +1,54 @@ +import { + passVCreateTables, + passVCreateMaterializedViews, + passVAddData, + passTrackTable, + passVAddManualObjRel, + passVAscendingSort, + passModifyMaterializedView, + passVFilterQueryEq, + passMaterializedViewRoute, + passVDeleteRelationships, + passVDeleteMaterializedView, + passVDeleteTables, +} from './spec'; +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; +import { getIndexRoute } from '../../../helpers/dataHelpers'; + +const setup = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Setup route', () => { + it('Visit the index route', () => { + // Visit the index route + cy.visit(getIndexRoute()); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runMaterializedViewsTest = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Materialized Views', () => { + it('Create Tables', passVCreateTables); + it('Add data to table', passVAddData); + it('Create MaterializedView', passVCreateMaterializedViews); + it('Adding it to the table', passTrackTable); + it('Check the materializedview route', passMaterializedViewRoute); + it('Ascending order MaterializedView Table', passVAscendingSort); + it('Filter the MaterializedView table', passVFilterQueryEq); + it('Modify the View', passModifyMaterializedView); + it('Adding Object Relationship to MaterializedView', passVAddManualObjRel); + it('Deleting Relationship', passVDeleteRelationships); + it('Deleting MaterializedView', passVDeleteMaterializedView); + it('Deleting Tables', passVDeleteTables); + }); +}; + +if (testMode !== 'cli') { + setup(); + runMaterializedViewsTest(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/migration-mode/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/data/migration-mode/spec.ts new file mode 100644 index 00000000000..f5a0c7394e7 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/migration-mode/spec.ts @@ -0,0 +1,29 @@ +import { validateMigrationMode } from '../../validators/validators'; + +import { toggleOnMigrationMode, toggleOffMigrationMode } from './utils'; + +export const testToggleButton = () => { + // Turn off migration mode + toggleOffMigrationMode(); + cy.wait(10000); + // Validate + validateMigrationMode(false); + cy.wait(7000); + // Turn on migration mode + toggleOnMigrationMode(); + cy.wait(10000); + // Validate + validateMigrationMode(true); + cy.wait(7000); +}; + +export const checkToggleButton = () => { + cy.window().then(win => { + const { consoleMode } = win.__env; + if (consoleMode === 'cli') { + testToggleButton(); + } else { + cy.get('[class=react-toggle-track]').should('not.exist'); + } + }); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/migration-mode/test.ts b/frontend/apps/console-ce-e2e/src/e2e/data/migration-mode/test.ts new file mode 100644 index 00000000000..d38d4f3dac1 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/migration-mode/test.ts @@ -0,0 +1,27 @@ +import { checkToggleButton } from './spec'; +import { testMode } from '../../../helpers/common'; + +import { setMetaData } from '../../validators/validators'; +import { getIndexRoute } from '../../../helpers/dataHelpers'; + +const setup = () => { + describe('Setup route', () => { + it('Visit the index route', () => { + // Visit the index route + cy.visit(getIndexRoute()); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runMigrationModeTests = () => { + describe('Migration mode', () => { + it('Check the toggle button', checkToggleButton); + }); +}; + +if (testMode !== 'cli') { + setup(); + runMigrationModeTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/migration-mode/utils.ts b/frontend/apps/console-ce-e2e/src/e2e/data/migration-mode/utils.ts new file mode 100644 index 00000000000..83596bd4d5a --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/migration-mode/utils.ts @@ -0,0 +1,37 @@ +import { migrateModeUrl } from '../../../helpers/common'; + +export const toggleOnMigrationMode = () => { + cy.request({ + method: 'GET', + url: migrateModeUrl, + }).then(response => { + if (response.body.migration_mode === 'false') { + // Go to migrations section + cy.get('a') + .contains('Migrations') + .click(); + cy.wait(3000); + // Toggle Migration mode + cy.get('[class=react-toggle-track]').click(); + cy.wait(10000); + } + }); +}; + +export const toggleOffMigrationMode = () => { + cy.request({ + method: 'GET', + url: migrateModeUrl, + }).then(response => { + if (response.body.migration_mode === 'true') { + // Go to migrations section + cy.get('a') + .contains('Migrations') + .click(); + cy.wait(3000); + // Toggle Migration mode + cy.get('[class=react-toggle-track]').click(); + cy.wait(10000); + } + }); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/modify/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/data/modify/spec.ts new file mode 100644 index 00000000000..a6b37c7f193 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/modify/spec.ts @@ -0,0 +1,439 @@ +import { + tableColumnTypeSelector, + baseUrl, + getTableName, + getColName, + getElementFromAlias, + createUntrackedFunctionSQL, + dropUntrackedFunctionSQL, + getIndexRoute, +} from '../../../helpers/dataHelpers'; + +import { + setMetaData, + validateCT, + validateColumn, + ResultType, + dataRequest, +} from '../../validators/validators'; +import { setPromptValue } from '../../../helpers/common'; + +const testName = 'mod'; + +export const passMTFunctionList = () => { + const tableName = getTableName(0, testName); + dataRequest( + createUntrackedFunctionSQL(`${tableName}_id_fn`, tableName), + ResultType.SUCCESS + ); + cy.wait(5000); + cy.get(getElementFromAlias('modify-table-edit-computed-field-0')).click(); + + cy.get(getElementFromAlias('functions-dropdown')).click(); + + cy.get('[data-test^="data_test_column_type_value_"]').should( + 'have.length', + 1 + ); + + cy.get('[data-test^="data_test_column_type_value_"]') + .first() + .should('have.text', `${getTableName(0, testName)}_id_fn`.toLowerCase()); + dataRequest( + dropUntrackedFunctionSQL(`${tableName}_id_fn`), + ResultType.SUCCESS + ); +}; + +export const passMTCreateTable = () => { + cy.get(getElementFromAlias('data-create-table')).click(); + cy.url().should('eq', `${baseUrl}/data/default/schema/public/table/add`); + cy.get(getElementFromAlias('tableName')).type(getTableName(0, testName)); + cy.get(getElementFromAlias('column-0')).type('id'); + tableColumnTypeSelector('col-type-0'); + cy.get(getElementFromAlias('data_test_column_type_value_integer')) + .first() + .click(); + cy.get(getElementFromAlias('primary-key-select-0')).select('id'); + cy.get(getElementFromAlias('table-create')).click(); + cy.wait(7000); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 0, + testName + )}/modify` + ); + validateCT(getTableName(0, testName), ResultType.SUCCESS); +}; + +export const passMTCheckRoute = () => { + // Click on the create table button + cy.get(getElementFromAlias('table-modify')).click(); + // Match the URL + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 0, + testName + )}/modify` + ); +}; + +export const passMTRenameTable = () => { + cy.get(getElementFromAlias('heading-edit-table')).click(); + cy.get(getElementFromAlias('heading-edit-table-input')) + .clear() + .type(getTableName(3, testName)); + cy.get(getElementFromAlias('heading-edit-table-save')).click(); + cy.wait(15000); + validateCT(getTableName(3, testName), ResultType.SUCCESS); + cy.get(getElementFromAlias('heading-edit-table')).click(); + cy.get(getElementFromAlias('heading-edit-table-input')) + .clear() + .type(getTableName(0, testName)); + cy.get(getElementFromAlias('heading-edit-table-save')).click(); + cy.wait(15000); + validateCT(getTableName(0, testName), ResultType.SUCCESS); +}; + +export const passMTRenameColumn = () => { + cy.wait(10000); + cy.get(getElementFromAlias('modify-table-edit-column-0')).click(); + cy.get(getElementFromAlias('edit-col-name')).clear().type(getColName(3)); + cy.get(getElementFromAlias('modify-table-column-0-save')).click(); + cy.wait(15000); + validateColumn( + getTableName(0, testName), + [getColName(3)], + ResultType.SUCCESS + ); + cy.get(getElementFromAlias('modify-table-edit-column-0')).click(); + cy.get(getElementFromAlias('edit-col-name')).clear().type('id'); + cy.get(getElementFromAlias('modify-table-column-0-save')).click(); + cy.wait(15000); + validateColumn(getTableName(0, testName), ['id'], ResultType.SUCCESS); +}; + +export const passMTChangeDefaultValueForPKey = () => { + cy.wait(10000); + cy.get(getElementFromAlias('modify-table-edit-column-0')).click(); + cy.get(getElementFromAlias('edit-col-default')).clear().type('1234'); + cy.get(getElementFromAlias('modify-table-column-0-save')).click(); + cy.wait(15000); +}; + +export const passMTMoveToTable = () => { + cy.get(getElementFromAlias(getTableName(0, testName))).click(); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 0, + testName + )}/browse` + ); +}; + +export const failMTWithoutColName = () => { + cy.get(getElementFromAlias('modify-table-edit-add-new-column')).click(); + cy.get(getElementFromAlias('modify-table-add-new-column-save')).click(); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 0, + testName + )}/modify` + ); + + validateColumn( + getTableName(0, testName), + [getColName(2)], + ResultType.FAILURE + ); +}; + +export const failMTWithoutColType = () => { + cy.get(getElementFromAlias('column-name')).type(getColName(2)); + cy.get(getElementFromAlias('modify-table-add-new-column-save')).click(); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 0, + testName + )}/modify` + ); + validateColumn( + getTableName(0, testName), + [getColName(2)], + ResultType.FAILURE + ); +}; + +export const Addcolumnnullable = () => { + cy.get(getElementFromAlias('column-name')).type('{selectall}{del}'); + cy.get(getElementFromAlias('column-name')).type(getColName(3)); + tableColumnTypeSelector('col-type-0'); + cy.get(getElementFromAlias('data_test_column_type_value_text')) + .first() + .click(); + cy.get(getElementFromAlias('nullable-checkbox')).uncheck({ force: true }); + cy.get(getElementFromAlias('modify-table-add-new-column-save')).click(); + cy.wait(2500); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 0, + testName + )}/modify` + ); + validateColumn( + getTableName(0, testName), + [getColName(3)], + ResultType.FAILURE + ); +}; + +export const Addcolumnname = (name: string) => { + cy.get(getElementFromAlias('column-name')).type('{selectall}{del}'); + cy.get(getElementFromAlias('column-name')).type(name); + + tableColumnTypeSelector('col-type-0'); + cy.get(getElementFromAlias('data_test_column_type_value_integer')) + .first() + .click(); + + cy.get(getElementFromAlias('modify-table-add-new-column-save')).click(); + cy.wait(5000); + validateColumn(getTableName(0, testName), [name], ResultType.SUCCESS); +}; + +export const passMTAddColumn = () => { + cy.get(getElementFromAlias('frequently-used-columns')).first().should('exist'); + cy.get(getElementFromAlias('column-name')).type('{selectall}{del}'); + cy.get(getElementFromAlias('column-name')).type(getColName(0)); + tableColumnTypeSelector('col-type-0'); + cy.get(getElementFromAlias('data_test_column_type_value_integer')) + .first() + .click(); + cy.get(getElementFromAlias('modify-table-add-new-column-save')).click(); + cy.wait(5000); + validateColumn( + getTableName(0, testName), + [getColName(0)], + ResultType.SUCCESS + ); +}; + +export const Movetocolumn = () => { + Addcolumnname(getColName(1)); + cy.get(getElementFromAlias('modify-table-edit-column-1')).click(); +}; + +export const failMCWithWrongDefaultValue = () => { + cy.get(getElementFromAlias('modify-table-edit-column-1')).click(); + cy.get(getElementFromAlias('edit-col-default')).type('abcd'); + cy.get(getElementFromAlias('modify-table-column-1-save')).click(); +}; + +export const passMCWithRightDefaultValue = () => { + cy.get(getElementFromAlias('edit-col-default')).clear().type('1234'); + cy.get(getElementFromAlias('modify-table-column-1-save')).click(); + cy.wait(10000); +}; + +export const passCreateForeignKey = () => { + cy.get(getElementFromAlias('modify-table-edit-fk-0')).click(); + cy.get(getElementFromAlias('foreign-key-ref-table-0')).select( + getTableName(0, testName) + ); + cy.get(getElementFromAlias('foreign-key-0-lcol-0')).select('0'); + cy.get(getElementFromAlias('foreign-key-0-rcol-0')).select('id'); + cy.get(getElementFromAlias('modify-table-fk-0-save')).click(); + cy.wait(10000); +}; + +export const passRemoveForeignKey = () => { + cy.get(getElementFromAlias('modify-table-edit-fk-0')).click(); + cy.get(getElementFromAlias('modify-table-fk-0-remove')).click(); + cy.wait(10000); +}; + +export const passModifyPkey = () => { + cy.get(getElementFromAlias('modify-table-edit-pks')).click(); + cy.get(getElementFromAlias('primary-key-select-1')).select('1'); + cy.get(getElementFromAlias('modify-table-pks-save')).click(); + cy.get(getElementFromAlias('pk-config-text')).within(() => { + cy.get('b').contains(getColName(0)); + cy.get('b').contains('id'); + }); + cy.wait(5000); + + cy.get(getElementFromAlias('remove-pk-column-1')).click(); + cy.get(getElementFromAlias('modify-table-pks-save')).click(); + cy.get(getElementFromAlias('pk-config-text')).within(() => { + cy.get('b').contains('id'); + }); + cy.get(getElementFromAlias('pk-config-text')).within(() => { + cy.get('b').should('not.contain', getColName(0)); + }); + cy.get(getElementFromAlias('modify-table-close-pks')).click(); + cy.wait(3000); +}; + +export const passCreateUniqueKey = () => { + cy.get(getElementFromAlias('modify-table-edit-unique-key-0')).click(); + cy.get(getElementFromAlias('unique-key-0-column-0')).select('0'); + cy.get(getElementFromAlias('unique-key-0-column-1')).select('1'); + cy.wait(1000); + cy.get(getElementFromAlias('modify-table-unique-key-0-save')).click(); + cy.wait(5000); + cy.get('div').contains( + `${getTableName(0, testName)}_id_${getColName(0)}_key` + ); +}; + +export const passModifyUniqueKey = () => { + cy.get(getElementFromAlias('modify-table-edit-unique-key-0')).click(); + cy.get(getElementFromAlias('remove-uk-0-column-0')).click(); + cy.get(getElementFromAlias('modify-table-unique-key-0-save')).click(); + cy.wait(5000); + cy.get('div').contains(`${getTableName(0, testName)}_${getColName(0)}_key`); +}; + +export const passRemoveUniqueKey = () => { + cy.get(getElementFromAlias('modify-table-edit-unique-key-0')).click(); + cy.get(getElementFromAlias('modify-table-unique-key-0-remove')).click(); + cy.wait(5000); +}; + +export const passMTDeleteCol = () => { + setPromptValue(getColName(0)); + cy.get(getElementFromAlias('modify-table-edit-column-1')).click(); + cy.get(getElementFromAlias('modify-table-column-1-remove')).click(); + cy.window().its('prompt').should('be.called'); + cy.wait(5000); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 0, + testName + )}/modify` + ); + validateColumn( + getTableName(0, testName), + [getColName(0)], + ResultType.FAILURE + ); +}; + +export const passMTDeleteTableCancel = () => { + setPromptValue(null); + cy.get(getElementFromAlias('delete-table')).click(); + cy.window().its('prompt').should('be.called'); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 0, + testName + )}/modify` + ); + + validateCT(getTableName(0, testName), ResultType.SUCCESS); +}; + +export const passMTDeleteTable = () => { + setPromptValue(getTableName(0, testName)); + cy.get(getElementFromAlias('delete-table')).click(); + cy.window().its('prompt').should('be.called'); + cy.wait(5000); + cy.url().should('eq', `${baseUrl}/data/default/schema/public`); + validateCT(getTableName(0, testName), ResultType.FAILURE); +}; + +export const setValidationMetaData = () => { + setMetaData(); +}; + +// Views Modify ///////////////////////////////////////////////// + +export const createTable = (name: string, dict: { [key: string]: any }) => { + cy.url().should('eq', `${baseUrl}/data/schema/public/table/add`); + cy.get(getElementFromAlias('tableName')).type(`${name}_table_mod`); + const keys = Object.keys(dict).map(k => k); + const values = Object.keys(dict).map(k => dict[k]); + for (let i = 0; i < keys.length; i += 1) { + cy.get('input[placeholder="column_name"]').last().type(keys[i]); + cy.get('select') + .find('option') + .contains('-- type --') + .parent() + .last() + .select(values[i]); + } + + cy.get('select').last().select('id'); + cy.get(getElementFromAlias('table-create')).click(); + cy.wait(7000); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${name}_table_mod/modify` + ); + + validateCT(`${name}_table_mod`, ResultType.SUCCESS); +}; + +export const Createtables = () => { + cy.get(getElementFromAlias('data-create-table')).click(); + createTable('author', { id: 'integer', name: 'Text' }); + cy.visit(getIndexRoute()); + cy.wait(5000); + cy.get(getElementFromAlias('data-create-table')).click(); + createTable('article', { + id: 'integer', + title: 'text', + Content: 'text', + author_id: 'integer', + rating: 'integer', + }); +}; + +export const Createview = () => { + cy.get(getElementFromAlias('sql-link')).click(); + cy.get('textarea').type(`CREATE VIEW author_average_rating_mod AS + SELECT author_table_mod.id, avg(article_table.rating) + From author_table_mod, article_table_mod + WHERE author_table_mod.id = article_table_mod.author_id + GROUP BY author_table_mod.id`); + cy.get(getElementFromAlias('run-sql')).click(); + validateCT('author_average_rating_mod', ResultType.SUCCESS); +}; + +export const Checkviewtable = () => { + cy.get(getElementFromAlias('author_average_rating_mod')).click(); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/views/author_average_rating_mod/browse` + ); + cy.get(getElementFromAlias('table-modify')).click(); + cy.get(getElementFromAlias('modify-view')).click(); + cy.url().should('eq', `${baseUrl}/data/sql`); +}; + +export const Checkviewtabledelete = () => { + cy.get(getElementFromAlias('author_average_rating_mod')).click(); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/views/author_average_rating_mod/browse` + ); + cy.get(getElementFromAlias('table-modify')).click(); + setPromptValue('author_average_rating_mod'); + cy.get(getElementFromAlias('delete-view')).click(); + cy.window().its('prompt').should('be.called'); + + cy.wait(7000); + validateCT('author_average_rating_mod', ResultType.FAILURE); +}; + +export const Issue = () => { + cy.get('.ace_text-input').first().type('#include'); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/modify/test.ts b/frontend/apps/console-ce-e2e/src/e2e/data/modify/test.ts new file mode 100644 index 00000000000..aba409744cb --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/modify/test.ts @@ -0,0 +1,75 @@ +import { + passMTCheckRoute, + passMTMoveToTable, + passMTCreateTable, + failMTWithoutColName, + failMTWithoutColType, + passMTAddColumn, + passMTDeleteTableCancel, + passMTDeleteCol, + passMTDeleteTable, + passMCWithRightDefaultValue, + failMCWithWrongDefaultValue, + passCreateForeignKey, + passRemoveForeignKey, + passMTRenameTable, + passMTRenameColumn, + passModifyPkey, + passCreateUniqueKey, + passModifyUniqueKey, + passRemoveUniqueKey, + passMTChangeDefaultValueForPKey, + passMTFunctionList, +} from './spec'; + +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; +import { getIndexRoute } from '../../../helpers/dataHelpers'; + +const setup = () => { + describe('Check Data Tab', () => { + it('Clicking on Data tab opens the correct route', () => { + // Visit the index route + cy.visit(getIndexRoute()); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runModifyTableTests = () => { + describe('Modify Table', () => { + it('Creating a table', passMTCreateTable); + it('Moving to the table', passMTMoveToTable); + it('Modify table button opens the correct route', passMTCheckRoute); + it( + 'Can create computed field with compatible functions', + passMTFunctionList + ); + it('Pass renaming table', passMTRenameTable); + it('Pass renaming column', passMTRenameColumn); + it('Fails to add column without column name', failMTWithoutColName); + it('Fails without type selected', failMTWithoutColType); + it('Add a column', passMTAddColumn); + it('Fail modify with wrong default value', failMCWithWrongDefaultValue); + it('Pass modify with wrong default value', passMCWithRightDefaultValue); + it('Pass create foreign-key', passCreateForeignKey); + it('Pass remove foreign-key', passRemoveForeignKey); + it( + 'Pass edit default value for primary key', + passMTChangeDefaultValueForPKey + ); + it('Pass modifying a primary key', passModifyPkey); + it('Pass creating a unique key', passCreateUniqueKey); + it('Pass modifying a unique key', passModifyUniqueKey); + it('Pass removing a unique key', passRemoveUniqueKey); + it('Delete the column', passMTDeleteCol); + it('Delete Table Cancel', passMTDeleteTableCancel); + it('Delete table', passMTDeleteTable); + }); +}; + +if (testMode !== 'cli') { + setup(); + runModifyTableTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/permissions/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/data/permissions/spec.ts new file mode 100644 index 00000000000..dad83435683 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/permissions/spec.ts @@ -0,0 +1,133 @@ +import { + tableColumnTypeSelector, + baseUrl, + getTableName, + getElementFromAlias, + getColName, + queryTypes, +} from '../../../helpers/dataHelpers'; + +import { setMetaData } from '../../validators/validators'; + +import { testPermissions, permRemove, createView, trackView } from './utils'; +import { setPromptValue } from '../../../helpers/common'; + +const testName = 'perm'; + +export const passPTCreateTable = () => { + // Click on create table + cy.get(getElementFromAlias('data-create-table')).click(); + // Match the URL + cy.url().should('eq', `${baseUrl}/data/default/schema/public/table/add`); + // Type table name + cy.get(getElementFromAlias('tableName')).type(getTableName(0, testName)); + // Set first column + cy.get(getElementFromAlias('column-0')).type(getColName(0)); + tableColumnTypeSelector('col-type-0'); + cy.get(getElementFromAlias('data_test_column_type_value_serial')) + .first() + .click(); + // Set second column + cy.get(getElementFromAlias('column-1')).type(getColName(1)); + tableColumnTypeSelector('col-type-1'); + cy.get(getElementFromAlias('data_test_column_type_value_integer')) + .first() + .click(); + + // Set third column + cy.get(getElementFromAlias('column-2')).type(getColName(2)); + tableColumnTypeSelector('col-type-2'); + cy.get(getElementFromAlias('data_test_column_type_value_text')) + .first() + .click(); + // Set primary key + cy.get(getElementFromAlias('primary-key-select-0')).select('0'); + // Create + cy.get(getElementFromAlias('table-create')).click(); + cy.wait(7000); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName(0, testName)}/modify` + ); +}; + +export const passPTCheckRoute = () => { + // Go to permissiosn tab + cy.get(getElementFromAlias('table-permissions')).click(); + // Match the URL + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 0, + testName + )}/permissions` + ); +}; + +export const passPTNoChecks = () => { + // Type role + cy.get(getElementFromAlias('role-textbox')).type('role0'); + // Set permissions + testPermissions(getTableName(0, testName), 'none'); +}; + +export const passPTCustomChecks = () => { + testPermissions(getTableName(0, testName), 'custom'); +}; + +export const passPTRemovePerms = () => { + queryTypes.forEach(query => { + permRemove(getTableName(0, testName), query); + }); +}; + +export const passPVCreateView = () => { + // create a view + createView(getTableName(1, testName), getTableName(0, testName)); + cy.wait(5000); +}; + +export const passPVPermissions = () => { + // Track the view + trackView(); + // Type role + cy.get(getElementFromAlias('role-textbox')).type('role0'); + // Test permissions + testPermissions(getTableName(1, testName), 'none', true); + testPermissions(getTableName(1, testName), 'custom', true); +}; + +export const passPVRemovePerms = () => { + permRemove(getTableName(1, testName), 'select'); +}; + +export const passPVDeleteView = () => { + // Go to modify view + cy.get(getElementFromAlias('table-modify')).click(); + // Delete view + setPromptValue(getTableName(1, testName)); + cy.get(getElementFromAlias('delete-view')).click(); + cy.window() + .its('prompt') + .should('be.called'); + cy.wait(7000); +}; + +export const passPTDeleteTable = () => { + // Go to the table + cy.get(getElementFromAlias(getTableName(0, testName))).click(); + cy.wait(7000); + // Go to modify table + cy.get(getElementFromAlias('table-modify')).click(); + // Delete table + setPromptValue(getTableName(0, testName)); + cy.get(getElementFromAlias('delete-table')).click(); + cy.window() + .its('prompt') + .should('be.called'); + cy.wait(7000); +}; + +export const setValidationMetaData = () => { + setMetaData(); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/permissions/test.ts b/frontend/apps/console-ce-e2e/src/e2e/data/permissions/test.ts new file mode 100644 index 00000000000..c8fe711674b --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/permissions/test.ts @@ -0,0 +1,50 @@ +import { + passPTCreateTable, + passPTCheckRoute, + passPTNoChecks, + passPTCustomChecks, + passPTRemovePerms, + passPVCreateView, + passPVPermissions, + passPVRemovePerms, + passPVDeleteView, + passPTDeleteTable, +} from './spec'; +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; +import { getIndexRoute } from '../../../helpers/dataHelpers'; + +const setup = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Check Data Tab', () => { + it('Visiting the data URL opens the correct route', () => { + // Visit the index route + cy.visit(getIndexRoute()); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runPermissionsTests = () => { + describe.skip('Permissions', () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + it('Create a table', passPTCreateTable); + it('Create a view', passPVCreateView); + it('Check permission route', passPTCheckRoute); + it('Table No-check permissions work as expected', passPTNoChecks); + it('Table Custom-check permissions work as expected', passPTCustomChecks); + it('Table Permissions removal works as expected', passPTRemovePerms); + it('View permissions work as expected', passPVPermissions); + it('View Permissions removal works as expected', passPVRemovePerms); + it('Delete the views', passPVDeleteView); + it('Delete the test table', passPTDeleteTable); + }); +}; + +if (testMode !== 'cli') { + setup(); + runPermissionsTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/permissions/utils.ts b/frontend/apps/console-ce-e2e/src/e2e/data/permissions/utils.ts new file mode 100644 index 00000000000..733e0dffd74 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/permissions/utils.ts @@ -0,0 +1,158 @@ +import { + getElementFromAlias, + getTableName, + getColName, + queryTypes, + makeDataAPIOptions, +} from '../../../helpers/dataHelpers'; + +import { + validatePermission, + QueryType, + ResultType, + CheckType, +} from '../../validators/validators'; + +const testName = 'perm'; + +export const savePermission = () => { + cy.get(getElementFromAlias('Save-Permissions-button')).click(); + cy.wait(7000); +}; + +export const permNoCheck = (tableName: string, query: QueryType) => { + // click on the query type to edit permission + cy.get(getElementFromAlias(`role0-${query}`)).click(); + cy.get(getElementFromAlias('without-checks')).first().click(); + // set filter { } + // Toggle all columns in case + if (query === 'select' || query === 'update') { + cy.get(getElementFromAlias('toggle-all-col-btn')).click(); + } + if (query === 'insert' || query === 'update') { + cy.get(getElementFromAlias('toggle-presets-permission')).click(); + cy.get(getElementFromAlias('column-presets-column-0')).select( + getColName(0) + ); + cy.get(getElementFromAlias('column-presets-type-0')).select('static'); + cy.get(getElementFromAlias('column-presets-value-0')).type('1').blur(); + cy.get(getElementFromAlias('column-presets-column-1')).select( + getColName(1) + ); + cy.get(getElementFromAlias('column-presets-type-1')).select('session'); + cy.get(getElementFromAlias('column-presets-value-1')).type('user-id'); + } + // Save + savePermission(); + // Validate + validatePermission( + tableName, + 'role0', + query, + 'none', + ResultType.SUCCESS, + null + ); +}; + +export const permCustomCheck = (tableName: string, query: QueryType) => { + // click on the query type to edit permission + cy.get(getElementFromAlias(`role0-${query}`)).click(); + // check the without checks textbox + cy.get(getElementFromAlias('toggle-row-permission')).click(); + cy.get(getElementFromAlias('custom-check')).first().click(); + + cy.get(getElementFromAlias('qb_container')) + .first() + .within(() => { + // Select column + cy.get(getElementFromAlias('qb-select')).first().select(getColName(0)); + // Select operator + cy.get(getElementFromAlias('qb-select')) + .last() + .select(`${getColName(0)}._eq`); + }); + // Set filter to 1 + cy.get(getElementFromAlias('perm-check-textbox')).first().type('1'); + // Save + savePermission(); + // Validate + validatePermission( + tableName, + 'role0', + query, + 'custom', + ResultType.SUCCESS, + [0, 1, 2].map(i => getColName(i)) + ); + // Do not allow users to make upset queries in case of Insert +}; + +export const permRemove = (tableName: string, query: QueryType) => { + // click on the query type to edit permission + cy.get(getElementFromAlias(`role0-${query}`)).click(); + // Remove permission + cy.get(getElementFromAlias('Delete-Permissions-button')).click(); + cy.wait(2500); + cy.wait(5000); + // Validate + validatePermission( + tableName, + 'role0', + query, + 'custom', + ResultType.FAILURE, + null + ); +}; + +export const testPermissions = ( + tableName: string, + check: CheckType, + isView?: boolean +) => { + let allQueryTypes: QueryType[] = queryTypes; + if (isView) { + allQueryTypes = ['select']; + } + + if (check === 'none') { + allQueryTypes.forEach(query => { + permNoCheck(tableName, query); + }); + } else { + allQueryTypes.forEach(query => { + permCustomCheck(tableName, query); + }); + } +}; + +export const trackView = () => { + // track view data-tab-link + cy.get(getElementFromAlias('data-tab-link')).click(); + cy.wait(7000); + cy.get( + getElementFromAlias(`add-track-table-${getTableName(1, testName)}`) + ).click(); + cy.wait(10000); + // Move to permissions + cy.get(getElementFromAlias('table-permissions')).click(); +}; + +export const createView = (viewName: string, tableName: string) => { + const reqBody = { + type: 'run_sql', + args: { + sql: `create view "${viewName}" as select * from "${tableName}"`, + }, + }; + cy.window().then(win => { + const { __env } = win; + const requestOptions = makeDataAPIOptions( + __env.dataApiUrl, + __env.adminSecret, + reqBody + ); + cy.request(requestOptions); + }); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/raw-sql/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/data/raw-sql/spec.ts new file mode 100644 index 00000000000..b61f8f6da0d --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/raw-sql/spec.ts @@ -0,0 +1,96 @@ +import { baseUrl, getElementFromAlias } from '../../../helpers/dataHelpers'; + +let prevStr = ''; + +export const openRawSQL = () => { + // Open RawSQL + cy.get('a').contains('Data').click(); + cy.wait(3000); + cy.get(getElementFromAlias('sql-link')).click(); + cy.wait(3000); + // Match URL + cy.url().should('eq', `${baseUrl}/data/sql`); +}; +const clearText = () => { + cy.get('textarea').type('{selectall}', { force: true }); + cy.get('textarea').trigger('keydown', { + keyCode: 46, + which: 46, + force: true, + }); + cy.wait(2000); // ace editor textarea doesn't expose the value to check, so wait +}; + +export const passCreateTable = () => { + prevStr = 'CREATE TABLE Apic_test_table_rsql (id serial PRIMARY KEY);'; + cy.get('textarea').type(prevStr, { force: true }); + cy.wait(1000); // debounce + cy.get(getElementFromAlias('run-sql')).click(); + // cy.get(getElementFromAlias('raw-sql-statement-timeout')); + cy.wait(5000); +}; + +export const passInsertValues = () => { + clearText(); + // eslint-disable-next-line prefer-spread + const str = Array.apply(null, Array(15)) + .map( + (_, ix: number) => `INSERT INTO Apic_test_table_rsql VALUES (${+ix + 1});` + ) + .join('\n'); + cy.get('textarea').type(str, { force: true }); + cy.wait(1000); + cy.get(getElementFromAlias('run-sql')).click(); + cy.wait(5000); +}; + +export const readQuery = () => { + clearText(); + prevStr = 'SELECT * FROM public.Apic_test_table_rsql;'; + cy.get('textarea').type(prevStr, { force: true }); + cy.wait(1000); // debounce + cy.get(getElementFromAlias('run-sql')).click(); + cy.wait(3000); // debounce + cy.get('div.rt-tr-group').should('have.length', 10); + cy.get('button.-btn').last().click(); + cy.wait(500); + cy.get('div.rt-tr-group').should('have.length', 5); + cy.get('div.rt-td').first().should('have.text', '11'); + cy.get('div.rt-td').last().should('have.text', 'NULL'); +}; +export const passAlterTable = () => { + clearText(); + prevStr = 'ALTER TABLE Apic_test_table_rsql ADD COLUMN name text;'; + cy.get('textarea').type(prevStr, { force: true }); + // Untrack table + cy.wait(1000); + cy.get(getElementFromAlias('raw-sql-track-check')).uncheck(); + cy.get(getElementFromAlias('run-sql')).click(); + cy.wait(5000); +}; + +export const passCreateView = () => { + clearText(); + prevStr = 'CREATE VIEW abcd AS SELECT * FROM Apic_test_table_rsql;'; + cy.get('textarea').type(prevStr, { force: true }); + // Track table + cy.wait(1000); + cy.get(getElementFromAlias('raw-sql-track-check')).check(); + cy.get(getElementFromAlias('run-sql')).click(); + cy.wait(5000); +}; + +export const delTestTables = () => { + clearText(); + prevStr = 'DROP TABLE Apic_test_table_rsql CASCADE;'; + cy.get('textarea').type(prevStr, { force: true }); + cy.wait(1000); + cy.get(getElementFromAlias('raw-sql-migration-check')).uncheck(); + cy.get(getElementFromAlias('run-sql')).click(); + // NOTE: This is only visible, when the console is in CLI mode + cy.get(getElementFromAlias('not-migration-confirm')).click(); + // cy.get(getElementFromAlias('raw-sql-statement-timeout')).type('20', { + // force: true, + // }); + cy.wait(5000); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/raw-sql/test.ts b/frontend/apps/console-ce-e2e/src/e2e/data/raw-sql/test.ts new file mode 100644 index 00000000000..a52ca24697f --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/raw-sql/test.ts @@ -0,0 +1,40 @@ +import { + openRawSQL, + passCreateTable, + delTestTables, + passCreateView, + passInsertValues, + passAlterTable, + readQuery, +} from './spec'; +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; +import { getIndexRoute } from '../../../helpers/dataHelpers'; + +const setup = () => { + describe('Setup route', () => { + it('Visit the index route', () => { + // Visit the index route + cy.visit(getIndexRoute()); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runRawSQLTests = () => { + describe('Raw SQL', () => { + it('Open Raw SQL page', openRawSQL); + it('Pass create table', passCreateTable); + it('Pass insert values', passInsertValues); + it('Pass alter table', passAlterTable); + it('Read from table', readQuery); + it('Pass create view', passCreateView); + it('Delete test table', delTestTables); + }); +}; + +if (testMode !== 'cli') { + setup(); + runRawSQLTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/relationships/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/data/relationships/spec.ts new file mode 100644 index 00000000000..c32addf2ae5 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/relationships/spec.ts @@ -0,0 +1,326 @@ +import { + baseUrl, + getElementFromAlias, + tableColumnTypeSelector, + getIndexRoute, +} from '../../../helpers/dataHelpers'; + +import { + setMetaData, + validateCT, + validateColumn, + ResultType, + TableFields, +} from '../../validators/validators'; +import { setPromptValue } from '../../../helpers/common'; + +const AWAIT_SHORT = 2000; + +const delRel = (table: string, relname: string) => { + cy.get(getElementFromAlias(table)).click(); + cy.get(getElementFromAlias('table-relationships')).click(); + cy.get(getElementFromAlias(`relationship-toggle-editor-${relname}`)).click(); + cy.get(getElementFromAlias(`relationship-remove-${relname}`)).click(); + cy.on('window:alert', str => { + expect(str === 'Are you sure?').to.be.true; + }); + cy.wait(15000); +}; + +export const createTable = (name: string, fields: TableFields) => { + // Click on the "Add table" button and input the table name + cy.visit(getIndexRoute()); + cy.wait(5000); + cy.get(getElementFromAlias('data-create-table')).click(); + cy.url().should('eq', `${baseUrl}/data/default/schema/public/table/add`); + cy.get(getElementFromAlias('tableName')).type(`${name}_table_rt`); + + // Enter column info + let i = 0; + // eslint-disable-next-line no-restricted-syntax + for (const key in fields) { + // eslint-disable-next-line no-prototype-builtins + if (fields.hasOwnProperty(key)) { + cy.get(getElementFromAlias(`column-${i}`)).type(key); + tableColumnTypeSelector(`col-type-${i}`); + cy.get(getElementFromAlias(`data_test_column_type_value_${fields[key]}`)) + .first() + .click(); + i += 1; + } + } + + // Select primary key + cy.get(getElementFromAlias('primary-key-select-0')).select('id'); + + if (name === 'article') { + cy.get(getElementFromAlias('add-table-edit-fk-0')).click(); + cy.get(getElementFromAlias('foreign-key-ref-table-0')).select( + 'author_table_rt' + ); + cy.get(getElementFromAlias('foreign-key-0-lcol-0')).select('3'); + cy.get(getElementFromAlias('foreign-key-0-rcol-0')).select('id'); + cy.get(getElementFromAlias('foreign-key-0-onUpdate-cascade')).check(); + cy.get(getElementFromAlias('foreign-key-0-onDelete-cascade')).check(); + } else if (name === 'comment') { + cy.get(getElementFromAlias('add-table-edit-fk-0')).click(); + cy.get(getElementFromAlias('foreign-key-ref-table-0')).select( + 'author_table_rt' + ); + cy.get(getElementFromAlias('foreign-key-0-lcol-0')).select('1'); + cy.get(getElementFromAlias('foreign-key-0-rcol-0')).select('id'); + cy.get(getElementFromAlias('foreign-key-0-onUpdate-cascade')).check(); + cy.get(getElementFromAlias('foreign-key-0-onDelete-cascade')).check(); + cy.get(getElementFromAlias('add-table-edit-fk-1')).click(); + cy.get(getElementFromAlias('foreign-key-ref-table-1')).select( + 'article_table_rt' + ); + cy.get(getElementFromAlias('foreign-key-1-lcol-0')).select('2'); + cy.get(getElementFromAlias('foreign-key-1-rcol-0')).select('id'); + cy.get(getElementFromAlias('foreign-key-1-onUpdate-cascade')).check(); + cy.get(getElementFromAlias('foreign-key-1-onDelete-cascade')).check(); + } + + cy.get(getElementFromAlias('table-create')).click(); + cy.wait(15000); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${name}_table_rt/modify` + ); + + validateCT(`${name}_table_rt`, ResultType.SUCCESS); +}; + +export const passRTCreateTables = () => { + createTable('author', { id: 'integer', name: 'text' }); + createTable('article', { + id: 'integer', + title: 'text', + Content: 'text', + author_id: 'integer', + rating: 'integer', + }); + createTable('comment', { + id: 'integer', + user_id: 'integer', + article_id: 'integer', + comment: 'text', + }); +}; + +export const Deletetable = (name: string) => { + cy.get(getElementFromAlias(name)).click(); + cy.get(getElementFromAlias('table-modify')).click(); + setPromptValue(name); + cy.get(getElementFromAlias('delete-table')).click(); + cy.window().its('prompt').should('be.called'); + cy.wait(15000); + validateCT(name, ResultType.FAILURE); +}; + +export const passRTDeleteTables = () => { + Deletetable('comment_table_rt'); + Deletetable('article_table_rt'); + Deletetable('author_table_rt'); +}; + +export const passRTAddManualObjRel = () => { + cy.get(getElementFromAlias('article_table_rt')).click(); + cy.get(getElementFromAlias('table-relationships')).click(); + cy.wait(4000); + cy.get(getElementFromAlias('article_table_rt')).click(); + cy.get(getElementFromAlias('table-relationships')).click(); + cy.get(getElementFromAlias('create-edit-manual-rel')).click(); + cy.get(getElementFromAlias('manual-relationship-type')).select('object'); + cy.get("input[placeholder='Enter relationship name']").type('author'); + cy.get(getElementFromAlias('manual-relationship-ref-schema')).select( + 'public' + ); + cy.get(getElementFromAlias('manual-relationship-ref-table')).select( + 'author_table_rt' + ); + cy.get(getElementFromAlias('manual-relationship-lcol-0')).select('author_id'); + cy.get(getElementFromAlias('manual-relationship-rcol-0')).select('id'); + cy.get(getElementFromAlias('create-manual-rel-save')).click(); + cy.wait(15000); + validateColumn( + 'article_table_rt', + ['title', { name: 'author', columns: ['name'] }], + ResultType.SUCCESS + ); +}; + +export const passRTAddManualArrayRel = () => { + cy.get(getElementFromAlias('article_table_rt')).click(); + cy.wait(4000); + cy.get(getElementFromAlias('table-relationships')).click(); + cy.get(getElementFromAlias('create-edit-manual-rel')).click(); + cy.get(getElementFromAlias('manual-relationship-type')).select('array'); + cy.get("input[placeholder='Enter relationship name']").type('comments'); + cy.get(getElementFromAlias('manual-relationship-ref-schema')).select( + 'public' + ); + cy.get(getElementFromAlias('manual-relationship-ref-table')).select( + 'comment_table_rt' + ); + cy.get(getElementFromAlias('manual-relationship-lcol-0')).select('id'); + cy.get(getElementFromAlias('manual-relationship-rcol-0')).select( + 'article_id' + ); + cy.get(getElementFromAlias('create-manual-rel-save')).click(); + cy.wait(15000); + validateColumn( + 'article_table_rt', + ['title', { name: 'comments', columns: ['comment'] }], + ResultType.SUCCESS + ); +}; + +export const passRTDeleteRelationships = () => { + delRel('article_table_rt', 'author'); + validateColumn( + 'article_table_rt', + ['title', { name: 'author', columns: ['name'] }], + ResultType.FAILURE + ); + delRel('article_table_rt', 'comments'); + validateColumn( + 'article_table_rt', + ['title', { name: 'comments', columns: ['comment'] }], + ResultType.FAILURE + ); +}; + +export const passRTAddSuggestedRel = () => { + cy.get(getElementFromAlias('article_table_rt')).click(); + cy.get(getElementFromAlias('table-relationships')).click(); + cy.get(getElementFromAlias('obj-rel-add-0')).click(); + cy.get(getElementFromAlias('suggested-rel-name')).clear().type('author'); + cy.get(getElementFromAlias('obj-rel-save-0')).click(); + cy.wait(5000); + validateColumn( + 'article_table_rt', + ['title', { name: 'author', columns: ['name'] }], + ResultType.SUCCESS + ); + cy.get(getElementFromAlias('article_table_rt')).click(); + cy.wait(5000); + cy.get(getElementFromAlias('table-relationships')).click(); + cy.get(getElementFromAlias('arr-rel-add-0')).click(); + cy.get(getElementFromAlias('suggested-rel-name')).clear().type('comments'); + cy.get(getElementFromAlias('arr-rel-save-0')).click(); + cy.wait(5000); + validateColumn( + 'article_table_rt', + ['title', { name: 'comments', columns: ['comment'] }], + ResultType.SUCCESS + ); +}; + +export const passRTRenameRelationship = () => { + cy.get(getElementFromAlias('relationship-toggle-editor-comments')).click(); + cy.get(getElementFromAlias('relationship-name-input-comments')) + .clear() + .type('comments_renamed'); + cy.get(getElementFromAlias('relationship-save-comments')).click(); + cy.wait(5000); + validateColumn( + 'article_table_rt', + ['title', { name: 'comments_renamed', columns: ['comment'] }], + ResultType.SUCCESS + ); + cy.get( + getElementFromAlias('relationship-toggle-editor-comments_renamed') + ).click(); + cy.get(getElementFromAlias('relationship-name-input-comments_renamed')) + .clear() + .type('comments'); + cy.get(getElementFromAlias('relationship-save-comments_renamed')).click(); + cy.wait(5000); + validateColumn( + 'article_table_rt', + ['title', { name: 'comments', columns: ['comment'] }], + ResultType.SUCCESS + ); +}; + +export const failRTAddSuggestedRel = () => { + cy.get(getElementFromAlias('article_table_rt')).click(); + cy.get(getElementFromAlias('table-relationships')).click(); + cy.get(getElementFromAlias('obj-rel-add-0')).click(); + cy.get(getElementFromAlias('suggested-rel-name')).clear(); + cy.get(getElementFromAlias('obj-rel-save-0')).click(); + cy.wait(15000); + cy.get(getElementFromAlias('suggested-rel-name')).clear().type(`${123123}`); + cy.get('button').contains('Save').click(); + cy.wait(15000); + validateColumn( + 'article_table_rt', + ['title', { name: 'author', columns: ['name'] }], + ResultType.FAILURE + ); + cy.get(getElementFromAlias('article_table_rt')).click(); + cy.get(getElementFromAlias('table-relationships')).click(); + cy.get(getElementFromAlias('obj-rel-add-0')).click(); + cy.get(getElementFromAlias('suggested-rel-name')).clear().type('author'); + cy.get(getElementFromAlias('obj-rel-save-0')).click(); + cy.wait(15000); + validateColumn( + 'article_table_rt', + ['title', { name: 'author', columns: ['name'] }], + ResultType.SUCCESS + ); + cy.get(getElementFromAlias('article_table_rt')).click(); + cy.get(getElementFromAlias('table-relationships')).click(); + cy.get(getElementFromAlias('arr-rel-add-0')).click(); + cy.get(getElementFromAlias('suggested-rel-name')).clear().type('comments'); + cy.get(getElementFromAlias('arr-rel-save-0')).click(); + cy.wait(15000); + delRel('article_table_rt', 'author'); + delRel('article_table_rt', 'comments'); +}; + +export const passRSTSetup = () => { + createTable('countries', { + id: 'integer', + name: 'text', + countryCode: 'text', + }); +}; + +export const passRSTReset = () => { + Deletetable('countries_table_rt'); +}; + +const AWAIT_MINOR = 500; + +export const passRSTAddRSRel = () => { + cy.reload(); + cy.getBySel('countries_table_rt').click(); + cy.getBySel('table-relationships').click(); + cy.getBySel('table-relationship-edit-remote-relationship-add').click(); + cy.wait(AWAIT_MINOR); + cy.getBySel('remote-rel-name-input').type('remote'); + cy.getBySel('remote-rel-schema-input').select('remote_rel_test_rs'); + cy.get('input[type="checkbox"]').eq(3).check(); + cy.wait(AWAIT_MINOR); + cy.get('input[type="checkbox"]').eq(4).check(); + cy.get('select').eq(2).select('countryCode'); + cy.getBySel('table-relationship-remote-relationship-add-save').click(); + cy.get('.notification', { timeout: AWAIT_SHORT }) + .should('be.visible') + .and('contain', 'Successfully created remote relationship'); +}; + +export const passRSTDeleteRSRel = () => { + cy.reload(); + cy.getBySel('table-relationship-edit-remote-relationship-edit').click(); + cy.getBySel('table-relationship-remote-relationship-edit-remove').click(); + cy.get('.notification', { timeout: AWAIT_SHORT }) + .should('be.visible') + .and('contain', 'Successfully deleted remote relationship'); +}; + +export const setValidationMetaData = () => { + setMetaData(); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/relationships/test.ts b/frontend/apps/console-ce-e2e/src/e2e/data/relationships/test.ts new file mode 100644 index 00000000000..289b9b57483 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/relationships/test.ts @@ -0,0 +1,46 @@ +import { + passRTCreateTables, + passRTDeleteTables, + passRTAddManualObjRel, + passRTAddManualArrayRel, + passRTDeleteRelationships, + passRTAddSuggestedRel, + failRTAddSuggestedRel, + passRTRenameRelationship, +} from './spec'; +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; +import { getIndexRoute } from '../../../helpers/dataHelpers'; + +const setup = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Check Data Tab', () => { + it('Clicking on Data tab opens the correct route', () => { + cy.visit(getIndexRoute()); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runRelationshipsTests = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Relationships Tests', () => { + it('Create testing tables', passRTCreateTables); + it('Add Manual Relationship Object', passRTAddManualObjRel); + it('Add Manual Relationship Array', passRTAddManualArrayRel); + it('Delete the relationships', passRTDeleteRelationships); + it('Add Suggested Relationships Error', failRTAddSuggestedRel); + it('Add Suggested Relationships', passRTAddSuggestedRel); + it('Rename relationships', passRTRenameRelationship); + it('Delete the relationships', passRTDeleteRelationships); + it('Delete test tables', passRTDeleteTables); + }); +}; + +if (testMode !== 'cli') { + setup(); + runRelationshipsTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/template-gallery/test.ts b/frontend/apps/console-ce-e2e/src/e2e/data/template-gallery/test.ts new file mode 100644 index 00000000000..2b9bc06ad32 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/template-gallery/test.ts @@ -0,0 +1,34 @@ +import { getDbRoute } from '../../../helpers/dataHelpers'; +import { setMetaData } from '../../validators/validators'; +import { setPromptValue, testMode } from '../../../helpers/common'; + +const setup = () => { + describe('Setup route', () => { + it('Visit the index route', () => { + cy.visit(getDbRoute()); + cy.wait(7000); + setMetaData(); + }); + }); +}; + +export const runSchemaSharingTests = () => { + describe('template gallery', () => { + it('display content', () => { + cy.get('[data-test=table-links]').contains('default').click(); + cy.get('table').contains('Relationships: One-to-One').click(); + cy.contains('Install Template').click(); + cy.wait(1000); + cy.get('[data-test=table-links]').contains('_onetoone').click(); + setPromptValue('_onetoone'); + cy.contains('_onetoone').parent().parent().contains('owner'); + cy.contains('_onetoone').parent().parent().contains('passport_info'); + cy.get('[title="Delete current schema"]').click(); + }); + }); +}; + +if (testMode !== 'cli') { + setup(); + runSchemaSharingTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/views/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/data/views/spec.ts new file mode 100644 index 00000000000..61a9f40f45f --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/views/spec.ts @@ -0,0 +1,415 @@ +import { + getElementFromAlias, + baseUrl, + tableColumnTypeSelector, + getIndexRoute, +} from '../../../helpers/dataHelpers'; + +import { + setMetaData, + validateCT, + createView, + validateColumn, + validateView, + ResultType, + TableFields, +} from '../../validators/validators'; +import { setPromptValue } from '../../../helpers/common'; + +const userId = 5555; + +export const createTable = (name: string, dict: TableFields) => { + cy.url().should('eq', `${baseUrl}/data/default/schema/public/table/add`); + cy.get(getElementFromAlias('tableName')).type(`${name}_table_vt`); + const keys = Object.keys(dict).map(k => k); + const values = Object.keys(dict).map(k => dict[k]); + for (let i = 0; i < keys.length; i += 1) { + cy.get(getElementFromAlias(`column-${i}`)).type(keys[i]); + tableColumnTypeSelector(`col-type-${i}`); + cy.get(getElementFromAlias(`data_test_column_type_value_${values[i]}`)) + .first() + .click(); + } + cy.get(getElementFromAlias('primary-key-select-0')).select('id'); + cy.get(getElementFromAlias('table-create')).click(); + cy.wait(7000); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${name}_table_vt/modify` + ); + + validateCT(`${name}_table_vt`, ResultType.SUCCESS); +}; + +export const passVCreateTables = () => { + cy.get(getElementFromAlias('data-create-table')).click(); + createTable('author', { id: 'integer', name: 'text' }); + cy.visit(getIndexRoute()); + cy.wait(5000); + cy.get(getElementFromAlias('data-create-table')).click(); + createTable('article', { + id: 'integer', + title: 'text', + Content: 'text', + author_id: 'integer', + rating: 'integer', + }); + cy.visit(getIndexRoute()); + cy.wait(5000); + cy.get(getElementFromAlias('data-create-table')).click(); + createTable('comment', { + id: 'integer', + user_id: 'integer', + article_id: 'integer', + comment: 'text', + }); +}; + +export const passVCreateViews = () => { + createView(`CREATE VIEW author_average_rating_vt AS + SELECT author_table_vt.id, avg(article_table_vt.rating) + From author_table_vt, article_table_vt + WHERE author_table_vt.id = article_table_vt.author_id + GROUP BY author_table_vt.id`); +}; + +export const passTrackTable = () => { + cy.visit('/data/default/schema/public/'); + cy.wait(7000); + cy.get( + getElementFromAlias('add-track-table-author_average_rating_vt') + ).click(); + cy.wait(7000); + validateView('author_average_rating_vt', ResultType.SUCCESS); +}; + +export const passViewRoute = () => { + cy.get(getElementFromAlias('author_average_rating_vt')).click(); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/views/author_average_rating_vt/browse` + ); +}; + +type Data = (string | number)[]; +export const passVAddDataarticle = (data: Data, index: number) => { + // Click the Insert Again button. + cy.get('label') + .contains('id') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label').contains('id').next().find('input').last().type(`${data[0]}`); + cy.get('label') + .contains('title') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('title') + .next() + .find('input') + .last() + .type(`${data[1]}`); + cy.get('label') + .contains('Content') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('Content') + .next() + .find('input') + .last() + .type(`${data[2]}`); + cy.get('label') + .contains('author_id') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('author_id') + .next() + .find('input') + .last() + .type(`${data[3]}`); + cy.get('label') + .contains('rating') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('rating') + .next() + .find('input') + .last() + .type(`${data[4]}`); + if (index) { + cy.get(getElementFromAlias('insert-save-button')).click(); + } else { + cy.get(getElementFromAlias('insert-save-button')).click(); + } + + cy.wait(5000); +}; + +export const passVAddDataauthor = (data: Data, index: number) => { + cy.get('label') + .contains('id') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label').contains('id').next().find('input').last().type(`${data[0]}`); + cy.get('label') + .contains('name') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('name') + .next() + .find('input') + .last() + .type(`${data[1]}`); + if (index) { + cy.get(getElementFromAlias('insert-save-button')).click(); + } else { + cy.get(getElementFromAlias('insert-save-button')).click(); + } + cy.wait(5000); +}; + +export const passVAddDatacomment = (data: Data, index: number) => { + cy.get('label') + .contains('id') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label').contains('id').next().find('input').last().type(`${data[0]}`); + cy.get('label') + .contains('user_id') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('user_id') + .next() + .find('input') + .last() + .type(`${data[1]}`); + cy.get('label') + .contains('article_id') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('article_id') + .next() + .find('input') + .last() + .type(`${data[2]}`); + cy.get('label') + .contains('comment') + .next() + .find('input') + .last() + .type('{selectall}{del}'); + cy.get('label') + .contains('comment') + .next() + .find('input') + .last() + .type(`${data[3]}`); + if (index) { + cy.get(getElementFromAlias('insert-save-button')).click(); + } else { + cy.get(getElementFromAlias('insert-save-button')).click(); + } + cy.wait(5000); +}; + +const checkQuerySuccess = () => { + // Expect only 4 rows i.e. expect fifth element to not exist + cy.get('[role=gridcell]').contains(userId); + cy.get('[role=row]').eq(2).should('not.exist'); +}; + +export const passVAddData = () => { + let data; + cy.get(getElementFromAlias('article_table_vt')).click(); + cy.get(getElementFromAlias('table-insert-rows')).click(); + data = [1, 'A', 'Sontent', userId, 4]; + passVAddDataarticle(data, 0); + data = [2, 'B', 'Sontenta', 2, 4]; + passVAddDataarticle(data, 1); + data = [3, 'C', 'Sontentb', userId, 4]; + passVAddDataarticle(data, 2); + cy.get(getElementFromAlias('author_table_vt')).click(); + cy.get(getElementFromAlias('table-insert-rows')).click(); + + data = [userId, 'A']; + passVAddDataauthor(data, 0); + data = [2, 'B']; + passVAddDataauthor(data, 1); + cy.get(getElementFromAlias('comment_table_vt')).click(); + cy.get(getElementFromAlias('table-insert-rows')).click(); + + data = [1, 1, 1, 'new comment']; + passVAddDatacomment(data, 0); + data = [2, 2, 2, 'new comment']; + passVAddDatacomment(data, 1); + data = [3, 1, 2, 'new comment']; + passVAddDatacomment(data, 2); +}; + +export const passVFilterQueryEq = () => { + // Select column with type `text` + cy.get('select') + .find('option') + .contains('-- column --') + .parent() + .first() + .select('id'); + // Type value as `filter-text` + cy.get("input[placeholder='-- value --']").last().type(`${userId}`); + // Run query + cy.get(getElementFromAlias('run-query')).click(); + cy.wait(5000); + // Check if the query was successful + checkQuerySuccess(); +}; + +const checkOrder = (order: string) => { + // Utility function to get right element + if (order === 'asc') { + cy.get('[role=row]').each(($el, index) => { + if (index === 1) { + cy.wrap($el).find('[role=gridcell]').first().next().next().contains(2); + } + if (index === 2) { + cy.wrap($el) + .find('[role=gridcell]') + .first() + .next() + .next() + .contains(userId); + } + }); + } else { + cy.get('[role=row]').each(($el, index) => { + if (index === 2) { + cy.wrap($el).find('[role=gridcell]').first().next().next().contains(2); + } + if (index === 1) { + cy.wrap($el) + .find('[role=gridcell]') + .first() + .next() + .next() + .contains(userId); + } + }); + } +}; + +export const passVAscendingSort = () => { + cy.wait(7000); + // Select column with type 'serial' + cy.get('select') + .find('option') + .contains('-- column --') + .parent() + .last() + .select('id'); + // Run query + cy.get(getElementFromAlias('run-query')).click(); + // Check order + checkOrder('asc'); +}; + +export const passModifyView = () => { + cy.get(getElementFromAlias('table-modify')).click(); + cy.get('button').contains('Modify').last().click(); + cy.url().should('eq', `${baseUrl}/data/sql`); +}; + +export const passVAddManualObjRel = () => { + cy.get(getElementFromAlias('author_average_rating_vt')).click(); + cy.wait(2000); + cy.get(getElementFromAlias('table-relationships')).click(); + cy.wait(2000); + cy.get(getElementFromAlias('create-edit-manual-rel')).click(); + cy.get(getElementFromAlias('manual-relationship-type')).select('object'); + cy.get("input[placeholder='Enter relationship name']").type('author'); + cy.get(getElementFromAlias('manual-relationship-ref-schema')).select( + 'public' + ); + cy.get(getElementFromAlias('manual-relationship-ref-table')).select( + 'author_table_vt' + ); + cy.get(getElementFromAlias('manual-relationship-lcol-0')).select('id'); + cy.get(getElementFromAlias('manual-relationship-rcol-0')).select('id'); + cy.get(getElementFromAlias('create-manual-rel-save')).click(); + cy.wait(7000); + validateColumn( + 'author_average_rating_vt', + ['avg', { name: 'author', columns: ['name'] }], + ResultType.SUCCESS + ); +}; + +export const passVDeleteRelationships = () => { + cy.get(getElementFromAlias('author_average_rating_vt')).click(); + cy.get(getElementFromAlias('table-relationships')).click(); + cy.get(getElementFromAlias('relationship-toggle-editor-author')).click(); + cy.get(getElementFromAlias('relationship-remove-author')).click(); + cy.on('window:alert', str => { + return expect(str === 'Are you sure?').to.be.true; + }); + cy.wait(7000); + validateColumn( + 'author_average_rating_vt', + ['avg', { name: 'author', columns: ['name'] }], + ResultType.FAILURE + ); +}; + +export const passVDeleteView = () => { + cy.get(getElementFromAlias('table-modify')).click(); + setPromptValue('author_average_rating_vt'); + cy.get(getElementFromAlias('delete-view')).click(); + cy.window().its('prompt').should('be.called'); + cy.wait(7000); + validateView('author_average_rating_vt', ResultType.FAILURE); +}; + +export const deleteTable = (name: string) => { + cy.get(getElementFromAlias(name)).click(); + cy.get(getElementFromAlias('table-modify')).click(); + setPromptValue(name); + cy.get(getElementFromAlias('delete-table')).click(); + cy.window().its('prompt').should('be.called'); + cy.wait(7000); + validateCT(name, ResultType.FAILURE); + cy.wait(7000); +}; + +export const passVDeleteTables = () => { + deleteTable('comment_table_vt'); + deleteTable('article_table_vt'); + deleteTable('author_table_vt'); +}; + +export const setValidationMetaData = () => { + setMetaData(); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/data/views/test.ts b/frontend/apps/console-ce-e2e/src/e2e/data/views/test.ts new file mode 100644 index 00000000000..7d489c53627 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/data/views/test.ts @@ -0,0 +1,57 @@ +import { + passVCreateTables, + passVCreateViews, + passVAddData, + passTrackTable, + passVAddManualObjRel, + passVAscendingSort, + passVFilterQueryEq, + passViewRoute, + passModifyView, + passVDeleteRelationships, + passVDeleteView, + passVDeleteTables, +} from './spec'; +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; +import { getIndexRoute } from '../../../helpers/dataHelpers'; + +const setup = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Setup route', () => { + it('Visit the index route', () => { + // Visit the index route + cy.visit(getIndexRoute()); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runViewsTest = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Views', () => { + // NOTE: Ideally, we should be adding "should" at the beginning of + // the test descriptions. It will sound like this when you read it. + // eg. it should create test tables ...and so on + it('Create Tables', passVCreateTables); + it('Insert test data to table(s)', passVAddData); + it('Create View', passVCreateViews); + it('Add View to comment table', passTrackTable); + it('Visit the view route', passViewRoute); + it('Order Ascending order View Table', passVAscendingSort); + it('Apply Filters on the View', passVFilterQueryEq); + it('Modify the View', passModifyView); + it('Add Object Relationship to View', passVAddManualObjRel); + it('Delete Relationship(s)', passVDeleteRelationships); + it('Delete View', passVDeleteView); + it('Delete Tables', passVDeleteTables); + }); +}; + +if (testMode !== 'cli') { + setup(); + runViewsTest(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/events/create-trigger/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/events/create-trigger/spec.ts new file mode 100644 index 00000000000..9be2734a3ca --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/events/create-trigger/spec.ts @@ -0,0 +1,409 @@ +import { + getElementFromAlias, + getTableName, + getTriggerName, + getWebhookURL, + getNoOfRetries, + getIntervalSeconds, + getTimeoutSeconds, + baseUrl, +} from '../../../helpers/eventHelpers'; +import { + getColName, + tableColumnTypeSelector, +} from '../../../helpers/dataHelpers'; +import { + setMetaData, + validateCT, + validateInsert, + ResultType, + validateCTrigger, +} from '../../validators/validators'; +import { setPromptValue } from '../../../helpers/common'; +import { + toggleRequestTransformSection, + togglePayloadTransformSection, + typeIntoRequestQueryParams, + typeIntoRequestUrl, + typeIntoTransformBody, + checkTransformRequestUrlError, + checkTransformRequestBodyError, + checkTransformRequestUrlPreview, + clearPayloadTransformBody, + clearRequestUrl, +} from '../../../helpers/webhookTransformHelpers'; + +const AWAIT_SHORT = 2000; +const AWAIT_LONG = 7000; + +const EVENT_REQUEST_BODY_TRANSFORM_TEXTAREA = 1; + +const EVENT_TRIGGER_INDEX_ROUTE = '/events/data'; + +const testName = 'ctr'; // create trigger + +const statements = { + createTransformIncorrectPayloadBody: ` + { + "tableInfo": { + "name": {{$table.name}} + `, + createTransformPayloadBody: ` + { + "tableInfo": { + "name": {{$body.table.name}}, + "schema": {{$body.table.schema}}, + "trigger": {{$body.trigger.name}} + `, +}; + +const createETForm = (allCols: boolean) => { + // Set trigger name and select table + cy.getBySel('trigger-name').clear().type(getTriggerName(0, testName)); + cy.getBySel('select-source').select('default'); + cy.wait(500); + cy.getBySel('select-schema').select('public'); + cy.getBySel('select-table').select(getTableName(0, testName)); + + // operations + cy.getBySel('insert-operation').check(); + cy.getBySel('update-operation').check(); + cy.getBySel('delete-operation').check(); + + // webhook url + cy.getBySel('webhook-input').clear().type(getWebhookURL()); + + // advanced settings + cy.getBySel('advanced-settings').click(); + if (!allCols) { + cy.getBySel('choose-column').click(); + cy.getBySel('select-column').first().click(); + } + + // retry configuration + cy.getBySel('no-of-retries').clear().type(getNoOfRetries()); + cy.getBySel('interval-seconds').clear().type(getIntervalSeconds()); + cy.getBySel('timeout-seconds').clear().type(getTimeoutSeconds()); +}; + +export const passPTCreateTable = () => { + // Click on create table + cy.getBySel('data-create-table').click(); + // Match the URL + cy.url().should('eq', `${baseUrl}/data/default/schema/public/table/add`); + // Type table name + cy.getBySel('tableName').type(getTableName(0, testName)); + // Set first column + cy.getBySel('column-0').type(getColName(0)); + tableColumnTypeSelector('col-type-0'); + cy.getBySel('data_test_column_type_value_serial').first().click(); + // cy.getBySel('col-type-0')).select('serial'); + // Set second column + cy.getBySel('column-1').type(getColName(1)); + tableColumnTypeSelector('col-type-1'); + cy.getBySel('data_test_column_type_value_integer').first().click(); + + // cy.getBySel('col-type-1')).select('integer'); + // Set third column + cy.getBySel('column-2').type(getColName(2)); + tableColumnTypeSelector('col-type-2'); + cy.getBySel('data_test_column_type_value_text').first().click(); + // cy.getBySel('col-type-2')).select('text'); + // Set primary key + cy.getBySel('primary-key-select-0').select('0'); + // Create + cy.getBySel('table-create').click(); + cy.wait(7000); + cy.url().should( + 'eq', + `${baseUrl}/data/default/schema/public/tables/${getTableName( + 0, + testName + )}/modify` + ); +}; + +export const checkCreateTriggerRoute = () => { + // Click on the create trigger button + cy.visit(`${EVENT_TRIGGER_INDEX_ROUTE}/manage`); + cy.wait(4000); + cy.visit(EVENT_TRIGGER_INDEX_ROUTE); + cy.wait(15000); + cy.getBySel('data-sidebar-add').click(); + // Match the URL + cy.url().should('eq', `${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/add`); +}; + +export const failCTWithoutData = () => { + // Type trigger name + cy.getBySel('trigger-name').type(getTriggerName(0, testName)); + // Click on create + cy.getBySel('trigger-create').click(); + // Check if the route didn't change + cy.url().should('eq', `${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/add`); + // Validate + validateCT(getTriggerName(0, testName), ResultType.FAILURE); +}; + +export const passCT1 = () => { + // select choose column from the radio input + const allCols = false; + createETForm(allCols); + + // Click on create + cy.getBySel('trigger-create').click(); + cy.wait(10000); + // Check if the trigger got created and navigated to processed events page + cy.url().should( + 'eq', + `${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/${getTriggerName( + 0, + testName + )}/modify` + ); + cy.getBySel(getTriggerName(0, testName)); + // Validate + validateCTrigger( + getTriggerName(0, testName), + getTableName(0, testName), + 'public', + ResultType.SUCCESS, + allCols + ); +}; + +export const passCT2 = () => { + cy.getBySel('data-sidebar-add').click(); + // select all columns from the radio input + const allCols = true; + createETForm(allCols); + + // Click on create + cy.getBySel('trigger-create').click(); + cy.wait(10000); + // Check if the trigger got created and navigated to processed events page + cy.url().should( + 'eq', + `${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/${getTriggerName( + 0, + testName + )}/modify` + ); + cy.getBySel(getTriggerName(0, testName)); + // Validate + validateCTrigger( + getTriggerName(0, testName), + getTableName(0, testName), + 'public', + ResultType.SUCCESS, + allCols + ); +}; + +export const failCTDuplicateTrigger = () => { + // Visit create trigger page + cy.visit(`${EVENT_TRIGGER_INDEX_ROUTE}/add`); + // trigger and table name + cy.getBySel('trigger-name').clear().type(getTriggerName(0, testName)); + cy.getBySel('select-source').select('default'); + cy.getBySel('select-schema').select('public'); + cy.getBySel('select-table').select(getTableName(0, testName)); + + // operations + cy.getBySel('insert-operation').check(); + cy.getBySel('update-operation').check(); + cy.getBySel('delete-operation').check(); + + // webhook url + cy.getBySel('webhook-input').clear().type(getWebhookURL()); + + // FIXME: Commenting this for now. Uncomment once, the server issue is fixed. + + // click on create + // cy.getBySel('trigger-create')).click(); + // cy.wait(5000); + // should be on the same URL + // cy.url().should('eq', `${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/add`); + cy.visit(`${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/add`); + cy.wait(4000); +}; + +export const insertTableRow = () => { + // visit insert row page + cy.visit( + `/data/default/schema/public/tables/${getTableName(0, testName)}/insert` + ); + // one serial column. so insert a row directly. + cy.getBySel(`typed-input-${1}`).type('123'); + cy.getBySel(`typed-input-${2}`).type('Some text'); + cy.getBySel('insert-save-button').click(); + cy.wait(300); + validateInsert(getTableName(0, testName), 1); + // now it should invoke the trigger to webhook + cy.wait(10000); + // check if processed events has a row and it is a successful response + cy.visit( + `${EVENT_TRIGGER_INDEX_ROUTE}/${getTriggerName(0, testName)}/processed` + ); + cy.wait(10000); + cy.get('.rt-tr-group').should('have.length.gte', 1); +}; + +export const deleteCTTestTrigger = () => { + // Go to the settings section of the trigger + cy.visit( + `${EVENT_TRIGGER_INDEX_ROUTE}/${getTriggerName(0, testName)}/processed` + ); + // click on settings tab + cy.getBySel('trigger-modify').click(); + setPromptValue(getTriggerName(0, testName)); + // Click on delete + cy.getBySel('delete-trigger').click(); + // Confirm + cy.window().its('prompt').should('be.called'); + cy.wait(7000); + // Match the URL + cy.url().should('eq', `${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/manage`); + // Validate + validateCTrigger( + getTriggerName(0, testName), + getTableName(0, testName), + 'public', + ResultType.FAILURE + ); +}; + +export const deleteCTTestTable = () => { + // Go to the modify section of the table + cy.visit( + `/data/default/schema/public/tables/${getTableName(0, testName)}/browse` + ); + cy.get(getElementFromAlias('table-modify'), { timeout: 5000 }).click(); + // Click on delete + setPromptValue(getTableName(0, testName)); + cy.getBySel('delete-table').click(); + // Confirm + cy.window().its('prompt').should('be.called'); + cy.get(getElementFromAlias('data-create-table'), { timeout: 5000 }).should( + 'exist' + ); + + // Match the URL + cy.url().should('eq', `${baseUrl}/data/default/schema/public`); + // Validate + validateCT(getTableName(0, testName), ResultType.FAILURE); +}; + +export const setValidationMetaData = () => { + setMetaData(); +}; + +export const clearHandler = () => { + cy.getBySel('webhook-input').clear(); +}; + +export const createEtTransform = () => { + // open create event trigger form + cy.getBySel('data-sidebar-add').click(); + // Match the URL + cy.url().should('eq', `${baseUrl}${EVENT_TRIGGER_INDEX_ROUTE}/add`); + + // fill up the basic event trigger form + createETForm(true); + + // open request transform section + toggleRequestTransformSection(); + cy.wait(AWAIT_SHORT); + cy.getBySel('transform-POST').click(); + + // give correct body without webhook handler + clearHandler(); + typeIntoRequestUrl('{{$base_url}}'); + cy.wait(AWAIT_SHORT); + // check for error + checkTransformRequestUrlError( + true, + 'Please configure your webhook handler to generate request url transform' + ); + + // clear handler + clearHandler(); + // type into handler + cy.getBySel('webhook-input').type(getWebhookURL()); + + // give incorrect body + clearRequestUrl(); + typeIntoRequestUrl('{{$url}}/users'); + cy.wait(AWAIT_SHORT); + // check for error + checkTransformRequestUrlError(true); + + // give correct body + clearRequestUrl(); + typeIntoRequestUrl('/{{$body.trigger.name}}'); + typeIntoRequestQueryParams([ + { key: 'id', value: '5' }, + { key: 'tableName', value: '{{$body.table.name}}' }, + ]); + cy.wait(AWAIT_SHORT); + // check there is no error + checkTransformRequestUrlError(false); + // check the preview is correctly shown + checkTransformRequestUrlPreview( + 'http://httpbin.org/post/Apic_test_trigger_ctr_0?id=5&tableName=Apic_test_table_ctr_0' + ); + + // open payload transform section + togglePayloadTransformSection(); + // give incorrect body + clearPayloadTransformBody(EVENT_REQUEST_BODY_TRANSFORM_TEXTAREA); + typeIntoTransformBody( + statements.createTransformIncorrectPayloadBody, + EVENT_REQUEST_BODY_TRANSFORM_TEXTAREA + ); + cy.wait(AWAIT_SHORT); + checkTransformRequestBodyError(true); + + // give correct body + clearPayloadTransformBody(EVENT_REQUEST_BODY_TRANSFORM_TEXTAREA); + typeIntoTransformBody( + statements.createTransformPayloadBody, + EVENT_REQUEST_BODY_TRANSFORM_TEXTAREA + ); + cy.wait(AWAIT_SHORT); + checkTransformRequestBodyError(false); + + // Click on create + cy.getBySel('trigger-create').click(); + // Check if the trigger got created + cy.wait(AWAIT_SHORT); + cy.get('.notification', { timeout: AWAIT_LONG }) + .should('be.visible') + .and('contain', 'Event Trigger Created'); +}; + +export const modifyEtTransform = () => { + cy.getBySel('transform-GET').click(); + cy.getBySel('transform-requestUrl') + .clear() + .type('/{{$body.trigger.name}}', { parseSpecialCharSequences: false }); + cy.getBySel('transform-query-params-kv-remove-button-0').click(); + togglePayloadTransformSection(); + cy.getBySel('save-modify-trigger-changes').click(); + cy.wait(AWAIT_SHORT); + cy.get('.notification', { timeout: AWAIT_LONG }) + .should('be.visible') + .and('contain', 'Saved'); +}; + +export const deleteEtTransform = () => { + setPromptValue(getTriggerName(0, testName)); + // Click on delete + cy.getBySel('delete-trigger').click(); + // Confirm + cy.window().its('prompt').should('be.called'); + cy.wait(AWAIT_SHORT); + cy.get('.notification', { timeout: AWAIT_LONG }) + .should('be.visible') + .and('contain', 'Deleted event trigger'); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/events/create-trigger/test.ts b/frontend/apps/console-ce-e2e/src/e2e/events/create-trigger/test.ts new file mode 100644 index 00000000000..23a46c8d0cc --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/events/create-trigger/test.ts @@ -0,0 +1,59 @@ +/* eslint no-unused-vars: 0 */ +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; + +import { + checkCreateTriggerRoute, + failCTWithoutData, + passCT2, + failCTDuplicateTrigger, + insertTableRow, + deleteCTTestTrigger, + deleteCTTestTable, + passPTCreateTable, + passCT1, + createEtTransform, + modifyEtTransform, + deleteEtTransform, +} from './spec'; +import { getIndexRoute } from '../../../helpers/dataHelpers'; + +const setup = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Check Data Tab', () => { + it('Clicking on Data tab opens the correct route', () => { + // Visit the index route + cy.visit(getIndexRoute()); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runCreateTriggerTests = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Create Trigger', () => { + it('Create test table', passPTCreateTable); + it( + 'Create trigger button opens the correct route', + checkCreateTriggerRoute + ); + it('Fails to create trigger without data', failCTWithoutData); + it('Successfuly creates trigger with selected columns for update', passCT1); + it('Successfuly creates trigger with all columns for update', passCT2); + it('Fails to create duplicate trigger', failCTDuplicateTrigger); + it('Insert a row and invoke trigger', insertTableRow); + it("Delete's the test trigger", deleteCTTestTrigger); + it('Create Event Trigger With Transform', createEtTransform); + it('Update Event Trigger With Transform', modifyEtTransform); + it('Delete Event Trigger With Transform', deleteEtTransform); + it("Delete's the test table", deleteCTTestTable); + }); +}; + +if (testMode !== 'cli') { + setup(); + runCreateTriggerTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/events/one-off-trigger/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/events/one-off-trigger/spec.ts new file mode 100644 index 00000000000..b8ca0d4bf12 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/events/one-off-trigger/spec.ts @@ -0,0 +1,97 @@ +import { + getElementFromAlias, + getTableName, + getTriggerName, + getWebhookURL, + getNoOfRetries, + getIntervalSeconds, + getTimeoutSeconds, + baseUrl, +} from '../../../helpers/eventHelpers'; +import { validateCTrigger, ResultType } from '../../validators/validators'; + +const EVENT_TRIGGER_INDEX_ROUTE = '/events/data'; + +export const checkCreateOneOffTriggerRoute = () => { + // Click on the one-off scheduled events + cy.visit(`${EVENT_TRIGGER_INDEX_ROUTE}/manage`); + cy.wait(4000); + cy.visit(EVENT_TRIGGER_INDEX_ROUTE); + cy.wait(15000); + cy.get(getElementFromAlias('one-off-trigger')).click(); + // Match the URL + cy.url().should('eq', `${baseUrl}/events/one-off-scheduled-events/info`); +}; + +export const scheduleOneoffEvent = () => { + // click on the schedule event tab + cy.visit('/events/one-off-scheduled-events/add'); + cy.url().should('eq', `${baseUrl}/events/one-off-scheduled-events/add`); + // webhook url + cy.get(getElementFromAlias('one-off-webhook')).type(getWebhookURL()); + // advanced settings + cy.get(getElementFromAlias('event-advanced-configuration')).click(); + // retry configuration + cy.get(getElementFromAlias('no-of-retries')).clear().type(getNoOfRetries()); + cy.get(getElementFromAlias('interval-seconds')) + .clear() + .type(getIntervalSeconds()); + cy.get(getElementFromAlias('timeout-seconds')) + .clear() + .type(getTimeoutSeconds()); + + // Click on create + cy.get(getElementFromAlias('create-schedule-event')).click(); + cy.wait(10000); + // Check if the trigger got created and navigated to processed events page + cy.url().should('eq', `${baseUrl}/events/one-off-scheduled-events/pending`); + validateCTrigger( + getTriggerName(0), + getTableName(0), + 'public', + ResultType.SUCCESS + ); +}; + +export const expandOneOffPendingEvent = () => { + // expand button + cy.get(getElementFromAlias('expand-event')).click(); + cy.wait(4000); + // expand recent invocation + cy.get(getElementFromAlias('expand-event')).click(); + cy.wait(4000); + // collaspe + cy.get(getElementFromAlias('collapse-event')).first().click(); + cy.wait(4000); +}; + +export const expandOneOffProcessedEvent = () => { + // processed events tab + cy.get( + getElementFromAlias('adhoc-events-container-tabs-events-processed') + ).click(); + cy.url().should('eq', `${baseUrl}/events/one-off-scheduled-events/processed`); + // expand processed event + cy.get(getElementFromAlias('expand-event')).first().click(); + cy.wait(4000); + // expand recent invocation + cy.get(getElementFromAlias('expand-event')).first().click(); + cy.wait(4000); + // collaspe + cy.get(getElementFromAlias('collapse-event')).first().click(); + cy.wait(4000); +}; + +export const expandOneOffLogs = () => { + // invocation logs tab + cy.get( + getElementFromAlias('adhoc-events-container-tabs-events-logs') + ).click(); + cy.url().should('eq', `${baseUrl}/events/one-off-scheduled-events/logs`); + // expand logs + cy.get(getElementFromAlias('expand-event')).first().click(); + cy.wait(4000); + // expand recent invocation + cy.get(getElementFromAlias('collapse-event')).click(); + cy.wait(4000); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/events/one-off-trigger/test.ts b/frontend/apps/console-ce-e2e/src/e2e/events/one-off-trigger/test.ts new file mode 100644 index 00000000000..8402ed9de4d --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/events/one-off-trigger/test.ts @@ -0,0 +1,40 @@ +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; + +import { getIndexRoute } from '../../../helpers/dataHelpers'; +import { + checkCreateOneOffTriggerRoute, + expandOneOffLogs, + expandOneOffPendingEvent, + expandOneOffProcessedEvent, + scheduleOneoffEvent, +} from './spec'; + +const setup = () => { + describe('Check Data Tab', () => { + it('Clicking on Data tab opens the correct route', () => { + // Visit the index route + cy.visit(getIndexRoute()); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runCreateOneOffTriggerTests = () => { + describe('Create One-off Trigger', () => { + it( + 'One-off trigger button opens the correct route', + checkCreateOneOffTriggerRoute + ); + it('schedule an event', scheduleOneoffEvent); + it('will expand the pending events', expandOneOffPendingEvent); + it('will expand the processed evenets', expandOneOffProcessedEvent); + it('will exapnd the invocation logs', expandOneOffLogs); + }); +}; + +if (testMode !== 'cli') { + setup(); + runCreateOneOffTriggerTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/create-remote-schema/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/create-remote-schema/spec.ts new file mode 100644 index 00000000000..b10d9e212df --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/create-remote-schema/spec.ts @@ -0,0 +1,279 @@ +import { + getElementFromAlias, + baseUrl, + getRemoteSchemaName, + getInvalidRemoteSchemaUrl, + getRemoteGraphQLURL, + getRemoteGraphQLURLFromEnv, + getRemoteSchemaRoleName, +} from '../../../helpers/remoteSchemaHelpers'; + +import { validateRS, ResultType } from '../../validators/validators'; +import { setPromptValue } from '../../../helpers/common'; + +const testName = 'rs'; + +export const checkCreateRemoteSchemaRoute = () => { + cy.visit('/remote-schemas/manage/schemas', { + onBeforeLoad(win) { + cy.stub(win, 'prompt').returns('DELETE'); + }, + }); + + cy.get(getElementFromAlias('data-create-remote-schemas')).click(); + cy.url().should('eq', `${baseUrl}/remote-schemas/manage/add`); +}; + +export const failRSWithInvalidRemoteUrl = () => { + + cy.get('[data-testid=name]').type( + getRemoteSchemaName(0, testName) + ); + cy.get('[data-testid=url]').type( + getInvalidRemoteSchemaUrl() + ); + + cy.get('[data-testid=submit]').click(); + cy.get('.notifications-wrapper').contains('Error'); + +}; + +export const createSimpleRemoteSchema = () => { + cy.get('[data-testid=name]') + .clear() + .type(getRemoteSchemaName(1, testName)); + cy.get('[data-testid=url]') + .clear() + .type(getRemoteGraphQLURL()); + cy.get('[data-testid=submit]').click(); + cy.get('.notifications-wrapper').contains('Success'); + validateRS(getRemoteSchemaName(1, testName), ResultType.SUCCESS); + cy.url().should( + 'eq', + `${baseUrl}/remote-schemas/manage/${getRemoteSchemaName( + 1, + testName + )}/details` + ); +}; + +export const failRSDuplicateSchemaName = () => { + cy.visit('remote-schemas/manage/add'); + cy.get('[data-testid=name]') + .clear() + .type(getRemoteSchemaName(1, testName)); + cy.get('[data-testid=url]') + .clear() + .type(getRemoteGraphQLURL()); + cy.get('[data-testid=submit]').click(); + cy.get('.notifications-wrapper').contains('Error'); + cy.url().should('eq', `${baseUrl}/remote-schemas/manage/add`); +}; + +export const failRSDuplicateSchemaNodes = () => { + cy.visit('remote-schemas/manage/add'); + cy.get('[data-testid=name]') + .clear() + .type(getRemoteSchemaName(2, testName)); + cy.get('[data-testid=url]') + .clear() + .type(getRemoteGraphQLURL()); + cy.get('[data-testid=submit]').click(); + cy.get('.notifications-wrapper').contains('Error'); + cy.url().should('eq', `${baseUrl}/remote-schemas/manage/add`); +}; + +export const deleteSimpleRemoteSchemaFailUserConfirmationError = () => { + cy.visit(`remote-schemas/manage/${getRemoteSchemaName(1, testName)}/details`); + + cy.get(getElementFromAlias('remote-schemas-modify')).click(); + setPromptValue(null); + cy.get(getElementFromAlias('remote-schema-edit-delete-btn')).click(); + cy.window().its('prompt').should('be.called'); + + cy.url().should( + 'eq', + `${baseUrl}/remote-schemas/manage/${getRemoteSchemaName( + 1, + testName + )}/modify` + ); +}; + +export const deleteSimpleRemoteSchema = () => { + cy.visit(`remote-schemas/manage/${getRemoteSchemaName(1, testName)}/details`); + + cy.get(getElementFromAlias('remote-schemas-modify')).click(); + setPromptValue(getRemoteSchemaName(1, testName)); + cy.get(getElementFromAlias('remote-schema-edit-delete-btn')).click(); + cy.window().its('prompt').should('be.called'); + cy.get(getElementFromAlias('delete-confirmation-error')).should('not.exist'); +}; + +export const failWithRemoteSchemaEnvUrl = () => { + cy.visit('remote-schemas/manage/add'); + cy.get('[data-testid=name]') + .clear() + .type(getRemoteSchemaName(3, testName)); + cy.get( + '[name="url.type"]' + ).select('from_env'); + cy.get('[data-testid=url]') + .clear() + .type(getRemoteGraphQLURLFromEnv()); + cy.get('[data-testid=submit]').click(); + cy.get('.notifications-wrapper').contains('Error'); + cy.url().should('eq', `${baseUrl}/remote-schemas/manage/add`); +}; + +export const failWithRemoteSchemaEnvHeader = () => { + cy.visit('remote-schemas/manage/add'); + cy.get('[data-testid=name]') + .clear() + .type(getRemoteSchemaName(3, testName)); + cy.get('[data-testid=url]') + .clear() + .type(getRemoteGraphQLURL()); + + cy.get('[data-testid="add-header') + .click() + + cy.get('[name="headers[0].name"]') + .clear() + .type('sampleHeader1'); + + cy.get('[name="headers[0].value"]') + .clear() + .type('sampleHeaderValue1'); + + cy.get('[data-testid="add-header') + .click() + + cy.get('[name="headers[1].name"]') + .clear() + .type('sampleHeader2'); + + cy.get( + '[name="headers[1].type"]' + ).select("from_env"); + + cy.get('[name="headers[1].value"]') + .clear() + .type('SAMPLE_ENV_HEADER'); + + cy.get('[data-testid=submit]').click(); + cy.get('.notifications-wrapper').contains('Error'); + cy.url().should('eq', `${baseUrl}/remote-schemas/manage/add`); +}; + +export const passWithRemoteSchemaHeader = () => { + cy.visit('remote-schemas/manage/add'); + cy.get('[data-testid=name]') + .clear() + .type(getRemoteSchemaName(3, testName)); + cy.get('[data-testid=url]') + .clear() + .type(getRemoteGraphQLURL()); + + cy.get('[data-testid="add-header') + .click() + + cy.get('[name="headers[0].name"]') + .clear() + .type('sampleHeader1'); + + cy.get('[name="headers[0].value"]') + .clear() + .type('sampleHeaderValue1'); + + cy.get('[data-testid="add-header') + .click() + + cy.get('[name="headers[1].name"]') + .clear() + .type('sampleHeader2'); + + cy.get('[name="headers[1].value"]') + .clear() + .type('sampleHeaderValue2'); + + cy.get('[data-testid=submit]').click(); + cy.get('.notifications-wrapper').contains('Success'); + validateRS(getRemoteSchemaName(3, testName), ResultType.SUCCESS); + cy.url().should( + 'eq', + `${baseUrl}/remote-schemas/manage/${getRemoteSchemaName( + 3, + testName + )}/details` + ); +}; + +export const deleteRemoteSchema = () => { + cy.visit(`remote-schemas/manage/${getRemoteSchemaName(3, testName)}/details`); + + cy.get(getElementFromAlias('remote-schemas-modify')).click(); + setPromptValue(getRemoteSchemaName(3, testName)); + cy.get(getElementFromAlias('remote-schema-edit-delete-btn')).click(); + cy.window().its('prompt').should('be.called'); + cy.get(getElementFromAlias('delete-confirmation-error')).should('not.exist'); +}; + +export const visitRemoteSchemaPermissionsTab = () => { + cy.visit( + `${baseUrl}/remote-schemas/manage/${getRemoteSchemaName( + 1, + testName + )}/permissions` + ); +}; + +export const createSimpleRemoteSchemaPermission = () => { + cy.get(getElementFromAlias('role-textbox')) + .clear() + .type(getRemoteSchemaRoleName(1, testName)); + cy.get( + getElementFromAlias(`${getRemoteSchemaRoleName(1, testName)}-Permission`) + ).click(); + cy.get(getElementFromAlias('field-__query_root')).click(); + cy.get(getElementFromAlias('checkbox-query')).click(); + cy.get(getElementFromAlias('save-remote-schema-permissions')).click({ + force: true, + }); + cy.get('.notifications-wrapper').contains('saved') + cy.url().should( + 'eq', + `${baseUrl}/remote-schemas/manage/${getRemoteSchemaName( + 1, + testName + )}/permissions` + ); + cy.get(getElementFromAlias('role-test-role-rs-1')).should('be.visible'); +}; + +export const passWithUpdateRemoteSchema = () => { + cy.visit( + `${baseUrl}/remote-schemas/manage/${getRemoteSchemaName( + 3, + testName + )}/modify` + ); + cy.get(getElementFromAlias('remote-schema-schema-name')).should( + 'have.attr', + 'disabled' + ); + cy.get(getElementFromAlias('remote-schema-comment')) + .clear() + .type('This is a new remote schema comment'); + + cy.get(getElementFromAlias('remote-schema-edit-save-btn')).click(); + cy.get('.notifications-wrapper').contains('modified'); + validateRS(getRemoteSchemaName(3, testName), ResultType.SUCCESS); + + cy.get(getElementFromAlias('remote-schemas-modify')).click(); + cy.get(getElementFromAlias('remote-schema-schema-name')).should( + 'have.attr', + 'value', + getRemoteSchemaName(3, testName) + ); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/create-remote-schema/test.ts b/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/create-remote-schema/test.ts new file mode 100644 index 00000000000..23bb583de0d --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/create-remote-schema/test.ts @@ -0,0 +1,83 @@ +/* eslint no-unused-vars: 0 */ +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; + +import { + checkCreateRemoteSchemaRoute, + failRSWithInvalidRemoteUrl, + createSimpleRemoteSchema, + failRSDuplicateSchemaName, + failRSDuplicateSchemaNodes, + deleteSimpleRemoteSchema, + deleteSimpleRemoteSchemaFailUserConfirmationError, + failWithRemoteSchemaEnvUrl, + failWithRemoteSchemaEnvHeader, + passWithRemoteSchemaHeader, + deleteRemoteSchema, + visitRemoteSchemaPermissionsTab, + createSimpleRemoteSchemaPermission, + passWithUpdateRemoteSchema, +} from './spec'; + +const setup = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Setup route', () => { + it('Visit the index route', () => { + // Visit the index route + cy.visit('/remote-schemas/manage/schemas'); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runCreateRemoteSchemaTableTests = () => { + // Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 + // TODO: Fix and restore it + describe.skip('Create Remote Schema', () => { + it( + 'Add remote schema button opens the correct route', + checkCreateRemoteSchemaRoute + ); + it( + 'Fails to create remote schema without valid url', + failRSWithInvalidRemoteUrl + ); + it('Create a simple remote schema', createSimpleRemoteSchema); + it('Fails to add remote schema with same name', failRSDuplicateSchemaName); + it( + 'Fails to add remote schema which is already added', + failRSDuplicateSchemaNodes + ); + it( + 'Delete simple remote schema fail due to user confirmation error', + deleteSimpleRemoteSchemaFailUserConfirmationError + ); + it( + 'Visits the remote schema permissions tab', + visitRemoteSchemaPermissionsTab + ); + it( + 'Create a simple remote schema permission role', + createSimpleRemoteSchemaPermission + ); + it('Delete simple remote schema', deleteSimpleRemoteSchema); + it( + 'Fails to create remote schema with url from env', + failWithRemoteSchemaEnvUrl + ); + it( + 'Fails to create remote schema with headers from env', + failWithRemoteSchemaEnvHeader + ); + it('Create remote schema with headers', passWithRemoteSchemaHeader); + it('Update remote schema on Modify page', passWithUpdateRemoteSchema); + it('Delete remote schema with headers', deleteRemoteSchema); + }); +}; + +if (testMode !== 'cli') { + setup(); + runCreateRemoteSchemaTableTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/edit-schema/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/edit-schema/spec.ts new file mode 100644 index 00000000000..9f9a448fd3d --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/edit-schema/spec.ts @@ -0,0 +1,102 @@ +import { getElementFromAlias } from '../../../helpers/eventHelpers'; + +type CustomizationSettingsType = { + root_fields_namespace: string; + type_names: { + prefix: string; + suffix: string; + mapping: Record; + }; + field_names: { + parent_type: string; + prefix: string; + suffix: string; + mapping: Record; + }[]; +}; +export const modifyCustomization = ( + customizationSettings: CustomizationSettingsType | undefined +) => { + cy.get(getElementFromAlias('remote-schema-customization-editor-expand-btn')) + .should('exist') + .click(); + + // add root field name + cy.get(getElementFromAlias('remote-schema-customization-root-field-input')) + .clear() + .type(customizationSettings?.root_fields_namespace || ''); + + cy.get( + getElementFromAlias('remote-schema-customization-type-name-prefix-input') + ) + .clear() + .type(customizationSettings?.type_names.prefix || ''); + + cy.get( + getElementFromAlias('remote-schema-customization-type-name-suffix-input') + ) + .clear() + .type(customizationSettings?.type_names.suffix || ''); + + // add type name + let key = Object.keys(customizationSettings?.type_names?.mapping || {})[0]; + cy.get( + getElementFromAlias('remote-schema-customization-type-name-lhs-input') + ).select(key); + cy.get( + getElementFromAlias('remote-schema-customization-type-name-0-rhs-input') + ) + .clear() + .type(customizationSettings?.type_names?.mapping[key] || ''); + + cy.get(getElementFromAlias('remote-schema-editor')).should('exist').click(); + + cy.get(getElementFromAlias('remote-schema-customization-open-field-mapping')) + .should('exist') + .click(); + + // click the field mapping button + cy.get( + getElementFromAlias( + 'remote-schema-customization-field-type-parent-type-input' + ) + ).select(customizationSettings?.field_names[0].parent_type || ''); + + cy.get( + getElementFromAlias( + 'remote-schema-customization-field-type-field-prefix-input' + ) + ) + .clear() + .type(customizationSettings?.field_names[0].prefix || ''); + + cy.get( + getElementFromAlias( + 'remote-schema-customization-field-type-field-suffix-input' + ) + ) + .clear() + .type(customizationSettings?.field_names[0].suffix || ''); + + // remote-schema-customization-field-type-lhs-input + key = Object.keys(customizationSettings?.field_names[0].mapping || {})[0]; + cy.get( + getElementFromAlias('remote-schema-customization-field-type-lhs-input') + ).select(key); + // remote-schema-customization-field-type-rhs-input + cy.get( + getElementFromAlias('remote-schema-customization-field-type-0-rhs-input') + ) + .clear() + .type(customizationSettings?.field_names[0].mapping[key] || ''); + + cy.get(getElementFromAlias('remote-schema-editor')).should('exist').click(); + + cy.get(getElementFromAlias('add-field-customization')) + .should('exist') + .click(); + + cy.get(getElementFromAlias('remote-schema-edit-save-btn')) + .should('exist') + .click(); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/edit-schema/test.ts b/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/edit-schema/test.ts new file mode 100644 index 00000000000..5b1e8e9f7ca --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/edit-schema/test.ts @@ -0,0 +1,103 @@ +/* eslint no-unused-vars: 0 */ +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; +import { modifyCustomization } from './spec'; + +// const visitRoute = () => { +// describe('Setup route', () => { +// it('Visit the index route', () => { +// // Visit the index route +// cy.visit('/remote-schemas/manage/schemas'); +// // Get and set validation metadata +// setMetaData(); +// }); +// }); +// }; + +const createRemoteSchema = (remoteSchemaName: string) => { + const postBody = { + type: 'add_remote_schema', + args: { + name: remoteSchemaName, + definition: { + url: 'https://graphql-pokemon2.vercel.app', + forward_client_headers: true, + timeout_seconds: 60, + }, + }, + }; + cy.request('POST', 'http://localhost:8080/v1/metadata', postBody).then( + response => { + expect(response.body).to.have.property('message', 'success'); // true + } + ); +}; + +const removeRemoteSchema = (remoteSchemaName: string) => { + const postBody = { + type: 'remove_remote_schema', + args: { + name: remoteSchemaName, + }, + }; + cy.request('POST', 'http://localhost:8080/v1/metadata', postBody).then( + response => { + expect(response.body).to.have.property('message', 'success'); // true + } + ); +}; + +const editSchemaTests = () => { + describe('Modify an existing remote schema', () => { + describe('Create a remote schema for testing', () => { + it('add a remote schema via the API', () => { + createRemoteSchema('test_remote_schema'); + }); + }); + + describe('Edit the remote schema settings', () => { + it('Visit the modify page', () => { + cy.visit('/remote-schemas/manage/test_remote_schema/modify'); + setMetaData(); + }); + + it('Modify the remote schema settings', () => { + modifyCustomization({ + root_fields_namespace: 'test_root_namespace', + type_names: { + prefix: 'test_prefix', + suffix: 'test_suffix', + mapping: { + Pokemon: 'renamed_type_name_mapping', + }, + }, + field_names: [ + { + parent_type: 'PokemonDimension', + prefix: 'test_parent_type_prefix', + suffix: 'test_parent_type_suffix', + mapping: { + minimum: 'test_field_name', + }, + }, + ], + }); + }); + + it('expect success notification', () => { + cy.expectSuccessNotificationWithTitle('Remote schema modified'); + }); + }); + + describe('Remove remote schema', () => { + it('Remove the remote schema via the API', () => { + removeRemoteSchema('test_remote_schema'); + }); + }); + }); +}; + +if (testMode !== 'cli') { + // setup(); + editSchemaTests(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/remote-schema-relationships/test.ts b/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/remote-schema-relationships/test.ts new file mode 100644 index 00000000000..e3ec284bc87 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/remote-schema-relationships/test.ts @@ -0,0 +1,126 @@ +import { getElementFromAlias } from '../../../helpers/eventHelpers'; +import { replaceMetadata, resetMetadata } from '../../../helpers/metadata'; +// import { postgres } from '../../data/manage-database/postgres.spec'; + +// Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 +// TODO: Fix and restore it +describe.skip('check if remote schema relationships are displayed properly', () => { + before(() => { + // create a table called destination_table + // postgres.helpers.createTable('destination_table'); + + // load stuff into the metadata + replaceMetadata({ + version: 3, + sources: [ + { + name: 'default', + kind: 'postgres', + tables: [ + { + table: { + schema: 'public', + name: 'destination_table', + }, + }, + ], + configuration: { + connection_info: { + use_prepared_statements: true, + database_url: { + from_env: 'HASURA_GRAPHQL_DATABASE_URL', + }, + isolation_level: 'read-committed', + pool_settings: { + connection_lifetime: 600, + retries: 1, + idle_timeout: 180, + max_connections: 50, + }, + }, + }, + }, + ], + remote_schemas: [ + { + name: 'destination_rs', + definition: { + url: 'https://graphql-pokemon2.vercel.app/', + timeout_seconds: 60, + }, + comment: '', + }, + { + name: 'source_rs', + definition: { + url: 'https://countries.trevorblades.com/', + timeout_seconds: 60, + }, + comment: '', + remote_relationships: [ + { + relationships: [ + { + definition: { + to_source: { + relationship_type: 'object', + source: 'default', + table: 'destination_table', + field_mapping: { + code: 'id', + }, + }, + }, + name: 'an_example_rs_to_db_relationship', + }, + { + definition: { + to_remote_schema: { + remote_field: { + pokemons: { + arguments: {}, + }, + }, + remote_schema: 'destination_rs', + lhs_fields: ['code'], + }, + }, + name: 'an_example_rs_to_rs_relationship', + }, + ], + type_name: 'Country', + }, + ], + }, + ], + }); + }); + + it('verify if the rows exist on the remote schema table', () => { + cy.visit( + 'http://localhost:3000/remote-schemas/manage/source_rs/relationships' + ); + cy.get(getElementFromAlias('remote-schema-relationships-table')).should( + 'exist' + ); + cy.get(getElementFromAlias('remote-schema-relationships-table')) + .find('tr') + .should('have.length', 3); + cy.get(getElementFromAlias('remote-schema-relationships-table')).contains( + 'td', + 'an_example_rs_to_db_relationship' + ); + cy.get(getElementFromAlias('remote-schema-relationships-table')).contains( + 'td', + 'an_example_rs_to_rs_relationship' + ); + }); + + after(() => { + // reset the metadata + resetMetadata(); + + // delete the table + // postgres.helpers.deleteTable('destination_table'); + }); +}); diff --git a/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/rs-to-db-relationships/test.ts b/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/rs-to-db-relationships/test.ts new file mode 100644 index 00000000000..92d1b3dbd29 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/rs-to-db-relationships/test.ts @@ -0,0 +1,92 @@ +import { getElementFromAlias } from '../../../helpers/eventHelpers'; +import { replaceMetadata, resetMetadata } from '../../../helpers/metadata'; +// import { postgres } from '../../data/manage-database/postgres.spec'; + +// Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 +// TODO: Fix and restore it +describe.skip('check if remote schema to db relationships are created properly', () => { + before(() => { + // create a table called destination_table + // postgres.helpers.createTable('destination_table'); + + // load stuff into the metadata + replaceMetadata({ + version: 3, + sources: [ + { + name: 'default', + kind: 'postgres', + tables: [ + { + table: { + schema: 'public', + name: 'destination_table', + }, + }, + ], + configuration: { + connection_info: { + use_prepared_statements: true, + database_url: { + from_env: 'HASURA_GRAPHQL_DATABASE_URL', + }, + isolation_level: 'read-committed', + pool_settings: { + connection_lifetime: 600, + retries: 1, + idle_timeout: 180, + max_connections: 50, + }, + }, + }, + }, + ], + remote_schemas: [ + { + name: 'source_rs', + definition: { + url: 'https://graphql-pokemon2.vercel.app/', + timeout_seconds: 60, + }, + comment: '', + }, + ], + }); + }); + + it('verify creating a new rs-to-db relationship', () => { + cy.visit( + 'http://localhost:3000/remote-schemas/manage/source_rs/relationships' + ); + cy.get(getElementFromAlias('add-a-new-rs-relationship')).click(); + cy.get(getElementFromAlias('radio-select-remoteDB')).click(); + cy.get(getElementFromAlias('rs-to-db-rel-name')).type('RelationshipName'); + cy.get(getElementFromAlias('select-rel-type')).select('array'); + cy.get(getElementFromAlias('select-source-type')).select('Pokemon'); + cy.get(getElementFromAlias('select-ref-db')).select('default'); + cy.get(getElementFromAlias('select-ref-schema')).select('public'); + cy.get(getElementFromAlias('select-ref-table')).select('destination_table'); + cy.get(getElementFromAlias('select-source-field')).select('id'); + cy.get(getElementFromAlias('select-ref-col')).select('name'); + cy.get(getElementFromAlias('add-rs-relationship')).click(); + + cy.get(getElementFromAlias('remote-schema-relationships-table')).should( + 'exist' + ); + cy.get(getElementFromAlias('remote-schema-relationships-table')) + .find('tr') + .should('have.length', 2); + cy.get(getElementFromAlias('remote-schema-relationships-table')).contains( + 'td', + 'RelationshipName' + ); + }); + + after(() => { + // reset the metadata + resetMetadata(); + + // delete the table + // postgres.helpers.deleteTable('destination_table'); + }); +}); diff --git a/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/rs-to-rs-relationships/test.ts b/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/rs-to-rs-relationships/test.ts new file mode 100644 index 00000000000..fcc2b33b931 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/remote-schemas/rs-to-rs-relationships/test.ts @@ -0,0 +1,116 @@ +import { getElementFromAlias } from '../../../helpers/eventHelpers'; +import { replaceMetadata, resetMetadata } from '../../../helpers/metadata'; +import type { HasuraMetadataV3 } from '../../../../src/metadata/types'; + +// Temporarily skipped because of its flakiness, see: https://github.com/hasura/graphql-engine-mono/issues/5433 +// TODO: Fix and restore it +describe.skip('check if remote schema to db relationships are created properly', () => { + before(() => { + const body: HasuraMetadataV3 = { + version: 3, + sources: [ + { + name: 'default', + kind: 'postgres', + tables: [], + configuration: { + connection_info: { + use_prepared_statements: true, + database_url: { + from_env: 'HASURA_GRAPHQL_DATABASE_URL', + }, + isolation_level: 'read-committed', + pool_settings: { + connection_lifetime: 600, + retries: 1, + idle_timeout: 180, + max_connections: 50, + }, + }, + }, + }, + ], + remote_schemas: [ + { + name: 'source_rs', + definition: { + url: 'https://graphql-pokemon2.vercel.app/', + timeout_seconds: 60, + }, + comment: '', + }, + { + name: 'ref_rs', + definition: { + url: 'https://hasura-console-test.herokuapp.com/v1/graphql/', + timeout_seconds: 60, + }, + comment: '', + }, + ], + inherited_roles: [], + }; + // load stuff into the metadata + replaceMetadata(body); + }); + + it('should create a new rs-to-rs relationship from source field', () => { + cy.visit( + 'http://localhost:3000/remote-schemas/manage/source_rs/relationships' + ); + cy.get(getElementFromAlias('add-a-new-rs-relationship')).click(); + cy.get(getElementFromAlias('radio-select-remoteSchema')).click(); + cy.get(getElementFromAlias('rs-to-rs-rel-name')).type('RelationshipName'); + cy.get(getElementFromAlias('select-source-type')).select('Pokemon'); + cy.get(getElementFromAlias('select-ref-rs')).select('ref_rs'); + cy.get('.ant-tree-switcher').first().click(); + cy.get('.ant-tree-switcher').eq(1).click(); + cy.get('.ant-tree-checkbox').eq(1).click(); + cy.get(getElementFromAlias('select-argument')).select('Source Field'); + cy.get(getElementFromAlias('select-source-field')).select('id'); + cy.get(getElementFromAlias('add-rs-relationship')).click(); + cy.get(getElementFromAlias('remote-schema-relationships-table')).should( + 'exist' + ); + cy.get(getElementFromAlias('remote-schema-relationships-table')) + .find('tr') + .should('have.length', 2); + cy.get(getElementFromAlias('remote-schema-relationships-table')).contains( + 'td', + 'RelationshipName' + ); + }); + it('should create a new reverse rs-to-rs relationship with static fill value', () => { + cy.visit( + 'http://localhost:3000/remote-schemas/manage/ref_rs/relationships' + ); + cy.get(getElementFromAlias('add-a-new-rs-relationship')).click(); + cy.get(getElementFromAlias('radio-select-remoteSchema')).click(); + cy.get(getElementFromAlias('rs-to-rs-rel-name')).type( + 'StaticRelationshipName' + ); + cy.get(getElementFromAlias('select-source-type')).select('test'); + cy.get(getElementFromAlias('select-ref-rs')).select('source_rs'); + cy.get('.ant-tree-switcher').first().click(); // expand Query + cy.get('.ant-tree-switcher').eq(3).click(); // expand pokemon + cy.get('.ant-tree-checkbox').eq(1).click(); // check name argument + cy.get(getElementFromAlias('select-argument')).select('Static Value'); + cy.get(getElementFromAlias('select-static-value')).type('Bulbasaur'); + cy.get(getElementFromAlias('add-rs-relationship')).click(); + cy.get(getElementFromAlias('remote-schema-relationships-table')).should( + 'exist' + ); + cy.get(getElementFromAlias('remote-schema-relationships-table')) + .find('tr') + .should('have.length', 2); + cy.get(getElementFromAlias('remote-schema-relationships-table')).contains( + 'td', + 'StaticRelationshipName' + ); + }); + + after(() => { + // reset the metadata + resetMetadata(); + }); +}); diff --git a/frontend/apps/console-ce-e2e/src/e2e/settings/metadata/insecure-domain/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/settings/metadata/insecure-domain/spec.ts new file mode 100644 index 00000000000..bf00ef4af95 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/settings/metadata/insecure-domain/spec.ts @@ -0,0 +1,69 @@ +import { getElementFromAlias } from '../../../../helpers/dataHelpers'; + +// fake inconsistentMetadata api response + +const addDomainOneByOne = (domain: string) => { + // click Add Domain + cy.get(getElementFromAlias('add-insecure-domain')).click(); + // type domain name-1 + cy.get(getElementFromAlias('domain-name')).type(domain); + // click Add to Allow List + cy.get(getElementFromAlias('add-tls-allow-list')).click(); + // check metadata + cy.get(getElementFromAlias(domain)).contains(domain); +}; + +export const addSuccessfulInsecureDomain = () => { + // open insecure domain page + cy.visit('/settings/insecure-domain'); + // add 1st domain + addDomainOneByOne('www.insecuredomain-1.com'); + // add 2nd domain + addDomainOneByOne('www.insecuredomain-2.com'); + // add 3rd domain + addDomainOneByOne('www.insecuredomain-3.com'); + cy.wait(2000); +}; + +export const duplicateAndEmptyDomainError = () => { + // click add domain + cy.get(getElementFromAlias('add-insecure-domain')).click(); + // type duplicate domain name + cy.get(getElementFromAlias('domain-name')).type('www.insecuredomain-1.com'); + // click Add to Allow List + cy.get(getElementFromAlias('add-tls-allow-list')).click(); + cy.wait(1000); + // duplicate error + + // NOTE: is a timeout 5000 really needed? + cy.expectErrorNotificationWithTitle( + 'Adding domain to insecure TLS allow list failed' + ); + + // close add domain dialog box + cy.get(getElementFromAlias('cancel-domain')).click(); + // click add domain + cy.get(getElementFromAlias('add-insecure-domain')).click(); + // click Add to Allow List with empty domain name + cy.get(getElementFromAlias('add-tls-allow-list')).click(); + // empty domain error + // NOTE: is a timeout 5000 really needed? + cy.expectErrorNotificationWithTitle('No domain found'); + cy.get(getElementFromAlias('cancel-domain')).click(); +}; + +export const deleteInsecureDomain = () => { + // click to delete domain + cy.get(getElementFromAlias('delete-domain-www.insecuredomain-1.com')).click({ + force: true, + }); + cy.get(getElementFromAlias('delete-domain-www.insecuredomain-2.com')).click({ + force: true, + }); + cy.get(getElementFromAlias('delete-domain-www.insecuredomain-3.com')).click({ + force: true, + }); + cy.get(getElementFromAlias('label-no-domain-found'), { timeout: 10000 }) + .should('be.visible') + .and('contain', 'No domains added to insecure TLS allow list'); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/settings/metadata/insecure-domain/test.ts b/frontend/apps/console-ce-e2e/src/e2e/settings/metadata/insecure-domain/test.ts new file mode 100644 index 00000000000..f9848aaada5 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/settings/metadata/insecure-domain/test.ts @@ -0,0 +1,21 @@ +import { testMode } from '../../../../helpers/common'; +import { + addSuccessfulInsecureDomain, + deleteInsecureDomain, + duplicateAndEmptyDomainError, +} from './spec'; + +export const testInsecureDomainPage = () => { + describe('Insecure Domain', () => { + it('should successfully add insecure domain', addSuccessfulInsecureDomain); + it( + 'should show error on passing duplicate and empty domain name', + duplicateAndEmptyDomainError + ); + it('should delete domains one by one', deleteInsecureDomain); + }); +}; + +if (testMode !== 'cli') { + testInsecureDomainPage(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/settings/metadata/spec.ts b/frontend/apps/console-ce-e2e/src/e2e/settings/metadata/spec.ts new file mode 100644 index 00000000000..7ff6f501912 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/settings/metadata/spec.ts @@ -0,0 +1,45 @@ +import { getElementFromAlias, baseUrl } from '../../../helpers/dataHelpers'; + +// fake inconsistentMetadata api response +const inconsistentMetadata = { + is_consistent: false, + inconsistent_objects: [ + { + definition: 'DB2', + reason: 'Inconsistent object: connection error', + name: 'source DB2', + type: 'source', + message: + 'could not translate host name "db" to address: Name or service not known\n', + }, + ], +}; + +export const inconsistentMetadataPage = () => { + // Set first column + cy.visit('/settings/metadata-status?is_redirected=true'); + cy.intercept('/v1/metadata', req => { + // dynamically respond to a request here + + if (req?.body?.type === 'get_inconsistent_metadata') { + // fake inconsistentMetadata api response to test the UI + return req.reply(inconsistentMetadata); + } + + // send all other requests to the destination server + req.reply(); + }); + cy.url().should( + 'eq', + `${baseUrl}/settings/metadata-status?is_redirected=true` + ); + + cy.get(getElementFromAlias('inconsistent_name_0')).contains('DB2'); + cy.get(getElementFromAlias('inconsistent_type_0')).contains('source'); + cy.get(getElementFromAlias('inconsistent_reason_0')).contains( + 'Inconsistent object: connection error' + ); + cy.get(getElementFromAlias('inconsistent_reason_0')).contains( + 'could not translate host name "db" to address: Name or service not known' + ); +}; diff --git a/frontend/apps/console-ce-e2e/src/e2e/settings/metadata/test.ts b/frontend/apps/console-ce-e2e/src/e2e/settings/metadata/test.ts new file mode 100644 index 00000000000..c337e2f7aa2 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/settings/metadata/test.ts @@ -0,0 +1,16 @@ +import { testMode } from '../../../helpers/common'; + +import { inconsistentMetadataPage } from './spec'; + +export const testInconsistentMetadatapage = () => { + describe('Inconsistent Metadata', () => { + it( + 'should render inconsistent metadata table with fake data', + inconsistentMetadataPage + ); + }); +}; + +if (testMode !== 'cli') { + testInconsistentMetadatapage(); +} diff --git a/frontend/apps/console-ce-e2e/src/e2e/validators/validators.ts b/frontend/apps/console-ce-e2e/src/e2e/validators/validators.ts new file mode 100644 index 00000000000..43328ea86ea --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/e2e/validators/validators.ts @@ -0,0 +1,529 @@ +import { + makeDataAPIOptions, + getColName, + QueryEndpoint, +} from '../../helpers/dataHelpers'; +import { migrateModeUrl } from '../../helpers/common'; +import { toggleOnMigrationMode } from '../data/migration-mode/utils'; +import { + getNoOfRetries, + getIntervalSeconds, + getTimeoutSeconds, +} from '../../helpers/eventHelpers'; + +// ***************** UTIL FUNCTIONS ************************** + +let adminSecret: string; +let dataApiUrl: string; + +export const setMetaData = () => { + cy.window().then(win => { + adminSecret = win.__env.adminSecret; + dataApiUrl = win.__env.dataApiUrl; + const { consoleMode } = win.__env; + if (consoleMode === 'cli') { + toggleOnMigrationMode(); + } + }); +}; + +export const createView = (sql: string) => { + const reqBody = { + type: 'run_sql', + args: { + sql, + }, + }; + const requestOptions = makeDataAPIOptions(dataApiUrl, adminSecret, reqBody); + cy.request(requestOptions); +}; + +// ******************* VALIDATION FUNCTIONS ******************************* + +export enum ResultType { + SUCCESS = 'success', + FAILURE = 'failure', +} +interface RequestBody { + [key: string]: any; +} + +export interface TableFields { + [x: string]: any; + id?: string; + name?: string; + title?: string; + Content?: string; + author_id?: string; + rating?: string; + user_id?: string; + article_id?: string; + comment?: string; +} +/** + * Remote Schema validator + * @param remoteSchemaName + * @param result + */ +export const validateRS = ( + remoteSchemaName: string, + result: ResultType +): void => { + const reqBody = { + type: 'export_metadata', + args: {}, + }; + const requestOptions = makeDataAPIOptions( + dataApiUrl, + adminSecret, + reqBody, + 'metadata' + ); + cy.request(requestOptions).then(response => { + const remoteSchemas = response.body?.remote_schemas ?? []; + if (result === ResultType.SUCCESS) { + expect( + remoteSchemas.length > 0 && remoteSchemas[0].name === remoteSchemaName + ).to.be.true; + } else { + expect(remoteSchemas.length > 0).to.be.false; + } + }); +}; + +/** + * Vaidate the given function + * @param functionName + * @param functionSchema + * @param result + */ +export const validateCFunc = ( + functionName: string, + functionSchema: string, + result: ResultType +): void => { + const reqBody = { + type: 'export_metadata', + args: {}, + }; + const requestOptions = makeDataAPIOptions( + dataApiUrl, + adminSecret, + reqBody, + 'metadata' + ); + cy.request(requestOptions).then(response => { + const defaultSourceData = response.body.sources.find( + (source: { name: string }) => source.name === 'default' + ); + const functionData = defaultSourceData.functions; + const foundFunction = + functionData.filter( + (fn: { function: { name: string; schema: string } }) => + fn.function.name === functionName && + fn.function.schema === functionSchema + ).length === 1; + if (result === ResultType.SUCCESS) { + expect(functionData.length && foundFunction).to.be.true; + } else { + // NOTE: functionData.length may not be true + expect(functionData.length && foundFunction).to.be.false; + } + }); +}; + +/** + * Validate untracked function + * @param functionName + * @param functionSchema + * @param result + */ +export const validateUntrackedFunc = ( + functionName: string, + functionSchema: string, + result: ResultType +): void => { + const reqBody = { + type: 'export_metadata', + args: {}, + }; + const requestOptions = makeDataAPIOptions( + dataApiUrl, + adminSecret, + reqBody, + 'metadata' + ); + cy.request(requestOptions).then(response => { + const defaultSourceData = response.body.sources.find( + (source: { name: string }) => source.name === 'default' + ); + const functionData = defaultSourceData?.functions ?? []; + const foundFunction = + functionData.filter( + (fn: { function: { name: string; schema: string } }) => + fn?.function?.name === functionName && + fn?.function?.schema === functionSchema + ).length === 1; + if (result === ResultType.SUCCESS) { + expect(!functionData.length || !foundFunction).to.be.true; + } else { + // NOTE: functionData.length may not be true + expect(!functionData.length || !foundFunction).to.be.false; + } + }); +}; + +/** + * Make date API Request and check the repsone status + * @param reqBody + * @param result + */ +export const dataRequest = ( + reqBody: RequestBody, + result: ResultType, + queryType: QueryEndpoint = 'query' +) => { + const requestOptions = makeDataAPIOptions( + dataApiUrl, + adminSecret, + reqBody, + queryType + ); + cy.request(requestOptions).then(response => { + if (result === ResultType.SUCCESS) { + expect( + (response.body.result_type === 'CommandOk' && + response.body.result === null) || + response.body.message === 'success' + ).to.be.true; + } else { + expect( + (response.body.result_type === 'CommandOk' && + response.body.result === null) || + response.body.message === 'success' + ).to.be.false; + } + }); +}; + +export const trackFunctionRequest = ( + reqBody: RequestBody, + result: ResultType +) => { + const requestOptions = makeDataAPIOptions( + dataApiUrl, + adminSecret, + reqBody, + 'metadata' + ); + cy.request(requestOptions).then(response => { + if (result === ResultType.SUCCESS) { + expect(response.body.message === ResultType.SUCCESS).to.be.true; + } else { + expect(response.body.message === ResultType.SUCCESS).to.be.false; + } + }); +}; + +/** + * Drop a table request + * @param reqBody + * @param result + */ +export const dropTableRequest = (reqBody: RequestBody, result: ResultType) => { + const requestOptions = makeDataAPIOptions(dataApiUrl, adminSecret, reqBody); + cy.request(requestOptions).then(response => { + if (result === ResultType.SUCCESS) { + expect( + response.body.length > 0 && response.body[0].result_type === 'CommandOk' + ).to.be.true; + } else { + expect( + response.body.length > 0 && response.body[0].result_type === 'CommandOk' + ).to.be.false; + } + }); +}; + +// ****************** Table Validator ********************* + +export const validateCT = (tableName: string, result: ResultType) => { + const reqBody = { + type: 'run_sql', + args: { + sql: `SELECT * FROM "public"."${tableName}";`, + }, + }; + const requestOptions = makeDataAPIOptions(dataApiUrl, adminSecret, reqBody); + cy.request(requestOptions).then(response => { + if (result === ResultType.SUCCESS) { + expect(response.status === 200).to.be.true; + } else { + expect(response.status === 200).to.be.false; + } + }); +}; + +// **************** View Validator ******************* + +export const validateView = (viewName: string, result: ResultType) => { + validateCT(viewName, result); +}; + +// *************** Column Validator ******************* + +export const validateColumn = ( + tableName: string, + column: (string | { name: string; columns: string[] })[], + result: ResultType +) => { + const reqBody = { + type: 'select', + args: { + table: tableName, + columns: column, + }, + }; + const requestOptions = makeDataAPIOptions(dataApiUrl, adminSecret, reqBody); + cy.request(requestOptions).then(response => { + if (result === ResultType.SUCCESS) { + expect(response.status === 200).to.be.true; + } else { + expect(response.status === 200).to.be.false; + } + }); +}; + +export const validateColumnWhere = ( + tableName: string, + column: string, + where: string, + result: ResultType +) => { + const reqBody = { + type: 'select', + args: { + table: tableName, + columns: column, + where, + }, + }; + const requestOptions = makeDataAPIOptions(dataApiUrl, adminSecret, reqBody); + cy.request(requestOptions).then(response => { + cy.log(JSON.stringify(response)); + if (result === ResultType.SUCCESS) { + expect(response.body.length > 0).to.be.true; + } else { + expect(response.body.length === 0).to.be.true; + } + }); +}; + +// ******************** Validate Insert ********************* + +export const validateInsert = (tableName: string, rows: number) => { + const reqBody = { + type: 'count', + source: 'default', + args: { + table: tableName, + schema: 'public', + }, + }; + const requestOptions = makeDataAPIOptions(dataApiUrl, adminSecret, reqBody); + cy.request(requestOptions).then(response => { + cy.log(JSON.stringify(response)); + expect(response.body.count === rows).to.be.true; + }); +}; + +// ******************* Permissiosn Validator **************** + +export type QueryType = 'insert' | 'select' | 'update' | 'delete'; +export type CheckType = 'custom' | 'none'; +interface SchemaObject { + [key: string]: any; +} + +const compareChecks = ( + permObj: SchemaObject, + check: CheckType, + query: QueryType, + columns: string[] | null +) => { + const perm = permObj.permission ?? {}; + if (check === 'none') { + if (query === 'insert') { + expect(Object.keys(perm?.check ?? {}).length === 0).to.be.true; + expect(perm?.set?.[getColName(0)] === '1').to.be.true; + expect(perm?.set?.[getColName(1)] === 'x-hasura-user-id').to.be.true; + } else { + expect(Object.keys(perm?.filter ?? {}).length === 0).to.be.true; + if (query === 'select' || query === 'update') { + [0, 1, 2].forEach(index => { + expect(perm?.columns.includes(getColName(index))); + }); + if (query === 'update') { + expect(perm?.set?.[getColName(0)] === '1').to.be.true; + expect(perm?.set?.[getColName(1)] === 'x-hasura-user-id').to.be.true; + } + } + } + } else if (query === 'insert') { + expect(perm?.check?.[getColName(0)]._eq === 1).to.be.true; + } else { + expect(perm?.filter?.[getColName(0)]._eq === 1).to.be.true; + if (query === 'select' || query === 'update') { + if (columns) { + columns.forEach((col, index) => { + expect(perm?.columns.includes(getColName(index))); + }); + } + } + } +}; + +const handlePermValidationResponse = ( + tableSchema: SchemaObject, + role: string, + query: QueryType, + check: CheckType, + result: ResultType, + columns: string[] | null +) => { + let rolePerms = {}; + if (tableSchema?.[`${query}_permissions`]) { + rolePerms = tableSchema[`${query}_permissions`].find( + (permission: { role: string }) => permission.role === role + ); + } + + if (Object.keys(rolePerms).length) { + compareChecks(rolePerms, check, query, columns); + } else { + // this block can be reached only if the permission doesn't exist (failure case) + expect(result === ResultType.FAILURE).to.be.true; + } +}; + +/** + * Validate Permissions + * @param tableName + * @param role + * @param query + * @param check + * @param result + * @param columns + */ +export const validatePermission = ( + tableName: string, + role: string, + query: QueryType, + check: CheckType, + result: ResultType, + columns: string[] | null +) => { + const reqBody = { + type: 'export_metadata', + args: {}, + }; + const requestOptions = makeDataAPIOptions( + dataApiUrl, + adminSecret, + reqBody, + 'metadata' + ); + cy.request(requestOptions).then(response => { + const sourceInfo = response.body.sources.find( + (source: { name: string }) => source.name === 'default' + ); + const tableSchema = sourceInfo.tables.find( + (table: { table: { name: string } }) => table.table.name === tableName + ); + handlePermValidationResponse( + tableSchema, + role, + query, + check, + result, + columns + ); + }); +}; + +// ********************** Validate Migration mode ****************** +/** + * Validate the migration mode + * @param mode + */ +export const validateMigrationMode = (mode: boolean) => { + cy.request({ + method: 'GET', + url: migrateModeUrl, + }).then(response => { + expect(response.body.migration_mode == mode.toString()).to.be.true; // eslint-disable-line + }); +}; + +// ****************** Trigger Validator ********************* +/** + * Validate the trigger based on trigger name + * @param triggerName + * @param result + */ +export const validateCTrigger = ( + triggerName: string, + tableName: string, + schemaName = 'public', + result: ResultType, + allCols?: boolean +) => { + const reqBody = { + type: 'export_metadata', + args: {}, + }; + const requestOptions = makeDataAPIOptions( + dataApiUrl, + adminSecret, + reqBody, + 'metadata' + ); + cy.request(requestOptions).then(response => { + const sourceInfo = response.body.sources.find( + (source: { name: string }) => source.name === 'default' + ); + const tableInfo = + sourceInfo?.tables?.find( + (table: { table: { schema: string; name: string } }) => + table.table.schema === schemaName && table.table.name === tableName + ) ?? {}; + const trigger = + tableInfo?.event_triggers?.find( + (trig: { name: string }) => trig.name === triggerName + ) ?? {}; + + if (result === ResultType.SUCCESS && Object.keys(tableInfo).length) { + expect(response.status === 200).to.be.true; + expect(trigger.definition.insert.columns === '*').to.be.true; + expect(trigger.definition.delete.columns === '*').to.be.true; + if (allCols) { + expect(trigger.definition.update.columns === '*').to.be.true; + } else { + expect(trigger.definition.update.columns.length === 2).to.be.true; + } + expect( + trigger.retry_conf.interval_sec === parseInt(getIntervalSeconds(), 10) + ).to.be.true; + expect(trigger.retry_conf.num_retries === parseInt(getNoOfRetries(), 10)) + .to.be.true; + expect( + trigger.retry_conf.timeout_sec === parseInt(getTimeoutSeconds(), 10) + ).to.be.true; + expect(schemaName === 'public').to.be.true; + expect(tableName === 'Apic_test_table_ctr_0').to.be.true; + } else { + expect(!Object.keys(tableInfo).length || !Object.keys(trigger).length).to + .be.true; + } + }); +}; diff --git a/frontend/apps/console-ce-e2e/src/fixtures/example.json b/frontend/apps/console-ce-e2e/src/fixtures/example.json index 294cbed6ce9..02e4254378e 100644 --- a/frontend/apps/console-ce-e2e/src/fixtures/example.json +++ b/frontend/apps/console-ce-e2e/src/fixtures/example.json @@ -1,4 +1,5 @@ { "name": "Using fixtures to represent data", - "email": "hello@cypress.io" + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" } diff --git a/frontend/apps/console-ce-e2e/src/global.d.ts b/frontend/apps/console-ce-e2e/src/global.d.ts new file mode 100644 index 00000000000..16acfbb862b --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/global.d.ts @@ -0,0 +1,25 @@ +interface Env { + adminSecret: string; + apiHost: string; + apiPort: string; + assetsPath: string; + assetsVersion: string; + assetVersion: string; + cdnAssets: boolean; + consoleAssetVersion: string; + consoleMode: string; + consolePath: string; + dataApiUrl: string; + enableTelemetry: boolean; + featuresCompatibility: string; + isAdminSecretSet: boolean; + isproduction: boolean; + nodeEnv: string; + serverVersion: string; + telemetryTopic: string; + urlPrefix: string; +} + +interface Window { + __env: Env; +} diff --git a/frontend/apps/console-ce-e2e/src/helpers/common.ts b/frontend/apps/console-ce-e2e/src/helpers/common.ts new file mode 100644 index 00000000000..f898f228400 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/helpers/common.ts @@ -0,0 +1,26 @@ +import { getTestMode } from './core/testMode'; + +export const testMode = getTestMode(); +export const baseUrl = Cypress.config('baseUrl'); +export const migrateUrl = Cypress.env('MIGRATE_URL'); +export const migrateModeUrl = `${migrateUrl}/settings`; + +// sets value of window.prompt and reloads page +export const setPromptValue = (value: string | null) => { + cy.log(`Set window.prompt to "${value}"`).then(() => { + cy.removeAllListeners('window:before:load'); + cy.on('window:before:load', win => { + cy.stub(win, 'prompt').returns(value); + }); + }); + + cy.reload(); +}; + +// This is works as setPromptValue with no unnecessary waiting +export const setPromptWithCb = (value: string | null, cb: () => void) => { + cy.window().then(win => { + cy.stub(win, 'prompt').returns(value); + cb(); + }); +}; diff --git a/frontend/apps/console-ce-e2e/src/helpers/constants.ts b/frontend/apps/console-ce-e2e/src/helpers/constants.ts new file mode 100644 index 00000000000..993d3c70084 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/helpers/constants.ts @@ -0,0 +1 @@ +export const ADMIN_SECRET_HEADER_KEY = 'x-hasura-admin-secret'; diff --git a/frontend/apps/console-ce-e2e/src/helpers/core/testMode.ts b/frontend/apps/console-ce-e2e/src/helpers/core/testMode.ts new file mode 100644 index 00000000000..0d1a21f1ce0 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/helpers/core/testMode.ts @@ -0,0 +1,28 @@ +type TestMode = + // the default one + | 'parallel' + // prevent all the tests from running at all + | 'cli' + // mentioned in the cypress/README but never used + // TODO: remove it or fix the docs if needed + | 'ui'; + +/** + * Read and check the TEST_MODE. + * + * @returns {TestMode} + */ +export function getTestMode(): TestMode { + const testMode = Cypress.env('TEST_MODE'); + + if ( + typeof testMode !== 'string' || + (testMode !== 'parallel' && testMode !== 'cli' && testMode !== 'ui') + ) { + throw new Error( + `Unexpected Cypress env variable TEST_MODE value: ${testMode}` + ); + } + + return testMode; +} diff --git a/frontend/apps/console-ce-e2e/src/helpers/dataHelpers.ts b/frontend/apps/console-ce-e2e/src/helpers/dataHelpers.ts new file mode 100644 index 00000000000..40bd1c25a27 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/helpers/dataHelpers.ts @@ -0,0 +1,264 @@ +import { QueryType } from './../integration/validators/validators'; +import { ADMIN_SECRET_HEADER_KEY } from './constants'; + +export const baseUrl = Cypress.config('baseUrl'); + +export const getDbRoute = (sourceName = 'default') => `/data/${sourceName}`; + +export const getIndexRoute = (sourceName = 'default', schemaName = 'public') => + `${getDbRoute(sourceName)}/schema/${schemaName}/`; + +export const createVolatileFunction = (name: string) => { + return { + type: 'run_sql', + args: { + sql: `CREATE OR REPLACE FUNCTION public.${name}() + RETURNS SETOF text_result + LANGUAGE sql + AS $function$ + SELECT * FROM text_result; + $function$`, + cascade: false, + }, + }; +}; + +export const dataTypes = [ + 'serial', + 'bigserial', + 'integer', + 'bigint', + 'uuid', + 'text', + 'numeric', + 'date', + 'timestamptz', + 'timetz', + 'boolean', +]; + +export const typeDefaults: { [key: string]: string } = { + integer: '5555', + bigint: '5555555555', + uuid: 'gen_random_uuid()', + text: 'test-text', + numeric: '0.55555', + date: 'now()', + timestamptz: 'now()', + timetz: 'now()', + boolean: 'false', +}; + +export const queryTypes: QueryType[] = ['insert', 'select', 'update', 'delete']; + +export const getColName = (i: number) => `Apic_test_column_${i}`; + +export const getTableName = (i: number, testName = '') => + `Apic_test_table_${testName}_${i}`; + +export const getElementFromAlias = (alias: string) => `[data-test="${alias}"]`; + +export const getElementFromClassName = (cn: string) => `.${cn}`; + +export const tableColumnTypeSelector = (alias: string) => { + cy.get(`${getElementFromAlias(alias)}`) + .children('div') + .click() + .find('input') + .focus(); +}; + +export const makeDataAPIUrl = ( + dataApiUrl: string, + queryEndpoint: QueryEndpoint = 'query' +) => { + if (queryEndpoint === 'query') { + return `${dataApiUrl}/v2/query`; + } + return `${dataApiUrl}/v1/metadata`; +}; + +interface APIPayload { + [key: string]: any; +} + +export const queryEndpoints = { + query: 'query', + metadata: 'metadata', +} as const; + +export type QueryEndpoint = keyof typeof queryEndpoints; + +export const makeDataAPIOptions = ( + dataApiUrl: string, + key: string, + body: APIPayload, + queryType: QueryEndpoint = 'query' +) => ({ + method: 'POST', + url: makeDataAPIUrl(dataApiUrl, queryType), + headers: { + [ADMIN_SECRET_HEADER_KEY]: key, + }, + body, + failOnStatusCode: false, +}); + +export const testCustomFunctionDefinition = ( + i: string +) => `create function search_posts${`_${i}`} (search text) returns setof post as $$ select * from post where title ilike ('%' || search || '%') or content ilike ('%' || search || '%') $$ language sql stable; +`; + +export const getCustomFunctionName = (i: number) => `search_posts${`_${i}`}`; + +export const getCreateTestFunctionQuery = (i: number) => { + return { + type: 'run_sql', + args: { + sql: `CREATE OR REPLACE FUNCTION public.search_posts_${i}(search text)\n RETURNS SETOF post\n LANGUAGE sql\n STABLE\nAS $function$\n select *\n from post\n where\n title ilike ('%' || search || '%') or\n content ilike ('%' || search || '%')\n $function$\n`, + cascade: false, + }, + }; +}; + +export const getTrackTestFunctionQuery = (i: number) => { + return { + type: 'pg_track_function', + args: { + function: `search_posts_${i}`, + schema: 'public', + source: 'default', + }, + }; +}; + +export const testCustomFunctionSQLWithSessArg = ( + name = 'customFunctionWithSessionArg' +) => { + return { + type: 'run_sql', + args: { + sql: `CREATE OR REPLACE FUNCTION ${name}( + hasura_session json, name text + ) RETURNS SETOF text_result LANGUAGE sql STABLE AS $$ + SELECT + q.* + FROM + ( + VALUES + (hasura_session ->> 'x-hasura-role') + ) q $$`, + cascade: false, + }, + }; +}; + +export const createUntrackedFunctionSQL = ( + fnName: string, + tableName: string +) => { + return { + type: 'run_sql', + args: { + sql: ` + CREATE OR REPLACE FUNCTION ${fnName}(table_row "${tableName}") + RETURNS int + LANGUAGE sql + STABLE + AS $function$ + SELECT table_row.id + $function$ + `, + cascade: false, + }, + }; +}; + +export const dropUntrackedFunctionSQL = (fnName: string) => { + return { + type: 'run_sql', + args: { + sql: ` + DROP FUNCTION public.${fnName}; + `, + cascade: false, + }, + }; +}; + +export const getTrackFnPayload = (name = 'customfunctionwithsessionarg') => ({ + type: 'pg_track_function', + args: { + function: name, + source: 'default', + schema: 'public', + }, +}); + +// has to go to query +export const createFunctionTable = () => { + return { + type: 'run_sql', + args: { + sql: 'create table post (id serial PRIMARY KEY,title TEXT,content TEXT);', + cascade: false, + }, + }; +}; +// has to go to metadata +export const trackCreateFunctionTable = () => { + return { + type: 'pg_track_table', + args: { + table: { + name: 'post', + schema: 'public', + }, + }, + }; +}; + +export const createSampleTable = () => ({ + type: 'run_sql', + source: 'default', + args: { + sql: 'CREATE TABLE text_result(result text);', + cascade: false, + }, +}); + +export const dropTableIfExists = ( + table: { name: string; schema: string }, + source = 'default' +) => ({ + type: 'run_sql', + source, + args: { + sql: `DROP TABLE IF EXISTS "${table.schema}"."${table.name}";`, + cascade: false, + }, +}); + +export const getTrackSampleTableQuery = () => { + return { + type: 'pg_track_table', + source: 'default', + args: { + table: { + name: 'text_result', + schema: 'public', + }, + }, + }; +}; +export const dropTable = (table = 'post', cascade = false) => { + return { + type: 'run_sql', + args: { + sql: `DROP table "public"."${table}"${cascade ? ' CASCADE;' : ';'}`, + cascade, + }, + }; +}; + +export const getSchema = () => 'public'; diff --git a/frontend/apps/console-ce-e2e/src/helpers/eventHelpers.ts b/frontend/apps/console-ce-e2e/src/helpers/eventHelpers.ts new file mode 100644 index 00000000000..148c4a2f6f9 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/helpers/eventHelpers.ts @@ -0,0 +1,35 @@ +import { ADMIN_SECRET_HEADER_KEY } from './constants'; + +export const baseUrl = Cypress.config('baseUrl'); +export const queryTypes = ['insert', 'update', 'delete']; + +export const getTriggerName = (i: number, testName = '') => + `Apic_test_trigger_${testName}_${i}`; + +export const getTableName = (i: number, testName = '') => + `Apic_test_table_${testName}_${i}`; + +export const getWebhookURL = () => 'http://httpbin.org/post'; +export const getNoOfRetries = () => '5'; +export const getIntervalSeconds = () => '10'; +export const getTimeoutSeconds = () => '25'; + +export const getElementFromAlias = (alias: string) => `[data-test=${alias}]`; +export const makeDataAPIUrl = (dataApiUrl: string) => `${dataApiUrl}/v1/query`; + +interface APIPayload { + [key: string]: any; +} +export const makeDataAPIOptions = ( + dataApiUrl: string, + key: string, + body: APIPayload +) => ({ + method: 'POST', + url: makeDataAPIUrl(dataApiUrl), + headers: { + [ADMIN_SECRET_HEADER_KEY]: key, + }, + body, + failOnStatusCode: false, +}); diff --git a/frontend/apps/console-ce-e2e/src/helpers/metadata.ts b/frontend/apps/console-ce-e2e/src/helpers/metadata.ts new file mode 100644 index 00000000000..bbd6c4acbe1 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/helpers/metadata.ts @@ -0,0 +1,17 @@ +export const replaceMetadata = (newMetadata: Record) => { + const postBody = { type: 'replace_metadata', args: newMetadata }; + cy.request('POST', 'http://localhost:8080/v1/metadata', postBody).then( + response => { + expect(response.body).to.have.property('message', 'success'); // true + } + ); +}; + +export const resetMetadata = () => { + const postBody = { type: 'clear_metadata', args: {} }; + cy.request('POST', 'http://localhost:8080/v1/metadata', postBody).then( + response => { + expect(response.body).to.have.property('message', 'success'); // true + } + ); +}; diff --git a/frontend/apps/console-ce-e2e/src/helpers/remoteSchemaHelpers.ts b/frontend/apps/console-ce-e2e/src/helpers/remoteSchemaHelpers.ts new file mode 100644 index 00000000000..e3670665e5b --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/helpers/remoteSchemaHelpers.ts @@ -0,0 +1,38 @@ +import { ADMIN_SECRET_HEADER_KEY } from './constants'; + +export const baseUrl = Cypress.config('baseUrl'); + +export const getRemoteSchemaName = (i: number, schemaName: string) => + `test-remote-schema-${schemaName}-${i}`; + +export const getRemoteGraphQLURL = () => + 'https://graphql-pokemon2.vercel.app/'; + +export const getRemoteGraphQLURLFromEnv = () => 'GRAPHQL_URL'; + +export const getInvalidRemoteSchemaUrl = () => 'http://httpbin.org/post'; + +export const getHeaderAccessKey = (i: string) => `ACCESS_KEY-${i}`; + +export const getHeaderAccessKeyValue = () => 'b94264abx98'; + +export const getElementFromAlias = (alias: string) => `[data-test=${alias}]`; + +export const makeDataAPIUrl = (dataApiUrl: string) => `${dataApiUrl}/v1/query`; + +export const makeDataAPIOptions = ( + dataApiUrl: string, + key: string, + body: { [key: string]: any } +) => ({ + method: 'POST', + url: makeDataAPIUrl(dataApiUrl), + headers: { + [ADMIN_SECRET_HEADER_KEY]: key, + }, + body, + failOnStatusCode: false, +}); + +export const getRemoteSchemaRoleName = (i: number, roleName: string) => + `test-role-${roleName}-${i}`; diff --git a/frontend/apps/console-ce-e2e/src/helpers/webhookTransformHelpers.ts b/frontend/apps/console-ce-e2e/src/helpers/webhookTransformHelpers.ts new file mode 100644 index 00000000000..040e793a5d8 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/helpers/webhookTransformHelpers.ts @@ -0,0 +1,82 @@ +import { getElementFromAlias } from './dataHelpers'; + +export const togglePayloadTransformSection = () => { + cy.getBySel('toggle-payload-transform').click({ + force: true, + }); +}; + +export const toggleRequestTransformSection = () => { + cy.getBySel('toggle-request-transform').click({ + force: true, + }); +}; + +export const clearRequestUrl = () => { + cy.get( + getElementFromAlias('transform-requestUrl') + ).type('{selectall}{backspace}', { force: true }); +}; + +export const typeIntoRequestUrl = (content: string) => { + cy.getBySel('transform-requestUrl').type(content, { + parseSpecialCharSequences: false, + }); +}; + +export const checkTransformRequestUrlError = ( + exists: boolean, + error?: string +) => { + if (exists) { + if (error) { + cy.getBySel('transform-requestUrl-error') + .should('exist') + .and('contain', error); + } else { + cy.getBySel('transform-requestUrl-error').should('exist'); + } + } else { + cy.getBySel('transform-requestUrl-error').should('not.exist'); + } +}; + +export const typeIntoRequestQueryParams = ( + queryParams: { key: string; value: string }[] +) => { + queryParams.forEach((q, i) => { + cy.getBySel(`transform-query-params-kv-key-${i}`).type(q.key, { + parseSpecialCharSequences: false, + }); + cy.getBySel(`transform-query-params-kv-value-${i}`).type(q.value, { + parseSpecialCharSequences: false, + }); + }); +}; + +export const checkTransformRequestUrlPreview = (previewText: string) => { + cy.getBySel('transform-requestUrl-preview').should('have.value', previewText); +}; + +export const clearPayloadTransformBody = (textArea: number) => { + cy.get('textarea').eq(textArea).type('{selectall}', { force: true }); + cy.get('textarea').eq(textArea).trigger('keydown', { + keyCode: 46, + which: 46, + force: true, + }); +}; + +export const typeIntoTransformBody = (content: string, textArea: number) => { + cy.get('textarea') + .eq(textArea) + .type(content, { force: true, parseSpecialCharSequences: false }); +}; + +export const checkTransformRequestBodyError = (exists: boolean) => { + if (exists) { + cy.getBySel('transform-requestBody-error').should('exist'); + } else { + cy.getBySel('transform-requestBody-error').should('not.exist'); + } +}; diff --git a/frontend/apps/console-ce-e2e/src/support/app.po.ts b/frontend/apps/console-ce-e2e/src/support/app.po.ts deleted file mode 100644 index 32934246969..00000000000 --- a/frontend/apps/console-ce-e2e/src/support/app.po.ts +++ /dev/null @@ -1 +0,0 @@ -export const getGreeting = () => cy.get('h1'); diff --git a/frontend/apps/console-ce-e2e/src/support/clearConsoleTextarea.ts b/frontend/apps/console-ce-e2e/src/support/clearConsoleTextarea.ts new file mode 100644 index 00000000000..f301b447161 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/clearConsoleTextarea.ts @@ -0,0 +1,11 @@ +/** + * Clear a Console's textarea. + * Work around cy.clear sometimes not working in the Console's textareas. + */ +Cypress.Commands.add('clearConsoleTextarea', { prevSubject: 'element' }, el => { + cy.wrap(el).type('{selectall}', { force: true }).trigger('keydown', { + keyCode: 46, + which: 46, + force: true, + }); +}); diff --git a/frontend/apps/console-ce-e2e/src/support/commands.ts b/frontend/apps/console-ce-e2e/src/support/commands.ts index 310f1fa0e04..5209117a287 100644 --- a/frontend/apps/console-ce-e2e/src/support/commands.ts +++ b/frontend/apps/console-ce-e2e/src/support/commands.ts @@ -7,19 +7,11 @@ // commands please read more here: // https://on.cypress.io/custom-commands // *********************************************** - -// eslint-disable-next-line @typescript-eslint/no-namespace -declare namespace Cypress { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface Chainable { - login(email: string, password: string): void; - } -} +// // // -- This is a parent command -- -Cypress.Commands.add('login', (email, password) => { - console.log('Custom command example: Login', email, password); -}); +// Cypress.Commands.add("login", (email, password) => { ... }) +// // // -- This is a child command -- // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) @@ -29,5 +21,16 @@ Cypress.Commands.add('login', (email, password) => { // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) // // -// -- This will overwrite an existing command -- +// -- This is will overwrite an existing command -- // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) + +import '@testing-library/cypress/add-commands'; + +import './visitEmptyPage'; +import './clearConsoleTextarea'; +import './notifications'; +import './contractIntercept'; + +Cypress.Commands.add('getBySel', (selector, ...args) => { + return cy.get(`[data-test=${selector}]`, ...args); +}); diff --git a/frontend/apps/console-ce-e2e/src/support/contractIntercept/contractIntercept.ts b/frontend/apps/console-ce-e2e/src/support/contractIntercept/contractIntercept.ts new file mode 100644 index 00000000000..ca38ce97eb1 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/contractIntercept/contractIntercept.ts @@ -0,0 +1,168 @@ +import type { + ContractRequest, + RunningTestsState, + StartContractInterceptOptions, +} from './types'; + +import { checkAndGetTestInfo } from './helpers/checkAndGetTestInfo'; +import { generateEmptyTestState } from './helpers/generateEmptyTestState'; + +const runningTestState: RunningTestsState = {}; + +/** + * A wrapper around `cy.intercept` that allows intercepting and recording the request/response series + * that made up the contract. + * + * It could be useful for sharing the contract's fixtures with the server folks. + * + * Be aware of the current limitations: + * 1. Only one test at the time is supported. `runningTestState` can store more tests at once but + * the fixture files are all saved starting from "1 - ", even if related to different tests + * 2. If you do not call cy.haltContractIntercept, no fixture files will be saved + * + * @see https://github.com/hasura/graphql-engine-mono/issues/4601 + */ +function startContractIntercept( + startContractInterceptOptions: StartContractInterceptOptions, + url: string +) { + const { thisTest, mode, createFixtureName } = startContractInterceptOptions; + const { testTitle, testPath } = checkAndGetTestInfo(thisTest); + + if (mode === 'disabled') { + Cypress.log({ + message: `*🤝 ❌ No Contract will be recorded for ${testTitle}*`, + }); + return; + } + + Cypress.log({ + message: `*🤝 ✅ Contract will be recorded for ${testTitle}*`, + }); + + runningTestState[testTitle] ??= generateEmptyTestState(testPath, testTitle); + + if (Object.keys(runningTestState).length > 1) { + throw new Error(`startContractIntercept support only one test at a time`); + } + // Start intercepting the requests + cy.intercept(url, request => { + // The recorded could have been halted + if (runningTestState[testTitle].halted) { + Cypress.log({ + message: `*🤝 ❌ Contract recording has been halted for: ${testTitle}*`, + }); + return; + } + + const fixtureName = createFixtureName(request); + if (fixtureName.includes('\\') || fixtureName.includes('/')) { + throw new Error( + `createFixtureName cannot return names that includes / or \\ like ${fixtureName}` + ); + } + + const contractLength = runningTestState[testTitle].contract.length; + + // start from 1 + const fixtureIndex = contractLength + 1; + const fixtureFileName = `${fixtureIndex}-${fixtureName}.json`; + + const recorded: ContractRequest = { + readme: + '////////// This fixture has been automatically generated through cy.startContractIntercept //////////', + request, + fixtureName, + fixtureFileName, + + // Temporary, empty, response + response: { + statusCode: undefined, + headers: undefined, + body: undefined, + }, + }; + // Add the request to the Contract + runningTestState[testTitle].contract.push(recorded); + + Cypress.log({ + message: `*🤝 ✅ Recorded ${fixtureFileName} in the contract*`, + consoleProps: () => request, + }); + + request.continue(response => { + // Add the request to the Contract too + recorded.response = response; + }); + }); +} + +/** + * Halt recording the contract and save the fixture files. + * Please note that it must be called just once + */ +function haltContractIntercept(options: { + thisTest: Mocha.Context; + saveFixtureFiles?: boolean; +}) { + const { thisTest, saveFixtureFiles = true } = options; + const { testTitle, testPath } = checkAndGetTestInfo(thisTest); + + if (!saveFixtureFiles) { + Cypress.log({ + message: `*🤝 ❌ No fixtures will be saved for this test: ${testTitle}*`, + }); + return; + } + + if (runningTestState[testTitle].halted) { + Cypress.log({ + message: `*🤝 ❌ Contract recording for this test has already been halted: ${testTitle}*`, + }); + } + + // Halt recording the requests for the current test. + // Please note that must be done asynchronously because of the double-run nature of the Cypress tests. + cy.wrap(null).then(() => { + Cypress.log({ + message: `*🤝 ❌ Halting the contract recording for this test: ${testTitle}*`, + }); + runningTestState[testTitle].halted = true; + }); + + // Split the current path + cy.task('splitPath', { path: testPath }).then(result => { + const splittedPath = result as string[]; + + // Remove the file name + splittedPath.pop(); + + // Create the directory + cy.task('joinPath', { path: [...splittedPath, 'fixtures'] }).then(path => { + cy.task('mkdirSync', { + dir: path as string, + }); + }); + + const testState = runningTestState[testTitle]; + + // Save all the files + for (let i = 0, n = testState.contract.length; i < n; i++) { + const request = testState.contract[i]; + + cy.task('joinPath', { + // Stores the fixture files close to the test file, in a "fixtures" directory + path: [...splittedPath, 'fixtures', request.fixtureFileName], + }).then(filePath => { + // Save the fixture file + cy.task('writeFileSync', { + file: filePath as string, + data: JSON.stringify(request, null, 2), + }); + }); + } + }); +} + +Cypress.Commands.add('startContractIntercept', startContractIntercept); +Cypress.Commands.add('haltContractIntercept', haltContractIntercept); diff --git a/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/checkAndGetTestInfo.ts b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/checkAndGetTestInfo.ts new file mode 100644 index 00000000000..e8c6600c41a --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/checkAndGetTestInfo.ts @@ -0,0 +1,24 @@ +import { generateTestTitle } from './generateTestTitle'; +import { throwIfCalledInTestHooks } from './throwIfCalledInTestHooks'; +import { throwIfCalledInsideArrowFunction } from './throwIfCalledInsideArrowFunction'; + +/** + * Perform pre-flight checks and return the most important test info. + */ +export function checkAndGetTestInfo(thisTest: Mocha.Context) { + const testTitle = generateTestTitle(thisTest); // ex. 'Describe 1 - Describe 2 - Describe 3 - Test title' + const testPath = thisTest?.invocationDetails?.relativeFile; + + throwIfCalledInTestHooks(thisTest.title); + throwIfCalledInsideArrowFunction(thisTest.title); + + // TS-only check, it should never happen at runtime + if (typeof testPath !== 'string') { + throw new Error(`No test Path available for ${testTitle}`); + } + + return { + testPath, + testTitle, + }; +} diff --git a/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateDescribesTitle.ts b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateDescribesTitle.ts new file mode 100644 index 00000000000..89a65717fe2 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateDescribesTitle.ts @@ -0,0 +1,27 @@ +/** + * Return an array containing all the "describe"s title. + * + * Ex. return ['Describe 1', 'Describe 2'] in this case + * describe('Describe 1', () => { + * describe('Describe 2', () => { + * it('Test', () => {}) + * }) + * }) + */ +export function generateDescribesTitle( + parent?: Mocha.Suite | undefined +): string[] { + if (!parent) return []; + + let parentNames: string[] = []; + // parents are recursive + if (parent.parent) { + parentNames = parentNames.concat(generateDescribesTitle(parent.parent)); + } + + if (parent.title !== '') { + parentNames.push(parent.title); + } + + return parentNames; +} diff --git a/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateDescribesTitle.unit.test.ts b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateDescribesTitle.unit.test.ts new file mode 100644 index 00000000000..8578696fcae --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateDescribesTitle.unit.test.ts @@ -0,0 +1,88 @@ +/// + +import { generateDescribesTitle } from './generateDescribesTitle'; + +// ------------------------------------------------------------------- +// TYPES ------------------------------------------------------------- +// ------------------------------------------------------------------- +// This is a simplified version of Mocha.Suite +type SimplifiedTestRoot = { + title: string; +}; + +type SimplifiedTestOrDescribe = { + parent: SimplifiedTestRoot | SimplifiedTestOrDescribe; + title: string; +}; + +type SimplifiedTestSuite = SimplifiedTestOrDescribe | SimplifiedTestRoot; +// ------------------------------------------------------------------- +// ------------------------------------------------------------------- +// ------------------------------------------------------------------- + +describe('generateDescribesTitle', () => { + it('Should return an array including all the titles of the chain of test.describe and the test itself', () => { + // Arrange + // In case of a test file like the following + // describe('Describe 1', () => { + // describe('Describe 2', () => { + // describe('Describe 3', () => { + // it('Test title', () => {}) + // }) + // }) + // }) + const testSuite: SimplifiedTestSuite = { + parent: { + parent: { + parent: { + title: 'Describe 1', + }, + title: 'Describe 2', + }, + title: 'Describe 3', + }, + title: 'Test title', + }; + + // Act + // testSuite is compatible for what concerns to generateDescribesTitle that will only look for the + // `parent` and `title` properties + const result = generateDescribesTitle(testSuite.parent as Mocha.Suite); + + // Assert + expect(result).to.deep.equal(['Describe 1', 'Describe 2', 'Describe 3']); + }); + + it('Should return an array including only the test title in case of no describes', () => { + // Act + // testSuite is compatible for what concerns to generateDescribesTitle that will only look for the + // `parent` and `title` properties + const result = generateDescribesTitle(undefined); + + // Assert + expect(result).to.deep.equal([]); + }); + + it('Should return an array including all the titles of the chain of test.describe and the test hook itself', () => { + // Arrange + // In case of a test file like the following + // describe('Describe 1', () => { + // before(() => {}) + // it('Test title', () => {}) + // }) + const testSuite: SimplifiedTestSuite = { + parent: { + title: 'Describe 1', + }, + title: '"before" hook', + }; + + // Act + // testSuite is compatible for what concerns to generateDescribesTitle that will only look for the + // `parent` and `title` properties + const result = generateDescribesTitle(testSuite.parent as Mocha.Suite); + + // Assert + expect(result).to.deep.equal(['Describe 1']); + }); +}); diff --git a/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateEmptyTestState.ts b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateEmptyTestState.ts new file mode 100644 index 00000000000..fec2f698cfa --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateEmptyTestState.ts @@ -0,0 +1,13 @@ +import type { RunningTestState } from '../types'; + +export function generateEmptyTestState( + testPath: string, + testTitle: string +): RunningTestState { + return { + testPath, + testTitle, + halted: false, + contract: [], + }; +} diff --git a/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateTestTitle.ts b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateTestTitle.ts new file mode 100644 index 00000000000..4d62987b77d --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateTestTitle.ts @@ -0,0 +1,19 @@ +import type { TestTitle } from '../types'; +import { generateDescribesTitle } from './generateDescribesTitle'; + +/** + * Return a string containing all the "describe"s titles and the test title concatenated. + * + * Ex. return 'Describe 1 - Describe 2 - Test' in this case + * describe('Describe 1', () => { + * describe('Describe 2', () => { + * it('Test', () => {}) + * }) + * }) + */ +export function generateTestTitle(thisTest: Mocha.Context): TestTitle { + const describesTitle = generateDescribesTitle(thisTest.parent); + const testTitle = thisTest.title; + + return [...describesTitle, testTitle].join(' - '); +} diff --git a/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateTestTitle.unit.test.ts b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateTestTitle.unit.test.ts new file mode 100644 index 00000000000..e57eaf485c4 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/generateTestTitle.unit.test.ts @@ -0,0 +1,57 @@ +/// + +import { generateTestTitle } from './generateTestTitle'; + +// ------------------------------------------------------------------- +// TYPES ------------------------------------------------------------- +// ------------------------------------------------------------------- +// This is a simplified version of Mocha.Suite +type SimplifiedTestRoot = { + title: string; +}; + +type SimplifiedTestOrDescribe = { + parent: SimplifiedTestRoot | SimplifiedTestOrDescribe; + title: string; +}; + +type SimplifiedTestSuite = SimplifiedTestOrDescribe; +// ------------------------------------------------------------------- +// ------------------------------------------------------------------- +// ------------------------------------------------------------------- + +describe('generateTestTitle', () => { + it('Should return an array including all the titles of the chain of test.describe and the test itself', () => { + // Arrange + // In case of a test file like the following + // describe('Describe 1', () => { + // describe('Describe 2', () => { + // describe('Describe 3', () => { + // it('Test title', () => {}) + // }) + // }) + // }) + const testSuite: SimplifiedTestSuite = { + parent: { + parent: { + parent: { + title: 'Describe 1', + }, + title: 'Describe 2', + }, + title: 'Describe 3', + }, + title: 'Test title', + }; + + // Act + // testSuite is compatible for what concerns to generateTestTitle that will only look for the + // `parent` and `title` properties + const result = generateTestTitle((testSuite as unknown) as Mocha.Context); + + // Assert + expect(result).to.deep.equal( + 'Describe 1 - Describe 2 - Describe 3 - Test title' + ); + }); +}); diff --git a/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/splitPathTask.unit.test.ts b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/splitPathTask.unit.test.ts new file mode 100644 index 00000000000..2f7fad2e869 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/splitPathTask.unit.test.ts @@ -0,0 +1,18 @@ +/// + +describe(`cy.task('splitPath')`, () => { + it('Should split the passed path', () => { + const path = + 'cypress/support/interceptAndRecordContract/helpers/temp.unit.test.ts'; + + cy.task('splitPath', { path }).then(result => { + expect(result).to.deep.equal([ + 'cypress', + 'support', + 'interceptAndRecordContract', + 'helpers', + 'temp.unit.test.ts', + ]); + }); + }); +}); diff --git a/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInTestHooks.ts b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInTestHooks.ts new file mode 100644 index 00000000000..b880164fff8 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInTestHooks.ts @@ -0,0 +1,25 @@ +/** + * Throws if called from inside a test hook. + * The problem is that it's impossible to know which test the hook relates to. Hence, it's + * impossible to detect the name of the test. + */ +export function throwIfCalledInTestHooks(testTitle: string) { + switch (testTitle) { + case '"after" hook': + throw new Error( + 'interceptAndRecordContract cannot be called inside a "after" hook' + ); + case '"after all" hook': + throw new Error( + 'interceptAndRecordContract cannot be called inside a "after all" hook' + ); + case '"before" hook': + throw new Error( + 'interceptAndRecordContract cannot be called inside a "before" hook' + ); + case '"before all" hook': + throw new Error( + 'interceptAndRecordContract cannot be called inside a "before all" hook' + ); + } +} diff --git a/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInTestHooks.unit.test.ts b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInTestHooks.unit.test.ts new file mode 100644 index 00000000000..4cf1f4858ac --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInTestHooks.unit.test.ts @@ -0,0 +1,55 @@ +/// + +import { throwIfCalledInTestHooks } from './throwIfCalledInTestHooks'; + +describe('throwIfCalledInTestHooks', () => { + it('Should throw an error if called with "after" hook', () => { + // Arrange + const executor = () => throwIfCalledInTestHooks('"after" hook'); + + // Assert + expect(executor).to.throw( + 'interceptAndRecordContract cannot be called inside a "after" hook' + ); + }); + + it('Should throw an error if called with "after all" hook', () => { + // Arrange + const executor = () => throwIfCalledInTestHooks('"after all" hook'); + + // Assert + expect(executor).to.throw( + 'interceptAndRecordContract cannot be called inside a "after all" hook' + ); + }); + + it('Should throw an error if called with "before" hook', () => { + // Arrange + const executor = () => throwIfCalledInTestHooks('"before" hook'); + + // Assert + expect(executor).to.throw( + 'interceptAndRecordContract cannot be called inside a "before" hook' + ); + }); + + it('Should throw an error if called with "before all" hook', () => { + // Arrange + const executor = () => throwIfCalledInTestHooks('"before all" hook'); + + // Assert + expect(executor).to.throw( + 'interceptAndRecordContract cannot be called inside a "before all" hook' + ); + }); + + it('Should not throw an error if called with another test title', () => { + // Arrange + const executor = () => throwIfCalledInTestHooks('Test title'); + + // Assert + expect(executor).not.to.throw( + 'interceptAndRecordContract cannot be called inside a "before all" hook' + ); + }); +}); diff --git a/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInsideArrowFunction.ts b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInsideArrowFunction.ts new file mode 100644 index 00000000000..8ae2e7a3979 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInsideArrowFunction.ts @@ -0,0 +1,19 @@ +/** + * Throw if interceptAndRecordContract has been called without the `this.test` element. + * + * It happens when the test is called inside an arrow function + * ex. + * it('...', () => { // <- THIS is the problem + * cy.startContractIntercept({ + * thisTest: this.test, + * // ... + */ +export function throwIfCalledInsideArrowFunction( + thisTest: Mocha.Context | Record +) { + if (Object.keys(thisTest).length === 0) { + throw new Error( + 'interceptAndRecordContract did not receive `this` that refers to the test itself. Have you called interceptAndRecordContract inside an arrow function? If yes, transform function into a regular one' + ); + } +} diff --git a/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInsideArrowFunction.unit.test.ts b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInsideArrowFunction.unit.test.ts new file mode 100644 index 00000000000..d60fcfc300d --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/contractIntercept/helpers/throwIfCalledInsideArrowFunction.unit.test.ts @@ -0,0 +1,15 @@ +/// + +import { throwIfCalledInsideArrowFunction } from './throwIfCalledInsideArrowFunction'; + +describe('throwIfCalledInsideArrowFunction', () => { + it('Should throw an error if called without a the reference to the `this.test`', () => { + // Arrange + const executor = () => throwIfCalledInsideArrowFunction({}); + + // Assert + expect(executor).to.throw( + 'interceptAndRecordContract did not receive `this` that refers to the test itself. Have you called interceptAndRecordContract inside an arrow function? If yes, transform function into a regular one' + ); + }); +}); diff --git a/frontend/apps/console-ce-e2e/src/support/contractIntercept/index.ts b/frontend/apps/console-ce-e2e/src/support/contractIntercept/index.ts new file mode 100644 index 00000000000..93c4565ff9b --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/contractIntercept/index.ts @@ -0,0 +1 @@ +export * from './contractIntercept'; diff --git a/frontend/apps/console-ce-e2e/src/support/contractIntercept/types.ts b/frontend/apps/console-ce-e2e/src/support/contractIntercept/types.ts new file mode 100644 index 00000000000..e9786fbcb9e --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/contractIntercept/types.ts @@ -0,0 +1,67 @@ +import type { CyHttpMessages } from 'cypress/types/net-stubbing'; + +// ------------------------------------------------------------------- +// PACT TYPES -------------------------------------------------------- +// ------------------------------------------------------------------- + +// Borrowed from Pact's types https://github.com/pactflow/pact-cypress-adapter/blob/main/src/types.ts +export type HeaderType = Record | undefined; + +type BaseXHR = { + headers: HeaderType; + body: any | undefined; +}; + +export type XHRRequestAndResponse = { + request: { + method: string; + url: string; + } & BaseXHR; + response: { + statusCode: string | number | undefined; + // statusText: string | undefined; + } & BaseXHR; +}; + +// ------------------------------------------------------------------- +// CONTRACT RECORDING TYPES ------------------------------------------ +// ------------------------------------------------------------------- + +export type TestTitle = string; +type TestPath = string; + +export type ContractRequest = XHRRequestAndResponse & { + readme: string; + fixtureName: string; + fixtureFileName: string; +}; + +export type RunningTestState = { + testTitle: TestTitle; + testPath: TestPath; + halted: boolean; + + contract: ContractRequest[]; +}; + +/** + * Considering the current limitation of "only one test at a time can be recorded", the whole state + * and startContractIntercept could be simplified a lot by: + * 1. Transforming the State into a Map + * 2. Using the test instance, instead of the test name, to store the test state + * 3. Getting read of generateTestTitle and its tests + * + * Please note that this would result also in not having the test name in the cypress logs but this + * is not important since only a single test can be recorded at a time... + */ +export type RunningTestsState = Record; + +// ------------------------------------------------------------------- +// OPTIONS TYPES ----------------------------------------------------- +// ------------------------------------------------------------------- + +export type StartContractInterceptOptions = { + thisTest: Mocha.Context; + mode: 'record' | 'disabled'; + createFixtureName: (req: CyHttpMessages.IncomingHttpRequest) => string; +}; diff --git a/frontend/apps/console-ce-e2e/src/support/index.d.ts b/frontend/apps/console-ce-e2e/src/support/index.d.ts new file mode 100644 index 00000000000..50f8c08717a --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/index.d.ts @@ -0,0 +1,73 @@ +// type definition for all custom commands +declare namespace Cypress { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Chainable { + /** + * Custom command to select DOM element by data-test attribute. + * + * @example cy.getBySel('greeting') + */ + getBySel(value: string): Chainable>; + + /** + * Custom command to work around the fact that cy.clear sometimes fails at clearing the + * Console's textarea + * @example cy.get('textarea').clearConsoleTextarea() + */ + clearConsoleTextarea(): Chainable>; + + /** + * Visit the initial empty page. + * Console's textarea + * @example cy.visitEmptyPage() + */ + visitEmptyPage(): Chainable; + + /** + * Success notifications + */ + expectSuccessNotification(): Chainable; + expectSuccessNotificationWithTitle(title: string): Chainable; + expectSuccessNotificationWithMessage(message: string): Chainable; + + /** + * Error notifications + */ + expectErrorNotification(): Chainable; + expectErrorNotificationWithTitle(title: string): Chainable; + expectErrorNotificationWithMessage(message: string): Chainable; + + /** + * Start intercepting the request/response contract between the Console and the server. + * @example + * cy.startContractIntercept( + * { + * thisTest: this.test, + * mode: 'record', + * createFixtureName: (req: CyHttpMessages.IncomingHttpRequest) => { + * if (req.url.endsWith('v1/metadata')) { + * return `v1-metadata-${req.body.type}`; + * } + * + * throw new Error(`Unknown url ${req.url}`); + * }, + * }, + * 'http://localhost:8080/**' + * ); + */ + startContractIntercept( + startContractInterceptOptions: import('./contractIntercept/types').StartContractInterceptOptions, + url: string + ): Chainable; + + /** + * Halt intercepting the request/response contract between the Console and the server and save the fixtures. + * @example + * cy.haltContractIntercept({ thisTest: this.test }) + */ + haltContractIntercept(options: { + thisTest: Mocha.Context; + saveFixtureFiles?: boolean; + }): Chainable; + } +} diff --git a/frontend/apps/console-ce-e2e/src/support/notifications.ts b/frontend/apps/console-ce-e2e/src/support/notifications.ts new file mode 100644 index 00000000000..664a2370239 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/notifications.ts @@ -0,0 +1,33 @@ +Cypress.Commands.add('expectSuccessNotification', () => { + cy.get('.notification-success').should('be.visible'); +}); + +Cypress.Commands.add('expectSuccessNotificationWithTitle', (title: string) => { + cy.get('.notification-success').should('be.visible').should('contain', title); +}); + +Cypress.Commands.add( + 'expectSuccessNotificationWithMessage', + (message: string) => { + cy.get('.notification-success') + .should('be.visible') + .should('contain', message); + } +); + +Cypress.Commands.add('expectErrorNotification', () => { + cy.get('.notification-error').should('be.visible'); +}); + +Cypress.Commands.add('expectErrorNotificationWithTitle', (title: string) => { + cy.get('.notification-error').should('be.visible').should('contain', title); +}); + +Cypress.Commands.add( + 'expectErrorNotificationWithMessage', + (message: string) => { + cy.get('.notification-error') + .should('be.visible') + .should('contain', message); + } +); diff --git a/frontend/apps/console-ce-e2e/src/support/tasks/index.ts b/frontend/apps/console-ce-e2e/src/support/tasks/index.ts new file mode 100644 index 00000000000..a7feb76c291 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/tasks/index.ts @@ -0,0 +1,4 @@ +export { joinPath } from './joinPath'; +export { mkdirSync } from './mkdirSync'; +export { splitPath } from './splitPath'; +export { writeFileSync } from './writeFileSync'; diff --git a/frontend/apps/console-ce-e2e/src/support/tasks/joinPath.ts b/frontend/apps/console-ce-e2e/src/support/tasks/joinPath.ts new file mode 100644 index 00000000000..4d22f63d2df --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/tasks/joinPath.ts @@ -0,0 +1,12 @@ +import * as nodePath from 'node:path'; + +interface Options { + path: string[]; +} + +/** + * Join the given path using the OS-based separator. + */ +export function joinPath(options: Options) { + return options.path.join(nodePath.sep); +} diff --git a/frontend/apps/console-ce-e2e/src/support/tasks/mkdirSync.ts b/frontend/apps/console-ce-e2e/src/support/tasks/mkdirSync.ts new file mode 100644 index 00000000000..1579db98836 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/tasks/mkdirSync.ts @@ -0,0 +1,16 @@ +import * as fs from 'fs'; + +interface Options { + dir: string; +} + +/** + * Wrapper task around mkdirSync. + */ +export function mkdirSync(options: Options) { + if (fs.existsSync(options.dir)) return null; + + fs.mkdirSync(options.dir); + + return null; +} diff --git a/frontend/apps/console-ce-e2e/src/support/tasks/splitPath.ts b/frontend/apps/console-ce-e2e/src/support/tasks/splitPath.ts new file mode 100644 index 00000000000..add72de8586 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/tasks/splitPath.ts @@ -0,0 +1,12 @@ +import * as nodePath from 'node:path'; + +interface Options { + path: string; +} + +/** + * Split the given path using the OS-based separator. + */ +export function splitPath(options: Options) { + return options.path.split(nodePath.sep); +} diff --git a/frontend/apps/console-ce-e2e/src/support/tasks/writeFileSync.ts b/frontend/apps/console-ce-e2e/src/support/tasks/writeFileSync.ts new file mode 100644 index 00000000000..96a7d5b2933 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/tasks/writeFileSync.ts @@ -0,0 +1,15 @@ +import * as fs from 'fs'; + +interface Options { + file: string; + data: string; +} + +/** + * Wrapper task around writeFileSync. + */ +export function writeFileSync(options: Options) { + fs.writeFileSync(options.file, options.data); + + return null; +} diff --git a/frontend/apps/console-ce-e2e/src/support/visitEmptyPage.ts b/frontend/apps/console-ce-e2e/src/support/visitEmptyPage.ts new file mode 100644 index 00000000000..366c5c88da0 --- /dev/null +++ b/frontend/apps/console-ce-e2e/src/support/visitEmptyPage.ts @@ -0,0 +1,10 @@ +/** + * Visit the initial empty page. + * + * @see https://glebbahmutov.com/blog/visit-blank-page-between-tests/ + */ +Cypress.Commands.add('visitEmptyPage', { prevSubject: false }, () => { + cy.window().then(win => { + win.location.href = 'about:blank'; + }); +}); diff --git a/frontend/apps/console-ce-e2e/tsconfig.json b/frontend/apps/console-ce-e2e/tsconfig.json index cc509a730e1..a159de92ffb 100644 --- a/frontend/apps/console-ce-e2e/tsconfig.json +++ b/frontend/apps/console-ce-e2e/tsconfig.json @@ -4,7 +4,7 @@ "sourceMap": false, "outDir": "../../dist/out-tsc", "allowJs": true, - "types": ["cypress", "node"] + "types": ["cypress", "node", "@testing-library/cypress"] }, "include": ["src/**/*.ts", "src/**/*.js", "cypress.config.ts"] } diff --git a/frontend/apps/console-pro-e2e/.eslintrc.json b/frontend/apps/console-pro-e2e/.eslintrc.json deleted file mode 100644 index 696cb8b1212..00000000000 --- a/frontend/apps/console-pro-e2e/.eslintrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], - "ignorePatterns": ["!**/*"], - "overrides": [ - { - "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], - "rules": {} - } - ] -} diff --git a/frontend/apps/console-pro-e2e/cypress.config.ts b/frontend/apps/console-pro-e2e/cypress.config.ts deleted file mode 100644 index 22f7c84eb63..00000000000 --- a/frontend/apps/console-pro-e2e/cypress.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { defineConfig } from 'cypress'; -import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset'; - -export default defineConfig({ - e2e: nxE2EPreset(__dirname), -}); diff --git a/frontend/apps/console-pro-e2e/project.json b/frontend/apps/console-pro-e2e/project.json deleted file mode 100644 index ca1e3e2e32b..00000000000 --- a/frontend/apps/console-pro-e2e/project.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "apps/console-pro-e2e/src", - "projectType": "application", - "targets": { - "e2e": { - "executor": "@nrwl/cypress:cypress", - "options": { - "cypressConfig": "apps/console-pro-e2e/cypress.config.ts", - "devServerTarget": "console-pro:serve:development", - "testingType": "e2e" - }, - "configurations": { - "production": { - "devServerTarget": "console-pro:serve:production" - } - } - }, - "lint": { - "executor": "@nrwl/linter:eslint", - "outputs": ["{options.outputFile}"], - "options": { - "lintFilePatterns": ["apps/console-pro-e2e/**/*.{js,ts}"] - } - } - }, - "tags": [], - "implicitDependencies": ["console-pro"] -} diff --git a/frontend/apps/console-pro-e2e/src/e2e/app.cy.ts b/frontend/apps/console-pro-e2e/src/e2e/app.cy.ts deleted file mode 100644 index b142714607c..00000000000 --- a/frontend/apps/console-pro-e2e/src/e2e/app.cy.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { getGreeting } from '../support/app.po'; - -describe('console-pro', () => { - beforeEach(() => cy.visit('/')); - - it('should display welcome message', () => { - // Custom command example, see `../support/commands.ts` file - cy.login('my-email@something.com', 'myPassword'); - - // Function helper example, see `../support/app.po.ts` file - getGreeting().contains('Welcome console-pro'); - }); -}); diff --git a/frontend/apps/console-pro-e2e/src/fixtures/example.json b/frontend/apps/console-pro-e2e/src/fixtures/example.json deleted file mode 100644 index 294cbed6ce9..00000000000 --- a/frontend/apps/console-pro-e2e/src/fixtures/example.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "Using fixtures to represent data", - "email": "hello@cypress.io" -} diff --git a/frontend/apps/console-pro-e2e/src/support/app.po.ts b/frontend/apps/console-pro-e2e/src/support/app.po.ts deleted file mode 100644 index 32934246969..00000000000 --- a/frontend/apps/console-pro-e2e/src/support/app.po.ts +++ /dev/null @@ -1 +0,0 @@ -export const getGreeting = () => cy.get('h1'); diff --git a/frontend/apps/console-pro-e2e/src/support/commands.ts b/frontend/apps/console-pro-e2e/src/support/commands.ts deleted file mode 100644 index 310f1fa0e04..00000000000 --- a/frontend/apps/console-pro-e2e/src/support/commands.ts +++ /dev/null @@ -1,33 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** - -// eslint-disable-next-line @typescript-eslint/no-namespace -declare namespace Cypress { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface Chainable { - login(email: string, password: string): void; - } -} -// -// -- This is a parent command -- -Cypress.Commands.add('login', (email, password) => { - console.log('Custom command example: Login', email, password); -}); -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/frontend/apps/console-pro-e2e/src/support/e2e.ts b/frontend/apps/console-pro-e2e/src/support/e2e.ts deleted file mode 100644 index 3d469a6b6cf..00000000000 --- a/frontend/apps/console-pro-e2e/src/support/e2e.ts +++ /dev/null @@ -1,17 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands'; diff --git a/frontend/apps/console-pro-e2e/tsconfig.json b/frontend/apps/console-pro-e2e/tsconfig.json deleted file mode 100644 index cc509a730e1..00000000000 --- a/frontend/apps/console-pro-e2e/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "sourceMap": false, - "outDir": "../../dist/out-tsc", - "allowJs": true, - "types": ["cypress", "node"] - }, - "include": ["src/**/*.ts", "src/**/*.js", "cypress.config.ts"] -} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7738b93efda..f23c25e493d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -176,6 +176,7 @@ "cypress": "^10.2.0", "eslint": "~8.15.0", "eslint-config-prettier": "8.1.0", + "eslint-plugin-chai-friendly": "^0.7.2", "eslint-plugin-cypress": "^2.10.3", "eslint-plugin-import": "2.26.0", "eslint-plugin-jsx-a11y": "6.6.1", @@ -506,6 +507,7 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.13.tgz", "integrity": "sha512-BQKE9kXkPlXHPeqissfxo0lySWJcYdEP0hdtJOH/iJfDdhOCcgtNCjftCJg3qqauB4h+lz2N6ixM++b9DN1Tcw==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", "@babel/generator": "^7.12.13", @@ -535,6 +537,7 @@ "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -22867,6 +22870,18 @@ "node": ">=4" } }, + "node_modules/eslint-plugin-chai-friendly": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-0.7.2.tgz", + "integrity": "sha512-LOIfGx5sZZ5FwM1shr2GlYAWV9Omdi+1/3byuVagvQNoGUuU0iHhp7AfjA1uR+4dJ4Isfb4+FwBJgQajIw9iAg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "eslint": ">=3.0.0" + } + }, "node_modules/eslint-plugin-cypress": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.12.1.tgz", @@ -34180,7 +34195,8 @@ "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true }, "node_modules/react-json-view": { "version": "1.21.3", @@ -38933,6 +38949,7 @@ "version": "4.7.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -41116,6 +41133,7 @@ "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "dev": true, "engines": { "node": ">=8.3.0" }, @@ -41555,6 +41573,7 @@ "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.13.tgz", "integrity": "sha512-BQKE9kXkPlXHPeqissfxo0lySWJcYdEP0hdtJOH/iJfDdhOCcgtNCjftCJg3qqauB4h+lz2N6ixM++b9DN1Tcw==", + "dev": true, "requires": { "@babel/code-frame": "^7.12.13", "@babel/generator": "^7.12.13", @@ -41576,7 +41595,8 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==" + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true } } }, @@ -44157,8 +44177,7 @@ "ws": { "version": "8.8.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", - "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", - "requires": {} + "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==" } } }, @@ -44220,8 +44239,7 @@ "@hookform/resolvers": { "version": "2.8.10", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.8.10.tgz", - "integrity": "sha512-DDFtNlugsbwAhCJHYp3NcN5LvJrwSsCLPi41Wo5O8UAIbUFnBfY/jW+zKnlX57BZ4jE0j/g6R9rB3JlO89ad0g==", - "requires": {} + "integrity": "sha512-DDFtNlugsbwAhCJHYp3NcN5LvJrwSsCLPi41Wo5O8UAIbUFnBfY/jW+zKnlX57BZ4jE0j/g6R9rB3JlO89ad0g==" }, "@humanwhocodes/config-array": { "version": "0.9.5", @@ -44722,8 +44740,7 @@ "@mdx-js/react": { "version": "1.6.22", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-1.6.22.tgz", - "integrity": "sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg==", - "requires": {} + "integrity": "sha512-TDoPum4SHdfPiGSAaRBw7ECyI8VaHpK8GJugbJIJuqyh6kzw9ZLJZW3HGL3NNrJGxcAixUvqROm+YuQOo5eXtg==" }, "@mdx-js/util": { "version": "1.6.22", @@ -44778,8 +44795,7 @@ "@n1ru4l/graphql-live-query": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@n1ru4l/graphql-live-query/-/graphql-live-query-0.10.0.tgz", - "integrity": "sha512-qZ7OHH/NB0NcG/Xa7irzgjE63UH0CkofZT0Bw4Ko6iRFagPRHBM8RgFXwTt/6JbFGIEUS4STRtaFoc/Eq/ZtzQ==", - "requires": {} + "integrity": "sha512-qZ7OHH/NB0NcG/Xa7irzgjE63UH0CkofZT0Bw4Ko6iRFagPRHBM8RgFXwTt/6JbFGIEUS4STRtaFoc/Eq/ZtzQ==" }, "@nodelib/fs.scandir": { "version": "2.1.5", @@ -45076,57 +45092,49 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.3.1.tgz", "integrity": "sha512-jDBKArXYO1u0B1dmd2Nf8Oy6aTF5vLDfLoO9Oon/GLkqZ/NiggYWZA+a2HpUMH4ITwNqS3z43k8LWApB8S583w==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-remove-jsx-attribute": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-6.3.1.tgz", "integrity": "sha512-dQzyJ4prwjcFd929T43Z8vSYiTlTu8eafV40Z2gO7zy/SV5GT+ogxRJRBIKWomPBOiaVXFg3jY4S5hyEN3IBjQ==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-remove-jsx-empty-expression": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-6.3.1.tgz", "integrity": "sha512-HBOUc1XwSU67fU26V5Sfb8MQsT0HvUyxru7d0oBJ4rA2s4HW3PhyAPC7fV/mdsSGpAvOdd8Wpvkjsr0fWPUO7A==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-replace-jsx-attribute-value": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-6.3.1.tgz", "integrity": "sha512-C12e6aN4BXAolRrI601gPn5MDFCRHO7C4TM8Kks+rDtl8eEq+NN1sak0eAzJu363x3TmHXdZn7+Efd2nr9I5dA==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-svg-dynamic-title": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-6.3.1.tgz", "integrity": "sha512-6NU55Mmh3M5u2CfCCt6TX29/pPneutrkJnnDCHbKZnjukZmmgUAZLtZ2g6ZoSPdarowaQmAiBRgAHqHmG0vuqA==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-svg-em-dimensions": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-6.3.1.tgz", "integrity": "sha512-HV1NGHYTTe1vCNKlBgq/gKuCSfaRlKcHIADn7P8w8U3Zvujdw1rmusutghJ1pZJV7pDt3Gt8ws+SVrqHnBO/Qw==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-transform-react-native-svg": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-6.3.1.tgz", "integrity": "sha512-2wZhSHvTolFNeKDAN/ZmIeSz2O9JSw72XD+o2bNp2QAaWqa8KGpn5Yk5WHso6xqfSAiRzAE+GXlsrBO4UP9LLw==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-plugin-transform-svg-component": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-6.3.1.tgz", "integrity": "sha512-cZ8Tr6ZAWNUFfDeCKn/pGi976iWSkS8ijmEYKosP+6ktdZ7lW9HVLHojyusPw3w0j8PI4VBeWAXAmi/2G7owxw==", - "dev": true, - "requires": {} + "dev": true }, "@svgr/babel-preset": { "version": "6.3.1", @@ -46295,8 +46303,7 @@ "redux-thunk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.1.tgz", - "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==", - "requires": {} + "integrity": "sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q==" } } }, @@ -50247,8 +50254,7 @@ "version": "8.8.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", - "dev": true, - "requires": {} + "dev": true }, "y18n": { "version": "4.0.3", @@ -54341,15 +54347,13 @@ "version": "1.8.0", "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", - "dev": true, - "requires": {} + "dev": true }, "acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "acorn-node": { "version": "1.8.2", @@ -54442,8 +54446,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true, - "requires": {} + "dev": true }, "ajv-formats": { "version": "2.1.1", @@ -54478,8 +54481,7 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} + "dev": true }, "ansi-align": { "version": "3.0.1", @@ -57088,8 +57090,7 @@ "cosmiconfig-typescript-loader": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-3.1.1.tgz", - "integrity": "sha512-SR5/NciF0vyYqcGsmB9WJ4QOKkcSSSzcBPLrnT6094BYahMy0eImWvlH3zoEOYqpF2zgiyAKHtWTXTo+fqgxPg==", - "requires": {} + "integrity": "sha512-SR5/NciF0vyYqcGsmB9WJ4QOKkcSSSzcBPLrnT6094BYahMy0eImWvlH3zoEOYqpF2zgiyAKHtWTXTo+fqgxPg==" }, "cp-file": { "version": "7.0.0", @@ -57657,8 +57658,7 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.3.0.tgz", "integrity": "sha512-OGT677UGHJTAVMRhPO+HJ4oKln3wkBTwtDFH0ojbqm+MJm6xuDMHp2nkhh/ThaBqq20IbraBQSWKfSLNHQO9Og==", - "dev": true, - "requires": {} + "dev": true }, "css-loader": { "version": "6.7.1", @@ -57859,8 +57859,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", - "dev": true, - "requires": {} + "dev": true }, "csso": { "version": "4.2.0", @@ -59144,8 +59143,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.1.0.tgz", "integrity": "sha512-oKMhGv3ihGbCIimCAjqkdzx2Q+jthoqnXSP+d86M9tptwugycmTFdVR4IpLgq2c4SHifbwO90z2fQ8/Aio73yw==", - "dev": true, - "requires": {} + "dev": true }, "eslint-import-resolver-node": { "version": "0.3.6", @@ -59238,6 +59236,12 @@ } } }, + "eslint-plugin-chai-friendly": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-chai-friendly/-/eslint-plugin-chai-friendly-0.7.2.tgz", + "integrity": "sha512-LOIfGx5sZZ5FwM1shr2GlYAWV9Omdi+1/3byuVagvQNoGUuU0iHhp7AfjA1uR+4dJ4Isfb4+FwBJgQajIw9iAg==", + "dev": true + }, "eslint-plugin-cypress": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.12.1.tgz", @@ -59403,8 +59407,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", - "dev": true, - "requires": {} + "dev": true }, "eslint-scope": { "version": "5.1.1", @@ -60785,8 +60788,7 @@ "graphiql-explorer": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/graphiql-explorer/-/graphiql-explorer-0.6.2.tgz", - "integrity": "sha512-hYSM+TI/0IAXltMOL7YXrvnA5xrKoDjjN7qiksxca2DY7yu46cyHVHG0IKIrBozMDBQLvFOhQMPrzplErwVZ1g==", - "requires": {} + "integrity": "sha512-hYSM+TI/0IAXltMOL7YXrvnA5xrKoDjjN7qiksxca2DY7yu46cyHVHG0IKIrBozMDBQLvFOhQMPrzplErwVZ1g==" }, "graphql": { "version": "14.5.8", @@ -60915,8 +60917,7 @@ "graphql-ws": { "version": "5.9.1", "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.9.1.tgz", - "integrity": "sha512-mL/SWGBwIT9Meq0NlfS55yXXTOeWPMbK7bZBEZhFu46bcGk1coTx2Sdtzxdk+9yHWngD+Fk1PZDWaAutQa9tpw==", - "requires": {} + "integrity": "sha512-mL/SWGBwIT9Meq0NlfS55yXXTOeWPMbK7bZBEZhFu46bcGk1coTx2Sdtzxdk+9yHWngD+Fk1PZDWaAutQa9tpw==" }, "handle-thing": { "version": "2.0.1", @@ -61534,8 +61535,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "requires": {} + "dev": true }, "identity-obj-proxy": { "version": "3.0.0", @@ -62330,8 +62330,7 @@ "isomorphic-ws": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", - "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", - "requires": {} + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==" }, "isstream": { "version": "0.1.2", @@ -62723,8 +62722,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true, - "requires": {} + "dev": true }, "jest-regex-util": { "version": "27.5.1", @@ -63180,14 +63178,12 @@ "jss-default-unit": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/jss-default-unit/-/jss-default-unit-8.0.2.tgz", - "integrity": "sha512-WxNHrF/18CdoAGw2H0FqOEvJdREXVXLazn7PQYU7V6/BWkCV0GkmWsppNiExdw8dP4TU1ma1dT9zBNJ95feLmg==", - "requires": {} + "integrity": "sha512-WxNHrF/18CdoAGw2H0FqOEvJdREXVXLazn7PQYU7V6/BWkCV0GkmWsppNiExdw8dP4TU1ma1dT9zBNJ95feLmg==" }, "jss-global": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/jss-global/-/jss-global-3.0.0.tgz", - "integrity": "sha512-wxYn7vL+TImyQYGAfdplg7yaxnPQ9RaXY/cIA8hawaVnmmWxDHzBK32u1y+RAvWboa3lW83ya3nVZ/C+jyjZ5Q==", - "requires": {} + "integrity": "sha512-wxYn7vL+TImyQYGAfdplg7yaxnPQ9RaXY/cIA8hawaVnmmWxDHzBK32u1y+RAvWboa3lW83ya3nVZ/C+jyjZ5Q==" }, "jss-nested": { "version": "6.0.1", @@ -63210,8 +63206,7 @@ "jss-props-sort": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/jss-props-sort/-/jss-props-sort-6.0.0.tgz", - "integrity": "sha512-E89UDcrphmI0LzmvYk25Hp4aE5ZBsXqMWlkFXS0EtPkunJkRr+WXdCNYbXbksIPnKlBenGB9OxzQY+mVc70S+g==", - "requires": {} + "integrity": "sha512-E89UDcrphmI0LzmvYk25Hp4aE5ZBsXqMWlkFXS0EtPkunJkRr+WXdCNYbXbksIPnKlBenGB9OxzQY+mVc70S+g==" }, "jss-vendor-prefixer": { "version": "7.0.0", @@ -63439,8 +63434,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/little-state-machine/-/little-state-machine-4.4.1.tgz", "integrity": "sha512-ZIBJz3dYmLBThE695yScD5biTdoG0qC+roWB6LFztWUhmxta6L9yjWdlBAyX/5cOB/rUPgWLT+5sPK+ZjAfeZw==", - "dev": true, - "requires": {} + "dev": true }, "load-json-file": { "version": "1.1.0", @@ -64119,8 +64113,7 @@ "meros": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/meros/-/meros-1.2.0.tgz", - "integrity": "sha512-3QRZIS707pZQnijHdhbttXRWwrHhZJ/gzolneoxKVz9N/xmsvY/7Ls8lpnI9gxbgxjcHsAVEW3mgwiZCo6kkJQ==", - "requires": {} + "integrity": "sha512-3QRZIS707pZQnijHdhbttXRWwrHhZJ/gzolneoxKVz9N/xmsvY/7Ls8lpnI9gxbgxjcHsAVEW3mgwiZCo6kkJQ==" }, "methods": { "version": "1.1.2", @@ -66255,29 +66248,25 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-duplicates": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-empty": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", - "dev": true, - "requires": {} + "dev": true }, "postcss-discard-overridden": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-flexbugs-fixes": { "version": "4.2.1", @@ -66440,8 +66429,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", - "dev": true, - "requires": {} + "dev": true }, "postcss-modules-local-by-default": { "version": "4.0.0", @@ -66485,8 +66473,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", - "dev": true, - "requires": {} + "dev": true }, "postcss-normalize-display-values": { "version": "5.1.0", @@ -67631,8 +67618,7 @@ "version": "2.2.2", "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.2.2.tgz", "integrity": "sha512-tvg2ZtOpOi6QDwsb3GZhOjDkkX0h8Z2gipvTg6OVMUyoYoURhEiRNePT8NZItTVCDh39JJHnLdfCOkzoLbFnTg==", - "dev": true, - "requires": {} + "dev": true }, "react-dom": { "version": "17.0.2", @@ -67712,8 +67698,7 @@ "react-hook-form": { "version": "7.15.4", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.15.4.tgz", - "integrity": "sha512-jEtsDBPfpkz1uuJVlTLDOg+jO3cG9pFHT3g5uayVvlNT551IetXE1iwrSaxUR/QPWyJA2FLx4Q/VjO2viZNfLg==", - "requires": {} + "integrity": "sha512-jEtsDBPfpkz1uuJVlTLDOg+jO3cG9pFHT3g5uayVvlNT551IetXE1iwrSaxUR/QPWyJA2FLx4Q/VjO2viZNfLg==" }, "react-hot-loader": { "version": "4.13.0", @@ -67742,8 +67727,7 @@ "react-icons": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.4.0.tgz", - "integrity": "sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==", - "requires": {} + "integrity": "sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==" }, "react-input-autosize": { "version": "2.2.2", @@ -67767,7 +67751,8 @@ "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true }, "react-json-view": { "version": "1.21.3", @@ -67824,8 +67809,7 @@ "react-onclickoutside": { "version": "6.12.2", "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz", - "integrity": "sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA==", - "requires": {} + "integrity": "sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA==" }, "react-overlays": { "version": "0.8.3", @@ -68013,8 +67997,7 @@ "version": "3.5.1", "resolved": "https://registry.npmjs.org/react-simple-animate/-/react-simple-animate-3.5.1.tgz", "integrity": "sha512-VpQS4nYU3G8+xU9hMinbeFZqHRVe+rNicec+y+Jaz3kHCV5TlBSkTzoaz/Nycsh2ShoWWfqfhV3Rn8E85QNqZg==", - "dev": true, - "requires": {} + "dev": true }, "react-style-singleton": { "version": "2.2.1", @@ -68856,8 +68839,7 @@ "version": "2.2.4", "resolved": "https://registry.npmjs.org/rollup-plugin-peer-deps-external/-/rollup-plugin-peer-deps-external-2.2.4.tgz", "integrity": "sha512-AWdukIM1+k5JDdAqV/Cxd+nejvno2FVLVeZ74NKggm3Q5s9cbbcOgUPGdbxPi4BXu7xGaZ8HG12F+thImYu/0g==", - "dev": true, - "requires": {} + "dev": true }, "rollup-plugin-postcss": { "version": "4.0.2", @@ -70538,8 +70520,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==", - "dev": true, - "requires": {} + "dev": true }, "style-to-object": { "version": "0.3.0", @@ -70620,8 +70601,7 @@ "stylis-rule-sheet": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz", - "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==", - "requires": {} + "integrity": "sha512-nTbZoaqoBnmK+ptANthb10ZRZOGC+EmTLLUxeYIuHNkEKcmKgXX1XWKkUBT2Ac4es3NybooPe0SmvKdhKJZAuw==" }, "stylus": { "version": "0.55.0", @@ -71254,8 +71234,7 @@ "ts-essentials": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/ts-essentials/-/ts-essentials-7.0.3.tgz", - "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==", - "requires": {} + "integrity": "sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ==" }, "ts-invariant": { "version": "0.4.4", @@ -71499,7 +71478,8 @@ "typescript": { "version": "4.7.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==" + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true }, "ua-parser-js": { "version": "0.7.31", @@ -71944,8 +71924,7 @@ "use-composed-ref": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/use-composed-ref/-/use-composed-ref-1.3.0.tgz", - "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==", - "requires": {} + "integrity": "sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==" }, "use-deep-compare-effect": { "version": "1.8.1", @@ -71960,8 +71939,7 @@ "use-isomorphic-layout-effect": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", - "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", - "requires": {} + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==" }, "use-latest": { "version": "1.2.1", @@ -71991,8 +71969,7 @@ "use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", - "requires": {} + "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==" }, "utf8-byte-length": { "version": "1.0.4", @@ -72897,8 +72874,7 @@ "version": "8.8.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", - "dev": true, - "requires": {} + "dev": true } } }, @@ -72906,8 +72882,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/webpack-filter-warnings-plugin/-/webpack-filter-warnings-plugin-1.2.1.tgz", "integrity": "sha512-Ez6ytc9IseDMLPo0qCuNNYzgtUl8NovOqjIq4uAU8LTD4uoa1w1KpZyyzFtLTEMZpkkOkLfL9eN+KGYdk1Qtwg==", - "dev": true, - "requires": {} + "dev": true }, "webpack-hot-middleware": { "version": "2.25.2", @@ -73155,7 +73130,7 @@ "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", - "requires": {} + "dev": true }, "x-default-browser": { "version": "0.4.0", diff --git a/frontend/package.json b/frontend/package.json index aeca51cc337..16014e0b324 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -176,6 +176,7 @@ "cypress": "^10.2.0", "eslint": "~8.15.0", "eslint-config-prettier": "8.1.0", + "eslint-plugin-chai-friendly": "^0.7.2", "eslint-plugin-cypress": "^2.10.3", "eslint-plugin-import": "2.26.0", "eslint-plugin-jsx-a11y": "6.6.1", diff --git a/frontend/workspace.json b/frontend/workspace.json index c0dfb4ed5e1..669f053e6b1 100644 --- a/frontend/workspace.json +++ b/frontend/workspace.json @@ -6,7 +6,6 @@ "console-ce-e2e": "apps/console-ce-e2e", "console-legacy-oss": "libs/console/legacy-oss", "console-legacy-pro": "libs/console/legacy-pro", - "console-pro": "apps/console-pro", - "console-pro-e2e": "apps/console-pro-e2e" + "console-pro": "apps/console-pro" } }