test(e2e): cover the Docker build gate at phone viewport (#3041)

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
João Salavisa
2026-08-25 20:10:02 +01:00
committed by GitHub
parent d5e4aed75e
commit 700efa9028
3 changed files with 115 additions and 17 deletions
+50
View File
@@ -7,6 +7,12 @@ type E2EStoreWindow = Window & {
tasks: { activeSessionId: string | null };
sessionAgentctl: { itemsBySessionId: Record<string, { status?: string }> };
setAvailableCommands: (sessionId: string, commands: AvailableCommand[]) => void;
setAuthState: (state: {
mode: string;
authenticated: boolean;
user: StoreUser;
ssoProviders: unknown[];
}) => void;
};
setState: (
updater: (state: {
@@ -16,12 +22,56 @@ type E2EStoreWindow = Window & {
};
};
type StoreUser = {
id: string;
email: string;
display_name: string;
role: string;
status: string;
};
type AvailableCommand = {
name: string;
description?: string;
input_hint?: string;
};
/**
* Inject an authenticated identity through the store bridge.
*
* E2E runs with authentication disabled, which leaves the role undefined and
* every role-gated control in its permissive single-user state. Specs that
* need to prove a member/admin difference set the identity directly rather
* than standing up a real login, matching what the system settings specs do.
*/
export async function setStoreRole(
page: Page,
role: "member" | "admin",
overrides: Partial<StoreUser> = {},
): Promise<void> {
await page.waitForFunction(() => Boolean((window as E2EStoreWindow).__KANDEV_E2E_STORE__));
await page.evaluate(
({ role, overrides }) => {
const store = (window as E2EStoreWindow).__KANDEV_E2E_STORE__;
if (!store) throw new Error("E2E store bridge is unavailable");
store.getState().setAuthState({
mode: "enabled",
authenticated: true,
user: {
id: `e2e-${role}`,
email: `${role}@e2e.dev`,
display_name: role === "admin" ? "E2E Admin" : "E2E Member",
role,
status: "active",
...overrides,
},
ssoProviders: [],
});
},
{ role, overrides },
);
}
/** Wait until the session agentctl is ready for controls that require it. */
export async function waitForSessionAgentctlReady(
page: Page,
@@ -1,4 +1,5 @@
import { test, expect } from "../../fixtures/test-base";
import { setStoreRole } from "../../helpers/session-store";
/**
* Regression test for the Docker executor profile UI persisting Dockerfile
@@ -220,23 +221,7 @@ test.describe("Docker executor profile persistence", () => {
const buildButton = testPage.getByRole("button", { name: "Build Image" });
await expect(buildButton).toBeEnabled();
await testPage.waitForFunction(() => Boolean(window.__KANDEV_E2E_STORE__));
await testPage.evaluate(() => {
const store = window.__KANDEV_E2E_STORE__;
if (!store) throw new Error("E2E store bridge is unavailable");
store.getState().setAuthState({
mode: "enabled",
authenticated: true,
user: {
id: "e2e-member",
email: "member@e2e.dev",
display_name: "E2E Member",
role: "member",
status: "active",
},
ssoProviders: [],
});
});
await setStoreRole(testPage, "member");
await expect(buildButton).toBeDisabled();
await expect(testPage.getByText("Only administrators can build images.")).toBeVisible();
@@ -0,0 +1,63 @@
import { test, expect } from "../../fixtures/test-base";
import { setStoreRole } from "../../helpers/session-store";
/**
* Mobile parity for the admin gating on the Dockerfile build control.
*
* The desktop coverage lives in docker-profile-persistence.spec.ts, which the
* `chromium` project runs at desktop viewport; only `mobile-*.spec.ts` files
* reach the Pixel 5 `mobile-chrome` project. The gating swaps the build status
* badge for a sentence of explanation inside a horizontal flex row, so the
* narrow viewport is where that row can overflow, and it is asserted here
* rather than assumed from the desktop run.
*
* The security boundary is the backend's authn.RequireAdmin() on
* POST /api/v1/docker/build; this covers the UI not offering a control that
* can only 403.
*/
test.describe("Docker build permissions on mobile", () => {
test("member sees the build control disabled and readable at phone width", async ({
testPage,
}) => {
await testPage.goto("/settings/executors/new/local_docker");
await expect(testPage.locator("#profile-name")).toHaveValue("Docker", { timeout: 10_000 });
await testPage.getByRole("button", { name: "Use defaults" }).click();
const buildButton = testPage.getByRole("button", { name: "Build Image" });
await expect(buildButton).toBeEnabled();
await setStoreRole(testPage, "member");
await expect(buildButton).toBeDisabled();
const explanation = testPage.getByText("Only administrators can build images.");
await expect(explanation).toBeVisible();
// The explanation must sit inside the viewport, not run off the side of
// the flex row it shares with the button.
const viewportWidth = testPage.viewportSize()?.width ?? 0;
expect(viewportWidth).toBeGreaterThan(0);
const explanationBox = await explanation.boundingBox();
expect(explanationBox).not.toBeNull();
expect(explanationBox!.x).toBeGreaterThanOrEqual(0);
expect(explanationBox!.x + explanationBox!.width).toBeLessThanOrEqual(viewportWidth);
// And it must not introduce document-level horizontal scrolling. The DOM
// guarantees scrollWidth >= clientWidth, so the only passing value is 0;
// asserting that exactly keeps the failure message readable.
const horizontalOverflow = await testPage.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth,
);
expect(horizontalOverflow).toBe(0);
});
test("admin keeps the build control usable at phone width", async ({ testPage }) => {
await testPage.goto("/settings/executors/new/local_docker");
await expect(testPage.locator("#profile-name")).toHaveValue("Docker", { timeout: 10_000 });
await testPage.getByRole("button", { name: "Use defaults" }).click();
await setStoreRole(testPage, "admin");
await expect(testPage.getByRole("button", { name: "Build Image" })).toBeEnabled();
await expect(testPage.getByText("Only administrators can build images.")).toHaveCount(0);
});
});