fix: preserve client-side navigation for task links (#3844)

This commit is contained in:
Carlos Florêncio
2026-09-21 18:22:29 +01:00
committed by GitHub
parent daccac2888
commit e1f43436b2
51 changed files with 1769 additions and 69 deletions
@@ -302,7 +302,7 @@ test("connects and browses Azure work items, PRs, and feedback", async ({
);
await taskDialog.getByTestId("submit-start-agent-chevron").click();
await testPage.getByTestId("submit-create-without-agent").click();
await expect(testPage).toHaveURL(/\/tasks\//);
await expect(testPage).toHaveURL(/\/(?:t|tasks)\//);
await testPage.goto("/azure-devops");
await testPage.getByTestId("azure-devops-work-items-mode").click();
await testPage.getByTestId("azure-devops-search-button").click();
@@ -271,7 +271,7 @@ test("mobile board opens a focused column editor without horizontal overflow", a
"Implement: Handle token rotation",
);
await testPage.getByRole("button", { name: "Create only" }).click();
await expect(testPage).toHaveURL(/\/tasks\//);
await expect(testPage).toHaveURL(/\/(?:t|tasks)\//);
await testPage.goto("/azure-devops");
await testPage.getByTestId("azure-devops-work-items-mode").click();
await testPage.getByTestId("azure-devops-mobile-filter-button").click();
@@ -0,0 +1,122 @@
// Filename starts with "mobile-" so this runs on the mobile-chrome project.
import { expect, test } from "../../fixtures/test-base";
import {
assertLocatorWithinViewportX,
assertNoDocumentHorizontalOverflow,
} from "../../helpers/layout-assertions";
test("mobile dependency links preserve the task workbench and touch layout", async ({
testPage,
apiClient,
seedData,
}) => {
const predecessor = await apiClient.createTask(
seedData.workspaceId,
"Mobile navigation predecessor",
{
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
},
);
const target = await apiClient.createTask(seedData.workspaceId, "Mobile navigation target", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
blocked_by: [predecessor.id],
});
const dependent = await apiClient.createTask(
seedData.workspaceId,
"Mobile navigation dependent",
{
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
blocked_by: [target.id],
},
);
await testPage.goto(`/t/${target.id}`);
const taskTitle = testPage.getByTestId("mobile-task-picker-trigger");
await expect(taskTitle).toContainText(target.title);
const documentRequests: string[] = [];
const onRequest = (request: import("@playwright/test").Request) => {
if (
request.isNavigationRequest() &&
request.frame() === testPage.mainFrame() &&
request.resourceType() === "document"
) {
documentRequests.push(request.url());
}
};
testPage.on("request", onRequest);
try {
await testPage.evaluate(() => {
(window as Window & { __taskLinkDocumentSentinel?: string }).__taskLinkDocumentSentinel =
"mobile-dependency-links-stay-in-spa";
});
const chip = testPage.getByTestId("task-dependency-chip");
await chip.tap();
const drawer = testPage.getByTestId("task-dependency-chip-drawer");
await expect(drawer).toBeVisible();
await expect(drawer.locator("[class*='overflow-y-auto']")).toHaveCount(1);
await assertLocatorWithinViewportX(drawer, "dependency drawer");
await assertNoDocumentHorizontalOverflow(testPage, "dependency drawer");
const entries = drawer.getByTestId("task-dependency-entry");
await expect(entries).toHaveCount(2);
for (const entry of await entries.all()) {
const box = await entry.boundingBox();
expect(box, "dependency entry must have geometry").not.toBeNull();
expect(box?.height ?? 0, "dependency entry touch target").toBeGreaterThanOrEqual(44);
await assertLocatorWithinViewportX(entry, "dependency entry");
}
const blockedBy = entries.filter({ hasText: predecessor.title });
await expect(blockedBy).toHaveAttribute("href", `/t/${predecessor.id}`);
await blockedBy.tap();
await expect(testPage).toHaveURL(new RegExp(`/t/${predecessor.id}$`));
await expect(testPage.getByTestId("mobile-task-picker-trigger")).toContainText(
predecessor.title,
);
await expect(testPage.getByTestId("task-dependency-chip-drawer")).toHaveCount(0);
expect(documentRequests).toHaveLength(0);
expect(
await testPage.evaluate(
() =>
(window as Window & { __taskLinkDocumentSentinel?: string }).__taskLinkDocumentSentinel,
),
).toBe("mobile-dependency-links-stay-in-spa");
await testPage.goBack();
await expect(testPage).toHaveURL(new RegExp(`/t/${target.id}$`));
await expect(testPage.getByTestId("mobile-task-picker-trigger")).toContainText(target.title);
expect(documentRequests).toHaveLength(0);
await chip.tap();
const reopenedDrawer = testPage.getByTestId("task-dependency-chip-drawer");
await expect(reopenedDrawer).toBeVisible();
const blocks = reopenedDrawer
.getByTestId("task-dependency-entry")
.filter({ hasText: dependent.title });
await expect(blocks).toHaveAttribute("href", `/t/${dependent.id}`);
await blocks.tap();
await expect(testPage).toHaveURL(new RegExp(`/t/${dependent.id}$`));
await expect(testPage.getByTestId("mobile-task-picker-trigger")).toContainText(dependent.title);
await expect(testPage.getByTestId("task-dependency-chip-drawer")).toHaveCount(0);
expect(documentRequests).toHaveLength(0);
expect(
await testPage.evaluate(
() =>
(window as Window & { __taskLinkDocumentSentinel?: string }).__taskLinkDocumentSentinel,
),
).toBe("mobile-dependency-links-stay-in-spa");
await assertNoDocumentHorizontalOverflow(testPage, "mobile dependency navigation");
} finally {
testPage.off("request", onRequest);
}
});
@@ -264,4 +264,95 @@ test.describe("Task dependencies", () => {
await testPage.goto(`/t/${unrelated.id}`);
await expect(testPage.getByTestId("task-dependency-chip")).toHaveCount(0);
});
test("dependency links preserve the desktop task workbench", async ({
testPage,
apiClient,
seedData,
}) => {
const predecessor = await apiClient.createTask(seedData.workspaceId, "Navigation predecessor", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
});
const target = await apiClient.createTask(seedData.workspaceId, "Navigation target", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
blocked_by: [predecessor.id],
});
const dependent = await apiClient.createTask(seedData.workspaceId, "Navigation dependent", {
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
blocked_by: [target.id],
});
await testPage.goto(`/t/${target.id}`);
const title = testPage.locator('[data-testid="task-topbar-title"]:visible');
await expect(title).toHaveText(target.title);
const documentRequests: string[] = [];
const onRequest = (request: import("@playwright/test").Request) => {
if (
request.isNavigationRequest() &&
request.frame() === testPage.mainFrame() &&
request.resourceType() === "document"
) {
documentRequests.push(request.url());
}
};
testPage.on("request", onRequest);
try {
await testPage.evaluate(() => {
(window as Window & { __taskLinkDocumentSentinel?: string }).__taskLinkDocumentSentinel =
"dependency-links-stay-in-spa";
});
const chip = testPage.getByTestId("task-dependency-chip");
await chip.click();
const blockedBy = testPage
.getByTestId("task-dependency-entry")
.filter({ hasText: predecessor.title });
await expect(blockedBy).toHaveAttribute("href", `/t/${predecessor.id}`);
await blockedBy.click();
await expect(testPage).toHaveURL(new RegExp(`/t/${predecessor.id}$`));
await expect(title).toHaveText(predecessor.title);
await expect(testPage.getByTestId("task-dependency-chip-popover")).toHaveCount(0);
expect(documentRequests).toHaveLength(0);
expect(
await testPage.evaluate(
() =>
(window as Window & { __taskLinkDocumentSentinel?: string }).__taskLinkDocumentSentinel,
),
).toBe("dependency-links-stay-in-spa");
await testPage.goBack();
await expect(testPage).toHaveURL(new RegExp(`/t/${target.id}$`));
await expect(title).toHaveText(target.title);
expect(documentRequests).toHaveLength(0);
await chip.click();
const blocks = testPage
.getByTestId("task-dependency-entry")
.filter({ hasText: dependent.title });
await expect(blocks).toHaveAttribute("href", `/t/${dependent.id}`);
await blocks.click();
await expect(testPage).toHaveURL(new RegExp(`/t/${dependent.id}$`));
await expect(title).toHaveText(dependent.title);
await expect(testPage.getByTestId("task-dependency-chip-popover")).toHaveCount(0);
expect(documentRequests).toHaveLength(0);
expect(
await testPage.evaluate(
() =>
(window as Window & { __taskLinkDocumentSentinel?: string }).__taskLinkDocumentSentinel,
),
).toBe("dependency-links-stay-in-spa");
} finally {
testPage.off("request", onRequest);
}
});
});