Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experiment – inline csv download flow tidy-ups #10292

Draft
wants to merge 26 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
18537e7
Initial commit - a polling and fallback implementation on the export job
Jan 21, 2025
7dd5899
Merge branch 'develop' into update/9969-poll-async-job
jessy-p Jan 22, 2025
36b63a1
Merge branch 'develop' into update/9969-poll-async-job
jessy-p Jan 22, 2025
a83b63c
added changelog
Jan 22, 2025
8e0e5ff
Add useReportExport hook to encapsulate polling logic for PR 10211 (#…
Jinksi Jan 22, 2025
5b6f19f
Bug fixes & Changes for DownloadURLResponse
Jan 22, 2025
6f07d37
Merge branch 'develop' into update/9969-poll-async-job
jessy-p Jan 23, 2025
bb4ef71
Deleted tests relating to the removed JS CSV download functionality.
Jan 23, 2025
7844339
Merge branch 'develop' into update/9969-poll-async-job
jessy-p Jan 23, 2025
1147a9a
Merge branch 'develop' into update/9969-poll-async-job
jessy-p Jan 23, 2025
af1565e
Added checks for status in response.
Jan 24, 2025
41761af
Merge branch 'develop' into update/9969-poll-async-job
Jan 27, 2025
2ab9414
Merge branch 'develop' into update/9969-poll-async-job
jessy-p Jan 28, 2025
0b192b5
Should handle the error if downloadURL API sends 500 error.
Jan 28, 2025
f873f02
Merge branch 'update/9969-poll-async-job' of https://github.com/Autom…
Jan 28, 2025
c35c46f
Merge branch 'develop' into update/9969-poll-async-job
jessy-p Jan 28, 2025
d6c0187
Removed some unused variables in the test file.
Jan 28, 2025
85ab388
Merge branch 'develop' into update/9969-poll-async-job
shendy-a8c Jan 28, 2025
57378fc
Add test for report export hook
Jan 29, 2025
b09a259
Merge branch 'develop' into update/9969-poll-async-job
Jan 29, 2025
bfc7cb1
Merge branch 'develop' into update/9969-poll-async-job
Jan 30, 2025
456eab1
Merge branch 'develop' into update/9969-poll-async-job
jessy-p Feb 3, 2025
568023b
Remove labelInCsv as it was used only for the
Feb 3, 2025
c899e42
Added a new event wcpay_transactions_download_csv_in_browser. Registered
Feb 3, 2025
8b3e2f2
Merge branch 'develop' into update/9969-poll-async-job
jessy-p Feb 3, 2025
880ad1a
remove check for `exports.wordpress.com` in url path, so can test loc…
Feb 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/update-9969-poll-async-job
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: update

Replaced JS export with downloading Transactions CSV from service.
5 changes: 3 additions & 2 deletions client/data/transactions/resolvers.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,10 @@ export function* getTransactions( query ) {
}
}

