-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
[Experiment] Use REST API in e2e tests to build up states #33414
Changes from all commits
647fcff
bc91277
e1f41bd
46d4324
15ac560
16adbe3
07d770e
79e32e4
6f10f06
f6416f7
11151d9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import { kebabCase } from 'lodash'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { rest } from './rest-api'; | ||
|
||
const pluginsMapPromise = ( async function getPluginsMap() { | ||
const plugins = await rest( { path: '/wp/v2/plugins' } ); | ||
const map = {}; | ||
for ( const plugin of plugins ) { | ||
// Ideally, we should be using sanitize_title() in PHP rather than kebabCase(), | ||
// but we don't have the exact port of it in JS. | ||
map[ kebabCase( plugin.name ) ] = plugin.plugin; | ||
} | ||
return map; | ||
} )(); | ||
|
||
/** | ||
* Activates an installed plugin. | ||
* | ||
* @param {string} slug Plugin slug. | ||
*/ | ||
async function activatePlugin( slug ) { | ||
const pluginsMap = await pluginsMapPromise; | ||
const plugin = pluginsMap[ slug ]; | ||
|
||
await rest( { | ||
method: 'PUT', | ||
path: `/wp/v2/plugins/${ plugin }`, | ||
data: { status: 'active' }, | ||
} ); | ||
} | ||
|
||
/** | ||
* Deactivates an active plugin. | ||
* | ||
* @param {string} slug Plugin slug. | ||
*/ | ||
async function deactivatePlugin( slug ) { | ||
const pluginsMap = await pluginsMapPromise; | ||
const plugin = pluginsMap[ slug ]; | ||
|
||
await rest( { | ||
method: 'PUT', | ||
path: `/wp/v2/plugins/${ plugin }`, | ||
data: { status: 'inactive' }, | ||
} ); | ||
} | ||
|
||
export { activatePlugin, deactivatePlugin }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
import fetch from 'node-fetch'; | ||
import FormData from 'form-data'; | ||
|
||
/** | ||
* WordPress dependencies | ||
*/ | ||
import apiFetch from '@wordpress/api-fetch'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import { WP_BASE_URL } from './shared/config'; | ||
import { createURL } from './create-url'; | ||
|
||
// `apiFetch` expects `window.fetch` to be available in its default handler. | ||
global.window = global.window || {}; | ||
global.window.fetch = fetch; | ||
|
||
const setAPIRootURL = ( async () => { | ||
// Discover the API root url using link header. | ||
// See https://developer.wordpress.org/rest-api/using-the-rest-api/discovery/#link-header | ||
const res = await fetch( WP_BASE_URL, { method: 'HEAD' } ); | ||
const links = res.headers.get( 'link' ); | ||
const restLink = links.match( /<([^>]+)>; rel="https:\/\/api\.w\.org\/"/ ); | ||
|
||
if ( ! restLink ) { | ||
throw new Error( `Failed to discover REST API endpoint. | ||
Link header: ${ links }` ); | ||
} | ||
|
||
const [ , rootURL ] = restLink; | ||
apiFetch.use( apiFetch.createRootURLMiddleware( rootURL ) ); | ||
} )(); | ||
|
||
const setNonce = ( async () => { | ||
const formData = new FormData(); | ||
formData.append( 'log', 'admin' ); | ||
formData.append( 'pwd', 'password' ); | ||
|
||
// Login to admin using fetch. | ||
const loginResponse = await fetch( createURL( 'wp-login.php' ), { | ||
method: 'POST', | ||
headers: formData.getHeaders(), | ||
body: formData, | ||
redirect: 'manual', | ||
} ); | ||
|
||
// Retrieve the cookies. | ||
const cookies = loginResponse.headers.get( 'set-cookie' ); | ||
const cookie = cookies | ||
.split( ',' ) | ||
.map( ( setCookie ) => setCookie.split( ';' )[ 0 ] ) | ||
.join( ';' ); | ||
|
||
apiFetch.nonceEndpoint = createURL( | ||
'wp-admin/admin-ajax.php', | ||
'action=rest-nonce' | ||
); | ||
|
||
// Get the initial nonce. | ||
const res = await fetch( apiFetch.nonceEndpoint, { | ||
headers: { cookie }, | ||
} ); | ||
const nonce = await res.text(); | ||
|
||
// Register the nonce middleware. | ||
apiFetch.use( apiFetch.createNonceMiddleware( nonce ) ); | ||
|
||
// For the nonce to work we have to also pass the cookies. | ||
apiFetch.use( function setCookieMiddleware( request, next ) { | ||
return next( { | ||
...request, | ||
headers: { | ||
...request.headers, | ||
cookie, | ||
}, | ||
} ); | ||
} ); | ||
} )(); | ||
|
||
/** | ||
* Call REST API using `apiFetch` to build and clear test states. | ||
* | ||
* @param {Object} options `apiFetch` options. | ||
* @return {Promise<any>} The response value. | ||
*/ | ||
async function rest( options = {} ) { | ||
// Only need to set them once but before any requests. | ||
await Promise.all( [ setAPIRootURL, setNonce ] ); | ||
|
||
return await apiFetch( options ); | ||
} | ||
|
||
/** | ||
* Call a set of REST APIs in batch. | ||
* See https://make.wordpress.org/core/2020/11/20/rest-api-batch-framework-in-wordpress-5-6/ | ||
* Note that calling GET requests in batch is not supported. | ||
* | ||
* @param {Array<Object>} requests The request objects. | ||
* @return {Promise<any>} The response value. | ||
*/ | ||
async function batch( requests ) { | ||
return await rest( { | ||
method: 'POST', | ||
path: '/batch/v1', | ||
data: { | ||
requests, | ||
validation: 'require-all-validate', | ||
}, | ||
} ); | ||
} | ||
|
||
export { rest, batch }; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,29 @@ | ||
/** | ||
* Internal dependencies | ||
*/ | ||
import { activatePlugin } from './activate-plugin'; | ||
import { deactivatePlugin } from './deactivate-plugin'; | ||
import { visitAdminPage } from './visit-admin-page'; | ||
import { rest, batch } from './rest-api'; | ||
|
||
/** | ||
* Delete all the widgets in the widgets screen. | ||
*/ | ||
export async function deleteAllWidgets() { | ||
// TODO: Deleting widgets in the new widgets screen is cumbersome and slow. | ||
// To workaround this for now, we visit the old widgets screen to delete them. | ||
await activatePlugin( 'gutenberg-test-classic-widgets' ); | ||
const [ widgets, sidebars ] = await Promise.all( [ | ||
rest( { path: '/wp/v2/widgets' } ), | ||
rest( { path: '/wp/v2/sidebars' } ), | ||
] ); | ||
|
||
await visitAdminPage( 'widgets.php' ); | ||
await batch( | ||
widgets.map( ( widget ) => ( { | ||
method: 'DELETE', | ||
path: `/wp/v2/widgets/${ widget.id }?force=true`, | ||
} ) ) | ||
); | ||
|
||
let widget = await page.$( '.widgets-sortables .widget' ); | ||
|
||
// We have to do this one-by-one since there might be race condition when deleting multiple widgets at once. | ||
while ( widget ) { | ||
const deleteButton = await widget.$( 'button.widget-control-remove' ); | ||
const id = await widget.evaluate( ( node ) => node.id ); | ||
await deleteButton.evaluate( ( node ) => node.click() ); | ||
// Wait for the widget to be removed from DOM. | ||
await page.waitForSelector( `#${ id }`, { hidden: true } ); | ||
|
||
widget = await page.$( '.widgets-sortables .widget' ); | ||
} | ||
|
||
await deactivatePlugin( 'gutenberg-test-classic-widgets' ); | ||
await batch( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this be batched with the previous batch, or are there concerns about limits? (I think the limit is 25 or something). The limit might actually be a concern here if a test ever adds lots of widgets. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We actually can't. I think it might have some race condition going on in those API, I didn't look into why though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok 👍 Might still be worth looking into the batch limit thing (some background - #28709) as a follow-up, I think this batch function should ideally split the requests array based on the limit when it's over the limit and then make sequential requests for each part. |
||
sidebars.map( ( sidebar ) => ( { | ||
method: 'POST', | ||
path: `/wp/v2/sidebars/${ sidebar.id }`, | ||
body: { id: sidebar.id, widgets: [] }, | ||
} ) ) | ||
); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,22 @@ | ||
/** | ||
* External dependencies | ||
*/ | ||
const path = require( 'path' ); | ||
|
||
const { ENVIRONMENT_DIRECTORY = '<rootDir>' } = process.env; | ||
|
||
module.exports = { | ||
...require( '@wordpress/scripts/config/jest-e2e.config' ), | ||
testMatch: [ '**/performance/*.test.js' ], | ||
setupFiles: [ '<rootDir>/config/gutenberg-phase.js' ], | ||
moduleNameMapper: { | ||
// Use different versions of e2e-test-utils for different environments | ||
// rather than always using the latest. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To use the PR's version of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unless I'm wrong, this was already the case, PRs use the PR's version of the e2e-test-utils always. There's already a |
||
[ `@wordpress\\/e2e-test-utils$` ]: path.join( | ||
ENVIRONMENT_DIRECTORY, | ||
'packages/e2e-test-utils/src' | ||
), | ||
}, | ||
setupFilesAfterEnv: [ | ||
'<rootDir>/config/setup-performance-test.js', | ||
'@wordpress/jest-console', | ||
|
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to use
wp.apiFetch
instead that should have already all params set when the user is logged in. Something like this:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would make the test depends on the page it's currently running on. Some pages don't have
wp.apiFetch
available by default, and sometimes the logged in user doesn't have the necessary permissions. It also means that a reload is required after making such requests. Calling it server-side seems like a more robust approach to me.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I must admit that it's all confusing. In the current shape, the
rest
method is doing a REST API request as an admin with hardcoded credentials toadmin
+password
. If the goal is to make it general purpose then it's far from it. Some concerns:rest
with a different user account or as logged out?wp-login.php
set the cookie so it's very likely that once the test refreshes the page then the rest of the test is going to be performed as an adminThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rest
isn't the best name here. It's running in the nodejs process rather than the browser context, so it's independent to the test and won't affect the current logged in user. As mentioned in the description of the PR, it should only be used to setup/clear states between or during tests. I'll keep searching for a better name here, fortunately it's still being marked as experimental.