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:
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user