Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFR] Add End2End (E2E) tests #57

Merged
merged 16 commits into from
Mar 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ npm-debug.log
node_modules
lib
es6
example/static
docs/_site/
14 changes: 13 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
language: node_js
node_js:
- '5'
- '7.4.0'
env:
global:
- CHROME_BIN="chromium-browser"
- DISPLAY=:99.0
- NODE_ENV=test
dist: trusty
cache:
directories:
- node_modules
before_install:
- "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16"
- sh -e /etc/init.d/xvfb start
- sleep 3
before_script:
- make example_install
branches:
only:
- master
Expand Down
20 changes: 17 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.PHONY: build help

help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
@grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

install: package.json ## install dependencies
@npm install
Expand All @@ -21,15 +21,29 @@ watch: ## continuously compile ES6 files to JS
doc: ## compile doc as html and launch doc web server
@cd docs && jekyll server . --watch

test: ## launch unit tests
test: test-unit test-e2e ## launch all tests

test-unit: ## launch unit tests
@NODE_ENV=test NODE_ICU_DATA=node_modules/full-icu ./node_modules/.bin/mocha \
--require ignore-styles \
--compilers js:babel-register \
'./src/**/*.spec.js'

test-watch: ## launch unit tests and watch for changes
test-unit-watch: ## launch unit tests and watch for changes
@NODE_ENV=test NODE_ICU_DATA=node_modules/full-icu ./node_modules/.bin/mocha \
--require ignore-styles \
--compilers js:babel-register \
--watch \
'./src/**/*.spec.js'

