diff --git a/packages/wp-now/jest.config.ts b/packages/wp-now/jest.config.ts deleted file mode 100644 index 2a9c01f810..0000000000 --- a/packages/wp-now/jest.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-disable */ -export default { - displayName: 'wp-now', - preset: '../../jest.preset.js', - testEnvironment: 'node', - transform: { - '^.+\\.[tj]s$': [ - 'ts-jest', - { tsconfig: '/tsconfig.spec.json' }, - ], - }, - moduleFileExtensions: ['ts', 'js', 'html'], - coverageDirectory: '../../coverage/packages/wp-now', -}; diff --git a/packages/wp-now/project.json b/packages/wp-now/project.json index ab48a43f1b..c59967705e 100644 --- a/packages/wp-now/project.json +++ b/packages/wp-now/project.json @@ -63,7 +63,8 @@ "outputs": ["coverage/packages/wp-now"], "options": { "passWithNoTests": true, - "reportsDirectory": "../../coverage/packages/wp-now/node" + "reportsDirectory": "../../coverage/packages/wp-now/node", + "testTimeout": 20000 } } }, diff --git a/packages/wp-now/src/download.ts b/packages/wp-now/src/download.ts index ef98310943..2ffa4542f5 100644 --- a/packages/wp-now/src/download.ts +++ b/packages/wp-now/src/download.ts @@ -12,6 +12,7 @@ import { WP_NOW_PATH, } from './constants'; import { isValidWordpressVersion } from './wp-playground-wordpress'; +import { output } from './output'; function getWordPressVersionUrl(version = DEFAULT_WORDPRESS_VERSION) { if (!isValidWordpressVersion(version)) { @@ -37,7 +38,7 @@ async function downloadFileAndUnzip({ itemName, }): Promise { if (fs.existsSync(checkFinalPath)) { - console.log(`${itemName} folder already exists. Skipping download.`); + output?.log(`${itemName} folder already exists. Skipping download.`); return { downloaded: false, statusCode: 0 }; } @@ -46,7 +47,7 @@ async function downloadFileAndUnzip({ try { fs.ensureDirSync(path.dirname(destinationFolder)); - console.log(`Downloading ${itemName}...`); + output?.log(`Downloading ${itemName}...`); const response = await new Promise((resolve) => https.get(url, (response) => resolve(response)) ); @@ -79,7 +80,7 @@ async function downloadFileAndUnzip({ .promise(); return { downloaded: true, statusCode }; } catch (err) { - console.error(`Error downloading or unzipping ${itemName}:`, err); + output?.error(`Error downloading or unzipping ${itemName}:`, err); } return { downloaded: false, statusCode }; } @@ -99,7 +100,7 @@ export async function downloadWordPress( fs.ensureDirSync(path.dirname(finalFolder)); fs.renameSync(path.join(tempFolder, 'wordpress'), finalFolder); } else if (404 === statusCode) { - console.log( + output?.log( `WordPress ${wordPressVersion} not found. Check https://wordpress.org/download/releases/ for available versions.` ); process.exit(1); diff --git a/packages/wp-now/src/output.ts b/packages/wp-now/src/output.ts new file mode 100644 index 0000000000..0497eb97a2 --- /dev/null +++ b/packages/wp-now/src/output.ts @@ -0,0 +1,5 @@ +function shouldOutput() { + return process.env.NODE_ENV !== 'test'; +} + +export const output = shouldOutput() ? console : null; diff --git a/packages/wp-now/src/run-cli.ts b/packages/wp-now/src/run-cli.ts index aab78f5376..c28b5f19c0 100644 --- a/packages/wp-now/src/run-cli.ts +++ b/packages/wp-now/src/run-cli.ts @@ -5,15 +5,16 @@ import { portFinder } from './port-finder'; import { DEFAULT_PHP_VERSION, DEFAULT_WORDPRESS_VERSION } from './constants'; import { SupportedPHPVersion } from '@php-wasm/universal'; import { spawn, SpawnOptionsWithoutStdio } from 'child_process'; +import { output } from './output'; function startSpinner(message: string) { process.stdout.write(`${message}...\n`); return { succeed: (text: string) => { - console.log(`${text}`); + output?.log(`${text}`); }, fail: (text: string) => { - console.error(`${text}`); + output?.error(`${text}`); }, }; } @@ -61,7 +62,7 @@ export async function runCli() { const { url } = await startServer(options); openInDefaultBrowser(url); } catch (error) { - console.error(error); + output?.error(error); spinner.fail( `Failed to start the server: ${ (error as Error).message @@ -92,7 +93,7 @@ function openInDefaultBrowser(url: string) { args = ['/c', `start ${url}`]; break; default: - console.log(`Platform '${process.platform}' not supported`); + output?.log(`Platform '${process.platform}' not supported`); return; } spawn(cmd, args); diff --git a/packages/wp-now/src/start-server.ts b/packages/wp-now/src/start-server.ts index 750c69502e..cf796142ae 100644 --- a/packages/wp-now/src/start-server.ts +++ b/packages/wp-now/src/start-server.ts @@ -6,6 +6,7 @@ import fileUpload from 'express-fileupload'; import { portFinder } from './port-finder'; import { NodePHP } from '@php-wasm/node'; import startWPNow from './wp-now'; +import { output } from './output'; function requestBodyToMultipartFormData(json, boundary) { let multipartData = ''; @@ -97,13 +98,13 @@ export async function startServer( }); res.end(resp.bytes); } catch (e) { - console.trace(e); + output?.trace(e); } }); const url = `http://127.0.0.1:${port}/`; app.listen(port, () => { - console.log(`Server running at ${url}`); + output?.log(`Server running at ${url}`); }); return { diff --git a/packages/wp-now/src/tests/mode-examples/wordpress/index.php b/packages/wp-now/src/tests/mode-examples/wordpress/index.php new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/wp-now/src/tests/mode-examples/wordpress/wp-config-sample.php b/packages/wp-now/src/tests/mode-examples/wordpress/wp-config-sample.php new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/wp-now/src/tests/wp-now.spec.ts b/packages/wp-now/src/tests/wp-now.spec.ts index 4e151e70f8..b4350c5a33 100644 --- a/packages/wp-now/src/tests/wp-now.spec.ts +++ b/packages/wp-now/src/tests/wp-now.spec.ts @@ -1,4 +1,9 @@ -import { inferMode, parseOptions, WPNowMode, WPNowOptions } from '../wp-now'; +import startWPNow, { + inferMode, + parseOptions, + WPNowMode, + WPNowOptions, +} from '../wp-now'; import fs from 'fs-extra'; import path from 'path'; import { @@ -8,7 +13,8 @@ import { isWordPressDirectory, isWordPressDevelopDirectory, } from '../wp-playground-wordpress'; -import jest from 'jest-mock'; +import os from 'os'; +import crypto from 'crypto'; const exampleDir = __dirname + '/mode-examples'; @@ -144,3 +150,170 @@ test('isWordPressDevelopDirectory returns false for incomplete WordPress-develop expect(isWordPressDevelopDirectory(projectPath)).toBe(false); expect(inferMode(projectPath)).toBe(WPNowMode.INDEX); }); + +describe('Test starting different modes', () => { + let tmpExampleDirectory; + + /** + * Copy example directory to a temporary directory + */ + beforeEach(() => { + const tmpDirectory = os.tmpdir(); + const directoryHash = crypto.randomBytes(20).toString('hex'); + + tmpExampleDirectory = path.join( + tmpDirectory, + `wp-now-tests-${directoryHash}` + ); + fs.ensureDirSync(tmpExampleDirectory); + fs.copySync(exampleDir, tmpExampleDirectory); + }); + + /** + * Remove temporary directory + */ + afterEach(() => { + fs.rmSync(tmpExampleDirectory, { recursive: true, force: true }); + }); + + /** + * Expect that all provided mount point paths are empty directories which are result of file system mounts. + * + * @param mountPaths List of mount point paths that should exist on file system. + * @param projectPath Project path. + */ + const expectEmptyMountPoints = (mountPaths, projectPath) => { + mountPaths.map((relativePath) => { + const fullPath = path.join(projectPath, relativePath); + + expect(fs.existsSync(fullPath)).toBe(true); + expect(fs.readdirSync(fullPath)).toEqual([]); + expect(fs.lstatSync(fullPath).isDirectory()).toBe(true); + }); + }; + + /** + * Expect that all listed files do not exist in project directory + * + * @param forbiddenFiles List of files that should not exist on file system. + * @param projectPath Project path. + */ + const expectForbiddenProjectFiles = (forbiddenFiles, projectPath) => { + forbiddenFiles.map((relativePath) => { + const fullPath = path.join(projectPath, relativePath); + expect({ + path: fullPath, + exists: fs.existsSync(fullPath), + }).toStrictEqual({ path: fullPath, exists: false }); + }); + }; + + /** + * Expect that all required files exist for PHP. + * + * @param requiredFiles List of files that should be accessible by PHP. + * @param documentRoot Document root of the PHP server. + * @param php NodePHP instance. + */ + const expectRequiredRootFiles = (requiredFiles, documentRoot, php) => { + requiredFiles.map((relativePath) => { + const fullPath = path.join(documentRoot, relativePath); + expect({ + path: fullPath, + exists: php.fileExists(fullPath), + }).toStrictEqual({ path: fullPath, exists: true }); + }); + }; + + /** + * Test that startWPNow in "index", "plugin" and "theme" modes doesn't change anything in the project directory. + */ + test.each(['index', 'plugin', 'theme'])( + 'startWPNow starts %s mode', + async (mode) => { + const exampleProjectPath = path.join(exampleDir, mode); + const projectPath = path.join(tmpExampleDirectory, mode); + + const rawOptions: Partial = { + projectPath: projectPath, + }; + + await startWPNow(rawOptions); + + const forbiddenPaths = ['wp-config.php']; + + expectForbiddenProjectFiles(forbiddenPaths, projectPath); + + expect(fs.readdirSync(projectPath)).toEqual( + fs.readdirSync(exampleProjectPath) + ); + } + ); + + /** + * Test that startWPNow in "wp-content" mode mounts required files and directories, and + * that required files exist for PHP. + */ + test('startWPNow starts wp-content mode', async () => { + const projectPath = path.join(tmpExampleDirectory, 'wp-content'); + + const rawOptions: Partial = { + projectPath: projectPath, + }; + + const { php, options: wpNowOptions } = await startWPNow(rawOptions); + + const mountPointPaths = [ + 'database', + 'db.php', + 'mu-plugins', + 'plugins/sqlite-database-integration', + ]; + + expectEmptyMountPoints(mountPointPaths, projectPath); + + const forbiddenPaths = ['wp-config.php']; + + expectForbiddenProjectFiles(forbiddenPaths, projectPath); + + const requiredFiles = [ + 'wp-content/db.php', + 'wp-content/mu-plugins/0-allow-wp-org.php', + 'playground-consts.json', + ]; + + expectRequiredRootFiles(requiredFiles, wpNowOptions.documentRoot, php); + }); + + /** + * Test that startWPNow in "wordpress" mode mounts required files and directories, and + * that required files exist for PHP. + */ + test('startWPNow starts wordpress mode', async () => { + const projectPath = path.join(tmpExampleDirectory, 'wordpress'); + + const rawOptions: Partial = { + projectPath: projectPath, + }; + + const { php, options: wpNowOptions } = await startWPNow(rawOptions); + + const mountPointPaths = [ + 'wp-content/database', + 'wp-content/db.php', + 'wp-content/mu-plugins', + 'wp-content/plugins/sqlite-database-integration', + ]; + + expectEmptyMountPoints(mountPointPaths, projectPath); + + const requiredFiles = [ + 'wp-content/db.php', + 'wp-content/mu-plugins/0-allow-wp-org.php', + 'playground-consts.json', + 'wp-config.php', + ]; + + expectRequiredRootFiles(requiredFiles, wpNowOptions.documentRoot, php); + }); +}); diff --git a/packages/wp-now/src/wp-now.ts b/packages/wp-now/src/wp-now.ts index 53ea811c45..f8c9d73803 100644 --- a/packages/wp-now/src/wp-now.ts +++ b/packages/wp-now/src/wp-now.ts @@ -33,6 +33,7 @@ import { isWordPressDirectory, isWordPressDevelopDirectory, } from './wp-playground-wordpress'; +import { output } from './output'; export const enum WPNowMode { PLUGIN = 'plugin', @@ -115,7 +116,7 @@ export default async function startWPNow( !seemsLikeAPHPFile(fullPath) ); } catch (e) { - console.error(e); + output?.error(e); return false; } }, @@ -125,10 +126,10 @@ export default async function startWPNow( php.chdir(documentRoot); php.writeFile(`${documentRoot}/index.php`, `