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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 48 additions & 38 deletions apps/cli/commands/site/tests/create.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,57 +18,67 @@ import {
readCliConfig,
runCli,
setupCliEnv,
waitForSiteResponse,
type CliEnv,
} from './helpers/cli-e2e';

describe.skipIf( ! cliE2ePrerequisitesMet() )( 'CLI e2e: studio site create', () => {
let env: CliEnv | undefined;

afterEach( () => {
afterEach( async () => {
if ( env ) {
// The homepage test starts a site; stop it before tearing down the env.
await runCli( [ 'site', 'stop', '--all' ], env );
cleanupCliEnv( env );
env = undefined;
}
} );
}, 60_000 );

it( 'creates a site with a custom name', { tags: [ 'e2e' ], timeout: 120_000 }, async () => {
env = setupCliEnv();
const siteName = 'Custom E2E Site';
const sitePath = path.join( env.sitesDir, 'custom-e2e-site' );
it(
'creates a site that serves its homepage',
{ tags: [ 'e2e' ], timeout: 180_000 },
async () => {
env = setupCliEnv();
const siteName = 'Custom E2E Site';
const sitePath = path.join( env.sitesDir, 'custom-e2e-site' );

const result = await runCli(
[
'site',
'create',
'--name',
siteName,
'--path',
sitePath,
'--wp',
'latest',
'--no-start',
'--skip-browser',
'--skip-log-details',
],
env
);
const result = await runCli(
[
'site',
'create',
'--name',
siteName,
'--path',
sitePath,
'--wp',
'latest',
'--skip-browser',
'--skip-log-details',
],
Comment on lines +46 to +57

@gcsecsey gcsecsey Jul 1, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I got this flagged by Claude:

Because this command no longer passes --runtime sandbox, site create uses the CLI default native runtime. The e2e harness only provisions bundled WordPress, not php-bin, so this test can download PHP or fail in offline/release environments instead of staying hermetic like the other CLI e2e tests.

As a temporary measure, we could adjust the test to pass the --runtime sandbox parameter, but as a long-term solution, I think we should extend the e2e harness to download PHP as well.

env
);

expect( result.code, result.stderr ).toBe( 0 );
expect( result.code, result.stderr ).toBe( 0 );

// The site is persisted to the real cli.json with the custom name.
const config = readCliConfig( env );
expect( config.sites ).toHaveLength( 1 );
const [ site ] = config.sites;
expect( site.name ).toBe( siteName );
expect( site.path ).toBe( sitePath );
expect( site.phpVersion ).toBeTruthy();
expect( site.running ).toBe( false );
const config = readCliConfig( env );
expect( config.sites ).toHaveLength( 1 );
const [ site ] = config.sites;
expect( site.name ).toBe( siteName );
expect( site.path ).toBe( sitePath );
expect( site.phpVersion ).toBeTruthy();
expect( site.port ).toBeTruthy();

// wp-config.php only exists if the server started (create alone doesn't generate it).
expect( fs.existsSync( path.join( sitePath, 'wp-load.php' ) ) ).toBe( true );
expect( fs.existsSync( path.join( sitePath, 'wp-includes', 'version.php' ) ) ).toBe( true );
expect( fs.existsSync( path.join( sitePath, 'wp-config.php' ) ) ).toBe( true );

// Real WordPress core files were copied into the site directory.
// (wp-config.php is generated at server start, which --no-start skips.)
expect( fs.existsSync( path.join( sitePath, 'wp-load.php' ) ) ).toBe( true );
expect( fs.existsSync( path.join( sitePath, 'wp-includes', 'version.php' ) ) ).toBe( true );
} );
// A plain localhost site has no custom domain, so it serves directly (no redirect).
const response = await waitForSiteResponse( `http://localhost:${ String( site.port ) }` );
expect( response.status ).toBe( 200 );
expect( response.headers.get( 'content-type' ) ).toMatch( /text\/html/ );
}
);

it(
'creates a site with a custom domain and HTTPS',
Expand Down Expand Up @@ -101,8 +111,8 @@ describe.skipIf( ! cliE2ePrerequisitesMet() )( 'CLI e2e: studio site create', ()

expect( result.code, result.stderr ).toBe( 0 );

// The custom domain and HTTPS preference are persisted to cli.json.
// (--no-start skips the hosts-file / certificate setup that running would do.)
// --no-start skips the hosts-file / certificate setup that running would do,
// so this only checks the custom domain and HTTPS preference are persisted.
const config = readCliConfig( env );
expect( config.sites ).toHaveLength( 1 );
const [ site ] = config.sites;
Expand Down
26 changes: 26 additions & 0 deletions apps/cli/commands/site/tests/helpers/cli-e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,29 @@ export function readCliConfig( env: CliEnv ): {
} {
return JSON.parse( fs.readFileSync( env.cliConfigPath, 'utf-8' ) );
}

/**
* Polls a URL until the server accepts a connection, then resolves with the
* response. A freshly started site can take a moment to begin listening, so
* this retries the connection until the deadline; once it responds, the caller
* asserts on the status and content type. Redirects are not followed so a
* canonical 302 is observed as-is rather than chased to its target.
*/
export async function waitForSiteResponse(
url: string,
{ timeoutMs = 30_000, intervalMs = 500 }: { timeoutMs?: number; intervalMs?: number } = {}
): Promise< Response > {
const deadline = Date.now() + timeoutMs;
let lastError: unknown;

while ( Date.now() < deadline ) {
try {
return await fetch( url, { redirect: 'manual' } );
} catch ( error ) {
lastError = error;
await new Promise( ( resolve ) => setTimeout( resolve, intervalMs ) );
}
}

throw new Error( `Timed out waiting for a response from ${ url }: ${ String( lastError ) }` );
}
Loading