diff --git a/apps/studio/src/components/app.tsx b/apps/studio/src/components/app.tsx index c0329b99d7..ed63c9e996 100644 --- a/apps/studio/src/components/app.tsx +++ b/apps/studio/src/components/app.tsx @@ -74,7 +74,7 @@ export default function App() {
@@ -32,7 +30,7 @@ export default function MainSidebar( { className }: MainSidebarProps ) {
diff --git a/apps/studio/src/components/site-icon.tsx b/apps/studio/src/components/site-icon.tsx new file mode 100644 index 0000000000..67fd77f360 --- /dev/null +++ b/apps/studio/src/components/site-icon.tsx @@ -0,0 +1,66 @@ +import { WordPressLogoCircle } from 'src/components/wordpress-logo-circle'; +import { useThemeDetails } from 'src/hooks/use-theme-details'; +import { cx } from 'src/lib/cx'; + +const SITE_ICON_COLORS = [ + '#5B8A72', // sage + '#7B6B8A', // lavender + '#8A7B5B', // khaki + '#5B7B8A', // steel + '#8A5B6B', // mauve + '#6B8A5B', // moss + '#5B6B8A', // slate + '#8A6B5B', // clay +]; + +/** + * Simple hash of a string to a number, used for deterministic + * color assignment for sites without an explicit iconColorIndex. + */ +function hashStringToIndex( str: string ): number { + let hash = 0; + for ( let i = 0; i < str.length; i++ ) { + hash = ( hash * 31 + str.charCodeAt( i ) ) | 0; + } + return Math.abs( hash ); +} + +export function SiteIcon( { + siteId, + iconColorIndex, + size = 16, + className, +}: { + siteId: string; + iconColorIndex?: number; + size?: number; + className?: string; +} ) { + const { siteIcons } = useThemeDetails(); + const iconData = siteIcons[ siteId ]; + + if ( iconData ) { + return ( + + ); + } + + const colorIndex = + typeof iconColorIndex === 'number' ? iconColorIndex : hashStringToIndex( siteId ); + const bgColor = SITE_ICON_COLORS[ colorIndex % SITE_ICON_COLORS.length ]; + + return ( +
+ +
+ ); +} diff --git a/apps/studio/src/components/site-menu.tsx b/apps/studio/src/components/site-menu.tsx index 7c3c1b44bb..a0d0746bd9 100644 --- a/apps/studio/src/components/site-menu.tsx +++ b/apps/studio/src/components/site-menu.tsx @@ -4,13 +4,14 @@ import { Spinner } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { useEffect, useState } from 'react'; import { XDebugIcon } from 'src/components/icons/xdebug-icon'; +import { SiteIcon } from 'src/components/site-icon'; import { Tooltip } from 'src/components/tooltip'; import { useSyncSites } from 'src/hooks/sync-sites'; import { useContentTabs } from 'src/hooks/use-content-tabs'; import { useDeleteSite } from 'src/hooks/use-delete-site'; import { useImportExport } from 'src/hooks/use-import-export'; import { useSiteDetails } from 'src/hooks/use-site-details'; -import { isMac, isWindows } from 'src/lib/app-globals'; +import { isWindows } from 'src/lib/app-globals'; import { cx } from 'src/lib/cx'; import { getIpcApi } from 'src/lib/get-ipc-api'; import { supportedEditorConfig } from 'src/modules/user-settings/lib/editor'; @@ -208,8 +209,7 @@ function SiteItem( { return (
  • { showSpinner ? ( @@ -386,11 +387,11 @@ export default function SiteMenu( { className }: SiteMenuProps ) { scrollbarGutter: 'stable', } } className={ cx( - 'w-full overflow-y-auto overflow-x-hidden flex flex-col gap-0.5 pb-4', + 'w-full overflow-y-auto overflow-x-hidden flex flex-col gap-1 px-2 pb-4', className ) } > -
      +
        { sites.map( ( site, index ) => ( false ), }; -vi.mock( 'src/hooks/use-site-details', () => ( { - useSiteDetails: () => ( { ...siteDetailsMocked } ), +vi.mock( 'src/hooks/use-site-details', async ( importOriginal ) => { + const actual = await importOriginal< typeof import('src/hooks/use-site-details') >(); + return { + ...actual, + useSiteDetails: () => ( { ...siteDetailsMocked } ), + }; +} ); + +vi.mock( 'src/hooks/use-theme-details', () => ( { + useThemeDetails: () => ( { siteIcons: {} } ), + ThemeDetailsContext: { Provider: ( { children }: { children: React.ReactNode } ) => children }, } ) ); const renderWithProvider = ( children: React.ReactElement ) => { diff --git a/apps/studio/src/constants.ts b/apps/studio/src/constants.ts index 999a29b7e4..188f0105e1 100644 --- a/apps/studio/src/constants.ts +++ b/apps/studio/src/constants.ts @@ -1,7 +1,7 @@ import { HOUR_MS } from '@studio/common/constants'; export const DEFAULT_WIDTH = 900; export const MAIN_MIN_HEIGHT = 600; -export const SIDEBAR_WIDTH = 208; +export const SIDEBAR_WIDTH = 248; export const MAIN_MIN_WIDTH = DEFAULT_WIDTH - SIDEBAR_WIDTH + 20; export const APP_CHROME_SPACING = 10; export const MIN_WIDTH_CLASS_TO_MEASURE = 'app-measure-tabs-width'; diff --git a/apps/studio/src/hooks/use-site-details.tsx b/apps/studio/src/hooks/use-site-details.tsx index e9e805f5db..c2b1fbc090 100644 --- a/apps/studio/src/hooks/use-site-details.tsx +++ b/apps/studio/src/hooks/use-site-details.tsx @@ -328,6 +328,7 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) { running: false, isAddingSite: true, phpVersion: '', + iconColorIndex: prevData.length % 8, }, ] ) ); @@ -524,6 +525,7 @@ export function SiteDetailsProvider( { children }: SiteDetailsProviderProps ) { running: false, isAddingSite: true, phpVersion: sourceSite.phpVersion, + iconColorIndex: prevData.length % 8, }, ] ) ); diff --git a/apps/studio/src/hooks/use-theme-details.tsx b/apps/studio/src/hooks/use-theme-details.tsx index 7ea95ecaea..a56f4cd211 100644 --- a/apps/studio/src/hooks/use-theme-details.tsx +++ b/apps/studio/src/hooks/use-theme-details.tsx @@ -6,15 +6,18 @@ import { getIpcApi } from 'src/lib/get-ipc-api'; type ThemeDetailsType = SiteDetails[ 'themeDetails' ] | undefined; type ThumbnailType = string | undefined; +type SiteIconType = string | undefined; interface ThemeDetailsContextType { loadingThemeDetails: Record< string, boolean >; loadingThumbnails: Record< string, boolean >; themeDetails: Record< string, ThemeDetailsType >; thumbnails: Record< string, ThumbnailType >; + siteIcons: Record< string, SiteIconType >; initialLoading: boolean; selectedThemeDetails: ThemeDetailsType; selectedThumbnail: ThumbnailType; + selectedSiteIcon: SiteIconType; selectedLoadingThemeDetails: boolean; selectedLoadingThumbnails: boolean; } @@ -24,9 +27,11 @@ export const ThemeDetailsContext = createContext< ThemeDetailsContextType >( { loadingThumbnails: {}, themeDetails: {}, thumbnails: {}, + siteIcons: {}, initialLoading: false, selectedThemeDetails: undefined, selectedThumbnail: undefined, + selectedSiteIcon: undefined, selectedLoadingThemeDetails: false, selectedLoadingThumbnails: false, } ); @@ -44,6 +49,7 @@ export const ThemeDetailsProvider: React.FC< ThemeDetailsProviderProps > = ( { c {} ); const [ loadingThumbnails, setLoadingThumbnails ] = useState< Record< string, boolean > >( {} ); + const [ siteIcons, setSiteIcons ] = useState< Record< string, SiteIconType > >( {} ); useIpcListener( 'theme-details-loading', ( _evt, { id } ) => { setLoadingThemeDetails( ( loadingThemeDetails ) => { @@ -81,31 +87,48 @@ export const ThemeDetailsProvider: React.FC< ThemeDetailsProviderProps > = ( { c } ); } ); + useIpcListener( 'site-icon-loaded', ( _evt, { id, imageData } ) => { + setSiteIcons( ( siteIcons ) => { + return { ...siteIcons, [ id ]: imageData ?? undefined }; + } ); + } ); + useWindowListener( 'focus', async () => { - // When the window is focused, we need to kick off a request to refetch the theme details, if server is running. - if ( ! selectedSite?.id || selectedSite.running === false ) { - return; - } - await getIpcApi().loadThemeDetails( selectedSite.id, false ); + // When the window is focused, refetch theme details for all running sites. + const runningSites = sites.filter( ( site ) => site.running ); + await Promise.all( + runningSites.map( ( site ) => getIpcApi().loadThemeDetails( site.id, false ) ) + ); } ); + useEffect( () => { + // When the selected site changes, refresh its theme details (including icon). + if ( selectedSite?.id && selectedSite.running ) { + void getIpcApi().loadThemeDetails( selectedSite.id, false ); + } + }, [ selectedSite?.id ] ); // eslint-disable-line react-hooks/exhaustive-deps + useEffect( () => { let isCurrent = true; // Initial load. Prefetch all the thumbnails for the sites. const run = async () => { const newThemeDetails = { ...themeDetails }; const newThumbnailData = { ...thumbnails }; + const newSiteIcons = { ...siteIcons }; for ( const site of sites ) { if ( site.themeDetails ) { newThemeDetails[ site.id ] = { ...site.themeDetails }; const thumbnailData = await getIpcApi().getThumbnailData( site.id ); newThumbnailData[ site.id ] = thumbnailData ?? undefined; } + const iconData = await getIpcApi().getSiteIconData( site.id ); + newSiteIcons[ site.id ] = iconData ?? undefined; } if ( isCurrent ) { setInitialLoad( true ); setThemeDetails( newThemeDetails ); setThumbnails( newThumbnailData ); + setSiteIcons( newSiteIcons ); } }; if ( sites.length > 0 && ! loadingSites && ! initialLoad && isCurrent ) { @@ -114,17 +137,19 @@ export const ThemeDetailsProvider: React.FC< ThemeDetailsProviderProps > = ( { c return () => { isCurrent = false; }; - }, [ initialLoad, loadingSites, sites, themeDetails, thumbnails ] ); + }, [ initialLoad, loadingSites, sites, themeDetails, thumbnails, siteIcons ] ); const contextValue = useMemo( () => { return { thumbnails, themeDetails, + siteIcons, loadingThemeDetails, loadingThumbnails, initialLoading: ! initialLoad, selectedThemeDetails: themeDetails[ selectedSite?.id ?? '' ], selectedThumbnail: thumbnails[ selectedSite?.id ?? '' ], + selectedSiteIcon: siteIcons[ selectedSite?.id ?? '' ], selectedLoadingThemeDetails: loadingThemeDetails[ selectedSite?.id ?? '' ], selectedLoadingThumbnails: loadingThumbnails[ selectedSite?.id ?? '' ], }; @@ -133,6 +158,7 @@ export const ThemeDetailsProvider: React.FC< ThemeDetailsProviderProps > = ( { c loadingThemeDetails, loadingThumbnails, selectedSite?.id, + siteIcons, themeDetails, thumbnails, ] ); diff --git a/apps/studio/src/ipc-handlers.ts b/apps/studio/src/ipc-handlers.ts index ea040162dc..77d2a4c67a 100644 --- a/apps/studio/src/ipc-handlers.ts +++ b/apps/studio/src/ipc-handlers.ts @@ -72,7 +72,7 @@ import { supportedEditorConfig, SupportedEditor } from 'src/modules/user-setting import { getUserTerminal } from 'src/modules/user-settings/lib/ipc-handlers'; import { winFindEditorPath } from 'src/modules/user-settings/lib/win-editor-path'; import { SiteServer, stopAllServers as triggerStopAllServers } from 'src/site-server'; -import { DEFAULT_SITE_PATH, getSiteThumbnailPath } from 'src/storage/paths'; +import { DEFAULT_SITE_PATH, getSiteIconPath, getSiteThumbnailPath } from 'src/storage/paths'; import { loadUserData, lockAppdata, @@ -264,6 +264,10 @@ export async function createSite( const siteId = providedSiteId || crypto.randomUUID(); + // Determine the icon color index for this site (round-robin from palette of 8 colors) + const existingSites = ( await loadUserData() ).sites; + const iconColorIndex = existingSites.length % 8; + const metric = getBlueprintMetric( blueprint?.slug ); bumpStat( StatsGroup.STUDIO_SITE_CREATE, metric ); @@ -280,7 +284,7 @@ export async function createSite( blueprint: blueprint?.blueprint, noStart, }, - { wpVersion, blueprint: blueprint?.blueprint } + { wpVersion, blueprint: blueprint?.blueprint, iconColorIndex } ); // If the site is running after creation, fetch theme details and update thumbnail @@ -608,6 +612,18 @@ export async function copySite( ); } + const sourceIconPath = getSiteIconPath( sourceSiteId ); + const newIconPath = getSiteIconPath( newSiteId ); + if ( fs.existsSync( sourceIconPath ) ) { + await fs.promises.copyFile( sourceIconPath, newIconPath ); + const iconData = await getImageData( newIconPath ); + sendIpcEventToRendererWithWindow( + BrowserWindow.fromWebContents( event.sender ), + 'site-icon-loaded', + { id: newSiteId, imageData: iconData } + ); + } + const port = await portFinder.getOpenPort(); const newSiteDetails: StoppedSiteDetails = { @@ -624,6 +640,7 @@ export async function copySite( try { await lockAppdata(); const userData = await loadUserData(); + newSiteDetails.iconColorIndex = userData.sites.length % 8; userData.sites.push( newSiteDetails ); await saveUserData( userData ); } finally { @@ -870,6 +887,19 @@ export async function loadThemeDetails( console.error( `Failed to update thumbnail for server ${ id }:`, error ); } + try { + sendIpcEventToRendererWithWindow( parentWindow, 'site-icon-loading', { id } ); + await server.updateCachedSiteIcon(); + const iconData = await getImageData( getSiteIconPath( id ) ); + sendIpcEventToRendererWithWindow( parentWindow, 'site-icon-loaded', { + id, + imageData: iconData, + } ); + } catch ( error ) { + sendIpcEventToRendererWithWindow( parentWindow, 'site-icon-load-error', { id } ); + console.error( `Failed to update site icon for server ${ id }:`, error ); + } + return themeDetails; } @@ -920,6 +950,11 @@ export function getThumbnailData( _event: IpcMainInvokeEvent, id: string ) { return getImageData( path ); } +export function getSiteIconData( _event: IpcMainInvokeEvent, id: string ) { + const path = getSiteIconPath( id ); + return getImageData( path ); +} + function promiseExec( command: string, options: ExecOptions = {} ): Promise< void > { return new Promise( ( resolve, reject ) => { exec( command, options, ( error ) => { diff --git a/apps/studio/src/ipc-types.d.ts b/apps/studio/src/ipc-types.d.ts index 00353d0da0..74a71a3e51 100644 --- a/apps/studio/src/ipc-types.d.ts +++ b/apps/studio/src/ipc-types.d.ts @@ -34,6 +34,7 @@ interface StoppedSiteDetails { enableDebugLog?: boolean; enableDebugDisplay?: boolean; sortOrder?: number; + iconColorIndex?: number; } interface StartedSiteDetails extends StoppedSiteDetails { diff --git a/apps/studio/src/ipc-utils.ts b/apps/studio/src/ipc-utils.ts index 25cbc40425..d1260a3efe 100644 --- a/apps/studio/src/ipc-utils.ts +++ b/apps/studio/src/ipc-utils.ts @@ -50,6 +50,9 @@ export interface IpcEvents { 'thumbnail-loading': [ { id: string } ]; 'thumbnail-loaded': [ { id: string; imageData: string | null } ]; 'thumbnail-load-error': [ { id: string } ]; + 'site-icon-loading': [ { id: string } ]; + 'site-icon-loaded': [ { id: string; imageData: string | null } ]; + 'site-icon-load-error': [ { id: string } ]; 'user-settings': [ { tabName?: string } ]; 'window-fullscreen-change': [ boolean ]; 'user-preference-changed': [ void ]; diff --git a/apps/studio/src/preload.ts b/apps/studio/src/preload.ts index 9e30c143bb..5b29781c9c 100644 --- a/apps/studio/src/preload.ts +++ b/apps/studio/src/preload.ts @@ -1,7 +1,7 @@ // See the Electron documentation for details on how to use preload scripts: // https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts -import '@sentry/electron/preload'; +// import '@sentry/electron/preload'; // Crashes with Electron 39 — Sentry bug import { IpcRendererEvent, contextBridge, ipcRenderer, webUtils } from 'electron'; import { IpcEvents } from 'src/ipc-utils'; @@ -91,6 +91,7 @@ const api: IpcApi = { loadThemeDetails: ( id, emitThemeDetailsLoadingEvent = true ) => ipcRendererInvoke( 'loadThemeDetails', id, emitThemeDetailsLoadingEvent ), getThumbnailData: ( id ) => ipcRendererInvoke( 'getThumbnailData', id ), + getSiteIconData: ( id ) => ipcRendererInvoke( 'getSiteIconData', id ), getInstalledAppsAndTerminals: () => ipcRendererInvoke( 'getInstalledAppsAndTerminals' ), importSite: ( { id, backupFile } ) => ipcRendererInvoke( 'importSite', { id, backupFile } ), executeWPCLiInline: ( options ) => ipcRendererInvoke( 'executeWPCLiInline', options ), diff --git a/apps/studio/src/site-server.ts b/apps/studio/src/site-server.ts index 51953d085c..e756530dcf 100644 --- a/apps/studio/src/site-server.ts +++ b/apps/studio/src/site-server.ts @@ -1,4 +1,7 @@ +import { nativeImage } from 'electron'; import fs from 'fs'; +import http from 'node:http'; +import https from 'node:https'; import nodePath from 'path'; import * as Sentry from '@sentry/electron/main'; import { SQLITE_FILENAME } from '@studio/common/constants'; @@ -14,7 +17,7 @@ import { CliServerProcess } from 'src/modules/cli/lib/cli-server-process'; import { createSiteViaCli, type CreateSiteOptions } from 'src/modules/cli/lib/cli-site-creator'; import { executeCliCommand } from 'src/modules/cli/lib/execute-command'; import { createScreenshotWindow } from 'src/screenshot-window'; -import { getSiteThumbnailPath } from 'src/storage/paths'; +import { getSiteIconPath, getSiteThumbnailPath } from 'src/storage/paths'; import { loadUserData, lockAppdata, saveUserData, unlockAppdata } from 'src/storage/user-data'; import type { BlueprintV1Declaration } from '@wp-playground/blueprints'; @@ -77,10 +80,41 @@ function getAbsoluteUrl( details: SiteDetails ): string { return `http://localhost:${ details.port }`; } +function fetchImageBuffer( url: string ): Promise< Buffer > { + return new Promise( ( resolve, reject ) => { + const protocol = url.startsWith( 'https' ) ? https : http; + const request = protocol.get( url, { timeout: 5000, rejectUnauthorized: false }, ( res ) => { + if ( + res.statusCode && + res.statusCode >= 300 && + res.statusCode < 400 && + res.headers.location + ) { + fetchImageBuffer( res.headers.location ).then( resolve, reject ); + return; + } + if ( ! res.statusCode || res.statusCode >= 400 ) { + reject( new Error( `HTTP ${ res.statusCode } fetching ${ url }` ) ); + return; + } + const chunks: Buffer[] = []; + res.on( 'data', ( chunk: Buffer ) => chunks.push( chunk ) ); + res.on( 'end', () => resolve( Buffer.concat( chunks ) ) ); + res.on( 'error', reject ); + } ); + request.on( 'timeout', () => { + request.destroy(); + reject( new Error( `Timeout fetching ${ url }` ) ); + } ); + request.on( 'error', reject ); + } ); +} + // We use SiteDetails for storing it in appdata-v1.json, so this meta was introduced for extra data which is not stored locally type SiteServerMeta = { wpVersion?: string; blueprint?: BlueprintV1Declaration; + iconColorIndex?: number; }; export class SiteServer { @@ -141,18 +175,34 @@ export class SiteServer { port: 0, phpVersion: options.phpVersion || '', running: false, + iconColorIndex: meta.iconColorIndex, }; const server = SiteServer.register( placeholderDetails, meta ); server.hasOngoingOperation = true; try { const result = await createSiteViaCli( { ...options, siteId } ); - const userData = await loadUserData(); - const siteData = userData.sites.find( ( s ) => s.id === result.id ); - if ( ! siteData ) { - throw new Error( `Site with ID ${ result.id } not found in appdata after CLI creation` ); + + // Persist iconColorIndex to appdata immediately so file-watcher + // reloads never see the site without it (prevents color flash). + let userData: Awaited< ReturnType< typeof loadUserData > >; + try { + await lockAppdata(); + userData = await loadUserData(); + const siteData = userData.sites.find( ( s ) => s.id === result.id ); + if ( ! siteData ) { + throw new Error( `Site with ID ${ result.id } not found in appdata after CLI creation` ); + } + if ( meta.iconColorIndex !== undefined ) { + siteData.iconColorIndex = meta.iconColorIndex; + await saveUserData( userData ); + } + } finally { + await unlockAppdata(); } + const siteData = userData.sites.find( ( s ) => s.id === result.id )!; + let siteDetails: SiteDetails; if ( result.running ) { const url = siteData.customDomain @@ -191,6 +241,11 @@ export class SiteServer { await fs.promises.unlink( thumbnailPath ); } + const iconPath = getSiteIconPath( this.details.id ); + if ( fs.existsSync( iconPath ) ) { + await fs.promises.unlink( iconPath ); + } + await this.server.delete( deleteFiles ); deletedServers.push( this.details.id ); servers.delete( this.details.id ); @@ -300,6 +355,44 @@ export class SiteServer { .finally( () => window.destroy() ); } + async updateCachedSiteIcon() { + if ( ! this.details.running ) { + return; + } + + let iconUrl = ''; + + // Use get_site_icon_url() PHP function to resolve the actual icon URL. + // The 'site_icon' WP option only stores an attachment ID, not a URL. + try { + const { stdout, exitCode } = await this.executeWpCliCommand( + [ 'eval', 'echo get_site_icon_url( 512 );' ], + { skipPluginsAndThemes: true } + ); + if ( exitCode === 0 && stdout.trim() ) { + iconUrl = stdout.trim(); + } + } catch { + // WP-CLI failed, no icon available + } + + if ( ! iconUrl ) { + return; + } + + const buffer = await fetchImageBuffer( iconUrl ); + const image = nativeImage.createFromBuffer( buffer ); + if ( image.isEmpty() ) { + throw new Error( 'Could not parse image from buffer' ); + } + + const resized = image.resize( { width: 64, height: 64 } ); + const outPath = getSiteIconPath( this.details.id ); + const outDir = nodePath.dirname( outPath ); + await fs.promises.mkdir( outDir, { recursive: true } ); + await fs.promises.writeFile( outPath, resized.toPNG() ); + } + async executeWpCliCommand( args: string | string[], { diff --git a/apps/studio/src/storage/paths.ts b/apps/studio/src/storage/paths.ts index 65d180fbf6..1ff0164e31 100644 --- a/apps/studio/src/storage/paths.ts +++ b/apps/studio/src/storage/paths.ts @@ -42,6 +42,10 @@ export function getSiteThumbnailPath( siteId: string ): string { return path.join( getAppDataPath(), getAppName(), 'thumbnails', `${ siteId }.png` ); } +export function getSiteIconPath( siteId: string ): string { + return path.join( getAppDataPath(), getAppName(), 'site-icons', `${ siteId }.png` ); +} + export function getResourcesPath(): string { if ( ! app ) { throw new Error( 'Electron app not available in child process' ); diff --git a/apps/studio/src/storage/user-data.ts b/apps/studio/src/storage/user-data.ts index d34cf32e76..d512acdd06 100644 --- a/apps/studio/src/storage/user-data.ts +++ b/apps/studio/src/storage/user-data.ts @@ -200,6 +200,7 @@ function toDiskFormat( { sites, ...rest }: UserData ): PersistedUserData { enableDebugLog, enableDebugDisplay, sortOrder, + iconColorIndex, } ) => { // No object spreading allowed. TypeScript's structural typing is too permissive and // will permit us to persist properties that aren't in the type definition. @@ -220,6 +221,7 @@ function toDiskFormat( { sites, ...rest }: UserData ): PersistedUserData { enableDebugLog, enableDebugDisplay, sortOrder, + iconColorIndex, themeDetails: { name: themeDetails?.name || '', path: themeDetails?.path || '', diff --git a/apps/studio/src/tests/ipc-handlers.test.ts b/apps/studio/src/tests/ipc-handlers.test.ts index 315e22cf82..db181e63a0 100644 --- a/apps/studio/src/tests/ipc-handlers.test.ts +++ b/apps/studio/src/tests/ipc-handlers.test.ts @@ -70,14 +70,17 @@ const mockSiteDetails: StoppedSiteDetails = { enableHttps: undefined, }; -vi.mocked( SiteServer.create ).mockResolvedValue( { - server: { - start: vi.fn(), - details: mockSiteDetails, - updateSiteDetails: vi.fn(), - updateCachedThumbnail: vi.fn().mockResolvedValue( undefined ), - } as unknown as SiteServer, - details: mockSiteDetails, +vi.mocked( SiteServer.create ).mockImplementation( async ( _options, meta ) => { + const details = { ...mockSiteDetails, iconColorIndex: meta?.iconColorIndex }; + return { + server: { + start: vi.fn(), + details, + updateSiteDetails: vi.fn(), + updateCachedThumbnail: vi.fn().mockResolvedValue( undefined ), + } as unknown as SiteServer, + details, + }; } ); vi.mocked( SiteServer.register, { partial: true } ).mockImplementation( ( details ) => ( { @@ -123,6 +126,7 @@ describe( 'createSite', () => { customDomain: undefined, enableHttps: undefined, isWpAutoUpdating: false, + iconColorIndex: 0, } ); expect( SiteServer.create ).toHaveBeenCalledWith(