GhostManSec
Server: LiteSpeed
System: Linux premium197.web-hosting.com 4.18.0-553.54.1.lve.el8.x86_64 #1 SMP Wed Jun 4 13:01:13 UTC 2025 x86_64
User: parhudrw (1725)
PHP: 7.4.33
Disabled: NONE
Upload Files
File: /home/parhudrw/emenu.anqa.it/wp-content/plugins/hello-plus/tests/playwright/pages/editor-page.ts
import { readFile } from 'fs/promises';
import { addElement, getElementSelector } from '../assets/elements-utils';
import { expect, type Page, type Frame, type TestInfo, type ElementHandle, Locator } from '@playwright/test';
import BasePage from './base-page';
import EditorSelectors from '../selectors/editor-selectors';
import _path, { resolve as pathResolve } from 'path';
// eslint-disable-next-line import/no-extraneous-dependencies
import { getComparator } from 'playwright-core/lib/utils';
import { $eType, Device, WindowType, BackboneType, ElementorType, GapControl, ContainerType, ContainerPreset } from '../types/types';
import TopBarSelectors, { TopBarSelector } from '../selectors/top-bar-selectors';
import Breakpoints from '../assets/breakpoints';
import { timeouts } from '../config/timeouts';

let $e: $eType;
let elementor: ElementorType;
let Backbone: BackboneType;
let window: WindowType;

export default class EditorPage extends BasePage {
	readonly previewFrame: Frame;
	postId: number;
	isPanelLoaded = false;

	/**
	 * Create an Elementor editor page.
	 *
	 * @param {Page}     page        - Playwright page instance.
	 * @param {TestInfo} testInfo    - Test information.
	 * @param {number}   cleanPostId - Optional. Post ID.
	 */
	constructor( page: Page, testInfo: TestInfo, cleanPostId: null | number = null ) {
		super( page, testInfo );
		this.previewFrame = this.getPreviewFrame();
		this.postId = cleanPostId;
	}

	/**
	 * Open a specific post in the elementor editor.
	 *
	 * @param {number|string} id - Optional. Post ID. Default is the ID of the current post.
	 *
	 * @return {Promise<void>}
	 */
	async gotoPostId( id: number|string = this.postId ): Promise<void> {
		await this.page.goto( `wp-admin/post.php?post=${ id }&action=elementor` );
		await this.page.waitForLoadState( 'load' );
		await this.waitForPanelToLoad();
	}

	/**
	 * Update image dates in the template data.
	 *
	 * @param {JSON} templateData - Template data.
	 *
	 * @return {JSON} The updated template data with current dates.
	 */
	updateImageDates( templateData: JSON ): JSON {
		const date = new Date();
		const month = date.toLocaleString( 'default', { month: '2-digit' } );
		const data = JSON.stringify( templateData );
		const updatedData = data.replace( /[0-9]{4}\/[0-9]{2}/g, `${ date.getFullYear() }/${ month }` );
		return JSON.parse( updatedData ) as JSON;
	}

	/**
	 * Upload SVG in the Media Library. Can be used on both Media Control and Icons Control.
	 *
	 * Please note that this method expects media library to be open as different controls
	 * have different ways to open the media library.
	 *
	 * @param {string} svgFileName - Optional. SVG file name, without extension.
	 *
	 * @return {Promise<void>}
	 */
	async uploadSVG( svgFileName?: string ): Promise<void> {
		const _svgFileName = svgFileName === undefined ? 'test-svg-wide' : svgFileName;
		const regex = new RegExp( _svgFileName );
		const response = this.page.waitForResponse( regex );
		await this.page.setInputFiles( EditorSelectors.media.imageInp, _path.resolve( __dirname, `../resources/${ _svgFileName }.svg` ) );
		await response;
		await this.page.getByRole( 'button', { name: 'Insert Media' } )
			.or( this.page.getByRole( 'button', { name: 'Select' } ) ).nth( 1 ).click();
	}

	/**
	 * Load a template from a file.
	 *
	 * @param {string}  filePath             - Path to the template file.
	 * @param {boolean} updateDatesForImages - Optional. Whether to update images dates. Default is false.
	 */
	async loadTemplate( filePath: string, updateDatesForImages: boolean = false ): Promise<void> {
		const rawFileData = await readFile( filePath );
		let templateData = JSON.parse( rawFileData.toString() );

		// For templates that use images, date when image is uploaded is hardcoded in template.
		// Element regression tests upload images before each test.
		// To update dates in template, use a flag updateDatesForImages = true
		if ( updateDatesForImages ) {
			templateData = this.updateImageDates( templateData );
		}

		await this.page.evaluate( ( data ) => {
			const model = new Backbone.Model( { title: 'test' } );

			window.$e.run( 'document/elements/import', {
				data,
				model,
				options: {
					at: 0,
					withPageSettings: false,
				},
			} );
		}, templateData );
	}

	async stabilizeForScreenshot( page: Page, editor?: any ): Promise<void> {
		try {
			if ( editor?.removeWpAdminBar ) {
				await editor.removeWpAdminBar();
			}
		} catch {
		}
		await page.addStyleTag( { content: '*{transition:none!important;animation:none!important}*,*:before,*:after{transition:none!important;animation:none!important}' } )
			.catch( () => {} );
		await page.evaluate( async () => {
			if ( document.fonts && 'ready' in document.fonts ) {
				await ( document.fonts as FontFaceSet ).ready;
			}
		} ).catch( () => {} );
		await page.waitForLoadState( 'networkidle' ).catch( () => {} );
		await page.waitForTimeout( 100 );
	}

	/**
	 * Remove all the content from the page.
	 *
	 * @return {Promise<void>}
	 */
	async cleanContent(): Promise<void> {
		await this.page.evaluate( () => {
			$e.run( 'document/elements/empty', { force: true } );
		} );
	}

	/**
	 * Wait for the editor panels to finish loading.
	 *
	 * @return {Promise<void>}
	 */
	async waitForPanelToLoad(): Promise<void> {
		await this.page.waitForSelector( '.elementor-panel-loading', { state: 'detached' } );
		await this.page.waitForSelector( '#elementor-loading', { state: 'hidden' } );
	}

	/**
	 * Add element to the page using a model.
	 *
	 * @param {Object}  model               - Model definition.
	 * @param {string}  container           - Optional. Container to create the element in.
	 * @param {boolean} isContainerASection - Optional. Whether the container is a section.
	 *
	 * @return {Promise<string>} Element ID
	 */
	async addElement( model: unknown, container: null | string = null, isContainerASection = false ): Promise<string> {
		return await this.page.evaluate( addElement, { model, container, isContainerASection } );
	}

	/**
	 * Remove element from the page.
	 *
	 * @param {string} elementId - Element ID.
	 *
	 * @return {Promise<void>}
	 */
	async removeElement( elementId: string ): Promise<void> {
		await this.page.evaluate( ( { id } ) => {
			$e.run( 'document/elements/delete', {
				container: elementor.getContainer( id ),
			} );
		}, { id: elementId } );
	}

