mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-14 16:07:52 +00:00
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>
156 lines
6.8 KiB
TypeScript
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:");
|
|
});
|