From 8dda96ab328c6634fa94a1c1a1d051d60890c714 Mon Sep 17 00:00:00 2001 From: Milly Date: Sat, 14 Sep 2024 01:56:48 +0900 Subject: [PATCH 1/3] :herb: add test cases of invalid plugin name --- .../functions/plugin/check_type_test.ts | 20 +++++++------- .../functions/plugin/is_loaded_test.ts | 20 +++++++------- .../runtime/functions/plugin/load_test.ts | 20 +++++++------- .../runtime/functions/plugin/reload_test.ts | 20 +++++++------- .../runtime/functions/plugin/unload_test.ts | 20 +++++++------- .../functions/plugin/wait_async_test.ts | 26 ++++++++++--------- .../runtime/functions/plugin/wait_test.ts | 20 +++++++------- tests/denops/testdata/invalid_plugin_names.ts | 6 +++++ 8 files changed, 86 insertions(+), 66 deletions(-) create mode 100644 tests/denops/testdata/invalid_plugin_names.ts diff --git a/tests/denops/runtime/functions/plugin/check_type_test.ts b/tests/denops/runtime/functions/plugin/check_type_test.ts index ca2187c8..68943665 100644 --- a/tests/denops/runtime/functions/plugin/check_type_test.ts +++ b/tests/denops/runtime/functions/plugin/check_type_test.ts @@ -3,6 +3,7 @@ import { assertNotMatch, assertRejects, } from "jsr:@std/assert@^1.0.1"; +import { INVALID_PLUGIN_NAMES } from "/denops-testdata/invalid_plugin_names.ts"; import { resolveTestDataPath } from "/denops-testdata/resolve.ts"; import { testHost } from "/denops-testutil/host.ts"; import { wait } from "/denops-testutil/wait.ts"; @@ -76,16 +77,17 @@ testHost({ }); }); - await t.step("if the plugin name is invalid", async (t) => { - await t.step("throws an error", async () => { - // NOTE: '.' is not allowed in plugin name. - await assertRejects( - () => host.call("denops#plugin#check_type", "dummy.invalid"), - Error, - "Invalid plugin name: dummy.invalid", - ); + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + await t.step("throws an error", async () => { + await assertRejects( + () => host.call("denops#plugin#check_type", plugin_name), + Error, + `Invalid plugin name: ${plugin_name}`, + ); + }); }); - }); + } await t.step("if the plugin is not yet loaded", async (t) => { outputs = []; diff --git a/tests/denops/runtime/functions/plugin/is_loaded_test.ts b/tests/denops/runtime/functions/plugin/is_loaded_test.ts index d710cad3..33b50c36 100644 --- a/tests/denops/runtime/functions/plugin/is_loaded_test.ts +++ b/tests/denops/runtime/functions/plugin/is_loaded_test.ts @@ -1,5 +1,6 @@ import { assertEquals, assertRejects } from "jsr:@std/assert@^1.0.1"; import { delay } from "jsr:@std/async@^1.0.1"; +import { INVALID_PLUGIN_NAMES } from "/denops-testdata/invalid_plugin_names.ts"; import { resolveTestDataPath } from "/denops-testdata/resolve.ts"; import { testHost } from "/denops-testutil/host.ts"; import { wait } from "/denops-testutil/wait.ts"; @@ -30,16 +31,17 @@ testHost({ "augroup END", ], ""); - await t.step("if the plugin name is invalid", async (t) => { - await t.step("throws an error", async () => { - // NOTE: '.' is not allowed in plugin name. - await assertRejects( - () => host.call("denops#plugin#is_loaded", "dummy.invalid"), - Error, - "Invalid plugin name: dummy.invalid", - ); + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + await t.step("throws an error", async () => { + await assertRejects( + () => host.call("denops#plugin#is_loaded", plugin_name), + Error, + `Invalid plugin name: ${plugin_name}`, + ); + }); }); - }); + } await t.step("if the plugin is not yet loaded", async (t) => { await t.step("returns 0", async () => { diff --git a/tests/denops/runtime/functions/plugin/load_test.ts b/tests/denops/runtime/functions/plugin/load_test.ts index 6c987cdd..616ecba3 100644 --- a/tests/denops/runtime/functions/plugin/load_test.ts +++ b/tests/denops/runtime/functions/plugin/load_test.ts @@ -4,6 +4,7 @@ import { assertRejects, } from "jsr:@std/assert@^1.0.1"; import { delay } from "jsr:@std/async@^1.0.1"; +import { INVALID_PLUGIN_NAMES } from "/denops-testdata/invalid_plugin_names.ts"; import { resolveTestDataPath } from "/denops-testdata/resolve.ts"; import { testHost } from "/denops-testutil/host.ts"; import { wait } from "/denops-testutil/wait.ts"; @@ -31,16 +32,17 @@ testHost({ "autocmd User DenopsPlugin* call add(g:__test_denops_events, expand(''))", ], ""); - await t.step("if the plugin name is invalid", async (t) => { - await t.step("throws an error", async () => { - // NOTE: '.' is not allowed in plugin name. - await assertRejects( - () => host.call("denops#plugin#load", "dummy.invalid", scriptValid), - Error, - "Invalid plugin name: dummy.invalid", - ); + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + await t.step("throws an error", async () => { + await assertRejects( + () => host.call("denops#plugin#load", plugin_name, scriptValid), + Error, + `Invalid plugin name: ${plugin_name}`, + ); + }); }); - }); + } await t.step("if the plugin is not yet loaded", async (t) => { outputs = []; diff --git a/tests/denops/runtime/functions/plugin/reload_test.ts b/tests/denops/runtime/functions/plugin/reload_test.ts index 6db01a96..4cf0b230 100644 --- a/tests/denops/runtime/functions/plugin/reload_test.ts +++ b/tests/denops/runtime/functions/plugin/reload_test.ts @@ -4,6 +4,7 @@ import { assertRejects, } from "jsr:@std/assert@^1.0.1"; import { delay } from "jsr:@std/async@^1.0.1"; +import { INVALID_PLUGIN_NAMES } from "/denops-testdata/invalid_plugin_names.ts"; import { resolveTestDataPath } from "/denops-testdata/resolve.ts"; import { testHost } from "/denops-testutil/host.ts"; import { wait } from "/denops-testutil/wait.ts"; @@ -32,16 +33,17 @@ testHost({ "autocmd User DenopsPlugin* call add(g:__test_denops_events, expand(''))", ], ""); - await t.step("if the plugin name is invalid", async (t) => { - await t.step("throws an error", async () => { - // NOTE: '.' is not allowed in plugin name. - await assertRejects( - () => host.call("denops#plugin#reload", "dummy.invalid"), - Error, - "Invalid plugin name: dummy.invalid", - ); + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + await t.step("throws an error", async () => { + await assertRejects( + () => host.call("denops#plugin#reload", plugin_name), + Error, + `Invalid plugin name: ${plugin_name}`, + ); + }); }); - }); + } await t.step("if the plugin is not yet loaded", async (t) => { outputs = []; diff --git a/tests/denops/runtime/functions/plugin/unload_test.ts b/tests/denops/runtime/functions/plugin/unload_test.ts index 5ae6b2d4..019348a3 100644 --- a/tests/denops/runtime/functions/plugin/unload_test.ts +++ b/tests/denops/runtime/functions/plugin/unload_test.ts @@ -4,6 +4,7 @@ import { assertRejects, } from "jsr:@std/assert@^1.0.1"; import { delay } from "jsr:@std/async@^1.0.1"; +import { INVALID_PLUGIN_NAMES } from "/denops-testdata/invalid_plugin_names.ts"; import { resolveTestDataPath } from "/denops-testdata/resolve.ts"; import { testHost } from "/denops-testutil/host.ts"; import { wait } from "/denops-testutil/wait.ts"; @@ -32,16 +33,17 @@ testHost({ "autocmd User DenopsPlugin* call add(g:__test_denops_events, expand(''))", ], ""); - await t.step("if the plugin name is invalid", async (t) => { - await t.step("throws an error", async () => { - // NOTE: '.' is not allowed in plugin name. - await assertRejects( - () => host.call("denops#plugin#unload", "dummy.invalid"), - Error, - "Invalid plugin name: dummy.invalid", - ); + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + await t.step("throws an error", async () => { + await assertRejects( + () => host.call("denops#plugin#unload", plugin_name), + Error, + `Invalid plugin name: ${plugin_name}`, + ); + }); }); - }); + } await t.step("if the plugin is not yet loaded", async (t) => { outputs = []; diff --git a/tests/denops/runtime/functions/plugin/wait_async_test.ts b/tests/denops/runtime/functions/plugin/wait_async_test.ts index c09d478d..258fb5c2 100644 --- a/tests/denops/runtime/functions/plugin/wait_async_test.ts +++ b/tests/denops/runtime/functions/plugin/wait_async_test.ts @@ -5,6 +5,7 @@ import { assertRejects, } from "jsr:@std/assert@^1.0.1"; import { delay } from "jsr:@std/async@^1.0.1"; +import { INVALID_PLUGIN_NAMES } from "/denops-testdata/invalid_plugin_names.ts"; import { resolveTestDataPath } from "/denops-testdata/resolve.ts"; import { testHost } from "/denops-testutil/host.ts"; import { wait } from "/denops-testutil/wait.ts"; @@ -29,19 +30,20 @@ testHost({ "autocmd User DenopsPlugin* call add(g:__test_denops_events, expand(''))", ], ""); - await t.step("if the plugin name is invalid", async (t) => { - await t.step("throws an error", async () => { - // NOTE: '.' is not allowed in plugin name. - await assertRejects( - () => - host.call("execute", [ - "call denops#plugin#wait_async('dummy.invalid', { -> 0 })", - ], ""), - Error, - "Invalid plugin name: dummy.invalid", - ); + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + await t.step("throws an error", async () => { + await assertRejects( + () => + host.call("execute", [ + `call denops#plugin#wait_async('${plugin_name}', { -> 0 })`, + ], ""), + Error, + `Invalid plugin name: ${plugin_name}`, + ); + }); }); - }); + } await t.step("if the plugin is load asynchronously", async (t) => { // Load plugin asynchronously. diff --git a/tests/denops/runtime/functions/plugin/wait_test.ts b/tests/denops/runtime/functions/plugin/wait_test.ts index 821be504..04a9494e 100644 --- a/tests/denops/runtime/functions/plugin/wait_test.ts +++ b/tests/denops/runtime/functions/plugin/wait_test.ts @@ -7,6 +7,7 @@ import { assertStringIncludes, } from "jsr:@std/assert@^1.0.1"; import { delay } from "jsr:@std/async@^1.0.1"; +import { INVALID_PLUGIN_NAMES } from "/denops-testdata/invalid_plugin_names.ts"; import { resolveTestDataPath } from "/denops-testdata/resolve.ts"; import { testHost } from "/denops-testutil/host.ts"; import { wait } from "/denops-testutil/wait.ts"; @@ -35,16 +36,17 @@ testHost({ "autocmd User DenopsPlugin* call add(g:__test_denops_events, expand(''))", ], ""); - await t.step("if the plugin name is invalid", async (t) => { - await t.step("throws an error", async () => { - // NOTE: '.' is not allowed in plugin name. - await assertRejects( - () => host.call("denops#plugin#wait", "dummy.invalid"), - Error, - "Invalid plugin name: dummy.invalid", - ); + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + await t.step("throws an error", async () => { + await assertRejects( + () => host.call("denops#plugin#wait", plugin_name), + Error, + `Invalid plugin name: ${plugin_name}`, + ); + }); }); - }); + } await t.step("if the plugin is loading", async (t) => { await host.call("execute", [ diff --git a/tests/denops/testdata/invalid_plugin_names.ts b/tests/denops/testdata/invalid_plugin_names.ts new file mode 100644 index 00000000..33ddd50c --- /dev/null +++ b/tests/denops/testdata/invalid_plugin_names.ts @@ -0,0 +1,6 @@ +export const INVALID_PLUGIN_NAMES: + readonly (readonly [plugin_name: string, label: string])[] = [ + ["", "empty"], + ["dummy.invalid", "'.'"], + ["dummy invalid", "' '"], + ]; From 79b4bf09922509d24cafabdbf43da24ab0944c3d Mon Sep 17 00:00:00 2001 From: Milly Date: Sat, 14 Sep 2024 03:57:42 +0900 Subject: [PATCH 2/3] :+1: rejects if the plugin name is invalid --- autoload/denops/_internal/plugin.vim | 1 + denops/@denops-private/service.ts | 21 +++- denops/@denops-private/service_test.ts | 128 +++++++++++++++++++++++++ 3 files changed, 147 insertions(+), 3 deletions(-) diff --git a/autoload/denops/_internal/plugin.vim b/autoload/denops/_internal/plugin.vim index a14a8f9a..84e5067c 100644 --- a/autoload/denops/_internal/plugin.vim +++ b/autoload/denops/_internal/plugin.vim @@ -4,6 +4,7 @@ const s:STATE_LOADED = 'loaded' const s:STATE_UNLOADING = 'unloading' const s:STATE_FAILED = 'failed' +" NOTE: same as denops/@denops-private/service.ts const s:VALID_NAME_PATTERN = '^[-_0-9a-zA-Z]\+$' let s:plugins = {} diff --git a/denops/@denops-private/service.ts b/denops/@denops-private/service.ts index a7e0e6a2..654d4f72 100644 --- a/denops/@denops-private/service.ts +++ b/denops/@denops-private/service.ts @@ -45,6 +45,7 @@ export class Service implements HostService, AsyncDisposable { if (!this.#host) { throw new Error("No host is bound to the service"); } + assertValidPluginName(name); if (this.#plugins.has(name)) { if (this.#meta.mode === "debug") { console.log(`A denops plugin '${name}' is already loaded. Skip`); @@ -79,21 +80,24 @@ export class Service implements HostService, AsyncDisposable { } async unload(name: string): Promise { + assertValidPluginName(name); await this.#unload(name); } async reload(name: string): Promise { + assertValidPluginName(name); const plugin = await this.#unload(name); if (plugin) { await this.load(name, plugin.script); } } - waitLoaded(name: string): Promise { + async waitLoaded(name: string): Promise { if (this.#closed) { - return Promise.reject(new Error("Service closed")); + throw new Error("Service closed"); } - return this.#getWaiter(name).promise; + assertValidPluginName(name); + await this.#getWaiter(name).promise; } interrupt(reason?: unknown): void { @@ -111,6 +115,7 @@ export class Service implements HostService, AsyncDisposable { async dispatch(name: string, fn: string, args: unknown[]): Promise { try { + assertValidPluginName(name); return await this.#dispatch(name, fn, args); } catch (e) { throw toVimError(e); @@ -128,6 +133,7 @@ export class Service implements HostService, AsyncDisposable { throw new Error("No host is bound to the service"); } try { + assertValidPluginName(name); const r = await this.#dispatch(name, fn, args); try { await this.#host.call("denops#callback#call", success, r); @@ -173,6 +179,15 @@ export class Service implements HostService, AsyncDisposable { } } +// NOTE: same as autoload/denops/_internal/plugin.vim +const VALID_NAME_PATTERN = /^[-_0-9a-zA-Z]+$/; + +function assertValidPluginName(name: string) { + if (!VALID_NAME_PATTERN.test(name)) { + throw new TypeError(`Invalid plugin name: ${name}`); + } +} + type PluginModule = { main: Entrypoint; }; diff --git a/denops/@denops-private/service_test.ts b/denops/@denops-private/service_test.ts index 851fa8e8..4c3d684c 100644 --- a/denops/@denops-private/service_test.ts +++ b/denops/@denops-private/service_test.ts @@ -6,8 +6,10 @@ import { assertInstanceOf, assertMatch, assertNotStrictEquals, + assertObjectMatch, assertRejects, assertStrictEquals, + assertStringIncludes, assertThrows, } from "jsr:@std/assert@^1.0.1"; import { @@ -21,6 +23,7 @@ import { toFileUrl } from "jsr:@std/path@^1.0.2/to-file-url"; import type { Meta } from "jsr:@denops/core@^7.0.0"; import { promiseState } from "jsr:@lambdalisue/async@^2.1.1"; import { unimplemented } from "jsr:@lambdalisue/errorutil@^1.1.0"; +import { INVALID_PLUGIN_NAMES } from "/denops-testdata/invalid_plugin_names.ts"; import { resolveTestDataURL } from "/denops-testdata/resolve.ts"; import type { Host } from "./denops.ts"; import { Service } from "./service.ts"; @@ -439,6 +442,26 @@ Deno.test("Service", async (t) => { ]); }); }); + + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + const service = new Service(meta); + service.bind(host); + using host_call = stub(host, "call"); + + await t.step("rejects", async () => { + await assertRejects( + () => service.load(plugin_name, scriptValid), + TypeError, + `Invalid plugin name: ${plugin_name}`, + ); + }); + + await t.step("does not calls the host", () => { + assertSpyCalls(host_call, 0); + }); + }); + } }); await t.step(".unload()", async (t) => { @@ -760,6 +783,26 @@ Deno.test("Service", async (t) => { ]); }); }); + + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + const service = new Service(meta); + service.bind(host); + using host_call = stub(host, "call"); + + await t.step("rejects", async () => { + await assertRejects( + () => service.unload(plugin_name), + TypeError, + `Invalid plugin name: ${plugin_name}`, + ); + }); + + await t.step("does not calls the host", () => { + assertSpyCalls(host_call, 0); + }); + }); + } }); await t.step(".reload()", async (t) => { @@ -1059,6 +1102,26 @@ Deno.test("Service", async (t) => { }); }); }); + + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + const service = new Service(meta); + service.bind(host); + using host_call = stub(host, "call"); + + await t.step("rejects", async () => { + await assertRejects( + () => service.reload(plugin_name), + TypeError, + `Invalid plugin name: ${plugin_name}`, + ); + }); + + await t.step("does not calls the host", () => { + assertSpyCalls(host_call, 0); + }); + }); + } }); await t.step(".waitLoaded()", async (t) => { @@ -1145,6 +1208,7 @@ Deno.test("Service", async (t) => { using _host_call = stub(host, "call"); const actual = service.waitLoaded("dummy"); + actual.catch(NOOP); await service.close(); assertEquals(await promiseState(actual), "rejected"); @@ -1154,6 +1218,21 @@ Deno.test("Service", async (t) => { "Service closed", ); }); + + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + const service = new Service(meta); + service.bind(host); + + await t.step("rejects", async () => { + await assertRejects( + () => service.waitLoaded(plugin_name), + TypeError, + `Invalid plugin name: ${plugin_name}`, + ); + }); + }); + } }); await t.step(".interrupt()", async (t) => { @@ -1296,6 +1375,21 @@ Deno.test("Service", async (t) => { }); }); }); + + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + const service = new Service(meta); + service.bind(host); + + await t.step("rejects", async () => { + const err = await assertRejects( + () => service.dispatch(plugin_name, "test", []), + ); + assert(typeof err === "string"); + assertStringIncludes(err, `Invalid plugin name: ${plugin_name}`); + }); + }); + } }); await t.step(".dispatchAsync()", async (t) => { @@ -1522,6 +1616,40 @@ Deno.test("Service", async (t) => { }); }); }); + + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + const service = new Service(meta); + service.bind(host); + using host_call = stub(host, "call"); + + await t.step("resolves", async () => { + await service.dispatchAsync( + plugin_name, + "test", + ["foo"], + "success", + "failure", + ); + }); + + await t.step("calls 'failure' callback", () => { + const err = host_call.calls[0]?.args[2]; + assert(err && typeof err === "object"); + assertSpyCall(host_call, 0, { + args: [ + "denops#callback#call", + "failure", + err, + ], + }); + assertObjectMatch(err, { + name: "TypeError", + message: `Invalid plugin name: ${plugin_name}`, + }); + }); + }); + } }); await t.step(".close()", async (t) => { From 908c59ea06b5929ff10d85cd4ca2c25ee66e4b55 Mon Sep 17 00:00:00 2001 From: Milly Date: Sun, 15 Sep 2024 00:53:42 +0900 Subject: [PATCH 3/3] :herb: add tests for denops#request methods --- .../runtime/functions/denops/notify_test.ts | 61 +++++++++ .../functions/denops/request_async_test.ts | 117 ++++++++++++++++++ .../runtime/functions/denops/request_test.ts | 57 +++++++++ 3 files changed, 235 insertions(+) create mode 100644 tests/denops/runtime/functions/denops/notify_test.ts create mode 100644 tests/denops/runtime/functions/denops/request_async_test.ts create mode 100644 tests/denops/runtime/functions/denops/request_test.ts diff --git a/tests/denops/runtime/functions/denops/notify_test.ts b/tests/denops/runtime/functions/denops/notify_test.ts new file mode 100644 index 00000000..5a93a23b --- /dev/null +++ b/tests/denops/runtime/functions/denops/notify_test.ts @@ -0,0 +1,61 @@ +import { assertEquals, assertStringIncludes } from "jsr:@std/assert@^1.0.1"; +import { delay } from "jsr:@std/async@^1.0.1/delay"; +import { INVALID_PLUGIN_NAMES } from "/denops-testdata/invalid_plugin_names.ts"; +import { resolveTestDataPath } from "/denops-testdata/resolve.ts"; +import { testHost } from "/denops-testutil/host.ts"; +import { wait } from "/denops-testutil/wait.ts"; + +const ASYNC_DELAY = 100; + +const scriptValid = resolveTestDataPath("dummy_valid_plugin.ts"); + +testHost({ + name: "denops#notify()", + mode: "all", + postlude: [ + "runtime plugin/denops.vim", + ], + fn: async ({ host, t, stderr }) => { + let outputs: string[] = []; + stderr.pipeTo( + new WritableStream({ write: (s) => void outputs.push(s) }), + ).catch(() => {}); + await wait(() => host.call("eval", "denops#server#status() ==# 'running'")); + await host.call("execute", [ + "let g:__test_denops_events = []", + "autocmd User DenopsPlugin* call add(g:__test_denops_events, expand(''))", + ], ""); + + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + await t.step("does not throw an error", async () => { + await host.call("denops#notify", plugin_name, "test", ["foo"]); + }); + }); + } + + await t.step("if the plugin is loaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyLoaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyLoaded") + ); + + outputs = []; + await host.call("denops#notify", "dummyLoaded", "test", ["foo"]); + + await t.step("returns immediately", () => { + assertEquals(outputs, []); + }); + + await t.step("calls dispatcher method", async () => { + await delay(ASYNC_DELAY); + assertStringIncludes(outputs.join(""), 'This is test call: ["foo"]'); + }); + }); + }, +}); diff --git a/tests/denops/runtime/functions/denops/request_async_test.ts b/tests/denops/runtime/functions/denops/request_async_test.ts new file mode 100644 index 00000000..09f92689 --- /dev/null +++ b/tests/denops/runtime/functions/denops/request_async_test.ts @@ -0,0 +1,117 @@ +import { + assertEquals, + assertObjectMatch, + assertStringIncludes, +} from "jsr:@std/assert@^1.0.1"; +import { INVALID_PLUGIN_NAMES } from "/denops-testdata/invalid_plugin_names.ts"; +import { resolveTestDataPath } from "/denops-testdata/resolve.ts"; +import { testHost } from "/denops-testutil/host.ts"; +import { wait } from "/denops-testutil/wait.ts"; + +const scriptValid = resolveTestDataPath("dummy_valid_plugin.ts"); + +testHost({ + name: "denops#request_async()", + mode: "all", + postlude: [ + "runtime plugin/denops.vim", + ], + fn: async ({ host, t, stderr, mode }) => { + let outputs: string[] = []; + stderr.pipeTo( + new WritableStream({ write: (s) => void outputs.push(s) }), + ).catch(() => {}); + await wait(() => host.call("eval", "denops#server#status() ==# 'running'")); + await host.call("execute", [ + "let g:__test_denops_events = []", + "autocmd User DenopsPlugin* call add(g:__test_denops_events, expand(''))", + "function TestDenopsRequestAsyncSuccess(...)", + " call add(g:__test_denops_events, ['TestDenopsRequestAsyncSuccess', a:000])", + "endfunction", + "function TestDenopsRequestAsyncFailure(...)", + " call add(g:__test_denops_events, ['TestDenopsRequestAsyncFailure', a:000])", + "endfunction", + ], ""); + + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + await host.call("execute", [ + "let g:__test_denops_events = []", + ], ""); + + await t.step("does not throw an error", async () => { + await host.call( + "denops#request_async", + plugin_name, + "test", + ["foo"], + "TestDenopsRequestAsyncSuccess", + "TestDenopsRequestAsyncFailure", + ); + }); + + await t.step("calls failure callback", async () => { + await wait(() => host.call("eval", "len(g:__test_denops_events)")); + assertObjectMatch( + await host.call("eval", "g:__test_denops_events") as [], + { + 0: [ + "TestDenopsRequestAsyncFailure", + [ + { + message: `Invalid plugin name: ${plugin_name}`, + name: "TypeError", + }, + ], + ], + }, + ); + }); + }); + } + + await t.step("if the plugin is loaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyLoaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyLoaded") + ); + await host.call("execute", [ + "let g:__test_denops_events = []", + ], ""); + + outputs = []; + await host.call( + "denops#request_async", + "dummyLoaded", + "test", + ["foo"], + "TestDenopsRequestAsyncSuccess", + "TestDenopsRequestAsyncFailure", + ); + + await t.step("returns immediately", () => { + assertEquals(outputs, []); + }); + + await t.step("calls success callback", async () => { + await wait(() => host.call("eval", "len(g:__test_denops_events)")); + const returnValue = mode === "vim" ? null : 0; + assertObjectMatch( + await host.call("eval", "g:__test_denops_events") as [], + { + 0: ["TestDenopsRequestAsyncSuccess", [returnValue]], + }, + ); + }); + + await t.step("calls dispatcher method", () => { + assertStringIncludes(outputs.join(""), 'This is test call: ["foo"]'); + }); + }); + }, +}); diff --git a/tests/denops/runtime/functions/denops/request_test.ts b/tests/denops/runtime/functions/denops/request_test.ts new file mode 100644 index 00000000..51e27413 --- /dev/null +++ b/tests/denops/runtime/functions/denops/request_test.ts @@ -0,0 +1,57 @@ +import { assertRejects, assertStringIncludes } from "jsr:@std/assert@^1.0.1"; +import { INVALID_PLUGIN_NAMES } from "/denops-testdata/invalid_plugin_names.ts"; +import { resolveTestDataPath } from "/denops-testdata/resolve.ts"; +import { testHost } from "/denops-testutil/host.ts"; +import { wait } from "/denops-testutil/wait.ts"; + +const scriptValid = resolveTestDataPath("dummy_valid_plugin.ts"); + +testHost({ + name: "denops#request()", + mode: "all", + postlude: [ + "runtime plugin/denops.vim", + ], + fn: async ({ host, t, stderr }) => { + let outputs: string[] = []; + stderr.pipeTo( + new WritableStream({ write: (s) => void outputs.push(s) }), + ).catch(() => {}); + await wait(() => host.call("eval", "denops#server#status() ==# 'running'")); + await host.call("execute", [ + "let g:__test_denops_events = []", + "autocmd User DenopsPlugin* call add(g:__test_denops_events, expand(''))", + ], ""); + + for (const [plugin_name, label] of INVALID_PLUGIN_NAMES) { + await t.step(`if the plugin name is invalid (${label})`, async (t) => { + await t.step("throws an error", async () => { + await assertRejects( + () => host.call("denops#request", plugin_name, "test", ["foo"]), + Error, + `Invalid plugin name: ${plugin_name}`, + ); + }); + }); + } + + await t.step("if the plugin is loaded", async (t) => { + // Load plugin and wait. + await host.call("execute", [ + "let g:__test_denops_events = []", + `call denops#plugin#load('dummyLoaded', '${scriptValid}')`, + ], ""); + await wait(async () => + (await host.call("eval", "g:__test_denops_events") as string[]) + .includes("DenopsPluginPost:dummyLoaded") + ); + + await t.step("calls dispatcher method", async () => { + outputs = []; + await host.call("denops#request", "dummyLoaded", "test", ["foo"]); + + assertStringIncludes(outputs.join(""), 'This is test call: ["foo"]'); + }); + }); + }, +});