	/**
	 * Add a widget by `widgetType`.
	 *
	 * @param {string}  widgetType          - Widget type.
	 * @param {string}  container           - Optional. Container to create the element in.
	 * @param {boolean} isContainerASection - Optional. Whether the container is a section.
	 *
	 * @return {Promise<string>} The widget ID.
	 */
	async addWidget( widgetType: string, container: string = null, isContainerASection: boolean = false ): Promise<string> {
		const widgetId = await this.addElement( { widgetType, elType: 'widget' }, container, isContainerASection );
		await this.getPreviewFrame().waitForSelector( `[data-id='${ widgetId }']` );

		return widgetId;
	}

	/**
	 * Add a page by importing a Json page object from PostMeta _elementor_data into Tests
	 *
	 * @param {string}  dirName              - Directory name, use `__dirname` for the current directory.
	 * @param {string}  fileName             - Name of the file without extension.
	 * @param {string}  widgetSelector       - Selector of the widget.
	 * @param {boolean} updateDatesForImages - Optional. Whether to update image dates in the template. Default is false.
	 *
	 * @return {Promise<void>}
	 */
	async loadJsonPageTemplate( dirName: string, fileName: string, widgetSelector: string, updateDatesForImages: boolean = false ): Promise<void> {
		const filePath = _path.resolve( dirName, `./templates/${ fileName }.json` );
		const rawFileData = await readFile( filePath );
		const templateData = JSON.parse( rawFileData.toString() );
		const pageTemplateData =
		{
			content: templateData,
			page_settings: [],
			version: '0.4',
			title: 'Elementor Test',
			type: 'page',
		};

		// For templates that use images, date when image is uploaded is hardcoded in template.
		// Element regression tests upload images before each test.
		// To update dates in template, use a flag updateDatesForImages = true
		if ( updateDatesForImages ) {
			this.updateImageDates( templateData );
		}

		await this.page.evaluate( ( data ) => {
			const model = new Backbone.Model( { title: 'test' } );

			window.$e.run( 'document/elements/import', {
				data,
				model,
				options: {
					at: 0,
					withPageSettings: false,
				},
			} );
		}, pageTemplateData );

		await this.waitForElement( false, widgetSelector );
	}

	/**
	 * Get element handle from the preview frame using its Container ID.
	 *
	 * @param {string} id - Container ID.
	 *
	 * @return {Promise<ElementHandle<SVGElement | HTMLElement> | null>} element handle
	 */
	async getElementHandle( id: string ): Promise<ElementHandle<SVGElement | HTMLElement> | null> {
		return this.getPreviewFrame().$( getElementSelector( id ) );
	}

	async waitForPreviewFrame(): Promise<Frame> {
		await this.page.waitForSelector( '[id="elementor-preview-iframe"]', { timeout: timeouts.longAction } );

		const frame = this.page.frame( { name: 'elementor-preview-iframe' } );
		if ( ! frame ) {
			throw new Error( 'Iframe is null even after it appeared in the DOM.' );
		}

		return frame;
	}

	/**
	 * Get the frame of the Elementor editor preview.
	 *
	 * @return {Frame} The preview iframe element.
	 */
	getPreviewFrame(): Frame {
		return this.page.frame( { name: 'elementor-preview-iframe' } );
	}

	/**
	 * Select an element inside the editor.
	 *
	 * @param {string} elementId - Element ID.
	 *
	 * @return {Promise<Locator>} element;
	 */
	async selectElement( elementId: string ): Promise<Locator> {
		await this.page.evaluate( ( { id } ) => {
			$e.run( 'document/elements/select', {
				container: elementor.getContainer( id ),
			} );
		}, { id: elementId } );

		await this.getPreviewFrame().waitForSelector( '.elementor-element-' + elementId + '.elementor-element-editable' );
		return this.getPreviewFrame().locator( '.elementor-element-' + elementId );
	}

	/**
	 * Add new container preset.
	 *
	 * @param {ContainerType}   element - Element type. Available values: 'flex', 'grid'.
	 * @param {ContainerPreset} preset  - Container preset.
	 *
	 * @return {Promise<void>}
	 */
	async addNewContainerPreset( element: ContainerType, preset: ContainerPreset ): Promise<void> {
		const frame = this.getPreviewFrame();
		await frame.locator( '.elementor-add-section-button' ).click();
		await frame.locator( `.${ element }-preset-button` ).click();
		await frame.locator( `[data-preset=${ preset }]` ).click();
	}

	/**
	 * Open the section that adds a new element.
	 *
	 * @param {string} elementId - Element ID.
	 *
	 * @return {Promise<void>}
	 */
	async openAddElementSection( elementId: string ): Promise<void> {
		const element = this.getPreviewFrame().locator( `.elementor-edit-mode .elementor-element-${ elementId }` );
		await element.hover();
		const elementAddButton = this.getPreviewFrame().locator( `.elementor-edit-mode .elementor-element-${ elementId } > .elementor-element-overlay > .elementor-editor-element-settings > .elementor-editor-element-add` );
		await elementAddButton.click();
		await this.getPreviewFrame().waitForSelector( '.elementor-add-section-inline' );
	}

	async setWidgetTab( tab: 'content' | 'style' | 'advanced' ): Promise<void> {
		await this.page.locator( `.elementor-tab-control-${ tab }` ).click();
	}

	/**
	 * Open a tab inside an Editor panel.
	 *
	 * @param {string} panelId - The panel tab to open.
	 *
	 * @return {Promise<void>}
	 */
	async openPanelTab( panelId: string ): Promise<void> {
		await this.page.waitForSelector( `.elementor-tab-control-${ panelId } span` );

		// Check if panel has been activated already.
		if ( await this.page.$( `.elementor-tab-control-${ panelId }.elementor-active` ) ) {
			return;
		}

		await this.page.locator( `.elementor-tab-control-${ panelId } span` ).click();
		await this.page.waitForSelector( `.elementor-tab-control-${ panelId }.elementor-active` );
	}

	/**
	 * Open a tab inside an Editor panel for V2 widgets.
	 *
	 * @param {'style' | 'general'} sectionName - The section to open.
	 *
	 * @return {Promise<void>}
	 */
	async openV2PanelTab( sectionName: 'style' | 'general' ) {
		const selectorMap: Record< 'style' | 'general', string > = {
			style: 'style',
			general: 'settings',
		};
		const sectionButtonSelector = `#tab-0-${ selectorMap[ sectionName ] }`,
			sectionContentSelector = `#tabpanel-0-${ selectorMap[ sectionName ] }`,
			isOpenSection = await this.page.evaluate( ( selector ) => {
				const sectionContentElement: HTMLElement = document.querySelector( selector );

				return ! sectionContentElement?.hidden;
			}, sectionContentSelector );

		if ( isOpenSection ) {
			return;
		}

		await this.page.locator( sectionButtonSelector ).click();
		await this.page.locator( sectionContentSelector ).waitFor();
	}

