From 77fdcb6f984c0c176dad81350e3c708b4f64a266 Mon Sep 17 00:00:00 2001 From: rg755421 Date: Thu, 1 Oct 2026 14:14:16 +0545 Subject: [PATCH 1/4] Fix - TG: Call to Action button links to an empty href when no link is set Co-Authored-By: Claude Opus 5.5 (1M context) --- inc/widgets/accelerate-call-to-action-widget.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inc/widgets/accelerate-call-to-action-widget.php b/inc/widgets/accelerate-call-to-action-widget.php index a0193fa..16a09c9 100644 --- a/inc/widgets/accelerate-call-to-action-widget.php +++ b/inc/widgets/accelerate-call-to-action-widget.php @@ -76,7 +76,7 @@ function widget( $args, $instance ) { $text_main = empty( $instance['text_main'] ) ? '' : $instance['text_main']; $text_additional = empty( $instance['text_additional'] ) ? '' : $instance['text_additional']; $button_text = isset( $instance[ 'button_text' ] ) ? $instance[ 'button_text' ] : ''; - $button_url = isset( $instance[ 'button_url' ] ) ? $instance[ 'button_url' ] : '#'; + $button_url = empty( $instance['button_url'] ) ? '#' : $instance['button_url']; echo $before_widget; ?> From 9852c54833ddee8f13e5996e0f5c847867bc6476 Mon Sep 17 00:00:00 2001 From: rg755421 Date: Thu, 1 Oct 2026 14:23:49 +0545 Subject: [PATCH 2/4] Add a regression spec for the empty Call to Action link Co-Authored-By: Claude Opus 5.5 (1M context) --- .themegrill-qa/suite.json | 4 + .../e2e/specs/widgets/cta-empty-link.spec.ts | 37 +++++++ tests/e2e/utils/wp.ts | 102 ++++++++++++++++++ 3 files changed, 143 insertions(+) create mode 100644 tests/e2e/specs/widgets/cta-empty-link.spec.ts create mode 100644 tests/e2e/utils/wp.ts diff --git a/.themegrill-qa/suite.json b/.themegrill-qa/suite.json index 188a874..b11c236 100644 --- a/.themegrill-qa/suite.json +++ b/.themegrill-qa/suite.json @@ -40,6 +40,10 @@ "inc/header-functions.php", "inc/customizer.php", "js/skip-link-focus-fix.js" + ], + "widgets": [ + "inc/widgets/**", + "tests/e2e/utils/**" ] } } diff --git a/tests/e2e/specs/widgets/cta-empty-link.spec.ts b/tests/e2e/specs/widgets/cta-empty-link.spec.ts new file mode 100644 index 0000000..fdceac3 --- /dev/null +++ b/tests/e2e/specs/widgets/cta-empty-link.spec.ts @@ -0,0 +1,37 @@ +import { test, expect } from "@playwright/test"; +import { addLegacyWidget, createPage, deletePage, login, removeWidget, requireAdmin } from "../../utils/wp"; + +/** + * @area widgets + * @tier fresh + * @guards accelerate-pro#54 + * @source accelerate-pro#54 (reported by iamprazol) + * @why The widget always saves button_url (as "" when left empty), so the + * isset() fallback to "#" never applied and the button rendered href="", + * which reloads the current page on click. + */ +test("TG: Call to Action button falls back to # when no link is set @widgets @fresh", async ({ page }, testInfo) => { + requireAdmin(testInfo); + test.setTimeout(120_000); + await login(page); + let hostId = 0; + let widget = ""; + try { + const host = await createPage(page, "TGQA call to action host", ""); + hostId = host.id; + widget = await addLegacyWidget(page, "accelerate_call_to_action_widget", "accelerate_right_sidebar", { + text_main: "TGQA call to action", + text_additional: "", + button_text: "Get Started", + button_url: "", + }); + + await page.goto(host.link); + const button = page.locator(`#${widget} a.read-more`); + await expect(button, "the call to action button is not rendered").toBeVisible(); + await expect(button).toHaveAttribute("href", "#"); + } finally { + if (widget) await removeWidget(page, widget); + if (hostId) await deletePage(page, hostId); + } +}); diff --git a/tests/e2e/utils/wp.ts b/tests/e2e/utils/wp.ts new file mode 100644 index 0000000..f1f407e --- /dev/null +++ b/tests/e2e/utils/wp.ts @@ -0,0 +1,102 @@ +import { expect, type Page, type TestInfo } from "@playwright/test"; + +/** + * Shared helpers for specs that need wp-admin. Specs that change site state + * put it back in a finally block. + */ + +export const adminUser = process.env.TGQA_ADMIN_USER ?? ""; +export const adminPass = process.env.TGQA_ADMIN_PASS ?? ""; + +/** Skip, with a reason in the report, when no admin credentials were supplied. */ +export function requireAdmin(testInfo: TestInfo): void { + testInfo.skip( + !adminUser || !adminPass, + "needs TGQA_ADMIN_USER / TGQA_ADMIN_PASS in .themegrill-qa/.env.local", + ); +} + +export async function login(page: Page): Promise { + await page.goto("/wp-login.php"); + await page.locator("#user_login").fill(adminUser); + await page.locator("#user_pass").fill(adminPass); + await page.locator("#wp-submit").click(); + await expect(page.locator("#wpadminbar")).toBeVisible(); +} + +/** A REST nonce for the logged-in session, so page.request can call wp/v2. */ +export async function restNonce(page: Page): Promise { + const res = await page.request.get("/wp-admin/admin-ajax.php?action=rest-nonce"); + expect(res.ok(), "could not get a REST nonce — is the admin logged in?").toBeTruthy(); + return (await res.text()).trim(); +} + +/** Create a published page through the REST API and return its id and link. */ +export async function createPage( + page: Page, + title: string, + content: string, +): Promise<{ id: number; link: string }> { + const nonce = await restNonce(page); + const res = await page.request.post("/?rest_route=/wp/v2/pages", { + headers: { "X-WP-Nonce": nonce }, + data: { title, content, status: "publish" }, + }); + expect(res.ok(), `creating the test page failed: HTTP ${res.status()}`).toBeTruthy(); + const body = await res.json(); + return { id: body.id, link: body.link }; +} + +// POST with a method override rather than DELETE: some hosts (this Local site's +// nginx among them) answer a bare DELETE with 405 before WordPress sees it. +export async function deletePage(page: Page, id: number): Promise { + const nonce = await restNonce(page); + const res = await page.request.post(`/?rest_route=/wp/v2/pages/${id}&force=true`, { + headers: { "X-WP-Nonce": nonce, "X-HTTP-Method-Override": "DELETE" }, + }); + expect(res.ok(), `deleting test page ${id} failed: HTTP ${res.status()}`).toBeTruthy(); +} + +/** Remove a widget through the REST API (method override, as for pages above). */ +export async function removeWidget(page: Page, id: string): Promise { + await page.request.post(`/?rest_route=/wp/v2/widgets/${id}&force=true`, { + headers: { "X-WP-Nonce": await restNonce(page), "X-HTTP-Method-Override": "DELETE" }, + }); +} + +/** + * Add a classic (legacy) widget to a sidebar through the REST API, the way the block Widgets + * editor does: encode the form, create the widget, then place it (over HTTP a new widget + * lands in Inactive Widgets). `fields` are the widget form's field names and values. + */ +export async function addLegacyWidget( + page: Page, + idBase: string, + sidebar: string, + fields: Record, +): Promise { + const headers = { "X-WP-Nonce": await restNonce(page) }; + const form = Object.entries(fields) + .map(([k, v]) => `widget-${idBase}[1][${k}]=${encodeURIComponent(String(v))}`) + .join("&"); + const encoded = await page.request.post(`/?rest_route=/wp/v2/widget-types/${idBase}/encode`, { headers, data: { form_data: form } }); + expect(encoded.ok(), `encoding the ${idBase} widget failed: HTTP ${encoded.status()}`).toBeTruthy(); + const created = await page.request.post("/?rest_route=/wp/v2/widgets", { + headers, + data: { id_base: idBase, sidebar, instance: (await encoded.json()).instance }, + }); + expect(created.ok(), `creating the ${idBase} widget failed: HTTP ${created.status()}`).toBeTruthy(); + const id: string = (await created.json()).id; + try { + const placed = await page.request.post(`/?rest_route=/wp/v2/widgets/${id}`, { + headers: { ...headers, "X-HTTP-Method-Override": "PUT" }, + data: { sidebar }, + }); + expect(placed.ok(), `placing the ${idBase} widget failed: HTTP ${placed.status()}`).toBeTruthy(); + } catch (error) { + // The caller never gets the id, so its cleanup cannot remove this widget. + await removeWidget(page, id); + throw error; + } + return id; +} From 29d342145dcc9e65696a3393980576e47993e530 Mon Sep 17 00:00:00 2001 From: rg755421 Date: Fri, 2 Oct 2026 08:07:49 +0545 Subject: [PATCH 3/4] Report failed test widget cleanup Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/e2e/utils/wp.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/e2e/utils/wp.ts b/tests/e2e/utils/wp.ts index f1f407e..5b25b72 100644 --- a/tests/e2e/utils/wp.ts +++ b/tests/e2e/utils/wp.ts @@ -59,9 +59,10 @@ export async function deletePage(page: Page, id: number): Promise { /** Remove a widget through the REST API (method override, as for pages above). */ export async function removeWidget(page: Page, id: string): Promise { - await page.request.post(`/?rest_route=/wp/v2/widgets/${id}&force=true`, { + const res = await page.request.post(`/?rest_route=/wp/v2/widgets/${id}&force=true`, { headers: { "X-WP-Nonce": await restNonce(page), "X-HTTP-Method-Override": "DELETE" }, }); + expect(res.ok(), `removing test widget ${id} failed: HTTP ${res.status()}`).toBeTruthy(); } /** @@ -95,7 +96,8 @@ export async function addLegacyWidget( expect(placed.ok(), `placing the ${idBase} widget failed: HTTP ${placed.status()}`).toBeTruthy(); } catch (error) { // The caller never gets the id, so its cleanup cannot remove this widget. - await removeWidget(page, id); + // A cleanup failure must not hide the placement error, which is the real cause. + await removeWidget(page, id).catch(() => {}); throw error; } return id; From f31afad27dff2390ac52d8387f44c36e35d05bdd Mon Sep 17 00:00:00 2001 From: rg755421 Date: Fri, 2 Oct 2026 10:13:48 +0545 Subject: [PATCH 4/4] Give the test login helper time for a slow dashboard Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/e2e/utils/wp.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/utils/wp.ts b/tests/e2e/utils/wp.ts index 5b25b72..32e37fd 100644 --- a/tests/e2e/utils/wp.ts +++ b/tests/e2e/utils/wp.ts @@ -21,7 +21,8 @@ export async function login(page: Page): Promise { await page.locator("#user_login").fill(adminUser); await page.locator("#user_pass").fill(adminPass); await page.locator("#wp-submit").click(); - await expect(page.locator("#wpadminbar")).toBeVisible(); + // The dashboard can take several seconds to load on a busy site. + await expect(page.locator("#wpadminbar")).toBeVisible({ timeout: 30_000 }); } /** A REST nonce for the logged-in session, so page.request can call wp/v2. */