doc: Enriched UX testing documentation, #3573 (#3583)

This commit is contained in:
Florian Giroud 2021-02-07 19:07:54 +01:00 committed by GitHub
parent 859828a0f0
commit 7003dd2d2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 187 additions and 44 deletions

View File

@ -6,53 +6,22 @@ sidebar_label: Functional tests
import useBaseUrl from '@docusaurus/useBaseUrl'; import useBaseUrl from '@docusaurus/useBaseUrl';
You will need: ## Introduction
- [Node.js 10 or 12 and above](https://nodejs.org) OpenRefine interface is tested with the [Cypress framework](https://www.cypress.io/).
- [Yarn or NPM](https://yarnpkg.com/) With Cypress, tests are performing assertions using a real browser, the same way a real user would use the software.
- A Unix/Linux shell environment or the Windows command line
## Installation Cypress tests can be ran
To install Cypress and dependencies, run : - using the Cypress test runner (development mode)
- using a command line (CI/CD mode)
``` If you are writing tests, the cypress test runner is good enough, and the command-line is mainly used by the CI/CD platform (Github actions)
cd ./main/tests/cypress
yarn install
```
Cypress tests can be started in two modes:
### Development / Debugging mode
Dev mode assumes that OpenRefine is up and running on the local machine, the tests themselves do not launch OpenRefine, nor restarts it.
Run :
```shell
yarn --cwd ./main/tests/cypress run cypress open
```
It will open the Cypress test runner, where you can choose, replay, visualize tests.
This is the recommended way to run tests when adding or fixing tests.
The runners assumes
### Command-line mode
Command line mode will starts OpenRefine with a temporary folder for data
```shell
./refine ui_test chrome
```
It will run all tests in the command-line, without windows, displaying results in the standard output
This is the way to run tests in CI/CD
## Cypress brief overview ## Cypress brief overview
Cypress operates insides a browser, it's internally using NodeJS. Cypress operates insides a browser, it's internally using NodeJS.
That's a key difference with tools such as selenium. That's a key difference with tools such as selenium.
**From the Cypress documentation:** **From the Cypress documentation:**
@ -67,12 +36,132 @@ The general workflow of a Cypress test is to
- Trigger user actions - Trigger user actions
- Assert that the DOM contains expected texts and elements using selectors - Assert that the DOM contains expected texts and elements using selectors
## Browsers ## Getting started
If that's the first time you use Cypress, it is recommended for you to get familiar with the tool.
- [Cypress overview](https://docs.cypress.io/guides/overview/why-cypress.html)
- [Cypress examples of tests and syntax](https://example.cypress.io/)
### 1. Install Cypress
You will need:
- [Node.js 10 or 12 and above](https://nodejs.org)
- [Yarn or NPM](https://yarnpkg.com/)
- A Unix/Linux shell environment or the Windows command line
To install Cypress and dependencies, run :
```
cd ./main/tests/cypress
yarn install
```
### 2. Start the test runner
The test runner assumes that OpenRefine is up and running on the local machine, the tests themselves do not launch OpenRefine, nor restarts it.
Start OpenRefine with
```shell
./refine
```
Then start Cypress
```shell
yarn --cwd ./main/tests/cypress run cypress open
```
### 3. Run the existing tests
Once the test runner is up, you can choose to run one or several tests by selecting them from the interface.
Click on one of them and the test will start.
### 4. Add your first test
- Add a `test.spec.js` into the cypress/integration folder.
- The test is instantly available in the list
- Click on the test
- Start to add some code
## Tests technical documentation
### A typical test
A typical OpenRefine test starts with the following code
```javascript
it('Ensure cells are blanked down', function () {
cy.loadAndVisitProject('food.mini')
cy.get('.viewpanel-sorting a').contains('Sort').click()
cy.get('.viewpanel').should('to.contain', 'Something')
})
```
The first noticeable thing about a test is the description (`Ensure cells are blanked down`), which describes what the test is doing.
Lines usually starts with `cy.something...`, which is the main way to interact with the Cypress framework.
A few examples:
- `cy.get('a.my-class')` will retrieve the `<a class="my-class" />` element
- `cy.click()` will click on the element
- eventually, `cy.should()` will perform an assertion, for example that the element contains an expected text with `cy.should('to.contains', 'my text')`
On top of that, OpenRefine contributors have added some functions for common OpenRefine interactions.
For example
- `cy.loadAndVisitProject` will create a fresh project in OpenRefine
- `cy.assertCellEquals` will ensure that a cell contains a given value
See below on the dedicated section 'Testing utilities'
### Testing guidelines
- `cy.wait` should be used in the last resort scenario. It's considered a bad practice, though sometimes there is no other choice
- Tests should remain isolated from each other. It's best to try one feature at the time
- A test should always start with a fresh project
- The name of the files should mirror the OpenRefine UI organization
### Testing utilities
OpenRefine contributors have added some utility methods on the top of the Cypress framework.
Those methods perform some common actions or assertions on OpenRefine, to avoid code duplication.
Utilities can be found in `cypress/support/commands.js`.
The most important utility method is `loadAndVisitProject`.
This method will create a fresh OpenRefine project based on a dataset given as a parameter.
The fixture parameter can be
- An arbitrary array, the first row is for the column names, other rows are for the values
Use an arbitrary array **only** if the test requires some specific grid values
**Example:**
```javascript
const fixture = [
['Column A', 'Column B', 'Column C'],
['0A', '0B', '0C'],
['1A', '1B', '1C'],
['2A', '2B', '2C'],
]
cy.loadAndVisitProject(fixture)
```
- A referenced dataset: `food.small` or `food.mini`
Most of the time, tests does not require any specific grid values
Use food.mini as much as possible, it loads 2 rows a very few columns in the grid
Use food.small if the test requires a few hundreds of rows in the grid
Those datasets live in `cypress/fixtures`
### Browsers
In terms of browsers, Cypress is using what is installed on your operating system. In terms of browsers, Cypress is using what is installed on your operating system.
See the [Cypress documentation](https://docs.cypress.io/guides/guides/launching-browsers.html#Browsers) for a list of supported browsers See the [Cypress documentation](https://docs.cypress.io/guides/guides/launching-browsers.html#Browsers) for a list of supported browsers
## Folder organization ### Folder organization
Tests are located in main/tests/cypress. Tests are located in main/tests/cypress.
The test should not use any file outside the cypress folder. The test should not use any file outside the cypress folder.
@ -83,7 +172,7 @@ The test should not use any file outside the cypress folder.
- `/screenshots` and `/videos` contains the recording of the tests, Git ignored - `/screenshots` and `/videos` contains the recording of the tests, Git ignored
- `/support` is a custom library of assertion and common user actions, to avoid code duplication in the tests themselves - `/support` is a custom library of assertion and common user actions, to avoid code duplication in the tests themselves
## Configuration ### Configuration
Cypress execution can be configured with environment variables, they can be declared at the OS level, or when running the test Cypress execution can be configured with environment variables, they can be declared at the OS level, or when running the test
@ -93,11 +182,11 @@ Available variables are
Cypress contains and [exaustive documentation](https://docs.cypress.io/guides/guides/environment-variables.html#Setting) about configuration, but here are two simple ways to configure the execution of the tests: Cypress contains and [exaustive documentation](https://docs.cypress.io/guides/guides/environment-variables.html#Setting) about configuration, but here are two simple ways to configure the execution of the tests:
### Overriding with a cypress.env.json file #### Overriding with a cypress.env.json file
This file is ignored by Git, and you can use it to configure Cypress locally This file is ignored by Git, and you can use it to configure Cypress locally
### Command-line #### Command-line
You can pass variables at the command-line level You can pass variables at the command-line level
@ -105,6 +194,17 @@ You can pass variables at the command-line level
yarn --cwd ./main/tests/cypress run cypress open --env OPENREFINE_URL="http://localhost:1234" yarn --cwd ./main/tests/cypress run cypress open --env OPENREFINE_URL="http://localhost:1234"
``` ```
## CI/CD
In CI/CD, tests are runned headless, with the following command-line
```shell
./refine ui_test chrome
```
Results are displayed in the standard output
## Resources ## Resources
[Cypress command line options](https://docs.cypress.io/guides/guides/command-line.html#Installation) [Cypress command line options](https://docs.cypress.io/guides/guides/command-line.html#Installation)
[Lots of good Cypress examples](https://example.cypress.io/)

View File

@ -35,6 +35,9 @@ Cypress.Commands.add('editCell', (rowIndex, columnName, value) => {
cy.get('.menu-container button[bind="okButton"]').click(); cy.get('.menu-container button[bind="okButton"]').click();
}); });
/**
* Ensure a textarea have a value that id equal to the JSON given as parameter
*/
Cypress.Commands.add('assertTextareaHaveJsonValue', (selector, json) => { Cypress.Commands.add('assertTextareaHaveJsonValue', (selector, json) => {
cy.get(selector).then((el) => { cy.get(selector).then((el) => {
// expected json needs to be parsed / restringified, to avoid inconsitencies about spaces and tabs // expected json needs to be parsed / restringified, to avoid inconsitencies about spaces and tabs
@ -42,6 +45,10 @@ Cypress.Commands.add('assertTextareaHaveJsonValue', (selector, json) => {
cy.expect(JSON.stringify(present)).to.equal(JSON.stringify(json)); cy.expect(JSON.stringify(present)).to.equal(JSON.stringify(json));
}); });
}); });
/**
* Open OpenRefine
*/
Cypress.Commands.add('visitOpenRefine', (options) => { Cypress.Commands.add('visitOpenRefine', (options) => {
cy.visit(Cypress.env('OPENREFINE_URL'), options); cy.visit(Cypress.env('OPENREFINE_URL'), options);
}); });
@ -78,6 +85,9 @@ Cypress.Commands.add('doCreateProjectThroughUserInterface', () => {
}); });
}); });
/**
* Cast a whole column to the given type, using Edit Cell / Common transform / To {type}
*/
Cypress.Commands.add('castColumnTo', (selector, target) => { Cypress.Commands.add('castColumnTo', (selector, target) => {
cy.get( cy.get(
'.data-table th:contains("' + selector + '") .column-header-menu' '.data-table th:contains("' + selector + '") .column-header-menu'
@ -90,6 +100,9 @@ Cypress.Commands.add('castColumnTo', (selector, target) => {
cy.get('body > .menu-container').eq(2).contains(targetAction).click(); cy.get('body > .menu-container').eq(2).contains(targetAction).click();
}); });
/**
* Return the td element for a given row index and column name
*/
Cypress.Commands.add('getCell', (rowIndex, columnName) => { Cypress.Commands.add('getCell', (rowIndex, columnName) => {
const cssRowIndex = rowIndex + 1; const cssRowIndex = rowIndex + 1;
// first get the header, to know the cell index // first get the header, to know the cell index
@ -102,6 +115,9 @@ Cypress.Commands.add('getCell', (rowIndex, columnName) => {
}); });
}); });
/**
* Make an assertion about the content of a cell, for a given row index and column name
*/
Cypress.Commands.add('assertCellEquals', (rowIndex, columnName, value) => { Cypress.Commands.add('assertCellEquals', (rowIndex, columnName, value) => {
const cssRowIndex = rowIndex + 1; const cssRowIndex = rowIndex + 1;
// first get the header, to know the cell index // first get the header, to know the cell index
@ -121,25 +137,40 @@ Cypress.Commands.add('assertCellEquals', (rowIndex, columnName, value) => {
}); });
}); });
/**
* Navigate to one of the entries of the main left menu of OpenRefine (Create Project, Open Project, Import Project, Language Settings)
*/
Cypress.Commands.add('navigateTo', (target) => { Cypress.Commands.add('navigateTo', (target) => {
cy.get('#action-area-tabs li').contains(target).click(); cy.get('#action-area-tabs li').contains(target).click();
}); });
/**
* Wait for OpenRefine to finish an Ajax load
*/
Cypress.Commands.add('waitForOrOperation', () => { Cypress.Commands.add('waitForOrOperation', () => {
cy.get('body[ajax_in_progress="true"]'); cy.get('body[ajax_in_progress="true"]');
cy.get('body[ajax_in_progress="false"]'); cy.get('body[ajax_in_progress="false"]');
}); });
/**
* Delete a column from the grid
*/
Cypress.Commands.add('deleteColumn', (columnName) => { Cypress.Commands.add('deleteColumn', (columnName) => {
cy.get('.data-table th[title="' + columnName + '"]').should('exist'); cy.get('.data-table th[title="' + columnName + '"]').should('exist');
cy.columnActionClick(columnName, ['Edit column', 'Remove this column']); cy.columnActionClick(columnName, ['Edit column', 'Remove this column']);
cy.get('.data-table th[title="' + columnName + '"]').should('not.exist'); cy.get('.data-table th[title="' + columnName + '"]').should('not.exist');
}); });
/**
* Wait until a dialog panel appear
*/
Cypress.Commands.add('waitForDialogPanel', () => { Cypress.Commands.add('waitForDialogPanel', () => {
cy.get('body > .dialog-container > .dialog-frame').should('be.visible'); cy.get('body > .dialog-container > .dialog-frame').should('be.visible');
}); });
/**
* Click on the OK button of a dialog panel
*/
Cypress.Commands.add('confirmDialogPanel', () => { Cypress.Commands.add('confirmDialogPanel', () => {
cy.get( cy.get(
'body > .dialog-container > .dialog-frame .dialog-footer button[bind="okButton"]' 'body > .dialog-container > .dialog-frame .dialog-footer button[bind="okButton"]'
@ -147,6 +178,9 @@ Cypress.Commands.add('confirmDialogPanel', () => {
cy.get('body > .dialog-container > .dialog-frame').should('not.exist'); cy.get('body > .dialog-container > .dialog-frame').should('not.exist');
}); });
/**
* Will click on a menu entry for a given column name
*/
Cypress.Commands.add('columnActionClick', (columnName, actions) => { Cypress.Commands.add('columnActionClick', (columnName, actions) => {
cy.get( cy.get(
'.data-table th:contains("' + columnName + '") .column-header-menu' '.data-table th:contains("' + columnName + '") .column-header-menu'
@ -158,11 +192,20 @@ Cypress.Commands.add('columnActionClick', (columnName, actions) => {
cy.get('body[ajax_in_progress="false"]'); cy.get('body[ajax_in_progress="false"]');
}); });
/**
* Go to a project, given it's id
*/
Cypress.Commands.add('visitProject', (projectId) => { Cypress.Commands.add('visitProject', (projectId) => {
cy.visit(Cypress.env('OPENREFINE_URL') + '/project?project=' + projectId); cy.visit(Cypress.env('OPENREFINE_URL') + '/project?project=' + projectId);
cy.get('#project-title').should('exist'); cy.get('#project-title').should('exist');
}); });
/**
* Load a new project in OpenRefine, and open the project
* The fixture can be
* * an arbitrary array that will be loaded in the grid. The first row is for the columns names
* * a file referenced in fixtures.js (food.mini | food.small)
*/
Cypress.Commands.add( Cypress.Commands.add(
'loadAndVisitProject', 'loadAndVisitProject',
(fixture, projectName = Date.now()) => { (fixture, projectName = Date.now()) => {