From f46eb68d9216cac781cd8e3a12f8b4d89dde4c2d Mon Sep 17 00:00:00 2001 From: Wojtek Naruniec Date: Tue, 19 Aug 2025 09:50:06 +0200 Subject: [PATCH 1/2] Enable Playground CLI feature flag by default --- src/lib/feature-flags.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 { From aa0fa2b4677b239a1834423ec199081e5d8473f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20Cs=C3=A9csey?= Date: Wed, 27 Aug 2025 11:34:46 +0100 Subject: [PATCH 2/2] Studio Blueprints: Adjust tests (#1697) * fix add site flow tests * enable blueprints when running e2e tests * click create site button to open site form * update e2e appdata paths * fix e2e sqlite filename * fix e2e sqlite filename * Add externals for @php-wasm/node and @php-wasm/logger in webpack configuration * resolve @php-wasm/node and @php-wasm/logger paths reliably * Revert "resolve @php-wasm/node and @php-wasm/logger paths reliably" This reverts commit 1af4ce744ce3c4531899b2a738ddb7dc788b70ea. * Don't use externals but let webpack do the bundling of @wp-playground/cli * accept 302 from the site frontend * use server files path for sqlite * check the removal from disk after site disappears from sidebar * Avoid glob in path.resolve to fix Windows build * metrics: fill the site name at onboarding * remove env var from e2e config * Update src/modules/add-site/tests/add-site.test.tsx Co-authored-by: Ivan Ottinger * Update src/modules/add-site/tests/add-site.test.tsx Co-authored-by: Ivan Ottinger --------- Co-authored-by: Alex Kirk Co-authored-by: Ivan Ottinger --- e2e/page-objects/add-site-modal.ts | 6 +- e2e/sites.test.ts | 8 ++- forge.config.ts | 2 - metrics/tests/site-editor.test.ts | 5 +- .../playground-cli/playground-cli-provider.ts | 4 +- src/modules/add-site/tests/add-site.test.tsx | 66 +++++++++++++++---- src/storage/paths.ts | 6 +- webpack.main.config.ts | 12 +++- 8 files changed, 79 insertions(+), 30 deletions(-) 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/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', - }, };