Files
2026-04-28 15:13:50 +02:00

538 lines
15 KiB
JavaScript

/**
* @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 );
} );
} );