PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.3
WooCommerce v10.9.3
11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / assets / js / frontend / test / address-autocomplete.js
woocommerce / assets / js / frontend / test Last commit date
address-autocomplete.js 2 months ago address-autocomplete.min.js 2 months ago checkout-place-order-api.js 4 months ago checkout-place-order-api.min.js 4 months ago custom-place-order-button.js 4 months ago custom-place-order-button.min.js 4 months ago
address-autocomplete.js
1704 lines
1 /**
2 * @jest-environment jest-fixed-jsdom
3 */
4
5 describe( 'Address Autocomplete Provider Registration', () => {
6 beforeEach( () => {
7 delete global.window.wc;
8 // Reset the window object and providers before each test
9 Object.assign( global.window, {
10 wc_address_autocomplete_params: {
11 address_providers: JSON.stringify( [
12 { id: 'test-provider', name: 'Test provider' },
13 { id: 'wc-payments', name: 'WooCommerce Payments' },
14 { id: 'provider-1', name: 'Provider 1' },
15 { id: 'provider-2', name: 'Provider 2' },
16 ] ),
17 },
18 } );
19
20 // Reset the module before each test
21 jest.resetModules();
22 require( '../utils/address-autocomplete-common' );
23 require( '../address-autocomplete' );
24 } );
25
26 test( 'should successfully register a valid provider', () => {
27 const validProvider = {
28 id: 'test-provider',
29 canSearch: () => {},
30 search: () => {},
31 select: () => {},
32 };
33
34 const result =
35 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
36 validProvider
37 );
38 expect( result ).toBe( true );
39 expect( console ).not.toHaveErrored();
40 } );
41
42 test( 'should reject invalid provider (null, undefined, non-object)', () => {
43 const invalidProviders = [ null, undefined, 'string', 123, true ];
44
45 invalidProviders.forEach( ( provider ) => {
46 const result =
47 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
48 provider
49 );
50 expect( result ).toBe( false );
51 expect( console ).toHaveErroredWith(
52 'Error registering address provider:',
53 'Address provider must be a valid object'
54 );
55 expect( console ).toHaveErrored();
56 } );
57 } );
58
59 test( 'should handle missing wc_address_autocomplete_params', () => {
60 delete global.window.wc; // ensure fresh load
61 global.window.wc_address_autocomplete_params = undefined;
62 jest.resetModules();
63 require( '../utils/address-autocomplete-common' );
64 require( '../address-autocomplete' );
65 const validProvider = {
66 id: 'test-provider',
67 canSearch: () => {},
68 search: () => {},
69 select: () => {},
70 };
71
72 const result =
73 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
74 validProvider
75 );
76 expect( result ).toBe( false );
77 expect( console ).toHaveErroredWith(
78 'Error registering address provider:',
79 'Provider test-provider not registered on server'
80 );
81 } );
82
83 test( 'should handle invalid address_providers type', () => {
84 delete global.window.wc; // ensure fresh load
85 global.window.wc_address_autocomplete_params = undefined;
86 jest.resetModules();
87 require( '../utils/address-autocomplete-common' );
88 require( '../address-autocomplete' );
89 const validProvider = {
90 id: 'test-provider',
91 canSearch: () => {},
92 search: () => {},
93 select: () => {},
94 };
95
96 const result =
97 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
98 validProvider
99 );
100 expect( result ).toBe( false );
101 expect( console ).toHaveErroredWith(
102 'Error registering address provider:',
103 'Provider test-provider not registered on server'
104 );
105 } );
106
107 test( 'should reject provider without ID', () => {
108 const invalidProvider = {
109 canSearch: () => {},
110 search: () => {},
111 select: () => {},
112 };
113
114 const result =
115 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
116 invalidProvider
117 );
118 expect( result ).toBe( false );
119 expect( console ).toHaveErroredWith(
120 'Error registering address provider:',
121 'Address provider must have a valid ID'
122 );
123 } );
124
125 test( 'should reject provider with non-string ID', () => {
126 const invalidProvider = {
127 id: 123,
128 canSearch: () => {},
129 search: () => {},
130 select: () => {},
131 };
132
133 const result =
134 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
135 invalidProvider
136 );
137 expect( result ).toBe( false );
138 expect( console ).toHaveErroredWith(
139 'Error registering address provider:',
140 'Address provider must have a valid ID'
141 );
142 } );
143
144 test( 'should reject provider without canSearch function', () => {
145 const invalidProvider = {
146 id: 'test-provider',
147 search: () => {},
148 select: () => {},
149 };
150
151 const result =
152 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
153 invalidProvider
154 );
155 expect( result ).toBe( false );
156 expect( console ).toHaveErroredWith(
157 'Error registering address provider:',
158 'Address provider must have a canSearch function'
159 );
160 } );
161
162 test( 'should reject provider without search function', () => {
163 const invalidProvider = {
164 id: 'test-provider',
165 canSearch: () => {},
166 select: () => {},
167 };
168
169 const result =
170 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
171 invalidProvider
172 );
173 expect( result ).toBe( false );
174 expect( console ).toHaveErroredWith(
175 'Error registering address provider:',
176 'Address provider must have a search function'
177 );
178 } );
179
180 test( 'should reject provider without select function', () => {
181 const invalidProvider = {
182 id: 'test-provider',
183 canSearch: () => {},
184 search: () => {},
185 };
186
187 const result =
188 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
189 invalidProvider
190 );
191 expect( result ).toBe( false );
192 expect( console ).toHaveErroredWith(
193 'Error registering address provider:',
194 'Address provider must have a select function'
195 );
196 } );
197
198 test( 'should reject provider not registered on server', () => {
199 const unregisteredProvider = {
200 id: 'unregistered-provider',
201 canSearch: () => {},
202 search: () => {},
203 select: () => {},
204 };
205
206 const result =
207 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
208 unregisteredProvider
209 );
210 expect( result ).toBe( false );
211 expect( console ).toHaveErroredWith(
212 'Error registering address provider:',
213 'Provider unregistered-provider not registered on server'
214 );
215 } );
216
217 test( 'should freeze provider after successful registration', () => {
218 const validProvider = {
219 id: 'test-provider',
220 canSearch: () => {},
221 search: () => {},
222 select: () => {},
223 };
224
225 const result =
226 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
227 validProvider
228 );
229 expect( result ).toBe( true );
230
231 // Verify provider is frozen
232 expect(
233 Object.isFrozen(
234 window.wc.addressAutocomplete.providers[ 'test-provider' ]
235 )
236 ).toBe( true );
237
238 // Attempt to modify should throw in strict mode
239 expect( () => {
240 window.wc.addressAutocomplete.providers[ 'test-provider' ].newProp =
241 'test';
242 } ).toThrow( TypeError );
243
244 // Verify the property wasn't added
245 expect(
246 window.wc.addressAutocomplete.providers[ 'test-provider' ].newProp
247 ).toBeUndefined();
248 } );
249
250 test( 'should not allow duplicate provider registration', () => {
251 const provider1 = {
252 id: 'test-provider',
253 canSearch: () => false,
254 search: () => [ 'original' ],
255 select: () => {},
256 };
257
258 const provider2 = {
259 id: 'test-provider',
260 canSearch: () => true,
261 search: () => [ 'duplicate' ],
262 select: () => {},
263 };
264
265 // Mock console.warn to capture warning message
266 const consoleSpy = jest
267 .spyOn( console, 'warn' )
268 .mockImplementation( () => {} );
269
270 // Register first provider
271 const firstResult =
272 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
273 provider1
274 );
275 expect( firstResult ).toBe( true );
276
277 // Try to register second provider with same ID
278 const duplicateResult =
279 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
280 provider2
281 );
282 expect( duplicateResult ).toBe( false );
283
284 // Verify warning was logged
285 expect( consoleSpy ).toHaveBeenCalledWith(
286 'Address provider with ID "test-provider" is already registered.'
287 );
288
289 // Verify the original provider is preserved (not overwritten)
290 expect(
291 window.wc.addressAutocomplete.providers[
292 'test-provider'
293 ].canSearch()
294 ).toBe( false );
295 expect(
296 window.wc.addressAutocomplete.providers[ 'test-provider' ].search()
297 ).toEqual( [ 'original' ] );
298
299 consoleSpy.mockRestore();
300 } );
301
302 test( 'should allow multiple providers with different IDs', () => {
303 const provider1 = {
304 id: 'provider-1',
305 canSearch: () => true,
306 search: () => [ 'provider1-results' ],
307 select: () => {},
308 };
309
310 const provider2 = {
311 id: 'provider-2',
312 canSearch: () => true,
313 search: () => [ 'provider2-results' ],
314 select: () => {},
315 };
316
317 // Register both providers
318 const result1 =
319 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
320 provider1
321 );
322 const result2 =
323 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
324 provider2
325 );
326
327 expect( result1 ).toBe( true );
328 expect( result2 ).toBe( true );
329
330 // Verify both providers are registered
331 expect(
332 window.wc.addressAutocomplete.providers[ 'provider-1' ]
333 ).toBeDefined();
334 expect(
335 window.wc.addressAutocomplete.providers[ 'provider-2' ]
336 ).toBeDefined();
337
338 // Verify they maintain their separate functionality
339 expect(
340 window.wc.addressAutocomplete.providers[ 'provider-1' ].search()
341 ).toEqual( [ 'provider1-results' ] );
342 expect(
343 window.wc.addressAutocomplete.providers[ 'provider-2' ].search()
344 ).toEqual( [ 'provider2-results' ] );
345 } );
346 } );
347
348 describe( 'Address Suggestions Component', () => {
349 let mockProvider;
350 let billingAddressInput;
351 let shippingAddressInput;
352
353 beforeEach( async () => {
354 // Reset DOM
355 document.body.innerHTML = '';
356 delete global.window.wc;
357
358 // Mock jQuery
359 global.window.jQuery = jest.fn( ( selector ) => ( {
360 hasClass: jest.fn( () => false ),
361 trigger: jest.fn(),
362 select2: jest.fn(),
363 on: jest.fn(),
364 } ) );
365
366 // Setup window object
367 Object.assign( global.window, {
368 DOMPurify: {
369 sanitize: ( input ) => input, // No-op for testing
370 },
371 wc_address_autocomplete_common_params: {
372 address_providers: JSON.stringify( [
373 {
374 id: 'test-provider',
375 name: 'Test provider',
376 branding_html:
377 '<div class="provider-branding">Powered by Test Provider</div>',
378 },
379 {
380 id: 'test-provider-unbranded',
381 name: 'Test provider unbranded',
382 },
383 ] ),
384 },
385 } );
386
387 // Create DOM structure
388 const form = document.createElement( 'form' );
389
390 // Billing fields
391 const billingCountry = document.createElement( 'select' );
392 billingCountry.id = 'billing_country';
393 const billingOption = document.createElement( 'option' );
394 billingOption.value = 'US';
395 billingOption.selected = true;
396 billingCountry.appendChild( billingOption );
397 billingCountry.value = 'US';
398
399 const billingAddress1 = document.createElement( 'input' );
400 billingAddress1.id = 'billing_address_1';
401 billingAddress1.type = 'text';
402
403 const billingCity = document.createElement( 'input' );
404 billingCity.id = 'billing_city';
405 billingCity.type = 'text';
406
407 const billingPostcode = document.createElement( 'input' );
408 billingPostcode.id = 'billing_postcode';
409 billingPostcode.type = 'text';
410
411 const billingState = document.createElement( 'input' );
412 billingState.id = 'billing_state';
413 billingState.type = 'text';
414
415 // Create wrapper for billing address
416 const billingWrapper = document.createElement( 'div' );
417 billingWrapper.className = 'woocommerce-input-wrapper';
418 billingWrapper.appendChild( billingAddress1 );
419
420 // Shipping fields
421 const shippingCountry = document.createElement( 'select' );
422 shippingCountry.id = 'shipping_country';
423 const shippingOption = document.createElement( 'option' );
424 shippingOption.value = 'US';
425 shippingOption.selected = true;
426 shippingCountry.appendChild( shippingOption );
427 shippingCountry.value = 'US';
428
429 const shippingAddress1 = document.createElement( 'input' );
430 shippingAddress1.id = 'shipping_address_1';
431 shippingAddress1.type = 'text';
432
433 const shippingCity = document.createElement( 'input' );
434 shippingCity.id = 'shipping_city';
435 shippingCity.type = 'text';
436
437 const shippingPostcode = document.createElement( 'input' );
438 shippingPostcode.id = 'shipping_postcode';
439 shippingPostcode.type = 'text';
440
441 const shippingState = document.createElement( 'input' );
442 shippingState.id = 'shipping_state';
443 shippingState.type = 'text';
444
445 // Create wrapper for shipping address
446 const shippingWrapper = document.createElement( 'div' );
447 shippingWrapper.className = 'woocommerce-input-wrapper';
448 shippingWrapper.appendChild( shippingAddress1 );
449
450 form.appendChild( billingCountry );
451 form.appendChild( billingWrapper );
452 form.appendChild( billingCity );
453 form.appendChild( billingPostcode );
454 form.appendChild( billingState );
455 form.appendChild( shippingCountry );
456 form.appendChild( shippingWrapper );
457 form.appendChild( shippingCity );
458 form.appendChild( shippingPostcode );
459 form.appendChild( shippingState );
460
461 document.body.appendChild( form );
462
463 billingAddressInput = billingAddress1;
464 shippingAddressInput = shippingAddress1;
465
466 // Create mock provider
467 mockProvider = {
468 id: 'test-provider',
469 canSearch: jest.fn( ( country ) => country === 'US' ),
470 search: jest.fn( async ( query, country, type ) => [
471 {
472 id: 'addr1',
473 label: '123 Main Street, City, US',
474 matchedSubstrings: [ { offset: 0, length: 3 } ],
475 },
476 {
477 id: 'addr2',
478 label: '456 Oak Avenue, Town, US',
479 matchedSubstrings: [ { offset: 0, length: 3 } ],
480 },
481 ] ),
482 select: jest.fn( async ( addressId ) => ( {
483 address_1: '123 Main Street',
484 city: 'City',
485 postcode: '12345',
486 country: 'US',
487 state: 'CA',
488 } ) ),
489 };
490
491 // Reset modules and require fresh instance
492 jest.resetModules();
493 require( '../utils/address-autocomplete-common' );
494 require( '../address-autocomplete' );
495
496 // Register the mock provider
497 window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
498 mockProvider
499 );
500
501 // Trigger DOMContentLoaded event and wait for initialization
502 const event = new Event( 'DOMContentLoaded' );
503 document.dispatchEvent( event );
504
505 // Wait a bit for DOM initialization to complete
506 await new Promise( ( resolve ) => setTimeout( resolve, 10 ) );
507 } );
508
509 afterEach( () => {
510 jest.clearAllMocks();
511 // Reset providers properly
512 if ( window.wc && window.wc.addressAutocomplete ) {
513 window.wc.addressAutocomplete.providers = {};
514 window.wc.addressAutocomplete.activeProvider = {
515 billing: null,
516 shipping: null,
517 };
518 }
519 } );
520
521 describe( 'DOM Initialization', () => {
522 test( 'should create suggestions container for address inputs', () => {
523 const billingSuggestions = document.getElementById(
524 'address_suggestions_billing'
525 );
526 const shippingSuggestions = document.getElementById(
527 'address_suggestions_shipping'
528 );
529
530 expect( billingSuggestions ).toBeTruthy();
531 expect( shippingSuggestions ).toBeTruthy();
532
533 expect( billingSuggestions.className ).toBe(
534 'woocommerce-address-suggestions'
535 );
536 expect( billingSuggestions.style.display ).toBe( 'none' );
537 expect( billingSuggestions.getAttribute( 'role' ) ).toBe(
538 'region'
539 );
540 expect( billingSuggestions.getAttribute( 'aria-live' ) ).toBe(
541 'polite'
542 );
543
544 // Check suggestions list
545 const billingList =
546 billingSuggestions.querySelector( '.suggestions-list' );
547 expect( billingList ).toBeTruthy();
548 expect( billingList.getAttribute( 'role' ) ).toBe( 'listbox' );
549 expect( billingList.getAttribute( 'aria-label' ) ).toBe(
550 'Address suggestions'
551 );
552
553 // Check search icon container exists
554 const billingIconContainer = document.querySelector(
555 '.address-search-icon'
556 );
557 expect( billingIconContainer ).toBeTruthy();
558 } );
559
560 test( 'should set active provider based on country value', () => {
561 expect( window.wc.addressAutocomplete.activeProvider.billing ).toBe(
562 mockProvider
563 );
564 expect(
565 window.wc.addressAutocomplete.activeProvider.shipping
566 ).toBe( mockProvider );
567 } );
568
569 test( 'should add autocomplete-available class when provider is active', () => {
570 const billingWrapper = billingAddressInput.closest(
571 '.woocommerce-input-wrapper'
572 );
573 const shippingWrapper = shippingAddressInput.closest(
574 '.woocommerce-input-wrapper'
575 );
576
577 expect(
578 billingWrapper.classList.contains( 'autocomplete-available' )
579 ).toBe( true );
580 expect(
581 shippingWrapper.classList.contains( 'autocomplete-available' )
582 ).toBe( true );
583 } );
584 } );
585
586 describe( 'Active Provider Management', () => {
587 test( 'should set active provider when country matches canSearch criteria', () => {
588 const billingCountry = document.getElementById( 'billing_country' );
589 billingCountry.value = 'US';
590 billingCountry.dispatchEvent( new Event( 'change' ) );
591
592 expect( mockProvider.canSearch ).toHaveBeenCalledWith( 'US' );
593 expect( window.wc.addressAutocomplete.activeProvider.billing ).toBe(
594 mockProvider
595 );
596 } );
597
598 test( 'should clear active provider when country does not match canSearch criteria', () => {
599 const billingCountry = document.getElementById( 'billing_country' );
600 // Create new option and select it
601 const frOption = document.createElement( 'option' );
602 frOption.value = 'FR';
603 billingCountry.appendChild( frOption );
604 billingCountry.value = 'FR';
605 billingCountry.dispatchEvent( new Event( 'change' ) );
606
607 expect( mockProvider.canSearch ).toHaveBeenCalledWith( 'FR' );
608 expect( window.wc.addressAutocomplete.activeProvider.billing ).toBe(
609 null
610 );
611 } );
612
613 test( 'should remove autocomplete-available class when no provider is active', () => {
614 const billingCountry = document.getElementById( 'billing_country' );
615 const billingWrapper = billingAddressInput.closest(
616 '.woocommerce-input-wrapper'
617 );
618
619 billingCountry.value = 'FR';
620 billingCountry.dispatchEvent( new Event( 'change' ) );
621
622 expect(
623 billingWrapper.classList.contains( 'autocomplete-available' )
624 ).toBe( false );
625 } );
626
627 test( 'should handle country change for both billing and shipping', () => {
628 const billingCountry = document.getElementById( 'billing_country' );
629 const shippingCountry =
630 document.getElementById( 'shipping_country' );
631
632 // Add FR option to billing
633 const frOption = document.createElement( 'option' );
634 frOption.value = 'FR';
635 billingCountry.appendChild( frOption );
636 billingCountry.value = 'FR';
637
638 billingCountry.dispatchEvent( new Event( 'change' ) );
639 shippingCountry.dispatchEvent( new Event( 'change' ) );
640
641 expect( window.wc.addressAutocomplete.activeProvider.billing ).toBe(
642 null
643 );
644 expect(
645 window.wc.addressAutocomplete.activeProvider.shipping
646 ).toBe( mockProvider );
647 } );
648 } );
649
650 describe( 'Address Suggestions Display', () => {
651 test( 'should not display suggestions for input less than 3 characters', async () => {
652 billingAddressInput.value = 'ab';
653 billingAddressInput.dispatchEvent( new Event( 'input' ) );
654
655 // Wait for timeout
656 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
657
658 const suggestionsList = document.querySelector(
659 '#address_suggestions_billing .suggestions-list'
660 );
661 expect( suggestionsList.innerHTML ).toBe( '' );
662 expect( mockProvider.search ).not.toHaveBeenCalled();
663 } );
664
665 test( 'should hide suggestions when input goes from 3+ characters to less than 3', async () => {
666 // First show suggestions with 3+ characters
667 billingAddressInput.value = '123';
668 billingAddressInput.focus();
669 billingAddressInput.dispatchEvent( new Event( 'input' ) );
670 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
671
672 const suggestionsContainer = document.getElementById(
673 'address_suggestions_billing'
674 );
675 expect( suggestionsContainer.style.display ).toBe( 'block' );
676
677 // Now reduce to less than 3 characters
678 billingAddressInput.value = '12';
679 billingAddressInput.dispatchEvent( new Event( 'input' ) );
680 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
681
682 expect( suggestionsContainer.style.display ).toBe( 'none' );
683 } );
684
685 test( 'should display suggestions for input with 3 or more characters', async () => {
686 billingAddressInput.value = '123';
687 billingAddressInput.focus();
688 billingAddressInput.dispatchEvent( new Event( 'input' ) );
689
690 // Wait for timeout and async operations
691 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
692
693 expect( mockProvider.search ).toHaveBeenCalledWith(
694 '123',
695 'US',
696 'billing'
697 );
698
699 const suggestionsList = document.querySelector(
700 '#address_suggestions_billing .suggestions-list'
701 );
702 const suggestions = suggestionsList.querySelectorAll( 'li' );
703
704 expect( suggestions ).toHaveLength( 2 );
705 expect( suggestions[ 0 ].textContent ).toContain(
706 '123 Main Street'
707 );
708 expect( suggestions[ 1 ].textContent ).toContain(
709 '456 Oak Avenue'
710 );
711 } );
712
713 test( 'should highlight matched text in suggestions', async () => {
714 billingAddressInput.value = '123';
715 billingAddressInput.focus();
716 billingAddressInput.dispatchEvent( new Event( 'input' ) );
717
718 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
719
720 const suggestionsList = document.querySelector(
721 '#address_suggestions_billing .suggestions-list'
722 );
723 const firstSuggestion = suggestionsList.querySelector( 'li' );
724 const strongElement = firstSuggestion.querySelector( 'strong' );
725
726 expect( strongElement ).toBeTruthy();
727 expect( strongElement.textContent ).toBe( '123' );
728 } );
729
730 test( 'should limit suggestions to maximum of 5', async () => {
731 // Mock provider to return more than 5 suggestions
732 mockProvider.search.mockResolvedValue(
733 Array.from( { length: 10 }, ( _, i ) => ( {
734 id: `addr${ i }`,
735 label: `${ i } Test Street`,
736 matchedSubstrings: [],
737 } ) )
738 );
739
740 billingAddressInput.value = 'test';
741 billingAddressInput.focus();
742 billingAddressInput.dispatchEvent( new Event( 'input' ) );
743
744 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
745
746 const suggestionsList = document.querySelector(
747 '#address_suggestions_billing .suggestions-list'
748 );
749 const suggestions = suggestionsList.querySelectorAll( 'li' );
750
751 expect( suggestions ).toHaveLength( 5 );
752 } );
753
754 test( 'should hide suggestions when no results returned', async () => {
755 mockProvider.search.mockResolvedValue( [] );
756
757 billingAddressInput.value = 'xyz';
758 billingAddressInput.focus();
759 billingAddressInput.dispatchEvent( new Event( 'input' ) );
760
761 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
762
763 const suggestionsContainer = document.getElementById(
764 'address_suggestions_billing'
765 );
766 expect( suggestionsContainer.style.display ).toBe( 'none' );
767 } );
768
769 test( 'should hide suggestions and log error when search throws exception', async () => {
770 mockProvider.search.mockRejectedValue(
771 new Error( 'Search failed' )
772 );
773
774 const consoleSpy = jest
775 .spyOn( console, 'error' )
776 .mockImplementation( () => {} );
777
778 billingAddressInput.value = 'test';
779 billingAddressInput.focus();
780 billingAddressInput.dispatchEvent( new Event( 'input' ) );
781
782 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
783
784 const suggestionsContainer = document.getElementById(
785 'address_suggestions_billing'
786 );
787 expect( suggestionsContainer.style.display ).toBe( 'none' );
788 expect( consoleSpy ).toHaveBeenCalledWith(
789 'Address search error:',
790 expect.any( Error )
791 );
792
793 consoleSpy.mockRestore();
794 } );
795 } );
796
797 describe( 'Keyboard Navigation', () => {
798 beforeEach( async () => {
799 // Setup suggestions
800 billingAddressInput.value = '123';
801 billingAddressInput.focus();
802 billingAddressInput.dispatchEvent( new Event( 'input' ) );
803 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
804 } );
805
806 test( 'should navigate down with ArrowDown key', () => {
807 const suggestions = document.querySelectorAll(
808 '#address_suggestions_billing .suggestions-list li'
809 );
810
811 // No suggestion should be active initially
812 expect( suggestions[ 0 ].classList.contains( 'active' ) ).toBe(
813 false
814 );
815 expect( suggestions[ 0 ].getAttribute( 'aria-selected' ) ).toBe(
816 null
817 );
818
819 // Press ArrowDown
820 const keydownEvent = new KeyboardEvent( 'keydown', {
821 key: 'ArrowDown',
822 bubbles: true,
823 } );
824 billingAddressInput.dispatchEvent( keydownEvent );
825
826 // First suggestion should now be active
827 expect( suggestions[ 0 ].classList.contains( 'active' ) ).toBe(
828 true
829 );
830 expect( suggestions[ 0 ].getAttribute( 'aria-selected' ) ).toBe(
831 'true'
832 );
833 expect( suggestions[ 1 ].classList.contains( 'active' ) ).toBe(
834 false
835 );
836 } );
837
838 test( 'should navigate up with ArrowUp key', () => {
839 const suggestions = document.querySelectorAll(
840 '#address_suggestions_billing .suggestions-list li'
841 );
842
843 // Navigate to first item first
844 let keydownEvent = new KeyboardEvent( 'keydown', {
845 key: 'ArrowDown',
846 bubbles: true,
847 } );
848 billingAddressInput.dispatchEvent( keydownEvent );
849
850 // Navigate to second item
851 keydownEvent = new KeyboardEvent( 'keydown', {
852 key: 'ArrowDown',
853 bubbles: true,
854 } );
855 billingAddressInput.dispatchEvent( keydownEvent );
856
857 // Press ArrowUp
858 keydownEvent = new KeyboardEvent( 'keydown', {
859 key: 'ArrowUp',
860 bubbles: true,
861 } );
862 billingAddressInput.dispatchEvent( keydownEvent );
863
864 // First suggestion should be active again
865 expect( suggestions[ 0 ].classList.contains( 'active' ) ).toBe(
866 true
867 );
868 expect( suggestions[ 1 ].classList.contains( 'active' ) ).toBe(
869 false
870 );
871 } );
872
873 test( 'should wrap around when navigating beyond bounds', () => {
874 const suggestions = document.querySelectorAll(
875 '#address_suggestions_billing .suggestions-list li'
876 );
877
878 // Navigate to first item
879 let keydownEvent = new KeyboardEvent( 'keydown', {
880 key: 'ArrowDown',
881 bubbles: true,
882 } );
883 billingAddressInput.dispatchEvent( keydownEvent );
884
885 // Navigate to second (last) item
886 keydownEvent = new KeyboardEvent( 'keydown', {
887 key: 'ArrowDown',
888 bubbles: true,
889 } );
890 billingAddressInput.dispatchEvent( keydownEvent );
891
892 // Navigate beyond last item - should wrap to first
893 keydownEvent = new KeyboardEvent( 'keydown', {
894 key: 'ArrowDown',
895 bubbles: true,
896 } );
897 billingAddressInput.dispatchEvent( keydownEvent );
898
899 expect( suggestions[ 0 ].classList.contains( 'active' ) ).toBe(
900 true
901 );
902 expect( suggestions[ 1 ].classList.contains( 'active' ) ).toBe(
903 false
904 );
905 } );
906
907 test( 'should select address with Enter key', async () => {
908 // Navigate to first suggestion first
909 let keydownEvent = new KeyboardEvent( 'keydown', {
910 key: 'ArrowDown',
911 bubbles: true,
912 } );
913 billingAddressInput.dispatchEvent( keydownEvent );
914
915 // Press Enter to select first suggestion
916 keydownEvent = new KeyboardEvent( 'keydown', {
917 key: 'Enter',
918 bubbles: true,
919 } );
920 billingAddressInput.dispatchEvent( keydownEvent );
921
922 // Wait for async operations
923 await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
924
925 expect( mockProvider.select ).toHaveBeenCalledWith( 'addr1' );
926
927 // Suggestions should be hidden
928 const suggestionsContainer = document.getElementById(
929 'address_suggestions_billing'
930 );
931 expect( suggestionsContainer.style.display ).toBe( 'none' );
932 } );
933
934 test( 'should hide suggestions with Escape key', () => {
935 const suggestionsContainer = document.getElementById(
936 'address_suggestions_billing'
937 );
938 expect( suggestionsContainer.style.display ).toBe( 'block' );
939
940 // Press Escape
941 const keydownEvent = new KeyboardEvent( 'keydown', {
942 key: 'Escape',
943 bubbles: true,
944 } );
945 billingAddressInput.dispatchEvent( keydownEvent );
946
947 expect( suggestionsContainer.style.display ).toBe( 'none' );
948 } );
949
950 test( 'should not handle keyboard events when suggestions are hidden', () => {
951 // Hide suggestions first
952 const escapeEvent = new KeyboardEvent( 'keydown', {
953 key: 'Escape',
954 bubbles: true,
955 } );
956 billingAddressInput.dispatchEvent( escapeEvent );
957
958 // Try to navigate with ArrowDown - should not throw error
959 const arrowEvent = new KeyboardEvent( 'keydown', {
960 key: 'ArrowDown',
961 bubbles: true,
962 } );
963 expect( () => {
964 billingAddressInput.dispatchEvent( arrowEvent );
965 } ).not.toThrow();
966 } );
967 } );
968
969 describe( 'Address Selection', () => {
970 test( 'should populate address fields when address is selected', async () => {
971 // Setup suggestions
972 billingAddressInput.value = '123';
973 billingAddressInput.focus();
974 billingAddressInput.dispatchEvent( new Event( 'input' ) );
975 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
976
977 // Click on first suggestion
978 const firstSuggestion = document.querySelector(
979 '#address_suggestions_billing .suggestions-list li'
980 );
981 firstSuggestion.click();
982
983 // Wait for async operations and timeout
984 await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
985
986 expect( mockProvider.select ).toHaveBeenCalledWith( 'addr1' );
987
988 // Check that fields are populated
989 expect( document.getElementById( 'billing_address_1' ).value ).toBe(
990 '123 Main Street'
991 );
992 expect( document.getElementById( 'billing_city' ).value ).toBe(
993 'City'
994 );
995 expect( document.getElementById( 'billing_postcode' ).value ).toBe(
996 '12345'
997 );
998 expect( document.getElementById( 'billing_country' ).value ).toBe(
999 'US'
1000 );
1001 expect( document.getElementById( 'billing_state' ).value ).toBe(
1002 'CA'
1003 );
1004 } );
1005
1006 test( 'should update value attribute of address fields when address is selected', async () => {
1007 // Pre-populate fields with old values (simulating pre-filled checkout)
1008 document.getElementById( 'billing_city' ).setAttribute( 'value', 'Old City' );
1009 document.getElementById( 'billing_city' ).value = 'Old City';
1010 document.getElementById( 'billing_postcode' ).setAttribute( 'value', '91210' );
1011 document.getElementById( 'billing_postcode' ).value = '91210';
1012
1013 // Setup suggestions
1014 billingAddressInput.value = '123';
1015 billingAddressInput.focus();
1016 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1017 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1018
1019 // Click on first suggestion
1020 const firstSuggestion = document.querySelector(
1021 '#address_suggestions_billing .suggestions-list li'
1022 );
1023 firstSuggestion.click();
1024
1025 // Wait for async operations and timeout
1026 await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
1027
1028 // Check that both the property and the HTML value attribute are updated
1029 expect( document.getElementById( 'billing_city' ).getAttribute( 'value' ) ).toBe( 'City' );
1030 expect( document.getElementById( 'billing_postcode' ).getAttribute( 'value' ) ).toBe( '12345' );
1031 expect( document.getElementById( 'billing_address_1' ).getAttribute( 'value' ) ).toBe( '123 Main Street' );
1032 expect( document.getElementById( 'billing_state' ).getAttribute( 'value' ) ).toBe( 'CA' );
1033 } );
1034
1035 test( 'should handle partial address data from provider', async () => {
1036 // Mock provider to return partial data
1037 mockProvider.select.mockResolvedValue( {
1038 address_1: '123 Main Street',
1039 city: 'City',
1040 // Missing postcode, country, state
1041 } );
1042
1043 billingAddressInput.value = '123';
1044 billingAddressInput.focus();
1045 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1046 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1047
1048 const firstSuggestion = document.querySelector(
1049 '#address_suggestions_billing .suggestions-list li'
1050 );
1051 firstSuggestion.click();
1052
1053 await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
1054
1055 // Only provided fields should be populated
1056 expect( document.getElementById( 'billing_address_1' ).value ).toBe(
1057 '123 Main Street'
1058 );
1059 expect( document.getElementById( 'billing_city' ).value ).toBe(
1060 'City'
1061 );
1062 expect( document.getElementById( 'billing_postcode' ).value ).toBe(
1063 ''
1064 );
1065 } );
1066
1067 test( 'should clear existing field values when not present in selected address data', async () => {
1068 // Create address_2 field since it's not in the initial setup
1069 const billingAddress2 = document.createElement( 'input' );
1070 billingAddress2.id = 'billing_address_2';
1071 billingAddress2.type = 'text';
1072 billingAddress2.value = 'Apt 101';
1073 document.querySelector( 'form' ).appendChild( billingAddress2 );
1074
1075 // Pre-populate some fields
1076 document.getElementById( 'billing_city' ).value = 'Old City';
1077 document.getElementById( 'billing_postcode' ).value = '99999';
1078 document.getElementById( 'billing_state' ).value = 'TX';
1079
1080 // Mock provider to return data with some fields missing
1081 mockProvider.select.mockResolvedValue( {
1082 address_1: '456 Oak Avenue',
1083 city: 'New City',
1084 country: 'US',
1085 // Missing address_2, postcode, and state
1086 } );
1087
1088 billingAddressInput.value = '456';
1089 billingAddressInput.focus();
1090 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1091 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1092
1093 const firstSuggestion = document.querySelector(
1094 '#address_suggestions_billing .suggestions-list li'
1095 );
1096 firstSuggestion.click();
1097
1098 await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
1099
1100 // Check that provided fields are populated
1101 expect( document.getElementById( 'billing_address_1' ).value ).toBe(
1102 '456 Oak Avenue'
1103 );
1104 expect( document.getElementById( 'billing_city' ).value ).toBe(
1105 'New City'
1106 );
1107 expect( document.getElementById( 'billing_country' ).value ).toBe(
1108 'US'
1109 );
1110
1111 // Check that missing fields are cleared
1112 expect( document.getElementById( 'billing_address_2' ).value ).toBe(
1113 ''
1114 );
1115 expect( document.getElementById( 'billing_postcode' ).value ).toBe(
1116 ''
1117 );
1118 expect( document.getElementById( 'billing_state' ).value ).toBe(
1119 ''
1120 );
1121 } );
1122
1123 test( 'should only clear fields that exist and have values', async () => {
1124 // Pre-populate only some fields
1125 document.getElementById( 'billing_city' ).value = 'Existing City';
1126 document.getElementById( 'billing_postcode' ).value = '12345';
1127
1128 // Mock provider to return partial data
1129 mockProvider.select.mockResolvedValue( {
1130 address_1: '789 Pine Street',
1131 state: 'CA',
1132 country: 'US',
1133 // Missing city and postcode
1134 } );
1135
1136 billingAddressInput.value = '789';
1137 billingAddressInput.focus();
1138 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1139 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1140
1141 const firstSuggestion = document.querySelector(
1142 '#address_suggestions_billing .suggestions-list li'
1143 );
1144 firstSuggestion.click();
1145
1146 await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
1147
1148 // Check that provided fields are populated
1149 expect( document.getElementById( 'billing_address_1' ).value ).toBe(
1150 '789 Pine Street'
1151 );
1152 expect( document.getElementById( 'billing_state' ).value ).toBe(
1153 'CA'
1154 );
1155 expect( document.getElementById( 'billing_country' ).value ).toBe(
1156 'US'
1157 );
1158
1159 // Check that city and postcode are cleared since they had values but weren't in the response
1160 expect( document.getElementById( 'billing_city' ).value ).toBe(
1161 ''
1162 );
1163 expect( document.getElementById( 'billing_postcode' ).value ).toBe(
1164 ''
1165 );
1166 } );
1167
1168 test( 'should handle provider selection errors gracefully', async () => {
1169 mockProvider.select.mockRejectedValue(
1170 new Error( 'Selection failed' )
1171 );
1172
1173 const consoleSpy = jest
1174 .spyOn( console, 'error' )
1175 .mockImplementation( () => {} );
1176
1177 billingAddressInput.value = '123';
1178 billingAddressInput.focus();
1179 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1180 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1181
1182 const firstSuggestion = document.querySelector(
1183 '#address_suggestions_billing .suggestions-list li'
1184 );
1185 firstSuggestion.click();
1186
1187 await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
1188
1189 expect( consoleSpy ).toHaveBeenCalledWith(
1190 'Error selecting address from provider',
1191 'test-provider',
1192 expect.any( Error )
1193 );
1194
1195 // Fields should remain unchanged
1196 expect( document.getElementById( 'billing_address_1' ).value ).toBe(
1197 '123'
1198 );
1199
1200 consoleSpy.mockRestore();
1201 } );
1202
1203 test( 'should handle invalid address data from provider', async () => {
1204 mockProvider.select.mockResolvedValue( null );
1205
1206 billingAddressInput.value = '123';
1207 billingAddressInput.focus();
1208 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1209 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1210
1211 const firstSuggestion = document.querySelector(
1212 '#address_suggestions_billing .suggestions-list li'
1213 );
1214 firstSuggestion.click();
1215
1216 await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
1217
1218 // Fields should remain unchanged
1219 expect( document.getElementById( 'billing_address_1' ).value ).toBe(
1220 '123'
1221 );
1222 } );
1223 } );
1224
1225 describe( 'Browser Autofill Management', () => {
1226 test( 'should disable browser autofill when suggestions are shown', async () => {
1227 billingAddressInput.value = '123';
1228 billingAddressInput.focus();
1229 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1230
1231 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1232
1233 expect( billingAddressInput.getAttribute( 'autocomplete' ) ).toBe(
1234 'none'
1235 );
1236 expect( billingAddressInput.getAttribute( 'data-lpignore' ) ).toBe(
1237 'true'
1238 );
1239 expect( billingAddressInput.getAttribute( 'data-op-ignore' ) ).toBe(
1240 'true'
1241 );
1242 expect( billingAddressInput.getAttribute( 'data-1p-ignore' ) ).toBe(
1243 'true'
1244 );
1245 } );
1246
1247 test( 'should enable browser autofill when suggestions are hidden', async () => {
1248 // First show suggestions
1249 billingAddressInput.value = '123';
1250 billingAddressInput.focus();
1251 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1252 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1253
1254 // Then hide them
1255 billingAddressInput.value = 'xy';
1256 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1257 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1258
1259 expect( billingAddressInput.getAttribute( 'autocomplete' ) ).toBe(
1260 'address-line1'
1261 );
1262 expect( billingAddressInput.getAttribute( 'data-lpignore' ) ).toBe(
1263 'false'
1264 );
1265 } );
1266 } );
1267
1268 describe( 'Security and Sanitization', () => {
1269 test( 'should sanitize input values for XSS protection', async () => {
1270 const maliciousInput = '<script>alert("xss")</script>';
1271 const consoleSpy = jest
1272 .spyOn( console, 'warn' )
1273 .mockImplementation( () => {} );
1274
1275 billingAddressInput.value = maliciousInput;
1276 billingAddressInput.focus();
1277 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1278
1279 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1280
1281 expect( consoleSpy ).toHaveBeenCalledWith(
1282 'Input was sanitized for security'
1283 );
1284 expect( mockProvider.search ).toHaveBeenCalledWith(
1285 'alert("xss")',
1286 'US',
1287 'billing'
1288 );
1289
1290 consoleSpy.mockRestore();
1291 } );
1292
1293 test( 'should handle invalid match data safely', async () => {
1294 // Mock provider to return invalid match data
1295 mockProvider.search.mockResolvedValue( [
1296 {
1297 id: 'addr1',
1298 label: '123 Main Street',
1299 matchedSubstrings: [
1300 { offset: -1, length: 5 }, // Invalid offset
1301 { offset: 50, length: 10 }, // Offset beyond string length
1302 { offset: 0, length: -1 }, // Invalid length
1303 null, // Null match
1304 ],
1305 },
1306 ] );
1307
1308 billingAddressInput.value = '123';
1309 billingAddressInput.focus();
1310 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1311
1312 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1313
1314 const suggestionsList = document.querySelector(
1315 '#address_suggestions_billing .suggestions-list'
1316 );
1317 const firstSuggestion = suggestionsList.querySelector( 'li' );
1318
1319 // Should still render the suggestion without highlighting
1320 expect( firstSuggestion.textContent ).toBe( '123 Main Street' );
1321 expect( firstSuggestion.querySelector( 'strong' ) ).toBe( null );
1322 } );
1323 } );
1324
1325 describe( 'Click Outside Behavior', () => {
1326 test( 'should hide suggestions when clicking outside', async () => {
1327 // Show suggestions first
1328 billingAddressInput.value = '123';
1329 billingAddressInput.focus();
1330 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1331 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1332
1333 const suggestionsContainer = document.getElementById(
1334 'address_suggestions_billing'
1335 );
1336 expect( suggestionsContainer.style.display ).toBe( 'block' );
1337
1338 // Click outside
1339 const outsideElement = document.createElement( 'div' );
1340 document.body.appendChild( outsideElement );
1341 outsideElement.click();
1342
1343 expect( suggestionsContainer.style.display ).toBe( 'none' );
1344 } );
1345
1346 test( 'should not hide suggestions when clicking inside suggestions container', async () => {
1347 // Show suggestions first
1348 billingAddressInput.value = '123';
1349 billingAddressInput.focus();
1350 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1351 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1352
1353 const suggestionsContainer = document.getElementById(
1354 'address_suggestions_billing'
1355 );
1356 expect( suggestionsContainer.style.display ).toBe( 'block' );
1357
1358 // Click inside suggestions container
1359 suggestionsContainer.click();
1360
1361 expect( suggestionsContainer.style.display ).toBe( 'block' );
1362 } );
1363
1364 test( 'should not hide suggestions when clicking address input', async () => {
1365 // Show suggestions first
1366 billingAddressInput.value = '123';
1367 billingAddressInput.focus();
1368 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1369 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1370
1371 const suggestionsContainer = document.getElementById(
1372 'address_suggestions_billing'
1373 );
1374 expect( suggestionsContainer.style.display ).toBe( 'block' );
1375
1376 // Click on address input
1377 billingAddressInput.click();
1378
1379 expect( suggestionsContainer.style.display ).toBe( 'block' );
1380 } );
1381 } );
1382
1383 describe( 'Branding HTML', () => {
1384 test( 'should display branding HTML when suggestions are shown', async () => {
1385 // Show suggestions
1386 billingAddressInput.value = '123';
1387 billingAddressInput.focus();
1388 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1389 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1390
1391 const suggestionsContainer = document.getElementById(
1392 'address_suggestions_billing'
1393 );
1394 const brandingElement = suggestionsContainer.querySelector(
1395 '.woocommerce-address-autocomplete-branding'
1396 );
1397
1398 expect( brandingElement ).toBeTruthy();
1399 expect( brandingElement.innerHTML ).toBe(
1400 '<div class="provider-branding">Powered by Test Provider</div>'
1401 );
1402 } );
1403
1404 test.skip( 'should hide branding HTML when suggestions are hidden', async () => {
1405 // Show suggestions first
1406 billingAddressInput.value = '123';
1407 billingAddressInput.focus();
1408 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1409 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1410
1411 const suggestionsContainer = document.getElementById(
1412 'address_suggestions_billing'
1413 );
1414 let brandingElement = suggestionsContainer.querySelector(
1415 '.woocommerce-address-autocomplete-branding'
1416 );
1417 expect( brandingElement.innerHTML ).toBe(
1418 '<div class="provider-branding">Powered by Test Provider</div>'
1419 );
1420 expect( brandingElement.style.display ).toBe( 'flex' );
1421
1422 // Hide suggestions
1423 billingAddressInput.value = 'xy';
1424 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1425 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1426
1427 brandingElement = suggestionsContainer.querySelector(
1428 '.woocommerce-address-autocomplete-branding'
1429 );
1430 // Element should still exist but be hidden
1431 expect( brandingElement ).toBeTruthy();
1432 expect( brandingElement.style.display ).toBe( 'none' );
1433 } );
1434
1435 test( 'should not create branding element when provider has no branding_html', async () => {
1436 // Re-initialize the module
1437 jest.resetModules();
1438 window.wc.addressAutocomplete.providers = [];
1439 require( '../address-autocomplete' );
1440
1441 // Re-register provider
1442 window.wc.addressAutocomplete.registerAddressAutocompleteProvider( {
1443 search: mockProvider,
1444 select: mockProvider,
1445 canSearch: mockProvider,
1446 id: 'mock-provider-unbranded',
1447 } );
1448
1449 // Trigger DOMContentLoaded again
1450 const event = new Event( 'DOMContentLoaded' );
1451 document.dispatchEvent( event );
1452 await new Promise( ( resolve ) => setTimeout( resolve, 10 ) );
1453
1454 // Show suggestions
1455 billingAddressInput.value = '456';
1456 billingAddressInput.focus();
1457 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1458 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1459
1460 const suggestionsContainer = document.getElementById(
1461 'address_suggestions_billing'
1462 );
1463 const brandingElement = suggestionsContainer.querySelector(
1464 '.woocommerce-address-autocomplete-branding'
1465 );
1466
1467 // Branding element should not be created when there's no branding_html
1468 expect( brandingElement ).toBeFalsy();
1469 } );
1470
1471 test( 'should reuse existing branding element on subsequent searches', async () => {
1472 // First search
1473 billingAddressInput.value = '123';
1474 billingAddressInput.focus();
1475 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1476 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1477
1478 const suggestionsContainer = document.getElementById(
1479 'address_suggestions_billing'
1480 );
1481 const firstBrandingElement = suggestionsContainer.querySelector(
1482 '.woocommerce-address-autocomplete-branding'
1483 );
1484
1485 // Clear and search again
1486 billingAddressInput.value = '12';
1487 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1488 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1489
1490 billingAddressInput.value = '456';
1491 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1492 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1493
1494 const secondBrandingElement = suggestionsContainer.querySelector(
1495 '.woocommerce-address-autocomplete-branding'
1496 );
1497
1498 // Should be the same element
1499 expect( secondBrandingElement ).toBe( firstBrandingElement );
1500 expect( secondBrandingElement.innerHTML ).toBe(
1501 '<div class="provider-branding">Powered by Test Provider</div>'
1502 );
1503 } );
1504
1505 test( 'should remove branding element when country changes', async () => {
1506 // Show suggestions first
1507 billingAddressInput.value = '123';
1508 billingAddressInput.focus();
1509 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1510 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1511
1512 const suggestionsContainer = document.getElementById(
1513 'address_suggestions_billing'
1514 );
1515 let brandingElement = suggestionsContainer.querySelector(
1516 '.woocommerce-address-autocomplete-branding'
1517 );
1518 expect( brandingElement ).toBeTruthy();
1519
1520 // Change country
1521 const billingCountry = document.getElementById( 'billing_country' );
1522 const frOption = document.createElement( 'option' );
1523 frOption.value = 'FR';
1524 billingCountry.appendChild( frOption );
1525 billingCountry.value = 'FR';
1526 billingCountry.dispatchEvent( new Event( 'change' ) );
1527
1528 // Branding element should be removed completely
1529 brandingElement = suggestionsContainer.querySelector(
1530 '.woocommerce-address-autocomplete-branding'
1531 );
1532 expect( brandingElement ).toBeFalsy();
1533 } );
1534
1535 test( 'should display branding HTML for both billing and shipping if DOMPurify is present', async () => {
1536 // Show suggestions for billing
1537 billingAddressInput.value = '123';
1538 billingAddressInput.focus();
1539 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1540 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1541 window.DOMPurify = { sanitize: ( html ) => html }; // Mock DOMPurify
1542
1543 const billingSuggestionsContainer = document.getElementById(
1544 'address_suggestions_billing'
1545 );
1546 const billingBrandingElement =
1547 billingSuggestionsContainer.querySelector(
1548 '.woocommerce-address-autocomplete-branding'
1549 );
1550
1551 expect( billingBrandingElement ).toBeTruthy();
1552 expect( billingBrandingElement.innerHTML ).toBe(
1553 '<div class="provider-branding">Powered by Test Provider</div>'
1554 );
1555
1556 // Show suggestions for shipping
1557 shippingAddressInput.value = '456';
1558 shippingAddressInput.focus();
1559 shippingAddressInput.dispatchEvent( new Event( 'input' ) );
1560 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1561
1562 const shippingSuggestionsContainer = document.getElementById(
1563 'address_suggestions_shipping'
1564 );
1565 const shippingBrandingElement =
1566 shippingSuggestionsContainer.querySelector(
1567 '.woocommerce-address-autocomplete-branding'
1568 );
1569
1570 expect( shippingBrandingElement ).toBeTruthy();
1571 expect( shippingBrandingElement.innerHTML ).toBe(
1572 '<div class="provider-branding">Powered by Test Provider</div>'
1573 );
1574 } );
1575
1576 test( 'should not display branding HTML for both billing and shipping if DOMPurify is not present', async () => {
1577 delete window.DOMPurify;
1578 // Show suggestions for billing
1579 billingAddressInput.value = '123';
1580 billingAddressInput.focus();
1581 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1582 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1583 const billingSuggestionsContainer = document.getElementById(
1584 'address_suggestions_billing'
1585 );
1586 const billingBrandingElement =
1587 billingSuggestionsContainer.querySelector(
1588 '.woocommerce-address-autocomplete-branding'
1589 );
1590
1591 expect( billingBrandingElement ).toBeNull();
1592
1593 // Show suggestions for shipping
1594 shippingAddressInput.value = '456';
1595 shippingAddressInput.focus();
1596 shippingAddressInput.dispatchEvent( new Event( 'input' ) );
1597 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1598
1599 const shippingSuggestionsContainer = document.getElementById(
1600 'address_suggestions_shipping'
1601 );
1602 const shippingBrandingElement =
1603 shippingSuggestionsContainer.querySelector(
1604 '.woocommerce-address-autocomplete-branding'
1605 );
1606
1607 expect( shippingBrandingElement ).toBeNull();
1608 } );
1609 } );
1610
1611 describe( 'Blur Event Behavior', () => {
1612 test( 'should hide suggestions when input loses focus', async () => {
1613 // Show suggestions first
1614 billingAddressInput.value = '123';
1615 billingAddressInput.focus();
1616 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1617 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1618
1619 const suggestionsContainer = document.getElementById(
1620 'address_suggestions_billing'
1621 );
1622 expect( suggestionsContainer.style.display ).toBe( 'block' );
1623
1624 // Blur the input
1625 billingAddressInput.dispatchEvent( new Event( 'blur' ) );
1626
1627 // Wait for blur timeout
1628 await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
1629
1630 expect( suggestionsContainer.style.display ).toBe( 'none' );
1631 } );
1632
1633 test( 'should not refocus input when blurred with suggestions active', async () => {
1634 // Show suggestions first
1635 billingAddressInput.value = '123';
1636 billingAddressInput.focus();
1637 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1638 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1639
1640 const suggestionsContainer = document.getElementById(
1641 'address_suggestions_billing'
1642 );
1643 expect( suggestionsContainer.style.display ).toBe( 'block' );
1644
1645 // Create another element to focus
1646 const otherElement = document.createElement( 'input' );
1647 document.body.appendChild( otherElement );
1648
1649 // Blur the address input and focus the other element
1650 billingAddressInput.blur();
1651 otherElement.focus();
1652
1653 // Wait for blur timeout
1654 await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
1655
1656 // The other element should still be focused (address input shouldn't refocus)
1657 expect( document.activeElement ).toBe( otherElement );
1658 expect( suggestionsContainer.style.display ).toBe( 'none' );
1659
1660 document.body.removeChild( otherElement );
1661 } );
1662
1663 test( 'should not have blur event listener when suggestions are not shown', () => {
1664 // No suggestions should be shown initially
1665 const suggestionsContainer = document.getElementById(
1666 'address_suggestions_billing'
1667 );
1668 expect( suggestionsContainer.style.display ).toBe( 'none' );
1669
1670 // Blur the input - should not cause any issues
1671 expect( () => {
1672 billingAddressInput.dispatchEvent( new Event( 'blur' ) );
1673 } ).not.toThrow();
1674 } );
1675
1676 test( 'should enable browser autofill without refocusing when suggestions are hidden via blur', async () => {
1677 // Show suggestions first
1678 billingAddressInput.value = '123';
1679 billingAddressInput.focus();
1680 billingAddressInput.dispatchEvent( new Event( 'input' ) );
1681 await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
1682
1683 // Verify autofill is disabled
1684 expect( billingAddressInput.getAttribute( 'autocomplete' ) ).toBe(
1685 'none'
1686 );
1687
1688 // Blur the input
1689 billingAddressInput.dispatchEvent( new Event( 'blur' ) );
1690
1691 // Wait for blur timeout
1692 await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
1693
1694 // Autofill should be re-enabled
1695 expect( billingAddressInput.getAttribute( 'autocomplete' ) ).toBe(
1696 'address-line1'
1697 );
1698 expect( billingAddressInput.getAttribute( 'data-lpignore' ) ).toBe(
1699 'false'
1700 );
1701 } );
1702 } );
1703 } );
1704