Stabilize cross-browser verification and upgrade rehearsal

This commit is contained in:
SimpleTest 2026-07-26 00:20:25 +03:00
parent 28940f9621
commit 9875fd7d05
7 changed files with 124 additions and 21 deletions

View File

@ -753,7 +753,7 @@ html {
} }
.category-picker-placeholder { .category-picker-placeholder {
color: color-mix(in oklab, var(--color-base-content) 48%, transparent); color: color-mix(in oklab, var(--color-base-content) 70%, var(--color-base-100));
} }
.category-picker-panel { .category-picker-panel {

View File

@ -2115,6 +2115,7 @@ export const Hooks = {
this.mountMap = () => { this.mountMap = () => {
if (!this.mapElement) return if (!this.mapElement) return
this.mapElement.dataset.mapIdle = "false"
const support = mapCanvasSupport() const support = mapCanvasSupport()
if (!support.supported) { if (!support.supported) {
@ -2149,6 +2150,12 @@ export const Hooks = {
this.map.touchZoomRotate.disableRotation() this.map.touchZoomRotate.disableRotation()
this.map.addControl(new maplibregl.NavigationControl({showCompass: false}), "top-left") this.map.addControl(new maplibregl.NavigationControl({showCompass: false}), "top-left")
this.map.on("dataloading", () => {
if (this.mapElement) this.mapElement.dataset.mapIdle = "false"
})
this.map.on("idle", () => {
if (this.mapElement) this.mapElement.dataset.mapIdle = "true"
})
this.map.on("load", () => { this.map.on("load", () => {
this.mapLoaded = true this.mapLoaded = true

View File

@ -91,7 +91,9 @@ test("public navigation does not overflow narrow or wide viewports", async ({ br
await expect(page.getByRole("link", { name: "Activities", exact: true })).toBeVisible(); await expect(page.getByRole("link", { name: "Activities", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "Categories", exact: true })).toBeVisible(); await expect(page.getByRole("link", { name: "Categories", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "Helpers", exact: true })).toBeVisible(); await expect(page.getByRole("link", { name: "Helpers", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "English", exact: true })).toBeVisible(); await expect(
page.locator("summary[aria-label='Language: English']:visible"),
).toBeVisible();
await expect(page.getByRole("link", { name: "Log in", exact: true })).toBeVisible(); await expect(page.getByRole("link", { name: "Log in", exact: true })).toBeVisible();
const openDimensions = await page.evaluate(() => ({ const openDimensions = await page.evaluate(() => ({
@ -103,7 +105,9 @@ test("public navigation does not overflow narrow or wide viewports", async ({ br
} else { } else {
await expect(page.locator("summary[aria-label='Primary navigation']")).toBeHidden(); await expect(page.locator("summary[aria-label='Primary navigation']")).toBeHidden();
await expect(page.getByRole("link", { name: "Requests", exact: true })).toBeVisible(); await expect(page.getByRole("link", { name: "Requests", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "English", exact: true })).toBeVisible(); await expect(
page.locator("summary[aria-label='Language: English']:visible"),
).toBeVisible();
await expect(page.getByRole("link", { name: "Register", exact: true })).toBeVisible(); await expect(page.getByRole("link", { name: "Register", exact: true })).toBeVisible();
await expect(page.getByRole("link", { name: "Log in", exact: true })).toBeVisible(); await expect(page.getByRole("link", { name: "Log in", exact: true })).toBeVisible();
} }

View File

@ -157,8 +157,22 @@ test("activity approval, privacy controls, reporting, and moderation work end to
await waitForMapReady(participant.page); await waitForMapReady(participant.page);
await gotoLiveView(participant.page, "/categories/proposals"); await gotoLiveView(participant.page, "/categories/proposals");
await participant.page.getByLabel("Proposed category").fill(proposalTitle); await participant.page
await participant.page.getByLabel("Mode").selectOption("activity"); .getByLabel("Where should it appear?")
.selectOption("activity");
await participant.page
.locator("label.category-proposal-kind")
.filter({ hasText: "New subcategory" })
.click();
await expect(
participant.page.getByRole("radio", { name: /New subcategory/ }),
).toBeChecked();
await selectOptionContaining(
participant.page,
"Main category",
"Social activity",
);
await participant.page.getByLabel("Subcategory name").fill(proposalTitle);
await participant.page await participant.page
.getByLabel("Why is this useful?") .getByLabel("Why is this useful?")
.fill("A moderated public activity category for quiet board-game meetups."); .fill("A moderated public activity category for quiet board-game meetups.");
@ -247,8 +261,17 @@ test("activity approval, privacy controls, reporting, and moderation work end to
await expect(admin.page.getByText("User status updated.")).toBeVisible(); await expect(admin.page.getByText("User status updated.")).toBeVisible();
await gotoLiveView(participant.page, "/categories/proposals"); await gotoLiveView(participant.page, "/categories/proposals");
await participant.page.getByLabel("Proposed category").fill("E2E restricted proposal"); await participant.page
await participant.page.getByLabel("Mode").selectOption("help"); .getByLabel("Where should it appear?")
.selectOption("help");
await participant.page
.locator("label.category-proposal-kind")
.filter({ hasText: "New main category" })
.click();
await expect(
participant.page.getByRole("radio", { name: /New main category/ }),
).toBeChecked();
await participant.page.getByLabel("Category name").fill("E2E restricted proposal");
await participant.page await participant.page
.getByLabel("Why is this useful?") .getByLabel("Why is this useful?")
.fill("This must be rejected because the account is restricted."); .fill("This must be rejected because the account is restricted.");

View File

@ -454,9 +454,14 @@ export async function loginWithPassword(
const form = page.locator("#login_form_password"); const form = page.locator("#login_form_password");
await page.getByText("Use a password instead", { exact: true }).click(); await page.getByText("Use a password instead", { exact: true }).click();
await expect(form).toBeVisible(); await expect(form).toBeVisible();
await form.getByLabel("Email").fill(email); const emailInput = form.getByLabel("Email");
await form.getByLabel("Password").fill(password); await emailInput.click();
await expect(form.getByLabel("Password")).toHaveValue(password); await emailInput.pressSequentially(email);
await expect(emailInput).toHaveValue(email);
const passwordInput = form.getByLabel("Password");
await passwordInput.click();
await passwordInput.pressSequentially(password);
await expect(passwordInput).toHaveValue(password);
await form.getByRole("button", { name: "Log in only this time" }).click(); await form.getByRole("button", { name: "Log in only this time" }).click();
await expect(page).not.toHaveURL(/\/users\/log-in$/, { timeout: 10_000 }); await expect(page).not.toHaveURL(/\/users\/log-in$/, { timeout: 10_000 });
await expect(page.getByText(email, { exact: true })).toBeVisible({ await expect(page.getByText(email, { exact: true })).toBeVisible({
@ -493,18 +498,27 @@ export async function selectOptionContaining(
expectedText: string, expectedText: string,
): Promise<void> { ): Promise<void> {
await waitForLiveViewConnected(page); await waitForLiveViewConnected(page);
const select = page.getByLabel(label); const picker = page
const options = await select.locator("option").evaluateAll((nodes) => .locator(".category-picker")
nodes.map((node) => ({ .filter({
text: node.textContent?.trim() ?? "", has: page.locator(":scope > .label", { hasText: label }),
value: (node as HTMLOptionElement).value, })
})), .first();
); const source = picker.locator("[data-category-picker-source]");
const option = options.find(({ text }) => text.includes(expectedText)); const option = picker
.locator("[data-category-picker-option]")
.filter({ hasText: expectedText })
.first();
if (!option) { await expect(picker, `No category picker labelled ${label}`).toBeVisible();
throw new Error(`No ${label} option contains ${expectedText}`); await picker.locator("[data-category-picker-trigger]").click();
await expect(option, `No ${label} option contains ${expectedText}`).toBeVisible();
const value = await option.getAttribute("data-value");
if (!value) {
throw new Error(`${label} option ${expectedText} has no value`);
} }
await select.selectOption(option.value); await option.click();
await expect(source).toHaveValue(value);
} }

View File

@ -59,6 +59,24 @@ test("nearby alerts, private notification inbox, preferences, and data export wo
await subscriptionForm await subscriptionForm
.locator('input[name="nearby_subscription[radius_meters]"][value="3000"]') .locator('input[name="nearby_subscription[radius_meters]"][value="3000"]')
.check(); .check();
await expect
.poll(
async () => {
const map = subscriber.page.locator("#nearby-subscription-map");
if ((await map.getAttribute("data-map-idle")) === "true") {
return "idle";
}
if ((await map.locator("[data-map-fallback=true]").count()) === 1) {
return "fallback";
}
return null;
},
{ timeout: 15_000 },
)
.not.toBeNull();
await subscriptionForm.getByLabel("Email notification").check(); await subscriptionForm.getByLabel("Email notification").check();
await subscriptionForm.getByRole("button", { name: "Create nearby alert" }).click(); await subscriptionForm.getByRole("button", { name: "Create nearby alert" }).click();

View File

@ -148,6 +148,10 @@ cleanup() {
if [ "$status" -ne 0 ]; then if [ "$status" -ne 0 ]; then
compose ps --all >"$output_dir/compose-ps.txt" 2>&1 || true compose ps --all >"$output_dir/compose-ps.txt" 2>&1 || true
compose logs --no-color proxy \
>"$output_dir/proxy.log" 2>&1 || true
compose logs --no-color docker-api-proxy \
>"$output_dir/docker-api-proxy.log" 2>&1 || true
fi fi
if ! compose down --volumes --remove-orphans >/dev/null 2>&1; then if ! compose down --volumes --remove-orphans >/dev/null 2>&1; then
@ -349,6 +353,39 @@ if [ -z "$web_container" ]; then
exit 1 exit 1
fi fi
# Docker Compose considers a running proxy without a container healthcheck
# ready before Traefik necessarily applies its first Docker-provider snapshot.
# Wait for the application route itself so the assertions below measure the
# configured router rather than that short provider-initialisation window.
route_wait_attempts=30
route_wait_attempt=1
route_status=000
while [ "$route_wait_attempt" -le "$route_wait_attempts" ]; do
route_status=$(
curl \
--silent \
--show-error \
--output /dev/null \
--write-out '%{http_code}' \
--header "Host: $PHX_HOST" \
"http://127.0.0.1:$proxy_port/healthz/ready" ||
printf '000'
)
if [ "$route_status" = "200" ]; then
break
fi
sleep 1
route_wait_attempt=$((route_wait_attempt + 1))
done
if [ "$route_status" != "200" ]; then
echo "The isolated proxy route did not become ready after $route_wait_attempts checks; last HTTP status: $route_status." >&2
exit 1
fi
case "$PHX_SCHEME:$PHX_URL_PORT" in case "$PHX_SCHEME:$PHX_URL_PORT" in
http:80 | https:443) http:80 | https:443)
public_origin="$PHX_SCHEME://$PHX_HOST" public_origin="$PHX_SCHEME://$PHX_HOST"