Category: GraphQL

  • Tutorial: Registering a Custom Post Type as a GraphQL Interface

    WPGraphQL v1.12.0 introduces new options for customizing how Post Types and Taxonomies are exposed to the WPGraphQL Schema. The goal of this post is to show how to use some of these options in action. You can read this post for an overview of all the options, or check out the release..

    To do that, we’re going to explore a few approaches to solving the same problem.

    Imagine that we’re working on a project for a restaurant that sells food. More specifically, they only sell Pizza and Cake.

    The restaurant needs to be able to enter the different types of pizza and cake they have available.

    For simplicity sake, we’ve determined that each all food has some properties, but each type of food also has unique properties.

    All cake is food, but not all food is cake. All pizza is food, but not all food is pizza.

    • All food: has a title and a price
    • Pizza: has toppings.
    • Cake: has frosting color.

    The goal of this exercise is to configure WordPress to be able to maintain this data in the CMS, and expose it in the WPGraphQL API.

    We would like to be able to query for a list of food, asking for the common fields like “title” and “price”, but also specifying the unique fields, such as “frostingColor” for cake and “toppings” for pizza.

    We want to be able to execute the following GraphQL Query:

    query GetFood {
      allFood {
        nodes {
          __typename
          id
          title
          price
          ...on Cake {
            frostingColor
          }
          ...on Pizza {
            toppings
          }
        }
      }
    }

    And in response, we’d like to get data like so:

    {
      "data": {
        "foods": {
          "nodes": [
            {
              "__typename": "Cake",
              "title": "Cake",
              "price": "$15.99",
              "frostingColor": "white"
            },
            {
              "__typename": "Pizza",
              "title": "Pepperoni Pizza",
              "price": "$10.99",
              "toppings": [
                "pepperoni",
                "sauce",
                "cheese",
                "jalapenos"
              ]
            }
          ]
        }
      }
    }

    There are probably many ways we could achieve this.

    In this post, I’ll show 2 different ways.

    In both scenarios, the query above should work. We should be able to query a list of food, asking for common fields, and also asking for unique fields for Pizza and Cake.

    Neither of these options is the “right” one. This is a simple example missing a lot of nuanced details that you would likely have in a real project. The goal of this post is not to prescribe some type of optimal information architecture using WordPress post types, the goal is to show the flexibility and options for exposing WordPress data to the WPGraphQL API.

    Let’s dive in.

    Scenario 1: One “food” post type

    In this scenario, we’ve decided to register a “food” post type where we can enter data for different food items.

    Register the Post Type

    To get started we will register the post type (without thinking too much about WPGraphQL).

    add_action( 'init', function() {
    
      $args = [
        'public' => true,
        'label' => 'Food',
        'supports' => [ 'title', 'editor', 'custom-fields' ]
      ];
    
      register_post_type( 'food', $args );
    
    } );

    Here we’ve registered a public “food” post type with support for title, editor and custom fields.

    At this point, we should have a Post Type in WordPress where we can start entering our Cakes and Pizzas into the CMS.

    Add some pizza and cake

    Click “Add new” and enter the following:

    • Title: Pepperoni Pizza
    • Description: Yum!
    • Custom Fields:
      • food_type: pizza
      • price: $10.99
      • topping: cheese
      • topping: pepperoni
      • topping: sauce

    The pizza should look like so:

    Screenshot of the "Edit Post" screen for the "food" post type showing data entered for a "pepperoni pizza"

    NOTE: For simplicity sake, I’m demonstrating with WordPress’s built-in custom fields, but on a real project I would likely use Advanced Custom Fields.

    Now let’s enter information for a Cake.

    Click “Add new” and enter the following information:

    • Title: Chocolate Cake
    • Description: Delicious!
    • Custom Fields:
      • food_type: cake
      • price: $15.99
      • frosting_color: white

    The cake should look like so:

    Screenshot of the "Edit Post" screen for the "food" post type showing data entered for a "pepperoni pizza"

    So now we have our post type setup and some food entered, how can we query this food in GraphQL?

    Show the Post Type in GraphQL

    If you’ve already familiar with WPGraphQL, you might know that exposing a Post Type to the WPGraphQL Schema requires 3 fields on the post type registration:

    • show_in_graphql: true/false
    • graphql_single_name: Name of the Type in the GraphQL Schema (no spaces or special characters. Value must be unique in the Schema)
    • graphql_plural_name: Plural name of the Type, used for querying lists of items. (no spaces or special characters. Value must be unique in the Schema)

    Let’s update our post type registration above to look like the following:

    add_action( 'init', function() {
    
      $args = [
        'public' => true,
        'label' => 'Food',
        'supports' => [ 'title', 'editor', 'custom-fields' ],
        'show_in_graphql' => true,
        'graphql_single_name' => 'Food',
        'graphql_plural_name' => 'Food',
      ];
    
      register_post_type( 'food', $args );
    
    } );

    By adding these 3 lines:

    'show_in_graphql' => true,
    'graphql_single_name' => 'Food',
    'graphql_plural_name' => 'Food',

    The data in the “food” post type is now exposed in the WPGraphQL Schema.

    We can open up the GraphiQL IDE and search for “food” and see all the ways it shows in our Schema now:

    We have a lot of new Types and Fields added to the Schema.

    Since our goal was to query a list of “allFood” we can see that a “RootQuery.allFood” field now exists in the Schema.

    NOTE: Since the “graphql_single_name” and “graphql_plural_name” were both “food” WPGraphQL adds the “plural” field as “allFood”. If the “graphql_plural_name” was a different value, such as “foodItems” it would add a field with that value instead.

    At this point, we can write a query to query a list of food, like so:

    query GetFood {
      allFood {
        nodes {
          __typename
          id
          title
        }
      }
    }

    And we’ll get a response like so:

    {
      "data": {
        "allFood": {
          "nodes": [
            {
              "__typename": "Food",
              "id": "cG9zdDoxMjY4",
              "title": "Chocolate Cake"
            },
            {
              "__typename": "Food",
              "id": "cG9zdDoxMjY3",
              "title": "Pepperoni Pizza"
            }
          ]
        }
      }
    }

    Here’s what it looks like executed in the GraphiQL IDE:

    One thing you might have noticed, is that we have only queried for 3 fields on each “node”:

    • __typename
    • id
    • title

    Our goal was to be able to also query “price” for all food items, and then specify that we want “toppings” for Pizza and “frostingColor” for Cake.

    If we look at our Schema docs in GraphiQL, we won’t find any mention of “price”, “toppings”, “frostingColor”, “pizza” or “cake”.

    Gif screen recording showing empty results when searching for “price”, “toppings”, “frosting”, “pizza” and “cake” and getting no results.

    Add the “price” field to the Schema

    Since we agreed that all “food” should have a price field, we can add this to the Schema using the register_graphql_field function.

    Let’s use the following snippet to add this field to the Schema:

    add_action( 'graphql_register_types', function() {
    
      register_graphql_field( 'Food', 'price', [
        'type' => 'String',
        'description' => __( 'The cost of the food item', 'your-textdomain' ),
        'resolve' => function( $food ) {
          $price = get_post_meta( $food->databaseId, 'price', true );
          return ! empty( $price ) ? $price : null;
        }
      ] );
    
    } );

    Let’s break down what this code is doing:

    First, it hooks into the “graphql_register_types” action. This ensures the code will only execute when the GraphQL Schema is being built. Most requests to WordPress aren’t GraphQL requests, so no need to execute GraphQL related functions unless necessary.

    Next, we call the register_graphql_field function. The first argument is the GraphQL Type we want to add a field to. And the 2nd argument is the name of the field to add. We passed “Food” as the first argument and “price” as the 2nd argument because we want “Food” to have a “price” field. The third argument is an array to configure the field.

    In this config array we define the “Type” of data the field will return. We’ve opted for “String” as the type of data we will return for the “price” field. This is the contract between the API consumer and the server. We’ve agreed now that whenever the “price” field is asked for, it will either be a “null” value, or a string. This field will not return an array or an object or an integer or any other type of data.

    Next, we added a description, which shows in tools like GraphiQL. This can be helpful to describe to consumers of the API what a field is meant to be used for.

    And last, we define our “resolve” function.

    The resolver will get the resolving object passed to it. Since we’ve registered a field to the “Food” type, this means we will get “Food” objects passed to the field.

    In WPGraphQL the objects passed down are typically “GraphQL Model” objects. In this case, the resolver will get a GraphQL\Model\Post object passed to the resolver.

    We use the databaseId from the model to get the value of the “price” post_meta. If there is a value, return it. Otherwise, return a “null” value.

    Now, with this field registered to the Schema, we can search GraphiQL again for “price” and we should find it in the Schema:

    At this point, we can update our query to query for the “price” field and we should see the values we entered as custom fields returned.

    Screenshot of a query for allFood including the price field.

    Differentiate types of food

    One other thing you might be noticing is that the response for the “__typename” field is “Food”, but in our goal we were hoping to have the values be “Cake” and “Pizza”.

    Additionally, we wanted to be able to query for “toppings” if the type of food is Pizza, and “frostingColor” if the type of food is Cake.

    How do we do this?

    We will convert the “Food” type to be a GraphQL Interface, then register new GraphQL Object Types for “Pizza” and “Cake” that implement the “Food” Interface.

    Let’s take a look:

    Make “Food” an Interface

    According to the GraphQL Documentation “Interface is an abstract type that includes a certain set of fields that a type must include to implement the interface”.

    In practice, this means that multiple Unique Types can have a common bond.

    All Pizza is Food, but not all Food is Pizza. All Cake is Food, but not all Food is Cake.

    We want to be able to define unique Types of Food (in our case Pizza and Cake), but let them implement the “Food” interface.

    We can update our post type registration to make the “Food” type an “Interface” by adding this line:

    $args = [
     ... // existing args
     'graphql_kind' => 'interface',
     'graphql_resolve_type' => function( $food ) {
       $food_type = get_post_meta( $food->databaseId, 'food_type', true );
    
       // if the "food_type" custom field is set to "pizza" return the "Pizza" type
       if ( 'pizza' === $food_type ) {
         $return_type = 'Pizza';
       } else {
         $return_type = 'Cake';
       }
    
       return $return_type;
     }
    ];
    
    register_post_type( 'Food', $args );

    By adding these 2 lines to our “register_post_type” $args, we’ve now told WPGraphQL to treat “Food” as an Interface instead of an Object Type, and we’ve told it how to determine what Object type to resolve, based on the “food_type” meta value.

    In order for this to work, however, we need a “Pizza” and “Cake” Type added to the Schema.

    Register the Pizza GraphQL Object Type

    In the “graphql_resolve_type” function above, we determined that “Food” can return a “Pizza” or “Cake” based on the value of the “food_type” post meta value.

    In order for this to work, we need a Pizza and Cake type to be added to the Schema.

    Within our “graphql_register_types” hook where we registered the “price” field, we can add the following:

    register_graphql_object_type( 'Pizza', [
      // Description shows in the Schema for client developers using tools like the GraphiQL IDE
      'description' => __( 'A tasty food best prepared in an air-fryer', 'your-textdomain' ),
      // This tells the Schema that "All Pizza is Food" and will inherit the "Food" fields (such as Price)
      'interfaces' => [ 'Food' ],
      // This helps with caching. If your Object Type is associated with a particular Model, you should define it here. In our case, Pizza is resolved by the Post model.
      'model' => WPGraphQL\Model\Post::class, 
       // This field shows the Type in the Schema even if there are no root fields that reference the Type directly.
      'eagerlyLoadType' => true,
      // Define the unique fields of this type. For Pizza, "toppings" will be a "list of strings"
      'fields' => [
        'toppings' => [
          'type' => [ 'list_of' => 'String' ],
          'resolve' => function( $pizza ) {
            $toppings = get_post_meta( $pizza->databaseId, 'toppings', false );
            return is_array( $toppings ) ? $toppings : null;
          }
        ],
      ],
    ]);

    Here we’ve registered a “Pizza” GraphQL Object Type. On the Type we declared that it implements the “Food” interface, and that it has a “toppings” field which returns a list of String, resolved from the “topping” meta key.

    Register the Cake GraphQL Object Type

    Now, we can register the “Cake” Object Type, very similar to how we registered the “Pizza” Type.

    register_graphql_object_type( 'Cake', [
      // Description shows in the Schema for client developers using tools like the GraphiQL IDE
      'description' => __( 'A tasty dessert, most likely also good if heated in an air-fryer', 'your-textdomain' ),
      // This tells the Schema that "All Cake is Food" and will inherit the "Food" fields (such as Price)
      'interfaces' => [ 'Food' ],
      'eagerlyLoadType' => true,
      'model' => WPGraphQL\Model\Post::class,
      'fields' => [
        'frostingColor' => [
          'type' => 'String',
          'resolve' => function( $cake ) {
            return get_post_meta( $cake->databaseId, 'frosting_color', true );
          }
        ],
      ]
    ]);

    Here we’ve registered a “Cake” object type. On the type we’ve declared that it implements the “Food” interface, and that it has a “frostingColor” field which returns a String, resolved from the “frosting_color” meta key.

    At this point, we’ve converted the “Food” type to be an Interface using the “graphql_kind” arg on “register_post_type”. We also declared the “graphql_resolve_type” function, returning either a “Pizza” or “Cake” when “Food” is queried.

    Then we defined the “Pizza” and “Cake” Types and their fields.

    At this point, we can successfully execute the query we set out to.

    query GetFood {
      allFood {
        nodes {
          __typename
          id
          title
          price
          ...on Cake {
            frostingColor
          }
          ...on Pizza {
            toppings
          }
        }
      }
    }

    Since the “allFood” connection is added to the schema from the “food” post type, the resolver will query posts of the “food” post type. Each post will be converted into a “Post” Model and then our “graphql_resolve_type” function will use the “food_type” meta to determine whether the “food” is “Pizza” or “Cake”, then each field (price, toppings, frostingColor) is resolved.

    Success! We end up with the expected results:

    {
      "data": {
        "foods": {
          "nodes": [
            {
              "__typename": "Cake",
              "title": "Cake",
              "price": "$15.99",
              "frostingColor": "white"
            },
            {
              "__typename": "Pizza",
              "title": "Pepperoni Pizza",
              "price": "$10.99",
              "toppings": [
                "pepperoni",
                "sauce",
                "cheese",
                "jalapenos"
              ]
            }
          ]
        }
      }
    }

    Scenario 2: 2 different “Pizza” and “Cake” post types

    Now, we already accomplished the goal we set out to, but I wanted to show a different way to get the the same goal.

    In this approach, instead of using one “food” post type, let’s use 2 different post types: “Pizza” and “Cake”.

    But, the goal is still the same. We want to be able to query for “allFood” and depending on whether the food is “Pizza” or “Cake” we’d like to query for “toppings” or “frostingColor” respectively.

    Register the Food Interface

    Instead of registering a “Food” post type and setting its “graphql_kind” as “interface”, this time we will manually register an Interface, then register 2 post types and apply the “Food” interface to those 2 post types.

    With the following snippet, we can register a “Food” interface:

    add_action( 'graphql_register_types', function() {
    
      register_graphql_interface_type( 'Food', [
        'description' => __( 'An item of food for sale', 'your-textdomain' ),
        // ensure all "food" nodes have a title
        'interfaces' => [ 'Node', 'NodeWithTitle' ],
        'fields' => [
          'price' => [
            'type' => 'String',
    	'description' => __( 'The cost of the food item', 'your-textdomain' ),
    	'resolve' => function( $food ) {
    	  return get_post_meta( $food->databaseId, 'price', true );
    	},
          ],
        ],
        'resolveType' => function( $node ) {
          // use the post_type to determine what GraphQL Type should be returned. Default to Cake
          return get_post_type_object( $node->post_type )->graphql_single_name ?: 'Cake';
        }
      ]);
    
    });

    Register the Post Types

    We just defined our “Food” Interface, but it doesn’t really do anything yet, because no Type in the Graph implements this interface.

    Let’s register our “Cake” and “Pizza” post types and implement the “Food” interface on them.

    add_action( 'init', function() {
    
      $pizza_args = [
        'public' => true,
        'label' => 'Pizza',
        'show_in_graphql' => true,
        'supports' => [ 'title', 'editor', 'custom-fields' ],
        'graphql_single_name' => 'Pizza',
        'graphql_plural_name' => 'Pizza',
        'graphql_interfaces' => [ 'Food' ],
        'graphql_fields' => [
          'toppings' => [
            'type' => [ 'list_of' => 'String' ],
            'resolve' => function( $pizza ) {
               $toppings = get_post_meta( $pizza->databaseId, 'topping', false );
              return is_array( $toppings ) ? $toppings : null;
            }
          ],
        ],
      ];
    
      register_post_type( 'pizza', $pizza_args );
    
      $cake_args = [
        'public' => true,
        'label' => 'Cake',
        'show_in_graphql' => true,
        'supports' => [ 'title', 'editor', 'custom-fields' ],
        'graphql_single_name' => 'Cake',
        'graphql_plural_name' => 'Cakes',
        'graphql_interfaces' => [ 'Food' ],
        'graphql_fields' => [
          'frostingColor' => [
            'type' => 'String',
            'resolve' => function( $cake ) {
              return get_post_meta( $cake->databaseId, 'frosting_color', true );
            }
          ],
        ]
      ];
    
      register_post_type( 'cake', $cake_args );
    
    });

    In this snippet, we’ve registered both a “cake” and a “pizza” post type. We set both to “show_in_graphql”, defined their “graphql_single_name” and “graphql_plural_name”, then we applied the “Food” interface using the “graphql_interfaces” argument.

    Additionally, we used the “graphql_fields” argument to add a “toppings” field to the “Pizza” Type and a “frostingColor” field to the “Cake” Type.

    At this point, we can search our GraphQL Schema for “food” and we’ll find the “Food” interface, and see that it is implemented by “Pizza” and “Cake”.


    However, we will also see that no field in the Schema returns the “food” type.

    There’s no “allFood” field, like we set out to query.

    There is a “RootQuery.allPizza” and “RootQuery.cakes” field for querying lists of Pizza and lists of Cakes independently, but no “allFood” field.

    Let’s add that!

    Register “allFood” connection

    Since we decided to manage our Pizza and Cake in different Post types, we need to provide an entry point into the Graph that allows us to query items of both of these post types as a single list.

    To do this we will use the register_graphql_connection API.

    Within the “graphql_register_types” hook above, we can add the following:

    register_graphql_connection([
      'fromType' => 'RootQuery',
      'toType' => 'Food',
      'fromFieldName' => 'allFood',
      'resolve' => function( $root, $args, $context, $info ) {
        $resolver = new \WPGraphQL\Data\Connection\PostObjectConnectionResolver( $root, $args, $context, $info );
        $resolver->set_query_arg( 'post_type', [ 'pizza', 'cake' ] );
        return $resolver->get_connection();
      }
    ]);

    This code registers a GraphQL Connection from the “RootQuery” to the “Food” type. A Connection is a way in GraphQL to query paginated lists of data. Here, we’ve defined our connection’s “fromType” and “toType”, and set the “fromFieldName”to “allFood’.

    This will expose the connection to the Schema. At this point, we would be able to execute our query, but we wouldn’t get any results unless we also had a resolver.

    Our resolver function handles querying the “food” from the database and returning data in the proper “connection” shape.

    In GraphQL, all resolvers are passed 4 arguments:

    • $source: The object being executed that the resolving field belongs to
    • $args: Any input arguments on the field
    • $context: Context about the request
    • $info: Info about where in the resolve tree execution is

    In our resolver above, we take all 4 of these arguments and pass them to a new instance of “\WPGraphQL\Data\Connection\PostObjectConnectionResolver”. This class handles a lot of the complicated work of fetching data from the WordPress Posts table and returning it in a proper shape for connections.

    After instantiating the PostObjectConnectionResolver, we use its “set_query_arg” method to ensure the connection resolver will resolve data from the “pizza” and “cake” post types. Then we return the connection.

    At this point, we should be able to explore the Schema and see that we now have a “RootQuery.allFood” field, and it returns a “RootQueryToFoodConnection”.

    If we tried to query it now, the query would be valid and we shouldn’t get any errors, but we would also get no results.

    That’s because we haven’t entered data into our Pizza and Cake post types!

    Create a Pizza and a Cake

    We can follow the same steps we did in the first scenario to enter data for the “Pizza” and “Cake” post type.

    The difference, this time, is that we can enter them in their own Custom Post Type, and we don’t need to specify the “food_type” meta field, as we’re using the post type itself to make the distinction.

    Here’s what my Pizza looks like:

    Here’s what my Cake looks like:

    Query for allFood

    Now that we have some data in our “pizza” and “cake” post types, we can successfully execute the query we set out for.

    Conclusion

    In this post we explored 2 different ways to manage different types of Food and then query that food using WPGraphQL.

    The first scenario opted for using a single post type for managing all food, and using a meta field to differentiate the type of food.

    In this scenario, since only one post type was registered to the Schema we didn’t need to register a custom connection, it was done automatically by WPGraphQL.

    One thing we didn’t cover in detail in this post, was mutations.

    In scenario 1, by registering just one post type, WPGraphQL would have added the following mutations: “createFood”, “updateFood”, “deleteFood”. But in scenario 2 each post type would have its own mutations: “createPizza”, “updatePizza”, “deletePizza” and “createCake”, “updateCake”, “deleteCake”.

    There are pros and cons to both options, here. I encourage you to play with both options, explore the Schema and understand how each option might best serve your projects.

    I hope you learned something in this post and look forward to hearing stories about how these APIs have helped speed up your project development!

  • Adding End 2 End Tests to WordPress plugins using wp-env and wp-scripts

    I recently published a video walking through how End to End tests are set up for WPGraphQL, but I thought it would be good to publish a more direct step-to-step tutorial to help WordPress plugin developers set up End 2 End tests for their own WordPress plugins.

    Setting up End to End tests for WordPress plugins can be done in a number of ways (Codeception, Cypress, Ghost Inspector, etc), but lately, the easiest way I’ve found to do this is to use the @wordpress/env and @wordpress/scripts packages, distributed by the team working on the WordPress Block Editor (a.k.a. Gutenberg), along with GitHub Actions.

    If you want to skip the article and jump straight to the code, you can find the repo here: https://github.com/wp-graphql/wp-graphql-e2e-tests-example

    What are End to End tests?

    Before we get too far, let’s cover what end to end tests even are.

    When it comes to testing code, there are three common testing approaches.

    • Unit Tests: Testing individual functions
    • Integration Tests: Testing various units when integrated with each other.
    • End to End Tests (often called Acceptance Tests): Testing that tests the application as an end user would interact with it. For WordPress, this typically means the test will open a browser and interact with the web page, click buttons, submit forms, etc.

    For WPGraphQL, the majority of the existing tests are Integration Tests, as it allows us to test the execution of GraphQL queries and mutations, which requires many function calls to execute in the WPGraphQL codebase and in WordPress core, but doesn’t require a browser to be loaded in the testing environment.

    The End to End tests in WPGraphQL are for the GraphiQL IDE tools that WPGraphQL adds to the WordPress dashboard.

    What’s needed for End to End Tests with WordPress?

    In order to set up End to End tests for WordPress, we need a way for the test suite to visit pages of a WordPress site and interact with the web page that WordPress is serving. We also need a way to write programs that can interact with the Web Page, and we need to be able to make assertions that specific behaviors are or are not happening when the web page(s) are interacted with. We also need a way for this to run automatically when our code changes.

    Let’s break down how we’ll tackle this:

    • @wordpress/env: Sets up a WordPress environment (site) for the test suites to interact with
    • @wordpress/scripts: Runs the tests using Puppeteer and Jest. This lets our tests open the WordPress site in a Chrome browser and interact with the page.
      • Puppeteer: A Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer has APIs that we will use to write tests that interact with the pages.
      • Jest: JavaScript Testing Framework with a focus on simplicity
    • GitHub Actions: We’ll be using GitHub actions for our Continuous Integration. You should be able to apply what is covered in this post to other CI tools, such as CircleCI.

    Setting up our dependencies

    I’m going to assume that you already have a WordPress plugin that you want to add tests to. But, if this is your first time building a WordPress plugin, you can see this commit to the example plugin to see what’s needed to get a basic WordPress plugin set up, with zero functionality.

    If you do not have a package.json already, you’ll need to create a new package.json file, with the following:

    {
      "name": "wp-graphql-e2e-tests-example",
      "version": "0.0.1",
      "description": "An example plugin showing how to set up End to End tests using @wordpress/env and @wordpress/scripts",
      "devDependencies": {},
      "scripts": {},
    }

    npm “devDependencies”

    ???? If you don’t already have node and npm installed on your machine, you will need to do that now.

    We need the following “dev dependencies” for our test suite:

    • @wordpress/e2e-test-utils
    • @wordpress/env
    • @wordpress/jest-console
    • @wordpress/jest-puppeteer-axe
    • @wordpress/scripts
    • expect-puppeteer
    • puppeteer-testing-library

    The difference between “dependencies” and “devDependencies” is that if you are bundling a JavaScript application for production, the “dedpendencies” will be included in the bundles for use at runtime, but “devDependencies” are only used during development for things like testing, linting, etc and are not included in the built application for use at runtime. We don’t need Jest or Puppeteer, etc in our runtime application, just while developing.

    We can install these via the command line:

    npm install @wordpress/e2e-test-utils @wordpress/env @wordpress/jest-console @wordpress/jest-puppeteer-axe @wordpress/scripts expect-puppeteer puppeteer-testing-library --d

    Or you can paste the devDependencies in the package.json and run npm install.

    Whether you install via the command line or pasting into package.json, the resulting devDependencies block in your package.json should look like the following:

    "devDependencies": {
      "@wordpress/e2e-test-utils": "^6.0.0",
      "@wordpress/env": "^4.2.0",
      "@wordpress/jest-console": "^5.0.0",
      "@wordpress/jest-puppeteer-axe": "^4.0.0",
      "@wordpress/scripts": "^20.0.2",
      "expect-puppeteer": "^6.1.0",
      "puppeteer-testing-library": "^0.6.0"
    }

    Adding a .gitignore

    It’s also a good idea to add a .gitignore file to ensure we don’t version the node_modules directory. These dependencies are only needed when developing, so they can be installed on the machine that needs them, when needed. They don’t need to be versioned in the project. I’ve also included an ignore for .idea which are files generated by PHPStorm. If your IDE or operating system includes hidden files that are not needed for the project, you can ignore them here as well.

    # Ignore the node_modules, we don't want to version this directory
    node_modules
    
    # This ignores files generated by JetBrains IDEs (I'm using PHPStorm)
    .idea

    At this point, we have our package.json and our .gitignore setup. You can see this update in this commit.

    Setting up the WordPress Environment

    Now that we’ve got the initial setup out of the way, let’s move on to getting the WordPress environment set up.

    The @wordpress/env package is awesome! It’s one, of many, packages that have been produced as part of the efforts to build the WordPress block editor (a.k.a. Gutenberg). It’s a great package, even if you’re not using the block editor for your projects. We’re going to use it here to quickly spin up a WordPress environment with our custom plugin active.

    Adding the wp-env script

    The command we want to run to start our WordPress environment, is npm run wp-env start, but we don’t have a script defined for this in our `package.json`.

    Let’s add the following script:

    ...
    "scripts": {
      "wp-env": "wp-env"
    }

    You can see the commit making this change here.

    Start the WordPress environment

    With this in place, we can now run the command: npm run wp-env start

    You should see output pretty similar to the following:

    > wp-graphql-e2e-tests-example@0.0.1 wp-env
    > wp-env "start"
    
    WordPress development site started at http://localhost:8888/
    WordPress test site started at http://localhost:8889/
    MySQL is listening on port 61812
    MySQL for automated testing is listening on port 61840
    

    Two WordPress environments are now running. You can click the links to open them in a browser.

    And just like that, you have a WordPress site up and running!

    Stopping the WordPress environment

    If you want to stop the environment, you can run npm run wp-env stop.

    This will generate output like the following:

    > wp-graphql-e2e-tests-example@0.0.1 wp-env
    > wp-env "stop"
    
    ✔ Stopped WordPress. (in 1s 987ms)

    And visiting the url in a browser will now 404, as there is no longer a WordPress site running on that port.

    Configuring wp-env

    At this point, we’re able to start a WordPress environment pretty quickly, but, if we want to be able to test functionality of our plugin, we’ll want the WordPress environment to start with our plugin active, so we can test it.

    We can do this by adding a .wp-env.json file to the root of our plugin, and configuring the environment to have our plugin active when the environment starts.

    Set our plugin to be active in WordPress

    At the root of the plugin, add a file named .wp-env.json with the following contents:

    {
      "plugins": [ "." ]
    }

    We can use this config file to tell WordPress which plugins and themes to have active by default, and we can configure WordPress in other ways as well.

    In this case, we’ve told WordPress we want the current directory to be activated as a plugin.

    You can see this change in this commit.

    Login and verify

    Now, if you start the environment again (by running npm run wp-env start), you can login to the WordPress dashboard to see the plugin is active.

    You can login at: http://localhost:8888/wp-admin using the credentials:

    • username: admin
    • password: password

    Then visit the plugins page at: http://localhost:8888/wp-admin/plugins.php

    You should see our plugin active:

    Screenshot of the Plugin page in the WordPress dashboard, showing our plugin active.

    Running tests

    Now that we’re able to get a WordPress site running with our plugin active, we’re ready to start testing!

    At this point, there are 2 more things we need to do before we can run some tests.

    • write some tests
    • define scripts to run the tests

    Writing our first test

    Since our plugin doesn’t have any functionality to test, we can write a simple test that just makes an assertion that we will know is always true, just so we can make sure our test suites are running as expected.

    Let’s add a new file under /tests/e2e/example.spec.js.

    The naming convention *.spec.js is the default naming convention for wp-scripts to be able to run the tests. We can override this pattern if needed, but we won’t be looking at overriding that in this post.

    Within that file, add the following:

    describe( 'example test', () => {
    
        it( 'works', () => {
            expect( true ).toBeTruthy()
        })
    
    })

    This code is using two global methods from Jest:

    • describe: Creates a block of related tests
    • it: A function used to run a test (this function is an alias of the “test” function)

    Adding scripts to run the tests

    In order to run the test we just wrote, we’ll need to add some test scripts to the package.json file.

    Right above where we added the wp-env script, paste the following scripts:

    "test": "echo \"Error: no test specified\" && exit 1",
    "test-e2e": "wp-scripts test-e2e",
    "test-e2e:debug": "wp-scripts --inspect-brk test-e2e --puppeteer-devtools",
    "test-e2e:watch": "npm run test-e2e -- --watch",

    These scripts work as follows:

    • npm run test: This will return an error that a specific test should be specified
    • npm run test-e2e: This will run any tests that live under the tests/e2e directory, within files named *.spec.js
    • npm run test-e2e:debug: This will run the e2e tests, but with Puppeteer devtools, which means a Chrome browser will open and we can watch the tests run. This is super handy, and a lot of fun to watch.
    • npm run test-e2e:watch: This will watch as files change and will re-run the tests automatically when changes are made.

    Run the tests

    Now that we have a basic test in place, and our scripts configured, let’s run the test command so we can see how it works.

    In your command line, run the command npm run test-e2e.

    This will run our test suite, and we should see output like the following:

    > wp-graphql-e2e-tests-example@0.0.1 test-e2e
    > wp-scripts test-e2e
    
    Chromium is already in /Users/jason.bahl/Sites/libs/wp-graphql-e2e-tests-example/node_modules/puppeteer-core/.local-chromium/mac-961656; skipping download.
     PASS  tests/e2e/example.spec.js
      example test
        ✓ works (2 ms)
    
    Test Suites: 1 passed, 1 total
    Tests:       1 passed, 1 total
    Snapshots:   0 total
    Time:        0.434 s, estimated 1 s
    Ran all test suites.

    Amazing! Our first test that checks if true is indeed truthy, worked! Great!

    Just to make sure things are working as expected, we can also add a test that we expect to fail.

    Under our first test, we can add:

      it ( 'fails', () => {
        expect( false ).toBeTruthy()
      })

    This test should fail.

    If we run the script again, we should see the following output:

    > wp-graphql-e2e-tests-example@0.0.1 test-e2e
    > wp-scripts test-e2e
    
    Chromium is already in /Users/jason.bahl/Sites/libs/wp-graphql-e2e-tests-example/node_modules/puppeteer-core/.local-chromium/mac-961656; skipping download.
     FAIL  tests/e2e/example.spec.js
      example test
        ✓ works (1 ms)
        ✕ fails (73 ms)
    
      ● example test › fails
    
        expect(received).toBeTruthy()
    
        Received: false
    
           6 |
           7 |     it ( 'fails', () => {
        >  8 |         expect( false ).toBeTruthy()
             |                         ^
           9 |     })
          10 |
          11 | })
    
          at Object. (tests/e2e/example.spec.js:8:25)
    
    Test Suites: 1 failed, 1 total
    Tests:       1 failed, 1 passed, 2 total
    Snapshots:   0 total
    Time:        0.519 s, estimated 1 s
    Ran all test suites.

    We can delete that 2nd test now that we’re sure the tests are running properly.

    You can see the state of the plugin at this commit.

    Testing that our plugin is active

    Right now, testing that true is truthy isn’t a very valuable test. It shows that the tests are running, but it’s not ensuring that our plugin is working properly.

    Since our plugin doesn’t have any functionality yet, we don’t have much to test.

    One thing we can do to get familiar with some of the test utilities, is testing that the plugin is active in the Admin.

    To do this we will need to:

    • Login to WordPress as an admin user
    • Visit the plugins page
    • Check to see if our plugin is active.
      • As a human, we can see a plugin is active because it’s highlighted different than inactive plugins. A machine (our tests) can see if a plugin is active by inspecting the HTML and seeing if the plugin has certain attributes.

    Writing the test

    In our example.spec.js file, we can add a new test. Go ahead and paste the following below the first test.

    it ( 'verifies the plugin is active', async () => {
      // Steps:
      // login as admin
      // visit the plugins page
      // assert that our plugin is active by checking the HTML
    });

    Right now, these steps are just comments to remind us what this test needs to do. Now, we need to tell the test to do these things.

    Login as Admin

    One of the dependencies we added in our package.json, was @wordpress/e2e-test-utils. This package has several helpful functions that we can use while writing e2e tests.

    One of the helpful functions is a loginUser function, that opens the login page of the WordPress site, enters a username and password, then clicks login.

    The loginUser function accepts a username and password as arguments, but if we don’t pass any arguments, the default behavior is to login as the admin user.

    In our /tests/e2e/example.spec.js file, let’s import the loginUser function at the top of the file:

    import { loginUser } from '@wordpress/e2e-test-utils'

    Then, let’s add this function to our test:

    it ( 'verifies the plugin is active', async () => {
    
      // login as admin
      await loginUser();
    
      // visit the plugins page
      // assert that our plugin is active by checking the HTML
    
    });

    Visit the Plugins Page

    Next, we want to visit the plugins page. And we can do this with another function from the @wordpress/e2e-test-utils package: visitAdminPage().

    Let’s import this function:

    import { loginUser, visitAdminPage } from '@wordpress/e2e-test-utils'

    And add it to our test:

    it ( 'verifies the plugin is active', async () => {
    
      // login as admin
      await loginUser();
    
      // visit the plugins page
      await visitAdminPage( 'plugins.php' );
    
      // assert that our plugin is active by checking the HTML
    
    });

    At this point, you should now be able to run the test suite in debug mode and watch the test script login to WordPress and visit the admin page.

    Run the command npm run test-e2e:debug.

    You should see the tests run, open Chrome, login as an admin that navigate away from the dashboard to the plugins page, then we should see the tests marked as passing in the terminal.

    Screen recording showing the test running in debug mode. The Chrome browser opens and logs into the admin then navigates to another page.

    NOTE: If you’re in PHP Storm or another JetBrains IDE, the debugger will kick in for you automatically. If you’re in VSCode, you might need to add a .vscode/launch.json file, like this.

    Asserting that the plugin is active

    Now that we’ve successfully logged into the admin and navigated to the Plugins page, we can now write our assertion that the plugin is active.

    If we wanted to inspect the HTML of the plugins page to see if the plugin is active, we could open up our browser dev tools and inspect the element. We would see that the row for our active plugin looks like so:

    <tr class="active" data-slug="wpgraphql-end-2-end-tests-example" data-plugin="wp-graphql-e2e-tests-example/wp-graphql-e2e-tests-example.php">

    We want to make an assertion that the plugins page contains a <tr> element, that has a class with the value of active, and a data-slug attribute with the value of wpgraphql-end-2-end-tests-example (or whatever your plugin name is).

    We can use XPath expressions for this.

    I’m not going to go deep into XPath here, but I will show you how to test this in your browser dev tools.

    You can open up the plugins page in your WordPress install (that you started from the npm run wp-env command). Then in the console, paste the following line:

    $x('//tr[contains(@class, "active") and contains(@data-slug, "wpgraphql-end-2-end-tests-example")]')

    You should see that it found exactly one element, as shown in the screenshot below.

    Screenshot of testing XPath in the Chrome browser dev tools.

    We can take this code that works in the browser dev tools, and convert it to use the page.$x method from Puppeteer.

    NOTE: the page object from Puppeteer is a global object in the test environment, so we don’t need to import it like we imported the other utils functions.

    // Select the plugin based on slug and active class
            const activePlugin = await page.$x('//tr[contains(@class, "active") and contains(@data-slug, "wpgraphql-end-2-end-tests-example")]');

    Then, we can use the (also global) expect method from jest, to make an assertion that the plugin is active:

    // assert that our plugin is active by checking the HTML
    expect( activePlugin?.length ).toBe( 1 );

    The full test should look like so:

      it ( 'verifies the plugin is active', async () => {
    
      // login as admin
      await loginUser();
    
      // visit the plugins page
      await visitAdminPage( 'plugins.php' );
    
      // Select the plugin based on slug and active class
      const activePlugin = await page.$x('//tr[contains(@class, "active") and contains(@data-slug, "wpgraphql-end-2-end-tests-example")]');
    
      // assert that our plugin is active by checking the HTML
      expect( activePlugin?.length ).toBe( 1 );
    
    });

    Running the test should pass. We can verify that the test is actually working and not providing a false pass, by changing the name of the slug in our expect statement. If we changed the slug to “non-existent-plugin” but still assert that there would be 1 active plugin with that slug, we would have a failing test!

    Continuous Integration

    Right now, we can run the tests on our own machine. And contributors could run the tests if they cloned the code to their machine.

    But, one thing that is nice to set up for tests like this, is to have the tests run when code changes. That will give us the confidence that new features and bugfixes don’t break old features and functionality.

    Setting up a GitHub Workflow

    We’re going to set up a GitHub Workflow (aka GitHub Action) that will run the tests when a Pull Request is opened against the repository, or when code is pushed directly to the master branch.

    To create a GitHub workflow, we can create a file at .github/workflows/e2e-tests.yml.

    Then, we can add the following:

    name: End-to-End Tests
    
    on:
      pull_request:
      push:
        branches:
          - master
    
    jobs:
      admin:
        name: E2E Tests
        runs-on: ubuntu-latest
        strategy:
          fail-fast: false
          matrix:
            node: ['14']
    
        steps:
          - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4
    
          - name: Setup environment to use the desired version of NodeJS
            uses: actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f # v2.2.2
            with:
              node-version: ${{ matrix.node }}
              cache: npm
    
          - name: Installing NPM dependencies
            run: |
              npm install
    
          - name: Starting the WordPress Environment
            run: |
              npm run wp-env start
    
          - name: Running the tests
            run: |
              npm run test-e2e

    If you’ve never setup a GitHub workflow, this might look intimidating, but if you slow down to read it carefully, it’s pretty self-descriptive.

    The file gives the Worfklow a name “End-to-End Tests”.

    name: End-to-End Tests

    Then, it configures what GitHub actions the Workflow should be triggered by. We configure it to run “on” the “pull_request” and the “push” actions, if the push is to the “master” branch.

    on:
      pull_request:
      push:
        branches:
          - master

    Then, we define what jobs to run and set up the environment to us ubuntu-latest and node 14.

    jobs:
      admin:
        name: E2E Tests
        runs-on: ubuntu-latest
        strategy:
          fail-fast: false
          matrix:
            node: ['14']

    Then, we define the steps for the job.

    The first step is to “checkout” the codebase.

    - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4

    Then, we setup Node JS using the specified version.

          - name: Setup environment to use the desired version of NodeJS
            uses: actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f # v2.2.2
            with:
              node-version: ${{ matrix.node }}
              cache: npm

    Then, we install our NPM dependencies.

          - name: Installing NPM dependencies
            run: |
              npm install

    Then, we start the WordPress environment.

          - name: Starting the WordPress Environment
            run: |
              npm run wp-env start

    And last, we run the tests.

          - name: Running the tests
            run: |
              npm run test-e2e

    And now, with this in place, our tests will run (and pass!) in GitHub!

    You can see the passing test run here.

    Conclusion

    I hope this post helps you understand how to use the @wordpress/scripts and @wordpress/env packages, Jest, Puppeteer, and GitHub actions to test your WordPress plugins and themes.

    If you’re interested in content like this, please subscribe to the WPGraphQL YouTube Channel and follow WPGraphQL on Twitter!

    If you’ve never tried using GraphQL with WordPress, be sure to install and activate WPGraphQL as well!

  • Query any page by its path using WPGraphQL

    One of the most common ways WordPress is used, is by users visiting a URL of a WordPress site and reading the content on the page.

    WordPress has internal mechanisms that take the url from the request, determine what type of entity the user is requesting (a page, a blog post, a taxonomy term archive, an author’s page, etc) and then returns a specific template for that type of content.

    This is a convention that users experience daily on the web, and something developers use to deliver unique experiences for their website users.

    When you go “headless” with WordPress, and use something other than WordPress’s native theme layer to display the content, it can be tricky to determine how to take a url provided by a user and convert that into content to show your users.

    In this post, we’ll take a look at a powerful feature of WPGraphQL, the nodeByUri query, which accepts a uri input (the path to the resource) and will return the node (the WordPress entity) in response.

    You can use this to re-create the same experience WordPress theme layer provides, by returning unique templates based on the type of content being requested.

    WPGraphQL’s “nodeByUri” query

    One of the benefits of GraphQL is that it can provide entry points into the “graph” that (using Interfaces or Unions) can return different Types of data from one field.

    WPGraphQL provides a field at the root of the graph named nodeByUri. This field accepts one argument as input, a $uri. And it returns a node, of any Type that has a uri. This means any public entity in WordPress, such as published authors, archive pages, posts of a public post type, terms of a public taxonomy, etc.

    When a URI is input, this field resolves to the “node” (post, page, etc) that is associated with the URI, much like entering the URI in a web browser would resolve to that piece of content.

    If you’ve not already used the “nodeByUri” query, it might be difficult to understand just reading about it, so let’s take a look at this in action.

    Here’s a video where I walk through it, and below are some highlights of what I show in the video.

    Video showing how to use the nodeByUri query in WPGraphQL

    Writing the query

    Let’s start by querying the homepage.

    First, we’ll write our query:

    query GetNodeByUri($uri: String!) {
      nodeByUri(uri: $uri) {
        __typename
      }
    }

    In this query, we’re doing a few things.

    First, we give our query a name “GetNodeByUri”. This name can be anything we want, but it can be helpful with tooling, so it’s best practice to give your queries good names.

    Next, we define our variable input to accept: $uri: String!. This tells GraphQL that there will be one input that we don’t know about up front, but we agree that we will submit the input as a string.

    Next, we declare what field we want to access in the graph: nodeByUri( uri: $uri ). We’re telling WPGraphQL that we want to give it a URI, and in response, we want a node back.

    The nodeByUri field is defined in the Schema to return the GraphQL Type UniformResourceIdentifiable, which is a GraphQL Interface implemented by any Type in the Graph that can be accessed via a public uri.

    Screenshot of the nodeByUri field shown in GraphiQL

    If we inspect the documentation in GraphiQL for this type, we can see all of the available Types that can be returned.

    Screenshot of the UniformResourceIdentifiable GraphQL Interface in GraphiQL.

    The Types that can be returned consist of public Post Types, Public Taxonomies, ContentType (archives), MediaItem, and User (published authors are public).

    So, we know that any uri (path) that we query, we know what we can ask for and what to expect in response.

    Execute the query

    Now that we have the query written, we can use GraphiQL to execute the query.

    GraphiQL has a “variables” pane that we will use to input our variables. In this case, the “uri” (or path) to the resource is our variable.

    First, we will enter “/” as our uri value so we can test querying the home page.

    Screenshot of the “uri” variable entered in the GraphiQL Variables pane.

    Now, we can execute our query by pressing the “Play” button in GraphiQL.

    And in response we should see the following response:

    {
      "data": {
        "nodeByUri": {
          "__typename": "ContentType"
        }
      }
    }
    Screenshot of the nodeByUri query for the “/” uri.

    Expanding the query

    We can see that when we query for the home page, we’re getting a “ContentType” node in response.

    We can expand the query to ask for more fields of the “ContentType”.

    If we look at the home page of https://demo.wpgraphql.com, we will see that it serves as the “blogroll” or the blog index. It’s a list of blog posts.

    This is why WPGraphQL returns a “ContentType” node from the Graph.

    We can write a fragment on this Type to ask for fields we want when the query returns a “ContentType” node.

    If we look at the documentation in GraphiQL for the ContentType type, we can see all the fields that we can ask for.

    Screenshot of the ContentType documentation in GraphiQL

    If our goal is to re-create the homepage we’re seeing in WordPress, then we certainly don’t need all the fields! We can specify exactly what we need.

    In this case, we want to ask for the following fields:

    • name: the name of the content type
    • isFrontPage: whether the contentType should be considered the front page
    • contentNodes (and sub-fields): a connection to the contentNodes on the page

    This should give us enough information to re-create what we’re seeing on the homepage.

    Let’s update our query to the following:

    query GetNodeByUri($uri: String!) {
      nodeByUri(uri: $uri) {
        __typename
        ... on ContentType {
          name
          uri
          isFrontPage
          contentNodes {
            nodes {
              __typename
              ... on Post {
                id
                title
              }
            }
          }
        }
      }
    }
    

    And then execute the query again.

    We now see the following results:

    {
      "data": {
        "nodeByUri": {
          "__typename": "ContentType",
          "name": "post",
          "uri": "/",
          "isFrontPage": true,
          "contentNodes": {
            "nodes": [
              {
                "__typename": "Post",
                "id": "cG9zdDoxMDMx",
                "title": "Tiled Gallery"
              },
              {
                "__typename": "Post",
                "id": "cG9zdDoxMDI3",
                "title": "Twitter Embeds"
              },
              {
                "__typename": "Post",
                "id": "cG9zdDoxMDE2",
                "title": "Featured Image (Vertical)…yo"
              },
              {
                "__typename": "Post",
                "id": "cG9zdDoxMDEx",
                "title": "Featured Image (Horizontal)…yo"
              },
              {
                "__typename": "Post",
                "id": "cG9zdDoxMDAw",
                "title": "Nested And Mixed Lists"
              },
              {
                "__typename": "Post",
                "id": "cG9zdDo5OTY=",
                "title": "More Tag"
              },
              {
                "__typename": "Post",
                "id": "cG9zdDo5OTM=",
                "title": "Excerpt"
              },
              {
                "__typename": "Post",
                "id": "cG9zdDo5MTk=",
                "title": "Markup And Formatting"
              },
              {
                "__typename": "Post",
                "id": "cG9zdDo5MDM=",
                "title": "Image Alignment"
              },
              {
                "__typename": "Post",
                "id": "cG9zdDo4OTU=",
                "title": "Text Alignment"
              }
            ]
          }
        }
      }
    }

    If we compare these results from our GraphQL Query, we can see that we’re starting to get data that matches the homepage that WordPress is rendering.

    Screenshot of the homepage

    There’s more information on each post, such as:

    • post author
      • name
      • avatar url
    • post date
    • post content
    • uri (to link to the post with)

    We can update our query once more with this additional information.

    query GetNodeByUri($uri: String!) {
      nodeByUri(uri: $uri) {
        __typename
        ... on ContentType {
          name
          uri
          isFrontPage
          contentNodes {
            nodes {
              __typename
              ... on Post {
                id
                title
                author {
                  node {
                    name
                    avatar {
                      url
                    }
                  }
                }
                date
                content
                uri
              }
            }
          }
        }
      }
    }

    Breaking into Fragments

    The query is now getting us all the information we need, but it’s starting to get a bit long.

    We can use a feature of GraphQL called Fragments to break this into smaller pieces.

    I’ve broken the query into several Fragments:

    query GetNodeByUri($uri: String!) {
      nodeByUri(uri: $uri) {
        __typename
        ...ContentType
      }
    }
    
    fragment ContentType on ContentType {
      name
      uri
      isFrontPage
      contentNodes {
        nodes {
          ...Post
        }
      }
    }
    
    fragment Post on Post {
      __typename
      id
      date
      uri
      content
      title
      ...Author
    }
    
    fragment Author on NodeWithAuthor {
      author {
        node {
          name
          avatar {
            url
          }
        }
      }
    }
    

    Fragments allow us to break the query into smaller pieces, and the fragments can ultimately be coupled with their components that need the data being asked for in the fragment.

    Here, I’ve created 3 named fragments:

    • ContentType
    • Post
    • Author

    And then we’ve reduced the nodeByUri field to only ask for 2 fields:

    • __typename
    • uri

    The primary responsibility of the nodeByUri field is to get the node and return it to us with the __typename of the node.

    The ContentType fragment is now responsible for declaring what is important if the node is of the ContentType type.

    The responsibility of this Fragment is to get some details about the type, then get the content nodes (posts) associated with it. It’s not concerned with the details of the post, though, so that becomes another fragment.

    The Post fragment defines the fields needed to render each post, then uses one last Author fragment to get the details of the post author.

    We can execute this query, and get all the data we need to re-create the homepage!! (sidebar widgets not included)

    Querying a Page

    Now, we can expand our query to account for different types.

    If we enter the /about path into our “uri” variable, and execute the same query, we will get this payload:

    {
      "data": {
        "nodeByUri": {
          "__typename": "Page"
        }
      }
    }
    Screenshot of initial query for the “/about” uri

    We’re only getting the __typename field in response, because we’ve told GraphQL to only return data ...on ContentType and since the node was not of the ContentType type, we’re not getting any data.

    Writing the fragment

    So now, we can write a fragment to ask for the specific information we need if the type is a Page.

    fragment Page on Page {
      title
      content
      commentCount
      comments {
        nodes {
          id
          content
          date
          author {
            node {
              id
              name
              ... on User {
                avatar {
                  url
                }
              }
            }
          }
        }
      }
    }

    And we can work that into the `nodeByUri` query like so:

    query GetNodeByUri($uri: String!) {
      nodeByUri(uri: $uri) {
        __typename
        ...ContentType
        ...Page
      }
    }

    So our full query document becomes (and we could break the comments of the page into fragments as well, too):

    query GetNodeByUri($uri: String!) {
      nodeByUri(uri: $uri) {
        __typename
        ...ContentType
        ...Page
      }
    }
    
    fragment Page on Page {
      title
      content
      commentCount
      comments {
        nodes {
          id
          content
          date
          author {
            node {
              id
              name
              ... on User {
                avatar {
                  url
                }
              }
            }
          }
        }
      }
    }
    
    fragment ContentType on ContentType {
      name
      uri
      isFrontPage
      contentNodes {
        nodes {
          ...Post
        }
      }
    }
    
    fragment Post on Post {
      __typename
      id
      date
      uri
      content
      title
      ...Author
    }
    
    fragment Author on NodeWithAuthor {
      author {
        node {
          name
          avatar {
            url
          }
        }
      }
    }
    

    And when we execute the query for the “/about” page now, we are getting enough information again, to reproduce the page that WordPress renders:

    {
      "data": {
        "nodeByUri": {
          "__typename": "Page",
          "title": "About",
          "content": "

    WP Test is a fantastically exhaustive set of test data to measure the integrity of your plugins and themes.

    \n

    The foundation of these tests are derived from WordPress’ Theme Unit Test Codex data. It’s paired with lessons learned from over three years of theme and plugin support, and baffling corner cases, to create a potent cocktail of simulated, quirky user content.

    \n

    The word “comprehensive” was purposely left off this description. It’s not. There will always be something new squarely scenario to test. That’s where you come in. Let us know of a test we’re not covering. We’d love to squash it.

    \n

    Let’s make WordPress testing easier and resilient together.

    \n", "commentCount": 1, "comments": { "nodes": [ { "id": "Y29tbWVudDo1NjUy", "content": "

    Test comment

    \n", "date": "2021-12-22 12:07:54", "author": { "node": { "id": "dXNlcjoy", "name": "wpgraphqldemo", "avatar": { "url": "https://secure.gravatar.com/avatar/94bf4ea789246f76c48bcf8509bcf01e?s=96&d=mm&r=g" } } } } ] } } } }

    Querying a Category Archive

    We’ve looked at querying the home page and a regular page, so now let’s look at querying a category archive page.

    If we navigate to https://demo.wpgraphql.com/category/alignment/, we’ll see that it’s the archive page for the “Alignment” category. It displays posts of the category.

    Screenshot of the Alignment category page rendered by WordPress

    If we add “/category/alignment” as our variable input to the query, we’ll now get the following response:

    {
      "data": {
        "nodeByUri": {
          "__typename": "Category"
        }
      }
    }
    Screenshot of querying the “alignment” category in GraphiQL

    So now we can write our fragment for what data we want returned when the response type is “Category”:

    Looking at the template we want to re-create, we know we need to ask for:

    • Category Name
    • Category Description
    • Posts of that category
      • title
      • content
      • author
        • name
        • avatar url
      • categories
        • name
        • uri

    So we can write a fragment like so:

    fragment Category on Category {
      name
      description
      posts {
        nodes {
          id
          title
          content
          author {
            node {
              name
              avatar {
                url
              }
            }
          }
          categories {
            nodes {
              name
              uri
            }
          }
        }
      }
    }

    And now our full query document looks like so:

    query GetNodeByUri($uri: String!) {
      nodeByUri(uri: $uri) {
        __typename
        ...ContentType
        ...Page
        ...Category
      }
    }
    
    fragment Category on Category {
      name
      description
      posts {
        nodes {
          id
          title
          content
          author {
            node {
              name
              avatar {
                url
              }
            }
          }
          categories {
            nodes {
              name
              uri
            }
          }
        }
      }
    }
    
    fragment Page on Page {
      title
      content
      commentCount
      comments {
        nodes {
          id
          content
          date
          author {
            node {
              id
              name
              ... on User {
                avatar {
                  url
                }
              }
            }
          }
        }
      }
    }
    
    fragment ContentType on ContentType {
      name
      uri
      isFrontPage
      contentNodes {
        nodes {
          ...Post
        }
      }
    }
    
    fragment Post on Post {
      __typename
      id
      date
      uri
      content
      title
      ...Author
    }
    
    fragment Author on NodeWithAuthor {
      author {
        node {
          name
          avatar {
            url
          }
        }
      }
    }

    And when I execute the query for the category, I get all the data I need to create the category archive page.

    Amazing!

    Any Type that can be returned by the nodeByUri field can be turned into a fragment, which can then be coupled with the Component that will render the data.