import { expect, Page } from '@playwright/test';
import { access, mkdir, rm, rmdir, writeFile } from 'fs/promises';
import path from 'path';
export const PLUGIN_FILE = 'laposta-woocommerce/laposta.php';
export const WOO_PLUGIN_FILE = 'woocommerce/woocommerce.php';
export const SETTINGS_PAGE = 'laposta_woocommerce_options';
const CHECKOUT_URLS = parseCheckoutUrls(process.env.LWC_E2E_CHECKOUT_URLS, ['/checkout/', '/afrekenen/']);
const CHECKBOX_LABEL_PREFIX = process.env.LWC_E2E_CHECKBOX_LABEL_PREFIX || 'Laposta WooCommerce E2E';
const API_KEY_FALLBACK = process.env.LWC_API_KEY || '';
const TEST_LIST_ID_FALLBACK = process.env.LWC_TEST_LIST_ID || '';
const WP_CONTENT_PATH = process.env.WP_CONTENT_PATH || path.resolve(process.cwd(), '..', '..');
const PRIORITY_FIXTURE_PATH = path.join(WP_CONTENT_PATH, 'mu-plugins', 'laposta-woocommerce-e2e-priority.php');
const PRIORITY_FIXTURE_LABEL = 'Laposta priority E2E sentinel';
const PRIORITY_FIXTURE_CONTENT = ` 'laposta-e2e/priority-sentinel',
'label' => '${PRIORITY_FIXTURE_LABEL}',
'optionalLabel' => '${PRIORITY_FIXTURE_LABEL}',
'location' => 'order',
'type' => 'checkbox',
'required' => false,
'show_in_order_confirmation' => false,
));
}, 10);
`;
const CHECKOUT_BLOCK_CONTENT = `
`;
export type LapostaConfig = {
apiKey: string;
listId: string;
};
export async function ensurePluginActive(page: Page) {
await ensurePluginFileActive(page, PLUGIN_FILE);
}
export async function ensureWooCommerceActive(page: Page) {
await ensurePluginFileActive(page, WOO_PLUGIN_FILE);
}
async function ensurePluginFileActive(page: Page, pluginFile: string) {
await page.goto('/wp-admin/plugins.php');
const pluginRow = page.locator(`tr[data-plugin="${pluginFile}"]:not(.plugin-update-tr)`);
await expect(pluginRow).toBeVisible();
const activateLink = pluginRow.locator(`a[href*="action=activate"][href*="${encodeURIComponent(pluginFile)}"]`);
if (await activateLink.count()) {
await Promise.all([page.waitForLoadState('domcontentloaded'), activateLink.first().click()]);
await expect(pluginRow).toHaveClass(/active/);
}
}
export async function gotoSettingsPage(page: Page) {
const urls = [
`/wp-admin/admin.php?page=${SETTINGS_PAGE}`,
`/wp-admin/options-general.php?page=${SETTINGS_PAGE}`,
];
for (const url of urls) {
await page.goto(url);
const heading = page.getByRole('heading', { name: /Laposta Woocommerce instellingen/i });
if (await heading.count()) {
await expect(heading).toBeVisible();
return;
}
}
throw new Error(`Could not open Laposta settings page with slug "${SETTINGS_PAGE}".`);
}
export async function saveSettings(page: Page) {
const saveButton = page.getByRole('button', { name: /(Save Changes|Wijzigingen opslaan)/i });
await Promise.all([page.waitForURL(/laposta_woocommerce_options/), saveButton.click()]);
await page.waitForLoadState('domcontentloaded');
}
export function buildCheckboxLabel(): string {
return `${CHECKBOX_LABEL_PREFIX} ${Date.now()}`;
}
export function buildRandomEmail(): string {
const randomPart = Math.random().toString(36).slice(2, 10);
return `laposta-woocommerce-e2e+${Date.now()}-${randomPart}@example.com`;
}
export async function withCheckoutLabel(page: Page, label: string, action: () => Promise): Promise {
await gotoSettingsPage(page);
const labelInput = page.locator('#laposta-checkout-title');
const originalLabel = await labelInput.inputValue();
await labelInput.fill(label);
await saveSettings(page);
try {
return await action();
} finally {
await gotoSettingsPage(page);
await page.locator('#laposta-checkout-title').fill(originalLabel);
await saveSettings(page);
}
}
export async function resolveLapostaConfig(page: Page): Promise {
await gotoSettingsPage(page);
const apiKeyInput = page.locator('#laposta-api_key');
let apiKey = (await apiKeyInput.inputValue()).trim();
let didUpdateSettings = false;
if (!apiKey && API_KEY_FALLBACK) {
await apiKeyInput.fill(API_KEY_FALLBACK);
apiKey = API_KEY_FALLBACK;
didUpdateSettings = true;
}
const selectedList = page.locator('input[name="laposta-checkout-list"]:checked').first();
let listId = '';
if (await selectedList.count()) {
listId = (await selectedList.inputValue()).trim();
}
if (!listId && TEST_LIST_ID_FALLBACK) {
const fallbackRadio = page.locator(`input[name="laposta-checkout-list"][value="${TEST_LIST_ID_FALLBACK}"]`).first();
if (await fallbackRadio.count()) {
await fallbackRadio.check();
listId = TEST_LIST_ID_FALLBACK;
didUpdateSettings = true;
}
}
if (didUpdateSettings) {
await saveSettings(page);
await gotoSettingsPage(page);
apiKey = (await page.locator('#laposta-api_key').inputValue()).trim();
const refreshedSelectedList = page.locator('input[name="laposta-checkout-list"]:checked').first();
if (await refreshedSelectedList.count()) {
listId = (await refreshedSelectedList.inputValue()).trim();
}
}
if (!apiKey) {
throw new Error('No Laposta API key configured for the WooCommerce plugin.');
}
if (!listId) {
throw new Error('No Laposta list configured for the WooCommerce plugin.');
}
return { apiKey, listId };
}
export async function getFirstStoreProductId(page: Page): Promise {
const response = await page.request.get('/wp-json/wc/store/v1/products?per_page=1');
expect(response.ok()).toBeTruthy();
const products = (await response.json()) as Array<{ id: number }>;
if (!products.length || !products[0].id) {
throw new Error('No published WooCommerce products found for checkout test.');
}
return products[0].id;
}
export async function openCheckoutWithProduct(page: Page, productId: number) {
await page.goto(`/?add-to-cart=${productId}`, { waitUntil: 'domcontentloaded' });
for (const url of CHECKOUT_URLS) {
await page.goto(url, { waitUntil: 'domcontentloaded' });
const classicCheckout = page.locator('form.checkout');
if (await classicCheckout.count()) {
await expect(classicCheckout.first()).toBeVisible();
return;
}
}
throw new Error('Could not open a classic WooCommerce checkout form.');
}
export async function withCheckoutBlock(page: Page, action: (checkoutPageId: number) => Promise): Promise {
const nonce = await getWordPressRestNonce(page);
const checkoutPage = await getCheckoutPage(page, nonce);
await updatePageContent(page, nonce, checkoutPage.id, CHECKOUT_BLOCK_CONTENT);
try {
return await action(checkoutPage.id);
} finally {
await updatePageContent(page, nonce, checkoutPage.id, checkoutPage.content);
}
}
export async function withCheckoutPriorityFixture(action: (sentinelLabel: string) => Promise): Promise {
const fixtureDirectory = path.dirname(PRIORITY_FIXTURE_PATH);
let createdFixtureDirectory = false;
try {
await access(fixtureDirectory);
} catch {
createdFixtureDirectory = true;
}
await mkdir(fixtureDirectory, { recursive: true });
try {
await writeFile(PRIORITY_FIXTURE_PATH, PRIORITY_FIXTURE_CONTENT, 'utf8');
return await action(PRIORITY_FIXTURE_LABEL);
} finally {
await rm(PRIORITY_FIXTURE_PATH, { force: true });
if (createdFixtureDirectory) {
await rmdir(fixtureDirectory);
}
}
}
export async function openCheckoutBlockEditor(page: Page, checkoutPageId: number) {
await page.goto(`/wp-admin/post.php?post=${checkoutPageId}&action=edit`, { waitUntil: 'domcontentloaded' });
await expect(page.locator('body')).toHaveClass(/block-editor-page/, { timeout: 30000 });
await expect
.poll(
() =>
page.evaluate(() => {
const editorWindow = window as typeof window & {
wp?: {
data?: {
select: (store: string) => {
getBlocks?: () => Array<{ name: string; innerBlocks?: unknown[] }>;
};
};
};
};
const blocks = editorWindow.wp?.data?.select('core/block-editor')?.getBlocks?.() || [];
const hasAdditionalInformationBlock = (items: Array<{ name: string; innerBlocks?: unknown[] }>): boolean =>
items.some(
(block) =>
block.name === 'woocommerce/checkout-additional-information-block' ||
hasAdditionalInformationBlock(
(block.innerBlocks || []) as Array<{ name: string; innerBlocks?: unknown[] }>,
),
);
return hasAdditionalInformationBlock(blocks);
}),
{ timeout: 30000 },
)
.toBe(true);
}
export async function expectCheckoutBlockEditorLabel(page: Page, checkboxLabel: string) {
const editorCanvas = page.locator('iframe[name="editor-canvas"]');
if (await editorCanvas.count()) {
await expect(page.frameLocator('iframe[name="editor-canvas"]').getByText(checkboxLabel, { exact: true })).toBeVisible({
timeout: 30000,
});
return;
}
await expect(page.getByText(checkboxLabel, { exact: true })).toBeVisible({ timeout: 30000 });
}
export async function openCheckoutBlockWithProduct(page: Page, productId: number) {
await page.goto(`/?add-to-cart=${productId}`, { waitUntil: 'domcontentloaded' });
for (const url of CHECKOUT_URLS) {
await page.goto(url, { waitUntil: 'domcontentloaded' });
const checkoutBlock = page.locator('.wc-block-checkout');
if (await checkoutBlock.count()) {
await expect(checkoutBlock.first()).toBeVisible();
await expect(checkoutBlock.first()).not.toHaveClass(/is-loading/, { timeout: 30000 });
return;
}
}
throw new Error('Could not open a WooCommerce Checkout Block.');
}
export async function placeWooOrder(page: Page, email: string, options: { subscribe: boolean }) {
await fillCheckoutField(page, '#billing_first_name', 'Laposta');
await fillCheckoutField(page, '#billing_last_name', 'E2E');
await fillCheckoutField(page, '#billing_address_1', 'Teststraat 1');
await fillCheckoutField(page, '#billing_postcode', '1011AB');
await fillCheckoutField(page, '#billing_city', 'Amsterdam');
await fillCheckoutField(page, '#billing_phone', '0612345678');
await fillCheckoutField(page, '#billing_email', email);
const checkbox = page.locator('#nieuwsbrief_signup');
await expect(checkbox).toBeVisible();
if (options.subscribe) {
await checkbox.check();
} else {
await checkbox.uncheck();
}
await Promise.all([
page.waitForURL(/order-received|bestelling-ontvangen/i, { timeout: 30000 }),
page.locator('#place_order').click(),
]);
}
export async function placeWooBlockOrder(
page: Page,
email: string,
checkboxLabel: string,
options: { subscribe: boolean },
) {
await fillFirstVisibleCheckoutField(page, ['#email'], email);
const editAddressButton = page.getByRole('button', {
name: /(Edit (shipping|billing) address|Verzendadres bewerken|Factuuradres bewerken)/i,
});
if (await editAddressButton.first().isVisible()) {
await editAddressButton.first().click();
}
await fillFirstVisibleCheckoutField(page, ['#shipping-first_name', '#billing-first_name'], 'Laposta');
await fillFirstVisibleCheckoutField(page, ['#shipping-last_name', '#billing-last_name'], 'E2E');
await fillFirstVisibleCheckoutField(page, ['#shipping-address_1', '#billing-address_1'], 'Teststraat 1');
await fillFirstVisibleCheckoutField(page, ['#shipping-postcode', '#billing-postcode'], '1011AB');
await fillFirstVisibleCheckoutField(page, ['#shipping-city', '#billing-city'], 'Amsterdam');
const phone = page.locator('#billing-phone');
if (await phone.isVisible()) {
await phone.fill('0612345678');
}
const checkbox = page.getByRole('checkbox', { name: checkboxLabel, exact: true });
await expect(checkbox).toBeVisible();
if (options.subscribe) {
await checkbox.check();
} else {
await checkbox.uncheck();
}
await Promise.all([
page.waitForURL(/order-received|bestelling-ontvangen/i, { timeout: 30000 }),
page.getByRole('button', { name: /(Place Order|Bestelling plaatsen|Plaats bestelling)/i }).click(),
]);
}
export async function findLapostaMember(config: LapostaConfig, email: string) {
const response = await fetch(`https://api.laposta.nl/v2/member?list_id=${encodeURIComponent(config.listId)}`, {
headers: {
Authorization: `Basic ${Buffer.from(`${config.apiKey}:`).toString('base64')}`,
},
});
const body = (await response.json()) as {
data?: Array<{ member?: { member_id?: string; email?: string } }>;
error?: { message?: string };
};
if (!response.ok) {
throw new Error(`Laposta API request failed: ${body.error?.message || response.statusText}`);
}
return body.data?.find((entry) => entry.member?.email === email) || null;
}
export async function deleteLapostaMemberIfExists(config: LapostaConfig, email: string) {
const member = await findLapostaMember(config, email);
const memberId = member?.member?.member_id;
if (!memberId) {
return;
}
const response = await fetch(
`https://api.laposta.nl/v2/member/${encodeURIComponent(memberId)}?list_id=${encodeURIComponent(config.listId)}`,
{
method: 'DELETE',
headers: {
Authorization: `Basic ${Buffer.from(`${config.apiKey}:`).toString('base64')}`,
},
},
);
if (!response.ok) {
const body = (await response.json()) as { error?: { message?: string } };
throw new Error(`Laposta member cleanup failed: ${body.error?.message || response.statusText}`);
}
}
export async function expectLapostaMemberState(config: LapostaConfig, email: string, shouldExist: boolean) {
await expect
.poll(async () => {
const member = await findLapostaMember(config, email);
return Boolean(member?.member?.member_id);
}, { timeout: 15000, intervals: [500, 1000, 2000] })
.toBe(shouldExist);
}
function parseCheckoutUrls(rawValue: string | undefined, fallback: string[]): string[] {
if (!rawValue) {
return fallback;
}
const parsed = rawValue
.split(',')
.map((item) => normalizePath(item))
.filter(Boolean);
return parsed.length ? parsed : fallback;
}
function normalizePath(input: string): string {
const value = input.trim();
if (!value) {
return '';
}
const withLeadingSlash = value.startsWith('/') ? value : `/${value}`;
return withLeadingSlash.endsWith('/') ? withLeadingSlash : `${withLeadingSlash}/`;
}
async function getWordPressRestNonce(page: Page): Promise {
await page.goto('/wp-admin/post-new.php?post_type=page', { waitUntil: 'domcontentloaded' });
const nonce = await page.evaluate(() => {
const settings = (window as typeof window & { wpApiSettings?: { nonce?: string } }).wpApiSettings;
return settings?.nonce || '';
});
if (!nonce) {
throw new Error('Could not resolve the WordPress REST API nonce.');
}
return nonce;
}
async function getCheckoutPage(page: Page, nonce: string): Promise<{ id: number; content: string }> {
const response = await page.request.get('/wp-json/wp/v2/pages?context=edit&per_page=100', {
headers: { 'X-WP-Nonce': nonce },
});
expect(response.ok()).toBeTruthy();
const pages = (await response.json()) as Array<{
id: number;
link: string;
content: { raw: string };
}>;
const checkoutPage = pages.find(({ link }) => CHECKOUT_URLS.includes(normalizePath(new URL(link).pathname)));
if (!checkoutPage) {
throw new Error(`Could not resolve a checkout page for: ${CHECKOUT_URLS.join(', ')}`);
}
return { id: checkoutPage.id, content: checkoutPage.content.raw };
}
async function updatePageContent(page: Page, nonce: string, pageId: number, content: string) {
const response = await page.request.post(`/wp-json/wp/v2/pages/${pageId}`, {
headers: { 'X-WP-Nonce': nonce },
data: { content },
});
expect(response.ok()).toBeTruthy();
}
async function fillCheckoutField(page: Page, selector: string, value: string) {
const field = page.locator(selector);
await expect(field).toBeVisible();
await field.fill(value);
}
async function fillFirstVisibleCheckoutField(page: Page, selectors: string[], value: string) {
for (const selector of selectors) {
const field = page.locator(selector);
if (await field.isVisible()) {
await field.fill(value);
return;
}
}
throw new Error(`Could not find a visible Checkout Block field: ${selectors.join(', ')}`);
}