дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
feat(run): Run All, Restart All, Stop All and Open in Browser on a type group (#2090)
Services type groups offer Run All, Restart All, Stop All and Open in Browser. Batches follow display order, skip live runs, wait for exits before restarting and continue after a launch failure. Open in Browser uses reported HTTP ports, with a chooser when there are several. Co-authored-by: qtvbwfn <[email protected]>
Цей коміт міститься в:
1 джерело
6d8e41c2c1
коміт
a73e3c21ad
6 files changed
+368
-8
No files matched your search
@@ -42,7 +42,7 @@ import { invalidateGoModules } from "./goModules";
|
||||
import { invalidatePythonPackages } from "./pythonPackages";
|
||||
import { RunWidget } from "./RunWidget";
|
||||
import { runConfigsRunAnything } from "./runAnything";
|
||||
import { cleanupRemovedProject, recordRun, runServiceProvider } from "./services";
|
||||
import { cleanupRemovedProject, recordRun, registerRunServiceActions, runServiceProvider } from "./services";
|
||||
import { liveServiceSession, serviceSessions } from "./serviceSessions";
|
||||
import { cmakeBuildId } from "./cmake";
|
||||
import { runBeforeLaunch } from "./beforeLaunch";
|
||||
@@ -342,6 +342,7 @@ export function activate(): () => void {
|
||||
}),
|
||||
registerOverlay({ id: "run.dialogs", component: RunDialogsOverlay }),
|
||||
registerServiceProvider(runServiceProvider),
|
||||
registerRunServiceActions(),
|
||||
registerRunningProcessesProvider({
|
||||
id: "run",
|
||||
running: () =>
|
||||
|
||||
@@ -10,7 +10,10 @@ vi.mock("@/platform/ipc", async (importOriginal) => ({
|
||||
}));
|
||||
|
||||
import {
|
||||
addType,
|
||||
cleanupRemovedProject,
|
||||
runServiceProvider,
|
||||
registerRunServiceActions,
|
||||
DEFAULT_TYPES,
|
||||
readRan,
|
||||
readTypes,
|
||||
@@ -19,8 +22,32 @@ import {
|
||||
sessionStatus,
|
||||
} from "./services";
|
||||
import { startedPort } from "@/platform/serverPort";
|
||||
import { useDebugRunSessions, type DebugServiceSession } from "@/workbench/contributions";
|
||||
import { useRun, type RunSession } from "./store";
|
||||
import type { DebugServiceSession } from "@/workbench/contributions";
|
||||
|
||||
const { startRun, stopServiceSessions, openUrl, showContextMenu } = vi.hoisted(() => ({
|
||||
startRun: vi.fn(),
|
||||
openUrl: vi.fn(),
|
||||
showContextMenu: vi.fn(),
|
||||
stopServiceSessions: vi.fn(),
|
||||
}));
|
||||
vi.mock("./store", async (importOriginal) => ({ ...(await importOriginal<typeof import("./store")>()), startRun }));
|
||||
vi.mock("./serviceSessions", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("./serviceSessions")>()),
|
||||
stopServiceSessions,
|
||||
}));
|
||||
|
||||
vi.mock("./navigate", () => ({ openUrl }));
|
||||
vi.mock("@/ui", async (importOriginal) => ({ ...(await importOriginal<typeof import("@/ui")>()), showContextMenu }));
|
||||
|
||||
let disposeActions: () => void;
|
||||
beforeEach(() => {
|
||||
disposeActions = registerRunServiceActions();
|
||||
useDebugRunSessions.setState({ serviceSessions: [] });
|
||||
openUrl.mockReset();
|
||||
showContextMenu.mockReset();
|
||||
});
|
||||
afterEach(() => disposeActions());
|
||||
|
||||
const config = (id: string, kind: string): RunConfig => ({
|
||||
id,
|
||||
@@ -307,6 +334,183 @@ describe("embedded web server port", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("open in browser", () => {
|
||||
const item = () => ({ key: "run:config:a", label: "a", icon: "" });
|
||||
const hasBrowser = () => !runServiceProvider.actions!(item()).find((a) => a.id === "open-browser")?.disabled;
|
||||
|
||||
beforeEach(() => {
|
||||
useProject.setState({ project: null });
|
||||
useRun.setState({ configs: [], sessions: [] });
|
||||
});
|
||||
|
||||
it("offers Open in Browser while a session runs with an HTTP port", () => {
|
||||
useRun.setState({
|
||||
configs: [config("a", "spring-boot")],
|
||||
sessions: [session("s1", config("a", "spring-boot"), "running", 1, { httpPort: 8080 })],
|
||||
});
|
||||
expect(hasBrowser()).toBe(true);
|
||||
});
|
||||
|
||||
it("disables Open in Browser without a port or a running session", () => {
|
||||
useRun.setState({
|
||||
configs: [config("a", "spring-boot")],
|
||||
sessions: [session("s1", config("a", "spring-boot"), "running", 1)],
|
||||
});
|
||||
expect(hasBrowser()).toBe(false);
|
||||
useRun.setState({ sessions: [session("s1", config("a", "spring-boot"), "finished", 1, { httpPort: 8080 })] });
|
||||
expect(hasBrowser()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("group batch actions", () => {
|
||||
const group = () => ({ key: "run:type:spring-boot", label: "Spring Boot", icon: "" });
|
||||
const action = (id: string) =>
|
||||
runServiceProvider.actions!(group() as never).find((a: { id: string }) => a.id === id)!;
|
||||
|
||||
beforeEach(() => {
|
||||
startRun.mockReset().mockResolvedValue(null);
|
||||
stopServiceSessions.mockReset().mockImplementation(async (sessions) => {
|
||||
const ids = sessions.flatMap((s: { run?: RunSession }) => (s.run ? [s.run.id] : []));
|
||||
useRun.setState((state) => ({
|
||||
sessions: state.sessions.map((s) => (ids.includes(s.id) ? { ...s, status: "finished" } : s)),
|
||||
}));
|
||||
});
|
||||
useProject.setState({ project: null });
|
||||
useRun.setState({ configs: [], sessions: [] });
|
||||
addType("spring-boot");
|
||||
});
|
||||
|
||||
it("enables Run All and Restart All with stopped configurations, Stop All only with running ones", () => {
|
||||
useRun.setState({ configs: [config("a", "spring-boot"), config("b", "spring-boot")], sessions: [] });
|
||||
expect(action("run-all").disabled).toBe(false);
|
||||
expect(action("restart-all").disabled).toBe(false);
|
||||
expect(action("stop-all").disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("enables Stop All when one runs and Run All while others are stopped", () => {
|
||||
useRun.setState({
|
||||
configs: [config("a", "spring-boot"), config("b", "spring-boot")],
|
||||
sessions: [session("s1", config("a", "spring-boot"), "running", 1)],
|
||||
});
|
||||
expect(action("stop-all").disabled).toBe(false);
|
||||
expect(action("run-all").disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("disables Run All when everything runs", () => {
|
||||
useRun.setState({
|
||||
configs: [config("a", "spring-boot")],
|
||||
sessions: [session("s1", config("a", "spring-boot"), "running", 1)],
|
||||
});
|
||||
expect(action("run-all").disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("starts stopped configurations in display order, skipping running ones", async () => {
|
||||
const a = config("a", "spring-boot");
|
||||
const b = config("b", "spring-boot");
|
||||
const c = config("c", "spring-boot");
|
||||
useRun.setState({ configs: [a, b, c], sessions: [session("s1", a, "running", 1)] });
|
||||
await action("run-all").run();
|
||||
expect(startRun.mock.calls).toEqual([
|
||||
[b, undefined, undefined, { show: false }],
|
||||
[c, undefined, undefined, { show: false }],
|
||||
]);
|
||||
});
|
||||
|
||||
it("restarts: stops the running ones, then starts every configuration in order", async () => {
|
||||
const a = config("a", "spring-boot");
|
||||
const b = config("b", "spring-boot");
|
||||
useRun.setState({ configs: [a, b], sessions: [session("s1", a, "running", 1)] });
|
||||
await action("restart-all").run();
|
||||
expect(stopServiceSessions).toHaveBeenCalledOnce();
|
||||
expect(stopServiceSessions.mock.calls[0]![0].map((s: { config: RunConfig }) => s.config.id)).toEqual(["a"]);
|
||||
expect(startRun.mock.calls).toEqual([
|
||||
[a, undefined, undefined, { show: false }],
|
||||
[b, undefined, undefined, { show: false }],
|
||||
]);
|
||||
});
|
||||
|
||||
it("waits for process exit before restarting any configuration and continues after a launch failure", async () => {
|
||||
const a = config("a", "spring-boot");
|
||||
const b = config("b", "spring-boot");
|
||||
useRun.setState({ configs: [a, b], sessions: [session("s1", a, "running", 1, { runId: 1 })] });
|
||||
stopServiceSessions.mockResolvedValue(undefined);
|
||||
startRun.mockRejectedValueOnce(new Error("launch failed"));
|
||||
const pending = action("restart-all").run();
|
||||
await vi.waitFor(() => expect(stopServiceSessions).toHaveBeenCalledOnce());
|
||||
expect(startRun).not.toHaveBeenCalled();
|
||||
useRun.setState({ sessions: [session("s1", a, "finished", 1, { runId: 1 })] });
|
||||
await pending;
|
||||
expect(startRun.mock.calls.map(([c]) => c.id)).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("uses the saved display order and leaves other types alone", async () => {
|
||||
useProject.setState({ project: { root: "/group-order" } as never });
|
||||
localStorage.setItem("openidea.services.runOrder:/group-order", JSON.stringify({ "spring-boot": ["c", "a", "b"] }));
|
||||
const configs = [
|
||||
config("a", "spring-boot"),
|
||||
config("b", "spring-boot"),
|
||||
config("c", "spring-boot"),
|
||||
config("d", "npm"),
|
||||
];
|
||||
useRun.setState({ configs, sessions: [session("other", configs[3]!, "running", 1)] });
|
||||
await action("run-all").run();
|
||||
expect(startRun.mock.calls.map(([c]) => c.id)).toEqual(["c", "a", "b"]);
|
||||
expect(action("stop-all").disabled).toBe(true);
|
||||
await action("stop-all").run();
|
||||
expect(stopServiceSessions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not resurrect a deleted configuration retained only as finished output", async () => {
|
||||
const a = config("removed", "spring-boot");
|
||||
useRun.setState({ configs: [a], sessions: [] });
|
||||
useRun.setState({ configs: [], sessions: [session("old", a, "finished", 1)] });
|
||||
expect(action("run-all").disabled).toBe(true);
|
||||
expect(action("restart-all").disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("rechecks enabled state when a registered action runs", async () => {
|
||||
const a = config("a", "spring-boot");
|
||||
useRun.setState({ configs: [a], sessions: [] });
|
||||
const run = action("run-all");
|
||||
useRun.setState({ sessions: [session("s1", a, "starting", 1)] });
|
||||
await run.run();
|
||||
expect(startRun).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("opens the known group URL and offers a named choice for several web apps", async () => {
|
||||
const a = config("a", "spring-boot");
|
||||
const b = config("b", "spring-boot");
|
||||
useRun.setState({ configs: [a, b], sessions: [session("s1", a, "running", 1, { httpPort: 8123 })] });
|
||||
await action("open-browser").run();
|
||||
expect(openUrl).toHaveBeenCalledWith("http://localhost:8123");
|
||||
openUrl.mockClear();
|
||||
useDebugRunSessions.setState({
|
||||
serviceSessions: [
|
||||
{
|
||||
id: 1,
|
||||
config: b,
|
||||
status: "running",
|
||||
processId: null,
|
||||
startedAt: 2,
|
||||
httpPort: 8456,
|
||||
stop: async () => {},
|
||||
rerun: null,
|
||||
show: () => {},
|
||||
},
|
||||
],
|
||||
});
|
||||
await action("open-browser").run();
|
||||
expect(openUrl).not.toHaveBeenCalled();
|
||||
const choices = showContextMenu.mock.calls[0]![1];
|
||||
expect(choices.map((c: { label: string }) => c.label)).toEqual([
|
||||
"a — http://localhost:8123",
|
||||
"b — http://localhost:8456",
|
||||
]);
|
||||
choices[1].run();
|
||||
expect(openUrl).toHaveBeenCalledWith("http://localhost:8456");
|
||||
});
|
||||
});
|
||||
|
||||
describe("removing a workspace member from Services", () => {
|
||||
const main = { ...config("app", "spring-boot"), cwd: "/w/main" };
|
||||
const vue = { ...config("dev", "npm"), cwd: "/w/vue" };
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
*/
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { create } from "zustand";
|
||||
import { executeAction } from "@/platform/actions";
|
||||
import { executeAction, registerActions } from "@/platform/actions";
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { projectLocation, useRemote } from "@/platform/remote";
|
||||
import { tx, useT, useTxTemplate } from "@/platform/i18n";
|
||||
import { Button, EmptyState, T } from "@/ui";
|
||||
import { Button, EmptyState, T, showContextMenu } from "@/ui";
|
||||
import type { MenuItem } from "@/ui";
|
||||
import {
|
||||
toolWindowService,
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
type ServiceTreeItem,
|
||||
} from "@/workbench/contributions";
|
||||
import { commandLine, configIcon, KIND_OPTIONS, kindLabel } from "./configs";
|
||||
import { openUrl } from "./navigate";
|
||||
import { SessionPanel } from "./RunView";
|
||||
import { showApplicationUpdate } from "./applicationUpdate";
|
||||
import { isUpdatable } from "./updatePolicies";
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
startRun,
|
||||
stopSession,
|
||||
useRun,
|
||||
waitForExit,
|
||||
type RunSession,
|
||||
isLive,
|
||||
} from "./store";
|
||||
@@ -369,9 +371,115 @@ function debug(r: Resolved) {
|
||||
else if (r.config) void executeAction("run.debugConfig", { config: r.config, show: false });
|
||||
}
|
||||
|
||||
function actions(item: ServiceTreeItem): ServiceItemAction[] {
|
||||
function groupConfigs(kind: string): RunConfig[] {
|
||||
const root = useProject.getState().project?.root ?? null;
|
||||
const location = projectLocation(root);
|
||||
const { configs, sessions } = useRun.getState();
|
||||
const debug = useDebugRunSessions.getState().serviceSessions;
|
||||
const latest = currentServiceSessions(serviceSessions(sessions, debug));
|
||||
const group = runServiceItems(
|
||||
configs,
|
||||
sessions,
|
||||
currentTypes(),
|
||||
ranIds(root),
|
||||
location ? currentServiceOrder(location) : {},
|
||||
debug,
|
||||
configRemoved,
|
||||
).find((item) => item.key === TYPE_PREFIX + kind);
|
||||
return (group?.children ?? []).flatMap((item) => {
|
||||
const id = item.key.slice(CONFIG_PREFIX.length);
|
||||
const config = configs.find((c) => c.id === id) ?? latest.get(id)?.config;
|
||||
return config ? [config] : [];
|
||||
});
|
||||
}
|
||||
|
||||
async function startAll(kind: string, restart: boolean): Promise<void> {
|
||||
const root = useProject.getState().project?.root;
|
||||
const entries = groupConfigs(kind).filter((c) => !isDebugOnly(c));
|
||||
if (restart) {
|
||||
const { sessions } = useRun.getState();
|
||||
const running = serviceSessions(sessions, useDebugRunSessions.getState().serviceSessions).filter(
|
||||
(s) => s.config.kind === kind && liveServiceSession(s),
|
||||
);
|
||||
await stopServiceSessions(running);
|
||||
await waitForExit(
|
||||
running.flatMap((s) => (s.run ? [s.run.id] : [])),
|
||||
null,
|
||||
);
|
||||
}
|
||||
for (const config of entries) {
|
||||
if (useProject.getState().project?.root !== root) return;
|
||||
const running = serviceSessions(useRun.getState().sessions, useDebugRunSessions.getState().serviceSessions);
|
||||
if (running.some((s) => s.config.id === config.id && liveServiceSession(s))) continue;
|
||||
try {
|
||||
await startRun(config, undefined, undefined, { show: false });
|
||||
} catch (e) {
|
||||
notifications().notify({ group: "run", level: "error", title: tx`Cannot run '${config.name}'`, body: String(e) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function browserActions(sessions: readonly ServiceSession[]): ServiceItemAction[] {
|
||||
const urls = new Map<string, string>();
|
||||
for (const s of sessions) {
|
||||
if (!liveServiceSession(s)) continue;
|
||||
const port = (s.run && isLive(s.run) ? s.run.httpPort : undefined) ?? serviceDebugger(s)?.httpPort;
|
||||
if (port && Number.isInteger(port) && port > 0 && port < 65536) urls.set(`http://localhost:${port}`, s.config.name);
|
||||
}
|
||||
return [
|
||||
{
|
||||
id: "open-browser",
|
||||
title: "Open in Browser",
|
||||
icon: "web",
|
||||
disabled: !urls.size,
|
||||
run: () => {
|
||||
if (urls.size === 1) return void openUrl(urls.keys().next().value!);
|
||||
if (!urls.size) return;
|
||||
showContextMenu(
|
||||
{ x: window.innerWidth / 2, y: window.innerHeight / 2 },
|
||||
[...urls].map(([url, name]) => ({
|
||||
label: `${name} — ${url}`,
|
||||
verbatim: true,
|
||||
icon: "web",
|
||||
run: () => void openUrl(url),
|
||||
})),
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function itemActions(item: ServiceTreeItem): ServiceItemAction[] {
|
||||
const r = resolve(item);
|
||||
const running = r.sessions.filter(liveServiceSession);
|
||||
if (!r.config && r.kind) {
|
||||
const kind = r.kind;
|
||||
const entries = groupConfigs(kind).filter((c) => !isDebugOnly(c));
|
||||
return [
|
||||
{
|
||||
id: "run-all",
|
||||
title: "Run All",
|
||||
icon: "run",
|
||||
disabled: !entries.some((c) => !running.some((s) => s.config.id === c.id)),
|
||||
run: () => startAll(kind, false),
|
||||
},
|
||||
{
|
||||
id: "restart-all",
|
||||
title: "Restart All",
|
||||
icon: "rerun",
|
||||
disabled: !entries.length,
|
||||
run: () => startAll(kind, true),
|
||||
},
|
||||
{
|
||||
id: "stop-all",
|
||||
title: "Stop All",
|
||||
icon: "stop",
|
||||
disabled: !running.length,
|
||||
run: () => stopServiceSessions(running),
|
||||
},
|
||||
...browserActions(r.sessions),
|
||||
];
|
||||
}
|
||||
const stop = {
|
||||
id: "stop",
|
||||
title: "Stop",
|
||||
@@ -391,6 +499,7 @@ function actions(item: ServiceTreeItem): ServiceItemAction[] {
|
||||
run: () => run(r),
|
||||
},
|
||||
{ id: "debug", title: "Debug", icon: "debug", disabled: !!debugging && !debugging.rerun, run: () => debug(r) },
|
||||
...browserActions(r.sessions),
|
||||
stop,
|
||||
...(isUpdatable(r.config)
|
||||
? [
|
||||
@@ -406,6 +515,44 @@ function actions(item: ServiceTreeItem): ServiceItemAction[] {
|
||||
];
|
||||
}
|
||||
|
||||
const SERVICE_ACTIONS = [
|
||||
{ id: "run-all", action: "run.services.runAll", title: "Run All", icon: "run" },
|
||||
{ id: "restart-all", action: "run.services.restartAll", title: "Restart All", icon: "rerun" },
|
||||
{ id: "stop-all", action: "run.services.stopAll", title: "Stop All", icon: "stop" },
|
||||
{ id: "open-browser", action: "run.services.openBrowser", title: "Open in Browser", icon: "web" },
|
||||
];
|
||||
|
||||
export function registerRunServiceActions(): () => void {
|
||||
return registerActions(
|
||||
SERVICE_ACTIONS.map(({ id, action, title, icon }) => ({
|
||||
id: action,
|
||||
title,
|
||||
icon,
|
||||
category: "Run",
|
||||
hidden: true,
|
||||
run: async ({ arg }) => {
|
||||
if (typeof arg !== "string" || (!arg.startsWith(TYPE_PREFIX) && !arg.startsWith(CONFIG_PREFIX))) return;
|
||||
const action = itemActions({ key: arg, label: "", icon: "" }).find((a) => a.id === id);
|
||||
if (action && !action.disabled) await action.run();
|
||||
},
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
function actions(item: ServiceTreeItem): ServiceItemAction[] {
|
||||
return itemActions(item).map((entry) => {
|
||||
const action = SERVICE_ACTIONS.find((a) => a.id === entry.id);
|
||||
return action
|
||||
? {
|
||||
...entry,
|
||||
run: async () => {
|
||||
await executeAction(action.action, item.key);
|
||||
},
|
||||
}
|
||||
: entry;
|
||||
});
|
||||
}
|
||||
|
||||
function menu(item: ServiceTreeItem): MenuItem[] {
|
||||
const items: MenuItem[] = actions(item).map((a) => ({
|
||||
id: a.id,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useProject } from "@/platform/project";
|
||||
import { validateSettings } from "@/features/settings/model/schema";
|
||||
import { adoptSession, appendConsole, useDebug } from "@/features/debugger/state";
|
||||
import { DebugServiceConsole } from "@/features/debugger/ui/DebugConsole";
|
||||
import { runServiceProvider } from "@/features/run/services";
|
||||
import { registerRunServiceActions, runServiceProvider } from "@/features/run/services";
|
||||
import { onRunExited, runtime, startRun, useRun } from "@/features/run/store";
|
||||
import {
|
||||
provideToolWindowService,
|
||||
@@ -53,6 +53,7 @@ const summary = (id: number, extra: Partial<DebugServiceSession> = {}): DebugSer
|
||||
...extra,
|
||||
});
|
||||
let dispose: () => void;
|
||||
let disposeActions: () => void;
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
useProject.setState({
|
||||
@@ -68,10 +69,12 @@ beforeEach(() => {
|
||||
expanded: new Set(["run:type:spring-boot"]),
|
||||
});
|
||||
dispose = registerServiceProvider(runServiceProvider);
|
||||
disposeActions = registerRunServiceActions();
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
dispose();
|
||||
disposeActions();
|
||||
useDebugRunSessions.setState({ sessions: [], serviceSessions: [], Console: null });
|
||||
});
|
||||
const select = () =>
|
||||
@@ -238,13 +241,13 @@ it("shows the owning Run output for attached debugging and overrides both sets o
|
||||
expect(actions.getByRole("button", { name: "Stop" })).toBeEnabled();
|
||||
});
|
||||
|
||||
it("group Stop stops all roots but terminates a shared Maven process only once", async () => {
|
||||
it("group Stop All stops all roots but terminates a shared Maven process only once", async () => {
|
||||
const first = summary(1, { processId: 7 }),
|
||||
second = summary(2, { processId: 7 });
|
||||
useDebugRunSessions.setState({ serviceSessions: [first, second] });
|
||||
render(<ServicesTreeView />);
|
||||
fireEvent.mouseDown(within(screen.getByRole("tree")).getByText("Spring Boot").closest('[role="treeitem"]')!);
|
||||
fireEvent.click(toolbar().getByRole("button", { name: "Stop" }));
|
||||
fireEvent.click(toolbar().getByRole("button", { name: "Stop All" }));
|
||||
await vi.waitFor(() => expect(second.stop).toHaveBeenCalledOnce());
|
||||
expect(first.stop).toHaveBeenCalledExactlyOnceWith({ stopProcess: false });
|
||||
expect(second.stop).toHaveBeenCalledExactlyOnceWith();
|
||||
|
||||
@@ -494,6 +494,10 @@
|
||||
"Could not copy. Select the details below and copy them.": "Could not copy. Select the details below and copy them.",
|
||||
"Startup error details": "Startup error details",
|
||||
"Startup did not finish within 30 seconds.": "Startup did not finish within 30 seconds.",
|
||||
"Run All": "Run All",
|
||||
"Restart All": "Restart All",
|
||||
"Stop All": "Stop All",
|
||||
"Open in Browser": "Open in Browser",
|
||||
"Build Module": "Build Module",
|
||||
"Build Artifacts": "Build Artifacts",
|
||||
"Build Module '{target}'": "Build Module '{target}'",
|
||||
|
||||
@@ -6836,6 +6836,7 @@
|
||||
"Could not copy. Select the details below and copy them.": "无法复制。请选择下方详情并手动复制。",
|
||||
"Startup error details": "启动错误详情",
|
||||
"Startup did not finish within 30 seconds.": "启动未在 30 秒内完成。",
|
||||
"Restart All": "全部重启",
|
||||
"Build Module '{target}'": "构建模块“{target}”",
|
||||
"Build cancelled": "构建已取消",
|
||||
"Build did not start": "构建未启动",
|
||||
|
||||
Reference in new issue
Заблокувати користувача