Skip to content
6 changes: 5 additions & 1 deletion e2e/page-objects/add-site-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ export default class AddSiteModal {
constructor( private page: Page ) {}

get locator() {
return this.page.getByRole( 'dialog', { name: 'Add a site' } );
return this.page.getByRole( 'dialog' );
}

get createSiteButton() {
return this.page.locator( 'button:has-text("Create a site")' ).first();
}

private get siteForm() {
Expand Down
8 changes: 6 additions & 2 deletions e2e/sites.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ test.describe( 'Servers', () => {
const sidebar = new MainSidebar( session.mainWindow );
const modal = await sidebar.openAddSiteModal();

await expect( modal.createSiteButton ).toBeVisible();
await modal.createSiteButton.click();

await modal.siteNameInput.fill( siteName );
await modal.addSiteButton.click();

Expand All @@ -65,7 +68,7 @@ test.describe( 'Servers', () => {
const response = await new Promise< http.IncomingMessage >( ( resolve, reject ) => {
http.get( `http://${ frontendUrl }`, resolve ).on( 'error', reject );
} );
expect( response.statusCode ).toBe( 200 );
expect( [ 200, 302 ] ).toContain( response.statusCode );
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks weird. So sometimes when we run test we get 200 and sometimes 302?

Copy link
Contributor

Choose a reason for hiding this comment

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

In my testing I received only 302 which is about the content being temporarily moved. I added both here for robustness, as I think the WP frontend can respond either one.

expect( response.headers[ 'content-type' ] ).toMatch( /text\/html/ );
} );

Expand Down Expand Up @@ -103,8 +106,9 @@ test.describe( 'Servers', () => {

await session.mainWindow.waitForTimeout( 200 ); // Short pause for site to delete.

expect( await pathExists( path.join( session.homePath, 'Studio', siteName ) ) ).toBe( false );
const sidebar = new MainSidebar( session.mainWindow );
await expect( sidebar.getSiteNavButton( siteName ) ).not.toBeAttached();

expect( await pathExists( path.join( session.homePath, 'Studio', siteName ) ) ).toBe( false );
} );
} );
2 changes: 0 additions & 2 deletions forge.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,6 @@ const config: ForgeConfig = {
// By default the dev server uses the same port as calypso.localhost
port: 3456,
} ),
// This plugin bundles the externals defined in the Webpack config file.
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add comment about - "why do we need it, in our case?", instead of "What does it do?".
Since now we have just description of the plugin, what is useless for devs who will be working around this code in the future.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry I don't get it fully, this comment was removed, so where should we add this explanation?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, it was removed, not added, sorry 😅

new ForgeExternalsPlugin( { externals: Object.keys( mainBaseConfig.externals ?? {} ) } ),
],
hooks: {
generateAssets: async () => {
Expand Down
5 changes: 3 additions & 2 deletions metrics/tests/site-editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,9 @@ test.describe( 'Site Editor Load Metrics', () => {
// Setup WordPress site
const onboarding = new Onboarding( session.mainWindow );
await expect( onboarding.heading ).toBeVisible();

// Wait for store initialization to complete (provider constants loading)
await new Promise( ( resolve ) => setTimeout( resolve, 500 ) );

await onboarding.siteNameInput.fill( siteName );
await onboarding.continueButton.click();

// Handle the What's New modal if it appears
Expand All @@ -46,6 +45,8 @@ test.describe( 'Site Editor Load Metrics', () => {
}

const siteContent = new SiteContent( session.mainWindow, siteName );

await expect( siteContent.siteNameHeading ).toBeVisible();
await expect( siteContent.runningButton ).toBeAttached();

// Get the WordPress admin URL from settings
Expand Down
8 changes: 6 additions & 2 deletions src/lib/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const FEATURE_FLAGS_DEFINITION: Record< keyof FeatureFlags, FeatureFlagDe
label: 'Enable Blueprints',
env: 'ENABLE_BLUEPRINTS',
flag: 'enableBlueprints',
default: false,
default: true,
},
} as const;

Expand All @@ -22,7 +22,11 @@ export function getFeatureFlagFromEnv( flag: keyof FeatureFlags ): boolean {
if ( ! flagDefinition ) {
return false;
}
return process.env[ flagDefinition.env ] === 'true';
const envValue = process.env[ flagDefinition.env ];
if ( envValue === undefined ) {
return flagDefinition.default;
}
return envValue === 'true';
}

export function setFeatureFlagInEnv( flag: keyof FeatureFlags, value: boolean ): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { recursiveCopyDirectory, pathExists } from 'src/lib/fs-utils';
import { installSqliteIntegration } from 'src/lib/sqlite-versions';
import { isValidWordPressVersion } from 'src/lib/wordpress-version-utils';
import { SiteServer } from 'src/site-server';
import { getResourcesPath } from 'src/storage/paths';
import { getResourcesPath, getServerFilesPath } from 'src/storage/paths';
import {
WordPressProvider,
WordPressServerInstance,
Expand Down Expand Up @@ -103,7 +103,7 @@ export class PlaygroundCliProvider implements WordPressProvider {
}

getSqlitePath(): string {
return nodePath.join( getResourcesPath(), 'wp-files', this.SQLITE_FILENAME );
return nodePath.join( getServerFilesPath(), this.SQLITE_FILENAME );
}

getWpLoadPath( _serverProcess: WordPressServerProcess ): string {
Expand Down
66 changes: 52 additions & 14 deletions src/modules/add-site/tests/add-site.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Run tests: yarn test -- src/components/add-site-button.test.tsx
import { jest } from '@jest/globals';
import { render, waitFor, screen } from '@testing-library/react';
import { render, waitFor, screen, within } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { Provider } from 'react-redux';
import { useOffline } from 'src/hooks/use-offline';
Expand Down Expand Up @@ -120,7 +120,7 @@ describe( 'AddSite', () => {
jest.clearAllMocks();
} );

it( 'should dismiss the modal when the cancel button is activated via keyboard', async () => {
it( 'should dismiss the modal when the close button is activated via keyboard', async () => {
const user = userEvent.setup();
mockGenerateProposedSitePath.mockResolvedValue( {
path: '/default_path/my-wordpress-website',
Expand All @@ -131,24 +131,25 @@ describe( 'AddSite', () => {
renderWithProvider( <AddSite /> );

await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );
await user.click( screen.getByRole( 'heading', { name: 'Create a site' } ) );

// Find the Cancel button
const cancelButton = screen.getByRole( 'button', { name: 'Cancel' } );
expect( cancelButton ).toBeInTheDocument();
// Find the Close button
const closeButton = screen.getByRole( 'button', { name: 'Close' } );
expect( closeButton ).toBeInTheDocument();

// Tab until we reach the Cancel button
// Tab until we reach the Closes button
let currentButton;
do {
await user.tab();
currentButton = document.activeElement;
} while ( currentButton !== cancelButton );
} while ( currentButton !== closeButton );

await user.keyboard( '{Enter}' );
expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument();
expect( mockCreateSite ).not.toHaveBeenCalled();
} );

it( 'calls createSite with selected path when add site button is clicked', async () => {
it( 'calls createSite with selected path when create a site button is clicked', async () => {
const user = userEvent.setup();
mockGenerateProposedSitePath.mockResolvedValue( {
path: '/default_path/my-wordpress-website',
Expand All @@ -167,11 +168,16 @@ describe( 'AddSite', () => {

await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );
expect( screen.getByRole( 'dialog' ) ).toBeVisible();

await user.click( screen.getByRole( 'button', { name: 'Create a site Create a clean site' } ) );

await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) );
await user.click( screen.getByTestId( 'select-path-button' ) );

expect( mockShowOpenFolderDialog ).toHaveBeenCalledWith( 'Choose folder for site', '' );
await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );
const dialog = screen.getByRole( 'dialog' );
const addSiteButton = within( dialog ).getByRole( 'button', { name: 'Add site' } );
await user.click( addSiteButton );

await waitFor( () => {
expect( mockCreateSite ).toHaveBeenCalledWith(
Expand Down Expand Up @@ -202,15 +208,20 @@ describe( 'AddSite', () => {
} );
renderWithProvider( <AddSite /> );

await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );
await user.click( screen.getAllByRole( 'button', { name: 'Add site' } )[ 0 ] );
expect( screen.getByRole( 'dialog' ) ).toBeVisible();

await user.click( screen.getByRole( 'button', { name: 'Create a site Create a clean site' } ) );

await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) );
await user.click( screen.getByTestId( 'select-path-button' ) );

expect( mockShowOpenFolderDialog ).toHaveBeenCalledWith( 'Choose folder for site', '' );

await waitFor( () => {
expect( screen.getByRole( 'button', { name: 'Add site' } ) ).toBeDisabled();
const dialog = screen.getByRole( 'dialog' );
const addSiteButton = within( dialog ).getByRole( 'button', { name: 'Add site' } );
expect( addSiteButton ).toBeDisabled();
expect( screen.getByRole( 'alert' ) ).toHaveTextContent(
'This directory is not empty. Please select an empty directory or an existing WordPress folder.'
);
Expand All @@ -236,13 +247,18 @@ describe( 'AddSite', () => {

await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );
expect( screen.getByRole( 'dialog' ) ).toBeVisible();

await user.click( screen.getByRole( 'button', { name: 'Create a site Create a clean site' } ) );

await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) );
await user.click( screen.getByTestId( 'select-path-button' ) );

expect( mockShowOpenFolderDialog ).toHaveBeenCalledWith( 'Choose folder for site', '' );

await waitFor( () => {
expect( screen.getByRole( 'button', { name: 'Add site' } ) ).not.toBeDisabled();
const dialog = screen.getByRole( 'dialog' );
const addSiteButton = within( dialog ).getByRole( 'button', { name: 'Add site' } );
expect( addSiteButton ).not.toBeDisabled();
expect( screen.getByRole( 'alert' ) ).toHaveTextContent(
'The existing WordPress site at this path will be added.'
);
Expand All @@ -265,6 +281,8 @@ describe( 'AddSite', () => {
await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );
expect( screen.getByRole( 'dialog' ) ).toBeVisible();

await user.click( screen.getByRole( 'button', { name: 'Create a site Create a clean site' } ) );

const siteNameInput = screen.getByDisplayValue( 'My WordPress Website' );
await user.click( siteNameInput );
await user.type( siteNameInput, ' changed' );
Expand All @@ -281,6 +299,9 @@ describe( 'AddSite', () => {
} );
await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );
expect( screen.getByRole( 'dialog' ) ).toBeVisible();

await user.click( screen.getByRole( 'button', { name: 'Create a site Create a clean site' } ) );

expect( screen.getByDisplayValue( 'My WordPress Website' ) ).toBeVisible();
await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) );
expect( screen.getByDisplayValue( '/default_path/my-wordpress-website' ) ).toBeVisible();
Expand All @@ -306,6 +327,9 @@ describe( 'AddSite', () => {
renderWithProvider( <AddSite /> );

await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );

await user.click( screen.getByRole( 'button', { name: 'Create a site Create a clean site' } ) );

await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) );
await user.click( screen.getByTestId( 'select-path-button' ) );

Expand Down Expand Up @@ -336,6 +360,9 @@ describe( 'AddSite', () => {
renderWithProvider( <AddSite /> );

await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );

await user.click( screen.getByRole( 'button', { name: 'Create a site Create a clean site' } ) );

await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) );

expect( screen.getByText( 'WordPress version' ) ).toBeInTheDocument();
Expand All @@ -355,7 +382,9 @@ describe( 'AddSite', () => {
isWordPress: false,
} );
await user.click( screen.getByTestId( 'select-path-button' ) );
await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );
const dialog = screen.getByRole( 'dialog' );
const addSiteButton = within( dialog ).getByRole( 'button', { name: 'Add site' } );
await user.click( addSiteButton );

await waitFor( () => {
expect( mockCreateSite ).toHaveBeenCalledWith(
Expand All @@ -382,6 +411,9 @@ describe( 'AddSite', () => {
renderWithProvider( <AddSite /> );

await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );

await user.click( screen.getByRole( 'button', { name: 'Create a site Create a clean site' } ) );

await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) );

expect( screen.getByText( 'PHP version' ) ).toBeInTheDocument();
Expand All @@ -401,7 +433,9 @@ describe( 'AddSite', () => {
isWordPress: false,
} );
await user.click( screen.getByTestId( 'select-path-button' ) );
await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );
const dialog = screen.getByRole( 'dialog' );
const addSiteButton = within( dialog ).getByRole( 'button', { name: 'Add site' } );
await user.click( addSiteButton );

await waitFor( () => {
expect( mockCreateSite ).toHaveBeenCalled();
Expand All @@ -415,6 +449,7 @@ describe( 'AddSite', () => {
const user = userEvent.setup();

await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );
await user.click( screen.getByRole( 'button', { name: 'Create a site Create a clean site' } ) );
await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) );

const wpVersionSelect = screen.getByLabelText( 'WordPress version' );
Expand All @@ -428,6 +463,7 @@ describe( 'AddSite', () => {
const user = userEvent.setup();

await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );
await user.click( screen.getByRole( 'button', { name: 'Create a site Create a clean site' } ) );
await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) );

const wpVersionSelect = screen.getByLabelText( 'WordPress version' );
Expand All @@ -441,6 +477,7 @@ describe( 'AddSite', () => {
const user = userEvent.setup();

await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );
await user.click( screen.getByRole( 'button', { name: 'Create a site Create a clean site' } ) );
await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) );

const wpVersionSelect = screen.getByLabelText( 'WordPress version' );
Expand All @@ -460,6 +497,7 @@ describe( 'AddSite', () => {
const user = userEvent.setup();

await user.click( screen.getByRole( 'button', { name: 'Add site' } ) );
await user.click( screen.getByRole( 'button', { name: 'Create a site Create a clean site' } ) );
await user.click( screen.getByRole( 'button', { name: 'Advanced settings' } ) );

const wpVersionSelect = screen.getByLabelText( 'WordPress version' );
Expand Down
6 changes: 2 additions & 4 deletions src/storage/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,8 @@ function getAppDataPath(): string {
return process.env.STUDIO_APP_DATA_PATH;
}
if ( process.env.E2E && process.env.E2E_APP_DATA_PATH ) {
if ( ! app ) {
throw new Error( 'Electron app not available in child process' );
}
return path.join( process.env.E2E_APP_DATA_PATH, app.getName(), 'appdata-v1.json' );
// In E2E mode, return the base appData path directly. Callers append app name and subpaths.
return process.env.E2E_APP_DATA_PATH;
}
if ( ! app ) {
throw new Error( 'Electron app not available in child process' );
Expand Down
12 changes: 9 additions & 3 deletions webpack.main.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,15 @@ export const mainBaseConfig: Configuration = {
from: path.join( phpWasmDir, dir ),
to: path.resolve( __dirname, `.webpack/main/${ dir }` ),
} ) ),
// Copy @wp-playground/cli worker files
{
from: path.resolve( __dirname, 'node_modules/@wp-playground/cli' ),
to: path.resolve( __dirname, '.webpack/main' ),
filter: ( resourcePath: string ) => {
const fileName = path.basename( resourcePath );
return fileName.startsWith( 'worker-thread-' );
},
},
],
} ),
],
Expand All @@ -111,7 +120,4 @@ export const mainBaseConfig: Configuration = {
common: path.resolve( __dirname, 'common/' ),
},
},
externals: {
'@wp-playground/cli': 'commonjs @wp-playground/cli',
},
};