	/**
	 * Open a section in an active panel tab.
	 *
	 * @param {string} sectionId - The section to open.
	 *
	 * @return {Promise<void>}
	 */
	async openSection( sectionId: string ): Promise<void> {
		const sectionSelector = `.elementor-control-${ sectionId }`,
			isOpenSection = await this.page.evaluate( ( selector ) => {
				const sectionElement = document.querySelector( selector );

				return sectionElement?.classList.contains( 'e-open' ) || sectionElement?.classList.contains( 'elementor-open' );
			}, sectionSelector ),
			section = await this.page.$( sectionSelector + ':not( .e-open ):not( .elementor-open ):visible' );

		if ( ! section || isOpenSection ) {
			return;
		}

		await this.page.locator( sectionSelector + ':not( .e-open ):not( .elementor-open ):visible' + ' .elementor-panel-heading' ).click();
	}

	/**
	 * Close a section in an active panel tab.
	 *
	 * @param {string} sectionId - The section to close.
	 *
	 * @return {Promise<void>}
	 */
	async closeSection( sectionId: string ): Promise<void> {
		const sectionSelector = `.elementor-control-${ sectionId }`,
			isOpenSection = await this.page.evaluate( ( selector ) => {
				const sectionElement = document.querySelector( selector );

				return sectionElement?.classList.contains( 'e-open' ) || sectionElement?.classList.contains( 'elementor-open' );
			}, sectionSelector ),
			section = await this.page.$( sectionSelector + '.e-open:visible' );

		if ( ! section || ! isOpenSection ) {
			return;
		}

		await this.page.locator( sectionSelector + '.e-open:visible .elementor-panel-heading' ).click();
	}

	/**
	 * Open a section in an active panel tab.
	 *
	 * @param {string} sectionId - The section to open.
	 *
	 * @return {Promise<void>}
	 */
	async openV2Section( sectionId: 'layout' | 'spacing' | 'size' | 'position' | 'typography' | 'background' | 'border' ) {
		const sectionButton = this.page.locator( '.MuiButtonBase-root', { hasText: new RegExp( sectionId, 'i' ) } );
		const contentSelector = await sectionButton.getAttribute( 'aria-controls' );
		const isContentVisible = await this.page.evaluate( ( selector ) => {
			return !! document.getElementById( selector );
		}, contentSelector );

		if ( isContentVisible ) {
			return;
		}

		await sectionButton.click();
	}

	/**
	 * Set a custom width value to a widget.
	 *
	 * @param {string} width - Optional. The custom width value (as a percentage). Default is '100'.
	 *
	 * @return {Promise<void>}
	 */
	async setWidgetCustomWidth( width: string = '100' ): Promise<void> {
		await this.openPanelTab( 'advanced' );
		await this.setSelectControlValue( '_element_width', 'initial' );
		await this.setSliderControlValue( '_element_custom_width', width );
	}

	/**
	 * Set tab control value.
	 *
	 * @param {string} controlId - The control to select.
	 * @param {string} tabId     - The tab to select.
	 *
	 * @return {Promise<void>}
	 */
	async setTabControlValue( controlId: string, tabId: string ): Promise<void> {
		await this.page.locator( `.elementor-control-${ controlId } .elementor-control-${ tabId }` ).first().click();
	}

	/**
	 * Set text control value.
	 *
	 * @param {string} controlId - The control to set the value to.
	 * @param {string} value     - The value to set.
	 *
	 * @return {Promise<void>}
	 */
	async setTextControlValue( controlId: string, value: string ): Promise<void> {
		await this.page.locator( `.elementor-control-${ controlId } input` ).nth( 0 ).fill( value.toString() );
	}

	/**
	 * Set textarea control value.
	 *
	 * @param {string} controlId - The control to set the value to.
	 * @param {string} value     - The value to set.
	 *
	 * @return {Promise<void>}
	 */
	async setTextareaControlValue( controlId: string, value: string ): Promise<void> {
		await this.page.locator( `.elementor-control-${ controlId } textarea` ).fill( value.toString() );
	}

	/**
	 * Set number control value.
	 *
	 * @param {string} controlId - The control to set the value to.
	 * @param {string} value     - The value to set.
	 *
	 * @return {Promise<void>}
	 */
	async setNumberControlValue( controlId: string, value: string ): Promise<void> {
		await this.page.locator( `.elementor-control-${ controlId } input >> nth=0` ).fill( value.toString() );
	}

	/**
	 * Set slider control value.
	 *
	 * @param {string} controlId - The control to set the value to.
	 * @param {string} value     - The value to set.
	 */
	async setSliderControlValue( controlId: string, value: string ): Promise<void> {
		await this.page.locator( `.elementor-control-${ controlId } .elementor-slider-input input` ).fill( value );
	}

	/**
	 * Set select control value.
	 *
	 * @param {string} controlId - The control to set the value to.
	 * @param {string} value     - The value to set.
	 *
	 * @return {Promise<void>}
	 */
	async setSelectControlValue( controlId: string, value: string ): Promise<void> {
		await this.page.selectOption( `.elementor-control-${ controlId } select`, value );
	}

	/**
	 * Set select2 control value.
	 *
	 * @param {string}  controlId  - The control to set the value to.
	 * @param {string}  value      - The value to set.
	 * @param {boolean} exactMatch - Optional. Select only items that exactly match the provided value. Default is true.
	 *
	 * @return {Promise<void>}
	 */
	async setSelect2ControlValue( controlId: string, value: string, exactMatch: boolean = true ): Promise<void> {
		await this.page.locator( `.elementor-control-${ controlId } .select2:not( .select2-container--disabled )` ).click();
		await this.page.locator( '.select2-search--dropdown input[type="search"]' ).first().fill( value );

		if ( exactMatch ) {
			await this.page.locator( `.select2-results__option:text-is("${ value }")` ).first().click();
		} else {
			await this.page.locator( `.select2-results__option:has-text("${ value }")` ).first().click();
		}

		await this.page.waitForLoadState( 'domcontentloaded' );
	}

	/**
	 * Set dimensions control value.
	 *
	 * @param {string} controlId - The control to set the value to.
	 * @param {string} value     - The value to set.
	 *
	 * @return {Promise<void>}
	 */
	async setDimensionsValue( controlId: string, value: string ): Promise<void> {
		await this.page.locator( `.elementor-control-${ controlId } .elementor-control-dimensions li:first-child input` ).fill( value );
	}

	/**
	 * Set choose control value.
	 *
	 * TODO: For consistency, we need to rewrite the logic, from icon based to value based.
	 *
	 * @param {string} controlId - The control to set the value to.
	 * @param {string} icon      - The icon to choose.
	 *
	 * @return {Promise<void>}
	 */
	async setChooseControlValue( controlId: string, icon: string ): Promise<void> {
		await this.page.locator( `.elementor-control-${ controlId } input[value="${ icon }"]` ).click();
	}

