Skip to content

Commit

Permalink
test: add cypress config and tests [TECH-464] (#1228)
Browse files Browse the repository at this point in the history
Included in this commit:
* cypress configuration based on current DHIS2 convention (cli-utils-cypress initialized the configuration)
* tests for viewing dashboard (can be run against actual DHIS2 instance or stubbed fixtures - useful for CI)
* tests for creating and deleting dashboard (can only be run against an actual DHIS2 instance)
* fixtures for non-mutating tests (viewing dashboard)
* github action to run non-mutating tests on CI when creating and updating PRs, and on push to master
* README updated with info on how to set up and run the tests
* linting forced a function name change for a deprecated react lifecycle method (componentWillReceiveProps to UNSAFE_componentWillReceiveProps). Doesn't affect functionality.
  • Loading branch information
jenniferarnesen authored Nov 5, 2020
1 parent 5f8d9e1 commit 7b66359
Show file tree
Hide file tree
Showing 49 changed files with 3,365 additions and 155 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { config } = require('@dhis2/cli-style')

module.exports = {
extends: [config.eslintReact],
extends: [config.eslintReact, 'plugin:cypress/recommended'],
}
40 changes: 40 additions & 0 deletions .github/workflows/cypress.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: 'dhis2: test (cypress)'

on:
push:
branches:
- master
pull_request:

env:
SERVER_START_CMD: 'yarn cypress:start'
SERVER_URL: 'http://localhost:3000'
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
CYPRESS_TAGS: '@nonmutating'
cypress_dhis2_base_url: 'http://localhost:8080'
cypress_dhis2_api_stub_mode: 'STUB'
REACT_APP_DHIS2_BASE_URL: 'http://localhost:8080'

jobs:
e2e:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
strategy:
matrix:
containers: [1, 2, 3]

steps:
- name: Checkout
uses: actions/checkout@v2

- name: Tests
uses: cypress-io/github-action@v2
with:
record: true
parallel: true
start: ${{ env.SERVER_START_CMD }}
wait-on: ${{ env.SERVER_URL }}
wait-on-timeout: 60
group: 'e2e'
env:
CI: true
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,9 @@ src/locales/
node_modules
.d2
src/locales
build
build

# test
cypress.env.json
cypress/screenshots
cypress/videos
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/node_modules/*
/i18n/*
/public/*
/public/*
/cypress/fixtures
59 changes: 45 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,67 @@
This project was bootstrapped with [DHIS2 Application Platform](https://github.com/dhis2/app-platform).

## Available Scripts

In the project directory, you can run:
## Development

### `yarn start`

Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.

In order for maps, event reports and event charts to display, you need to provide the api server instance in the environment variable `REACT_APP_DHIS2_BASE_URL`.
#### Configuration

Two environment variables need to be set for running dashboards-app in development mode. It is recommended to set these environment variables in a `.env` or `.env.local` file.

##### api base url: REACT_APP_DHIS2_BASE_URL

The api base url points to a running DHIS2 instance. This can be for example `http://localhost:8080`.

```
REACT_APP_DHIS2_BASE_URL=http://localhost:8080
```

##### api authentication: REACT_APP_DHIS2_AUTHORIZATION

In order for maps, event reports and event charts to display in development mode, you also need to provide the authenticaion credentials for the api. The following example is the base64 encoded value for the username/password combination of `admin:district`:

You can provide this inline, for example:
`REACT_APP_DHIS2_BASE_URL=http://localhost:8080 yarn start`
```
REACT_APP_DHIS2_AUTHORIZATION=Basic YWRtaW46ZGlzdHJpY3Q=
```

You can also set this in a `.env*.local` file or straight up as an environment variable in your local system.
### e2e tests

#### Configuration

Additional environment variables are needed in order to run the Cypress e2e tests. It is recommended to define these in the same place as the REACT_APP_DHIS2_BASE_URL env var (for example. `.env`). REACT_APP_DHIS2_BASE_URL and CYPRESS_DHIS2_BASE_URL must match.

```
REACT_APP_DHIS2_BASE_URL=http://localhost:8080
CYPRESS_DHIS2_BASE_URL=http://localhost:8080
CYPRESS_DHIS2_USERNAME=admin
CYPRESS_DHIS2_PASSWORD=district
```

#### Run the e2e tests

The following commands can be used to run the tests:

| Command | Backend | Environment | Tests |
| ------------------- | :--------: | ----------: | -----------: |
| `yarn cy:open-live` | API server | Cypress UI | All |
| `yarn cy:run-live` | API server | Headless | All |
| `yarn cy:open-stub` | Fixtures | Cypress UI | Non-mutating |
| `yarn cy:run-stub` | Fixtures | Headless | Non-mutating |

### `yarn test`

Launches the test runner and runs all available tests found in `/src`.<br />
Launches the test runner and runs all available unit tests found in `/src`.<br />

See the section about [running tests](https://platform.dhis2.nu/#/scripts/test) for more information.

### `yarn build`

Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.

The build is minified and the filenames include the hashes.<br />
A deployable `.zip` file can be found in `build/bundle`!
Builds the app for production to the `build` folder.<br />. This command is run by the continuous integration server.

See the section about [building](https://platform.dhis2.nu/#/scripts/build) for more information.
See the [building](https://platform.dhis2.nu/#/scripts/build) section for more information.

## Learn More

Expand Down
3 changes: 3 additions & 0 deletions cypress-cucumber-preprocessor.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
nonGlobalStepDefinitions: true,
}
6 changes: 6 additions & 0 deletions cypress.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"baseUrl": "http://localhost:3000",
"video": true,
"projectId": "5fk191",
"testFiles": "**/*.feature"
}
83 changes: 83 additions & 0 deletions cypress/assets/unfetch.umd.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* See
*
* * https://github.com/cypress-io/cypress/issues/95
* * https://github.com/cypress-io/cypress-example-recipes/tree/master/examples/stubbing-spying__window-fetch
* * https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/stubbing-spying__window-fetch/cypress/integration/polyfill-fetch-from-tests-spec.js
*
* for an explanation why this is currently necessary...
*/
/* global define */
!(function(e, n) {
'object' == typeof exports && 'undefined' != typeof module
? (module.exports = n())
: 'function' == typeof define && define.amd
? define(n)
: (e.unfetch = n())
})(this, function() {
return function(e, n) {
return (
(n = n || {}),
new Promise(function(t, o) {
var r = new XMLHttpRequest(),
s = [],
u = [],
i = {},
f = function() {
return {
ok: 2 == ((r.status / 100) | 0),
statusText: r.statusText,
status: r.status,
url: r.responseURL,
text: function() {
return Promise.resolve(r.responseText)
},
json: function() {
return Promise.resolve(
JSON.parse(r.responseText)
)
},
blob: function() {
return Promise.resolve(new Blob([r.response]))
},
clone: f,
headers: {
keys: function() {
return s
},
entries: function() {
return u
},
get: function(e) {
return i[e.toLowerCase()]
},
has: function(e) {
return e.toLowerCase() in i
},
},
}
}
for (var a in (r.open(n.method || 'get', e, !0),
(r.onload = function() {
r
.getAllResponseHeaders()
.replace(/^(.*?):[^\S\n]*([\s\S]*?)$/gm, function(
e,
n,
t
) {
s.push((n = n.toLowerCase())),
u.push([n, t]),
(i[n] = i[n] ? i[n] + ',' + t : t)
}),
t(f())
}),
(r.onerror = o),
(r.withCredentials = 'include' == n.credentials),
n.headers))
r.setRequestHeader(a, n.headers[a])
r.send(n.body || null)
})
)
}
})
746 changes: 746 additions & 0 deletions cypress/fixtures/network/requests.json

