Skip to content

Commit

Permalink
Add DB Seed to Cypress and setup Percy (#3155)
Browse files Browse the repository at this point in the history
* Update Cypress element selectors

* Add seed data function to Cypress

* Change Cypress setup to be part of db-seed

* Add DatabaseSource selector to Create Data Source spec

* Add getElement command to Cypress

* Fix eslint issues

* Change Cypress getElement to getByTestId

* Add Percy and test it with the CI

* Change Percy dependency for CI to Cypress' Dockerfile

* Change Percy's execution to the docker container
- add --no-save to avoid errors on Dockerfile.cypress
 - pass PERCY_TOKEN from the CI to docker container

* Fix missed char on CircleCI config file

* Move Percy execution back to host on the CI

* Test adding PERCY_TOKEN to frontend-e2e-tests on CI config

* Undo add PERCY_TOKEN to config.yml

* Add Percy token and .git folder to Cypress

* Remove Percy install from config.yml

* Ignore .git folder again and use Percy env vars instead

* Update PERCY_PULL_REQUEST to be CIRCLE_PR_NUMBER

* Update cypress-server.js to handle other cypress commands
- cypress-server.js -> cypress.js
- new commands added to cypress.js
- CircleCI config updated accordingly
- added a Homepage screenshot

* Remove trailing spaces

* Add Create Query spec

* Disable Cypress videos

* Update run browser to Chrome

* Add missing --browser chrome
  • Loading branch information
gabrieldutra authored and arikfr committed Dec 10, 2018
1 parent 38ed046 commit cfe12c5
Show file tree
Hide file tree
Showing 20 changed files with 186 additions and 93 deletions.
10 changes: 6 additions & 4 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,22 +58,24 @@ jobs:
environment:
COMPOSE_FILE: .circleci/docker-compose.cypress.yml
COMPOSE_PROJECT_NAME: cypress
PERCY_TOKEN_ENCODED: MWM3OGUzNzk4ZWQ2NTE4YTBhMDAwZDNiNWE1Nzc4ZjEzZjYyMzY1MjE0NjY0NDRiOGE5ODc5ZGYzYTU4ZmE4NQ==
docker:
- image: circleci/node:8
steps:
- setup_remote_docker
- checkout
- run:
name: Install npm dependencies
command: npm install
command: |
npm install
- run:
name: Setup Redash server
command: |
npm run cypress:server start-ci
docker-compose run cypress node ./cypress/cypress-server.js setup
npm run cypress start
docker-compose run cypress node ./cypress/cypress.js db-seed
- run:
name: Execute Cypress tests
command: docker-compose run cypress ./node_modules/.bin/cypress run
command: npm run cypress run-ci
build-tarball:
docker:
- image: circleci/node:8
Expand Down
4 changes: 4 additions & 0 deletions .circleci/docker-compose.cypress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ services:
- worker
environment:
CYPRESS_baseUrl: "http://server:5000"
PERCY_TOKEN: ${PERCY_TOKEN}
PERCY_BRANCH: ${CIRCLE_BRANCH}
PERCY_COMMIT: ${CIRCLE_SHA1}
PERCY_PULL_REQUEST: ${CIRCLE_PR_NUMBER}
redis:
image: redis:3.0-alpine
restart: unless-stopped
Expand Down
9 changes: 5 additions & 4 deletions Dockerfile.cypress
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
FROM cypress/browsers:chrome67

WORKDIR /usr/src/app
ENV APP /usr/src/app
WORKDIR $APP

RUN npm install cypress > /dev/null
RUN npm install --no-save cypress @percy/cypress > /dev/null

COPY cypress /usr/src/app/cypress
COPY cypress.json /usr/src/app/cypress.json
COPY cypress $APP/cypress
COPY cypress.json $APP/cypress.json

RUN ./node_modules/.bin/cypress verify
3 changes: 2 additions & 1 deletion client/app/components/QueryEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ class QueryEditor extends React.Component {
const isExecuteDisabled = this.props.queryExecuting || !this.props.canExecuteQuery();

return (
<section style={{ height: '100%' }}>
<section style={{ height: '100%' }} data-test="QueryEditor">
<div className="container p-15 m-b-10" style={{ height: '100%' }}>
<div style={{ height: 'calc(100% - 40px)', marginBottom: '0px' }} className="editor__container">
<AceEditor
Expand Down Expand Up @@ -262,6 +262,7 @@ class QueryEditor extends React.Component {
className={'btn btn-primary m-l-5' + (isExecuteDisabled ? ' disabled' : '')}
disabled={isExecuteDisabled}
onClick={this.props.executeQuery}
data-test="ExecuteButton"
>
<span className="zmdi zmdi-play" />
<span className="hidden-xs m-l-5">Execute</span>
Expand Down
2 changes: 1 addition & 1 deletion client/app/components/app-header/app-header.html
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
<!--<a href="users" title="Settings"><i class="fa fa-cog"></i></a>-->
<!--</li>-->
<li class="dropdown" uib-dropdown>
<a href="#" class="dropdown-toggle dropdown--profile" uib-dropdown-toggle data-cy="dropdown-profile">
<a href="#" class="dropdown-toggle dropdown--profile" uib-dropdown-toggle data-test="ProfileDropdown">
<img ng-src="{{ $ctrl.currentUser.profile_image_url }}" class="profile__image--navbar" width="20"/>
<span class="dropdown--profile__username" ng-bind="$ctrl.currentUser.name"></span> <span
class="caret caret--nav"></span></a>
Expand Down
9 changes: 5 additions & 4 deletions client/app/components/dynamic-form.html
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
<form name="dynamicForm">
<div class="form-group required" ng-class='{"has-error": (dynamicForm.targetName | showError)}'>
<label class="control-label" for="dataSourceName">Name</label>
<input type="string" class="form-control" name="targetName" ng-model="target.name" autofocus required>
<input type="string" class="form-control" name="targetName" ng-model="target.name"
data-test="TargetName" autofocus required>
<error-messages input="dynamicForm.targetName" form="dynamicForm"></error-messages>
</div>
<hr>
<div class="form-group" ng-class='{"has-error": (inner.input | showError), "required": field.property.required}' ng-form="inner" ng-repeat="field in fields">
<label ng-if="field.property.type !== 'checkbox'" class="control-label">{{field.property.title || field.name | toHuman}}</label>
<input name="input" type="{{field.property.type}}" class="form-control" ng-model="target.options[field.name]" ng-required="field.property.required"
ng-if="field.property.type !== 'file' && field.property.type !== 'checkbox'" accesskey="tab" placeholder="{{field.property.default}}"
data-cy="{{field.property.title || field.name | toHuman}}">
data-test="{{field.property.title || field.name | toHuman}}">

<label ng-if="field.property.type=='checkbox'">
<input name="input" type="{{field.property.type}}" ng-model="target.options[field.name]" ng-required="field.property.required"
ng-if="field.property.type !== 'file'" accesskey="tab" placeholder="{{field.property.default}}"
data-cy="{{field.property.title || field.name | toHuman}}">
data-test="{{field.property.title || field.name | toHuman}}">
{{field.property.title || field.name | toHuman}}
</label>

Expand All @@ -25,7 +26,7 @@
<error-messages input="inner.input" form="inner"></error-messages>
</div>

<button class="btn btn-block btn-primary m-b-10" ng-disabled="!dynamicForm.$valid" ng-click="saveChanges()">Save</button>
<button class="btn btn-block btn-primary m-b-10" ng-disabled="!dynamicForm.$valid" ng-click="saveChanges()" data-test="Submit">Save</button>
<span ng-repeat="action in actions">
<button class="btn"
ng-class="action.class"
Expand Down
2 changes: 1 addition & 1 deletion client/app/components/dynamic-table/dynamic-table.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<div class="dynamic-table-container">
<table class="table table-condensed table-hover">
<table class="table table-condensed table-hover" data-test="DynamicTable">
<thead>
<tr>
<th ng-repeat="column in $ctrl.columns" ng-click="$ctrl.onColumnHeaderClick($event, column)"
Expand Down
2 changes: 1 addition & 1 deletion client/app/components/type-picker.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ <h3 class="text-center">{{$ctrl.title}}</h3>
</div>

<div class="row">
<div class="col-lg-12 database-source">
<div class="col-lg-12 database-source" data-test="DatabaseSource">
<div class="visual-card" ng-repeat="type in $ctrl.types | filter:$ctrl.filter:strict" ng-click="$ctrl.onTypeSelect(type)">
<img ng-src="{{$ctrl.imgRoot}}/{{type.type}}.png" alt="{{type.name}}">
<h3>{{type.name}}</h3>
Expand Down
3 changes: 2 additions & 1 deletion client/app/pages/queries/query.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ <h3>
<main class="query-fullscreen">
<nav resizable r-directions="['right']" r-flex="true" resizable-toggle toggle-shortcut="Alt+Shift+D, Alt+D">
<div class="editor__left__data-source">
<ui-select ng-model="query.data_source_id" remove-selected="false" ng-disabled="!isQueryOwner || !sourceMode" on-select="updateDataSource()">
<ui-select ng-model="query.data_source_id" remove-selected="false" ng-disabled="!isQueryOwner || !sourceMode"
on-select="updateDataSource()" data-test="SelectDataSource">
<ui-select-match placeholder="Select Data Source...">{{$select.selected.name}}</ui-select-match>
<ui-select-choices repeat="ds.id as ds in dataSources | filter:$select.search">
{{ds.name}}
Expand Down
3 changes: 2 additions & 1 deletion cypress.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"baseUrl": "http://localhost:5000"
"baseUrl": "http://localhost:5000",
"video": false
}
52 changes: 0 additions & 52 deletions cypress/cypress-server.js

This file was deleted.

72 changes: 72 additions & 0 deletions cypress/cypress.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/* eslint-disable import/no-extraneous-dependencies, no-console */
const atob = require('atob');
const { execSync } = require('child_process');
const { post } = require('request').defaults({ jar: true });
const { seedData } = require('./seed-data');

const baseUrl = process.env.CYPRESS_baseUrl || 'http://localhost:5000';

function seedDatabase(seedValues) {
const request = seedValues.shift();
const data = request.type === 'form' ? { formData: request.data } : { json: request.data };

post(baseUrl + request.route, data, (err, response) => {
const result = response ? response.statusCode : err;
console.log('POST ' + request.route + ' - ' + result);
if (seedValues.length) {
seedDatabase(seedValues);
}
});
}

function startServer() {
console.log('Starting the server...');

execSync('docker-compose -p cypress build --build-arg skip_ds_deps=true', { stdio: 'inherit' });
execSync('docker-compose -p cypress up -d', { stdio: 'inherit' });
execSync('docker-compose -p cypress run server create_db', { stdio: 'inherit' });
}

function stopServer() {
console.log('Stopping the server...');
execSync('docker-compose -p cypress down', { stdio: 'inherit' });
}

function runCypressCI() {
if (process.env.PERCY_TOKEN_ENCODED) {
process.env.PERCY_TOKEN = atob(`${process.env.PERCY_TOKEN_ENCODED}`);
}
execSync('docker-compose run cypress ./node_modules/.bin/percy exec -- ./node_modules/.bin/cypress run --browser chrome', { stdio: 'inherit' });
}

const command = process.argv[2] || 'all';

switch (command) {
case 'start':
startServer();
break;
case 'db-seed':
seedDatabase(seedData);
break;
case 'run':
execSync('cypress run --browser chrome', { stdio: 'inherit' });
break;
case 'open':
execSync('cypress open', { stdio: 'inherit' });
break;
case 'run-ci':
runCypressCI();
break;
case 'stop':
stopServer();
break;
case 'all':
startServer();
seedDatabase(seedData);
execSync('cypress run --browser chrome', { stdio: 'inherit' });
stopServer();
break;
default:
console.log('Usage: npm run cypress [start|db-seed|open|run|stop]');
break;
}
15 changes: 7 additions & 8 deletions cypress/integration/data-source/create_data_source_spec.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
describe('Create Data Source', () => {
beforeEach(() => {
cy.login();
cy.visit('/data_sources');
cy.visit('/data_sources/new');
});

it('creates a new PostgreSQL data source', () => {
cy.contains('New Data Source').click();
cy.contains('PostgreSQL').click();
cy.getByTestId('DatabaseSource').contains('PostgreSQL').click();

cy.get('[name=targetName]').type('Redash');
cy.get('[data-cy=Host]').type('{selectall}localhost');
cy.get('[data-cy=User]').type('postgres');
cy.get('[data-cy=Password]').type('postgres');
cy.get('[data-cy="Database Name"]').type('postgres{enter}');
cy.getByTestId('TargetName').type('Redash');
cy.getByTestId('Host').type('{selectall}postgres');
cy.getByTestId('User').type('postgres');
cy.getByTestId('Password').type('postgres');
cy.getByTestId('Database Name').type('postgres{enter}');

cy.contains('Saved.');
});
Expand Down
21 changes: 21 additions & 0 deletions cypress/integration/query/create_query_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
describe('Create Query', () => {
beforeEach(() => {
cy.login();
cy.visit('/queries/new');
});

it('executes the query', () => {
cy.getByTestId('SelectDataSource')
.click()
.contains('Test PostgreSQL').click();

cy.getByTestId('QueryEditor')
.get('.ace_text-input')
.type('SELECT id, name FROM organizations{esc}', { force: true });

cy.getByTestId('ExecuteButton').click();

cy.getByTestId('DynamicTable').should('exist');
cy.percySnapshot('Edit Query page');
});
});
18 changes: 12 additions & 6 deletions cypress/integration/user/login_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,28 @@ describe('Login', () => {
cy.visit('/login');
});

it('greets the user', () => {
it('greets the user and take a screenshot', () => {
cy.contains('h3', 'Login to Redash');

cy.wait(1000);
cy.percySnapshot('Login page');
});

it('shows message on failed login', () => {
cy.get('#inputEmail').type('admin@redash.io');
cy.get('#inputPassword').type('wrongpassword{enter}');
cy.getByTestId('Email').type('admin@redash.io');
cy.getByTestId('Password').type('wrongpassword{enter}');

cy.get('.alert').should('contain', 'Wrong email or password.');
cy.getByTestId('ErrorMessage').should('contain', 'Wrong email or password.');
});

it('navigates to homepage with successful login', () => {
cy.get('#inputEmail').type('admin@redash.io');
cy.get('#inputPassword').type('password{enter}');
cy.getByTestId('Email').type('admin@redash.io');
cy.getByTestId('Password').type('password{enter}');

cy.title().should('eq', 'Redash');
cy.contains('Example Admin');

cy.wait(1000);
cy.percySnapshot('Homepage');
});
});
2 changes: 1 addition & 1 deletion cypress/integration/user/logout_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ describe('Logout', () => {
});

it('shows login page after logout', () => {
cy.get('[data-cy=dropdown-profile]').click();
cy.getByTestId('ProfileDropdown').click();
cy.contains('Log out').click();

cy.title().should('eq', 'Login to Redash');
Expand Down
Loading

0 comments on commit cfe12c5

Please sign in to comment.