fix: open agent file links that carry line-range selectors (#1412)
This commit is contained in:
@@ -312,4 +312,131 @@ test.describe("Mobile file viewer panel", () => {
|
||||
// Header must show full relative path, not just the basename.
|
||||
await expect(viewer.getByText(filePath)).toBeVisible();
|
||||
});
|
||||
|
||||
// A chat read/edit file link can target a line deep in a file. Tapping it sets
|
||||
// a pending cursor position (use-file-editors), then on mobile the file opens
|
||||
// through MobileFileViewerPanel → FileViewerContent (CodeMirror), which must
|
||||
// consume that pending entry and scroll the target line into view. Regression
|
||||
// guard for the mobile half of "open-and-scroll-to-line": before the fix
|
||||
// CodeMirror ignored the pending map and the file opened pinned at the top.
|
||||
test("tapping a chat read link scrolls the CodeMirror viewer to the target line", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
backend,
|
||||
}) => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
const fileName = `chat-read-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.ts`;
|
||||
const filePath = fileName;
|
||||
const git = new GitHelper(
|
||||
path.join(backend.tmpDir, "repos", "e2e-repo"),
|
||||
makeGitEnv(backend.tmpDir),
|
||||
);
|
||||
git.createFile(filePath, createLongFileContent(500));
|
||||
git.stageAll();
|
||||
git.commit(`add ${filePath}`);
|
||||
|
||||
const profile = await createStandardProfile(apiClient, `mobile-fv-scroll-${Date.now()}`);
|
||||
// `e2e:message(...)` emits a single non-activity text message — no thinking
|
||||
// and no tool calls — so the seeded read card below is the only activity
|
||||
// message and renders as a standalone, directly tappable card (a turn group
|
||||
// only forms with ≥2 activity messages in the same turn).
|
||||
const task = await apiClient.createTaskWithAgent(
|
||||
seedData.workspaceId,
|
||||
"Mobile FV Scroll To Line",
|
||||
profile.id,
|
||||
{
|
||||
description: 'e2e:message("ready")',
|
||||
workflow_id: seedData.workflowId,
|
||||
workflow_step_id: seedData.startStepId,
|
||||
repository_ids: [seedData.repositoryId],
|
||||
},
|
||||
);
|
||||
if (!task.session_id) throw new Error("createTaskWithAgent did not return a session_id");
|
||||
|
||||
// Seed a real read card pointing deep into the file. The chat read-message
|
||||
// component sets the pending cursor position from `offset` before opening,
|
||||
// keyed by this exact `file_path` — the same string the mobile viewer keys
|
||||
// FileViewerContent on, so the consume matches.
|
||||
const targetLine = 300;
|
||||
await apiClient.seedSessionMessage(task.session_id, {
|
||||
type: "tool_read",
|
||||
content: "Read file",
|
||||
metadata: {
|
||||
status: "complete",
|
||||
tool_call_id: "tc-chat-read-scroll",
|
||||
normalized: {
|
||||
read_file: {
|
||||
file_path: filePath,
|
||||
offset: targetLine,
|
||||
limit: 20,
|
||||
output: { content: 'export const line_299 = "line 299";', line_count: 20 },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await testPage.goto(`/t/${task.id}`);
|
||||
const session = new SessionPage(testPage);
|
||||
await session.waitForLoad();
|
||||
await session.waitForChatIdle({ timeout: 45_000 });
|
||||
|
||||
// FilePathButton renders the openable link with the seeded path as `title`.
|
||||
const chat = session.activeChat();
|
||||
const fileLink = chat.locator(`button[title="${filePath}"]`);
|
||||
await expect(fileLink).toBeVisible({ timeout: 15_000 });
|
||||
await fileLink.tap();
|
||||
|
||||
const viewer = testPage.getByTestId("mobile-file-viewer-panel");
|
||||
await expect(viewer).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
const cmScroller = viewer.locator(".cm-scroller").first();
|
||||
await expect(cmScroller).toBeVisible();
|
||||
|
||||
// FileViewerContent consumed the pending position on mount and scrolled the
|
||||
// target line to centre — the scroller is no longer pinned at the top.
|
||||
await expect
|
||||
.poll(async () => cmScroller.evaluate((element) => element.scrollTop), { timeout: 10_000 })
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
// The target line's gutter number is rendered and on-screen, proving the
|
||||
// scroll landed on the requested line rather than an arbitrary offset.
|
||||
await expect(
|
||||
viewer.locator(".cm-lineNumbers .cm-gutterElement", { hasText: "300" }),
|
||||
).toBeInViewport();
|
||||
});
|
||||
|
||||
// Opening a file with NO pending cursor position (normal file browsing) must
|
||||
// not scroll — the CodeMirror viewer stays pinned at the top.
|
||||
test("opening a file without a pending cursor position stays at the top", async ({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
backend,
|
||||
}) => {
|
||||
test.setTimeout(90_000);
|
||||
|
||||
const { filePath } = await setupMobileFileViewerTest({
|
||||
testPage,
|
||||
apiClient,
|
||||
seedData,
|
||||
backend,
|
||||
taskTitle: "Mobile FV No Scroll",
|
||||
options: { extension: "ts", content: createLongFileContent(500) },
|
||||
});
|
||||
|
||||
await testPage.getByRole("button", { name: "Files" }).tap();
|
||||
const fileNode = testPage.locator(`[data-testid="file-tree-node"][data-path="${filePath}"]`);
|
||||
await expect(fileNode).toBeVisible({ timeout: 15_000 });
|
||||
await fileNode.tap();
|
||||
|
||||
const viewer = testPage.getByTestId("mobile-file-viewer-panel");
|
||||
await expect(viewer).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
const cmScroller = viewer.locator(".cm-scroller").first();
|
||||
await expect(cmScroller).toBeVisible();
|
||||
// No pending entry → no scroll-to-line dispatch → scroller stays at the top.
|
||||
expect(await cmScroller.evaluate((element) => element.scrollTop)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user