/**
* GET /facilities(F-08)のクエリ処理のテスト。
*
* ★期待値表(先に確定させたもの。実装より先にこの表を書いている)★
*
* | # | ケース | 入力 | 期待 |
* |----|-------------------------------|----------------------------------------|---------------------------------------------|
* | 1 | 0件 | items=[] | items=[] / meta.total=0(例外を投げない) |
* | 2 | 1件 | items=[A] | そのまま A が返る |
* | 3 | 部分一致(施設名) | facility_name="バリューマート" | 複数ヒット |
* | 4 | 大文字小文字違い | "8111r" | 8111R5628060011 にヒット |
* | 5 | 該当なし | q="ZZZ-NOMATCH" | items=[] / meta.total=0 |
* | 6 | ページング境界 | 12件 / per_page=5 | p1=5件, p2=5件, p3=2件, p99=0件・total=12 |
* | 7 | ロボット0台 | robots=[] に serial 検索 | 落ちない・ヒットしない |
* | 8 | 複数台 | 3台目のシリアル | ヒットする |
* | 9 | q の OR | q=住所の一部(施設名は不一致) | 住所でヒットする |
* | 10 | 個別パラメータの AND | facility_name + address | 両方満たす施設だけ残る |
* | 11 | per_page/page の丸め | per_page=9999 / 0, page=-1 | 200 / 1, 1(422にはしない) |
*
* ★fixtures には依存しない★ — `row()` で合成データを組み立てる。
* `lib/fixtures/facilities.ts` を使うと、fixtures を1件足しただけで
* 件数を固定したテストが落ちる(fixtures はデモ・画面確認用で、期待値の正ではない)。
*/
import { describe, expect, it } from "vitest";
import {
filterFacilitiesByFields,
filterFacilitiesByQ,
paginateFacilities,
parseFacilityQuery,
sortFacilities,
type FacilityQuery,
} from "@/lib/facilities/query";
import { isSupportedModel } from "@/lib/format/robot-model";
import type { Facility, RobotSummary } from "@/lib/types";
function robot(overrides: Partial<RobotSummary> = {}): RobotSummary {
return {
id: 101,
maker: "PUDU",
model: "CC1",
serial_number: "8111R5628060011",
...overrides,
};
}
function row(overrides: Partial<Facility> = {}): Facility {
return {
id: 1,
facility_name: "メガバリューマート中央本店",
// ★所属(指示書AK 段階5)★ — この関数群は絞り込み・並べ替え・ページングだけを見るので
// 既定値は1つで足りる(分類での絞り込みは `app/api/__tests__/master-filter.test.ts`)。
customer_id: 1,
customer_name: "メガバリューマート",
group_id: 1,
group_name: "バリューホールディングス",
prefecture: "東京都",
address: "東京都○○区1-2-3",
phone: "03-0000-0001",
manager_name: "田中 一郎",
charger_name: "小林 美咲",
email: "store001@example.com",
monitoring_enabled: true,
d_security_enabled: true,
robots: [robot()],
...overrides,
};
}
/** 個別パラメータ用の引数。指定しないものは空文字(=指定なし) */
function fields(
overrides: Partial<{ facilityName: string; serial: string; address: string }> = {}
) {
return { facilityName: "", serial: "", address: "", ...overrides };
}
/**
* ★422 にならない入力専用のヘルパー★
* `parseFacilityQuery()` は指示書AK 段階2 で `QueryParseResult` を返すようになった
* (`group_id` / `customer_id` がこのエンドポイント初の「不正値がありうるパラメータ」)。
* ★422 のケースはここを通さず、`parseFacilityQuery()` を直接呼んで検証すること★。
*/
function parsed(params: URLSearchParams): FacilityQuery {
const result = parseFacilityQuery(params);
if (!result.ok) throw new Error("422を期待していない入力で422になった");
return result.query;
}
describe("parseFacilityQuery", () => {
it("省略時はすべて空文字・group_id / customer_id は null・page=1・per_page=50", () => {
const query = parsed(new URLSearchParams());
expect(query).toEqual({
q: "",
facilityName: "",
serial: "",
address: "",
groupId: null,
customerId: null,
page: 1,
perPage: 50,
});
});
it("q / facility_name / serial / address を読む", () => {
const query = parsed(
new URLSearchParams({
q: "中央",
facility_name: "バリューマート",
serial: "8111R",
address: "東京都",
})
);
expect(query.q).toBe("中央");
expect(query.facilityName).toBe("バリューマート");
expect(query.serial).toBe("8111R");
expect(query.address).toBe("東京都");
});
it("文字列パラメータは前後の空白を除去する", () => {
const query = parsed(
new URLSearchParams({
q: " 中央 ",
facility_name: " バリューマート ",
serial: " 8111R ",
address: " 東京都 ",
})
);
expect(query.q).toBe("中央");
expect(query.facilityName).toBe("バリューマート");
expect(query.serial).toBe("8111R");
expect(query.address).toBe("東京都");
});
/**
* ★空白だけの入力は「記号だけ」ではなく「未指定」になる★(2026-09-14 に確認)
*
* 半角スペース・全角スペース・タブは ★`foldName` の除去対象17文字に含まれる★が、
* ★`.trim()` の除去対象でもある★(JS の `trim()` は U+3000 もタブも落とす)。
* → ★`includesFoldName` に届く前に空文字になり、素通し(全件)になる★。
* ★`・` や `-` は `trim()` では落ちない★ので、記号だけの入力として 0件になる。
*/
it("★空白だけの入力は trim で空文字=未指定になる★(記号だけの 0件 とは別の道)", () => {
const query = parsed(
new URLSearchParams({ q: " ", facility_name: " ", serial: "\t", address: " " })
);
expect(query.q).toBe("");
expect(query.facilityName).toBe("");
expect(query.serial).toBe("");
expect(query.address).toBe("");
// ★中点・ハイフンは trim で落ちない★ → 記号だけの入力としてフィルタまで届く
const symbols = parsed(
new URLSearchParams({ q: "・", facility_name: "-", serial: "_", address: "ー" })
);
expect(symbols.q).toBe("・");
expect(symbols.facilityName).toBe("-");
expect(symbols.serial).toBe("_");
expect(symbols.address).toBe("ー");
});
// ケース11: 不正値でも422にせず既定値・上限へ丸める(parseDsDeviceQuery と同じ挙動)
it("per_page=9999 は maxPerPage(200) に丸める", () => {
expect(parsed(new URLSearchParams({ per_page: "9999" })).perPage).toBe(
200
);
});
it("per_page=0 は 1 に丸める", () => {
expect(parsed(new URLSearchParams({ per_page: "0" })).perPage).toBe(1);
});
it("page=-1 は 1 に丸める", () => {
expect(parsed(new URLSearchParams({ page: "-1" })).page).toBe(1);
});
it("数値でない page / per_page は既定値になる", () => {
const query = parsed(
new URLSearchParams({ page: "abc", per_page: "xyz" })
);
expect(query.page).toBe(1);
expect(query.perPage).toBe(50);
});
/**
* ★group_id / customer_id(指示書AK 段階2)★
*
* ★★元は「渡してもパース結果に含まれない(D-053)」だったテストを反転させたもの★★ —
* ★`it` ごと消さないこと★。1本も残っていないと、次に誰かが「受け取って捨てる」
* 実装に戻しても気づけない(指示書AK 落とし穴8)。
*
* | # | 入力 | 期待 |
* |---|---|---|
* | 2-1 | `group_id=1` | `1`(★`toHaveProperty` ではなく値まで見る★) |
* | 2-2 | `customer_id=2` | `2` |
* | 2-3 | 未指定 | ★`null`★(絞らない。上の「省略時は…」のテスト) |
* | 2-4 | `group_id=0` | ★422★(0 は「グループなし」のセンチネル。有効なIDではない。D-011) |
* | 2-5 | `group_id=1abc` | ★422★(★`parseInt` の前方一致で `1` にしない★。D-123 で実際に踏んだ罠) |
* | 2-6 | `group_id=-1` | 422 |
* | 2-7 | `group_id=1.5` | 422 |
* | 2-8 | `9007199254740992`(安全整数超) | 422 |
*
* ★★このエンドポイント初の 422★★ — 以前は「不正値がありえない」ため
* `parseFacilityQuery()` は素の型を返していた(YAML にも 422 を追記済み)。
*/
it("group_id / customer_id を値として読む(D-053 解除。指示書AK 段階2)", () => {
const query = parsed(new URLSearchParams({ group_id: "1", customer_id: "2" }));
expect(query.groupId).toBe(1);
expect(query.customerId).toBe(2);
});
it.each(["0", "1abc", "-1", "1.5", "9007199254740992"])(
"group_id=%s は422",
(value) => {
const result = parseFacilityQuery(new URLSearchParams({ group_id: value }));
expect(result.ok).toBe(false);
if (result.ok) throw new Error("422を期待");
expect(result.status).toBe(422);
expect(result.error.error.details).toEqual({
group_id: ["1以上の整数で指定してください"],
});
}
);
it.each(["0", "1abc"])("customer_id=%s も同じ規則で422", (value) => {
const result = parseFacilityQuery(new URLSearchParams({ customer_id: value }));
expect(result.ok).toBe(false);
if (result.ok) throw new Error("422を期待");
expect(result.status).toBe(422);
expect(result.error.error.details).toEqual({
customer_id: ["1以上の整数で指定してください"],
});
});
});
// ★fixturesからはKeenon/Phantasを外したが、メーカー混在・複数台の境界ケースは
// ここの合成データ(row()/robot())で担保している(指示書N / D-087)。
// fixturesに無いメーカーのテストだからといって不要と判断して消さないこと。
describe("filterFacilitiesByQ", () => {
const items: Facility[] = [
row({
id: 1,
facility_name: "メガバリューマート中央本店",
prefecture: "東京都",
address: "東京都○○区1-2-3",
robots: [robot({ id: 101, serial_number: "8111R5628060011" })],
}),
row({
id: 2,
facility_name: "フレッシュタウン湖畔店",
prefecture: "三重県",
address: "三重県○○市○○町12-4",
robots: [
robot({ id: 102, maker: "Keenon", model: "T5", serial_number: "9333K7412050111" }),
],
}),
];
it("q が空なら全件返す", () => {
expect(filterFacilitiesByQ(items, "")).toHaveLength(2);
});
// ★記号ゆれ(指示書AG 段階3)★ ★F-08 の検索欄が実際に呼ぶのはこの q の側★
// 個別パラメータ側(`filterFacilitiesByFields`)にも同じ固定があるが、
// 画面が使う経路に無いと、q 側だけ壊れたときに赤くならない組み合わせが残る。
// `ドソ・キホーテ` は中点入りの架空名(fixtures の施設名には記号が1文字も無く
// 再現できないため合成データで組む。D-6。fixtures は変更しない)。
it("施設名は中点を打たなくても引ける/シリアルの記号は除かない", () => {
const symbolItems: Facility[] = [
row({
id: 10,
facility_name: "ドソ・キホーテ戸塚",
robots: [robot({ id: 110, serial_number: "PUDU-CC1-0001" })],
}),
];
expect(filterFacilitiesByQ(symbolItems, "ドソキホーテ").map((f) => f.id)).toEqual([
10,
]);
// ★シリアルは記号を除かない(除くと `CC1` の `1` と `0001` が繋がって
// `10001` が誤ヒットする。D-2)★
expect(filterFacilitiesByQ(symbolItems, "10001")).toEqual([]);
expect(filterFacilitiesByQ(symbolItems, "cc1-0001").map((f) => f.id)).toEqual([10]);
});
/**
* ★記号だけの入力(2026-09-14 に反転。案A)と OR の関係★
*
* ★反転前は `-` と打つと全件が出ていた★(施設名の照合が素通ししていたため)。
* ★反転後は「施設名・都道府県では当たらない・住所やシリアルでは当たる」★になり、
* ★ハイフンを含む行だけが出る★。★「全件」でも「0件」でもない★のが狙い。
*/
it("★`-` は施設名では当たらず、ハイフンを含む住所・シリアルの行だけが出る(OR)★", () => {
const mixed: Facility[] = [
row({
id: 20,
facility_name: "ドソ・キホーテ戸塚",
prefecture: "東京都",
// ★住所・シリアルの両方にハイフンが無い行★(出てはいけない)
address: "東京都○○区○○町三丁目二番一号",
robots: [robot({ id: 120, serial_number: "8111R5628060011" })],
}),
row({
id: 21,
facility_name: "バリューマート西通り店",
prefecture: "東京都",
address: "東京都○○区4-5-6", // ハイフン入りの住所
robots: [robot({ id: 121, serial_number: "8111R5628060022" })],
}),
row({
id: 22,
facility_name: "フレッシュタウン湖畔店",
prefecture: "三重県",
address: "三重県○○市○○町一番地",
robots: [robot({ id: 122, serial_number: "PUDU-CC1-0001" })], // ハイフン入りのシリアル
}),
];
// ★施設名(`includesFoldName`)では1件も当たらず、住所・シリアル側だけが当たる★
expect(filterFacilitiesByQ(mixed, "-").map((f) => f.id)).toEqual([21, 22]);
// ★中点は施設名にしかない★ — 施設名が素通ししていないので 0件
expect(filterFacilitiesByQ(mixed, "・")).toEqual([]);
expect(filterFacilitiesByQ(mixed, " ")).toEqual([]);
expect(filterFacilitiesByQ(mixed, "・ -")).toEqual([]);
// ★空文字(未指定)は反転していない★ — 素通しで全件
expect(filterFacilitiesByQ(mixed, "")).toHaveLength(3);
});
// ケース3
it("施設名の部分一致で絞り込む", () => {
expect(filterFacilitiesByQ(items, "中央").map((f) => f.id)).toEqual([1]);
});
// ケース9: 施設名が一致しなくても住所でヒットする(qの内部はOR)
it("住所の部分一致でヒットする(施設名は一致しない)", () => {
const hit = filterFacilitiesByQ(items, "○○市○○町12-4");
expect(hit.map((f) => f.id)).toEqual([2]);
expect(hit[0].facility_name).not.toContain("○○市");
});
it("都道府県の部分一致でヒットする", () => {
expect(filterFacilitiesByQ(items, "三重県").map((f) => f.id)).toEqual([2]);
});
it("シリアルの部分一致でヒットする", () => {
expect(filterFacilitiesByQ(items, "5628060011").map((f) => f.id)).toEqual([1]);
});
it("メーカーの部分一致でヒットする", () => {
expect(filterFacilitiesByQ(items, "Keenon").map((f) => f.id)).toEqual([2]);
});
it("機種の部分一致でヒットする", () => {
expect(filterFacilitiesByQ(items, "CC1").map((f) => f.id)).toEqual([1]);
});
// ケース4
it("大文字小文字を区別しない(8111r で 8111R5628060011 にヒット)", () => {
expect(filterFacilitiesByQ(items, "8111r").map((f) => f.id)).toEqual([1]);
});
// ケース5
it("該当なしなら空配列", () => {
expect(filterFacilitiesByQ(items, "ZZZ-NOMATCH")).toEqual([]);
});
// ケース7: ロボット0台でも落ちない
it("ロボット0台の施設があっても落ちない(施設名では拾える)", () => {
const withEmpty: Facility[] = [
...items,
row({ id: 3, facility_name: "フレッシュタウン未設置店", robots: [] }),
];
expect(() => filterFacilitiesByQ(withEmpty, "8111R")).not.toThrow();
expect(filterFacilitiesByQ(withEmpty, "8111R").map((f) => f.id)).toEqual([1]);
expect(filterFacilitiesByQ(withEmpty, "未設置").map((f) => f.id)).toEqual([3]);
});
// ケース8: 3台目のシリアルでもヒットする(先頭1台しか見ない実装を検出する)
it("複数台の施設は3台目のシリアルでもヒットする", () => {
const multi: Facility[] = [
row({
id: 7,
facility_name: "メガバリューマート緑野店",
robots: [
robot({ id: 107, serial_number: "8111R5628060099" }),
robot({ id: 108, serial_number: "8111R5628060100" }),
robot({ id: 109, maker: "Keenon", model: "T5", serial_number: "9333K7412050111" }),
],
}),
];
expect(filterFacilitiesByQ(multi, "9333K7412050111").map((f) => f.id)).toEqual([7]);
});
});
describe("filterFacilitiesByFields", () => {
const items: Facility[] = [
row({
id: 1,
facility_name: "バリューマート西通り店",
prefecture: "東京都",
address: "東京都○○区4-5-6",
robots: [robot({ id: 101, serial_number: "8111R5628060011" })],
}),
row({
id: 2,
facility_name: "バリューマート川下店",
prefecture: "神奈川県",
address: "神奈川県○○市○○区11-2-3",
robots: [robot({ id: 102, serial_number: "8111R5628060155" })],
}),
row({
id: 3,
facility_name: "フレッシュタウン湖畔店",
prefecture: "神奈川県",
address: "神奈川県○○市○○町12-4",
robots: [],
}),
];
it("すべて空なら全件返す", () => {
expect(filterFacilitiesByFields(items, fields())).toHaveLength(3);
});
// ★記号ゆれ(指示書AG 段階3)★ 5系統すべてに1件ずつ置いている
// ★施設名は記号を無視し、住所は無視しない★(D-2)。
// `ドソ・キホーテ` は中点入りの架空名(fixtures の施設名には記号が1文字も無く
// 再現できないため合成データで組む。D-6。fixtures は変更しない)。
// ★この個別パラメータはどの画面からも送られていない★が、API 契約上は存在するので
// q と同じ規則にしてある(罠10)。
it("施設名は中点を打たなくても引ける/住所のハイフンは除かない", () => {
const symbolItems: Facility[] = [
row({
id: 10,
facility_name: "ドソ・キホーテ戸塚",
address: "東京都○○区1-2-3",
}),
];
expect(
filterFacilitiesByFields(
symbolItems,
fields({ facilityName: "ドソキホーテ" })
).map((f) => f.id)
).toEqual([10]);
// ★住所は `1-2-3` の `-` が番地の区切りなので除かない★
// 除く実装にすると `東京都○○区123` になり `23` が誤ヒットする
expect(
filterFacilitiesByFields(symbolItems, fields({ address: "23" }))
).toHaveLength(0);
expect(
filterFacilitiesByFields(symbolItems, fields({ address: "1-2-3" })).map(
(f) => f.id
)
).toEqual([10]);
});
/**
* ★記号だけの入力(2026-09-14 に反転。案A)★ — 個別パラメータ側でも同じ規則にする。
* ★反転前は `facility_name=-` で全件が出ていた★。
* ★住所・シリアルは `includesFoldPlain` なので反転していない★(記号をそのまま探す)。
*/
it("★facility_name に記号だけを入れると 0件★/★address・serial は記号をそのまま探す★", () => {
const symbolItems: Facility[] = [
row({
id: 10,
facility_name: "ドソ・キホーテ戸塚",
address: "東京都○○区1-2-3",
robots: [robot({ id: 110, serial_number: "PUDU-CC1-0001" })],
}),
];
for (const needle of ["・", "-", "_", "ー", "・ー"]) {
expect(
filterFacilitiesByFields(symbolItems, fields({ facilityName: needle }))
).toHaveLength(0);
}
// ★空文字(未指定)は反転していない★ — 素通し
expect(
filterFacilitiesByFields(symbolItems, fields({ facilityName: "" }))
).toHaveLength(1);
// ★address / serial は `includesFoldPlain` のまま★ — `-` は実際に当たる
expect(
filterFacilitiesByFields(symbolItems, fields({ address: "-" })).map((f) => f.id)
).toEqual([10]);
expect(
filterFacilitiesByFields(symbolItems, fields({ serial: "-" })).map((f) => f.id)
).toEqual([10]);
});
// ケース3
it("facility_name の部分一致で複数ヒットする", () => {
expect(
filterFacilitiesByFields(items, fields({ facilityName: "バリューマート" })).map(
(f) => f.id
)
).toEqual([1, 2]);
});
it("address の部分一致で絞り込む", () => {
expect(
filterFacilitiesByFields(items, fields({ address: "神奈川県" })).map((f) => f.id)
).toEqual([2, 3]);
});
it("serial の部分一致で絞り込む", () => {
expect(
filterFacilitiesByFields(items, fields({ serial: "5628060155" })).map((f) => f.id)
).toEqual([2]);
});
// ケース4
it("大文字小文字を区別しない(8111r で 8111R… にヒット)", () => {
expect(
filterFacilitiesByFields(items, fields({ serial: "8111r" })).map((f) => f.id)
).toEqual([1, 2]);
});
// ケース7
it("ロボット0台の施設は serial 検索で落ちないしヒットもしない", () => {
expect(() =>
filterFacilitiesByFields(items, fields({ serial: "8111R" }))
).not.toThrow();
expect(
filterFacilitiesByFields(items, fields({ serial: "8111R" })).map((f) => f.id)
).not.toContain(3);
});
// ケース8
// ★このシリアル(9333K7412050111)はfixturesから外したKeenon T5由来の値だが、
// ここでは合成データとして意図的に残している(指示書N / D-087)。
// 3台目のシリアルでヒットするかという回帰テストの主旨であり、fixturesの実態とは無関係。
it("複数台の施設は3台目のシリアルでもヒットする", () => {
const multi: Facility[] = [
row({
id: 7,
robots: [
robot({ id: 107, serial_number: "8111R5628060099" }),
robot({ id: 108, serial_number: "8111R5628060100" }),
robot({ id: 109, serial_number: "9333K7412050111" }),
],
}),
];
expect(
filterFacilitiesByFields(multi, fields({ serial: "9333K7412050111" })).map(
(f) => f.id
)
).toEqual([7]);
});
// ケース10
it("facility_name と address は AND(両方満たす施設だけ残る)", () => {
expect(
filterFacilitiesByFields(
items,
fields({ facilityName: "バリューマート", address: "神奈川県" })
).map((f) => f.id)
).toEqual([2]);
});
// ケース5
it("AND の結果0件なら空配列", () => {
expect(
filterFacilitiesByFields(
items,
fields({ facilityName: "フレッシュタウン", address: "東京都" })
)
).toEqual([]);
});
});
describe("q と個別パラメータの関係(AND。OR は q の内部だけ)", () => {
const items: Facility[] = [
row({ id: 1, facility_name: "バリューマート西通り店", address: "東京都○○区4-5-6" }),
row({ id: 2, facility_name: "バリューマート川下店", address: "神奈川県○○市11-2-3" }),
];
it("q が住所で一致しても facility_name が一致しなければ落ちる", () => {
// route.ts と同じ組み立て順(q → 個別パラメータ)
const filtered = filterFacilitiesByFields(
filterFacilitiesByQ(items, "神奈川県"),
fields({ facilityName: "西通り" })
);
expect(filtered).toEqual([]);
});
it("両方満たすものだけ残る", () => {
const filtered = filterFacilitiesByFields(
filterFacilitiesByQ(items, "神奈川県"),
fields({ facilityName: "バリューマート" })
);
expect(filtered.map((f) => f.id)).toEqual([2]);
});
});
describe("sortFacilities", () => {
it("id の昇順になる", () => {
const items: Facility[] = [row({ id: 12 }), row({ id: 2 }), row({ id: 7 })];
expect(sortFacilities(items).map((f) => f.id)).toEqual([2, 7, 12]);
});
it("元の配列を書き換えない(純粋関数)", () => {
const items: Facility[] = [row({ id: 3 }), row({ id: 1 })];
sortFacilities(items);
expect(items.map((f) => f.id)).toEqual([3, 1]);
});
it("0件でも落ちない", () => {
expect(sortFacilities([])).toEqual([]);
});
});
describe("paginateFacilities", () => {
// ケース6: 12件 / per_page=5 → 3ページ(最終ページが端数の2件)
const items: Facility[] = Array.from({ length: 12 }, (_, i) => row({ id: i + 1 }));
// ケース1
it("0件でも落ちない(items=[] / meta.total=0)", () => {
expect(paginateFacilities([], 1, 50)).toEqual({
items: [],
meta: { total: 0, page: 1, per_page: 50 },
});
});
// ケース2
it("1件ならそのまま返る", () => {
const one = [row({ id: 5 })];
const body = paginateFacilities(one, 1, 50);
expect(body.items).toEqual(one);
expect(body.meta).toEqual({ total: 1, page: 1, per_page: 50 });
});
it("page=1 は先頭5件", () => {
const body = paginateFacilities(items, 1, 5);
expect(body.items.map((f) => f.id)).toEqual([1, 2, 3, 4, 5]);
expect(body.meta).toEqual({ total: 12, page: 1, per_page: 5 });
});
it("page=2 は6〜10件目", () => {
const body = paginateFacilities(items, 2, 5);
expect(body.items.map((f) => f.id)).toEqual([6, 7, 8, 9, 10]);
expect(body.meta.total).toBe(12);
});
it("page=3(最終ページ)は端数の2件", () => {
const body = paginateFacilities(items, 3, 5);
expect(body.items.map((f) => f.id)).toEqual([11, 12]);
expect(body.meta.total).toBe(12);
});
it("範囲外の page=99 は空だが meta.total は全件数のまま", () => {
const body = paginateFacilities(items, 99, 5);
expect(body.items).toEqual([]);
expect(body.meta).toEqual({ total: 12, page: 99, per_page: 5 });
});
// ケース5: フィルタ後0件でも meta の形は変わらない
it("フィルタ後0件なら items=[] / meta.total=0", () => {
const filtered = filterFacilitiesByQ(items, "ZZZ-NOMATCH");
expect(paginateFacilities(filtered, 1, 50)).toEqual({
items: [],
meta: { total: 0, page: 1, per_page: 50 },
});
});
// ★meta.total は「フィルタ後・ページング前」の件数★(ページング後の件数ではない)
it("meta.total はページング後の件数ではなくフィルタ後の総件数", () => {
const body = paginateFacilities(items, 1, 5);
expect(body.items).toHaveLength(5);
expect(body.meta.total).toBe(12);
});
});
// ★境界ケース: 未対応機種を持つ施設を合成し、isSupportedModel()がfalseを返すことの確認
// (指示書N 4-2。fixturesからはKeenon/Phantasを外したため、この合成データが
// 「未対応機種の検出」を担保する唯一の場所になる)
describe("isSupportedModel との組み合わせ(未対応機種の検出。指示書N / D-087)", () => {
it("Keenon T5(fixturesには存在しない機種)を持つ施設を合成すると、そのロボットは未対応と判定される", () => {
const facility = row({
id: 7,
facility_name: "メガバリューマート緑野店",
robots: [
robot({ id: 109, maker: "Keenon", model: "T5", serial_number: "9333K7412050111" }),
],
});
expect(isSupportedModel(facility.robots[0].maker, facility.robots[0].model)).toBe(
false
);
});
it("対応済み機種(PUDU CC1)を持つ施設は未対応と判定されない", () => {
const facility = row({ robots: [robot({ maker: "PUDU", model: "CC1" })] });
expect(isSupportedModel(facility.robots[0].maker, facility.robots[0].model)).toBe(
true
);
});
});