Large diffs are not rendered by default.

28 changes: 28 additions & 0 deletions cypress/integration/ui/create_dashboard.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Feature: Creating and deleting dashboard

@mutating
Scenario: I create a new dashboard
Given I choose to create new dashboard
And dashboard title is added
And dashboard items are added
And escape key is pressed
And dashboard is saved
Then dashboard displays in view mode
And the saved dashboard should be displayed

@mutating
Scenario: I cancel a delete dashboard action
Given I open existing dashboard
When I choose to edit dashboard
And I choose to delete dashboard
And I cancel delete
Then the dashboard displays in edit mode

@mutating
Scenario: I delete a dashboard
Given I open existing dashboard
When I choose to edit dashboard
And I choose to delete dashboard
And I confirm delete
Then dashboard displays in view mode
And the dashboard is deleted and first starred dashboard displayed
117 changes: 117 additions & 0 deletions cypress/integration/ui/create_dashboard/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'

// the length of the root route of the app (after the slash): #/
const ROOT_ROUTE_LENGTH = 0
// the length of UIDs (after the slash): '#/nghVC4wtyzi'
const UID_LENGTH = 11

const TEST_DASHBOARD_TITLE = new Date().toUTCString()

const ROUTE_EDIT = 'edit'
const ROUTE_NEW = 'new'
const ROUTE_PRINTLAYOUT = 'printlayout'
const ROUTE_PRINTOIPP = 'printoipp'
const nonViewRoutes = [
ROUTE_NEW,
ROUTE_EDIT,
ROUTE_PRINTLAYOUT,
ROUTE_PRINTOIPP,
]

