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