feat(applications): consolidate row actions into keyboard menus
This commit is contained in:
@@ -90,6 +90,7 @@ for (const mode of ["light", "dark"] as const) for (const language of ["en", "nb
|
||||
await expect(density).toHaveText(t.densityComfortable);
|
||||
await expect(main.getByRole("columnheader", { name: t.jobTableRole, exact: true })).toHaveAttribute("aria-sort", "ascending");
|
||||
await capture(page, info, `jobs-top-desktop-${suffix}`);
|
||||
await main.getByRole("button", { name: t.jobTableSelectionMode, exact: true }).click();
|
||||
const rowText = await first.innerText();
|
||||
const headers = await main.getByRole("columnheader").allTextContents();
|
||||
const select = first.getByRole("checkbox");
|
||||
@@ -157,13 +158,18 @@ for (const mode of ["light", "dark"] as const) for (const language of ["en", "nb
|
||||
// Native row status request, listing-link isolation and workspace navigation.
|
||||
await select.uncheck();
|
||||
// Match by the translated action prefix; the long title remains in its accessible name.
|
||||
const statusAction = first.getByRole("button", { name: new RegExp(`^${t.jobTableQuickStatus}:`) });
|
||||
await keyboardFocus(page, statusAction);
|
||||
await page.keyboard.press("Escape"); // Dismiss the existing tooltip, preserving focus.
|
||||
const actions = first.getByRole("button", { name: new RegExp(`^${t.jobTableActions}:`) });
|
||||
await keyboardFocus(page, actions);
|
||||
await actions.press("Enter");
|
||||
const actionsMenu = page.getByRole("menu");
|
||||
await expect(actionsMenu).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(actionsMenu).toBeHidden();
|
||||
await expect(actions).toBeFocused();
|
||||
const targets = await first.locator(".MuiIconButton-root,.MuiCheckbox-root").evaluateAll(nodes => nodes.map(node => ({ width: node.getBoundingClientRect().width, height: node.getBoundingClientRect().height })));
|
||||
expect(targets.every(target => target.width >= 32 && target.height >= 32)).toBe(true);
|
||||
await capture(page, info, `row-action-focus-${suffix}`);
|
||||
await statusAction.press("Enter");
|
||||
await actions.press("Enter");
|
||||
const changed = page.waitForResponse(r => r.url() === `${apiUrl}/jobapplications/${owner.ids[0]}/status` && r.request().method() === "PATCH");
|
||||
await page.getByRole("menuitem", { name: t.jobTableSetStatus.replace("{status}", t.statusInterview), exact: true }).click();
|
||||
expect((await changed).status()).toBe(204);
|
||||
@@ -210,7 +216,8 @@ for (const mode of ["light", "dark"] as const) for (const language of ["en", "nb
|
||||
expect(JSON.parse((await page.evaluate(() => localStorage.getItem("jobColumns")))!).jobUrl).toBe(false);
|
||||
|
||||
// Real confirm/soft-delete and the same renderer's existing Trash restore path.
|
||||
await first.getByRole("button", { name: new RegExp(`^${t.jobTableSoftDelete}:`) }).click();
|
||||
await actions.click();
|
||||
await actionsMenu.getByRole("menuitem", { name: t.jobTableSoftDelete, exact: true }).click();
|
||||
const deleted = page.waitForResponse(r => r.url() === `${apiUrl}/jobapplications/${owner.ids[0]}` && r.request().method() === "DELETE");
|
||||
await page.getByRole("dialog").getByRole("button", { name: t.jobTableMove, exact: true }).click();
|
||||
expect((await deleted).status()).toBe(204);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, { lazy, Suspense, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import { Link as RouterLink, useLocation, useNavigate } from "react-router-dom";
|
||||
|
||||
import {
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Chip,
|
||||
Divider,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
@@ -29,9 +31,6 @@ import {
|
||||
} from "@mui/material";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
|
||||
import RestoreFromTrashOutlinedIcon from "@mui/icons-material/RestoreFromTrashOutlined";
|
||||
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
|
||||
import ViewColumnIcon from "@mui/icons-material/ViewColumn";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
@@ -180,9 +179,9 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
const [companyFilterId, setCompanyFilterId] = useState<number | "All">(routeQuery.companyId ?? "All");
|
||||
const [editJobId, setEditJobId] = useState<number | null>(null);
|
||||
const [reloadToken, setReloadToken] = useState(0);
|
||||
const [statusAnchor, setStatusAnchor] = useState<null | HTMLElement>(null);
|
||||
const [rowMenu, setRowMenu] = useState<{ id: number; anchor: HTMLButtonElement; mobile: boolean; mode: "jobs" | "trash" } | null>(null);
|
||||
const rowMenuId = useId();
|
||||
const [bulkStatusAnchor, setBulkStatusAnchor] = useState<null | HTMLElement>(null);
|
||||
const [statusJobId, setStatusJobId] = useState<number | null>(null);
|
||||
const [sortBy, setSortBy] = useState<JobSortKey>(routeQuery.sortBy);
|
||||
const [sortDir, setSortDir] = useState<"asc" | "desc">(routeQuery.sortDir);
|
||||
|
||||
@@ -347,6 +346,33 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
};
|
||||
|
||||
const filteredJobs = jobs;
|
||||
const menuJob = rowMenu?.anchor.isConnected && rowMenu.mobile === isMobile && rowMenu.mode === mode
|
||||
&& !boardView && !jobsResource.loading && !jobsResource.error ? filteredJobs.find(job => job.id === rowMenu.id) : undefined;
|
||||
const rowActions = (job: JobApplication) => {
|
||||
const expanded = menuJob?.id === job.id;
|
||||
const label = `${t("jobTableActions")}: ${job.jobTitle}`;
|
||||
return (
|
||||
<Tooltip title={t("jobTableActions")}>
|
||||
<IconButton
|
||||
aria-label={label}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={expanded}
|
||||
aria-controls={expanded ? rowMenuId : undefined}
|
||||
onClick={event => setRowMenu({ id: job.id, anchor: event.currentTarget, mobile: isMobile, mode })}
|
||||
sx={{ flexShrink: 0, width: isMobile ? 44 : 32, height: isMobile ? 44 : 32, "@media (pointer: coarse)": { width: 44, height: 44 } }}
|
||||
>
|
||||
<MoreHorizIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const runRowAction = (action: () => void) => {
|
||||
// Return focus to the trigger before an action opens a dialog.
|
||||
flushSync(() => setRowMenu(null));
|
||||
rowMenu?.anchor.focus({ preventScroll: true });
|
||||
action();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const focusJobId = window.history.state?.jobListFocusId ?? (location.state as { focusJobId?: unknown } | null)?.focusJobId;
|
||||
@@ -741,7 +767,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
>
|
||||
<Stack spacing="var(--app-list-gap)">
|
||||
<Box sx={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 1, flexWrap: "wrap" }}>
|
||||
<Box sx={{ display: "flex", alignItems: "flex-start", gap: 1, minWidth: 0, flex: "1 1 180px" }}>
|
||||
<Box sx={{ display: "flex", alignItems: "flex-start", gap: 1, minWidth: 0, flex: "1 1 120px" }}>
|
||||
{showSelection ? <Checkbox inputProps={{ "aria-label": t("jobTableSelectJob", { title: job.jobTitle }) }} checked={isSelected} onChange={(e) => toggleSelected(job.id, e.target.checked)} sx={{ mt: -0.5, ml: -1 }} /> : null}
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography sx={{ fontWeight: 700, fontSize: 16, lineHeight: 1.35, overflowWrap: "anywhere" }}>
|
||||
@@ -752,7 +778,10 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
{columns.status ? <Chip label={statusLabel(t, job.status)} size="small" color={toneName as any} variant="outlined" sx={{ fontWeight: 700, maxWidth: "100%", height: "auto", minHeight: 24, "& .MuiChip-label": { whiteSpace: "normal", overflowWrap: "anywhere", py: 0.5 } }} /> : null}
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.25, maxWidth: "100%", ml: "auto" }}>
|
||||
{columns.status ? <Chip label={statusLabel(t, job.status)} size="small" color={toneName as any} variant="outlined" sx={{ fontWeight: 700, maxWidth: "100%", height: "auto", minHeight: 24, "& .MuiChip-label": { whiteSpace: "normal", overflowWrap: "anywhere", py: 0.5 } }} /> : null}
|
||||
{rowActions(job)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{actionSignals.length > 0 && <Box sx={{ display: "flex", gap: 0.75, flexWrap: "wrap" }}>
|
||||
@@ -807,15 +836,6 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
<Box sx={{ display: "flex", gap: 0.5, "& .MuiIconButton-root": { width: 44, height: 44 } }}>
|
||||
<Tooltip title={t("jobTableEdit")}><IconButton aria-label={t("jobTableEdit")} onClick={() => setEditJobId(job.id)}><EditOutlinedIcon /></IconButton></Tooltip>
|
||||
<Tooltip title={t("jobTableQuickStatus")}><IconButton aria-label={t("jobTableQuickStatus")} onClick={(e) => { setStatusJobId(job.id); setStatusAnchor(e.currentTarget); }}><MoreHorizIcon /></IconButton></Tooltip>
|
||||
{(mode === "trash" || (includeDeleted && job.isDeleted)) ? (
|
||||
<Tooltip title={t("jobTableRestore")}><IconButton aria-label={t("jobTableRestore")} onClick={() => void restore(job.id)}><RestoreFromTrashOutlinedIcon /></IconButton></Tooltip>
|
||||
) : (
|
||||
<Tooltip title={t("jobTableSoftDelete")}><IconButton color="error" aria-label={t("jobTableSoftDelete")} onClick={() => void softDelete(job)}><DeleteOutlineIcon /></IconButton></Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
@@ -895,13 +915,7 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
{columns.daysSince ? <TableCell>{job.daysSince ?? "—"}</TableCell> : null}
|
||||
<TableCell>{job.deadline ? new Date(job.deadline).toLocaleDateString(language === "nb" ? "nb-NO" : "en") : "—"}</TableCell>
|
||||
{columns.jobUrl ? <TableCell>{job.jobUrl ? <a href={job.jobUrl} target="_blank" rel="noreferrer">{t("jobTableLink")}</a> : ""}</TableCell> : null}
|
||||
<TableCell align="right">
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 0.5, whiteSpace: "nowrap" }}>
|
||||
<Tooltip title={t("jobTableEdit")}><IconButton size="small" aria-label={`${t("jobTableEdit")}: ${job.jobTitle}`} onClick={() => setEditJobId(job.id)}><EditOutlinedIcon fontSize="small" /></IconButton></Tooltip>
|
||||
<Tooltip title={t("jobTableQuickStatus")}><IconButton size="small" aria-label={`${t("jobTableQuickStatus")}: ${job.jobTitle}`} onClick={(e) => { setStatusJobId(job.id); setStatusAnchor(e.currentTarget); }}><MoreHorizIcon fontSize="small" /></IconButton></Tooltip>
|
||||
{(mode === "trash" || (includeDeleted && job.isDeleted)) ? <Tooltip title={t("jobTableRestore")}><IconButton size="small" aria-label={`${t("jobTableRestore")}: ${job.jobTitle}`} onClick={() => void restore(job.id)}><RestoreFromTrashOutlinedIcon fontSize="small" /></IconButton></Tooltip> : <Tooltip title={t("jobTableSoftDelete")}><IconButton size="small" aria-label={`${t("jobTableSoftDelete")}: ${job.jobTitle}`} onClick={() => void softDelete(job)}><DeleteOutlineIcon fontSize="small" /></IconButton></Tooltip>}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell align="right">{rowActions(job)}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
@@ -916,8 +930,13 @@ export default function JobTable({ refreshToken, pageSize, onPageSizeChange, col
|
||||
</Paper>}
|
||||
|
||||
<EditJobDialog open={editJobId !== null} jobId={editJobId} onClose={() => setEditJobId(null)} onSaved={() => setReloadToken((token) => token + 1)} />
|
||||
<Menu anchorEl={statusAnchor} open={Boolean(statusAnchor)} onClose={() => { setStatusAnchor(null); setStatusJobId(null); }}>
|
||||
{statusOptions.map((status) => <MenuItem key={status} onClick={() => { if (statusJobId) void setStatusQuick(statusJobId, status); setStatusAnchor(null); setStatusJobId(null); }}>{t("jobTableSetStatus", { status: statusLabel(t, status) })}</MenuItem>)}
|
||||
<Menu anchorEl={rowMenu?.anchor} open={Boolean(menuJob)} onClose={() => setRowMenu(null)} slotProps={{ list: { id: rowMenuId, "aria-label": t("jobTableActions") } }}>
|
||||
{menuJob && <MenuItem onClick={() => runRowAction(() => setEditJobId(menuJob.id))}>{t("jobTableEdit")}</MenuItem>}
|
||||
{menuJob && (mode === "trash" || (includeDeleted && menuJob.isDeleted)
|
||||
? <MenuItem onClick={() => runRowAction(() => void restore(menuJob.id))}>{t("jobTableRestore")}</MenuItem>
|
||||
: <MenuItem sx={{ color: "error.main" }} onClick={() => runRowAction(() => void softDelete(menuJob))}>{t("jobTableSoftDelete")}</MenuItem>)}
|
||||
{menuJob && <Divider />}
|
||||
{menuJob && statusOptions.map((status) => <MenuItem key={status} onClick={() => runRowAction(() => void setStatusQuick(menuJob.id, status))}>{t("jobTableSetStatus", { status: statusLabel(t, status) })}</MenuItem>)}
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -58,25 +58,28 @@ test("ordinary applications hide checkboxes until explicit selection mode and di
|
||||
expect(screen.getByLabelText("Current route")).toHaveTextContent("/jobs?q=platform");
|
||||
});
|
||||
|
||||
test("row status/edit/delete controls retain selection and do not open the workspace", async () => {
|
||||
test("row action menu retains selection and does not open the workspace", async () => {
|
||||
show();
|
||||
const row = await screen.findByRole("row", { name: /platform engineer/i });
|
||||
fireEvent.click(screen.getByRole("button", { name: t.jobTableSelectionMode }));
|
||||
fireEvent.click(within(row).getByRole("checkbox"));
|
||||
fireEvent.click(within(row).getByRole("button", { name: `${t.jobTableEdit}: ${job.jobTitle}` }));
|
||||
const actions = within(row).getByRole("button", { name: `${t.jobTableActions}: ${job.jobTitle}` });
|
||||
fireEvent.click(actions);
|
||||
fireEvent.click(screen.getByRole("menuitem", { name: t.jobTableEdit }));
|
||||
expect(screen.getByRole("dialog", { name: "Edit job" })).toBeVisible();
|
||||
fireEvent.click(screen.getByText("Close editor"));
|
||||
expect(within(row).getByRole("checkbox")).toBeChecked();
|
||||
fireEvent.click(within(row).getByRole("button", { name: `${t.jobTableSoftDelete}: ${job.jobTitle}` }));
|
||||
fireEvent.click(actions);
|
||||
fireEvent.click(screen.getByRole("menuitem", { name: t.jobTableSoftDelete }));
|
||||
fireEvent.click(within(await screen.findByRole("dialog")).getByRole("button", { name: /cancel/i }));
|
||||
expect(mockedApi.delete).not.toHaveBeenCalled();
|
||||
fireEvent.click(await within(row).findByRole("button", { name: `${t.jobTableQuickStatus}: ${job.jobTitle}` }));
|
||||
fireEvent.click(actions);
|
||||
fireEvent.click(await screen.findByRole("menuitem", { name: /Interview/ }));
|
||||
await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith("/jobapplications/42/status", { status: "Interview" }));
|
||||
expect(screen.getByLabelText("Current route")).toHaveTextContent(/^\/jobs$/);
|
||||
});
|
||||
|
||||
test.each(["en", "nb"] as const)("%s mobile row actions remain direct, named and independent of row navigation", async language => {
|
||||
test.each(["en", "nb"] as const)("%s mobile row menu is named and independent of row navigation", async language => {
|
||||
const originalMedia = Object.getOwnPropertyDescriptor(window, "matchMedia");
|
||||
Object.defineProperty(window, "matchMedia", { configurable: true, writable: true, value: (query: string) => ({
|
||||
matches: query.includes("max-width:767.95px"), media: query, onchange: null,
|
||||
@@ -93,14 +96,17 @@ test.each(["en", "nb"] as const)("%s mobile row actions remain direct, named and
|
||||
expect(roleTitle.compareDocumentPosition(employer) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole("button", { name: copy.jobTableSelectionMode }));
|
||||
fireEvent.click(within(row).getByRole("checkbox"));
|
||||
fireEvent.click(within(row).getByRole("button", { name: copy.jobTableEdit }));
|
||||
const actions = within(row).getByRole("button", { name: `${copy.jobTableActions}: ${job.jobTitle}` });
|
||||
fireEvent.click(actions);
|
||||
fireEvent.click(screen.getByRole("menuitem", { name: copy.jobTableEdit }));
|
||||
expect(screen.getByRole("dialog", { name: "Edit job" })).toBeVisible();
|
||||
fireEvent.click(screen.getByText("Close editor"));
|
||||
expect(within(row).getByRole("checkbox")).toBeChecked();
|
||||
fireEvent.click(within(row).getByRole("button", { name: copy.jobTableSoftDelete }));
|
||||
fireEvent.click(actions);
|
||||
fireEvent.click(screen.getByRole("menuitem", { name: copy.jobTableSoftDelete }));
|
||||
fireEvent.click(within(await screen.findByRole("dialog")).getByRole("button", { name: copy.cancel }));
|
||||
expect(mockedApi.delete).not.toHaveBeenCalled();
|
||||
fireEvent.click(await within(row).findByRole("button", { name: copy.jobTableQuickStatus }));
|
||||
fireEvent.click(actions);
|
||||
fireEvent.click(await screen.findByRole("menuitem", { name: copy.jobTableSetStatus.replace("{status}", copy.statusInterview) }));
|
||||
await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith("/jobapplications/42/status", { status: "Interview" }));
|
||||
expect(screen.getByLabelText("Current route")).toHaveTextContent("/jobs?q=platform");
|
||||
@@ -140,6 +146,15 @@ test("Trash select-all and bulk restore use the original IDs without workspace n
|
||||
expect(screen.getByLabelText("Current route")).toHaveTextContent(/^\/trash$/);
|
||||
});
|
||||
|
||||
test("Trash row menu restores the original ID and omits the delete action", async () => {
|
||||
show("trash");
|
||||
fireEvent.click(await screen.findByRole("button", { name: `${t.jobTableActions}: ${job.jobTitle}` }));
|
||||
expect(screen.queryByRole("menuitem", { name: t.jobTableSoftDelete })).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole("menuitem", { name: t.jobTableRestore }));
|
||||
await waitFor(() => expect(mockedApi.post).toHaveBeenCalledWith("/jobapplications/42/restore"));
|
||||
expect(screen.getByLabelText("Current route")).toHaveTextContent(/^\/trash$/);
|
||||
});
|
||||
|
||||
async function compact() {
|
||||
fireEvent.mouseDown(screen.getByRole("combobox", { name: t.listDensity }));
|
||||
fireEvent.click(await screen.findByRole("option", { name: t.densityCompact }));
|
||||
@@ -204,7 +219,7 @@ test("Bokmål display localizes standard statuses without changing API values or
|
||||
expect(within(row).getByText(new Date(standardJob.dateApplied).toLocaleDateString("nb-NO"))).toBeVisible();
|
||||
expect(within(row).getByText(new Date(standardJob.deadline).toLocaleDateString("nb-NO"))).toBeVisible();
|
||||
expect(screen.getByRole("combobox", { name: translations.nb.jobTableStatus })).toHaveTextContent("Custom review");
|
||||
fireEvent.click(within(row).getByRole("button", { name: `${translations.nb.jobTableQuickStatus}: ${job.jobTitle}` }));
|
||||
fireEvent.click(within(row).getByRole("button", { name: `${translations.nb.jobTableActions}: ${job.jobTitle}` }));
|
||||
fireEvent.click(await screen.findByRole("menuitem", { name: translations.nb.jobTableSetStatus.replace("{status}", translations.nb.statusOffer) }));
|
||||
await waitFor(() => expect(mockedApi.patch).toHaveBeenCalledWith("/jobapplications/42/status", { status: "Offer" }));
|
||||
});
|
||||
|
||||
@@ -198,7 +198,7 @@ test('package-work workflow signals route all overview surfaces to the shared ta
|
||||
<Route path="/jobs/:id" element={<LocationIndicator />} />
|
||||
</>);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /build package/i }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: /prepare application/i }));
|
||||
await waitFor(() => expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs/43?section=cv'));
|
||||
dashboardRender.unmount();
|
||||
|
||||
@@ -208,7 +208,7 @@ test('package-work workflow signals route all overview surfaces to the shared ta
|
||||
<Route path="/jobs/:id" element={<LocationIndicator />} />
|
||||
</>);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /build package/i }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: /prepare application/i }));
|
||||
await waitFor(() => expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs/43?section=cv'));
|
||||
remindersRender.unmount();
|
||||
|
||||
@@ -218,7 +218,7 @@ test('package-work workflow signals route all overview surfaces to the shared ta
|
||||
<Route path="/jobs/:id" element={<LocationIndicator />} />
|
||||
</>);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /platform engineer — build package signal/i }));
|
||||
fireEvent.click(await screen.findByRole('button', { name: /platform engineer — prepare application signal/i }));
|
||||
await waitFor(() => expect(screen.getByTestId('location-indicator')).toHaveTextContent('/jobs/43?section=cv'));
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user