import { type APIRequestContext, type Page, Response, type TestInfo } from '@playwright/test'; import BasePage from './base-page'; import EditorPage from './editor-page'; import { ElementorType, WindowType } from '../types/types'; import { wpCli } from '../assets/wp-cli'; import ApiRequests from '../assets/api-requests'; let elementor: ElementorType; export default class WpAdminPage extends BasePage { protected readonly apiRequests: ApiRequests; constructor( page: Page, testInfo: TestInfo, apiRequests: ApiRequests ) { super( page, testInfo ); this.apiRequests = apiRequests; } /** * Go to the WordPress dashboard. * * @return {Promise} */ async gotoDashboard(): Promise { await this.page.goto( '/wp-admin' ); } /** * If not logged in, log in to WordPress. Otherwise, go to the WordPress dashboard. * * @return {Promise} */ async login(): Promise { await this.gotoDashboard(); const loggedIn = await this.page.$( 'text=Dashboard' ); if ( loggedIn ) { return; } await this.page.waitForSelector( 'text=Log In' ); await this.page.fill( 'input[name="log"]', process.env.USERNAME ); await this.page.fill( 'input[name="pwd"]', process.env.PASSWORD ); await this.page.click( 'text=Log In' ); await this.page.waitForSelector( 'text=Dashboard' ); } /** * Log in to WordPress with custom credentials. * * @param {string} username - The username to log in with. * @param {string} password - The password to log in with. * * @return {Promise} */ async customLogin( username: string, password: string ): Promise { await this.gotoDashboard(); const loggedIn = await this.page.$( 'text=Dashboard' ); if ( loggedIn ) { await this.page.hover( '#wp-admin-bar-top-secondary' ); await this.page.click( '#wp-admin-bar-logout > a' ); } await this.page.fill( 'input[name="log"]', username ); await this.page.fill( 'input[name="pwd"]', password ); await this.page.locator( 'text=Log In' ).last().click(); await this.page.waitForSelector( 'text=Dashboard' ); } /** * Open a new Elementor page. * * @param {boolean} setWithApi - Optional. Whether to create the page with the API. Default is true. * @param {boolean} setPageName - Optional. Whether to set the page name. Default is true. * * @return {Promise} A promise that resolves to the new editor page instance. */ async openNewPage( setWithApi: boolean = true, setPageName: boolean = true ): Promise { if ( setWithApi ) { await this.createNewPostWithAPI(); } else { await this.createNewPostFromDashboard( setPageName ); } await this.page.waitForLoadState( 'load', { timeout: 20000 } ); await this.waitForPanel(); await this.closeAnnouncementsIfVisible(); return new EditorPage( this.page, this.testInfo ); } /** * Create a new page with the API and open it in Elementor. * * @return {Promise} A promise that resolves to the created page ID. */ async createNewPostWithAPI(): Promise { const request: APIRequestContext = this.page.context().request, postDataInitial = { title: 'Playwright Test Page - Uninitialized', content: '', }, postId = await this.apiRequests.create( request, 'pages', postDataInitial ), postDataUpdated = { title: `Playwright Test Page #${ postId }`, }; await this.apiRequests.create( request, `pages/${ postId }`, postDataUpdated ); await this.page.goto( `/wp-admin/post.php?post=${ postId }&action=elementor` ); return postId; } /** * Create a new page from the WordPress dashboard. * * @param {boolean} setPageName - Whether to set the page name. * * @return {Promise} */ async createNewPostFromDashboard( setPageName: boolean ): Promise { if ( ! await this.page.$( '.e-overview__create > a' ) ) { await this.gotoDashboard(); } await this.page.click( '.e-overview__create > a' ); if ( ! setPageName ) { return; } await this.setPageName(); } /** * Set the page name. * * @return {Promise} */ async setPageName(): Promise { await this.page.locator( '#elementor-panel-footer-settings' ).click(); const pageId = await this.page.evaluate( () => elementor.config.initialDocument.id ); await this.page.locator( '.elementor-control-post_title input' ).fill( `Playwright Test Page #${ pageId }` ); await this.page.locator( '#elementor-panel-footer-saver-options' ).click(); await this.page.locator( '#elementor-panel-footer-sub-menu-item-save-draft' ).click(); await this.page.locator( '#elementor-panel-header-add-button' ).click(); } /** * Convert the current page from Gutenberg to Elementor. * * @return {Promise} A promise that resolves to the editor page instance. */ async convertFromGutenberg(): Promise { await Promise.all( [ this.page.waitForResponse( async ( response ) => await this.blockUrlResponse( response ) ), this.page.click( '#elementor-switch-mode' ), ] ); await this.page.waitForURL( '**/post.php?post=*&action=elementor' ); await this.page.waitForLoadState( 'load', { timeout: 20000 } ); await this.waitForPanel(); await this.closeAnnouncementsIfVisible(); return new EditorPage( this.page, this.testInfo ); } /** * Get the response status for the API request. * * @param {Response} response - The response object. * * @return {Promise} A promise that resolves to true if the response is a valid REST/JSON request with a 200 status. */ async blockUrlResponse( response: Response ): Promise { const isRestRequest = response.url().includes( 'rest_route=%2Fwp%2Fv2%2Fpages%2' ); // For local testing const isJsonRequest = response.url().includes( 'wp-json/wp/v2/pages' ); // For CI testing return ( isJsonRequest || isRestRequest ) && 200 === response.status(); } /** * Wait for the Elementor editor panel to finish loading. * * @return {Promise} */ async waitForPanel(): Promise { await this.page.waitForSelector( '.elementor-panel-loading', { state: 'detached' } ); await this.page.waitForSelector( '#elementor-loading', { state: 'hidden' } ); } /** * Activate and deactivate Elementor experiments. * * TODO: The testing environment isn't clean between tests - Use with caution! * * @param {Object} experiments - Experiments settings ( `{ experiment_id: true / false }` ); * @param {(boolean|string)=} oldUrl - Optional. Whether to use the old URL structure. Default is false. * * @return {Promise} */ async setExperiments( experiments: { [ n: string ]: boolean | string }, oldUrl: boolean = false ): Promise { if ( oldUrl ) { await this.page.goto( '/wp-admin/admin.php?page=elementor#tab-experiments' ); await this.page.click( '#elementor-settings-tab-experiments' ); } else { await this.page.goto( '/wp-admin/admin.php?page=elementor-settings#tab-experiments' ); } const prefix = 'e-experiment'; for ( const [ id, state ] of Object.entries( experiments ) ) { const selector = `#${ prefix }-${ id }`; // Try to make the element visible - Since some experiments may be hidden for the user, // but actually exist and need to be tested. await this.page.evaluate( ( el ) => { const element: HTMLElement = document.querySelector( el ); if ( element ) { element.style.display = 'block'; } }, `.elementor_experiment-${ id }` ); await this.page.selectOption( selector, state ? 'active' : 'inactive' ); // Click to confirm any experiment that has dependencies. await this.confirmExperimentModalIfOpen(); } await this.page.click( '#submit' ); } /** * Reset all Elementor experiments to their default settings. * * @return {Promise} */ async resetExperiments(): Promise { await this.page.goto( '/wp-admin/admin.php?page=elementor-settings#tab-experiments' ); await this.page.getByRole( 'button', { name: 'default' } ).click(); } /** * Set site language. * * @param {string} language - The site language to set. * @param {string|null} userLanguage - Optional. The user language to set. Default is null. * * @return {Promise} */ async setSiteLanguage( language: string, userLanguage: string = null ): Promise { let languageCheck = language; if ( 'he_IL' === language ) { languageCheck = 'he-IL'; } else if ( '' === language ) { languageCheck = 'en_US'; } await this.page.goto( '/wp-admin/options-general.php' ); const isLanguageActive = await this.page.locator( 'html[lang=' + languageCheck + ']' ).isVisible(); if ( ! isLanguageActive ) { await this.page.selectOption( '#WPLANG', language ); await this.page.locator( '#submit' ).click(); } const userProfileLanguage = null !== userLanguage ? userLanguage : language; await this.setUserLanguage( userProfileLanguage ); } /** * Set user language. * * @param {string} language - The language to set. * * @return {Promise} */ async setUserLanguage( language: string ): Promise { await this.page.goto( 'wp-admin/profile.php' ); await this.page.selectOption( '[name="locale"]', language ); await this.page.locator( '#submit' ).click(); } /** * Confirm the Elementor experiment modal if it's open. * * @return {Promise} */ async confirmExperimentModalIfOpen(): Promise { const dialogButton = this.page.locator( '.dialog-type-confirm .dialog-confirm-ok' ); if ( await dialogButton.isVisible() ) { await dialogButton.click(); // Clicking the confirm button - "Activate" or "Deactivate" - will immediately save the existing experiments, // so we need to wait for the page to save and reload before we continue on to set any more experiments. await this.page.waitForLoadState( 'load' ); } } /** * Get the active WordPress theme. * * @return {Promise} The name of the active WordPress theme. */ async getActiveTheme(): Promise { const request: APIRequestContext = this.page.context().request; const themeData = await this.apiRequests.getTheme( request, 'active' ); return themeData[ 0 ].stylesheet; } async activateTheme( theme: string ) { await wpCli( `wp theme activate ${ theme }` ); } /** * Enable uploading SVG files. * * @return {Promise} */ async enableAdvancedUploads(): Promise { await this.page.goto( '/wp-admin/admin.php?page=elementor-settings#tab-advanced' ); await this.page.locator( 'select[name="elementor_unfiltered_files_upload"]' ).selectOption( '1' ); await this.page.getByRole( 'button', { name: 'Save Changes' } ).click(); } /** * Disable uploading SVG files. * * @return {Promise} */ async disableAdvancedUploads(): Promise { await this.page.goto( '/wp-admin/admin.php?page=elementor-settings#tab-advanced' ); await this.page.locator( 'select[name="elementor_unfiltered_files_upload"]' ).selectOption( '' ); await this.page.getByRole( 'button', { name: 'Save Changes' } ).click(); } /** * Close the Elementor announcements if they are visible. * * @return {Promise} */ async closeAnnouncementsIfVisible(): Promise { if ( await this.page.locator( '#e-announcements-root' ).count() > 0 ) { await this.page.evaluate( ( selector ) => document.getElementById( selector ).remove(), 'e-announcements-root' ); } let window: WindowType; await this.page.evaluate( () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore editor session is on the window object const editorSessionId = window.EDITOR_SESSION_ID; window.sessionStorage.setItem( 'ai_promotion_introduction_editor_session_key', editorSessionId ); } ); } /** * Edit the page with Elementor. * * @return {Promise} */ async editWithElementor(): Promise { await this.page.getByRole( 'link', { name: ' Edit with Elementor' } ).click(); } /** * Close the block editor popup if it's visible. * * @return {Promise} */ async closeBlockEditorPopupIfVisible(): Promise { await this.page.locator( '#elementor-switch-mode-button' ).waitFor(); if ( await this.page.getByRole( 'button', { name: 'Close' } ).isVisible() ) { await this.page.getByRole( 'button', { name: 'Close' } ).click(); } } /** * Open a new WordPress page. * * @return {Promise} */ async openNewWordpressPage(): Promise { await this.page.goto( '/wp-admin/post-new.php?post_type=page' ); await this.closeBlockEditorPopupIfVisible(); } /** * Hide the WordPress admin bar. * * @return {Promise} */ async hideAdminBar(): Promise { await this.page.goto( '/wp-admin/profile.php' ); await this.page.locator( '#admin_bar_front' ).uncheck(); await this.page.locator( '#submit' ).click(); } /** * Show the WordPress admin bar. * * @return {Promise} */ async showAdminBar(): Promise { await this.page.goto( '/wp-admin/profile.php' ); await this.page.locator( '#admin_bar_front' ).check(); await this.page.locator( '#submit' ).click(); } /** * Wait for the Elementor editor to finish loading. * * @return {Promise} */ async waitForEditorToLoad(): Promise { await this.page.waitForLoadState( 'load', { timeout: 20000 } ); await this.waitForPanel(); } async createNewMenu( menuName: string ) { await this.deleteAllMenus(); await this.page.goto( '/wp-admin/nav-menus.php' ); await this.page.waitForLoadState( 'networkidle' ); if ( await this.page.getByRole( 'link', { name: 'create a new menu' } ).isVisible() ) { await this.page.getByRole( 'link', { name: 'create a new menu' } ).click(); } await this.page.getByRole( 'textbox', { name: 'Menu Name' } ).click(); await this.page.getByRole( 'textbox', { name: 'Menu Name' } ).fill( menuName ); await this.page.getByRole( 'textbox', { name: 'Menu Name' } ).press( 'Enter' ); await this.page.getByRole( 'checkbox', { name: 'Header' } ).check(); if ( await this.page.getByRole( 'button', { name: 'Create Menu' } ).isVisible() ) { await this.page.getByRole( 'button', { name: 'Create Menu' } ).click(); } else { await this.page.getByRole( 'button', { name: 'Save Menu' } ).click(); } await this.page.getByRole( 'button', { name: 'Custom Links' } ).click(); await this.page.getByRole( 'textbox', { name: 'URL' } ).click(); await this.page.getByRole( 'textbox', { name: 'URL' } ).fill( '#' ); await this.page.getByRole( 'textbox', { name: 'URL' } ).press( 'Tab' ); await this.page.getByRole( 'textbox', { name: 'Link Text' } ).fill( 'Parent menu item' ); await this.page.getByRole( 'textbox', { name: 'Link Text' } ).press( 'Enter' ); await this.page.getByRole( 'textbox', { name: 'URL' } ).click(); await this.page.getByRole( 'textbox', { name: 'URL' } ).fill( '#' ); await this.page.getByRole( 'textbox', { name: 'URL' } ).press( 'Tab' ); await this.page.getByRole( 'textbox', { name: 'Link Text' } ).fill( 'Child menu item' ); await this.page.getByRole( 'textbox', { name: 'Link Text' } ).press( 'Enter' ); await this.page.waitForTimeout( 1000 ); const itemOne = this.page.locator( '#menu-to-edit > li:nth-child(1) .menu-item-handle' ); const itemTwo = this.page.locator( '#menu-to-edit > li:nth-child(2) .menu-item-handle' ); const itemOneBox = await itemOne.boundingBox(); const itemTwoBox = await itemTwo.boundingBox(); if ( itemOneBox && itemTwoBox ) { // Drag `two` near and slightly right below `one` to make it a child await this.page.mouse.move( itemTwoBox.x + ( itemTwoBox.width / 2 ), itemTwoBox.y + ( itemTwoBox.height / 2 ), ); await this.page.mouse.down(); await this.page.mouse.move( itemOneBox.x + 30, // ← indent to the right to trigger submenu nesting itemOneBox.y + ( itemOneBox.height + 10 ), { steps: 10 }, ); await this.page.mouse.up(); } await this.page.getByRole( 'button', { name: 'Save Menu' } ).click(); } /** * Delete all existing Menus. * Loops all the menus, and delete them one by one until the delete menu button disappears. */ async deleteAllMenus(): Promise { await this.page.goto( '/wp-admin/nav-menus.php' ); await this.page.waitForLoadState( 'networkidle' ); const deleteMenuButton = await this.page.$( '#nav-menu-footer .menu-delete' ); // If the 'delete menu' button exists, delete the current menu. if ( deleteMenuButton ) { const deleteHref: string | null = await this.page.evaluate( () => document.querySelector( '#nav-menu-footer .menu-delete' )!.getAttribute( 'href' ) ); const page2 = await this.page.context().newPage(); await page2.goto( deleteHref! ); await page2.close(); await this.deleteAllMenus(); } } }