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(gost): update vitest version and add testing-library for accessible queries #3794

Merged
merged 4 commits into from
Dec 16, 2024
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
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 [email protected] 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 [email protected] 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
Loading