­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ /** * @jest-environment jest-fixed-jsdom */ describe( 'Custom Place Order Button API', () => { let jQueryMock; let $form; let $placeOrderButton; beforeEach( () => { // Resetting the window object delete global.window.wc; // creating some mocked DOM elements $form = { length: 1, first: jest.fn( () => $form ), find: jest.fn( () => ( { length: 1, val: jest.fn( () => 'test-gateway' ), after: jest.fn(), } ) ), addClass: jest.fn( () => $form ), removeClass: jest.fn( () => $form ), }; $placeOrderButton = { length: 1, after: jest.fn(), }; jQueryMock = jest.fn( ( selector ) => { if ( selector === 'form.checkout' ) { return { length: 1, first: jest.fn( () => $form ) }; } if ( selector === '#order_review' ) { return { length: 0 }; } if ( selector === '#add_payment_method' ) { return { length: 0 }; } if ( typeof selector === 'string' && selector.includes( 'div' ) ) { return { length: 1, get: jest.fn( () => document.createElement( 'div' ) ), empty: jest.fn(), remove: jest.fn(), append: jest.fn(), }; } return { length: 0 }; } ); jQueryMock.contains = jest.fn( () => false ); global.window.jQuery = jQueryMock; global.window.$ = jQueryMock; global.window.wc_checkout_params = { gateways_with_custom_place_order_button: [ 'test-gateway' ], }; // mocking the event triggering on document.body jQueryMock.fn = {}; const mockBody = { trigger: jest.fn(), }; jQueryMock.mockImplementation( ( selector ) => { if ( selector === document.body ) { return mockBody; } if ( selector === 'form.checkout' ) { return { length: 1, first: jest.fn( () => $form ) }; } if ( selector === '#order_review' ) { return { length: 0 }; } if ( selector === '#add_payment_method' ) { return { length: 0 }; } return { length: 0 }; } ); // using a fresh instance on each test jest.resetModules(); require( '../utils/custom-place-order-button' ); } ); afterEach( () => { jest.clearAllMocks(); } ); describe( 'Base tests', () => { test( 'should expose the API', () => { expect( window.wc ).toBeDefined(); expect( window.wc.customPlaceOrderButton ).toBeDefined(); expect( typeof window.wc.customPlaceOrderButton.register ).toBe( 'function' ); expect( typeof window.wc.customPlaceOrderButton.__maybeShow ).toBe( 'function' ); expect( typeof window.wc.customPlaceOrderButton.__maybeHideDefaultButtonOnInit ).toBe( 'function' ); expect( typeof window.wc.customPlaceOrderButton.__cleanup ).toBe( 'function' ); expect( typeof window.wc.customPlaceOrderButton.__getForm ).toBe( 'function' ); } ); test( 'should reject registration without proper configuration', () => { const consoleSpy = jest .spyOn( console, 'error' ) .mockImplementation( () => {} ); window.wc.customPlaceOrderButton.register( 'test-gateway', { cleanup: jest.fn(), } ); expect( consoleSpy ).toHaveBeenLastCalledWith( 'wc.customPlaceOrderButton.register: render must be a function' ); window.wc.customPlaceOrderButton.register( 'test-gateway', { render: jest.fn(), } ); expect( consoleSpy ).toHaveBeenLastCalledWith( 'wc.customPlaceOrderButton.register: cleanup must be a function' ); window.wc.customPlaceOrderButton.register( null, { render: jest.fn(), cleanup: jest.fn(), } ); expect( consoleSpy ).toHaveBeenLastCalledWith( 'wc.customPlaceOrderButton.register: gatewayId must be a non-empty string' ); window.wc.customPlaceOrderButton.register( '', { render: jest.fn(), cleanup: jest.fn(), } ); expect( consoleSpy ).toHaveBeenLastCalledWith( 'wc.customPlaceOrderButton.register: gatewayId must be a non-empty string' ); window.wc.customPlaceOrderButton.register( 'test-gateway', null ); expect( consoleSpy ).toHaveBeenLastCalledWith( 'wc.customPlaceOrderButton.register: config must be an object' ); window.wc.customPlaceOrderButton.register( 'test-gateway', undefined ); expect( consoleSpy ).toHaveBeenLastCalledWith( 'wc.customPlaceOrderButton.register: config must be an object' ); window.wc.customPlaceOrderButton.register( 'test-gateway', 'not-an-object' ); expect( consoleSpy ).toHaveBeenLastCalledWith( 'wc.customPlaceOrderButton.register: config must be an object' ); consoleSpy.mockRestore(); } ); test( 'should inject critical CSS on load', () => { const styleElement = document.getElementById( 'wc-custom-place-order-button-styles' ); expect( styleElement ).toBeTruthy(); expect( styleElement.textContent ).toContain( '.has-custom-place-order-button #place_order' ); expect( styleElement.textContent ).toContain( 'display: none' ); } ); test( 'should not inject duplicate styles', () => { // Re-require the module jest.resetModules(); require( '../utils/custom-place-order-button' ); const styleElements = document.querySelectorAll( '#wc-custom-place-order-button-styles' ); expect( styleElements.length ).toBe( 1 ); } ); } ); describe( 'getGatewaysWithCustomButton', () => { test( 'should hide default button for gateway in wc_checkout_params list', () => { // Gateway 'test-gateway' is in the server list, so maybeHideDefaultButtonOnInit // should add the class to hide the default button window.wc.customPlaceOrderButton.__maybeHideDefaultButtonOnInit( 'test-gateway' ); expect( $form.addClass ).toHaveBeenCalledWith( 'has-custom-place-order-button' ); } ); test( 'should not hide default button for gateway not in list', () => { // Gateway 'unknown-gateway' is NOT in the server list window.wc.customPlaceOrderButton.__maybeHideDefaultButtonOnInit( 'unknown-gateway' ); expect( $form.addClass ).not.toHaveBeenCalled(); } ); test( 'should not hide default button when wc_checkout_params is undefined', () => { delete global.window.wc_checkout_params; delete global.window.wc_add_payment_method_params; jest.resetModules(); require( '../utils/custom-place-order-button' ); window.wc.customPlaceOrderButton.__maybeHideDefaultButtonOnInit( 'test-gateway' ); expect( $form.addClass ).not.toHaveBeenCalled(); } ); test( 'should use wc_add_payment_method_params as fallback', () => { delete global.window.wc_checkout_params; global.window.wc_add_payment_method_params = { gateways_with_custom_place_order_button: [ 'add-method-gateway' ], }; jest.resetModules(); require( '../utils/custom-place-order-button' ); window.wc.customPlaceOrderButton.__maybeHideDefaultButtonOnInit( 'add-method-gateway' ); expect( $form.addClass ).toHaveBeenCalledWith( 'has-custom-place-order-button' ); } ); test( 'should prefer wc_checkout_params over wc_add_payment_method_params', () => { global.window.wc_checkout_params = { gateways_with_custom_place_order_button: [ 'checkout-gateway' ], }; global.window.wc_add_payment_method_params = { gateways_with_custom_place_order_button: [ 'add-method-gateway' ], }; jest.resetModules(); require( '../utils/custom-place-order-button' ); window.wc.customPlaceOrderButton.__maybeHideDefaultButtonOnInit( 'checkout-gateway' ); expect( $form.addClass ).toHaveBeenCalledWith( 'has-custom-place-order-button' ); $form.addClass.mockClear(); window.wc.customPlaceOrderButton.__maybeHideDefaultButtonOnInit( 'add-method-gateway' ); expect( $form.addClass ).not.toHaveBeenCalled(); } ); } ); describe( 'Gateway switching behavior', () => { let $form; let selectedGateway; let mockContainer; let mockApi; beforeEach( () => { delete global.window.wc; selectedGateway = 'gateway-a'; mockApi = { validate: jest.fn(), submit: jest.fn() }; $form = { length: 1, first: jest.fn( function () { return this; } ), find: jest.fn( ( selector ) => { if ( selector === 'input[name="payment_method"]:checked' ) { return { length: 1, val: jest.fn( () => selectedGateway ), }; } if ( selector === '#place_order' ) { return { length: 1, after: jest.fn(), }; } return { length: 0 }; } ), addClass: jest.fn( function () { return this; } ), removeClass: jest.fn( function () { return this; } ), }; mockContainer = { length: 1, get: jest.fn( () => document.createElement( 'div' ) ), empty: jest.fn(), remove: jest.fn(), append: jest.fn(), }; const mockBody = { trigger: jest.fn(), }; global.window.jQuery = jest.fn( ( selector ) => { if ( selector === document.body ) { return mockBody; } if ( selector === 'form.checkout' ) { return { length: 1, first: jest.fn( () => $form ) }; } if ( selector === '#order_review' ) { return { length: 0 }; } if ( selector === '#add_payment_method' ) { return { length: 0 }; } if ( typeof selector === 'string' && selector.includes( 'div' ) ) { return mockContainer; } return { length: 0 }; } ); global.window.jQuery.fn = {}; global.window.jQuery.contains = jest.fn( () => true ); global.window.$ = global.window.jQuery; global.window.wc_checkout_params = { gateways_with_custom_place_order_button: [ 'gateway-a', 'gateway-b' ], }; jest.resetModules(); require( '../utils/custom-place-order-button' ); } ); afterEach( () => { jest.clearAllMocks(); } ); test( 'should call cleanup when switching between two gateways with custom buttons', () => { const renderA = jest.fn(); const cleanupA = jest.fn(); const renderB = jest.fn(); const cleanupB = jest.fn(); window.wc.customPlaceOrderButton.register( 'gateway-a', { render: renderA, cleanup: cleanupA, } ); window.wc.customPlaceOrderButton.register( 'gateway-b', { render: renderB, cleanup: cleanupB, } ); // Simulating to select `gateway-a` selectedGateway = 'gateway-a'; window.wc.customPlaceOrderButton.__maybeShow( selectedGateway, mockApi ); expect( renderA ).toHaveBeenCalledTimes( 1 ); expect( cleanupA ).not.toHaveBeenCalled(); expect( $form.addClass ).toHaveBeenCalledWith( 'has-custom-place-order-button' ); // Simulating to switch to `gateway-b` selectedGateway = 'gateway-b'; window.wc.customPlaceOrderButton.__maybeShow( selectedGateway, mockApi ); expect( cleanupA ).toHaveBeenCalledTimes( 1 ); expect( renderB ).toHaveBeenCalledTimes( 1 ); expect( cleanupB ).not.toHaveBeenCalled(); } ); test( 'should call cleanup when switching from custom button gateway to regular gateway', () => { const renderA = jest.fn(); const cleanupA = jest.fn(); window.wc.customPlaceOrderButton.register( 'gateway-a', { render: renderA, cleanup: cleanupA, } ); // Simulating to selecting `gateway-a` (which has a custom button) selectedGateway = 'gateway-a'; window.wc.customPlaceOrderButton.__maybeShow( selectedGateway, mockApi ); expect( renderA ).toHaveBeenCalledTimes( 1 ); expect( $form.addClass ).toHaveBeenCalledWith( 'has-custom-place-order-button' ); // Reset mocks to track new calls $form.addClass.mockClear(); $form.removeClass.mockClear(); // Simulating to switch to `no-custom-button-gateway` selectedGateway = 'no-custom-button-gateway'; window.wc.customPlaceOrderButton.__maybeShow( selectedGateway, mockApi ); expect( cleanupA ).toHaveBeenCalledTimes( 1 ); expect( $form.removeClass ).toHaveBeenCalledWith( 'has-custom-place-order-button' ); } ); test( 'should show custom button when switching from regular gateway to custom button gateway', () => { const renderA = jest.fn(); const cleanupA = jest.fn(); window.wc.customPlaceOrderButton.register( 'gateway-a', { render: renderA, cleanup: cleanupA, } ); // Starting with `no-custom-button-gateway` selectedGateway = 'no-custom-button-gateway'; window.wc.customPlaceOrderButton.__maybeShow( selectedGateway, mockApi ); expect( renderA ).not.toHaveBeenCalled(); expect( $form.addClass ).not.toHaveBeenCalledWith( 'has-custom-place-order-button' ); // Simulating to switch to `gateway-a` (which has custom button) selectedGateway = 'gateway-a'; window.wc.customPlaceOrderButton.__maybeShow( selectedGateway, mockApi ); expect( renderA ).toHaveBeenCalledTimes( 1 ); expect( $form.addClass ).toHaveBeenCalledWith( 'has-custom-place-order-button' ); } ); } ); } ); describe( 'getForm helper', () => { beforeEach( () => { delete global.window.wc; // Default mock - no forms found global.window.jQuery = jest.fn( ( ) => { return { length: 0 }; } ); global.window.wc_checkout_params = { gateways_with_custom_place_order_button: [], }; jest.resetModules(); require( '../utils/custom-place-order-button' ); } ); test( 'should return form.checkout if present', () => { const mockForm = { length: 1, first: jest.fn( () => mockForm ) }; global.window.jQuery = jest.fn( ( selector ) => { if ( selector === 'form.checkout' ) { return mockForm; } return { length: 0 }; } ); jest.resetModules(); require( '../utils/custom-place-order-button' ); const form = window.wc.customPlaceOrderButton.__getForm(); expect( form ).toBe( mockForm ); } ); test( 'should return #order_review if form.checkout not present', () => { const mockOrderReview = { length: 1, first: jest.fn( () => mockOrderReview ) }; global.window.jQuery = jest.fn( ( selector ) => { if ( selector === 'form.checkout' ) { return { length: 0 }; } if ( selector === '#order_review' ) { return mockOrderReview; } return { length: 0 }; } ); jest.resetModules(); require( '../utils/custom-place-order-button' ); const form = window.wc.customPlaceOrderButton.__getForm(); expect( form ).toBe( mockOrderReview ); } ); test( 'should return #add_payment_method as last resort', () => { const mockAddPaymentMethod = { length: 1, first: jest.fn( () => mockAddPaymentMethod ), }; global.window.jQuery = jest.fn( ( selector ) => { if ( selector === 'form.checkout' ) { return { length: 0 }; } if ( selector === '#order_review' ) { return { length: 0 }; } if ( selector === '#add_payment_method' ) { return mockAddPaymentMethod; } return { length: 0 }; } ); jest.resetModules(); require( '../utils/custom-place-order-button' ); const form = window.wc.customPlaceOrderButton.__getForm(); expect( form ).toBe( mockAddPaymentMethod ); } ); test( 'should return empty jQuery object if no form found', () => { const emptyJQuery = { length: 0 }; global.window.jQuery = jest.fn( ( selector ) => { if ( selector === 'form.checkout' ) { return { length: 0 }; } if ( selector === '#order_review' ) { return { length: 0 }; } if ( selector === '#add_payment_method' ) { return { length: 0 }; } if ( Array.isArray( selector ) && selector.length === 0 ) { return emptyJQuery; } return { length: 0 }; } ); jest.resetModules(); require( '../utils/custom-place-order-button' ); const form = window.wc.customPlaceOrderButton.__getForm(); expect( form.length ).toBe( 0 ); } ); } );