149 lines
7.7 KiB
TypeScript
149 lines
7.7 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
import test from "node:test";
|
|
import {
|
|
birthPlaceDraftFromLevels,
|
|
formatBirthPlaceCoordinate,
|
|
formatBirthPlaceTimezone,
|
|
hasVirtualCityLevel,
|
|
findProvinceNode,
|
|
resolveBirthPlaceNode,
|
|
} from "../src/lib/china-birth-place.ts";
|
|
|
|
const pickerSource = readFileSync(new URL("../src/components/birth-place-picker.tsx", import.meta.url), "utf8");
|
|
const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
|
|
|
test("resolves a province, city and district to the district centre", () => {
|
|
const resolved = resolveBirthPlaceNode({ provinceCode: "130000", cityCode: "130400", districtCode: "130402" });
|
|
assert.ok(resolved);
|
|
assert.equal(resolved.label, "河北省 · 邯郸市 · 邯山区");
|
|
assert.equal(resolved.placeType, "district");
|
|
assert.equal(resolved.node.code, "130402");
|
|
assert.equal(resolved.latitude, 36.603196);
|
|
assert.equal(resolved.longitude, 114.484989);
|
|
});
|
|
|
|
test("falls back to the city centre when the district is left unnamed", () => {
|
|
const resolved = resolveBirthPlaceNode({ provinceCode: "130000", cityCode: "130400", districtCode: "" });
|
|
assert.ok(resolved);
|
|
assert.equal(resolved.label, "河北省 · 邯郸市");
|
|
assert.equal(resolved.placeType, "city");
|
|
assert.equal(resolved.node.code, "130400");
|
|
assert.equal(resolved.latitude, 36.612273);
|
|
});
|
|
|
|
test("collapses the repeated city level of municipalities and special regions", () => {
|
|
assert.equal(hasVirtualCityLevel(findProvinceNode("110000")!), true);
|
|
assert.equal(hasVirtualCityLevel(findProvinceNode("130000")!), false);
|
|
|
|
// The city code may be absent because the picker never shows that level.
|
|
const resolved = resolveBirthPlaceNode({ provinceCode: "110000", cityCode: "", districtCode: "110105" });
|
|
assert.ok(resolved);
|
|
assert.equal(resolved.label, "北京市 · 朝阳区");
|
|
assert.equal(resolved.placeType, "district");
|
|
});
|
|
|
|
test("completes at the city for prefecture cities that have no districts", () => {
|
|
const resolved = resolveBirthPlaceNode({ provinceCode: "440000", cityCode: "441900", districtCode: "" });
|
|
assert.ok(resolved);
|
|
assert.equal(resolved.label, "广东省 · 东莞市");
|
|
assert.equal(resolved.placeType, "city");
|
|
assert.equal(resolved.node.code, "441900");
|
|
});
|
|
|
|
test("completes at the province when neither lower level offers a choice", () => {
|
|
const resolved = resolveBirthPlaceNode({ provinceCode: "710000", cityCode: "", districtCode: "" });
|
|
assert.ok(resolved);
|
|
assert.equal(resolved.label, "台湾省");
|
|
assert.equal(resolved.placeType, "province");
|
|
});
|
|
|
|
test("rejects codes that do not belong together", () => {
|
|
assert.equal(resolveBirthPlaceNode({ provinceCode: "999999", cityCode: "", districtCode: "" }), null);
|
|
assert.equal(resolveBirthPlaceNode({ provinceCode: "130000", cityCode: "440100", districtCode: "" }), null);
|
|
assert.equal(resolveBirthPlaceNode({ provinceCode: "130000", cityCode: "130400", districtCode: "110105" }), null);
|
|
});
|
|
|
|
test("re-syncing a saved profile keeps the same draft so the picker cannot clear itself", () => {
|
|
const saved = { provinceCode: "130000", cityCode: "130400", districtCode: "" };
|
|
const draft = birthPlaceDraftFromLevels(saved);
|
|
assert.deepEqual(draft, { provinceCode: "130000", cityCode: "130400", districtCode: "", cityCentre: true });
|
|
assert.deepEqual(birthPlaceDraftFromLevels(draft), draft);
|
|
|
|
// A city without districts is complete without being marked as an unsure centre.
|
|
assert.deepEqual(birthPlaceDraftFromLevels({ provinceCode: "440000", cityCode: "441900", districtCode: "" }), {
|
|
provinceCode: "440000",
|
|
cityCode: "441900",
|
|
districtCode: "",
|
|
cityCentre: false,
|
|
});
|
|
|
|
// A municipality resumes with its virtual city filled in.
|
|
assert.deepEqual(birthPlaceDraftFromLevels({ provinceCode: "110000", cityCode: "", districtCode: "110105" }), {
|
|
provinceCode: "110000",
|
|
cityCode: "110000-city",
|
|
districtCode: "110105",
|
|
cityCentre: false,
|
|
});
|
|
});
|
|
|
|
test("states coordinates and the resolved offset in plain Chinese", () => {
|
|
assert.equal(formatBirthPlaceCoordinate(36.612273, 114.490686), "东经 114.49° · 北纬 36.61°");
|
|
assert.equal(formatBirthPlaceTimezone("Asia/Shanghai", 8), "Asia/Shanghai · UTC+8");
|
|
assert.equal(formatBirthPlaceTimezone("Asia/Kathmandu", 5.75), "Asia/Kathmandu · UTC+5:45");
|
|
assert.equal(formatBirthPlaceTimezone("Asia/Shanghai", null), "Asia/Shanghai");
|
|
});
|
|
|
|
test("the picker offers three cascading levels instead of a free-text search", () => {
|
|
assert.equal(existsSync(new URL("../src/components/location-search-combobox.tsx", import.meta.url)), false);
|
|
assert.doesNotMatch(pickerSource, /role="combobox"/);
|
|
assert.match(pickerSource, /aria-label="出生省份"/);
|
|
assert.match(pickerSource, /aria-label="出生城市"/);
|
|
assert.match(pickerSource, /aria-label="出生区县"/);
|
|
// The level is hidden rather than shown empty when it carries no choice.
|
|
assert.match(pickerSource, /\{isChina && province && !virtualCity && \(/);
|
|
assert.match(pickerSource, /\{isChina && city && districts\.length > 0 && \(/);
|
|
assert.match(pickerSource, /不确定,用市区中心/);
|
|
assert.match(pickerSource, /精确到区 \/ 县就足够/);
|
|
});
|
|
|
|
test("timezone is resolved once for the chosen place rather than during browsing", () => {
|
|
assert.equal(existsSync(new URL("../src/app/api/locations/timezone/route.ts", import.meta.url)), true);
|
|
assert.match(pickerSource, /fetch\("\/api\/locations\/timezone"/);
|
|
// Browsing the levels cannot spend a lookup: only a resolved place gets a sync key.
|
|
assert.match(pickerSource, /if \(syncKey === "empty"\)/);
|
|
assert.match(pickerSource, /: \{ status: "loading" \};/);
|
|
// An unresolved zone must not be reported upward as a usable birth place.
|
|
assert.match(pickerSource, /setOutcome\(\{ key: syncKey, state: \{ status: "error" \} \}\);\s*onChangeRef\.current\(null\);/);
|
|
|
|
const service = readFileSync(new URL("../src/lib/birth-location-timezone-service.ts", import.meta.url), "utf8");
|
|
assert.match(service, /\/api\/location\/timezone/);
|
|
assert.match(service, /timezoneSource: "iana_historical"/);
|
|
});
|
|
|
|
test("mounting a saved birth place neither refetches nor clears the stored location", () => {
|
|
// Reporting null on mount would look like a location edit and would drop a
|
|
// rectified birth-time candidate just because the profile dialog was opened.
|
|
assert.match(pickerSource, /const savedIsCurrent = resolved !== null/);
|
|
assert.match(pickerSource, /if \(syncKey === "saved"\) return;/);
|
|
assert.match(pickerSource, /if \(saved\.countryCode !== "CN" \|\| saved\.provinceCode \|\| saved\.timezoneId\) onChangeRef\.current\(null\);/);
|
|
// A changed birth date invalidates the stored offset, so the zone is resolved again.
|
|
assert.match(pickerSource, /mountedMoment === birthMoment/);
|
|
});
|
|
|
|
test("the profile stores administrative codes alongside coordinates and the IANA zone", () => {
|
|
assert.match(pageSource, /<BirthPlacePicker/);
|
|
assert.match(pageSource, /provinceCode: location\?\.provinceCode \|\| ""/);
|
|
assert.match(pageSource, /cityCode: location\?\.cityCode \|\| ""/);
|
|
assert.match(pageSource, /districtCode: location\?\.districtCode \|\| ""/);
|
|
assert.match(pageSource, /birthPlaceProvider: location\?\.provider \|\| ""/);
|
|
assert.match(pageSource, /timezoneSource: location \? "iana_historical" : ""/);
|
|
assert.match(pageSource, /birth_place_provider_id:\s*nextProfile\.birthPlaceProviderId/);
|
|
assert.match(pageSource, /timezone_id:\s*nextProfile\.timezoneId/);
|
|
});
|
|
|
|
test("profile accepts a selected IANA location before a numeric offset is resolved", () => {
|
|
assert.match(pageSource, /profile\.timezoneId\.trim\(\)/);
|
|
assert.match(pageSource, /profile\.timezoneOffset === null \|\| Number\.isFinite\(profile\.timezoneOffset\)/);
|
|
});
|