Stabilize cross-browser verification and upgrade rehearsal
This commit is contained in:
parent
28940f9621
commit
9875fd7d05
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.");
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user