274 lines
8.4 KiB
JavaScript
274 lines
8.4 KiB
JavaScript
/**
|
|
* @jest-environment jest-fixed-jsdom
|
|
*/
|
|
|
|
describe( 'createCheckoutPlaceOrderApi', () => {
|
|
let $form;
|
|
let $termsCheckbox;
|
|
let $termsRow;
|
|
let capturedApi;
|
|
|
|
beforeEach( () => {
|
|
capturedApi = null;
|
|
|
|
// used to track whether terms checkbox is checked
|
|
let termsChecked = false;
|
|
|
|
const termsRowClasses = new Set();
|
|
const formInvalidElements = new Set();
|
|
|
|
$termsRow = {
|
|
addClass: jest.fn( ( cls ) => {
|
|
cls.split( ' ' ).forEach( ( c ) => formInvalidElements.add( 'terms-row' ) );
|
|
cls.split( ' ' ).forEach( ( c ) => termsRowClasses.add( c ) );
|
|
return $termsRow;
|
|
} ),
|
|
removeClass: jest.fn( ( cls ) => {
|
|
cls.split( ' ' ).forEach( ( c ) => termsRowClasses.delete( c ) );
|
|
if ( cls.includes( 'woocommerce-invalid' ) ) {
|
|
formInvalidElements.delete( 'terms-row' );
|
|
}
|
|
return $termsRow;
|
|
} ),
|
|
hasClass: jest.fn( ( cls ) => termsRowClasses.has( cls ) ),
|
|
length: 1,
|
|
offset: jest.fn( () => ( { top: 100 } ) ),
|
|
};
|
|
|
|
$termsCheckbox = {
|
|
length: 1,
|
|
is: jest.fn( ( selector ) => {
|
|
if ( selector === ':checked' ) {
|
|
return termsChecked;
|
|
}
|
|
return false;
|
|
} ),
|
|
closest: jest.fn( () => $termsRow ),
|
|
trigger: jest.fn(),
|
|
};
|
|
|
|
// a helper to set the checkbox's state.
|
|
$termsCheckbox.setChecked = ( checked ) => {
|
|
termsChecked = checked;
|
|
};
|
|
|
|
$form = {
|
|
length: 1,
|
|
find: jest.fn( ( selector ) => {
|
|
if ( selector === 'input[name="terms"]:visible' ) {
|
|
return $termsCheckbox;
|
|
}
|
|
if ( selector === '.input-text, select, input:checkbox' ) {
|
|
return { trigger: jest.fn() };
|
|
}
|
|
if ( selector === '.woocommerce-invalid' ) {
|
|
return {
|
|
length: formInvalidElements.size,
|
|
first: jest.fn( () => ( {
|
|
length: formInvalidElements.size > 0 ? 1 : 0,
|
|
offset: jest.fn( () => ( { top: 100 } ) ),
|
|
} ) ),
|
|
};
|
|
}
|
|
if ( selector === '.validate-required:visible' ) {
|
|
return { each: jest.fn() };
|
|
}
|
|
if ( selector === 'input[name="payment_method"]:checked' ) {
|
|
return { val: jest.fn( () => 'test-gateway' ) };
|
|
}
|
|
return { length: 0, trigger: jest.fn() };
|
|
} ),
|
|
trigger: jest.fn(),
|
|
};
|
|
|
|
// Add methods to $form for checkout.js initialization
|
|
$form.on = jest.fn( () => $form );
|
|
$form.attr = jest.fn( () => $form );
|
|
|
|
// Default mock for unhandled selectors - provides all common jQuery methods
|
|
const createDefaultMock = () => {
|
|
const mock = {
|
|
length: 0,
|
|
on: jest.fn( () => mock ),
|
|
off: jest.fn( () => mock ),
|
|
attr: jest.fn( () => mock ),
|
|
find: jest.fn( () => createDefaultMock() ),
|
|
first: jest.fn( () => createDefaultMock() ),
|
|
filter: jest.fn( () => createDefaultMock() ),
|
|
eq: jest.fn( () => createDefaultMock() ),
|
|
trigger: jest.fn( () => mock ),
|
|
val: jest.fn(),
|
|
prop: jest.fn( () => mock ),
|
|
each: jest.fn( () => mock ),
|
|
data: jest.fn(),
|
|
serialize: jest.fn( () => '' ),
|
|
addClass: jest.fn( () => mock ),
|
|
removeClass: jest.fn( () => mock ),
|
|
hasClass: jest.fn( () => false ),
|
|
is: jest.fn( () => false ),
|
|
get: jest.fn( () => [] ),
|
|
text: jest.fn( () => '' ),
|
|
html: jest.fn( () => '' ),
|
|
closest: jest.fn( () => createDefaultMock() ),
|
|
parent: jest.fn( () => createDefaultMock() ),
|
|
parents: jest.fn( () => createDefaultMock() ),
|
|
siblings: jest.fn( () => createDefaultMock() ),
|
|
children: jest.fn( () => createDefaultMock() ),
|
|
append: jest.fn( () => mock ),
|
|
prepend: jest.fn( () => mock ),
|
|
remove: jest.fn( () => mock ),
|
|
empty: jest.fn( () => mock ),
|
|
show: jest.fn( () => mock ),
|
|
hide: jest.fn( () => mock ),
|
|
css: jest.fn( () => mock ),
|
|
slideUp: jest.fn( () => mock ),
|
|
slideDown: jest.fn( () => mock ),
|
|
fadeIn: jest.fn( () => mock ),
|
|
fadeOut: jest.fn( () => mock ),
|
|
offset: jest.fn( () => ( { top: 0, left: 0 } ) ),
|
|
width: jest.fn( () => 0 ),
|
|
height: jest.fn( () => 0 ),
|
|
outerWidth: jest.fn( () => 0 ),
|
|
outerHeight: jest.fn( () => 0 ),
|
|
scrollTop: jest.fn( () => 0 ),
|
|
focus: jest.fn( () => mock ),
|
|
blur: jest.fn( () => mock ),
|
|
block: jest.fn( () => mock ),
|
|
unblock: jest.fn( () => mock ),
|
|
};
|
|
return mock;
|
|
};
|
|
|
|
// Simple event system for document.body to enable event-based API capture
|
|
const bodyEventHandlers = {};
|
|
const mockBody = {
|
|
on: jest.fn( ( event, handler ) => {
|
|
if ( ! bodyEventHandlers[ event ] ) {
|
|
bodyEventHandlers[ event ] = [];
|
|
}
|
|
bodyEventHandlers[ event ].push( handler );
|
|
return mockBody;
|
|
} ),
|
|
trigger: jest.fn( ( event, args ) => {
|
|
const handlers = bodyEventHandlers[ event ] || [];
|
|
handlers.forEach( ( handler ) => handler( {}, ...( args || [] ) ) );
|
|
return mockBody;
|
|
} ),
|
|
hasClass: jest.fn( () => false ),
|
|
};
|
|
|
|
// Mock jQuery - needs to handle document ready pattern: jQuery(function($) { ... })
|
|
const jQueryMock = jest.fn( ( selectorOrCallback ) => {
|
|
// Handle document ready: jQuery(function($) { ... })
|
|
if ( typeof selectorOrCallback === 'function' ) {
|
|
// Execute immediately with jQuery mock as argument
|
|
selectorOrCallback( jQueryMock );
|
|
return jQueryMock;
|
|
}
|
|
if ( selectorOrCallback === 'form.checkout' ) {
|
|
return $form;
|
|
}
|
|
if ( selectorOrCallback === '#order_review' ) {
|
|
return { length: 0, on: jest.fn(), attr: jest.fn(), find: jest.fn( () => ( { length: 0, val: jest.fn() } ) ) };
|
|
}
|
|
if ( selectorOrCallback === 'html, body' ) {
|
|
return { animate: jest.fn() };
|
|
}
|
|
if ( selectorOrCallback === document.body ) {
|
|
return mockBody;
|
|
}
|
|
// Return a default mock for any other selector
|
|
return createDefaultMock();
|
|
} );
|
|
jQueryMock.blockUI = { defaults: { overlayCSS: {} } };
|
|
|
|
global.window.jQuery = jQueryMock;
|
|
global.window.$ = jQueryMock;
|
|
global.jQuery = jQueryMock;
|
|
global.$ = jQueryMock;
|
|
|
|
global.window.wc_checkout_params = {
|
|
gateways_with_custom_place_order_button: [ 'test-gateway' ],
|
|
};
|
|
|
|
global.window.wc = {
|
|
customPlaceOrderButton: {
|
|
__getForm: jest.fn( () => $form ),
|
|
__maybeShow: jest.fn( ( gatewayId, api ) => {
|
|
capturedApi = api;
|
|
} ),
|
|
__maybeHideDefaultButtonOnInit: jest.fn(),
|
|
__cleanup: jest.fn(),
|
|
},
|
|
};
|
|
|
|
// requiring checkout.js - this will execute the jQuery wrapper
|
|
jest.resetModules();
|
|
require( '../checkout' );
|
|
|
|
// Trigger the event to capture the API via __maybeShow
|
|
// This simulates a gateway registering after page load
|
|
mockBody.trigger( 'wc_custom_place_order_button_registered', [ 'test-gateway' ] );
|
|
} );
|
|
|
|
afterEach( () => {
|
|
jest.clearAllMocks();
|
|
} );
|
|
|
|
describe( 'Terms checkbox validation', () => {
|
|
test( 'should return hasError: true when terms checkbox is not checked', async () => {
|
|
$termsCheckbox.setChecked( false );
|
|
|
|
const result = await capturedApi.validate();
|
|
|
|
expect( result.hasError ).toBe( true );
|
|
expect( $termsRow.addClass ).toHaveBeenCalledWith( 'woocommerce-invalid' );
|
|
} );
|
|
|
|
test( 'should return hasError: false when terms checkbox is checked', async () => {
|
|
$termsCheckbox.setChecked( true );
|
|
|
|
const result = await capturedApi.validate();
|
|
|
|
expect( result.hasError ).toBe( false );
|
|
} );
|
|
|
|
test( 'should clear stale invalid state before re-validating terms', async () => {
|
|
// First validation: terms not checked
|
|
$termsCheckbox.setChecked( false );
|
|
await capturedApi.validate();
|
|
|
|
expect( $termsRow.addClass ).toHaveBeenCalledWith( 'woocommerce-invalid' );
|
|
|
|
// clearing the mock history so the expectations are clearer.
|
|
$termsRow.removeClass.mockClear();
|
|
$termsRow.addClass.mockClear();
|
|
|
|
// Second validation: marking the terms as checked
|
|
$termsCheckbox.setChecked( true );
|
|
const result = await capturedApi.validate();
|
|
|
|
// Should have cleared the invalid state first
|
|
expect( $termsRow.removeClass ).toHaveBeenCalledWith( 'woocommerce-invalid' );
|
|
// Should NOT have re-added the invalid class
|
|
expect( $termsRow.addClass ).not.toHaveBeenCalledWith( 'woocommerce-invalid' );
|
|
// Should pass validation
|
|
expect( result.hasError ).toBe( false );
|
|
} );
|
|
|
|
test( 'should allow submission after checking terms following a failed validation', async () => {
|
|
// First attempt: terms not checked - should fail
|
|
$termsCheckbox.setChecked( false );
|
|
const firstResult = await capturedApi.validate();
|
|
expect( firstResult.hasError ).toBe( true );
|
|
|
|
// pretending the user checked the terms checkbox
|
|
$termsCheckbox.setChecked( true );
|
|
|
|
// Second attempt: should pass on first try (not require double-click)
|
|
const secondResult = await capturedApi.validate();
|
|
expect( secondResult.hasError ).toBe( false );
|
|
} );
|
|
} );
|
|
} );
|