- Introduced `.prettierrc.json` to enforce consistent code formatting across the project. - Updated unit and e2e test files to address formatting issues, improve readability, and ensure alignment with the new Prettier configuration.
472 lines
18 KiB
JavaScript
472 lines
18 KiB
JavaScript
import { expect, test } from "@playwright/test";
|
|
import {
|
|
CARD_CUSTOMER_ID,
|
|
DEFAULT_ORDER_ID,
|
|
REGULAR_CUSTOMER_ID,
|
|
createMobilePosFixture,
|
|
createPaymentIntent,
|
|
getStoredPosSnapshot,
|
|
setupMobilePosPage,
|
|
suppressVueDevtoolsOverlay,
|
|
waitForMobileNextStepCooldown,
|
|
} from "./support/mobilePos.js";
|
|
|
|
test.describe("POS mobile card payments", () => {
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
test.skip(testInfo.project.name !== "chromium-mobile", "Mobile card payment suite is scoped to chromium-mobile.");
|
|
await suppressVueDevtoolsOverlay(page);
|
|
});
|
|
|
|
test("card mode entry sets customer 999 and preserves order context", async ({ page }) => {
|
|
const fixture = createMobilePosFixture();
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-card-entry-token",
|
|
seedState: {
|
|
customerId: null,
|
|
reg: "ZZ00000",
|
|
reference: "CARD-CTX-REF",
|
|
includePrimaryItem: false,
|
|
vehicleType: 53,
|
|
},
|
|
route: {
|
|
step: 1,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-next-step")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await expect(page.getByTestId("pos-mobile-direct-card-payment")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-direct-card-payment").click();
|
|
await expect(page.getByTestId("pos-mobile-customer-popup")).toBeHidden({ timeout: 10_000 });
|
|
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const snapshot = await getStoredPosSnapshot(page);
|
|
return snapshot?.metadata?.customerId ?? null;
|
|
},
|
|
{ timeout: 10_000 }
|
|
)
|
|
.toBe(CARD_CUSTOMER_ID);
|
|
|
|
const snapshot = await getStoredPosSnapshot(page);
|
|
expect(snapshot?.vehicles?.vehicle_1?.reg).toBe("ZZ00000");
|
|
expect(snapshot?.metadata?.reference).toBe("CARD-CTX-REF");
|
|
});
|
|
|
|
test("@smoke direct card flow bridges step 1 to capture and resets to scanner", async ({ page }) => {
|
|
const fixture = createMobilePosFixture();
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-card-bridge-token",
|
|
seedState: {
|
|
customerId: null,
|
|
reg: "CD99999",
|
|
reference: "CARD-BRIDGE-REF",
|
|
includePrimaryItem: false,
|
|
vehicleType: 53,
|
|
},
|
|
route: {
|
|
step: 1,
|
|
},
|
|
});
|
|
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await expect(page.getByTestId("pos-mobile-direct-card-payment")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-direct-card-payment").click();
|
|
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await waitForMobileNextStepCooldown(page);
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(1);
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-stripe-create-intent").click();
|
|
await expect(page.getByTestId("pos-stripe-capture-intent")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-stripe-capture-intent").click();
|
|
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(1);
|
|
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
|
|
await expect
|
|
.poll(
|
|
() => {
|
|
const currentUrl = new URL(page.url());
|
|
return currentUrl.searchParams.get("step");
|
|
},
|
|
{ timeout: 12_000 }
|
|
)
|
|
.toBe("1");
|
|
await expect(page.getByTestId("pos-mobile-next-step")).toBeVisible({ timeout: 10_000 });
|
|
});
|
|
|
|
test("step 2 transitions to mobile card payment step 3 for customer 999", async ({ page }) => {
|
|
const fixture = createMobilePosFixture();
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-card-step-transition-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
includePrimaryItem: true,
|
|
primaryItemId: 53,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
|
|
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(1);
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page).toHaveURL(/step=3/);
|
|
expect(fixture.requestCounters.markAsCompleted).toBe(0);
|
|
});
|
|
|
|
test("reader unavailable state is recoverable and non-fatal", async ({ page }) => {
|
|
const fixture = createMobilePosFixture({
|
|
readers: [],
|
|
});
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-card-reader-unavailable-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
includePrimaryItem: true,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-stripe-no-readers")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-stripe-reader-unavailable-message")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toHaveCount(0);
|
|
await expect(page.getByTestId("pos-stripe-error-message")).toHaveCount(0);
|
|
|
|
fixture.readers = [
|
|
{
|
|
id: "reader_online_2",
|
|
label: "Recovered Reader",
|
|
status: "online",
|
|
action: null,
|
|
},
|
|
];
|
|
|
|
await page.getByTestId("pos-stripe-no-readers").click();
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeEnabled({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-stripe-selected-reader")).toContainText("Recovered Reader");
|
|
expect(fixture.requestCounters.paymentIntentCreate).toBe(0);
|
|
});
|
|
|
|
test("reader available creates intent once and reaches capture state", async ({ page }) => {
|
|
const fixture = createMobilePosFixture();
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-card-create-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
includePrimaryItem: true,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeEnabled();
|
|
await page.getByTestId("pos-stripe-create-intent").click();
|
|
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentCreate, { timeout: 10_000 }).toBe(1);
|
|
await expect(page.getByTestId("pos-stripe-capture-intent")).toBeVisible({ timeout: 10_000 });
|
|
expect(fixture.requestCounters.markAsCompleted).toBe(0);
|
|
});
|
|
|
|
test("step 3 back offers resume or cancel when payment is active", async ({ page }) => {
|
|
const fixture = createMobilePosFixture();
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-card-cancel-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
includePrimaryItem: true,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-stripe-create-intent").click();
|
|
await expect(page.getByTestId("pos-stripe-capture-intent")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.getByTestId("pos-mobile-step-3-back").click();
|
|
await expect(page.locator(".swal2-popup")).toBeVisible({ timeout: 10_000 });
|
|
await page.locator(".swal2-confirm").click();
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
|
|
expect(fixture.requestCounters.paymentIntentDelete).toBe(0);
|
|
|
|
await page.getByTestId("pos-mobile-step-3-back").click();
|
|
await expect(page.locator(".swal2-popup")).toBeVisible({ timeout: 10_000 });
|
|
await page.locator(".swal2-deny").click();
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentDelete, { timeout: 10_000 }).toBe(1);
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await expect(page).toHaveURL(/step=2/);
|
|
expect(fixture.requestCounters.markAsCompleted).toBe(0);
|
|
});
|
|
|
|
test("capture success completes once and resets scanner flow", async ({ page }) => {
|
|
const fixture = createMobilePosFixture();
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-card-capture-success-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
includePrimaryItem: true,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-stripe-create-intent").click();
|
|
await expect(page.getByTestId("pos-stripe-capture-intent")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.getByTestId("pos-stripe-capture-intent").click();
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(1);
|
|
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
|
|
await expect
|
|
.poll(
|
|
() => {
|
|
const currentUrl = new URL(page.url());
|
|
return currentUrl.searchParams.get("step");
|
|
},
|
|
{ timeout: 12_000 }
|
|
)
|
|
.toBe("1");
|
|
await expect(page.getByTestId("pos-mobile-next-step")).toBeVisible({ timeout: 10_000 });
|
|
});
|
|
|
|
test("guardrail: completion never runs before capture success", async ({ page }) => {
|
|
const fixture = createMobilePosFixture();
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-card-guardrail-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
includePrimaryItem: true,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-stripe-create-intent").click();
|
|
await expect(page.getByTestId("pos-stripe-capture-intent")).toBeVisible({ timeout: 10_000 });
|
|
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 2_500 }).toBe(0);
|
|
});
|
|
|
|
test("step 3 bootstrap recovery fetches existing requires_capture intent", async ({ page }) => {
|
|
const fixture = createMobilePosFixture({
|
|
paymentIntentsByOrderId: {
|
|
[DEFAULT_ORDER_ID]: createPaymentIntent(DEFAULT_ORDER_ID, {
|
|
status: "requires_capture",
|
|
}),
|
|
},
|
|
});
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-card-recovery-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
includePrimaryItem: true,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentGet, { timeout: 10_000 }).toBeGreaterThan(0);
|
|
await expect(page.getByTestId("pos-stripe-capture-intent")).toBeVisible({ timeout: 10_000 });
|
|
expect(fixture.requestCounters.paymentIntentCreate).toBe(0);
|
|
});
|
|
|
|
test("step 3 bootstrap recovery completes already succeeded intent once", async ({ page }) => {
|
|
const fixture = createMobilePosFixture({
|
|
paymentIntentsByOrderId: {
|
|
[DEFAULT_ORDER_ID]: createPaymentIntent(DEFAULT_ORDER_ID, {
|
|
status: "succeeded",
|
|
}),
|
|
},
|
|
});
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-card-succeeded-recovery-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
includePrimaryItem: true,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentGet, { timeout: 10_000 }).toBeGreaterThan(0);
|
|
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
|
|
expect(fixture.requestCounters.paymentIntentCreate).toBe(0);
|
|
expect(fixture.requestCounters.paymentIntentCapture).toBe(0);
|
|
await expect
|
|
.poll(
|
|
() => {
|
|
const currentUrl = new URL(page.url());
|
|
return currentUrl.searchParams.get("step");
|
|
},
|
|
{ timeout: 12_000 }
|
|
)
|
|
.toBe("1");
|
|
});
|
|
|
|
test("returning to step 2 and re-entering step 3 does not duplicate order items", async ({ page }) => {
|
|
const fixture = createMobilePosFixture();
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-card-step-reentry-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
includePrimaryItem: true,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(1);
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.getByTestId("pos-mobile-step-3-back").click();
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.waitForTimeout(2100);
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toBeVisible({ timeout: 10_000 });
|
|
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(1);
|
|
await expect.poll(() => fixture.requestCounters.orderItemsGet, { timeout: 10_000 }).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("payment-intent get/create/capture failures surface retryable error states", async ({ page }) => {
|
|
const fixture = createMobilePosFixture({
|
|
failureBudget: {
|
|
paymentIntentGet: 0,
|
|
paymentIntentCreate: 2,
|
|
paymentIntentCapture: 2,
|
|
},
|
|
});
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-card-error-recovery-token",
|
|
seedState: {
|
|
customerId: CARD_CUSTOMER_ID,
|
|
includePrimaryItem: true,
|
|
},
|
|
route: {
|
|
step: 3,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: CARD_CUSTOMER_ID,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-stripe-create-intent").click();
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentCreate, { timeout: 10_000 }).toBe(2);
|
|
await expect(page.getByTestId("pos-stripe-error")).toBeVisible({ timeout: 10_000 });
|
|
|
|
fixture.failureBudget.paymentIntentGet = 2;
|
|
await page.getByTestId("pos-stripe-error").click();
|
|
await expect(page.getByTestId("pos-stripe-error")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-stripe-error").click();
|
|
await expect(page.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.getByTestId("pos-stripe-create-intent").click();
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentCreate, { timeout: 10_000 }).toBe(3);
|
|
await expect(page.getByTestId("pos-stripe-capture-intent")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.getByTestId("pos-stripe-capture-intent").click();
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(2);
|
|
await expect(page.getByTestId("pos-stripe-error")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-stripe-error").click();
|
|
await expect(page.getByTestId("pos-stripe-capture-intent")).toBeVisible({ timeout: 10_000 });
|
|
|
|
await page.getByTestId("pos-stripe-capture-intent").click();
|
|
await expect.poll(() => fixture.requestCounters.paymentIntentCapture, { timeout: 10_000 }).toBe(3);
|
|
await expect.poll(() => fixture.requestCounters.markAsCompleted, { timeout: 10_000 }).toBe(1);
|
|
});
|
|
|
|
test("regression: non-card mobile customer still follows step-2 complete/reset path", async ({ page }) => {
|
|
const regularCustomerId = REGULAR_CUSTOMER_ID;
|
|
const fixture = createMobilePosFixture({
|
|
ordersById: {
|
|
[DEFAULT_ORDER_ID]: {
|
|
id: DEFAULT_ORDER_ID,
|
|
customer_id: regularCustomerId,
|
|
department_id: 1,
|
|
reference: "REF-9201",
|
|
notes: "",
|
|
reg_1: "AB12345",
|
|
reg_2: "",
|
|
reg_3: "",
|
|
invoice_collection_id: null,
|
|
booking_id: null,
|
|
completed_at: null,
|
|
created_at: "2026-01-01T10:00:00.000Z",
|
|
},
|
|
},
|
|
});
|
|
|
|
await setupMobilePosPage(page, fixture, {
|
|
token: "mobile-non-card-regression-token",
|
|
seedState: {
|
|
customerId: regularCustomerId,
|
|
includePrimaryItem: true,
|
|
},
|
|
route: {
|
|
step: 2,
|
|
orderId: DEFAULT_ORDER_ID,
|
|
customerId: regularCustomerId,
|
|
},
|
|
});
|
|
|
|
await expect(page.getByTestId("pos-mobile-step-2")).toBeVisible({ timeout: 10_000 });
|
|
await page.getByTestId("pos-mobile-next-step").click();
|
|
await expect.poll(() => fixture.requestCounters.orderItemsPost, { timeout: 10_000 }).toBe(1);
|
|
await expect
|
|
.poll(
|
|
() => {
|
|
const currentUrl = new URL(page.url());
|
|
return currentUrl.searchParams.get("step");
|
|
},
|
|
{ timeout: 12_000 }
|
|
)
|
|
.toBe("1");
|
|
|
|
expect(fixture.requestCounters.markAsCompleted).toBe(0);
|
|
await expect(page.getByTestId("pos-mobile-step-3")).toHaveCount(0);
|
|
});
|
|
});
|