Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,13 @@ export const MapStep = ({ groupName, onBack, onResolved, onStepChange, parsed }:
</p>
)}

<div className="flex flex-col gap-2" data-testid="bulk-column-mapping">
<div className="flex flex-col gap-2">
<h3 className="text-sm font-medium">
{t({ en: 'Column Mapping', es: 'Correspondencia de columnas', fr: 'Correspondance des colonnes' })}
</h3>
<WizardTable
className="max-h-[28rem] overflow-y-auto"
data-testid="bulk-column-mapping"
head={
<React.Fragment>
<Table.Head>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,9 @@ export const SourceStep = ({
/>
);
},
id: 'select'
id: 'select',
maxSize: 40,
size: 40
},
{
accessorKey: 'subject',
Expand Down Expand Up @@ -293,9 +295,6 @@ export const SourceStep = ({
}
]}
data={rows}
// The table paints its rows and ground in the page colour; inside the card they
// take the card's, so the grid does not read as one more box.
rootStyle={{ '--color-background': 'var(--color-card)' }}
onRowClick={(row) => toggle(row.id)}
/>
</div>
Expand All @@ -318,7 +317,7 @@ export const SourceStep = ({
'text/csv': ['.csv'],
'text/tab-separated-values': ['.tsv']
}}
className="min-h-72"
className="bg-background min-h-72"
data-testid="bulk-file-dropzone"
description={ACCEPTED_FILE_EXTENSIONS.join(', ')}
file={null}
Expand All @@ -337,7 +336,7 @@ export const SourceStep = ({
})}
</p>
<TextArea
className="min-h-72"
className="bg-background min-h-72"
data-testid="bulk-paste-input"
rows={8}
value={pasted}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type WizardTableProps = {
* header row, so the mapping, instrument, review and result tables all read as the same thing.
*/
export const WizardTable = ({ children, className, 'data-testid': testId, head }: WizardTableProps) => (
<div className={cn('overflow-x-auto rounded-md border', className)} data-testid={testId}>
<div className={cn('bg-background overflow-x-auto rounded-md border', className)} data-testid={testId}>
<Table>
<Table.Header className="bg-muted/40">
<Table.Row>{head}</Table.Row>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ describe('MapStep', () => {
expect(cells.map(({ textContent }) => textContent)).toEqual(['001', 'Alice', '002', 'Bob']);
});

// A file with many columns grew the mapping table until the Continue button sat below the fold,
// so the frame is capped and scrolls within itself rather than lengthening the step.
it('should cap the column mapping table and scroll it, so a wide file cannot push the step controls away', () => {
renderMapStep(byId);
const frame = screen.getByTestId('bulk-column-mapping');
expect(frame.classList.contains('max-h-[28rem]')).toBe(true);
expect(frame.classList.contains('overflow-y-auto')).toBe(true);
});

it('should badge a file with a subject ID column as identified by ID', () => {
renderMapStep(byId);
expect(screen.getByTestId('bulk-detected-mode').textContent).toBe('Subject ID');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,15 @@ describe('WizardTable', () => {
);
expect(screen.getByTestId('wizard-table').classList.contains('mt-4')).toBe(true);
});

// `cn` lets a later class win, so a caller's own class must not cost the frame its surface —
// inheriting the card colour is what made these tables read differently from the subject table.
it('should keep the page background when a caller passes its own class', () => {
render(
<WizardTable className="mt-4" data-testid="wizard-table" head={<th>{headLabel}</th>}>
{null}
</WizardTable>
);
expect(screen.getByTestId('wizard-table').classList.contains('bg-background')).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,6 @@ export const DeleteRemoteAssignments = ({ groupId, onBack, subjectIdDisplayLengt
}
]}
data={rows}
rootStyle={{ '--color-background': 'var(--color-card)' }}
onRowClick={(row) => toggle(row.id)}
/>
)}
Expand Down
17 changes: 17 additions & 0 deletions testing/src/pages/_app/group/remote-assignments.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ type WizardStepName = 'INSTRUMENTS' | 'REVIEW' | 'SUBJECTS';

export class RemoteAssignmentsPage extends AppPage {
readonly breadcrumbs: Locator;
/** The scrolling frame around the column mapping grid, capped so a wide file cannot lengthen the step. */
readonly columnMapping: Locator;
/** The page column the wizard must fit inside; `Layout` renders exactly one `main`. */
readonly container: Locator;
readonly fileInput: Locator;
readonly mapStep: Locator;
readonly selectAllSubjects: Locator;
readonly timepointsStep: Locator;
readonly useSelectedSubjects: Locator;
Expand All @@ -16,7 +20,12 @@ export class RemoteAssignmentsPage extends AppPage {
constructor(page: Page) {
super(page);
this.breadcrumbs = page.getByTestId('bulk-breadcrumbs');
this.columnMapping = page.getByTestId('bulk-column-mapping');
this.container = page.locator('main');
// libui's FileDropzone hardcodes its own testid and forwards none, so the `bulk-file-dropzone`
// the wizard passes it never reaches the DOM.
this.fileInput = page.getByTestId('dropzone').locator('input[type="file"]');
this.mapStep = page.getByTestId('bulk-map-step');
this.selectAllSubjects = page.getByTestId('bulk-select-all-subjects');
this.timepointsStep = page.getByTestId('bulk-timepoints-step');
this.useSelectedSubjects = page.getByTestId('bulk-use-selected-subjects');
Expand All @@ -26,4 +35,12 @@ export class RemoteAssignmentsPage extends AppPage {
breadcrumb(step: WizardStepName): Locator {
return this.$ref.getByTestId(`bulk-breadcrumb-${step}`);
}

async sourceMode(mode: 'FILE' | 'PASTE' | 'SELECT') {
await this.$ref.getByTestId(`bulk-source-mode-${mode}`).click();
}

async uploadFile(file: Parameters<Locator['setInputFiles']>[0]) {
await this.fileInput.setInputFiles(file);
}
}
44 changes: 44 additions & 0 deletions testing/src/specs/remote-assignments-wizard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,48 @@ test.describe('remote assignments wizard', () => {
await expect(wizardPage.breadcrumb('INSTRUMENTS')).toHaveAttribute('aria-current', 'step');
await expect(wizardPage.breadcrumb('SUBJECTS')).toBeEnabled();
});

/** One row under a header of `columnCount` columns, the first of which is the subject id. */
const wideCsv = (columnCount: number, uniqueId: string) => {
const headers = ['subject_id', ...Array.from({ length: columnCount - 1 }, (_, index) => `extra_${index}`)];
const row = [`wide_${uniqueId}`, ...Array.from({ length: columnCount - 1 }, (_, index) => `value_${index}`)];
return `${headers.join(',')}\n${row.join(',')}\n`;
};

// A file with many columns grew the mapping table until the step's own controls sat below the
// fold, so the frame is capped at 28rem and scrolls within itself instead.
test('should cap the column mapping table and scroll it rather than lengthening the step', async ({
api,
authenticateAs,
page,
uniqueId
}) => {
const group = await api.createGroup({ name: `WizardWide${uniqueId}` });
const { credentials } = await api.createUser({ basePermissionLevel: 'GROUP_MANAGER', groupIds: [group.id] });

await authenticateAs(credentials);
await page.goto('/group/remote-assignments?mode=create');
const wizardPage = new RemoteAssignmentsPage(page);
await expect(wizardPage.wizard).toBeVisible();

await wizardPage.sourceMode('FILE');
await wizardPage.uploadFile({
buffer: Buffer.from(wideCsv(24, uniqueId)),
mimeType: 'text/csv',
name: 'wide.csv'
});

await expect(wizardPage.mapStep).toBeVisible();
// One row per column of the file, so the grid is certainly taller than the cap.
await expect(wizardPage.columnMapping.locator('tbody tr')).toHaveCount(24);

const mapping = await boundingBox(wizardPage.columnMapping);
expect(mapping.height).toBeLessThanOrEqual(448);

const [scrollHeight, clientHeight] = await wizardPage.columnMapping.evaluate((element) => [
element.scrollHeight,
element.clientHeight
]);
expect(scrollHeight).toBeGreaterThan(clientHeight);
});
});
Loading