Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pageUtils.pressKeys to playwright utils #49009

Merged
merged 1 commit into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/contributors/code/e2e/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ This document outlines a typical flow of migrating a Jest + Puppeteer test to Pl
## Migration steps for test utils
Before migrating a test utility function, think twice about whether it's necessary. Playwright offers a lot of readable and powerful APIs which make a lot of the utils obsolete. Try implementing the same thing inline directly in the test first. Only follow the below guide if that doesn't work for you. Some examples of utils that deserve to be implemented in the `e2e-test-utils-playwright` package include complex browser APIs (like `pageUtils.dragFiles` and `pageUtils.pressKeyWithModifier`) and APIs that set states (`requestUtils.*`).
Before migrating a test utility function, think twice about whether it's necessary. Playwright offers a lot of readable and powerful APIs which make a lot of the utils obsolete. Try implementing the same thing inline directly in the test first. Only follow the below guide if that doesn't work for you. Some examples of utils that deserve to be implemented in the `e2e-test-utils-playwright` package include complex browser APIs (like `pageUtils.dragFiles` and `pageUtils.pressKeys`) and APIs that set states (`requestUtils.*`).
> **Note**
> The `e2e-test-utils-playwright` package is not meant to be a drop-in replacement of the Jest + Puppeteer's `e2e-test-utils` package. Some utils are only created to ease the migration process, but they are not necessarily required.

Playwright utilities are organized a little differently from those in the `e2e-test-utils` package. The `e2e-test-utils-playwright` package has the following folders that utils are divided up into:
- `admin` - Utilities related to WordPress admin or WordPress admin's user interface (e.g. `visitAdminPage`).
- `editor` - Utilities for the block editor (e.g. `clickBlockToolbarButton`).
- `pageUtils` - General utilities for interacting with the browser (e.g. `pressKeyWithModifier`).
- `pageUtils` - General utilities for interacting with the browser (e.g. `pressKeys`).
- `requestUtils` - Utilities for making REST API requests (e.g. `activatePlugin`). These utilities are used for setup and teardown of tests.
1. Copy the existing file in `e2e-test-utils` and paste it in the `admin`, `editor`, `page` or `request` folder in `e2e-test-utils-playwright` depending on the type of util.
Expand Down
2 changes: 1 addition & 1 deletion packages/e2e-test-utils-playwright/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Generic Playwright utilities for interacting with web pages.

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

### RequestUtils
Expand Down
13 changes: 3 additions & 10 deletions packages/e2e-test-utils-playwright/src/page-utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@ import type { Browser, Page, BrowserContext } from '@playwright/test';
*/
import { dragFiles } from './drag-files';
import { isCurrentURL } from './is-current-url';
import {
setClipboardData,
pressKeyWithModifier,
} from './press-key-with-modifier';
import { pressKeyTimes } from './press-key-times';
import { setClipboardData, pressKeys } from './press-keys';
import { setBrowserViewport } from './set-browser-viewport';

