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