Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/narou-search-results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export interface UserSearchResult {
* ユーザ名のフリガナの頭文字
* ひらがな以外の場合はnullまたは空文字となります。
*/
name1st: string;
name1st: string | null;
/** 小説投稿数 */
novel_cnt: number;
/** レビュー投稿数 */
Expand Down
4 changes: 1 addition & 3 deletions src/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,6 @@ export const R18Fields = {
title: "t",
/** Nコード */
ncode: "n",
/** 作者のユーザID(数値) */
userid: "u",
/** 作者名 */
writer: "w",
/** 小説のあらすじ */
Expand Down Expand Up @@ -321,7 +319,7 @@ export const R18Fields = {

export type R18Fields = (typeof R18Fields)[keyof Omit<
NarouSearchResult,
"novel_type" | "weekly_unique" | "biggenre" | "genre" | "isr15"
"novel_type" | "weekly_unique" | "biggenre" | "genre" | "isr15" | "userid"
>];

/**
Expand Down
2 changes: 1 addition & 1 deletion src/search-builder-r18.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {

export type DefaultR18SearchResultFields = keyof Omit<
NarouSearchResult,
"weekly_unique" | "noveltype" | "biggenre" | "genre" | "isr15" | "id"
"weekly_unique" | "noveltype" | "biggenre" | "genre" | "isr15" | "userid"
>;

/**
Expand Down
4 changes: 4 additions & 0 deletions src/search-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ export abstract class SearchBuilderBase<
}
}

/**
* 要素の抽出条件と除外条件は独立して保持します。
* 同じ条件にtrueとfalseを指定すると両方をAPIへ送信し、検索結果は0件になります。
*/
export abstract class NovelSearchBuilderBase<
T extends SearchResultFieldNames,
> extends SearchBuilderBase<SearchParams, Order> {
Expand Down
29 changes: 29 additions & 0 deletions test/builder-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,35 @@ describe.each([
>().toEqualTypeOf<readonly { noveltype: 1 | 2 }[]>();
});

test("R18検索はuseridを取得項目に含めない", () => {
const r18 = api.searchR18();
const novel = api.search();
expectTypeOf<
Awaited<ReturnType<typeof r18.execute>>["values"][number]
>().not.toHaveProperty("userid");
expectTypeOf<
Awaited<ReturnType<typeof novel.execute>>["values"][number]
>().toHaveProperty("userid").toEqualTypeOf<number>();
expectTypeOf(R18Fields).not.toHaveProperty("userid");

function invalidUsage() {
// @ts-expect-error R18 APIはユーザーIDの出力に対応していない
api.searchR18().fields(Fields.userid);
}
expectTypeOf(invalidUsage).returns.toBeVoid();
});

test("ユーザーのname1stはnullを許容する", () => {
const all = api.searchUser();
const selected = api.searchUser().fields(UserFields.name1st);
expectTypeOf<
Awaited<ReturnType<typeof all.execute>>["values"][number]["name1st"]
>().toEqualTypeOf<string | null>();
expectTypeOf<
Awaited<ReturnType<typeof selected.execute>>["values"]
>().toEqualTypeOf<readonly { name1st: string | null }[]>();
});

test("通常検索のoptとfieldsの順序に依存しない型推論", () => {
const optFirst = api
.search()
Expand Down
8 changes: 8 additions & 0 deletions test/narou-search-results.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,14 @@ describe('NarouSearchResults', () => {
expect(results.values).toEqual(data);
});

it('開始位置がページ境界でなくても従来の計算を維持する', () => {
const results = new NarouSearchResults<MockData, 'id'>(
[{ allcount: 100 }], { st: 21, lim: 20 }
);
expect(results.start).toBe(21);
expect(results.page).toBe(1.05);
});

it('should handle empty results', () => {
const header = { allcount: 0 };
const data: Pick<MockData, 'id'>[] = [];
Expand Down
46 changes: 46 additions & 0 deletions test/search-builder-r18.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,52 @@ describe("SearchBuilderR18", () => {
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe("抽出・除外条件の併存", () => {
test.each([
["isBL", "isbl", "notbl"],
["isGL", "isgl", "notgl"],
["isZankoku", "iszankoku", "notzankoku"],
["isTensei", "istensei", "nottensei"],
["isTenni", "istenni", "nottenni"],
] as const)(
"%sの両条件を呼び出し順に関係なく保持する",
async (method, include, exclude) => {
const queries: URLSearchParams[] = [];
server.use(
http.get(`https://api.syosetu.com/novel18api/api/`, ({ request }) => {
queries.push(new URL(request.url).searchParams);
return responseGzipOrJson([{ allcount: 0 }], new URL(request.url));
})
);
for (const first of [true, false]) {
const result = await NarouAPI.searchR18()
.gzip(0)
[method](first)
[method](!first)
.execute();
expect(result.allcount).toBe(0);
expect(result.values).toEqual([]);
}
expect(queries).toHaveLength(2);
for (const query of queries) {
expect(query.get(include)).toBe("1");
expect(query.get(exclude)).toBe("1");
}
}
);
});

describe("page", () => {
test.each([0, 1, 2])("page=%iの従来の開始位置を維持する", async (page) => {
const mockFn = vi.fn();
setupMockHandler(mockFn, ["st", "lim"], { ncode: "N1234AB" });
const result = await NarouAPI.searchR18().page(page, 20).execute();
expect(mockFn).toHaveBeenCalledWith(String(page * 20), "20", 4);
expect(result.start).toBe(page * 20);
expect(result.page).toBe(page);
});
});

describe("execute", () => {
test("execute should call executeNovel18", async () => {
const mockFn = vi.fn();
Expand Down
61 changes: 59 additions & 2 deletions test/search-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,41 @@ describe("SearchBuilder", () => {
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe("抽出・除外条件の併存", () => {
test.each([
["isBL", "isbl", "notbl"],
["isGL", "isgl", "notgl"],
["isZankoku", "iszankoku", "notzankoku"],
["isTensei", "istensei", "nottensei"],
["isTenni", "istenni", "nottenni"],
] as const)(
"%sの両条件を呼び出し順に関係なく保持する",
async (method, include, exclude) => {
const queries: URLSearchParams[] = [];
server.use(
http.get(`https://api.syosetu.com/novelapi/api/`, ({ request }) => {
queries.push(new URL(request.url).searchParams);
return responseGzipOrJson([{ allcount: 0 }], new URL(request.url));
})
);
for (const first of [true, false]) {
const result = await NarouAPI.search()
.gzip(0)
[method](first)
[method](!first)
.execute();
expect(result.allcount).toBe(0);
expect(result.values).toEqual([]);
}
expect(queries).toHaveLength(2);
for (const query of queries) {
expect(query.get(include)).toBe("1");
expect(query.get(exclude)).toBe("1");
}
}
);
});

describe("gzip", () => {
test("default", async () => {
const mockFn = vi.fn();
Expand Down Expand Up @@ -176,11 +211,13 @@ describe("SearchBuilder", () => {

describe("page", () => {
describe("default count", () => {
test.each([1, 2, 3, 4, 5] as const)("if page = %i", async (page) => {
test.each([0, 1, 2, 3, 4, 5] as const)("if page = %i", async (page) => {
const mockFn = vi.fn();
setupMockHandler(mockFn, ["lim", "st", "gzip", "out"]);

const result = await NarouAPI.search().page(page).execute();
expect(result.start).toBe(page * 20);
expect(result.page).toBe(page);
expect(result.allcount).toBe(1);
expect(result.length).toBe(1);
expect(result.values).toHaveLength(1);
Expand All @@ -197,11 +234,13 @@ describe("SearchBuilder", () => {
});

describe.each([1, 10, 100, 500] as const)("if count = %i", (count) => {
test.each([1, 2, 3, 4, 5] as const)("if page = %i", async (page) => {
test.each([0, 1, 2, 3, 4, 5] as const)("if page = %i", async (page) => {
const mockFn = vi.fn();
setupMockHandler(mockFn, ["lim", "st", "gzip", "out"]);

const result = await NarouAPI.search().page(page, count).execute();
expect(result.start).toBe(page * count);
expect(result.page).toBe(page);
expect(result.allcount).toBe(1);
expect(result.length).toBe(1);
expect(result.values).toHaveLength(1);
Expand Down Expand Up @@ -1319,6 +1358,24 @@ describe("SearchBuilder", () => {
});

describe("isR15", () => {
test("R15の抽出・除外条件を両方保持する", async () => {
const queries: URLSearchParams[] = [];
server.use(
http.get("https://api.syosetu.com/novelapi/api/", ({ request }) => {
queries.push(new URL(request.url).searchParams);
return responseGzipOrJson([{ allcount: 0 }], new URL(request.url));
})
);
for (const first of [true, false]) {
await NarouAPI.search().gzip(0).isR15(first).isR15(!first).execute();
}
expect(queries).toHaveLength(2);
for (const query of queries) {
expect(query.get("isr15")).toBe("1");
expect(query.get("notr15")).toBe("1");
}
});

test("default", async () => {
const mockFn = vi.fn();
setupMockHandler(mockFn, ["gzip", "out"]);
Expand Down
24 changes: 22 additions & 2 deletions test/user-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,11 +169,13 @@ describe("UserSearchBuilder", () => {

describe("page", () => {
describe("default count", () => {
test.each([1, 2, 3, 4, 5] as const)("if page = %i", async (page) => {
test.each([0, 1, 2, 3, 4, 5] as const)("if page = %i", async (page) => {
const mockFn = vi.fn();
setupMockHandler(mockFn, ["lim", "st", "gzip", "out"]);

const result = await NarouAPI.searchUser().page(page).execute();
expect(result.start).toBe(page * 20);
expect(result.page).toBe(page);
expect(result.allcount).toBe(1);
expect(result.length).toBe(1);
expect(result.values).toHaveLength(1);
Expand All @@ -190,11 +192,13 @@ describe("UserSearchBuilder", () => {
});

describe.each([1, 10, 100, 500] as const)("if count = %i", (count) => {
test.each([1, 2, 3, 4, 5] as const)("if page = %i", async (page) => {
test.each([0, 1, 2, 3, 4, 5] as const)("if page = %i", async (page) => {
const mockFn = vi.fn();
setupMockHandler(mockFn, ["lim", "st", "gzip", "out"]);

const result = await NarouAPI.searchUser().page(page, count).execute();
expect(result.start).toBe(page * count);
expect(result.page).toBe(page);
expect(result.allcount).toBe(1);
expect(result.length).toBe(1);
expect(result.values).toHaveLength(1);
Expand Down Expand Up @@ -326,6 +330,22 @@ describe("UserSearchBuilder", () => {
});

describe("name1st", () => {
test.each([null, "", "あ"])(
"ユーザーのname1st=%sを保持する",
async (name1st) => {
server.use(
http.get("https://api.syosetu.com/userapi/api/", ({ request }) =>
responseGzipOrJson([{ allcount: 1 }, { name1st }], new URL(request.url))
)
);
const result = await NarouAPI.searchUser()
.gzip(0)
.fields(UserFields.name1st)
.execute();
expect(result.values).toEqual([{ name1st }]);
}
);

test("default", async () => {
const mockFn = vi.fn();
setupMockHandler(mockFn, ["gzip", "out"]);
Expand Down
Loading