Files
kandev/apps/web/e2e/tests/task/mobile-change-workflow.spec.ts
T
2026-09-25 15:54:32 +01:00

173 lines
6.4 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import { assertNoDocumentHorizontalOverflow } from "../../helpers/layout-assertions";
import { waitForLatestSessionDone } from "../../helpers/session";
import { ChangeWorkflowPage } from "../../pages/change-workflow-page";
import { ThreadActionsPage } from "./threads-task-actions-helpers";
import {
seedWorkflowAgentOverrideFixture,
waitForNewWorkflowProfileSession,
waitForWorkflowMoveLifecycle,
waitForWorkflowStep,
} from "./task-workflow-agent-overrides-helpers";
test("changes workflow from phone task actions with task-local agent routing", async ({
testPage,
apiClient,
seedData,
prCapture,
}, testInfo) => {
test.setTimeout(180_000);
await testPage.setViewportSize({ width: 360, height: 640 });
const fixture = await seedWorkflowAgentOverrideFixture(
apiClient,
seedData,
"Change Workflow Phone",
);
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Phone change workflow task",
fixture.profileA.id,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
executor_profile_id: seedData.worktreeExecutorProfileId,
},
);
await waitForLatestSessionDone(apiClient, task.id, 1, "phone change workflow source task");
const originalSessionId = await waitForNewWorkflowProfileSession(
apiClient,
task.id,
fixture.profileA.id,
);
const before = await apiClient.getTask(task.id);
await testPage.goto(`/threads?workspace=${seedData.workspaceId}&taskId=${task.id}`);
const actions = new ThreadActionsPage(testPage, true);
await expect(actions.trigger(task.id)).toBeVisible();
await actions.expectHeaderActionAlignment(task.id);
const openChangeForm = async () => {
await actions.open(task.id);
await expect(actions.choice("Change workflow...")).toBeVisible();
await actions.pick("Change workflow...");
};
const chooseDraft = async () => {
const form = new ChangeWorkflowPage(testPage, true);
await expect(form.phoneDrawer).toBeVisible();
await form.chooseWorkflow(fixture.workflow.id);
await form.chooseStep(fixture.prStep.id);
await form.chooseProfile(fixture.profileA.id, fixture.profileB.name);
return form;
};
await openChangeForm();
const form = await chooseDraft();
await actions.contained(form.phoneDrawer);
const scroll = form.form.getByTestId("change-workflow-scroll");
await expect(scroll).toHaveCSS("overflow-y", "auto");
expect(
await form.phoneDrawer.evaluate(
(element) =>
[element, ...element.querySelectorAll("*")].filter((node) =>
/auto|scroll/.test(getComputedStyle(node).overflowY),
).length,
),
).toBe(1);
const footer = testPage.getByTestId("change-workflow-submit").locator("xpath=..");
await expect(footer).toHaveClass(/safe-area-inset-bottom/);
for (const selector of [
form.form.getByTestId("change-workflow-destination"),
form.form.getByTestId("change-workflow-step"),
form.form.getByTestId(`change-workflow-profile-selector-${fixture.profileA.id}`),
form.form.getByTestId("change-workflow-submit"),
]) {
await selector.scrollIntoViewIfNeeded();
const target = await selector.evaluate((element) => {
const rect = element.getBoundingClientRect();
return {
width: rect.width,
height: rect.height,
receivesTouch: element.contains(
document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2),
),
};
});
expect(target.width).toBeGreaterThanOrEqual(44);
expect(target.height).toBeGreaterThanOrEqual(44);
expect(target.receivesTouch).toBe(true);
}
await assertNoDocumentHorizontalOverflow(testPage, "phone change workflow form");
await scroll.evaluate((element) => element.scrollTo({ top: 0 }));
await testPage.screenshot({ path: testInfo.outputPath("phone-change-workflow-form.png") });
if (prCapture.capturing) {
await testPage.evaluate(async () => {
await Promise.all(
document
.getAnimations()
.filter((animation) => animation.playState === "running")
.map((animation) => animation.finished.catch(() => undefined)),
);
});
await prCapture.screenshot("phone-change-workflow-form", {
caption: "Phone Change workflow form with contained scrolling and a fixed submit action.",
});
}
await actions.press(form.form.getByTestId("change-workflow-cancel"));
await expect(form.phoneDrawer).toBeHidden();
await expect(actions.trigger(task.id)).toBeFocused();
await openChangeForm();
const submitForm = await chooseDraft();
await submitForm.submit();
await expect(submitForm.phoneDrawer).toBeHidden();
await waitForWorkflowStep(apiClient, task.id, fixture.prStep.id);
await waitForNewWorkflowProfileSession(apiClient, task.id, fixture.profileB.id, [
originalSessionId,
]);
await waitForWorkflowMoveLifecycle(apiClient, task.id);
await expect(actions.column(task.id)).toBeVisible();
await actions.open(task.id);
await actions.nested("Move to");
const currentDestinationStep = testPage.getByTestId(`task-context-step-${fixture.prStep.id}`);
await expect(currentDestinationStep).toBeVisible();
await expect(currentDestinationStep).toBeDisabled();
if (prCapture.capturing) {
await testPage.evaluate(async () => {
await Promise.all(
document
.getAnimations()
.filter((animation) => animation.playState === "running")
.map((animation) => animation.finished.catch(() => undefined)),
);
});
await prCapture.screenshot("phone-task-destination-step", {
caption:
"Phone task actions showing the task in its destination workflow step after migration.",
});
}
const changed = await apiClient.getTask(task.id);
expect(changed).toMatchObject({
id: before.id,
title: before.title,
description: before.description,
workflow_id: fixture.workflow.id,
workflow_step_id: fixture.prStep.id,
repositories: before.repositories,
workflow_agent_overrides: {
workflow_id: fixture.workflow.id,
steps: [
{
step_id: fixture.implementStep.id,
source_profile_id: fixture.profileA.id,
replacement_profile_id: fixture.profileB.id,
},
],
},
});
const { sessions } = await apiClient.listTaskSessions(task.id);
expect(sessions.some((session) => session.id === originalSessionId)).toBe(true);
});