дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
Services followed configuration detection order, so frequently used services could not be kept together. Services can now be dragged before or after a sibling, with an insertion indicator and edge scrolling, or moved with Move Up/Down from the toolbar and context menu. The order is stored per project location and type, separately from configuration definitions. New configurations append, windows stay in sync, and a failed save keeps the order for the session with a warning. Sorting in Tree is opt-in, so other trees are unchanged. Co-authored-by: wangxga <[email protected]>
116 рядки
4.3 KiB
TypeScript
116 рядки
4.3 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { act, renderHook } from "@testing-library/react";
|
|
import { moveServiceOrder, orderServices, readServiceOrder, saveServiceOrder, useServiceOrder } from "./serviceOrder";
|
|
|
|
beforeEach(() => localStorage.clear());
|
|
|
|
describe("service display order", () => {
|
|
it("orders known IDs, appends new ones and leaves configurations untouched", () => {
|
|
const entries = [{ id: "gateway" }, { id: "portal" }, { id: "new" }];
|
|
expect(orderServices(entries, ["gone", "portal", "gateway"]).map((x) => x.id)).toEqual([
|
|
"portal",
|
|
"gateway",
|
|
"new",
|
|
]);
|
|
expect(entries.map((x) => x.id)).toEqual(["gateway", "portal", "new"]);
|
|
});
|
|
|
|
it("moves before or after the target while retaining temporarily hidden IDs", () => {
|
|
expect(moveServiceOrder(["a", "hidden", "b"], ["a", "b", "new"], "b", "a", "before")).toEqual([
|
|
"b",
|
|
"a",
|
|
"hidden",
|
|
"new",
|
|
]);
|
|
expect(moveServiceOrder([], ["a", "b", "c"], "a", "b", "after")).toEqual(["b", "a", "c"]);
|
|
});
|
|
|
|
it("ignores missing, self and stale drop targets", () => {
|
|
for (const [source, target] of [
|
|
["a", "a"],
|
|
["gone", "b"],
|
|
["a", "gone"],
|
|
]) {
|
|
expect(moveServiceOrder(["b", "a"], ["a", "b"], source!, target!, "before")).toEqual(["b", "a"]);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("service order storage", () => {
|
|
it("isolates project locations and normalizes malformed records and duplicate IDs", () => {
|
|
localStorage.setItem(
|
|
"openidea.services.runOrder:/app",
|
|
JSON.stringify({ npm: ["b", "b", 4, "", "a"], python: null }),
|
|
);
|
|
expect(readServiceOrder("/app")).toEqual({ npm: ["b", "a"] });
|
|
expect(readServiceOrder("ssh://other/app")).toEqual({});
|
|
localStorage.setItem("openidea.services.runOrder:/app", "broken");
|
|
expect(readServiceOrder("/app")).toEqual({});
|
|
localStorage.setItem("openidea.services.runOrder:/app", "[]");
|
|
expect(readServiceOrder("/app")).toEqual({});
|
|
});
|
|
|
|
it("continues with default order when storage cannot be read", () => {
|
|
const read = vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
|
|
throw new Error("denied");
|
|
});
|
|
try {
|
|
expect(readServiceOrder("/denied")).toEqual({});
|
|
} finally {
|
|
read.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("updates mounted views, survives remounts, and reads changes from other windows", () => {
|
|
const { result, unmount } = renderHook(() => useServiceOrder("/persist"));
|
|
act(() => {
|
|
saveServiceOrder("/persist", { npm: ["b", "a"] });
|
|
});
|
|
expect(result.current).toEqual({ npm: ["b", "a"] });
|
|
unmount();
|
|
const next = renderHook(() => useServiceOrder("/persist"));
|
|
expect(next.result.current).toEqual({ npm: ["b", "a"] });
|
|
act(() => {
|
|
localStorage.setItem("openidea.services.runOrder:/persist", '{"npm":["a","b"]}');
|
|
window.dispatchEvent(
|
|
new StorageEvent("storage", { key: "openidea.services.runOrder:/persist", storageArea: localStorage }),
|
|
);
|
|
});
|
|
expect(next.result.current).toEqual({ npm: ["a", "b"] });
|
|
act(() => {
|
|
localStorage.clear();
|
|
window.dispatchEvent(new StorageEvent("storage", { key: null, storageArea: localStorage }));
|
|
});
|
|
expect(next.result.current).toEqual({});
|
|
});
|
|
|
|
it("switches projects without leaking the previous project's order", () => {
|
|
saveServiceOrder("/one", { npm: ["b", "a"] });
|
|
saveServiceOrder("/two", { python: ["y", "x"] });
|
|
const { result, rerender } = renderHook(({ location }) => useServiceOrder(location), {
|
|
initialProps: { location: "/one" },
|
|
});
|
|
expect(result.current).toEqual({ npm: ["b", "a"] });
|
|
rerender({ location: "/two" });
|
|
expect(result.current).toEqual({ python: ["y", "x"] });
|
|
});
|
|
|
|
it("keeps the in-memory order when writing storage fails", () => {
|
|
const { result, unmount } = renderHook(() => useServiceOrder("/unavailable"));
|
|
const write = vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
|
|
throw new Error("quota");
|
|
});
|
|
try {
|
|
act(() => {
|
|
expect(saveServiceOrder("/unavailable", { npm: ["b", "a"] })).toBe(false);
|
|
});
|
|
expect(result.current).toEqual({ npm: ["b", "a"] });
|
|
unmount();
|
|
const next = renderHook(() => useServiceOrder("/unavailable"));
|
|
expect(next.result.current).toEqual({ npm: ["b", "a"] });
|
|
} finally {
|
|
write.mockRestore();
|
|
}
|
|
});
|
|
});
|