From 709fac50152d656d8715efe775526a3fe032b99f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20Cs=C3=A9csey?= Date: Wed, 24 Jun 2026 20:33:59 +0100 Subject: [PATCH 1/3] Add Site Management UI integration tests (STU-1867) --- .../content-tab-settings.actions.test.tsx | 178 +++++++++++++ .../tests/create-site.actions.test.tsx | 235 ++++++++++++++++++ .../tests/edit-site-details.actions.test.tsx | 141 +++++++++++ 3 files changed, 554 insertions(+) create mode 100644 apps/studio/src/components/tests/content-tab-settings.actions.test.tsx create mode 100644 apps/studio/src/modules/add-site/tests/create-site.actions.test.tsx create mode 100644 apps/studio/src/modules/site-settings/tests/edit-site-details.actions.test.tsx diff --git a/apps/studio/src/components/tests/content-tab-settings.actions.test.tsx b/apps/studio/src/components/tests/content-tab-settings.actions.test.tsx new file mode 100644 index 0000000000..a071222f07 --- /dev/null +++ b/apps/studio/src/components/tests/content-tab-settings.actions.test.tsx @@ -0,0 +1,178 @@ +// To run tests, execute `npm run test -- src/components/tests/content-tab-settings.actions.test.tsx` from the root directory +/** + * Site Management UI integration tests — duplicate & delete (STU-1867). + * + * Part of migrating apps/studio/e2e/sites.test.ts to fast jsdom tests. Unlike the + * sibling content-tab-settings.test.tsx (which mocks `useSiteDetails`), these mount + * the real `SiteDetailsProvider` and mock only the IPC bridge, then assert the exact + * `getIpcApi()` command a user interaction generates — the "what command does the UI + * fire" model agreed in the 6/24 testing huddle. Duplicate has no CLI command, so this + * is its primary automated coverage; the old e2e delete tests were skipped on Windows. + * + * Smoke-sheet rows: Site Management #9 (duplicate), #12 (delete, keep dir), #13 (delete, +dir). + */ +import { render, screen, waitFor } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import { ReactNode } from 'react'; +import { Provider } from 'react-redux'; +import { vi, beforeAll, beforeEach } from 'vitest'; +import { ContentTabSettings } from 'src/components/content-tab-settings'; +import { ContentTabsProvider } from 'src/hooks/use-content-tabs'; +import { SiteDetailsProvider } from 'src/hooks/use-site-details'; +import { getIpcApi } from 'src/lib/get-ipc-api'; +import { store } from 'src/stores'; + +vi.mock( 'src/lib/get-ipc-api' ); +vi.mock( 'src/lib/app-globals', () => ( { + isWindows: () => false, + isLinux: () => false, +} ) ); +vi.mock( 'src/hooks/use-get-wp-version', () => ( { + useGetWpVersion: () => [ '6.5', vi.fn() ], +} ) ); +vi.mock( 'src/stores/certificate-trust-api', async () => { + const actual = await vi.importActual( 'src/stores/certificate-trust-api' ); + return { + ...( actual || {} ), + useCheckCertificateTrustQuery: vi.fn().mockReturnValue( { data: true } ), + }; +} ); +vi.mock( 'src/stores/wordpress-versions-api', async () => { + const actual = await vi.importActual( 'src/stores/wordpress-versions-api' ); + return { + ...actual, + useGetWordPressVersions: vi.fn( () => ( { + sites: [ { label: 'Latest', value: 'latest', isBeta: false, isDevelopment: false } ], + isLoading: false, + } ) ), + }; +} ); + +const SITE_ID = 'site-id'; +const site: SiteDetails = { + id: SITE_ID, + name: 'Test Site', + path: '/path/to/site', + port: 8881, + phpVersion: '8.4', + running: false, + autoStart: false, + adminPassword: btoa( 'test-password' ), +}; + +const wrapper = ( { children }: { children: ReactNode } ) => ( + + + { children } + + +); + +function renderSettings() { + return render( , { wrapper } ); +} + +async function openMoreOptions( user: ReturnType< typeof userEvent.setup > ) { + await user.click( screen.getByRole( 'button', { name: 'More options' } ) ); +} + +// Wait until the provider has finished its initial async site load before interacting. +async function waitForMount() { + await waitFor( () => expect( getIpcApi().getAllCustomDomains ).toHaveBeenCalled() ); +} + +beforeAll( () => { + Object.defineProperty( window, 'ipcListener', { + value: { subscribe: vi.fn().mockReturnValue( () => {} ) }, + writable: true, + } ); +} ); + +beforeEach( () => { + vi.clearAllMocks(); + vi.mocked( getIpcApi, { partial: true } ).mockReturnValue( { + // SiteDetailsProvider + ContentTabSettings/EditSiteDetails mount-time calls + getSiteDetails: vi.fn().mockResolvedValue( [ site ] ), + getConnectedWpcomSites: vi.fn().mockResolvedValue( [] ), + startServer: vi.fn().mockResolvedValue( undefined ), + getAllCustomDomains: vi.fn().mockResolvedValue( [] ), + getXdebugEnabledSite: vi.fn().mockResolvedValue( null ), + executeWPCLiInline: vi.fn().mockResolvedValue( { stdout: '', stderr: '', exitCode: 0 } ), + isCATrusted: vi.fn().mockResolvedValue( true ), + // Duplicate + generateNumberedNameFromList: vi.fn().mockResolvedValue( 'Test Site Copy' ), + copySite: vi.fn().mockResolvedValue( { ...site, id: 'new-site-id', name: 'Test Site Copy' } ), + showNotification: vi.fn(), + // Delete — default to "Cancel" so unrelated tests can't accidentally delete. + showMessageBox: vi.fn().mockResolvedValue( { response: 1, checkboxChecked: false } ), + deleteSite: vi.fn().mockResolvedValue( undefined ), + } ); +} ); + +describe( 'ContentTabSettings — site actions (IPC command boundary)', () => { + it( 'duplicate: fires copySite with the source id and " Copy" name', async () => { + const user = userEvent.setup(); + renderSettings(); + await waitForMount(); + + await openMoreOptions( user ); + await user.click( await screen.findByRole( 'menuitem', { name: 'Duplicate site' } ) ); + + await waitFor( () => { + expect( getIpcApi().copySite ).toHaveBeenCalledWith( + SITE_ID, + expect.any( String ), + 'Test Site Copy' + ); + } ); + } ); + + it( 'delete (+dir): fires deleteSite(id, true) when the files checkbox is checked', async () => { + const user = userEvent.setup(); + vi.mocked( getIpcApi().showMessageBox ).mockResolvedValue( { + response: 0, + checkboxChecked: true, + } ); + renderSettings(); + await waitForMount(); + + await openMoreOptions( user ); + await user.click( await screen.findByRole( 'menuitem', { name: 'Delete site' } ) ); + + await waitFor( () => { + expect( getIpcApi().deleteSite ).toHaveBeenCalledWith( SITE_ID, true ); + } ); + } ); + + it( 'delete (keep dir): fires deleteSite(id, false) when the files checkbox is unchecked', async () => { + const user = userEvent.setup(); + vi.mocked( getIpcApi().showMessageBox ).mockResolvedValue( { + response: 0, + checkboxChecked: false, + } ); + renderSettings(); + await waitForMount(); + + await openMoreOptions( user ); + await user.click( await screen.findByRole( 'menuitem', { name: 'Delete site' } ) ); + + await waitFor( () => { + expect( getIpcApi().deleteSite ).toHaveBeenCalledWith( SITE_ID, false ); + } ); + } ); + + it( 'delete (cancelled): does not fire deleteSite when the dialog is dismissed', async () => { + const user = userEvent.setup(); + // showMessageBox already defaults to the Cancel response (index 1). + renderSettings(); + await waitForMount(); + + await openMoreOptions( user ); + await user.click( await screen.findByRole( 'menuitem', { name: 'Delete site' } ) ); + + await waitFor( () => { + expect( getIpcApi().showMessageBox ).toHaveBeenCalled(); + } ); + expect( getIpcApi().deleteSite ).not.toHaveBeenCalled(); + } ); +} ); diff --git a/apps/studio/src/modules/add-site/tests/create-site.actions.test.tsx b/apps/studio/src/modules/add-site/tests/create-site.actions.test.tsx new file mode 100644 index 0000000000..c5b1ae3b60 --- /dev/null +++ b/apps/studio/src/modules/add-site/tests/create-site.actions.test.tsx @@ -0,0 +1,235 @@ +// To run tests, execute `npm run test -- src/modules/add-site/tests/create-site.actions.test.tsx` from the root directory +/** + * Site Management UI integration tests — site creation (STU-1867). + * + * UI counterpart to apps/cli/commands/site/tests/create.e2e.test.ts (PR #3947). Mounts the + * real AddSite modal + SiteDetailsProvider and mocks only the IPC bridge, then asserts the + * exact `getIpcApi().createSite(path, config)` command each create flow generates — the + * "what command does the UI fire" model agreed in the 6/24 testing huddle. + * + * The sibling add-site.test.tsx mocks `useSiteDetails` and asserts the modal -> hook call; + * these run the real hook and assert the hook -> IPC command translation, so the full + * user-flow -> command path is covered end to end. + * + * Smoke-sheet rows: Site Management #1 (suggested name), #2 (custom name), + * #3 (default location), #4 (custom location), #5 (custom domain + HTTPS). + */ +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import { ReactNode } from 'react'; +import { Provider } from 'react-redux'; +import { vi, beforeAll, beforeEach } from 'vitest'; +import { ContentTabsProvider } from 'src/hooks/use-content-tabs'; +import { SiteDetailsProvider } from 'src/hooks/use-site-details'; +import { getIpcApi } from 'src/lib/get-ipc-api'; +import AddSite from 'src/modules/add-site'; +import { store } from 'src/stores'; + +vi.mock( 'src/lib/get-ipc-api' ); +vi.mock( 'src/lib/app-globals', () => ( { + isWindows: () => false, + isLinux: () => false, +} ) ); +vi.mock( 'src/components/dot-grid', () => ( { DotGrid: () => null } ) ); +vi.mock( 'src/hooks/use-offline', () => ( { + useOffline: vi.fn().mockReturnValue( false ), +} ) ); +vi.mock( 'src/hooks/use-import-export', () => ( { + useImportExport: () => ( { + importState: {}, + importFile: vi.fn(), + clearImportState: vi.fn(), + } ), +} ) ); +vi.mock( 'src/stores/certificate-trust-api', async () => { + const actual = await vi.importActual( 'src/stores/certificate-trust-api' ); + return { + ...( actual || {} ), + useCheckCertificateTrustQuery: vi.fn().mockReturnValue( { data: true } ), + }; +} ); +vi.mock( 'src/stores/wordpress-versions-api', async () => { + const actual = await vi.importActual( 'src/stores/wordpress-versions-api' ); + return { + ...actual, + useGetWordPressVersions: () => ( { + data: [ { value: '6.4.0', isBeta: false, isDevelopment: false, label: '6.4' } ], + } ), + selectWordPressVersionsWithLatest: vi.fn(), + selectLatestStableVersion: vi.fn(), + }; +} ); +vi.mock( 'src/stores/wpcom-api', async () => { + const actual = await vi.importActual( 'src/stores/wpcom-api' ); + return { + ...( actual || {} ), + useGetBlueprints: vi.fn().mockReturnValue( { + data: { blueprints: [], total: 0 }, + isLoading: false, + refetch: vi.fn(), + isUninitialized: false, + } ), + }; +} ); + +const DEFAULT_PATH = '/default_path/my-wordpress-website'; +const CUSTOM_PATH = '/custom/location'; +const SUGGESTED_NAME = 'My WordPress Website'; + +const wrapper = ( { children }: { children: ReactNode } ) => ( + + + { children } + + +); + +beforeAll( () => { + Object.defineProperty( window, 'ipcListener', { + value: { subscribe: vi.fn().mockReturnValue( () => {} ) }, + writable: true, + } ); +} ); + +beforeEach( () => { + vi.clearAllMocks(); + vi.mocked( getIpcApi, { partial: true } ).mockReturnValue( { + // SiteDetailsProvider mount + getSiteDetails: vi.fn().mockResolvedValue( [] ), + getConnectedWpcomSites: vi.fn().mockResolvedValue( [] ), + startServer: vi.fn().mockResolvedValue( undefined ), + // AddSite / useAddSite mount + interaction + generateProposedSitePath: vi.fn().mockResolvedValue( { + path: DEFAULT_PATH, + name: SUGGESTED_NAME, + isEmpty: true, + isWordPress: false, + } ), + generateSiteNameFromList: vi.fn().mockResolvedValue( SUGGESTED_NAME ), + getAllCustomDomains: vi.fn().mockResolvedValue( [] ), + showOpenFolderDialog: vi.fn().mockResolvedValue( { + path: CUSTOM_PATH, + name: SUGGESTED_NAME, + isEmpty: true, + isWordPress: false, + } ), + comparePaths: vi.fn().mockResolvedValue( false ), + isCATrusted: vi.fn().mockResolvedValue( true ), + setWindowControlVisibility: vi.fn(), + setupAppMenu: vi.fn(), + // Create command (assertion target) + success-callback notification + createSite: vi.fn().mockImplementation( ( path: string ) => + Promise.resolve( { + id: 'new-site', + name: SUGGESTED_NAME, + path, + port: 8881, + running: false, + } ) + ), + showNotification: vi.fn(), + } ); +} ); + +function setup() { + const user = userEvent.setup(); + render( , { wrapper } ); + return user; +} + +// Drive the AddSite modal from the launcher button to the empty-site create form. +async function openCreateForm( user: ReturnType< typeof userEvent.setup > ) { + await user.click( screen.getByRole( 'button', { name: 'Add site' } ) ); + await user.click( screen.getByTestId( 'create-site-option-button' ) ); + await user.click( await screen.findByRole( 'button', { name: /Empty site/ } ) ); + await user.click( screen.getByRole( 'button', { name: 'Continue' } ) ); +} + +async function submitForm( user: ReturnType< typeof userEvent.setup > ) { + const dialog = screen.getByRole( 'dialog' ); + await user.click( within( dialog ).getByRole( 'button', { name: 'Add site' } ) ); +} + +describe( 'AddSite — create site (IPC command boundary)', () => { + it( 'suggested name: createSite( _, { siteName: suggested } )', async () => { + const user = setup(); + await openCreateForm( user ); + await submitForm( user ); + + await waitFor( () => { + expect( getIpcApi().createSite ).toHaveBeenCalledWith( + expect.any( String ), + expect.objectContaining( { siteName: SUGGESTED_NAME } ) + ); + } ); + } ); + + it( 'default location: createSite( defaultPath, ... )', async () => { + const user = setup(); + await openCreateForm( user ); + await submitForm( user ); + + // With no folder picked, the site is created at the proposed default path. + await waitFor( () => { + expect( getIpcApi().createSite ).toHaveBeenCalledWith( + DEFAULT_PATH, + expect.objectContaining( { siteName: SUGGESTED_NAME } ) + ); + } ); + } ); + + it( 'custom name: createSite( _, { siteName: custom } )', async () => { + const user = setup(); + await openCreateForm( user ); + + const nameInput = screen.getByDisplayValue( SUGGESTED_NAME ); + await user.clear( nameInput ); + await user.type( nameInput, 'My Custom Site' ); + await submitForm( user ); + + await waitFor( () => { + expect( getIpcApi().createSite ).toHaveBeenCalledWith( + expect.any( String ), + expect.objectContaining( { siteName: 'My Custom Site' } ) + ); + } ); + } ); + + it( 'custom location: createSite( chosenPath, ... )', async () => { + const user = setup(); + await openCreateForm( user ); + + await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) ); + await user.click( screen.getByTestId( 'select-path-button' ) ); + await submitForm( user ); + + await waitFor( () => { + expect( getIpcApi().createSite ).toHaveBeenCalledWith( + CUSTOM_PATH, + expect.objectContaining( { siteName: SUGGESTED_NAME } ) + ); + } ); + } ); + + it( 'custom domain + HTTPS: createSite( _, { customDomain, enableHttps: true } )', async () => { + const user = setup(); + await openCreateForm( user ); + + await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) ); + await user.click( screen.getByLabelText( 'Use custom domain' ) ); + // Set the controlled domain field in one shot: userEvent.type loops on this input + // because its displayed value falls back to the generated domain until edited. + fireEvent.change( screen.getByLabelText( 'Domain name' ), { + target: { value: 'mysite.local' }, + } ); + await submitForm( user ); + + // Enabling a custom domain with a trusted certificate auto-enables HTTPS. + await waitFor( () => { + expect( getIpcApi().createSite ).toHaveBeenCalledWith( + expect.any( String ), + expect.objectContaining( { customDomain: 'mysite.local', enableHttps: true } ) + ); + } ); + } ); +} ); diff --git a/apps/studio/src/modules/site-settings/tests/edit-site-details.actions.test.tsx b/apps/studio/src/modules/site-settings/tests/edit-site-details.actions.test.tsx new file mode 100644 index 0000000000..8e419ad6b8 --- /dev/null +++ b/apps/studio/src/modules/site-settings/tests/edit-site-details.actions.test.tsx @@ -0,0 +1,141 @@ +// To run tests, execute `npm run test -- src/modules/site-settings/tests/edit-site-details.actions.test.tsx` from the root directory +/** + * Site Management UI integration tests — edit settings & change PHP version (STU-1867). + * + * Unlike the sibling edit-site-details.test.tsx (which mocks `useSiteDetails` and asserts the + * hook call), these mount the real `SiteDetailsProvider`, open the Edit Site modal for real, + * and assert the exact `getIpcApi().updateSite(site, wpVersion)` command the save generates — + * the "what command does the UI fire" model agreed in the 6/24 testing huddle. Changing the + * PHP version was flagged flaky in the old Playwright suite (Redux/_events round-trip); asserting + * at the IPC boundary sidesteps that. + * + * Smoke-sheet rows: Site Management #10 (edit settings — site name), #11 (change PHP version). + */ +import { render, screen, waitFor } from '@testing-library/react'; +import { userEvent } from '@testing-library/user-event'; +import { ReactNode } from 'react'; +import { Provider } from 'react-redux'; +import { vi, beforeAll, beforeEach } from 'vitest'; +import { ContentTabsProvider } from 'src/hooks/use-content-tabs'; +import { SiteDetailsProvider } from 'src/hooks/use-site-details'; +import { getIpcApi } from 'src/lib/get-ipc-api'; +import EditSiteDetails from 'src/modules/site-settings/edit-site-details'; +import { store } from 'src/stores'; + +vi.mock( 'src/lib/get-ipc-api' ); +vi.mock( 'src/lib/app-globals', () => ( { + isWindows: () => false, + isLinux: () => false, +} ) ); +vi.mock( 'src/hooks/use-offline', () => ( { + useOffline: vi.fn().mockReturnValue( false ), +} ) ); +vi.mock( 'src/stores/certificate-trust-api', async () => { + const actual = await vi.importActual( 'src/stores/certificate-trust-api' ); + return { + ...( actual || {} ), + useCheckCertificateTrustQuery: vi.fn().mockReturnValue( { data: true } ), + }; +} ); +vi.mock( 'src/stores/wordpress-versions-api', async () => { + const actual = await vi.importActual( 'src/stores/wordpress-versions-api' ); + return { + ...actual, + useGetWordPressVersions: vi.fn( () => ( { + data: [ + { label: 'Latest', value: '6.7.2' }, + { label: '6.4', value: '6.4', isBeta: false, isDevelopment: false }, + ], + isLoading: false, + } ) ), + }; +} ); + +const SITE_ID = 'site-id'; +const site: SiteDetails = { + id: SITE_ID, + name: 'Test Site', + path: '/path/to/site', + port: 8881, + phpVersion: '8.4', + running: false, + autoStart: false, + adminPassword: btoa( 'test-password' ), +}; + +const wrapper = ( { children }: { children: ReactNode } ) => ( + + + { children } + + +); + +function setup() { + const user = userEvent.setup(); + render( , { wrapper } ); + return user; +} + +// Open the Edit Site modal and wait for it to render. +async function openEditModal( user: ReturnType< typeof userEvent.setup > ) { + await waitFor( () => expect( getIpcApi().getAllCustomDomains ).toHaveBeenCalled() ); + await user.click( screen.getByRole( 'button', { name: 'Edit site' } ) ); + await screen.findByRole( 'dialog' ); +} + +// updateSite is called as updateSite( site, wpVersion ); assert on the persisted site arg. +function savedSiteArg() { + return vi.mocked( getIpcApi().updateSite ).mock.calls[ 0 ][ 0 ]; +} + +beforeAll( () => { + Object.defineProperty( window, 'ipcListener', { + value: { subscribe: vi.fn().mockReturnValue( () => {} ) }, + writable: true, + } ); +} ); + +beforeEach( () => { + vi.clearAllMocks(); + // Make the provider select our seeded site deterministically. + localStorage.setItem( 'selectedSiteId', SITE_ID ); + vi.mocked( getIpcApi, { partial: true } ).mockReturnValue( { + getSiteDetails: vi.fn().mockResolvedValue( [ site ] ), + getConnectedWpcomSites: vi.fn().mockResolvedValue( [] ), + startServer: vi.fn().mockResolvedValue( undefined ), + getAllCustomDomains: vi.fn().mockResolvedValue( [] ), + getXdebugEnabledSite: vi.fn().mockResolvedValue( null ), + executeWPCLiInline: vi.fn().mockResolvedValue( { stdout: '', stderr: '', exitCode: 0 } ), + isCATrusted: vi.fn().mockResolvedValue( true ), + // Save command (assertion target) — updateSite then re-reads the site list. + updateSite: vi.fn().mockResolvedValue( undefined ), + showNotification: vi.fn(), + } ); +} ); + +describe( 'EditSiteDetails — save (IPC command boundary)', () => { + it( 'edit settings: fires updateSite with the renamed site', async () => { + const user = setup(); + await openEditModal( user ); + + const nameInput = screen.getByLabelText( 'Site name' ); + await user.clear( nameInput ); + await user.type( nameInput, 'Renamed Site' ); + await user.click( screen.getByRole( 'button', { name: 'Save' } ) ); + + await waitFor( () => expect( getIpcApi().updateSite ).toHaveBeenCalled() ); + expect( savedSiteArg() ).toMatchObject( { id: SITE_ID, name: 'Renamed Site' } ); + } ); + + it( 'change PHP version: fires updateSite with the new phpVersion', async () => { + const user = setup(); + await openEditModal( user ); + + await user.selectOptions( screen.getByLabelText( 'PHP version' ), '8.2' ); + await user.click( screen.getByRole( 'button', { name: 'Save' } ) ); + + await waitFor( () => expect( getIpcApi().updateSite ).toHaveBeenCalled() ); + expect( savedSiteArg() ).toMatchObject( { id: SITE_ID, phpVersion: '8.2' } ); + } ); +} ); From 7621aab56fec1b242347b2cc3c44abc848b2824b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20Cs=C3=A9csey?= Date: Thu, 25 Jun 2026 11:41:15 +0100 Subject: [PATCH 2/3] Fix useGetWordPressVersions mock shape (data, not sites) in site management tests --- .../src/components/tests/content-tab-settings.actions.test.tsx | 2 +- apps/studio/src/components/tests/content-tab-settings.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/src/components/tests/content-tab-settings.actions.test.tsx b/apps/studio/src/components/tests/content-tab-settings.actions.test.tsx index a071222f07..ede32c7e6b 100644 --- a/apps/studio/src/components/tests/content-tab-settings.actions.test.tsx +++ b/apps/studio/src/components/tests/content-tab-settings.actions.test.tsx @@ -42,7 +42,7 @@ vi.mock( 'src/stores/wordpress-versions-api', async () => { return { ...actual, useGetWordPressVersions: vi.fn( () => ( { - sites: [ { label: 'Latest', value: 'latest', isBeta: false, isDevelopment: false } ], + data: [ { label: 'Latest', value: 'latest', isBeta: false, isDevelopment: false } ], isLoading: false, } ) ), }; diff --git a/apps/studio/src/components/tests/content-tab-settings.test.tsx b/apps/studio/src/components/tests/content-tab-settings.test.tsx index b70d0fbea0..e1d04201be 100644 --- a/apps/studio/src/components/tests/content-tab-settings.test.tsx +++ b/apps/studio/src/components/tests/content-tab-settings.test.tsx @@ -71,7 +71,7 @@ vi.mock( 'src/stores/wordpress-versions-api', async () => { return { ...actual, useGetWordPressVersions: vi.fn( () => ( { - sites: [ + data: [ { label: 'Latest', value: 'latest', isBeta: false, isDevelopment: false }, { label: '6.4', value: '6.4', isBeta: false, isDevelopment: false }, { label: '6.3', value: '6.3', isBeta: false, isDevelopment: false }, From 544ae5aa883e8183e01589d7a117b1b135417638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20Cs=C3=A9csey?= Date: Wed, 1 Jul 2026 14:16:01 +0100 Subject: [PATCH 3/3] Reduce Site Management UI tests to reference examples; drop create-site suite (covered by CLI #3947) --- .../tests/create-site.actions.test.tsx | 235 ------------------ 1 file changed, 235 deletions(-) delete mode 100644 apps/studio/src/modules/add-site/tests/create-site.actions.test.tsx diff --git a/apps/studio/src/modules/add-site/tests/create-site.actions.test.tsx b/apps/studio/src/modules/add-site/tests/create-site.actions.test.tsx deleted file mode 100644 index c5b1ae3b60..0000000000 --- a/apps/studio/src/modules/add-site/tests/create-site.actions.test.tsx +++ /dev/null @@ -1,235 +0,0 @@ -// To run tests, execute `npm run test -- src/modules/add-site/tests/create-site.actions.test.tsx` from the root directory -/** - * Site Management UI integration tests — site creation (STU-1867). - * - * UI counterpart to apps/cli/commands/site/tests/create.e2e.test.ts (PR #3947). Mounts the - * real AddSite modal + SiteDetailsProvider and mocks only the IPC bridge, then asserts the - * exact `getIpcApi().createSite(path, config)` command each create flow generates — the - * "what command does the UI fire" model agreed in the 6/24 testing huddle. - * - * The sibling add-site.test.tsx mocks `useSiteDetails` and asserts the modal -> hook call; - * these run the real hook and assert the hook -> IPC command translation, so the full - * user-flow -> command path is covered end to end. - * - * Smoke-sheet rows: Site Management #1 (suggested name), #2 (custom name), - * #3 (default location), #4 (custom location), #5 (custom domain + HTTPS). - */ -import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'; -import { userEvent } from '@testing-library/user-event'; -import { ReactNode } from 'react'; -import { Provider } from 'react-redux'; -import { vi, beforeAll, beforeEach } from 'vitest'; -import { ContentTabsProvider } from 'src/hooks/use-content-tabs'; -import { SiteDetailsProvider } from 'src/hooks/use-site-details'; -import { getIpcApi } from 'src/lib/get-ipc-api'; -import AddSite from 'src/modules/add-site'; -import { store } from 'src/stores'; - -vi.mock( 'src/lib/get-ipc-api' ); -vi.mock( 'src/lib/app-globals', () => ( { - isWindows: () => false, - isLinux: () => false, -} ) ); -vi.mock( 'src/components/dot-grid', () => ( { DotGrid: () => null } ) ); -vi.mock( 'src/hooks/use-offline', () => ( { - useOffline: vi.fn().mockReturnValue( false ), -} ) ); -vi.mock( 'src/hooks/use-import-export', () => ( { - useImportExport: () => ( { - importState: {}, - importFile: vi.fn(), - clearImportState: vi.fn(), - } ), -} ) ); -vi.mock( 'src/stores/certificate-trust-api', async () => { - const actual = await vi.importActual( 'src/stores/certificate-trust-api' ); - return { - ...( actual || {} ), - useCheckCertificateTrustQuery: vi.fn().mockReturnValue( { data: true } ), - }; -} ); -vi.mock( 'src/stores/wordpress-versions-api', async () => { - const actual = await vi.importActual( 'src/stores/wordpress-versions-api' ); - return { - ...actual, - useGetWordPressVersions: () => ( { - data: [ { value: '6.4.0', isBeta: false, isDevelopment: false, label: '6.4' } ], - } ), - selectWordPressVersionsWithLatest: vi.fn(), - selectLatestStableVersion: vi.fn(), - }; -} ); -vi.mock( 'src/stores/wpcom-api', async () => { - const actual = await vi.importActual( 'src/stores/wpcom-api' ); - return { - ...( actual || {} ), - useGetBlueprints: vi.fn().mockReturnValue( { - data: { blueprints: [], total: 0 }, - isLoading: false, - refetch: vi.fn(), - isUninitialized: false, - } ), - }; -} ); - -const DEFAULT_PATH = '/default_path/my-wordpress-website'; -const CUSTOM_PATH = '/custom/location'; -const SUGGESTED_NAME = 'My WordPress Website'; - -const wrapper = ( { children }: { children: ReactNode } ) => ( - - - { children } - - -); - -beforeAll( () => { - Object.defineProperty( window, 'ipcListener', { - value: { subscribe: vi.fn().mockReturnValue( () => {} ) }, - writable: true, - } ); -} ); - -beforeEach( () => { - vi.clearAllMocks(); - vi.mocked( getIpcApi, { partial: true } ).mockReturnValue( { - // SiteDetailsProvider mount - getSiteDetails: vi.fn().mockResolvedValue( [] ), - getConnectedWpcomSites: vi.fn().mockResolvedValue( [] ), - startServer: vi.fn().mockResolvedValue( undefined ), - // AddSite / useAddSite mount + interaction - generateProposedSitePath: vi.fn().mockResolvedValue( { - path: DEFAULT_PATH, - name: SUGGESTED_NAME, - isEmpty: true, - isWordPress: false, - } ), - generateSiteNameFromList: vi.fn().mockResolvedValue( SUGGESTED_NAME ), - getAllCustomDomains: vi.fn().mockResolvedValue( [] ), - showOpenFolderDialog: vi.fn().mockResolvedValue( { - path: CUSTOM_PATH, - name: SUGGESTED_NAME, - isEmpty: true, - isWordPress: false, - } ), - comparePaths: vi.fn().mockResolvedValue( false ), - isCATrusted: vi.fn().mockResolvedValue( true ), - setWindowControlVisibility: vi.fn(), - setupAppMenu: vi.fn(), - // Create command (assertion target) + success-callback notification - createSite: vi.fn().mockImplementation( ( path: string ) => - Promise.resolve( { - id: 'new-site', - name: SUGGESTED_NAME, - path, - port: 8881, - running: false, - } ) - ), - showNotification: vi.fn(), - } ); -} ); - -function setup() { - const user = userEvent.setup(); - render( , { wrapper } ); - return user; -} - -// Drive the AddSite modal from the launcher button to the empty-site create form. -async function openCreateForm( user: ReturnType< typeof userEvent.setup > ) { - await user.click( screen.getByRole( 'button', { name: 'Add site' } ) ); - await user.click( screen.getByTestId( 'create-site-option-button' ) ); - await user.click( await screen.findByRole( 'button', { name: /Empty site/ } ) ); - await user.click( screen.getByRole( 'button', { name: 'Continue' } ) ); -} - -async function submitForm( user: ReturnType< typeof userEvent.setup > ) { - const dialog = screen.getByRole( 'dialog' ); - await user.click( within( dialog ).getByRole( 'button', { name: 'Add site' } ) ); -} - -describe( 'AddSite — create site (IPC command boundary)', () => { - it( 'suggested name: createSite( _, { siteName: suggested } )', async () => { - const user = setup(); - await openCreateForm( user ); - await submitForm( user ); - - await waitFor( () => { - expect( getIpcApi().createSite ).toHaveBeenCalledWith( - expect.any( String ), - expect.objectContaining( { siteName: SUGGESTED_NAME } ) - ); - } ); - } ); - - it( 'default location: createSite( defaultPath, ... )', async () => { - const user = setup(); - await openCreateForm( user ); - await submitForm( user ); - - // With no folder picked, the site is created at the proposed default path. - await waitFor( () => { - expect( getIpcApi().createSite ).toHaveBeenCalledWith( - DEFAULT_PATH, - expect.objectContaining( { siteName: SUGGESTED_NAME } ) - ); - } ); - } ); - - it( 'custom name: createSite( _, { siteName: custom } )', async () => { - const user = setup(); - await openCreateForm( user ); - - const nameInput = screen.getByDisplayValue( SUGGESTED_NAME ); - await user.clear( nameInput ); - await user.type( nameInput, 'My Custom Site' ); - await submitForm( user ); - - await waitFor( () => { - expect( getIpcApi().createSite ).toHaveBeenCalledWith( - expect.any( String ), - expect.objectContaining( { siteName: 'My Custom Site' } ) - ); - } ); - } ); - - it( 'custom location: createSite( chosenPath, ... )', async () => { - const user = setup(); - await openCreateForm( user ); - - await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) ); - await user.click( screen.getByTestId( 'select-path-button' ) ); - await submitForm( user ); - - await waitFor( () => { - expect( getIpcApi().createSite ).toHaveBeenCalledWith( - CUSTOM_PATH, - expect.objectContaining( { siteName: SUGGESTED_NAME } ) - ); - } ); - } ); - - it( 'custom domain + HTTPS: createSite( _, { customDomain, enableHttps: true } )', async () => { - const user = setup(); - await openCreateForm( user ); - - await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) ); - await user.click( screen.getByLabelText( 'Use custom domain' ) ); - // Set the controlled domain field in one shot: userEvent.type loops on this input - // because its displayed value falls back to the generated domain until edited. - fireEvent.change( screen.getByLabelText( 'Domain name' ), { - target: { value: 'mysite.local' }, - } ); - await submitForm( user ); - - // Enabling a custom domain with a trusted certificate auto-enables HTTPS. - await waitFor( () => { - expect( getIpcApi().createSite ).toHaveBeenCalledWith( - expect.any( String ), - expect.objectContaining( { customDomain: 'mysite.local', enableHttps: true } ) - ); - } ); - } ); -} );