mirror of
https://github.com/elementor/hello-theme.git
synced 2026-07-30 15:16:56 +08:00
Some checks failed
Build / Build theme (push) Failing after 1s
Lint / ESLint (push) Failing after 0s
PHPUnit / File Diff (push) Failing after 1s
Lint / PHPCS (push) Failing after 41s
PHPUnit / PHPUnit - WordPress 6.8 - PHP version 7.4 (push) Has been skipped
PHPUnit / PHPUnit - WordPress 6.9 - PHP version 7.4 (push) Has been skipped
PHPUnit / PHPUnit - WordPress latest - PHP version 7.4 (push) Has been skipped
PHPUnit / PHPUnit - WordPress nightly - PHP version 7.4 (push) Has been skipped
PHPUnit / PHPUnit - WordPress 6.8 - PHP version 8.0 (push) Has been skipped
PHPUnit / PHPUnit - WordPress 6.9 - PHP version 8.0 (push) Has been skipped
PHPUnit / PHPUnit - WordPress latest - PHP version 8.0 (push) Has been skipped
PHPUnit / PHPUnit - WordPress nightly - PHP version 8.0 (push) Has been skipped
PHPUnit / PHPUnit - WordPress 6.8 - PHP version 8.1 (push) Has been skipped
PHPUnit / PHPUnit - WordPress 6.9 - PHP version 8.1 (push) Has been skipped
PHPUnit / PHPUnit - WordPress latest - PHP version 8.1 (push) Has been skipped
PHPUnit / PHPUnit - WordPress nightly - PHP version 8.1 (push) Has been skipped
PHPUnit / PHPUnit - WordPress 6.8 - PHP version 8.2 (push) Has been skipped
PHPUnit / PHPUnit - WordPress 6.9 - PHP version 8.2 (push) Has been skipped
PHPUnit / PHPUnit - WordPress latest - PHP version 8.2 (push) Has been skipped
PHPUnit / PHPUnit - WordPress nightly - PHP version 8.2 (push) Has been skipped
PHPUnit / PHPUnit - WordPress 6.8 - PHP version 8.3 (push) Has been skipped
PHPUnit / PHPUnit - WordPress 6.9 - PHP version 8.3 (push) Has been skipped
PHPUnit / PHPUnit - WordPress latest - PHP version 8.3 (push) Has been skipped
PHPUnit / PHPUnit - WordPress nightly - PHP version 8.3 (push) Has been skipped
PHPUnit / PHPUnit - Test Results (push) Successful in 2s
* Setup prettier * Fix lint/format issues
1734 lines
46 KiB
TypeScript
1734 lines
46 KiB
TypeScript
import { readFile } from 'fs/promises';
|
|
import { addElement, getElementSelector } from '../assets/elements-utils.ts';
|
|
import {
|
|
expect,
|
|
type Page,
|
|
type Frame,
|
|
type TestInfo,
|
|
type ElementHandle,
|
|
Locator,
|
|
} from '@playwright/test';
|
|
import BasePage from './base-page.ts';
|
|
import EditorSelectors from '../selectors/editor-selectors.ts';
|
|
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.ts';
|
|
import TopBarSelectors, {
|
|
TopBarSelector,
|
|
} from '../selectors/top-bar-selectors.ts';
|
|
import Breakpoints from '../assets/breakpoints.ts';
|
|
import { timeouts } from '../config/timeouts.ts';
|
|
|
|
let $e: $eType;
|
|
let elementor: ElementorType;
|
|
let Backbone: BackboneType;
|
|
let window: WindowType;
|
|
|
|
export default class EditorPage extends BasePage {
|
|
readonly previewFrame: Frame;
|
|
postId: number;
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
|
|
/**
|
|
* 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"]')
|
|
.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} .${icon}`).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'));
|
|
}
|
|
|
|
/**
|
|
* 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> {
|
|
const hasTopBar = await this.hasTopBar();
|
|
|
|
await this.publishPage();
|
|
|
|
if (hasTopBar) {
|
|
await this.clickTopBarItem(TopBarSelectors.saveOptions);
|
|
await this.page.getByRole('menuitem', { name: 'View Page' }).click();
|
|
const pageId = await this.getPageId();
|
|
await this.page.goto(`/?p=${pageId}`);
|
|
} else {
|
|
await this.openMenuPanel('view-page');
|
|
}
|
|
|
|
await this.page.waitForLoadState();
|
|
}
|
|
|
|
async viewPage() {
|
|
const pageId = await this.getPageId();
|
|
await this.page.goto(`/?p=${pageId}`);
|
|
await this.page.waitForLoadState();
|
|
}
|
|
|
|
/**
|
|
* 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> {
|
|
return await this.page.evaluate(() => elementor.config.initialDocument.id);
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
}
|