319 lines
13 KiB
TypeScript
319 lines
13 KiB
TypeScript
import { test, expect } from "../../fixtures/test-base";
|
|
import {
|
|
assertNoDocumentHorizontalOverflow,
|
|
assertNoElementHorizontalOverflow,
|
|
} from "../../helpers/layout-assertions";
|
|
import {
|
|
createKotlinTask,
|
|
expectFakeLspEvent,
|
|
installFakeKotlinLsp,
|
|
LONG_LSP_PROGRESS_MESSAGE,
|
|
openLspStatus,
|
|
performLspAction,
|
|
readFakeLspEvents,
|
|
releaseFakeLspInitialization,
|
|
} from "./lsp-e2e-helpers";
|
|
import { dwell } from "../../helpers/causal-waits";
|
|
import { SessionPage } from "../../pages/session-page";
|
|
|
|
function isProcessAlive(pid: number): boolean {
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code === "ESRCH") return false;
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
test.describe("Mobile LSP boundaries", () => {
|
|
test.describe.configure({ timeout: 90_000 });
|
|
|
|
test("opens Kotlin in the mobile viewer without starting a language server", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
const releaseFeature = await backend.useEnv({
|
|
KANDEV_FEATURES_LSP_BROWSER_CONTINUITY: "true",
|
|
});
|
|
const initial = await apiClient.getUserSettings();
|
|
const initialAutoStart = Array.isArray(initial.settings.lsp_auto_start_languages)
|
|
? (initial.settings.lsp_auto_start_languages as string[])
|
|
: [];
|
|
const initialLocation =
|
|
initial.settings.lsp_status_location === "status_bar" ? "status_bar" : "toolbar";
|
|
|
|
try {
|
|
installFakeKotlinLsp(backend);
|
|
await apiClient.rawRequest("PATCH", "/api/v1/user/settings", {
|
|
lsp_auto_start_languages: ["kotlin"],
|
|
lsp_status_location: "status_bar",
|
|
});
|
|
|
|
const lspSockets: string[] = [];
|
|
testPage.on("websocket", (socket) => {
|
|
if (socket.url().includes("/lsp/")) lspSockets.push(socket.url());
|
|
});
|
|
const task = await createKotlinTask(testPage, apiClient, seedData, backend, {
|
|
title: "Mobile Kotlin LSP Boundary",
|
|
});
|
|
|
|
await testPage.getByRole("button", { name: "Files", exact: true }).tap();
|
|
const fileNode = testPage.locator(
|
|
`[data-testid="file-tree-node"][data-path="${task.filePaths[0]}"]`,
|
|
);
|
|
await expect(fileNode).toBeVisible({ timeout: 15_000 });
|
|
await fileNode.tap();
|
|
|
|
const viewer = testPage.getByTestId("mobile-file-viewer-panel");
|
|
await expect(viewer).toBeVisible();
|
|
await expect(viewer.locator(".cm-editor")).toBeVisible();
|
|
await expect(viewer.getByTestId("lsp-status-button")).toHaveCount(0);
|
|
await expect(testPage.getByTestId("app-status-lsp")).toHaveCount(0);
|
|
await dwell(
|
|
testPage,
|
|
1_000,
|
|
"negative-assertion",
|
|
"asserts that no LSP socket is ever opened on mobile; a connection that must not happen publishes nothing, so the check needs the window in which it would have opened to elapse",
|
|
);
|
|
expect(lspSockets).toEqual([]);
|
|
expect(readFakeLspEvents(backend)).toEqual([]);
|
|
} finally {
|
|
await apiClient.rawRequest("PATCH", "/api/v1/user/settings", {
|
|
lsp_auto_start_languages: initialAutoStart,
|
|
lsp_status_location: initialLocation,
|
|
});
|
|
await releaseFeature();
|
|
}
|
|
});
|
|
|
|
test("shows the same project progress in a contained tablet drawer", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
const initial = await apiClient.getUserSettings();
|
|
const initialLocation =
|
|
initial.settings.lsp_status_location === "status_bar" ? "status_bar" : "toolbar";
|
|
|
|
try {
|
|
await testPage.setViewportSize({ width: 820, height: 900 });
|
|
await apiClient.rawRequest("PATCH", "/api/v1/user/settings", {
|
|
lsp_status_location: "status_bar",
|
|
});
|
|
installFakeKotlinLsp(backend, {
|
|
progress: {
|
|
title: "Importing Kotlin project",
|
|
message: LONG_LSP_PROGRESS_MESSAGE,
|
|
percentage: 42,
|
|
endMessage: "Tablet project model loaded",
|
|
},
|
|
});
|
|
const task = await createKotlinTask(testPage, apiClient, seedData, backend, {
|
|
title: "Tablet Kotlin LSP Progress",
|
|
});
|
|
await expect(testPage.getByTestId("tablet-task-layout")).toBeVisible();
|
|
const fileNode = testPage.locator(
|
|
`[data-testid="file-tree-node"][data-path="${task.filePaths[0]}"]`,
|
|
);
|
|
await expect(fileNode).toBeVisible({ timeout: 15_000 });
|
|
await fileNode.tap();
|
|
await expect(testPage.locator(".monaco-editor:visible")).toBeVisible({ timeout: 15_000 });
|
|
|
|
const statusButton = testPage.getByTestId("lsp-status-button");
|
|
await expect(statusButton).toBeVisible();
|
|
await expect(testPage.getByTestId("app-status-lsp")).toHaveCount(0);
|
|
expect((await apiClient.getUserSettings()).settings.lsp_status_location).toBe("status_bar");
|
|
const triggerBox = await statusButton.boundingBox();
|
|
expect(triggerBox?.width).toBeGreaterThanOrEqual(44);
|
|
expect(triggerBox?.height).toBeGreaterThanOrEqual(44);
|
|
await performLspAction(testPage, "start");
|
|
|
|
const drawer = await openLspStatus(testPage);
|
|
await expect(drawer).toHaveAttribute("data-testid", "lsp-status-drawer");
|
|
await expect(testPage.getByTestId("lsp-status-popover")).toHaveCount(0);
|
|
const projectProgress = drawer.getByTestId("lsp-project-progress");
|
|
await expect(projectProgress).toHaveAttribute("data-lsp-progress-kind", "active", {
|
|
timeout: 15_000,
|
|
});
|
|
await expect(projectProgress).toContainText("Importing Kotlin project");
|
|
const progressMessage = projectProgress.getByText(LONG_LSP_PROGRESS_MESSAGE, {
|
|
exact: true,
|
|
});
|
|
await expect(progressMessage).toBeVisible();
|
|
await expect(projectProgress).toContainText("42%");
|
|
|
|
const drawerBox = await drawer.boundingBox();
|
|
expect(drawerBox).not.toBeNull();
|
|
expect(drawerBox!.x).toBeGreaterThanOrEqual(0);
|
|
expect(drawerBox!.y).toBeGreaterThanOrEqual(0);
|
|
expect(drawerBox!.x + drawerBox!.width).toBeLessThanOrEqual(820);
|
|
expect(drawerBox!.y + drawerBox!.height).toBeLessThanOrEqual(900);
|
|
const actionBox = await drawer.getByTestId("lsp-lifecycle-action").boundingBox();
|
|
expect(actionBox?.height).toBeGreaterThanOrEqual(44);
|
|
const verticalScrollOwner = drawer.locator("[data-vaul-no-drag]");
|
|
await expect(verticalScrollOwner).toHaveCount(1);
|
|
await expect(verticalScrollOwner).toHaveCSS("overflow-y", "auto");
|
|
await assertNoElementHorizontalOverflow(progressMessage, "tablet LSP progress text");
|
|
await assertNoElementHorizontalOverflow(drawer, "tablet LSP progress drawer");
|
|
await assertNoDocumentHorizontalOverflow(testPage, "tablet LSP progress drawer");
|
|
|
|
releaseFakeLspInitialization(backend);
|
|
await expect(projectProgress).toHaveAttribute("data-lsp-progress-kind", "completed", {
|
|
timeout: 15_000,
|
|
});
|
|
await expect(projectProgress).toContainText("Tablet project model loaded");
|
|
await performLspAction(testPage, "stop");
|
|
} finally {
|
|
await apiClient.rawRequest("PATCH", "/api/v1/user/settings", {
|
|
lsp_status_location: initialLocation,
|
|
});
|
|
}
|
|
});
|
|
|
|
test("reattaches the retained language server in the tablet drawer", async ({
|
|
testPage,
|
|
apiClient,
|
|
seedData,
|
|
backend,
|
|
}) => {
|
|
test.setTimeout(180_000);
|
|
const releaseFeature = await backend.useEnv({
|
|
KANDEV_FEATURES_LSP_BROWSER_CONTINUITY: "true",
|
|
});
|
|
const initial = await apiClient.getUserSettings();
|
|
const initialAutoStart = Array.isArray(initial.settings.lsp_auto_start_languages)
|
|
? (initial.settings.lsp_auto_start_languages as string[])
|
|
: [];
|
|
const initialLocation =
|
|
initial.settings.lsp_status_location === "status_bar" ? "status_bar" : "toolbar";
|
|
try {
|
|
await testPage.setViewportSize({ width: 820, height: 900 });
|
|
installFakeKotlinLsp(backend, {
|
|
keepProgress: true,
|
|
progress: {
|
|
title: "Importing Kotlin project",
|
|
message: "Tablet project model is warming up",
|
|
percentage: 42,
|
|
},
|
|
});
|
|
await apiClient.rawRequest("PATCH", "/api/v1/user/settings", {
|
|
lsp_auto_start_languages: [...new Set([...initialAutoStart, "kotlin"])],
|
|
lsp_status_location: "toolbar",
|
|
});
|
|
const task = await createKotlinTask(testPage, apiClient, seedData, backend, {
|
|
title: "Tablet Kotlin LSP Continuity",
|
|
});
|
|
await expect(testPage.getByTestId("tablet-task-layout")).toBeVisible();
|
|
const fileNode = testPage.locator(
|
|
`[data-testid="file-tree-node"][data-path="${task.filePaths[0]}"]`,
|
|
);
|
|
await expect(fileNode).toBeVisible({ timeout: 15_000 });
|
|
await fileNode.tap();
|
|
await expect(testPage.locator(".monaco-editor:visible")).toBeVisible({ timeout: 15_000 });
|
|
const initialStatus = testPage.getByTestId("lsp-status-button");
|
|
await expect(initialStatus).toHaveAttribute("data-lsp-state", "ready", { timeout: 15_000 });
|
|
const started = await expectFakeLspEvent(
|
|
backend,
|
|
(event) => event.event === "started",
|
|
"tablet task-host process",
|
|
);
|
|
const initializeCount = readFakeLspEvents(backend).filter(
|
|
(event) => event.event === "message" && event.method === "initialize",
|
|
).length;
|
|
expect(initializeCount).toBe(1);
|
|
|
|
await testPage.reload();
|
|
await testPage.setViewportSize({ width: 820, height: 900 });
|
|
const reopenedSession = new SessionPage(testPage);
|
|
await reopenedSession.waitForLoad(45_000);
|
|
testPage.setDefaultTimeout(15_000);
|
|
await expect(testPage.getByTestId("tablet-task-layout")).toBeVisible();
|
|
const reopenedFileNode = testPage.locator(
|
|
`[data-testid="file-tree-node"][data-path="${task.filePaths[0]}"]`,
|
|
);
|
|
await expect(reopenedFileNode).toBeVisible({ timeout: 15_000 });
|
|
await reopenedFileNode.tap();
|
|
await expect(testPage.locator(".monaco-editor:visible")).toBeVisible({ timeout: 15_000 });
|
|
const reopenedStatus = testPage.getByTestId("lsp-status-button");
|
|
await expect(reopenedStatus).toHaveAttribute("data-lsp-state", "ready", { timeout: 15_000 });
|
|
await expect(testPage.getByTestId("app-status-lsp")).toHaveCount(0);
|
|
const drawer = await openLspStatus(testPage);
|
|
await expect(drawer).toHaveAttribute("data-testid", "lsp-status-drawer");
|
|
await expect(drawer.getByTestId("lsp-project-progress")).toContainText(
|
|
"Tablet project model is warming up",
|
|
);
|
|
expect(readFakeLspEvents(backend).filter((event) => event.event === "started")).toHaveLength(
|
|
1,
|
|
);
|
|
expect(
|
|
readFakeLspEvents(backend).filter(
|
|
(event) => event.event === "message" && event.method === "initialize",
|
|
),
|
|
).toHaveLength(initializeCount);
|
|
expect(isProcessAlive(started.pid)).toBe(true);
|
|
await performLspAction(testPage, "stop");
|
|
await expect.poll(() => isProcessAlive(started.pid)).toBe(false);
|
|
} finally {
|
|
await apiClient.rawRequest("PATCH", "/api/v1/user/settings", {
|
|
lsp_auto_start_languages: initialAutoStart,
|
|
lsp_status_location: initialLocation,
|
|
});
|
|
await releaseFeature();
|
|
}
|
|
});
|
|
|
|
test("persists Kotlin settings with usable mobile guidance", async ({ testPage, apiClient }) => {
|
|
const initial = await apiClient.getUserSettings();
|
|
const initialAutoStart = Array.isArray(initial.settings.lsp_auto_start_languages)
|
|
? (initial.settings.lsp_auto_start_languages as string[])
|
|
: [];
|
|
const initialLocation =
|
|
initial.settings.lsp_status_location === "status_bar" ? "status_bar" : "toolbar";
|
|
|
|
try {
|
|
await apiClient.rawRequest("PATCH", "/api/v1/user/settings", {
|
|
lsp_auto_start_languages: [],
|
|
lsp_status_location: "toolbar",
|
|
});
|
|
await testPage.goto("/settings/preferences/terminal-editors");
|
|
await expect(testPage.getByRole("heading", { name: "Editors", exact: true })).toBeVisible();
|
|
|
|
const kotlinCard = testPage.getByTestId("lsp-language-card-kotlin");
|
|
await expect(kotlinCard).toBeVisible();
|
|
await expect(kotlinCard.getByTestId("lsp-install-guidance-kotlin")).toContainText(
|
|
"inside the task container",
|
|
);
|
|
await expect(
|
|
testPage.getByText("the mobile file viewer does not start them", { exact: false }),
|
|
).toBeVisible();
|
|
await expect(testPage.getByRole("radio", { name: /Application status bar/ })).toBeVisible();
|
|
await expect(testPage.getByText(/touch-oriented layout is active/)).toBeVisible();
|
|
|
|
const autoStart = kotlinCard.getByTestId("lsp-auto-start-kotlin");
|
|
await expect(autoStart).not.toBeChecked();
|
|
await autoStart.tap();
|
|
const floatingSave = testPage.getByTestId("settings-floating-save");
|
|
await floatingSave.getByRole("button", { name: "Save changes" }).tap();
|
|
await expect(floatingSave).not.toBeVisible({ timeout: 15_000 });
|
|
|
|
expect(
|
|
(
|
|
(await apiClient.getUserSettings()).settings.lsp_auto_start_languages as string[]
|
|
).includes("kotlin"),
|
|
).toBe(true);
|
|
await testPage.reload();
|
|
await expect(testPage.getByTestId("lsp-auto-start-kotlin")).toBeChecked();
|
|
await assertNoDocumentHorizontalOverflow(testPage, "mobile Editors settings");
|
|
} finally {
|
|
await apiClient.rawRequest("PATCH", "/api/v1/user/settings", {
|
|
lsp_auto_start_languages: initialAutoStart,
|
|
lsp_status_location: initialLocation,
|
|
});
|
|
}
|
|
});
|
|
});
|