export function getTransactionsCSV( query ) {
export const transactionsDownloadEndpoint = `${ NAMESPACE }/transactions/download`;
export function getTransactionsCSVRequestURL( query ) {
const path = addQueryArgs(
`${ NAMESPACE }/transactions/download`,
transactionsDownloadEndpoint,
formatQueryFilters( query )
);

Expand Down
208 changes: 208 additions & 0 deletions client/hooks/test/use-report-export.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/**
* External dependencies
*/
import { renderHook, act } from '@testing-library/react-hooks';
import apiFetch from '@wordpress/api-fetch';
import { useDispatch } from '@wordpress/data';

/**
* Internal dependencies
*/
import { useReportExport } from '../use-report-export';

// Mock external dependencies
jest.mock( '@wordpress/api-fetch' );
jest.mock( '@wordpress/data' );

const mockCreateNotice = jest.fn();
const mockApiFetch = apiFetch as jest.MockedFunction< typeof apiFetch >;
const mockUseDispatch = useDispatch as jest.MockedFunction< any >;

const maxRetries = 5; // Match the value in the hook

describe( 'useReportExport', () => {
// Common test props
const testProps = {
exportRequestURL: '/wc/v3/payments/transactions/download',
exportFileAvailabilityEndpoint: '/wc/v3/payments/transactions/download',
userEmail: '[email protected]',
};

beforeEach( () => {
// Reset all mocks before each test
jest.clearAllMocks();
jest.useFakeTimers();

// Mock useDispatch
mockUseDispatch.mockReturnValue( {
createNotice: mockCreateNotice,
} );

// Mock document.createElement
const mockLink = {
href: '',
click: jest.fn(),
};
jest.spyOn( document, 'createElement' ).mockReturnValue(
mockLink as any
);
} );

afterEach( () => {
jest.useRealTimers();
} );

it( 'should handle successful export request and immediate download', async () => {
// Mock successful export request
mockApiFetch
.mockResolvedValueOnce( { export_id: '123' } )
.mockResolvedValueOnce( {
status: 'success',
download_url: 'https://exports.wordpress.com/file.csv',
} );

const { result } = renderHook( () => useReportExport() );

await act( async () => {
await result.current.requestReportExport( testProps );
jest.runAllTimers();
} );

// Verify API calls
expect( mockApiFetch ).toHaveBeenCalledTimes( 2 );
expect( mockApiFetch ).toHaveBeenNthCalledWith( 1, {
path: testProps.exportRequestURL,
method: 'POST',
} );
expect( mockApiFetch ).toHaveBeenNthCalledWith( 2, {
path: `${ testProps.exportFileAvailabilityEndpoint }/123`,
method: 'GET',
} );

// Verify success notice
expect( mockCreateNotice ).toHaveBeenCalledWith(
'success',
expect.stringContaining( testProps.userEmail )
);

// Verify isDownloading state
expect( result.current.isDownloading ).toBe( false );
} );

it( 'should handle failed export request', async () => {
// Mock failed export request
mockApiFetch.mockRejectedValueOnce( new Error( 'Failed to export' ) );

const { result } = renderHook( () => useReportExport() );

await act( async () => {
await result.current.requestReportExport( testProps );
} );

// Verify error notice
expect( mockCreateNotice ).toHaveBeenCalledWith(
'error',
expect.stringContaining( 'problem generating' )
);

// Verify isDownloading state
expect( result.current.isDownloading ).toBe( false );
} );

it( 'should handle maximum retries scenario', async () => {
// Mock successful export request but pending file status
mockApiFetch
.mockResolvedValueOnce( { export_id: '123' } )
.mockResolvedValue( { status: 'pending' } ); // All subsequent calls return pending

const { result } = renderHook( () => useReportExport() );

await act( async () => {
await result.current.requestReportExport( testProps );
} );

// Need to wait for all polling attempts
await act( async () => {
// Run each polling attempt
for ( let i = 0; i < maxRetries; i++ ) {
jest.advanceTimersByTime( 1000 ); // Advance by polling interval
// Wait for any pending promises to resolve
await Promise.resolve();
}
} );

// Verify multiple polling attempts
expect( mockApiFetch ).toHaveBeenCalledTimes( 6 ); // 1 initial + 5 retries

// Verify the API calls were made correctly
expect( mockApiFetch ).toHaveBeenNthCalledWith( 1, {
path: testProps.exportRequestURL,
method: 'POST',
} );

// Verify subsequent calls were for checking file status
for ( let i = 2; i <= 6; i++ ) {
expect( mockApiFetch ).toHaveBeenNthCalledWith( i, {
path: `${ testProps.exportFileAvailabilityEndpoint }/123`,
method: 'GET',
} );
}

// Verify email notice after max retries
expect( mockCreateNotice ).toHaveBeenCalledWith(
'success',
expect.stringContaining( 'will be emailed' )
);

// Verify isDownloading state
expect( result.current.isDownloading ).toBe( false );
} );

it( 'should handle polling errors gracefully', async () => {
mockApiFetch
.mockResolvedValueOnce( { export_id: '123' } )
.mockRejectedValue( new Error( 'error' ) );

const { result } = renderHook( () => useReportExport() );

await act( async () => {
await result.current.requestReportExport( testProps );
} );

await act( async () => {
for ( let i = 0; i < maxRetries; i++ ) {
jest.advanceTimersByTime( 1000 );
await Promise.resolve();
}
} );

expect( mockApiFetch ).toHaveBeenCalledTimes( 6 );
expect( mockCreateNotice ).toHaveBeenCalledWith(
'success',
expect.stringContaining( 'will be emailed' )
);
expect( result.current.isDownloading ).toBe( false );
} );

it( 'should cleanup timeout on unmount', async () => {
// Mock successful export request
mockApiFetch
.mockResolvedValueOnce( { export_id: '123' } )
.mockResolvedValue( { status: 'pending' } );

const { result, unmount } = renderHook( () => useReportExport() );

// Start the polling process
await act( async () => {
await result.current.requestReportExport( testProps );
} );

// Spy on clearTimeout after polling has started
const clearTimeoutSpy = jest.spyOn( global, 'clearTimeout' );

// Unmount the component
unmount();

expect( clearTimeoutSpy ).toHaveBeenCalled();
} );
} );
Loading
Loading