@wordpress/e2e-test-utilsを触ってみる
WordPressのE2Eテスト用ライブラリができたらしいので、さっそく触ってみました。 Reusable packages to setup and run e2e tests have been built in t […]
目次
WordPressのE2Eテスト用ライブラリができたらしいので、さっそく触ってみました。
Reusable packages to setup and run e2e tests have been built in the repository. Starting today, this setup was brought into WordPress and included in our CI pipeline.
https://make.wordpress.org/core/2019/06/27/introducing-the-wordpress-e2e-tests/
インストール
内部でいろいろ使っているみたいなので、結構インストールする必要がありました。
$ npm init -y
$ yarn add -D @babel/polyfill @wordpress/babel-preset-default @wordpress/e2e-test-utils @wordpress/scripts core-js jest jest-puppeteer puppeteer
ディレクトリ構成
個人的にJestのテストはtests配下に置きたい派なので、こんな構成にしています。
$ tree -I node_modules
.
├── __tests__
│   ├── config
│   │   └── bootstrap.js
│   └── specs
│       └── index.test.js
├── babel-transform.js
├── jest.config.js
├── package.json
└── yarn.lock
設定ファイルを用意する
設定ファイルがいくつか必要なので作ります。おおよそコアのコードから引っ張ってきました。
__tests__/config/bootstrap.js
import { get } from 'lodash';
import {
    clearLocalStorage,
    enablePageDialogAccept,
    setBrowserViewport,
} from '@wordpress/e2e-test-utils';
/**
 * Environment variables
 */
const { PUPPETEER_TIMEOUT } = process.env;
/**
 * Set of console logging types observed to protect against unexpected yet
 * handled (i.e. not catastrophic) errors or warnings. Each key corresponds
 * to the Puppeteer ConsoleMessage type, its value the corresponding function
 * on the console global object.
 *
 * @type {Object<string,string>}
 */
const OBSERVED_CONSOLE_MESSAGE_TYPES = {
    warning: 'warn',
    error: 'error',
};
/**
 * Array of page event tuples of [ eventName, handler ].
 *
 * @type {Array}
 */
const pageEvents = [];
// The Jest timeout is increased because these tests are a bit slow
jest.setTimeout( PUPPETEER_TIMEOUT || 100000 );
/**
 * Adds an event listener to the page to handle additions of page event
 * handlers, to assure that they are removed at test teardown.
 */
function capturePageEventsForTearDown() {
    page.on( 'newListener', ( eventName, listener ) => {
        pageEvents.push( [ eventName, listener ] );
    } );
}
/**
 * Removes all bound page event handlers.
 */
function removePageEvents() {
    pageEvents.forEach( ( [ eventName, handler ] ) => {
        page.removeListener( eventName, handler );
    } );
}
/**
 * Adds a page event handler to emit uncaught exception to process if one of
 * the observed console logging types is encountered.
 */
function observeConsoleLogging() {
    page.on( 'console', ( message ) => {
        const type = message.type();
        if ( ! OBSERVED_CONSOLE_MESSAGE_TYPES.hasOwnProperty( type ) ) {
            return;
        }
        let text = message.text();
        // An exception is made for _blanket_ deprecation warnings: Those
        // which log regardless of whether a deprecated feature is in use.
        if ( text.includes( 'This is a global warning' ) ) {
            return;
        }
        // Viewing posts on the front end can result in this error, which
        // has nothing to do with Gutenberg.
        if ( text.includes( 'net::ERR_UNKNOWN_URL_SCHEME' ) ) {
            return;
        }
        // A bug present in WordPress 5.2 will produce console warnings when
        // loading the Dashicons font. These can be safely ignored, as they do
        // not otherwise regress on application behavior. This logic should be
        // removed once the associated ticket has been closed.
        //
        // See: https://core.trac.wordpress.org/ticket/47183
        if (
            text.startsWith( 'Failed to decode downloaded font:' ) ||
            text.startsWith( 'OTS parsing error:' )
        ) {
            return;
        }
        const logFunction = OBSERVED_CONSOLE_MESSAGE_TYPES[ type ];
        // As of Puppeteer 1.6.1, `message.text()` wrongly returns an object of
        // type JSHandle for error logging, instead of the expected string.
        //
        // See: https://github.com/GoogleChrome/puppeteer/issues/3397
        //
        // The recommendation there to asynchronously resolve the error value
        // upon a console event may be prone to a race condition with the test
        // completion, leaving a possibility of an error not being surfaced
        // correctly. Instead, the logic here synchronously inspects the
        // internal object shape of the JSHandle to find the error text. If it
        // cannot be found, the default text value is used instead.
        text = get( message.args(), [ 0, '_remoteObject', 'description' ], text );
        // Disable reason: We intentionally bubble up the console message
        // which, unless the test explicitly anticipates the logging via
        // @wordpress/jest-console matchers, will cause the intended test
        // failure.
        // eslint-disable-next-line no-console
        console[ logFunction ]( text );
    } );
}
// Before every test suite run, delete all content created by the test. This ensures
// other posts/comments/etc. aren't dirtying tests and tests don't depend on
// each other's side-effects.
beforeAll( async () => {
    capturePageEventsForTearDown();
    enablePageDialogAccept();
    observeConsoleLogging();
    await setBrowserViewport( 'large' );
} );
afterEach( async () => {
    await clearLocalStorage();
    await setBrowserViewport( 'large' );
} );
afterAll( () => {
    removePageEvents();
} );
babel-transform.js
/**
 * External dependencies
 */
