feat: 增强地址处理逻辑,支持日本地区并优化相关测试用例
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
FR: '/fr-address',
|
||||
US: '/',
|
||||
};
|
||||
const MEIGUODIZHI_SUPPORTED_COUNTRIES = new Set(Object.keys(MEIGUODIZHI_PATH_BY_COUNTRY));
|
||||
|
||||
function createPlusCheckoutBillingExecutor(deps = {}) {
|
||||
const {
|
||||
@@ -75,6 +76,9 @@
|
||||
if (typeof fetchImpl !== 'function') {
|
||||
return null;
|
||||
}
|
||||
if (!MEIGUODIZHI_SUPPORTED_COUNTRIES.has(countryCode)) {
|
||||
return null;
|
||||
}
|
||||
const path = MEIGUODIZHI_PATH_BY_COUNTRY[countryCode] || MEIGUODIZHI_PATH_BY_COUNTRY.DE;
|
||||
const city = normalizeText(fallbackSeed?.fallback?.city || fallbackSeed?.query || '');
|
||||
const response = await fetchImpl(MEIGUODIZHI_ADDRESS_ENDPOINT, {
|
||||
|
||||
+146
-13
@@ -503,6 +503,10 @@ function readCountryText() {
|
||||
const option = countrySelect.selectedOptions?.[0];
|
||||
return option?.textContent || countrySelect.value || '';
|
||||
}
|
||||
const countryDropdown = findCountryDropdown();
|
||||
if (countryDropdown) {
|
||||
return getCountryDropdownValue(countryDropdown);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -664,14 +668,16 @@ function getRegionCandidates(value) {
|
||||
tas: 'Tasmania',
|
||||
vic: 'Victoria',
|
||||
wa: 'Western Australia',
|
||||
tokyo: '東京都',
|
||||
osaka: '大阪府',
|
||||
};
|
||||
const compact = raw.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const compact = raw.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
|
||||
const candidates = [raw];
|
||||
if (aliases[compact]) {
|
||||
candidates.push(aliases[compact]);
|
||||
}
|
||||
for (const [abbr, name] of Object.entries(aliases)) {
|
||||
const compactName = name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
const compactName = name.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
|
||||
if (compact === compactName) {
|
||||
candidates.push(abbr.toUpperCase());
|
||||
}
|
||||
@@ -679,13 +685,73 @@ function getRegionCandidates(value) {
|
||||
return Array.from(new Set(candidates.filter(Boolean)));
|
||||
}
|
||||
|
||||
function getCountryCandidates(value = '') {
|
||||
const raw = normalizeText(value);
|
||||
const compact = raw.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
|
||||
const aliases = {
|
||||
AU: ['Australia', '澳大利亚'],
|
||||
DE: ['Germany', 'Deutschland', '德国'],
|
||||
FR: ['France', '法国'],
|
||||
JP: ['Japan', '日本', '日本国'],
|
||||
US: ['United States', 'United States of America', 'USA', '美国'],
|
||||
};
|
||||
const direct = aliases[String(raw || '').trim().toUpperCase()] || [];
|
||||
const matched = Object.entries(aliases).find(([code, names]) => {
|
||||
if (String(code).toLowerCase() === compact) return true;
|
||||
return names.some((name) => {
|
||||
const normalizedName = normalizeText(name).toLowerCase();
|
||||
const compactName = normalizedName.replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
|
||||
return compact === compactName || normalizedName === raw.toLowerCase();
|
||||
});
|
||||
});
|
||||
return Array.from(new Set([raw, ...direct, ...(matched ? matched[1] : [])].filter(Boolean)));
|
||||
}
|
||||
|
||||
function matchesCountryOption(text, desiredValue) {
|
||||
const normalizedText = normalizeText(text).toLowerCase();
|
||||
const compactText = normalizedText.replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
|
||||
if (!compactText) return false;
|
||||
return getCountryCandidates(desiredValue).some((candidate) => {
|
||||
const normalizedCandidate = normalizeText(candidate).toLowerCase();
|
||||
const compactCandidate = normalizedCandidate.replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
|
||||
if (!compactCandidate) return false;
|
||||
return normalizedText === normalizedCandidate
|
||||
|| compactText === compactCandidate
|
||||
|| (compactCandidate.length > 3 && compactText.includes(compactCandidate));
|
||||
});
|
||||
}
|
||||
|
||||
function findCountryDropdown() {
|
||||
const controls = getVisibleControls('select, button, [role="button"], [role="combobox"], [aria-haspopup="listbox"]');
|
||||
return controls.find((control) => {
|
||||
if (!isEnabledControl(control) || isDocumentLevelContainer(control)) return false;
|
||||
const text = getFieldText(control);
|
||||
return /country/i.test(text) || /\u56fd\u5bb6|\u56fd\u5bb6\u6216\u5730\u533a/.test(text);
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function getCountryDropdownValue(control) {
|
||||
if (!control) return '';
|
||||
if (String(control.tagName || '').toUpperCase() === 'SELECT') {
|
||||
const selected = control.selectedOptions?.[0];
|
||||
return normalizeText(selected?.textContent || control.value || '');
|
||||
}
|
||||
return normalizeText(
|
||||
control.getAttribute?.('aria-valuetext')
|
||||
|| control.getAttribute?.('aria-label')
|
||||
|| control.getAttribute?.('data-value')
|
||||
|| control.textContent
|
||||
|| ''
|
||||
);
|
||||
}
|
||||
|
||||
function matchesRegionOption(text, desiredValue) {
|
||||
const normalizedText = normalizeText(text).toLowerCase();
|
||||
const compactText = normalizedText.replace(/[^a-z0-9]/g, '');
|
||||
const compactText = normalizedText.replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
|
||||
if (!compactText) return false;
|
||||
return getRegionCandidates(desiredValue).some((candidate) => {
|
||||
const normalizedCandidate = normalizeText(candidate).toLowerCase();
|
||||
const compactCandidate = normalizedCandidate.replace(/[^a-z0-9]/g, '');
|
||||
const compactCandidate = normalizedCandidate.replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
|
||||
if (!compactCandidate) return false;
|
||||
return normalizedText === normalizedCandidate
|
||||
|| compactText === compactCandidate
|
||||
@@ -701,7 +767,7 @@ function findRegionDropdown() {
|
||||
if (/country/i.test(text) || /\u56fd\u5bb6|\u5730\u533a/.test(text)) return false;
|
||||
return /state|province|county/i.test(text)
|
||||
|| /(?:^|\s)region(?:\s|$)/i.test(text)
|
||||
|| /\u5dde|\u7701/.test(text);
|
||||
|| /\u5dde|\u7701|\u8f96\u533a|\u90fd\u9053\u5e9c\u53bf/.test(text);
|
||||
}) || null;
|
||||
}
|
||||
|
||||
@@ -786,6 +852,53 @@ async function selectRegionDropdown(regionDropdown, value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function selectCountryDropdown(countryDropdown, value) {
|
||||
if (!countryDropdown || !value) return false;
|
||||
if (matchesCountryOption(getCountryDropdownValue(countryDropdown), value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (String(countryDropdown.tagName || '').toUpperCase() === 'SELECT') {
|
||||
const option = Array.from(countryDropdown.options || []).find((item) => (
|
||||
matchesCountryOption(item.textContent || '', value)
|
||||
|| matchesCountryOption(item.value || '', value)
|
||||
));
|
||||
if (!option) {
|
||||
throw new Error(`Plus Checkout: country dropdown option "${value}" was not found.`);
|
||||
}
|
||||
countryDropdown.value = option.value;
|
||||
option.selected = true;
|
||||
countryDropdown.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
countryDropdown.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
await sleep(500);
|
||||
return true;
|
||||
}
|
||||
|
||||
simulateClick(countryDropdown);
|
||||
await sleep(250);
|
||||
const startedAt = Date.now();
|
||||
let option = null;
|
||||
while (Date.now() - startedAt < 2500) {
|
||||
throwIfStopped();
|
||||
option = getVisibleRegionOptions().find((item) => (
|
||||
matchesCountryOption(getActionText(item) || item.textContent || '', value)
|
||||
));
|
||||
if (option) break;
|
||||
await sleep(100);
|
||||
}
|
||||
if (!option) {
|
||||
const visibleOptions = getVisibleRegionOptions()
|
||||
.map((item) => normalizeText(getActionText(item) || item.textContent || ''))
|
||||
.filter(Boolean)
|
||||
.slice(0, 12)
|
||||
.join(' | ');
|
||||
throw new Error(`Plus Checkout: country dropdown option "${value}" was not found. Visible options: ${visibleOptions || 'none'}.`);
|
||||
}
|
||||
simulateClick(option);
|
||||
await sleep(700);
|
||||
return true;
|
||||
}
|
||||
|
||||
function getStructuredAddressFields() {
|
||||
const address1 = findInputByFieldText([
|
||||
/address\s*(?:line)?\s*1|street/i,
|
||||
@@ -810,15 +923,31 @@ function getStructuredAddressFields() {
|
||||
return { address1, address2, city, region, postalCode };
|
||||
}
|
||||
|
||||
function fillIfEmpty(input, value) {
|
||||
function fillIfEmpty(input, value, options = {}) {
|
||||
if (!input || !value) return false;
|
||||
if (String(input.value || '').trim()) return false;
|
||||
if (!options.overwrite && String(input.value || '').trim()) return false;
|
||||
if (options.overwrite && String(input.value || '').trim() === String(value || '').trim()) return false;
|
||||
fillInput(input, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function ensureStructuredAddress(seed) {
|
||||
function isDropdownStructuredAddressForm(fields = getStructuredAddressFields()) {
|
||||
return Boolean(
|
||||
findCountryDropdown()
|
||||
&& findRegionDropdown()
|
||||
&& fields.address1
|
||||
&& fields.city
|
||||
&& fields.postalCode
|
||||
);
|
||||
}
|
||||
|
||||
async function ensureStructuredAddress(seed, options = {}) {
|
||||
const fallback = seed?.fallback || {};
|
||||
const overwrite = Boolean(options.overwrite);
|
||||
const countryDropdown = findCountryDropdown();
|
||||
if (countryDropdown && seed?.countryCode) {
|
||||
await selectCountryDropdown(countryDropdown, seed.countryCode);
|
||||
}
|
||||
const fields = await waitUntil(() => {
|
||||
const currentFields = getStructuredAddressFields();
|
||||
if (currentFields.address1 || currentFields.city || currentFields.postalCode) {
|
||||
@@ -830,10 +959,10 @@ async function ensureStructuredAddress(seed) {
|
||||
intervalMs: 250,
|
||||
});
|
||||
|
||||
fillIfEmpty(fields.address1, fallback.address1);
|
||||
fillIfEmpty(fields.city, fallback.city);
|
||||
fillIfEmpty(fields.address1, fallback.address1, { overwrite });
|
||||
fillIfEmpty(fields.city, fallback.city, { overwrite });
|
||||
await selectRegionDropdown(findRegionDropdown(), fallback.region);
|
||||
fillIfEmpty(fields.postalCode, fallback.postalCode);
|
||||
fillIfEmpty(fields.postalCode, fallback.postalCode, { overwrite });
|
||||
await sleep(500);
|
||||
|
||||
const latest = getStructuredAddressFields();
|
||||
@@ -895,10 +1024,14 @@ async function fillPlusBillingAddress(payload = {}) {
|
||||
},
|
||||
};
|
||||
let selected = { selectedText: '' };
|
||||
if (!seed.skipAutocomplete) {
|
||||
const fields = getStructuredAddressFields();
|
||||
const useDirectStructuredBranch = Boolean(seed.skipAutocomplete || isDropdownStructuredAddressForm(fields));
|
||||
if (!useDirectStructuredBranch) {
|
||||
selected = await selectAddressSuggestion(seed);
|
||||
}
|
||||
const structuredAddress = await ensureStructuredAddress(seed);
|
||||
const structuredAddress = await ensureStructuredAddress(seed, {
|
||||
overwrite: useDirectStructuredBranch,
|
||||
});
|
||||
|
||||
return {
|
||||
countryText,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
AU: ['au', 'aus', 'australia', '澳大利亚'],
|
||||
DE: ['de', 'deu', 'germany', 'deutschland', '德国'],
|
||||
FR: ['fr', 'fra', 'france', '法国'],
|
||||
JP: ['jp', 'jpn', 'japan', '日本', '日本国'],
|
||||
US: ['us', 'usa', 'united states', 'united states of america', 'america', '美国'],
|
||||
};
|
||||
|
||||
@@ -75,6 +76,28 @@
|
||||
},
|
||||
},
|
||||
],
|
||||
JP: [
|
||||
{
|
||||
query: 'Tokyo Marunouchi',
|
||||
suggestionIndex: 1,
|
||||
fallback: {
|
||||
address1: 'Marunouchi 1-1',
|
||||
city: 'Chiyoda-ku',
|
||||
region: 'Tokyo',
|
||||
postalCode: '100-0005',
|
||||
},
|
||||
},
|
||||
{
|
||||
query: 'Osaka Umeda',
|
||||
suggestionIndex: 1,
|
||||
fallback: {
|
||||
address1: 'Umeda 3-1',
|
||||
city: 'Kita-ku',
|
||||
region: 'Osaka',
|
||||
postalCode: '530-0001',
|
||||
},
|
||||
},
|
||||
],
|
||||
US: [
|
||||
{
|
||||
query: 'New York NY',
|
||||
|
||||
@@ -9,6 +9,7 @@ test('address sources normalize supported countries and return local seeds', ()
|
||||
|
||||
assert.equal(api.normalizeCountryCode('Deutschland'), 'DE');
|
||||
assert.equal(api.normalizeCountryCode('澳大利亚'), 'AU');
|
||||
assert.equal(api.normalizeCountryCode('日本'), 'JP');
|
||||
assert.equal(api.normalizeCountryCode('unknown'), '');
|
||||
|
||||
const deSeed = api.getAddressSeedForCountry('DE');
|
||||
@@ -20,4 +21,8 @@ test('address sources normalize supported countries and return local seeds', ()
|
||||
const fallbackSeed = api.getAddressSeedForCountry('unknown', { fallbackCountry: 'AU' });
|
||||
assert.equal(fallbackSeed.countryCode, 'AU');
|
||||
assert.equal(fallbackSeed.fallback.region, 'New South Wales');
|
||||
|
||||
const jpSeed = api.getAddressSeedForCountry('日本');
|
||||
assert.equal(jpSeed.countryCode, 'JP');
|
||||
assert.equal(jpSeed.fallback.region, 'Tokyo');
|
||||
});
|
||||
|
||||
@@ -253,3 +253,89 @@ return { selectRegionDropdown };
|
||||
|
||||
assert.deepEqual(clicks, [stateDropdown, options[1]]);
|
||||
});
|
||||
|
||||
test('country and region helpers recognize the dropdown-style localized address form', () => {
|
||||
const bundle = [
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getActionText'),
|
||||
extractFunction('getFieldText'),
|
||||
extractFunction('getVisibleControls'),
|
||||
extractFunction('isEnabledControl'),
|
||||
extractFunction('isDocumentLevelContainer'),
|
||||
extractFunction('getCountryCandidates'),
|
||||
extractFunction('matchesCountryOption'),
|
||||
extractFunction('findCountryDropdown'),
|
||||
extractFunction('getRegionCandidates'),
|
||||
extractFunction('matchesRegionOption'),
|
||||
extractFunction('findRegionDropdown'),
|
||||
].join('\n');
|
||||
|
||||
const countryDropdown = createElement({
|
||||
tagName: 'DIV',
|
||||
text: '国家或地区 日本',
|
||||
attrs: {
|
||||
role: 'combobox',
|
||||
'aria-haspopup': 'listbox',
|
||||
},
|
||||
});
|
||||
const regionDropdown = createElement({
|
||||
tagName: 'DIV',
|
||||
text: '辖区 选择',
|
||||
attrs: {
|
||||
role: 'combobox',
|
||||
'aria-haspopup': 'listbox',
|
||||
},
|
||||
});
|
||||
const elements = [countryDropdown, regionDropdown];
|
||||
const documentMock = {
|
||||
documentElement: {},
|
||||
body: {},
|
||||
querySelectorAll: (selector) => {
|
||||
if (String(selector || '').includes('label[for=')) return [];
|
||||
if (String(selector || '').includes('combobox') || String(selector || '').includes('button') || String(selector || '').includes('select')) {
|
||||
return elements;
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
const windowMock = {
|
||||
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
|
||||
};
|
||||
const cssMock = {
|
||||
escape: (value) => String(value),
|
||||
};
|
||||
|
||||
const api = new Function('window', 'document', 'CSS', `
|
||||
${bundle}
|
||||
return { findCountryDropdown, findRegionDropdown, matchesCountryOption, matchesRegionOption };
|
||||
`)(windowMock, documentMock, cssMock);
|
||||
|
||||
assert.equal(api.findCountryDropdown(), countryDropdown);
|
||||
assert.equal(api.findRegionDropdown(), regionDropdown);
|
||||
assert.equal(api.matchesCountryOption('日本', 'JP'), true);
|
||||
assert.equal(api.matchesCountryOption('德国', 'DE'), true);
|
||||
assert.equal(api.matchesRegionOption('東京都', 'Tokyo'), true);
|
||||
});
|
||||
|
||||
test('fillIfEmpty can overwrite invalid structured address values in the dropdown branch', () => {
|
||||
const bundle = [
|
||||
extractFunction('fillIfEmpty'),
|
||||
].join('\n');
|
||||
const input = { value: '77022' };
|
||||
const writes = [];
|
||||
const api = new Function('input', 'writes', `
|
||||
function fillInput(el, value) {
|
||||
writes.push(value);
|
||||
el.value = value;
|
||||
}
|
||||
${bundle}
|
||||
return { fillIfEmpty };
|
||||
`)(input, writes);
|
||||
|
||||
assert.equal(api.fillIfEmpty(input, '100-0005'), false);
|
||||
assert.equal(input.value, '77022');
|
||||
assert.equal(api.fillIfEmpty(input, '100-0005', { overwrite: true }), true);
|
||||
assert.equal(input.value, '100-0005');
|
||||
assert.deepEqual(writes, ['100-0005']);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user