Playground

  • Don’t use URL to construct paths in Node.js

    I’ve been spending some of my time recently making sure WordPress Playground works in Windows.
    While working on Windows fixes I learned a lot about making sure Javascript code works POSIX and Windows when running it in Node.js.

    One thing that I learned is that you shouldn’t use URL to construct paths.

    If you construct a path using new URL(import.meta.url) in Node.js and use pathname to to obtain the path, it will always have a leading slash in front of the path.
    This is ok for POSIX but in Windows it results in invalid paths
    like /C:/Users/.

    What to use instead of URL

    You can use fileURLToPath from the URL package, it will correctly construct both POSIX and Windows paths from file:/// paths.

    Polyfilling __dirname and __filename in Node 20+

    In Node 20 and above you can use import.meta.filename and import.meta.dirname to polyfill __dirname and __filename.

    Example

    import { fileURLToPath } from 'url';
    import path from 'path';
    
    const windowsImportMetaUrl = "file:///C:/Users/me/Downloads/index.js";
    console.log('windowsImportMetaUrl', windowsImportMetaUrl);
    
    let __dirname = new URL('.', windowsImportMetaUrl).pathname;
    let __filename = new URL(windowsImportMetaUrl).pathname;
    console.log('__filename', __filename);
    console.log('__dirname', __dirname);
    
    
    __filename = fileURLToPath(
        windowsImportMetaUrl,
        {
            windows: true,
        }
    );
    __dirname = path.win32.dirname(__filename);
    console.log('__filename', __filename);
    console.log('__dirname', __dirname);
    
    __filename = import.meta.filename;
    __dirname = import.meta.dirname;
    console.log('__filename', __filename);
    console.log('__dirname', __dirname);

    The above code will result in:

    windowsImportMetaUrl file:///C:/Users/me/Downloads/index.js
    
    # new URL output 
    __filename /C:/Users/me/Downloads/index.js
    __dirname /C:/Users/me/Downloads/
    
    
    # fileURLToPath output
    __filename C:\Users\me\Downloads\index.js
    __dirname C:\Users\me\Downloads
    
    
    # import.meta.filename and import.meta.dirname output
    __filename C:\Users\me\Downloads\index.js
    __dirname C:\Users\me\Downloads

  • Building Automated Tests with WordPress Playground

    A big advantage of WordPress Playground is that it can start a new WordPress site quickly and be configured during boot.

    Both of these features are important for automated testing, so Playground seems like a good fit for testing WordPress projects.

    Previously Jan and I worked on the Playground tester which automatically tests Playground compatibility of all WordPress.org themes and plugins using GitHub actions (for free).

    The original approach using Bash was a bit complicated, but after Playground created a way to start the Playground CLI from JavaScript by calling runCLI, using Playground for automated tests became easy.

    In this post I will demonstrate how to get started with using the Playground CLI for testing WordPress projects.

    If you want to skip the tutorial and see the final code, it’s available on GitHub.

    Or if you prefer video, you can checkout this WCEU workshop.

    Setup

    To follow this tutorial checkout the playground-testing-demo repository and follow the setup instructions in the readme.

    The demo project includes a simple WordPress plugin, Vitest for integration testing and Playwright for end-to-end testing.

    Playground CLI

    Before we can start building tests, we need to get familiar with the Playground CLI.

    The Playground CLI enables us creates a local server for our WordPress projects.

    Mount a project into Playground

    To start using the CLI make sure you have at least Node 20 installed and after that run npx @wp-playground/cli server --mount=.:/wordpress/wp-content/plugins/playground-testing-demo to start a server and mount your current directory as a WordPress plugin.

    Configure Playground using a Blueprint

    To automatically activate the plugin we can create a Blueprint with an activatePlugin step.

    Here’s a Blueprint that will activate this sample plugin:

    {
        "steps": [
            {
                "step": "activatePlugin",
                "pluginPath": "/wordpress/wp-content/plugins/playground-testing-demo/playground-testing-demo.php"
            }
        ]
    }

    Now we can start a server and activate the plugin during boot using npx @wp-playground/cli server --mount=.:/wordpress/wp-content/plugins/ --blueprint=./blueprint.json

    Notes

    You can replace . with a path to your project and /wordpress/wp-content/plugins/ with a path where you would like your project to be loaded.

    /wordpress/ is the site root folder, similar to /var/www/ in some other severs.

    If you are mounting an entire WordPress site use a combination of --mountBeforeInstall and --skipWordPressSetup.

    End-to-end tests

    In the sample plugin you can run end-to-end tests using npm run test:e2e.

    For the sample plugin we want to test the form and confirm that the data is persisted after a page reload.

    Before starting, you can copy all the necessary imports from here.

    First we need to start Playground just like we did with the CLI.

    const blueprint = JSON.parse(
          readFileSync(resolve("./blueprint.json"), "utf8")
    );
    const cliServer = await runCLI({
          command: "server",
          mount: [".:/wordpress/wp-content/plugins/playground-testing-demo"],
          blueprint,
          quiet: true,
    });
    const handler = cliServer.requestHandler;
    const php = await handler.getPrimaryPhp();

    After that we need to login, so we get access to the admin page.

    await login(php, {
          username: "admin",
    });

    Now we can generate the admin page URL by using the PHPRequestHandler from Playground.

    const wpAdminUrl = new URL(handler.absoluteUrl);
        wpAdminUrl.pathname = "/wp-admin/admin.php";
        wpAdminUrl.searchParams.set("page", "workshop-tests");

    And finally we can run our tests using Playwright.

    await page.goto(wpAdminUrl.toString());
    
    await expect(page).toHaveTitle(/Workshop Tests/);
    
    await page.getByPlaceholder("Enter a message").fill("Hello, world!");
    await page.getByRole("button", { name: "Send" }).click();
    await expect(page.getByText("User says: Hello, world!")).toBeVisible();
    
    await page.reload();
    await expect(page.getByText("User says: Hello, world!")).toBeVisible();

    Also, let’s not forget to stop the server in the end.

    await cliServer.server.close();

    Here’s the full end-to-end test for the sample plugin.

    Integration tests

    In the sample plugin you can run integration tests using npm run test:integration.

    For this tutorial we will cover how to test a Rest API endpoint, more test examples are available here.

    Before starting, you can copy all the necessary imports from here.

    First we need to start Playground just like we did with the CLI and in end-to-end tests.

    const blueprint = JSON.parse(
          readFileSync(resolve("./blueprint.json"), "utf8")
    );
    const cliServer = await runCLI({
          command: "server",
          mount: [".:/wordpress/wp-content/plugins/playground-testing-demo"],
          blueprint,
          quiet: true,
    });
    const handler = cliServer.requestHandler;
    const php = await handler.getPrimaryPhp();

    Now we can generate the API endpoint url by using the PHPRequestHandler from Playground.

    const apiUrl = new URL("/wp-json/PTD/v1/message", handler.absoluteUrl);

    Before we can send a request to the API, we need to generate a Rest API nonce.

    Nonces are tied to cookies, but they aren’t stored in Node, so we need to extend fetch with a Cookie store.

    const fetchCookie = makeFetchCookie(fetch);

    Once we can store cookies, we can create a PHP script using Playground’s file system API and call that script using fetchCookie to obtain the nonce.

    
    
    const getAuthHeaders = async (handler: PHPRequestHandler) => {
      const php = await handler.getPrimaryPhp();
      if (!(await php.fileExists("/wordpress/get_rest_auth_data.php"))) {
        await php.writeFile(
          "/wordpress/get_rest_auth_data.php",
          `<?php
                require_once '/wordpress/wp-load.php';
                echo json_encode(
                    array(
                        'X-WP-Nonce' => wp_create_nonce('wp_rest'),
                    )
                );
                `
        );
      }
    
      await login(php, {
        username: "admin",
      });
      const response = await fetchCookie(handler.absoluteUrl + "/get_rest_auth_data.php");
      return await response.json();
    };

    Now we can run out integration test and check if the API returns a valid response.

    In the test we first obtain the nonce, so that we can send it in the request.
    Because we use fetchCookie, the cookie that was sent in the nonce response will now be sent back together with the request.

    const authHeaders = await getAuthHeaders(handler);
    const formData = new FormData();
    formData.append("message", "John Doe");
    const apiResponse = await fetchCookie(apiUrl, {
          method: "POST",
          headers: authHeaders,
          body: formData,
    });
    const responseJson = await apiResponse.json();
    expect(apiResponse.status).toBe(200);
    expect(responseJson).toMatchObject({
          success: true,
          message: "User says: John Doe",
    });

    And in the end, let’s not forget to stop the server.

    await cliServer.server.close();

    Have fun and build something awesome!