testcafe-drupal

A set of helper functions and examples for running TestCafe with Drupal

Usage no npm install needed!

<script type="module">
  import testcafeDrupal from 'https://cdn.skypack.dev/testcafe-drupal';
</script>

README

TestCafe Drupal

Introduction

This library provides a set of handy helper functions for running TestCafe tests on your Drupal application.

Example tests are provided are provided in the project's /example/tests directory.

Prerequisites

  • A Drupal project. Note: only Drupal 8 projects are officially supported.
  • Access to the Drush command line tool.
  • Node.js 8.6+

Setup

Installing dependencies

  • Create NPM project: npm init
  • Install this library and dependencies: npm install --save-dev testcafe-drupal testcafe testcafe-browser-provider-puppeteer @ffmpeg-installer/ffmpeg or yarn add -D ...

Preparing your tests environment

  • Create a directory tests in the root of your project
  • Create a file called config.js in the tests directory (See Config for more information)
  • Set your configuration including base URL and other parameters (Again see Config)

Creating tests

  • Create a fixtures directory within the tests directory.
  • Test files should be saved within the fixtures directory. For examples of how to write tests see this repository's example/tests folder. If you like you can copy these tests into your own project and customize them as necessary. For more information on writing test see the Testcafe documentation site.

Preparing Drupal user accounts

If your tests require a user account to be present, then you will need to either configure Drupal Testcafe to use those existing user accounts or create user accounts for the default Drupal Testcafe users.

Configure Drupal Testcafe user to use existing user account

Drupal Testcafe support three functional user types (admin, editor and authenticated user). To change the configuration of these functional user types:

  • edit test/config.js file.
  • For each of the users update the username, password and role values to those of the Drupal user account you wish to use. Note the role value should be the machine name of the user role in the Drupal system.

Create default Drupal Testcafe users

  • Make sure you have a running Drupal site which is browser accessible.
  • Create an authenticated user: drush user-create testcafe_user --password="testcafe_user" --mail="testcade_user@localhost"
  • Create an admin user: drush user-create testcafe_admin --password="testcafe_admin" --mail="testcade_admin@localhost" && drush user-add-role "administrator" testcafe_admin
  • Create an editor user: drush user-create testcafe_editor --password="testcafe_editor" --mail="testcafe_editor@localhost" && drush user-add-role "editor" testcafe_editor

The above instructions assume that Drupal user roles administrator and editor exist on the site. If your site uses different names for the "admin" and "editor" roles, then you should update the roles in the config.js file.

In addition you would need to modify the create user drush commands above.

For example, if the maching name of the editor role is ed, then the modified Drush command whould be:

drush user-create testcafe_editor --password="testcafe_editor" --mail="testcafe_editor@localhost" && drush user-add-role "ed" testcafe_editor

Running tests

To run all tests run the following command from the {PROJECT_ROOT} directory:

node ./tests

To make life easier you can edit your {PROJECT_ROOT}/package.json file to include the following script entry:

{
  ...
  "scripts": {
    ...
    "tests": "node ./tests/index.js"
  }
}

You can then run tests by using:

npm run tests

After the tests have completed you can find information on reported errors (including screenshots and videos) in the {PROJECT_ROOT}/tests/reports directory.

Config

Some configuration is required to be set in order for your tests to work.

Configuration is set in the following order of precedence, and can be composed with a combination:

  1. Environment variables
  2. {PROJECT_ROOT}/tests/config.js
  3. A set of fallback defaults

Here is a full list of available configuration and their default values

{
  baseUrl: "http://localhost:8080",
  node: {
    create: {
      path: "/node/add",
      selectors: {
        title: "#edit-title-0-value",
        save_button: "#edit-submit"
      }
    }
  },
  users: {
    admin: {
      username: "testcafe_admin",
      password: "testcafe_admin",
      role: "administrator"
    },
    editor: {
      username: "testcafe_editor",
      password: "testcafe_editor",
      role: "editor"
    },
    authenticated_user: {
      username: "testcafe_user",
      password: "testcafe_user"
    }
  },
  user: {
    login: {
      path: "/user/login",
      selectors: {
        username: "#edit-name",
        password: "#edit-pass",
        login_button: "form.user-login-form #edit-submit"
      }
    },
    add: {
      path: "/admin/people/create"
    }
  },
  message: {
    selectors: {
      status: ".messages--status",
      error: ".messages--error"
    }
  }
};

API Reference

Field

{class}

 @param object t
   TestCafe test controller.

Provides methods for interacting with Drupal fields created using the Field API.

Whilst the Field class can be used directly it should be noted that these methods can be accessed by other classes which extend the Field class. For example, the Node class provides useful methods for working with node related tests, whilst also having access to all Field class methods.

Usage:

const { Field } = require("testcafe-drupal");
...
test("Example test", async t => {
  const field = new Field(t);
  // Use field methods
});

addFileToField

@param {string} fileFieldId
  ID property of the file field.
