Files
Jyotisha/frontend/tests/birth-place-picker.test.ts
T
Jesse_Chen 9dc2948ab6
Independent Staging Quality Gate / validate (push) Has been cancelled
Independent Staging Quality Gate / publish (push) Has been cancelled
feat(onboarding): pick the birth place by level instead of by search
The single search field asked people to type a place name and then judge
which of several near-identical results was theirs, which is the one thing
they cannot verify about their own birth record. Province, city and district
are now chosen from the dataset the app already ships, so there is nothing to
type and nothing to disambiguate.

Levels that offer no choice collapse: municipalities show one level, and
prefecture cities without districts stop at the city. A district can be left
as the city centre, which is accurate enough because the chart only needs
coordinates and a timezone.

The timezone is resolved once for the chosen place rather than on every
keystroke, through a dedicated route that both this picker and the Geoapify
path share.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 18:42:54 +08:00

149 lines
7.6 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, /\{province && !virtualCity && \(/);
assert.match(pickerSource, /\{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\.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 \? "china_locations" : ""/);
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\)/);
});