From 5271ec5b5936c4b8003e3acf8622ec40eb34550c Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Sun, 26 Apr 2026 15:59:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E5=9C=B0=E5=9D=80?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=97=A5=E6=9C=AC=E5=9C=B0=E5=8C=BA=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background/steps/fill-plus-checkout.js | 4 + content/plus-checkout.js | 159 ++++++++++++++++++++-- data/address-sources.js | 23 ++++ tests/address-sources.test.js | 5 + tests/plus-checkout-address-input.test.js | 86 ++++++++++++ 5 files changed, 264 insertions(+), 13 deletions(-) diff --git a/background/steps/fill-plus-checkout.js b/background/steps/fill-plus-checkout.js index cec29ba..c02c58a 100644 --- a/background/steps/fill-plus-checkout.js +++ b/background/steps/fill-plus-checkout.js @@ -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, { diff --git a/content/plus-checkout.js b/content/plus-checkout.js index 749a733..6ba1026 100644 --- a/content/plus-checkout.js +++ b/content/plus-checkout.js @@ -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, diff --git a/data/address-sources.js b/data/address-sources.js index df56c85..b670fb6 100644 --- a/data/address-sources.js +++ b/data/address-sources.js @@ -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', diff --git a/tests/address-sources.test.js b/tests/address-sources.test.js index 98dd917..5ffeb6f 100644 --- a/tests/address-sources.test.js +++ b/tests/address-sources.test.js @@ -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'); }); diff --git a/tests/plus-checkout-address-input.test.js b/tests/plus-checkout-address-input.test.js index fd95e91..68214cb 100644 --- a/tests/plus-checkout-address-input.test.js +++ b/tests/plus-checkout-address-input.test.js @@ -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']); +});