@param {string} file
  Path to the image file to upload. Will use a default PDF image if this
  argument is not provided.

Add file to a Drupal file upload field. By default it will use a placeholder PDF file.

Usage:

const { Field } = require("testcafe-drupal");
...
test("Example test", async t => {
  const field = new Field(t);
  await field.addFileToField("edit-field-file-0-upload");
}); 

addImageToField

@param {string} fileFieldId
  ID property of the file field.
@param {object} options
  An optional argument which can contain following key-value pairs:
  - "alt": image alt text. Note that if alt text is provided but the image 
    alt text has not been enabled in Drupal, then this function will throw
    an error.
  - "title": image title text. Note that if title text is provided but the 
    image title has not been enabled in Drupal, then this funciton will 
    throw an error. 
@param {string} image
  Path to the image file to upload. Will use a default JPG image if this
  argument is not provided.

Add Image to a Drupal file upload field. By default it will use a placeholder JPEG file.

Usage:

const { Field } = require("testcafe-drupal");
...
test("Example test", async t => {
  const field = new Field(t);
  await field.addImageToField(
    "edit-field-image-0-upload",
    { alt: "my alt text", title: "my title text" }
  );
}); 

addTextToField

@param {string} id
  CSS id selector of the form field.
@param {string} text
  Text to be added to field.

Add text for a text based field.

Works with <input[type='text']>, <textarea> and CKEditor based text fields.

Usage:

const { Field } = require("testcafe-drupal");
...
test("Example test", async t => {
  const field = new Field(t);

  await field.addTextToField("edit-body-0-value", "hello")
});

checkSelectFieldHasOption

@param {string} id
  CSS id selector of the <select> element.
@param {array} text
  Select element option text to find. This value should be the text that
  is visible to the site visitor and is case sensitive.

Check <select> contains one or more given options.

Usage:

const { Field } = require("testcafe-drupal");
...
test("Example test", async t => {
  const field = new Field(t);

  await field.checkSelectFieldHasOption(
    "edit-field-country",
    ["Australia", "China", "Mali"]
  )
});

chooseSelectFieldOption

@param {string} id
  Id property of the <select> element.
@param {string} text
  Select element option text. This value is case sensitve.

Choose <select> element option

Usage:

const { Field } = require("testcafe-drupal");
...
test("Example test", async t => {
  const field = new Field(t);

  await field.chooseSelectFieldOption("edit-field-options", "Australia")
});

removeFileFromField

@param {string} id
  Id property of the Drupal file field.

Remove a file from a Drupal file field.

Usage:

test("Example test", async t => {
  const field = new Field(t);
  await field.removeFileFromField(
    "edit-field-file-0-upload"
  );
});

removeImageFromField

@param {string} id
  Id property of the Drupal file field.

Remove an image file from a Drupal image field.

Usage:

test("Example test", async t => {
  const field = new Field(t);
  await field.removeImageFromField(
    "edit-field-image-0-upload"
  );
});

Node

Provides methods for interacting with Drupal node entities.

{class}

@param {object} t
  TestCafe test controller.
@param {string} nodeType
  Machine name of the node type.
@param {object} config
  User's specific Drupal Testcafe configuration.

Usage:

const { Node, config } = require("testcafe-drupal");
...
test("Example test", async t => {
  const nodeType = "article";
  const node = new Node(t, nodeType, config);

  // Use node methods
});

The Node class extends the Field class so you can access the various field methods directly via the Node class.

