/*! 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' ); } ); } ); } ); body.woocommerce-cart .cross-sells { padding-left: 1em; padding-right: 1em; } body.woocommerce-cart .cross-sells h2 { padding-left: 1em; padding-right: 1em; margin: 0 -15px 15px; } body.woocommerce-cart .cross-sells ul.products { display: block; } body.woocommerce-cart .cross-sells ul.products .ast-article-single .astra-shop-thumbnail-wrap { width: 70px; } body.woocommerce-cart .cross-sells ul.products .ast-article-single .astra-shop-thumbnail-wrap .ast-quick-view-text { font-size: 0.8em; } body.woocommerce-cart .cross-sells ul.products .ast-article-single .astra-shop-thumbnail-wrap .onsale, body.woocommerce-cart .cross-sells ul.products .ast-article-single .astra-shop-thumbnail-wrap .ast-onsale-card { top: 0.7em; left: 0; color: var(--ast-global-color-3); background-color: var(--ast-global-color-primary, --ast-global-color-5); width: fit-content; border-radius: 20px; padding: 0.3em 0.8em; font-size: 0.75em; font-weight: normal; line-height: 1em; letter-spacing: normal; box-shadow: 0 4px 4px rgba(0, 0, 0, 0.15); min-height: auto; transform: scale(0.8); } body.woocommerce-cart .cross-sells ul.products .ast-article-single .astra-shop-thumbnail-wrap .ast-onsale-card { top: 0.3em; } body.woocommerce-cart .cross-sells ul.products .ast-article-single .astra-shop-thumbnail-wrap .ast-select-options-trigger { display: none; } body.woocommerce-cart .cross-sells ul.products .ast-article-single .astra-shop-thumbnail-wrap .ast-quick-view-trigger { transform: scale(0.9); bottom: 0; right: 0; top: inherit; } body.woocommerce-cart .cross-sells ul.products .ast-article-single .astra-shop-summary-wrap .woocommerce-loop-product__title { color: var(--ast-global-color-0); font-weight: 500; } body.woocommerce-cart .cross-sells ul.products .ast-article-single .astra-shop-summary-wrap .woocommerce-loop-product__title:hover { color: var(--ast-global-color-3); } body.woocommerce-cart .cross-sells ul.products .ast-article-single .astra-shop-summary-wrap .price { position: absolute; right: 0; top: 1em; width: 100%; max-width: 7.5em; text-align: right; } body.woocommerce-cart .cross-sells ul.products .ast-article-single .astra-shop-summary-wrap > .button, body.woocommerce-cart .cross-sells ul.products .ast-article-single .astra-shop-summary-wrap .ast-quick-view-button { position: absolute; right: 0; bottom: 0.5em; padding: 0.7em 0.8em; width: 9em; font-size: 0.8em; text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } body.woocommerce-cart .cross-sells ul.products .ast-article-single .astra-shop-summary-wrap .ast-quick-view-button { bottom: 3.5em; } body.woocommerce-cart .cross-sells ul.products .ast-article-single:nth-last-child(1) { border-bottom: 0; } body.woocommerce-cart .cross-sells ul.products .ast-article-single:nth-child(1) { margin-top: -1em; } body.woocommerce-cart .woocommerce .cross-sells ul.products .ast-article-single { -js-display: flex; display: flex; flex-direction: row; align-items: flex-start; border-bottom: 1px solid var(--ast-border-color); padding: 1em 0; box-shadow: none; } body.woocommerce-cart .woocommerce .cross-sells ul.products .ast-article-single .astra-shop-summary-wrap { width: calc(100% - 70px); padding-right: 7.5em; padding-left: 1em; } body.woocommerce-cart .woocommerce .cross-sells ul.products .ast-article-single:nth-last-child(1) { border-bottom: 0; }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 ); } } } !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.sbjs=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0) { for (var i = 0; i < p.referrals.length; i++) { if (uri.parse(referer).host.match(new RegExp('^(?:.*\\.)?' + utils.escapeRegexp(p.referrals[i].host) + '$', 'i'))) { __sbjs_source = p.referrals[i].display || p.referrals[i].host; __sbjs_medium = p.referrals[i].medium || terms.referer.referral; return true; } if (i + 1 === p.referrals.length) { __sbjs_source = uri.getHost(referer); return true; } } } else { __sbjs_source = uri.getHost(referer); return true; } } function setFirstAndCurrentExtraData() { cookies.set(data.containers.current_extra, data.pack.extra(p.timezone_offset), lifetime, domain, isolate); if (!cookies.get(data.containers.first_extra)) { cookies.set(data.containers.first_extra, data.pack.extra(p.timezone_offset), lifetime, domain, isolate); } } (function setData() { // Main data cookies.set(data.containers.current, mainData(), lifetime, domain, isolate); if (!cookies.get(data.containers.first)) { cookies.set(data.containers.first, cookies.get(data.containers.current), lifetime, domain, isolate); } // User data var visits, udata; if (!cookies.get(data.containers.udata)) { visits = 1; udata = data.pack.user(visits, p.user_ip); } else { visits = parseInt(cookies.parse(data.containers.udata)[cookies.unsbjs(data.containers.udata)][data.aliases.udata.visits]) || 1; visits = cookies.get(data.containers.session) ? visits : visits + 1; udata = data.pack.user(visits, p.user_ip); } cookies.set(data.containers.udata, udata, lifetime, domain, isolate); // Session var pages_count; if (!cookies.get(data.containers.session)) { pages_count = 1; } else { pages_count = parseInt(cookies.parse(data.containers.session)[cookies.unsbjs(data.containers.session)][data.aliases.session.pages_seen]) || 1; pages_count += 1; } cookies.set(data.containers.session, data.pack.session(pages_count), p.session_length, domain, isolate); // Promocode if (p.promocode && !cookies.get(data.containers.promocode)) { cookies.set(data.containers.promocode, data.pack.promo(p.promocode), lifetime, domain, isolate); } })(); return cookies.parse(data.containers); }; },{"./data":2,"./helpers/cookies":3,"./helpers/uri":4,"./helpers/utils":5,"./migrations":7,"./params":8,"./terms":9}],7:[function(_dereq_,module,exports){ "use strict"; var data = _dereq_('./data'), cookies = _dereq_('./helpers/cookies'); module.exports = { go: function(lifetime, domain, isolate) { var migrate = this.migrations, _with = { l: lifetime, d: domain, i: isolate }; var i; if (!cookies.get(data.containers.first) && !cookies.get(data.service.migrations)) { var mids = []; for (i = 0; i < migrate.length; i++) { mids.push(migrate[i].id); } var advance = ''; for (i = 0; i < mids.length; i++) { advance += mids[i] + '=1'; if (i < mids.length - 1) { advance += data.delimiter; } } cookies.set(data.service.migrations, advance, _with.l, _with.d, _with.i); } else if (!cookies.get(data.service.migrations)) { // We have only one migration for now, so just for (i = 0; i < migrate.length; i++) { migrate[i].go(migrate[i].id, _with); } } }, migrations: [ { id: '1418474375998', version: '1.0.0-beta', go: function(mid, _with) { var success = mid + '=1', fail = mid + '=0'; var safeReplace = function($0, $1, $2) { return ($1 || $2 ? $0 : data.delimiter); }; try { // Switch delimiter and renew cookies var _in = []; for (var prop in data.containers) { if (data.containers.hasOwnProperty(prop)) { _in.push(data.containers[prop]); } } for (var i = 0; i < _in.length; i++) { if (cookies.get(_in[i])) { var buffer = cookies.get(_in[i]).replace(/(\|)?\|(\|)?/g, safeReplace); cookies.destroy(_in[i], _with.d, _with.i); cookies.destroy(_in[i], _with.d, !_with.i); cookies.set(_in[i], buffer, _with.l, _with.d, _with.i); } } // Update `session` if (cookies.get(data.containers.session)) { cookies.set(data.containers.session, data.pack.session(0), _with.l, _with.d, _with.i); } // Yay! cookies.set(data.service.migrations, success, _with.l, _with.d, _with.i); } catch (err) { // Oops cookies.set(data.service.migrations, fail, _with.l, _with.d, _with.i); } } } ] }; },{"./data":2,"./helpers/cookies":3}],8:[function(_dereq_,module,exports){ "use strict"; var terms = _dereq_('./terms'), uri = _dereq_('./helpers/uri'); module.exports = { fetch: function(prefs) { var user = prefs || {}, params = {}; // Set `lifetime of the cookie` in months params.lifetime = this.validate.checkFloat(user.lifetime) || 6; params.lifetime = parseInt(params.lifetime * 30 * 24 * 60); // Set `session length` in minutes params.session_length = this.validate.checkInt(user.session_length) || 30; // Set `timezone offset` in hours params.timezone_offset = this.validate.checkInt(user.timezone_offset); // Enable `base64 encoding` params.base64 = user.base64 || false; // Set `campaign param` for AdWords links params.campaign_param = user.campaign_param || false; // Set `term param` and `content param` for AdWords links params.term_param = user.term_param || false; params.content_param = user.content_param || false; // Set `user ip` params.user_ip = user.user_ip || terms.none; // Set `promocode` if (user.promocode) { params.promocode = {}; params.promocode.min = parseInt(user.promocode.min) || 100000; params.promocode.max = parseInt(user.promocode.max) || 999999; } else { params.promocode = false; } // Set `typein attributes` if (user.typein_attributes && user.typein_attributes.source && user.typein_attributes.medium) { params.typein_attributes = {}; params.typein_attributes.source = user.typein_attributes.source; params.typein_attributes.medium = user.typein_attributes.medium; } else { params.typein_attributes = { source: '(direct)', medium: '(none)' }; } // Set `domain` if (user.domain && this.validate.isString(user.domain)) { params.domain = { host: user.domain, isolate: false }; } else if (user.domain && user.domain.host) { params.domain = user.domain; } else { params.domain = { host: uri.getHost(document.location.hostname), isolate: false }; } // Set `referral sources` params.referrals = []; if (user.referrals && user.referrals.length > 0) { for (var ir = 0; ir < user.referrals.length; ir++) { if (user.referrals[ir].host) { params.referrals.push(user.referrals[ir]); } } } // Set `organic sources` params.organics = []; if (user.organics && user.organics.length > 0) { for (var io = 0; io < user.organics.length; io++) { if (user.organics[io].host && user.organics[io].param) { params.organics.push(user.organics[io]); } } } params.organics.push({ host: 'bing.com', param: 'q', display: 'bing' }); params.organics.push({ host: 'yahoo.com', param: 'p', display: 'yahoo' }); params.organics.push({ host: 'about.com', param: 'q', display: 'about' }); params.organics.push({ host: 'aol.com', param: 'q', display: 'aol' }); params.organics.push({ host: 'ask.com', param: 'q', display: 'ask' }); params.organics.push({ host: 'globososo.com', param: 'q', display: 'globo' }); params.organics.push({ host: 'go.mail.ru', param: 'q', display: 'go.mail.ru' }); params.organics.push({ host: 'rambler.ru', param: 'query', display: 'rambler' }); params.organics.push({ host: 'tut.by', param: 'query', display: 'tut.by' }); params.referrals.push({ host: 't.co', display: 'twitter.com' }); params.referrals.push({ host: 'plus.url.google.com', display: 'plus.google.com' }); return params; }, validate: { checkFloat: function(v) { return v && this.isNumeric(parseFloat(v)) ? parseFloat(v) : false; }, checkInt: function(v) { return v && this.isNumeric(parseInt(v)) ? parseInt(v) : false; }, isNumeric: function(v){ return !isNaN(v); }, isString: function(v) { return Object.prototype.toString.call(v) === '[object String]'; } } }; },{"./helpers/uri":4,"./terms":9}],9:[function(_dereq_,module,exports){ "use strict"; module.exports = { traffic: { utm: 'utm', organic: 'organic', referral: 'referral', typein: 'typein' }, referer: { referral: 'referral', organic: 'organic', social: 'social' }, none: '(none)', oops: '(Houston, we have a problem)' }; },{}]},{},[1])(1) });