	/**
	 * Set choose-image control value (custom Choose_Img_Control).
	 *
	 * @param {string} controlId - The control to set the value to.
	 * @param {string} value     - The option value to choose (e.g., 'focus').
	 *
	 * @return {Promise<void>}
	 */
	async setPresetImageControlValue( controlId: string, value: string ): Promise<void> {
		const control = this.page.locator( `.elementor-control-${ controlId }` );
		await control.locator( '.elementor-choices.elementor-choices-img' ).first().waitFor();
		const choice = control.locator( '.elementor-choices-element' ).filter( { has: this.page.locator( `img.elementor-choices-image[data-hover="${ value }"]` ) } ).first();
		await choice.scrollIntoViewIfNeeded();
		await choice.locator( 'label.elementor-choices-label' ).first().click();
	}

	async setIconControlValueByName( controlId: string, iconName: string ): Promise<void> {
		const control = this.page.locator( `.elementor-control-${ controlId }` );
		await control.locator( '.elementor-control-icons--inline__icon' ).first().click();
		await this.page.locator( '#elementor-icons-manager-modal' ).waitFor();
		const item = this.page.locator( 'div' ).filter( { hasText: new RegExp( `^${ iconName }$` ) } ).first();
		await item.waitFor();
		await item.click();
		await this.page.getByRole( 'button', { name: 'Insert' } ).click();
	}

	/**
	 * Set color control value.
	 *
	 * @param {string} controlId - The control to set the value to.
	 * @param {string} value     - The value to set.
	 *
	 * @return {Promise<void>}
	 */
	async setColorControlValue( controlId: string, value: string ): Promise<void> {
		const controlSelector = `.elementor-control-${ controlId }`;

		await this.page.locator( controlSelector + ' .pcr-button' ).click();
		await this.page.locator( '.pcr-app.visible .pcr-interaction input.pcr-result' ).fill( value );
		await this.page.locator( controlSelector ).click();
	}

	/**
	 * Set switcher control value.
	 *
	 * @param {string}  controlId - The control to set the value to.
	 * @param {boolean} value     - Optional. The value to set (true|false). Default is true.
	 *
	 * @return {Promise<void>}
	 */
	async setSwitcherControlValue( controlId: string, value: boolean = true ): Promise<void> {
		const controlSelector = `.elementor-control-${ controlId }`,
			controlLabel = this.page.locator( controlSelector + ' label.elementor-switch' ),
			currentState = await this.page.locator( controlSelector + ' input[type="checkbox"]' ).isChecked();

		if ( currentState !== Boolean( value ) ) {
			await controlLabel.click();
		}
	}

	/**
	 * Set gap control value.
	 *
	 * @param {string}     controlId - The control to set the value to.
	 * @param {GapControl} value     - The value to set. Either a string or an object with column, row and unit values.
	 *
	 * @return {Promise<void>}
	 */
	async setGapControlValue( controlId: string, value: GapControl ): Promise<void> {
		const control = this.page.locator( `.elementor-control-${ controlId }` );

		if ( 'string' === typeof value ) {
			await control.locator( '.elementor-control-gap >> nth=0' ).locator( 'input' ).fill( value );
		} else if ( 'object' === typeof value ) {
			await control.locator( '.elementor-link-gaps' ).click();
			await control.locator( '.elementor-control-gap input[data-setting="column"]' ).fill( value.column );
			await control.locator( '.elementor-control-gap input[data-setting="row"]' ).fill( value.row );
			if ( value.unit ) {
				await control.locator( '.e-units-switcher' ).click();
				await control.locator( `[data-choose="${ value.unit }"]` ).click();
			}
		}
	}

	/**
	 * Set an image on a media control.
	 *
	 * @param {string}  controlId  - The control to set the value to.
	 * @param {boolean} imageTitle - The title of the image to set.
	 *
	 * @return {Promise<void>}
	 */
	async setMediaControlImageValue( controlId: string, imageTitle: string ): Promise<void> {
		await this.page.locator( `.elementor-control-${ controlId } .elementor-control-media__preview` ).click();
		await this.page.getByRole( 'tab', { name: 'Media Library' } ).click();
		await this.page.locator( `[aria-label="${ imageTitle }"]` ).click();
		await this.page.locator( '.button.media-button' ).click();
	}

	/**
	 * Set typography control value.
	 *
	 * @param {string} controlId - The control to set the value to.
	 * @param {string} fontsize  - Font size value.
	 *
	 * @return {Promise<void>}
	 */
	async setTypographyControlValue( controlId: string, fontsize: string ): Promise<void> {
		const controlSelector = `.elementor-control-${ controlId }_typography .eicon-edit`;

		await this.page.locator( controlSelector ).click();
		await this.setSliderControlValue( controlId + '_font_size', fontsize );
		await this.page.locator( controlSelector ).click();
	}

	async setShadowControlValue( controlId: string, shadowType: string ): Promise<void> {
		await this.page.locator( `.elementor-control-${ controlId }_${ shadowType }_shadow_type i.eicon-edit` ).click();
		await this.page.locator( `.elementor-control-${ controlId }_${ shadowType }_shadow_type label` ).first().click();
	}

	async setTextStrokeControlValue( controlId: string, strokeType: string, value: number, color: string ): Promise<void> {
		await this.page.locator( `.elementor-control-${ controlId }_${ strokeType }_stroke_type i.eicon-edit` ).click();
		await this.page.locator( `.elementor-control-${ controlId }_${ strokeType }_stroke input[type="number"]` ).first().fill( value.toString() );
		await this.page.locator( `.elementor-control-${ controlId }_stroke_color .pcr-button` ).first().click();
		await this.page.locator( '.pcr-app.visible .pcr-result' ).first().fill( color );
		await this.page.locator( `.elementor-control-${ controlId }_${ strokeType }_stroke_type label` ).first().click();
	}

	async setWidgetMask(): Promise<void> {
		await this.openSection( '_section_masking' );
		await this.setSwitcherControlValue( '_mask_switch', true );
		await this.setSelectControlValue( '_mask_size', 'custom' );
		await this.setSliderControlValue( '_mask_size_scale', '30' );
		await this.setSelectControlValue( '_mask_position', 'top right' );
	}

	/**
	 * Hide controls from the video widgets.
	 *
	 * @return {Promise<void>}
	 */
	async hideVideoControls(): Promise<void> {
		await this.getPreviewFrame().waitForSelector( '.elementor-video' );

		const videoFrame = this.getPreviewFrame().frameLocator( '.elementor-video' ),
			videoButton = videoFrame.locator( 'button.ytp-large-play-button.ytp-button.ytp-large-play-button-red-bg' ),
			videoGradient = videoFrame.locator( '.ytp-gradient-top' ),
			videoTitle = videoFrame.locator( '.ytp-show-cards-title' ),
			videoBottom = videoFrame.locator( '.ytp-impression-link' );

		await videoButton.evaluate( ( element ) => element.style.opacity = '0' );
		await videoGradient.evaluate( ( element ) => element.style.opacity = '0' );
		await videoTitle.evaluate( ( element ) => element.style.opacity = '0' );
		await videoBottom.evaluate( ( element ) => element.style.opacity = '0' );
	}

