diff --git a/src/components/content-tab-sync.tsx b/src/components/content-tab-sync.tsx
index 36f1945d..8a451a58 100644
--- a/src/components/content-tab-sync.tsx
+++ b/src/components/content-tab-sync.tsx
@@ -2,10 +2,10 @@ import { check, Icon } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import { PropsWithChildren, useState } from 'react';
import { CLIENT_ID, PROTOCOL_PREFIX, SCOPES, WP_AUTHORIZE_ENDPOINT } from '../constants';
+import { useSyncSites } from '../hooks/sync-sites';
import { useAuth } from '../hooks/use-auth';
import { SyncSite } from '../hooks/use-fetch-wpcom-sites';
import { useOffline } from '../hooks/use-offline';
-import { useSiteSyncManagement } from '../hooks/use-site-sync-management';
import { cx } from '../lib/cx';
import { getIpcApi } from '../lib/get-ipc-api';
import { ArrowIcon } from './arrow-icon';
@@ -162,8 +162,7 @@ function NoAuthSyncTab() {
export function ContentTabSync( { selectedSite }: { selectedSite: SiteDetails } ) {
const { __ } = useI18n();
- const { connectedSites, connectSite, disconnectSite, syncSites, isFetching } =
- useSiteSyncManagement();
+ const { connectedSites, connectSite, disconnectSite, syncSites, isFetching } = useSyncSites();
const [ isSyncSitesSelectorOpen, setIsSyncSitesSelectorOpen ] = useState( false );
const { isAuthenticated } = useAuth();
if ( ! isAuthenticated ) {
diff --git a/src/components/tests/content-tab-sync.test.tsx b/src/components/tests/content-tab-sync.test.tsx
index 9ccb33f0..d469cb46 100644
--- a/src/components/tests/content-tab-sync.test.tsx
+++ b/src/components/tests/content-tab-sync.test.tsx
@@ -1,14 +1,16 @@
// To run tests, execute `npm run test -- src/components/tests/content-tab-sync.test.tsx` from the root directory
import { render, screen, fireEvent } from '@testing-library/react';
-import { SyncSitesProvider } from '../../hooks/sync-sites/sync-sites-context';
+import { SyncSitesProvider, useSyncSites } from '../../hooks/sync-sites';
import { useAuth } from '../../hooks/use-auth';
-import { useSiteSyncManagement } from '../../hooks/use-site-sync-management';
import { getIpcApi } from '../../lib/get-ipc-api';
import { ContentTabSync } from '../content-tab-sync';
jest.mock( '../../hooks/use-auth' );
jest.mock( '../../lib/get-ipc-api' );
-jest.mock( '../../hooks/use-site-sync-management' );
+jest.mock( '../../hooks/sync-sites/sync-sites-context', () => ( {
+ ...jest.requireActual( '../../hooks/sync-sites/sync-sites-context' ),
+ useSyncSites: jest.fn(),
+} ) );
const selectedSite: SiteDetails = {
name: 'Test Site',
@@ -29,9 +31,13 @@ describe( 'ContentTabSync', () => {
generateProposedSitePath: jest.fn(),
showMessageBox: jest.fn(),
} );
- ( useSiteSyncManagement as jest.Mock ).mockReturnValue( {
+ ( useSyncSites as jest.Mock ).mockReturnValue( {
connectedSites: [],
syncSites: [],
+ pullSite: jest.fn(),
+ pullStates: {},
+ isAnySitePulling: false,
+ getPullState: jest.fn(),
} );
} );
@@ -43,13 +49,13 @@ describe( 'ContentTabSync', () => {
renderWithProvider( );
expect( screen.getByText( 'Sync with' ) ).toBeInTheDocument();
- const loginButton = screen.getByRole( 'button', { name: 'Log in to WordPress.com ↗' } );
+ const loginButton = screen.getByRole( 'button', { name: /Log in to WordPress.com/i } );
expect( loginButton ).toBeInTheDocument();
fireEvent.click( loginButton );
expect( useAuth().authenticate ).toHaveBeenCalled();
- const freeAccountButton = screen.getByRole( 'button', { name: 'Create a free account ↗' } );
+ const freeAccountButton = screen.getByRole( 'button', { name: /Create a free account/i } );
expect( freeAccountButton ).toBeInTheDocument();
fireEvent.click( freeAccountButton );
@@ -59,7 +65,7 @@ describe( 'ContentTabSync', () => {
it( 'displays create new site button to authenticated user', () => {
( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true, authenticate: jest.fn() } );
renderWithProvider( );
- const createSiteButton = screen.getByRole( 'button', { name: 'Create new site ↗' } );
+ const createSiteButton = screen.getByRole( 'button', { name: /Create new site/i } );
fireEvent.click( createSiteButton );
expect( screen.getByText( 'Sync with' ) ).toBeInTheDocument();
@@ -70,7 +76,7 @@ describe( 'ContentTabSync', () => {
it( 'displays connect site button to authenticated user', () => {
( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true, authenticate: jest.fn() } );
renderWithProvider( );
- const connectSiteButton = screen.getByRole( 'button', { name: 'Connect site' } );
+ const connectSiteButton = screen.getByRole( 'button', { name: /Connect site/i } );
expect( connectSiteButton ).toBeInTheDocument();
} );
@@ -78,7 +84,7 @@ describe( 'ContentTabSync', () => {
it( 'opens the site selector modal to connect a site authenticated user', () => {
( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true, authenticate: jest.fn() } );
renderWithProvider( );
- const connectSiteButton = screen.getByRole( 'button', { name: 'Connect site' } );
+ const connectSiteButton = screen.getByRole( 'button', { name: /Connect site/i } );
fireEvent.click( connectSiteButton );
expect( screen.getByText( 'Connect a WordPress.com site' ) ).toBeInTheDocument();
} );
@@ -93,26 +99,21 @@ describe( 'ContentTabSync', () => {
syncSupport: 'syncable',
};
( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true, authenticate: jest.fn() } );
- ( useSiteSyncManagement as jest.Mock ).mockReturnValue( {
+ ( useSyncSites as jest.Mock ).mockReturnValue( {
connectedSites: [ fakeSyncSite ],
syncSites: [ fakeSyncSite ],
+ pullSite: jest.fn(),
+ pullStates: {},
+ isAnySitePulling: false,
+ getPullState: jest.fn(),
} );
renderWithProvider( );
- const title = screen.getByText( 'My simple business site that needs a transfer' );
- expect( title ).toBeInTheDocument();
-
- const disconnectButton = screen.getByRole( 'button', { name: 'Disconnect' } );
- expect( disconnectButton ).toBeInTheDocument();
-
- const pullButton = screen.getByRole( 'button', { name: 'Pull' } );
- expect( pullButton ).toBeInTheDocument();
-
- const pushButton = screen.getByRole( 'button', { name: 'Push' } );
- expect( pushButton ).toBeInTheDocument();
-
- const productionText = screen.getByText( 'Production' );
- expect( productionText ).toBeInTheDocument();
+ expect( screen.getByText( fakeSyncSite.name ) ).toBeInTheDocument();
+ expect( screen.getByRole( 'button', { name: /Disconnect/i } ) ).toBeInTheDocument();
+ expect( screen.getByRole( 'button', { name: /Pull/i } ) ).toBeInTheDocument();
+ expect( screen.getByRole( 'button', { name: /Push/i } ) ).toBeInTheDocument();
+ expect( screen.getByText( 'Production' ) ).toBeInTheDocument();
} );
it( 'opens URL for connected sites', async () => {
@@ -125,19 +126,21 @@ describe( 'ContentTabSync', () => {
syncSupport: 'syncable',
};
( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true, authenticate: jest.fn() } );
- ( useSiteSyncManagement as jest.Mock ).mockReturnValue( {
+ ( useSyncSites as jest.Mock ).mockReturnValue( {
connectedSites: [ fakeSyncSite ],
syncSites: [ fakeSyncSite ],
+ pullSite: jest.fn(),
+ pullStates: {},
+ isAnySitePulling: false,
+ getPullState: jest.fn(),
} );
renderWithProvider( );
- const urlButton = screen.getByRole( 'button', {
- name: 'https:/developer.wordpress.com/studio/ ↗',
- } );
+ const urlButton = screen.getByRole( 'button', { name: new RegExp( fakeSyncSite.url, 'i' ) } );
expect( urlButton ).toBeInTheDocument();
fireEvent.click( urlButton );
- expect( getIpcApi().openURL ).toHaveBeenCalledWith( 'https:/developer.wordpress.com/studio/' );
+ expect( getIpcApi().openURL ).toHaveBeenCalledWith( fakeSyncSite.url );
} );
it( 'displays both production and staging sites when a production site is connected', async () => {
@@ -159,35 +162,31 @@ describe( 'ContentTabSync', () => {
};
( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true, authenticate: jest.fn() } );
- ( useSiteSyncManagement as jest.Mock ).mockReturnValue( {
+ ( useSyncSites as jest.Mock ).mockReturnValue( {
connectedSites: [ fakeProductionSite, fakeStagingSite ],
syncSites: [ fakeProductionSite ],
+ pullSite: jest.fn(),
+ pullStates: {},
+ isAnySitePulling: false,
+ getPullState: jest.fn(),
} );
renderWithProvider( );
- // Check for production site
- const productionTitle = screen.getByText( 'My simple business site' );
- expect( productionTitle ).toBeInTheDocument();
- const productionText = screen.getByText( 'Production' );
- expect( productionText ).toBeInTheDocument();
+ expect( screen.getByText( fakeProductionSite.name ) ).toBeInTheDocument();
+ expect( screen.getByText( 'Production' ) ).toBeInTheDocument();
- // Check for staging site where title is not displayed
- const stagingTitle = screen.queryByText( 'Staging: My simple business site' );
- expect( stagingTitle ).not.toBeInTheDocument();
- const stagingText = screen.getByText( 'Staging' );
- expect( stagingText ).toBeInTheDocument();
+ expect( screen.queryByText( fakeStagingSite.name ) ).not.toBeInTheDocument();
+ expect( screen.getByText( 'Staging' ) ).toBeInTheDocument();
- // Check for buttons on both sites, with only one disconnect button
- const disconnectButtons = screen.getAllByRole( 'button', { name: 'Disconnect' } );
+ const disconnectButtons = screen.getAllByRole( 'button', { name: /Disconnect/i } );
expect( disconnectButtons ).toHaveLength( 1 );
- const pullButtons = screen.getAllByRole( 'button', { name: 'Pull' } );
+ const pullButtons = screen.getAllByRole( 'button', { name: /Pull/i } );
expect( pullButtons ).toHaveLength( 2 );
- const pushButtons = screen.getAllByRole( 'button', { name: 'Push' } );
+ const pushButtons = screen.getAllByRole( 'button', { name: /Push/i } );
expect( pushButtons ).toHaveLength( 2 );
- // Check for URLs
const productionUrl = screen.getAllByRole( 'button', {
name: 'https://developer.wordpress.com/studio/ ↗',
} );
diff --git a/src/components/tests/site-content-tabs.test.tsx b/src/components/tests/site-content-tabs.test.tsx
index 9ee62fe3..9650ab78 100644
--- a/src/components/tests/site-content-tabs.test.tsx
+++ b/src/components/tests/site-content-tabs.test.tsx
@@ -29,6 +29,12 @@ jest.mock( '../../lib/app-globals', () => ( {
...jest.requireActual( '../../lib/app-globals' ),
getAppGlobals: jest.fn().mockReturnValue( { locale: ' en' } ),
} ) );
+jest.mock( '../../lib/get-ipc-api', () => ( {
+ ...jest.requireActual( '../../lib/get-ipc-api' ),
+ getIpcApi: jest.fn().mockReturnValue( {
+ getConnectedWpcomSites: jest.fn().mockResolvedValue( [] ),
+ } ),
+} ) );
( useFeatureFlags as jest.Mock ).mockReturnValue( {} );
diff --git a/src/hooks/sync-sites/sync-sites-context.tsx b/src/hooks/sync-sites/sync-sites-context.tsx
index 0b4d1465..9c49e430 100644
--- a/src/hooks/sync-sites/sync-sites-context.tsx
+++ b/src/hooks/sync-sites/sync-sites-context.tsx
@@ -1,7 +1,10 @@
import React, { createContext, useContext, useState } from 'react';
+import { SyncSite } from '../use-fetch-wpcom-sites';
+import { useSiteSyncManagement } from './use-site-sync-management';
import { useSyncPull } from './use-sync-pull';
-type SyncSitesContextType = ReturnType< typeof useSyncPull >;
+type SyncSitesContextType = ReturnType< typeof useSyncPull > &
+ ReturnType< typeof useSiteSyncManagement >;
const SyncSitesContext = createContext< SyncSitesContextType | undefined >( undefined );
@@ -14,6 +17,10 @@ export function SyncSitesProvider( { children }: { children: React.ReactNode } )
}
);
+ const [ connectedSites, setConnectedSites ] = useState< SyncSite[] >( [] );
+ const { loadConnectedSites, connectSite, disconnectSite, syncSites, isFetching } =
+ useSiteSyncManagement( { connectedSites, setConnectedSites } );
+
return (
diff --git a/src/hooks/use-site-sync-management.ts b/src/hooks/sync-sites/use-site-sync-management.ts
similarity index 80%
rename from src/hooks/use-site-sync-management.ts
rename to src/hooks/sync-sites/use-site-sync-management.ts
index e51c4860..3ed68ab4 100644
--- a/src/hooks/use-site-sync-management.ts
+++ b/src/hooks/sync-sites/use-site-sync-management.ts
@@ -1,11 +1,16 @@
-import { useState, useEffect, useCallback } from 'react';
-import { getIpcApi } from '../lib/get-ipc-api';
-import { useAuth } from './use-auth';
-import { SyncSite, useFetchWpComSites } from './use-fetch-wpcom-sites';
-import { useSiteDetails } from './use-site-details';
+import { useEffect, useCallback } from 'react';
+import { getIpcApi } from '../../lib/get-ipc-api';
+import { useAuth } from '../use-auth';
+import { SyncSite, useFetchWpComSites } from '../use-fetch-wpcom-sites';
+import { useSiteDetails } from '../use-site-details';
-export const useSiteSyncManagement = () => {
- const [ connectedSites, setConnectedSites ] = useState< SyncSite[] >( [] );
+export const useSiteSyncManagement = ( {
+ connectedSites,
+ setConnectedSites,
+}: {
+ connectedSites: SyncSite[];
+ setConnectedSites: React.Dispatch< React.SetStateAction< SyncSite[] > >;
+} ) => {
const { isAuthenticated } = useAuth();
const { syncSites, isFetching } = useFetchWpComSites( connectedSites );
const { selectedSite } = useSiteDetails();
@@ -24,7 +29,7 @@ export const useSiteSyncManagement = () => {
console.error( 'Failed to load connected sites:', error );
setConnectedSites( [] );
}
- }, [ localSiteId ] );
+ }, [ localSiteId, setConnectedSites ] );
useEffect( () => {
if ( isAuthenticated ) {
diff --git a/src/hooks/tests/use-site-sync-management.test.ts b/src/hooks/tests/use-sync-sites.test.tsx
similarity index 87%
rename from src/hooks/tests/use-site-sync-management.test.ts
rename to src/hooks/tests/use-sync-sites.test.tsx
index b8521468..1c70acc4 100644
--- a/src/hooks/tests/use-site-sync-management.test.ts
+++ b/src/hooks/tests/use-sync-sites.test.tsx
@@ -1,8 +1,8 @@
import { renderHook, waitFor } from '@testing-library/react';
+import { SyncSitesProvider, useSyncSites } from '../sync-sites';
import { useAuth } from '../use-auth';
import { useFetchWpComSites } from '../use-fetch-wpcom-sites';
import { useSiteDetails } from '../use-site-details';
-import { useSiteSyncManagement } from '../use-site-sync-management';
jest.mock( '../use-auth' );
jest.mock( '../use-site-details' );
@@ -63,7 +63,11 @@ jest.mock( '../../lib/get-ipc-api', () => ( {
} ),
} ) );
-describe( 'useSiteSyncManagement', () => {
+describe( 'useSyncSites management', () => {
+ const wrapper = ( { children }: { children: React.ReactNode } ) => (
+ { children }
+ );
+
beforeEach( () => {
( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true } );
( useSiteDetails as jest.Mock ).mockReturnValue( {
@@ -80,7 +84,7 @@ describe( 'useSiteSyncManagement', () => {
} );
it( 'loads connected sites on mount when authenticated', async () => {
- const { result } = renderHook( () => useSiteSyncManagement() );
+ const { result } = renderHook( () => useSyncSites(), { wrapper } );
await waitFor( () => {
expect( result.current.connectedSites ).toEqual( mockConnectedWpcomSites );
@@ -89,7 +93,7 @@ describe( 'useSiteSyncManagement', () => {
it( 'does not load connected sites when not authenticated', async () => {
( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: false } );
- const { result } = renderHook( () => useSiteSyncManagement() );
+ const { result } = renderHook( () => useSyncSites(), { wrapper } );
await waitFor( () => {
expect( result.current.connectedSites ).toEqual( [] );
@@ -97,7 +101,7 @@ describe( 'useSiteSyncManagement', () => {
} );
it( 'connects a site and its staging sites successfully', async () => {
- const { result } = renderHook( () => useSiteSyncManagement() );
+ const { result } = renderHook( () => useSyncSites(), { wrapper } );
const siteToConnect = mockSyncSites[ 0 ];
await waitFor( async () => {
@@ -116,7 +120,7 @@ describe( 'useSiteSyncManagement', () => {
} );
it( 'disconnects a site and its staging sites successfully', async () => {
- const { result } = renderHook( () => useSiteSyncManagement() );
+ const { result } = renderHook( () => useSyncSites(), { wrapper } );
const siteToDisconnect = mockConnectedWpcomSites[ 0 ];
await waitFor( () => {