Skip to content

Commit

Permalink
chore(gost): update vitest version and add testing-library for access…
Browse files Browse the repository at this point in the history
…ible queries (#3794)

* chore(gost): update vitest version and add testing-library for accessible queries

* chore(arpa_reporter): add decision report for testing library

---------

Co-authored-by: Tyler Hendrickson <[email protected]>
  • Loading branch information
lsr-explore and TylerHendrickson authored Dec 16, 2024
1 parent 698c351 commit 994ac9a
Show file tree
Hide file tree
Showing 5 changed files with 883 additions and 280 deletions.
81 changes: 81 additions & 0 deletions docs/decisions/0011-use-testing-library-vue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
https://github.com/usdigitalresponse/usdr-gost/pull/3794# 0011. Use testing-library/vue

Date: 2024-12-11
Status: Proposed <!-- Proposed | Accepted | Rejected | Superceded -->

## Context and Problem Statement

Accessibility is a key aspect to development and testing-library supports testing from the user perspective using accessible selectors. testing-library is also a widely used testing library that can make achieving test case coverage easier.

## Decision Drivers <!-- optional -->

- Ability to write tests from the user perspective keeping accesibility in mind
- Improve developer experience through better testing experience. Tests written using testing-library are more readable and easier to maintain.
<!-- numbers of drivers can vary -->

## Considered Options

1. Do not make any changes to the testing libraries
1. Use vitest browser testing locators
1. Use testing-library/vue
<!-- numbers of options can vary -->

## Decision Outcome

TBD

### Positive Consequences <!-- optional -->

- Tests better reflect the user experience
- Tests check for accessible names
- Provides a better developer experience

### Negative Consequences <!-- optional -->

- May require some ramp up time for devs who are not familiar with testing-library approach to testing.

## Pros and Cons of the Options <!-- optional -->

### Do not make any changes to the current test libraries and version

- Good, because no changes to current tooling
- Bad, because it is harder to test in a way that catches accessible names and achieve test case coverage <!-- numbers of pros and cons can vary -->

### Use vitest browser testing locators

[Vitest Browser Mode - locators documentation](https://main.vitest.dev/guide/browser/locators)

- Good, because it continues to rely on vitest which may be familiar to vue developers.
- Bad, because it is only available in browser testing mode and requires playwright to be installed.
- Bad, because it required an separate script from the current test script.
- Bad, because it generated separate coverage report which would need to be reconciled with the current test report to calculate total code coverage.
- Bad, because the browser mode is currently experimental.

### [Use testing-library/vue]

[Testing Library Vue documentation](https://testing-library.com/docs/vue-testing-library/intro/)

- Good, because it is widely used and well supported as part of the testing-library family.
- Good, tests are written in a way that is more readable and easier to maintain
- Good, because it is doesn't require any additional tooling
- Good, because developers with react experience can use the same testing library as they are familiar with.
- Bad, because it requires additional npm packages - testing-library/vue, @testing-library/jest-dom, @testing-library/user-event

## Code Examples

### Option 1 - No changes - Code Example <!-- optional -->

n/a

### Option 2 - vitest browser mode - Code Example <!-- optional -->

const sendTreasuryReportButton = wrapper.get('#sendTreasuryReportButton');
expect(sendTreasuryReportButton.text()).toContain('Send Treasury Report by Email');

### Option 3 - testing-library/vue - Code Example <!-- optional -->

screen.getByRole('button', { name: /Send Treasury Report by Email/i });

## Links <!-- optional -->

[Link to PR #3794 - chore(gost): update vitest version and add testing-library for accessible queries](https://github.com/usdigitalresponse/usdr-gost/pull/3794)
7 changes: 5 additions & 2 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@
"vuex": "^4.0.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/vue": "^8.1.0",
"@testing-library/user-event": "14.5.2",
"@typescript-eslint/eslint-plugin": "^5.26.0",
"@typescript-eslint/parser": "^5.26.0",
"@vitest/coverage-istanbul": "^1.6.0",
"@vitest/coverage-istanbul": "^2.1.8",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/test-utils": "^2.4.6",
"eslint": "^8.2.0",
Expand All @@ -61,6 +64,6 @@
"prettier": "2.6.1",
"sass": "1.52.1",
"typescript": "^4.7.2",
"vitest": "^1.6.0"
"vitest": "^2.1.8"
}
}
240 changes: 239 additions & 1 deletion packages/client/src/arpa_reporter/views/HomeView.spec.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import {
describe, beforeEach, afterEach, it, expect,
describe, beforeEach, afterEach, it, expect, vi,
} from 'vitest';
import { shallowMount } from '@vue/test-utils';
import { createStore } from 'vuex';
import HomeView from '@/arpa_reporter/views/HomeView.vue';
import {
fireEvent, render, screen, cleanup,
} from '@testing-library/vue';
import userEvent from '@testing-library/user-event';

import * as handlers from '@/arpa_reporter/store';

let store;
let wrapper;
Expand All @@ -13,6 +19,8 @@ afterEach(() => {
store = undefined;
wrapper = undefined;
route = { query: { } };
cleanup();
vi.clearAllMocks();
});

describe('HomeView.vue', () => {
Expand Down Expand Up @@ -176,3 +184,233 @@ describe('HomeView.vue', () => {
});
});
});
describe('HomeView.vue - react testing tests', () => {
describe('when a non-admin user views the home page - react testing tests', () => {
it('displays welcome message and closed reporting period', () => {
const testStore = createStore({
getters: {
user: () => ({ role: { name: 'not an admin' } }),
},
});

render(HomeView, {
global: {
plugins: [testStore],
},
});

expect(screen.getByText(/Welcome to the ARPA reporter/i)).toBeInTheDocument();
expect(screen.getByText(/This reporting period is closed/i)).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Send Treasury Report/i })).not.toBeInTheDocument();
});
});
describe('admin user - during reporting period', () => {
const testStore = createStore({
getters: {
user: () => ({ role: { name: 'admin' } }),
viewPeriodIsCurrent: () => true,
viewPeriod: () => ({ id: 124 }),
},
});
describe('basic view checks', () => {
it('should show expected buttons and welcome message', () => {
render(HomeView, {
global: {
plugins: [testStore],
mocks: { $route: { query: {} } },
},
});
screen.getByRole('button', { name: /Submit Workbook/i });
screen.getByRole('link', { name: /Download Empty Template/i });
screen.getByRole('button', { name: /Send Treasury Report by Email/i });
screen.getByRole('button', { name: /Send Audit Report by Email/i });
screen.getByText(/Welcome to the ARPA reporter. To get started, click the "Download Empty Template" button, above, to get a copy of an empty template for reporting./i);
screen.getByText(/You will need to fill out one template for every EC code that your agency uses. Once you've filled out a template, please return here to submit it. To do that, click the "Submit Workbook" button, above. You can only submit workbooks for the currently-open reporting period./i);
screen.getByText(/To view a list of all submitted workbooks, please click on the "Uploads" tab./i);
});
it('should push /new_upload route when Submit Workbook is clicked', async () => {
const mockRouter = {
push: vi.fn(),
};

render(HomeView, {
global: {
plugins: [testStore],
mocks: {
$route: { query: {} },
$router: mockRouter,
},
},
});
const submitWorkbook = screen.getByRole('button', { name: /Submit Workbook/i });
await fireEvent.click(submitWorkbook);
expect(mockRouter.push).toHaveBeenCalledWith({ path: '/new_upload' });
});
});

describe('click Send Treasury Report By Email', async () => {
it('click Send Treasury Report By Email - displays expected alert when error is returned', async () => {
vi.spyOn(handlers, 'getJson').mockReturnValue(
new Promise((resolve) => {
resolve({
error: 'Unable to generate treasury report and send email.',
});
}),
);

render(HomeView, {
global: {
plugins: [testStore],
mocks: { $route: { query: {} } },
},
});

const sendTreasuryButton = screen.getByRole('button', { name: /Send Treasury Report by Email/i });
await fireEvent.click(sendTreasuryButton);
screen.getByRole('alert');
screen.getByText('Something went wrong. Unable to send an email containing the treasury report. Reach out to [email protected] if this happens again.');
screen.getByRole('button', { name: /Close/i });
});
it('displays expected success message', async () => {
vi.spyOn(handlers, 'getJson').mockReturnValue(
new Promise((resolve) => {
resolve({
success: true,
});
}),
);

render(HomeView, {
global: {
plugins: [testStore],
mocks: { $route: { query: {} } },
},
});

const sendTreasuryButton = screen.getByRole('button', { name: /Send Treasury Report by Email/i });
await fireEvent.click(sendTreasuryButton);
screen.getByRole('alert');
screen.getByText(/Sent. Please note, it could take up to 1 hour for this email to arrive./i);
screen.getByRole('button', { name: /Close/i });
});
it('displays error message when Treasury Report fails to send', async () => {
vi.spyOn(handlers, 'getJson').mockRejectedValue(
new Promise((reject) => {
reject(new Error('Failed to send report'));
}),
);
const mockRouter = {
push: vi.fn(),
};

const storeWithError = createStore({
getters: {
user: () => ({ role: { name: 'admin' } }),
viewPeriodIsCurrent: () => true,
viewPeriod: () => ({}),
},
});

render(HomeView, {
global: {
plugins: [storeWithError],
mocks: {
$route: { path: '/' },
$router: mockRouter,
},
},
});

const treasuryButton = screen.getByRole('button', { name: /Send Treasury Report by Email/i });
await fireEvent.click(treasuryButton);

screen.getByText(/Something went wrong. Unable to send an email containing the treasury report. Reach out to grants-helpdesk@usdigitalresponse.org if this happens again./i);
});
});
describe('click Send Audit Report By Email', async () => {
it('displays expected alert when error is returned', async () => {
vi.spyOn(handlers, 'getJson').mockReturnValue(
new Promise((resolve) => {
resolve({
error: 'Unable to generate audit report and send email.',
});
}),
);

render(HomeView, {
global: {
plugins: [testStore],
mocks: { $route: { query: {} } },
},
});

const sendAuditButton = screen.getByRole('button', { name: /Send Audit Report by Email/i });
await fireEvent.click(sendAuditButton);
screen.getByRole('alert');
screen.getByText('Something went wrong. Unable to send an email containing the audit report. Reach out to [email protected] if this happens again.');
await fireEvent.click(screen.getByRole('button', { name: /Close/i }));
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
it('click Send Audit Report By Email - displays expected success message', async () => {
const user = userEvent.setup();

vi.spyOn(handlers, 'getJson').mockReturnValue(
new Promise((resolve) => {
resolve({
success: true,
});
}),
);

render(HomeView, {
global: {
plugins: [testStore],
mocks: { $route: { query: {} } },
},
});

const sendAuditButton = screen.getByRole('button', { name: /Send Audit Report by Email/i });
await fireEvent.click(sendAuditButton);
screen.getByRole('alert');
screen.getByText(/Sent. Please note, it could take up to 1 hour for this email to arrive./i);
screen.getByRole('button', { name: /Close/i });
user.keyboard('[tab]');
await user.keyboard('[Enter]');
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
it('displays error message when Audit Report fails to send', async () => {
vi.spyOn(handlers, 'getJson').mockRejectedValue(
new Promise((reject) => {
reject(new Error('Failed to send report'));
}),
);
const mockRouter = {
push: vi.fn(),
};

const storeWithError = createStore({
getters: {
user: () => ({ role: { name: 'admin' } }),
viewPeriodIsCurrent: () => true,
viewPeriod: () => ({}),
},
});

render(HomeView, {
global: {
plugins: [storeWithError],
mocks: {
$route: { path: '/' },
$router: mockRouter,
},
},
});

const auditButton = screen.getByRole('button', { name: /Send Audit Report by Email/i });
await fireEvent.click(auditButton);

screen.getByText(/Something went wrong. Unable to send an email containing the audit report. Reach out to grants-helpdesk@usdigitalresponse.org if this happens again./i);
});
});
});
});
4 changes: 4 additions & 0 deletions packages/client/vitest.setup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { config } from '@vue/test-utils';
import * as matchers from '@testing-library/jest-dom/matchers';
import { expect } from 'vitest';

expect.extend(matchers);

const stubs = [
'v-select',
Expand Down
Loading

0 comments on commit 994ac9a

Please sign in to comment.