const babelJest = require( 'babel-jest' );
module.exports = babelJest.createTransformer( {
    presets: [ '@wordpress/babel-preset-default' ],
} );
jest.config.js
const path = require( 'path' );
const jestE2EConfig = {
    preset: 'jest-puppeteer',
    setupFilesAfterEnv: [
        '<rootDir>/__tests__/config/bootstrap.js',
    ],
    testMatch: [
        '<rootDir>/__tests__/specs/**/?(*.)(spec|test).js',
    ],
    transform: {
        '^.+\\.[jt]sx?: path.join( __dirname, 'babel-transform' ),
    },
    transformIgnorePatterns: [
        'node_modules',
        'scripts/__tests__/config/puppeteer.config.js',
    ],
};
module.exports = jestE2EConfig;
環境変数を設定
最低限以下の環境変数が必要な様子です。ちなみに値は未設定時のデフォルトです。
export WP_USERNAME=admin
export WP_PASSWORD=password
export WP_BASE_URL=https://localhost:8889
テストコードを書く
あとはテストコードを書くだけです。__tests__/specs/index.test.jsにコードを書きます。
import { visitAdminPage } from '@wordpress/e2e-test-utils';
describe( 'Hello World', () => {
    it( 'Should load properly', async () => {
        await visitAdminPage( '/' );
        const nodes = await page.$x(
            '//h2[contains(text(), "Welcome to WordPress!")]'
        );
        expect( nodes.length ).not.toEqual( 0 );
    } );
    it( 'Should fail the test', async () => {
        await visitAdminPage( '/' );
        const nodes = await page.$x(
            '//h2[contains(text(), "aaaaaa")]'
        );
        expect( nodes.length ).not.toEqual( 0 );
    } );
} );
テストを実行する
あとはテストを実行するだけです。
$ ./node_modules/.bin/wp-scripts test-e2e --config ./jest.config.js
 FAIL  __tests__/specs/index.test.js
  Hello World
    ✓ Should load properly (1865ms)
    ✕ Should fail the test (305ms)
  ● Hello World › Should fail the test
    expect(received).not.toEqual(expected) // deep equality
    Expected: not 0
      14 |                      '//h2[contains(text(), "aaaaaa")]'
      15 |              );
    > 16 |              expect( nodes.length ).not.toEqual( 0 );
         |                                         ^
      17 |      } );
      18 | } );
2つ目のテストがちゃんとコケています。
どんなテストができるのか?
ドキュメントをまだ探してないのですが、とりあえずindex.jsを見るといろいろはいっているのがわかります。
関数名を見る限りでは、
- プラグインのインストールや有効化・停止・削除
 - 投稿の作成・編集・削除
 - Gutenbergのブロック操作
 - Viewportの変更
 - キー操作
 - などができる様子です。
 
余談
実体としてはpuppeteerのラッパーなので、puppeteerのコードも利用できます。というか内部でめっちゃ使われてました。
なので下のようにスクリーンショットやPDFを適宜撮りながらテストしたり、自作プラグインなどで追加した操作を個別で実行させたりもできます。
await visitAdminPage( '/' );
await page.screenshot({path: 'admin.png'})
また、今回のコードは以下のGitHubリポジトリに公開しています。