Skip to content

Commit

Permalink
Reorganise e2e utils into admin, editor, pageUtils and requestUtils c…
Browse files Browse the repository at this point in the history
…lasses

Add new editor canvas utility

Update tests to use EditorCanvas util

WIP

Reorganize e2e utils

Fix a few things

Fix test issues

Rename folders for pageUtils and requestUtils and move isCurrentUrl back to pageUtils

Fix remaining test issues

Update docs

Make constructors consistent

Update new image block test to use reorganized utils

Update migration guide

Add step for configuring editor utils

Avoid configuring Editor in every test
  • Loading branch information
talldan committed May 11, 2022
1 parent 9120449 commit e2e9383
Show file tree
Hide file tree
Showing 49 changed files with 700 additions and 300 deletions.
39 changes: 36 additions & 3 deletions packages/e2e-test-utils-playwright/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,55 @@ npm install @wordpress/e2e-test-utils-playwright --save-dev

### test

The extended Playwright's [test](https://playwright.dev/docs/api/class-test) module with the `pageUtils` and the `requestUtils` fixtures.
The extended Playwright's [test](https://playwright.dev/docs/api/class-test) module with the `admin`, `pageUtils` and the `requestUtils` fixtures.

### expect

The Playwright/Jest's [expect](https://jestjs.io/docs/expect) function.

### Admin

End to end test utilities for WordPress admin's user interface.

```js
const admin = new Admin( { page, pageUtils } );
await admin.visitAdminPage( 'options-general.php' );
```

### Editor

End to end test utilities for the WordPress Block Editor.

To use these utilities, instantiate them within each test file:
```js
test.use( {
editor: async ( { page }, use ) => {
await use( new Editor( { page, hasIframe: true } ) );
},
} );
```

The `hasIframe` property denotes whether the editor canvas uses an Iframe, as the site editor currently does. Omit this for non-iframe editors.

Within a test or test utility, use the `canvas` property to select elements within the iframe canvas:

```js
await editor.canvas.locator( 'role=document[name="Paragraph block"i]' )
```

### PageUtils

Create a page utils instance of the current page.
Generic Playwright utilities for interacting with web pages.

```js
const pageUtils = new PageUtils( page );
const pageUtils = new PageUtils( { page } );
await pageUtils.pressKeyWithModifier( 'primary', 'a' );
```

### RequestUtils

Playwright utilities for interacting with the WordPress REST API.

Create a request utils instance.

