Skip to content

Commit ef22f87

Browse files
sompylasarKent C. Dodds
authored andcommitted
feat(commands): use waitForElement; add jest & cypress tests (#2)
* feat(commands): use waitForElement; add jest & cypress tests @kentcdodds I apologize for this one huge commit but I wanted to make it all real, fast (in a few hours). - change commands to use `waitForElement` from `dom-testing-library` (updated to latest) - update `kcd-scripts` dependency to latest - add jest tests for basic smoke check on the exports - add cypress tests for functionality check of the commands - add npm scripts that run the tests in both development and CI environments (somewhat convoluted because cypress hasn't got enough hooks to spin up the fixture server from JavaScript; I didn't want to write a Node app so I used `serve` which needs to be killed after cypress exits but regardless of its exit code, so I used `npm-run-all` and `fkill-cli` and a combination of npm scripts with parallel/sequential execution) * fix(ci): attempt to use `[` instead of `[[` for bash test tool * fix(package): add missing `fkill-cli` needed by npm scripts * Update package.json * Update cypress.json * Update commands.spec.js * Update package.json
1 parent 5106558 commit ef22f87

File tree

13 files changed

+194
-39
lines changed

13 files changed

+194
-39
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ dist
1010
# when working with contributors
1111
package-lock.json
1212
yarn.lock
13+
14+
# cypress recordings during a test run are temporary files
15+
/cypress/videos/
16+
/cypress/screenshots/

cypress.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"baseUrl": "http://localhost:13370",
3+
"videoRecording": false,
4+
"screenshotOnHeadlessFailure": false
5+
}

cypress/fixtures/test-app/index.html

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
7+
<title>cypress-testing-library</title>
8+
<style>
9+
blockquote {
10+
margin: 0;
11+
border-left: 4px solid grey;
12+
padding-left: 10px;
13+
color: grey;
14+
}
15+
section {
16+
padding: 10px;
17+
}
18+
</style>
19+
</head>
20+
<body>
21+
<blockquote>
22+
No auto-reload after changing this static HTML markup:
23+
click <span title="Run All Tests"></span> Run All Tests.
24+
</blockquote>
25+
<section>
26+
<h2>getByPlaceholderText</h2>
27+
<input type="text" placeholder="Placeholder Text" />
28+
</section>
29+
<section>
30+
<h2>getByText</h2>
31+
<button onclick="this.innerText = 'Button Clicked'">Button Text</button>
32+
</section>
33+
<section>
34+
<h2>getByLabelText</h2>
35+
<label for="input-labelled-by-id">Label For Input Labelled By Id</label>
36+
<input type="text" placeholder="Input Labelled By Id" id="input-labelled-by-id" />
37+
</section>
38+
<section>
39+
<h2>getByAltText</h2>
40+
<img
41+
src="data:image/png;base64,"
42+
alt="Image Alt Text"
43+
onclick="this.style.border = '5px solid red'"
44+
/>
45+
</section>
46+
<section>
47+
<h2>getByTestId</h2>
48+
<img
49+
data-testid="image-with-random-alt-tag"
50+
src="data:image/png;base64,"
51+
onclick="this.style.border = '5px solid red'"
52+
/>
53+
</section>
54+
<!-- Prettier unindents the script tag below -->
55+
<script>
56+
document
57+
.querySelector('[data-testid="image-with-random-alt-tag"]')
58+
.setAttribute('alt', 'Image Random Alt Text ' + Math.random())
59+
</script>
60+
</body>
61+
</html>

cypress/integration/commands.spec.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
describe('dom-testing-library commands', () => {
2+
beforeEach(() => {
3+
cy.visit('/')
4+
})
5+
it('getByPlaceholderText', () => {
6+
cy
7+
.getByPlaceholderText('Placeholder Text')
8+
.click()
9+
.type('Hello Placeholder')
10+
})
11+
12+
it('getByText', () => {
13+
cy
14+
.getByText('Button Text')
15+
.click()
16+
})
17+
18+
it('getByLabelText', () => {
19+
cy
20+
.getByLabelText('Label For Input Labelled By Id')
21+
.click()
22+
.type('Hello Input Labelled By Id')
23+
})
24+
25+
it('getByAltText', () => {
26+
cy
27+
.getByAltText('Image Alt Text')
28+
.click()
29+
})
30+
31+
it('getByTestId', () => {
32+
cy
33+
.getByTestId('image-with-random-alt-tag')
34+
.click()
35+
})
36+
})
37+
38+
/* global cy */

cypress/plugins/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// Keeping this file here, otherwise it gets recreated by Cypress on each run.
2+
module.exports = () => {}

cypress/support/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import '../../src/add-commands'

jest.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const jestConfig = require('kcd-scripts/jest')
2+
3+
module.exports = Object.assign(jestConfig, {
4+
testEnvironment: 'jest-environment-jsdom',
5+
})