	/**
	 * Hide controls and overlays on map widgets.
	 *
	 * @return {Promise<void>}
	 */
	async hideMapControls(): Promise<void> {
		await this.getPreviewFrame().waitForSelector( '.elementor-widget-google_maps iframe' );

		const mapFrame = this.getPreviewFrame().frameLocator( '.elementor-widget-google_maps iframe' ),
			mapText = mapFrame.locator( '.gm-style iframe + div + div' ),
			mapInset = mapFrame.locator( 'button.gm-inset-map.gm-inset-light' ),
			mapControls = mapFrame.locator( '.gmnoprint.gm-bundled-control.gm-bundled-control-on-bottom' );

		await mapText.evaluate( ( element ) => element.style.opacity = '0' );
		await mapInset.evaluate( ( element ) => element.style.opacity = '0' );
		await mapControls.evaluate( ( element ) => element.style.opacity = '0' );
	}

	async hideContactMapControls(): Promise<void> {
		await this.getPreviewFrame().waitForSelector( '.ehp-contact__map iframe' );

		const mapFrame = this.getPreviewFrame().frameLocator( '.ehp-contact__map iframe' ),
			mapText = mapFrame.locator( '.gm-style iframe + div + div' ),
			mapInset = mapFrame.locator( 'button.gm-inset-map.gm-inset-light' ),
			mapControls = mapFrame.locator( '.gmnoprint.gm-bundled-control.gm-bundled-control-on-bottom' );

		if ( await mapText.count() > 0 ) {
			await mapText.evaluate( ( element ) => element.style.opacity = '0' );
		}
		if ( await mapInset.count() > 0 ) {
			await mapInset.evaluate( ( element ) => element.style.opacity = '0' );
		}
		if ( await mapControls.count() > 0 ) {
			await mapControls.evaluate( ( element ) => element.style.opacity = '0' );
		}
	}

	/**
	 * Open the page in the Preview mode.
	 *
	 * @return {Promise<void>}
	 */
	async togglePreviewMode(): Promise<void> {
		if ( ! await this.page.$( 'body.elementor-editor-preview' ) ) {
			await this.page.locator( '#elementor-mode-switcher' ).click();
			await this.page.waitForSelector( 'body.elementor-editor-preview' );
			await this.page.waitForTimeout( 500 );
		} else {
			await this.page.locator( '#elementor-mode-switcher-preview' ).click();
			await this.page.waitForSelector( 'body.elementor-editor-active' );
		}
	}

	/**
	 * Wait for the Elementor preview to finish loading.
	 *
	 * @return {Promise<void>}
	 */
	async waitForPreviewToLoad(): Promise<void> {
		await this.page.waitForSelector( '#elementor-preview-loading' );
		await this.page.waitForSelector( '#elementor-preview-loading', { state: 'hidden' } );
	}

	/**
	 * Hide all editor elements from the screenshots.
	 *
	 * @return {Promise<void>}
	 */
	async hideEditorElements(): Promise<void> {
		const css = '<style>.elementor-element-overlay,.elementor-empty-view{opacity: 0;}.elementor-widget,.elementor-widget:hover{box-shadow:none!important;}</style>';

		await this.addWidget( 'html' );
		await this.setTextareaControlValue( 'type-code', css );
	}

	/**
	 * Whether the Top Bar is active or not.
	 *
	 * @return {Promise<boolean>} Returns true if the Top Bar is visible, false otherwise.
	 */
	async hasTopBar(): Promise<boolean> {
		return await this.page.locator( EditorSelectors.panels.topBar.wrapper ).isVisible();
	}

	/**
	 * Click on a top bar item.
	 *
	 * @param {TopBarSelector} selector - The selector object for the top bar button.
	 *
	 * @return {Promise<void>}
	 */
	async clickTopBarItem( selector: TopBarSelector ): Promise<void> {
		const topbarLocator = this.page.locator( EditorSelectors.panels.topBar.wrapper );
		if ( 'text' === selector.attribute ) {
			await topbarLocator.getByRole( 'button', { name: selector.attributeValue } ).click();
		} else {
			await topbarLocator.locator( `button[${ selector.attribute }="${ selector.attributeValue }"]` ).click();
		}
	}

	/**
	 * Open the menu panel. Or, when an inner panel is provided, open the inner panel.
	 *
	 * TODO: Delete when Editor Top Bar feature is merged.
	 *
	 * @param {string} innerPanel - Optional. The inner menu to open.
	 *
	 * @return {Promise<void>}
	 */
	async openMenuPanel( innerPanel?: string ): Promise<void> {
		await this.page.locator( EditorSelectors.panels.menu.footerButton ).click();
		await this.page.locator( EditorSelectors.panels.menu.wrapper ).waitFor();

		if ( innerPanel ) {
			await this.page.locator( `.elementor-panel-menu-item-${ innerPanel }` ).click();
		}
	}

	/**
	 * Open the elements/widgets panel.
	 *
	 * @return {Promise<void>}
	 */
	async openElementsPanel(): Promise<void> {
		const hasTopBar = await this.hasTopBar();

		if ( hasTopBar ) {
			await this.clickTopBarItem( TopBarSelectors.elementsPanel );
		} else {
			await this.page.locator( EditorSelectors.panels.elements.footerButton ).click();
		}

		await this.page.locator( EditorSelectors.panels.elements.wrapper ).waitFor();
	}

	/**
	 * Open the page settings panel.
	 *
	 * @return {Promise<void>}
	 */
	async openPageSettingsPanel(): Promise<void> {
		const hasTopBar = await this.hasTopBar();

		if ( hasTopBar ) {
			await this.clickTopBarItem( TopBarSelectors.documentSettings );
		} else {
			await this.page.locator( EditorSelectors.panels.pageSettings.footerButton ).click();
		}

		await this.page.locator( EditorSelectors.panels.pageSettings.wrapper ).waitFor();
	}

	/**
	 * Open the site settings panel. Or, when an inner panel is provided, open the inner panel.
	 *
	 * @param {string} innerPanel - Optional. The inner menu to open.
	 *
	 * @return {Promise<void>}
	 */
	async openSiteSettings( innerPanel?: string ): Promise<void> {
		const hasTopBar = await this.hasTopBar();

		if ( hasTopBar ) {
			await this.clickTopBarItem( TopBarSelectors.siteSettings );
		} else {
			await this.openMenuPanel( 'global-settings' );
		}

		await this.page.locator( EditorSelectors.panels.siteSettings.wrapper ).waitFor();

		if ( innerPanel ) {
			await this.page.locator( `.elementor-panel-menu-item-${ innerPanel }` ).click();
		}
	}

	/**
	 * Open the user preferences panel.
	 *
	 * @return {Promise<void>}
	 */
	async openUserPreferencesPanel(): Promise<void> {
		const hasTopBar = await this.hasTopBar();

		if ( hasTopBar ) {
			await this.clickTopBarItem( TopBarSelectors.elementorLogo );
			await this.page.waitForTimeout( 100 );
			await this.page.getByRole( 'menuitem', { name: 'User Preferences' } ).click();
		} else {
			await this.openMenuPanel( 'editor-preferences' );
		}

		await this.page.locator( EditorSelectors.panels.userPreferences.wrapper ).waitFor();
	}

