From b1b01ae0f106ec78fc405ce118aa28088f989dc0 Mon Sep 17 00:00:00 2001 From: Roland Szoke Date: Fri, 21 Feb 2020 12:07:17 +0100 Subject: [PATCH 1/2] refactor(tests): lint test files --- .eslintrc.json | 14 +- __tests__/Clock.test.js | 40 --- __tests__/Clock.test.jsx | 40 +++ __tests__/Clock.test.native.js | 37 --- __tests__/Clock.test.native.jsx | 40 +++ __tests__/Contacts.test.js | 96 ------- __tests__/Contacts.test.jsx | 115 ++++++++ __tests__/TodoMVC.test.js | 94 ------- __tests__/TodoMVC.test.jsx | 108 ++++++++ ...ts.test.js.snap => Contacts.test.jsx.snap} | 0 ...MVC.test.js.snap => TodoMVC.test.jsx.snap} | 0 .../{batching.test.js => batching.test.jsx} | 25 +- __tests__/edgeCases.test.js | 236 ---------------- __tests__/edgeCases.test.jsx | 261 ++++++++++++++++++ __tests__/router.test.js | 108 -------- __tests__/router.test.jsx | 121 ++++++++ __tests__/staticProps.test.js | 86 +++--- __tests__/{styled.test.js => styled.test.jsx} | 99 +++---- jest.native.json | 2 +- jest.web.json | 2 +- 20 files changed, 809 insertions(+), 715 deletions(-) delete mode 100644 __tests__/Clock.test.js create mode 100644 __tests__/Clock.test.jsx delete mode 100644 __tests__/Clock.test.native.js create mode 100644 __tests__/Clock.test.native.jsx delete mode 100644 __tests__/Contacts.test.js create mode 100644 __tests__/Contacts.test.jsx delete mode 100644 __tests__/TodoMVC.test.js create mode 100644 __tests__/TodoMVC.test.jsx rename __tests__/__snapshots__/{Contacts.test.js.snap => Contacts.test.jsx.snap} (100%) rename __tests__/__snapshots__/{TodoMVC.test.js.snap => TodoMVC.test.jsx.snap} (100%) rename __tests__/{batching.test.js => batching.test.jsx} (96%) delete mode 100644 __tests__/edgeCases.test.js create mode 100644 __tests__/edgeCases.test.jsx delete mode 100644 __tests__/router.test.js create mode 100644 __tests__/router.test.jsx rename __tests__/{styled.test.js => styled.test.jsx} (57%) diff --git a/.eslintrc.json b/.eslintrc.json index 6bbed5c..76771f5 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -10,11 +10,21 @@ "no-nested-ternary": "off", "no-param-reassign": "off", "func-names": "off", - "max-classes-per-file": "off" + "max-classes-per-file": "off", + "react/prefer-stateless-function": "off", + "react/prop-types": "off" }, "globals": { "window": true, "EventTarget": true, - "WebSocket": true + "WebSocket": true, + "describe": true, + "afterAll": true, + "test": true, + "expect": true, + "afterEach": true, + "document": true, + "jest": true, + "Event": true } } diff --git a/__tests__/Clock.test.js b/__tests__/Clock.test.js deleted file mode 100644 index febce7d..0000000 --- a/__tests__/Clock.test.js +++ /dev/null @@ -1,40 +0,0 @@ -import React, { StrictMode } from 'react' -import { render, cleanup, act } from '@testing-library/react/pure' -import sinon from 'sinon' -import App from '../examples/clock/src/App' - -describe('Clock App', () => { - const clock = sinon.useFakeTimers() - const { container, unmount } = render( - - - - ) - - const clearIntervalSpy = sinon.spy(global, 'clearInterval') - - afterAll(() => { - cleanup() - clock.restore() - clearIntervalSpy.restore() - }) - - test('should update to display the current time every second', () => { - expect(container).toHaveTextContent('12:00:00 AM') - - act(() => { - clock.tick(2000) - }) - expect(container).toHaveTextContent('12:00:02 AM') - - act(() => { - clock.tick(8500) - }) - expect(container).toHaveTextContent('12:00:10 AM') - }) - - test('should clean up the interval timer when the component is unmounted', () => { - unmount() - expect(clearIntervalSpy.callCount).toBe(1) - }) -}) diff --git a/__tests__/Clock.test.jsx b/__tests__/Clock.test.jsx new file mode 100644 index 0000000..fbd68b3 --- /dev/null +++ b/__tests__/Clock.test.jsx @@ -0,0 +1,40 @@ +import React, { StrictMode } from 'react'; +import { render, cleanup, act } from '@testing-library/react/pure'; +import sinon from 'sinon'; +import App from '../examples/clock/src/App'; + +describe('Clock App', () => { + const clock = sinon.useFakeTimers(); + const { container, unmount } = render( + + + , + ); + + const clearIntervalSpy = sinon.spy(global, 'clearInterval'); + + afterAll(() => { + cleanup(); + clock.restore(); + clearIntervalSpy.restore(); + }); + + test('should update to display the current time every second', () => { + expect(container).toHaveTextContent('12:00:00 AM'); + + act(() => { + clock.tick(2000); + }); + expect(container).toHaveTextContent('12:00:02 AM'); + + act(() => { + clock.tick(8500); + }); + expect(container).toHaveTextContent('12:00:10 AM'); + }); + + test('should clean up the interval timer when the component is unmounted', () => { + unmount(); + expect(clearIntervalSpy.callCount).toBe(1); + }); +}); diff --git a/__tests__/Clock.test.native.js b/__tests__/Clock.test.native.js deleted file mode 100644 index c57ef6d..0000000 --- a/__tests__/Clock.test.native.js +++ /dev/null @@ -1,37 +0,0 @@ -import React, { StrictMode } from 'react' -import { render, flushMicrotasksQueue } from 'react-native-testing-library' -import sinon from 'sinon' -import App from '../examples/native-clock/App' - -describe('Clock App', () => { - const clock = sinon.useFakeTimers() - const { getByText, unmount } = render( - - - - ) - // flush the inital didMount effect - flushMicrotasksQueue() - - const clearIntervalSpy = sinon.spy(global, 'clearInterval') - - afterAll(() => { - clock.restore() - clearIntervalSpy.restore() - }) - - test('should update to display the current time every second', () => { - expect(getByText('12:00:00 AM')).toBeDefined() - - clock.tick(2000) - expect(getByText('12:00:02 AM')).toBeDefined() - - clock.tick(8500) - expect(getByText('12:00:10 AM')).toBeDefined() - }) - - test('should clean up the interval timer when the component is unmounted', () => { - unmount() - expect(clearIntervalSpy.callCount).toBe(1) - }) -}) diff --git a/__tests__/Clock.test.native.jsx b/__tests__/Clock.test.native.jsx new file mode 100644 index 0000000..8436f97 --- /dev/null +++ b/__tests__/Clock.test.native.jsx @@ -0,0 +1,40 @@ +import React, { StrictMode } from 'react'; +import { + render, + flushMicrotasksQueue, +} from 'react-native-testing-library'; +import sinon from 'sinon'; +import App from '../examples/native-clock/App'; + +describe('Clock App', () => { + const clock = sinon.useFakeTimers(); + const { getByText, unmount } = render( + + + , + ); + // flush the inital didMount effect + flushMicrotasksQueue(); + + const clearIntervalSpy = sinon.spy(global, 'clearInterval'); + + afterAll(() => { + clock.restore(); + clearIntervalSpy.restore(); + }); + + test('should update to display the current time every second', () => { + expect(getByText('12:00:00 AM')).toBeDefined(); + + clock.tick(2000); + expect(getByText('12:00:02 AM')).toBeDefined(); + + clock.tick(8500); + expect(getByText('12:00:10 AM')).toBeDefined(); + }); + + test('should clean up the interval timer when the component is unmounted', () => { + unmount(); + expect(clearIntervalSpy.callCount).toBe(1); + }); +}); diff --git a/__tests__/Contacts.test.js b/__tests__/Contacts.test.js deleted file mode 100644 index b1270d3..0000000 --- a/__tests__/Contacts.test.js +++ /dev/null @@ -1,96 +0,0 @@ -import React, { StrictMode } from 'react' -import { render, cleanup, fireEvent } from '@testing-library/react/pure' -import App from '../examples/contacts/src/App' - -describe('Contacts App', () => { - const { container } = render( - - - - ) - afterAll(cleanup) - - test('should add new contacts', () => { - expect(container).toMatchSnapshot('01. Initial state') - - const nameField = container.querySelector('input[name="name"]') - const emailField = container.querySelector('input[name="email"]') - const createButton = container.querySelector('button') - - fireEvent.change(nameField, { - target: { name: 'name', value: 'Test Contact' } - }) - expect(container).toMatchSnapshot('02. Create Test Contact name') - - fireEvent.change(emailField, { - target: { name: 'email', value: 'test.contact@gmail.com' } - }) - expect(container).toMatchSnapshot('03. Create Test Contact email') - - fireEvent.click(createButton) - expect(container).toMatchSnapshot('04. Add Test Contact') - - fireEvent.change(nameField, { - target: { name: 'name', value: '' } - }) - fireEvent.change(emailField, { - target: { name: 'email', value: '' } - }) - fireEvent.click(createButton) - expect(container).toMatchSnapshot('05. Add Placeholder Contact') - }) - - test('should edit contact', () => { - let display, editor, editButton - - display = container.querySelector('.contact-display') - editButton = display.querySelector('.zmdi-edit') - - fireEvent.click(editButton) - expect(container).toMatchSnapshot('06. Switch Test Contact to Edit Mode') - - editor = container.querySelector('.contact-editor') - const nameField = editor.querySelector('input[name="name"]') - const cancelButton = editor.querySelector('.zmdi-close') - - fireEvent.change(nameField, { - target: { name: 'name', value: 'Edited Test Contact' } - }) - expect(container).toMatchSnapshot('07. Edit Test Contact name') - - fireEvent.click(cancelButton) - expect(container).toMatchSnapshot('08. Cancel Test Contact edit') - - display = container.querySelector('.contact-display') - editButton = display.querySelector('.zmdi-edit') - - fireEvent.click(editButton) - expect(container).toMatchSnapshot('09. Switch Test Contact to edit Mode') - - editor = container.querySelector('.contact-editor') - const emailField = editor.querySelector('input[name="email"]') - const saveButton = editor.querySelector('.zmdi-save') - - fireEvent.change(emailField, { - target: { name: 'email', value: 'test.contact.edited@gmail.com' } - }) - expect(container).toMatchSnapshot('10. Edit Test Contact email') - - fireEvent.click(saveButton) - expect(container).toMatchSnapshot('11. Save Test Contact edit') - }) - - test('should delete contact', () => { - let deleteButton = container.querySelectorAll( - '.contact-display .zmdi-delete' - )[1] - - fireEvent.click(deleteButton) - expect(container).toMatchSnapshot('12. Delete Placeholder Contact') - - deleteButton = container.querySelector('.contact-display .zmdi-delete') - - fireEvent.click(deleteButton) - expect(container).toMatchSnapshot('13. Delete Test Contact') - }) -}) diff --git a/__tests__/Contacts.test.jsx b/__tests__/Contacts.test.jsx new file mode 100644 index 0000000..babd7bf --- /dev/null +++ b/__tests__/Contacts.test.jsx @@ -0,0 +1,115 @@ +import React, { StrictMode } from 'react'; +import { + render, + cleanup, + fireEvent, +} from '@testing-library/react/pure'; +import App from '../examples/contacts/src/App'; + +describe('Contacts App', () => { + const { container } = render( + + + , + ); + afterAll(cleanup); + + test('should add new contacts', () => { + expect(container).toMatchSnapshot('01. Initial state'); + + const nameField = container.querySelector('input[name="name"]'); + const emailField = container.querySelector('input[name="email"]'); + const createButton = container.querySelector('button'); + + fireEvent.change(nameField, { + target: { name: 'name', value: 'Test Contact' }, + }); + expect(container).toMatchSnapshot('02. Create Test Contact name'); + + fireEvent.change(emailField, { + target: { name: 'email', value: 'test.contact@gmail.com' }, + }); + expect(container).toMatchSnapshot( + '03. Create Test Contact email', + ); + + fireEvent.click(createButton); + expect(container).toMatchSnapshot('04. Add Test Contact'); + + fireEvent.change(nameField, { + target: { name: 'name', value: '' }, + }); + fireEvent.change(emailField, { + target: { name: 'email', value: '' }, + }); + fireEvent.click(createButton); + expect(container).toMatchSnapshot('05. Add Placeholder Contact'); + }); + + test('should edit contact', () => { + let display; + let editor; + let editButton; + + display = container.querySelector('.contact-display'); + editButton = display.querySelector('.zmdi-edit'); + + fireEvent.click(editButton); + expect(container).toMatchSnapshot( + '06. Switch Test Contact to Edit Mode', + ); + + editor = container.querySelector('.contact-editor'); + const nameField = editor.querySelector('input[name="name"]'); + const cancelButton = editor.querySelector('.zmdi-close'); + + fireEvent.change(nameField, { + target: { name: 'name', value: 'Edited Test Contact' }, + }); + expect(container).toMatchSnapshot('07. Edit Test Contact name'); + + fireEvent.click(cancelButton); + expect(container).toMatchSnapshot('08. Cancel Test Contact edit'); + + display = container.querySelector('.contact-display'); + editButton = display.querySelector('.zmdi-edit'); + + fireEvent.click(editButton); + expect(container).toMatchSnapshot( + '09. Switch Test Contact to edit Mode', + ); + + editor = container.querySelector('.contact-editor'); + const emailField = editor.querySelector('input[name="email"]'); + const saveButton = editor.querySelector('.zmdi-save'); + + fireEvent.change(emailField, { + target: { + name: 'email', + value: 'test.contact.edited@gmail.com', + }, + }); + expect(container).toMatchSnapshot('10. Edit Test Contact email'); + + fireEvent.click(saveButton); + expect(container).toMatchSnapshot('11. Save Test Contact edit'); + }); + + test('should delete contact', () => { + let deleteButton = container.querySelectorAll( + '.contact-display .zmdi-delete', + )[1]; + + fireEvent.click(deleteButton); + expect(container).toMatchSnapshot( + '12. Delete Placeholder Contact', + ); + + deleteButton = container.querySelector( + '.contact-display .zmdi-delete', + ); + + fireEvent.click(deleteButton); + expect(container).toMatchSnapshot('13. Delete Test Contact'); + }); +}); diff --git a/__tests__/TodoMVC.test.js b/__tests__/TodoMVC.test.js deleted file mode 100644 index f0db1a6..0000000 --- a/__tests__/TodoMVC.test.js +++ /dev/null @@ -1,94 +0,0 @@ -import React, { StrictMode } from 'react' -import { render, cleanup, fireEvent } from '@testing-library/react/pure' -import App from '../examples/todo-mvc/src/App' - -describe('TodoMVC App', () => { - const { container } = render( - - - - ) - afterAll(cleanup) - - test('should add todos', () => { - expect(container).toMatchSnapshot('01. Initial state') - - const input = container.querySelector('.new-todo') - - fireEvent.keyUp(input, { - keyCode: 13, - target: { value: 'Test Todo' } - }) - expect(container).toMatchSnapshot('02. Add Test Todo') - - fireEvent.keyUp(input, { - keyCode: 13, - target: { value: 'Other Todo' } - }) - expect(container).toMatchSnapshot('03. Add Other Todo') - - fireEvent.keyUp(input, { - keyCode: 27, - target: { value: 'Final Tod' } - }) - fireEvent.keyUp(input, { - keyCode: 13, - target: { value: 'Final Todo' } - }) - expect(container).toMatchSnapshot('04. Add Final Todo') - }) - - test('should toggle todo status', () => { - const toggles = container.querySelectorAll('.todo-list .toggle') - - fireEvent.click(toggles[0]) - expect(container).toMatchSnapshot('05. Toggle Test Todo to completed') - - fireEvent.click(toggles[1]) - expect(container).toMatchSnapshot('06. Toggle Other Todo to completed') - - fireEvent.click(toggles[0]) - expect(container).toMatchSnapshot('07. Toggle Test Todo to active') - }) - - test('should filter todos', () => { - const completedFilter = container.querySelector( - 'button[value="completed"]' - ) - const activeFilter = container.querySelector('button[value="active"]') - const allFilter = container.querySelector('button[value="all"]') - - fireEvent.click(completedFilter) - expect(container).toMatchSnapshot('08. Filter completed') - - fireEvent.click(activeFilter) - expect(container).toMatchSnapshot('09. Filter active') - - fireEvent.click(allFilter) - expect(container).toMatchSnapshot('10. Filter all') - }) - - test('should clear completed', () => { - const clearCompleted = container.querySelector('.clear-completed') - - fireEvent.click(clearCompleted) - expect(container).toMatchSnapshot('11. Clear completed') - }) - - test('should toggle all todo state at once', () => { - const toggleAll = container.querySelector('.toggle-all') - - fireEvent.click(toggleAll) - expect(container).toMatchSnapshot('12. Toggle all to completed') - - fireEvent.click(toggleAll) - expect(container).toMatchSnapshot('13. Toggle all to active') - }) - - test('should delete todo', () => { - const deleter = container.querySelector('.todo-list .destroy') - - fireEvent.click(deleter) - expect(container).toMatchSnapshot('14. Delete Test Todo') - }) -}) diff --git a/__tests__/TodoMVC.test.jsx b/__tests__/TodoMVC.test.jsx new file mode 100644 index 0000000..7c1b836 --- /dev/null +++ b/__tests__/TodoMVC.test.jsx @@ -0,0 +1,108 @@ +import React, { StrictMode } from 'react'; +import { + render, + cleanup, + fireEvent, +} from '@testing-library/react/pure'; +import App from '../examples/todo-mvc/src/App'; + +describe('TodoMVC App', () => { + const { container } = render( + + + , + ); + afterAll(cleanup); + + test('should add todos', () => { + expect(container).toMatchSnapshot('01. Initial state'); + + const input = container.querySelector('.new-todo'); + + fireEvent.keyUp(input, { + keyCode: 13, + target: { value: 'Test Todo' }, + }); + expect(container).toMatchSnapshot('02. Add Test Todo'); + + fireEvent.keyUp(input, { + keyCode: 13, + target: { value: 'Other Todo' }, + }); + expect(container).toMatchSnapshot('03. Add Other Todo'); + + fireEvent.keyUp(input, { + keyCode: 27, + target: { value: 'Final Tod' }, + }); + fireEvent.keyUp(input, { + keyCode: 13, + target: { value: 'Final Todo' }, + }); + expect(container).toMatchSnapshot('04. Add Final Todo'); + }); + + test('should toggle todo status', () => { + const toggles = container.querySelectorAll('.todo-list .toggle'); + + fireEvent.click(toggles[0]); + expect(container).toMatchSnapshot( + '05. Toggle Test Todo to completed', + ); + + fireEvent.click(toggles[1]); + expect(container).toMatchSnapshot( + '06. Toggle Other Todo to completed', + ); + + fireEvent.click(toggles[0]); + expect(container).toMatchSnapshot( + '07. Toggle Test Todo to active', + ); + }); + + test('should filter todos', () => { + const completedFilter = container.querySelector( + 'button[value="completed"]', + ); + const activeFilter = container.querySelector( + 'button[value="active"]', + ); + const allFilter = container.querySelector('button[value="all"]'); + + fireEvent.click(completedFilter); + expect(container).toMatchSnapshot('08. Filter completed'); + + fireEvent.click(activeFilter); + expect(container).toMatchSnapshot('09. Filter active'); + + fireEvent.click(allFilter); + expect(container).toMatchSnapshot('10. Filter all'); + }); + + test('should clear completed', () => { + const clearCompleted = container.querySelector( + '.clear-completed', + ); + + fireEvent.click(clearCompleted); + expect(container).toMatchSnapshot('11. Clear completed'); + }); + + test('should toggle all todo state at once', () => { + const toggleAll = container.querySelector('.toggle-all'); + + fireEvent.click(toggleAll); + expect(container).toMatchSnapshot('12. Toggle all to completed'); + + fireEvent.click(toggleAll); + expect(container).toMatchSnapshot('13. Toggle all to active'); + }); + + test('should delete todo', () => { + const deleter = container.querySelector('.todo-list .destroy'); + + fireEvent.click(deleter); + expect(container).toMatchSnapshot('14. Delete Test Todo'); + }); +}); diff --git a/__tests__/__snapshots__/Contacts.test.js.snap b/__tests__/__snapshots__/Contacts.test.jsx.snap similarity index 100% rename from __tests__/__snapshots__/Contacts.test.js.snap rename to __tests__/__snapshots__/Contacts.test.jsx.snap diff --git a/__tests__/__snapshots__/TodoMVC.test.js.snap b/__tests__/__snapshots__/TodoMVC.test.jsx.snap similarity index 100% rename from __tests__/__snapshots__/TodoMVC.test.js.snap rename to __tests__/__snapshots__/TodoMVC.test.jsx.snap diff --git a/__tests__/batching.test.js b/__tests__/batching.test.jsx similarity index 96% rename from __tests__/batching.test.js rename to __tests__/batching.test.jsx index b77ce86..9ccbc98 100644 --- a/__tests__/batching.test.js +++ b/__tests__/batching.test.jsx @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import { render, cleanup, act } from '@testing-library/react/pure'; +// eslint-disable-next-line import/no-unresolved import { view, store, batch } from 'react-easy-state'; describe('batching', () => { @@ -9,7 +10,7 @@ describe('batching', () => { let renderCount = 0; const person = store({ name: 'Bob' }); const MyComp = view(() => { - renderCount++; + renderCount += 1; return
{person.name}
; }); @@ -32,7 +33,7 @@ describe('batching', () => { const MyComp = view( class extends Component { render() { - renderCount++; + renderCount += 1; return
{person.name}
; } }, @@ -53,7 +54,7 @@ describe('batching', () => { let renderCount = 0; const person = store({ name: 'Bob' }); const MyComp = view(() => { - renderCount++; + renderCount += 1; return
{person.name}
; }); @@ -74,7 +75,7 @@ describe('batching', () => { let renderCount = 0; const person = store({ name: 'Bob' }); const MyComp = view(() => { - renderCount++; + renderCount += 1; return
{person.name}
; }); @@ -100,7 +101,7 @@ describe('batching', () => { let renderCount = 0; const person = store({ name: 'Bob' }); const MyComp = view(() => { - renderCount++; + renderCount += 1; return
{person.name}
; }); @@ -128,7 +129,7 @@ describe('batching', () => { let renderCount = 0; const person = store({ name: 'Bob' }); const MyComp = view(() => { - renderCount++; + renderCount += 1; return
{person.name}
; }); @@ -152,7 +153,7 @@ describe('batching', () => { let renderCount = 0; const person = store({ name: 'Bob' }); const MyComp = view(() => { - renderCount++; + renderCount += 1; return
{person.name}
; }); @@ -179,7 +180,7 @@ describe('batching', () => { let renderCount = 0; const person = store({ name: 'Bob' }); const MyComp = view(() => { - renderCount++; + renderCount += 1; return
{person.name}
; }); @@ -205,7 +206,7 @@ describe('batching', () => { let renderCount = 0; const person = store({ name: 'Bob' }); const MyComp = view(() => { - renderCount++; + renderCount += 1; return
{person.name}
; }); @@ -235,7 +236,7 @@ describe('batching', () => { let renderCount = 0; const person = store({ name: 'Bob' }); const MyComp = view(() => { - renderCount++; + renderCount += 1; return
{person.name}
; }); @@ -281,7 +282,9 @@ describe('batching', () => { test('should not break event listeners', () => { let callCount = 0; - const fn = () => callCount++; + const fn = () => { + callCount += 1; + }; document.body.addEventListener('click', fn); expect(callCount).toBe(0); diff --git a/__tests__/edgeCases.test.js b/__tests__/edgeCases.test.js deleted file mode 100644 index ade77eb..0000000 --- a/__tests__/edgeCases.test.js +++ /dev/null @@ -1,236 +0,0 @@ -import React, { Component, useState } from 'react' -import { render, cleanup, fireEvent, act } from '@testing-library/react/pure' -import { view, store, batch } from 'react-easy-state' - -describe('edge cases', () => { - afterEach(cleanup) - - test('view() should respect shouldComponentUpdate', () => { - const person = store({ name: 'Bob' }) - const MyComp = view( - class extends Component { - shouldComponentUpdate () { - return false - } - - render () { - return
{person.name}
- } - } - ) - - const { container } = render() - expect(container).toHaveTextContent('Bob') - person.name = 'Ann' - expect(container).toHaveTextContent('Bob') - }) - - test('view() should respect componentWillUnmount', () => { - let didUnMount = false - - const MyComp = view( - class extends Component { - componentWillUnmount () { - didUnMount = true - } - - render () { - return
Hello
- } - } - ) - - const { unmount } = render() - expect(didUnMount).toBe(false) - unmount() - expect(didUnMount).toBe(true) - }) - - test('should not change vanilla setState behavior', () => { - const MyComp = view( - class extends Component { - state = { counter: 0 }; - handleIncrement = () => - this.setState({ counter: this.state.counter + 1 }); - - render () { - return
{this.state.counter}
- } - } - ) - - const { container } = render() - expect(container).toHaveTextContent('0') - fireEvent.click(container.querySelector('div')) - expect(container).toHaveTextContent('1') - }) - - test("should not render when state or props don't change", () => { - const MyComp = view( - class extends Component { - state = { counter: 0 }; - handleIncrement = () => - this.setState({ counter: this.state.counter + 1 }); - - render () { - return ( -
- - -
- ) - } - } - ) - - const RawChild = jest.fn().mockReturnValue(

Test

) - const Child = view(RawChild) - - const { container } = render() - expect(RawChild.mock.calls.length).toBe(1) - fireEvent.click(container.querySelector('button')) - expect(RawChild.mock.calls.length).toBe(1) - }) - - test('view() should respect custom deriveStoresFromProps', () => { - const MyComp = view( - class extends Component { - store1 = store({ num: 0 }); - store2 = store({ num: 1 }); - - static deriveStoresFromProps (props, store1, store2) { - store1.num = props.num1 || store1.num - store2.num = props.num2 || store2.num - } - - handleOnClick = () => this.store1.num++; - - render () { - return ( -
- {this.store1.num} - {this.store2.num} -
- ) - } - } - ) - - const { container } = render() - expect(container).toHaveTextContent('11') - fireEvent.click(container.querySelector('div')) - expect(container).toHaveTextContent('11') - }) - - test('view() should respect getDerivedStateFromProps', () => { - const MyComp = view( - class extends Component { - state = { num: 2 }; - static getDerivedStateFromProps (props, state) { - return { - num: props.num || state.num - } - } - - render () { - return
{this.state.num}
- } - } - ) - - const { container } = render() - expect(container).toHaveTextContent('2') - }) - - test('view() should work with other hooks', () => { - const MyComp = view(() => { - const [num, setNum] = useState(0) - return - }) - - const { container } = render() - expect(container).toHaveTextContent('0') - fireEvent.click(container.querySelector('button')) - expect(container).toHaveTextContent('1') - }) - - describe('reactive renders should run in parent - child order with no duplicate child runs from props', () => { - test('should work with function components', () => { - const appStore = store({ num: 1, nested: { num: 12 } }) - - function change () { - appStore.num = 0 - appStore.nested = undefined - } - - let parentCalls = 0 - let childCalls = 0 - - const Child = view(function Child () { - childCalls++ - return ( -
- {appStore.nested.num}, {appStore.num} -
- ) - }) - - const Parent = view(function Parent () { - parentCalls++ - return appStore.nested ? : null - }) - - const { container } = render() - expect(container).toHaveTextContent('12, 1') - expect(parentCalls).toBe(1) - expect(childCalls).toBe(1) - act(() => batch(change)) - expect(container).toHaveTextContent('') - expect(parentCalls).toBe(2) - expect(childCalls).toBe(1) - }) - - test('should work with class components', () => { - const appStore = store({ num: 1, nested: { num: 12 } }) - - function change () { - appStore.num = 0 - appStore.nested = undefined - } - - let parentCalls = 0 - let childCalls = 0 - - const Child = view( - class Child extends Component { - render () { - childCalls++ - return ( -
- {appStore.nested.num}, {appStore.num} -
- ) - } - } - ) - - const Parent = view( - class Parent extends Component { - render () { - parentCalls++ - return appStore.nested ? : null - } - } - ) - - const { container } = render() - expect(container).toHaveTextContent('12, 1') - expect(parentCalls).toBe(1) - expect(childCalls).toBe(1) - act(() => batch(change)) - expect(container).toHaveTextContent('') - expect(parentCalls).toBe(2) - expect(childCalls).toBe(1) - }) - }) -}) diff --git a/__tests__/edgeCases.test.jsx b/__tests__/edgeCases.test.jsx new file mode 100644 index 0000000..2fc3de3 --- /dev/null +++ b/__tests__/edgeCases.test.jsx @@ -0,0 +1,261 @@ +/* eslint-disable react/destructuring-assignment */ +/* eslint-disable react/state-in-constructor */ +import React, { Component, useState } from 'react'; +import { + render, + cleanup, + fireEvent, + act, +} from '@testing-library/react/pure'; +// eslint-disable-next-line import/no-unresolved +import { view, store, batch } from 'react-easy-state'; + +describe('edge cases', () => { + afterEach(cleanup); + + test('view() should respect shouldComponentUpdate', () => { + const person = store({ name: 'Bob' }); + const MyComp = view( + class extends Component { + shouldComponentUpdate() { + return false; + } + + render() { + return
{person.name}
; + } + }, + ); + + const { container } = render(); + expect(container).toHaveTextContent('Bob'); + person.name = 'Ann'; + expect(container).toHaveTextContent('Bob'); + }); + + test('view() should respect componentWillUnmount', () => { + let didUnMount = false; + + const MyComp = view( + class extends Component { + componentWillUnmount() { + didUnMount = true; + } + + render() { + return
Hello
; + } + }, + ); + + const { unmount } = render(); + expect(didUnMount).toBe(false); + unmount(); + expect(didUnMount).toBe(true); + }); + + test('should not change vanilla setState behavior', () => { + const MyComp = view( + class extends Component { + state = { counter: 0 }; + + handleIncrement = () => { + this.setState(prevState => ({ + counter: prevState.counter + 1, + })); + }; + + render() { + return ( + + ); + } + }, + ); + + const { container } = render(); + expect(container).toHaveTextContent('0'); + fireEvent.click(container.querySelector('button')); + expect(container).toHaveTextContent('1'); + }); + + test("should not render when state or props don't change", () => { + const MyComp = view( + class extends Component { + state = { counter: 0 }; + + handleIncrement = () => { + this.setState(prevState => ({ + counter: prevState.counter + 1, + })); + }; + + render() { + return ( +
+ + +
+ ); + } + }, + ); + + const RawChild = jest.fn().mockReturnValue(

Test

); + const Child = view(RawChild); + + const { container } = render(); + expect(RawChild.mock.calls.length).toBe(1); + fireEvent.click(container.querySelector('button')); + expect(RawChild.mock.calls.length).toBe(1); + }); + + test('view() should respect custom deriveStoresFromProps', () => { + const MyComp = view( + class extends Component { + // eslint-disable-next-line react/sort-comp + store1 = store({ num: 0 }); + + store2 = store({ num: 1 }); + + static deriveStoresFromProps(props, store1, store2) { + store1.num = props.num1 || store1.num; + store2.num = props.num2 || store2.num; + } + + handleOnClick = () => { + this.store1.num += 1; + }; + + render() { + return ( + + ); + } + }, + ); + + const { container } = render(); + expect(container).toHaveTextContent('11'); + fireEvent.click(container.querySelector('button')); + expect(container).toHaveTextContent('11'); + }); + + test('view() should respect getDerivedStateFromProps', () => { + const MyComp = view( + class extends Component { + state = { num: 2 }; + + static getDerivedStateFromProps(props, state) { + return { + num: props.num || state.num, + }; + } + + render() { + return
{this.state.num}
; + } + }, + ); + + const { container } = render(); + expect(container).toHaveTextContent('2'); + }); + + test('view() should work with other hooks', () => { + const MyComp = view(() => { + const [num, setNum] = useState(0); + return ( + + ); + }); + + const { container } = render(); + expect(container).toHaveTextContent('0'); + fireEvent.click(container.querySelector('button')); + expect(container).toHaveTextContent('1'); + }); + + describe('reactive renders should run in parent - child order with no duplicate child runs from props', () => { + test('should work with function components', () => { + const appStore = store({ num: 1, nested: { num: 12 } }); + + function change() { + appStore.num = 0; + appStore.nested = undefined; + } + + let parentCalls = 0; + let childCalls = 0; + + const Child = view(function Child() { + childCalls += 1; + return
{`${appStore.nested.num}, ${appStore.num}`}
; + }); + + const Parent = view(function Parent() { + parentCalls += 1; + return appStore.nested ? : null; + }); + + const { container } = render(); + expect(container).toHaveTextContent('12, 1'); + expect(parentCalls).toBe(1); + expect(childCalls).toBe(1); + act(() => batch(change)); + expect(container).toHaveTextContent(''); + expect(parentCalls).toBe(2); + expect(childCalls).toBe(1); + }); + + test('should work with class components', () => { + const appStore = store({ num: 1, nested: { num: 12 } }); + + function change() { + appStore.num = 0; + appStore.nested = undefined; + } + + let parentCalls = 0; + let childCalls = 0; + + const Child = view( + class Child extends Component { + render() { + childCalls += 1; + return ( +
{`${appStore.nested.num}, ${appStore.num}`}
+ ); + } + }, + ); + + const Parent = view( + class Parent extends Component { + render() { + parentCalls += 1; + return appStore.nested ? : null; + } + }, + ); + + const { container } = render(); + expect(container).toHaveTextContent('12, 1'); + expect(parentCalls).toBe(1); + expect(childCalls).toBe(1); + act(() => batch(change)); + expect(container).toHaveTextContent(''); + expect(parentCalls).toBe(2); + expect(childCalls).toBe(1); + }); + }); +}); diff --git a/__tests__/router.test.js b/__tests__/router.test.js deleted file mode 100644 index 6d39948..0000000 --- a/__tests__/router.test.js +++ /dev/null @@ -1,108 +0,0 @@ -import React, { Component } from 'react' -import { render, cleanup, fireEvent, act } from '@testing-library/react/pure' -import { view, store } from 'react-easy-state' -import { - BrowserRouter as Router, - Route, - Link, - withRouter -} from 'react-router-dom' - -describe('withRouter interaction', () => { - afterEach(() => { - cleanup() - window.history.replaceState({}, '', '/') - }) - - describe('function components', () => { - test('should be reactive with withRouter(view(Comp))', () => { - const counter = store({ num: 0 }) - const MyComp = withRouter(view(() =>
{counter.num}
)) - - const { container } = render( - - - - ) - expect(container).toHaveTextContent('0') - act(() => { - counter.num++ - }) - expect(container).toHaveTextContent('1') - }) - - test('should properly route with withRouter(view(Comp))', () => { - const MyComp = withRouter( - view(() => ( -
- To Settings -

Settings

} /> -
- )) - ) - - const { container, getByText } = render( - - - - ) - - expect(container.querySelector('p')).toBe(null) - fireEvent.click(getByText('To Settings')) - expect(container.querySelector('p')).toHaveTextContent('Settings') - }) - }) - - describe('class components', () => { - test('should be reactive with withRouter(view(Comp))', () => { - const counter = store({ num: 0 }) - const MyComp = withRouter( - view( - class MyComp extends Component { - render () { - return
{counter.num}
- } - } - ) - ) - - const { container } = render( - - - - ) - expect(container).toHaveTextContent('0') - act(() => { - counter.num++ - }) - expect(container).toHaveTextContent('1') - }) - - test('should properly route with withRouter(view(Comp))', () => { - const MyComp = withRouter( - view( - class MyComp extends Component { - render () { - return ( -
- To Settings -

Settings

} /> -
- ) - } - } - ) - ) - - const { container, getByText } = render( - - - - ) - - expect(container.querySelector('p')).toBe(null) - fireEvent.click(getByText('To Settings')) - expect(container.querySelector('p')).toHaveTextContent('Settings') - }) - }) -}) diff --git a/__tests__/router.test.jsx b/__tests__/router.test.jsx new file mode 100644 index 0000000..d00edf5 --- /dev/null +++ b/__tests__/router.test.jsx @@ -0,0 +1,121 @@ +import React, { Component } from 'react'; +import { + render, + cleanup, + fireEvent, + act, +} from '@testing-library/react/pure'; +// eslint-disable-next-line import/no-unresolved +import { view, store } from 'react-easy-state'; +import { + BrowserRouter as Router, + Route, + Link, + withRouter, +} from 'react-router-dom'; + +describe('withRouter interaction', () => { + afterEach(() => { + cleanup(); + window.history.replaceState({}, '', '/'); + }); + + describe('function components', () => { + test('should be reactive with withRouter(view(Comp))', () => { + const counter = store({ num: 0 }); + const MyComp = withRouter(view(() =>
{counter.num}
)); + + const { container } = render( + + + , + ); + expect(container).toHaveTextContent('0'); + act(() => { + counter.num += 1; + }); + expect(container).toHaveTextContent('1'); + }); + + test('should properly route with withRouter(view(Comp))', () => { + const MyComp = withRouter( + view(() => ( +
+ To Settings +

Settings

} /> +
+ )), + ); + + const { container, getByText } = render( + + + , + ); + + expect(container.querySelector('p')).toBe(null); + fireEvent.click(getByText('To Settings')); + expect(container.querySelector('p')).toHaveTextContent( + 'Settings', + ); + }); + }); + + describe('class components', () => { + test('should be reactive with withRouter(view(Comp))', () => { + const counter = store({ num: 0 }); + const MyComp = withRouter( + view( + class MyComp extends Component { + render() { + return
{counter.num}
; + } + }, + ), + ); + + const { container } = render( + + + , + ); + expect(container).toHaveTextContent('0'); + act(() => { + counter.num += 1; + }); + expect(container).toHaveTextContent('1'); + }); + + test('should properly route with withRouter(view(Comp))', () => { + const MyComp = withRouter( + view( + class MyComp extends Component { + render() { + return ( +
+ To Settings +

Settings

} + /> +
+ ); + } + }, + ), + ); + + const { container, getByText } = render( + + + , + ); + + expect(container.querySelector('p')).toBe(null); + fireEvent.click(getByText('To Settings')); + expect(container.querySelector('p')).toHaveTextContent( + 'Settings', + ); + }); + }); +}); diff --git a/__tests__/staticProps.test.js b/__tests__/staticProps.test.js index 3eb35b6..ddd9151 100644 --- a/__tests__/staticProps.test.js +++ b/__tests__/staticProps.test.js @@ -1,59 +1,63 @@ -import { Component } from 'react' -import { view } from 'react-easy-state' +/* eslint-disable react/forbid-foreign-prop-types */ +/* eslint-disable no-multi-assign */ +import { Component } from 'react'; +// eslint-disable-next-line import/no-unresolved +import { view } from 'react-easy-state'; describe('static props', () => { test('view() should proxy static properties from wrapped components', () => { class Comp extends Component {} - function FuncComp () {} - - Comp.displayName = FuncComp.displayName = 'Name' - Comp.contextTypes = FuncComp.contextTypes = {} - Comp.propTypes = FuncComp.propTypes = {} - Comp.defaultProps = FuncComp.defaultProps = {} - Comp.customProp = FuncComp.customProp = {} - - const ViewComp = view(Comp) - const ViewFuncComp = view(FuncComp) - - expect(ViewComp.displayName).toBe(Comp.displayName) - expect(ViewComp.contextTypes).toBe(Comp.contextTypes) - expect(ViewComp.propTypes).toBe(Comp.propTypes) - expect(ViewComp.defaultProps).toBe(Comp.defaultProps) - expect(ViewComp.customProp).toBe(Comp.customProp) - - expect(ViewFuncComp.displayName).toBe(FuncComp.displayName) - expect(ViewFuncComp.contextTypes).toBe(FuncComp.contextTypes) - expect(ViewFuncComp.propTypes).toBe(FuncComp.propTypes) - expect(ViewFuncComp.defaultProps).toBe(FuncComp.defaultProps) - expect(ViewFuncComp.customProp).toBe(FuncComp.customProp) - }) + function FuncComp() {} + + Comp.displayName = FuncComp.displayName = 'Name'; + Comp.contextTypes = FuncComp.contextTypes = {}; + Comp.propTypes = FuncComp.propTypes = {}; + Comp.defaultProps = FuncComp.defaultProps = {}; + Comp.customProp = FuncComp.customProp = {}; + + const ViewComp = view(Comp); + const ViewFuncComp = view(FuncComp); + + expect(ViewComp.displayName).toBe(Comp.displayName); + expect(ViewComp.contextTypes).toBe(Comp.contextTypes); + expect(ViewComp.propTypes).toBe(Comp.propTypes); + expect(ViewComp.defaultProps).toBe(Comp.defaultProps); + expect(ViewComp.customProp).toBe(Comp.customProp); + + expect(ViewFuncComp.displayName).toBe(FuncComp.displayName); + expect(ViewFuncComp.contextTypes).toBe(FuncComp.contextTypes); + expect(ViewFuncComp.propTypes).toBe(FuncComp.propTypes); + expect(ViewFuncComp.defaultProps).toBe(FuncComp.defaultProps); + expect(ViewFuncComp.customProp).toBe(FuncComp.customProp); + }); test('view() should proxy static methods', () => { class Comp extends Component { - static getDerivedStateFromError () {} - static customMethod () {} + static getDerivedStateFromError() {} + + static customMethod() {} } - const ViewComp = view(Comp) + const ViewComp = view(Comp); expect(ViewComp.getDerivedStateFromError).toBe( - Comp.getDerivedStateFromError - ) - expect(ViewComp.customMethod).toBe(Comp.customMethod) - }) + Comp.getDerivedStateFromError, + ); + expect(ViewComp.customMethod).toBe(Comp.customMethod); + }); test('view() should proxy static getters', () => { class Comp extends Component { - static get defaultProp () { - return { key: 'value' } + static get defaultProp() { + return { key: 'value' }; } - static get customProp () { - return { key: 'hello' } + static get customProp() { + return { key: 'hello' }; } } - const ViewComp = view(Comp) - expect(ViewComp.defaultProps).toEqual(Comp.defaultProps) - expect(ViewComp.customProp).toEqual(Comp.customProp) - }) -}) + const ViewComp = view(Comp); + expect(ViewComp.defaultProps).toEqual(Comp.defaultProps); + expect(ViewComp.customProp).toEqual(Comp.customProp); + }); +}); diff --git a/__tests__/styled.test.js b/__tests__/styled.test.jsx similarity index 57% rename from __tests__/styled.test.js rename to __tests__/styled.test.jsx index 4c3e218..5503cd0 100644 --- a/__tests__/styled.test.js +++ b/__tests__/styled.test.jsx @@ -1,91 +1,94 @@ -import React, { Component } from 'react' -import { render, cleanup, act } from '@testing-library/react/pure' -import { view, store } from 'react-easy-state' -import { withTheme, ThemeProvider } from 'styled-components' +import React, { Component } from 'react'; +import { render, cleanup, act } from '@testing-library/react/pure'; +// eslint-disable-next-line import/no-unresolved +import { view, store } from 'react-easy-state'; +import { withTheme, ThemeProvider } from 'styled-components'; describe('withRouter interaction', () => { - const theme = { color: 'red' } + const theme = { color: 'red' }; const Theme = ({ children }) => ( {children} - ) + ); afterEach(() => { - cleanup() - }) + cleanup(); + }); describe('function components', () => { test('should be reactive with withTheme(view(Comp))', () => { - const counter = store({ num: 0 }) - const MyComp = withTheme(view(() =>
{counter.num}
)) + const counter = store({ num: 0 }); + const MyComp = withTheme(view(() =>
{counter.num}
)); const { container } = render( - - ) - expect(container).toHaveTextContent('0') + , + ); + expect(container).toHaveTextContent('0'); act(() => { - counter.num++ - }) - expect(container).toHaveTextContent('1') - }) + counter.num += 1; + }); + expect(container).toHaveTextContent('1'); + }); test('should properly inject theme with withTheme(view(Comp))', () => { const MyComp = withTheme( - view(({ theme }) =>

Hello

) - ) + view(({ theme: hocTheme }) => ( +

Hello

+ )), + ); const { container } = render( - - ) - expect(container.querySelector('p')).toHaveStyle('color: red;') - }) - }) + , + ); + expect(container.querySelector('p')).toHaveStyle('color: red;'); + }); + }); describe('class components', () => { test('should be reactive with withTheme(view(Comp))', () => { - const counter = store({ num: 0 }) + const counter = store({ num: 0 }); const MyComp = withTheme( view( class MyComp extends Component { - render () { - return
{counter.num}
+ render() { + return
{counter.num}
; } - } - ) - ) + }, + ), + ); const { container } = render( - - ) - expect(container).toHaveTextContent('0') + , + ); + expect(container).toHaveTextContent('0'); act(() => { - counter.num++ - }) - expect(container).toHaveTextContent('1') - }) + counter.num += 1; + }); + expect(container).toHaveTextContent('1'); + }); test('should properly route with withRouter(view(Comp))', () => { const MyComp = withTheme( view( class MyComp extends Component { - render () { - return

Hello

+ render() { + return

Hello

; } - } - ) - ) + }, + ), + ); const { container } = render( - - ) - expect(container.querySelector('p')).toHaveStyle('color: red;') - }) - }) -}) + , + ); + expect(container.querySelector('p')).toHaveStyle('color: red;'); + }); + }); +}); diff --git a/jest.native.json b/jest.native.json index 01961dc..e4870dd 100644 --- a/jest.native.json +++ b/jest.native.json @@ -1,6 +1,6 @@ { "preset": "react-native", - "testRegex": "\\.test\\.native\\.js$", + "testRegex": "\\.test\\.native\\.jsx?$", "transform": { "^.+\\.(js|jsx)$": "react-native/jest/preprocessor.js" }, diff --git a/jest.web.json b/jest.web.json index 136b086..1e361f7 100644 --- a/jest.web.json +++ b/jest.web.json @@ -1,7 +1,7 @@ { "setupFilesAfterEnv": ["./scripts/testSetup.js"], "testURL": "http://react-easy-state.com", - "testRegex": "\\.test\\.js$", + "testRegex": "\\.test\\.jsx?$", "collectCoverage": true, "coverageReporters": ["lcovonly", "text"], "collectCoverageFrom": [ From 24ba71fbd91589f42655890a113866fb8d2cabb4 Mon Sep 17 00:00:00 2001 From: Roland Szoke Date: Fri, 21 Feb 2020 13:28:35 +0100 Subject: [PATCH 2/2] ci(lint): lint tests before commits --- __mocks__/react-native.js | 2 +- package.json | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/__mocks__/react-native.js b/__mocks__/react-native.js index 56f6104..1230fb3 100644 --- a/__mocks__/react-native.js +++ b/__mocks__/react-native.js @@ -1,3 +1,3 @@ // this is here to avoid duplicate react entries in the examples // (one from the example's node_modules and one from the root's node_modules) -module.exports = require('react-native') +module.exports = require('react-native'); diff --git a/package.json b/package.json index 06b181c..5526710 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,8 @@ "test-builds": "node ./scripts/testBuilds.js", "lint": "eslint --max-warnings 0 --ext js,jsx src scripts", "lint-fix": "eslint --max-warnings 0 --ext js,jsx src scripts --fix", + "lint-tests": "eslint --max-warnings 0 --ext js,jsx __tests__ __mocks__", + "lint-tests-fix": "eslint --max-warnings 0 --ext js,jsx __tests__ __mocks__ --fix", "install-examples": "node ./scripts/installExamples.js", "build-examples": "node ./scripts/buildExamples.js", "link-examples": "node ./scripts/linkExamples.js", @@ -105,7 +107,7 @@ }, "husky": { "hooks": { - "pre-commit": "npm run lint", + "pre-commit": "npm run lint && npm run lint-tests", "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" } },