Files
openidea/e2e/services.spec.ts
T
Deng Liuanddengliu d78a73452b feat(docker): Compose schema association and internal reference completion/navigation (#2455)
Docker Compose files get Compose Specification completion, hover and validation from yaml-language-server: languages/yaml.toml maps the pinned compose-spec schema to the standard Compose filename globs, and ide-sdk / ide-lsp feed content-detected Compose files and user overrides (Kept / Excluded as !path negations) into yaml.schemas, reconfiguring only when that set changes. The Compose editor completes and navigates (⌘-click / ⌘B) references to declared services, networks, volumes, configs and secrets in depends_on, links, volumes_from, extends.service, networks, volumes, configs and secrets.

Co-authored-by: dengliu <5108773+dengliu@users.noreply.github.com>
2026-11-11 01:06:56 +08:00

156 lines
6.8 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:");
});