183 lines
7.8 KiB
TypeScript
183 lines
7.8 KiB
TypeScript
import { test, expect } from "../../fixtures/test-base";
|
|
import { WorkflowSettingsPage } from "../../pages/workflow-settings-page";
|
|
|
|
const GITLAB_PROJECT = "platform/kandev";
|
|
|
|
const SYNCED_WORKFLOW_DESCRIPTION = "rev 1 (2026-08-08) — synced from GitLab";
|
|
|
|
const VALID_WORKFLOW_YAML = `version: 1
|
|
type: kandev_workflow
|
|
workflows:
|
|
- name: GitLab Synced Workflow
|
|
description: "${SYNCED_WORKFLOW_DESCRIPTION}"
|
|
steps:
|
|
- name: Open
|
|
position: 0
|
|
color: bg-blue-500
|
|
is_start_step: true
|
|
allow_manual_move: true
|
|
- name: Closed
|
|
position: 1
|
|
color: bg-green-500
|
|
allow_manual_move: true`;
|
|
|
|
async function openGitLabSyncTab(testPage: import("@playwright/test").Page) {
|
|
await testPage.getByTestId("workflow-sync-open").click();
|
|
const dialog = testPage.getByTestId("workflow-sync-dialog");
|
|
await expect(dialog).toBeVisible();
|
|
await dialog.getByRole("tab", { name: "GitLab" }).click();
|
|
return dialog;
|
|
}
|
|
|
|
test.describe("GitLab workflow sync", () => {
|
|
test("syncs a workflow from a seeded GitLab repo file", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
await apiClient.configureGitLab(seedData.workspaceId);
|
|
await apiClient.mockGitLabAddRepoFiles(seedData.workspaceId, GITLAB_PROJECT, "main", [
|
|
{ path: ".kandev/workflows/e2e.yaml", content: VALID_WORKFLOW_YAML },
|
|
]);
|
|
|
|
const page = new WorkflowSettingsPage(testPage);
|
|
await page.goto(seedData.workspaceId);
|
|
|
|
const dialog = await openGitLabSyncTab(testPage);
|
|
await dialog.getByTestId("workflow-sync-url-input").fill(GITLAB_PROJECT);
|
|
await dialog.getByTestId("workflow-sync-branch-input").fill("main");
|
|
await dialog.getByTestId("workflow-sync-directory-input").fill(".kandev/workflows");
|
|
await dialog.getByTestId("workflow-sync-save").click();
|
|
await expect(dialog).not.toBeVisible();
|
|
|
|
const status = testPage.getByTestId("workflow-sync-status");
|
|
await expect(status).toBeVisible();
|
|
|
|
// A successful sync that changed something calls the router's refresh(),
|
|
// which in this SPA is a literal window.location.reload() — a real
|
|
// browser navigation, not a React re-render. Start waiting for the load
|
|
// event before the click that triggers it (not after), otherwise a fast
|
|
// reload can complete before the wait begins, or a locator can be
|
|
// evaluated mid-navigation and throw "Execution context was destroyed".
|
|
await Promise.all([
|
|
testPage.waitForLoadState("load"),
|
|
status.getByTestId("workflow-sync-now").click(),
|
|
]);
|
|
|
|
// Post-reload hydration: wait for a stable element before touching the
|
|
// DOM further. The "Workflow sync completed" toast itself is transient
|
|
// and can auto-dismiss before an assertion runs, so it isn't checked
|
|
// directly — the durable signal is the synced workflow card below.
|
|
await expect(page.addWorkflowButton).toBeVisible();
|
|
|
|
// The workflow list's own data fetch can still be in flight for a
|
|
// moment after the reload finishes loading. findWorkflowCard is a
|
|
// one-shot scan with its own internal 500ms-per-card timeout, which is
|
|
// both too slow to poll with directly and (worse) can independently
|
|
// disagree with a check done a moment earlier under different timing.
|
|
// Poll for the card's own data-testid directly in one evaluateAll, so
|
|
// there is exactly one source of truth for "is it there yet" — no
|
|
// separate re-check afterward that could race against this one.
|
|
let cardTestId: string | null = null;
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
try {
|
|
cardTestId = await testPage
|
|
.locator('[data-testid^="workflow-card-"]')
|
|
.evaluateAll((elements, targetName) => {
|
|
for (const element of elements) {
|
|
const input = element.querySelector("input");
|
|
if (input && (input as HTMLInputElement).value === targetName) {
|
|
return element.getAttribute("data-testid");
|
|
}
|
|
}
|
|
return null;
|
|
}, "GitLab Synced Workflow");
|
|
} catch (err) {
|
|
// expect.poll does not retry on a thrown exception, only on a
|
|
// value mismatch — so a transient "Execution context was
|
|
// destroyed" (the reload's own document swap still settling,
|
|
// which addWorkflowButton above can observe a moment before
|
|
// evaluateAll's very next call does) must be swallowed here as
|
|
// "not found yet" rather than aborting the whole poll on its
|
|
// first tick. Any other error is a real failure and propagates.
|
|
if (err instanceof Error && err.message.includes("Execution context was destroyed")) {
|
|
return null;
|
|
}
|
|
throw err;
|
|
}
|
|
return cardTestId;
|
|
},
|
|
{ message: "waiting for the synced workflow card to render", timeout: 15_000 },
|
|
)
|
|
.not.toBeNull();
|
|
|
|
const card = testPage.getByTestId(cardTestId!);
|
|
await expect(card).toBeVisible();
|
|
await expect(card.getByText("Open")).toBeVisible();
|
|
await expect(card.getByText("Closed")).toBeVisible();
|
|
|
|
// Synced workflows are read-only: the description renders the synced
|
|
// value but must not be editable, or a later reconcile would just
|
|
// revert a user's in-UI edit.
|
|
const descriptionInput = card.getByLabel("Description", { exact: true });
|
|
await expect(descriptionInput).toHaveValue(SYNCED_WORKFLOW_DESCRIPTION);
|
|
await expect(descriptionInput).toBeDisabled();
|
|
});
|
|
|
|
test("requires a project path before Save is enabled", async ({ testPage, seedData }) => {
|
|
const page = new WorkflowSettingsPage(testPage);
|
|
await page.goto(seedData.workspaceId);
|
|
|
|
const dialog = await openGitLabSyncTab(testPage);
|
|
await expect(dialog.getByTestId("workflow-sync-save")).toBeDisabled();
|
|
|
|
await dialog.getByTestId("workflow-sync-url-input").fill(GITLAB_PROJECT);
|
|
await expect(dialog.getByTestId("workflow-sync-save")).toBeEnabled();
|
|
});
|
|
|
|
// A branch name containing a slash (a common convention, e.g.
|
|
// "features/TICKET-123") can't always be told apart from the directory
|
|
// that follows it when parsing a pasted link — this is exactly what
|
|
// broke a real config in production. The Branch/Directory fields exist
|
|
// specifically so a wrong guess is directly correctable.
|
|
test("corrects a mis-parsed slash-containing branch via the Branch field", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
await apiClient.configureGitLab(seedData.workspaceId);
|
|
|
|
const page = new WorkflowSettingsPage(testPage);
|
|
await page.goto(seedData.workspaceId);
|
|
|
|
const dialog = await openGitLabSyncTab(testPage);
|
|
await dialog
|
|
.getByTestId("workflow-sync-url-input")
|
|
.fill(`${GITLAB_PROJECT}/-/tree/features/kegmil-location-adoption/.kandev/workflows`);
|
|
|
|
// The parser can only assume a single-segment branch, so this is the
|
|
// (wrong) guess it makes.
|
|
await expect(dialog.getByTestId("workflow-sync-branch-input")).toHaveValue("features");
|
|
await expect(dialog.getByTestId("workflow-sync-directory-input")).toHaveValue(
|
|
"kegmil-location-adoption/.kandev/workflows",
|
|
);
|
|
|
|
await dialog
|
|
.getByTestId("workflow-sync-branch-input")
|
|
.fill("features/kegmil-location-adoption");
|
|
await dialog.getByTestId("workflow-sync-directory-input").fill(".kandev/workflows");
|
|
await dialog.getByTestId("workflow-sync-save").click();
|
|
await expect(dialog).not.toBeVisible();
|
|
|
|
const response = await apiClient.rawRequest(
|
|
"GET",
|
|
`/api/v1/workflow-sync/config?workspace_id=${encodeURIComponent(seedData.workspaceId)}`,
|
|
);
|
|
const config = (await response.json()) as { branch: string; path: string };
|
|
expect(config.branch).toBe("features/kegmil-location-adoption");
|
|
expect(config.path).toBe(".kandev/workflows");
|
|
});
|
|
});
|