520 lines
22 KiB
TypeScript
520 lines
22 KiB
TypeScript
import { execSync } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { test, expect, type SeedData } from "../../fixtures/test-base";
|
|
import type { ApiClient } from "../../helpers/api-client";
|
|
import {
|
|
assertLocatorWithinViewportX,
|
|
assertNoDocumentHorizontalOverflow,
|
|
} from "../../helpers/layout-assertions";
|
|
import { GITLAB_HOST, GITLAB_PROJECT, gitLabMR, seedGitLabReview } from "../../helpers/gitlab";
|
|
import { makeGitEnv } from "../../helpers/git-helper";
|
|
import { GitLabPage } from "../../pages/gitlab-page";
|
|
import { GitLabSettingsPage } from "../../pages/gitlab-settings-page";
|
|
import { KanbanPage } from "../../pages/kanban-page";
|
|
import { expectContentSizedBottomConfirmation } from "../../helpers/mobile-confirmations";
|
|
import { waitForFiniteAnimations } from "../../helpers/animations";
|
|
import { SessionPage } from "../../pages/session-page";
|
|
|
|
async function expectTouchTarget(locator: ReturnType<GitLabPage["mrRow"]>, label: string) {
|
|
const box = await locator.boundingBox();
|
|
expect(box, `${label} has no bounding box`).not.toBeNull();
|
|
if (!box) return;
|
|
// Chromium can report a CSS 44px box as 43.9999 after device-scale
|
|
// conversion. Compare whole CSS pixels so floating-point noise does not
|
|
// turn a conforming touch target into a retry-only failure.
|
|
expect(Math.round(box.width), `${label} width`).toBeGreaterThanOrEqual(44);
|
|
expect(Math.round(box.height), `${label} height`).toBeGreaterThanOrEqual(44);
|
|
}
|
|
|
|
async function seedMultiRepoGitLabTask(
|
|
apiClient: ApiClient,
|
|
seedData: SeedData,
|
|
tmpDir: string,
|
|
mrIID: number,
|
|
) {
|
|
await seedGitLabReview(apiClient, seedData.workspaceId, mrIID, "Mobile contextual GitLab MR");
|
|
await apiClient.updateRepository(seedData.repositoryId, {
|
|
provider: "gitlab",
|
|
provider_host: GITLAB_HOST,
|
|
provider_owner: "platform",
|
|
provider_name: "kandev",
|
|
pull_before_worktree: false,
|
|
});
|
|
const secondaryRepoDir = path.join(tmpDir, "repos", "mobile-gitlab-secondary");
|
|
fs.mkdirSync(secondaryRepoDir, { recursive: true });
|
|
const gitEnv = makeGitEnv(tmpDir);
|
|
execSync("git init -b main", { cwd: secondaryRepoDir, env: gitEnv });
|
|
execSync('git commit --allow-empty -m "init"', { cwd: secondaryRepoDir, env: gitEnv });
|
|
const secondaryRepo = await apiClient.createRepository(
|
|
seedData.workspaceId,
|
|
secondaryRepoDir,
|
|
"main",
|
|
{
|
|
name: "Mobile GitLab secondary",
|
|
provider: "gitlab",
|
|
provider_host: GITLAB_HOST,
|
|
provider_owner: "platform",
|
|
provider_name: "docs",
|
|
pull_before_worktree: false,
|
|
},
|
|
);
|
|
return apiClient.createTask(seedData.workspaceId, "Mobile contextual GitLab link", {
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
repository_ids: [seedData.repositoryId, secondaryRepo.id],
|
|
});
|
|
}
|
|
|
|
test.describe("Mobile GitLab parity", () => {
|
|
let previousSavedPresets: unknown;
|
|
test.afterEach(async ({ apiClient }) => {
|
|
if (previousSavedPresets === undefined) return;
|
|
const restored = await apiClient.rawRequest("PATCH", "/api/v1/user/settings", {
|
|
gitlab_saved_presets: previousSavedPresets,
|
|
});
|
|
previousSavedPresets = undefined;
|
|
expect(restored.ok).toBe(true);
|
|
});
|
|
|
|
test("confirms saved-query deletion in the existing filter sheet", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}, testInfo) => {
|
|
const phoneViewport = { width: 393, height: 640 };
|
|
await testPage.setViewportSize(phoneViewport);
|
|
await seedGitLabReview(apiClient, seedData.workspaceId, 121, "Mobile saved-query review");
|
|
previousSavedPresets = (await apiClient.getUserSettings()).settings.gitlab_saved_presets ?? [];
|
|
const saved = {
|
|
id: "mobile-gitlab-saved",
|
|
kind: "mr",
|
|
label: "Release review queue",
|
|
customQuery: "release",
|
|
projectFilter: "",
|
|
milestone: "",
|
|
preset: "",
|
|
createdAt: "2026-09-10T00:00:00Z",
|
|
};
|
|
const neighbors = Array.from({ length: 8 }, (_, index) => ({
|
|
...saved,
|
|
id: `mobile-gitlab-neighbor-${index}`,
|
|
label: `Other review queue ${index}`,
|
|
}));
|
|
const seeded = await apiClient.rawRequest("PATCH", "/api/v1/user/settings", {
|
|
gitlab_saved_presets: [...neighbors, saved],
|
|
});
|
|
expect(seeded.ok).toBe(true);
|
|
const gitlab = new GitLabPage(testPage);
|
|
await gitlab.goto();
|
|
await gitlab.mobileFiltersButton.tap();
|
|
// @covers AC-UI-MOBILE-CONFIRMATION-001.8
|
|
await expect(gitlab.mobileSidebar).toHaveAttribute("data-vaul-drawer-direction", "bottom");
|
|
await waitForFiniteAnimations(gitlab.mobileSidebar);
|
|
const originalHeight = (await gitlab.mobileSidebar.boundingBox())!.height;
|
|
const sheetId = await gitlab.mobileSidebar.getAttribute("id");
|
|
const trigger = gitlab.mobileSidebar.getByTestId("gitlab-saved-delete-mobile-gitlab-saved");
|
|
await trigger.scrollIntoViewIfNeeded();
|
|
const scrollArea = gitlab.mobileSidebar.getByTestId("integration-filters-scroll");
|
|
const scrollTop = await scrollArea.evaluate((element) => element.scrollTop);
|
|
expect(scrollTop).toBeGreaterThan(0);
|
|
await testPage.screenshot({ path: testInfo.outputPath("bottom-query-filters.png") });
|
|
await trigger.tap();
|
|
const confirmation = gitlab.mobileSidebar.getByTestId("saved-task-view-delete-confirmation");
|
|
await expect(confirmation).toHaveAccessibleName("Delete Release review queue?");
|
|
const compactBounds = await expectContentSizedBottomConfirmation(
|
|
gitlab.mobileSidebar,
|
|
confirmation,
|
|
);
|
|
expect(compactBounds.height).toBeLessThan(originalHeight);
|
|
await expect(testPage.getByText("Kandev update available", { exact: true })).toBeHidden();
|
|
await testPage.screenshot({
|
|
path: testInfo.outputPath("compact-saved-query-confirmation.png"),
|
|
});
|
|
await testInfo.attach("confirmation-geometry", {
|
|
body: JSON.stringify({ originalHeight, compactBounds }),
|
|
contentType: "application/json",
|
|
});
|
|
await expect(gitlab.mobileSidebar).toHaveAttribute("id", sheetId!);
|
|
await expect(testPage.getByRole("dialog")).toHaveCount(1);
|
|
await confirmation.getByRole("button", { name: "Back" }).tap();
|
|
await expect(trigger).toBeFocused();
|
|
await waitForFiniteAnimations(gitlab.mobileSidebar);
|
|
expect((await gitlab.mobileSidebar.boundingBox())!.height).toBeCloseTo(originalHeight, 0);
|
|
expect(await scrollArea.evaluate((element) => element.scrollTop)).toBe(scrollTop);
|
|
await trigger.tap();
|
|
await expect(confirmation).toBeVisible();
|
|
await testPage.setViewportSize({ width: 768, height: 800 });
|
|
await expect(confirmation).toHaveCount(0);
|
|
await expect(gitlab.mobileSidebar).toHaveAttribute("data-side", "right");
|
|
expect((await apiClient.getUserSettings()).settings.gitlab_saved_presets).toEqual([
|
|
...neighbors,
|
|
saved,
|
|
]);
|
|
await testPage.setViewportSize(phoneViewport);
|
|
await expect(gitlab.mobileSidebar).toHaveAttribute("data-vaul-drawer-direction", "bottom");
|
|
await expect(confirmation).toHaveCount(0);
|
|
await trigger.tap();
|
|
const removed = testPage.waitForResponse(
|
|
(response) =>
|
|
response.url().includes("/api/v1/user/settings") &&
|
|
response.request().method() === "PATCH" &&
|
|
response.ok(),
|
|
);
|
|
await confirmation.getByTestId("saved-task-view-delete-confirm").tap();
|
|
await removed;
|
|
await expect(trigger).toHaveCount(0);
|
|
await expect(gitlab.mobileSidebar).toBeVisible();
|
|
expect((await apiClient.getUserSettings()).settings.gitlab_saved_presets).toEqual(neighbors);
|
|
});
|
|
test("opens the exact linked MR selected from a multi-MR topbar", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
}) => {
|
|
test.setTimeout(180_000);
|
|
const primaryIID = 109;
|
|
const selectedIID = 110;
|
|
const selectedTitle = "Selected mobile GitLab review";
|
|
await seedGitLabReview(apiClient, seedData.workspaceId, primaryIID, "Primary mobile review");
|
|
await seedGitLabReview(apiClient, seedData.workspaceId, selectedIID, selectedTitle);
|
|
await apiClient.mockGitLabAddMRs(seedData.workspaceId, GITLAB_PROJECT, [
|
|
gitLabMR(primaryIID, "Primary mobile review"),
|
|
gitLabMR(selectedIID, selectedTitle),
|
|
]);
|
|
await apiClient.updateRepository(seedData.repositoryId, {
|
|
provider: "gitlab",
|
|
provider_host: GITLAB_HOST,
|
|
provider_owner: "platform",
|
|
provider_name: "kandev",
|
|
pull_before_worktree: false,
|
|
});
|
|
|
|
const gitlab = new GitLabPage(testPage);
|
|
await gitlab.goto();
|
|
await gitlab.startMRTask(primaryIID);
|
|
const taskId = new URL(testPage.url()).pathname.match(/^\/t\/([^/]+)$/)?.[1];
|
|
if (!taskId) throw new Error(`Expected task detail URL, got ${testPage.url()}`);
|
|
await apiClient.linkTaskGitLabMR(seedData.workspaceId, {
|
|
task_id: taskId,
|
|
repository_id: seedData.repositoryId,
|
|
mr_url: `${GITLAB_HOST}/${GITLAB_PROJECT}/-/merge_requests/${selectedIID}`,
|
|
});
|
|
await testPage.reload();
|
|
await new SessionPage(testPage).waitForLoad();
|
|
|
|
await gitlab.openLinkedMR(selectedIID);
|
|
const panel = testPage.getByTestId("mr-detail-panel").last();
|
|
await expect(panel.getByText(selectedTitle, { exact: true })).toBeVisible();
|
|
const expectedReviewId = ["gitlab", GITLAB_HOST, seedData.repositoryId, String(selectedIID)]
|
|
.map(encodeURIComponent)
|
|
.join(":");
|
|
await expect
|
|
.poll(() =>
|
|
testPage.evaluate(
|
|
"window.__KANDEV_E2E_STORE__?.getState().mobileSession.reviewItemIdBySessionId[window.__KANDEV_E2E_STORE__?.getState().tasks.activeSessionId ?? '']",
|
|
),
|
|
)
|
|
.toBe(expectedReviewId);
|
|
});
|
|
|
|
test("browses, quick launches, reviews, subscribes, and unlinks without overflow", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
prCapture,
|
|
}) => {
|
|
test.setTimeout(180_000);
|
|
await seedGitLabReview(apiClient, seedData.workspaceId, 111, "Mobile GitLab review");
|
|
await apiClient.updateRepository(seedData.repositoryId, {
|
|
provider: "gitlab",
|
|
provider_host: GITLAB_HOST,
|
|
provider_owner: "platform",
|
|
provider_name: "kandev",
|
|
pull_before_worktree: false,
|
|
});
|
|
|
|
const gitlab = new GitLabPage(testPage);
|
|
await gitlab.goto();
|
|
await expect(gitlab.mobileFiltersButton).toBeVisible();
|
|
await expect(gitlab.mobileSidebar).toBeHidden();
|
|
await expectTouchTarget(gitlab.mobileFiltersButton, "GitLab filters button");
|
|
await gitlab.mobileFiltersButton.tap();
|
|
await expect(gitlab.mobileSidebar).toBeVisible();
|
|
await gitlab.mobileSidebar.evaluate(async (element) => {
|
|
await Promise.allSettled(element.getAnimations().map((animation) => animation.finished));
|
|
});
|
|
await assertLocatorWithinViewportX(gitlab.mobileSidebar, "GitLab mobile filters");
|
|
await gitlab.mobileSidebar.getByRole("button", { name: "Review requested" }).tap();
|
|
await expect(gitlab.mobileSidebar).toBeHidden();
|
|
await expect(gitlab.mrRow(111)).toContainText("Mobile GitLab review");
|
|
await assertNoDocumentHorizontalOverflow(testPage, "GitLab mobile browse");
|
|
|
|
await gitlab.startMRTask(111);
|
|
const mrButton = testPage.getByTestId("mr-topbar-button");
|
|
await expect(mrButton).toHaveAttribute("data-mr-iid", "111");
|
|
await expectTouchTarget(mrButton, "linked MR button");
|
|
await gitlab.openLinkedMR(111);
|
|
|
|
const panel = testPage.getByTestId("mr-detail-panel").last();
|
|
await expect(panel.getByText("Mobile GitLab review", { exact: true })).toBeVisible();
|
|
await assertLocatorWithinViewportX(panel, "mobile MR review panel");
|
|
await prCapture.screenshot("mobile-merge-request-review", {
|
|
caption: "Mobile GitLab merge request review with touch-sized task controls",
|
|
});
|
|
await panel.getByRole("button", { name: "Approve", exact: true }).tap();
|
|
await expect(testPage.getByText("Merge request approved", { exact: true })).toBeVisible();
|
|
await panel.getByRole("button", { name: "Subscribe to GitLab notifications" }).tap();
|
|
await expect(
|
|
testPage.getByText("Subscribed to GitLab notifications", { exact: true }),
|
|
).toBeVisible();
|
|
await assertNoDocumentHorizontalOverflow(testPage, "GitLab mobile review");
|
|
|
|
await panel.getByRole("button", { name: "Unlink merge request" }).tap();
|
|
await expect(testPage.getByTestId("mr-topbar-button")).toHaveCount(0);
|
|
await expect(testPage.getByTestId("mobile-mr-review-panel")).toHaveCount(0);
|
|
await expect(testPage.getByTestId("session-chat")).toBeVisible();
|
|
await expect(testPage.getByRole("button", { name: "Chat", exact: true })).toHaveClass(
|
|
/text-primary/,
|
|
);
|
|
await expect
|
|
.poll(() =>
|
|
testPage.evaluate(
|
|
"window.__KANDEV_E2E_STORE__?.getState().mobileSession.activePanelBySessionId[window.__KANDEV_E2E_STORE__?.getState().tasks.activeSessionId ?? '']",
|
|
),
|
|
)
|
|
.toBe("chat");
|
|
});
|
|
|
|
test("links a GitLab MR from the visible task actions menu", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
await testPage.setViewportSize({ width: 393, height: 851 });
|
|
const mrIID = 113;
|
|
const title = "Mobile contextual GitLab link";
|
|
const task = await seedMultiRepoGitLabTask(apiClient, seedData, backend.tmpDir, mrIID);
|
|
|
|
await testPage.goto(`/t/${task.id}`);
|
|
const session = new SessionPage(testPage);
|
|
await session.waitForLoad();
|
|
await expect(testPage.getByTestId("mr-topbar-button")).toHaveCount(0);
|
|
|
|
await testPage.getByTestId("mobile-task-picker-trigger").click();
|
|
const taskDrawer = testPage.getByRole("dialog", { name: "Tasks" });
|
|
const taskRow = taskDrawer.getByTestId("sidebar-task-item").filter({ hasText: title });
|
|
await expect(taskRow).toBeVisible({ timeout: 10_000 });
|
|
const taskActions = taskRow.getByRole("button", { name: "Task actions" });
|
|
await expect(taskActions).toBeVisible();
|
|
await expectTouchTarget(taskActions, "task actions button");
|
|
await taskActions.click();
|
|
|
|
const linkTrigger = testPage.getByRole("menuitem", { name: "Link", exact: true });
|
|
await expect(linkTrigger).toBeVisible();
|
|
await linkTrigger.click();
|
|
|
|
const gitLabItem = testPage.getByRole("menuitem", { name: "GitLab Merge Request" });
|
|
await expect(gitLabItem).toBeVisible();
|
|
await expect(gitLabItem).toHaveCSS("min-height", "48px");
|
|
await expectTouchTarget(gitLabItem, "GitLab merge request link action");
|
|
const nestedMenu = gitLabItem.locator("xpath=ancestor::*[@role='menu'][1]");
|
|
await nestedMenu.evaluate((element) =>
|
|
Promise.all(
|
|
element
|
|
.getAnimations({ subtree: true })
|
|
.map((animation) => animation.finished.catch(() => undefined)),
|
|
),
|
|
);
|
|
const menuBox = await nestedMenu.boundingBox();
|
|
const viewport = testPage.viewportSize();
|
|
if (!menuBox || !viewport) throw new Error("mobile GitLab link menu has no layout box");
|
|
expect(menuBox.x).toBeGreaterThanOrEqual(8);
|
|
expect(menuBox.x).toBeLessThanOrEqual(18);
|
|
expect(menuBox.width).toBeGreaterThanOrEqual(viewport.width - 36);
|
|
expect(viewport.width - (menuBox.x + menuBox.width)).toBeGreaterThanOrEqual(8);
|
|
expect(viewport.width - (menuBox.x + menuBox.width)).toBeLessThanOrEqual(18);
|
|
expect(menuBox.y).toBeGreaterThanOrEqual(0);
|
|
expect(menuBox.y + menuBox.height).toBeLessThanOrEqual(viewport.height);
|
|
expect(viewport.height - (menuBox.y + menuBox.height)).toBeGreaterThanOrEqual(8);
|
|
await assertNoDocumentHorizontalOverflow(testPage, "mobile GitLab task link menu");
|
|
|
|
await gitLabItem.click();
|
|
const linkDialog = testPage.getByRole("dialog", { name: "Link GitLab merge request" });
|
|
await expect(linkDialog).toBeVisible();
|
|
await assertLocatorWithinViewportX(linkDialog, "mobile GitLab link dialog");
|
|
await assertNoDocumentHorizontalOverflow(testPage, "mobile GitLab link dialog");
|
|
await expect(linkDialog.getByRole("combobox", { name: "Task repository" })).toContainText(
|
|
GITLAB_PROJECT,
|
|
);
|
|
await linkDialog
|
|
.getByLabel("Merge request URL")
|
|
.fill(`${GITLAB_HOST}/${GITLAB_PROJECT}/-/merge_requests/${mrIID}`);
|
|
await linkDialog.getByRole("button", { name: "Link merge request" }).click();
|
|
|
|
const mrButton = testPage.getByTestId("mr-topbar-button");
|
|
await expect(mrButton).toHaveAttribute("data-mr-iid", String(mrIID));
|
|
await expect(mrButton).toHaveAttribute("data-mr-state", "opened");
|
|
const response = await apiClient.rawRequest("GET", `/api/v1/gitlab/tasks/${task.id}/mrs`);
|
|
expect(response.ok).toBe(true);
|
|
const linked = (await response.json()) as {
|
|
task_mrs: Array<{ repository_id?: string }>;
|
|
};
|
|
expect(linked.task_mrs).toEqual([
|
|
expect.objectContaining({ repository_id: seedData.repositoryId }),
|
|
]);
|
|
});
|
|
|
|
test("watch controls remain touch sized and persist a pause", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
prCapture,
|
|
}) => {
|
|
test.setTimeout(120_000);
|
|
await apiClient.configureGitLab(seedData.workspaceId);
|
|
const watch = await apiClient.createGitLabReviewWatch({
|
|
workspace_id: seedData.workspaceId,
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
agent_profile_id: seedData.agentProfileId,
|
|
executor_profile_id: seedData.worktreeExecutorProfileId,
|
|
projects: [{ path: GITLAB_PROJECT }],
|
|
});
|
|
await expect
|
|
.poll(async () => {
|
|
const response = await apiClient.rawRequest(
|
|
"GET",
|
|
`/api/v1/gitlab/watches/review?workspace_id=${encodeURIComponent(seedData.workspaceId)}`,
|
|
);
|
|
const body = (await response.json()) as {
|
|
watches: Array<{ id: string; last_polled_at?: string }>;
|
|
};
|
|
return body.watches.find((item) => item.id === watch.id)?.last_polled_at ?? null;
|
|
})
|
|
.not.toBeNull();
|
|
await seedGitLabReview(apiClient, seedData.workspaceId, 112, "Mobile watch dispatch MR");
|
|
|
|
const settings = new GitLabSettingsPage(testPage);
|
|
await settings.goto(seedData.workspaceId);
|
|
const mobileList = settings.reviewWatches.getByTestId("gitlab-watch-mobile-list");
|
|
await expect(mobileList).toBeVisible();
|
|
await expect(mobileList.getByText(GITLAB_PROJECT, { exact: true })).toBeVisible();
|
|
const pause = mobileList.getByRole("button", { name: "Pause watch" });
|
|
const check = mobileList.getByRole("button", { name: "Check now" });
|
|
await expectTouchTarget(pause, "pause watch button");
|
|
await expectTouchTarget(check, "check watch button");
|
|
await check.tap();
|
|
await expect(testPage.getByText(/Found 1 matching merge request/)).toBeVisible();
|
|
const taskTitle = `[${GITLAB_PROJECT}!112] Mobile watch dispatch MR`;
|
|
const kanban = new KanbanPage(testPage);
|
|
await kanban.goto();
|
|
await expect(kanban.taskCardByTitle(taskTitle)).toHaveCount(1, {
|
|
timeout: 20_000,
|
|
});
|
|
await settings.goto(seedData.workspaceId);
|
|
await check.tap();
|
|
await expect(
|
|
testPage.getByText("No new merge requests matched", { exact: true }),
|
|
).toBeVisible();
|
|
await kanban.goto();
|
|
await expect(kanban.taskCardByTitle(taskTitle)).toHaveCount(1);
|
|
|
|
await settings.goto(seedData.workspaceId);
|
|
await pause.tap();
|
|
const save = testPage
|
|
.getByTestId("settings-floating-save")
|
|
.getByRole("button", { name: /save changes/i });
|
|
await expect(save).toBeEnabled();
|
|
await save.tap();
|
|
await expect(mobileList.getByText("Paused", { exact: true })).toBeVisible();
|
|
await expect(check).toBeDisabled();
|
|
await prCapture.screenshot("mobile-review-watch-paused", {
|
|
caption: "Mobile review watch remains paused after saving",
|
|
});
|
|
await assertLocatorWithinViewportX(mobileList, "mobile watch list");
|
|
await assertNoDocumentHorizontalOverflow(testPage, "GitLab mobile watch settings");
|
|
});
|
|
|
|
// @covers AC-UI-MOBILE-TASK-CHROME-001.4
|
|
test("creates and auto-links an MR with GitLab terminology", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
test.setTimeout(120_000);
|
|
const remoteURL = `${backend.baseUrl}/${GITLAB_PROJECT}.git`;
|
|
await apiClient.configureGitLab(seedData.workspaceId, backend.baseUrl);
|
|
await apiClient.configureGitLabRepositoryRemote(seedData.repositoryId, remoteURL);
|
|
await apiClient.updateRepository(seedData.repositoryId, {
|
|
provider: "gitlab",
|
|
provider_host: backend.baseUrl,
|
|
provider_owner: "platform",
|
|
provider_name: "kandev",
|
|
pull_before_worktree: false,
|
|
});
|
|
const task = await apiClient.createTaskWithAgent(
|
|
seedData.workspaceId,
|
|
"Create mobile GitLab MR",
|
|
seedData.agentProfileId,
|
|
{
|
|
description: "/e2e:diff-update-setup",
|
|
workflow_id: seedData.workflowId,
|
|
workflow_step_id: seedData.startStepId,
|
|
repository_ids: [seedData.repositoryId],
|
|
executor_profile_id: seedData.worktreeExecutorProfileId,
|
|
},
|
|
);
|
|
if (!task.session_id) throw new Error("Mobile GitLab creation task did not return a session");
|
|
|
|
await testPage.goto(`/t/${task.id}`);
|
|
const session = new SessionPage(testPage);
|
|
await session.waitForLoad();
|
|
await expect(
|
|
session.chat.getByText("diff-update-setup complete", { exact: false }),
|
|
).toBeVisible({
|
|
timeout: 45_000,
|
|
});
|
|
await testPage
|
|
.getByRole("navigation")
|
|
.getByRole("button", { name: /Changes$/ })
|
|
.tap();
|
|
const changes = testPage.getByTestId("mobile-changes-panel");
|
|
const createMR = changes.getByTestId("commits-repo-create-pr");
|
|
await expect(createMR).toBeVisible();
|
|
await createMR.tap();
|
|
const dialog = testPage.getByRole("dialog", { name: "Create merge request" });
|
|
await expect(dialog).toBeVisible();
|
|
await assertLocatorWithinViewportX(dialog, "mobile create MR dialog");
|
|
await dialog
|
|
.getByRole("textbox", { name: "Merge Request title", exact: true })
|
|
.fill("Mobile runtime-created GitLab MR");
|
|
await dialog
|
|
.getByRole("textbox", { name: "Description", exact: true })
|
|
.fill("Created from the mobile GitLab flow.");
|
|
const draft = dialog.getByLabel("Create as draft");
|
|
await expect(draft).toBeChecked();
|
|
await dialog.getByRole("button", { name: "Create MR", exact: true }).tap();
|
|
|
|
await expect
|
|
.poll(async () => {
|
|
try {
|
|
return (await apiClient.getGitLabPushRecord(seedData.repositoryId)).args;
|
|
} catch {
|
|
return "";
|
|
}
|
|
})
|
|
.toBe("push --set-upstream origin HEAD");
|
|
const mrButton = testPage.getByTestId("mr-topbar-button");
|
|
await expect(mrButton).toHaveAttribute("data-mr-iid", "100", { timeout: 120_000 });
|
|
await expectTouchTarget(mrButton, "mobile auto-linked MR");
|
|
await testPage.reload();
|
|
await expect(testPage.getByTestId("mr-topbar-button")).toHaveAttribute("data-mr-iid", "100", {
|
|
timeout: 60_000,
|
|
});
|
|
await assertNoDocumentHorizontalOverflow(testPage, "mobile created MR task");
|
|
});
|
|
});
|