diff --git a/.github/workflows/jest-tests.yml b/.github/workflows/jest-tests.yml index 3f5021f..0dbdb1d 100644 --- a/.github/workflows/jest-tests.yml +++ b/.github/workflows/jest-tests.yml @@ -41,4 +41,27 @@ jobs: run: npm ci --ignore-scripts - name: Check formatting - run: npx prettier --check packages/join-flow + run: npx prettier --check packages/join-flow packages/join-e2e + + typecheck: + name: Type check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + # TypeScript, plus each package's dependencies. The root postinstall + # also runs Composer, which the type check doesn't need, so it's + # skipped and join-flow's dependencies are installed directly. + - name: Install dependencies + run: | + npm ci --ignore-scripts + npm --prefix packages/join-flow ci + npm --prefix packages/join-e2e ci + + # The join form and its Jest tests, the E2E tests and the fake API. + - name: Type check + run: npm run typecheck diff --git a/.prettierignore b/.prettierignore index c5e79c1..032e813 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,4 @@ packages/join-flow/build -packages/join-flow/dist \ No newline at end of file +packages/join-flow/dist +packages/join-e2e/test-results +packages/join-e2e/playwright-report diff --git a/package.json b/package.json index 1224b1d..e2fc845 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "build": "npm --prefix packages/join-flow run build", "dist": "npm run build && scripts/package.sh", "deploy": "npm run build && sh scripts/deploy.sh", - "prettier": "prettier --write packages/join-flow" + "prettier": "prettier --write packages/join-flow packages/join-e2e", + "typecheck": "tsc --noEmit -p packages/join-flow && npm --prefix packages/join-e2e run typecheck" }, "devDependencies": { "prettier": "^3.9.10", diff --git a/packages/join-block/phpcs.xml b/packages/join-block/phpcs.xml index ba5130f..dcfb0ce 100644 --- a/packages/join-block/phpcs.xml +++ b/packages/join-block/phpcs.xml @@ -4,7 +4,6 @@ . vendor/* - tests/* wordpress/* @@ -18,6 +17,12 @@ + + + tests/* + + diff --git a/packages/join-block/phpstan.neon b/packages/join-block/phpstan.neon index 8a22e42..b5e7c21 100644 --- a/packages/join-block/phpstan.neon +++ b/packages/join-block/phpstan.neon @@ -6,6 +6,7 @@ parameters: paths: - src - join.php + - tests scanDirectories: - vendor - wordpress diff --git a/packages/join-block/tests/JoinServiceFreeMembershipTest.php b/packages/join-block/tests/JoinServiceFreeMembershipTest.php index ee4818f..30f6e7d 100644 --- a/packages/join-block/tests/JoinServiceFreeMembershipTest.php +++ b/packages/join-block/tests/JoinServiceFreeMembershipTest.php @@ -147,13 +147,14 @@ public function get_results(string $query, $output = null): array $joinBlockLog = new class { public array $errors = []; - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - public function info(string $msg, array $ctx = []): void {} + public function info(string $msg, array $ctx = []): void + { + } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - public function warning(string $msg, array $ctx = []): void {} + public function warning(string $msg, array $ctx = []): void + { + } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps public function error(string $msg, array $ctx = []): void { $this->errors[] = $msg; diff --git a/packages/join-block/tests/JoinServiceMailchimpTest.php b/packages/join-block/tests/JoinServiceMailchimpTest.php index 43755bc..586996f 100644 --- a/packages/join-block/tests/JoinServiceMailchimpTest.php +++ b/packages/join-block/tests/JoinServiceMailchimpTest.php @@ -121,13 +121,14 @@ public function get_results(string $query, $output = null): array $joinBlockLog = new class { public array $errors = []; - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - public function info(string $msg, array $ctx = []): void {} + public function info(string $msg, array $ctx = []): void + { + } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - public function warning(string $msg, array $ctx = []): void {} + public function warning(string $msg, array $ctx = []): void + { + } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps public function error(string $msg, array $ctx = []): void { $this->errors[] = $msg; diff --git a/packages/join-block/tests/LapsingFilterTest.php b/packages/join-block/tests/LapsingFilterTest.php index 9315db6..6ca828e 100644 --- a/packages/join-block/tests/LapsingFilterTest.php +++ b/packages/join-block/tests/LapsingFilterTest.php @@ -23,15 +23,12 @@ protected function setUp(): void global $joinBlockLog; $joinBlockLog = new class { - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps public function info($msg, $ctx = []) { } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps public function warning($msg, $ctx = []) { } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps public function error($msg, $ctx = []) { } diff --git a/packages/join-block/tests/MailchimpServiceLegacyTagsTest.php b/packages/join-block/tests/MailchimpServiceLegacyTagsTest.php index be8fe96..162aa32 100644 --- a/packages/join-block/tests/MailchimpServiceLegacyTagsTest.php +++ b/packages/join-block/tests/MailchimpServiceLegacyTagsTest.php @@ -40,19 +40,16 @@ protected function setUp(): void public array $warnings = []; public array $errors = []; - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps public function info(string $msg, array $ctx = []): void { $this->infos[] = $msg; } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps public function warning(string $msg, array $ctx = []): void { $this->warnings[] = $msg; } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps public function error(string $msg, array $ctx = []): void { $this->errors[] = $msg; diff --git a/packages/join-block/tests/MailchimpServiceTagsTest.php b/packages/join-block/tests/MailchimpServiceTagsTest.php index 627ab61..262cc20 100644 --- a/packages/join-block/tests/MailchimpServiceTagsTest.php +++ b/packages/join-block/tests/MailchimpServiceTagsTest.php @@ -38,13 +38,14 @@ protected function setUp(): void $joinBlockLog = new class { public array $errors = []; - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - public function info(string $msg, array $ctx = []): void {} + public function info(string $msg, array $ctx = []): void + { + } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - public function warning(string $msg, array $ctx = []): void {} + public function warning(string $msg, array $ctx = []): void + { + } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps public function error(string $msg, array $ctx = []): void { $this->errors[] = $msg; diff --git a/packages/join-block/tests/SessionLockTestProcess.php b/packages/join-block/tests/SessionLockTestProcess.php index bc9a14e..9ee5e19 100644 --- a/packages/join-block/tests/SessionLockTestProcess.php +++ b/packages/join-block/tests/SessionLockTestProcess.php @@ -3,8 +3,8 @@ require_once(__DIR__ . "/../vendor/autoload.php"); // Load WordPress functions required by JoinService::acquireLock() -define( 'ABSPATH', __DIR__ . '/../wordpress/' ); -define( 'WPINC', 'wp-includes' ); +define('ABSPATH', __DIR__ . '/../wordpress/'); +define('WPINC', 'wp-includes'); require_once(__DIR__ . "/../wordpress/wp-includes/formatting.php"); require_once(__DIR__ . "/../wordpress/wp-includes/functions.php"); diff --git a/packages/join-block/tests/SettingsComputeTagsToRemoveTest.php b/packages/join-block/tests/SettingsComputeTagsToRemoveTest.php index 3074deb..2a5dfa6 100644 --- a/packages/join-block/tests/SettingsComputeTagsToRemoveTest.php +++ b/packages/join-block/tests/SettingsComputeTagsToRemoveTest.php @@ -23,9 +23,6 @@ class SettingsComputeTagsToRemoveTest extends TestCase /** Values returned by the carbon_get_theme_option stub, keyed by field name. */ private array $settings = []; - /** Plan rows returned by the $wpdb stub, as stored in wp_options. */ - private array $storedPlans = []; - protected function setUp(): void { parent::setUp(); @@ -49,12 +46,10 @@ public function prepare(string $query, ...$args): string { return $query; } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps public function esc_like(string $text): string { return $text; } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps public function get_results(string $query, $output = null): array { return $this->rows; diff --git a/packages/join-block/tests/SettingsMembershipPlanLookupTest.php b/packages/join-block/tests/SettingsMembershipPlanLookupTest.php index 1d18cd7..0b0c8fe 100644 --- a/packages/join-block/tests/SettingsMembershipPlanLookupTest.php +++ b/packages/join-block/tests/SettingsMembershipPlanLookupTest.php @@ -160,7 +160,8 @@ public function testReturnsFalsyWhenNoStoredPlanMatches(): void private function stubWpdbWithRows(array $rows): void { global $wpdb; - $wpdb = Mockery::mock(); + /** @var \wpdb&\Mockery\MockInterface $wpdb */ + $wpdb = Mockery::mock(\wpdb::class); $wpdb->options = 'wp_options'; $wpdb->shouldReceive('esc_like')->andReturnUsing(fn($s) => $s); $wpdb->shouldReceive('prepare')->andReturnUsing(fn($sql) => $sql); diff --git a/packages/join-block/tests/StripeInvoicePaidTest.php b/packages/join-block/tests/StripeInvoicePaidTest.php index 0ae85f1..8b0a95a 100644 --- a/packages/join-block/tests/StripeInvoicePaidTest.php +++ b/packages/join-block/tests/StripeInvoicePaidTest.php @@ -32,13 +32,14 @@ protected function setUp(): void $joinBlockLog = new class { public array $errors = []; - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - public function info(string $msg, array $ctx = []): void {} + public function info(string $msg, array $ctx = []): void + { + } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps - public function warning(string $msg, array $ctx = []): void {} + public function warning(string $msg, array $ctx = []): void + { + } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps public function error(string $msg, array $ctx = []): void { $this->errors[] = $msg; @@ -52,7 +53,8 @@ protected function tearDown(): void { global $joinBlockLog; $joinBlockLog = null; - \Stripe\ApiRequestor::setHttpClient(null); + // Back to the client Stripe uses by default. + \Stripe\ApiRequestor::setHttpClient(\Stripe\HttpClient\CurlClient::instance()); Monkey\tearDown(); parent::tearDown(); } diff --git a/packages/join-block/tests/StripeWebhookTest.php b/packages/join-block/tests/StripeWebhookTest.php index 201675f..9e2f41d 100644 --- a/packages/join-block/tests/StripeWebhookTest.php +++ b/packages/join-block/tests/StripeWebhookTest.php @@ -200,8 +200,8 @@ public function testResolveTierTagChangesSharedTagsAreNotRemoved(): void $result = $this->callResolveTierTagChanges($newPlan, $oldPlan); $this->assertContains('high-wage', $result['addTags']); - $this->assertContains('member', $result['addTags']); - $this->assertContains('low-wage', $result['removeTags']); + $this->assertContains('member', $result['addTags']); + $this->assertContains('low-wage', $result['removeTags']); $this->assertNotContains('member', $result['removeTags'], 'member must not be removed when it is also being added'); } diff --git a/packages/join-block/tests/ZetkinServiceTagsTest.php b/packages/join-block/tests/ZetkinServiceTagsTest.php index 295b098..15dabd6 100644 --- a/packages/join-block/tests/ZetkinServiceTagsTest.php +++ b/packages/join-block/tests/ZetkinServiceTagsTest.php @@ -34,19 +34,16 @@ protected function setUp(): void public array $warnings = []; public array $errors = []; - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps public function info(string $msg, array $ctx = []): void { $this->infos[] = $msg; } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps public function warning(string $msg, array $ctx = []): void { $this->warnings[] = $msg; } - // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps public function error(string $msg, array $ctx = []): void { $this->errors[] = $msg; diff --git a/packages/join-e2e/.prettierrc.json b/packages/join-e2e/.prettierrc.json new file mode 100644 index 0000000..dc8c710 --- /dev/null +++ b/packages/join-e2e/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "printWidth": 120, + "trailingComma": "all" +} diff --git a/packages/join-e2e/fake-api/server.js b/packages/join-e2e/fake-api/server.js index a95ef53..91929d2 100644 --- a/packages/join-e2e/fake-api/server.js +++ b/packages/join-e2e/fake-api/server.js @@ -27,6 +27,7 @@ function freshStripe() { return { counter: 0, objects: { products: [], prices: [], customers: [], subscriptions: [], invoices: [] } }; } +/** @returns {{ counter: number, tags: { id: number, title: string }[] }} */ function freshZetkin() { return { counter: 0, tags: [] }; } @@ -115,18 +116,29 @@ function fakeStripe(method, url, body) { return { status: 200, body: { object: 'search_result', url: url.pathname, has_more: false, data } }; } if (method === 'GET' && !idOrAction) { - return { status: 200, body: { object: 'list', url: url.pathname, has_more: false, data: listStripe(type, store, url) } }; + return { + status: 200, + body: { object: 'list', url: url.pathname, has_more: false, data: listStripe(type, store, url) }, + }; } if (method === 'POST' && !idOrAction) { stripe.counter += 1; - const object = { id: `${prefix}_e2e_${stripe.counter}`, object: type.replace(/s$/, ''), active: true, livemode: false, ...body }; + const object = { + id: `${prefix}_e2e_${stripe.counter}`, + object: type.replace(/s$/, ''), + active: true, + livemode: false, + ...body, + }; if (body && body.unit_amount) object.unit_amount = Number(body.unit_amount); store.push(object); return { status: 200, body: object }; } if (method === 'GET' && idOrAction) { const object = store.find((candidate) => candidate.id === idOrAction); - return object ? { status: 200, body: object } : { status: 404, body: { error: { type: 'invalid_request_error', message: 'No such object' } } }; + return object + ? { status: 200, body: object } + : { status: 404, body: { error: { type: 'invalid_request_error', message: 'No such object' } } }; } return null; } @@ -180,12 +192,17 @@ function respond(host, method, url, body) { return { status: 200, body: {} }; } +/** + * @param {import('http').IncomingMessage} request + * @param {import('http').ServerResponse} response + */ function handleService(request, response) { + /** @type {Buffer[]} */ const chunks = []; request.on('data', (chunk) => chunks.push(chunk)); request.on('end', () => { const host = (request.headers.host || '').split(':')[0]; - const url = new URL(request.url, `https://${host}`); + const url = new URL(request.url || '/', `https://${host}`); let buffer = Buffer.concat(chunks); // Sentry, for one, gzips what it sends. A body that won't unzip is // recorded as it came, rather than crashing the fake. @@ -213,7 +230,12 @@ function handleService(request, response) { }); } +/** + * @param {import('http').IncomingMessage} request + * @param {import('http').ServerResponse} response + */ function handleControl(request, response) { + /** @type {Buffer[]} */ const chunks = []; request.on('data', (chunk) => chunks.push(chunk)); request.on('end', () => { @@ -252,7 +274,7 @@ function handleControl(request, response) { } // Puts an object straight into the fake Stripe, e.g. POST /stripe/customers // with {"id": "cus_1", "email": "..."}, for tests that need one to exist. - const stripeSeed = request.url.match(/^\/stripe\/([a-z_]+)$/); + const stripeSeed = (request.url || '').match(/^\/stripe\/([a-z_]+)$/); if (request.method === 'POST' && stripeSeed && stripe.objects[stripeSeed[1]]) { const object = JSON.parse(Buffer.concat(chunks).toString('utf8')); stripe.objects[stripeSeed[1]].push(object); @@ -265,7 +287,10 @@ function handleControl(request, response) { const certs = path.join(__dirname, 'certs'); https - .createServer({ key: fs.readFileSync(path.join(certs, 'server.key')), cert: fs.readFileSync(path.join(certs, 'server.crt')) }, handleService) + .createServer( + { key: fs.readFileSync(path.join(certs, 'server.key')), cert: fs.readFileSync(path.join(certs, 'server.crt')) }, + handleService, + ) .listen(443); http.createServer(handleService).listen(80); http.createServer(handleControl).listen(8080); diff --git a/packages/join-e2e/fake-api/tsconfig.json b/packages/join-e2e/fake-api/tsconfig.json new file mode 100644 index 0000000..2e1d290 --- /dev/null +++ b/packages/join-e2e/fake-api/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "noImplicitAny": false, + "useUnknownInCatchVariables": false + }, + "include": ["."] +} diff --git a/packages/join-e2e/package-lock.json b/packages/join-e2e/package-lock.json index f35d0d8..098636b 100644 --- a/packages/join-e2e/package-lock.json +++ b/packages/join-e2e/package-lock.json @@ -8,7 +8,9 @@ "name": "@ck/join-e2e", "version": "1.0.0", "devDependencies": { - "@playwright/test": "^1.63.0" + "@playwright/test": "^1.63.0", + "@types/node": "^20.19.0", + "typescript": "^5.9.3" } }, "node_modules/@playwright/test": { @@ -27,6 +29,16 @@ "node": ">=20" } }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/playwright": { "version": "1.63.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", @@ -55,6 +67,27 @@ "engines": { "node": ">=20" } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" } } } diff --git a/packages/join-e2e/package.json b/packages/join-e2e/package.json index 5966612..148dbe3 100644 --- a/packages/join-e2e/package.json +++ b/packages/join-e2e/package.json @@ -8,9 +8,12 @@ "env:start": "bash scripts/env.sh start", "env:stop": "bash scripts/env.sh stop", "env:destroy": "bash scripts/env.sh destroy", - "pretest": "bash scripts/seed.sh" + "pretest": "bash scripts/seed.sh", + "typecheck": "tsc --noEmit -p . && tsc --noEmit -p fake-api" }, "devDependencies": { - "@playwright/test": "^1.63.0" + "@playwright/test": "^1.63.0", + "@types/node": "^20.19.0", + "typescript": "^5.9.3" } } diff --git a/packages/join-e2e/tests/admin-plan-redirect.spec.ts b/packages/join-e2e/tests/admin-plan-redirect.spec.ts index a00e46d..e5a13de 100644 --- a/packages/join-e2e/tests/admin-plan-redirect.spec.ts +++ b/packages/join-e2e/tests/admin-plan-redirect.spec.ts @@ -25,9 +25,7 @@ const REDIRECT_CHECKBOX = 'Redirect to a page instead of taking payment'; function field(scope: Locator, label: string): Locator { // One selector rather than a `has` filter, so it also works inside the // block editor's iframe. - return scope - .locator(`.cf-field:not(.cf-separator):has(.cf-field__label:text-matches("^${label}[*]?$"))`) - .first(); + return scope.locator(`.cf-field:not(.cf-separator):has(.cf-field__label:text-matches("^${label}[*]?$"))`).first(); } /** The CK Join Form block. The block editor renders its content in an iframe. */ @@ -82,7 +80,12 @@ async function openNewPageWithJoinBlock(page: Page): Promise { // 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)) { + if ( + await patterns.waitFor({ state: 'visible', timeout: 5000 }).then( + () => true, + () => false, + ) + ) { await patterns.getByRole('button', { name: 'Close' }).click(); } @@ -207,9 +210,9 @@ test.describe('Redirect help text', () => { await openNewPageWithJoinBlock(page); const plansField = field(joinFormBlock(page), 'Custom Membership Plans'); await plansField.locator('.cf-complex__inserter-button').click(); - await expect( - plansField.locator('.cf-field.cf-checkbox', { hasText: REDIRECT_CHECKBOX }).first(), - ).toContainText(NOT_RECORDED); + await expect(plansField.locator('.cf-field.cf-checkbox', { hasText: REDIRECT_CHECKBOX }).first()).toContainText( + NOT_RECORDED, + ); } finally { resetSettings(); } @@ -225,14 +228,13 @@ test.describe('Custom plans in the CK Join Form block', () => { // Same as above, but picks the page without the picker's search, so the // redirect itself stays covered while JOIN-158 is open. - test('a redirecting plan set up in the block editor redirects on the published page, picking the page from the full list', async ({ page }) => { + test('a redirecting plan set up in the block editor redirects on the published page, picking the page from the full list', async ({ + page, + }) => { await redirectingPlanInBlockEditor(page, pickRedirectTargetFromList); }); - async function redirectingPlanInBlockEditor( - page: Page, - choose: (picker: Locator) => Promise, - ): Promise { + async function redirectingPlanInBlockEditor(page: Page, choose: (picker: Locator) => Promise): Promise { await loginAsAdmin(page); await openNewPageWithJoinBlock(page); diff --git a/packages/join-e2e/tests/free-membership.spec.ts b/packages/join-e2e/tests/free-membership.spec.ts index 89da143..ee21e79 100644 --- a/packages/join-e2e/tests/free-membership.spec.ts +++ b/packages/join-e2e/tests/free-membership.spec.ts @@ -27,9 +27,7 @@ test.beforeEach(async ({ page }) => { }); test.describe('Confirm stage reached without payment', () => { - test('continuing through Details and Plan skips payment and shows confirm stage', async ({ - page, - }) => { + test('continuing through Details and Plan skips payment and shows confirm stage', async ({ page }) => { // Intercept the /join endpoint so the confirm page does not error on submit. await page.route('**/wp-json/join/v1/join', async (route) => { await route.fulfill({ diff --git a/packages/join-e2e/tests/helpers.ts b/packages/join-e2e/tests/helpers.ts index e0eb602..fe182f5 100644 --- a/packages/join-e2e/tests/helpers.ts +++ b/packages/join-e2e/tests/helpers.ts @@ -42,17 +42,14 @@ export async function injectEnvOverrides( await page.route(urlPattern, async (route) => { const response = await route.fetch(); let body = await response.text(); - body = body.replace( - ENV_SCRIPT_PATTERN, - (_, open: string, json: string, close: string) => { - try { - const env = JSON.parse(json); - return `${open}${JSON.stringify({ ...env, ...overrides })}${close}`; - } catch { - return `${open}${json}${close}`; - } - }, - ); + body = body.replace(ENV_SCRIPT_PATTERN, (_, open: string, json: string, close: string) => { + try { + const env = JSON.parse(json); + return `${open}${JSON.stringify({ ...env, ...overrides })}${close}`; + } catch { + return `${open}${json}${close}`; + } + }); await route.fulfill({ response, body }); }); } @@ -68,10 +65,7 @@ export async function injectEnvOverrides( * Call this after the form has advanced past the details stage (so that * sessionStorage already contains the accumulated form data). */ -export async function captureJoinBodyViaStripeRedirect( - page: Page, - pageUrl: string, -): Promise> { +export async function captureJoinBodyViaStripeRedirect(page: Page, pageUrl: string): Promise> { // Inject the fake payment intent ID into the existing session state. await page.evaluate((key: string) => { const state = JSON.parse(sessionStorage.getItem(key) || '{}'); @@ -94,10 +88,7 @@ export async function captureJoinBodyViaStripeRedirect( await page.goto(`${pageUrl}?stripe_success=true`); // Wait for the confirm page heading to appear. - await page.waitForFunction( - () => document.querySelector('h2') !== null, - { timeout: 10000 }, - ); + await page.waitForFunction(() => document.querySelector('h2') !== null, { timeout: 10000 }); // Poll until /join fires — the confirm page auto-submits on mount and the // POST may arrive slightly after the heading renders. diff --git a/packages/join-e2e/tests/multi-currency-plans.spec.ts b/packages/join-e2e/tests/multi-currency-plans.spec.ts index 690c4d0..9c33714 100644 --- a/packages/join-e2e/tests/multi-currency-plans.spec.ts +++ b/packages/join-e2e/tests/multi-currency-plans.spec.ts @@ -18,7 +18,6 @@ import { MULTI_CURRENCY_PAGE } from './settings/support'; * - "Standard" EUR €7/month (id: standard-eur) */ - async function advanceToPlanStep(page: import('@playwright/test').Page) { await mockRestEndpoints(page); await page.goto(MULTI_CURRENCY_PAGE); diff --git a/packages/join-e2e/tests/settings/copy.spec.ts b/packages/join-e2e/tests/settings/copy.spec.ts index ecad708..fb6b45e 100644 --- a/packages/join-e2e/tests/settings/copy.spec.ts +++ b/packages/join-e2e/tests/settings/copy.spec.ts @@ -324,18 +324,24 @@ test.describe('Details step', () => { test('Privacy Copy', async ({ settings, page }) => { await settings.open('Copy'); - await settings.setRichText('Privacy Copy', '

