Skip to content

chore: add all missing tests for backstage-plugin-coder #101

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

Merged
merged 12 commits into from
Mar 29, 2024
Merged
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>
Comment on lines +109 to +111
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was missing from the component before – has two purposes:

  • Increasing accessibility for the overall form
  • Providing another way to hook into the component during testing


<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);
Comment on lines +71 to +72
Copy link
Member Author

@Parkreiner Parkreiner Mar 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the updated test helpers, I don't think the old comment applies anymore

});
});
Loading