package.json

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@
1010
"add-contributor": "kcd-scripts contributors add",
1111
"build": "kcd-scripts build",
1212
"lint": "kcd-scripts lint",
13-
"test": "echo TODO",
14-
"test:ci": "echo TODO",
15-
"test:update": "npm test -- --updateSnapshot --coverage",
13+
"test": "npm-run-all --parallel test:unit test:cypress",
14+
"test:unit": "kcd-scripts test --no-watch",
15+
"test:unit:watch": "kcd-scripts test",
16+
"test:cypress:serve": "serve --clipless --local --port 13370 ./cypress/fixtures/test-app",
17+
"test:cypress:run": "cypress run",
18+
"test:cypress:open": "cypress open",
19+
"test:cypress": "npm-run-all --silent --parallel --race test:cypress:serve test:cypress:run",
20+
"test:cypress:dev": "npm-run-all --silent --parallel --race test:cypress:serve test:cypress:open",
1621
"validate": "kcd-scripts validate build,lint,test",
1722
"setup": "npm install && npm run validate -s",
1823
"precommit": "kcd-scripts precommit"
@@ -34,16 +39,23 @@
3439
"author": "Kent C. Dodds <[email protected]> (http://kentcdodds.com/)",
3540
"license": "MIT",
3641
"dependencies": {
37-
"dom-testing-library": "^1.0.0"
42+
"dom-testing-library": "^1.3.0"
3843
},
3944
"devDependencies": {
40-
"kcd-scripts": "^0.36.1"
45+
"cypress": "^2.1.0",
46+
"kcd-scripts": "^0.37.0",
47+
"npm-run-all": "^4.1.2",
48+
"serve": "^6.5.4"
4149
},
4250
"peerDependencies": {
4351
"cypress": "^2.1.0"
4452
},
4553
"eslintConfig": {
46-
"extends": "./node_modules/kcd-scripts/eslint.js"
54+
"extends": "./node_modules/kcd-scripts/eslint.js",
55+
"rules": {
56+
"import/prefer-default-export": "off",
57+
"import/no-unassigned-import": "off"
58+
}
4759
},
4860
"eslintIgnore": [
4961
"node_modules",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`exports expected commands 1`] = `
4+
Array [
5+
"getByPlaceholderText",
6+
"getByText",
7+
"getByLabelText",
8+
"getByAltText",
9+
"getByTestId",
10+
]
11+
`;

src/__tests__/add-commands.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {commands} from '../'
2+
3+
test('adds commands to Cypress', () => {
4+
const addMock = jest.fn().mockName('Cypress.Commands.add')
5+
global.Cypress = {Commands: {add: addMock}}
6+
global.cy = {}
7+
8+
require('../add-commands')
9+
10+
expect(addMock).toHaveBeenCalledTimes(commands.length)
11+
commands.forEach(({name}, index) => {
12+
expect(addMock.mock.calls[index]).toMatchObject([
13+
name,
14+
// We get a new function that is `command.bind(null, cy)` i.e. global `cy` passed into the first argument.
15+
// The commands themselves will be tested separately in the Cypress end-to-end tests.
16+
expect.any(Function),
17+
])
18+
})
19+
})

src/__tests__/commands.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {commands} from '../'
2+
3+
test('exports expected commands', () => {
4+
expect(commands).toMatchObject(expect.any(Array))
5+
expect(commands.map(({name}) => name)).toMatchSnapshot()
6+
commands.forEach(command =>
7+
expect(command).toMatchObject({
8+
name: expect.any(String),
9+
// We get a new function that wraps the respective query from `dom-testing-library`.
10+
// The commands themselves will be tested separately in the Cypress end-to-end tests.
11+
command: expect.any(Function),
12+
}),
13+
)
14+
})

src/add-commands.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {commands} from './'
22

33
commands.forEach(({name, command}) => {
4-
Cypress.Commands.add(name, command)
4+
Cypress.Commands.add(name, command.bind(null, cy))
55
})
66

7-
/* global Cypress */
7+
/* global Cypress, cy */

src/index.js

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,27 @@
1-
import {queries} from 'dom-testing-library'
1+
import {queries, waitForElement} from 'dom-testing-library'
22

33
const commands = Object.keys(queries)
44
.filter(queryName => queryName.startsWith('getBy'))
55
.map(queryName => {
66
return {
77
name: queryName,
8-
command: (...args) => {
9-
const fn = new Function(
10-
'args',
11-
'query',
12-
'getCommandWaiter',
8+
command: (cy, ...args) => {
9+
const queryImpl = queries[queryName]
10+
const commandImpl = doc =>
11+
waitForElement(() => queryImpl(doc, ...args), {container: doc})
12+
const thenHandler = new Function(
13+
'commandImpl',
1314
`
14-
return function Command__${queryName}({document}) {
15-
return getCommandWaiter(document, () => query(document, ...args))();
16-
};
15+
return function Command__${queryName}(thenArgs) {
16+
return commandImpl(thenArgs.document)
17+
}
1718
`,
18-
)(args, queries[queryName], getCommandWaiter)
19-
return cy.window({log: false}).then(fn)
19+
)(commandImpl)
20+
return cy.window({log: false}).then(thenHandler)
2021
},
2122
}
2223
})
2324

24-
function getCommandWaiter(container, fn) {
25-
return function waiter() {
26-
const val = fn()
27-
if (val) {
28-
return val
29-
} else {
30-
return new Promise(resolve => {
31-
const observer = new MutationObserver(() => {
32-
observer.disconnect()
33-
resolve(waiter())
34-
})
35-
observer.observe(container, {subtree: true, childList: true})
36-
})
37-
}
38-
}
39-
}
40-
41-
export {commands, getCommandWaiter}
25+
export {commands}
4226

43-
/* eslint no-new-func:0, import/default:0 */
44-
/* global cy */
27+
/* eslint no-new-func:0 */

0 commit comments

Comments
 (0)