/*! elementor - v3.26.0 - 07-01-2025 */
.e-contact-buttons{--e-contact-buttons-chat-box-width:360px;--e-contact-buttons-size-small:55px;--e-contact-buttons-size-medium:65px;--e-contact-buttons-size-large:75px;--e-contact-buttons-svg-size-small:32px;--e-contact-buttons-svg-size-medium:38px;--e-contact-buttons-svg-size-large:42px;--e-contact-buttons-profile-image-size-small:65px;--e-contact-buttons-profile-image-size-medium:75px;--e-contact-buttons-profile-image-size-large:85px;--e-contact-buttons-dot:red;--e-contact-buttons-dot-size:16px;--e-contact-buttons-profile-dot-bg:#39aa59;--e-contact-buttons-border-radius:20px;--e-contact-button-chat-button-animation-delay:0;--e-contact-buttons-icon-size-small:45px;--e-contact-buttons-icon-size-medium:50px;--e-contact-buttons-icon-size-large:55px;--e-contact-buttons-contact-gap:15px;--e-contact-buttons-horizontal-offset:25px;--e-contact-buttons-vertical-offset:25px;--e-contact-buttons-box-shadow:4px 4px 10px 0px rgba(0,0,0,.15);--e-contact-buttons-drop-shadow:drop-shadow(4px 4px 10px rgba(0,0,0,.15));--e-contact-buttons-button-bg:#467ff7;--e-contact-buttons-button-bg-hover:#1c2448;--e-contact-buttons-button-icon:#fff;--e-contact-buttons-button-icon-hover:#fff;--e-contact-buttons-top-bar-bg:#1c2448;--e-contact-buttons-top-bar-title:#fff;--e-contact-buttons-top-bar-subtitle:#fff;--e-contact-buttons-close-button-color:#fff;--e-contact-buttons-active-button-bg:#fff;--e-contact-buttons-message-bubble-name:#000;--e-contact-buttons-message-bubble-body:#000;--e-contact-buttons-message-bubble-time:#000;--e-contact-buttons-message-bubble-bubble-bg:#fff;--e-contact-buttons-message-bubble-chat-bg:#c8d5dc;--e-contact-buttons-send-button-icon:#fff;--e-contact-buttons-send-button-bg:#467ff7;--e-contact-buttons-send-button-icon-hover:#fff;--e-contact-buttons-send-button-bg-hover:#1c2448;--e-contact-buttons-chat-box-bg:#fff;--e-contact-buttons-contact-button-icon:#fff;--e-contact-buttons-contact-button-icon-hover:#fff;--e-contact-buttons-contact-button-bg:#467ff7;--e-contact-buttons-contact-button-bg-hover:#1c2448;--e-contact-buttons-tooltip-text:#1c2448;--e-contact-buttons-tooltip-bg:#fff;--e-contact-buttons-contact-title-text-color:#1c2448;--e-contact-buttons-contact-description-text-color:#1c2448;display:flex;flex-direction:column;gap:20px;pointer-events:none;position:fixed;width:var(--e-contact-buttons-chat-box-width);z-index:10000}@media (max-width:767px){.e-contact-buttons{inset-inline-end:0;width:90vw}}.e-contact-buttons.has-h-alignment-start{inset-inline-start:var(--e-contact-buttons-horizontal-offset);justify-content:flex-start}@media (max-width:767px){.e-contact-buttons.has-h-alignment-start{inset-inline-start:0}}.e-contact-buttons.has-h-alignment-start .e-contact-buttons__chat-button-container{justify-content:flex-start;padding-inline-end:0;padding-inline-start:20px}@media (max-width:767px){.e-contact-buttons.has-h-alignment-start .e-contact-buttons__chat-button-container{inset-inline-end:unset;inset-inline-start:var(--e-contact-buttons-horizontal-offset)}}.e-contact-buttons.has-h-alignment-end{align-items:flex-end;inset-inline-end:var(--e-contact-buttons-horizontal-offset);justify-content:flex-end}.e-contact-buttons.has-h-alignment-end .e-contact-buttons__chat-button-container{inset-inline-end:var(--e-contact-buttons-horizontal-offset);justify-content:flex-end;padding-inline-end:20px}@media (max-width:767px){.e-contact-buttons.has-h-alignment-end .e-contact-buttons__chat-button-container{inset-inline-end:unset}}.e-contact-buttons.has-h-alignment-center{inset-inline-start:50%;justify-content:center;transform:translateX(-50%)}.e-contact-buttons.has-h-alignment-center .e-contact-buttons__chat-button-container{justify-content:center;padding-inline:0}.e-contact-buttons.has-h-alignment-center .e-contact-buttons__content-wrapper{inset-inline-end:calc(var(--e-contact-buttons-chat-box-width) / 2 - 40px);position:relative}.e-contact-buttons.has-v-alignment-top{top:var(--e-contact-buttons-vertical-offset)}.e-contact-buttons.has-v-alignment-top .e-contact-buttons__content-wrapper{order:2}.e-contact-buttons.has-v-alignment-top .e-contact-buttons__chat-button-container{order:1}.e-contact-buttons.has-v-alignment-middle{align-items:center;flex-direction:row;top:50%;transform:translateY(-50%)}.e-contact-buttons.has-v-alignment-middle .e-contact-buttons__chat-button-container{padding-inline:0}.e-contact-buttons.has-v-alignment-middle.has-h-alignment-start .e-contact-buttons__content-wrapper{order:2}.e-contact-buttons.has-v-alignment-middle.has-h-alignment-start .e-contact-buttons__chat-button-container{order:1;padding-inline:0}.e-contact-buttons.has-h-alignment-center.has-v-alignment-middle{flex-direction:column;transform:translate(-50%,-50%)}.e-contact-buttons.has-v-alignment-bottom{bottom:var(--e-contact-buttons-vertical-offset)}.e-contact-buttons.has-platform-whatsapp{--e-contact-buttons-button-bg:#25d366;--e-contact-buttons-button-bg-hover:#075e54;--e-contact-buttons-button-icon:#fff;--e-contact-buttons-button-icon-hover:#fff;--e-contact-buttons-top-bar-bg:#075e54;--e-contact-buttons-top-bar-title:#fff;--e-contact-buttons-top-bar-subtitle:#fff;--e-contact-buttons-close-button-color:#fff;--e-contact-buttons-message-bubble-body:#000;--e-contact-buttons-message-bubble-time:#000;--e-contact-buttons-message-bubble-name:#000;--e-contact-buttons-message-bubble-bubble-bg:#fff;--e-contact-buttons-message-bubble-chat-bg:#ece5dd;--e-contact-buttons-send-button-icon:#fff;--e-contact-buttons-send-button-bg:#25d366;--e-contact-buttons-send-button-icon-hover:#fff;--e-contact-buttons-send-button-bg-hover:#075e54;--e-contact-buttons-chat-box-bg:#fff}.e-contact-buttons.has-platform-skype{--e-contact-buttons-button-bg:#00aff0;--e-contact-buttons-button-bg-hover:#0d72cf;--e-contact-buttons-button-icon:#fff;--e-contact-buttons-button-icon-hover:#fff;--e-contact-buttons-top-bar-bg:#0d72cf;--e-contact-buttons-top-bar-title:#fff;--e-contact-buttons-top-bar-subtitle:#fff;--e-contact-buttons-close-button-color:#fff;--e-contact-buttons-message-bubble-body:#000;--e-contact-buttons-message-bubble-time:#000;--e-contact-buttons-message-bubble-name:#000;--e-contact-buttons-message-bubble-bubble-bg:#fff;--e-contact-buttons-message-bubble-chat-bg:#cdf7ff;--e-contact-buttons-send-button-icon:#fff;--e-contact-buttons-send-button-bg:#00aff0;--e-contact-buttons-send-button-icon-hover:#fff;--e-contact-buttons-send-button-bg-hover:#0d72cf;--e-contact-buttons-chat-box-bg:#fff}.e-contact-buttons.has-platform-messenger{--e-contact-buttons-button-bg:#168aff;--e-contact-buttons-button-bg-hover:#168aff;--e-contact-buttons-button-icon:#fff;--e-contact-buttons-button-icon-hover:#fff;--e-contact-buttons-top-bar-bg:#168aff;--e-contact-buttons-top-bar-title:#fff;--e-contact-buttons-top-bar-subtitle:#fff;--e-contact-buttons-close-button-color:#fff;--e-contact-buttons-message-bubble-body:#000;--e-contact-buttons-message-bubble-time:#000;--e-contact-buttons-message-bubble-name:#000;--e-contact-buttons-message-bubble-bubble-bg:#fff;--e-contact-buttons-message-bubble-chat-bg:#f0f0f0;--e-contact-buttons-send-button-icon:#fff;--e-contact-buttons-send-button-bg:#168aff;--e-contact-buttons-send-button-icon-hover:#fff;--e-contact-buttons-send-button-bg-hover:#168aff;--e-contact-buttons-chat-box-bg:#fff}.e-contact-buttons.has-platform-viber{--e-contact-buttons-button-bg:#7360f2;--e-contact-buttons-button-bg-hover:#4e4879;--e-contact-buttons-button-icon:#fff;--e-contact-buttons-button-icon-hover:#fff;--e-contact-buttons-top-bar-bg:#4e4879;--e-contact-buttons-top-bar-title:#fff;--e-contact-buttons-top-bar-subtitle:#fff;--e-contact-buttons-close-button-color:#fff;--e-contact-buttons-message-bubble-body:#000;--e-contact-buttons-message-bubble-time:#000;--e-contact-buttons-message-bubble-name:#000;--e-contact-buttons-message-bubble-bubble-bg:#fff;--e-contact-buttons-message-bubble-chat-bg:#e5e1ff;--e-contact-buttons-send-button-icon:#fff;--e-contact-buttons-send-button-bg:#7360f2;--e-contact-buttons-send-button-icon-hover:#fff;--e-contact-buttons-send-button-bg-hover:#4e4879;--e-contact-buttons-chat-box-bg:#fff}.e-contact-buttons.has-platform-waze{--e-contact-buttons-button-bg:#3cf;--e-contact-buttons-button-bg-hover:#09f;--e-contact-buttons-button-icon:#fff;--e-contact-buttons-button-icon-hover:#fff;--e-contact-buttons-top-bar-bg:#09f;--e-contact-buttons-top-bar-title:#fff;--e-contact-buttons-top-bar-subtitle:#fff;--e-contact-buttons-close-button-color:#fff;--e-contact-buttons-message-bubble-body:#000;--e-contact-buttons-message-bubble-time:#000;--e-contact-buttons-message-bubble-name:#000;--e-contact-buttons-message-bubble-bubble-bg:#fff;--e-contact-buttons-message-bubble-chat-bg:#ece5dd;--e-contact-buttons-send-button-icon:#fff;--e-contact-buttons-send-button-bg:#3cf;--e-contact-buttons-send-button-icon-hover:#fff;--e-contact-buttons-send-button-bg-hover:#09f;--e-contact-buttons-chat-box-bg:#fff}.e-contact-buttons.has-corners-rounded{--e-contact-buttons-border-radius:20px}.e-contact-buttons.has-corners-round{--e-contact-buttons-border-radius:50px}.e-contact-buttons.has-corners-sharp{--e-contact-buttons-border-radius:0}.e-contact-buttons:not(.has-animations) .e-contact-buttons__content-wrapper.hidden{display:none}.e-contact-buttons.has-animations .e-contact-buttons__content-wrapper.hidden{display:block;transition:1s;visibility:hidden}.e-contact-buttons.has-animations .e-contact-buttons__content-wrapper.animated-wrapper{animation:e-contact-buttons-close 1s;opacity:0;transform:none;visibility:hidden}.e-contact-buttons__chat-button-shadow,.e-contact-buttons__contact-box-shadow,.e-contact-buttons__contact-box-shadow:is(a),.e-contact-buttons__content{box-shadow:var(--e-contact-buttons-box-shadow)}.e-contact-buttons__chat-button-drop-shadow{filter:var(--e-contact-buttons-drop-shadow)}.e-contact-buttons__content{border-radius:var(--e-contact-buttons-border-radius);font-family:var(--e-global-typography-text-font-family,"Poppins"),Sans-serif;overflow:hidden}.e-contact-buttons__top-bar{align-items:center;background-color:var(--e-contact-buttons-top-bar-bg);display:flex;gap:20px;padding:20px;position:relative}.e-contact-buttons__top-bar-title{color:var(--e-contact-buttons-top-bar-title);font-size:24px;font-weight:700;margin-block-end:0}.e-contact-buttons__top-bar-subtitle{color:var(--e-contact-buttons-top-bar-subtitle);font-size:20px;margin-block-end:0}.e-contact-buttons__profile-image{align-items:center;display:flex;position:relative}.e-contact-buttons__profile-image img{border-radius:50%;-o-object-fit:cover;object-fit:cover}.e-contact-buttons__profile-image.has-size-small img{height:var(--e-contact-buttons-profile-image-size-small);width:var(--e-contact-buttons-profile-image-size-small)}.e-contact-buttons__profile-image.has-size-medium img{height:var(--e-contact-buttons-profile-image-size-medium);width:var(--e-contact-buttons-profile-image-size-medium)}.e-contact-buttons__profile-image.has-size-large img{height:var(--e-contact-buttons-profile-image-size-large);width:var(--e-contact-buttons-profile-image-size-large)}.e-contact-buttons__profile-image.has-dot:after{background-color:var(--e-contact-buttons-profile-dot-bg);border:3px solid var(--e-contact-buttons-top-bar-bg);border-radius:50%;bottom:5px;content:"";height:20px;position:absolute;right:0;width:20px}.e-contact-buttons__close-button,.e-contact-buttons__close-button[type=button]{background:none;border:0;color:var(--e-contact-buttons-close-button-color);inset-inline-end:20px;padding:0;position:absolute;top:20px}.e-contact-buttons__close-button:focus,.e-contact-buttons__close-button:hover,.e-contact-buttons__close-button[type=button]:focus,.e-contact-buttons__close-button[type=button]:hover{background:none;border:0;color:var(--e-contact-buttons-close-button-color)}.e-contact-buttons__chat-button-container,.e-contact-buttons__contact-icon-link,.e-contact-buttons__content-wrapper{pointer-events:auto}.e-contact-buttons__chat-button-container{display:flex;max-width:-moz-max-content;max-width:max-content}@media (max-width:767px){.e-contact-buttons__chat-button-container{position:relative}}.e-contact-buttons__chat-button,.e-contact-buttons__chat-button[type=button]{align-items:center;background-color:var(--e-contact-buttons-button-bg);border:0;border-radius:50%;color:var(--e-contact-buttons-button-icon);display:flex;justify-content:center;padding:0;position:relative;transition:all .3s}.e-contact-buttons__chat-button svg,.e-contact-buttons__chat-button[type=button] svg{fill:var(--e-contact-buttons-button-icon)}.e-contact-buttons__chat-button:focus,.e-contact-buttons__chat-button:hover,.e-contact-buttons__chat-button[type=button]:focus,.e-contact-buttons__chat-button[type=button]:hover{background-color:var(--e-contact-buttons-button-bg-hover);color:var(--e-contact-buttons-button-icon-hover);transition:all .3s}.e-contact-buttons__chat-button:focus svg,.e-contact-buttons__chat-button:hover svg,.e-contact-buttons__chat-button[type=button]:focus svg,.e-contact-buttons__chat-button[type=button]:hover svg{fill:var(--e-contact-buttons-button-icon-hover)}.e-contact-buttons__chat-button.has-dot:after,.e-contact-buttons__chat-button[type=button].has-dot:after{background-color:var(--e-contact-buttons-dot);border-radius:50%;content:"";height:var(--e-contact-buttons-dot-size);position:absolute;right:0;top:0;width:var(--e-contact-buttons-dot-size)}.e-contact-buttons__chat-button.has-size-small,.e-contact-buttons__chat-button[type=button].has-size-small{height:var(--e-contact-buttons-size-small);width:var(--e-contact-buttons-size-small)}.e-contact-buttons__chat-button.has-size-small svg,.e-contact-buttons__chat-button[type=button].has-size-small svg{height:var(--e-contact-buttons-svg-size-small);width:var(--e-contact-buttons-svg-size-small)}.e-contact-buttons__chat-button.has-size-small i,.e-contact-buttons__chat-button[type=button].has-size-small i{font-size:var(--e-contact-buttons-svg-size-small)}.e-contact-buttons__chat-button.has-size-medium,.e-contact-buttons__chat-button[type=button].has-size-medium{height:var(--e-contact-buttons-size-medium);width:var(--e-contact-buttons-size-medium)}.e-contact-buttons__chat-button.has-size-medium svg,.e-contact-buttons__chat-button[type=button].has-size-medium svg{height:var(--e-contact-buttons-svg-size-medium);width:var(--e-contact-buttons-svg-size-medium)}.e-contact-buttons__chat-button.has-size-medium i,.e-contact-buttons__chat-button[type=button].has-size-medium i{font-size:var(--e-contact-buttons-svg-size-medium)}.e-contact-buttons__chat-button.has-size-large,.e-contact-buttons__chat-button[type=button].has-size-large{height:var(--e-contact-buttons-size-large);width:var(--e-contact-buttons-size-large)}.e-contact-buttons__chat-button.has-size-large svg,.e-contact-buttons__chat-button[type=button].has-size-large svg{height:var(--e-contact-buttons-svg-size-large);width:var(--e-contact-buttons-svg-size-large)}.e-contact-buttons__chat-button.has-size-large i,.e-contact-buttons__chat-button[type=button].has-size-large i{font-size:var(--e-contact-buttons-svg-size-large)}.e-contact-buttons__chat-button.has-entrance-animation-delay,.e-contact-buttons__chat-button[type=button].has-entrance-animation-delay{animation-delay:var(--e-contact-button-chat-button-animation-delay)}.e-contact-buttons__chat-button.has-entrance-animation-duration-slow,.e-contact-buttons__chat-button[type=button].has-entrance-animation-duration-slow{animation-duration:2s}.e-contact-buttons__chat-button.has-entrance-animation-duration-normal,.e-contact-buttons__chat-button[type=button].has-entrance-animation-duration-normal{animation-duration:1s}.e-contact-buttons__chat-button.has-entrance-animation-duration-fast,.e-contact-buttons__chat-button[type=button].has-entrance-animation-duration-fast{animation-duration:.8s}.e-contact-buttons__chat-button.has-entrance-animation,.e-contact-buttons__chat-button[type=button].has-entrance-animation{opacity:0}.e-contact-buttons__chat-button.visible,.e-contact-buttons__chat-button[type=button].visible{opacity:1}.e-contact-buttons__message-bubble{background-color:var(--e-contact-buttons-message-bubble-chat-bg);padding:25px 20px;padding-inline-start:40px}.e-contact-buttons__message-bubble.has-typing-animation .e-contact-buttons__bubble-container{height:0;opacity:0;visibility:hidden}.e-contact-buttons__bubble{background-color:var(--e-contact-buttons-message-bubble-bubble-bg);border-radius:15px;padding:20px;position:relative}.e-contact-buttons__bubble:after{border-block-end-color:transparent;border-block-end-width:40px;border-block-start-color:transparent;border-block-start-width:0;border-inline-end-color:var(--e-contact-buttons-message-bubble-bubble-bg);border-inline-end-width:40px;border-inline-start-color:transparent;border-inline-start-width:0;border-style:solid;content:"";height:0;inset-inline-start:-20px;position:absolute;top:0;width:0}.e-contact-buttons__message-bubble-name{color:var(--e-contact-buttons-message-bubble-name);font-size:20px;font-weight:600;line-height:25px;margin-block-end:8px}.e-contact-buttons__message-bubble-body{color:var(--e-contact-buttons-message-bubble-body);font-size:20px;line-height:25px;margin-block-end:8px}.e-contact-buttons__message-bubble-time{color:var(--e-contact-buttons-message-bubble-time);font-size:20px;font-weight:600;line-height:25px;margin-block-end:0;text-align:end}.e-contact-buttons__powered-container{text-align:center}.e-contact-buttons__powered-text{color:#000;font-size:16px;font-weight:500;margin-block-end:12px}.e-contact-buttons__dots-container{background-color:var(--e-contact-buttons-message-bubble-bubble-bg);border-radius:15px;display:inline-flex;padding:10px 12px}.e-contact-buttons__dot{animation:e-contact-buttons-typing-jump 1s infinite;background-color:var(--e-contact-buttons-message-bubble-name);border-radius:50%;display:inline-block;height:7px;margin-left:auto;margin-right:3px;position:relative;width:7px}.e-contact-buttons__dot-1{animation-delay:.2s}.e-contact-buttons__dot-2{animation-delay:.4s}.e-contact-buttons__dot-3{animation-delay:.6s}.e-contact-buttons__send-button{background-color:var(--e-contact-buttons-chat-box-bg);padding:12px 20px 20px}.e-contact-buttons__send-button .e-contact-buttons__send-cta{color:var(--e-contact-buttons-send-button-icon)}.e-contact-buttons__send-button .e-contact-buttons__send-cta:focus,.e-contact-buttons__send-button .e-contact-buttons__send-cta:hover{color:var(--e-contact-buttons-send-button-icon-hover)}.e-contact-buttons__send-cta{align-items:center;background-color:var(--e-contact-buttons-send-button-bg);border-radius:30px;display:flex;font-size:18px;font-weight:500;gap:8px;justify-content:center;padding:10px;text-align:center;transition:all .3s;width:100%}.e-contact-buttons__send-cta svg{fill:var(--e-contact-buttons-send-button-icon);height:28px;width:28px}.e-contact-buttons__send-cta:focus,.e-contact-buttons__send-cta:hover{background-color:var(--e-contact-buttons-send-button-bg-hover);transition:all .3s}.e-contact-buttons__send-cta:focus svg,.e-contact-buttons__send-cta:hover svg{fill:var(--e-contact-buttons-send-button-icon-hover)}.e-contact-buttons__content.visible .e-contact-buttons__message-bubble.has-typing-animation .e-contact-buttons__dots-container{animation-delay:0;animation-duration:2s;animation-fill-mode:forwards;animation-iteration-count:1;animation-name:e-contact-buttons-disappear}.e-contact-buttons__content.visible .e-contact-buttons__message-bubble.has-typing-animation .e-contact-buttons__bubble-container{animation-delay:2s;animation-duration:.1s;animation-fill-mode:forwards;animation-iteration-count:1;animation-name:e-contact-buttons-appear}.e-con:has(.e-contact-buttons)>.e-con-inner,.e-con>.e-con-inner.e-con-inner--floating-buttons{padding-block-end:0;padding-block-start:0}@keyframes e-contact-buttons-typing-jump{0%{bottom:0}20%{bottom:5px}40%{bottom:0}}@keyframes e-contact-buttons-appear{0%{height:0;opacity:0;visibility:hidden}to{height:auto;opacity:1;visibility:visible}}@keyframes e-contact-buttons-disappear{0%{display:inline-flex}to{display:none}}@keyframes e-contact-buttons-close{0%,99.99%{opacity:1;visibility:visible}to{opacity:0;transform:none;visibility:hidden}}{
"title": "SCF Textarea Field Fragment",
"description": "Type-specific properties for textarea fields",
"type": "object",
"properties": {
"type": {
"enum": [ "textarea" ]
},
"default_value": { "$ref": "#/definitions/default_value" },
"maxlength": { "$ref": "#/definitions/maxlength" },
"rows": { "$ref": "#/definitions/rows" },
"placeholder": { "$ref": "#/definitions/placeholder" },
"new_lines": { "$ref": "#/definitions/new_lines" }
}
}
/**
* @jest-environment jest-fixed-jsdom
*/
describe( 'Address Autocomplete Provider Registration', () => {
beforeEach( () => {
delete global.window.wc;
// Reset the window object and providers before each test
Object.assign( global.window, {
wc_address_autocomplete_params: {
address_providers: JSON.stringify( [
{ id: 'test-provider', name: 'Test provider' },
{ id: 'wc-payments', name: 'WooCommerce Payments' },
{ id: 'provider-1', name: 'Provider 1' },
{ id: 'provider-2', name: 'Provider 2' },
] ),
},
} );
// Reset the module before each test
jest.resetModules();
require( '../utils/address-autocomplete-common' );
require( '../address-autocomplete' );
} );
test( 'should successfully register a valid provider', () => {
const validProvider = {
id: 'test-provider',
canSearch: () => {},
search: () => {},
select: () => {},
};
const result =
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
validProvider
);
expect( result ).toBe( true );
expect( console ).not.toHaveErrored();
} );
test( 'should reject invalid provider (null, undefined, non-object)', () => {
const invalidProviders = [ null, undefined, 'string', 123, true ];
invalidProviders.forEach( ( provider ) => {
const result =
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
provider
);
expect( result ).toBe( false );
expect( console ).toHaveErroredWith(
'Error registering address provider:',
'Address provider must be a valid object'
);
expect( console ).toHaveErrored();
} );
} );
test( 'should handle missing wc_address_autocomplete_params', () => {
delete global.window.wc; // ensure fresh load
global.window.wc_address_autocomplete_params = undefined;
jest.resetModules();
require( '../utils/address-autocomplete-common' );
require( '../address-autocomplete' );
const validProvider = {
id: 'test-provider',
canSearch: () => {},
search: () => {},
select: () => {},
};
const result =
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
validProvider
);
expect( result ).toBe( false );
expect( console ).toHaveErroredWith(
'Error registering address provider:',
'Provider test-provider not registered on server'
);
} );
test( 'should handle invalid address_providers type', () => {
delete global.window.wc; // ensure fresh load
global.window.wc_address_autocomplete_params = undefined;
jest.resetModules();
require( '../utils/address-autocomplete-common' );
require( '../address-autocomplete' );
const validProvider = {
id: 'test-provider',
canSearch: () => {},
search: () => {},
select: () => {},
};
const result =
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
validProvider
);
expect( result ).toBe( false );
expect( console ).toHaveErroredWith(
'Error registering address provider:',
'Provider test-provider not registered on server'
);
} );
test( 'should reject provider without ID', () => {
const invalidProvider = {
canSearch: () => {},
search: () => {},
select: () => {},
};
const result =
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
invalidProvider
);
expect( result ).toBe( false );
expect( console ).toHaveErroredWith(
'Error registering address provider:',
'Address provider must have a valid ID'
);
} );
test( 'should reject provider with non-string ID', () => {
const invalidProvider = {
id: 123,
canSearch: () => {},
search: () => {},
select: () => {},
};
const result =
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
invalidProvider
);
expect( result ).toBe( false );
expect( console ).toHaveErroredWith(
'Error registering address provider:',
'Address provider must have a valid ID'
);
} );
test( 'should reject provider without canSearch function', () => {
const invalidProvider = {
id: 'test-provider',
search: () => {},
select: () => {},
};
const result =
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
invalidProvider
);
expect( result ).toBe( false );
expect( console ).toHaveErroredWith(
'Error registering address provider:',
'Address provider must have a canSearch function'
);
} );
test( 'should reject provider without search function', () => {
const invalidProvider = {
id: 'test-provider',
canSearch: () => {},
select: () => {},
};
const result =
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
invalidProvider
);
expect( result ).toBe( false );
expect( console ).toHaveErroredWith(
'Error registering address provider:',
'Address provider must have a search function'
);
} );
test( 'should reject provider without select function', () => {
const invalidProvider = {
id: 'test-provider',
canSearch: () => {},
search: () => {},
};
const result =
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
invalidProvider
);
expect( result ).toBe( false );
expect( console ).toHaveErroredWith(
'Error registering address provider:',
'Address provider must have a select function'
);
} );
test( 'should reject provider not registered on server', () => {
const unregisteredProvider = {
id: 'unregistered-provider',
canSearch: () => {},
search: () => {},
select: () => {},
};
const result =
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
unregisteredProvider
);
expect( result ).toBe( false );
expect( console ).toHaveErroredWith(
'Error registering address provider:',
'Provider unregistered-provider not registered on server'
);
} );
test( 'should freeze provider after successful registration', () => {
const validProvider = {
id: 'test-provider',
canSearch: () => {},
search: () => {},
select: () => {},
};
const result =
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
validProvider
);
expect( result ).toBe( true );
// Verify provider is frozen
expect(
Object.isFrozen(
window.wc.addressAutocomplete.providers[ 'test-provider' ]
)
).toBe( true );
// Attempt to modify should throw in strict mode
expect( () => {
window.wc.addressAutocomplete.providers[ 'test-provider' ].newProp =
'test';
} ).toThrow( TypeError );
// Verify the property wasn't added
expect(
window.wc.addressAutocomplete.providers[ 'test-provider' ].newProp
).toBeUndefined();
} );
test( 'should not allow duplicate provider registration', () => {
const provider1 = {
id: 'test-provider',
canSearch: () => false,
search: () => [ 'original' ],
select: () => {},
};
const provider2 = {
id: 'test-provider',
canSearch: () => true,
search: () => [ 'duplicate' ],
select: () => {},
};
// Mock console.warn to capture warning message
const consoleSpy = jest
.spyOn( console, 'warn' )
.mockImplementation( () => {} );
// Register first provider
const firstResult =
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
provider1
);
expect( firstResult ).toBe( true );
// Try to register second provider with same ID
const duplicateResult =
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
provider2
);
expect( duplicateResult ).toBe( false );
// Verify warning was logged
expect( consoleSpy ).toHaveBeenCalledWith(
'Address provider with ID "test-provider" is already registered.'
);
// Verify the original provider is preserved (not overwritten)
expect(
window.wc.addressAutocomplete.providers[
'test-provider'
].canSearch()
).toBe( false );
expect(
window.wc.addressAutocomplete.providers[ 'test-provider' ].search()
).toEqual( [ 'original' ] );
consoleSpy.mockRestore();
} );
test( 'should allow multiple providers with different IDs', () => {
const provider1 = {
id: 'provider-1',
canSearch: () => true,
search: () => [ 'provider1-results' ],
select: () => {},
};
const provider2 = {
id: 'provider-2',
canSearch: () => true,
search: () => [ 'provider2-results' ],
select: () => {},
};
// Register both providers
const result1 =
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
provider1
);
const result2 =
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
provider2
);
expect( result1 ).toBe( true );
expect( result2 ).toBe( true );
// Verify both providers are registered
expect(
window.wc.addressAutocomplete.providers[ 'provider-1' ]
).toBeDefined();
expect(
window.wc.addressAutocomplete.providers[ 'provider-2' ]
).toBeDefined();
// Verify they maintain their separate functionality
expect(
window.wc.addressAutocomplete.providers[ 'provider-1' ].search()
).toEqual( [ 'provider1-results' ] );
expect(
window.wc.addressAutocomplete.providers[ 'provider-2' ].search()
).toEqual( [ 'provider2-results' ] );
} );
} );
describe( 'Address Suggestions Component', () => {
let mockProvider;
let billingAddressInput;
let shippingAddressInput;
beforeEach( async () => {
// Reset DOM
document.body.innerHTML = '';
delete global.window.wc;
// Mock jQuery
global.window.jQuery = jest.fn( ( selector ) => ( {
hasClass: jest.fn( () => false ),
trigger: jest.fn(),
select2: jest.fn(),
on: jest.fn(),
} ) );
// Setup window object
Object.assign( global.window, {
DOMPurify: {
sanitize: ( input ) => input, // No-op for testing
},
wc_address_autocomplete_common_params: {
address_providers: JSON.stringify( [
{
id: 'test-provider',
name: 'Test provider',
branding_html:
'
Powered by Test Provider
',
},
{
id: 'test-provider-unbranded',
name: 'Test provider unbranded',
},
] ),
},
} );
// Create DOM structure
const form = document.createElement( 'form' );
// Billing fields
const billingCountry = document.createElement( 'select' );
billingCountry.id = 'billing_country';
const billingOption = document.createElement( 'option' );
billingOption.value = 'US';
billingOption.selected = true;
billingCountry.appendChild( billingOption );
billingCountry.value = 'US';
const billingAddress1 = document.createElement( 'input' );
billingAddress1.id = 'billing_address_1';
billingAddress1.type = 'text';
const billingCity = document.createElement( 'input' );
billingCity.id = 'billing_city';
billingCity.type = 'text';
const billingPostcode = document.createElement( 'input' );
billingPostcode.id = 'billing_postcode';
billingPostcode.type = 'text';
const billingState = document.createElement( 'input' );
billingState.id = 'billing_state';
billingState.type = 'text';
// Create wrapper for billing address
const billingWrapper = document.createElement( 'div' );
billingWrapper.className = 'woocommerce-input-wrapper';
billingWrapper.appendChild( billingAddress1 );
// Shipping fields
const shippingCountry = document.createElement( 'select' );
shippingCountry.id = 'shipping_country';
const shippingOption = document.createElement( 'option' );
shippingOption.value = 'US';
shippingOption.selected = true;
shippingCountry.appendChild( shippingOption );
shippingCountry.value = 'US';
const shippingAddress1 = document.createElement( 'input' );
shippingAddress1.id = 'shipping_address_1';
shippingAddress1.type = 'text';
const shippingCity = document.createElement( 'input' );
shippingCity.id = 'shipping_city';
shippingCity.type = 'text';
const shippingPostcode = document.createElement( 'input' );
shippingPostcode.id = 'shipping_postcode';
shippingPostcode.type = 'text';
const shippingState = document.createElement( 'input' );
shippingState.id = 'shipping_state';
shippingState.type = 'text';
// Create wrapper for shipping address
const shippingWrapper = document.createElement( 'div' );
shippingWrapper.className = 'woocommerce-input-wrapper';
shippingWrapper.appendChild( shippingAddress1 );
form.appendChild( billingCountry );
form.appendChild( billingWrapper );
form.appendChild( billingCity );
form.appendChild( billingPostcode );
form.appendChild( billingState );
form.appendChild( shippingCountry );
form.appendChild( shippingWrapper );
form.appendChild( shippingCity );
form.appendChild( shippingPostcode );
form.appendChild( shippingState );
document.body.appendChild( form );
billingAddressInput = billingAddress1;
shippingAddressInput = shippingAddress1;
// Create mock provider
mockProvider = {
id: 'test-provider',
canSearch: jest.fn( ( country ) => country === 'US' ),
search: jest.fn( async ( query, country, type ) => [
{
id: 'addr1',
label: '123 Main Street, City, US',
matchedSubstrings: [ { offset: 0, length: 3 } ],
},
{
id: 'addr2',
label: '456 Oak Avenue, Town, US',
matchedSubstrings: [ { offset: 0, length: 3 } ],
},
] ),
select: jest.fn( async ( addressId ) => ( {
address_1: '123 Main Street',
city: 'City',
postcode: '12345',
country: 'US',
state: 'CA',
} ) ),
};
// Reset modules and require fresh instance
jest.resetModules();
require( '../utils/address-autocomplete-common' );
require( '../address-autocomplete' );
// Register the mock provider
window.wc.addressAutocomplete.registerAddressAutocompleteProvider(
mockProvider
);
// Trigger DOMContentLoaded event and wait for initialization
const event = new Event( 'DOMContentLoaded' );
document.dispatchEvent( event );
// Wait a bit for DOM initialization to complete
await new Promise( ( resolve ) => setTimeout( resolve, 10 ) );
} );
afterEach( () => {
jest.clearAllMocks();
// Reset providers properly
if ( window.wc && window.wc.addressAutocomplete ) {
window.wc.addressAutocomplete.providers = {};
window.wc.addressAutocomplete.activeProvider = {
billing: null,
shipping: null,
};
}
} );
describe( 'DOM Initialization', () => {
test( 'should create suggestions container for address inputs', () => {
const billingSuggestions = document.getElementById(
'address_suggestions_billing'
);
const shippingSuggestions = document.getElementById(
'address_suggestions_shipping'
);
expect( billingSuggestions ).toBeTruthy();
expect( shippingSuggestions ).toBeTruthy();
expect( billingSuggestions.className ).toBe(
'woocommerce-address-suggestions'
);
expect( billingSuggestions.style.display ).toBe( 'none' );
expect( billingSuggestions.getAttribute( 'role' ) ).toBe(
'region'
);
expect( billingSuggestions.getAttribute( 'aria-live' ) ).toBe(
'polite'
);
// Check suggestions list
const billingList =
billingSuggestions.querySelector( '.suggestions-list' );
expect( billingList ).toBeTruthy();
expect( billingList.getAttribute( 'role' ) ).toBe( 'listbox' );
expect( billingList.getAttribute( 'aria-label' ) ).toBe(
'Address suggestions'
);
// Check search icon container exists
const billingIconContainer = document.querySelector(
'.address-search-icon'
);
expect( billingIconContainer ).toBeTruthy();
} );
test( 'should set active provider based on country value', () => {
expect( window.wc.addressAutocomplete.activeProvider.billing ).toBe(
mockProvider
);
expect(
window.wc.addressAutocomplete.activeProvider.shipping
).toBe( mockProvider );
} );
test( 'should add autocomplete-available class when provider is active', () => {
const billingWrapper = billingAddressInput.closest(
'.woocommerce-input-wrapper'
);
const shippingWrapper = shippingAddressInput.closest(
'.woocommerce-input-wrapper'
);
expect(
billingWrapper.classList.contains( 'autocomplete-available' )
).toBe( true );
expect(
shippingWrapper.classList.contains( 'autocomplete-available' )
).toBe( true );
} );
} );
describe( 'Active Provider Management', () => {
test( 'should set active provider when country matches canSearch criteria', () => {
const billingCountry = document.getElementById( 'billing_country' );
billingCountry.value = 'US';
billingCountry.dispatchEvent( new Event( 'change' ) );
expect( mockProvider.canSearch ).toHaveBeenCalledWith( 'US' );
expect( window.wc.addressAutocomplete.activeProvider.billing ).toBe(
mockProvider
);
} );
test( 'should clear active provider when country does not match canSearch criteria', () => {
const billingCountry = document.getElementById( 'billing_country' );
// Create new option and select it
const frOption = document.createElement( 'option' );
frOption.value = 'FR';
billingCountry.appendChild( frOption );
billingCountry.value = 'FR';
billingCountry.dispatchEvent( new Event( 'change' ) );
expect( mockProvider.canSearch ).toHaveBeenCalledWith( 'FR' );
expect( window.wc.addressAutocomplete.activeProvider.billing ).toBe(
null
);
} );
test( 'should remove autocomplete-available class when no provider is active', () => {
const billingCountry = document.getElementById( 'billing_country' );
const billingWrapper = billingAddressInput.closest(
'.woocommerce-input-wrapper'
);
billingCountry.value = 'FR';
billingCountry.dispatchEvent( new Event( 'change' ) );
expect(
billingWrapper.classList.contains( 'autocomplete-available' )
).toBe( false );
} );
test( 'should handle country change for both billing and shipping', () => {
const billingCountry = document.getElementById( 'billing_country' );
const shippingCountry =
document.getElementById( 'shipping_country' );
// Add FR option to billing
const frOption = document.createElement( 'option' );
frOption.value = 'FR';
billingCountry.appendChild( frOption );
billingCountry.value = 'FR';
billingCountry.dispatchEvent( new Event( 'change' ) );
shippingCountry.dispatchEvent( new Event( 'change' ) );
expect( window.wc.addressAutocomplete.activeProvider.billing ).toBe(
null
);
expect(
window.wc.addressAutocomplete.activeProvider.shipping
).toBe( mockProvider );
} );
} );
describe( 'Address Suggestions Display', () => {
test( 'should not display suggestions for input less than 3 characters', async () => {
billingAddressInput.value = 'ab';
billingAddressInput.dispatchEvent( new Event( 'input' ) );
// Wait for timeout
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsList = document.querySelector(
'#address_suggestions_billing .suggestions-list'
);
expect( suggestionsList.innerHTML ).toBe( '' );
expect( mockProvider.search ).not.toHaveBeenCalled();
} );
test( 'should hide suggestions when input goes from 3+ characters to less than 3', async () => {
// First show suggestions with 3+ characters
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
expect( suggestionsContainer.style.display ).toBe( 'block' );
// Now reduce to less than 3 characters
billingAddressInput.value = '12';
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
expect( suggestionsContainer.style.display ).toBe( 'none' );
} );
test( 'should display suggestions for input with 3 or more characters', async () => {
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
// Wait for timeout and async operations
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
expect( mockProvider.search ).toHaveBeenCalledWith(
'123',
'US',
'billing'
);
const suggestionsList = document.querySelector(
'#address_suggestions_billing .suggestions-list'
);
const suggestions = suggestionsList.querySelectorAll( 'li' );
expect( suggestions ).toHaveLength( 2 );
expect( suggestions[ 0 ].textContent ).toContain(
'123 Main Street'
);
expect( suggestions[ 1 ].textContent ).toContain(
'456 Oak Avenue'
);
} );
test( 'should highlight matched text in suggestions', async () => {
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsList = document.querySelector(
'#address_suggestions_billing .suggestions-list'
);
const firstSuggestion = suggestionsList.querySelector( 'li' );
const strongElement = firstSuggestion.querySelector( 'strong' );
expect( strongElement ).toBeTruthy();
expect( strongElement.textContent ).toBe( '123' );
} );
test( 'should limit suggestions to maximum of 5', async () => {
// Mock provider to return more than 5 suggestions
mockProvider.search.mockResolvedValue(
Array.from( { length: 10 }, ( _, i ) => ( {
id: `addr${ i }`,
label: `${ i } Test Street`,
matchedSubstrings: [],
} ) )
);
billingAddressInput.value = 'test';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsList = document.querySelector(
'#address_suggestions_billing .suggestions-list'
);
const suggestions = suggestionsList.querySelectorAll( 'li' );
expect( suggestions ).toHaveLength( 5 );
} );
test( 'should hide suggestions when no results returned', async () => {
mockProvider.search.mockResolvedValue( [] );
billingAddressInput.value = 'xyz';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
expect( suggestionsContainer.style.display ).toBe( 'none' );
} );
test( 'should hide suggestions and log error when search throws exception', async () => {
mockProvider.search.mockRejectedValue(
new Error( 'Search failed' )
);
const consoleSpy = jest
.spyOn( console, 'error' )
.mockImplementation( () => {} );
billingAddressInput.value = 'test';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
expect( suggestionsContainer.style.display ).toBe( 'none' );
expect( consoleSpy ).toHaveBeenCalledWith(
'Address search error:',
expect.any( Error )
);
consoleSpy.mockRestore();
} );
} );
describe( 'Keyboard Navigation', () => {
beforeEach( async () => {
// Setup suggestions
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
} );
test( 'should navigate down with ArrowDown key', () => {
const suggestions = document.querySelectorAll(
'#address_suggestions_billing .suggestions-list li'
);
// No suggestion should be active initially
expect( suggestions[ 0 ].classList.contains( 'active' ) ).toBe(
false
);
expect( suggestions[ 0 ].getAttribute( 'aria-selected' ) ).toBe(
null
);
// Press ArrowDown
const keydownEvent = new KeyboardEvent( 'keydown', {
key: 'ArrowDown',
bubbles: true,
} );
billingAddressInput.dispatchEvent( keydownEvent );
// First suggestion should now be active
expect( suggestions[ 0 ].classList.contains( 'active' ) ).toBe(
true
);
expect( suggestions[ 0 ].getAttribute( 'aria-selected' ) ).toBe(
'true'
);
expect( suggestions[ 1 ].classList.contains( 'active' ) ).toBe(
false
);
} );
test( 'should navigate up with ArrowUp key', () => {
const suggestions = document.querySelectorAll(
'#address_suggestions_billing .suggestions-list li'
);
// Navigate to first item first
let keydownEvent = new KeyboardEvent( 'keydown', {
key: 'ArrowDown',
bubbles: true,
} );
billingAddressInput.dispatchEvent( keydownEvent );
// Navigate to second item
keydownEvent = new KeyboardEvent( 'keydown', {
key: 'ArrowDown',
bubbles: true,
} );
billingAddressInput.dispatchEvent( keydownEvent );
// Press ArrowUp
keydownEvent = new KeyboardEvent( 'keydown', {
key: 'ArrowUp',
bubbles: true,
} );
billingAddressInput.dispatchEvent( keydownEvent );
// First suggestion should be active again
expect( suggestions[ 0 ].classList.contains( 'active' ) ).toBe(
true
);
expect( suggestions[ 1 ].classList.contains( 'active' ) ).toBe(
false
);
} );
test( 'should wrap around when navigating beyond bounds', () => {
const suggestions = document.querySelectorAll(
'#address_suggestions_billing .suggestions-list li'
);
// Navigate to first item
let keydownEvent = new KeyboardEvent( 'keydown', {
key: 'ArrowDown',
bubbles: true,
} );
billingAddressInput.dispatchEvent( keydownEvent );
// Navigate to second (last) item
keydownEvent = new KeyboardEvent( 'keydown', {
key: 'ArrowDown',
bubbles: true,
} );
billingAddressInput.dispatchEvent( keydownEvent );
// Navigate beyond last item - should wrap to first
keydownEvent = new KeyboardEvent( 'keydown', {
key: 'ArrowDown',
bubbles: true,
} );
billingAddressInput.dispatchEvent( keydownEvent );
expect( suggestions[ 0 ].classList.contains( 'active' ) ).toBe(
true
);
expect( suggestions[ 1 ].classList.contains( 'active' ) ).toBe(
false
);
} );
test( 'should select address with Enter key', async () => {
// Navigate to first suggestion first
let keydownEvent = new KeyboardEvent( 'keydown', {
key: 'ArrowDown',
bubbles: true,
} );
billingAddressInput.dispatchEvent( keydownEvent );
// Press Enter to select first suggestion
keydownEvent = new KeyboardEvent( 'keydown', {
key: 'Enter',
bubbles: true,
} );
billingAddressInput.dispatchEvent( keydownEvent );
// Wait for async operations
await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
expect( mockProvider.select ).toHaveBeenCalledWith( 'addr1' );
// Suggestions should be hidden
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
expect( suggestionsContainer.style.display ).toBe( 'none' );
} );
test( 'should hide suggestions with Escape key', () => {
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
expect( suggestionsContainer.style.display ).toBe( 'block' );
// Press Escape
const keydownEvent = new KeyboardEvent( 'keydown', {
key: 'Escape',
bubbles: true,
} );
billingAddressInput.dispatchEvent( keydownEvent );
expect( suggestionsContainer.style.display ).toBe( 'none' );
} );
test( 'should not handle keyboard events when suggestions are hidden', () => {
// Hide suggestions first
const escapeEvent = new KeyboardEvent( 'keydown', {
key: 'Escape',
bubbles: true,
} );
billingAddressInput.dispatchEvent( escapeEvent );
// Try to navigate with ArrowDown - should not throw error
const arrowEvent = new KeyboardEvent( 'keydown', {
key: 'ArrowDown',
bubbles: true,
} );
expect( () => {
billingAddressInput.dispatchEvent( arrowEvent );
} ).not.toThrow();
} );
} );
describe( 'Address Selection', () => {
test( 'should populate address fields when address is selected', async () => {
// Setup suggestions
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
// Click on first suggestion
const firstSuggestion = document.querySelector(
'#address_suggestions_billing .suggestions-list li'
);
firstSuggestion.click();
// Wait for async operations and timeout
await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
expect( mockProvider.select ).toHaveBeenCalledWith( 'addr1' );
// Check that fields are populated
expect( document.getElementById( 'billing_address_1' ).value ).toBe(
'123 Main Street'
);
expect( document.getElementById( 'billing_city' ).value ).toBe(
'City'
);
expect( document.getElementById( 'billing_postcode' ).value ).toBe(
'12345'
);
expect( document.getElementById( 'billing_country' ).value ).toBe(
'US'
);
expect( document.getElementById( 'billing_state' ).value ).toBe(
'CA'
);
} );
test( 'should handle partial address data from provider', async () => {
// Mock provider to return partial data
mockProvider.select.mockResolvedValue( {
address_1: '123 Main Street',
city: 'City',
// Missing postcode, country, state
} );
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const firstSuggestion = document.querySelector(
'#address_suggestions_billing .suggestions-list li'
);
firstSuggestion.click();
await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
// Only provided fields should be populated
expect( document.getElementById( 'billing_address_1' ).value ).toBe(
'123 Main Street'
);
expect( document.getElementById( 'billing_city' ).value ).toBe(
'City'
);
expect( document.getElementById( 'billing_postcode' ).value ).toBe(
''
);
} );
test( 'should clear existing field values when not present in selected address data', async () => {
// Create address_2 field since it's not in the initial setup
const billingAddress2 = document.createElement( 'input' );
billingAddress2.id = 'billing_address_2';
billingAddress2.type = 'text';
billingAddress2.value = 'Apt 101';
document.querySelector( 'form' ).appendChild( billingAddress2 );
// Pre-populate some fields
document.getElementById( 'billing_city' ).value = 'Old City';
document.getElementById( 'billing_postcode' ).value = '99999';
document.getElementById( 'billing_state' ).value = 'TX';
// Mock provider to return data with some fields missing
mockProvider.select.mockResolvedValue( {
address_1: '456 Oak Avenue',
city: 'New City',
country: 'US',
// Missing address_2, postcode, and state
} );
billingAddressInput.value = '456';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const firstSuggestion = document.querySelector(
'#address_suggestions_billing .suggestions-list li'
);
firstSuggestion.click();
await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
// Check that provided fields are populated
expect( document.getElementById( 'billing_address_1' ).value ).toBe(
'456 Oak Avenue'
);
expect( document.getElementById( 'billing_city' ).value ).toBe(
'New City'
);
expect( document.getElementById( 'billing_country' ).value ).toBe(
'US'
);
// Check that missing fields are cleared
expect( document.getElementById( 'billing_address_2' ).value ).toBe(
''
);
expect( document.getElementById( 'billing_postcode' ).value ).toBe(
''
);
expect( document.getElementById( 'billing_state' ).value ).toBe(
''
);
} );
test( 'should only clear fields that exist and have values', async () => {
// Pre-populate only some fields
document.getElementById( 'billing_city' ).value = 'Existing City';
document.getElementById( 'billing_postcode' ).value = '12345';
// Mock provider to return partial data
mockProvider.select.mockResolvedValue( {
address_1: '789 Pine Street',
state: 'CA',
country: 'US',
// Missing city and postcode
} );
billingAddressInput.value = '789';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const firstSuggestion = document.querySelector(
'#address_suggestions_billing .suggestions-list li'
);
firstSuggestion.click();
await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
// Check that provided fields are populated
expect( document.getElementById( 'billing_address_1' ).value ).toBe(
'789 Pine Street'
);
expect( document.getElementById( 'billing_state' ).value ).toBe(
'CA'
);
expect( document.getElementById( 'billing_country' ).value ).toBe(
'US'
);
// Check that city and postcode are cleared since they had values but weren't in the response
expect( document.getElementById( 'billing_city' ).value ).toBe(
''
);
expect( document.getElementById( 'billing_postcode' ).value ).toBe(
''
);
} );
test( 'should handle provider selection errors gracefully', async () => {
mockProvider.select.mockRejectedValue(
new Error( 'Selection failed' )
);
const consoleSpy = jest
.spyOn( console, 'error' )
.mockImplementation( () => {} );
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const firstSuggestion = document.querySelector(
'#address_suggestions_billing .suggestions-list li'
);
firstSuggestion.click();
await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
expect( consoleSpy ).toHaveBeenCalledWith(
'Error selecting address from provider',
'test-provider',
expect.any( Error )
);
// Fields should remain unchanged
expect( document.getElementById( 'billing_address_1' ).value ).toBe(
'123'
);
consoleSpy.mockRestore();
} );
test( 'should handle invalid address data from provider', async () => {
mockProvider.select.mockResolvedValue( null );
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const firstSuggestion = document.querySelector(
'#address_suggestions_billing .suggestions-list li'
);
firstSuggestion.click();
await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
// Fields should remain unchanged
expect( document.getElementById( 'billing_address_1' ).value ).toBe(
'123'
);
} );
} );
describe( 'Browser Autofill Management', () => {
test( 'should disable browser autofill when suggestions are shown', async () => {
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
expect( billingAddressInput.getAttribute( 'autocomplete' ) ).toBe(
'none'
);
expect( billingAddressInput.getAttribute( 'data-lpignore' ) ).toBe(
'true'
);
expect( billingAddressInput.getAttribute( 'data-op-ignore' ) ).toBe(
'true'
);
expect( billingAddressInput.getAttribute( 'data-1p-ignore' ) ).toBe(
'true'
);
} );
test( 'should enable browser autofill when suggestions are hidden', async () => {
// First show suggestions
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
// Then hide them
billingAddressInput.value = 'xy';
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
expect( billingAddressInput.getAttribute( 'autocomplete' ) ).toBe(
'address-line1'
);
expect( billingAddressInput.getAttribute( 'data-lpignore' ) ).toBe(
'false'
);
} );
} );
describe( 'Security and Sanitization', () => {
test( 'should sanitize input values for XSS protection', async () => {
const maliciousInput = '';
const consoleSpy = jest
.spyOn( console, 'warn' )
.mockImplementation( () => {} );
billingAddressInput.value = maliciousInput;
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
expect( consoleSpy ).toHaveBeenCalledWith(
'Input was sanitized for security'
);
expect( mockProvider.search ).toHaveBeenCalledWith(
'alert("xss")',
'US',
'billing'
);
consoleSpy.mockRestore();
} );
test( 'should handle invalid match data safely', async () => {
// Mock provider to return invalid match data
mockProvider.search.mockResolvedValue( [
{
id: 'addr1',
label: '123 Main Street',
matchedSubstrings: [
{ offset: -1, length: 5 }, // Invalid offset
{ offset: 50, length: 10 }, // Offset beyond string length
{ offset: 0, length: -1 }, // Invalid length
null, // Null match
],
},
] );
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsList = document.querySelector(
'#address_suggestions_billing .suggestions-list'
);
const firstSuggestion = suggestionsList.querySelector( 'li' );
// Should still render the suggestion without highlighting
expect( firstSuggestion.textContent ).toBe( '123 Main Street' );
expect( firstSuggestion.querySelector( 'strong' ) ).toBe( null );
} );
} );
describe( 'Click Outside Behavior', () => {
test( 'should hide suggestions when clicking outside', async () => {
// Show suggestions first
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
expect( suggestionsContainer.style.display ).toBe( 'block' );
// Click outside
const outsideElement = document.createElement( 'div' );
document.body.appendChild( outsideElement );
outsideElement.click();
expect( suggestionsContainer.style.display ).toBe( 'none' );
} );
test( 'should not hide suggestions when clicking inside suggestions container', async () => {
// Show suggestions first
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
expect( suggestionsContainer.style.display ).toBe( 'block' );
// Click inside suggestions container
suggestionsContainer.click();
expect( suggestionsContainer.style.display ).toBe( 'block' );
} );
test( 'should not hide suggestions when clicking address input', async () => {
// Show suggestions first
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
expect( suggestionsContainer.style.display ).toBe( 'block' );
// Click on address input
billingAddressInput.click();
expect( suggestionsContainer.style.display ).toBe( 'block' );
} );
} );
describe( 'Branding HTML', () => {
test( 'should display branding HTML when suggestions are shown', async () => {
// Show suggestions
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
const brandingElement = suggestionsContainer.querySelector(
'.woocommerce-address-autocomplete-branding'
);
expect( brandingElement ).toBeTruthy();
expect( brandingElement.innerHTML ).toBe(
'Powered by Test Provider
'
);
} );
test.skip( 'should hide branding HTML when suggestions are hidden', async () => {
// Show suggestions first
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
let brandingElement = suggestionsContainer.querySelector(
'.woocommerce-address-autocomplete-branding'
);
expect( brandingElement.innerHTML ).toBe(
'Powered by Test Provider
'
);
expect( brandingElement.style.display ).toBe( 'flex' );
// Hide suggestions
billingAddressInput.value = 'xy';
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
brandingElement = suggestionsContainer.querySelector(
'.woocommerce-address-autocomplete-branding'
);
// Element should still exist but be hidden
expect( brandingElement ).toBeTruthy();
expect( brandingElement.style.display ).toBe( 'none' );
} );
test( 'should not create branding element when provider has no branding_html', async () => {
// Re-initialize the module
jest.resetModules();
window.wc.addressAutocomplete.providers = [];
require( '../address-autocomplete' );
// Re-register provider
window.wc.addressAutocomplete.registerAddressAutocompleteProvider( {
search: mockProvider,
select: mockProvider,
canSearch: mockProvider,
id: 'mock-provider-unbranded',
} );
// Trigger DOMContentLoaded again
const event = new Event( 'DOMContentLoaded' );
document.dispatchEvent( event );
await new Promise( ( resolve ) => setTimeout( resolve, 10 ) );
// Show suggestions
billingAddressInput.value = '456';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
const brandingElement = suggestionsContainer.querySelector(
'.woocommerce-address-autocomplete-branding'
);
// Branding element should not be created when there's no branding_html
expect( brandingElement ).toBeFalsy();
} );
test( 'should reuse existing branding element on subsequent searches', async () => {
// First search
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
const firstBrandingElement = suggestionsContainer.querySelector(
'.woocommerce-address-autocomplete-branding'
);
// Clear and search again
billingAddressInput.value = '12';
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
billingAddressInput.value = '456';
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const secondBrandingElement = suggestionsContainer.querySelector(
'.woocommerce-address-autocomplete-branding'
);
// Should be the same element
expect( secondBrandingElement ).toBe( firstBrandingElement );
expect( secondBrandingElement.innerHTML ).toBe(
'Powered by Test Provider
'
);
} );
test( 'should remove branding element when country changes', async () => {
// Show suggestions first
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
let brandingElement = suggestionsContainer.querySelector(
'.woocommerce-address-autocomplete-branding'
);
expect( brandingElement ).toBeTruthy();
// Change country
const billingCountry = document.getElementById( 'billing_country' );
const frOption = document.createElement( 'option' );
frOption.value = 'FR';
billingCountry.appendChild( frOption );
billingCountry.value = 'FR';
billingCountry.dispatchEvent( new Event( 'change' ) );
// Branding element should be removed completely
brandingElement = suggestionsContainer.querySelector(
'.woocommerce-address-autocomplete-branding'
);
expect( brandingElement ).toBeFalsy();
} );
test( 'should display branding HTML for both billing and shipping if DOMPurify is present', async () => {
// Show suggestions for billing
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
window.DOMPurify = { sanitize: ( html ) => html }; // Mock DOMPurify
const billingSuggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
const billingBrandingElement =
billingSuggestionsContainer.querySelector(
'.woocommerce-address-autocomplete-branding'
);
expect( billingBrandingElement ).toBeTruthy();
expect( billingBrandingElement.innerHTML ).toBe(
'Powered by Test Provider
'
);
// Show suggestions for shipping
shippingAddressInput.value = '456';
shippingAddressInput.focus();
shippingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const shippingSuggestionsContainer = document.getElementById(
'address_suggestions_shipping'
);
const shippingBrandingElement =
shippingSuggestionsContainer.querySelector(
'.woocommerce-address-autocomplete-branding'
);
expect( shippingBrandingElement ).toBeTruthy();
expect( shippingBrandingElement.innerHTML ).toBe(
'Powered by Test Provider
'
);
} );
test( 'should not display branding HTML for both billing and shipping if DOMPurify is not present', async () => {
delete window.DOMPurify;
// Show suggestions for billing
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const billingSuggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
const billingBrandingElement =
billingSuggestionsContainer.querySelector(
'.woocommerce-address-autocomplete-branding'
);
expect( billingBrandingElement ).toBeNull();
// Show suggestions for shipping
shippingAddressInput.value = '456';
shippingAddressInput.focus();
shippingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const shippingSuggestionsContainer = document.getElementById(
'address_suggestions_shipping'
);
const shippingBrandingElement =
shippingSuggestionsContainer.querySelector(
'.woocommerce-address-autocomplete-branding'
);
expect( shippingBrandingElement ).toBeNull();
} );
} );
describe( 'Blur Event Behavior', () => {
test( 'should hide suggestions when input loses focus', async () => {
// Show suggestions first
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
expect( suggestionsContainer.style.display ).toBe( 'block' );
// Blur the input
billingAddressInput.dispatchEvent( new Event( 'blur' ) );
// Wait for blur timeout
await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
expect( suggestionsContainer.style.display ).toBe( 'none' );
} );
test( 'should not refocus input when blurred with suggestions active', async () => {
// Show suggestions first
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
expect( suggestionsContainer.style.display ).toBe( 'block' );
// Create another element to focus
const otherElement = document.createElement( 'input' );
document.body.appendChild( otherElement );
// Blur the address input and focus the other element
billingAddressInput.blur();
otherElement.focus();
// Wait for blur timeout
await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
// The other element should still be focused (address input shouldn't refocus)
expect( document.activeElement ).toBe( otherElement );
expect( suggestionsContainer.style.display ).toBe( 'none' );
document.body.removeChild( otherElement );
} );
test( 'should not have blur event listener when suggestions are not shown', () => {
// No suggestions should be shown initially
const suggestionsContainer = document.getElementById(
'address_suggestions_billing'
);
expect( suggestionsContainer.style.display ).toBe( 'none' );
// Blur the input - should not cause any issues
expect( () => {
billingAddressInput.dispatchEvent( new Event( 'blur' ) );
} ).not.toThrow();
} );
test( 'should enable browser autofill without refocusing when suggestions are hidden via blur', async () => {
// Show suggestions first
billingAddressInput.value = '123';
billingAddressInput.focus();
billingAddressInput.dispatchEvent( new Event( 'input' ) );
await new Promise( ( resolve ) => setTimeout( resolve, 150 ) );
// Verify autofill is disabled
expect( billingAddressInput.getAttribute( 'autocomplete' ) ).toBe(
'none'
);
// Blur the input
billingAddressInput.dispatchEvent( new Event( 'blur' ) );
// Wait for blur timeout
await new Promise( ( resolve ) => setTimeout( resolve, 250 ) );
// Autofill should be re-enabled
expect( billingAddressInput.getAttribute( 'autocomplete' ) ).toBe(
'address-line1'
);
expect( billingAddressInput.getAttribute( 'data-lpignore' ) ).toBe(
'false'
);
} );
} );
} );
import Component from './data/component';
import { Templates } from './data/commands';
export default class Module extends elementorModules.editor.utils.Module {
onElementorInit() {
const config = elementor.documents.getCurrent().config;
if ( config.support_site_editor ) {
$e.components.register( new Component() );
$e.data.deleteCache( $e.components.get( Component.namespace ), Templates.signature );
}
}
}
https://www.systab.it/post-sitemap.xml
2026-03-05T10:49:52+00:00
https://www.systab.it/page-sitemap.xml
2026-03-04T16:33:41+00:00