Skip to content
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
40 changes: 40 additions & 0 deletions apps/web/src/__tests__/datahub-table-controls.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { cleanup, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { Route } from '@/routes/_app/datahub/index';

import '@/services/i18n';

vi.mock('@tanstack/react-router', async (importOriginal) => ({
...(await importOriginal<typeof import('@tanstack/react-router')>()),
useNavigate: () => vi.fn()
}));
vi.mock('@/hooks/useSubjectsQuery', () => ({
subjectsQueryOptions: vi.fn(),
useSubjectsQuery: () => ({ data: [] })
}));
vi.mock('@/store', () => ({
useAppStore: (selector: (store: { currentGroup: null; currentUser: null }) => unknown) =>
selector({ currentGroup: null, currentUser: null })
}));

describe('data hub table controls', () => {
beforeEach(() => {
// The toggles call `Route.useNavigate()`, which warns outside a RouterProvider; no navigation happens here.
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
});

afterEach(() => {
cleanup();
vi.restoreAllMocks();
});

// happy-dom computes no layout, so the wrapping is asserted through its utility classes;
// testing/src/specs/datahub.spec.ts measures the rendered result at phone width.
it('should let the controls wrap below the md breakpoint, so a phone never pushes Export off screen', () => {
const Component = Route.options.component!;
render(<Component />);
const controls = screen.getByTestId('subject-lookup-search-button').parentElement!;
expect([...controls.classList]).toEqual(expect.arrayContaining(['flex-wrap', 'md:flex-nowrap']));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -99,18 +99,18 @@ export const InstrumentShowcase: React.FC<{
tabIndex={0}
onKeyDown={handleKeyDown}
>
<div className="flex items-center gap-2.5">
<div className="flex flex-col gap-2.5 lg:flex-row lg:items-center">
<SearchBar
className="grow"
className="w-full grow"
data-testid="instrument-search-bar"
value={searchTerm}
onValueChange={setSearchTerm}
/>
<div className="flex items-center gap-2.5">
<div data-testid="instrument-kind-filter">
<div className="flex w-full items-center gap-2.5 lg:w-auto">
<div className="grow lg:grow-0" data-testid="instrument-kind-filter">
<InstrumentKindDropdown selected={selectedKinds} setSelected={setSelectedKinds} />
</div>
<div data-testid="instrument-tag-filter">
<div className="grow lg:grow-0" data-testid="instrument-tag-filter">
<ListboxDropdown
widthFull
options={tagOptions}
Expand All @@ -119,7 +119,7 @@ export const InstrumentShowcase: React.FC<{
title={t('core.tags')}
/>
</div>
<div data-testid="instrument-language-filter">
<div className="grow lg:grow-0" data-testid="instrument-language-filter">
<InstrumentLanguageDropdown selected={selectedLanguages} setSelected={setSelectedLanguages} />
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ describe('InstrumentShowcase', () => {
expect(fireEvent.keyDown(searchBar, { key: 'Enter' })).toBe(false);
});

// happy-dom computes no layout, so the responsive stacking is asserted through its utility classes;
// testing/src/specs/accessible-instruments.spec.ts measures the rendered result at phone width.
it('should stack the search bar above the filters below the lg breakpoint, so a phone gives it the full width', () => {
render(<InstrumentShowcase data={[]} onSelect={vi.fn()} />);
const searchBar = screen.getByTestId('instrument-search-bar');
expect([...searchBar.classList]).toContain('w-full');
expect([...searchBar.parentElement!.classList]).toEqual(expect.arrayContaining(['flex-col', 'lg:flex-row']));
});

it('should not select anything when Enter is pressed with no matching instruments', () => {
const onSelect = vi.fn();
render(<InstrumentShowcase data={[]} onSelect={onSelect} />);
Expand Down
6 changes: 3 additions & 3 deletions apps/web/src/routes/_app/datahub/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const Filters: React.FC<{
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenu.Trigger asChild>
<Button
className="flex items-center justify-between gap-2"
className="flex grow items-center justify-between gap-2 md:grow-0"
data-testid="datahub-filters-trigger"
variant="outline"
>
Expand Down Expand Up @@ -300,11 +300,11 @@ const Toggles: React.FC<{
};

return (
<div className="flex gap-3">
<div className="flex flex-wrap gap-3 md:flex-nowrap">
<Dialog open={isLookupOpen} onOpenChange={setIsLookupOpen}>
<Dialog.Trigger asChild>
<Button
className="gap-2"
className="grow gap-2 md:grow-0"
data-spotlight-type="subject-lookup-search-button"
data-testid="subject-lookup-search-button"
id="subject-lookup-search-button"
Expand Down
6 changes: 5 additions & 1 deletion testing/src/pages/_app/datahub/index.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ export class DatahubPage extends AppPage {
readonly pageHeader: Locator;
readonly rowActionsTrigger: Locator;
readonly rows: Locator;
readonly searchBar: Locator;
readonly searchInput: Locator;
readonly subjectLookupButton: Locator;
constructor(page: Page) {
super(page);
this.exportDropdown = page.getByTestId('datahub-export-dropdown');
Expand All @@ -18,7 +20,9 @@ export class DatahubPage extends AppPage {
this.pageHeader = page.getByTestId('page-header');
this.rowActionsTrigger = page.getByTestId('row-actions-trigger').first();
this.rows = page.getByTestId('data-table-body').getByTestId('data-table-row');
this.searchInput = page.getByTestId('data-table-search-bar').getByRole('searchbox');
this.searchBar = page.getByTestId('data-table-search-bar');
this.searchInput = this.searchBar.getByRole('searchbox');
this.subjectLookupButton = page.getByTestId('subject-lookup-search-button');
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import { AppPage } from '../route.page';

export class AccessibleInstrumentsPage extends AppPage {
readonly instrumentShowcase: Locator;
readonly kindFilter: Locator;
readonly pageHeader: Locator;
readonly searchBar: Locator;
readonly searchBox: Locator;

constructor(page: Page) {
super(page);
this.pageHeader = page.getByTestId('page-header');
this.instrumentShowcase = page.getByTestId('instrument-showcase');
this.searchBox = page.getByTestId('instrument-search-bar').locator('input');
this.kindFilter = page.getByTestId('instrument-kind-filter');
this.searchBar = page.getByTestId('instrument-search-bar');
this.searchBox = this.searchBar.locator('input');
}

/** Cards carry a content-hash testid, so match on the visible title instead. */
Expand Down
19 changes: 19 additions & 0 deletions testing/src/specs/accessible-instruments.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
import { PHONE_VIEWPORT } from '../support/constants';
import { expect, test } from '../support/fixtures';

test.describe('accessible instruments on a phone', () => {
test.use({ viewport: PHONE_VIEWPORT });

test('should give the search bar a full-width row above the filters, so the dropdowns cannot crowd it out', async ({
getPageModel
}) => {
const accessibleInstrumentsPage = await getPageModel('/instruments/accessible-instruments');
await expect(accessibleInstrumentsPage.searchBar).toBeVisible();

const showcaseBox = (await accessibleInstrumentsPage.instrumentShowcase.boundingBox())!;
const searchBarBox = (await accessibleInstrumentsPage.searchBar.boundingBox())!;
const kindFilterBox = (await accessibleInstrumentsPage.kindFilter.boundingBox())!;

expect(searchBarBox.width).toBeCloseTo(showcaseBox.width, 0);
expect(kindFilterBox.y).toBeGreaterThanOrEqual(searchBarBox.y + searchBarBox.height);
});
});

test.describe('accessible instruments', () => {
test('should filter the instrument showcase to cards matching the search query', async ({ getPageModel }) => {
const accessibleInstrumentsPage = await getPageModel('/instruments/accessible-instruments');
Expand Down
19 changes: 18 additions & 1 deletion testing/src/specs/datahub.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
import { readFile } from 'node:fs/promises';

import { DatahubPage } from '../pages/_app/datahub/index.page';
import { HAPPINESS_RECORD } from '../support/constants';
import { HAPPINESS_RECORD, PHONE_VIEWPORT } from '../support/constants';
import { expect, test } from '../support/fixtures';

test.describe('data hub on a phone', () => {
test.use({ viewport: PHONE_VIEWPORT });

test('should keep every table control within the width of the search bar, so none is pushed off screen', async ({
getPageModel
}) => {
const datahubPage = await getPageModel('/datahub');
await expect(datahubPage.exportDropdown).toBeVisible();

const searchBarBox = (await datahubPage.searchBar.boundingBox())!;
for (const control of [datahubPage.subjectLookupButton, datahubPage.filtersTrigger, datahubPage.exportDropdown]) {
const controlBox = (await control.boundingBox())!;
expect(controlBox.x + controlBox.width).toBeLessThanOrEqual(searchBarBox.x + searchBarBox.width);
}
});
});

test.describe('data hub', () => {
test("should export the records of a group manager's own group, since the export applies the caller's read rules", async ({
api,
Expand Down
3 changes: 3 additions & 0 deletions testing/src/support/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ export const E2E_MAIL_CONFIG = {
senderName: 'Open Data Capture',
username: 'e2e'
} as const;

/** A typical phone screen, narrower than the `md` breakpoint at which `apps/web` switches to its desktop layout. */
export const PHONE_VIEWPORT = { height: 844, width: 390 } as const;
Loading