diff --git a/deno.jsonc b/deno.jsonc index f8ac3c9..ed54e7e 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -7,7 +7,7 @@ "tasks": { "check": "deno check ./**/*.ts", "test": "deno test -A --parallel --shuffle --doc", - "test:coverage": "deno task test --coverage=.coverage", + "test:coverage": "deno test -A --parallel --shuffle --doc --coverage=.coverage --ignore=denops/fall/custom_test.ts", "coverage": "deno coverage .coverage --exclude=testdata/", "update": "deno run --allow-env --allow-read --allow-write=. --allow-run=git,deno --allow-net=deno.land,jsr.io,registry.npmjs.org jsr:@molt/cli ./**/*.ts", "update:write": "deno task -q update --write", diff --git a/denops/fall/custom_test.ts b/denops/fall/custom_test.ts new file mode 100644 index 0000000..10d4db7 --- /dev/null +++ b/denops/fall/custom_test.ts @@ -0,0 +1,249 @@ +import { test } from "jsr:@denops/test@^3.0.0"; +import { assertEquals } from "jsr:@std/assert@^1.0.0"; +import { describe, it } from "jsr:@std/testing@^1.0.0/bdd"; +import { join } from "jsr:@std/path@^1.0.8/join"; + +import { + getActionPickerParams, + getPickerParams, + getSetting, + listPickerNames, + loadUserCustom, +} from "./custom.ts"; + +describe("getSetting", () => { + it("should return default setting", () => { + const setting = getSetting(); + assertEquals(typeof setting.coordinator, "object"); + assertEquals(typeof setting.theme, "object"); + assertEquals(setting.theme.border.length, 8); + assertEquals(setting.theme.divider.length, 6); + }); +}); + +describe("getActionPickerParams", () => { + it("should return default action picker params", () => { + const params = getActionPickerParams(); + assertEquals(Array.isArray(params.matchers), true); + assertEquals(params.matchers.length, 1); + assertEquals(typeof params.coordinator, "object"); + }); +}); + +describe("getPickerParams", () => { + it("should return undefined for non-existent picker", () => { + const params = getPickerParams("non-existent"); + assertEquals(params, undefined); + }); +}); + +describe("listPickerNames", () => { + it("should return empty array initially", () => { + const names = listPickerNames(); + assertEquals(Array.isArray(names), true); + }); +}); + +// Tests that require real denops instance +test({ + mode: "all", + name: "loadUserCustom - default custom", + fn: async (denops) => { + await denops.cmd("let g:fall_custom_path = '/non/existent/path.ts'"); + + await loadUserCustom(denops); + + const setting = getSetting(); + assertEquals(typeof setting.coordinator, "object"); + assertEquals(typeof setting.theme, "object"); + }, +}); + +test({ + mode: "all", + name: "loadUserCustom - user custom", + fn: async (denops) => { + const tempDir = await Deno.makeTempDir(); + const customPath = join(tempDir, "custom.ts"); + + await Deno.writeTextFile( + customPath, + ` + export async function main({ refineSetting }) { + refineSetting({ + theme: { + border: ["1", "2", "3", "4", "5", "6", "7", "8"], + divider: ["a", "b", "c", "d", "e", "f"], + }, + }); + } + `, + ); + + await denops.cmd(`let g:fall_custom_path = '${customPath}'`); + await loadUserCustom(denops, { reload: true }); + + const setting = getSetting(); + assertEquals(setting.theme.border, [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + ]); + assertEquals(setting.theme.divider, ["a", "b", "c", "d", "e", "f"]); + + await Deno.remove(tempDir, { recursive: true }); + }, +}); + +test({ + mode: "all", + name: "loadUserCustom - reload option", + fn: async (denops) => { + const tempDir = await Deno.makeTempDir(); + const customPath = join(tempDir, "custom.ts"); + + // First version + await Deno.writeTextFile( + customPath, + ` + export async function main({ refineSetting }) { + refineSetting({ + theme: { + border: ["1", "2", "3", "4", "5", "6", "7", "8"], + divider: ["a", "b", "c", "d", "e", "f"], + }, + }); + } + `, + ); + + await denops.cmd(`let g:fall_custom_path = '${customPath}'`); + await loadUserCustom(denops); + + // Second version + await Deno.writeTextFile( + customPath, + ` + export async function main({ refineSetting }) { + refineSetting({ + theme: { + border: ["x", "x", "x", "x", "x", "x", "x", "x"], + divider: ["y", "y", "y", "y", "y", "y"], + }, + }); + } + `, + ); + + await loadUserCustom(denops, { reload: true }); + + const setting = getSetting(); + assertEquals(setting.theme.border, [ + "x", + "x", + "x", + "x", + "x", + "x", + "x", + "x", + ]); + assertEquals(setting.theme.divider, ["y", "y", "y", "y", "y", "y"]); + + await Deno.remove(tempDir, { recursive: true }); + }, +}); + +test({ + mode: "all", + name: "loadUserCustom - validate picker names", + fn: async (denops) => { + const tempDir = await Deno.makeTempDir(); + const customPath = join(tempDir, "custom.ts"); + + await Deno.writeTextFile( + customPath, + ` + import { fzf } from "jsr:@vim-fall/std@^0.10.0/builtin/matcher/fzf"; + + export async function main({ definePickerFromSource }) { + definePickerFromSource( + "@invalid", + { collect: async function* () {} }, + { + matchers: [fzf()], + actions: { default: { invoke: async () => {} } }, + defaultAction: "default", + } + ); + } + `, + ); + + await denops.cmd(`let g:fall_custom_path = '${customPath}'`); + + // Should not throw but log warning + await loadUserCustom(denops, { reload: true }); + + await Deno.remove(tempDir, { recursive: true }); + }, +}); + +test({ + mode: "all", + name: "loadUserCustom - validate action names", + fn: async (denops) => { + const tempDir = await Deno.makeTempDir(); + const customPath = join(tempDir, "custom.ts"); + + await Deno.writeTextFile( + customPath, + ` + import { fzf } from "jsr:@vim-fall/std@^0.10.0/builtin/matcher/fzf"; + + export async function main({ definePickerFromSource }) { + definePickerFromSource( + "test", + { collect: async function* () {} }, + { + matchers: [fzf()], + actions: { + "@invalid": { invoke: async () => {} }, + valid: { invoke: async () => {} }, + }, + defaultAction: "valid", + } + ); + } + `, + ); + + await denops.cmd(`let g:fall_custom_path = '${customPath}'`); + + // Should not throw but log warning + await loadUserCustom(denops, { reload: true }); + + await Deno.remove(tempDir, { recursive: true }); + }, +}); + +test({ + mode: "all", + name: "loadUserCustom - verbose option", + fn: async (denops) => { + // Test that verbose option doesn't throw errors + // We can't reliably intercept cmd calls on real Denops instances + await denops.cmd("let g:fall_custom_path = '/non/existent/path.ts'"); + + // Should not throw + await loadUserCustom(denops, { verbose: true, reload: true }); + + // If we reach here, the verbose option worked without errors + assertEquals(true, true); + }, +}); diff --git a/denops/fall/error_test.ts b/denops/fall/error_test.ts new file mode 100644 index 0000000..2fa4cca --- /dev/null +++ b/denops/fall/error_test.ts @@ -0,0 +1,164 @@ +import { DenopsStub } from "jsr:@denops/test@^3.0.0"; +import { assertEquals, assertInstanceOf } from "jsr:@std/assert@^1.0.0"; +import { describe, it } from "jsr:@std/testing@^1.0.0/bdd"; +import { AssertError } from "jsr:@core/unknownutil@^4.3.0/assert"; +import { ExpectedError, handleError, withHandleError } from "./error.ts"; + +describe("ExpectedError", () => { + it("should create an error with message", () => { + const error = new ExpectedError("test message"); + assertInstanceOf(error, Error); + assertEquals(error.message, "test message"); + assertEquals(error.source, undefined); + }); + + it("should create an error with message and source", () => { + const source = new Error("source error"); + const error = new ExpectedError("test message", source); + assertEquals(error.message, "test message"); + assertEquals(error.source, source); + }); +}); + +describe("handleError", () => { + it("should handle ExpectedError by showing echo message", async () => { + const called: string[] = []; + const denops = new DenopsStub({ + cmd(command: string): Promise { + called.push(command); + return Promise.resolve(); + }, + }); + const error = new ExpectedError("expected error"); + + await handleError(denops, error); + + assertEquals(called.length, 1); + assertEquals( + called[0], + "redraw | echohl Error | echomsg '[fall] expected error' | echohl None", + ); + }); + + it("should handle AssertError by showing echo message", async () => { + const called: string[] = []; + const denops = new DenopsStub({ + cmd(command: string): Promise { + called.push(command); + return Promise.resolve(); + }, + }); + const error = new AssertError("assertion failed"); + + await handleError(denops, error); + + assertEquals(called.length, 1); + assertEquals( + called[0], + "redraw | echohl Error | echomsg '[fall] assertion failed' | echohl None", + ); + }); + + it("should handle unknown errors by logging to console", async () => { + const called: string[] = []; + const denops = new DenopsStub({ + cmd(command: string): Promise { + called.push(command); + return Promise.resolve(); + }, + }); + const error = new Error("unknown error"); + const originalConsoleError = console.error; + let capturedError: unknown; + + console.error = (err: unknown) => { + capturedError = err; + }; + + try { + await handleError(denops, error); + assertEquals(capturedError, error); + assertEquals(called.length, 0); + } finally { + console.error = originalConsoleError; + } + }); +}); + +describe("withHandleError", () => { + it("should return the result when no error occurs", async () => { + const denops = new DenopsStub(); + const fn = (x: number, y: number) => x + y; + const wrapped = withHandleError(denops, fn); + + const result = await wrapped(1, 2); + assertEquals(result, 3); + }); + + it("should return the result for async functions", async () => { + const denops = new DenopsStub(); + const fn = async (x: number, y: number) => { + await Promise.resolve(); + return x + y; + }; + const wrapped = withHandleError(denops, fn); + + const result = await wrapped(1, 2); + assertEquals(result, 3); + }); + + it("should handle errors and return undefined", async () => { + const called: string[] = []; + const denops = new DenopsStub({ + cmd(command: string): Promise { + called.push(command); + return Promise.resolve(); + }, + }); + const fn = () => { + throw new ExpectedError("test error"); + }; + const wrapped = withHandleError(denops, fn); + + const result = await wrapped(); + assertEquals(result, undefined); + + // Wait for setTimeout to execute + await new Promise((resolve) => setTimeout(resolve, 10)); + + assertEquals(called.length, 1); + assertEquals( + called[0], + "redraw | echohl Error | echomsg '[fall] test error' | echohl None", + ); + }); + + it("should handle async function errors", async () => { + const denops = new DenopsStub(); + const fn = async () => { + await Promise.resolve(); + throw new Error("async error"); + }; + const wrapped = withHandleError(denops, fn); + + const originalConsoleError = console.error; + let capturedError: unknown; + + console.error = (err: unknown) => { + capturedError = err; + }; + + try { + const result = await wrapped(); + assertEquals(result, undefined); + + // Wait for setTimeout to execute + await new Promise((resolve) => setTimeout(resolve, 10)); + + assertInstanceOf(capturedError, Error); + assertEquals((capturedError as Error).message, "async error"); + } finally { + console.error = originalConsoleError; + } + }); +}); diff --git a/denops/fall/lib/polyfill_test.ts b/denops/fall/lib/polyfill_test.ts new file mode 100644 index 0000000..bbad757 --- /dev/null +++ b/denops/fall/lib/polyfill_test.ts @@ -0,0 +1,65 @@ +import { assertEquals, assertInstanceOf } from "jsr:@std/assert@^1.0.0"; +import { describe, it } from "jsr:@std/testing@^1.0.0/bdd"; + +import "./polyfill.ts"; + +describe("polyfill", () => { + it("should provide DisposableStack in globalThis", () => { + // deno-lint-ignore no-explicit-any + const stack = new (globalThis as any).DisposableStack(); + assertInstanceOf(stack, Object); + assertEquals(typeof stack.dispose, "function"); + assertEquals(typeof stack.use, "function"); + assertEquals(typeof stack.adopt, "function"); + assertEquals(typeof stack.defer, "function"); + assertEquals(typeof stack.move, "function"); + + // Clean up + stack.dispose(); + }); + + it("should provide AsyncDisposableStack in globalThis", () => { + // deno-lint-ignore no-explicit-any + const stack = new (globalThis as any).AsyncDisposableStack(); + assertInstanceOf(stack, Object); + assertEquals(typeof stack.disposeAsync, "function"); + assertEquals(typeof stack.use, "function"); + assertEquals(typeof stack.adopt, "function"); + assertEquals(typeof stack.defer, "function"); + assertEquals(typeof stack.move, "function"); + + // Clean up + stack.disposeAsync(); + }); + + it("should allow using DisposableStack functionality", () => { + // deno-lint-ignore no-explicit-any + const DisposableStack = (globalThis as any).DisposableStack; + const stack = new DisposableStack(); + let disposed = false; + + stack.defer(() => { + disposed = true; + }); + + assertEquals(disposed, false); + stack.dispose(); + assertEquals(disposed, true); + }); + + it("should allow using AsyncDisposableStack functionality", async () => { + // deno-lint-ignore no-explicit-any + const AsyncDisposableStack = (globalThis as any).AsyncDisposableStack; + const stack = new AsyncDisposableStack(); + let disposed = false; + + stack.defer(async () => { + await Promise.resolve(); + disposed = true; + }); + + assertEquals(disposed, false); + await stack.disposeAsync(); + assertEquals(disposed, true); + }); +}); diff --git a/denops/fall/main/submatch_test.ts b/denops/fall/main/submatch_test.ts new file mode 100644 index 0000000..2d210a1 --- /dev/null +++ b/denops/fall/main/submatch_test.ts @@ -0,0 +1,229 @@ +import { DenopsStub } from "jsr:@denops/test@^3.0.0"; +import { assertEquals } from "jsr:@std/assert@^1.0.0"; +import { describe, it } from "jsr:@std/testing@^1.0.0/bdd"; + +import { main } from "./submatch.ts"; + +describe("submatch", () => { + it("should register submatch dispatcher", () => { + const denops = new DenopsStub(); + main(denops); + + assertEquals(typeof denops.dispatcher["submatch"], "function"); + }); + + it("should handle invalid context gracefully", async () => { + const denops = new DenopsStub(); + main(denops); + + // withHandleError catches errors, so it won't reject but return undefined + const result = await denops.dispatcher["submatch"]({}, {}, {}); + assertEquals(result, undefined); + }); + + it("should handle invalid params gracefully", async () => { + const denops = new DenopsStub(); + main(denops); + + const validContext = { + filteredItems: [], + _submatch: { + pickerParams: { + name: "test", + source: { + collect: async function* () { + yield { value: "test" }; + }, + }, + actions: { default: { invoke: async () => {} } }, + defaultAction: "default", + matchers: [{ + match: async function* () { + yield { value: "test", score: 1 }; + }, + }], + }, + }, + }; + + // withHandleError catches errors, so it won't reject but return undefined + const result = await denops.dispatcher["submatch"](validContext, {}, {}); + assertEquals(result, undefined); + }); + + it("should process valid submatch call", async () => { + const denops = new DenopsStub(); + + // Track dispatch calls + let dispatchCalled = false; + let dispatchArgs: unknown[] = []; + + denops.dispatch = (name: string, ...args: unknown[]) => { + if (name === denops.name && args[0] === "picker") { + dispatchCalled = true; + dispatchArgs = args; + } + return Promise.resolve(undefined); + }; + + main(denops); + + const validContext = { + filteredItems: [{ value: "item1" }, { value: "item2" }], + _submatch: { + pickerParams: { + name: "test", + source: { + collect: async function* () { + yield { value: "test" }; + }, + }, + actions: { default: { invoke: async () => {} } }, + defaultAction: "default", + matchers: [{ + match: async function* () { + yield { value: "test", score: 1 }; + }, + }], + }, + }, + }; + + const validParams = { + matchers: [{ + match: async function* () { + yield { value: "test", score: 1 }; + }, + }], + }; + + await denops.dispatcher["submatch"](validContext, validParams, {}); + + assertEquals(dispatchCalled, true); + assertEquals(dispatchArgs[0], "picker"); + }); + + it("should forward optional parameters to picker", async () => { + const denops = new DenopsStub(); + + // Track dispatch calls + let capturedPickerParams: unknown; + + denops.dispatch = (name: string, ...args: unknown[]) => { + if (name === denops.name && args[0] === "picker") { + capturedPickerParams = args[2]; // The picker params + } + return Promise.resolve(undefined); + }; + + main(denops); + + const validContext = { + filteredItems: [{ value: "item1" }], + selectedItems: [{ value: "selected" }], + _submatch: { + pickerParams: { + name: "test", + source: { + collect: async function* () { + yield { value: "test" }; + }, + }, + actions: { default: { invoke: async () => {} } }, + defaultAction: "default", + matchers: [{ + match: async function* () { + yield { value: "test", score: 1 }; + }, + }], + }, + }, + }; + + const validParams = { + matchers: [{ + match: async function* () { + yield { value: "test", score: 1 }; + }, + }], + actions: { custom: { invoke: async () => {} } }, + defaultAction: "custom", + sorters: [{ + sort: async function* () { + yield { value: "test", score: 1 }; + }, + }], + renderers: [{ render: () => Promise.resolve([]) }], + previewers: [{ preview: () => Promise.resolve([]) }], + coordinator: { + style: () => {}, + layout: () => ({ width: 10, height: 10 }), + }, + theme: { + border: ["a", "b", "c", "d", "e", "f", "g", "h"], + divider: ["a", "b", "c", "d", "e", "f"], + }, + }; + + await denops.dispatcher["submatch"](validContext, validParams, {}); + + assertEquals(typeof capturedPickerParams, "object"); + const params = capturedPickerParams as Record; + assertEquals(params.defaultAction, "custom"); + assertEquals(Array.isArray(params.sorters), true); + assertEquals(Array.isArray(params.renderers), true); + assertEquals(Array.isArray(params.previewers), true); + assertEquals(typeof params.coordinator, "object"); + assertEquals(typeof params.theme, "object"); + }); + + it("should return true when picker returns true", async () => { + const denops = new DenopsStub(); + + denops.dispatch = (name: string, ...args: unknown[]) => { + if (name === denops.name && args[0] === "picker") { + return Promise.resolve(true); + } + return Promise.resolve(undefined); + }; + + main(denops); + + const validContext = { + filteredItems: [], + _submatch: { + pickerParams: { + name: "test", + source: { + collect: async function* () { + yield { value: "test" }; + }, + }, + actions: { default: { invoke: async () => {} } }, + defaultAction: "default", + matchers: [{ + match: async function* () { + yield { value: "test", score: 1 }; + }, + }], + }, + }, + }; + + const validParams = { + matchers: [{ + match: async function* () { + yield { value: "test", score: 1 }; + }, + }], + }; + + const result = await denops.dispatcher["submatch"]( + validContext, + validParams, + {}, + ); + + assertEquals(result, true); + }); +}); diff --git a/denops/fall/util/predicate_test.ts b/denops/fall/util/predicate_test.ts new file mode 100644 index 0000000..80a336a --- /dev/null +++ b/denops/fall/util/predicate_test.ts @@ -0,0 +1,324 @@ +import { assertEquals } from "jsr:@std/assert@^1.0.0"; +import { describe, it } from "jsr:@std/testing@^1.0.0/bdd"; +import type { Detail, Matcher, Theme } from "jsr:@vim-fall/core@^0.3.0"; + +import { + isAbortSignal, + isAction, + isCoordinator, + isCurator, + isIncrementalMatcher, + isMatcher, + isOptions, + isPickerParams, + isPreviewer, + isRenderer, + isSetting, + isSorter, + isSource, + isStringArray, + isTheme, +} from "./predicate.ts"; + +describe("isStringArray", () => { + it("should return true for string arrays", () => { + assertEquals(isStringArray([]), true); + assertEquals(isStringArray(["a", "b", "c"]), true); + assertEquals(isStringArray(["hello", "world"]), true); + }); + + it("should return false for non-string arrays", () => { + assertEquals(isStringArray([1, 2, 3]), false); + assertEquals(isStringArray(["a", 1, "c"]), false); + assertEquals(isStringArray(null), false); + assertEquals(isStringArray(undefined), false); + assertEquals(isStringArray("string"), false); + assertEquals(isStringArray({}), false); + }); +}); + +describe("isAbortSignal", () => { + it("should return true for AbortSignal instances", () => { + const controller = new AbortController(); + assertEquals(isAbortSignal(controller.signal), true); + }); + + it("should return false for non-AbortSignal values", () => { + assertEquals(isAbortSignal({}), false); + assertEquals(isAbortSignal(null), false); + assertEquals(isAbortSignal(undefined), false); + assertEquals(isAbortSignal("signal"), false); + }); +}); + +describe("isTheme", () => { + it("should return true for valid theme objects", () => { + const theme: Theme = { + border: ["a", "b", "c", "d", "e", "f", "g", "h"], + divider: ["a", "b", "c", "d", "e", "f"], + }; + assertEquals(isTheme(theme), true); + }); + + it("should return false for invalid theme objects", () => { + assertEquals(isTheme({}), false); + assertEquals(isTheme({ border: [], divider: [] }), false); + assertEquals(isTheme({ border: ["a"], divider: ["a"] }), false); + assertEquals( + isTheme({ + border: ["a", "b", "c", "d", "e", "f", "g"], + divider: ["a", "b", "c", "d", "e", "f"], + }), + false, + ); + }); +}); + +describe("isCoordinator", () => { + it("should return true for valid coordinator objects", () => { + const coordinator = { + style: () => {}, + layout: () => ({ width: 10, height: 10 }), + }; + assertEquals(isCoordinator(coordinator), true); + }); + + it("should return false for invalid coordinator objects", () => { + assertEquals(isCoordinator({}), false); + assertEquals(isCoordinator({ style: () => {} }), false); + assertEquals(isCoordinator({ layout: () => {} }), false); + assertEquals( + isCoordinator({ style: "not a function", layout: () => {} }), + false, + ); + }); +}); + +describe("isCurator", () => { + it("should return true for valid curator objects", () => { + const curator = { + curate: async function* () { + yield { value: "test" }; + }, + }; + assertEquals(isCurator(curator), true); + }); + + it("should return false for invalid curator objects", () => { + assertEquals(isCurator({}), false); + assertEquals(isCurator({ curate: "not a function" }), false); + }); +}); + +describe("isSource", () => { + it("should return true for valid source objects", () => { + const source = { + collect: async function* () { + yield { value: "test" }; + }, + }; + assertEquals(isSource(source), true); + }); + + it("should return false for invalid source objects", () => { + assertEquals(isSource({}), false); + assertEquals(isSource({ collect: "not a function" }), false); + }); +}); + +describe("isMatcher", () => { + it("should return true for valid matcher objects", () => { + const matcher = { + match: async function* () { + yield { value: "test", score: 1 }; + }, + }; + assertEquals(isMatcher(matcher), true); + }); + + it("should return false for invalid matcher objects", () => { + assertEquals(isMatcher({}), false); + assertEquals(isMatcher({ match: "not a function" }), false); + }); +}); + +describe("isSorter", () => { + it("should return true for valid sorter objects", () => { + const sorter = { + sort: async function* () { + yield { value: "test", score: 1 }; + }, + }; + assertEquals(isSorter(sorter), true); + }); + + it("should return false for invalid sorter objects", () => { + assertEquals(isSorter({}), false); + assertEquals(isSorter({ sort: "not a function" }), false); + }); +}); + +describe("isRenderer", () => { + it("should return true for valid renderer objects", () => { + const renderer = { + render: () => Promise.resolve([]), + }; + assertEquals(isRenderer(renderer), true); + }); + + it("should return false for invalid renderer objects", () => { + assertEquals(isRenderer({}), false); + assertEquals(isRenderer({ render: "not a function" }), false); + }); +}); + +describe("isPreviewer", () => { + it("should return true for valid previewer objects", () => { + const previewer = { + preview: () => Promise.resolve([]), + }; + assertEquals(isPreviewer(previewer), true); + }); + + it("should return false for invalid previewer objects", () => { + assertEquals(isPreviewer({}), false); + assertEquals(isPreviewer({ preview: "not a function" }), false); + }); +}); + +describe("isAction", () => { + it("should return true for valid action objects", () => { + const action = { + invoke: () => Promise.resolve(), + }; + assertEquals(isAction(action), true); + }); + + it("should return false for invalid action objects", () => { + assertEquals(isAction({}), false); + assertEquals(isAction({ invoke: "not a function" }), false); + }); +}); + +describe("isSetting", () => { + it("should return true for valid setting objects", () => { + const setting = { + coordinator: { + style: () => {}, + layout: () => ({ width: 10, height: 10 }), + }, + theme: { + border: ["a", "b", "c", "d", "e", "f", "g", "h"], + divider: ["a", "b", "c", "d", "e", "f"], + }, + }; + assertEquals(isSetting(setting), true); + }); + + it("should return false for invalid setting objects", () => { + assertEquals(isSetting({}), false); + assertEquals(isSetting({ coordinator: {} }), false); + assertEquals(isSetting({ theme: {} }), false); + }); +}); + +describe("isPickerParams", () => { + it("should return true for valid picker params", () => { + const params = { + name: "test", + source: { + collect: async function* () { + yield { value: "test" }; + }, + }, + actions: { + default: { invoke: async () => {} }, + }, + defaultAction: "default", + matchers: [{ match: async function* () {} }], + }; + assertEquals(isPickerParams(params), true); + }); + + it("should return true for params with optional fields", () => { + const params = { + name: "test", + source: { + collect: async function* () { + yield { value: "test" }; + }, + }, + actions: { + default: { invoke: async () => {} }, + }, + defaultAction: "default", + matchers: [{ match: async function* () {} }], + sorters: [{ sort: async function* () {} }], + renderers: [{ render: () => Promise.resolve([]) }], + previewers: [{ preview: () => Promise.resolve([]) }], + coordinator: { + style: () => {}, + layout: () => ({ width: 10, height: 10 }), + }, + theme: { + border: ["a", "b", "c", "d", "e", "f", "g", "h"], + divider: ["a", "b", "c", "d", "e", "f"], + }, + }; + assertEquals(isPickerParams(params), true); + }); + + it("should return false for invalid picker params", () => { + assertEquals(isPickerParams({}), false); + assertEquals(isPickerParams({ name: "test" }), false); + assertEquals( + isPickerParams({ + name: "test", + source: { + collect: async function* () { + yield { value: "test" }; + }, + }, + }), + false, + ); + }); +}); + +describe("isOptions", () => { + it("should return true for valid options", () => { + assertEquals(isOptions({}), true); + assertEquals(isOptions({ signal: new AbortController().signal }), true); + }); + + it("should return false for invalid options", () => { + assertEquals(isOptions({ signal: "not a signal" }), false); + assertEquals(isOptions(null), false); + assertEquals(isOptions(undefined), false); + }); +}); + +describe("isIncrementalMatcher", () => { + it("should return true for incremental matchers", () => { + const matcher = { + match: async function* () {}, + incremental: true, + }; + assertEquals(isIncrementalMatcher(matcher as Matcher), true); + }); + + it("should return false for non-incremental matchers", () => { + const matcher = { + match: async function* () {}, + incremental: false, + }; + assertEquals(isIncrementalMatcher(matcher as Matcher), false); + }); + + it("should return false when incremental property is not set", () => { + const matcher = { + match: async function* () {}, + }; + assertEquals(isIncrementalMatcher(matcher as Matcher), false); + }); +});