import { createRoot } from 'react-dom/client'; import { act } from 'react'; import PackagingSection from '../features/order-ship/packaging-section'; import type { Order } from '../types'; ( globalThis as any ).IS_REACT_ACT_ENVIRONMENT = true; const order = { id: 323, shipping: { country: 'GB', postcode: 'YO22 5HS' }, units: { weight: 'kg', dimension: 'cm' }, items: [ { name: 'Mini Pouch', quantity: 1, total: '10.00', dimensions: { length: 10, width: 10, height: 10, weight: 1 }, }, ], } as unknown as Order; function deferred() { let resolve!: () => void; const promise = new Promise< void >( ( r ) => ( resolve = r ) ); return { promise, resolve }; } // Set a React-controlled input's value the way a user typing would. function typeInto( input: HTMLInputElement, value: string ) { const setter = Object.getOwnPropertyDescriptor( HTMLInputElement.prototype, 'value' )!.set!; setter.call( input, value ); input.dispatchEvent( new Event( 'input', { bubbles: true } ) ); } describe( 'PackagingSection', () => { it( 'keeps an edit made while the quotes for the applied parcel are loading', async () => { const quotes = deferred(); const onFetchQuotes = jest.fn( () => quotes.promise ); const onParcelChange = jest.fn(); const container = document.createElement( 'div' ); document.body.appendChild( container ); const root = createRoot( container ); act( () => root.render( ) ); const button = ( text: string ) => Array.from( container.querySelectorAll( 'button' ) ).find( ( b ) => b.textContent?.includes( text ) ) as HTMLButtonElement; const weightInput = () => Array.from( container.querySelectorAll< HTMLInputElement >( 'input[type=number]' ) )[ 3 ]; act( () => button( 'Parcel' ).click() ); act( () => typeInto( weightInput(), '3' ) ); act( () => button( 'Apply' ).click() ); // The merchant keeps editing before the quotes come back. act( () => typeInto( weightInput(), '4' ) ); await act( async () => { quotes.resolve(); await quotes.promise; } ); expect( onParcelChange ).toHaveBeenCalledWith( expect.objectContaining( { weight: 3 } ) ); expect( weightInput().value ).toBe( '4' ); expect( container.textContent ).toContain( 'changes that are not applied' ); act( () => root.unmount() ); container.remove(); } ); it( 'allows one Apply at a time, so an earlier request cannot set the parcel last', async () => { const quotes = deferred(); const onFetchQuotes = jest.fn( () => quotes.promise ); const onParcelChange = jest.fn(); const onApplyingChange = jest.fn(); const container = document.createElement( 'div' ); document.body.appendChild( container ); const root = createRoot( container ); act( () => root.render( ) ); const button = ( text: string ) => Array.from( container.querySelectorAll( 'button' ) ).find( ( b ) => b.textContent?.includes( text ) ) as HTMLButtonElement; const weightInput = () => Array.from( container.querySelectorAll< HTMLInputElement >( 'input[type=number]' ) )[ 3 ]; act( () => button( 'Parcel' ).click() ); act( () => typeInto( weightInput(), '3' ) ); act( () => button( 'Apply' ).click() ); // The page is told, so it can hold booking until the new quotes arrive. expect( onApplyingChange ).toHaveBeenLastCalledWith( true ); // Editing still works while the first request runs, but Apply does not. act( () => typeInto( weightInput(), '4' ) ); expect( button( 'Apply' ).disabled ).toBe( true ); act( () => button( 'Apply' ).click() ); expect( onFetchQuotes ).toHaveBeenCalledTimes( 1 ); await act( async () => { quotes.resolve(); await quotes.promise; } ); expect( button( 'Apply' ).disabled ).toBe( false ); expect( onApplyingChange ).toHaveBeenLastCalledWith( false ); expect( weightInput().value ).toBe( '4' ); act( () => root.unmount() ); container.remove(); } ); } );