The Liquid Template Unit Testing Framework was created to replace Cypress for unit testing the logic in liquid templates because Cypress is slow, heavy, and is overkill for this purpose. More importantly, this framework provides us with total control over the test data which is critical for testing liquid templates, something that Cypress does not provide. For example, a you can test a given liquid template against any number of fixtures that represent different entries into the Drupal CMS, so bugs in liquid templates

End-to-end (e2e) tests on VA.gov use Cypress; Cypress has NOT been replaced by this tool.

How to Use the Framework

To test a liquid template, use the parseFixture() and renderHTML() functions in src/site/tests/support/index.js to create an HTML document. Run accessibility checks using the axeCheck() function in src/site/tests/support/axe.js.

Be sure to import the functions into your spec file like so:

import { parseFixture, renderHTML } from '~/site/tests/support';
import axeCheck from '~/site/tests/support/axe';
JS

Folder and File Naming Conventions

To create a new test for a template in the layouts directory:

  1. Create a new folder in the tests directory under layouts and give the folder the same name as the template under test (but don't include the .drupal.liquid extension).

  2. Create a new spec file using the following naming convention: 'template name' (without the .drupal.liquid extension) + .unit.spec.js.

  3. Create a fixtures folder in the same directory where the JSON fixtures can live.

Here's an illustration of this folder structure:

/layouts
  /tests
    /example_page
      /fixtures
        example_fixture_1.json
        example_fixture_2.json
      example_page.unit.spec.js
  example_page.drupal.liquid
CODE

Use the same pattern for templates under test in other directories like includes, navigation, etc.

parseFixture(filePath)

parseFixture() takes a JSON fixtures path starting from src/ and returns a JavaScript object.

renderHTML(layoutPath, data, dataName)

renderHTML() takes a liquid template path starting from src/, the JavaScript object returned by parseFixture(), and an optional dataName and renders an HTML document. We can then run the usual Mocha assertions on the result. This function uses the same code as our build process, so all of our custom liquid filters can be used.

This technique can be used to generate tests of varying complexity, ranging from simple rendering sanity checks to complex logic. Since we control the JSON test data, we can easily test different scenarios.

axeCheck(container)

axeCheck() takes the HTML document returned by renderHTML() and returns an array of accessibility violations.

Disabled Axe Checks

The following Axe Checks are disabled:

bypass

The 'bypass' check has been disabled because it may give a false positive for lists of 4-5 links. VA.gov includes a global skip to content link that is evaluated in full page e2e tests.

color-contrast

The CSS file for the static pages found on VA.gov is generated during the build process and, therefore, cannot be referenced by the HTML files generated by this tool. The color-contrast check has been disabled because it requires the CSS file.

document-title

The document-title check has been disabled because liquid template include files (not entire page templates) can also be tested by this tool and those templates are missing the title tag (along with the html, head, and body sections, etc., etc., etc.).

Here's the problem:

If you want to test the breadcrumbs include file (src/site/includes/breadcrumbs.drupal.liquid) the HTML used to instantiate a new JSDOM object looks like this:

<nav data-template="includes/breadcrumbs" aria-label="Breadcrumb" aria-live="polite" class="va-nav-breadcrumbs"
  <!-- lots of liquid code here -->
</nav>
HTML

The resulting html document looks like this:

<html>
<head></head>
<body>
  <nav data-template="includes/breadcrumbs" aria-label="Breadcrumb" aria-live="polite" class="va-nav-breadcrumbs"
    <!-- lots of liquid code here -->
  </nav>
</body>
</html>
HTML

Just like a browser, JSDOM adds whatever HTML is missing to make the document valid.

Notice there's no title tag in the head of the document. Because of this, we can't perform Axe Checks to make sure a title is present, so we need to disable the document-title check.

In addition to the reason outlined above, we experienced false-positives on pages that had a valid title tag when running the document-title check.

If you want to test the presence of a title tag, and that the title isn't an empty string, you'll have write unit tests for those 'the old fashion way'.

How to Run an Axe Check

Run an accessibility check like this:

it('reports no axe violations', async () => {
  const violations = await axeCheck(container);
  expect(violations.length).to.equal(0);
});
JS

Accessibility violations are logged to the console and look like this:

Accessibility Violations
6 Accessibility Violations Were Detected

Axe Violation 1:
{
  id: 'aria-roles',
  impact: 'critical',
  tags: [ 'cat.aria', 'wcag2a', 'wcag412' ],
  description: 'Ensures all elements with a role attribute use a valid value',
  help: 'ARIA roles used must conform to valid values',
  helpUrl: '<https://dequeuniversity.com/rules/axe/4.1/aria-roles?application=axeAPI',>
  nodes: [
    {
      any: [],
      all: [],
      none: [Array],
      impact: 'critical',
      html: '<h1 role="nonsense">VA Pittsburgh health care</h1>',
      target: [Array],
      failureSummary: 'Fix all of the following:\n' +
        '  Role must be one of the valid ARIA roles: nonsense'
    }
  ]
}

Node 1:
{
  any: [],
  all: [],
  none: [
    CheckResult {
      id: 'invalidrole',
      data: [Array],
      relatedNodes: [],
      impact: 'critical',
      message: 'Role must be one of the valid ARIA roles: nonsense'
    }
  ],
  impact: 'critical',
  html: '<h1 role="nonsense">VA Pittsburgh health care</h1>',
  target: [ 'h1' ],
  failureSummary: 'Fix all of the following:\n' +
    '  Role must be one of the valid ARIA roles: nonsense'
}
JS

axe-core Smoke Tests

A smoke test was created to ensure axe-core is picking up the expected violiations. This tests acts as a circuit breaker for potential silent failures.

The liquid template for the smoke test is located at src/site/layouts/liquid_template_axe_check_smoke_test.drupal.liquid. axe-core currently reports 6 violations on this page.

The spec file and fixture is in the src/site/layouts/tests/liquid_template_axe_check_smoke_test directory.

DOM Testing Library

To perform queries using the DOM Testing Library, simply import the queries or functions you want to use. Example:

import { getByText } from '@testing-library/dom';
CODE

From the Dom Testing Library docs: "All of the queries exported by DOM Testing Library accept a container as the first argument."

In the Liquid Template Testing Framework examples found below, the HTML document generated is assigned to a variable called container in the spec.js files. Use this variable as the first argument to the imported DOM Testing Library functions, like this:

const node = getByText(container, '3500 Ludington Street');
CODE

Rendered HTML Is Saved to Disk

For convenience, the HTML that's generated from each liquid template is automatically saved to src/site/tests/html when tests are executed so the HTML can be inspected when writing tests. These files are gitignored.

The name of the HTML file is created from liquid template path.

Example:
Given the path src/site/components/phone-number.drupal.liquid, an HTML file called phone-number.html will be created.

However, if you're calling renderHTML() many times using different JavaScript objects returned by parseFixture(), you might want to save an HTML file for each JavaScript object. In that case, pass in a third, optional argument to renderHTML() called dataName to add dataName to the HTML filename.

Example:
Given the path src/site/components/phone-number.drupal.liquid, and labelAndLocationName for dataName an HTML file called phone-number.labelAndLocationName.html will be created.

Sample Test

Here is a sample test:

Sample Liquid Template Unit Test
const layoutPath = 'src/site/layouts/landing_page.drupal.liquid';

describe('intro', () => {
  describe('no fieldTitleIcon', () => {
    const data = parseFixture(
      'src/site/layouts/tests/landing_page/fixtures/landing_page.json',
    );

    it('renders elements with expected values', async () => {
      const container = await renderHTML(layoutPath, data);
      expect(container.querySelector('h1').innerHTML).to.equal(data.title);
      expect(container.querySelector('.va-introtext p').innerHTML).to.equal(
        data.fieldIntroText,
      );
      expect(
        container.querySelector('i.icon-large.white.hub-icon-foo'),
      ).to.equal(null);
    });
  });
});
JS

Sample Spec Files

Here are several example spec files: