fix(notifications): Earlier opens on Back only for a row that can be nowhere else, and Mark read reads read at once
A namesake in Earlier while the row itself was still in Needs you or Recent opened the fold, which took the place match away and left the name twice on the page: the focus fell to <body>. Row names are trimmed as Back saves them, so a title ending in a space still matches. Mark read handed the focus to a row that still said it was unread until the write landed; the row now reads read in the press's own render, and unread again if the write is refused.
This commit is contained in:
@@ -173,6 +173,48 @@ describe("NotificationsPanel · order", () => {
|
||||
).toBe("true");
|
||||
});
|
||||
|
||||
it("does not open Earlier for a namesake there while the row Back left is still in sight", async () => {
|
||||
// A video downloaded twice, a scheduler run that failed the same way again: names repeat. The
|
||||
// extra list took the place match away, the name stood twice on the page, and the focus fell to
|
||||
// <body> with Earlier open for nothing.
|
||||
const twin = { ...server(3, 9 * D, true), data: { title: "Ready 1" } } as AppNotification;
|
||||
h.server = [server(1, 1 * H, true), twin];
|
||||
h.client = [];
|
||||
const cancel = restoreNavPosition({
|
||||
list: 0,
|
||||
lists: 1,
|
||||
index: 0,
|
||||
name: "inbox.downloadDone.title: Ready 1",
|
||||
});
|
||||
mount();
|
||||
await waitFor(() =>
|
||||
expect(document.activeElement?.closest("[data-nav-index]")?.textContent).toMatch(/Ready 1/),
|
||||
);
|
||||
cancel();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "inbox.earlier:1" }).getAttribute("aria-expanded"),
|
||||
).toBe("false");
|
||||
});
|
||||
|
||||
it("opens Earlier on a row whose title ends in a space, as Back saved it trimmed", async () => {
|
||||
// A caption cut at the title's length often ends on a space.
|
||||
const spaced = { ...server(2, 9 * D, true), data: { title: "Ready 2 " } } as AppNotification;
|
||||
h.server = [server(1, 8 * D, false), spaced];
|
||||
h.client = [];
|
||||
const cancel = restoreNavPosition({
|
||||
list: 0,
|
||||
lists: 2,
|
||||
index: 0,
|
||||
name: "inbox.downloadDone.title: Ready 2",
|
||||
});
|
||||
mount();
|
||||
await loaded();
|
||||
await waitFor(() =>
|
||||
expect(document.activeElement?.closest("[data-nav-index]")?.textContent).toMatch(/Ready 2/),
|
||||
);
|
||||
cancel();
|
||||
});
|
||||
|
||||
it("numbers each list's rows from 0 for the keyboard walk", async () => {
|
||||
h.server = [server(1, 1 * H, false), server(2, 2 * H, true), server(3, 10 * D, true)];
|
||||
h.client = [client(4, 3 * H)];
|
||||
@@ -388,6 +430,37 @@ describe("NotificationsPanel · order", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("a row marked read reads read as it takes the focus, not when the write lands", async () => {
|
||||
// The button hands the focus to its row first: a screen reader announced "…, unread" right
|
||||
// after the reader marked it read.
|
||||
vi.spyOn(api, "markNotificationRead").mockImplementation(() => new Promise(() => {}));
|
||||
h.server = [server(1, 1 * H, false)];
|
||||
h.client = [];
|
||||
mount();
|
||||
await loaded();
|
||||
const row = screen.getByText("Ready 1").closest<HTMLElement>("[data-nav-index]")!;
|
||||
const button = within(row).getByRole("button", { name: "inbox.markRead" });
|
||||
button.focus();
|
||||
// Within the press's own task (the label is read out when the task ends): no render later.
|
||||
fireEvent.click(button);
|
||||
await Promise.resolve();
|
||||
expect(document.activeElement).toBe(row);
|
||||
expect(row.getAttribute("aria-label")).toBe("inbox.downloadDone.title: Ready 1");
|
||||
});
|
||||
|
||||
it("a refused Mark read brings the row back unread", async () => {
|
||||
vi.spyOn(api, "markNotificationRead").mockRejectedValue(new Error("offline"));
|
||||
h.server = [server(1, 1 * H, false)];
|
||||
h.client = [];
|
||||
mount();
|
||||
await loaded();
|
||||
const row = screen.getByText("Ready 1").closest<HTMLElement>("[data-nav-index]")!;
|
||||
fireEvent.click(within(row).getByRole("button", { name: "inbox.markRead" }));
|
||||
await waitFor(() =>
|
||||
expect(within(row).queryByRole("button", { name: "inbox.markRead" })).not.toBeNull(),
|
||||
);
|
||||
});
|
||||
|
||||
it("names each row by what it is about, so two of one kind tell apart", async () => {
|
||||
// A screen reader hears the name while the keyboard is on the row, and Back finds the row it
|
||||
// left by it: "Download ready" alone was every download's name.
|
||||
|
||||
Reference in New Issue
Block a user