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( () => {