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

Menus and React Testing Library migration #359

Merged
merged 38 commits into from
Mar 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
11b4751
Migrate @testing-library/user-event from dependencies to devDependencies
nelsonni Mar 1, 2021
718cebf
Card.saveable migrated to Metafile.state
nelsonni Mar 1, 2021
0c1e2eb
Initial Editor-only saving logic
nelsonni Mar 1, 2021
15fee9a
Modal subsumes Error for types, actions, and reducers
nelsonni Mar 4, 2021
c47595d
Card backs stylized grey instead of white to match theme
nelsonni Mar 4, 2021
b3f7dfc
Electron ApplicationMenu configuration added
nelsonni Mar 4, 2021
dfaca05
ModalComponent added to capture dialogs and errors
nelsonni Mar 4, 2021
9e02e2c
Consolidate JSDoc status table for git.getStatus
nelsonni Mar 4, 2021
371c714
CanvasMenu upgraded to use NavMenu menu bar instead of buttons
nelsonni Mar 4, 2021
dbb3513
FilePickerDialog converted to redux-thunk function for calling Electr…
nelsonni Mar 4, 2021
8c2a77f
NewCardDialog button logic subsumed by dialog component
nelsonni Mar 4, 2021
7ed903b
DiffPickerDialog button logic subsumed by dialog component
nelsonni Mar 4, 2021
721a0a5
RepoBranchList button logic subsumed by status component
nelsonni Mar 4, 2021
d016ac9
Tests updated to use modals instead of errors
nelsonni Mar 4, 2021
6af4a8d
MergeDialog button logic subsumed by dialog component
nelsonni Mar 4, 2021
65ae53c
Cards default to below menu bar
nelsonni Mar 4, 2021
076d22e
Add missing @testing-library/dom dependency for React Testing Library…
nelsonni Mar 4, 2021
9341dad
Partial refactoring of DiffPickerDialog tests to RTL
nelsonni Mar 4, 2021
de2e2a8
Deprecate Enzyme and switch to Testing Library with Jest only
nelsonni Mar 5, 2021
1edbfe2
DiffPickerDialog testing migrated to RTL
nelsonni Mar 5, 2021
c0a9ea2
Set AceEditor 'basePath' and 'modePath' for dynamic importing in Jest…
nelsonni Mar 6, 2021
868334d
Bump react-dnd-test-utils from 11.1.3 to 12.0.0 (add @types/node peer…
nelsonni Mar 6, 2021
c0b86b3
ContainsRequiredMetafile renamed to MetafileWithContains for clarity
nelsonni Mar 6, 2021
32b02dc
Typo fixed in Metafile.filetype JSDoc comments
nelsonni Mar 6, 2021
eef1bb0
Test fixtures created to reduce test suite boilerplate code
nelsonni Mar 6, 2021
a9edfd7
Deprecated Enzyme testing and replaced with React Testing Library
nelsonni Mar 6, 2021
b440fe6
useDirectory.fetch migrated to useEffect and cleanup fn to prevent me…
nelsonni Mar 6, 2021
85c8b39
Remove working test file for migrating to RTL
nelsonni Mar 6, 2021
7cd23b2
Remove Enzyme dependencies
nelsonni Mar 6, 2021
4af3a5c
Update Browser and Explorer tests for dndReduxMock deprecation
nelsonni Mar 6, 2021
e6d1661
Updated documentation to reflect Enzyme removal and RTL promotion
nelsonni Mar 6, 2021
e14d072
Remove unused and incomplete CanvasMenu components
nelsonni Mar 7, 2021
07a420f
GitGraph repo selection integrated into NavMenu
nelsonni Mar 8, 2021
440e794
Merge branch 'development' into feat/saving-changes
nelsonni Mar 9, 2021
8d3288d
Simplified useDirectory hook and async tests setup
nelsonni Mar 12, 2021
a07cd1e
isHidden shim function added to use winattr instead of hidefile on Wi…
nelsonni Mar 9, 2021
cc9f610
removing winattr dependency
nelsonni Mar 12, 2021
0171d76
isHidden fn on Windows only examines for paths beginning with '.'
nelsonni Mar 12, 2021
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
55 changes: 18 additions & 37 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ Synectic has the following ESLint options set in `.eslintrc.js`:

[Jest](https://jestjs.io/) is a JavaScript testing framework with a focus on simplicity. It is maintained by Facebook, and supports [Babel](https://babeljs.io/), [TypeScript](#TypeScript), [Node.js](https://nodejs.org/en/about/), [React](#React), [Angular](https://angular.io/), and [Vue.js](https://vuejs.org/). Jest is built on top of Jasmine, and serves as a test runner with predefined tests for mocking and stub React components.

Synectic uses Jest for unit testing, integration testing, code coverage, and interfacing with [Enzyme](#Enzyme) for React component testing.
Synectic uses Jest for unit testing, integration testing, code coverage, and interfacing with [React Testing Library](#React-Testing-Library-(RTL)) for React component testing.

The [`ts-jest`](https://kulshekhar.github.io/ts-jest/) module is a TypeScript preprocessor with source map support for Jest that lets Synectic use Jest to test projects written in TypeScript. In particular, the choice to use TypeScript (with `ts-jest`) instead of Babel7 (with `@babel/preset-typescript`) is based upon the reasons outlined in a blog post from Kulshekhar Kabra, ["Babel7 or TypeScript"](https://kulshekhar.github.io/ts-jest/user/babel7-or-ts) (published 2018.09.16).

Expand All @@ -212,11 +212,26 @@ The [`react-test-renderer`](https://reactjs.org/docs/test-renderer.html) module
* `react-test-renderer`
* `ts-jest`


**Configuration:**

Synectic has the following [Jest](#Jest) options set in `jest.config.js`:

| Setting | Value | Description |
| ------------------------------------------ |:---------------------------:| ----------------------------------------------------:|
| `setupFilesAfterEnv` | `['<rootDir>/__test__/setupTests.ts']` | A list of paths to modules that configure or setup the testing framework before each test (i.e. the actions defined in `setupTests.ts` executes after environment setup) |
| `preset` | `ts-jest` | All TypeScript files (`.ts` and `.tsx`) will be handled by `ts-jest`; JavaScript files are not processed |
| `roots` | `['<rootDir>/__test__']` | Jest will only search for test files in the `__test__` directory |
| `snapshotSerializers` | `['jest-serializer-path']` | Enables the `jest-serializer-path` for removing absolute paths and normalizing paths across all platforms in Jest snapshots |
| `moduleNameMapper` | `{"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": '<rootDir>/__mocks__/fileMock.js'}` | Use a mocked CSS proxy for CSS Modules via [`identity-obj-proxy`](https://jestjs.io/docs/en/webpack#mocking-css-modules) during testing |
| `moduleNameMapper` | `{"\\.(css|less)$": 'identity-obj-proxy'}` | Mocks all static assets (e.g. stylesheets and images) during testing |
| `moduleNameMapper` | `{"^dnd-cores$": "dnd-core/dist/cjs", "^react-dnd$": "react-dnd/dist/cjs", "^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs", "^react-dnd-touch-backend$": "react-dnd-touch-backend/dist/cjs", "^react-dnd-test-backend$": "react-dnd-test-backend/dist/cjs", "^react-dnd-test-utils$": "react-dnd-test-utils/dist/cjs"}` | Jest does not work well with ES Modules yet, but can use CommonJS builds for `react-dnd` libraries (per [React DnD testing docs](https://react-dnd.github.io/react-dnd/docs/testing)) |

# React Testing Library (RTL)

[Testing Library](https://testing-library.com/) is a family of testing utility libraries that adhere to the guiding principle that _"the more your tests resemble the way your software is used, the more confidence they can give you."_ The manifestation of this principle is that tests are composed by querying for nodes in similar fashion to how users would find them (which makes this methodology ideal for UI testing). The [`React Testing Library` (RTL)](https://testing-library.com/docs/react-testing-library/intro) builds on top of the [`DOM Testing Library`](https://testing-library.com/docs/dom-testing-library/intro) by adding APIs for working with React components.

Synectic uses React Testing Library to render React components within tests written using Jest's custom assertions and convenience functions in order to verify UI interactions. The configuration of RTL was inspired by a detailed blog post from Robert Cooper, ["Testing Stateful React Function Components with React Testing Library"](https://www.robertcooper.me/testing-stateful-react-function-components-with-react-testing-library) (published 2019.04.08). In particular, the blog posting provides examples of testing React function components using [Enzyme](#Enzume) and [React Testing Library](#React-Testing-Library-(RTL)), and finds that there are less chances of test suites that produce false negatives (tests that fail when the underlying implementation changes) and false positives (tests that continue to pass when the underlying implementation is broken) when adhering what Kent C. Dodds calls [_"implementation detail free testing"_](https://kentcdodds.com/blog/testing-implementation-details).
Synectic uses React Testing Library to render React components within tests written using Jest's custom assertions and convenience functions in order to verify UI interactions. The configuration of RTL was inspired by a detailed blog post from Robert Cooper, ["Testing Stateful React Function Components with React Testing Library"](https://www.robertcooper.me/testing-stateful-react-function-components-with-react-testing-library) (published 2019.04.08). In particular, the blog posting provides examples of testing React function components using [React Testing Library](#React-Testing-Library-(RTL)), and finds that there are less chances of test suites that produce false negatives (tests that fail when the underlying implementation changes) and false positives (tests that continue to pass when the underlying implementation is broken) when adhering what Kent C. Dodds calls [_"implementation detail free testing"_](https://kentcdodds.com/blog/testing-implementation-details).

The [`@testing-library/jest-dom`](https://testing-library.com/docs/ecosystem-jest-dom) module provides custom DOM element matchers for Jest.

Expand All @@ -226,42 +241,8 @@ The [`@testing-library/react-hooks`](https://github.com/testing-library/react-ho

**Packages:**
* *`devDependencies`*
* `@testing-library/dom`
* `@testing-library/jest-dom`
* `@testing-library/react`
* `@testing-library/react-hooks`
* `react-select-event`

# Enzyme

[Enzyme](https://airbnb.io/enzyme/) is a JavaScript testing utility for React that tests components with assertions that simulate UI interactions. Enzyme is developed by AirBnB and wraps packages like [ReactTestUtils](https://reactjs.org/docs/test-utils.html), [JSDOM](https://github.com/jsdom/jsdom), and [CheerIO](https://cheerio.js.org/) to create a simpler interface for writing unit tests. The API is meant to be intuitive and flexible by mimicking the jQuery API for DOM manipulation and traversal.

Synectic uses Enzyme to model and render React components and hooks within tests written using Jest's custom assertions and convenience functions.

The [`jest-environment-enzyme`](https://github.com/FormidableLabs/enzyme-matchers/tree/master/packages/jest-environment-enzyme) module from [FormidableLabs](https://formidable.com/) provides a simplified declarative setup for configuring Enzyme with Jest and React. This package also simplifies test files by declaring React, and enzyme wrappers in the global scope. This means that all test files do not need to include imports for React or enzyme.

The `enzyme-to-json` module converts enzyme wrappers to a format compatible with Jest snapshot testing, by providing a serializer plugin to [Jest](#Jest).

**Packages:**
* *`devDependencies`*
* `@types/enzyme`
* `@types/enzyme-adapter-react-16`
* `enzyme`
* `enzyme-adapter-react-16`
* `enzyme-to-json`
* `jest-environment-enzyme`
* `jest-enzyme`

**Configuration:**

Synectic has the following [Jest](#Jest) and [Enzyme](#Enzyme) options set in `jest.config.js`:
| Setting | Value | Description |
| ------------------------------------------ |:---------------------------:| ----------------------------------------------------:|
| `testEnvironment` | `enzyme` | Specifies the test environment that will be used for Jest testing |
| `setupFilesAfterEnv` | `['jest-enzyme']` | A list of paths to modules that configure or setup the testing framework before each test (i.e. the `jest-enzyme` plugin executes after environment setup) |
| `testEnvironmentOptions` : `enzymeAdapter` | `react16` | Sets `enzyme-adapter-react-16` as the default Enzyme adapter |
| `preset` | `ts-jest` | All TypeScript files (`.ts` and `.tsx`) will be handled by `ts-jest`; JavaScript files are not processed |
| `roots` | `['<rootDir>/__test__']` | Jest will only search for test files in the `__test__` directory |
| `snapshotSerializers` | `['enzyme-to-json/serializer']` | Enables the `enzyme-to-json` for serializing all Jest snapshots |
| `moduleNameMapper` | `{"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": '<rootDir>/__mocks__/fileMock.js'}` | Use a mocked CSS proxy for CSS Modules via [`identity-obj-proxy`](https://jestjs.io/docs/en/webpack#mocking-css-modules) during testing |
| `moduleNameMapper` | `{"\\.(css|less)$": 'identity-obj-proxy'}` | Mocks all static assets (e.g. stylesheets and images) during testing |
| `moduleNameMapper` | `{"^dnd-cores$": "dnd-core/dist/cjs", "^react-dnd$": "react-dnd/dist/cjs", "^react-dnd-html5-backend$": "react-dnd-html5-backend/dist/cjs", "^react-dnd-touch-backend$": "react-dnd-touch-backend/dist/cjs", "^react-dnd-test-backend$": "react-dnd-test-backend/dist/cjs", "^react-dnd-test-utils$": "react-dnd-test-uttils/dist/cjs"}` | Jest does not work well with ES Modules yet, but can use CommonJS builds for `react-dnd` libraries (per [React DnD testing docs](https://react-dnd.github.io/react-dnd/docs/testing)) |
40 changes: 9 additions & 31 deletions __test__/Browser.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,14 @@
import '@testing-library/jest-dom';
import { DateTime } from 'luxon';
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { v4 } from 'uuid';
import userEvent from '@testing-library/user-event';

import Browser from '../src/components/Browser';
import { mockStore } from './__mocks__/reduxStoreMock';
import { wrapInReduxContext } from './__mocks__/dndReduxMock';

describe('Browser', () => {
const store = mockStore({
canvas: {
id: v4(),
created: DateTime.fromISO('1991-12-26T08:00:00.000-08:00'),
repos: [],
cards: [],
stacks: []
},
stacks: {},
cards: {},
filetypes: {},
metafiles: {},
repos: {},
errors: {}
});

const BrowserContext = wrapInReduxContext(Browser, store);

it('Browser allows the user to enter/edit a URL', async () => {
render(<BrowserContext />);
const textBox = screen.getByRole('textbox') as HTMLInputElement;
const { getByRole } = render(<Browser />);
const textBox = getByRole('textbox');

expect(textBox).toHaveValue('https://epiclab.github.io/');

Expand All @@ -45,10 +23,10 @@ describe('Browser', () => {
});

it('Browser allows the user to navigate backwards and forwards in history', async () => {
render(<BrowserContext />);
const backButton = screen.getAllByRole('button')[0];
const forwardButton = screen.getAllByRole('button')[1];
const textBox = screen.getByRole('textbox') as HTMLInputElement;
const { getByRole, getAllByRole } = render(<Browser />);
const textBox = getByRole('textbox');
const backButton = getAllByRole('button')[0];
const forwardButton = getAllByRole('button')[1];

expect(textBox).toHaveValue('https://epiclab.github.io/');
textBox.focus();
Expand All @@ -71,9 +49,9 @@ describe('Browser', () => {
});

it('Browser does not change the page URL when the refresh button is clicked', async () => {
render(<BrowserContext />);
const textBox = screen.getByRole('textbox') as HTMLInputElement;
const refreshButton = screen.getAllByRole('button')[2];
const { getByRole, getAllByRole } = render(<Browser />);
const textBox = getByRole('textbox');
const refreshButton = getAllByRole('button')[2];

expect(textBox).toHaveValue('https://epiclab.github.io/');
fireEvent.click(refreshButton);
Expand Down
115 changes: 72 additions & 43 deletions __test__/CanvasComponent.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,88 @@
import React from 'react';
import isUUID from 'validator/lib/isUUID';
import { mount } from 'enzyme';
import { v4 } from 'uuid';
import { DateTime } from 'luxon';
import mock from 'mock-fs';
import { Provider } from 'react-redux';
import { cleanup, render, act } from '@testing-library/react';
import { wrapWithTestBackend } from 'react-dnd-test-utils';
import * as path from 'path';
import { homedir } from 'os';

import { wrapInReduxContext } from './__mocks__/dndReduxMock';
import { mockStore } from './__mocks__/reduxStoreMock';
import CanvasComponent from '../src/components/CanvasComponent';
import CardComponent from '../src/components/CardComponent';
import StackComponent from '../src/components/StackComponent';
import { mockStore, extractFieldArray } from './__mocks__/reduxStoreMock';
import { testStore } from './__fixtures__/ReduxStore';
import { fullCanvas } from './__fixtures__/Canvas';
import { flattenArray } from '../src/containers/flatten';

const store = mockStore(testStore);

describe('CanvasComponent', () => {

const domElement = document.getElementById('app');
const mountOptions = { attachTo: domElement, };
const store = mockStore({
canvas: {
id: v4(),
created: DateTime.fromISO('1991-12-26T08:00:00.000-08:00'),
repos: [],
cards: [],
stacks: []
},
stacks: {},
cards: {},
filetypes: {},
metafiles: {},
repos: {},
errors: {}
// mocks for git config are required; ReduxStore fixture contains MergeDialog components which check for config values
beforeEach(() => {
mock({
'foo': {},
[path.join(homedir(), '.gitconfig')]: mock.file({
content: `[user]
name = Sandy Updates
email = supdate@oregonstate.edu
[core]
editor = vim
whitespace = fix,-indent-with-non-tab,trailing-space,cr-at-eol`,
}),
'.git/config': mock.file({
content: `[user]
name = Bobby Tables
email = bdrop@oregonstate.edu
[credential]
helper = osxkeychain
[pull]
rebase = true
[alias]
last = log -1 HEAD`,
}),
}, { createCwd: false });
});

afterEach(store.clearActions);
afterEach(() => {
cleanup;
jest.clearAllMocks();
mock.restore();
});

it('Canvas has a valid UUID when props contain valid UUID', () => {
const CanvasContext = wrapInReduxContext(CanvasComponent, store);
const canvasProps = store.getState().canvas;
const wrapper = mount(<CanvasContext {...canvasProps} />, mountOptions);
const component = wrapper.find(CanvasComponent).first();
expect(isUUID(component.props().id, 4)).toBe(true);
it('Canvas renders correctly', async () => {
await act(async () => {
const [WrappedComponent] = wrapWithTestBackend(CanvasComponent);
const { getByTestId } = render(
<Provider store={store}>
<WrappedComponent {...fullCanvas} />
</Provider>
);
expect(getByTestId('canvas-component')).toBeInTheDocument();
})
});

it('Canvas resolves props into React Components for cards', () => {
const CanvasContext = wrapInReduxContext(CanvasComponent, store);
const canvasProps = store.getState().canvas;
const wrapper = mount(<CanvasContext {...canvasProps} />, mountOptions);
const component = wrapper.find(CanvasComponent).first();
expect(wrapper.find(CardComponent)).toHaveLength(component.props().cards.length);
it('Canvas resolves props to render Cards', async () => {
await act(async () => {
const [WrappedComponent] = wrapWithTestBackend(CanvasComponent);
const { getAllByTestId } = render(
<Provider store={store}>
<WrappedComponent {...fullCanvas} />
</Provider>
);
const cardsInStacks = flattenArray(extractFieldArray(store.getState().stacks).map(s => s.cards));
expect(getAllByTestId('card-component')).toHaveLength(fullCanvas.cards.length + cardsInStacks.length);
});
});

it('Canvas resolves props into React Components for stacks', () => {
const CanvasContext = wrapInReduxContext(CanvasComponent, store);
const canvasProps = store.getState().canvas;
const wrapper = mount(<CanvasContext {...canvasProps} />, mountOptions);
const component = wrapper.find(CanvasComponent).first();
expect(wrapper.find(StackComponent)).toHaveLength(component.props().stacks.length);
it('Canvas resolves props to render Stacks', async () => {
await act(async () => {
const [WrappedComponent] = wrapWithTestBackend(CanvasComponent);
const { getAllByTestId } = render(
<Provider store={store}>
<WrappedComponent {...fullCanvas} />
</Provider>
);
expect(getAllByTestId('stack-component')).toHaveLength(fullCanvas.stacks.length);
});
});

});
Loading