	/**
	 * Close the navigator/structure panel.
	 *
	 * @return {Promise<void>}
	 */
	async closeNavigatorIfOpen(): Promise<void> {
		await this.waitForPreviewFrame();
		const isOpen = await this.getPreviewFrame().evaluate( () => elementor.navigator.isOpen() );

		if ( ! isOpen ) {
			return;
		}

		await this.page.locator( EditorSelectors.panels.navigator.closeButton ).click();
	}

	/**
	 * Set WordPress page template.
	 *
	 * @param {string} template - The page template to set. Available options: 'default', 'canvas', 'full-width'.
	 *
	 * @return {Promise<void>}
	 */
	async setPageTemplate( template: 'default' | 'canvas' | 'full-width' ): Promise<void> {
		let templateValue: string;
		let templateClass: string;

		switch ( template ) {
			case 'default':
				templateValue = 'default';
				templateClass = '.elementor-default';
				break;
			case 'canvas':
				templateValue = 'elementor_canvas';
				templateClass = '.elementor-template-canvas';
				break;
			case 'full-width':
				templateValue = 'elementor_header_footer';
				templateClass = '.elementor-template-full-width';
				break;
		}

		// Check if the template is already set
		if ( await this.getPreviewFrame().$( templateClass ) ) {
			return;
		}

		// Select the template
		await this.openPageSettingsPanel();
		await this.setSelectControlValue( 'template', templateValue );
		await this.getPreviewFrame().waitForSelector( templateClass );
	}

	/**
	 * Change the display mode of the editor.
	 *
	 * @param {string} uiMode - Either 'light', 'dark', or 'auto'.
	 *
	 * @return {Promise<void>}
	 */
	async setDisplayMode( uiMode: string ):	Promise<void> {
		const uiThemeOptions = {
			light: 'eicon-light-mode',
			dark: 'eicon-dark-mode',
			auto: 'eicon-header',
		};

		await this.openUserPreferencesPanel();
		await this.setChooseControlValue( 'ui_theme', uiThemeOptions[ uiMode ] );
	}

	/**
	 * Open the responsive view bar.
	 *
	 * TODO: Delete when Editor Top Bar feature is merged.
	 *
	 * @return {Promise<void>}
	 */
	async openResponsiveViewBar(): Promise<void> {
		const hasResponsiveViewBar = await this.page.evaluate( () => elementor.isDeviceModeActive() );

		if ( ! hasResponsiveViewBar ) {
			await this.page.locator( '#elementor-panel-footer-responsive i' ).click();
		}
	}

	/**
	 * Select a responsive view.
	 *
	 * @param {Device} device - The name of the device breakpoint, such as `tablet_extra`.
	 *
	 * @return {Promise<void>}
	 */
	async changeResponsiveView( device: Device ): Promise<void> {
		const hasTopBar = await this.hasTopBar();
		if ( hasTopBar ) {
			await Breakpoints.getDeviceLocator( this.page, device ).click();
		} else {
			await this.openResponsiveViewBar();
			await this.page.locator( `#e-responsive-bar-switcher__option-${ device }` ).first().locator( 'i' ).click();
		}
	}

	/**
	 * Publish the current page.
	 *
	 * @return {Promise<void>}
	 */
	async publishPage(): Promise<void> {
		const hasTopBar = await this.hasTopBar();

		if ( hasTopBar ) {
			await this.clickTopBarItem( TopBarSelectors.publish );
			await this.page.waitForLoadState();
			await this.page.locator( EditorSelectors.panels.topBar.wrapper + ' button[disabled]', { hasText: 'Publish' } ).waitFor( { timeout: timeouts.longAction } );
		} else {
			await this.page.locator( 'button#elementor-panel-saver-button-publish' ).click();
			await this.page.waitForLoadState();
			await this.page.getByRole( 'button', { name: 'Update' } ).waitFor();
		}
	}

	/**
	 * Publish the current page and view it.
	 *
	 * @return {Promise<void>}
	 */
	async publishAndViewPage(): Promise<void> {
		await this.publishPage();
		await this.viewPage();
	}

	async viewPage() {
		const pageId = await this.getPageId();

		if ( ! pageId ) {
			return;
		}

		await this.page.goto( `/?p=${ pageId }` );
		await this.page.waitForLoadState();
	}

	/**
	 * Get a control value by index with modulo cycling for array access.
	 *
	 * @param {Array}  controlValues - Array of control values.
	 * @param {number} loopIndex     - The loop index.
	 *
	 * @return {any} The control value at the calculated index.
	 */
	getControlValueByIndex( controlValues: any[], loopIndex: number ): any {
		return controlValues[ loopIndex % controlValues.length ];
	}

	/**
	 * Set background color control value with proper visibility check.
	 * Ensures the color picker is opened before setting the color value.
	 *
	 * @param {string} backgroundControlId - The background control ID (e.g., 'background_background').
	 * @param {string} colorControlId      - The color control ID (e.g., 'background_color').
	 * @param {string} colorValue          - The color value to set.
	 *
	 * @return {Promise<void>}
	 */
	async setBackgroundColorControlValue( backgroundControlId: string, colorControlId: string, colorValue: string ): Promise<void> {
		const colorControl = this.page.locator( `.elementor-control-${ colorControlId }` );

		if ( ! await colorControl.isVisible() ) {
			await this.setChooseControlValue( backgroundControlId, 'eicon-paint-brush' );
		}

		await this.setColorControlValue( colorControlId, colorValue );
	}

	/**
	 * Save and reload the current page.
	 *
	 * @return {Promise<void>}
	 */
	async saveAndReloadPage(): Promise<void> {
		const hasTopBar = await this.hasTopBar();

		if ( hasTopBar ) {
			await this.clickTopBarItem( TopBarSelectors.publish );
		} else {
			await this.page.locator( '#elementor-panel-saver-button-publish' ).click();
		}

		await this.page.waitForLoadState();
		await this.page.waitForResponse( '/wp-admin/admin-ajax.php' );
		await this.page.reload();
	}

	/**
	 * Get the current page ID.
	 *
	 * @return {Promise<string>} The ID of the current page.
	 */
	async getPageId(): Promise<string | null> {
		return await this.page.evaluate( () => {
			const urlParams = new URLSearchParams( window.location.search );
			return urlParams.get( 'post' );
		} );
	}

	/**
	 * Apply Element Settings
	 *
	 * Apply settings to a widget without having to navigate through its Panels and Sections to set each individual
	 * control value.
	 *
	 * You can get the Element settings by right-clicking an existing widget or element in the Editor, choose "Copy",
	 * then paste the content into a text editor and filter out just the settings you want to apply to your element.
	 *
	 * Example usage:
	 * ```
	 * await editor.applyElementSettings( 'cdefd82', {
	 *     background_background: 'classic',
	 *     background_color: 'rgb(255, 10, 10)',
	 * } );
	 * ```
	 *
	 * @param {string} elementId - Id of the element you intend to apply the settings to.
	 * @param {Object} settings  - Object settings from the Editor > choose element > right-click > "Copy".
	 *
	 * @return {Promise<void>}
	 */
	async applyElementSettings( elementId: string, settings: unknown ): Promise<void> {
		await this.page.evaluate(
			( args ) => $e.run( 'document/elements/settings', {
				container: elementor.getContainer( args.elementId ),
				settings: args.settings,
			} ),
			{ elementId, settings },
		);
	}

