mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-14 16:07:52 +00:00
The Services tool window listed services nobody added: an injected Docker connection, Spring Boot by default and every configuration that had ever run. Now only added types, connections and registries are listed, removal takes effect at once, an untouched injected Docker connection is dropped from older docker.json files, and the detail pane has a single scrollbar with console controls only (Run/Debug/Stop stay on the window toolbar). Co-authored-by: qtvbwfn <5848026+qtvbwfn@users.noreply.github.com>
199 lines
9.3 KiB
TypeScript
199 lines
9.3 KiB
TypeScript
import type { Page } from "@playwright/test";
|
|
import { expect, test } from "./fixtures";
|
|
|
|
/** Supply detected configurations without launching desktop processes. */
|
|
async function configure(page: Page, extra = 0) {
|
|
await page.evaluate(async (extra) => {
|
|
const storePath = "/src/features/run/store.ts";
|
|
const projectPath = "/src/platform/project.ts";
|
|
const { useRun } = await import(storePath);
|
|
const { useProject } = await import(projectPath);
|
|
localStorage.setItem(`openidea.services.runTypes:${useProject.getState().project.root}`, '["spring-boot","npm"]');
|
|
useRun.setState({
|
|
configs: [
|
|
["gateway", "spring-boot"],
|
|
["portal", "spring-boot"],
|
|
["web", "npm"],
|
|
["worker", "npm"],
|
|
...Array.from({ length: extra }, (_, i) => [`app-${i}`, "spring-boot"]),
|
|
].map(([id, kind]) => ({
|
|
id,
|
|
name: id,
|
|
kind,
|
|
command: kind,
|
|
args: [],
|
|
env: {},
|
|
cwd: null,
|
|
detected: true,
|
|
isTest: false,
|
|
debugAdapter: null,
|
|
})),
|
|
});
|
|
}, extra);
|
|
if (!(await page.getByRole("tree", { name: "Services", exact: true }).isVisible()))
|
|
await page.keyboard.press("Meta+8");
|
|
await expect(page.getByRole("tree", { name: "Services", exact: true })).toBeVisible();
|
|
}
|
|
|
|
async function move(page: Page, source: string, target: string) {
|
|
const tree = page.getByRole("tree", { name: "Services", exact: true });
|
|
const sourceRow = tree.getByRole("treeitem", { name: source, exact: true });
|
|
await sourceRow.click();
|
|
const from = await sourceRow.boundingBox();
|
|
const to = await tree.getByRole("treeitem", { name: target, exact: true }).boundingBox();
|
|
if (!from || !to) throw new Error("Service row is not visible");
|
|
await page.mouse.move(from.x + 70, from.y + from.height / 2);
|
|
await page.mouse.down();
|
|
await page.mouse.move(to.x + 70, to.y + 3, { steps: 8 });
|
|
await page.mouse.up();
|
|
}
|
|
|
|
test("service pointer ordering works for Java and npm and survives reload", async ({ workbench: page }) => {
|
|
await configure(page);
|
|
const tree = page.getByRole("tree", { name: "Services", exact: true });
|
|
await move(page, "portal", "gateway");
|
|
await expect(tree.getByRole("treeitem")).toHaveText(["Spring Boot", "portal", "gateway", "npm", "web", "worker"]);
|
|
await expect(tree.getByRole("treeitem", { name: "portal", exact: true })).toHaveAttribute("aria-selected", "true");
|
|
await move(page, "worker", "web");
|
|
await expect(tree.getByRole("treeitem")).toHaveText(["Spring Boot", "portal", "gateway", "npm", "worker", "web"]);
|
|
await page.reload();
|
|
await expect(page.getByRole("tree", { name: "Project" })).toBeVisible();
|
|
await configure(page);
|
|
await expect(tree.getByRole("treeitem")).toHaveText(["Spring Boot", "portal", "gateway", "npm", "worker", "web"]);
|
|
await tree.getByRole("treeitem", { name: "web", exact: true }).click();
|
|
await page.getByRole("button", { name: "Move Up", exact: true }).click();
|
|
await expect(tree.getByRole("treeitem")).toHaveText(["Spring Boot", "portal", "gateway", "npm", "web", "worker"]);
|
|
});
|
|
|
|
test("service dragging scrolls across virtual rows without losing source identity", async ({ workbench: page }) => {
|
|
await configure(page, 100);
|
|
const tree = page.getByRole("tree", { name: "Services", exact: true });
|
|
const source = tree.getByRole("treeitem", { name: "portal", exact: true });
|
|
await source.click();
|
|
const from = await source.boundingBox();
|
|
const bounds = await tree.boundingBox();
|
|
if (!from || !bounds) throw new Error("Service rows are not visible");
|
|
await page.mouse.move(from.x + 70, from.y + from.height / 2);
|
|
await page.mouse.down();
|
|
await page.mouse.move(bounds.x + 100, bounds.y + bounds.height - 5, { steps: 8 });
|
|
await expect.poll(() => tree.evaluate((el) => el.scrollTop)).toBeGreaterThan(1000);
|
|
const target = tree.getByRole("treeitem", { name: "app-99", exact: true });
|
|
await target.scrollIntoViewIfNeeded();
|
|
const to = await target.boundingBox();
|
|
if (!to) throw new Error("Drop target is not visible");
|
|
await page.mouse.move(to.x + 70, to.y + to.height - 3);
|
|
await page.mouse.up();
|
|
await expect(source).toHaveAttribute("aria-selected", "true");
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => {
|
|
const key = Object.keys(localStorage).find((k) => k.startsWith("openidea.services.runOrder:"));
|
|
return key ? JSON.parse(localStorage.getItem(key)!)["spring-boot"].slice(-2) : [];
|
|
}),
|
|
)
|
|
.toEqual(["app-99", "portal"]);
|
|
});
|
|
|
|
test("Docker Compose editor completes service references and navigates to declarations", async ({
|
|
workbench: page,
|
|
}) => {
|
|
await page.evaluate(async () => {
|
|
const backendModule = "/src/editor/core/backend.ts";
|
|
const contribModule = "/src/workbench/contributions.ts";
|
|
const { docBackend } = await import(/* @vite-ignore */ backendModule);
|
|
const { editorService } = await import(/* @vite-ignore */ contribModule);
|
|
const compose = [
|
|
"services:",
|
|
" postgres:",
|
|
" image: postgres:16",
|
|
" networks:",
|
|
" - backend",
|
|
" redis:",
|
|
" image: redis:7",
|
|
" api:",
|
|
" build: .",
|
|
" depends_on:",
|
|
"networks:",
|
|
" backend:",
|
|
" driver: bridge",
|
|
"",
|
|
].join("\n");
|
|
docBackend().files.set("/dev/acme-api/docker-compose.yml", compose);
|
|
await editorService().openFile("/dev/acme-api/docker-compose.yml");
|
|
});
|
|
|
|
const editor = page.locator(".cm-editor");
|
|
await expect(editor).toContainText("postgres:16");
|
|
|
|
// Complete a service reference under `depends_on:`
|
|
await editor.locator(".cm-line", { hasText: "depends_on:" }).click();
|
|
await page.keyboard.press("End");
|
|
await page.keyboard.press("Enter");
|
|
await page.keyboard.type("- ");
|
|
await page.keyboard.press("Control+Space");
|
|
const popup = page.locator(".cm-tooltip-autocomplete");
|
|
await expect(popup.getByText("postgres", { exact: true })).toBeVisible();
|
|
await expect(popup.getByText("redis", { exact: true })).toBeVisible();
|
|
await expect(popup.getByText("api", { exact: true })).toHaveCount(0);
|
|
await page.keyboard.type("post");
|
|
await expect(popup.getByText("redis", { exact: true })).toBeHidden();
|
|
await page.keyboard.press("Enter");
|
|
await expect(popup).toBeHidden();
|
|
await expect(editor.locator(".cm-activeLine")).toHaveText(" - postgres");
|
|
|
|
// Go to Declaration (⌘B) on `- postgres` jumps to ` postgres:` declaration
|
|
await page.keyboard.press("ArrowLeft");
|
|
await page.keyboard.press("Meta+b");
|
|
await expect(editor.locator(".cm-activeLine")).toHaveText(" postgres:");
|
|
|
|
// Go to Declaration (⌘B) on `- backend` jumps to ` backend:` under `networks:`
|
|
await editor.locator(".cm-line", { hasText: " - backend" }).click();
|
|
await page.keyboard.press("End");
|
|
await page.keyboard.press("ArrowLeft");
|
|
await page.keyboard.press("Meta+b");
|
|
await expect(editor.locator(".cm-activeLine")).toHaveText(" backend:");
|
|
});
|
|
|
|
test("the Services detail pane adds no second scrollbar over a console", async ({ workbench: page }) => {
|
|
// A short window: the run toolbar is taller than the detail pane, which used to make
|
|
// the split panel scroll too (the console had its own scrollbar right beside it).
|
|
await page.setViewportSize({ width: 1200, height: 640 });
|
|
await page.evaluate(async () => {
|
|
const store = await import(/* @vite-ignore */ "/src/features/run/store.ts" as string);
|
|
await store.startSampleOutput(200, 50);
|
|
const id = store.useRun.getState().activeSession as string;
|
|
store.useRun.setState((s: { sessions: { id: string }[]; versions: Record<string, number> }) => ({
|
|
sessions: s.sessions.map((x) => (x.id === id ? { ...x, status: "running" } : x)),
|
|
versions: { ...s.versions, [id]: (s.versions[id] ?? 0) + 1 },
|
|
}));
|
|
const svc = await import(/* @vite-ignore */ "/src/features/run/services.tsx" as string);
|
|
const tree = await import(/* @vite-ignore */ "/src/features/services/tree.ts" as string);
|
|
const session = store.useRun.getState().sessions.find((s: { id: string }) => s.id === id);
|
|
svc.addType(session.config.kind);
|
|
tree.useServicesTree.setState({
|
|
selectedKey: `run:config:${session.config.id}`,
|
|
selectedKeys: new Set([`run:config:${session.config.id}`]),
|
|
});
|
|
});
|
|
await page.keyboard.press("Meta+8");
|
|
await expect(page.getByRole("log").first()).toBeVisible();
|
|
const result = await page.evaluate(() => {
|
|
const log = document.querySelector('[data-toolwindow="services"] [role="log"]');
|
|
let panel: Element | null = log;
|
|
while (panel && !panel.hasAttribute("data-panel")) panel = panel.parentElement;
|
|
const inner = panel?.firstElementChild as HTMLElement | null;
|
|
const scrollers: string[] = [];
|
|
for (const el of document.querySelectorAll<HTMLElement>('[data-toolwindow="services"] *')) {
|
|
const cs = getComputedStyle(el);
|
|
if ((cs.overflowY === "auto" || cs.overflowY === "scroll") && el.scrollHeight - el.clientHeight > 2)
|
|
scrollers.push(String(el.className));
|
|
}
|
|
return { overflowY: inner ? getComputedStyle(inner).overflowY : "missing", scrollers };
|
|
});
|
|
// The panel's inner div carries the library's inline `overflow: auto`; the detail pane
|
|
// overrides it, so the console is the only scroller.
|
|
expect(result.overflowY).toBe("hidden");
|
|
expect(result.scrollers.filter((c) => c.includes("detailPane") || c.includes("_panel_"))).toEqual([]);
|
|
expect(result.scrollers.length).toBeGreaterThan(0);
|
|
});
|