Skip to content

Commit

Permalink
chore: add all missing tests for backstage-plugin-coder (#101)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Parkreiner authored Mar 29, 2024
1 parent 343c866 commit f2f0689
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,21 @@ export const CoderAuthInputForm = () => {
registerNewToken(newToken);
};

const formHeaderId = `${hookId}-form-header`;
const legendId = `${hookId}-legend`;
const authTokenInputId = `${hookId}-auth-token`;
const warningBannerId = `${hookId}-warning-banner`;

return (
<form className={styles.formContainer} onSubmit={onSubmit}>
<form
aria-labelledby={formHeaderId}
className={styles.formContainer}
onSubmit={onSubmit}
>
<h3 hidden id={formHeaderId}>
Authenticate with Coder
</h3>

<div>
<CoderLogo className={styles.coderLogo} />
<p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* @file Defines integration tests for all sub-components in the
* CoderWorkspacesCard directory.
*/
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import { renderInCoderEnvironment } from '../../testHelpers/setup';
import { mockAuthStates } from '../../testHelpers/mockBackstageData';
import {
mockWorkspaceNoParameters,
mockWorkspaceWithMatch2,
mockWorkspacesList,
} from '../../testHelpers/mockCoderAppData';
import { type CoderAuthStatus } from '../CoderProvider';
import { CoderWorkspacesCard } from './CoderWorkspacesCard';
import userEvent from '@testing-library/user-event';

type RenderInputs = Readonly<{
authStatus?: CoderAuthStatus;
readEntityData?: boolean;
}>;

function renderWorkspacesCard(input?: RenderInputs) {
const { authStatus = 'authenticated', readEntityData = false } = input ?? {};

return renderInCoderEnvironment({
auth: mockAuthStates[authStatus],
children: <CoderWorkspacesCard readEntityData={readEntityData} />,
});
}

const matchers = {
authenticationForm: /Authenticate with Coder/i,
searchTitle: /Coder Workspaces/i,
searchbox: /Search your Coder workspaces/i,
emptyState: /Use the search bar to find matching Coder workspaces/i,
} as const satisfies Record<string, RegExp>;

describe(`${CoderWorkspacesCard.name}`, () => {
describe('General behavior', () => {
it('Shows the authentication form when the user is not authenticated', async () => {
await renderWorkspacesCard({
authStatus: 'tokenMissing',
});

expect(() => {
screen.getByRole('form', { name: matchers.authenticationForm });
}).not.toThrow();
});

it('Shows the workspaces list when the user is authenticated (exposed as an accessible search landmark)', async () => {
await renderWorkspacesCard();

await waitFor(() => {
expect(() => {
screen.getByRole('search', { name: matchers.searchTitle });
}).not.toThrow();
});
});

it('Shows zero workspaces when the query text matches nothing', async () => {
const entityValues = [true, false] as const;
const user = userEvent.setup();

for (const value of entityValues) {
const { unmount } = await renderWorkspacesCard({
readEntityData: value,
});

const searchbox = await screen.findByRole('searchbox', {
name: matchers.searchbox,
});

await user.tripleClick(searchbox);
await user.keyboard('[Backspace]');
await user.keyboard('I can do it - I can do it nine times');

await waitFor(() => {
// getAllByRole will throw if there isn't at least one node matched
const listItems = screen.queryAllByRole('listitem');
expect(listItems.length).toBe(0);
});

unmount();
}
});
});

describe('With readEntityData set to false', () => {
it('Will NOT filter any workspaces by the current repo', async () => {
await renderWorkspacesCard({ readEntityData: false });
const workspaceItems = await screen.findAllByRole('listitem');
expect(workspaceItems.length).toEqual(mockWorkspacesList.length);
});

it('Lets the user filter the workspaces by their query text', async () => {
await renderWorkspacesCard({ readEntityData: false });
const searchbox = await screen.findByRole('searchbox', {
name: matchers.searchbox,
});

const user = userEvent.setup();
await user.tripleClick(searchbox);
await user.keyboard(mockWorkspaceNoParameters.name);

// If more than one workspace matches, that throws an error
const onlyWorkspace = await screen.findByRole('listitem');
expect(onlyWorkspace).toHaveTextContent(mockWorkspaceNoParameters.name);
});

it('Shows all workspaces when query text is empty', async () => {
await renderWorkspacesCard({ readEntityData: false });
const searchbox = await screen.findByRole('searchbox', {
name: matchers.searchbox,
});

const user = userEvent.setup();
await user.tripleClick(searchbox);
await user.keyboard('[Backspace]');

const allWorkspaces = await screen.findAllByRole('listitem');
expect(allWorkspaces.length).toEqual(mockWorkspacesList.length);
});
});

describe('With readEntityData set to true', () => {
it('Will show only the workspaces that match the current repo', async () => {
await renderWorkspacesCard({ readEntityData: true });
const workspaceItems = await screen.findAllByRole('listitem');
expect(workspaceItems.length).toEqual(2);
});

it('Lets the user filter the workspaces by their query text (on top of filtering from readEntityData)', async () => {
await renderWorkspacesCard({ readEntityData: true });

await waitFor(() => {
const workspaceItems = screen.getAllByRole('listitem');
expect(workspaceItems.length).toBe(2);
});

const user = userEvent.setup();
const searchbox = await screen.findByRole('searchbox', {
name: matchers.searchbox,
});

await user.tripleClick(searchbox);
await user.keyboard(mockWorkspaceWithMatch2.name);

await waitFor(() => {
const newWorkspaceItems = screen.getAllByRole('listitem');
expect(newWorkspaceItems.length).toBe(1);
});
});

/**
* 2024-03-28 - MES - This is a test case to account for a previous
* limitation around querying workspaces by repo URL.
*
* This limitation no longer exists, so this test should be removed once the
* rest of the codebase is updated to support the new API endpoint for
* searching by build parameter
*/
it('Will not show any workspaces at all when the query text is empty', async () => {
await renderWorkspacesCard({ readEntityData: true });

const user = userEvent.setup();
const searchbox = await screen.findByRole('searchbox', {
name: matchers.searchbox,
});

await user.tripleClick(searchbox);
await user.keyboard('[Backspace]');

const emptyState = await screen.findByText(matchers.emptyState);
expect(emptyState).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type WorkspacesListProps, WorkspacesList } from './WorkspacesList';
import { renderInCoderEnvironment } from '../../testHelpers/setup';
import { CardContext, WorkspacesCardContext, WorkspacesQuery } from './Root';
import { mockCoderWorkspacesConfig } from '../../testHelpers/mockBackstageData';
import { mockWorkspace } from '../../testHelpers/mockCoderAppData';
import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderAppData';
import { Workspace } from '../../typesConstants';
import { screen } from '@testing-library/react';

Expand Down Expand Up @@ -42,9 +42,9 @@ describe(`${WorkspacesList.name}`, () => {
await renderWorkspacesList({
workspacesQuery: {
data: workspaceNames.map<Workspace>((name, index) => ({
...mockWorkspace,
...mockWorkspaceWithMatch,
name,
id: `${mockWorkspace.id}-${index}`,
id: `${mockWorkspaceWithMatch.id}-${index}`,
})),
},

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { renderInCoderEnvironment } from '../../testHelpers/setup';
import { mockWorkspace } from '../../testHelpers/mockCoderAppData';
import { mockWorkspaceWithMatch } from '../../testHelpers/mockCoderAppData';
import type { Workspace } from '../../typesConstants';
import { WorkspacesListItem } from './WorkspacesListItem';

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

const workspace: Workspace = {
...mockWorkspace,
...mockWorkspaceWithMatch,
latest_build: {
...mockWorkspace.latest_build,
...mockWorkspaceWithMatch.latest_build,
status: isOnline ? 'running' : 'stopped',
resources: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { useCoderWorkspacesQuery } from './useCoderWorkspacesQuery';

import { renderHookAsCoderEntity } from '../testHelpers/setup';
import { mockCoderWorkspacesConfig } from '../testHelpers/mockBackstageData';
import {
mockWorkspaceNoParameters,
mockWorkspacesList,
} from '../testHelpers/mockCoderAppData';

beforeAll(() => {
jest.useFakeTimers();
Expand Down Expand Up @@ -38,12 +42,22 @@ describe(`${useCoderWorkspacesQuery.name}`, () => {
await jest.advanceTimersByTimeAsync(10_000);
});

/* eslint-disable-next-line jest/no-disabled-tests --
Putting this off for the moment, because figuring out how to mock this out
without making the code fragile/flaky will probably take some time
*/
it.skip('Will filter workspaces by search criteria when it is provided', async () => {
expect.hasAssertions();
it('Will filter workspaces by search criteria when it is provided', async () => {
const { result, rerender } = await renderHookAsCoderEntity(
({ coderQuery }) => useCoderWorkspacesQuery({ coderQuery }),
{ initialProps: { coderQuery: 'owner:me' } },
);

await waitFor(() => {
expect(result.current.data?.length).toEqual(mockWorkspacesList.length);
});

rerender({ coderQuery: mockWorkspaceNoParameters.name });

await waitFor(() => {
const firstItemName = result.current.data?.[0]?.name;
expect(firstItemName).toBe(mockWorkspaceNoParameters.name);
});
});

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

// This query takes a little bit longer to run and process; waitFor will
// almost always give up too early if a longer timeout isn't specified
await waitFor(() => expect(result.current.status).toBe('success'), {
timeout: 3_000,
});

expect(result.current.data?.length).toBe(1);
await waitFor(() => expect(result.current.status).toBe('success'));
expect(result.current.data?.length).toBe(2);
});
});
Loading

0 comments on commit f2f0689

Please sign in to comment.