type PageUtilConstructorParams = {
Expand All @@ -34,11 +30,8 @@ class PageUtils {
dragFiles: typeof dragFiles = dragFiles.bind( this );
/** @borrows isCurrentURL as this.isCurrentURL */
isCurrentURL: typeof isCurrentURL = isCurrentURL.bind( this );
/** @borrows pressKeyTimes as this.pressKeyTimes */
pressKeyTimes: typeof pressKeyTimes = pressKeyTimes.bind( this );
/** @borrows pressKeyWithModifier as this.pressKeyWithModifier */
pressKeyWithModifier: typeof pressKeyWithModifier =
pressKeyWithModifier.bind( this );
/** @borrows pressKeys as this.pressKeys */
pressKeys: typeof pressKeys = pressKeys.bind( this );
/** @borrows setBrowserViewport as this.setBrowserViewport */
setBrowserViewport: typeof setBrowserViewport =
setBrowserViewport.bind( this );
Expand Down

This file was deleted.

This file was deleted.

176 changes: 176 additions & 0 deletions packages/e2e-test-utils-playwright/src/page-utils/press-keys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/**
* External dependencies
*/
import { capitalCase } from 'change-case';
import type { Page } from '@playwright/test';

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

/**
* WordPress dependencies
*/
import {
modifiers as baseModifiers,
SHIFT,
ALT,
CTRL,
} from '@wordpress/keycodes';

let clipboardDataHolder: {
plainText: string;
html: string;
} = {
plainText: '',
html: '',
};

/**
* Sets the clipboard data that can be pasted with
* `pressKeys( 'primary+v' )`.
*
* @param this
* @param clipboardData
* @param clipboardData.plainText
* @param clipboardData.html
*/
export function setClipboardData(
this: PageUtils,
{ plainText = '', html = '' }: typeof clipboardDataHolder
) {
clipboardDataHolder = {
plainText,
html,
};
}

async function emulateClipboard( page: Page, type: 'copy' | 'cut' | 'paste' ) {
clipboardDataHolder = await page.evaluate(
( [ _type, _clipboardData ] ) => {
const clipboardDataTransfer = new DataTransfer();

if ( _type === 'paste' ) {
clipboardDataTransfer.setData(
'text/plain',
_clipboardData.plainText
);
clipboardDataTransfer.setData(
'text/html',
_clipboardData.html
);
} else {
const selection = window.getSelection()!;
const plainText = selection.toString();
let html = plainText;
if ( selection.rangeCount ) {
const range = selection.getRangeAt( 0 );
const fragment = range.cloneContents();
html = Array.from( fragment.childNodes )
.map(
( node ) =>
( node as Element ).outerHTML ?? node.nodeValue
)
.join( '' );
}
clipboardDataTransfer.setData( 'text/plain', plainText );
clipboardDataTransfer.setData( 'text/html', html );
}

document.activeElement?.dispatchEvent(
new ClipboardEvent( _type, {
bubbles: true,
cancelable: true,
clipboardData: clipboardDataTransfer,
} )
);

return {
plainText: clipboardDataTransfer.getData( 'text/plain' ),
html: clipboardDataTransfer.getData( 'text/html' ),
};
},
[ type, clipboardDataHolder ] as const
);
}

const isAppleOS = () => process.platform === 'darwin';

const isWebkit = ( page: Page ) =>
page.context().browser()!.browserType().name() === 'webkit';

const browserCache = new WeakMap();
const getHasNaturalTabNavigation = async ( page: Page ) => {
if ( ! isAppleOS() || ! isWebkit( page ) ) {
return true;
}
if ( browserCache.has( page.context().browser()! ) ) {
return browserCache.get( page.context().browser()! );
}
const testPage = await page.context().newPage();
await testPage.setContent( `<button>1</button><button>2</button>` );
await testPage.getByText( '1' ).focus();
await testPage.keyboard.press( 'Tab' );
const featureDetected = await testPage
.getByText( '2' )
.evaluate( ( node ) => node === document.activeElement );
browserCache.set( page.context().browser()!, featureDetected );
await testPage.close();
return featureDetected;
};

type Options = {
times?: number;
delay?: number;
};

const modifiers = {
...baseModifiers,
shiftAlt: ( _isApple: () => boolean ) =>
_isApple() ? [ SHIFT, ALT ] : [ SHIFT, CTRL ],
};

export async function pressKeys(
this: PageUtils,
key: string,
{ times, ...pressOptions }: Options = {}
) {
const hasNaturalTabNavigation = await getHasNaturalTabNavigation(
this.page
);

let command: () => Promise< void >;

if ( key.toLowerCase() === 'primary+c' ) {
command = () => emulateClipboard( this.page, 'copy' );
} else if ( key.toLowerCase() === 'primary+x' ) {
command = () => emulateClipboard( this.page, 'cut' );
} else if ( key.toLowerCase() === 'primary+v' ) {
command = () => emulateClipboard( this.page, 'paste' );
} else {
const keys = key.split( '+' ).flatMap( ( keyCode ) => {
if ( Object.prototype.hasOwnProperty.call( modifiers, keyCode ) ) {
return modifiers[ keyCode as keyof typeof modifiers ](
isAppleOS
).map( ( modifier ) =>
modifier === CTRL ? 'Control' : capitalCase( modifier )
);
} else if ( keyCode === 'Tab' && ! hasNaturalTabNavigation ) {
return [ 'Alt', 'Tab' ];
}
return keyCode;
} );
const normalizedKeys = keys.join( '+' );
command = () => this.page.keyboard.press( normalizedKeys );
}

times = times ?? 1;
for ( let i = 0; i < times; i += 1 ) {
await command();

if ( times > 1 && pressOptions.delay ) {
await this.page.waitForTimeout( pressOptions.delay );
}
}
}
Loading