fix: harden protected login and browser accessibility

This commit is contained in:
SimpleTest 2026-07-20 17:55:19 +03:00
parent 63bf7fad3b
commit 2372947430
8 changed files with 60 additions and 21 deletions

View File

@ -30,7 +30,7 @@
--color-base-100: oklch(30.33% 0.016 252.42);
--color-base-200: oklch(25.26% 0.014 253.1);
--color-base-300: oklch(20.15% 0.012 254.09);
--color-base-content: #f3f7ff;
--color-base-content: #ffffff;
--color-primary: #a5b4fc;
--color-primary-content: #18162a;
--color-secondary: #a5b4fc;
@ -128,6 +128,21 @@
color: var(--color-base-content) !important;
}
[data-theme="dark"] .btn-ghost,
[data-theme="dark"] .btn-outline {
background-color: var(--color-base-300);
}
[data-theme="dark"] .btn-soft.btn-primary {
color: var(--color-primary-content);
background-color: var(--color-primary);
}
.locale-active {
color: var(--color-neutral-content) !important;
background-color: var(--color-neutral) !important;
}
.aid-map {
min-height: 22rem;
border-radius: 1.25rem;

View File

@ -90,6 +90,7 @@ const activateProtectedTokenFragment = () => {
}
activateProtectedTokenFragment()
window.addEventListener("hashchange", activateProtectedTokenFragment)
const liveSocket = new LiveSocket("/live", Socket, {
longPollFallbackMs: 2500,

View File

@ -24,7 +24,7 @@ test("browser and Android share chat and live tracking state", async ({ browser
await expect(requester.page.getByText(androidMessage, { exact: true })).toBeVisible();
if (phase === "observe_android") {
await expect.poll(() => markerKinds(requester.page)).toContain("Shared live location");
await expect.poll(() => markerKinds(requester.page)).toContain("Shared location");
await requester.page
.getByPlaceholder("Write a safe coordination message…")
.fill(browserReply);
@ -32,7 +32,7 @@ test("browser and Android share chat and live tracking state", async ({ browser
await expect(requester.page.getByText(browserReply, { exact: true })).toBeVisible();
} else if (phase === "observe_stopped") {
await expect(requester.page.getByText(browserReply, { exact: true })).toBeVisible();
await expect.poll(() => markerKinds(requester.page)).not.toContain("Shared live location");
await expect.poll(() => markerKinds(requester.page)).not.toContain("Shared location");
} else {
throw new Error(`Unsupported CROSS_CLIENT_PHASE: ${phase}`);
}

View File

@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
const CACHE = "who-need-help-static-v3";
const CACHE_PREFIX = "who-need-help-static-";
const STALE_CACHE = "who-need-help-static-v0";
const SHELL_PATHS = [
"/offline.html",
@ -45,6 +45,21 @@ test("public HTTPS PWA installs, updates its cache, and falls back safely offlin
expect(manifestResponse.status()).toBe(200);
expect(manifestResponse.headers()["content-type"]).toContain("application/manifest+json");
const serviceWorkerResponse = await request.get("/sw.js");
expect(serviceWorkerResponse.status()).toBe(200);
expect(serviceWorkerResponse.headers()["content-type"]).toContain("javascript");
const serviceWorker = await serviceWorkerResponse.text();
const cacheVersionMatch = serviceWorker.match(
/const CACHE = `\$\{CACHE_PREFIX\}(v[0-9]+)`/,
);
expect(cacheVersionMatch).not.toBeNull();
if (!cacheVersionMatch) {
throw new Error("The service worker does not declare a versioned static cache");
}
const cacheName = `${CACHE_PREFIX}${cacheVersionMatch[1]}`;
const manifest = (await manifestResponse.json()) as {
name: string;
short_name: string;
@ -102,12 +117,12 @@ test("public HTTPS PWA installs, updates its cache, and falls back safely offlin
await expect
.poll(() => page.evaluate(() => caches.keys()))
.toContain(CACHE);
.toContain(cacheName);
const cachedShell = await page.evaluate(async (cacheName) => {
const cache = await caches.open(cacheName);
const cachedShell = await page.evaluate(async (activeCacheName) => {
const cache = await caches.open(activeCacheName);
return (await cache.keys()).map((entry) => new URL(entry.url).pathname).sort();
}, CACHE);
}, cacheName);
expect(cachedShell).toEqual(expect.arrayContaining(SHELL_PATHS));
await page.goto("/safety");
@ -146,7 +161,7 @@ test("public HTTPS PWA installs, updates its cache, and falls back safely offlin
await expect
.poll(() => updatedPage.evaluate(() => caches.keys()))
.not.toContain(STALE_CACHE);
expect(await updatedPage.evaluate(() => caches.keys())).toContain(CACHE);
expect(await updatedPage.evaluate(() => caches.keys())).toContain(cacheName);
await context.setOffline(true);
const offlineResponse = await updatedPage.goto("/requests");

View File

@ -99,7 +99,7 @@ test("active chat and browser tracking recover after the serving BEAM node resta
await expect(helper.page.getByRole("button", { name: "Stop and delete position" })).toBeVisible();
await expect.poll(() => positionCount(helper.page)).toBe(1);
await expect.poll(() => positionCount(requester.page)).toBe(1);
await expect.poll(() => markerKinds(requester.page)).toContain("Shared live location");
await expect.poll(() => markerKinds(requester.page)).toContain("Shared location");
await helper.page
.getByPlaceholder("Write a safe coordination message…")
@ -173,7 +173,7 @@ test("active chat and browser tracking recover after the serving BEAM node resta
await expect.poll(() => positionCount(helper.page), { timeout: 30_000 }).toBe(1);
await expect.poll(() => positionCount(requester.page), { timeout: 30_000 }).toBe(1);
await expect.poll(() => markerKinds(requester.page), { timeout: 30_000 }).toContain(
"Shared live location",
"Shared location",
);
await helper.page
@ -188,7 +188,7 @@ test("active chat and browser tracking recover after the serving BEAM node resta
await expect(helper.page.getByRole("button", { name: "Share location" })).toBeVisible();
await expect.poll(() => positionCount(helper.page)).toBe(0);
await expect.poll(() => positionCount(requester.page)).toBe(0);
await expect.poll(() => markerKinds(requester.page)).not.toContain("Shared live location");
await expect.poll(() => markerKinds(requester.page)).not.toContain("Shared location");
await testInfo.attach("active-failover-evidence", {
contentType: "application/json",

View File

@ -322,7 +322,13 @@ export async function registerAndConfirm(
.check();
await page.getByRole("button", { name: "Create an account" }).click();
await expect(page).toHaveURL(/\/users\/log-in$/);
await expect(page.getByText(`An email was sent to ${email}`)).toBeVisible();
await expect(
page
.getByRole("alert")
.getByText(
"If this address can be registered or signed in, login instructions will arrive shortly.",
),
).toBeVisible();
await gotoWithTransientRetry(page, await waitForMagicLink(request, email));
await expect(page.locator("#magic-link-fragment-form")).toBeVisible();
@ -357,8 +363,10 @@ export async function loginWithMagicLink(
page,
await waitForMagicLink(request, email, previousMessageID),
);
await expect(page.getByRole("heading", { name: `Welcome ${email}` })).toBeVisible();
await page.getByRole("button", { name: "Log me in only this time" }).click();
await expect(page.locator("#magic-link-fragment-form")).toBeVisible();
await page
.getByRole("button", { name: "Confirm and log in only this time" })
.click();
await expect(page.getByText(email, { exact: true })).toBeVisible();
return { context, page, email };

View File

@ -83,11 +83,11 @@ test("two users complete medicine tracking, handover, realtime chat, and blind r
});
await helper.page.getByRole("button", { name: "Share location" }).click();
await expect(helper.page.getByRole("button", { name: "Stop and delete position" })).toBeVisible();
await expect.poll(() => markerKinds(requester.page)).toContain("Shared live location");
await expect.poll(() => markerKinds(requester.page)).toContain("Shared location");
await helper.page.getByRole("button", { name: "Stop and delete position" }).click();
await expect(helper.page.getByRole("button", { name: "Share location" })).toBeVisible();
await expect.poll(() => markerKinds(requester.page)).not.toContain("Shared live location");
await expect.poll(() => markerKinds(requester.page)).not.toContain("Shared location");
await helper.page.getByRole("button", { name: "Start helping" }).click();
const handoverCode = (

View File

@ -35,7 +35,7 @@ defmodule WhoNeedHelpWeb.Layouts do
def app(assigns) do
~H"""
<header class="sticky top-0 z-30 border-b border-base-300 bg-base-100/90 px-4 py-2 backdrop-blur sm:px-6 lg:px-8">
<header class="sticky top-0 z-30 border-b border-base-300 bg-base-100 px-4 py-2 sm:px-6 lg:px-8">
<div class="mx-auto flex min-h-12 w-full max-w-7xl items-center justify-between gap-3">
<a
href="/"
@ -195,7 +195,7 @@ defmodule WhoNeedHelpWeb.Layouts do
href="?locale=en"
class={[
"btn btn-ghost btn-xs join-item",
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "en" && "btn-active"
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "en" && "btn-active locale-active"
]}
aria-label="English"
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "en" && "page"}
@ -206,7 +206,7 @@ defmodule WhoNeedHelpWeb.Layouts do
href="?locale=uk"
class={[
"btn btn-ghost btn-xs join-item",
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "uk" && "btn-active"
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "uk" && "btn-active locale-active"
]}
aria-label="Українська"
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "uk" && "page"}
@ -217,7 +217,7 @@ defmodule WhoNeedHelpWeb.Layouts do
href="?locale=ru"
class={[
"btn btn-ghost btn-xs join-item",
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "ru" && "btn-active"
Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "ru" && "btn-active locale-active"
]}
aria-label="Русский"
aria-current={Gettext.get_locale(WhoNeedHelpWeb.Gettext) == "ru" && "page"}