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

Add context for sync sites #642

Merged
merged 51 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
42f16e9
Create basic logic with new context
sejas Oct 22, 2024
a6eb057
create download function to be used as in sciprts and ipc handler
sejas Oct 22, 2024
b3f0e32
create states and progress to download a remote site zip
sejas Oct 22, 2024
111de25
Merge branch 'trunk' of github.com:Automattic/studio into add/sync-pu…
sejas Oct 29, 2024
3446340
remove calculated pull states type
sejas Oct 29, 2024
d79d62a
create auxiliar function onBackupCompleted
sejas Oct 29, 2024
b2485d2
pass selected site
sejas Oct 29, 2024
16e7943
Pass the correct filetype
sejas Oct 29, 2024
498a01c
import tar.gz jetpack backup
sejas Oct 29, 2024
dfc15d4
Add provider on content tab sync tests
sejas Oct 29, 2024
3f774d1
Merge branch 'trunk' of github.com:Automattic/studio into add/sync-pu…
sejas Oct 31, 2024
6180331
move pull state progress info to its own hook
sejas Oct 31, 2024
e7ddb99
disable importing when any site is pulling
sejas Oct 31, 2024
8663bb9
disconect pull push and disconnect when a site is pulling
sejas Oct 31, 2024
62488d6
Display min width and correct font type for progress indicator
sejas Oct 31, 2024
27f6e6b
handle error when creating backup
sejas Oct 31, 2024
369316f
Merge branch 'trunk' of github.com:Automattic/studio into add/sync-pu…
sejas Oct 31, 2024
9a7175a
Fix tests
sejas Oct 31, 2024
1c8d1db
Remove use site sync management in favor of use sync sites context
sejas Oct 31, 2024
f6fc2cd
fix tests to use useSyncSites mock
sejas Oct 31, 2024
4f3e14e
Simplify test checks
sejas Oct 31, 2024
b2aaf98
Follow same convention for import and export site components
sejas Nov 11, 2024
e8a0a07
block export while a site is being pulled
sejas Nov 11, 2024
44b70bc
Merge branch 'trunk' of github.com:Automattic/studio into add/sync-pu…
sejas Nov 11, 2024
a22422f
Refactor context to live in its own folder and separate hooks
sejas Nov 13, 2024
63641a8
Move the state management to the context to avoid using useSyncPull d…
sejas Nov 13, 2024
a8d6497
extract setPullStates logic in favor of a custom callback
sejas Nov 13, 2024
e7387df
do not update state before calling onBackupCompleted
sejas Nov 13, 2024
48ace4b
Disable start stop button when pulling a site
sejas Nov 13, 2024
fe30df2
capture sentry error when pulling
sejas Nov 13, 2024
27a8214
capture error on sentry
sejas Nov 13, 2024
33b521a
using timeout instead of interval as the state update will retrigger …
sejas Nov 13, 2024
41d070d
Replace states for consistency, mapping old completed, current finish…
sejas Nov 13, 2024
6baea65
remove sync backup after imoprt
sejas Nov 13, 2024
fd635f0
add provider to tests
sejas Nov 13, 2024
fc570c3
add provider to remaining tests
sejas Nov 13, 2024
776f1c2
display pull complete state to allow user to clear it
sejas Nov 13, 2024
05f2b98
avoid jumping row height when changing states
sejas Nov 13, 2024
51e5b9b
Display notificiation when pull completes making difference between s…
sejas Nov 13, 2024
8eef663
Display error message inside connected sites list
sejas Nov 13, 2024
4e0eb56
Merge branch 'add/sync-pull-remote-site' of github.com:Automattic/stu…
sejas Nov 13, 2024
b1bcd17
Refactor use site sync management to be inside the context
sejas Nov 13, 2024
3e95562
update tests to use the provider main hook
sejas Nov 13, 2024
7575350
clear import state after pull site to avoid showing import complete s…
sejas Nov 13, 2024
def0e92
rename type and make sure we track the sync pull progress per local site
sejas Nov 13, 2024
6f045ba
refactor to use local site id and remote site id as pull state key
sejas Nov 13, 2024
2f71b9c
create getPullState function to access the pull state
sejas Nov 13, 2024
6d7c92e
update clear site key, and make sure that we clear pull state after d…
sejas Nov 14, 2024
991b8bf
Merge branch 'add/sync-pull-remote-site' of github.com:Automattic/stu…
sejas Nov 14, 2024
8f1b599
fix tests
sejas Nov 14, 2024
a3941f7
Merge branch 'trunk' of github.com:Automattic/studio into add/sync-co…
sejas Nov 14, 2024
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
5 changes: 2 additions & 3 deletions src/components/content-tab-sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 ) {
Expand Down
89 changes: 44 additions & 45 deletions src/components/tests/content-tab-sync.test.tsx
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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(),
} );
} );