const { Node, config } = require("testcafe-drupal");
...
test("Example test", async t => {
  const nodeType = "article";
  const node = new Node(t, nodeType, config);

  await node.goToNodeCreationPage();
  await node.addTextToField(
    "edit-body-0-value",
    "Here is my test text. What do you think? Something else here as well."
  );
}

checkOnNodePage

Check if currently on view node page.

Usage:

const { Node, config } = require("testcafe-drupal");
...
test("Example test", async t => {
  const nodeType = "article";
  const node = new Node(t, nodeType, config);
  
  await node.goToNodeCreationPage();
  await node.checkOnNodePage()
});

goToNodeCreationPage

Go to node creation page of given node type.

Usage:

const { Node, config } = require("testcafe-drupal");
...
test("Example test", async t => {
  const nodeType = "article";
  const node = new Node(t, nodeType, config);

  await node.goToNodeCreationPage()
});

saveNode

Save node. Clicks save button on node add/edit form.

Usage:

const { Node, config } = require("testcafe-drupal");
...
test("Example test", async t => {
  const nodeType = "article";
  const node = new Node(t, nodeType, config);

  await node.goToNodeCreationPage();
  ...
  await node.saveNode()
});

setTitle

Set node title text.

@param {string} text
  Node title text.

Usage:

const { Node, config } = require("testcafe-drupal");
...
test("Example test", async t => {
  const nodeType = "article";
  const node = new Node(t, nodeType, config);

  await node.goToNodeCreationPage();
  node.setTitle("This is the title")
  ...
});

Message

Provides methods for interacting with Drupal generated messages.

{class}

@param {object} t
  Testcafe test controller.
@param {object} config
  Drupal Testcafe configuration.

Usage:

const { Message, config } = require("testcafe-drupal");
...
test("Example test", async t => {
  const message = new Message(t, config);

  // Use message methods.
  ...
});

statusMessageContainsText

Check if Drupal generated status message contains given text.

@param {string} text
  Text to search for within message.

Usage:

const { Message, config } = require("testcafe-drupal");
...
test("Example test", async t => {
  const title = "MY NODE TITLE";
  const message = new Message(t, config);
  message.statusMessageContainsText(title + " has been created");
});

errorMessageContainsText

Check if Drupal generated error message contains given text.

@param {string} text
  Text to search for within message.

Usage:

const { Message, config } = require("testcafe-drupal");
...
test("Example test", async t => {
  const message = new Message(t, config);
  message.errorMessageContainsText("Oops, you can't do that");
});

System

config

Return an object containing the users' specified Drupal TestCafe configuration Order of precedence

  1. Environment variables
  2. tests/config.js
  3. package.json Note that environment variables will overwrite individual properties, however for config files, it's all or none.

Usage:

const { Node, config } = require("testcafe-drupal");
...
test("Example test", async t => {
  // Use config when initializing an instance of the Node class.
  const node = new Node(t, nodeType, config);
});

baseUrl

Return the target test site domain as defined in the configuration (e.g. "http://www.mysite.com").

Usage:

const { baseUrl } = require("testcafe-drupal");
...
test("Example test", async t => {
  // Use the baseUrl variable in your tests, or pre-test setup.
});

Environment variables

Certain config values can be overridden via the use of environmental variables.

Variable Overrides
process.env.TESTCAFE_BASEURL config.baseUrl
process.env.TESTCAFE_DRUPAL_USERS_ADMIN_USERNAME config.users.admin.username
process.env.TESTCAFE_DRUPAL_USERS_ADMIN_PASSWORD config.users.admin.password
process.env.TESTCAFE_DRUPAL_USERS_EDITOR_USERNAME config.users.editor.username
process.env.TESTCAFE_DRUPAL_USERS_EDITOR_PASSWORD config.users.editor.password
process.env.TESTCAFE_DRUPAL_USERS_AUTHUSER_USERNAME config.users.authenticated_user.username
process.env.TESTCAFE_DRUPAL_USERS_AUTHUSER_PASSWORD config.users.authenticated_user.password

Users

Define your Testcafe Roles.

administratorUser

Return a user with the administrator role.

Usage:

const { Role } = require("testcafe");
const { administratorUser } = require("testcafe-drupal");
...
test("Example test", async t => {
  await t.useRole(Role(...administratorUser));

  // Expectations based on this role
});

authenticatedUser

Return a regular authenticated user.

Usage:

const { Role } = require("testcafe");
const { authenticatedUser } = require("testcafe-drupal");
...
test("Example test", async t => {
  await t.useRole(Role(...authenticatedUser));

  // Expectations based on this role
});

editorUser

Return an editor user.

Usage:

const { Role } = require("testcafe");
const { editorUser } = require("testcafe-drupal");
...
test("Example test", async t => {
  await t.useRole(Role(...editorUser));

  // Expectations based on this role
});

Setting up local development environment

Provides instructions for developers on how to setup a local environment for development work on Drupal Testcafe package. It is recommended that you use Yarn due to it's support of Workspaces, which makes life easier when developing NPM packages. The following instructions assume you are using Yarn.

  1. Create a project directory and add create a package.json file with the following content.
{
  "private": true,
  "name": "my-project-name",
  "version": "1.0.0",
  "workspaces": [
    "workspace",
    "packages/*"
  ]
}
  1. Create the following directory structure in your project:
-- <project root>/
  |-- package.json
  |-- packages/
  |-- workspace/
  1. Clone the testcafe-drupal project in packages/ directory.
cd packages
git clone git@github.com:ironstar-io/testcafe-drupal.git
  1. Change into workspace/ directory and then set up new yarn project. Add the testcafe-drupal package:
cd ../workspace
yarn init
yarn add -D testcafe-drupal testcafe testcafe-browser-provider-puppeteer @ffmpeg-installer/ffmpeg
yarn install
  1. Copy the tests directory from the testcafe-drupal/example directory into the workspace directory. If you are intending to work on the example tests, then you may wish to sybolically link the tests directory to the workspace directory so that any changes will be automatically applied to the tests directory in the cloned testcafe-drupal repository.

  2. To run tests go to workspace/tests directory and run the appropriate command. For example:

cd workspace
node ./tests/index.js
  1. Final project structure should be like this:
-- <project root>/
 |--- package.json
 |--- packages/
 |  |-- testcafe-drupal
 |
 |--- workspace/
    |--package.json
    |-- tests

Built by the teams at Technocrat and Ironstar