diff --git a/e2e/page-objects/add-site-modal.ts b/e2e/page-objects/add-site-modal.ts
index db453a4a6..4dfee82d0 100644
--- a/e2e/page-objects/add-site-modal.ts
+++ b/e2e/page-objects/add-site-modal.ts
@@ -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() {
diff --git a/e2e/sites.test.ts b/e2e/sites.test.ts
index 8c458e321..5f08e4f18 100644
--- a/e2e/sites.test.ts
+++ b/e2e/sites.test.ts
@@ -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();
@@ -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 );
expect( response.headers[ 'content-type' ] ).toMatch( /text\/html/ );
} );
@@ -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 );
} );
} );
diff --git a/forge.config.ts b/forge.config.ts
index 3fb1f853a..511647a7b 100644
--- a/forge.config.ts
+++ b/forge.config.ts
@@ -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.
- new ForgeExternalsPlugin( { externals: Object.keys( mainBaseConfig.externals ?? {} ) } ),
],
hooks: {
generateAssets: async () => {
diff --git a/metrics/tests/site-editor.test.ts b/metrics/tests/site-editor.test.ts
index 6e642ae75..0df8b7035 100644
--- a/metrics/tests/site-editor.test.ts
+++ b/metrics/tests/site-editor.test.ts
@@ -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
@@ -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
diff --git a/src/lib/feature-flags.ts b/src/lib/feature-flags.ts
index d0b2dbdfe..e415ba48b 100644
--- a/src/lib/feature-flags.ts
+++ b/src/lib/feature-flags.ts
@@ -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;
@@ -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 {
diff --git a/src/lib/wordpress-provider/playground-cli/playground-cli-provider.ts b/src/lib/wordpress-provider/playground-cli/playground-cli-provider.ts
index 3848dba94..95ba12517 100644
--- a/src/lib/wordpress-provider/playground-cli/playground-cli-provider.ts
+++ b/src/lib/wordpress-provider/playground-cli/playground-cli-provider.ts
@@ -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,
@@ -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 {
diff --git a/src/modules/add-site/tests/add-site.test.tsx b/src/modules/add-site/tests/add-site.test.tsx
index 47d8e270b..3f5911139 100644
--- a/src/modules/add-site/tests/add-site.test.tsx
+++ b/src/modules/add-site/tests/add-site.test.tsx
@@ -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';
@@ -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',
@@ -131,24 +131,25 @@ describe( 'AddSite', () => {
renderWithProvider( );
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',
@@ -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(
@@ -202,15 +208,20 @@ describe( 'AddSite', () => {
} );
renderWithProvider( );
- 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.'
);
@@ -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.'
);
@@ -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' );
@@ -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();
@@ -306,6 +327,9 @@ describe( 'AddSite', () => {
renderWithProvider( );
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' ) );
@@ -336,6 +360,9 @@ describe( 'AddSite', () => {
renderWithProvider( );
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();
@@ -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(
@@ -382,6 +411,9 @@ describe( 'AddSite', () => {
renderWithProvider( );
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();
@@ -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();
@@ -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' );
@@ -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' );
@@ -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' );
@@ -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' );
diff --git a/src/storage/paths.ts b/src/storage/paths.ts
index b0b1b0a82..e1fe93e0a 100644
--- a/src/storage/paths.ts
+++ b/src/storage/paths.ts
@@ -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' );
diff --git a/webpack.main.config.ts b/webpack.main.config.ts
index 67701063d..817d31084 100644
--- a/webpack.main.config.ts
+++ b/webpack.main.config.ts
@@ -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-' );
+ },
+ },
],
} ),
],
@@ -111,7 +120,4 @@ export const mainBaseConfig: Configuration = {
common: path.resolve( __dirname, 'common/' ),
},
},
- externals: {
- '@wp-playground/cli': 'commonjs @wp-playground/cli',
- },
};