test-e2e: ## launch end-to-end tests
@if [ "$(build)" != "false" ]; then \
echo 'Building example code (call "make build=false test-e2e" to skip the build)...'; \
cd example && ../node_modules/.bin/webpack; \
fi
@echo 'Launching e2e tests...'
@NODE_ENV=test node_modules/.bin/mocha \
--compilers js:babel-register \
--timeout 10000 \
./e2e/tests/server.js \
./e2e/tests/*.js
6 changes: 6 additions & 0 deletions e2e/chromeDriver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const chrome = require('chromedriver');
const webdriver = require('selenium-webdriver');

module.exports = new webdriver.Builder()
.forBrowser('chrome')
.build();
52 changes: 52 additions & 0 deletions e2e/pages/EditPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { By, until } from 'selenium-webdriver';

module.exports = (url) => (driver) => ({
elements: {
appLoader: By.css('.app-loader'),
input: name => By.css(`.edit-page input[name='${name}']`),
submitButton: By.css(".edit-page button[type='submit']"),
tab: index => By.css(`button.form-tab:nth-of-type(${index})`),
title: By.css('.title'),
},

navigate() {
driver.navigate().to(url);
return this.waitUntilDataLoaded();
},

waitUntilVisible() {
return driver.wait(until.elementLocated(this.elements.title));
},

waitUntilDataLoaded() {
let continued = true;
return driver.wait(until.elementLocated(this.elements.appLoader), 400)
.catch(() => continued = false) // no loader - we're on the same page !
.then(() => continued ? driver.wait(until.stalenessOf(driver.findElement(this.elements.appLoader))) : true)
.then(() => driver.sleep(100)); // let some time to redraw
},

getInputValue(name) {
const input = driver.findElement(this.elements.input(name));
return input.getAttribute('value');
},

setInputValue(name, value, clearPreviousValue = true) {
const input = driver.findElement(this.elements.input(name));
if (clearPreviousValue) {
input.clear();
}
return input.sendKeys(value);
},

gotoTab(index) {
const tab = driver.findElement(this.elements.tab(index));
tab.click();
return driver.sleep(200);
},

submit() {
driver.findElement(this.elements.submitButton).click();
return this.waitUntilDataLoaded();
},
});
81 changes: 81 additions & 0 deletions e2e/pages/ListPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { By, until } from 'selenium-webdriver';

module.exports = (url) => (driver) => ({
elements: {
addFilterButton: By.css('.add-filter'),
appLoader: By.css('.app-loader'),
displayedRecords: By.css('.displayed-records'),
filter: name => By.css(`.filter-field[data-source='${name}'] input`),
filterMenuItem: source => By.css(`.new-filter-item[data-key="${source}"]`),
hideFilterButton: source => By.css(`.filter-field[data-source="${source}"] .hide-filter`),
nextPage: By.css('.next-page'),
pageNumber: n => By.css(`.page-number[data-page='${n}']`),
previousPage: By.css('.previous-page'),
recordRows: By.css('.datagrid-body tr'),
title: By.css('.title'),
},

navigate() {
driver.navigate().to(url);
return this.waitUntilDataLoaded();
},

waitUntilVisible() {
return driver.wait(until.elementLocated(this.elements.title));
},

waitUntilDataLoaded() {
let continued = true;
return driver.wait(until.elementLocated(this.elements.appLoader), 400)
.catch(() => continued = false) // no loader - we're on the same page !
.then(() => continued ? driver.wait(until.stalenessOf(driver.findElement(this.elements.appLoader))) : true)
.then(() => driver.sleep(100)); // let some time to redraw
},

getNbRows() {
return driver.findElements(this.elements.recordRows)
.then(rows => rows.length);
},

getNbPagesText() {
return driver.findElement(this.elements.displayedRecords).getText();
},

nextPage() {
driver.findElement(this.elements.nextPage).click();
return this.waitUntilDataLoaded();
},

previousPage() {
driver.findElement(this.elements.previousPage).click();
return this.waitUntilDataLoaded();
},

goToPage(n) {
driver.findElement(this.elements.pageNumber(n)).click();
return this.waitUntilDataLoaded();
},

setFilterValue(name, value, clearPreviousValue = true) {
const filterField = driver.findElement(this.elements.filter(name));
if (clearPreviousValue) {
filterField.clear();
}
filterField.sendKeys(value);
return driver.sleep(3000); // wait for debounce and reload
},

showFilter(name) {
const addFilterButton = driver.findElement(this.elements.addFilterButton);
addFilterButton.click();
driver.sleep(500); // wait until the dropdown animation ends
driver.wait(until.elementLocated(this.elements.filterMenuItem(name)));
driver.findElement(this.elements.filterMenuItem(name)).click();
return driver.sleep(400); // wait until the menu ClickAwayListener disappears
},

hideFilter(name) {
const hideFilterButton = driver.findElement(this.elements.hideFilterButton(name));
return hideFilterButton.click();
},
});
28 changes: 28 additions & 0 deletions e2e/tests/edit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import assert from 'assert';
import { By, until } from 'selenium-webdriver';
import driver from '../chromeDriver';
import editPageFactory from '../pages/EditPage';

describe('Edit Page', () => {
const EditPage = editPageFactory('http://localhost:8083/#posts/5')(driver);

beforeEach(async () => await EditPage.navigate());

describe('TabbedForm', () => {
it('should display the title in a TextField', async () => {
assert.equal(await EditPage.getInputValue('title'), 'Sed quo et et fugiat modi');
});

it('should allow to update elements', async () => {
await EditPage.setInputValue('title', 'Lorem Ipsum');
await EditPage.submit();
await EditPage.navigate();
assert.equal(await EditPage.getInputValue('title'), 'Lorem Ipsum');
});

it('should allow to switch tabs', async () => {
await EditPage.gotoTab(2);
assert.equal(await EditPage.getInputValue('average_note'), '3');
})
});
});
56 changes: 56 additions & 0 deletions e2e/tests/list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import assert from 'assert';
import { By, until } from 'selenium-webdriver';
import driver from '../chromeDriver';
import listPageFactory from '../pages/ListPage';

describe('List Page', () => {
const ListPage = listPageFactory('http://localhost:8083/#posts')(driver);

beforeEach(async () => await ListPage.navigate());

describe('Pagination', () => {
it('should display paginated list of available posts', async () => {
assert.equal(await ListPage.getNbPagesText(), '1-10 of 13');
});

it('should switch page when clicking on previous/next page buttons or page numbers', async () => {
await ListPage.nextPage();
assert.equal(await ListPage.getNbPagesText(), '11-13 of 13');

await ListPage.previousPage();
assert.equal(await ListPage.getNbPagesText(), '1-10 of 13');

await ListPage.goToPage(2);
assert.equal(await ListPage.getNbPagesText(), '11-13 of 13');
});
});

describe('Filtering', () => {
it('should display `alwaysOn` filters by default', async () => {
await driver.wait(until.elementLocated(ListPage.elements.filter('q')));

const qFilter = await driver.findElements(ListPage.elements.filter('q'));
assert.equal(qFilter.length, 1);
});

it('should filter directly while typing (with some debounce)', async () => {
await ListPage.setFilterValue('q', 'quis culpa impedit');
assert.equal(await ListPage.getNbRows(), 1);
const displayedPosts = await driver.findElements(ListPage.elements.recordRows);
const title = await displayedPosts[0].findElement(By.css('.column-title'));
assert.equal(await title.getText(), 'Omnis voluptate enim similique est possimus');
});

it('should display new filter when clicking on "Add Filter"', async () => {
await ListPage.showFilter('title');
const filters = await driver.findElements(ListPage.elements.filter('title'));
assert.equal(filters.length, 1);
});

it('should hide filter when clicking on hide button', async () => {
await ListPage.hideFilter('title');
const filters = await driver.findElements(ListPage.elements.filter('title'));
assert.equal(filters.length, 0);
});
});
});
16 changes: 16 additions & 0 deletions e2e/tests/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import express from 'express';
import path from 'path';
import driver from '../chromeDriver';

let listeningServer;

before(() => {
const server = express();
server.use('/', express.static(path.join(__dirname, '../../example')));
listeningServer = server.listen(8083);
});

after(async () => {
listeningServer.close();
return driver.quit();
});
2 changes: 1 addition & 1 deletion example/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const uploadCapableClient = addUploadFeature(restClient);
const delayedRestClient = (type, resource, params) => new Promise(resolve => setTimeout(() => resolve(uploadCapableClient(type, resource, params)), 1000));

render(
<Admin restClient={delayedRestClient} title="Example Admin" locale={resolveBrowserLocale()} messages={messages}>
<Admin restClient={delayedRestClient} title="Example Admin" locale="en" messages={messages}>
<Resource name="posts" list={PostList} create={PostCreate} edit={PostEdit} show={PostShow} remove={Delete} icon={PostIcon} />
<Resource name="comments" list={CommentList} create={CommentCreate} edit={CommentEdit} remove={Delete} icon={CommentIcon} />
</Admin>,
Expand Down
2 changes: 1 addition & 1 deletion example/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module.exports = {
devtool: 'eval',
entry: './app.js',
output: {
path: path.join(__dirname, 'dist'),
path: path.join(__dirname, 'static'),
filename: 'bundle.js',
publicPath: '/static/',
},
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,21 @@
"babel-preset-react": "~6.16.0",
"babel-preset-stage-0": "~6.16.0",
"babel-register": "~6.18.0",
"chromedriver": "~2.26.1",
"css-loader": "~0.26.0",
"enzyme": "~2.6.0",
"eslint": "~3.11.0",
"eslint-config-airbnb": "~13.0.0",
"eslint-plugin-import": "~2.2.0",
"eslint-plugin-jsx-a11y": "~2.2.3",
"eslint-plugin-react": "~6.7.1",
"express": "^4.14.0",
"extract-text-webpack-plugin": "~1.0.1",
"full-icu": "~1.0.3",
"ignore-styles": "~5.0.1",
"mocha": "~3.2.0",
"react-addons-test-utils": "~15.4.0",
"selenium-webdriver": "^3.0.0-beta-3",
"sinon": "~1.17.6",
"style-loader": "~0.13.1",
"webpack": "~1.13.2",
Expand Down
2 changes: 1 addition & 1 deletion src/mui/detail/Edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class Edit extends Component {
const titleElement = data ? <Title title={title} record={data} defaultTitle={defaultTitle} /> : '';

return (
<div>
<div className="edit-page">
<Card style={{ opacity: isLoading ? 0.8 : 1 }} key={key}>
{actions && React.cloneElement(actions, {
basePath,
Expand Down
2 changes: 1 addition & 1 deletion src/mui/form/SimpleForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import FormField from './FormField';
import Toolbar from './Toolbar';

export const SimpleForm = ({ children, handleSubmit, invalid, record, resource, basePath }) => (
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit} className="simple-form">
<div style={{ padding: '0 1em 1em 1em' }}>
{React.Children.map(children, input => input && (
<div key={input.props.source} style={input.props.style}>
Expand Down
16 changes: 13 additions & 3 deletions src/mui/form/TabbedForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,21 @@ export class TabbedForm extends Component {
render() {
const { children, contentContainerStyle, handleSubmit, invalid, record, resource, basePath, translate } = this.props;
return (
<form onSubmit={handleSubmit}>
<form onSubmit={handleSubmit} className="tabbed-form">
<div style={{ padding: '0 1em 1em 1em' }}>
<Tabs value={this.state.value} onChange={this.handleChange} contentContainerStyle={contentContainerStyle}>
<Tabs
value={this.state.value}
onChange={this.handleChange}
contentContainerStyle={contentContainerStyle}
>
{React.Children.map(children, (tab, index) =>
<Tab key={tab.props.value} label={translate(tab.props.label)} value={index} icon={tab.props.icon}>
<Tab
key={tab.props.value}
className="form-tab"
label={translate(tab.props.label)}
value={index}
icon={tab.props.icon}
>
{React.cloneElement(tab, { resource, record, basePath })}
</Tab>
)}
Expand Down
Loading