	/**
	 * Check if an item is in the viewport.
	 *
	 * @param {string} itemSelector - The item selector.
	 *
	 * @return {Promise<boolean>} Returns true if the item is in the viewport, false otherwise.
	 */
	async isItemInViewport( itemSelector: string ): Promise<boolean> {
		return this.page.evaluate( ( item: string ) => {
			let isVisible = false;

			const element: HTMLElement = document.querySelector( item );

			if ( element ) {
				const rect = element.getBoundingClientRect();

				if ( rect.top >= 0 && rect.left >= 0 ) {
					const vw = Math.max( document.documentElement.clientWidth || 0, window.innerWidth || 0 ),
						vh = Math.max( document.documentElement.clientHeight || 0, window.innerHeight || 0 );

					if ( rect.right <= vw && rect.bottom <= vh ) {
						isVisible = true;
					}
				}
			}
			return isVisible;
		}, itemSelector );
	}

	/**
	 * Get the number of widgets in the editor.
	 *
	 * @return {Promise<number>} The number of widgets in the editor.
	 */
	async getWidgetCount(): Promise<number> {
		return ( await this.getPreviewFrame().$$( EditorSelectors.widget ) ).length;
	}

	/**
	 * Based on the widget type, wait for the iframe to load.
	 *
	 * @param {string}  widgetType  - The widget type. Available options: 'video', 'google_maps', 'sound_cloud'.
	 * @param {boolean} isPublished - Optional. Whether the element is published. Default is false.
	 *
	 * @return {Promise<void>}
	 */
	async waitForIframeToLoaded( widgetType: string, isPublished: boolean = false ): Promise<void> {
		const frames = {
			video: [ EditorSelectors.video.iframe, EditorSelectors.video.playIcon ],
			google_maps: [ EditorSelectors.googleMaps.iframe, EditorSelectors.googleMaps.showSatelliteViewBtn ],
			sound_cloud: [ EditorSelectors.soundCloud.iframe, EditorSelectors.soundCloud.waveForm ],
		};

		if ( ! ( widgetType in frames ) ) {
			return;
		}

		if ( isPublished ) {
			await this.page.locator( frames[ widgetType ][ 0 ] ).first().waitFor();
			const count = await this.page.locator( frames[ widgetType ][ 0 ] ).count();
			for ( let i = 1; i < count; i++ ) {
				await this.page.frameLocator( frames[ widgetType ][ 0 ] ).nth( i ).locator( frames[ widgetType ][ 1 ] ).waitFor();
			}
		} else {
			const frame = this.getPreviewFrame();
			await frame.waitForLoadState();
			await frame.waitForSelector( frames[ widgetType ][ 0 ] );
			await frame.frameLocator( frames[ widgetType ][ 0 ] ).first().locator( frames[ widgetType ][ 1 ] ).waitFor();
			const iframeCount: number = await new Promise( ( resolved ) => {
				resolved( frame.childFrames().length );
			} );
			for ( let i = 1; i < iframeCount; i++ ) {
				await frame.frameLocator( frames[ widgetType ][ 0 ] ).nth( i ).locator( frames[ widgetType ][ 1 ] ).waitFor();
			}
		}
	}

	/**
	 * Wait for the element to be visible.
	 *
	 * @param {boolean} isPublished - Whether the element is published.
	 * @param {string}  selector    - Element selector.
	 *
	 * @return {Promise<void>}
	 */
	async waitForElement( isPublished: boolean, selector: string ): Promise<void> {
		if ( selector === undefined ) {
			return;
		}

		if ( isPublished ) {
			await this.page.waitForSelector( selector );
		} else {
			const frame = this.getPreviewFrame();
			await frame.waitForLoadState();
			await frame.waitForSelector( selector );
		}
	}

	/**
	 * Verify class in element.
	 *
	 * @param {Object}  args             - Arguments.
	 * @param {string}  args.selector    - Element selector.
	 * @param {string}  args.className   - Class name.
	 * @param {boolean} args.isPublished - Whether the element is published.
	 *
	 * @return {Promise<void>}
	 */
	async verifyClassInElement( args: { selector: string, className: string, isPublished: boolean } ): Promise<void> {
		const regex = new RegExp( args.className );
		if ( args.isPublished ) {
			await expect( this.page.locator( args.selector ) ).toHaveClass( regex );
		} else {
			await expect( this.getPreviewFrame().locator( args.selector ) ).toHaveClass( regex );
		}
	}

	/**
	 * Verify image size.
	 *
	 * @param {Object}  args             - Arguments.
	 * @param {string}  args.selector    - Element selector.
	 * @param {number}  args.width       - Image width.
	 * @param {number}  args.height      - Image height.
	 * @param {boolean} args.isPublished - Whether the element is published.
	 *
	 * @return {Promise<void>}
	 */
	async verifyImageSize( args: { selector: string, width: number, height: number, isPublished: boolean } ): Promise<void> {
		const imageSize = args.isPublished
			? await this.page.locator( args.selector ).boundingBox()
			: await this.getPreviewFrame().locator( args.selector ).boundingBox();
		expect( imageSize.width ).toEqual( args.width );
		expect( imageSize.height ).toEqual( args.height );
	}

	/**
	 * Checks for a stable UI state by comparing screenshots at intervals and expecting a match.
	 * Can be used to check for completed rendering. Useful to wait out animations before screenshots and expects.
	 * Should be less flaky than waitForLoadState( 'load' ) in editor where Ajax re-rendering is triggered.
	 *
	 * @param {Locator} locator - The locator to check for.
	 * @param {number}  retries - Optional. Number of retries. Default is 3.
	 * @param {number}  timeout - Optional. Time to wait between retries, in milliseconds. Default is 500.
	 *
	 * @return {Promise<void>}
	 */
	async isUiStable( locator: Locator, retries: number = 3, timeout: number = 500 ): Promise<void> {
		const comparator = getComparator( 'image/png' );
		let retry = 0,
			beforeImage: Buffer,
			afterImage: Buffer;

		await locator.waitFor();

		do {
			if ( retry === retries ) {
				break;
			}

			beforeImage = await locator.screenshot( {
				path: `./before.png`,
			} );

			await new Promise( ( resolve ) => setTimeout( resolve, timeout ) );

			afterImage = await locator.screenshot( {
				path: `./after.png`,
			} );
			retry = retry++;
		} while ( null !== comparator( beforeImage, afterImage ) );
	}

	/**
	 * Remove classes from the page.
	 *
	 * @param {string} className - The class to remove.
	 *
	 * @return {Promise<void>}
	 */
	async removeClasses( className: string ): Promise<void> {
		await this.page.evaluate( async ( _class ) => {
			await new Promise( ( resolve1 ) => {
				const elems = document.querySelectorAll( `.${ _class }` );

				[].forEach.call( elems, function( el: HTMLElement ) {
					el.classList.remove( _class );
				} );
				resolve1( 'Foo' );
			} );
		}, className );
	}

