From bff470a740337c02030d4c3a44d7b50ae5ff97a5 Mon Sep 17 00:00:00 2001 From: Alex Worrad-Andrews Date: Thu, 1 Oct 2026 16:45:24 +0100 Subject: [PATCH 01/14] Add an income guidance flag that add-ons can switch on --- packages/join-block/src/Settings.php | 6 +++ .../tests/SettingsIncomeGuidanceTest.php | 52 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 packages/join-block/tests/SettingsIncomeGuidanceTest.php diff --git a/packages/join-block/src/Settings.php b/packages/join-block/src/Settings.php index 78520a7..d2a53a8 100644 --- a/packages/join-block/src/Settings.php +++ b/packages/join-block/src/Settings.php @@ -679,6 +679,12 @@ private static function describePlanRedirectCheckboxes() } } + // Income guidance is off unless an add-on switches it on. + public static function isIncomeGuidanceEnabled() + { + return (bool) apply_filters('ck_join_flow_income_guidance_enabled', false); + } + // Names the CRMs this site uses, so admins can see where redirected people // will not appear. public static function getMembershipPlanRedirectHelpText() diff --git a/packages/join-block/tests/SettingsIncomeGuidanceTest.php b/packages/join-block/tests/SettingsIncomeGuidanceTest.php new file mode 100644 index 0000000..1c03b64 --- /dev/null +++ b/packages/join-block/tests/SettingsIncomeGuidanceTest.php @@ -0,0 +1,52 @@ +assertFalse(Settings::isIncomeGuidanceEnabled()); + } + + public function test_an_add_on_can_switch_it_on() + { + Monkey\Filters\expectApplied('ck_join_flow_income_guidance_enabled') + ->once() + ->with(false) + ->andReturn(true); + + $this->assertTrue(Settings::isIncomeGuidanceEnabled()); + } + + public function test_a_truthy_filter_result_counts_as_on() + { + Monkey\Filters\expectApplied('ck_join_flow_income_guidance_enabled')->andReturn('1'); + + $this->assertTrue(Settings::isIncomeGuidanceEnabled()); + } +} From 07b83ff1be91c97a7d002ee6a4cc678525cda5c9 Mon Sep 17 00:00:00 2001 From: Alex Worrad-Andrews Date: Thu, 1 Oct 2026 16:51:26 +0100 Subject: [PATCH 02/14] Let e2e tests switch income guidance on and off --- packages/join-e2e/.wp-env.json | 3 ++- packages/join-e2e/mu-plugins/ck-e2e-income-guidance.php | 9 +++++++++ packages/join-e2e/scripts/setup.php | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 packages/join-e2e/mu-plugins/ck-e2e-income-guidance.php diff --git a/packages/join-e2e/.wp-env.json b/packages/join-e2e/.wp-env.json index 81a4016..f72c874 100644 --- a/packages/join-e2e/.wp-env.json +++ b/packages/join-e2e/.wp-env.json @@ -2,7 +2,8 @@ "core": "https://wordpress.org/wordpress-6.9.4.zip", "plugins": ["../../packages/join-block"], "mappings": { - "wp-content/e2e-scripts": "../../packages/join-e2e/scripts" + "wp-content/e2e-scripts": "../../packages/join-e2e/scripts", + "wp-content/mu-plugins/ck-e2e-income-guidance.php": "../../packages/join-e2e/mu-plugins/ck-e2e-income-guidance.php" }, "config": { "WP_DEBUG": true, diff --git a/packages/join-e2e/mu-plugins/ck-e2e-income-guidance.php b/packages/join-e2e/mu-plugins/ck-e2e-income-guidance.php new file mode 100644 index 0000000..731666e --- /dev/null +++ b/packages/join-e2e/mu-plugins/ck-e2e-income-guidance.php @@ -0,0 +1,9 @@ + Date: Thu, 1 Oct 2026 16:53:14 +0100 Subject: [PATCH 03/14] Share the admin and plan-picking e2e helpers --- .../tests/admin-plan-redirect.spec.ts | 68 ++++--------------- packages/join-e2e/tests/helpers.ts | 54 ++++++++++++++- packages/join-e2e/tests/plan-redirect.spec.ts | 10 +-- 3 files changed, 68 insertions(+), 64 deletions(-) diff --git a/packages/join-e2e/tests/admin-plan-redirect.spec.ts b/packages/join-e2e/tests/admin-plan-redirect.spec.ts index 93caeaf..96ba3bf 100644 --- a/packages/join-e2e/tests/admin-plan-redirect.spec.ts +++ b/packages/join-e2e/tests/admin-plan-redirect.spec.ts @@ -1,7 +1,15 @@ -import { test, expect, Locator, Page } from '@playwright/test'; -import { execSync } from 'child_process'; -import path from 'path'; -import { loginAsAdmin, mockRestEndpoints, CONTINUE } from './helpers'; +import { test, expect, Locator } from '@playwright/test'; +import { + choosePlan, + field, + fillPlan, + loginAsAdmin, + mockRestEndpoints, + openNewPageWithJoinBlock, + reseed, + SETTINGS_PAGE, + wpCli, +} from './helpers'; /** * Configuring a redirecting plan through wp-admin, end to end @@ -16,35 +24,12 @@ import { loginAsAdmin, mockRestEndpoints, CONTINUE } from './helpers'; * the seed is re-run afterwards to put them back for the specs that follow. */ -function wpCli(command: string): void { - execSync(`npx wp-env run tests-cli ${command}`, { cwd: path.resolve(__dirname, '..'), stdio: 'ignore' }); -} - -function reseed(): void { - wpCli('wp eval-file /var/www/html/wp-content/e2e-scripts/setup.php'); -} - -const SETTINGS_PAGE = '/wp-admin/admin.php?page=crb_carbon_fields_container_ck_join_flow.php'; const GLOBAL_PLANS_PAGE = '/e2e-global-plans-join/'; const REDIRECT_TARGET = '/e2e-redirect-target/'; const REDIRECT_TARGET_TITLE = 'E2E Redirect Target'; const REDIRECT_TARGET_COPY = 'You have been redirected here instead of paying.'; const REDIRECT_CHECKBOX = 'Redirect to a page instead of taking payment'; -/** A Carbon Fields field inside `scope`, matched on its exact label. Section headings are skipped. */ -function field(scope: Locator, label: string): Locator { - return scope - .locator('.cf-field:not(.cf-separator)') - .filter({ has: scope.page().locator('.cf-field__label', { hasText: new RegExp(`^${label}\\*?$`) }) }) - .first(); -} - -/** Fills in a plan's name and price. "Price" alone would also match "Price Point ID". */ -async function fillPlan(plan: Locator, name: string, price: string): Promise { - await field(plan, 'Name').locator('input').fill(name); - await field(plan, 'Price').locator('input').fill(price); -} - function redirectPicker(plan: Locator): Locator { return field(plan, 'Page to redirect to'); } @@ -65,31 +50,6 @@ function selectedPages(picker: Locator): Locator { return picker.locator('.cf-association__col').nth(1); } -/** Opens a new, unsaved page in the block editor with a CK Join Form block on it. */ -async function openNewPageWithJoinBlock(page: Page): Promise { - await page.goto('/wp-admin/post-new.php?post_type=page'); - await page.waitForFunction(() => (window as any).wp?.data?.select('core/editor')); - - // New pages open with WordPress's starter pattern picker over the editor. - const patterns = page.getByRole('dialog', { name: 'Choose a pattern' }); - if (await patterns.waitFor({ state: 'visible', timeout: 5000 }).then(() => true, () => false)) { - await patterns.getByRole('button', { name: 'Close' }).click(); - } - - await page.evaluate(() => { - const wp = (window as any).wp; - wp.data.dispatch('core/editor').editPost({ title: 'E2E Block Editor Redirect' }); - wp.data.dispatch('core/block-editor').insertBlocks(wp.blocks.createBlock('carbon-fields/ck-join-form')); - }); -} - -async function choosePlan(page: Page, label: string): Promise { - await page.locator(CONTINUE).click(); - await page.waitForSelector('[role="radiogroup"]'); - await page.locator('label.radio-panel', { has: page.locator(`[id="membership-${label}"]`) }).click(); - await page.locator(CONTINUE).click(); -} - test.describe('Global plans on the Join settings page', () => { test.describe.configure({ mode: 'serial' }); @@ -194,7 +154,7 @@ test.describe('Redirect help text', () => { await expect(checkbox).toContainText(NOT_RECORDED); // The block's own plans are built separately, so check them too. - await openNewPageWithJoinBlock(page); + await openNewPageWithJoinBlock(page, 'E2E Block Editor Redirect'); const plansField = field(page.locator('[data-type="carbon-fields/ck-join-form"]'), 'Custom Membership Plans'); await plansField.locator('.cf-complex__inserter-button').click(); await expect( @@ -209,7 +169,7 @@ test.describe('Redirect help text', () => { test.describe('Custom plans in the CK Join Form block', () => { test('a redirecting plan set up in the block editor redirects on the published page', async ({ page }) => { await loginAsAdmin(page); - await openNewPageWithJoinBlock(page); + await openNewPageWithJoinBlock(page, 'E2E Block Editor Redirect'); const block = page.locator('[data-type="carbon-fields/ck-join-form"]'); await chooseRedirectTarget(field(block, 'Page to redirect to after joining')); diff --git a/packages/join-e2e/tests/helpers.ts b/packages/join-e2e/tests/helpers.ts index a1eebd9..08c3ccc 100644 --- a/packages/join-e2e/tests/helpers.ts +++ b/packages/join-e2e/tests/helpers.ts @@ -1,4 +1,6 @@ -import { Page, expect } from '@playwright/test'; +import { Locator, Page, expect } from '@playwright/test'; +import { execSync } from 'child_process'; +import path from 'path'; export const SAVED_STATE_KEY = 'ck_join_state_flow'; export const CONTINUE = 'button[type="submit"]:has-text("Continue")'; @@ -120,3 +122,53 @@ export async function loginAsAdmin(page: Page): Promise { await page.goto('/wp-admin/', { waitUntil: 'domcontentloaded' }); await expect(page.locator('#wpadminbar')).toBeVisible(); } + +export const SETTINGS_PAGE = '/wp-admin/admin.php?page=crb_carbon_fields_container_ck_join_flow.php'; + +export function wpCli(command: string): void { + execSync(`npx wp-env run tests-cli ${command}`, { cwd: path.resolve(__dirname, '..'), stdio: 'ignore' }); +} + +export function reseed(): void { + wpCli('wp eval-file /var/www/html/wp-content/e2e-scripts/setup.php'); +} + +/** A Carbon Fields field inside `scope`, matched on its exact label. Section headings are skipped. */ +export function field(scope: Locator, label: string): Locator { + return scope + .locator('.cf-field:not(.cf-separator)') + .filter({ has: scope.page().locator('.cf-field__label', { hasText: new RegExp(`^${label}\\*?$`) }) }) + .first(); +} + +/** Fills in a plan's name and price. "Price" alone would also match "Price Point ID". */ +export async function fillPlan(plan: Locator, name: string, price: string): Promise { + await field(plan, 'Name').locator('input').fill(name); + await field(plan, 'Price').locator('input').fill(price); +} + +/** Opens a new, unsaved page in the block editor with a CK Join Form block on it. */ +export async function openNewPageWithJoinBlock(page: Page, title: string): Promise { + await page.goto('/wp-admin/post-new.php?post_type=page'); + await page.waitForFunction(() => (window as any).wp?.data?.select('core/editor')); + + // New pages open with WordPress's starter pattern picker over the editor. + const patterns = page.getByRole('dialog', { name: 'Choose a pattern' }); + if (await patterns.waitFor({ state: 'visible', timeout: 5000 }).then(() => true, () => false)) { + await patterns.getByRole('button', { name: 'Close' }).click(); + } + + await page.evaluate((title) => { + const wp = (window as any).wp; + wp.data.dispatch('core/editor').editPost({ title }); + wp.data.dispatch('core/block-editor').insertBlocks(wp.blocks.createBlock('carbon-fields/ck-join-form')); + }, title); +} + +/** Continues from the details step, picks the plan by its label, and continues again. */ +export async function choosePlan(page: Page, label: string): Promise { + await page.locator(CONTINUE).click(); + await page.waitForSelector('[role="radiogroup"]'); + await page.locator('label.radio-panel', { has: page.locator(`[id="membership-${label}"]`) }).click(); + await page.locator(CONTINUE).click(); +} diff --git a/packages/join-e2e/tests/plan-redirect.spec.ts b/packages/join-e2e/tests/plan-redirect.spec.ts index 2e4e314..af773a9 100644 --- a/packages/join-e2e/tests/plan-redirect.spec.ts +++ b/packages/join-e2e/tests/plan-redirect.spec.ts @@ -1,5 +1,5 @@ import { test, expect, Page } from '@playwright/test'; -import { mockRestEndpoints, CONTINUE, SAVED_STATE_KEY } from './helpers'; +import { choosePlan, mockRestEndpoints, CONTINUE, SAVED_STATE_KEY } from './helpers'; /** * Membership plans that redirect instead of taking payment @@ -25,14 +25,6 @@ async function readEnv(page: Page): Promise> { return page.evaluate(() => JSON.parse(document.getElementById('env')!.textContent || '{}')); } -async function choosePlan(page: Page, label: string): Promise { - await page.locator(CONTINUE).click(); - await page.waitForSelector('[role="radiogroup"]'); - // Each plan's radio is given the id "membership-