Skip to content

Commit

Permalink
Add pressKeys
Browse files Browse the repository at this point in the history
  • Loading branch information
kevin940726 committed Mar 13, 2023
1 parent 0d7bf7c commit f0d0d56
Show file tree
Hide file tree
Showing 33 changed files with 450 additions and 420 deletions.
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

0 comments on commit f0d0d56

Please sign in to comment.