feat(plugins): add batch install update and uninstall actions
This commit is contained in:
13 files changed
+988
-138
No files matched your search
+460
@@ -0,0 +1,460 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { createApp, nextTick, type App, type ComponentPublicInstance } from "vue";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { InstalledPlugin, PluginRepositoryCatalogResult } from "@/types/database";
|
||||
import type { MarketplacePluginListing } from "@/lib/plugins/pluginMarketplace";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
listPlugins: vi.fn(),
|
||||
listPluginTrustedKeys: vi.fn(),
|
||||
listPluginRepositories: vi.fn(),
|
||||
fetchPluginMarketplaceCatalogs: vi.fn(),
|
||||
installMarketplacePlugin: vi.fn(),
|
||||
installPluginPackage: vi.fn(),
|
||||
installPluginPackageFromUrl: vi.fn(),
|
||||
uninstallPlugin: vi.fn(),
|
||||
rollbackPlugin: vi.fn(),
|
||||
savePluginRepository: vi.fn(),
|
||||
removePluginRepository: vi.fn(),
|
||||
savePluginTrustedKey: vi.fn(),
|
||||
removePluginTrustedKey: vi.fn(),
|
||||
toast: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/backend/api", () => mocks);
|
||||
vi.mock("@/composables/useToast", () => ({ useToast: () => ({ toast: mocks.toast }) }));
|
||||
vi.mock("@/stores/connectionStore", () => ({ useConnectionStore: () => ({ connections: [] }) }));
|
||||
vi.mock("@/stores/queryStore", () => ({ useQueryStore: () => ({}) }));
|
||||
vi.mock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.mock("vue-i18n", async () => {
|
||||
const { ref } = await import("vue");
|
||||
return { useI18n: () => ({ locale: ref("en"), t: (key: string, values = {}) => `${key}:${JSON.stringify(values)}` }) };
|
||||
});
|
||||
vi.mock("@/components/ui/button", async () => ({ Button: (await import("@/components/grid/__tests__/vueHostHarness")).createPassthroughStub("Button", "button") }));
|
||||
vi.mock("@/components/ui/badge", async () => ({ Badge: (await import("@/components/grid/__tests__/vueHostHarness")).createPassthroughStub("Badge", "span") }));
|
||||
vi.mock("@/components/ui/input", async () => ({ Input: (await import("@/components/grid/__tests__/vueHostHarness")).createPassthroughStub("Input", "input") }));
|
||||
vi.mock("@/components/ui/label", async () => ({ Label: (await import("@/components/grid/__tests__/vueHostHarness")).createPassthroughStub("Label", "label") }));
|
||||
vi.mock("@/components/ui/switch", async () => ({ Switch: (await import("@/components/grid/__tests__/vueHostHarness")).createPassthroughStub("Switch") }));
|
||||
vi.mock("@/components/ui/select", async () => {
|
||||
const { createPassthroughStub } = await import("@/components/grid/__tests__/vueHostHarness");
|
||||
const stub = createPassthroughStub("Select");
|
||||
return { Select: stub, SelectContent: stub, SelectItem: stub, SelectTrigger: stub, SelectValue: stub };
|
||||
});
|
||||
vi.mock("@/components/ui/tabs", async () => {
|
||||
const { createPassthroughStub } = await import("@/components/grid/__tests__/vueHostHarness");
|
||||
const stub = createPassthroughStub("Tabs");
|
||||
return { Tabs: stub, TabsContent: stub, TabsList: stub, TabsTrigger: stub };
|
||||
});
|
||||
vi.mock("@/components/ui/tooltip", async () => {
|
||||
const { createPassthroughStub } = await import("@/components/grid/__tests__/vueHostHarness");
|
||||
const stub = createPassthroughStub("Tooltip");
|
||||
return { Tooltip: stub, TooltipContent: stub, TooltipTrigger: stub };
|
||||
});
|
||||
vi.mock("@/components/plugins/PluginIcon.vue", async () => ({ default: (await import("@/components/grid/__tests__/vueHostHarness")).createPassthroughStub("PluginIcon") }));
|
||||
|
||||
import PluginContributionsPanel from "@/components/plugins/PluginContributionsPanel.vue";
|
||||
|
||||
type PanelState = {
|
||||
batchRunning: boolean;
|
||||
batchMode: boolean;
|
||||
marketplaceViewMode: "grid" | "list";
|
||||
marketplaceRepositoryId: string;
|
||||
marketplaceListings: MarketplacePluginListing[];
|
||||
catalogResults: PluginRepositoryCatalogResult[];
|
||||
installedPlugins: InstalledPlugin[];
|
||||
selectedListingKeys: Set<string>;
|
||||
selectedInstalledIds: Set<string>;
|
||||
selectedPluginId: string;
|
||||
installUrl: string;
|
||||
repositoryId: string;
|
||||
repositoryName: string;
|
||||
repositoryCatalogUrl: string;
|
||||
trustedKeyId: string;
|
||||
trustedPublicKey: string;
|
||||
error: string;
|
||||
toggleListingSelection: (listing: MarketplacePluginListing) => void;
|
||||
selectAllUpdatable: () => void;
|
||||
runBatchInstallUpdate: () => Promise<void>;
|
||||
runBatchUninstall: () => Promise<void>;
|
||||
installMarketplaceListing: (listing: MarketplacePluginListing) => Promise<void>;
|
||||
installPlugin: (source: string) => Promise<void>;
|
||||
installPluginFromUrl: () => Promise<void>;
|
||||
uninstallSelectedPlugin: () => Promise<void>;
|
||||
rollbackSelectedPlugin: () => Promise<void>;
|
||||
saveRepository: () => Promise<void>;
|
||||
toggleRepository: (repository: PluginRepositoryCatalogResult["repository"]) => Promise<void>;
|
||||
removeRepository: (repository: PluginRepositoryCatalogResult["repository"]) => Promise<void>;
|
||||
saveTrustedKey: () => Promise<void>;
|
||||
removeTrustedKey: (keyId: string) => Promise<void>;
|
||||
toggleBatchMode: () => void;
|
||||
toggleInstalledSelection: (pluginId: string) => void;
|
||||
};
|
||||
|
||||
function installed(id: string, version = "1.0.0"): InstalledPlugin {
|
||||
return {
|
||||
manifest: { manifest_version: 1, id, name: id, version, publisher: "DBX", description: "", engines: { dbx: "", host_api: "" }, permissions: [], entrypoints: {}, contributions: [], drivers: [], protocol_version: 1 },
|
||||
compatibility: { compatible: true, errors: [], warnings: [], target: "darwin-arm64" },
|
||||
};
|
||||
}
|
||||
|
||||
function catalog(repositoryId: string, ids: string[], version = "3.0.0"): PluginRepositoryCatalogResult {
|
||||
return {
|
||||
repository: { id: repositoryId, name: repositoryId, kind: "custom", enabled: true, managed: false },
|
||||
target: "darwin-arm64",
|
||||
catalog: {
|
||||
catalogVersion: 1,
|
||||
repository: { id: repositoryId, name: repositoryId },
|
||||
plugins: ids.map((id) => ({
|
||||
id,
|
||||
name: id,
|
||||
description: "",
|
||||
publisher: "DBX",
|
||||
verified: false,
|
||||
tags: [],
|
||||
permissions: [],
|
||||
latestVersion: version,
|
||||
versions: [{ version, artifacts: [{ target: "darwin-arm64", url: "https://example.invalid/plugin.dbxp", sha256: "a".repeat(64), signingKeyId: "test.key" }] }],
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T) => void;
|
||||
let reject!: (cause: unknown) => void;
|
||||
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
||||
resolve = resolvePromise;
|
||||
reject = rejectPromise;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
let app: App;
|
||||
let host: HTMLElement;
|
||||
let state: PanelState;
|
||||
|
||||
async function flushUi() {
|
||||
for (let index = 0; index < 8; index++) {
|
||||
await Promise.resolve();
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
|
||||
function button(key: string): HTMLButtonElement {
|
||||
const match = [...host.querySelectorAll("button")].find((element) => element.textContent?.includes(`pluginPlatform.${key}:`));
|
||||
expect(match, key).toBeDefined();
|
||||
return match!;
|
||||
}
|
||||
|
||||
const mutationApis = [mocks.installMarketplacePlugin, mocks.uninstallPlugin, mocks.rollbackPlugin, mocks.installPluginPackage, mocks.installPluginPackageFromUrl];
|
||||
|
||||
function mutationCount() {
|
||||
return mutationApis.reduce((count, mock) => count + mock.mock.calls.length, 0);
|
||||
}
|
||||
|
||||
async function expectBusyControls() {
|
||||
for (const view of ["grid", "list"] as const) {
|
||||
state.marketplaceViewMode = view;
|
||||
await nextTick();
|
||||
for (const key of ["batchInstallUpdate", "batchUninstall", "marketplaceStatus.update", "uninstall", "rollback", "installPackage", "installFromUrl"]) {
|
||||
expect(button(key).disabled, `${view}: ${key}`).toBe(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetAllMocks();
|
||||
localStorage.clear();
|
||||
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({}));
|
||||
vi.stubGlobal("confirm", vi.fn().mockReturnValue(true));
|
||||
mocks.listPlugins.mockResolvedValue([installed("a"), installed("b"), installed("c")]);
|
||||
mocks.listPluginTrustedKeys.mockResolvedValue([]);
|
||||
mocks.listPluginRepositories.mockResolvedValue([]);
|
||||
mocks.fetchPluginMarketplaceCatalogs.mockResolvedValue([catalog("first", ["a", "b", "c"])]);
|
||||
for (const mock of mutationApis) mock.mockResolvedValue({ plugin: installed("a", "3.0.0") });
|
||||
mocks.uninstallPlugin.mockResolvedValue([]);
|
||||
mocks.savePluginRepository.mockResolvedValue([]);
|
||||
mocks.removePluginRepository.mockResolvedValue([]);
|
||||
mocks.savePluginTrustedKey.mockResolvedValue([]);
|
||||
mocks.removePluginTrustedKey.mockResolvedValue([]);
|
||||
host = document.createElement("div");
|
||||
document.body.append(host);
|
||||
app = createApp(PluginContributionsPanel);
|
||||
const instance = app.mount(host) as ComponentPublicInstance & { $: { setupState: PanelState } };
|
||||
state = instance.$.setupState;
|
||||
await flushUi();
|
||||
state.batchMode = true;
|
||||
state.selectedPluginId = "a";
|
||||
state.selectedListingKeys = new Set(["first:a"]);
|
||||
state.selectedInstalledIds = new Set(["a"]);
|
||||
state.installUrl = "https://example.invalid/plugin.dbxp";
|
||||
state.repositoryId = "custom";
|
||||
state.repositoryName = "Custom";
|
||||
state.repositoryCatalogUrl = "https://example.invalid/catalog.json";
|
||||
state.trustedKeyId = "custom";
|
||||
state.trustedPublicKey = "public-key";
|
||||
mocks.listPlugins.mockClear();
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
app?.unmount();
|
||||
host?.remove();
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("PluginContributionsPanel batch source validation", () => {
|
||||
it.each(["first", "second"])("does not silently resolve selected source conflicts by filtering to %s", async (repositoryId) => {
|
||||
state.catalogResults = [catalog("first", ["a"]), catalog("second", ["a"], "2.0.0")];
|
||||
state.selectAllUpdatable();
|
||||
state.marketplaceRepositoryId = repositoryId;
|
||||
await state.runBatchInstallUpdate();
|
||||
expect(mutationCount()).toBe(0);
|
||||
expect(state.selectedListingKeys.size).toBe(2);
|
||||
state.toggleListingSelection(state.marketplaceListings.find((listing) => listing.repository.id !== repositoryId)!);
|
||||
await state.runBatchInstallUpdate();
|
||||
expect(mocks.installMarketplacePlugin).toHaveBeenCalledExactlyOnceWith({ repositoryId, pluginId: "a", version: repositoryId === "first" ? "3.0.0" : "2.0.0" });
|
||||
});
|
||||
|
||||
it.each(["2.0.0", "3.0.0"].flatMap((version) => ["manual", "all"].map((selection) => ({ version, selection }))))("rejects $selection duplicate sources at $version before any mutation", async ({ version, selection }) => {
|
||||
state.catalogResults = [catalog("first", ["a", "b"]), catalog("second", ["b"], version)];
|
||||
state.selectedListingKeys = new Set();
|
||||
if (selection === "all") state.selectAllUpdatable();
|
||||
else for (const listing of state.marketplaceListings) state.toggleListingSelection(listing);
|
||||
await state.runBatchInstallUpdate();
|
||||
expect(mutationCount()).toBe(0);
|
||||
expect(mocks.listPlugins).not.toHaveBeenCalled();
|
||||
expect(state.selectedListingKeys.size).toBe(3);
|
||||
expect(state.batchRunning).toBe(false);
|
||||
expect(mocks.toast).toHaveBeenCalledWith(expect.stringContaining('pluginPlatform.batchDuplicateSources:{"names":"b"}'), 8000);
|
||||
|
||||
state.toggleListingSelection(state.marketplaceListings.find((listing) => listing.key === "first:b")!);
|
||||
await state.runBatchInstallUpdate();
|
||||
expect(mocks.installMarketplacePlugin.mock.calls.map(([request]) => request)).toEqual([
|
||||
{ repositoryId: "first", pluginId: "a", version: "3.0.0" },
|
||||
{ repositoryId: "second", pluginId: "b", version },
|
||||
]);
|
||||
expect(state.selectedListingKeys.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
const batches = ["install", "uninstall"] as const;
|
||||
type Batch = (typeof batches)[number];
|
||||
const singles = ["marketplace", "uninstall", "rollback", "package", "url"] as const;
|
||||
type Single = (typeof singles)[number];
|
||||
|
||||
function startBatch(batch: Batch) {
|
||||
return batch === "install" ? state.runBatchInstallUpdate() : state.runBatchUninstall();
|
||||
}
|
||||
|
||||
function startSingle(single: Single) {
|
||||
switch (single) {
|
||||
case "marketplace":
|
||||
return state.installMarketplaceListing(state.marketplaceListings[0]);
|
||||
case "uninstall":
|
||||
return state.uninstallSelectedPlugin();
|
||||
case "rollback":
|
||||
return state.rollbackSelectedPlugin();
|
||||
case "package":
|
||||
return state.installPlugin("plugin.dbxp");
|
||||
case "url":
|
||||
return state.installPluginFromUrl();
|
||||
}
|
||||
}
|
||||
|
||||
function singleApi(single: Single) {
|
||||
return { marketplace: mocks.installMarketplacePlugin, uninstall: mocks.uninstallPlugin, rollback: mocks.rollbackPlugin, package: mocks.installPluginPackage, url: mocks.installPluginPackageFromUrl }[single];
|
||||
}
|
||||
|
||||
describe("PluginContributionsPanel mutation exclusion", () => {
|
||||
it.each(singles.flatMap((single) => [false, true].map((reject) => ({ single, reject }))))("releases single $single exclusion after rejection=$reject", async ({ single, reject }) => {
|
||||
if (reject) singleApi(single).mockRejectedValueOnce(new Error("single denied"));
|
||||
await startSingle(single);
|
||||
expect(mutationCount()).toBe(1);
|
||||
if (reject) expect(mocks.toast).toHaveBeenLastCalledWith("single denied", expect.any(Number));
|
||||
await nextTick();
|
||||
expect(button("batchInstallUpdate").disabled).toBe(false);
|
||||
await state.runBatchInstallUpdate();
|
||||
expect(mutationCount()).toBe(2);
|
||||
expect(state.batchRunning).toBe(false);
|
||||
});
|
||||
|
||||
it.each(batches.flatMap((batch) => singles.map((single) => ({ batch, single }))))("blocks $single while batch $batch is pending", async ({ batch, single }) => {
|
||||
const pending = deferred<unknown>();
|
||||
(batch === "install" ? mocks.installMarketplacePlugin : mocks.uninstallPlugin).mockReturnValueOnce(pending.promise);
|
||||
const running = startBatch(batch);
|
||||
try {
|
||||
await startSingle(single);
|
||||
expect(mutationCount()).toBe(1);
|
||||
await expectBusyControls();
|
||||
} finally {
|
||||
pending.resolve(batch === "install" ? { plugin: installed("a") } : []);
|
||||
await running;
|
||||
}
|
||||
});
|
||||
|
||||
it.each(singles.flatMap((single) => batches.map((batch) => ({ single, batch }))))("blocks batch $batch while single $single is pending", async ({ single, batch }) => {
|
||||
const pending = deferred<unknown>();
|
||||
singleApi(single).mockReturnValueOnce(pending.promise);
|
||||
const running = startSingle(single);
|
||||
try {
|
||||
await startBatch(batch);
|
||||
expect(mutationCount()).toBe(1);
|
||||
await expectBusyControls();
|
||||
} finally {
|
||||
pending.resolve(single === "uninstall" ? [] : { plugin: installed("a") });
|
||||
await running;
|
||||
}
|
||||
});
|
||||
|
||||
it.each(batches.flatMap((first) => batches.map((second) => ({ first, second }))))("blocks batch $second while batch $first is pending", async ({ first, second }) => {
|
||||
const pending = deferred<unknown>();
|
||||
(first === "install" ? mocks.installMarketplacePlugin : mocks.uninstallPlugin).mockReturnValueOnce(pending.promise);
|
||||
const running = startBatch(first);
|
||||
try {
|
||||
await startBatch(second);
|
||||
expect(mutationCount()).toBe(1);
|
||||
} finally {
|
||||
pending.resolve(first === "install" ? { plugin: installed("a") } : []);
|
||||
await running;
|
||||
}
|
||||
});
|
||||
|
||||
it.each(batches)("keeps selections stable and blocks settings mutations during batch %s", async (batch) => {
|
||||
const pending = deferred<unknown>();
|
||||
(batch === "install" ? mocks.installMarketplacePlugin : mocks.uninstallPlugin).mockReturnValueOnce(pending.promise);
|
||||
const running = startBatch(batch);
|
||||
try {
|
||||
state.toggleBatchMode();
|
||||
state.selectAllUpdatable();
|
||||
state.toggleListingSelection(state.marketplaceListings[1]);
|
||||
state.toggleInstalledSelection("b");
|
||||
expect(state.batchMode).toBe(true);
|
||||
expect([...state.selectedListingKeys]).toEqual(["first:a"]);
|
||||
expect([...state.selectedInstalledIds]).toEqual(["a"]);
|
||||
await state.saveRepository();
|
||||
await state.toggleRepository(catalog("custom", []).repository);
|
||||
await state.removeRepository(catalog("custom", []).repository);
|
||||
await state.saveTrustedKey();
|
||||
await state.removeTrustedKey("custom");
|
||||
for (const mock of [mocks.savePluginRepository, mocks.removePluginRepository, mocks.savePluginTrustedKey, mocks.removePluginTrustedKey]) expect(mock).not.toHaveBeenCalled();
|
||||
await nextTick();
|
||||
for (const key of ["batchDone", "batchSelectAllUpdatable", "addRepository"]) expect(button(key).disabled).toBe(true);
|
||||
} finally {
|
||||
pending.resolve(batch === "install" ? { plugin: installed("a") } : []);
|
||||
await running;
|
||||
}
|
||||
});
|
||||
|
||||
it.each(batches)("blocks batch %s while repository settings are pending", async (batch) => {
|
||||
const pending = deferred<[]>();
|
||||
mocks.savePluginRepository.mockReturnValueOnce(pending.promise);
|
||||
const running = state.saveRepository();
|
||||
try {
|
||||
await startBatch(batch);
|
||||
expect(mutationCount()).toBe(0);
|
||||
await expectBusyControls();
|
||||
} finally {
|
||||
pending.resolve([]);
|
||||
await running;
|
||||
}
|
||||
await startBatch(batch);
|
||||
expect(mutationCount()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PluginContributionsPanel completed batch outcomes", () => {
|
||||
it.each(batches.flatMap((batch) => [0, 1, 3].map((failures) => ({ batch, failures }))))("preserves batch $batch outcomes with $failures failures when refresh rejects", async ({ batch, failures }) => {
|
||||
state.selectAllUpdatable();
|
||||
state.selectedInstalledIds = new Set(["a", "b", "c"]);
|
||||
const mutation = batch === "install" ? mocks.installMarketplacePlugin : mocks.uninstallPlugin;
|
||||
for (let index = 0; index < failures; index++) mutation.mockRejectedValueOnce(index === 0 ? "denied" : new Error("denied"));
|
||||
const refresh = deferred<InstalledPlugin[]>();
|
||||
mocks.listPlugins.mockReturnValueOnce(refresh.promise);
|
||||
const running = startBatch(batch);
|
||||
await flushUi();
|
||||
const summary = `pluginPlatform.${failures ? "batchSummaryWithFailures" : "batchSummary"}:${JSON.stringify({ success: 3 - failures, failed: failures, names: ["a", "b", "c"].slice(0, failures).join("、") })}`;
|
||||
try {
|
||||
expect(mocks.toast).toHaveBeenLastCalledWith(summary, failures ? 8000 : 4000);
|
||||
expect(state.selectedListingKeys.size).toBe(0);
|
||||
expect(state.selectedInstalledIds.size).toBe(0);
|
||||
expect(state.batchRunning).toBe(true);
|
||||
await state.installMarketplaceListing(state.marketplaceListings[0]);
|
||||
expect(mutationCount()).toBe(3);
|
||||
} finally {
|
||||
refresh.reject(new Error("refresh offline"));
|
||||
await expect(running).resolves.toBeUndefined();
|
||||
}
|
||||
expect(mutation).toHaveBeenCalledTimes(3);
|
||||
expect(mocks.toast).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.toast).toHaveBeenLastCalledWith(summary, failures ? 8000 : 4000);
|
||||
expect(state.error).toBe('pluginPlatform.batchRefreshFailed:{"error":"refresh offline"}');
|
||||
await nextTick();
|
||||
expect(host.textContent).toContain(state.error);
|
||||
expect(state.batchRunning).toBe(false);
|
||||
state.selectedListingKeys = new Set(["first:a"]);
|
||||
await state.runBatchInstallUpdate();
|
||||
expect(state.error).toBe("");
|
||||
});
|
||||
|
||||
it.each(batches)("runs batch %s in order, continues after rejection and refreshes once", async (batch) => {
|
||||
state.selectAllUpdatable();
|
||||
state.selectedInstalledIds = new Set(["a", "b", "c"]);
|
||||
const mutation = batch === "install" ? mocks.installMarketplacePlugin : mocks.uninstallPlugin;
|
||||
const first = deferred<unknown>();
|
||||
mutation.mockReturnValueOnce(first.promise).mockRejectedValueOnce(new Error("denied"));
|
||||
const running = startBatch(batch);
|
||||
await flushUi();
|
||||
expect(mutation).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.listPlugins).not.toHaveBeenCalled();
|
||||
first.resolve(batch === "install" ? { plugin: installed("a") } : []);
|
||||
await running;
|
||||
expect(mutation.mock.calls.map(([request]) => (batch === "install" ? request.pluginId : request))).toEqual(["a", "b", "c"]);
|
||||
expect(mocks.listPlugins).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.toast).toHaveBeenLastCalledWith('pluginPlatform.batchSummaryWithFailures:{"success":2,"failed":1,"names":"b"}', 8000);
|
||||
expect(state.error).toBe("");
|
||||
expect(state.batchRunning).toBe(false);
|
||||
expect(state.selectedListingKeys.size).toBe(0);
|
||||
expect(state.selectedInstalledIds.size).toBe(0);
|
||||
});
|
||||
|
||||
it("does nothing for empty selections or cancelled uninstall", async () => {
|
||||
state.selectedListingKeys = new Set();
|
||||
await state.runBatchInstallUpdate();
|
||||
vi.mocked(window.confirm).mockReturnValueOnce(false);
|
||||
await state.runBatchUninstall();
|
||||
expect(state.selectedInstalledIds.size).toBe(1);
|
||||
state.selectedInstalledIds = new Set();
|
||||
await state.runBatchUninstall();
|
||||
expect(mutationCount()).toBe(0);
|
||||
expect(mocks.listPlugins).not.toHaveBeenCalled();
|
||||
expect(mocks.toast).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("ignores installed and unsupported listings in single and batch handlers", async () => {
|
||||
state.catalogResults = [catalog("first", ["a"], "1.0.0"), { ...catalog("second", ["b"]), target: "unsupported" }];
|
||||
state.selectedListingKeys = new Set();
|
||||
for (const listing of state.marketplaceListings) {
|
||||
state.toggleListingSelection(listing);
|
||||
await state.installMarketplaceListing(listing);
|
||||
}
|
||||
state.selectAllUpdatable();
|
||||
await state.runBatchInstallUpdate();
|
||||
expect(state.selectedListingKeys.size).toBe(0);
|
||||
expect(mutationCount()).toBe(0);
|
||||
});
|
||||
|
||||
it("installs newly selected plugins through the rendered batch button", async () => {
|
||||
state.installedPlugins = [];
|
||||
state.selectedListingKeys = new Set();
|
||||
state.toggleListingSelection(state.marketplaceListings[0]);
|
||||
await nextTick();
|
||||
button("batchInstallUpdate").click();
|
||||
await flushUi();
|
||||
expect(mocks.installMarketplacePlugin).toHaveBeenCalledExactlyOnceWith({ repositoryId: "first", pluginId: "a", version: "3.0.0" });
|
||||
expect(mocks.toast).toHaveBeenLastCalledWith('pluginPlatform.batchSummary:{"success":1,"failed":0,"names":""}', 4000);
|
||||
expect(state.batchRunning).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user