Browse Source
* fix: repair account geographic selects * fix: harden geographic select data handlingpull/11883/head
committed by
GitHub
15 changed files with 518 additions and 2037 deletions
@ -0,0 +1,281 @@ |
|||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; |
|||
import { render, waitFor } from '@testing-library/react'; |
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'; |
|||
import * as service from '../service'; |
|||
import BaseView from './base'; |
|||
|
|||
const mocks = vi.hoisted(() => ({ |
|||
form: { |
|||
setFieldValue: vi.fn(), |
|||
}, |
|||
initialValues: undefined as Record<string, any> | undefined, |
|||
onValuesChange: undefined as |
|||
| ((changedValues: Record<string, unknown>) => void) |
|||
| undefined, |
|||
dependencyProvince: { label: '浙江省', value: '330000' }, |
|||
requestResults: {} as Record<string, any>, |
|||
})); |
|||
|
|||
vi.mock('@ant-design/pro-components', async () => { |
|||
const React = await import('react'); |
|||
|
|||
const ProForm = ({ |
|||
children, |
|||
formRef, |
|||
initialValues, |
|||
onValuesChange, |
|||
}: any) => { |
|||
if (formRef) { |
|||
formRef.current = mocks.form; |
|||
} |
|||
mocks.initialValues = initialValues; |
|||
mocks.onValuesChange = onValuesChange; |
|||
return <form>{children}</form>; |
|||
}; |
|||
|
|||
ProForm.Group = ({ children }: any) => <div>{children}</div>; |
|||
|
|||
return { |
|||
ProForm, |
|||
ProFormDependency: ({ children }: any) => ( |
|||
<div>{children({ province: mocks.dependencyProvince })}</div> |
|||
), |
|||
ProFormFieldSet: ({ children }: any) => <div>{children}</div>, |
|||
ProFormSelect: ({ name, params, request }: any) => { |
|||
React.useEffect(() => { |
|||
request?.(params).then((result: any) => { |
|||
mocks.requestResults[name] = result; |
|||
}); |
|||
}, [name, params, request]); |
|||
return <div />; |
|||
}, |
|||
ProFormText: () => <div />, |
|||
ProFormTextArea: () => <div />, |
|||
}; |
|||
}); |
|||
|
|||
vi.mock('antd', () => ({ |
|||
Button: ({ children }: any) => <button type="button">{children}</button>, |
|||
Input: (props: any) => <input {...props} />, |
|||
Upload: ({ children }: any) => <div>{children}</div>, |
|||
message: { |
|||
success: vi.fn(), |
|||
}, |
|||
})); |
|||
|
|||
vi.mock('@ant-design/icons', () => ({ |
|||
UploadOutlined: () => <span />, |
|||
})); |
|||
|
|||
vi.mock('./index.style', () => ({ |
|||
default: () => ({ |
|||
styles: { |
|||
area_code: 'area-code', |
|||
avatar: 'avatar', |
|||
avatar_title: 'avatar-title', |
|||
baseView: 'base-view', |
|||
button_view: 'button-view', |
|||
left: 'left', |
|||
phone_number: 'phone-number', |
|||
right: 'right', |
|||
}, |
|||
}), |
|||
})); |
|||
|
|||
vi.mock('../service', () => ({ |
|||
queryCity: vi.fn(), |
|||
queryCurrent: vi.fn(), |
|||
queryProvince: vi.fn(), |
|||
})); |
|||
|
|||
describe('BaseView geographic selects', () => { |
|||
let queryClient: QueryClient; |
|||
|
|||
beforeEach(() => { |
|||
queryClient = new QueryClient({ |
|||
defaultOptions: { |
|||
queries: { retry: false }, |
|||
}, |
|||
}); |
|||
mocks.initialValues = undefined; |
|||
mocks.onValuesChange = undefined; |
|||
mocks.dependencyProvince = { label: '浙江省', value: '330000' }; |
|||
mocks.requestResults = {}; |
|||
vi.clearAllMocks(); |
|||
|
|||
vi.mocked(service.queryCurrent).mockResolvedValue({ |
|||
data: { |
|||
address: '西湖区工专路 77 号', |
|||
avatar: '', |
|||
country: 'China', |
|||
email: 'antdesign@alipay.com', |
|||
geographic: { |
|||
province: { label: '浙江省', key: '330000' }, |
|||
city: { label: '杭州市', key: '330100' }, |
|||
}, |
|||
group: '', |
|||
name: 'Ant Design', |
|||
notice: [], |
|||
notifyCount: 0, |
|||
phone: '0752-268888888', |
|||
signature: '', |
|||
tags: [], |
|||
title: '', |
|||
unreadCount: 0, |
|||
userid: '00000001', |
|||
}, |
|||
}); |
|||
vi.mocked(service.queryProvince).mockResolvedValue([ |
|||
{ key: '330000', label: '浙江省' }, |
|||
]); |
|||
vi.mocked(service.queryCity).mockResolvedValue([ |
|||
{ key: '330100', label: '杭州市' }, |
|||
]); |
|||
}); |
|||
|
|||
it('normalizes geographic initial values for labelInValue selects', async () => { |
|||
render( |
|||
<QueryClientProvider client={queryClient}> |
|||
<BaseView /> |
|||
</QueryClientProvider>, |
|||
); |
|||
|
|||
await waitFor(() => { |
|||
expect(mocks.initialValues?.province).toEqual({ |
|||
label: '浙江省', |
|||
value: '330000', |
|||
}); |
|||
expect(mocks.initialValues?.city).toEqual({ |
|||
label: '杭州市', |
|||
value: '330100', |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
it('does not set incomplete geographic initial values', async () => { |
|||
vi.mocked(service.queryCurrent).mockResolvedValue({ |
|||
data: { |
|||
address: '西湖区工专路 77 号', |
|||
avatar: '', |
|||
country: 'China', |
|||
email: 'antdesign@alipay.com', |
|||
geographic: { |
|||
province: { label: '浙江省' }, |
|||
city: { label: '杭州市' }, |
|||
}, |
|||
group: '', |
|||
name: 'Ant Design', |
|||
notice: [], |
|||
notifyCount: 0, |
|||
phone: '0752-268888888', |
|||
signature: '', |
|||
tags: [], |
|||
title: '', |
|||
unreadCount: 0, |
|||
userid: '00000001', |
|||
} as any, |
|||
}); |
|||
|
|||
render( |
|||
<QueryClientProvider client={queryClient}> |
|||
<BaseView /> |
|||
</QueryClientProvider>, |
|||
); |
|||
|
|||
await waitFor(() => { |
|||
expect(mocks.initialValues?.name).toBe('Ant Design'); |
|||
expect(mocks.initialValues?.province).toBeUndefined(); |
|||
expect(mocks.initialValues?.city).toBeUndefined(); |
|||
}); |
|||
}); |
|||
|
|||
it('loads cities with the selected province value', async () => { |
|||
render( |
|||
<QueryClientProvider client={queryClient}> |
|||
<BaseView /> |
|||
</QueryClientProvider>, |
|||
); |
|||
|
|||
await waitFor(() => { |
|||
expect(service.queryCity).toHaveBeenCalledWith('330000'); |
|||
}); |
|||
}); |
|||
|
|||
it('clears city when province changes', async () => { |
|||
render( |
|||
<QueryClientProvider client={queryClient}> |
|||
<BaseView /> |
|||
</QueryClientProvider>, |
|||
); |
|||
|
|||
await waitFor(() => { |
|||
expect(mocks.onValuesChange).toBeTypeOf('function'); |
|||
}); |
|||
|
|||
mocks.onValuesChange?.({ |
|||
province: { label: '河北省', value: '130000' }, |
|||
}); |
|||
|
|||
expect(mocks.form.setFieldValue).toHaveBeenCalledWith('city', undefined); |
|||
}); |
|||
|
|||
it('returns label/value option arrays for geographic selects', async () => { |
|||
render( |
|||
<QueryClientProvider client={queryClient}> |
|||
<BaseView /> |
|||
</QueryClientProvider>, |
|||
); |
|||
|
|||
await waitFor(() => { |
|||
expect(mocks.requestResults.province).toEqual([ |
|||
{ label: '浙江省', value: '330000' }, |
|||
]); |
|||
expect(mocks.requestResults.city).toEqual([ |
|||
{ label: '杭州市', value: '330100' }, |
|||
]); |
|||
}); |
|||
}); |
|||
|
|||
it('supports local mock name/id geographic responses', async () => { |
|||
vi.mocked(service.queryProvince).mockResolvedValue([ |
|||
{ id: '440000', name: '广东省' }, |
|||
]); |
|||
vi.mocked(service.queryCity).mockResolvedValue([ |
|||
{ id: '440300', name: '深圳市' }, |
|||
]); |
|||
|
|||
render( |
|||
<QueryClientProvider client={queryClient}> |
|||
<BaseView /> |
|||
</QueryClientProvider>, |
|||
); |
|||
|
|||
await waitFor(() => { |
|||
expect(mocks.requestResults.province).toEqual([ |
|||
{ label: '广东省', value: '440000' }, |
|||
]); |
|||
expect(mocks.requestResults.city).toEqual([ |
|||
{ label: '深圳市', value: '440300' }, |
|||
]); |
|||
}); |
|||
}); |
|||
|
|||
it('falls back to local city options when remote city data is empty', async () => { |
|||
mocks.dependencyProvince = { label: '江苏省', value: '320000' }; |
|||
vi.mocked(service.queryCity).mockResolvedValue([]); |
|||
|
|||
render( |
|||
<QueryClientProvider client={queryClient}> |
|||
<BaseView /> |
|||
</QueryClientProvider>, |
|||
); |
|||
|
|||
await waitFor(() => { |
|||
expect(service.queryCity).toHaveBeenCalledWith('320000'); |
|||
expect(mocks.requestResults.city).toContainEqual({ |
|||
label: '南京市', |
|||
value: '320100', |
|||
}); |
|||
}); |
|||
}); |
|||
}); |
|||
File diff suppressed because it is too large
@ -1,138 +0,0 @@ |
|||
[ |
|||
{ |
|||
"name": "北京市", |
|||
"id": "110000" |
|||
}, |
|||
{ |
|||
"name": "天津市", |
|||
"id": "120000" |
|||
}, |
|||
{ |
|||
"name": "河北省", |
|||
"id": "130000" |
|||
}, |
|||
{ |
|||
"name": "山西省", |
|||
"id": "140000" |
|||
}, |
|||
{ |
|||
"name": "内蒙古自治区", |
|||
"id": "150000" |
|||
}, |
|||
{ |
|||
"name": "辽宁省", |
|||
"id": "210000" |
|||
}, |
|||
{ |
|||
"name": "吉林省", |
|||
"id": "220000" |
|||
}, |
|||
{ |
|||
"name": "黑龙江省", |
|||
"id": "230000" |
|||
}, |
|||
{ |
|||
"name": "上海市", |
|||
"id": "310000" |
|||
}, |
|||
{ |
|||
"name": "江苏省", |
|||
"id": "320000" |
|||
}, |
|||
{ |
|||
"name": "浙江省", |
|||
"id": "330000" |
|||
}, |
|||
{ |
|||
"name": "安徽省", |
|||
"id": "340000" |
|||
}, |
|||
{ |
|||
"name": "福建省", |
|||
"id": "350000" |
|||
}, |
|||
{ |
|||
"name": "江西省", |
|||
"id": "360000" |
|||
}, |
|||
{ |
|||
"name": "山东省", |
|||
"id": "370000" |
|||
}, |
|||
{ |
|||
"name": "河南省", |
|||
"id": "410000" |
|||
}, |
|||
{ |
|||
"name": "湖北省", |
|||
"id": "420000" |
|||
}, |
|||
{ |
|||
"name": "湖南省", |
|||
"id": "430000" |
|||
}, |
|||
{ |
|||
"name": "广东省", |
|||
"id": "440000" |
|||
}, |
|||
{ |
|||
"name": "广西壮族自治区", |
|||
"id": "450000" |
|||
}, |
|||
{ |
|||
"name": "海南省", |
|||
"id": "460000" |
|||
}, |
|||
{ |
|||
"name": "重庆市", |
|||
"id": "500000" |
|||
}, |
|||
{ |
|||
"name": "四川省", |
|||
"id": "510000" |
|||
}, |
|||
{ |
|||
"name": "贵州省", |
|||
"id": "520000" |
|||
}, |
|||
{ |
|||
"name": "云南省", |
|||
"id": "530000" |
|||
}, |
|||
{ |
|||
"name": "西藏自治区", |
|||
"id": "540000" |
|||
}, |
|||
{ |
|||
"name": "陕西省", |
|||
"id": "610000" |
|||
}, |
|||
{ |
|||
"name": "甘肃省", |
|||
"id": "620000" |
|||
}, |
|||
{ |
|||
"name": "青海省", |
|||
"id": "630000" |
|||
}, |
|||
{ |
|||
"name": "宁夏回族自治区", |
|||
"id": "640000" |
|||
}, |
|||
{ |
|||
"name": "新疆维吾尔自治区", |
|||
"id": "650000" |
|||
}, |
|||
{ |
|||
"name": "台湾省", |
|||
"id": "710000" |
|||
}, |
|||
{ |
|||
"name": "香港特别行政区", |
|||
"id": "810000" |
|||
}, |
|||
{ |
|||
"name": "澳门特别行政区", |
|||
"id": "820000" |
|||
} |
|||
] |
|||
@ -0,0 +1,22 @@ |
|||
import { request } from '@umijs/max'; |
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'; |
|||
import { queryCity } from './service'; |
|||
|
|||
vi.mock('@umijs/max', () => ({ |
|||
request: vi.fn(), |
|||
})); |
|||
|
|||
describe('account settings service', () => { |
|||
beforeEach(() => { |
|||
vi.clearAllMocks(); |
|||
vi.mocked(request).mockResolvedValue({ data: [] }); |
|||
}); |
|||
|
|||
it('encodes province when requesting city options', async () => { |
|||
await queryCity('33/000?x=1'); |
|||
|
|||
expect(request).toHaveBeenCalledWith( |
|||
'/api/geographic/city/33%2F000%3Fx%3D1', |
|||
); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,52 @@ |
|||
import { describe, expect, it } from 'vitest'; |
|||
import { getCityOptions, provinceOptions } from './chinaDivision'; |
|||
|
|||
describe('chinaDivision geographic options', () => { |
|||
it('keeps province values compatible with existing six-digit province ids', () => { |
|||
expect(provinceOptions).toContainEqual({ |
|||
label: '浙江省', |
|||
key: '330000', |
|||
}); |
|||
expect(provinceOptions).toContainEqual({ |
|||
label: '台湾省', |
|||
key: '710000', |
|||
}); |
|||
}); |
|||
|
|||
it('returns city options for ordinary provinces', () => { |
|||
expect(getCityOptions('130000')).toContainEqual({ |
|||
label: '石家庄市', |
|||
key: '130100', |
|||
}); |
|||
expect(getCityOptions('320000')).toContainEqual({ |
|||
label: '南京市', |
|||
key: '320100', |
|||
}); |
|||
expect(getCityOptions('32')).toContainEqual({ |
|||
label: '南京市', |
|||
key: '320100', |
|||
}); |
|||
}); |
|||
|
|||
it('returns district options for municipalities', () => { |
|||
expect(getCityOptions('110000')).toContainEqual({ |
|||
label: '东城区', |
|||
key: '110101', |
|||
}); |
|||
}); |
|||
|
|||
it('returns city options for Hong Kong, Macao and Taiwan', () => { |
|||
expect(getCityOptions('710000')).toContainEqual({ |
|||
label: '台北市', |
|||
key: '710100', |
|||
}); |
|||
expect(getCityOptions('810000')).toContainEqual({ |
|||
label: '香港岛', |
|||
key: '810100', |
|||
}); |
|||
expect(getCityOptions('820000')).toContainEqual({ |
|||
label: '澳门半岛', |
|||
key: '820100', |
|||
}); |
|||
}); |
|||
}); |
|||
@ -0,0 +1,69 @@ |
|||
import hkMoTwSource from 'china-division/dist/HK-MO-TW.json'; |
|||
import pcaSource from 'china-division/dist/pca-code.json'; |
|||
|
|||
export type GeographicOption = { |
|||
label: string; |
|||
key: string; |
|||
}; |
|||
|
|||
type DivisionNode = { |
|||
code: string; |
|||
name: string; |
|||
children?: DivisionNode[]; |
|||
}; |
|||
|
|||
const directAdminCodes = new Set(['11', '12', '31', '50']); |
|||
|
|||
const specialRegions: GeographicOption[] = [ |
|||
{ label: '台湾省', key: '710000' }, |
|||
{ label: '香港特别行政区', key: '810000' }, |
|||
{ label: '澳门特别行政区', key: '820000' }, |
|||
]; |
|||
|
|||
const toProvinceKey = (code: string) => `${code}0000`; |
|||
|
|||
const toCityKey = (code: string) => (code.length === 4 ? `${code}00` : code); |
|||
|
|||
const toOption = (item: DivisionNode): GeographicOption => ({ |
|||
label: item.name, |
|||
key: toCityKey(item.code), |
|||
}); |
|||
|
|||
export const provinceOptions: GeographicOption[] = [ |
|||
...(pcaSource as DivisionNode[]).map((item) => ({ |
|||
label: item.name, |
|||
key: toProvinceKey(item.code), |
|||
})), |
|||
...specialRegions, |
|||
]; |
|||
|
|||
export const getCityOptions = (provinceKey: string): GeographicOption[] => { |
|||
const provinceCode = |
|||
provinceKey.length === 2 ? provinceKey : provinceKey.slice(0, 2); |
|||
const province = (pcaSource as DivisionNode[]).find( |
|||
(item) => item.code === provinceCode, |
|||
); |
|||
|
|||
if (province) { |
|||
const children = province.children ?? []; |
|||
const cityNodes = directAdminCodes.has(provinceCode) |
|||
? children.flatMap((item) => item.children ?? [item]) |
|||
: children; |
|||
|
|||
return cityNodes.map(toOption); |
|||
} |
|||
|
|||
const specialRegion = specialRegions.find((item) => item.key === provinceKey); |
|||
if (!specialRegion) { |
|||
return []; |
|||
} |
|||
|
|||
return Object.keys( |
|||
(hkMoTwSource as Record<string, Record<string, string[]>>)[ |
|||
specialRegion.label |
|||
] ?? {}, |
|||
).map((name, index) => ({ |
|||
label: name, |
|||
key: `${provinceCode}${String(index + 1).padStart(2, '0')}00`, |
|||
})); |
|||
}; |
|||
Loading…
Reference in new issue