327 lines
13 KiB
JavaScript
327 lines
13 KiB
JavaScript
import { describe, expect, test } from "bun:test";
|
|
import { normalizePluginPanelChrome, parseExtensionManifest, parseMarketplaceRegistry } from "./index.ts";
|
|
|
|
describe("extension manifests", () => {
|
|
test("normalizes a plugin manifest", () => {
|
|
expect(parseExtensionManifest({
|
|
type: "plugin",
|
|
id: "org.edgeever.example",
|
|
name: "Example",
|
|
version: "1.0.0",
|
|
apiVersion: "2",
|
|
settingsUi: "host",
|
|
entry: "./main.js",
|
|
permissions: ["notes:read", "notes:read", "templates:read", "templates:write", "schedules", "ui:commands", "ui:navigation", "ui:embeds"],
|
|
})).toMatchObject({ permissions: ["notes:read", "templates:read", "templates:write", "schedules", "ui:commands", "ui:navigation", "ui:embeds"] });
|
|
});
|
|
|
|
test("normalizes localized plugin metadata", () => {
|
|
const manifest = parseExtensionManifest({
|
|
type: "plugin",
|
|
id: "org.edgeever.localized",
|
|
name: "Localized",
|
|
version: "1.0.0",
|
|
apiVersion: "2",
|
|
settingsUi: "host",
|
|
description: "English description",
|
|
locales: {
|
|
zh_CN: { name: "本地化插件", description: " 中文说明 " },
|
|
ja: { description: "日本語の説明", html: "<script>" },
|
|
},
|
|
entry: "./main.js",
|
|
permissions: [],
|
|
});
|
|
|
|
expect(manifest.locales).toEqual({
|
|
"zh-CN": { name: "本地化插件", description: "中文说明" },
|
|
ja: { description: "日本語の説明" },
|
|
});
|
|
});
|
|
|
|
test("rejects invalid localized plugin metadata", () => {
|
|
const base = {
|
|
type: "plugin",
|
|
id: "org.edgeever.localized",
|
|
name: "Localized",
|
|
version: "1.0.0",
|
|
apiVersion: "2",
|
|
settingsUi: "host",
|
|
entry: "./main.js",
|
|
permissions: [],
|
|
};
|
|
expect(() => parseExtensionManifest({ ...base, locales: { invalid_locale_tag: { description: "Bad" } } })).toThrow("BCP 47");
|
|
expect(() => parseExtensionManifest({ ...base, locales: { "zh-CN": {} } })).toThrow("name or description");
|
|
});
|
|
|
|
test("rejects unsupported capability metadata", () => {
|
|
expect(() => parseExtensionManifest({
|
|
type: "plugin",
|
|
id: "org.edgeever.bad",
|
|
name: "Bad",
|
|
version: "1.0.0",
|
|
apiVersion: "2",
|
|
settingsUi: "host",
|
|
entry: "./main.js",
|
|
permissions: ["database:raw"],
|
|
})).toThrow("Unsupported plugin permission");
|
|
});
|
|
|
|
test("requires API v2 plugins to use host-rendered settings", () => {
|
|
const base = {
|
|
type: "plugin",
|
|
id: "org.edgeever.policy",
|
|
name: "Policy",
|
|
version: "1.0.0",
|
|
entry: "./main.js",
|
|
permissions: [],
|
|
};
|
|
expect(() => parseExtensionManifest({ ...base, apiVersion: "1", settingsUi: "host" })).toThrow("Unsupported plugin API version");
|
|
expect(() => parseExtensionManifest({ ...base, apiVersion: "2" })).toThrow('settingsUi to be "host"');
|
|
expect(() => parseExtensionManifest({ ...base, apiVersion: "2", settingsUi: "custom" })).toThrow('settingsUi to be "host"');
|
|
});
|
|
|
|
test("rejects unknown theme tokens", () => {
|
|
expect(() => parseExtensionManifest({
|
|
type: "theme",
|
|
id: "org.edgeever.theme",
|
|
name: "Theme",
|
|
version: "1.0.0",
|
|
themeApiVersion: "1",
|
|
modes: ["light"],
|
|
light: { "unsafe.selector": "body" },
|
|
})).toThrow("Unsupported theme token");
|
|
});
|
|
|
|
test("rejects CSS injection through theme values", () => {
|
|
expect(() => parseExtensionManifest({
|
|
type: "theme",
|
|
id: "org.edgeever.remote-theme",
|
|
name: "Remote Theme",
|
|
version: "1.0.0",
|
|
themeApiVersion: "1",
|
|
modes: ["light"],
|
|
light: { "color.background": "url(https://example.com/track)" },
|
|
})).toThrow("must use #RRGGBB");
|
|
});
|
|
|
|
test("allows direct network plugins without a static host list", () => {
|
|
expect(parseExtensionManifest({
|
|
type: "plugin",
|
|
id: "org.edgeever.network",
|
|
name: "Network",
|
|
version: "1.0.0",
|
|
apiVersion: "2",
|
|
settingsUi: "host",
|
|
entry: "./main.js",
|
|
permissions: ["network"],
|
|
})).toMatchObject({ permissions: ["network"] });
|
|
});
|
|
|
|
test("normalizes an Obsidian-style trusted plugin without capability declarations", () => {
|
|
expect(parseExtensionManifest({
|
|
type: "plugin",
|
|
id: "org.edgeever.trusted",
|
|
name: "Trusted",
|
|
version: "1.0.0",
|
|
apiVersion: "2",
|
|
settingsUi: "host",
|
|
entry: "./main.js",
|
|
})).toMatchObject({ permissions: [] });
|
|
});
|
|
|
|
test("allows public read-only network plugins without a static host list", () => {
|
|
expect(parseExtensionManifest({
|
|
type: "plugin",
|
|
id: "org.edgeever.public-network",
|
|
name: "Public network",
|
|
version: "1.0.0",
|
|
apiVersion: "2",
|
|
settingsUi: "host",
|
|
entry: "./main.js",
|
|
permissions: ["network", "network:public"],
|
|
})).toMatchObject({ permissions: ["network", "network:public"] });
|
|
});
|
|
|
|
test("normalizes a host-rendered plugin settings schema", () => {
|
|
const manifest = parseExtensionManifest({
|
|
type: "plugin",
|
|
id: "org.edgeever.settings",
|
|
name: "Settings",
|
|
version: "1.0.0",
|
|
apiVersion: "2",
|
|
settingsUi: "host",
|
|
entry: "./main.js",
|
|
permissions: [],
|
|
settings: {
|
|
fields: [
|
|
{
|
|
key: "endpoint",
|
|
type: "text",
|
|
label: "Endpoint",
|
|
locales: { "zh-CN": { label: "服务地址", description: "连接地址", placeholder: "请输入地址" } },
|
|
default: "https://example.com",
|
|
className: "plugin-owned-layout",
|
|
style: { color: "red" },
|
|
html: "<script>alert(1)</script>",
|
|
},
|
|
{ key: "token", type: "secret", label: "Token", required: true },
|
|
{ key: "limit", type: "number", label: "Limit", default: 10, min: 1, max: 100 },
|
|
{ key: "enabled", type: "boolean", label: "Enabled", default: true },
|
|
{ key: "format", type: "select", label: "Format", options: [{ value: "md", label: "Markdown" }], locales: { ja: { label: "形式", options: { md: "マークダウン" } } } },
|
|
{
|
|
key: "topics.ai",
|
|
type: "boolean",
|
|
label: "AI",
|
|
default: true,
|
|
className: "plugin-owned-layout",
|
|
list: {
|
|
title: "AI sources",
|
|
actionLabel: "View sources",
|
|
className: "ignored",
|
|
items: [
|
|
{ title: "OpenAI News", description: "openai.com", html: "<script>" },
|
|
{ title: "Google AI" },
|
|
],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
});
|
|
|
|
expect(manifest.type).toBe("plugin");
|
|
expect(manifest.settings?.fields).toHaveLength(6);
|
|
expect(manifest.settings?.fields[0]).toMatchObject({ key: "endpoint", default: "https://example.com" });
|
|
expect(manifest.settings?.fields[0].locales?.["zh-CN"]).toEqual({ label: "服务地址", description: "连接地址", placeholder: "请输入地址" });
|
|
expect(manifest.settings?.fields[4].locales?.ja?.options).toEqual({ md: "マークダウン" });
|
|
expect(manifest.settings?.fields[0]).not.toHaveProperty("className");
|
|
expect(manifest.settings?.fields[0]).not.toHaveProperty("style");
|
|
expect(manifest.settings?.fields[0]).not.toHaveProperty("html");
|
|
expect(manifest.settings?.fields[5]).toMatchObject({
|
|
key: "topics.ai",
|
|
list: {
|
|
title: "AI sources",
|
|
actionLabel: "View sources",
|
|
items: [{ title: "OpenAI News", description: "openai.com" }, { title: "Google AI" }],
|
|
},
|
|
});
|
|
expect(manifest.settings?.fields[5].list).not.toHaveProperty("className");
|
|
expect(manifest.settings?.fields[5].list.items[0]).not.toHaveProperty("html");
|
|
});
|
|
|
|
test("rejects unsafe or ambiguous plugin settings", () => {
|
|
const base = {
|
|
type: "plugin",
|
|
id: "org.edgeever.settings-invalid",
|
|
name: "Settings",
|
|
version: "1.0.0",
|
|
apiVersion: "2",
|
|
settingsUi: "host",
|
|
entry: "./main.js",
|
|
permissions: [],
|
|
};
|
|
expect(() => parseExtensionManifest({
|
|
...base,
|
|
settings: { fields: [{ key: "token", type: "secret", label: "Token", default: "plaintext" }] },
|
|
})).toThrow("cannot declare a default");
|
|
expect(() => parseExtensionManifest({
|
|
...base,
|
|
settings: { fields: [{ key: "mode", type: "select", label: "Mode", options: [{ value: "a", label: "A" }, { value: "a", label: "Again" }] }] },
|
|
})).toThrow("duplicate select value");
|
|
expect(() => parseExtensionManifest({
|
|
...base,
|
|
settings: { fields: [{ key: "topics.ai", type: "boolean", label: "AI", list: { items: [] } }] },
|
|
})).toThrow("between 1 and 100 items");
|
|
expect(() => parseExtensionManifest({
|
|
...base,
|
|
settings: { fields: [{ key: "mode", type: "select", label: "Mode", options: [{ value: "a", label: "A" }], locales: { ja: { options: { missing: "不明" } } } }] },
|
|
})).toThrow("invalid options");
|
|
});
|
|
});
|
|
|
|
describe("marketplace registry", () => {
|
|
test("accepts a verified GitHub entry", () => {
|
|
expect(parseMarketplaceRegistry({
|
|
registryVersion: "1",
|
|
updatedAt: "2026-08-16T00:00:00.000Z",
|
|
entries: [{
|
|
id: "org.edgeever.example",
|
|
name: "Example",
|
|
description: "Example plugin",
|
|
locales: { "zh-CN": { description: "示例插件" } },
|
|
author: "EdgeEver",
|
|
publisher: "edgeever",
|
|
category: "Productivity",
|
|
repositoryUrl: "https://github.com/edgeever/example",
|
|
distribution: { type: "github", repositoryUrl: "https://github.com/edgeever/example" },
|
|
verification: { version: "1.0.0", checksums: { manifestJson: "a".repeat(64), mainJs: "b".repeat(64) } },
|
|
}],
|
|
}).entries[0]).toMatchObject({
|
|
id: "org.edgeever.example",
|
|
publisher: "edgeever",
|
|
locales: { "zh-CN": { description: "示例插件" } },
|
|
verification: { version: "1.0.0" },
|
|
});
|
|
});
|
|
|
|
test("rejects unknown automatic-update publishers", () => {
|
|
expect(() => parseMarketplaceRegistry({
|
|
registryVersion: "1",
|
|
updatedAt: "2026-08-16T00:00:00.000Z",
|
|
entries: [{
|
|
id: "org.edgeever.example",
|
|
name: "Example",
|
|
description: "Example plugin",
|
|
author: "Example",
|
|
publisher: "third-party",
|
|
category: "Productivity",
|
|
repositoryUrl: "https://github.com/example/plugin",
|
|
distribution: { type: "github", repositoryUrl: "https://github.com/example/plugin" },
|
|
verification: { version: "1.0.0", checksums: { manifestJson: "a".repeat(64) } },
|
|
}],
|
|
})).toThrow("invalid publisher");
|
|
});
|
|
|
|
test("rejects duplicate plugin ids", () => {
|
|
const entry = {
|
|
id: "org.edgeever.example",
|
|
name: "Example",
|
|
description: "Example plugin",
|
|
author: "EdgeEver",
|
|
category: "Productivity",
|
|
repositoryUrl: "https://github.com/edgeever/example",
|
|
distribution: { type: "github", repositoryUrl: "https://github.com/edgeever/example" },
|
|
verification: { version: "1.0.0", checksums: { manifestJson: "a".repeat(64) } },
|
|
};
|
|
expect(() => parseMarketplaceRegistry({ registryVersion: "1", updatedAt: "2026-08-16T00:00:00Z", entries: [entry, entry] })).toThrow("Duplicate");
|
|
});
|
|
});
|
|
|
|
describe("panel chrome", () => {
|
|
test("keeps known chrome fields and drops unknown toolbar items", () => {
|
|
const onAction = () => {};
|
|
const chrome = normalizePluginPanelChrome({
|
|
header: { title: "Tasks", description: null, actions: [{ id: "refresh", label: "Refresh", variant: "primary" }] },
|
|
toolbar: [
|
|
{ type: "search", key: "q", placeholder: "Search", value: "ship" },
|
|
{ type: "tabs", key: "view", value: "open", options: [{ value: "open", label: "Open" }, { value: "done", label: "Done" }] },
|
|
{ type: "weird", key: "nope" },
|
|
],
|
|
empty: { title: "Nothing here", description: "Create a task" },
|
|
onAction,
|
|
});
|
|
expect(chrome.header).toEqual({ title: "Tasks", description: null, actions: [{ id: "refresh", label: "Refresh", variant: "primary" }] });
|
|
expect(chrome.toolbar).toEqual([
|
|
{ type: "search", key: "q", placeholder: "Search", value: "ship" },
|
|
{ type: "tabs", key: "view", value: "open", options: [{ value: "open", label: "Open" }, { value: "done", label: "Done" }] },
|
|
]);
|
|
expect(chrome.empty).toEqual({ title: "Nothing here", description: "Create a task" });
|
|
expect(chrome.onAction).toBe(onAction);
|
|
});
|
|
|
|
test("falls back to the first tab option when the value is unknown", () => {
|
|
expect(normalizePluginPanelChrome({
|
|
toolbar: [{ type: "tabs", key: "view", value: "missing", options: [{ value: "open", label: "Open" }] }],
|
|
}).toolbar).toEqual([{ type: "tabs", key: "view", value: "open", options: [{ value: "open", label: "Open" }] }]);
|
|
});
|
|
});
|