From 5eb271d4660596ae31e462ecb5bbaa813bbbaebc Mon Sep 17 00:00:00 2001 From: Netanel Baba Date: Wed, 19 Aug 2026 11:46:47 +0300 Subject: [PATCH 1/2] fix: Prevent conversion banner placeholder from shifting admin buttons Mount the conversion banner in JavaScript only when welcome config exists, place it after the page title action by default, and add Playwright regression tests for suppressed and visible placement flows. Ref: ED-25235 Co-authored-by: Cursor --- .../js/hello-elementor-conversion-banner.js | 113 ++++++++++++---- .../assets/js/providers/admin-provider.js | 14 +- .../components/conversion-banner.php | 11 -- .../tests/conversion-banner-layout.test.ts | 128 ++++++++++++++++++ 4 files changed, 226 insertions(+), 40 deletions(-) create mode 100644 tests/playwright/tests/conversion-banner-layout.test.ts diff --git a/modules/admin-home/assets/js/hello-elementor-conversion-banner.js b/modules/admin-home/assets/js/hello-elementor-conversion-banner.js index 35e6efcd..e2ffa480 100644 --- a/modules/admin-home/assets/js/hello-elementor-conversion-banner.js +++ b/modules/admin-home/assets/js/hello-elementor-conversion-banner.js @@ -1,13 +1,18 @@ +import apiFetch from '@wordpress/api-fetch'; import { createRoot } from 'react-dom/client'; +import { useLayoutEffect } from 'react'; import { ThemeProvider } from '@elementor/ui/styles'; import { Welcome } from './components/paper/welcome'; import { AdminProvider } from './providers/admin-provider'; -import Box from '@elementor/ui/Box'; -const App = () => { +const App = ({ config, container }) => { + useLayoutEffect(() => { + container.style.visibility = 'visible'; + }, [container]); + return ( - + { ); }; -document.addEventListener('DOMContentLoaded', () => { - const container = document.getElementById('ehe-admin-cb'); - - if (container) { - const { beforeWrap = false } = window.ehe_cb; - const { selector, before = false } = window.ehe_cb.data; - const headerEnd = document.querySelector(selector); - - if (headerEnd) { - if (beforeWrap) { - const wrapElement = document.querySelector('.wrap'); - if (wrapElement) { - wrapElement.insertAdjacentElement('beforebegin', container); - } - } else if (before) { - headerEnd.insertAdjacentElement('beforebegin', container); - } else { - headerEnd.insertAdjacentElement('afterend', container); - } +const insertBanner = (container, placement) => { + const { beforeWrap = false, selector, before = false } = placement; + + if (beforeWrap) { + const wrapElement = document.querySelector('.wrap'); + + if (!wrapElement) { + return false; + } + + wrapElement.insertAdjacentElement('beforebegin', container); + return true; + } + + if (before) { + const anchor = document.querySelector(selector); + + if (!anchor) { + return false; } - const root = createRoot(container); - root.render(); + anchor.insertAdjacentElement('beforebegin', container); + return true; + } + + const pageTitleAction = document.querySelector('.wrap .page-title-action'); + + if (pageTitleAction) { + pageTitleAction.insertAdjacentElement('afterend', container); + return true; + } + + const headerEnd = document.querySelector(selector); + + if (!headerEnd) { + return false; + } + + headerEnd.insertAdjacentElement('afterend', container); + return true; +}; + +const init = async () => { + if ('undefined' === typeof window.ehe_cb) { + return; + } + + let config; + + try { + const response = await apiFetch({ + path: '/elementor-hello-elementor/v1/admin-settings', + }); + config = response.config; + } catch (e) { + return; + } + + if (!config?.welcome?.title) { + return; } -}); + + const { beforeWrap = false } = window.ehe_cb; + const { selector, before = false } = window.ehe_cb.data; + + const container = document.createElement('div'); + container.id = 'ehe-admin-cb'; + container.className = 'ehe-admin-cb'; + container.style.visibility = 'hidden'; + + if (!insertBanner(container, { beforeWrap, selector, before })) { + return; + } + + const root = createRoot(container); + root.render(); +}; + +if ('loading' === document.readyState) { + document.addEventListener('DOMContentLoaded', init); +} else { + init(); +} diff --git a/modules/admin-home/assets/js/providers/admin-provider.js b/modules/admin-home/assets/js/providers/admin-provider.js index b5f929d4..55646a7a 100644 --- a/modules/admin-home/assets/js/providers/admin-provider.js +++ b/modules/admin-home/assets/js/providers/admin-provider.js @@ -3,11 +3,17 @@ import apiFetch from '@wordpress/api-fetch'; export const AdminContext = createContext(); -export const AdminProvider = ({ children }) => { - const [isLoading, setIsLoading] = React.useState(true); - const [adminSettings, setAdminSettings] = React.useState({}); +export const AdminProvider = ({ children, initialSettings = null }) => { + const [isLoading, setIsLoading] = React.useState(!initialSettings); + const [adminSettings, setAdminSettings] = React.useState( + initialSettings || {}, + ); useEffect(() => { + if (initialSettings) { + return; + } + apiFetch({ path: '/elementor-hello-elementor/v1/admin-settings' }) .then((settings) => { setAdminSettings(settings.config); @@ -15,7 +21,7 @@ export const AdminProvider = ({ children }) => { .finally(() => { setIsLoading(false); }); - }, []); + }, [initialSettings]); return ( -
-
- [ 'selector' => '#wpbody #wpbody-content .wrap h1' ], @@ -199,10 +192,6 @@ public function __construct() { return; } - add_action( 'in_admin_header', function () { - $this->render_conversion_banner(); - }, 11 ); - add_action( 'admin_enqueue_scripts', function () use ( $conversion_banner_active ) { $this->enqueue_scripts( $conversion_banner_active ); } ); diff --git a/tests/playwright/tests/conversion-banner-layout.test.ts b/tests/playwright/tests/conversion-banner-layout.test.ts new file mode 100644 index 00000000..ee519de7 --- /dev/null +++ b/tests/playwright/tests/conversion-banner-layout.test.ts @@ -0,0 +1,128 @@ +import { parallelTest as test } from '../parallelTest.ts'; +import { expect } from '@playwright/test'; +import WpAdminPage from '../pages/wp-admin-page.ts'; +import { timeouts } from '../config/timeouts.ts'; + +test.describe('Conversion banner flows [ED-25235]', () => { + test('Pages list does not mount banner when Elementor suppresses welcome config', async ({ + page, + apiRequests, + }, testInfo) => { + const wpAdmin = new WpAdminPage(page, testInfo, apiRequests); + + await wpAdmin.login(); + + await page.goto('/wp-admin/edit.php?post_type=page', { + waitUntil: 'networkidle', + timeout: timeouts.longAction, + }); + + await page.waitForSelector('.wrap h1.wp-heading-inline', { + timeout: timeouts.longAction, + }); + + await page.waitForTimeout(timeouts.longAction / 5); + + await expect(page.locator('#ehe-admin-cb')).toHaveCount(0); + + const headingBox = await page + .locator('.wrap h1.wp-heading-inline') + .boundingBox(); + const actionBox = await page + .locator('.wrap .page-title-action') + .first() + .boundingBox(); + + expect(headingBox).not.toBeNull(); + expect(actionBox).not.toBeNull(); + + if (headingBox && actionBox) { + expect(actionBox.y).toBeLessThan(headingBox.y + headingBox.height + 8); + } + }); + + test('Hello settings page still loads without conversion banner regressions', async ({ + page, + apiRequests, + }, testInfo) => { + const wpAdmin = new WpAdminPage(page, testInfo, apiRequests); + + await wpAdmin.login(); + + await page.goto('/wp-admin/admin.php?page=hello-elementor-settings', { + waitUntil: 'networkidle', + timeout: timeouts.longAction, + }); + + await expect(page.locator('#ehe-admin-settings')).toBeVisible({ + timeout: timeouts.longAction, + }); + + const bannerCount = await page.locator('#ehe-admin-cb').count(); + + if (bannerCount > 0) { + const action = page.locator('.wrap .page-title-action').first(); + const banner = page.locator('#ehe-admin-cb'); + + if ((await action.count()) > 0) { + const isAfterAction = await page.evaluate(() => { + const actionNode = document.querySelector('.wrap .page-title-action'); + const bannerNode = document.getElementById('ehe-admin-cb'); + + return Boolean( + actionNode && + bannerNode && + actionNode.nextElementSibling === bannerNode, + ); + }); + + expect(isAfterAction).toBe(true); + } + } + }); + + test('Plugins page keeps native title row when banner is absent', async ({ + page, + apiRequests, + }, testInfo) => { + const wpAdmin = new WpAdminPage(page, testInfo, apiRequests); + + await wpAdmin.login(); + + await page.goto('/wp-admin/plugins.php', { + waitUntil: 'networkidle', + timeout: timeouts.longAction, + }); + + await page.waitForSelector('.wrap h1', { + timeout: timeouts.longAction, + }); + + await page.waitForTimeout(timeouts.longAction / 5); + + const banner = page.locator('#ehe-admin-cb'); + const bannerCount = await banner.count(); + + if (bannerCount > 0) { + const heading = page.locator('.wrap h1').first(); + const action = page.locator('.wrap .page-title-action').first(); + + if ((await action.count()) > 0) { + const isAfterAction = await page.evaluate(() => { + const actionNode = document.querySelector('.wrap .page-title-action'); + const bannerNode = document.getElementById('ehe-admin-cb'); + + return Boolean( + actionNode && + bannerNode && + actionNode.nextElementSibling === bannerNode, + ); + }); + + expect(isAfterAction).toBe(true); + } + + await expect(heading).toBeVisible(); + } + }); +}); From 918d3769686995f31c456e974ef28917917cf9f3 Mon Sep 17 00:00:00 2001 From: Netanel Baba Date: Wed, 19 Aug 2026 12:18:55 +0300 Subject: [PATCH 2/2] test: Mock empty welcome REST in conversion banner Playwright test CI uses free Elementor without guaranteed Go Pro suppression on list tables, so stub admin-settings to assert no DOM mount deterministically. Ref: ED-25235 Co-authored-by: Cursor --- .../tests/conversion-banner-layout.test.ts | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/playwright/tests/conversion-banner-layout.test.ts b/tests/playwright/tests/conversion-banner-layout.test.ts index ee519de7..efcd4ff1 100644 --- a/tests/playwright/tests/conversion-banner-layout.test.ts +++ b/tests/playwright/tests/conversion-banner-layout.test.ts @@ -3,11 +3,32 @@ import { expect } from '@playwright/test'; import WpAdminPage from '../pages/wp-admin-page.ts'; import { timeouts } from '../config/timeouts.ts'; +const emptyWelcomeAdminSettings = { + config: { + welcome: [], + config: { + nonceInstall: 'test-nonce', + slug: 'elementor', + }, + }, +}; + test.describe('Conversion banner flows [ED-25235]', () => { - test('Pages list does not mount banner when Elementor suppresses welcome config', async ({ + test('Pages list does not mount banner when welcome config is empty', async ({ page, apiRequests, }, testInfo) => { + await page.route( + '**/elementor-hello-elementor/v1/admin-settings**', + async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(emptyWelcomeAdminSettings), + }); + }, + ); + const wpAdmin = new WpAdminPage(page, testInfo, apiRequests); await wpAdmin.login();