test(a11y): reproduce native keyboard file-picker failures
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
# P5-15-04-A11Y-01 — Native file-picker keyboard repair
|
||||
|
||||
## Reproduction checkpoint
|
||||
|
||||
From `f55c761ac229a69357a1c5b68a06e837cfa31ec0`, the unchanged native audit again reports six Enter/Space failures for Upload, Import JSON and Convert CSV to Import JSON; the three pointer comparisons succeed. The native suite's other failure is the separately owned CV source route. No files were uploaded by these chooser observations.
|
||||
|
||||
The label-only MUI buttons have no click handler. MUI keyboard activation invokes a nonnative button's handler; it does not emulate the browser's label-to-input default activation. The same pattern exists in Add Job's four attachment buckets. It is not an API, permissions or file-validation failure.
|
||||
|
||||
`src/file-picker-activation.test.tsx` adds eight real-component cases: seven keyboard/input activation boundaries (all fail with expected one input click/actual zero), plus file/Job association, uploading disabled state and post-upload selection reset. The latter also exposes that a label is not a native disabled button. Earlier test-fixture label/step mistakes were corrected before recording the final RED; they are not application defects.
|
||||
|
||||
Affected contracts: PRES-W08 attachments and PRES-J10 import/export; Add Job same-pattern caller is covered without changing its domain workflow. Whole groups remain FAIL at this reproduction checkpoint. No implementation correction or fresh full-suite claim is made here. Production remains NO-GO; human screen-reader/device acceptance remains incomplete.
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from "react";
|
||||
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import Attachments from "./components/Attachments";
|
||||
import ImportExportJobs from "./components/ImportExportJobs";
|
||||
import AddJobModal from "./components/AddJobModal";
|
||||
import { ConfirmProvider } from "./confirm";
|
||||
import { PromptProvider } from "./prompt";
|
||||
import { I18nProvider } from "./i18n/I18nProvider";
|
||||
import { ToastProvider } from "./toast";
|
||||
import { api } from "./api";
|
||||
|
||||
// These tests exercise file selection; the unrelated date-picker adapter is ESM.
|
||||
jest.mock("@mui/x-date-pickers/DatePicker", () => ({ DatePicker: ({ label }: { label: string }) => <div>{label}</div> }));
|
||||
|
||||
const mockedApi = api as jest.Mocked<typeof api>;
|
||||
|
||||
function renderPicker(component: React.ReactNode) {
|
||||
return render(<ToastProvider><I18nProvider><ConfirmProvider><PromptProvider>{component}</PromptProvider></ConfirmProvider></I18nProvider></ToastProvider>);
|
||||
}
|
||||
|
||||
async function expectNativeInputActivation(button: HTMLElement, input: HTMLInputElement) {
|
||||
const activated = jest.fn();
|
||||
input.addEventListener("click", activated);
|
||||
act(() => button.focus());
|
||||
await userEvent.keyboard("{Enter}");
|
||||
expect(activated).toHaveBeenCalledTimes(1);
|
||||
await userEvent.keyboard(" ");
|
||||
expect(activated).toHaveBeenCalledTimes(2);
|
||||
userEvent.click(button);
|
||||
expect(activated).toHaveBeenCalledTimes(3);
|
||||
expect(button).toHaveFocus();
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("attachment Upload activates the same native input from Enter, Space and pointer", async () => {
|
||||
mockedApi.get.mockResolvedValue({ data: [] });
|
||||
const { container } = renderPicker(<Attachments jobId={42} />);
|
||||
await screen.findByText(/No attachments yet\./);
|
||||
await expectNativeInputActivation(screen.getByRole("button", { name: "Upload" }), container.querySelector<HTMLInputElement>('input[type="file"]')!);
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test.each([
|
||||
["Import JSON", "application/json"],
|
||||
["Convert CSV to Import JSON", ".csv,text/csv"],
|
||||
])("%s activates its own native input from Enter, Space and pointer", async (name, accept) => {
|
||||
const { container } = renderPicker(<ImportExportJobs />);
|
||||
await expectNativeInputActivation(screen.getByRole("button", { name }), container.querySelector<HTMLInputElement>(`input[accept="${accept}"]`)!);
|
||||
expect(mockedApi.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test.each([0, 1, 2, 3])("Add Job attachment bucket %s retains keyboard selection and distinct pending files", async (bucket) => {
|
||||
mockedApi.post.mockImplementation((url) => Promise.resolve({ data: url === "/jobimport/preview" ? { success: true, title: "Supported role", company: "Fixture company", description: "Known description" } : {} }));
|
||||
renderPicker(<AddJobModal open initialUrl="https://example.test/jobs/1" onClose={() => {}} onCreated={() => {}} />);
|
||||
await screen.findByDisplayValue("Supported role");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Continue" }));
|
||||
for (let step = 0; step < bucket; step += 1) fireEvent.click(screen.getByRole("button", { name: "Skip and continue" }));
|
||||
const input = document.querySelector<HTMLInputElement>('input[type="file"]')!;
|
||||
expect(input.multiple).toBe(true);
|
||||
expect(input.accept).toContain(".pdf,.doc,.docx,.txt,.md");
|
||||
await expectNativeInputActivation(screen.getByRole("button", { name: "Choose files" }), input);
|
||||
fireEvent.change(input, { target: { files: [new File(["fixture"], `bucket-${bucket}.txt`, { type: "text/plain" })] } });
|
||||
expect(screen.getByText(`bucket-${bucket}.txt`)).toBeInTheDocument();
|
||||
expect(mockedApi.post).not.toHaveBeenCalledWith("/attachments", expect.anything(), expect.anything());
|
||||
});
|
||||
|
||||
test("attachment selection retains files and job ownership, disables activation while uploading and clears selection after completion", async () => {
|
||||
mockedApi.get.mockResolvedValue({ data: [] });
|
||||
let finishUpload!: (value: { data: object }) => void;
|
||||
mockedApi.post.mockImplementationOnce(() => new Promise((resolve) => { finishUpload = resolve; }));
|
||||
const { container } = renderPicker(<Attachments jobId={42} />);
|
||||
await screen.findByText(/No attachments yet\./);
|
||||
const input = container.querySelector<HTMLInputElement>('input[type="file"]')!;
|
||||
const files = [new File(["Known CV"], "cv.pdf", { type: "application/pdf" }), new File(["Søknad"], "søknad.txt", { type: "text/plain" })];
|
||||
fireEvent.change(input, { target: { files } });
|
||||
const uploading = await screen.findByRole("button", { name: "Uploading..." });
|
||||
expect(uploading).toBeDisabled();
|
||||
const body = mockedApi.post.mock.calls[0][1] as FormData;
|
||||
expect(body.get("jobId")).toBe("42");
|
||||
expect(body.getAll("files")).toEqual(files);
|
||||
const activated = jest.fn();
|
||||
input.addEventListener("click", activated);
|
||||
userEvent.click(uploading);
|
||||
expect(activated).not.toHaveBeenCalled();
|
||||
finishUpload({ data: {} });
|
||||
await waitFor(() => expect(screen.getByRole("button", { name: "Upload" })).toBeEnabled());
|
||||
expect(input.value).toBe("");
|
||||
});
|
||||
Reference in New Issue
Block a user