From ef22f8759fc22216df1decc5208f914a5d546b2d Mon Sep 17 00:00:00 2001 From: Ivan Babak Date: Tue, 10 Apr 2018 06:19:57 -0700 Subject: [PATCH] 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 --- .gitignore | 4 ++ cypress.json | 5 ++ cypress/fixtures/test-app/index.html | 61 ++++++++++++++++++++ cypress/integration/commands.spec.js | 38 ++++++++++++ cypress/plugins/index.js | 2 + cypress/support/index.js | 1 + jest.config.js | 5 ++ package.json | 24 ++++++-- src/__tests__/__snapshots__/commands.js.snap | 11 ++++ src/__tests__/add-commands.js | 19 ++++++ src/__tests__/commands.js | 14 +++++ src/add-commands.js | 4 +- src/index.js | 45 +++++---------- 13 files changed, 194 insertions(+), 39 deletions(-) create mode 100644 cypress.json create mode 100644 cypress/fixtures/test-app/index.html create mode 100644 cypress/integration/commands.spec.js create mode 100644 cypress/plugins/index.js create mode 100644 cypress/support/index.js create mode 100644 jest.config.js create mode 100644 src/__tests__/__snapshots__/commands.js.snap create mode 100644 src/__tests__/add-commands.js create mode 100644 src/__tests__/commands.js diff --git a/.gitignore b/.gitignore index 09048d2..5713cd4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ dist # when working with contributors package-lock.json yarn.lock + +# cypress recordings during a test run are temporary files +/cypress/videos/ +/cypress/screenshots/ diff --git a/cypress.json b/cypress.json new file mode 100644 index 0000000..11a38d4 --- /dev/null +++ b/cypress.json @@ -0,0 +1,5 @@ +{ + "baseUrl": "http://localhost:13370", + "videoRecording": false, + "screenshotOnHeadlessFailure": false +} diff --git a/cypress/fixtures/test-app/index.html b/cypress/fixtures/test-app/index.html new file mode 100644 index 0000000..fa3450e --- /dev/null +++ b/cypress/fixtures/test-app/index.html @@ -0,0 +1,61 @@ + + + + + + + cypress-testing-library + + + +
+ No auto-reload after changing this static HTML markup: + click Run All Tests. +
+
+

getByPlaceholderText

+ +
+
+

getByText

+ +
+
+

getByLabelText

+ + +
+
+

getByAltText

+ Image Alt Text +
+
+

getByTestId

