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

Allow PHP version to be changed from Site Settings #225

Merged
merged 9 commits into from
Jun 17, 2024
20 changes: 20 additions & 0 deletions jest-setup.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
import '@testing-library/jest-dom';
// We need this polyfill because the `ReadableStream` class is
// used by `@php-wasm/universal` and it's not available in the Jest environment.
// eslint-disable-next-line import/no-unresolved
import 'web-streams-polyfill/polyfill';
import nock from 'nock';

if ( typeof window !== 'undefined' ) {
// The ipcListener global is usually defined in preload.ts
window.ipcListener = { subscribe: jest.fn() };

// Mock `matchMedia` as it's not implemented in JSDOM
// Reference: https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
Object.defineProperty( window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation( ( query ) => ( {
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
} ) ),
} );
}

nock.disableNetConnect();
Expand Down
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"ts-loader": "^9.2.2",
"ts-node": "^10.0.0",
"typescript": "~5.3.2",
"web-streams-polyfill": "^4.0.0",
"webpack-dev-middleware": "5.3.4"
},
"dependencies": {
Expand Down
9 changes: 9 additions & 0 deletions src/components/content-tab-settings.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Icon, file } from '@wordpress/icons';
import { useI18n } from '@wordpress/react-i18n';
import { PropsWithChildren } from 'react';
import { DEFAULT_PHP_VERSION } from '../../vendor/wp-now/src/constants';
import { useGetWpVersion } from '../hooks/use-get-wp-version';
import { getIpcApi } from '../lib/get-ipc-api';
import { decodePassword } from '../lib/passwords';
import Button from './button';
import { CopyTextButton } from './copy-text-button';
import DeleteSite from './delete-site';
import EditPhpVersion from './edit-php-version';
import EditSite from './edit-site';