Read our privacy policy.

'); + await settings.setRichText( + 'Privacy Copy', + '

Read our privacy policy.

', + ); await settings.save(); expect(getSetting('privacy_copy')).toContain('privacy policy'); await openJoinForm(page); - await expect(page.getByRole('link', { name: 'privacy policy' })).toHaveAttribute('href', 'https://example.org/privacy'); + await expect(page.getByRole('link', { name: 'privacy policy' })).toHaveAttribute( + 'href', + 'https://example.org/privacy', + ); }); }); test.describe('Plan step', () => { - test('Membership Tiers Heading and Copy, with the copy\'s built-in default', async ({ settings, page }) => { + test("Membership Tiers Heading and Copy, with the copy's built-in default", async ({ settings, page }) => { expect(getSetting('membership_tiers_copy')).toBe(''); await goToPlans(page); await expect(page.getByText('You can change or cancel whenever you want.')).toBeVisible(); @@ -370,7 +376,10 @@ test.describe('Plan step', () => { }); // Linear JOIN-163: passed to the form, but nothing in the form shows it. - test.fixme('"Only valid for monthly subscriptions" (day of month copy) labels the payment day', async ({ settings, page }) => { + test.fixme('"Only valid for monthly subscriptions" (day of month copy) labels the payment day', async ({ + settings, + page, + }) => { await settings.open('Copy'); await settings.setText('Only valid for monthly subscriptions', 'Which day should we take payment?'); await settings.save(); diff --git a/packages/join-e2e/tests/settings/fake-api.ts b/packages/join-e2e/tests/settings/fake-api.ts index fa87a74..d13693d 100644 --- a/packages/join-e2e/tests/settings/fake-api.ts +++ b/packages/join-e2e/tests/settings/fake-api.ts @@ -56,6 +56,9 @@ export async function overrideFakeApi(rule: { * Puts an object straight into the fake Stripe, as if it had been created * there earlier, e.g. seedFakeStripe('customers', { id: 'cus_1', email: '...' }). */ -export async function seedFakeStripe(type: 'products' | 'prices' | 'customers' | 'subscriptions' | 'invoices', object: Record): Promise { +export async function seedFakeStripe( + type: 'products' | 'prices' | 'customers' | 'subscriptions' | 'invoices', + object: Record, +): Promise { await control('POST', `/stripe/${type}`, object); } diff --git a/packages/join-e2e/tests/settings/features-integrations.spec.ts b/packages/join-e2e/tests/settings/features-integrations.spec.ts index 519e532..28dfbcd 100644 --- a/packages/join-e2e/tests/settings/features-integrations.spec.ts +++ b/packages/join-e2e/tests/settings/features-integrations.spec.ts @@ -180,7 +180,7 @@ test.describe('Payment providers', () => { await expect(logo(page, 'Stripe')).toBeVisible(); }); - test('Use GoCardless sends Direct Debit members to GoCardless\'s hosted pages', async ({ settings, page }) => { + test("Use GoCardless sends Direct Debit members to GoCardless's hosted pages", async ({ settings, page }) => { await settings.open('Features'); await settings.enableIntegration('Use GoCardless'); await settings.save(); @@ -224,7 +224,10 @@ test.describe('Payment providers', () => { expect(redirected).toBe(false); }); - test('Use Chargebee shows the Chargebee logo and sets Chargebee up with the site and publishable key', async ({ settings, page }) => { + test('Use Chargebee shows the Chargebee logo and sets Chargebee up with the site and publishable key', async ({ + settings, + page, + }) => { await settings.open('Features'); await settings.enableIntegration('Use Chargebee'); await settings.save(); diff --git a/packages/join-e2e/tests/settings/features.spec.ts b/packages/join-e2e/tests/settings/features.spec.ts index 66d760a..755d5b8 100644 --- a/packages/join-e2e/tests/settings/features.spec.ts +++ b/packages/join-e2e/tests/settings/features.spec.ts @@ -244,7 +244,10 @@ test.describe('Enable international phone numbers', () => { await expect(page.getByText('Please enter a valid UK phone number')).toBeVisible(); }); - test('when on, a country code can be chosen and other countries\' numbers are accepted', async ({ settings, page }) => { + test("when on, a country code can be chosen and other countries' numbers are accepted", async ({ + settings, + page, + }) => { await settings.open('Features'); await settings.setCheckbox('Enable international phone numbers', true); await settings.save(); diff --git a/packages/join-e2e/tests/settings/integrations-crm.spec.ts b/packages/join-e2e/tests/settings/integrations-crm.spec.ts index 881e7cf..c42c476 100644 --- a/packages/join-e2e/tests/settings/integrations-crm.spec.ts +++ b/packages/join-e2e/tests/settings/integrations-crm.spec.ts @@ -116,7 +116,7 @@ test.describe('Zetkin', () => { expect(await fakeApiRequests('api.dev.zetkin.org')).toEqual([]); }); - test('with the OAuth credentials, the member is added through Zetkin\'s API and tagged', async ({ + test("with the OAuth credentials, the member is added through Zetkin's API and tagged", async ({ settings, page, }) => { @@ -275,7 +275,7 @@ test.describe('Webhooks', () => { expect(await fakeApiRequests('fake-api')).toEqual([]); }); - test('Step Webhook Url is sent the form\'s data after each step', async ({ settings, page }) => { + test("Step Webhook Url is sent the form's data after each step", async ({ settings, page }) => { await settings.open('Integrations'); await settings.setText('Step Webhook Url', STEP_WEBHOOK); await settings.save(); @@ -302,7 +302,7 @@ test.describe('Webhooks', () => { }); test.describe('Auth0', () => { - test('a join creates the member\'s Auth0 account, with the credentials set here', async ({ settings, page }) => { + test("a join creates the member's Auth0 account, with the credentials set here", async ({ settings, page }) => { await settings.open('Features'); await settings.enableIntegration('Create Auth0 Account'); await settings.save(); diff --git a/packages/join-e2e/tests/settings/integrations-logging.spec.ts b/packages/join-e2e/tests/settings/integrations-logging.spec.ts index f71d685..7a9d098 100644 --- a/packages/join-e2e/tests/settings/integrations-logging.spec.ts +++ b/packages/join-e2e/tests/settings/integrations-logging.spec.ts @@ -90,7 +90,7 @@ test.describe('Loggly', () => { test.describe('Sentry', () => { const DSN = 'https://e2epublickey@sentry.e2e.example/42'; - test('Sentry Dsn sends the plugin\'s errors to Sentry, and sets Sentry up in the join form', async ({ + test("Sentry Dsn sends the plugin's errors to Sentry, and sets Sentry up in the join form", async ({ settings, request, page, @@ -118,16 +118,18 @@ test.describe('Sentry', () => { return route.fulfill({ status: 200, body: '{}' }); }); await openJoinForm(page); - await page.evaluate(() => setTimeout(() => { - throw new Error('e2e form error'); - })); + await page.evaluate(() => + setTimeout(() => { + throw new Error('e2e form error'); + }), + ); await expect.poll(() => reported.length).toBeGreaterThan(0); expect(reported[0]).toContain('/api/42/'); }); }); test.describe('Google Cloud Logging', () => { - test('Google Cloud Project Id and Key File Contents send logs to that project\'s log', async ({ + test("Google Cloud Project Id and Key File Contents send logs to that project's log", async ({ settings, request, }) => { diff --git a/packages/join-e2e/tests/settings/integrations-payments.spec.ts b/packages/join-e2e/tests/settings/integrations-payments.spec.ts index 1d3fd1a..eaef426 100644 --- a/packages/join-e2e/tests/settings/integrations-payments.spec.ts +++ b/packages/join-e2e/tests/settings/integrations-payments.spec.ts @@ -93,7 +93,7 @@ test.describe('Stripe', () => { }); }); - test('Download Subscriptions exports Stripe\'s subscriptions as a spreadsheet', async ({ settings }) => { + test("Download Subscriptions exports Stripe's subscriptions as a spreadsheet", async ({ settings }) => { await settings.open('Features'); await settings.enableIntegration('Use Stripe'); await settings.save(); @@ -115,7 +115,10 @@ test.describe('Stripe', () => { status: 'active', created: 1767225600, current_period_end: 1769904000, - items: { object: 'list', data: [{ id: 'si_export', object: 'subscription_item', price: { id: 'price_export', object: 'price' } }] }, + items: { + object: 'list', + data: [{ id: 'si_export', object: 'subscription_item', price: { id: 'price_export', object: 'price' } }], + }, }); // With no invoices the export breaks (Linear JOIN-167, tested below). await seedFakeStripe('invoices', { @@ -132,13 +135,17 @@ test.describe('Stripe', () => { settings.page.waitForEvent('download'), settings.page.getByRole('link', { name: 'Download Subscriptions' }).click(), ]); - const csv = readFileSync((await download.path()) as string, 'utf8').trim().split('\n'); + const csv = readFileSync((await download.path()) as string, 'utf8') + .trim() + .split('\n'); expect(csv[0]).toBe( '"Email","Customer ID","Subscription ID","Subscription Status","Subscription Created","Subscription End","Price ID","Price Name","First Payment","Last Payment"', ); expect(csv[1]).toMatch(/^"exported@example.org","cus_export","sub_export","active",/); - expect(csv[1]).toContain('"price_export","Membership: Standard - GBP 5.00","2026-01-02 00:00:00","2026-01-02 00:00:00"'); + expect(csv[1]).toContain( + '"price_export","Membership: Standard - GBP 5.00","2026-01-02 00:00:00","2026-01-02 00:00:00"', + ); // The plugin reads Stripe with the secret key set on this tab. const exportReads = (await fakeApiRequests('api.stripe.com')).filter( @@ -170,7 +177,9 @@ test.describe('Stripe', () => { settings.page.waitForEvent('download'), settings.page.getByRole('link', { name: 'Download Subscriptions' }).click(), ]); - const csv = readFileSync((await download.path()) as string, 'utf8').trim().split('\n'); + const csv = readFileSync((await download.path()) as string, 'utf8') + .trim() + .split('\n'); expect(csv[0]).toMatch(/^"Email",/); expect(csv[1]).toMatch(/^"exported@example.org","cus_export","sub_export",/); }); @@ -278,7 +287,15 @@ test.describe('Chargebee', () => { host: 'e2e.chargebee.com', method: 'POST', path: '/hosted_pages/checkout_new_for_items$', - body: { hosted_page: { id: 'hp_e2e', type: 'checkout_new', url: hostedPageUrl, state: 'created', object: 'hosted_page' } }, + body: { + hosted_page: { + id: 'hp_e2e', + type: 'checkout_new', + url: hostedPageUrl, + state: 'created', + object: 'hosted_page', + }, + }, }); await page.route('https://e2e.chargebee.example/**', (route) => route.fulfill({ contentType: 'text/html', body: '