const getRouteFromHash = hash => {
const lastSlashIdx = hash.lastIndexOf('/')
return hash.slice(lastSlashIdx + 1)
}

const toggleShowMoreButton = () => {
cy.get('[data-test="dhis2-dashboard-showmore-button"]').click()
}

beforeEach(() => {
cy.visit('/')
})

Given('I choose to create new dashboard', () => {
cy.get('[data-test="dhis2-dashboard-link-new-dashboard"]').click()
})

When('dashboard title is added', () => {
cy.get('[data-test="dhis2-dashboard-dashboard-title-input"]').type(
TEST_DASHBOARD_TITLE
)
})

When('dashboard items are added', () => {
cy.get('[data-test="dhis2-dashboard-item-search"]').click()
cy.get(
'[data-test="dhis2-dashboard-menu-item-ANC: 1 and 3 coverage Yearly"]'
).click()
})

When('escape key is pressed', () => {
cy.get('body').trigger('keydown', { key: 'Escape' })
cy.get('[data-test="dhis2-dashboard-item-menu]').should('not.be.visible')
})

When('dashboard is saved', () => {
cy.get('[data-test="dhis2-dashboard-save-dashboard-button"]').click()
})

Then('the saved dashboard should be displayed', () => {
cy.get('[data-test="dhis2-dashboard-view-dashboard-title"]').contains(
TEST_DASHBOARD_TITLE
)
})

Then('dashboard displays in view mode', () => {
cy.location().should(loc => {
const currentRoute = getRouteFromHash(loc.hash)

expect(nonViewRoutes).not.to.include(currentRoute)
expect([ROOT_ROUTE_LENGTH, UID_LENGTH]).to.include(currentRoute.length)
})
})

Given('I open existing dashboard', () => {
toggleShowMoreButton()
cy.get('[data-test="dhis2-dashboard-dashboard-chip"]')
.contains(TEST_DASHBOARD_TITLE)
.click()
})

When('I choose to edit dashboard', () => {
cy.get('[data-test="dhis2-dashboard-link-edit-dashboard"]').click()
})

When('I choose to delete dashboard', () => {
cy.get('[data-test="dhis2-dashboard-delete-dashboard-button"]').click()
})

When('I confirm delete', () => {
cy.get('[data-test="dhis2-dashboard-confirm-delete-dashboard"]').click()
})

When('I cancel delete', () => {
cy.get('[data-test="dhis2-dashboard-cancel-delete-dashboard"]').click()
})

Then('the dashboard displays in edit mode', () => {
cy.get('[data-test="dhis2-dashboard-dashboard-title-input"]').should(
'exist'
)

cy.location().should(loc => {
expect(getRouteFromHash(loc.hash)).to.eq(ROUTE_EDIT)
})
})

Then('the dashboard is deleted and first starred dashboard displayed', () => {
toggleShowMoreButton()
cy.get('[data-test="dhis2-dashboard-dashboard-chip"]')
.contains(TEST_DASHBOARD_TITLE)
.should('not.exist')

cy.get('[data-test="dhis2-dashboard-view-dashboard-title"]')
.should('exist')
.should('not.be.empty')
})
Loading

0 comments on commit 7b66359

Please sign in to comment.