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

chore: add jsdom tests for mobile/touch screen #57

Merged
merged 1 commit into from
Mar 17, 2022
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
4 changes: 3 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@
"testing-library/no-wait-for-side-effects": "error",
"testing-library/prefer-query-by-disappearance": "error",
"testing-library/prefer-user-event": "error",
"testing-library/no-debugging-utils": "warn",
"react/require-default-props": [
"error",
{
Expand Down Expand Up @@ -123,7 +124,8 @@
{
"checksVoidReturn": false
}
]
],
"jest/no-focused-tests": "warn"
},
"settings": {
"react": {
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# temporary test file
/src/generated/output.css
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@
"build": "react-scripts build",
"predeploy": "npm run build",
"deploy": "gh-pages -d build",
"pretest": "npx tailwindcss -i ./src/index.css -o ./src/generated/output.css",
"test": "react-scripts test",
"test:related": "npm run test -- --bail --watchAll=false --findRelatedTests src/**/*.{ts,tsx} --passWithNoTests",
"posttest": "rm ./src/generated/output.css",
"eject": "react-scripts eject",
"lint": "eslint src/**/*.{ts,tsx} --quiet",
"lint:fix": "eslint --fix --ext src/**/*.{ts,tsx}",
Expand Down
333 changes: 308 additions & 25 deletions src/App/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,322 @@
import { render, screen, within } from '@testing-library/react';
import { fireEvent, render, screen, waitFor, waitForElementToBeRemoved, within } from '@testing-library/react';
import { QueryClientProvider } from 'react-query';
import { BrowserRouter } from 'react-router-dom';
import userEvent from '@testing-library/user-event';
import { App } from './App';
import { queryClient } from '../queryClient';
import { matchMedia } from '../setupTests';

beforeEach(() => {
const mockIntersectionObserver = jest.fn();
mockIntersectionObserver.mockReturnValue({
observe: () => null,
unobserve: () => null,
disconnect: () => null,
});
const TestApp = (): JSX.Element => {
// mock touch screen (as we can only test mobile behavior)
matchMedia.useMediaQuery('(pointer: coarse)');

window.IntersectionObserver = mockIntersectionObserver;
});
return (
<BrowserRouter basename={process.env.PUBLIC_URL}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</BrowserRouter>
);
};

const simulateTapEvent = (element: Element): void => {
fireEvent.touchStart(element, { touches: [{ clientX: 0, clientY: 0 }] });
fireEvent.touchEnd(element, { touches: [{ clientX: 0, clientY: 0 }] });
};

describe('Todo App', () => {
test('renders todo app link', async () => {
render(
<BrowserRouter basename={process.env.PUBLIC_URL}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</BrowserRouter>,
);
beforeEach(() => {
matchMedia.clear();
});

/* NOTE: it is not possible to properly test tailwind responsive ui behavior
with Jest(jsdom). Generally jsdom neither loads the application css files,
nor does it support media queries. We can address the former by manually
assembling and injecting the tailwind css styles (see setupTests.ts),
however, the latter is a bigger and [currently] unsolvable problem. Mocking
media query support (window.matchMedia) is possible either manually or via
packages like jest-matchmedia-mock but this only patches scenarios where the
component under test actually calls window.matchMedia(...) programmatically.
In our [tailwind] scenario we are dynamically injecting the css (including the
@media statements for sm/md/lg/etc. screen modifiers) but jsdom does not
trigger media query computation hence the screen modifiers remain inactive. As
tailwind is a mobile-first library this effectively means we are stuck with the
mobile view for testing). */
describe('on mobile (touch-enabled) screen', () => {
test('renders without crashing', async () => {
render(<TestApp />);

const linkElement = screen.getByText(/todo app/i);
expect(linkElement).toBeVisible();

const searchFormElement = screen.getByRole('search');
expect(searchFormElement).toBeVisible();

const listElement = await screen.findByTestId('todo-list');
expect(listElement).toBeVisible();

const listScope = within(listElement);
const itemElements = await listScope.findAllByRole('listitem');
expect(itemElements.length).toEqual(10);

const fabElement = screen.getByTestId('cta-button');
expect(fabElement).toBeVisible();
expect(fabElement).toHaveClass('fixed');

const topNavElement = screen.getByTestId('top-navigation');
expect(topNavElement).not.toBeVisible();

const bottomNavElement = screen.getByTestId('bottom-navigation');
expect(bottomNavElement).toBeVisible();
});

test('renders no checkboxes for todo items', async () => {
render(<TestApp />);

const listElement = await screen.findByTestId('todo-list');
const listScope = within(listElement);
const itemElements = await listScope.findAllByRole('listitem');
expect(itemElements.length).toEqual(10);

const checkboxElement = listScope.queryByRole('checkbox');
expect(checkboxElement).not.toBeInTheDocument();
});

test('renders correct bottom navigation items', async () => {
render(<TestApp />);

const listElement = await screen.findByTestId('todo-list');
expect(listElement).toBeVisible();

const bottomNavElement = screen.getByTestId('bottom-navigation');
const navScope = within(bottomNavElement);
const linkElements = navScope.getAllByRole('link');
expect(linkElements.length).toEqual(3);

const tasksElement = linkElements[0];
expect(tasksElement).toBeVisible();
expect(tasksElement).toHaveTextContent(/tasks/);
expect(tasksElement).toHaveClass('bg-primary-variant');

const analyticsElement = linkElements[1];
expect(analyticsElement).toBeVisible();
expect(analyticsElement).toHaveTextContent(/analytics/);
expect(analyticsElement).not.toHaveClass('bg-primary-variant');

const settingsElement = linkElements[2];
expect(settingsElement).toBeVisible();
expect(settingsElement).toHaveTextContent(/settings/);
expect(settingsElement).not.toHaveClass('bg-primary-variant');
});

test('switches tabs through bottom navigation', async () => {
render(<TestApp />);

let listElement = await screen.findByTestId('todo-list');
expect(listElement).toBeVisible();

const bottomNavElement = screen.getByTestId('bottom-navigation');
const navScope = within(bottomNavElement);

const analyticsLink = navScope.getByText(/analytics/i);
expect(analyticsLink).toBeVisible();

analyticsLink.click();
const analyticsTabElement = await screen.findByTestId('analytics');
expect(analyticsTabElement).toBeVisible();
expect(listElement).not.toBeVisible();

const settingsLink = navScope.getByText(/settings/i);
expect(settingsLink).toBeVisible();

settingsLink.click();
const settingsTabElement = await screen.findByTestId('settings');
expect(settingsTabElement).toBeVisible();
expect(analyticsTabElement).not.toBeVisible();
expect(listElement).not.toBeVisible();

const tasksLink = navScope.getByText(/tasks/i);
expect(tasksLink).toBeVisible();

tasksLink.click();
listElement = await screen.findByTestId('todo-list');
expect(listElement).toBeVisible();
expect(analyticsTabElement).not.toBeVisible();
expect(settingsTabElement).not.toBeVisible();
});

test('search for todo item', async () => {
render(<TestApp />);

const listElement = await screen.findByTestId('todo-list');
expect(listElement).toBeVisible();
const listScope = within(listElement);

let itemElements = await listScope.findAllByRole('listitem');
expect(itemElements.length).toEqual(10);

const searchElement = screen.getByRole('searchbox');
expect(searchElement).toBeVisible();

userEvent.type(searchElement, 'vivacious');
itemElements = await listScope.findAllByRole('listitem');
expect(itemElements.length).toEqual(1);

userEvent.clear(searchElement);
userEvent.type(searchElement, 'asdf');
itemElements = listScope.queryAllByRole('listitem');
expect(itemElements.length).toEqual(0);

const resetElement = screen.getByText(/reset search/i);
expect(resetElement).toBeVisible();

resetElement.click();
expect(resetElement).not.toBeInTheDocument();

expect(listElement).toBeVisible();
itemElements = await listScope.findAllByRole('listitem');
expect(itemElements.length).toEqual(10);
expect(searchElement).toHaveValue('');
});

test('create todo item', async () => {
render(<TestApp />);

const listElement = await screen.findByTestId('todo-list');
expect(listElement).toBeVisible();
const listScope = within(listElement);

let itemElements = await listScope.findAllByRole('listitem');
expect(itemElements.length).toEqual(10);

const fabElement = screen.getByTestId('cta-button');
expect(fabElement).toBeVisible();

userEvent.click(fabElement);

const modalTestId = 'add-new-modal';
const modalElement = screen.getByTestId(modalTestId);
expect(modalElement).toBeVisible();

const modalScope = within(modalElement);
const inputElements = modalScope.getAllByRole('textbox');
expect(inputElements.length).toEqual(2);

const noteElement = inputElements[1];
const testValue = 'Add the qwerty!';

userEvent.type(noteElement, testValue);

const saveButton = modalScope.getByText(/save/i);
expect(saveButton).toBeVisible();

userEvent.click(saveButton);
await waitForElementToBeRemoved(() => screen.queryByTestId(modalTestId));

await waitFor(() => expect(listScope.queryByText(testValue)).toBeVisible());
itemElements = listScope.getAllByRole('listitem');
expect(itemElements[0]).toHaveTextContent(testValue);
});

test('create todo item validation', async () => {
render(<TestApp />);

const listElement = await screen.findByTestId('todo-list');
expect(listElement).toBeVisible();
const listScope = within(listElement);

let itemElements = await listScope.findAllByRole('listitem');
expect(itemElements.length).toEqual(10);

const fabElement = screen.getByTestId('cta-button');
expect(fabElement).toBeVisible();

userEvent.click(fabElement);

const modalTestId = 'add-new-modal';
const modalElement = screen.getByTestId(modalTestId);
expect(modalElement).toBeVisible();

const modalScope = within(modalElement);
const saveButton = modalScope.getByText(/save/i);
expect(saveButton).toBeVisible();

userEvent.click(saveButton);

const errorElements = await modalScope.findAllByText(/at least one of the fields is required/i);

expect(errorElements.length).toEqual(2);
errorElements.forEach((errorElement) => expect(errorElement).toBeVisible());

const inputElements = modalScope.getAllByRole('textbox');
expect(inputElements.length).toEqual(2);

const titleElement = inputElements[0];
const testValue = 'Add the deadbeef!';

userEvent.type(titleElement, testValue);

userEvent.click(saveButton);

await waitForElementToBeRemoved(() => screen.queryByTestId(modalTestId));

await waitFor(() => expect(listScope.queryByText(testValue)).toBeVisible());
itemElements = listScope.getAllByRole('listitem');
expect(itemElements[0]).toHaveTextContent(testValue);
});

test('cancel create todo item should have no side effects', async () => {
render(<TestApp />);

const listElement = await screen.findByTestId('todo-list');
expect(listElement).toBeVisible();
const listScope = within(listElement);

let itemElements = await listScope.findAllByRole('listitem');
expect(itemElements.length).toEqual(10);
const firstElement = itemElements[0];

const fabElement = screen.getByTestId('cta-button');
expect(fabElement).toBeVisible();

userEvent.click(fabElement);

const modalTestId = 'add-new-modal';
const modalElement = screen.getByTestId(modalTestId);
expect(modalElement).toBeVisible();

const modalScope = within(modalElement);
const closeButton = modalScope.getByText(/close/i);
expect(closeButton).toBeVisible();

userEvent.click(closeButton);

expect(screen.queryByTestId(modalTestId)).not.toBeInTheDocument();

itemElements = listScope.getAllByRole('listitem');
expect(itemElements[0]).toEqual(firstElement);
});

test('update todo item', async () => {
render(<TestApp />);

const listElement = await screen.findByTestId('todo-list');
expect(listElement).toBeVisible();
const listScope = within(listElement);

const itemElements = await listScope.findAllByRole('listitem');
expect(itemElements.length).toEqual(10);

const linkElement = screen.getByText(/todo app/i);
expect(linkElement).toBeInTheDocument();
const labelMatcher = /write the damn resolver!/i;
const labelElement = screen.getByText(labelMatcher);
expect(labelElement).not.toHaveClass('line-through');

const listElement = await screen.findByTestId('todo-list');
expect(listElement).toBeInTheDocument();
simulateTapEvent(labelElement);

const listScope = within(listElement);
const itemElements = await listScope.findAllByRole('listitem');
await waitFor(() => expect(screen.getByText(labelMatcher)).toHaveClass('line-through'));
});

expect(itemElements.length).toEqual(10);
// TODO: add keyboard support to test in jsdom
test.todo('delete todo item');
});
});
Loading