дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
fix(build-tools): load Maven and Gradle builds of linked workspace projects (#2187)
The Maven and Gradle tool windows disappeared in a workspace, because detection only inspected the workspace metadata folder. They now discover builds in every linked project; models, caches, Maven profile choices, refreshes and run directories use the owning project, and Java reload notifications target its build files. CMake already handled linked projects. Spring's endpoint filter is unchanged. Fixes #932
Цей коміт міститься в:
1 джерело
3dae4ef729
коміт
7579ecbbe1
11 files changed
+704
-128
No files matched your search
@@ -238,7 +238,7 @@ function NodeRow({ node, state }: { node: BuildNode; state: TreeRowState }) {
|
||||
<span className={styles.row} onClick={(e) => e.stopPropagation()} onDoubleClick={(e) => e.stopPropagation()}>
|
||||
<Checkbox
|
||||
checked={profileChecked(node.profile, choices)}
|
||||
onChange={(v) => setProfile(node.profile.id, v)}
|
||||
onChange={(v) => setProfile(node.profile, v)}
|
||||
label={node.profile.id}
|
||||
className={styles.profile}
|
||||
/>
|
||||
@@ -246,13 +246,15 @@ function NodeRow({ node, state }: { node: BuildNode; state: TreeRowState }) {
|
||||
);
|
||||
}
|
||||
const detail =
|
||||
node.kind === "dependency" && node.dependency.scope !== "compile"
|
||||
? `(${node.dependency.scope}${node.dependency.optional ? ", optional" : ""})`
|
||||
: node.kind === "dependency" && node.dependency.optional
|
||||
? "(optional)"
|
||||
: node.kind === "task"
|
||||
? node.task.description
|
||||
: null;
|
||||
node.kind === "profiles" && node.projectRoot
|
||||
? node.projectRoot.split("/").pop()
|
||||
: node.kind === "dependency" && node.dependency.scope !== "compile"
|
||||
? `(${node.dependency.scope}${node.dependency.optional ? ", optional" : ""})`
|
||||
: node.kind === "dependency" && node.dependency.optional
|
||||
? "(optional)"
|
||||
: node.kind === "task"
|
||||
? node.task.description
|
||||
: null;
|
||||
return (
|
||||
<span className={styles.row} title={node.kind === "task" ? (node.task.description ?? undefined) : undefined}>
|
||||
<Icon name={nodeIcon(node)} size={16} className={styles.icon} />
|
||||
@@ -338,7 +340,8 @@ function BuildToolView({ tool }: { tool: BuildTool }) {
|
||||
{
|
||||
label: "Open pom.xml",
|
||||
icon: "file-maven",
|
||||
run: () => void editorService().openFile(`${root}/${node.project.pom}`),
|
||||
run: () =>
|
||||
void editorService().openFile(node.project.projectRoot ? node.project.pom : `${root}/${node.project.pom}`),
|
||||
},
|
||||
{ kind: "action", action: "maven.reload", label: "Reload All Maven Projects" },
|
||||
{ kind: "action", action: "maven.executeGoal" },
|
||||
@@ -415,7 +418,7 @@ function BuildToolView({ tool }: { tool: BuildTool }) {
|
||||
renderNode={(n, state) => <NodeRow node={n} state={state} />}
|
||||
onActivate={(n) => {
|
||||
if (n.kind === "profile") {
|
||||
setProfile(n.profile.id, !profileChecked(n.profile, useBuild.getState().settings.maven.profiles));
|
||||
setProfile(n.profile, !profileChecked(n.profile, useBuild.getState().settings.maven.profiles));
|
||||
return;
|
||||
}
|
||||
if (isRunnable(n)) run([n], "run");
|
||||
|
||||
@@ -22,7 +22,15 @@ import { ExecutePopupHost } from "./ExecutePopup";
|
||||
import { pomNavigationExtension } from "./pomNavigation";
|
||||
import { buildToolsRunAnything } from "./runAnything";
|
||||
import { versionCatalogExtension } from "./versionCatalog";
|
||||
import { expandableKeys, gradleChildren, gradleRoots, mavenChildren, mavenRoots, gradleProjectKey } from "./model";
|
||||
import {
|
||||
expandableKeys,
|
||||
gradleChildren,
|
||||
gradleRoots,
|
||||
mavenChildren,
|
||||
mavenRoots,
|
||||
mavenPomAtPath,
|
||||
gradleProjectKey,
|
||||
} from "./model";
|
||||
import {
|
||||
ANALYZER_KIND,
|
||||
addProjects,
|
||||
@@ -81,8 +89,8 @@ function analyzerPom(arg: unknown): string | null {
|
||||
const a = (arg ?? {}) as { pom?: unknown; path?: unknown };
|
||||
if (typeof a.pom === "string") return a.pom;
|
||||
if (typeof a.path === "string" && root) {
|
||||
const rel = a.path.startsWith(`${root}/`) ? a.path.slice(root.length + 1) : a.path;
|
||||
if (projects.some((p) => p.pom === rel)) return rel;
|
||||
const pom = mavenPomAtPath(projects, root, a.path);
|
||||
if (pom) return pom;
|
||||
}
|
||||
return current.maven ?? null;
|
||||
}
|
||||
@@ -143,10 +151,12 @@ function apply(model: BuildModel | null, registered: Partial<Record<BuildTool, (
|
||||
}
|
||||
// Import each independent build once; linking another build must not be hidden by the first sync.
|
||||
const root = useBuild.getState().root;
|
||||
const mavenKey = `maven:${root}`;
|
||||
if (model?.maven?.projects.length && model.maven.syncedAt == null && !synced.has(mavenKey)) {
|
||||
synced.add(mavenKey);
|
||||
requestSync("maven");
|
||||
for (const member of new Set(model?.maven?.projects.map((p) => p.projectRoot ?? null))) {
|
||||
const key = JSON.stringify(["maven", root, member]);
|
||||
if (model?.maven?.syncedAt == null && !synced.has(key)) {
|
||||
synced.add(key);
|
||||
requestSync("maven", member);
|
||||
}
|
||||
}
|
||||
for (const build of model?.gradle ?? []) {
|
||||
const key = JSON.stringify([root, build.dir]);
|
||||
@@ -209,8 +219,8 @@ export function activate(): () => void {
|
||||
const path = paths.length === 1 && !dirs?.[0] ? paths[0]! : null;
|
||||
if (!path || !path.endsWith("/pom.xml")) return [];
|
||||
const { model, root } = useBuild.getState();
|
||||
const pom = root && path.startsWith(`${root}/`) ? path.slice(root.length + 1) : null;
|
||||
if (!pom || !model?.maven?.projects.some((p) => p.pom === pom)) return [];
|
||||
const pom = mavenPomAtPath(model?.maven?.projects ?? [], root, path);
|
||||
if (!pom) return [];
|
||||
return [{ kind: "action", action: "maven.analyzeDependencies", arg: { pom }, label: "Analyze Dependencies…" }];
|
||||
},
|
||||
}),
|
||||
@@ -269,7 +279,7 @@ export function activate(): () => void {
|
||||
title: "Add Maven Projects",
|
||||
category: "Maven",
|
||||
icon: "plus",
|
||||
when: "projectOpen",
|
||||
when: "projectOpen && !workspaceOpen",
|
||||
run: () => addProjects(),
|
||||
},
|
||||
{
|
||||
@@ -277,7 +287,7 @@ export function activate(): () => void {
|
||||
title: "Unlink Maven Projects",
|
||||
category: "Maven",
|
||||
hidden: true,
|
||||
when: MAVEN_WHEN,
|
||||
when: `${MAVEN_WHEN} && !workspaceOpen`,
|
||||
run: ({ arg }) => unlinkProjects(analyzerPom(arg)),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
mavenChildren,
|
||||
mavenGoalConfig,
|
||||
mavenRoots,
|
||||
mavenPomAtPath,
|
||||
nodeIcon,
|
||||
nodeLabel,
|
||||
parseGoalLine,
|
||||
@@ -395,3 +396,14 @@ describe("Gradle tree and runs", () => {
|
||||
expect(taskCompletions("clean wr", gradle).map((c) => c.line)).toEqual(["clean wrapper"]);
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["pom.xml", "/workspace/pom.xml"],
|
||||
["module/pom.xml", "/workspace/module/pom.xml"],
|
||||
["/linked/pom.xml", "/linked/pom.xml"],
|
||||
["/workspace/member/pom.xml", "/workspace/member/pom.xml"],
|
||||
])("resolves the Maven model POM %s at %s", (pom, path) => {
|
||||
const projects = [project({ pom })];
|
||||
expect(mavenPomAtPath(projects, "/workspace", path)).toBe(pom);
|
||||
expect(mavenPomAtPath(projects, "/workspace", "/other/pom.xml")).toBeNull();
|
||||
});
|
||||
@@ -55,7 +55,7 @@ export const ALL_PHASES = [
|
||||
];
|
||||
|
||||
export type BuildNode =
|
||||
| { kind: "profiles"; key: string }
|
||||
| { kind: "profiles"; key: string; projectRoot?: string | null }
|
||||
| { kind: "profile"; key: string; profile: MavenProfile }
|
||||
| { kind: "mavenProject"; key: string; project: MavenProject }
|
||||
| { kind: "lifecycle"; key: string; project: MavenProject }
|
||||
@@ -76,7 +76,7 @@ export type BuildNode =
|
||||
export interface MavenRunOptions {
|
||||
offline: boolean;
|
||||
skipTests: boolean;
|
||||
/** Explicit profile choices: id → checked. Unlisted profiles keep their default. */
|
||||
/** Explicit profile choices by profileKey. Unlisted profiles keep their default. */
|
||||
profiles: Record<string, boolean>;
|
||||
}
|
||||
|
||||
@@ -100,9 +100,14 @@ export function pluginLabel(p: MavenPlugin): string {
|
||||
return `${p.prefix} (${p.groupId}:${p.artifactId}${p.version ? `:${p.version}` : ""})`;
|
||||
}
|
||||
|
||||
/** Profile choices in different workspace members are independent. */
|
||||
export function profileKey(profile: MavenProfile): string {
|
||||
return profile.projectRoot ? JSON.stringify([profile.projectRoot, profile.id]) : profile.id;
|
||||
}
|
||||
|
||||
/** Whether a profile is active for runs: the explicit choice, else `activeByDefault`. */
|
||||
export function profileChecked(profile: MavenProfile, choices: Record<string, boolean>): boolean {
|
||||
return choices[profile.id] ?? profile.activeByDefault;
|
||||
return choices[profileKey(profile)] ?? profile.activeByDefault;
|
||||
}
|
||||
|
||||
/** `-P` value for the choices that differ from the defaults (`a,!b`), or null. */
|
||||
@@ -123,11 +128,17 @@ export function profilesList(profiles: readonly MavenProfile[], choices: Record<
|
||||
|
||||
export function mavenRoots(model: MavenModel): BuildNode[] {
|
||||
const out: BuildNode[] = [];
|
||||
if (model.profiles.length) out.push({ kind: "profiles", key: "mvn:profiles" });
|
||||
for (const projectRoot of new Set(model.profiles.map((p) => p.projectRoot)))
|
||||
out.push({ kind: "profiles", key: `mvn:profiles${projectRoot ?? ""}`, projectRoot });
|
||||
for (const p of model.projects) if (!p.aggregated) out.push({ kind: "mavenProject", key: projectKey(p), project: p });
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Resolves a POM from either a project-relative or workspace-absolute model path. */
|
||||
export function mavenPomAtPath(projects: readonly MavenProject[], root: string | null, path: string): string | null {
|
||||
return projects.find((p) => p.pom === path || (root && `${root}/${p.pom}` === path))?.pom ?? null;
|
||||
}
|
||||
|
||||
/** Run configurations of a Maven project: `maven` configs whose directory is the project's. */
|
||||
export function mavenRunConfigs(project: MavenProject, configs: readonly RunConfig[]): RunConfig[] {
|
||||
return configs.filter((c) => c.kind === "maven" && normalizeDir(c.cwd) === project.dir);
|
||||
@@ -171,7 +182,9 @@ export function mavenChildren(
|
||||
): BuildNode[] | undefined {
|
||||
switch (node.kind) {
|
||||
case "profiles":
|
||||
return model.profiles.map((profile) => ({ kind: "profile", key: `mvn:profile:${profile.id}`, profile }));
|
||||
return model.profiles
|
||||
.filter((p) => p.projectRoot === node.projectRoot)
|
||||
.map((profile) => ({ kind: "profile", key: `mvn:profile:${profileKey(profile)}`, profile }));
|
||||
case "mavenProject": {
|
||||
const p = node.project;
|
||||
const k = projectKey(p);
|
||||
@@ -474,7 +487,11 @@ export function mavenGoalConfig(
|
||||
model: Pick<MavenModel, "profiles">,
|
||||
options: MavenRunOptions,
|
||||
): RunConfig {
|
||||
const args = mavenArgs(goals, model, options);
|
||||
const args = mavenArgs(
|
||||
goals,
|
||||
{ profiles: model.profiles.filter((p) => p.projectRoot === project.projectRoot) },
|
||||
options,
|
||||
);
|
||||
return {
|
||||
id: `maven:goal:${project.pom}:${goals.join(" ")}`,
|
||||
name: mavenConfigName(project, goals),
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
nextRequestId,
|
||||
type BuildModel,
|
||||
type BuildTool,
|
||||
type MavenProfile,
|
||||
type RequestId,
|
||||
type RunConfig,
|
||||
type SyncProgress,
|
||||
@@ -27,7 +28,7 @@ import { useBuildModel } from "@/platform/buildModel";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { editorService, notifications } from "@/workbench/contributions";
|
||||
import { cancelTask, removeTask, upsertTask, useTasks } from "@/workbench/tasks";
|
||||
import { gradleRoots, profilesList, type GradleRunOptions, type MavenRunOptions } from "./model";
|
||||
import { gradleRoots, profileKey, profilesList, type GradleRunOptions, type MavenRunOptions } from "./model";
|
||||
|
||||
export interface BuildSettings {
|
||||
maven: MavenRunOptions;
|
||||
@@ -126,15 +127,20 @@ export const toggleGradleOffline = () =>
|
||||
/** Delay before reloading Maven after a profile toggle, so a few toggles in a row reload once. */
|
||||
export const PROFILE_RELOAD_DELAY_MS = 800;
|
||||
let profileReload: ReturnType<typeof setTimeout> | undefined;
|
||||
const profileRoots = new Set<string | null>();
|
||||
|
||||
export function setProfile(id: string, checked: boolean) {
|
||||
export function setProfile(profile: string | MavenProfile, checked: boolean) {
|
||||
const id = typeof profile === "string" ? profile : profileKey(profile);
|
||||
const projectRoot = typeof profile === "string" ? null : (profile.projectRoot ?? null);
|
||||
updateSettings((s) => ({ ...s, maven: { ...s.maven, profiles: { ...s.maven.profiles, [id]: checked } } }));
|
||||
// The Java language server and Build Project resolve the project with the profiles of the
|
||||
// last sync, so a new choice reloads it (IDEA re-imports on a profile change too).
|
||||
profileRoots.add(projectRoot);
|
||||
clearTimeout(profileReload);
|
||||
profileReload = setTimeout(() => {
|
||||
profileReload = undefined;
|
||||
requestSync("maven");
|
||||
for (const root of profileRoots) requestSync("maven", root);
|
||||
profileRoots.clear();
|
||||
}, PROFILE_RELOAD_DELAY_MS);
|
||||
}
|
||||
|
||||
@@ -240,7 +246,7 @@ const LINKED_PROJECTS = "languages.java.maven.linkedProjects";
|
||||
* reactor lists them), or removes them (a linked one is dropped, a discovered one unlinked).
|
||||
*/
|
||||
async function setLinked(root: string, poms: string[], linked: boolean) {
|
||||
if (useProject.getState().project?.root !== root) return;
|
||||
if (useProject.getState().project?.root !== root || useProject.getState().project?.workspace) return;
|
||||
const settings = await commands.settingsGet();
|
||||
if (useProject.getState().project?.root !== root) return;
|
||||
const maven = settings.languages.java?.maven;
|
||||
@@ -266,7 +272,7 @@ async function setLinked(root: string, poms: string[], linked: boolean) {
|
||||
export async function unlinkProjects(pom: string | null) {
|
||||
const { root, model } = useBuild.getState();
|
||||
const project = model?.maven?.projects.find((p) => p.pom === pom);
|
||||
if (!root || !pom || !project) return;
|
||||
if (!root || !pom || !project || useProject.getState().project?.workspace) return;
|
||||
if (
|
||||
!(await confirm({
|
||||
title: "Unlink Maven projects?",
|
||||
@@ -295,7 +301,7 @@ function projectPoms(root: string, picked: readonly string[]): string[] {
|
||||
|
||||
export async function addProjects() {
|
||||
const root = useProject.getState().project?.root;
|
||||
if (!root || !isTauri()) return;
|
||||
if (!root || useProject.getState().project?.workspace || !isTauri()) return;
|
||||
const { open } = await import("@tauri-apps/plugin-dialog");
|
||||
const picked = await open({
|
||||
title: tx`Add Maven Projects`,
|
||||
@@ -392,7 +398,7 @@ export function requestSync(tool: BuildTool, buildDir: string | null = null) {
|
||||
} else void syncTool(tool, buildDir);
|
||||
}
|
||||
|
||||
/** Reloads the build tool model; `buildDir` limits a Gradle reload to one linked build. */
|
||||
/** Reloads one Maven workspace member / linked Gradle build, or all builds when `buildDir` is null. */
|
||||
export async function syncTool(tool: BuildTool, buildDir: string | null = null): Promise<void> {
|
||||
const state = useBuild.getState();
|
||||
const gen = generation;
|
||||
@@ -411,10 +417,27 @@ export async function syncTool(tool: BuildTool, buildDir: string | null = null):
|
||||
upsertTask({ id: taskId, title: TITLES[tool], message: p.message, fraction: p.fraction, done: false, cancel });
|
||||
};
|
||||
const { settings, model } = state;
|
||||
const profiles = tool === "maven" && model?.maven ? profilesList(model.maven.profiles, settings.maven.profiles) : [];
|
||||
const offline = tool === "maven" ? settings.maven.offline : settings.gradle.offline;
|
||||
try {
|
||||
const next = await call(commands.buildSync(tool, request, offline, profiles, buildDir, channel));
|
||||
const roots =
|
||||
tool === "maven" && useProject.getState().project?.workspace
|
||||
? [...new Set(model?.maven?.projects.map((p) => p.projectRoot ?? null))].filter(
|
||||
(r) => !buildDir || r === buildDir,
|
||||
)
|
||||
: [buildDir];
|
||||
let next = model;
|
||||
for (const dir of roots) {
|
||||
if (useBuild.getState().root !== root || useBuild.getState().syncing[tool]?.request !== request) return;
|
||||
const profiles =
|
||||
tool === "maven" && model?.maven
|
||||
? profilesList(
|
||||
model.maven.profiles.filter((p) => (p.projectRoot ?? null) === dir),
|
||||
settings.maven.profiles,
|
||||
)
|
||||
: [];
|
||||
next = await call(commands.buildSync(tool, request, offline, profiles, dir, channel));
|
||||
}
|
||||
if (!next) return;
|
||||
if (useBuild.getState().root !== root || gen !== generation) return;
|
||||
const configs = await call(commands.runConfigs()).catch(() => useBuild.getState().configs);
|
||||
if (useBuild.getState().root !== root || gen !== generation) return;
|
||||
@@ -472,6 +495,7 @@ export function resetBuild() {
|
||||
generation++;
|
||||
clearTimeout(profileReload);
|
||||
profileReload = undefined;
|
||||
profileRoots.clear();
|
||||
queuedSyncs.clear();
|
||||
useAnalyzer.setState({ module: null });
|
||||
for (const tool of ["maven", "gradle"] as const) {
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
||||
import { act, fireEvent, render, screen } from "@testing-library/react";
|
||||
import type { BuildModel, ProjectInfo, RunConfig } from "@/platform/ipc";
|
||||
import { registerAction } from "@/platform/actions";
|
||||
import { setContext } from "@/platform/context";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { MavenView, GradleView } from "./BuildToolView";
|
||||
import { activate } from "./index";
|
||||
import { expandableKeys, gradleChildren, gradleRoots, mavenChildren, mavenRoots, profileKey } from "./model";
|
||||
import { executeBuildLine } from "./runAnything";
|
||||
import { loadModel, PROFILE_RELOAD_DELAY_MS, resetBuild, setProfile, syncTool, useBuild } from "./store";
|
||||
|
||||
const mocks = vi.hoisted(() => ({ model: vi.fn(), sync: vi.fn() }));
|
||||
vi.mock("@/platform/ipc", async (original) => {
|
||||
const real = await original<typeof import("@/platform/ipc")>();
|
||||
return {
|
||||
...real,
|
||||
isTauri: () => true,
|
||||
call: (value: unknown) => value,
|
||||
commands: {
|
||||
...real.commands,
|
||||
buildModel: mocks.model,
|
||||
runConfigs: () => Promise.resolve([]),
|
||||
buildSync: mocks.sync,
|
||||
cancelRequest: () => Promise.resolve(),
|
||||
},
|
||||
};
|
||||
});
|
||||
vi.mock("@tauri-apps/api/core", async (original) => ({
|
||||
...(await original<typeof import("@tauri-apps/api/core")>()),
|
||||
Channel: class {
|
||||
onmessage: unknown = null;
|
||||
},
|
||||
}));
|
||||
|
||||
const members = ["/projects/first", "/projects/second"];
|
||||
const model: BuildModel = {
|
||||
maven: {
|
||||
syncedAt: 1,
|
||||
stale: false,
|
||||
syncError: null,
|
||||
profiles: members.map((projectRoot) => ({ id: "local", activeByDefault: false, projectRoot })),
|
||||
projects: members.map((projectRoot) => ({
|
||||
projectRoot,
|
||||
pom: `${projectRoot}/pom.xml`,
|
||||
dir: projectRoot,
|
||||
groupId: "example",
|
||||
artifactId: "app",
|
||||
version: "1",
|
||||
packaging: "jar",
|
||||
displayName: projectRoot.split("/").pop()!,
|
||||
parent: null,
|
||||
modules: [],
|
||||
aggregated: false,
|
||||
plugins: [],
|
||||
dependencies: [],
|
||||
profiles: ["local"],
|
||||
javaVersion: null,
|
||||
command: `${projectRoot}/mvnw`,
|
||||
options: [],
|
||||
env: {},
|
||||
})),
|
||||
},
|
||||
gradle: members.map((dir) => ({
|
||||
dir,
|
||||
command: `${dir}/gradlew`,
|
||||
env: {},
|
||||
javaVersion: null,
|
||||
syncedAt: 1,
|
||||
stale: false,
|
||||
syncError: null,
|
||||
projects: [
|
||||
{
|
||||
path: ":",
|
||||
name: dir.split("/").pop()!,
|
||||
dir,
|
||||
buildFile: `${dir}/build.gradle`,
|
||||
tasks: [{ name: "test", group: "verification", description: null }],
|
||||
},
|
||||
],
|
||||
})),
|
||||
};
|
||||
let dispose: () => void;
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
resetBuild();
|
||||
useProject.setState({
|
||||
project: {
|
||||
root: "/workspace",
|
||||
trusted: true,
|
||||
workspace: { version: 1, name: "Workspace", projects: members },
|
||||
} as ProjectInfo,
|
||||
});
|
||||
setContext("projectOpen", true);
|
||||
setContext("workspaceOpen", true);
|
||||
mocks.model.mockResolvedValue(model);
|
||||
mocks.sync.mockResolvedValue(model);
|
||||
await act(async () => {
|
||||
dispose = activate();
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
dispose();
|
||||
resetBuild();
|
||||
useProject.setState({ project: null });
|
||||
setContext("projectOpen", false);
|
||||
setContext("workspaceOpen", false);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("lists linked Maven projects and keeps duplicate profile choices and goal directories independent", async () => {
|
||||
const maven = model.maven!;
|
||||
const roots = mavenRoots(maven);
|
||||
act(() =>
|
||||
useBuild.setState((s) => ({
|
||||
expanded: { ...s.expanded, maven: expandableKeys(roots, (n) => mavenChildren(n, maven, [])) },
|
||||
})),
|
||||
);
|
||||
render(<MavenView />);
|
||||
expect(screen.getAllByText("first")).toHaveLength(2);
|
||||
expect(screen.getAllByText("second")).toHaveLength(2);
|
||||
const profiles = screen.getAllByRole("checkbox", { name: "local" });
|
||||
expect(profiles).toHaveLength(2);
|
||||
vi.useFakeTimers();
|
||||
await act(async () => {
|
||||
fireEvent.click(profiles[1]!);
|
||||
});
|
||||
expect(profiles[0]).not.toBeChecked();
|
||||
expect(profiles[1]).toBeChecked();
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(PROFILE_RELOAD_DELAY_MS);
|
||||
});
|
||||
expect(mocks.sync).toHaveBeenCalledWith("maven", expect.any(Number), false, ["local"], members[1], expect.anything());
|
||||
const runs: RunConfig[] = [];
|
||||
const off = registerAction({
|
||||
id: "run.runConfig",
|
||||
title: "Run",
|
||||
hidden: true,
|
||||
run: ({ arg }) => {
|
||||
runs.push((arg as { config: RunConfig }).config);
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.doubleClick(screen.getAllByText("validate")[1]!);
|
||||
});
|
||||
expect(executeBuildLine("maven", "validate", maven.projects[0]!.pom)).toBe(true);
|
||||
off();
|
||||
expect(runs[0]).toMatchObject({ cwd: members[1], command: `${members[1]}/mvnw`, args: ["validate", "-Plocal"] });
|
||||
expect(runs[1]).toMatchObject({ cwd: members[0], command: `${members[0]}/mvnw`, args: ["validate"] });
|
||||
});
|
||||
|
||||
it("reloads all Maven members with their own profile choices", async () => {
|
||||
vi.useFakeTimers();
|
||||
setProfile(model.maven!.profiles[0]!, true);
|
||||
setProfile(model.maven!.profiles[1]!, true);
|
||||
await vi.advanceTimersByTimeAsync(PROFILE_RELOAD_DELAY_MS);
|
||||
expect(mocks.sync.mock.calls.map((c) => [c[3], c[4]])).toEqual([
|
||||
[["local"], members[0]],
|
||||
[["local"], members[1]],
|
||||
]);
|
||||
mocks.sync.mockClear();
|
||||
useBuild.setState((s) => ({
|
||||
settings: {
|
||||
...s.settings,
|
||||
maven: { ...s.settings.maven, profiles: { [profileKey(model.maven!.profiles[1]!)]: true } },
|
||||
},
|
||||
}));
|
||||
await syncTool("maven");
|
||||
expect(mocks.sync.mock.calls.map((c) => [c[3], c[4]])).toEqual([
|
||||
[[], members[0]],
|
||||
[["local"], members[1]],
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows and runs duplicate Gradle tasks with each member's wrapper and refresh directory", async () => {
|
||||
const roots = gradleRoots(model.gradle ?? []);
|
||||
act(() =>
|
||||
useBuild.setState((s) => ({
|
||||
expanded: {
|
||||
...s.expanded,
|
||||
gradle: expandableKeys(roots, (n) => gradleChildren(n, model.gradle ?? [], [], "/workspace")),
|
||||
},
|
||||
})),
|
||||
);
|
||||
render(<GradleView />);
|
||||
const runs: RunConfig[] = [];
|
||||
const off = registerAction({
|
||||
id: "run.runConfig",
|
||||
title: "Run",
|
||||
hidden: true,
|
||||
run: ({ arg }) => {
|
||||
runs.push((arg as { config: RunConfig }).config);
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.doubleClick(screen.getAllByText("test")[1]!);
|
||||
});
|
||||
off();
|
||||
expect(runs[0]).toMatchObject({ cwd: members[1], command: `${members[1]}/gradlew`, args: ["test"] });
|
||||
await syncTool("gradle", members[1]);
|
||||
expect(mocks.sync).toHaveBeenCalledWith("gradle", expect.any(Number), false, [], members[1], expect.anything());
|
||||
});
|
||||
|
||||
it("refreshes availability after workspace membership changes and does not load untrusted windows", async () => {
|
||||
mocks.model.mockResolvedValue({ maven: null, gradle: [] });
|
||||
await loadModel();
|
||||
expect(useBuild.getState().model).toEqual({ maven: null, gradle: [] });
|
||||
mocks.model.mockClear();
|
||||
act(() => useProject.setState({ project: { ...useProject.getState().project!, trusted: false } }));
|
||||
await loadModel();
|
||||
expect(mocks.model).not.toHaveBeenCalled();
|
||||
expect(useBuild.getState().model).toBeNull();
|
||||
});
|
||||
Reference in new issue
Заблокувати користувача