PluginProbe
Hello Plus / trunk
Hello Plus vtrunk
trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.2.1 1.3.0 1.4.0 1.4.1 1.5.0 1.5.1 1.5.2 1.5.3 1.6.0 1.6.1 1.6.2 1.7.0 1.7.1 1.7.2 1.7.3 All 30 releases
hello-plus / tests / playwright / pages / wp-admin-page.ts

wp-admin-page.ts in Hello Plus trunk, at tests/playwright/pages/wp-admin-page.ts

570 lines 18.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { type APIRequestContext, type Page, Response, type TestInfo } from '@playwright/test';
2 import BasePage from './base-page';
3 import EditorPage from './editor-page';
4 import { ElementorType, WindowType } from '../types/types';
5 import { wpCli } from '../assets/wp-cli';
6 import ApiRequests from '../assets/api-requests';
7 import path from 'path';
8 let elementor: ElementorType;
9
10 export default class WpAdminPage extends BasePage {
11 protected readonly apiRequests: ApiRequests;
12
13 constructor( page: Page, testInfo: TestInfo, apiRequests: ApiRequests ) {
14 super( page, testInfo );
15 this.apiRequests = apiRequests;
16 }
17
18 /**
19 * Go to the WordPress dashboard.
20 *
21 * @return {Promise<void>}
22 */
23 async gotoDashboard(): Promise<void> {
24 await this.page.goto( '/wp-admin' );
25 }
26
27 /**
28 * If not logged in, log in to WordPress. Otherwise, go to the WordPress dashboard.
29 *
30 * @return {Promise<void>}
31 */
32 async login(): Promise<void> {
33 await this.gotoDashboard();
34
35 const loggedIn = await this.page.$( 'text=Dashboard' );
36
37 if ( loggedIn ) {
38 return;
39 }
40
41 await this.page.waitForSelector( 'text=Log In' );
42 await this.page.fill( 'input[name="log"]', process.env.USERNAME );
43 await this.page.fill( 'input[name="pwd"]', process.env.PASSWORD );
44 await this.page.click( 'text=Log In' );
45 await this.page.waitForSelector( 'text=Dashboard' );
46 }
47
48 /**
49 * Log in to WordPress with custom credentials.
50 *
51 * @param {string} username - The username to log in with.
52 * @param {string} password - The password to log in with.
53 *
54 * @return {Promise<void>}
55 */
56 async customLogin( username: string, password: string ): Promise<void> {
57 await this.gotoDashboard();
58 const loggedIn = await this.page.$( 'text=Dashboard' );
59
60 if ( loggedIn ) {
61 await this.page.hover( '#wp-admin-bar-top-secondary' );
62 await this.page.click( '#wp-admin-bar-logout > a' );
63 }
64
65 await this.page.fill( 'input[name="log"]', username );
66 await this.page.fill( 'input[name="pwd"]', password );
67 await this.page.locator( 'text=Log In' ).last().click();
68 await this.page.waitForSelector( 'text=Dashboard' );
69 }
70
71 /**
72 * Open a new Elementor page.
73 *
74 * @param {boolean} setWithApi - Optional. Whether to create the page with the API. Default is true.
75 * @param {boolean} setPageName - Optional. Whether to set the page name. Default is true.
76 *
77 * @return {Promise<EditorPage>} A promise that resolves to the new editor page instance.
78 */
79 async openNewPage( setWithApi: boolean = true, setPageName: boolean = true ): Promise<EditorPage> {
80 let postId: number | null = null;
81
82 if ( setWithApi ) {
83 const postIdString = await this.createNewPostWithAPI();
84 postId = parseInt( postIdString, 10 );
85 } else {
86 await this.createNewPostFromDashboard( setPageName );
87 const postIdString = await this.page.evaluate( () => {
88 const urlParams = new URLSearchParams( window.location.search );
89 return urlParams.get( 'post' );
90 } );
91 if ( postIdString ) {
92 postId = parseInt( postIdString, 10 );
93 }
94 }
95
96 await this.page.waitForLoadState( 'load', { timeout: 20000 } );
97 await this.waitForPanel();
98 await this.closeAnnouncementsIfVisible();
99
100 return new EditorPage( this.page, this.testInfo, postId );
101 }
102
103 /**
104 * Create a new page with the API and open it in Elementor.
105 *
106 * @return {Promise<string>} A promise that resolves to the created page ID.
107 */
108 async createNewPostWithAPI(): Promise<string> {
109 const request: APIRequestContext = this.page.context().request,
110 postDataInitial = {
111 title: 'Playwright Test Page - Uninitialized',
112 content: '',
113 },
114 postId = await this.apiRequests.create( request, 'pages', postDataInitial ),
115 postDataUpdated = {
116 title: `Playwright Test Page #${ postId }`,
117 };
118
119 await this.apiRequests.create( request, `pages/${ postId }`, postDataUpdated );
120 await this.page.goto( `/wp-admin/post.php?post=${ postId }&action=elementor` );
121
122 return postId;
123 }
124
125 /**
126 * Create a new page from the WordPress dashboard.
127 *
128 * @param {boolean} setPageName - Whether to set the page name.
129 *
130 * @return {Promise<void>}
131 */
132 async createNewPostFromDashboard( setPageName: boolean ): Promise<void> {
133 if ( ! await this.page.$( '.e-overview__create > a' ) ) {
134 await this.gotoDashboard();
135 }
136
137 await this.page.click( '.e-overview__create > a' );
138
139 if ( ! setPageName ) {
140 return;
141 }
142
143 await this.setPageName();
144 }
145
146 /**
147 * Set the page name.
148 *
149 * @return {Promise<void>}
150 */
151 async setPageName(): Promise<void> {
152 await this.page.locator( '#elementor-panel-footer-settings' ).click();
153
154 const pageId = await this.page.evaluate( () => elementor.config.initialDocument.id );
155 await this.page.locator( '.elementor-control-post_title input' ).fill( `Playwright Test Page #${ pageId }` );
156
157 await this.page.locator( '#elementor-panel-footer-saver-options' ).click();
158 await this.page.locator( '#elementor-panel-footer-sub-menu-item-save-draft' ).click();
159 await this.page.locator( '#elementor-panel-header-add-button' ).click();
160 }
161
162 /**
163 * Convert the current page from Gutenberg to Elementor.
164 *
165 * @return {Promise<EditorPage>} A promise that resolves to the editor page instance.
166 */
167 async convertFromGutenberg(): Promise<EditorPage> {
168 await Promise.all( [
169 this.page.waitForResponse( async ( response ) => await this.blockUrlResponse( response ) ),
170 this.page.click( '#elementor-switch-mode' ),
171 ] );
172
173 await this.page.waitForURL( '**/post.php?post=*&action=elementor' );
174 await this.page.waitForLoadState( 'load', { timeout: 20000 } );
175 await this.waitForPanel();
176
177 await this.closeAnnouncementsIfVisible();
178
179 const postIdString = await this.page.evaluate( () => {
180 const urlParams = new URLSearchParams( window.location.search );
181 return urlParams.get( 'post' );
182 } );
183 const postId = postIdString ? parseInt( postIdString, 10 ) : null;
184
185 return new EditorPage( this.page, this.testInfo, postId );
186 }
187
188 /**
189 * Get the response status for the API request.
190 *
191 * @param {Response} response - The response object.
192 *
193 * @return {Promise<boolean>} A promise that resolves to true if the response is a valid REST/JSON request with a 200 status.
194 */
195 async blockUrlResponse( response: Response ): Promise<boolean> {
196 const isRestRequest = response.url().includes( 'rest_route=%2Fwp%2Fv2%2Fpages%2' ); // For local testing
197 const isJsonRequest = response.url().includes( 'wp-json/wp/v2/pages' ); // For CI testing
198 return ( isJsonRequest || isRestRequest ) && 200 === response.status();
199 }
200
201 /**
202 * Wait for the Elementor editor panel to finish loading.
203 *
204 * @return {Promise<void>}
205 */
206 async waitForPanel(): Promise<void> {
207 await this.page.waitForSelector( '.elementor-panel-loading', { state: 'detached' } );
208 await this.page.waitForSelector( '#elementor-loading', { state: 'hidden' } );
209 }
210
211 /**
212 * Activate and deactivate Elementor experiments.
213 *
214 * TODO: The testing environment isn't clean between tests - Use with caution!
215 *
216 * @param {Object} experiments - Experiments settings ( `{ experiment_id: true / false }` );
217 * @param {(boolean|string)=} oldUrl - Optional. Whether to use the old URL structure. Default is false.
218 *
219 * @return {Promise<void>}
220 */
221 async setExperiments( experiments: { [ n: string ]: boolean | string }, oldUrl: boolean = false ): Promise<void> {
222 if ( oldUrl ) {
223 await this.page.goto( '/wp-admin/admin.php?page=elementor#tab-experiments' );
224 await this.page.click( '#elementor-settings-tab-experiments' );
225 } else {
226 await this.page.goto( '/wp-admin/admin.php?page=elementor-settings#tab-experiments' );
227 }
228
229 const prefix = 'e-experiment';
230
231 for ( const [ id, state ] of Object.entries( experiments ) ) {
232 const selector = `#${ prefix }-${ id }`;
233
234 // Try to make the element visible - Since some experiments may be hidden for the user,
235 // but actually exist and need to be tested.
236 await this.page.evaluate( ( el ) => {
237 const element: HTMLElement = document.querySelector( el );
238
239 if ( element ) {
240 element.style.display = 'block';
241 }
242 }, `.elementor_experiment-${ id }` );
243
244 await this.page.selectOption( selector, state ? 'active' : 'inactive' );
245
246 // Click to confirm any experiment that has dependencies.
247 await this.confirmExperimentModalIfOpen();
248 }
249
250 await this.page.click( '#submit' );
251 }
252
253 /**
254 * Reset all Elementor experiments to their default settings.
255 *
256 * @return {Promise<void>}
257 */
258 async resetExperiments(): Promise<void> {
259 await this.page.goto( '/wp-admin/admin.php?page=elementor-settings#tab-experiments' );
260 await this.page.getByRole( 'button', { name: 'default' } ).click();
261 }
262
263 /**
264 * Set site language.
265 *
266 * @param {string} language - The site language to set.
267 * @param {string|null} userLanguage - Optional. The user language to set. Default is null.
268 *
269 * @return {Promise<void>}
270 */
271 async setSiteLanguage( language: string, userLanguage: string = null ): Promise<void> {
272 let languageCheck = language;
273
274 if ( 'he_IL' === language ) {
275 languageCheck = 'he-IL';
276 } else if ( '' === language ) {
277 languageCheck = 'en_US';
278 }
279
280 await this.page.goto( '/wp-admin/options-general.php' );
281
282 const isLanguageActive = await this.page.locator( 'html[lang=' + languageCheck + ']' ).isVisible();
283
284 if ( ! isLanguageActive ) {
285 await this.page.selectOption( '#WPLANG', language );
286 await this.page.locator( '#submit' ).click();
287 }
288
289 const userProfileLanguage = null !== userLanguage ? userLanguage : language;
290 await this.setUserLanguage( userProfileLanguage );
291 }
292
293 /**
294 * Set user language.
295 *
296 * @param {string} language - The language to set.
297 *
298 * @return {Promise<void>}
299 */
300 async setUserLanguage( language: string ): Promise<void> {
301 await this.page.goto( 'wp-admin/profile.php' );
302 await this.page.selectOption( '[name="locale"]', language );
303 await this.page.locator( '#submit' ).click();
304 }
305
306 /**
307 * Confirm the Elementor experiment modal if it's open.
308 *
309 * @return {Promise<void>}
310 */
311 async confirmExperimentModalIfOpen(): Promise<void> {
312 const dialogButton = this.page.locator( '.dialog-type-confirm .dialog-confirm-ok' );
313
314 if ( await dialogButton.isVisible() ) {
315 await dialogButton.click();
316
317 // Clicking the confirm button - "Activate" or "Deactivate" - will immediately save the existing experiments,
318 // so we need to wait for the page to save and reload before we continue on to set any more experiments.
319 await this.page.waitForLoadState( 'load' );
320 }
321 }
322
323 /**
324 * Get the active WordPress theme.
325 *
326 * @return {Promise<string>} The name of the active WordPress theme.
327 */
328 async getActiveTheme(): Promise<string> {
329 const request: APIRequestContext = this.page.context().request;
330 const themeData = await this.apiRequests.getTheme( request, 'active' );
331 return themeData[ 0 ].stylesheet;
332 }
333
334 async activateTheme( theme: string ) {
335 await wpCli( `wp theme activate ${ theme }` );
336 }
337
338 /**
339 * Enable uploading SVG files.
340 *
341 * @return {Promise<void>}
342 */
343 async enableAdvancedUploads(): Promise<void> {
344 await this.page.goto( '/wp-admin/admin.php?page=elementor-settings#tab-advanced' );
345 await this.page.locator( 'select[name="elementor_unfiltered_files_upload"]' ).selectOption( '1' );
346 await this.page.getByRole( 'button', { name: 'Save Changes' } ).click();
347 }
348
349 /**
350 * Disable uploading SVG files.
351 *
352 * @return {Promise<void>}
353 */
354 async disableAdvancedUploads(): Promise<void> {
355 await this.page.goto( '/wp-admin/admin.php?page=elementor-settings#tab-advanced' );
356 await this.page.locator( 'select[name="elementor_unfiltered_files_upload"]' ).selectOption( '' );
357 await this.page.getByRole( 'button', { name: 'Save Changes' } ).click();
358 }
359
360 /**
361 * Close the Elementor announcements if they are visible.
362 *
363 * @return {Promise<void>}
364 */
365 async closeAnnouncementsIfVisible(): Promise<void> {
366 if ( await this.page.locator( '#e-announcements-root' ).count() > 0 ) {
367 await this.page.evaluate( ( selector ) => document.getElementById( selector ).remove(), 'e-announcements-root' );
368 }
369 let window: WindowType;
370 await this.page.evaluate( () => {
371 // eslint-disable-next-line @typescript-eslint/ban-ts-comment
372 // @ts-ignore editor session is on the window object
373 const editorSessionId = window.EDITOR_SESSION_ID;
374 window.sessionStorage.setItem( 'ai_promotion_introduction_editor_session_key', editorSessionId );
375 } );
376 }
377
378 /**
379 * Edit the page with Elementor.
380 *
381 * @return {Promise<void>}
382 */
383 async editWithElementor(): Promise<void> {
384 await this.page.getByRole( 'link', { name: ' Edit with Elementor' } ).click();
385 }
386
387 /**
388 * Close the block editor popup if it's visible.
389 *
390 * @return {Promise<void>}
391 */
392 async closeBlockEditorPopupIfVisible(): Promise<void> {
393 await this.page.locator( '#elementor-switch-mode-button' ).waitFor();
394 if ( await this.page.getByRole( 'button', { name: 'Close' } ).isVisible() ) {
395 await this.page.getByRole( 'button', { name: 'Close' } ).click();
396 }
397 }
398
399 /**
400 * Open a new WordPress page.
401 *
402 * @return {Promise<void>}
403 */
404 async openNewWordpressPage(): Promise<void> {
405 await this.page.goto( '/wp-admin/post-new.php?post_type=page' );
406 await this.closeBlockEditorPopupIfVisible();
407 }
408
409 /**
410 * Hide the WordPress admin bar.
411 *
412 * @return {Promise<void>}
413 */
414 async hideAdminBar(): Promise<void> {
415 await this.page.goto( '/wp-admin/profile.php' );
416 await this.page.locator( '#admin_bar_front' ).uncheck();
417 await this.page.locator( '#submit' ).click();
418 }
419
420 /**
421 * Show the WordPress admin bar.
422 *
423 * @return {Promise<void>}
424 */
425 async showAdminBar(): Promise<void> {
426 await this.page.goto( '/wp-admin/profile.php' );
427 await this.page.locator( '#admin_bar_front' ).check();
428 await this.page.locator( '#submit' ).click();
429 }
430
431 /**
432 * Wait for the Elementor editor to finish loading.
433 *
434 * @return {Promise<void>}
435 */
436 async waitForEditorToLoad(): Promise<void> {
437 await this.page.waitForLoadState( 'load', { timeout: 20000 } );
438 await this.waitForPanel();
439 }
440
441 async createNewMenu( menuName: string ) {
442 await this.deleteAllMenus();
443
444 await this.page.goto( '/wp-admin/nav-menus.php' );
445 await this.page.waitForLoadState( 'load' );
446
447 if ( await this.page.getByRole( 'link', { name: 'create a new menu' } ).isVisible() ) {
448 await this.page.getByRole( 'link', { name: 'create a new menu' } ).click();
449 }
450
451 await this.page.getByRole( 'textbox', { name: 'Menu Name' } ).click();
452 await this.page.getByRole( 'textbox', { name: 'Menu Name' } ).fill( menuName );
453 await this.page.getByRole( 'textbox', { name: 'Menu Name' } ).press( 'Enter' );
454 await this.page.getByRole( 'checkbox', { name: 'Header' } ).check();
455
456 if ( await this.page.getByRole( 'button', { name: 'Create Menu' } ).isVisible() ) {
457 await this.page.getByRole( 'button', { name: 'Create Menu' } ).click();
458 } else {
459 await this.page.getByRole( 'button', { name: 'Save Menu' } ).click();
460 }
461
462 await this.page.getByRole( 'button', { name: 'Custom Links' } ).click();
463 await this.page.getByRole( 'textbox', { name: 'URL' } ).click();
464 await this.page.getByRole( 'textbox', { name: 'URL' } ).fill( '#' );
465 await this.page.getByRole( 'textbox', { name: 'URL' } ).press( 'Tab' );
466 await this.page.getByRole( 'textbox', { name: 'Link Text' } ).fill( 'Parent menu item' );
467 await this.page.getByRole( 'textbox', { name: 'Link Text' } ).press( 'Enter' );
468 await this.page.getByRole( 'textbox', { name: 'URL' } ).click();
469 await this.page.getByRole( 'textbox', { name: 'URL' } ).fill( '#' );
470 await this.page.getByRole( 'textbox', { name: 'URL' } ).press( 'Tab' );
471 await this.page.getByRole( 'textbox', { name: 'Link Text' } ).fill( 'Child menu item' );
472 await this.page.getByRole( 'textbox', { name: 'Link Text' } ).press( 'Enter' );
473
474 await this.page.waitForTimeout( 1000 );
475
476 const itemOne = this.page.locator( '#menu-to-edit > li:nth-child(1) .menu-item-handle' );
477 const itemTwo = this.page.locator( '#menu-to-edit > li:nth-child(2) .menu-item-handle' );
478
479 const itemOneBox = await itemOne.boundingBox();
480 const itemTwoBox = await itemTwo.boundingBox();
481
482 if ( itemOneBox && itemTwoBox ) {
483 // Drag `two` near and slightly right below `one` to make it a child
484 await this.page.mouse.move(
485 itemTwoBox.x + ( itemTwoBox.width / 2 ),
486 itemTwoBox.y + ( itemTwoBox.height / 2 ),
487 );
488 await this.page.mouse.down();
489 await this.page.mouse.move(
490 itemOneBox.x + 30, // ← indent to the right to trigger submenu nesting
491 itemOneBox.y + ( itemOneBox.height + 10 ),
492 { steps: 10 },
493 );
494 await this.page.mouse.up();
495 }
496
497 await this.page.getByRole( 'button', { name: 'Save Menu' } ).click();
498 }
499
500 /**
501 * Import WordPress XML file using the WordPress importer.
502 *
503 * @param {string} filePath - Path to the XML file to import.
504 * @param {Page} page - Optional. Page instance to use. Defaults to this.page.
505 * @return {Promise<void>}
506 */
507 async importWordPressXml( filePath: string, page: Page = this.page ): Promise<void> {
508 await page.goto( '/wp-admin/import.php?import=wordpress' );
509 await page.waitForLoadState( 'load' );
510 await this.dismissEditorOnePointerIfVisible();
511
512 await page.setInputFiles( 'input[name="import"]', filePath );
513
514 await Promise.all( [
515 page.waitForNavigation(),
516 page.click( 'input:has-text("Upload file and import")' ),
517 ] );
518
519 await page.selectOption( 'select[name="user_map[0]"]', '1' );
520 await page.check( 'input[name="fetch_attachments"]' );
521 await page.click( 'text=Submit' );
522 await page.click( 'text=All done. Have fun!' );
523 }
524
525 /**
526 * Import menu from XML file.
527 *
528 * @return {Promise<void>}
529 */
530 async importMenu(): Promise<void> {
531 const menuImportPath = path.resolve( __dirname, '..', 'sample-data', 'menu-import.xml' );
532 await this.importWordPressXml( menuImportPath );
533 }
534
535 /**
536 * Delete all existing Menus.
537 * Loops all the menus, and delete them one by one until the delete menu button disappears.
538 */
539 async deleteAllMenus(): Promise<void> {
540 await this.page.goto( '/wp-admin/nav-menus.php' );
541 await this.page.waitForLoadState( 'load' );
542
543 const deleteMenuButton = await this.page.$( '#nav-menu-footer .menu-delete' );
544 // If the 'delete menu' button exists, delete the current menu.
545 if ( deleteMenuButton ) {
546 const deleteHref: string | null = await this.page.evaluate( () => document.querySelector( '#nav-menu-footer .menu-delete' )!.getAttribute( 'href' ) );
547 const page2 = await this.page.context().newPage();
548
549 await page2.goto( deleteHref! );
550 await page2.close();
551 await this.deleteAllMenus();
552 }
553 }
554
555 async dismissEditorOnePointerIfVisible(): Promise<void> {
556 const pointer = this.page.locator( '.wp-pointer' );
557 if ( ! await pointer.isVisible() ) {
558 return;
559 }
560
561 const hasEditorOneTitle = await pointer.locator( 'h3:has-text("The Editor has a new home")' ).isVisible();
562 if ( ! hasEditorOneTitle ) {
563 return;
564 }
565
566 await pointer.locator( '.wp-pointer-buttons a.close' ).click();
567 await pointer.waitFor( { state: 'hidden' } );
568 }
569 }
570