diff --git a/conf.ts b/conf.ts index 38763e1..ea86d7b 100644 --- a/conf.ts +++ b/conf.ts @@ -79,3 +79,13 @@ export function getConfig(): Config { }; return conf; } + +/** @internal for test */ +function resetConfig(newConf?: Config): void { + conf = newConf; +} + +/** @internal */ +export const _internal = { + resetConfig, +}; diff --git a/conf_test.ts b/conf_test.ts new file mode 100644 index 0000000..88b7da2 --- /dev/null +++ b/conf_test.ts @@ -0,0 +1,129 @@ +import { + assert, + assertEquals, + assertObjectMatch, + assertThrows, +} from "jsr:@std/assert@0.225.1"; +import { stub } from "jsr:@std/testing@0.224/mock"; +import { basename, isAbsolute } from "jsr:@std/path@0.224.0"; +import { _internal, getConfig } from "./conf.ts"; + +const ENV_VARS: Readonly> = { + DENOPS_TEST_DENOPS_PATH: "denops.vim", + DENOPS_TEST_VIM_EXECUTABLE: undefined, + DENOPS_TEST_NVIM_EXECUTABLE: undefined, + DENOPS_TEST_VERBOSE: undefined, + DENOPS_TEST_CONNECT_TIMEOUT: undefined, +}; + +function stubEnvVars(envVars: Readonly>) { + return stub(Deno.env, "get", (name) => envVars[name]); +} + +function stubConfModule(): Disposable { + const savedConf = getConfig(); + _internal.resetConfig(undefined); + return { + [Symbol.dispose]() { + _internal.resetConfig(savedConf); + }, + }; +} + +Deno.test("getConfig() throws if DENOPS_TEST_DENOPS_PATH env var is not set", () => { + using _module = stubConfModule(); + using _env = stubEnvVars({ ...ENV_VARS, DENOPS_TEST_DENOPS_PATH: undefined }); + assertThrows( + () => { + getConfig(); + }, + Error, + "'DENOPS_TEST_DENOPS_PATH' is required", + ); +}); + +Deno.test("getConfig() returns `{ denopsPath: ... }` with resolved DENOPS_TEST_DENOPS_PATH env var", () => { + using _module = stubConfModule(); + using _env = stubEnvVars({ ...ENV_VARS, DENOPS_TEST_DENOPS_PATH: "foo" }); + const actual = getConfig(); + assert(isAbsolute(actual.denopsPath), "`denopsPath` should be absolute path"); + assertEquals(basename(actual.denopsPath), "foo"); +}); + +Deno.test("getConfig() returns `{ vimExecutable: 'vim' }` if DENOPS_TEST_VIM_EXECUTABLE env var is not set", () => { + using _module = stubConfModule(); + using _env = stubEnvVars({ + ...ENV_VARS, + DENOPS_TEST_VIM_EXECUTABLE: undefined, + }); + const actual = getConfig(); + assertObjectMatch(actual, { vimExecutable: "vim" }); +}); + +Deno.test("getConfig() returns `{ vimExecutable: ... }` with DENOPS_TEST_VIM_EXECUTABLE env var", () => { + using _module = stubConfModule(); + using _env = stubEnvVars({ ...ENV_VARS, DENOPS_TEST_VIM_EXECUTABLE: "foo" }); + const actual = getConfig(); + assertObjectMatch(actual, { vimExecutable: "foo" }); +}); + +Deno.test("getConfig() returns `{ nvimExecutable: 'nvim' }` if DENOPS_TEST_NVIM_EXECUTABLE env var is not set", () => { + using _module = stubConfModule(); + using _env = stubEnvVars({ + ...ENV_VARS, + DENOPS_TEST_NVIM_EXECUTABLE: undefined, + }); + const actual = getConfig(); + assertObjectMatch(actual, { nvimExecutable: "nvim" }); +}); + +Deno.test("getConfig() returns `{ nvimExecutable: ... }` with DENOPS_TEST_NVIM_EXECUTABLE env var", () => { + using _module = stubConfModule(); + using _env = stubEnvVars({ ...ENV_VARS, DENOPS_TEST_NVIM_EXECUTABLE: "foo" }); + const actual = getConfig(); + assertObjectMatch(actual, { nvimExecutable: "foo" }); +}); + +Deno.test("getConfig() returns `{ verbose: false }` if DENOPS_TEST_VERBOSE env var is not set", () => { + using _module = stubConfModule(); + using _env = stubEnvVars({ ...ENV_VARS, DENOPS_TEST_VERBOSE: undefined }); + const actual = getConfig(); + assertObjectMatch(actual, { verbose: false }); +}); + +for ( + const [input, expected] of [ + ["false", false], + ["0", false], + ["invalid", false], + ["true", true], + ["1", true], + ] as const +) { + Deno.test(`getConfig() returns \`{ verbose: ${expected} }\` if DENOPS_TEST_VERBOSE env var is '${input}'`, () => { + using _module = stubConfModule(); + using _env = stubEnvVars({ ...ENV_VARS, DENOPS_TEST_VERBOSE: input }); + const actual = getConfig(); + assertObjectMatch(actual, { verbose: expected }); + }); +} + +for ( + const [input, expected] of [ + ["123", 123], + ["123.456", 123], + ["0", undefined], + ["-123", undefined], + ["string", undefined], + ] as const +) { + Deno.test(`getConfig() returns \`{ connectTimeout: ${expected} }\` if DENOPS_TEST_CONNECT_TIMEOUT env var is '${input}'`, () => { + using _module = stubConfModule(); + using _env = stubEnvVars({ + ...ENV_VARS, + DENOPS_TEST_CONNECT_TIMEOUT: input, + }); + const actual = getConfig(); + assertObjectMatch(actual, { connectTimeout: expected }); + }); +} diff --git a/denops.ts b/denops.ts index 4981c62..958f993 100644 --- a/denops.ts +++ b/denops.ts @@ -24,17 +24,8 @@ export class DenopsImpl implements Denops { return this.#client.call("invoke", "redraw", [force]) as Promise; } - async call(fn: string, ...args: unknown[]): Promise { - try { - return await this.#client.call("invoke", "call", [fn, ...args]); - } catch (err) { - // Denops v5 or earlier may throws an error as string in Neovim - // so convert the error into an Error instance - if (typeof err === "string") { - throw new Error(err); - } - throw err; - } + call(fn: string, ...args: unknown[]): Promise { + return this.#client.call("invoke", "call", [fn, ...args]); } batch( diff --git a/tester.ts b/tester.ts index 35e5503..21476c1 100644 --- a/tester.ts +++ b/tester.ts @@ -93,12 +93,6 @@ export function test( fn?: TestDefinition["fn"], ): void { if (typeof modeOrDefinition === "string") { - if (!name) { - throw new Error(`'name' attribute is required`); - } - if (!fn) { - throw new Error(`'fn' attribute is required`); - } testInternal({ mode: modeOrDefinition, name, @@ -109,35 +103,57 @@ export function test( } } -function testInternal(def: TestDefinition): void { - const { mode } = def; +function testInternal(def: Partial): void { + const { + mode, + name, + fn, + pluginName, + verbose, + prelude, + postlude, + ...denoTestDef + } = def; + if (!mode) { + throw new Error("'mode' attribute is required"); + } + if (!name) { + throw new Error("'name' attribute is required"); + } + if (!fn) { + throw new Error("'fn' attribute is required"); + } if (mode === "all") { testInternal({ ...def, - name: `${def.name} (vim)`, + name: `${name} (vim)`, mode: "vim", }); testInternal({ ...def, - name: `${def.name} (nvim)`, + name: `${name} (nvim)`, mode: "nvim", }); } else if (mode === "any") { const m = sample(["vim", "nvim"] as const)!; testInternal({ ...def, - name: `${def.name} (${m})`, + name: `${name} (${m})`, mode: m, }); } else { + if (!["vim", "nvim"].includes(mode)) { + throw new Error(`'mode' attribute is invalid: ${mode}`); + } Deno.test({ - ...def, + ...denoTestDef, + name, fn: (t) => { - return withDenops(mode, (denops) => def.fn(denops, t), { - pluginName: def.pluginName, - verbose: def.verbose, - prelude: def.prelude, - postlude: def.postlude, + return withDenops(mode, (denops) => fn.call(def, denops, t), { + pluginName, + verbose, + prelude, + postlude, }); }, }); diff --git a/tester_test.ts b/tester_test.ts index 4205fee..0511d3e 100644 --- a/tester_test.ts +++ b/tester_test.ts @@ -1,4 +1,9 @@ -import { assert, assertEquals, assertFalse } from "jsr:@std/assert@0.225.1"; +import { + assert, + assertEquals, + assertFalse, + assertThrows, +} from "jsr:@std/assert@0.225.1"; import { test } from "./tester.ts"; test({ @@ -106,3 +111,59 @@ test({ }); }, }); + +Deno.test("test() throws if 'mode' option is empty", () => { + assertThrows( + () => { + test({ + mode: "" as "vim", + name: "name", + fn: () => {}, + }); + }, + Error, + "'mode' attribute is required", + ); +}); + +Deno.test("test() throws if 'mode' option is invalid", () => { + assertThrows( + () => { + test({ + mode: "foo" as "vim", + name: "name", + fn: () => {}, + }); + }, + Error, + "'mode' attribute is invalid", + ); +}); + +Deno.test("test() throws if 'name' option is empty", () => { + assertThrows( + () => { + test({ + mode: "vim", + name: "", + fn: () => {}, + }); + }, + Error, + "'name' attribute is required", + ); +}); + +Deno.test("test() throws if 'fn' option is empty", () => { + assertThrows( + () => { + test({ + mode: "vim", + name: "name", + fn: undefined as unknown as () => void, + }); + }, + Error, + "'fn' attribute is required", + ); +}); diff --git a/with.ts b/with.ts index 20a017a..ec6f5c1 100644 --- a/with.ts +++ b/with.ts @@ -138,14 +138,6 @@ export async function withDenops( session.start(); try { const denops = await createDenops(session); - - // Workaround for an unexpected "leaking async ops" - // https://github.com/denoland/deno/issues/15425#issuecomment-1368245954 - // Maybe fixed in v1.41.0 - // https://github.com/denoland/deno/pull/22413 - // TODO: Remove this workaround when Deno minimum version changes to v1.41.0 or higher - await new Promise((resolve) => setTimeout(resolve, 0)); - await main(denops); } finally { try { diff --git a/with_test.ts b/with_test.ts index 88f8b5d..638873c 100644 --- a/with_test.ts +++ b/with_test.ts @@ -1,5 +1,10 @@ -import { assert, assertFalse, assertRejects } from "jsr:@std/assert@0.225.1"; -import { assertSpyCalls, spy } from "jsr:@std/testing@0.224.0/mock"; +import { + assert, + assertEquals, + assertFalse, + assertRejects, +} from "jsr:@std/assert@0.225.1"; +import { assertSpyCalls, spy, stub } from "jsr:@std/testing@0.224.0/mock"; import type { Denops } from "jsr:@denops/core@6.0.6"; import { withDenops } from "./with.ts"; @@ -20,6 +25,78 @@ Deno.test("test(mode:nvim) start nvim to test denops features", async () => { }); for (const mode of ["vim", "nvim"] as const) { + Deno.test(`test(mode:${mode}) outputs ${mode} messages if 'verbose' option is true`, async () => { + using s = stub(console, "error"); + await withDenops("vim", async (denops: Denops) => { + await denops.cmd("echomsg 'foobar'"); + }, { verbose: true }); + // NOTE: Maybe some other values are included, so find target with `Array.some`. + // NOTE: Maybe "\r" or "\n" is prepend or postpend, so use `String.trim`. + assert(s.calls.some((c) => c.args[0].trim() === "foobar")); + }); + + Deno.test(`test(mode:${mode}) should be able to call Denops#redraw()`, async () => { + await withDenops("vim", async (denops: Denops) => { + await denops.redraw(); + await denops.redraw(true); + // FIXME: assert redraw is correctly called. + }); + }); + + Deno.test(`test(mode:${mode}) should be able to call Denops#call()`, async () => { + await withDenops("vim", async (denops: Denops) => { + await denops.call("execute", [`let g:with_test__${mode}__call = 'foo'`]); + assertEquals(await denops.eval(`g:with_test__${mode}__call`), "foo"); + }); + }); + + Deno.test(`test(mode:${mode}) should be able to call Denops#batch()`, async () => { + await withDenops("vim", async (denops: Denops) => { + await denops.batch( + ["execute", [`let g:with_test__${mode}__batch_1 = 'foo'`]], + ["execute", [`let g:with_test__${mode}__batch_2 = 'bar'`]], + ); + assertEquals(await denops.eval(`g:with_test__${mode}__batch_1`), "foo"); + assertEquals(await denops.eval(`g:with_test__${mode}__batch_2`), "bar"); + }); + }); + + Deno.test(`test(mode:${mode}) should be able to call Denops#cmd()`, async () => { + await withDenops("vim", async (denops: Denops) => { + await denops.cmd(`let g:with_test__${mode}__cmd = 'foo'`); + assertEquals(await denops.eval(`g:with_test__${mode}__cmd`), "foo"); + }); + }); + + Deno.test(`test(mode:${mode}) should be able to call Denops#eval()`, async () => { + await withDenops("vim", async (denops: Denops) => { + await denops.eval(`execute('let g:with_test__${mode}__eval = "foo"')`); + assertEquals(await denops.eval(`g:with_test__${mode}__eval`), "foo"); + }); + }); + + Deno.test(`test(mode:${mode}) should be able to call Denops#dispatch()`, async () => { + const api = spy(() => Promise.resolve()); + await withDenops("vim", async (denops: Denops) => { + denops.dispatcher = { + foo: api, + }; + await denops.dispatch(denops.name, "foo", [123, "bar"]); + assertSpyCalls(api, 1); + }); + }); + + Deno.test(`test(mode:${mode}) calls plugin dispatcher from ${mode}`, async () => { + const api = spy(() => Promise.resolve()); + await withDenops("vim", async (denops: Denops) => { + denops.dispatcher = { + foo: api, + }; + await denops.call("denops#notify", denops.name, "foo", [123, "bar"]); + assertSpyCalls(api, 1); + }); + }); + Deno.test(`test(mode:${mode}) rejects if process aborted`, async () => { const fn = spy(() => {}); await assertRejects(