f8708da1e0
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
166 lines
5.8 KiB
TypeScript
166 lines
5.8 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { pluginRegistry } from "@/lib/plugins/registry";
|
|
import type { PluginTaskMenuContext } from "@/lib/plugins/types";
|
|
import { buildPrimaryPluginEntries } from "./task-menu-actions";
|
|
|
|
// Key identity. A plugin entry key is a palette row's React key and cmdk value,
|
|
// so it has to be unique for any strings a bundle can author -- including the
|
|
// shapes that make a naive escape and a bare separator collide.
|
|
const PLUGIN_ID = "kandev-plugin-keys";
|
|
|
|
const CONTEXT: PluginTaskMenuContext = {
|
|
workspaceId: "ws-1",
|
|
taskId: "task-1",
|
|
taskTitle: "Fix the bug",
|
|
workflowStepId: "step-1",
|
|
presentation: "desktop",
|
|
};
|
|
|
|
afterEach(() => {
|
|
pluginRegistry.unregisterPlugin(PLUGIN_ID);
|
|
pluginRegistry.unregisterPlugin("p");
|
|
pluginRegistry.unregisterPlugin("p-a");
|
|
pluginRegistry.unregisterPlugin("p-q");
|
|
pluginRegistry.unregisterPlugin("p-pick");
|
|
pluginRegistry.unregisterPlugin("p\uABCD");
|
|
});
|
|
|
|
describe("plugin entry keys — escapes and separators", () => {
|
|
it("keeps keys distinct when an id ends where another's escape token begins", () => {
|
|
// The separator cannot be read as the start of an escape: with a `%XX`-style
|
|
// token and a bare `%` separator, plugin `p\uABCD` with action `abcd` and
|
|
// plugin `p` with action `abcd\uABCD` both produced `plugin-primary-p%abcd%abcd`.
|
|
pluginRegistry.forPlugin("p\uABCD").registerTaskMenuAction({
|
|
id: "abcd",
|
|
label: "Shifted",
|
|
group: "primary",
|
|
run: vi.fn(),
|
|
});
|
|
pluginRegistry.forPlugin("p").registerTaskMenuAction({
|
|
id: "abcd\uABCD",
|
|
label: "Trailing",
|
|
group: "primary",
|
|
run: vi.fn(),
|
|
});
|
|
pluginRegistry.forPlugin("p").registerTaskMenuAction({
|
|
id: "abcd",
|
|
label: "Parent",
|
|
group: "primary",
|
|
items: () => [
|
|
{ id: "\uABCD", label: "Child unit", run: vi.fn() },
|
|
{ id: "%3A", label: "Looks like an escape", run: vi.fn() },
|
|
],
|
|
run: vi.fn(),
|
|
});
|
|
|
|
const entries = buildPrimaryPluginEntries({ context: CONTEXT });
|
|
const keys = entries.flatMap((entry) =>
|
|
entry
|
|
? [entry.key, ...(entry.kind === "submenu" ? entry.children.map((c) => c.key) : [])]
|
|
: [],
|
|
);
|
|
|
|
expect(keys).toHaveLength(5);
|
|
expect(new Set(keys).size).toBe(5);
|
|
|
|
for (const pluginId of ["p\uABCD", "p"]) pluginRegistry.unregisterPlugin(pluginId);
|
|
});
|
|
|
|
it("keeps keys distinct when an id carries the child delimiter or a dash", () => {
|
|
// A `#` inside an action id must not forge a sibling child's key, and a `-`
|
|
// must not let one (pluginId, actionId) pair spell another's.
|
|
pluginRegistry.forPlugin("p").registerTaskMenuAction({
|
|
id: "quick",
|
|
label: "Quick",
|
|
group: "primary",
|
|
items: () => [{ id: "x", label: "Child", run: vi.fn() }],
|
|
run: vi.fn(),
|
|
});
|
|
pluginRegistry.forPlugin("p").registerTaskMenuAction({
|
|
id: "quick#x",
|
|
label: "Hashed",
|
|
group: "primary",
|
|
run: vi.fn(),
|
|
});
|
|
pluginRegistry.forPlugin("p").registerTaskMenuAction({
|
|
id: "a-b",
|
|
label: "Dashed",
|
|
group: "primary",
|
|
run: vi.fn(),
|
|
});
|
|
pluginRegistry.forPlugin("p-a").registerTaskMenuAction({
|
|
id: "b",
|
|
label: "Shifted",
|
|
group: "primary",
|
|
run: vi.fn(),
|
|
});
|
|
|
|
const entries = buildPrimaryPluginEntries({ context: CONTEXT });
|
|
const keys = entries.flatMap((entry) =>
|
|
entry
|
|
? [entry.key, ...(entry.kind === "submenu" ? entry.children.map((c) => c.key) : [])]
|
|
: [],
|
|
);
|
|
|
|
expect(keys).toHaveLength(5);
|
|
expect(new Set(keys).size).toBe(5);
|
|
// A flat key never contains the raw child delimiter, so it cannot equal a child key.
|
|
expect(keys.filter((key) => key.includes("#"))).toHaveLength(1);
|
|
|
|
for (const pluginId of ["p", "p-a"]) pluginRegistry.unregisterPlugin(pluginId);
|
|
});
|
|
});
|
|
|
|
describe("plugin entry keys — dash joins", () => {
|
|
it("keeps child keys distinct when two actions' ids dash-join identically", () => {
|
|
// Every key here is a dash-join (before this fix) of plugin-controlled ids,
|
|
// and the palette flattens all plugin entries into one list where a key is
|
|
// both a React key and cmdk's value. Both pairs below collided under a
|
|
// dash-join: a child against a flat action's key, and two actions whose id
|
|
// joins agree (`p` + `q-x` versus `p-q` + `x`).
|
|
pluginRegistry.forPlugin("p").registerTaskMenuAction({
|
|
id: "pick",
|
|
label: "Pick",
|
|
group: "primary",
|
|
items: () => [{ id: "x", label: "A", run: vi.fn() }],
|
|
run: vi.fn(),
|
|
});
|
|
pluginRegistry.forPlugin("p-pick").registerTaskMenuAction({
|
|
id: "pick",
|
|
label: "Pick x",
|
|
group: "primary",
|
|
items: () => [{ id: "pick-x", label: "B", run: vi.fn() }],
|
|
run: vi.fn(),
|
|
});
|
|
pluginRegistry.forPlugin("p").registerTaskMenuAction({
|
|
id: "q-x",
|
|
label: "Q x",
|
|
group: "primary",
|
|
items: () => [{ id: "c", label: "C", run: vi.fn() }],
|
|
run: vi.fn(),
|
|
});
|
|
pluginRegistry.forPlugin("p-q").registerTaskMenuAction({
|
|
id: "x",
|
|
label: "X",
|
|
group: "primary",
|
|
items: () => [{ id: "c", label: "D", run: vi.fn() }],
|
|
run: vi.fn(),
|
|
});
|
|
|
|
const entries = buildPrimaryPluginEntries({ context: CONTEXT });
|
|
const keys = entries.map((entry) => entry.key);
|
|
const childKeys = entries.flatMap((entry) =>
|
|
entry && entry.kind === "submenu" ? entry.children.map((child) => child.key) : [],
|
|
);
|
|
|
|
expect(keys).toHaveLength(4);
|
|
expect(new Set(keys).size).toBe(4);
|
|
expect(childKeys).toHaveLength(4);
|
|
expect(new Set(childKeys).size).toBe(4);
|
|
// A child key can never equal a flat action key either.
|
|
expect(childKeys.filter((key) => keys.includes(key))).toEqual([]);
|
|
|
|
for (const pluginId of ["p", "p-pick", "p-q"]) pluginRegistry.unregisterPlugin(pluginId);
|
|
});
|
|
});
|