mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
fix(share): the dialog says where Revoke lives, and the repro is pinned
A persona could not reach Revoke from the Share panel they opened for it,
and getByRole("button", { name: /revoke/i }) timed out against the visible
label. Reproduced: Radix's modal DialogContent calls hideOthers(), so while
the dialog is open #main carries aria-hidden and the banner's Revoke — which
does carry a matching aria-label — is in the DOM and out of the
accessibility tree. Row 1 of the spec's decision table: nothing wrong with
the banner.
So the dialog gets one sentence naming where Revoke lives, not a second
destructive control (BEA-32's invariant: exactly one Revoke per link). A new
e2e test pins both states so the recorded repro can't go stale, and the
file-page revoke path now clicks by accessible name instead of .ai-del so
the aria-label cannot regress silently.
This commit is contained in:
@@ -380,14 +380,56 @@ test("public link: the file page says it is shared, and revokes without a reload
|
||||
await expect(banner).toContainText("no expiry");
|
||||
expect((await page.request.get(url)).status()).toBe(200);
|
||||
|
||||
// Revoking from the file page kills the link and updates in place.
|
||||
await banner.locator(".ai-del").click();
|
||||
// Revoking from the file page kills the link and updates in place. Queried
|
||||
// by its accessible name (BEA-100), so the aria-label can't regress into a
|
||||
// control only a CSS selector can find.
|
||||
await page.getByRole("button", { name: /revoke/i }).click();
|
||||
await page.click(".modal .danger-btn");
|
||||
await expectToast(page, "Share revoked");
|
||||
await expect(banner).toHaveCount(0);
|
||||
expect((await page.request.get(url)).status()).toBe(404);
|
||||
});
|
||||
|
||||
// BEA-100: a persona reported the file page's Revoke as unreachable by its
|
||||
// accessible name. It is reachable — except while the Share dialog is open,
|
||||
// because Radix's modal aria-hides the rest of the page, leaving the banner's
|
||||
// button in the DOM and out of the accessibility tree. Both states are pinned
|
||||
// here so the recorded repro can't go stale, and so the answer stays a
|
||||
// sentence in the dialog rather than a second destructive control (BEA-32).
|
||||
test("revoke answers to its accessible name, and the open dialog says where it lives", async ({
|
||||
page,
|
||||
}) => {
|
||||
await login(page);
|
||||
const pid = await wikiId(page);
|
||||
const minted = await page.request.post(`/api/p/${pid}/shares`, { data: { path: "guide.md" } });
|
||||
const url = (await minted.json()).url as string;
|
||||
await page.goto(`/${pid}/guide.md`);
|
||||
const revoke = page.getByRole("button", { name: /revoke/i });
|
||||
|
||||
// Dialog closed: the banner's Revoke answers to the query that timed out.
|
||||
await expect(page.locator(".share-banner")).toBeVisible();
|
||||
await expect(revoke).toHaveCount(1);
|
||||
|
||||
// Dialog open: same query, nothing to find — the banner is still rendered,
|
||||
// its ancestor is aria-hidden. The dialog names where Revoke lives instead
|
||||
// of growing one of its own.
|
||||
await page.click("#share-btn");
|
||||
await page.waitForSelector(".modal-url");
|
||||
await expect(page.locator(".share-banner")).toHaveCount(1);
|
||||
await expect(page.locator("#main")).toHaveAttribute("aria-hidden", "true");
|
||||
await expect(revoke).toHaveCount(0);
|
||||
await expect(page.locator(".modal .ai-del")).toHaveCount(0);
|
||||
await expect(page.locator(".modal")).toContainText("Publicly shared");
|
||||
|
||||
// Closed again: reachable, and it still revokes end to end.
|
||||
await page.click(".modal button:has-text('Done')");
|
||||
await revoke.click();
|
||||
await page.click(".modal .danger-btn");
|
||||
await expectToast(page, "Share revoked");
|
||||
await expect(page.locator(".share-banner")).toHaveCount(0);
|
||||
expect((await page.request.get(url)).status()).toBe(404);
|
||||
});
|
||||
|
||||
test("project settings lists this project's public links and revokes them", async ({ page }) => {
|
||||
await login(page);
|
||||
const pid = await wikiId(page);
|
||||
|
||||
@@ -78,6 +78,12 @@ export function ShareDialog({
|
||||
<p>
|
||||
<b>Anyone with this link can view this file</b> — no account needed. It always shows the
|
||||
latest version until it expires or you revoke it.
|
||||
{/* BEA-100: the one thing a user opens this dialog for and cannot do
|
||||
here. Radix aria-hides the page behind an open modal, so the
|
||||
banner's Revoke is unreachable — to assistive tech and to the
|
||||
user — until this closes. A sentence, not a second button. */}{" "}
|
||||
To revoke it, close this — <b>Revoke</b> is on the file itself, in the “Publicly shared”
|
||||
banner.
|
||||
</p>
|
||||
<div className="modal-url">{url}</div>
|
||||
<div className="modal-expiry">
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>BearDrive</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='%23f5a623'><rect x='4' y='4' width='5.6' height='24'/><rect x='11.2' y='4' width='14.4' height='11.2'/><rect x='11.2' y='16.8' width='16.8' height='11.2'/></svg>">
|
||||
<script type="module" crossorigin src="/assets/index-Cg3Ijtd_.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BAns3BK4.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/assets/_commonjsHelpers-CqkleIqs.js">
|
||||
<link rel="modulepreload" crossorigin href="/assets/mermaid-CP2pUOT9.js">
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-aFlVpSeL.css">
|
||||
|
||||
Reference in New Issue
Block a user