console: Setup cypress on the Nx monorepo (close #5463)

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5675
GitOrigin-RevId: b320b6f5bb092d20a4de8e51f6711323dd2b0a05
This commit is contained in:
Stefano Magni 2022-08-31 11:03:22 +02:00 committed by hasura-bot
parent 5764174087
commit d864bed4f4
118 changed files with 9034 additions and 275 deletions

View File

@ -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();

View File

@ -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;

View File

@ -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;

View File

@ -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
}
]
}
}
]
}

View File

@ -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.

View File

@ -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}',
],
},
});

View File

@ -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');
};

View File

@ -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();
}

View File

@ -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');
});
});
}

View File

@ -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}*` });
}
});
}

View File

@ -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**' }));
}

View File

@ -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**' });
});
}

View File

@ -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();
}
});
}

View File

@ -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();
// }

View File

@ -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');
});
});
}

View File

@ -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}*` });
}
});
}

View File

@ -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**' }));
}

View File

@ -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**' }));
}

View File

@ -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**' });
});
}

View File

@ -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();
}
});
}

View File

@ -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);
};

View File

@ -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();
}

View File

@ -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');
});
});

View File

@ -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();
}

View File

@ -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`);
};

View File

@ -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();
}

View File

@ -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();
};

View File

@ -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();
}

View File

@ -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`);
};

View File

@ -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();
}

View File

@ -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);
};

View File

@ -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();
}

View File

@ -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)');
};

View File

@ -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();
}

View File

@ -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();
});
};

View File

@ -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();
}

View File

@ -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
}
);
};

View File

@ -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();
};

View File

@ -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();
}

View File

@ -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');
}
});
};

View File

@ -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();
}

View File

@ -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);
}
});
};

View File

@ -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');
};

View File

@ -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();
}

View File

@ -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();
};

View File

@ -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();
}

View File

@ -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);
});
};

View File

@ -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);
};

View File

@ -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();
}

View File

@ -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();
};

View File

@ -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();
}

View File

@ -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();
}

View File

@ -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();
};

View File

@ -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();
}

View File

@ -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');
};

View File

@ -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();
}

View File

@ -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);
};

View File

@ -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();
}

View File

@ -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)
);
};

View File

@ -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();
}

View File

@ -0,0 +1,102 @@
import { getElementFromAlias } from '../../../helpers/eventHelpers';
type CustomizationSettingsType = {
root_fields_namespace: string;
type_names: {
prefix: string;
suffix: string;
mapping: Record<string, string>;
};
field_names: {
parent_type: string;
prefix: string;
suffix: string;
mapping: Record<string, string>;
}[];
};
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();
};

View File

@ -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();
}

View File

@ -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');
});
});

View File

@ -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');
});
});

View File

@ -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();
});
});

View File

@ -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');
};

View File

@ -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();
}

View File

@ -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'
);
};

View File

@ -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();
}

View File

@ -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;
}
});
};

View File

@ -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"
}

View File

@ -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;
}

View File

@ -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();
});
};

View File

@ -0,0 +1 @@
export const ADMIN_SECRET_HEADER_KEY = 'x-hasura-admin-secret';

View File

@ -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;
}

View File

@ -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';

View File

@ -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,
});

View File

@ -0,0 +1,17 @@
export const replaceMetadata = (newMetadata: Record<string, any>) => {
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
}
);
};

View File

@ -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}`;

View File

@ -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');
}
};

View File

@ -1 +0,0 @@
export const getGreeting = () => cy.get('h1');

View File

@ -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,
});
});

View File

@ -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<Subject> {
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);
});

View File

@ -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);

View File

@ -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,
};
}

View File

@ -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;
}

View File

@ -0,0 +1,88 @@
/// <reference types="Cypress" />
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']);
});
});

View File

@ -0,0 +1,13 @@
import type { RunningTestState } from '../types';
export function generateEmptyTestState(
testPath: string,
testTitle: string
): RunningTestState {
return {
testPath,
testTitle,
halted: false,
contract: [],
};
}

View File

@ -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(' - ');
}

View File

@ -0,0 +1,57 @@
/// <reference types="Cypress" />
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'
);
});
});

View File

@ -0,0 +1,18 @@
/// <reference types="Cypress" />
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',
]);
});
});
});

View File

@ -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'
);
}
}

View File

@ -0,0 +1,55 @@
/// <reference types="Cypress" />
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'
);
});
});

View File

@ -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<string, unknown>
) {
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'
);
}
}

View File

@ -0,0 +1,15 @@
/// <reference types="Cypress" />
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'
);
});
});

View File

@ -0,0 +1 @@
export * from './contractIntercept';

View File

@ -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<string, string | string[]> | 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<Mocha.Context, RunningTestState>
* 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<TestTitle, RunningTestState>;
// -------------------------------------------------------------------
// OPTIONS TYPES -----------------------------------------------------
// -------------------------------------------------------------------
export type StartContractInterceptOptions = {
thisTest: Mocha.Context;
mode: 'record' | 'disabled';
createFixtureName: (req: CyHttpMessages.IncomingHttpRequest) => string;
};

View File

@ -0,0 +1,73 @@
// type definition for all custom commands
declare namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
/**
* Custom command to select DOM element by data-test attribute.
* <button data-test="greeting"> </button>
* @example cy.getBySel('greeting')
*/
getBySel(value: string): Chainable<JQuery<Element>>;
/**
* 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<JQuery<HTMLTextAreaElement>>;
/**
* Visit the initial empty page.
* Console's textarea
* @example cy.visitEmptyPage()
*/
visitEmptyPage(): Chainable<unknown>;
/**
* Success notifications
*/
expectSuccessNotification(): Chainable<unknown>;
expectSuccessNotificationWithTitle(title: string): Chainable<unknown>;
expectSuccessNotificationWithMessage(message: string): Chainable<unknown>;
/**
* Error notifications
*/
expectErrorNotification(): Chainable<unknown>;
expectErrorNotificationWithTitle(title: string): Chainable<unknown>;
expectErrorNotificationWithMessage(message: string): Chainable<unknown>;
/**
* 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<unknown>;
/**
* 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<unknown>;
}
}

View File

@ -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);
}
);

View File

@ -0,0 +1,4 @@
export { joinPath } from './joinPath';
export { mkdirSync } from './mkdirSync';
export { splitPath } from './splitPath';
export { writeFileSync } from './writeFileSync';

Some files were not shown because too many files have changed in this diff Show More