Expand All @@ -43,13 +49,13 @@ describe( 'ContentTabSync', () => {
renderWithProvider( <ContentTabSync selectedSite={ selectedSite } /> );
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 );
Expand All @@ -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( <ContentTabSync selectedSite={ selectedSite } /> );
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();
Expand All @@ -70,15 +76,15 @@ describe( 'ContentTabSync', () => {
it( 'displays connect site button to authenticated user', () => {
( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true, authenticate: jest.fn() } );
renderWithProvider( <ContentTabSync selectedSite={ selectedSite } /> );
const connectSiteButton = screen.getByRole( 'button', { name: 'Connect site' } );
const connectSiteButton = screen.getByRole( 'button', { name: /Connect site/i } );

expect( connectSiteButton ).toBeInTheDocument();
} );

it( 'opens the site selector modal to connect a site authenticated user', () => {
( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true, authenticate: jest.fn() } );
renderWithProvider( <ContentTabSync selectedSite={ selectedSite } /> );
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();
} );
Expand All @@ -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( <ContentTabSync selectedSite={ selectedSite } /> );

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 () => {
Expand All @@ -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( <ContentTabSync selectedSite={ selectedSite } /> );

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 () => {
Expand All @@ -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( <ContentTabSync selectedSite={ selectedSite } /> );

// 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/ ↗',
} );
Expand Down
6 changes: 6 additions & 0 deletions src/components/tests/site-content-tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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( {} );

Expand Down
15 changes: 14 additions & 1 deletion src/hooks/sync-sites/sync-sites-context.tsx
Original file line number Diff line number Diff line change
@@ -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 );

Expand All @@ -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 (
<SyncSitesContext.Provider
value={ {
Expand All @@ -22,6 +29,12 @@ export function SyncSitesProvider( { children }: { children: React.ReactNode } )
isAnySitePulling,
isSiteIdPulling,
clearPullState,
connectedSites,
loadConnectedSites,
connectSite,
disconnectSite,
syncSites,
isFetching,
getPullState,
} }
>
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -24,7 +29,7 @@ export const useSiteSyncManagement = () => {
console.error( 'Failed to load connected sites:', error );
setConnectedSites( [] );
}
}, [ localSiteId ] );
}, [ localSiteId, setConnectedSites ] );

useEffect( () => {
if ( isAuthenticated ) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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' );
Expand Down Expand Up @@ -63,7 +63,11 @@ jest.mock( '../../lib/get-ipc-api', () => ( {
} ),
} ) );

describe( 'useSiteSyncManagement', () => {
describe( 'useSyncSites management', () => {
const wrapper = ( { children }: { children: React.ReactNode } ) => (
<SyncSitesProvider>{ children }</SyncSitesProvider>
);

beforeEach( () => {
( useAuth as jest.Mock ).mockReturnValue( { isAuthenticated: true } );
( useSiteDetails as jest.Mock ).mockReturnValue( {
Expand All @@ -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 );
Expand All @@ -89,15 +93,15 @@ 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( [] );
} );
} );

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 () => {
Expand All @@ -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( () => {
Expand Down
Loading