fix: preserve client-side navigation for task links (#3844)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user