Skip to content

Commit 343c866

Browse files
authored
chore: add missing unit tests for Coder plugin (#83)
* fix: fix hover behavior for last list item * fix: shrink default max height for container * fix: ensure divider bar appears when there is overflow * refactor: add workspaceCreationLink prop to context provider * refactor: split Placeholder into separate component * chore: finish cta button * fix: make sure button only appears when loading is finished * docs: remove bad comment * chore: add explicit return type to useCoderAppConfig for clarity * refactor: consolidate and decouple type definitions * refactor: move dynamic entity config logic * refactor: update references for workspaces config * refactor: centralize creationUrl logic * refactor: rename useCoderEntityConfig to useCoderWorkspacesConfig * refactor: rename old useCoderWorkspaces to useCoderWorkspacesQuery * fix: update typo in test case * fix: update test logic to account for creationUrl * fix: update query logic to account for always-defined workspacesConfig * docs: fix typo in comment * refactor: clean up how mock data is defined * fix: make logic for showing reminder more airtight * refactor: split DataReminder into separate file * refactor: simplify API for useCoderWorkspacesQuery * fix: make sure data reminder only shows when appropriate * wip: commit progress on auth test * chore: simplify setup for CoderProviderWithMockAuth * wip: reorganize test structure * chore: update test helper to accept mock callbacks * wip: commit more test progress * chore: finish tests for CoderAuthWrapper * refactor: make error message more user-friendly * fix: delete stale DataReminder file * fix: delete untested test helpers * chore: finish tests for CreateWorkspaceLink * refactor: extract test setup logic into helper * chore: finish tests for EntityDataReminder * fix: update indenting level for comment * wip: add stubs for ExtraActionsButton * refactor: update APIs for test helpers * chore: add test case for submitting new token * chore: let user pass in custom query client for test helper * wip: lay out test stubs for ExtraActionsButton * refactor: simplify API for renderInCoderEnvironment * wip: commit progress on ExtraActionsButton * wip: add test case for keyboard input * fix: make better assertions about auto-focus * chore: finish tests for ExtraActionsButton * chore: update test helper to accept custom entity * chore: add mock repo name value * wip: commit current progress on HeaderRow tests * chore: finish tests for HeaderRow * docs: update comment for clarity * fix: update repo URL parsing logic * chore: add test for Placeholder * refactor: update type definitions for ExtraActionsButton test * wip: add stub logic for SearchBox tests * docs: add note about how Root probably shouldn't be tested * wip: add stubs for CoderWorkspacesCard tests * wip: reorganize test cases * chore: finish initial draft of tests for SearchBox * chore: update test logic to account for debounces * fix: update test to account for throttles better * wip: commit progress for Root tests * chore: finish tests for Root * wip: commit progress on WorkspacesListIcon test * refactor: update WorkspacesListIcon to be easier to test * chore: finish tests for WorkspacesListIcon * chore: add tests for WorkspacesListItem * docs: add note about scope of tests for Root * chore: finish tests for WorkspacesListItem * chore: finish all unit tests * fix: delete empty test file (to be added in future PR) * docs: update type definitions * docs: update hook/type docs to reflect new APIs * docs: fix typo * chore: try removing react-use dependency to make CI happy
1 parent b19c08e commit 343c866

21 files changed

+1053
-117
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ export const CoderAuthInputForm = () => {
128128
// won't connect the label and input together, which breaks
129129
// accessibility for screen readers. Need to wire up extra IDs, sadly.
130130
label="Auth token"
131-
id={authTokenInputId}
132131
InputLabelProps={{ htmlFor: authTokenInputId }}
132+
InputProps={{ id: authTokenInputId }}
133133
required
134134
name="authToken"
135135
type="password"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import React from 'react';
2+
import { screen } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import { CoderProviderWithMockAuth } from '../../testHelpers/setup';
5+
import type { CoderAuth, CoderAuthStatus } from '../CoderProvider';
6+
import {
7+
mockAppConfig,
8+
mockAuthStates,
9+
mockCoderAuthToken,
10+
} from '../../testHelpers/mockBackstageData';
11+
import { CoderAuthWrapper } from './CoderAuthWrapper';
12+
import { renderInTestApp } from '@backstage/test-utils';
13+
14+
type RenderInputs = Readonly<{
15+
childButtonText: string;
16+
authStatus: CoderAuthStatus;
17+
}>;
18+
19+
async function renderAuthWrapper({
20+
authStatus,
21+
childButtonText,
22+
}: RenderInputs) {
23+
const ejectToken = jest.fn();
24+
const registerNewToken = jest.fn();
25+
26+
const auth: CoderAuth = {
27+
...mockAuthStates[authStatus],
28+
ejectToken,
29+
registerNewToken,
30+
};
31+
32+
/**
33+
* @todo RTL complains about the current environment not being configured to
34+
* support act. Luckily, it doesn't cause any of our main test cases to kick
35+
* up false positives.
36+
*
37+
* This may not be an issue with our code, and might be a bug from Backstage's
38+
* migration to React 18. Need to figure out where this issue is coming from,
39+
* and open an issue upstream if necessary
40+
*/
41+
const renderOutput = await renderInTestApp(
42+
<CoderProviderWithMockAuth appConfig={mockAppConfig} auth={auth}>
43+
<CoderAuthWrapper type="card">
44+
<button>{childButtonText}</button>
45+
</CoderAuthWrapper>
46+
</CoderProviderWithMockAuth>,
47+
);
48+
49+
return { ...renderOutput, ejectToken, registerNewToken };
50+
}
51+
52+
describe(`${CoderAuthWrapper.name}`, () => {
53+
describe('Displaying main content', () => {
54+
it('Displays the main children when the user is authenticated', async () => {
55+
const buttonText = 'I have secret Coder content!';
56+
renderAuthWrapper({
57+
authStatus: 'authenticated',
58+
childButtonText: buttonText,
59+
});
60+
61+
const button = await screen.findByRole('button', { name: buttonText });
62+
63+
// This assertion isn't necessary because findByRole will throw an error
64+
// if the button can't be found within the expected period of time. Doing
65+
// this purely to make the Backstage linter happy
66+
expect(button).toBeInTheDocument();
67+
});
68+
});
69+
70+
describe('Loading UI', () => {
71+
it('Is displayed while the auth is initializing', async () => {
72+
const buttonText = "You shouldn't be able to see me!";
73+
renderAuthWrapper({
74+
authStatus: 'initializing',
75+
childButtonText: buttonText,
76+
});
77+
78+
await screen.findByText(/Loading/);
79+
const button = screen.queryByRole('button', { name: buttonText });
80+
expect(button).not.toBeInTheDocument();
81+
});
82+
});
83+
84+
describe('Token distrusted form', () => {
85+
it("Is displayed when the user's auth status cannot be verified", async () => {
86+
const buttonText = 'Not sure if you should be able to see me';
87+
const distrustedTextMatcher = /Unable to verify token authenticity/;
88+
const distrustedStatuses: readonly CoderAuthStatus[] = [
89+
'distrusted',
90+
'noInternetConnection',
91+
'deploymentUnavailable',
92+
];
93+
94+
for (const status of distrustedStatuses) {
95+
const { unmount } = await renderAuthWrapper({
96+
authStatus: status,
97+
childButtonText: buttonText,
98+
});
99+
100+
await screen.findByText(distrustedTextMatcher);
101+
const button = screen.queryByRole('button', { name: buttonText });
102+
expect(button).not.toBeInTheDocument();
103+
104+
unmount();
105+
}
106+
});
107+
108+
it('Lets the user eject the current token', async () => {
109+
const { ejectToken } = await renderAuthWrapper({
110+
authStatus: 'distrusted',
111+
childButtonText: "I don't matter",
112+
});
113+
114+
const user = userEvent.setup();
115+
const ejectButton = await screen.findByRole('button', {
116+
name: 'Eject token',
117+
});
118+
119+
await user.click(ejectButton);
120+
expect(ejectToken).toHaveBeenCalled();
121+
});
122+
123+
it('Will appear if auth status changes during re-renders', async () => {
124+
const buttonText = "Now you see me, now you don't";
125+
const { rerender } = await renderAuthWrapper({
126+
authStatus: 'authenticated',
127+
childButtonText: buttonText,
128+
});
129+
130+
// Capture button after it first appears on the screen
131+
const button = await screen.findByRole('button', { name: buttonText });
132+
133+
rerender(
134+
<CoderProviderWithMockAuth
135+
appConfig={mockAppConfig}
136+
authStatus="distrusted"
137+
>
138+
<CoderAuthWrapper type="card">
139+
<button>{buttonText}</button>
140+
</CoderAuthWrapper>
141+
</CoderProviderWithMockAuth>,
142+
);
143+
144+
// Assert that the button is now gone
145+
expect(button).not.toBeInTheDocument();
146+
});
147+
});
148+
149+
describe('Token submission form', () => {
150+
it("Is displayed when the token either doesn't exist or is definitely not valid", async () => {
151+
const buttonText = "You're not allowed to gaze upon my visage";
152+
const tokenFormMatcher = /Please enter a new token/;
153+
const statusesForInvalidUser: readonly CoderAuthStatus[] = [
154+
'invalid',
155+
'tokenMissing',
156+
];
157+
158+
for (const status of statusesForInvalidUser) {
159+
const { unmount } = await renderAuthWrapper({
160+
authStatus: status,
161+
childButtonText: buttonText,
162+
});
163+
164+
await screen.findByText(tokenFormMatcher);
165+
const button = screen.queryByRole('button', { name: buttonText });
166+
expect(button).not.toBeInTheDocument();
167+
168+
unmount();
169+
}
170+
171+
expect.hasAssertions();
172+
});
173+
174+
it('Lets the user submit a new token', async () => {
175+
const { registerNewToken } = await renderAuthWrapper({
176+
authStatus: 'tokenMissing',
177+
childButtonText: "I don't matter",
178+
});
179+
180+
/**
181+
* Two concerns that make the selection for inputField a little hokey:
182+
* 1. The auth input is of type password, which does not have a role
183+
* compatible with Testing Library; can't use getByRole to select it
184+
* 2. MUI adds a star to its labels that are required, meaning that any
185+
* attempts at trying to match the string "Auth token" will fail
186+
*/
187+
const inputField = screen.getByLabelText(/Auth token/);
188+
const submitButton = screen.getByRole('button', { name: 'Authenticate' });
189+
190+
const user = userEvent.setup();
191+
await user.click(inputField);
192+
await user.keyboard(mockCoderAuthToken);
193+
await user.click(submitButton);
194+
195+
expect(registerNewToken).toHaveBeenCalledWith(mockCoderAuthToken);
196+
});
197+
});
198+
});

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export const CoderAuthWrapper = ({ children, type }: WrapperProps) => {
7070
case 'authenticated':
7171
case 'distrustedWithGracePeriod': {
7272
throw new Error(
73-
'This code should be unreachable because of the auth check near the start of the component',
73+
'Tried to process authenticated user after main content should already be shown',
7474
);
7575
}
7676

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React from 'react';
2+
import { screen } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import { mockAppConfig } from '../../testHelpers/mockBackstageData';
5+
import { renderInCoderEnvironment } from '../../testHelpers/setup';
6+
import { Root } from './Root';
7+
import { CreateWorkspaceLink } from './CreateWorkspaceLink';
8+
9+
function render() {
10+
return renderInCoderEnvironment({
11+
children: (
12+
<Root>
13+
<CreateWorkspaceLink />
14+
</Root>
15+
),
16+
});
17+
}
18+
19+
describe(`${CreateWorkspaceLink.name}`, () => {
20+
it('Displays a link based on the current entity', async () => {
21+
await render();
22+
const link = screen.getByRole<HTMLAnchorElement>('link');
23+
24+
expect(link).not.toBeDisabled();
25+
expect(link.target).toEqual('_blank');
26+
expect(link.href).toMatch(
27+
new RegExp(`^${mockAppConfig.deployment.accessUrl}/`),
28+
);
29+
});
30+
31+
it('Will display a tooltip while hovered over', async () => {
32+
await render();
33+
const link = screen.getByRole<HTMLAnchorElement>('link');
34+
const user = userEvent.setup();
35+
36+
await user.hover(link);
37+
const tooltip = await screen.findByText('Add a new workspace');
38+
expect(tooltip).toBeInTheDocument();
39+
});
40+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
import { screen } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import { renderInCoderEnvironment } from '../../testHelpers/setup';
5+
import { Root } from './Root';
6+
import { EntityDataReminder } from './EntityDataReminder';
7+
8+
function render() {
9+
return renderInCoderEnvironment({
10+
children: (
11+
<Root>
12+
<EntityDataReminder />
13+
</Root>
14+
),
15+
});
16+
}
17+
18+
describe(`${EntityDataReminder.name}`, () => {
19+
it('Will toggle between showing/hiding the disclosure info when the user clicks it', async () => {
20+
await render();
21+
const user = userEvent.setup();
22+
const disclosureButton = screen.getByRole('button', {
23+
name: /Why am I seeing all workspaces\?/,
24+
});
25+
26+
await user.click(disclosureButton);
27+
const disclosureInfo = await screen.findByText(
28+
/This component displays all workspaces when the entity has no repo URL to filter by/,
29+
);
30+
31+
await user.click(disclosureButton);
32+
expect(disclosureInfo).not.toBeInTheDocument();
33+
});
34+
});

0 commit comments

Comments
 (0)