Skip to content

Commit f2f0689

Browse files
authored
chore: add all missing tests for backstage-plugin-coder (#101)
* fix: make sure auth form has accessible name * refactor: update how mock workspace data is defined * chore: finish first search test * chore: finish test for querying * wip: commit progress on last test * fix: finalize tests * refactor: rename variable for clarity * chore: finish all network-based tests * docs: add comment for clarity * chore: add one extra test case for failing to find results * refactor: consolidate regex logic * refactor: make test logic a little more clear
1 parent 343c866 commit f2f0689

File tree

7 files changed

+347
-93
lines changed

7 files changed

+347
-93
lines changed

plugins/backstage-plugin-coder/src/components/CoderAuthWrapper/CoderAuthInputForm.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,21 @@ export const CoderAuthInputForm = () => {
9595
registerNewToken(newToken);
9696
};
9797

98+
const formHeaderId = `${hookId}-form-header`;
9899
const legendId = `${hookId}-legend`;
99100
const authTokenInputId = `${hookId}-auth-token`;
100101
const warningBannerId = `${hookId}-warning-banner`;
101102

102103
return (
103-
<form className={styles.formContainer} onSubmit={onSubmit}>
104+
<form
105+
aria-labelledby={formHeaderId}
106+
className={styles.formContainer}
107+
onSubmit={onSubmit}
108+
>
109+
<h3 hidden id={formHeaderId}>
110+
Authenticate with Coder
111+
</h3>
112+
104113
<div>
105114
<CoderLogo className={styles.coderLogo} />
106115
<p>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/**
2+
* @file Defines integration tests for all sub-components in the
3+
* CoderWorkspacesCard directory.
4+
*/
5+
import React from 'react';
6+
import { screen, waitFor } from '@testing-library/react';
7+
import { renderInCoderEnvironment } from '../../testHelpers/setup';
8+
import { mockAuthStates } from '../../testHelpers/mockBackstageData';
9+
import {
10+
mockWorkspaceNoParameters,
11+
mockWorkspaceWithMatch2,
12+
mockWorkspacesList,
13+
} from '../../testHelpers/mockCoderAppData';
14+
import { type CoderAuthStatus } from '../CoderProvider';
15+
import { CoderWorkspacesCard } from './CoderWorkspacesCard';
16+
import userEvent from '@testing-library/user-event';
17+
18+
type RenderInputs = Readonly<{
19+
authStatus?: CoderAuthStatus;
20+
readEntityData?: boolean;
21+
}>;
22+
23+
function renderWorkspacesCard(input?: RenderInputs) {
24+
const { authStatus = 'authenticated', readEntityData = false } = input ?? {};
25+
26+
return renderInCoderEnvironment({
27+
auth: mockAuthStates[authStatus],
28+
children: <CoderWorkspacesCard readEntityData={readEntityData} />,
29+
});
30+
}
31+
32+
const matchers = {
33+
authenticationForm: /Authenticate with Coder/i,
34+
searchTitle: /Coder Workspaces/i,
35+
searchbox: /Search your Coder workspaces/i,
36+
emptyState: /Use the search bar to find matching Coder workspaces/i,
37+
} as const satisfies Record<string, RegExp>;
38+
39+
describe(`${CoderWorkspacesCard.name}`, () => {
40+
describe('General behavior', () => {
41+
it('Shows the authentication form when the user is not authenticated', async () => {
42+
await renderWorkspacesCard({
43+
authStatus: 'tokenMissing',
44+
});
45+
46+
expect(() => {
47+
screen.getByRole('form', { name: matchers.authenticationForm });
48+
}).not.toThrow();
49+
});
50+
51+
it('Shows the workspaces list when the user is authenticated (exposed as an accessible search landmark)', async () => {
52+
await renderWorkspacesCard();
53+
54+
await waitFor(() => {
55+
expect(() => {
56+
screen.getByRole('search', { name: matchers.searchTitle });
57+
}).not.toThrow();
58+
});
59+
});
60+
61+
it('Shows zero workspaces when the query text matches nothing', async () => {
62+
const entityValues = [true, false] as const;
63+
const user = userEvent.setup();
64+
65+
for (const value of entityValues) {
66+
const { unmount } = await renderWorkspacesCard({
67+
readEntityData: value,
68+
});
69+
70+
const searchbox = await screen.findByRole('searchbox', {
71+
name: matchers.searchbox,
72+
});
73+
74+
await user.tripleClick(searchbox);
75+
await user.keyboard('[Backspace]');
76+
await user.keyboard('I can do it - I can do it nine times');
77+
78+
await waitFor(() => {
79+
// getAllByRole will throw if there isn't at least one node matched
80+
const listItems = screen.queryAllByRole('listitem');
81+
expect(listItems.length).toBe(0);
82+
});
83+
84+
unmount();
85+
}
86+
});
87+
});
88+
89+
describe('With readEntityData set to false', () => {
90+
it('Will NOT filter any workspaces by the current repo', async () => {
91+
await renderWorkspacesCard({ readEntityData: false });
92+
const workspaceItems = await screen.findAllByRole('listitem');
93+
expect(workspaceItems.length).toEqual(mockWorkspacesList.length);
94+
});
95+
96+
it('Lets the user filter the workspaces by their query text', async () => {
97+
await renderWorkspacesCard({ readEntityData: false });
98+
const searchbox = await screen.findByRole('searchbox', {
99+
name: matchers.searchbox,
100+
});
101+
102+
const user = userEvent.setup();
103+
await user.tripleClick(searchbox);
104+
await user.keyboard(mockWorkspaceNoParameters.name);
105+
106+
// If more than one workspace matches, that throws an error
107+
const onlyWorkspace = await screen.findByRole('listitem');
108+
expect(onlyWorkspace).toHaveTextContent(mockWorkspaceNoParameters.name);
109+
});
110+
111+
it('Shows all workspaces when query text is empty', async () => {
112+
await renderWorkspacesCard({ readEntityData: false });
113+
const searchbox = await screen.findByRole('searchbox', {
114+
name: matchers.searchbox,
115+
});
116+
117+
const user = userEvent.setup();
118+
await user.tripleClick(searchbox);
119+
await user.keyboard('[Backspace]');
120+
121+
const allWorkspaces = await screen.findAllByRole('listitem');
122+
expect(allWorkspaces.length).toEqual(mockWorkspacesList.length);
123+
});
124+
});
125+
126+
describe('With readEntityData set to true', () => {
127+
it('Will show only the workspaces that match the current repo', async () => {
128+
await renderWorkspacesCard({ readEntityData: true });
129+
const workspaceItems = await screen.findAllByRole('listitem');
130+
expect(workspaceItems.length).toEqual(2);
131+
});
132+
133+
it('Lets the user filter the workspaces by their query text (on top of filtering from readEntityData)', async () => {
134+
await renderWorkspacesCard({ readEntityData: true });
135+
136+
await waitFor(() => {
137+
const workspaceItems = screen.getAllByRole('listitem');
138+
expect(workspaceItems.length).toBe(2);
139+
});
140+
141+
const user = userEvent.setup();
142+
const searchbox = await screen.findByRole('searchbox', {
143+
name: matchers.searchbox,
144+
});
145+
146+
await user.tripleClick(searchbox);
147+
await user.keyboard(mockWorkspaceWithMatch2.name);
148+
149+
await waitFor(() => {
150+
const newWorkspaceItems = screen.getAllByRole('listitem');
151+
expect(newWorkspaceItems.length).toBe(1);
152+
});
153+
});
154+
155+
/**
156+
* 2024-03-28 - MES - This is a test case to account for a previous
157+
* limitation around querying workspaces by repo URL.
158+
*
159+
* This limitation no longer exists, so this test should be removed once the
160+
* rest of the codebase is updated to support the new API endpoint for
161+
* searching by build parameter
162+
*/
163+
it('Will not show any workspaces at all when the query text is empty', async () => {
164+
await renderWorkspacesCard({ readEntityData: true });
165+
166+
const user = userEvent.setup();
167+
const searchbox = await screen.findByRole('searchbox', {
168+
name: matchers.searchbox,
169+
});
170+
171+
await user.tripleClick(searchbox);
172+
await user.keyboard('[Backspace]');
173+
174+
const emptyState = await screen.findByText(matchers.emptyState);
175+
expect(emptyState).toBeInTheDocument();
176+
});
177+
});
178+
});

plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesList.test.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { type WorkspacesListProps, WorkspacesList } from './WorkspacesList';
33
import { renderInCoderEnvironment } from '../../testHelpers/setup';
44
import { CardContext, WorkspacesCardContext, WorkspacesQuery } from './Root';
55
import { mockCoderWorkspacesConfig } from '../../testHelpers/mockBackstageData';
6-
import { mockWorkspace } from '../../testHelpers/mockCoderAppData';
6+
import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderAppData';
77
import { Workspace } from '../../typesConstants';
88
import { screen } from '@testing-library/react';
99

@@ -42,9 +42,9 @@ describe(`${WorkspacesList.name}`, () => {
4242
await renderWorkspacesList({
4343
workspacesQuery: {
4444
data: workspaceNames.map<Workspace>((name, index) => ({
45-
...mockWorkspace,
45+
...mockWorkspaceWithMatch,
4646
name,
47-
id: `${mockWorkspace.id}-${index}`,
47+
id: `${mockWorkspaceWithMatch.id}-${index}`,
4848
})),
4949
},
5050

plugins/backstage-plugin-coder/src/components/CoderWorkspacesCard/WorkspacesListItem.test.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { screen } from '@testing-library/react';
33
import { renderInCoderEnvironment } from '../../testHelpers/setup';
4-
import { mockWorkspace } from '../../testHelpers/mockCoderAppData';
4+
import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderAppData';
55
import type { Workspace } from '../../typesConstants';
66
import { WorkspacesListItem } from './WorkspacesListItem';
77

@@ -13,9 +13,9 @@ async function renderListItem(inputs?: RenderInput) {
1313
const { isOnline = true } = inputs ?? {};
1414

1515
const workspace: Workspace = {
16-
...mockWorkspace,
16+
...mockWorkspaceWithMatch,
1717
latest_build: {
18-
...mockWorkspace.latest_build,
18+
...mockWorkspaceWithMatch.latest_build,
1919
status: isOnline ? 'running' : 'stopped',
2020
resources: [
2121
{

plugins/backstage-plugin-coder/src/hooks/useCoderWorkspacesQuery.test.ts

+22-13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { useCoderWorkspacesQuery } from './useCoderWorkspacesQuery';
33

44
import { renderHookAsCoderEntity } from '../testHelpers/setup';
55
import { mockCoderWorkspacesConfig } from '../testHelpers/mockBackstageData';
6+
import {
7+
mockWorkspaceNoParameters,
8+
mockWorkspacesList,
9+
} from '../testHelpers/mockCoderAppData';
610

711
beforeAll(() => {
812
jest.useFakeTimers();
@@ -38,12 +42,22 @@ describe(`${useCoderWorkspacesQuery.name}`, () => {
3842
await jest.advanceTimersByTimeAsync(10_000);
3943
});
4044

41-
/* eslint-disable-next-line jest/no-disabled-tests --
42-
Putting this off for the moment, because figuring out how to mock this out
43-
without making the code fragile/flaky will probably take some time
44-
*/
45-
it.skip('Will filter workspaces by search criteria when it is provided', async () => {
46-
expect.hasAssertions();
45+
it('Will filter workspaces by search criteria when it is provided', async () => {
46+
const { result, rerender } = await renderHookAsCoderEntity(
47+
({ coderQuery }) => useCoderWorkspacesQuery({ coderQuery }),
48+
{ initialProps: { coderQuery: 'owner:me' } },
49+
);
50+
51+
await waitFor(() => {
52+
expect(result.current.data?.length).toEqual(mockWorkspacesList.length);
53+
});
54+
55+
rerender({ coderQuery: mockWorkspaceNoParameters.name });
56+
57+
await waitFor(() => {
58+
const firstItemName = result.current.data?.[0]?.name;
59+
expect(firstItemName).toBe(mockWorkspaceNoParameters.name);
60+
});
4761
});
4862

4963
it('Will only return workspaces for a given repo when a repoConfig is provided', async () => {
@@ -54,12 +68,7 @@ describe(`${useCoderWorkspacesQuery.name}`, () => {
5468
});
5569
});
5670

57-
// This query takes a little bit longer to run and process; waitFor will
58-
// almost always give up too early if a longer timeout isn't specified
59-
await waitFor(() => expect(result.current.status).toBe('success'), {
60-
timeout: 3_000,
61-
});
62-
63-
expect(result.current.data?.length).toBe(1);
71+
await waitFor(() => expect(result.current.status).toBe('success'));
72+
expect(result.current.data?.length).toBe(2);
6473
});
6574
});

0 commit comments

Comments
 (0)