Chargebee

' }), @@ -348,7 +365,10 @@ test.describe('Postcode address lookup', () => { postcode_address_provider: 'get_address_io', get_address_io_api_key: 'e2e-getaddress-key', }); - expect(await joinFormEnv(page)).toMatchObject({ USE_POSTCODE_LOOKUP: true, POSTCODE_ADDRESS_PROVIDER: 'get_address_io' }); + expect(await joinFormEnv(page)).toMatchObject({ + USE_POSTCODE_LOOKUP: true, + POSTCODE_ADDRESS_PROVIDER: 'get_address_io', + }); await overrideFakeApi({ host: 'api.getaddress.io', @@ -382,13 +402,25 @@ test.describe('Postcode address lookup', () => { postcode_address_provider: 'ideal_postcodes', ideal_postcodes_api_key: 'e2e-ideal-key', }); - expect(await joinFormEnv(page)).toMatchObject({ USE_POSTCODE_LOOKUP: true, POSTCODE_ADDRESS_PROVIDER: 'ideal_postcodes' }); + expect(await joinFormEnv(page)).toMatchObject({ + USE_POSTCODE_LOOKUP: true, + POSTCODE_ADDRESS_PROVIDER: 'ideal_postcodes', + }); await overrideFakeApi({ host: 'api.ideal-postcodes.co.uk', path: '^/v1/postcodes/', body: { - result: [{ line_1: '2 Ideal Street', line_2: '', post_town: 'Oxford', county: 'Oxfordshire', postcode: 'OX1 4PE', country: 'England' }], + result: [ + { + line_1: '2 Ideal Street', + line_2: '', + post_town: 'Oxford', + county: 'Oxfordshire', + postcode: 'OX1 4PE', + country: 'England', + }, + ], }, }); diff --git a/packages/join-e2e/tests/settings/logging-debug.spec.ts b/packages/join-e2e/tests/settings/logging-debug.spec.ts index fa15fef..ca24cc1 100644 --- a/packages/join-e2e/tests/settings/logging-debug.spec.ts +++ b/packages/join-e2e/tests/settings/logging-debug.spec.ts @@ -50,7 +50,9 @@ async function rotateLogFiles(request: APIRequestContext, days: number) { return $dates; `); await logInfo(request); - const kept = wpEvalJson(`return array_map(fn ($file) => substr(basename($file, '.log'), -10), ${PHP_LOG_FILES});`) + const kept = wpEvalJson( + `return array_map(fn ($file) => substr(basename($file, '.log'), -10), ${PHP_LOG_FILES});`, + ) .sort() .reverse(); return { laidDown, kept }; diff --git a/packages/join-e2e/tests/settings/membership-plans.spec.ts b/packages/join-e2e/tests/settings/membership-plans.spec.ts index b8f5543..04aa8f2 100644 --- a/packages/join-e2e/tests/settings/membership-plans.spec.ts +++ b/packages/join-e2e/tests/settings/membership-plans.spec.ts @@ -67,7 +67,9 @@ const planPanel = (page: Page, label: string) => async function mailchimpTagChanges(): Promise { return (await fakeApiRequests(MAILCHIMP)) .filter((request) => request.method === 'POST' && request.path === MEMBER_TAGS_PATH) - .flatMap((request) => request.body.tags.map((tag: { name: string; status: string }) => `${tag.name}: ${tag.status}`)); + .flatMap((request) => + request.body.tags.map((tag: { name: string; status: string }) => `${tag.name}: ${tag.status}`), + ); } test.describe('Global membership plans', () => { @@ -128,7 +130,7 @@ test.describe('Global membership plans', () => { }); test.describe('Membership plan fields', () => { - test('Price Point ID becomes the plan\'s ID', async ({ settings, page, request }) => { + test("Price Point ID becomes the plan's ID", async ({ settings, page, request }) => { await settings.open('Membership Plans'); await settings.addPlan({ name: 'Gold', id: 'gold-tier', price: '20' }); await settings.save(); @@ -179,8 +181,12 @@ test.describe('Membership plan fields', () => { await expect(page.locator(CONTINUE)).toHaveText('Continue and pay £15 monthly'); // The plugin enforces the minimum too, not just the form. - expect((await postJoin(request, { membership: 'baseline-plan_monthly_gbp', customMembershipAmount: 5 })).status()).toBe(500); - expect((await postJoin(request, { membership: 'baseline-plan_monthly_gbp', customMembershipAmount: 15 })).ok()).toBe(true); + expect( + (await postJoin(request, { membership: 'baseline-plan_monthly_gbp', customMembershipAmount: 5 })).status(), + ).toBe(500); + expect( + (await postJoin(request, { membership: 'baseline-plan_monthly_gbp', customMembershipAmount: 15 })).ok(), + ).toBe(true); }); test('without it, the price is fixed', async ({ page }) => { @@ -260,7 +266,7 @@ test.describe('Changing the plan list', () => { // Only the page checks a plan has a Name and Price; the plugin saves one // without (Linear JOIN-169). Re-enable once it's fixed. - test.fixme('the plugin refuses a plan without a Name, even when the page\'s own checks are skipped', async ({ + test.fixme("the plugin refuses a plan without a Name, even when the page's own checks are skipped", async ({ settings, }) => { await settings.open('Membership Plans'); @@ -289,7 +295,10 @@ test.describe('Global plans in Stripe', () => { const prices = created.filter((request) => request.path === '/v1/prices').map((request) => request.body); expect(products).toEqual([ - expect.objectContaining({ name: 'Membership: Baseline Plan', metadata: { membership_plan: 'baseline-plan_weekly_gbp' } }), + expect.objectContaining({ + name: 'Membership: Baseline Plan', + metadata: { membership_plan: 'baseline-plan_weekly_gbp' }, + }), expect.objectContaining({ name: 'Membership: Dollars', description: 'Paid in dollars', @@ -367,7 +376,13 @@ test.describe('Member tags', () => { } const subscriptionDeleted = (reason: string) => ({ - object: { id: 'sub_old', object: 'subscription', customer: CUSTOMER.id, status: 'canceled', cancellation_details: { reason } }, + object: { + id: 'sub_old', + object: 'subscription', + customer: CUSTOMER.id, + status: 'canceled', + cancellation_details: { reason }, + }, }); test('have defaults', () => { @@ -378,7 +393,7 @@ test.describe('Member tags', () => { }); }); - test('joining adds the plan\'s Add Tags, and takes off the other plans\' tags and the lapsed, cancelled and lapsing tags', async ({ + test("joining adds the plan's Add Tags, and takes off the other plans' tags and the lapsed, cancelled and lapsing tags", async ({ settings, request, }) => { @@ -425,12 +440,19 @@ test.describe('Member tags', () => { await settings.save(); await seedFakeStripe('customers', CUSTOMER); // For example the replacement /join creates when a member changes plan. - await seedFakeStripe('subscriptions', { id: 'sub_new', object: 'subscription', customer: CUSTOMER.id, status: 'active' }); + await seedFakeStripe('subscriptions', { + id: 'sub_new', + object: 'subscription', + customer: CUSTOMER.id, + status: 'active', + }); await postStripeWebhook(request, 'customer.subscription.deleted', subscriptionDeleted('cancellation_requested')); // It did look, so it's the other subscription that stopped the tagging. - expect((await fakeApiRequests('api.stripe.com')).some((request) => request.path === '/v1/subscriptions')).toBe(true); + expect((await fakeApiRequests('api.stripe.com')).some((request) => request.path === '/v1/subscriptions')).toBe( + true, + ); expect(await mailchimpTagChanges()).toEqual([]); }); @@ -473,7 +495,7 @@ test.describe('Member tags', () => { expect(await mailchimpTagChanges()).toEqual(['Left: inactive', 'Payment trouble: inactive']); }); - test('moving to another plan in Stripe swaps the old plan\'s Add Tags for the new one\'s', async ({ + test("moving to another plan in Stripe swaps the old plan's Add Tags for the new one's", async ({ settings, request, }) => { @@ -483,7 +505,10 @@ test.describe('Member tags', () => { await settings.save(); await seedFakeStripe('customers', CUSTOMER); - const stored = storedPlans(['baseline-plan_monthly_gbp', 'gold_monthly_gbp']) as Record>; + const stored = storedPlans(['baseline-plan_monthly_gbp', 'gold_monthly_gbp']) as Record< + string, + Record + >; const oldPrice = stored['baseline-plan_monthly_gbp'].stripe_price_id; const newPrice = stored['gold_monthly_gbp'].stripe_price_id; await postStripeWebhook(request, 'customer.subscription.updated', { diff --git a/packages/join-e2e/tests/settings/support.ts b/packages/join-e2e/tests/settings/support.ts index b50966f..09a21cd 100644 --- a/packages/join-e2e/tests/settings/support.ts +++ b/packages/join-e2e/tests/settings/support.ts @@ -153,13 +153,17 @@ export async function postJoin(request: APIRequestContext, data: Record { - const response = await request.post('/wp-json/join/v1/step', { data: { email: 'someone@example.com', stage: 'details' } }); + const response = await request.post('/wp-json/join/v1/step', { + data: { email: 'someone@example.com', stage: 'details' }, + }); expect(response.ok()).toBe(true); } /** Makes the plugin log an error naming `plan` ("Invalid membership plan: "). */ export async function logInvalidPlan(request: APIRequestContext, plan = 'no-such-plan'): Promise { - const response = await request.post('/wp-json/join/v1/join', { data: { email: 'someone@example.com', membership: plan } }); + const response = await request.post('/wp-json/join/v1/join', { + data: { email: 'someone@example.com', membership: plan }, + }); expect(response.status()).toBe(500); } diff --git a/packages/join-e2e/tests/settings/theme.spec.ts b/packages/join-e2e/tests/settings/theme.spec.ts index 35c2c3c..85d027d 100644 --- a/packages/join-e2e/tests/settings/theme.spec.ts +++ b/packages/join-e2e/tests/settings/theme.spec.ts @@ -15,7 +15,10 @@ const cssVariable = (page: Page, name: string) => /** A computed style property of the first element matching `selector`. */ const computed = (page: Page, selector: string, property: string) => - page.locator(selector).first().evaluate((element, name) => getComputedStyle(element).getPropertyValue(name), property); + page + .locator(selector) + .first() + .evaluate((element, name) => getComputedStyle(element).getPropertyValue(name), property); test('the colours have defaults before anything is saved', async ({ page }) => { expect(getSettings(['theme_primary_color', 'theme_gray_color', 'theme_background_color'])).toEqual({ @@ -30,7 +33,7 @@ test('the colours have defaults before anything is saved', async ({ page }) => { expect(await cssVariable(page, '--ck-join-form-background-color')).toBe('#f4f1ee'); }); -test('Primary Color colours the form\'s buttons', async ({ settings, page }) => { +test("Primary Color colours the form's buttons", async ({ settings, page }) => { await settings.open('Theme'); await settings.setColor('Primary Color', '#336699'); await settings.save(); @@ -45,7 +48,7 @@ test('Primary Color colours the form\'s buttons', async ({ settings, page }) => /** The circle of a step still to come, which Gray Color borders. */ const NEXT_STEP_CIRCLE = '.progress-step--next .progress-circle'; -test('Gray Color and Background Color reach the form\'s styles', async ({ settings, page }) => { +test("Gray Color and Background Color reach the form's styles", async ({ settings, page }) => { await settings.open('Theme'); await settings.setColor('Gray Color', '#aabbcc'); await settings.setColor('Background Color', '#fafafa'); @@ -63,7 +66,7 @@ test('Gray Color and Background Color reach the form\'s styles', async ({ settin expect(await computed(page, 'body', 'background-color')).toBe('rgb(250, 250, 250)'); }); -test('Custom Css is added to the join form\'s page', async ({ settings, page }) => { +test("Custom Css is added to the join form's page", async ({ settings, page }) => { const css = '.ck-join-flow { outline: 3px solid rgb(255, 0, 0); }'; await settings.open('Theme'); await settings.setText('Custom Css', css); diff --git a/packages/join-e2e/tests/supporter-mode-edge-cases.spec.ts b/packages/join-e2e/tests/supporter-mode-edge-cases.spec.ts index 2ffd506..745ddd5 100644 --- a/packages/join-e2e/tests/supporter-mode-edge-cases.spec.ts +++ b/packages/join-e2e/tests/supporter-mode-edge-cases.spec.ts @@ -52,9 +52,7 @@ test.describe('One-off tab disabled when STRIPE_DIRECT_DEBIT_ONLY=true', () => { }); test('explanatory note about Direct Debit is shown', async ({ page }) => { - await expect( - page.locator('p:has-text("One-off donations are not available with Direct Debit")'), - ).toBeVisible(); + await expect(page.locator('p:has-text("One-off donations are not available with Direct Debit")')).toBeVisible(); }); test('Monthly tab still works and advances the form', async ({ page }) => { @@ -79,9 +77,7 @@ test.describe('No plans configured warning', () => { }); test('"No donation amounts configured" warning is shown', async ({ page }) => { - await expect( - page.locator('[role="alert"]:has-text("No donation amounts configured")'), - ).toBeVisible(); + await expect(page.locator('[role="alert"]:has-text("No donation amounts configured")')).toBeVisible(); }); test('no tier buttons or frequency toggle are rendered', async ({ page }) => { diff --git a/packages/join-e2e/tsconfig.json b/packages/join-e2e/tsconfig.json new file mode 100644 index 0000000..bc2abac --- /dev/null +++ b/packages/join-e2e/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es2022", + "lib": ["es2022", "dom"], + "module": "commonjs", + "moduleResolution": "node", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "types": ["node"] + }, + "include": ["tests", "playwright.config.ts"] +} diff --git a/readme.md b/readme.md index 210354e..f3095f2 100644 --- a/readme.md +++ b/readme.md @@ -436,14 +436,18 @@ cd packages/join-flow npm test ``` -### Frontend formatting +### Formatting and type checks -CI fails if `packages/join-flow` isn't formatted with Prettier. To fix formatting, from the repository root: +CI fails if `packages/join-flow` or `packages/join-e2e` isn't formatted with Prettier, or if either doesn't type-check (the join form, its Jest tests, the E2E tests and the E2E suite's fake API). The type check needs the E2E dependencies, which `npm install` at the root doesn't install. From the repository root: ```bash -npm run prettier +npm --prefix packages/join-e2e ci # once +npm run prettier # fix formatting +npm run typecheck # type check both packages ``` +`packages/join-e2e` has its own Prettier settings (`packages/join-e2e/.prettierrc.json`), matching how its tests were already written. + ### Backend unit tests ```bash @@ -453,7 +457,7 @@ cd packages/join-block ### Backend linting -CI runs phpcs and PHPStan on `packages/join-block` and fails on any phpcs error or PHPStan error. Run both locally with: +CI runs phpcs and PHPStan on `packages/join-block`, its PHPUnit tests included, and fails on any phpcs error or PHPStan error. Run both locally with: ```bash cd packages/join-block