+ +
+ + + + diff --git a/cypress/integration/commands.spec.js b/cypress/integration/commands.spec.js new file mode 100644 index 0000000..7f18b34 --- /dev/null +++ b/cypress/integration/commands.spec.js @@ -0,0 +1,38 @@ +describe('dom-testing-library commands', () => { + beforeEach(() => { + cy.visit('/') + }) + it('getByPlaceholderText', () => { + cy + .getByPlaceholderText('Placeholder Text') + .click() + .type('Hello Placeholder') + }) + + it('getByText', () => { + cy + .getByText('Button Text') + .click() + }) + + it('getByLabelText', () => { + cy + .getByLabelText('Label For Input Labelled By Id') + .click() + .type('Hello Input Labelled By Id') + }) + + it('getByAltText', () => { + cy + .getByAltText('Image Alt Text') + .click() + }) + + it('getByTestId', () => { + cy + .getByTestId('image-with-random-alt-tag') + .click() + }) +}) + +/* global cy */ diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 0000000..07dd7dd --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,2 @@ +// Keeping this file here, otherwise it gets recreated by Cypress on each run. +module.exports = () => {} diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 0000000..b2c012d --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1 @@ +import '../../src/add-commands' diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..4160d66 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,5 @@ +const jestConfig = require('kcd-scripts/jest') + +module.exports = Object.assign(jestConfig, { + testEnvironment: 'jest-environment-jsdom', +}) diff --git a/package.json b/package.json index 5b96b98..d6f821d 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,14 @@ "add-contributor": "kcd-scripts contributors add", "build": "kcd-scripts build", "lint": "kcd-scripts lint", - "test": "echo TODO", - "test:ci": "echo TODO", - "test:update": "npm test -- --updateSnapshot --coverage", + "test": "npm-run-all --parallel test:unit test:cypress", + "test:unit": "kcd-scripts test --no-watch", + "test:unit:watch": "kcd-scripts test", + "test:cypress:serve": "serve --clipless --local --port 13370 ./cypress/fixtures/test-app", + "test:cypress:run": "cypress run", + "test:cypress:open": "cypress open", + "test:cypress": "npm-run-all --silent --parallel --race test:cypress:serve test:cypress:run", + "test:cypress:dev": "npm-run-all --silent --parallel --race test:cypress:serve test:cypress:open", "validate": "kcd-scripts validate build,lint,test", "setup": "npm install && npm run validate -s", "precommit": "kcd-scripts precommit" @@ -34,16 +39,23 @@ "author": "Kent C. Dodds (http://kentcdodds.com/)", "license": "MIT", "dependencies": { - "dom-testing-library": "^1.0.0" + "dom-testing-library": "^1.3.0" }, "devDependencies": { - "kcd-scripts": "^0.36.1" + "cypress": "^2.1.0", + "kcd-scripts": "^0.37.0", + "npm-run-all": "^4.1.2", + "serve": "^6.5.4" }, "peerDependencies": { "cypress": "^2.1.0" }, "eslintConfig": { - "extends": "./node_modules/kcd-scripts/eslint.js" + "extends": "./node_modules/kcd-scripts/eslint.js", + "rules": { + "import/prefer-default-export": "off", + "import/no-unassigned-import": "off" + } }, "eslintIgnore": [ "node_modules", diff --git a/src/__tests__/__snapshots__/commands.js.snap b/src/__tests__/__snapshots__/commands.js.snap new file mode 100644 index 0000000..c9dd4f1 --- /dev/null +++ b/src/__tests__/__snapshots__/commands.js.snap @@ -0,0 +1,11 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`exports expected commands 1`] = ` +Array [ + "getByPlaceholderText", + "getByText", + "getByLabelText", + "getByAltText", + "getByTestId", +] +`; diff --git a/src/__tests__/add-commands.js b/src/__tests__/add-commands.js new file mode 100644 index 0000000..e754820 --- /dev/null +++ b/src/__tests__/add-commands.js @@ -0,0 +1,19 @@ +import {commands} from '../' + +test('adds commands to Cypress', () => { + const addMock = jest.fn().mockName('Cypress.Commands.add') + global.Cypress = {Commands: {add: addMock}} + global.cy = {} + + require('../add-commands') + + expect(addMock).toHaveBeenCalledTimes(commands.length) + commands.forEach(({name}, index) => { + expect(addMock.mock.calls[index]).toMatchObject([ + name, + // We get a new function that is `command.bind(null, cy)` i.e. global `cy` passed into the first argument. + // The commands themselves will be tested separately in the Cypress end-to-end tests. + expect.any(Function), + ]) + }) +}) diff --git a/src/__tests__/commands.js b/src/__tests__/commands.js new file mode 100644 index 0000000..5e88f83 --- /dev/null +++ b/src/__tests__/commands.js @@ -0,0 +1,14 @@ +import {commands} from '../' + +test('exports expected commands', () => { + expect(commands).toMatchObject(expect.any(Array)) + expect(commands.map(({name}) => name)).toMatchSnapshot() + commands.forEach(command => + expect(command).toMatchObject({ + name: expect.any(String), + // We get a new function that wraps the respective query from `dom-testing-library`. + // The commands themselves will be tested separately in the Cypress end-to-end tests. + command: expect.any(Function), + }), + ) +}) diff --git a/src/add-commands.js b/src/add-commands.js index 43e8f14..52d90ba 100644 --- a/src/add-commands.js +++ b/src/add-commands.js @@ -1,7 +1,7 @@ import {commands} from './' commands.forEach(({name, command}) => { - Cypress.Commands.add(name, command) + Cypress.Commands.add(name, command.bind(null, cy)) }) -/* global Cypress */ +/* global Cypress, cy */ diff --git a/src/index.js b/src/index.js index a18dff4..97528bd 100644 --- a/src/index.js +++ b/src/index.js @@ -1,44 +1,27 @@ -import {queries} from 'dom-testing-library' +import {queries, waitForElement} from 'dom-testing-library' const commands = Object.keys(queries) .filter(queryName => queryName.startsWith('getBy')) .map(queryName => { return { name: queryName, - command: (...args) => { - const fn = new Function( - 'args', - 'query', - 'getCommandWaiter', + command: (cy, ...args) => { + const queryImpl = queries[queryName] + const commandImpl = doc => + waitForElement(() => queryImpl(doc, ...args), {container: doc}) + const thenHandler = new Function( + 'commandImpl', ` - return function Command__${queryName}({document}) { - return getCommandWaiter(document, () => query(document, ...args))(); - }; + return function Command__${queryName}(thenArgs) { + return commandImpl(thenArgs.document) + } `, - )(args, queries[queryName], getCommandWaiter) - return cy.window({log: false}).then(fn) + )(commandImpl) + return cy.window({log: false}).then(thenHandler) }, } }) -function getCommandWaiter(container, fn) { - return function waiter() { - const val = fn() - if (val) { - return val - } else { - return new Promise(resolve => { - const observer = new MutationObserver(() => { - observer.disconnect() - resolve(waiter()) - }) - observer.observe(container, {subtree: true, childList: true}) - }) - } - } -} - -export {commands, getCommandWaiter} +export {commands} -/* eslint no-new-func:0, import/default:0 */ -/* global cy */ +/* eslint no-new-func:0 */