Files
kandev/apps/web/e2e/tests/settings/mobile-kubernetes-task-environment.spec.ts
T

200 lines
8.1 KiB
TypeScript

import path from "node:path";
import type { Locator } from "@playwright/test";
import { expect, test } from "../../fixtures/test-base";
import { assertNoDocumentHorizontalOverflow } from "../../helpers/layout-assertions";
import { SessionPage } from "../../pages/session-page";
import { seedKubernetesTaskEnvironment } from "./kubernetes-task-environment-helpers";
const POD_NAME = "kandev-mobile-task-pod";
async function expectTouchLocator(locator: Locator, label: string) {
await expect(locator).toBeVisible();
const box = await locator.boundingBox();
expect(box, `${label} must have geometry`).not.toBeNull();
expect(box!.height, `${label} must be at least 44px tall`).toBeGreaterThanOrEqual(44);
}
async function expectExpandedTouchTarget(locator: Locator, label: string) {
await expect(locator).toBeVisible();
const size = await locator.evaluate((element) => {
const style = getComputedStyle(element, "::after");
return { height: Number.parseFloat(style.height), width: Number.parseFloat(style.width) };
});
expect(size.height, `${label} must be at least 44px tall`).toBeGreaterThanOrEqual(44);
expect(size.width, `${label} must be at least 44px wide`).toBeGreaterThanOrEqual(44);
}
test("Kubernetes task disclosure exposes live Pod details and safe actions by touch", async ({
apiClient,
backend,
seedData,
testPage,
}) => {
const executor = await apiClient.createExecutor("Mobile task Kubernetes", "k8s", {
auth_mode: "in_cluster",
kubeconfig_path: "",
namespace: "default",
request_timeout_seconds: "1",
});
const profile = await apiClient.createExecutorProfile(executor.id, {
name: "Mobile task Kubernetes profile",
config: {
platform: "linux/amd64",
main_container: "kandev-agent",
pod_template_yaml:
"apiVersion: v1\nkind: PodTemplate\ntemplate:\n spec:\n containers:\n - name: kandev-agent\n image: ghcr.io/kdlbs/kandev:latest\n",
"workspace.mode": "empty_dir",
},
prepare_script: "",
cleanup_script: "",
env_vars: [],
});
let taskId = "";
let navigationTaskId = "";
let sessionReads = 0;
let statusUnauthorized = true;
try {
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Mobile Kubernetes task disclosure",
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
executor_id: executor.id,
executor_profile_id: profile.id,
},
);
taskId = task.id;
expect(task.session_id).toBeTruthy();
const sessionId = task.session_id!;
seedKubernetesTaskEnvironment(
path.join(backend.tmpDir, "kandev.db"),
task.id,
sessionId,
executor.id,
profile.id,
);
const navigationTask = await apiClient.seedTask(
seedData.workspaceId,
"Mobile executor disclosure navigation",
{
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
},
);
navigationTaskId = navigationTask.task_id;
await apiClient.seedTaskSession(navigationTaskId, {
state: "WAITING_FOR_INPUT",
agentProfileId: seedData.agentProfileId,
});
await testPage.route(
`**/api/v1/kubernetes/executors/${executor.id}/sessions?*`,
async (route) => {
const query = new URL(route.request().url()).searchParams;
if (query.get("task_id") !== task.id || query.get("session_id") !== sessionId) {
await route.continue();
return;
}
sessionReads += 1;
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([
{
task_id: task.id,
session_id: sessionId,
pod_name: POD_NAME,
pod_phase: "Running",
container_state: "running",
restarts: 0,
failure_reason: statusUnauthorized ? "Unauthorized" : undefined,
workspace_kind: "empty_dir",
created_at: "2026-08-25T10:00:00Z",
},
]),
});
},
);
await testPage.clock.install();
await testPage.goto(`/t/${navigationTaskId}`);
await new SessionPage(testPage).waitForLoad(30_000);
await testPage.getByTestId("mobile-session-menu").tap();
const taskSwitcher = testPage.getByRole("dialog", { name: "Tasks" });
const taskRow = taskSwitcher.locator(`[data-task-row-id="${task.id}"]`);
const statusTrigger = taskRow.getByTestId("remote-executor-status-trigger");
await expect.poll(() => sessionReads).toBeGreaterThan(0);
await expect(statusTrigger).toHaveClass(/text-destructive/);
statusUnauthorized = false;
const beforeRefresh = sessionReads;
await testPage.clock.fastForward(90_001);
await expect.poll(() => sessionReads).toBeGreaterThan(beforeRefresh);
await expect(statusTrigger).toHaveClass(/text-emerald-500/);
await expect(statusTrigger).toHaveAttribute("aria-haspopup", "dialog");
await expectExpandedTouchTarget(statusTrigger, "Mobile task executor status action");
await statusTrigger.tap();
const statusDrawer = testPage.getByTestId("remote-executor-status-drawer");
await expect(statusDrawer).toBeVisible();
await expect(statusDrawer.getByTestId("remote-executor-status-summary")).toBeVisible();
await expect(statusDrawer.getByTestId("remote-executor-status-identity")).toHaveText(POD_NAME);
await expect(statusDrawer.getByTestId("remote-executor-status-state")).toContainText("running");
await expect(statusDrawer.getByTestId("remote-executor-status-restarts")).toContainText("0");
await expect(testPage).toHaveURL(new RegExp(`/t/${navigationTaskId}$`));
await statusDrawer.getByRole("button", { name: "Close" }).tap();
await expect(statusDrawer).toBeHidden();
await expect(statusTrigger).toBeFocused();
await assertNoDocumentHorizontalOverflow(testPage);
await expect(testPage).toHaveURL(new RegExp(`/t/${navigationTaskId}$`));
await testPage.goto(`/t/${task.id}`);
await new SessionPage(testPage).waitForLoad(30_000);
const trigger = testPage.getByTestId("executor-settings-button");
await expect(trigger).toBeVisible();
await expect(trigger).toHaveAccessibleName(/executor settings/i);
const triggerBox = await trigger.boundingBox();
expect(triggerBox).not.toBeNull();
expect(triggerBox!.height).toBeGreaterThanOrEqual(44);
expect(triggerBox!.width).toBeGreaterThanOrEqual(44);
await trigger.tap();
const drawer = testPage.getByTestId("executor-settings-drawer");
await expect(drawer).toBeVisible();
await expect(drawer.getByTestId("kubernetes-environment-summary")).toBeVisible();
await expect(drawer.locator("svg.tabler-icon-package")).toBeVisible();
await expect(drawer).toContainText(POD_NAME);
await expect(drawer).toContainText("Running");
await expect(drawer).toContainText("empty_dir");
await expect(drawer).not.toContainText("No resource details available.");
await expect(drawer.getByTestId("kubernetes-restart-count")).toHaveText("0");
await expectTouchLocator(
drawer.getByRole("button", { name: "Copy Pod" }),
"Mobile Pod copy button",
);
await expectTouchLocator(
drawer.getByTestId("executor-settings-refresh"),
"Mobile Kubernetes refresh action",
);
await expect(drawer.getByTestId("executor-settings-reset")).toHaveCount(0);
await expect(drawer.getByTestId("executor-settings-link")).toHaveAttribute(
"href",
`/settings/executors/${profile.id}`,
);
await expectTouchLocator(
drawer.getByTestId("executor-settings-link"),
"Mobile Kubernetes settings action",
);
await assertNoDocumentHorizontalOverflow(testPage, "mobile Kubernetes task disclosure");
} finally {
if (navigationTaskId) await apiClient.deleteTask(navigationTaskId).catch(() => undefined);
if (taskId) await apiClient.deleteTask(taskId).catch(() => undefined);
await apiClient.deleteExecutorProfile(profile.id).catch(() => undefined);
await apiClient.deleteExecutor(executor.id).catch(() => undefined);
}
});