diff --git a/packages/join-flow/scss/theme.scss b/packages/join-flow/scss/theme.scss index 2bc29efe..2664d4bf 100644 --- a/packages/join-flow/scss/theme.scss +++ b/packages/join-flow/scss/theme.scss @@ -8,10 +8,10 @@ } $black: var(--wp--preset--color--black, #212529); -$gray: var(--ck-join-form-gray-color, --wp--preset--color--cyan-bluish-gray, #dfdcda); -$background: var(--ck-join-form-background-color, --wp--preset--color--base, #f4f1ee); +$gray: var(--ck-join-form-gray-color, var(--wp--preset--color--cyan-bluish-gray, #dfdcda)); +$background: var(--ck-join-form-background-color, var(--wp--preset--color--base, #f4f1ee)); -$primary: var(--ck-join-form-primary-color, --wp--preset--color--accent, #007bff); +$primary: var(--ck-join-form-primary-color, var(--wp--preset--color--accent, #007bff)); $primary-focus: native-rgb(from $primary r g b, 0.6); $danger: var(--wp--preset--color--accent-5, #e3220c); diff --git a/packages/join-flow/src/components/atoms.tsx b/packages/join-flow/src/components/atoms.tsx index 94a62d83..e58ee1b9 100644 --- a/packages/join-flow/src/components/atoms.tsx +++ b/packages/join-flow/src/components/atoms.tsx @@ -142,6 +142,14 @@ export const PlanRadioPanel: FC = ({ }); }; + // Typing a custom amount must select this tier, otherwise the amount + // is ignored on submit and the default plan price is charged. + const onChangeCustomAmount = () => { + if (!checked) { + onChange(currentPlan.value); + } + }; + return ( = ({ min={currentPlan.amount || 0.01} step="0.01" max="1000" + onChange={onChangeCustomAmount} /> @@ -231,6 +240,7 @@ export const PlanRadioPanel: FC = ({ min={currentPlan.amount || 0.01} step="0.01" max="1000" + onChange={onChangeCustomAmount} /> diff --git a/packages/join-flow/src/pages/plan.page.test.tsx b/packages/join-flow/src/pages/plan.page.test.tsx new file mode 100644 index 00000000..b155ac23 --- /dev/null +++ b/packages/join-flow/src/pages/plan.page.test.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { PlanPage } from './plan.page'; + +jest.mock('../env', () => ({ + get: jest.fn(), + getStr: jest.fn(() => ''), +})); + +jest.mock('../components/summary', () => ({ + Summary: () => null, +})); + +import { get as getEnv } from '../env'; +const mockGetEnv = getEnv as jest.Mock; + +const mockOnCompleted = jest.fn(); + +const MOCK_PLANS = [ + { value: 'higher', label: 'Higher income', description: '', amount: '20', currency: 'GBP', frequency: 'monthly', allowCustomAmount: false }, + { value: 'lower', label: 'Lower income', description: '', amount: '5', currency: 'GBP', frequency: 'monthly', allowCustomAmount: false }, + { value: 'other', label: 'Other', description: '', amount: '1', currency: 'GBP', frequency: 'monthly', allowCustomAmount: true }, +]; + +beforeEach(() => { + mockGetEnv.mockImplementation((key: string) => { + if (key === 'MEMBERSHIP_PLANS') return MOCK_PLANS; + return false; + }); + mockOnCompleted.mockClear(); +}); + +describe('PlanPage — custom amount', () => { + test('typing a custom amount selects that tier instead of the default', async () => { + render(); + + const customInput = document.getElementById('other-amount') as HTMLInputElement; + expect(customInput).toBeInTheDocument(); + + fireEvent.change(customInput, { target: { value: '7' } }); + + fireEvent.submit(screen.getByRole('button', { name: /continue/i }).closest('form')!); + + await waitFor(() => expect(mockOnCompleted).toHaveBeenCalled()); + const submitted = mockOnCompleted.mock.calls[0][0]; + expect(submitted.membership).toBe('other'); + expect(Number(submitted.customMembershipAmount)).toBe(7); + }); + + test('submitting without touching the custom amount keeps the default tier', async () => { + render(); + + fireEvent.submit(screen.getByRole('button', { name: /continue/i }).closest('form')!); + + await waitFor(() => expect(mockOnCompleted).toHaveBeenCalled()); + expect(mockOnCompleted.mock.calls[0][0].membership).toBe('higher'); + }); +}); diff --git a/packages/join-flow/webpack/dev.js b/packages/join-flow/webpack/dev.js index 79f528e2..fa6feee0 100644 --- a/packages/join-flow/webpack/dev.js +++ b/packages/join-flow/webpack/dev.js @@ -19,6 +19,17 @@ module.exports = merge(commonConfig, { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS", "Access-Control-Allow-Headers": "X-Requested-With, content-type, Authorization" + }, + // The standalone harness has no WordPress backend, so stub the REST + // endpoints the form posts to (mirrors mockRestEndpoints in join-e2e). + // Bodies are logged so the payload can be inspected in the terminal. + setupMiddlewares: (middlewares, devServer) => { + devServer.app.use(require("express").json()); + devServer.app.post("/join/v1/:resource", (req, res) => { + console.log(`[mock] POST /join/v1/${req.params.resource}`, JSON.stringify(req.body)); + res.json({}); + }); + return middlewares; } }, devtool: "cheap-module-source-map",