diff --git a/src/features/enable-all-features.feature b/src/features/enable-all-features.feature index 9468b9db..3de654cf 100644 --- a/src/features/enable-all-features.feature +++ b/src/features/enable-all-features.feature @@ -9,6 +9,7 @@ Feature: C1205 - Enabling all WP Rocket features should not throw any fatal erro Scenario: Enable all features When I go to 'wp-admin/options-general.php?page=wprocket#dashboard' And I enable all settings + # And I enable all settings with text inputs And I log out Then page loads successfully When I log in diff --git a/src/support/steps/general.ts b/src/support/steps/general.ts index eb52fdd8..4d8f34fc 100644 --- a/src/support/steps/general.ts +++ b/src/support/steps/general.ts @@ -245,6 +245,13 @@ When('I enable all settings', async function (this: ICustomWorld) { await this.utils.enableAllOptions(); }); +/** + * Executes the step to enable all settings and validates text inputs. + */ +When('I enable all settings with text inputs', async function (this: ICustomWorld) { + await this.utils.enableAllOptionsWithTextInputs(); +}); + /** * Executes the step to log out. */ @@ -514,4 +521,4 @@ Then('page navigated to the new page {string}', async function (this: ICustomWor const url = `${WP_BASE_URL}/${path}`; const regex = new RegExp(url); await expect(this.page).toHaveURL(regex); -}); \ No newline at end of file +}); diff --git a/utils/page-utils.ts b/utils/page-utils.ts index a78119b5..ebf07a33 100644 --- a/utils/page-utils.ts +++ b/utils/page-utils.ts @@ -8,9 +8,9 @@ * @requires {@link ../config/wp.config} * @requires {@link ./configurations} */ -import type {Page} from '@playwright/test'; +import type { Dialog, Page } from '@playwright/test'; import type {Sections} from '../src/common/sections'; -import type {Locators, Selector, Pickle} from './types'; +import type {Locators, Selector, Pickle, SectionId} from './types'; import {expect} from "@playwright/test"; import { ICustomWorld } from '../src/common/custom-world'; import fs from "fs/promises"; @@ -567,6 +567,151 @@ export class PageUtils { } + /** + * Enables all options and ensures every visible text input accepts arbitrary values without native validation dialogs. + * + * @return {Promise} + */ + public enableAllOptionsWithTextInputs = async (): Promise => { + await this.enableAllOptions(); + await this.validateTextInputsWithoutDialogs(); + } + + /** + * Iterates over every visible text field in each section, types a test value, presses Enter, + * and fails if a browser dialog appears (e.g. "Please enter a valid URL"). + * + * The original field values are restored after each check, and form submissions are suppressed to avoid unwanted saves. + * + * @param {string} testValue - Value to use while validating the inputs. + * + * @return {Promise} + */ + private validateTextInputsWithoutDialogs = async (testValue: string = 'test'): Promise => { + const sectionIds: SectionId[] = [ + 'dashboard', + 'cache', + 'file_optimization', + 'media', + 'preload', + 'advanced_cache', + 'database', + 'page_cdn', + 'heartbeat', + 'addons', + ]; + const allowedInputTypes = new Set(['', 'text', 'search', 'url']); + const dialogMessages: string[] = []; + let activeField = ''; + + await this.gotoWpr(); + await this.preventSettingsFormSubmission(); + + const handleDialog = async (dialog: Dialog): Promise => { + dialogMessages.push(`${dialog.message()}${activeField ? ` (Field: ${activeField})` : ''}`); + await dialog.dismiss(); + }; + + this.page.on('dialog', handleDialog); + + try { + for (const sectionId of sectionIds) { + const navLocator = this.page.locator(`#wpr-nav-${sectionId}`); + if (!await navLocator.isVisible()) { + continue; + } + + await navLocator.scrollIntoViewIfNeeded(); + await navLocator.click(); + await this.page.locator(`#${sectionId}`).waitFor({ state: 'visible' }); + + const textInputs = this.page + .locator(`#${sectionId}`) + .locator('textarea, input[type="text"], input[type="search"], input[type="url"], input:not([type])'); + + const fieldCount = await textInputs.count(); + + for (let index = 0; index < fieldCount; index++) { + const field = textInputs.nth(index); + + if (!await field.isVisible() || await field.isDisabled() || !await field.isEditable()) { + continue; + } + + const tagName = await field.evaluate((el) => el.tagName.toLowerCase()); + if (tagName === 'input') { + const typeAttr = (await field.getAttribute('type'))?.toLowerCase() ?? ''; + if (!allowedInputTypes.has(typeAttr)) { + continue; + } + } + + const fieldId = await field.getAttribute('id'); + const fieldName = await field.getAttribute('name'); + activeField = `${sectionId}:${fieldId ?? fieldName ?? `field-${index}`}`; + + const originalValue = await field.inputValue(); + + await field.fill(testValue); + await field.press('Enter', { noWaitAfter: true }); + await this.page.waitForTimeout(100); + await field.fill(originalValue); + } + } + } finally { + this.page.off('dialog', handleDialog); + activeField = ''; + await this.allowSettingsFormSubmission(); + } + + expect( + dialogMessages, + `Unexpected validation dialog(s) triggered while interacting with text inputs: ${dialogMessages.join(' | ')}` + ).toHaveLength(0); + } + + /** + * Prevents the WP Rocket settings form from submitting while we simulate Enter presses on inputs. + * + * @return {Promise} + */ + private preventSettingsFormSubmission = async (): Promise => { + await this.page.waitForSelector('form[id$="_options"]'); + await this.page.evaluate(() => { + const form = document.querySelector('form[id$="_options"]'); + const win = window as typeof window & { wprSubmitInterceptor?: (event: Event) => void }; + + if (!form || win.wprSubmitInterceptor) { + return; + } + + win.wprSubmitInterceptor = (event: Event): void => { + event.preventDefault(); + }; + + form.addEventListener('submit', win.wprSubmitInterceptor, true); + }); + } + + /** + * Restores the default submit behavior for the WP Rocket settings form. + * + * @return {Promise} + */ + private allowSettingsFormSubmission = async (): Promise => { + await this.page.evaluate(() => { + const form = document.querySelector('form[id$="_options"]'); + const win = window as typeof window & { wprSubmitInterceptor?: (event: Event) => void }; + + if (!form || !win.wprSubmitInterceptor) { + return; + } + + form.removeEventListener('submit', win.wprSubmitInterceptor, true); + delete win.wprSubmitInterceptor; + }); + } + /** * Performs setting import action in WP Rocket. * @@ -794,4 +939,4 @@ export class PageUtils { await scrollPage; }); } -} \ No newline at end of file +}