interface ContentTabSettingsProps {
Expand All @@ -30,6 +32,7 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps )
// Empty strings account for legacy sites lacking a stored password.
const storedPassword = decodePassword( selectedSite.adminPassword ?? '' );
const password = storedPassword === '' ? 'password' : storedPassword;
const phpVersion = selectedSite.phpVersion ?? DEFAULT_PHP_VERSION;
const wpVersion = useGetWpVersion( selectedSite );
return (
<div className="p-8">
Expand Down Expand Up @@ -67,6 +70,12 @@ export function ContentTabSettings( { selectedSite }: ContentTabSettingsProps )
</Button>
</SettingsRow>
<SettingsRow label={ __( 'WP Version' ) }>{ wpVersion }</SettingsRow>
<SettingsRow label={ __( 'PHP Version' ) }>
<div className="flex">
<span className="line-clamp-1 break-all">{ phpVersion }</span>
<EditPhpVersion />
</div>
</SettingsRow>

<tr>
<th colSpan={ 2 } className="pb-4 ltr:text-left rtl:text-right">
Expand Down
116 changes: 116 additions & 0 deletions src/components/edit-php-version.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { SupportedPHPVersions } from '@php-wasm/universal';
import { SelectControl } from '@wordpress/components';
import { useI18n } from '@wordpress/react-i18n';
import { FormEvent, useCallback, useEffect, useState } from 'react';
import { DEFAULT_PHP_VERSION } from '../../vendor/wp-now/src/constants';
import { useSiteDetails } from '../hooks/use-site-details';
import Button from './button';
import Modal from './modal';

export default function EditPhpVersion() {
const { __ } = useI18n();
const { updateSite, selectedSite, stopServer, startServer } = useSiteDetails();
const [ editPhpVersionError, setEditPhpVersionError ] = useState( '' );
const [ selectedPhpVersion, setSelectedPhpVersion ] = useState( DEFAULT_PHP_VERSION );
const [ needsToEditPhpVersion, setNeedsToEditPhpVersion ] = useState( false );
const [ isEditingSite, setIsEditingSite ] = useState( false );

useEffect( () => {
if ( selectedSite ) {
setSelectedPhpVersion( selectedSite.phpVersion );
}
}, [ selectedSite ] );

const resetLocalState = useCallback( () => {
setNeedsToEditPhpVersion( false );
setSelectedPhpVersion( '' );
setEditPhpVersionError( '' );
}, [] );

const onSiteEdit = useCallback(
async ( event: FormEvent ) => {
event.preventDefault();
if ( ! selectedSite ) {
return;
}
setIsEditingSite( true );
try {
const running = selectedSite.running;
await updateSite( {
...selectedSite,
phpVersion: selectedPhpVersion,
} );
if ( running ) {
await stopServer( selectedSite.id );
await startServer( selectedSite.id );
}
setNeedsToEditPhpVersion( false );
resetLocalState();
} catch ( e ) {
setEditPhpVersionError( ( e as Error )?.message );
}
setIsEditingSite( false );
},
[ updateSite, selectedSite, selectedPhpVersion, resetLocalState, startServer, stopServer ]
);

return (
<>
{ needsToEditPhpVersion && (
<Modal
size="medium"
title={ __( 'Edit PHP version' ) }
isDismissible
focusOnMount="firstContentElement"
onRequestClose={ resetLocalState }
>
<form onSubmit={ onSiteEdit }>
<label className="flex flex-col gap-1.5 leading-4">
<span className="font-semibold">{ __( 'PHP version' ) }</span>
<SelectControl
value={ selectedPhpVersion }
options={ SupportedPHPVersions.map( ( version ) => ( {
label: version,
value: version,
} ) ) }
onChange={ ( version ) => setSelectedPhpVersion( version ) }
/>
</label>
<div className="flex flex-row justify-end gap-x-5 mt-6">
<Button onClick={ resetLocalState } disabled={ isEditingSite } variant="tertiary">
{ __( 'Cancel' ) }
</Button>
<Button
type="submit"
variant="primary"
isBusy={ isEditingSite }
disabled={ Boolean(
isEditingSite ||
! selectedSite ||
selectedSite?.phpVersion === selectedPhpVersion ||
editPhpVersionError
) }
>
{ isEditingSite ? __( 'Restarting server…' ) : __( 'Save' ) }
</Button>
</div>
</form>
</Modal>
) }
<Button
disabled={ ! selectedSite }
className="!mx-4 shrink-0"
onClick={ () => {
if ( selectedSite ) {
setSelectedPhpVersion( selectedSite.phpVersion );
}
setNeedsToEditPhpVersion( true );
} }
label={ __( 'Edit PHP version' ) }
variant="link"
>
{ __( 'Edit' ) }
</Button>
</>
);
}
1 change: 1 addition & 0 deletions src/components/tests/content-tab-assistant.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const runningSite = {
port: 8881,
path: '/path/to/site',
running: true,
phpVersion: '8.0',
id: 'site-id',
url: 'http://example.com',
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const selectedSite: StartedSiteDetails = {
port: 8881,
path: '/path/to/site',
running: true,
phpVersion: '8.0',
id: 'site-id',
url: 'http://example.com',
};
Expand Down
2 changes: 2 additions & 0 deletions src/components/tests/content-tab-overview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const runningSite: StartedSiteDetails = {
name: 'Test Site',
port: 8881,
path: '/path/to/site',
phpVersion: '8.0',
running: true,
id: 'site-id',
url: 'http://example.com',
Expand All @@ -18,6 +19,7 @@ const notRunningSite: SiteDetails = {
name: 'Test Site',
port: 8881,
path: '/path/to/site',
phpVersion: '8.0',
running: false,
id: 'site-id',
};
Expand Down
83 changes: 82 additions & 1 deletion src/components/tests/content-tab-settings.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// To run tests, execute `npm run test -- src/components/content-tab-settings.test.tsx` from the root directory
import { fireEvent, render, screen } from '@testing-library/react';
import { fireEvent, render, screen, within } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { useGetWpVersion } from '../../hooks/use-get-wp-version';
import { useOffline } from '../../hooks/use-offline';
Expand All @@ -17,6 +17,7 @@ const selectedSite: SiteDetails = {
path: '/path/to/site',
adminPassword: btoa( 'test-password' ),
running: false,
phpVersion: '8.0',
id: 'site-id',
};

Expand Down Expand Up @@ -148,4 +149,84 @@ describe( 'ContentTabSettings', () => {
expect( copyText ).toHaveBeenCalledWith( 'password' );
} );
} );

describe( 'PHP version', () => {
it( 'changes PHP version when site is not running', async () => {
const user = userEvent.setup();

const updateSite = jest.fn();
const startServer = jest.fn();
const stopServer = jest.fn();
// Mock snapshots to include a snapshot for the selected site
( useSiteDetails as jest.Mock ).mockReturnValue( {
selectedSite: { ...selectedSite, running: false } as SiteDetails,
snapshots: [ { localSiteId: selectedSite.id } ],
updateSite,
startServer,
stopServer,
} );

const { rerender } = render( <ContentTabSettings selectedSite={ selectedSite } /> );
expect( screen.getByText( '8.0' ) ).toBeVisible();
await user.click( screen.getByRole( 'button', { name: 'Edit PHP version' } ) );
const dialog = screen.getByRole( 'dialog' );
expect( dialog ).toBeVisible();
await user.selectOptions(
within( dialog ).getByRole( 'combobox', {
name: 'PHP version',
} ),
'8.2'
);
await user.click(
within( dialog ).getByRole( 'button', {
name: 'Save',
} )
);
expect( updateSite ).toHaveBeenCalledWith( expect.objectContaining( { phpVersion: '8.2' } ) );
expect( stopServer ).not.toHaveBeenCalled();
expect( startServer ).not.toHaveBeenCalled();

rerender( <ContentTabSettings selectedSite={ { ...selectedSite, phpVersion: '8.2' } } /> );
expect( screen.getByText( '8.2' ) ).toBeVisible();
} );

it( 'changes PHP version and restarts site when site is running', async () => {
const user = userEvent.setup();

const updateSite = jest.fn();
const startServer = jest.fn();
const stopServer = jest.fn();
// Mock snapshots to include a snapshot for the selected site
( useSiteDetails as jest.Mock ).mockReturnValue( {
selectedSite: { ...selectedSite, running: true } as SiteDetails,
snapshots: [ { localSiteId: selectedSite.id } ],
updateSite,
startServer,
stopServer,
} );

const { rerender } = render( <ContentTabSettings selectedSite={ selectedSite } /> );
expect( screen.getByText( '8.0' ) ).toBeVisible();
await user.click( screen.getByRole( 'button', { name: 'Edit PHP version' } ) );
const dialog = screen.getByRole( 'dialog' );
expect( dialog ).toBeVisible();
await user.selectOptions(
within( dialog ).getByRole( 'combobox', {
name: 'PHP version',
} ),
'8.2'
);
await user.click(
within( dialog ).getByRole( 'button', {
name: 'Save',
} )
);
expect( updateSite ).toHaveBeenCalledWith( expect.objectContaining( { phpVersion: '8.2' } ) );
expect( stopServer ).toHaveBeenCalled();
expect( startServer ).toHaveBeenCalled();

rerender( <ContentTabSettings selectedSite={ { ...selectedSite, phpVersion: '8.2' } } /> );
expect( screen.getByText( '8.2' ) ).toBeVisible();
} );
} );
} );
1 change: 1 addition & 0 deletions src/components/tests/content-tab-snapshots.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ const selectedSite = {
name: 'Test Site',
running: false as const,
path: '/test-site',
phpVersion: '8.0',
adminPassword: btoa( 'test-password' ),
};

Expand Down
1 change: 1 addition & 0 deletions src/hooks/tests/use-update-demo-site.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describe( 'useUpdateDemoSite', () => {
const mockLocalSite: SiteDetails = {
name: 'Test Site',
running: false,
phpVersion: '8.0',
id: '54321',
path: '/path/to/site',
};
Expand Down
3 changes: 2 additions & 1 deletion src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import nodePath from 'path';
import * as Sentry from '@sentry/electron/main';
import archiver from 'archiver';
import { copySync } from 'fs-extra';
import { SQLITE_FILENAME } from '../vendor/wp-now/src/constants';
import { SQLITE_FILENAME, DEFAULT_PHP_VERSION } from '../vendor/wp-now/src/constants';
import { downloadSqliteIntegrationPlugin } from '../vendor/wp-now/src/download';
import { executeWPCli } from '../vendor/wp-now/src/execute-wp-cli';
import { LIMIT_ARCHIVE_SIZE } from './constants';
Expand Down Expand Up @@ -157,6 +157,7 @@ export async function createSite(
path,
adminPassword: createPassword(),
running: false,
phpVersion: DEFAULT_PHP_VERSION,
} as const;

const server = SiteServer.create( details );
Expand Down
Loading
Loading