	/**
	 * Scroll the page.
	 *
	 * @return {Promise<void>}
	 */
	async scrollPage(): Promise<void> {
		await this.page.evaluate( async () => {
			await new Promise( ( resolve1 ) => {
				let totalHeight = 0;
				const distance = 400;
				const timer = setInterval( () => {
					const scrollHeight = document.body.scrollHeight;
					window.scrollBy( 0, distance );
					totalHeight += distance;
					if ( totalHeight >= scrollHeight ) {
						clearInterval( timer );
						window.scrollTo( 0, 0 );
						resolve1( 'Foo' );
					}
				}, 100 );
			} );
		} );
	}

	/**
	 * Remove the WordPress admin bar.
	 *
	 * @return {Promise<void>}
	 */
	async removeWpAdminBar(): Promise<void> {
		const adminBar = 'wpadminbar';
		await this.page.locator( `#${ adminBar }` ).waitFor( { timeout: timeouts.longAction } );
		await this.page.evaluate( ( selector ) => {
			const admin = document.getElementById( selector );
			admin.remove();
		}, adminBar );
	}

	/**
	 * Isolated ID number.
	 *
	 * @param {string} idPrefix - The prefix of the item.
	 * @param {string} itemID   - The item ID.
	 *
	 * @return {Promise<number>} The numeric part of the ID with the prefix removed.
	 */
	async isolatedIdNumber( idPrefix: string, itemID: string ): Promise<number> {
		return Number( itemID.replace( idPrefix, '' ) );
	}

	async addImagesToGalleryControl( args?: { images?: string[], metaData?: boolean } ) {
		const defaultImages = [ 'A.jpg', 'B.jpg', 'C.jpg', 'D.jpg', 'E.jpg' ];

		await this.page.locator( EditorSelectors.galleryControl.addGalleryBtn ).nth( 0 ).click();
		await this.page.getByRole( 'tab', { name: 'Media Library' } ).click();

		const _images = args?.images === undefined ? defaultImages : args.images;

		for ( const i in _images ) {
			await this.page.setInputFiles( EditorSelectors.media.imageInp, pathResolve( __dirname, `../resources/${ _images[ i ] }` ) );

			if ( args?.metaData ) {
				await this.addTestImageMetaData();
			}
		}

		await this.page.locator( EditorSelectors.media.addGalleryButton ).click();
		await this.page.locator( 'text=Insert gallery' ).click();
	}

	async addTestImageMetaData( args = { caption: 'Test caption!', description: 'Test description!' } ) {
		await this.page.locator( EditorSelectors.media.images ).first().click();
		await this.page.locator( EditorSelectors.media.imgCaption ).clear();
		await this.page.locator( EditorSelectors.media.imgCaption ).type( args.caption );

		await this.page.locator( EditorSelectors.media.images ).first().click();
		await this.page.locator( EditorSelectors.media.imgDescription ).clear();
		await this.page.locator( EditorSelectors.media.imgDescription ).type( args.description );
	}

	/**
	 * Save the site settings with the top bar.
	 *
	 * TODO: Rename when Editor Top Bar feature is merged.
	 *
	 * @param {boolean} toReload - Whether to reload the page after saving.
	 *
	 * @return {Promise<void>}
	 */
	async saveSiteSettingsWithTopBar( toReload: boolean ): Promise<void> {
		if ( await this.page.locator( EditorSelectors.panels.siteSettings.saveButton ).isEnabled() ) {
			await this.page.locator( EditorSelectors.panels.siteSettings.saveButton ).click();
		} else {
			await this.page.evaluate( ( selector ) => {
				const button: HTMLElement = document.evaluate( selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE ).singleNodeValue as HTMLElement;
				button.click();
			}, EditorSelectors.panels.siteSettings.saveButton );
		}

		if ( toReload ) {
			await this.page.locator( EditorSelectors.refreshPopup.reloadButton ).click();
		}
	}

	/**
	 * Save the site settings without the top bar.
	 *
	 * TODO: Delete when Editor Top Bar feature is merged.
	 *
	 * @return {Promise<void>}
	 */
	async saveSiteSettingsNoTopBar(): Promise<void> {
		await this.page.locator( EditorSelectors.panels.footerTools.updateButton ).click();
		await this.page.locator( EditorSelectors.toast ).waitFor();
	}

	async assertCorrectVwWidthStylingOfElement( element: Locator, vwValue: number = 100 ): Promise<void> {
		const viewport = this.page.viewportSize();
		const vwConvertedToPxUnit = viewport.width * vwValue / 100;
		const elementWidthInPxUnit = await element.boundingBox().then( ( box ) => box?.width ?? 0 );
		const vwAndPxValuesAreEqual = Math.abs( vwConvertedToPxUnit - elementWidthInPxUnit ) <= 1;
		expect( vwAndPxValuesAreEqual ).toBeTruthy();
	}

	async importTemplateUI( filePath: string ) {
		await this.page.getByRole( 'link', { name: 'Templates', exact: true } ).click();
		await this.page.getByRole( 'link', { name: 'Hello+ Header' } ).first().click();

		if ( await this.page.locator( '.wp-list-table' ).first().locator( '[type="checkbox"]' ).first().isVisible() ) {
			await this.page.locator( '.wp-list-table' ).first().locator( '[type="checkbox"]' ).first().check();
			await this.page.locator( '#bulk-action-selector-top' ).selectOption( 'trash' );
			await this.page.locator( '#doaction' ).click();
		}

		await this.page.getByRole( 'link', { name: 'Add New Template' } ).click();
		await this.page.selectOption( '#elementor-new-template__form__template-type', 'ehp-header' );
		await this.page.getByRole( 'button', { name: 'Create Template' } ).click();
		await this.ensurePanelLoaded();
		await this.page.getByText( 'Templates', { exact: true } ).click();
		await this.page.getByText( 'Site templates' ).click();
		await this.page.locator( EditorSelectors.templateImport.importIcon ).click();
		await this.page.getByText( 'Select File' ).click();
		await this.page.locator( EditorSelectors.media.imageInp ).setInputFiles( filePath );
		await this.page.getByRole( 'button', { name: 'Continue' } ).click();
		const enableImportBtn = this.page.getByRole( 'button', { name: 'Enable and Import' } );
		if ( await enableImportBtn.count() > 0 ) {
			await enableImportBtn.click();
		}
		await this.page.getByRole( 'button', { name: 'Insert' } ).first().click();
	}

	/**
	 * Make sure that the elements panel is loaded.
	 *
	 * @return {Promise<void>}
	 */
	async ensurePanelLoaded(): Promise<void> {
		if ( this.isPanelLoaded ) {
			return;
		}

		await this.page.waitForSelector( '.elementor-panel-loading', { state: 'detached' } );
		await this.page.waitForSelector( '#elementor-loading', { state: 'hidden' } );

		this.isPanelLoaded = true;
	}
}