```js
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { addQueryArgs } from '@wordpress/url';
/**
* Creates new post.
*
* @this {import('.').PageUtils}
* @this {import('.').Editor}
* @param {Object} object Object to create new post, along with tips enabling option.
* @param {string} [object.postType] Post type of the new post.
* @param {string} [object.title] Title of the new post.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
/**
* Internal dependencies
*/
import type { Admin } from './';

/**
* Regular expression matching a displayed PHP error within a markup string.
*
Expand All @@ -15,11 +20,12 @@ const REGEXP_PHP_ERROR = /(<b>)?(Fatal error|Recoverable fatal error|Warning|Par
*
* @see http://php.net/manual/en/function.error-reporting.php
*
* @this {import('./').PageUtils}
* @param {Admin} this
*
* @return {Promise<?string>} Promise resolving to a string or null, depending
* whether a page error is present.
*/
export async function getPageError() {
export async function getPageError( this: Admin ) {
const content = await this.page.content();
const match = content.match( REGEXP_PHP_ERROR );
return match ? match[ 0 ] : null;
Expand Down
40 changes: 40 additions & 0 deletions packages/e2e-test-utils-playwright/src/admin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* External dependencies
*/
import type { Browser, Page, BrowserContext } from '@playwright/test';

/**
* Internal dependencies
*/
/**
* Internal dependencies
*/
import { createNewPost } from './create-new-post';
import { getPageError } from './get-page-error';
import { visitAdminPage } from './visit-admin-page';
import { visitSiteEditor } from './visit-site-editor';
import type { PageUtils } from '../page-utils';

type AdminConstructorProps = {
page: Page;
pageUtils: PageUtils;
};

export class Admin {
browser: Browser;
page: Page;
pageUtils: PageUtils;
context: BrowserContext;

constructor( { page, pageUtils }: AdminConstructorProps ) {
this.page = page;
this.context = page.context();
this.browser = this.context.browser()!;
this.pageUtils = pageUtils;
}

createNewPost = createNewPost;
getPageError = getPageError;
visitAdminPage = visitAdminPage;
visitSiteEditor = visitSiteEditor;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,36 @@
*/
import { join } from 'path';

/**
* Internal dependencies
*/
import type { Admin } from './';

/**
* Visits admin page and handle errors.
*
* @this {import('./').PageUtils}
* @param {Admin} this
* @param {string} adminPath String to be serialized as pathname.
* @param {string} query String to be serialized as query portion of URL.
*/
export async function visitAdminPage( adminPath, query ) {
export async function visitAdminPage(
this: Admin,
adminPath: string,
query: string
) {
await this.page.goto(
join( 'wp-admin', adminPath ) + ( query ? `?${ query }` : '' )
);

// Handle upgrade required screen
if ( this.isCurrentURL( 'wp-admin/upgrade.php' ) ) {
if ( this.pageUtils.isCurrentURL( 'wp-admin/upgrade.php' ) ) {
// Click update
await this.page.click( '.button.button-large.button-primary' );
// Click continue
await this.page.click( '.button.button-large' );
}

if ( this.isCurrentURL( 'wp-login.php' ) ) {
if ( this.pageUtils.isCurrentURL( 'wp-login.php' ) ) {
throw new Error( 'Not logged in' );
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
/**
* External dependencies
*/
/**
* WordPress dependencies
*/
Expand All @@ -9,26 +6,26 @@ import { addQueryArgs } from '@wordpress/url';
/**
* Internal dependencies
*/
import type { PageUtils } from './index';

const VISUAL_EDITOR_SELECTOR = 'iframe[title="Editor canvas"i]';
import type { Admin } from './';

interface SiteEditorQueryParams {
postId: string | number;
postType: string;
}

const CANVAS_SELECTOR = 'iframe[title="Editor canvas"i]';

/**
* Visits the Site Editor main page
*
* By default, it also skips the welcome guide. The option can be disabled if need be.
*
* @param this
* @param query Query params to be serialized as query portion of URL.
* @param skipWelcomeGuide Whether to skip the welcome guide as part of the navigation.
* @param {Admin} this
* @param {SiteEditorQueryParams} query Query params to be serialized as query portion of URL.
* @param {boolean} skipWelcomeGuide Whether to skip the welcome guide as part of the navigation.
*/
export async function visitSiteEditor(
this: PageUtils,
this: Admin,
query: SiteEditorQueryParams,
skipWelcomeGuide = true
) {
Expand All @@ -38,7 +35,7 @@ export async function visitSiteEditor(
} ).slice( 1 );

await this.visitAdminPage( 'themes.php', path );
await this.page.waitForSelector( VISUAL_EDITOR_SELECTOR );
await this.page.waitForSelector( CANVAS_SELECTOR );

if ( skipWelcomeGuide ) {
await this.page.evaluate( () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Internal dependencies
*/
import type { Editor } from './index';

/**
* Clicks a block toolbar button.
*
* @param {Editor} this
* @param {string} label The text string of the button label.
*/
export async function clickBlockOptionsMenuItem( this: Editor, label: string ) {
await this.clickBlockToolbarButton( 'Options' );
await this.page
.locator(
`role=menu[name="Options"i] >> role=menuitem[name="${ label }"i]`
)
.click();
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
/**
* Internal dependencies
*/
import type { Editor } from './index';

/**
* Clicks a block toolbar button.
*
* @this {import('./').PageUtils}
* @param {Editor} this
* @param {string} label The text string of the button label.
*/
export async function clickBlockToolbarButton( label ) {
export async function clickBlockToolbarButton( this: Editor, label: string ) {
await this.showBlockToolbar();

const blockToolbar = this.page.locator(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
/**
* Internal dependencies
*/
import type { Editor } from './index';

/**
* Returns a promise which resolves with the edited post content (HTML string).
*
* @this {import('./').PageUtils}
* @param {Editor} this
*
* @return {Promise} Promise resolving with post content markup.
*/
export async function getEditedPostContent() {
export async function getEditedPostContent( this: Editor ) {
return await this.page.evaluate( () =>
// @ts-ignore (Reason: wp isn't typed)
window.wp.data.select( 'core/editor' ).getEditedPostContent()
);
}
64 changes: 64 additions & 0 deletions packages/e2e-test-utils-playwright/src/editor/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* External dependencies
*/
import type { Browser, Page, BrowserContext, Frame } from '@playwright/test';

/**
* Internal dependencies
*/
import { clickBlockOptionsMenuItem } from './click-block-options-menu-item';
import { clickBlockToolbarButton } from './click-block-toolbar-button';
import { getEditedPostContent } from './get-edited-post-content';
import { insertBlock } from './insert-block';
import { openDocumentSettingsSidebar } from './open-document-settings-sidebar';
import { openPreviewPage } from './preview';
import { selectBlockByClientId } from './select-block-by-client-id';
import { showBlockToolbar } from './show-block-toolbar';
import { saveSiteEditorEntities } from './site-editor';

type EditorConstructorProps = {
page: Page;
hasIframe?: boolean;
};

export class Editor {
browser: Browser;
page: Page;
context: BrowserContext;
#hasIframe: boolean;

constructor( { page, hasIframe = false }: EditorConstructorProps ) {
this.page = page;
this.context = page.context();
this.browser = this.context.browser()!;
this.#hasIframe = hasIframe;
}

get canvas(): Frame | Page {
let frame;

if ( this.#hasIframe ) {
frame = this.page.frame( 'editor-canvas' );
} else {
frame = this.page;
}

if ( ! frame ) {
throw new Error(
'EditorUtils: unable to find editor canvas iframe or page'
);
}

return frame;
}

clickBlockOptionsMenuItem = clickBlockOptionsMenuItem;
clickBlockToolbarButton = clickBlockToolbarButton;
getEditedPostContent = getEditedPostContent;
insertBlock = insertBlock;
openDocumentSettingsSidebar = openDocumentSettingsSidebar;
openPreviewPage = openPreviewPage;
saveSiteEditorEntities = saveSiteEditorEntities;
selectBlockByClientId = selectBlockByClientId;
showBlockToolbar = showBlockToolbar;
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
/**
* @typedef {Object} BlockRepresentation
* @property {string} name Block name.
* @property {?Object} attributes Block attributes.
* @property {?BlockRepresentation[]} innerBlocks Nested blocks.
* Internal dependencies
*/
import type { Editor } from './index';

interface BlockRepresentation {
name: string;
attributes: Object;
innerBlocks: BlockRepresentation[];
}

/**
* @this {import('./').PageUtils}
* Insert a block.
*
* @param {Editor} this
* @param {BlockRepresentation} blockRepresentation Inserted block representation.
*/
async function insertBlock( blockRepresentation ) {
async function insertBlock(
this: Editor,
blockRepresentation: BlockRepresentation
) {
await this.page.evaluate( ( _blockRepresentation ) => {
function recursiveCreateBlock( {
name,
attributes = {},
innerBlocks = [],
} ) {
}: BlockRepresentation ): Object {
// @ts-ignore (Reason: wp isn't typed).
return window.wp.blocks.createBlock(
name,
attributes,
Expand All @@ -26,6 +36,7 @@ async function insertBlock( blockRepresentation ) {
}
const block = recursiveCreateBlock( _blockRepresentation );

// @ts-ignore (Reason: wp isn't typed).
window.wp.data.dispatch( 'core/block-editor' ).insertBlock( block );
}, blockRepresentation );
}
Expand Down
Loading

0 comments on commit e2e9383

Please sign in to comment.