From 9476fc0fd3be6f6f664a647f10c8bb110ff8f416 Mon Sep 17 00:00:00 2001 From: deflis Date: Mon, 2 Nov 2026 02:19:00 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=E6=A4=9C=E7=B4=A2=E3=83=93=E3=83=AB?= =?UTF-8?q?=E3=83=80=E3=83=BC=E3=81=AE=E6=A9=9F=E8=83=BD=E5=88=A5=E5=9B=9E?= =?UTF-8?q?=E5=B8=B0=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/search-builder-r18.test.ts | 96 +++++++++++++++++++++++++++++++++ test/search-builder.test.ts | 92 +++++++++++++++++++++++++++++++ test/user-search.test.ts | 91 +++++++++++++++++++++++++++++++ 3 files changed, 279 insertions(+) diff --git a/test/search-builder-r18.test.ts b/test/search-builder-r18.test.ts index 1859584..8521a9d 100644 --- a/test/search-builder-r18.test.ts +++ b/test/search-builder-r18.test.ts @@ -16,9 +16,17 @@ import NarouAPI, { R18Site, } from "../src"; import { http } from "msw/http"; +import NarouBrowserAPI from "../src/index.browser.js"; +import NarouNovelFetch from "../src/narou-fetch.js"; const server = setupServer(); +// ブラウザ入口でもfetch実装を注入し、外部通信なしで互換性を確認する。 +const apiEntries = [ + { name: "Node", api: NarouAPI }, + { name: "browser", api: NarouBrowserAPI }, +]; + const setupMockHandler = ( mockFn: (...args: unknown[]) => void, watchParams: string[] = ["out"], @@ -91,7 +99,68 @@ describe("SearchBuilderR18", () => { ); }); + describe("word", () => { + test.each(apiEntries)("$name: 戻り値を受け取らずに再指定できる", async ({ api }) => { + const mockFn = vi.fn(); + setupMockHandler(mockFn, ["word"], { ncode: "N1234AB" }); + const builder = api.searchR18("初期条件", new NarouNovelFetch()); + builder.word("変更後"); + + const result = await builder.execute(); + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockFn).toHaveBeenCalledWith("変更後", 3); + expect(result.allcount).toBe(1); + }); + }); + + describe("limit", () => { + test.each(apiEntries)("$name: 戻り値を受け取らずに再指定できる", async ({ api }) => { + const mockFn = vi.fn(); + setupMockHandler(mockFn, ["lim"], { ncode: "N1234AB" }); + const builder = api.searchR18("", new NarouNovelFetch()); + builder.limit(3); + builder.limit(10); + + const result = await builder.execute(); + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockFn).toHaveBeenCalledWith("10", 3); + expect(result.limit).toBe(10); + }); + }); + describe("page", () => { + test.each(apiEntries)("$name: 再指定は開始位置と件数を上書きし、既定の20件に戻す", async ({ api }) => { + const queries: URLSearchParams[] = []; + server.use( + http.get("https://api.syosetu.com/novel18api/api/", ({ request }) => { + const url = new URL(request.url); + queries.push(url.searchParams); + return responseGzipOrJson([{ allcount: 100 }], url); + }) + ); + + const builder = api.searchR18("", new NarouNovelFetch()); + builder.start(7).limit(3); + builder.page(2, 10); + const custom = await builder.execute(); + expect(custom).toMatchObject({ start: 20, limit: 10, page: 2 }); + + builder.page(1); + const defaults = await builder.execute(); + expect(defaults).toMatchObject({ start: 20, limit: 20, page: 1 }); + + builder.page(0); + const first = await builder.execute(); + expect(first).toMatchObject({ start: 0, limit: 20, page: 0 }); + expect( + queries.map((query) => [query.get("st"), query.get("lim")]) + ).toEqual([ + ["20", "10"], + ["20", "20"], + ["0", "20"], + ]); + }); + test.each([0, 1, 2])("page=%iの従来の開始位置を維持する", async (page) => { const mockFn = vi.fn(); setupMockHandler(mockFn, ["st", "lim"], { ncode: "N1234AB" }); @@ -220,6 +289,33 @@ describe("SearchBuilderR18", () => { }); describe("fields", () => { + test.each(apiEntries)("$name: 別の参照からの項目再指定を保持する", async ({ api }) => { + const queries: URLSearchParams[] = []; + server.use( + http.get("https://api.syosetu.com/novel18api/api/", ({ request }) => { + const url = new URL(request.url); + queries.push(url.searchParams); + return responseGzipOrJson( + [{ allcount: 1 }, { ncode: "N1234AB", weekly_unique: 100 }], + url + ); + }) + ); + + const builder = api.searchR18("", new NarouNovelFetch()); + builder.opt(OptionalFields.weekly_unique); + const first = builder.fields(R18Fields.title); + expect(first).toBe(builder); + expect(builder.fields(R18Fields.ncode)).toBe(builder); + + // 取得項目の再指定後は、以前の参照から実行しても最後の設定を使用する。 + const result = await first.execute(); + expect(result.values).toEqual([{ ncode: "N1234AB", weekly_unique: 100 }]); + expect(queries).toHaveLength(1); + expect(queries[0].get("of")).toBe(R18Fields.ncode); + expect(queries[0].get("opt")).toBe(OptionalFields.weekly_unique); + }); + test("default", async () => { const mockFn = vi.fn(); setupMockHandler(mockFn, ["of", "out"], { ncode: "N1234AB" }); diff --git a/test/search-builder.test.ts b/test/search-builder.test.ts index fc85de3..a3b748d 100644 --- a/test/search-builder.test.ts +++ b/test/search-builder.test.ts @@ -21,9 +21,17 @@ import NarouAPI, { StopParam, } from "../src"; import { http } from "msw/http"; +import NarouBrowserAPI from "../src/index.browser.js"; +import NarouNovelFetch from "../src/narou-fetch.js"; const server = setupServer(); +// ブラウザ入口でもfetch実装を注入し、外部通信なしで互換性を確認する。 +const apiEntries = [ + { name: "Node", api: NarouAPI }, + { name: "browser", api: NarouBrowserAPI }, +]; + const setupMockHandler = ( mockFn: (...args: unknown[]) => void, watchParams: string[] = ["out"] @@ -136,6 +144,19 @@ describe("SearchBuilder", () => { }); describe("limit", () => { + test.each(apiEntries)("$name: 戻り値を受け取らずに再指定できる", async ({ api }) => { + const mockFn = vi.fn(); + setupMockHandler(mockFn, ["lim"]); + const builder = api.search("", new NarouNovelFetch()); + builder.limit(3); + builder.limit(10); + + const result = await builder.execute(); + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockFn).toHaveBeenCalledWith("10", 3); + expect(result.limit).toBe(10); + }); + test("default", async () => { const mockFn = vi.fn(); setupMockHandler(mockFn, ["gzip", "out"]); @@ -210,6 +231,38 @@ describe("SearchBuilder", () => { }); describe("page", () => { + test.each(apiEntries)("$name: 再指定は開始位置と件数を上書きし、既定の20件に戻す", async ({ api }) => { + const queries: URLSearchParams[] = []; + server.use( + http.get("https://api.syosetu.com/novelapi/api/", ({ request }) => { + const url = new URL(request.url); + queries.push(url.searchParams); + return responseGzipOrJson([{ allcount: 100 }], url); + }) + ); + + const builder = api.search("", new NarouNovelFetch()); + builder.start(7).limit(3); + builder.page(2, 10); + const custom = await builder.execute(); + expect(custom).toMatchObject({ start: 20, limit: 10, page: 2 }); + + builder.page(1); + const defaults = await builder.execute(); + expect(defaults).toMatchObject({ start: 20, limit: 20, page: 1 }); + + builder.page(0); + const first = await builder.execute(); + expect(first).toMatchObject({ start: 0, limit: 20, page: 0 }); + expect( + queries.map((query) => [query.get("st"), query.get("lim")]) + ).toEqual([ + ["20", "10"], + ["20", "20"], + ["0", "20"], + ]); + }); + describe("default count", () => { test.each([0, 1, 2, 3, 4, 5] as const)("if page = %i", async (page) => { const mockFn = vi.fn(); @@ -288,6 +341,18 @@ describe("SearchBuilder", () => { }); describe("word", () => { + test.each(apiEntries)("$name: 戻り値を受け取らずに再指定できる", async ({ api }) => { + const mockFn = vi.fn(); + setupMockHandler(mockFn, ["word"]); + const builder = api.search("初期条件", new NarouNovelFetch()); + builder.word("変更後"); + + const result = await builder.execute(); + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockFn).toHaveBeenCalledWith("変更後", 3); + expect(result.allcount).toBe(1); + }); + test("default", async () => { const mockFn = vi.fn(); setupMockHandler(mockFn, ["gzip", "out"]); @@ -1408,6 +1473,33 @@ describe("SearchBuilder", () => { }); describe("fields", () => { + test.each(apiEntries)("$name: 別の参照からの項目再指定を保持する", async ({ api }) => { + const queries: URLSearchParams[] = []; + server.use( + http.get("https://api.syosetu.com/novelapi/api/", ({ request }) => { + const url = new URL(request.url); + queries.push(url.searchParams); + return responseGzipOrJson( + [{ allcount: 1 }, { ncode: "N1234AB", weekly_unique: 100 }], + url + ); + }) + ); + + const builder = api.search("", new NarouNovelFetch()); + builder.opt(OptionalFields.weekly_unique); + const first = builder.fields(Fields.title); + expect(first).toBe(builder); + expect(builder.fields(Fields.ncode)).toBe(builder); + + // 取得項目の再指定後は、以前の参照から実行しても最後の設定を使用する。 + const result = await first.execute(); + expect(result.values).toEqual([{ ncode: "N1234AB", weekly_unique: 100 }]); + expect(queries).toHaveLength(1); + expect(queries[0].get("of")).toBe(Fields.ncode); + expect(queries[0].get("opt")).toBe(OptionalFields.weekly_unique); + }); + test("default", async () => { const mockFn = vi.fn(); setupMockHandler(mockFn, ["gzip", "out"]); diff --git a/test/user-search.test.ts b/test/user-search.test.ts index c801a5f..df4207d 100644 --- a/test/user-search.test.ts +++ b/test/user-search.test.ts @@ -14,9 +14,17 @@ import NarouAPI, { UserOrder, } from "../src"; import { http } from "msw/http"; +import NarouBrowserAPI from "../src/index.browser.js"; +import NarouNovelFetch from "../src/narou-fetch.js"; const server = setupServer(); +// ブラウザ入口でもfetch実装を注入し、外部通信なしで互換性を確認する。 +const apiEntries = [ + { name: "Node", api: NarouAPI }, + { name: "browser", api: NarouBrowserAPI }, +]; + const setupMockHandler = ( mockFn: (...args: unknown[]) => void, watchParams: string[] = ["out"] @@ -112,6 +120,19 @@ describe("UserSearchBuilder", () => { }); describe("limit", () => { + test.each(apiEntries)("$name: 戻り値を受け取らずに再指定できる", async ({ api }) => { + const mockFn = vi.fn(); + setupMockHandler(mockFn, ["lim"]); + const builder = api.searchUser("", new NarouNovelFetch()); + builder.limit(3); + builder.limit(10); + + const result = await builder.execute(); + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockFn).toHaveBeenCalledWith("10", 3); + expect(result.limit).toBe(10); + }); + test("default", async () => { const mockFn = vi.fn(); setupMockHandler(mockFn, ["gzip", "out"]); @@ -168,6 +189,38 @@ describe("UserSearchBuilder", () => { }); describe("page", () => { + test.each(apiEntries)("$name: 再指定は開始位置と件数を上書きし、既定の20件に戻す", async ({ api }) => { + const queries: URLSearchParams[] = []; + server.use( + http.get("https://api.syosetu.com/userapi/api/", ({ request }) => { + const url = new URL(request.url); + queries.push(url.searchParams); + return responseGzipOrJson([{ allcount: 100 }], url); + }) + ); + + const builder = api.searchUser("", new NarouNovelFetch()); + builder.start(7).limit(3); + builder.page(2, 10); + const custom = await builder.execute(); + expect(custom).toMatchObject({ start: 20, limit: 10, page: 2 }); + + builder.page(1); + const defaults = await builder.execute(); + expect(defaults).toMatchObject({ start: 20, limit: 20, page: 1 }); + + builder.page(0); + const first = await builder.execute(); + expect(first).toMatchObject({ start: 0, limit: 20, page: 0 }); + expect( + queries.map((query) => [query.get("st"), query.get("lim")]) + ).toEqual([ + ["20", "10"], + ["20", "20"], + ["0", "20"], + ]); + }); + describe("default count", () => { test.each([0, 1, 2, 3, 4, 5] as const)("if page = %i", async (page) => { const mockFn = vi.fn(); @@ -246,6 +299,18 @@ describe("UserSearchBuilder", () => { }); describe("word", () => { + test.each(apiEntries)("$name: 戻り値を受け取らずに再指定できる", async ({ api }) => { + const mockFn = vi.fn(); + setupMockHandler(mockFn, ["word"]); + const builder = api.searchUser("初期条件", new NarouNovelFetch()); + builder.word("変更後"); + + const result = await builder.execute(); + expect(mockFn).toHaveBeenCalledTimes(1); + expect(mockFn).toHaveBeenCalledWith("変更後", 3); + expect(result.allcount).toBe(1); + }); + test("default", async () => { const mockFn = vi.fn(); setupMockHandler(mockFn, ["gzip", "out"]); @@ -486,6 +551,32 @@ describe("UserSearchBuilder", () => { }); describe("fields", () => { + test.each(apiEntries)("$name: 別の参照からの項目再指定を保持する", async ({ api }) => { + const queries: URLSearchParams[] = []; + server.use( + http.get("https://api.syosetu.com/userapi/api/", ({ request }) => { + const url = new URL(request.url); + queries.push(url.searchParams); + return responseGzipOrJson( + [{ allcount: 1 }, { userid: 1234 }], + url + ); + }) + ); + + const builder = api.searchUser("", new NarouNovelFetch()); + const first = builder.fields(UserFields.name); + expect(first).toBe(builder); + expect(builder.fields(UserFields.userid)).toBe(builder); + + // 取得項目の再指定後は、以前の参照から実行しても最後の設定を使用する。 + const result = await first.execute(); + expect(result.values).toEqual([{ userid: 1234 }]); + expect(queries).toHaveLength(1); + expect(queries[0].get("of")).toBe(UserFields.userid); + expect(queries[0].get("opt")).toBe(null); + }); + test("default", async () => { const mockFn = vi.fn(); setupMockHandler(mockFn, ["gzip", "out"]); From 3eaf1dd07835c26500dba4f3320d453b8de55c52 Mon Sep 17 00:00:00 2001 From: deflis Date: Mon, 2 Nov 2026 02:19:02 +0900 Subject: [PATCH 2/2] =?UTF-8?q?NarouNovelFetch=E3=81=AE=E5=9C=A7=E7=B8=AE?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E7=9C=81=E7=95=A5=E6=99=82=E3=81=AE=E3=82=A8?= =?UTF-8?q?=E3=83=A9=E3=83=BC=E5=9B=9E=E5=B8=B0=E3=83=86=E3=82=B9=E3=83=88?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/narou-fetch.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/narou-fetch.test.ts b/test/narou-fetch.test.ts index 01af106..0bc19f7 100644 --- a/test/narou-fetch.test.ts +++ b/test/narou-fetch.test.ts @@ -111,7 +111,7 @@ describe('NarouNovelFetch', () => { } }); - it.each([0, 5] as const)('should still reject plain API errors returned with HTTP 200 (gzip=%i)', async (gzip) => { + it.each([0, 5, undefined] as const)('should still reject plain API errors returned with HTTP 200 (gzip=%s)', async (gzip) => { const response = new Response('Error: Invalid date.', { status: 200 }); const narouFetch = new NarouNovelFetch(vi.fn().mockResolvedValue(response)); const result = narouFetch.executeRanking({ rtype: '20260101-d', gzip });