Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/components/add-site.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export default function AddSite( { className }: AddSiteProps ) {
setCustomDomainError,
enableHttps,
setEnableHttps,
loadAllCustomDomains,
} = useAddSite();
const { importState } = useImportExport();

Expand Down Expand Up @@ -86,6 +87,7 @@ export default function AddSite( { className }: AddSiteProps ) {
setSitePath( '' );
setError( '' );
setDoesPathContainWordPress( isWordPress );
loadAllCustomDomains();
}, [
sites,
setSiteName,
Expand All @@ -95,6 +97,7 @@ export default function AddSite( { className }: AddSiteProps ) {
setDoesPathContainWordPress,
setWpVersion,
latestStableVersion,
loadAllCustomDomains,
] );

useEffect( () => {
Expand Down
2 changes: 2 additions & 0 deletions src/components/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export default function Onboarding() {
setCustomDomainError,
enableHttps,
setEnableHttps,
loadAllCustomDomains,
} = useAddSite();
const [ fileError, setFileError ] = useState( '' );

Expand Down Expand Up @@ -112,6 +113,7 @@ export default function Onboarding() {
setCustomDomain( null );
setCustomDomainError( '' );
setEnableHttps( false );
loadAllCustomDomains();
};
run();
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
3 changes: 3 additions & 0 deletions src/components/tests/add-site.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,15 @@ const mockShowOpenFolderDialog =
jest.fn< ( dialogTitle: string ) => Promise< FolderDialogResponse | null > >();
const mockGenerateProposedSitePath =
jest.fn< ( siteName: string ) => Promise< FolderDialogResponse > >();
const mockGetAllCustomDomains = jest.fn< () => Promise< string[] > >().mockResolvedValue( [] );

jest.mock( 'src/lib/get-ipc-api', () => ( {
__esModule: true,
default: jest.fn(),
getIpcApi: () => ( {
showOpenFolderDialog: mockShowOpenFolderDialog,
generateProposedSitePath: mockGenerateProposedSitePath,
getAllCustomDomains: mockGetAllCustomDomains,
} ),
} ) );

Expand Down
3 changes: 3 additions & 0 deletions src/components/tests/content-tab-settings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,16 @@ describe( 'ContentTabSettings', () => {
const copyText = jest.fn();
const openLocalPath = jest.fn();
const generateProposedSitePath = jest.fn();
const getAllCustomDomains = jest.fn().mockResolvedValue( [] );

beforeEach( () => {
jest.clearAllMocks();
( useGetWpVersion as jest.Mock ).mockReturnValue( [ '7.7.7', jest.fn() ] );
( getIpcApi as jest.Mock ).mockReturnValue( {
copyText,
openLocalPath,
generateProposedSitePath,
getAllCustomDomains,
} );

( useSnapshots as jest.Mock ).mockReturnValue( {
Expand Down
1 change: 1 addition & 0 deletions src/components/tests/main-sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jest.mock( 'src/lib/get-ipc-api', () => ( {
showOpenFolderDialog: jest.fn(),
generateProposedSitePath: jest.fn(),
openURL: mockOpenURL,
getAllCustomDomains: jest.fn().mockResolvedValue( [] ),
} ),
} ) );

Expand Down
127 changes: 39 additions & 88 deletions src/components/tests/onboarding.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,40 @@ const mockCreateSite = jest.fn();

describe( 'Onboarding Component', () => {
const user = userEvent.setup();
const useAddSiteMockValue = {
setSiteName: jest.fn(),
setProposedSitePath: jest.fn(),
setSitePath: jest.fn(),
setError: jest.fn(),
setDoesPathContainWordPress: jest.fn(),
setPhpVersion: jest.fn(),
setWpVersion: jest.fn(),
siteName: 'My Site',
sitePath: '/path/to/my/site',
phpVersion: '8.0',
wpVersion: DEFAULT_WORDPRESS_VERSION,
error: '',
doesPathContainWordPress: false,
handleAddSiteClick: jest.fn(),
handleSiteNameChange: jest.fn(),
handlePathSelectorClick: jest.fn(),
onSelectPath: jest.fn(),
setFileForImport: jest.fn(),
fileForImport: null,
isAdvancedSettingsVisible: true,
handleSubmit: jest.fn( () => {
mockCreateSite( '/path/to/my/site', 'My Site', DEFAULT_WORDPRESS_VERSION );
} ),
setUseCustomDomain: jest.fn(),
useCustomDomain: false,
customDomain: '',
setCustomDomain: jest.fn(),
setCustomDomainError: jest.fn(),
customDomainError: '',
enableHttps: false,
setEnableHttps: jest.fn(),
loadAllCustomDomains: jest.fn(),
};

beforeEach( () => {
jest.clearAllMocks();
Expand All @@ -76,39 +110,7 @@ describe( 'Onboarding Component', () => {
( useOnboarding as jest.Mock ).mockReturnValue( {
needsOnboarding: true,
} );
( useAddSite as jest.Mock ).mockReturnValue( {
setSiteName: jest.fn(),
setProposedSitePath: jest.fn(),
setSitePath: jest.fn(),
setError: jest.fn(),
setDoesPathContainWordPress: jest.fn(),
setPhpVersion: jest.fn(),
setWpVersion: jest.fn(),
siteName: 'My Site',
sitePath: '/path/to/my/site',
phpVersion: '8.0',
wpVersion: DEFAULT_WORDPRESS_VERSION,
error: '',
doesPathContainWordPress: false,
handleAddSiteClick: jest.fn(),
handleSiteNameChange: jest.fn(),
handlePathSelectorClick: jest.fn(),
onSelectPath: jest.fn(),
setFileForImport: jest.fn(),
fileForImport: null,
isAdvancedSettingsVisible: true,
handleSubmit: jest.fn( () => {
mockCreateSite( '/path/to/my/site', 'My Site', DEFAULT_WORDPRESS_VERSION );
} ),
setUseCustomDomain: jest.fn(),
useCustomDomain: false,
customDomain: '',
setCustomDomain: jest.fn(),
setCustomDomainError: jest.fn(),
customDomainError: '',
enableHttps: false,
setEnableHttps: jest.fn(),
} );
( useAddSite as jest.Mock ).mockReturnValue( useAddSiteMockValue );
} );

it( 'renders onboarding screen correctly', () => {
Expand Down Expand Up @@ -160,34 +162,8 @@ describe( 'Onboarding Component', () => {
const mockSetWpVersion = jest.fn();

( useAddSite as jest.Mock ).mockReturnValue( {
setSiteName: jest.fn(),
setProposedSitePath: jest.fn(),
setSitePath: jest.fn(),
setError: jest.fn(),
setDoesPathContainWordPress: jest.fn(),
setPhpVersion: jest.fn(),
...useAddSiteMockValue,
setWpVersion: mockSetWpVersion,
siteName: 'My Site',
sitePath: '/path/to/my/site',
phpVersion: '8.0',
wpVersion: DEFAULT_WORDPRESS_VERSION,
error: '',
doesPathContainWordPress: false,
handleAddSiteClick: jest.fn(),
handleSiteNameChange: jest.fn(),
handlePathSelectorClick: jest.fn(),
onSelectPath: jest.fn(),
setFileForImport: jest.fn(),
fileForImport: null,
isAdvancedSettingsVisible: true,
setUseCustomDomain: jest.fn(),
useCustomDomain: false,
customDomain: '',
setCustomDomain: jest.fn(),
setCustomDomainError: jest.fn(),
customDomainError: '',
enableHttps: false,
setEnableHttps: jest.fn(),
} );

render( <Onboarding /> );
Expand Down Expand Up @@ -234,41 +210,15 @@ describe( 'Onboarding Component', () => {
return { status: 'succeeded' };
} );

const mockSetWpVersion = jest.fn();
const mockHandleAddSiteClick = jest.fn().mockImplementation( () => {
mockCreateSite( '/path/to/my/site', 'My Site', '6.3.3' );
return Promise.resolve();
} );

( useAddSite as jest.Mock ).mockReturnValue( {
setSiteName: jest.fn(),
setProposedSitePath: jest.fn(),
setSitePath: jest.fn(),
setError: jest.fn(),
setDoesPathContainWordPress: jest.fn(),
setPhpVersion: jest.fn(),
setWpVersion: mockSetWpVersion,
siteName: 'My Site',
sitePath: '/path/to/my/site',
phpVersion: '8.0',
wpVersion: '6.3.3', // Changed from default
error: '',
doesPathContainWordPress: false,
...useAddSiteMockValue,
handleAddSiteClick: mockHandleAddSiteClick,
handleSiteNameChange: jest.fn(),
handlePathSelectorClick: jest.fn(),
onSelectPath: jest.fn(),
setFileForImport: jest.fn(),
fileForImport: null,
isAdvancedSettingsVisible: true,
setUseCustomDomain: jest.fn(),
useCustomDomain: false,
customDomain: '',
setCustomDomain: jest.fn(),
setCustomDomainError: jest.fn(),
customDomainError: '',
enableHttps: false,
setEnableHttps: jest.fn(),
wpVersion: '6.3.3',
} );

render( <Onboarding /> );
Expand Down Expand Up @@ -335,6 +285,7 @@ describe( 'Onboarding Component', () => {
customDomainError: '',
enableHttps: false,
setEnableHttps: jest.fn(),
loadAllCustomDomains: jest.fn(),
} );

render( <Onboarding /> );
Expand Down
1 change: 1 addition & 0 deletions src/hooks/tests/use-add-site.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jest.mock( 'src/lib/get-ipc-api', () => ( {
isWordPress: false,
} ),
showNotification: jest.fn(),
getAllCustomDomains: jest.fn().mockResolvedValue( [] ),
} ),
} ) );

Expand Down
24 changes: 20 additions & 4 deletions src/hooks/use-add-site.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as Sentry from '@sentry/electron/renderer';
import { useI18n } from '@wordpress/react-i18n';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useImportExport } from 'src/hooks/use-import-export';
import { useSiteDetails } from 'src/hooks/use-site-details';
import { generateCustomDomainFromSiteName, validateDomainName } from 'src/lib/domains';
import { generateCustomDomainFromSiteName, getDomainNameValidationError } from 'src/lib/domains';
import { getIpcApi } from 'src/lib/get-ipc-api';
import {
DEFAULT_PHP_VERSION,
Expand All @@ -28,8 +28,20 @@ export function useAddSite() {
const [ useCustomDomain, setUseCustomDomain ] = useState( false );
const [ customDomain, setCustomDomain ] = useState< string | null >( null );
const [ customDomainError, setCustomDomainError ] = useState( '' );
const [ existingDomainNames, setExistingDomainNames ] = useState< string[] >( [] );
const [ enableHttps, setEnableHttps ] = useState( false );

const loadAllCustomDomains = useCallback( () => {
getIpcApi()
.getAllCustomDomains()
.then( ( domains ) => {
setExistingDomainNames( domains );
} )
.catch( () => {
// Do nothing
} );
}, [] );

const siteWithPathAlreadyExists = useCallback(
( path: string ) => {
return sites.some( ( site ) => site.path.toLowerCase() === path.toLowerCase() );
Expand All @@ -40,9 +52,11 @@ export function useAddSite() {
const handleCustomDomainChange = useCallback(
( value: string | null ) => {
setCustomDomain( value );
setCustomDomainError( validateDomainName( useCustomDomain, value ) );
setCustomDomainError(
getDomainNameValidationError( useCustomDomain, value, existingDomainNames )
);
},
[ useCustomDomain, setCustomDomain, setCustomDomainError ]
[ useCustomDomain, setCustomDomain, setCustomDomainError, existingDomainNames ]
);

const handlePathSelectorClick = useCallback( async () => {
Expand Down Expand Up @@ -222,6 +236,7 @@ export function useAddSite() {
setCustomDomainError,
enableHttps,
setEnableHttps,
loadAllCustomDomains,
};
}, [
__,
Expand All @@ -247,5 +262,6 @@ export function useAddSite() {
setCustomDomainError,
enableHttps,
setEnableHttps,
loadAllCustomDomains,
] );
}
8 changes: 8 additions & 0 deletions src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1236,3 +1236,11 @@ export async function isFullscreen( _event: IpcMainInvokeEvent ): Promise< boole
const window = await getMainWindow();
return window.isFullScreen();
}

export async function getAllCustomDomains(): Promise< string[] > {
const userData = await loadUserData();

return userData.sites
.map( ( site ) => site.customDomain )
.filter( ( domain ): domain is string => domain !== undefined );
}
28 changes: 18 additions & 10 deletions src/lib/domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,34 @@ export const generateCustomDomainFromSiteName = ( siteName: string ): string =>
return `${ domainBase }.wp.local`;
};

export const validateDomainName = (
export const getDomainNameValidationError = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice rename and validation improvements.

useCustomDomain: boolean,
domainName: string | null
domainName: string | null,
existingDomainNames: string[]
): string => {
// Validate custom domain if enabled
if ( ! useCustomDomain ) {
return '';
}

if ( ! domainName ) {
return __( 'The domain name is required' );
}

if ( existingDomainNames.includes( domainName ) ) {
return __( 'The domain name is already in use' );
}

const domainPattern =
/^(?!-)[\p{L}\p{N}][\p{L}\p{N}-]{0,61}[\p{L}\p{N}](?<!-)(?:\.(?!-)[\p{L}\p{N}-]{1,61}[\p{L}\p{N}](?<!-))+$/u;
if ( useCustomDomain && domainName && ! domainPattern.test( domainName ) ) {
if ( ! domainPattern.test( domainName ) ) {
return __( 'Please enter a valid domain name' );
}

if ( useCustomDomain && domainName && domainName.length > 253 ) {
if ( domainName.length > 253 ) {
return __( 'The domain name is too long' );
}

if ( useCustomDomain && domainName === '' ) {
return __( 'The domain name is required' );
}

if ( useCustomDomain && domainName && ! domainName.endsWith( '.local' ) ) {
if ( ! domainName.endsWith( '.local' ) ) {
return __( 'The domain name must end with .local' );
}

Expand Down
Loading