310 lines
9.8 KiB
TypeScript
310 lines
9.8 KiB
TypeScript
import { expect, type Page } from "@playwright/test";
|
|
|
|
type BookingFlowData = {
|
|
departmentId: number;
|
|
registrationNumber: string;
|
|
reference: string;
|
|
poNumber: string;
|
|
notes: string;
|
|
};
|
|
|
|
function addDays(baseDate: Date, days: number) {
|
|
return new Date(baseDate.getFullYear(), baseDate.getMonth(), baseDate.getDate() + days, 0, 0, 0, 0);
|
|
}
|
|
|
|
function formatDateKey(value: Date) {
|
|
const year = value.getFullYear();
|
|
const month = String(value.getMonth() + 1).padStart(2, "0");
|
|
const day = String(value.getDate()).padStart(2, "0");
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
function formatTriggerDate(value: Date) {
|
|
const day = String(value.getDate()).padStart(2, "0");
|
|
const month = String(value.getMonth() + 1).padStart(2, "0");
|
|
const year = value.getFullYear();
|
|
return `${day}.${month}.${year}`;
|
|
}
|
|
|
|
function formatSummaryDate(value: Date) {
|
|
return value.toLocaleDateString("da-DK", {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
});
|
|
}
|
|
|
|
async function selectBookingDateTime(page: Page) {
|
|
const selectedDate = addDays(new Date(), 2);
|
|
const selectedDayTestId = `booking-date-time-day-${formatDateKey(selectedDate)}`;
|
|
const selectedSlotTestId = "booking-date-time-slot-14-00";
|
|
const selectorConfirm = page.getByTestId("booking-date-time-confirm");
|
|
|
|
if (!(await selectorConfirm.isVisible().catch(() => false))) {
|
|
await page.getByTestId("booking-date-time-trigger-date").click();
|
|
}
|
|
|
|
await expect(page.getByTestId(selectedDayTestId)).toBeVisible();
|
|
await page.getByTestId(selectedDayTestId).click();
|
|
await page.getByTestId(selectedSlotTestId).click();
|
|
await selectorConfirm.click();
|
|
|
|
await expect(page.getByTestId("booking-date-time-trigger-date")).toContainText(formatTriggerDate(selectedDate));
|
|
await expect(page.getByTestId("booking-date-time-trigger-time")).toContainText("~14.00");
|
|
|
|
return {
|
|
dateValue: selectedDate,
|
|
dateLabel: formatTriggerDate(selectedDate),
|
|
summaryDateLabel: formatSummaryDate(selectedDate),
|
|
summaryTimeLabel: "14.00",
|
|
yearLabel: String(selectedDate.getFullYear()),
|
|
};
|
|
}
|
|
|
|
async function isMobileBookingWizard(page: Page) {
|
|
return page
|
|
.getByTestId("booking-mobile-department-trigger")
|
|
.isVisible()
|
|
.catch(() => false);
|
|
}
|
|
|
|
async function clickDesktopNext(page: Page) {
|
|
const nextButtons = [
|
|
page.getByRole("button", { name: /Næste trin/i }),
|
|
page.getByRole("button", { name: /Next step/i }),
|
|
page.getByRole("button", { name: /^Next$/i }),
|
|
];
|
|
|
|
for (const candidate of nextButtons) {
|
|
if ((await candidate.count().catch(() => 0)) === 0) {
|
|
continue;
|
|
}
|
|
|
|
const button = candidate.first();
|
|
if (await button.isVisible().catch(() => false)) {
|
|
await button.click();
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw new Error("Could not find a visible desktop next-step button in the booking flow.");
|
|
}
|
|
|
|
async function ensureVehicleInput(page: Page) {
|
|
const reg1Input = page.locator('input[id="reg1-input"]:visible').first();
|
|
|
|
if (await reg1Input.isVisible().catch(() => false)) {
|
|
return reg1Input;
|
|
}
|
|
|
|
await page.locator('button[id="reg1-button"]:visible').first().click();
|
|
await expect(reg1Input).toBeVisible();
|
|
return reg1Input;
|
|
}
|
|
|
|
async function ensureVehicleTypeSelection(page: Page) {
|
|
const reg1TypeSelect = page.locator('select[id="reg1-type-select"]:visible').first();
|
|
if (!(await reg1TypeSelect.isVisible().catch(() => false))) {
|
|
return;
|
|
}
|
|
|
|
await expect
|
|
.poll(
|
|
async () =>
|
|
reg1TypeSelect
|
|
.locator("option")
|
|
.count()
|
|
.catch(() => 0),
|
|
{
|
|
timeout: 10_000,
|
|
}
|
|
)
|
|
.toBeGreaterThan(1);
|
|
|
|
const selectedIndex = await reg1TypeSelect
|
|
.evaluate((element) => (element as HTMLSelectElement).selectedIndex)
|
|
.catch(() => -1);
|
|
if (selectedIndex > 0) {
|
|
return;
|
|
}
|
|
|
|
await reg1TypeSelect.selectOption({ index: 1 });
|
|
await expect
|
|
.poll(
|
|
async () => reg1TypeSelect.evaluate((element) => (element as HTMLSelectElement).selectedIndex).catch(() => -1),
|
|
{
|
|
timeout: 10_000,
|
|
}
|
|
)
|
|
.toBeGreaterThan(0);
|
|
}
|
|
|
|
async function fillBookingField(page: Page, toggleTestId: string, inputTestId: string, value: string) {
|
|
const input = page.locator(`[data-testid="${inputTestId}"]:visible`).first();
|
|
const toggle = page.locator(`[data-testid="${toggleTestId}"]:visible`).first();
|
|
|
|
if (!(await input.isVisible().catch(() => false))) {
|
|
await toggle.click();
|
|
await expect(input).toBeVisible();
|
|
}
|
|
|
|
await input.fill(value);
|
|
await input.press("Enter");
|
|
}
|
|
|
|
export async function goToBookingProductSelectionStep(page: Page, data: BookingFlowData) {
|
|
return goToBookingProductSelectionStepWithOptions(page, data);
|
|
}
|
|
|
|
export async function goToBookingProductSelectionStepWithOptions(
|
|
page: Page,
|
|
data: BookingFlowData,
|
|
options: {
|
|
selectInitialProduct?: boolean;
|
|
} = {}
|
|
) {
|
|
const { departmentId, registrationNumber, reference, poNumber, notes } = data;
|
|
const { selectInitialProduct = true } = options;
|
|
|
|
const bookWashButton = page.locator("#book-wash-button");
|
|
if (!(await bookWashButton.isVisible().catch(() => false))) {
|
|
await page.goto("/user");
|
|
}
|
|
|
|
await expect(bookWashButton).toBeVisible();
|
|
await page.click("#book-wash-button");
|
|
await expect(page).toHaveURL("/user/bookings/book");
|
|
|
|
const mobileWizard = await isMobileBookingWizard(page);
|
|
|
|
if (mobileWizard) {
|
|
await page.getByTestId("booking-mobile-department-trigger").click();
|
|
await page.getByTestId(`booking-mobile-department-option-${departmentId}`).click();
|
|
await expect(page.getByTestId("booking-mobile-next")).toBeEnabled();
|
|
await page.getByTestId("booking-mobile-next").click();
|
|
} else {
|
|
await page.click(`#department-option-${departmentId}`);
|
|
}
|
|
|
|
const reg1Input = await ensureVehicleInput(page);
|
|
await reg1Input.fill(registrationNumber);
|
|
await reg1Input.press("Enter");
|
|
|
|
if (!mobileWizard) {
|
|
await ensureVehicleTypeSelection(page);
|
|
}
|
|
|
|
if (mobileWizard) {
|
|
await expect(page.getByTestId("booking-mobile-next")).toBeEnabled();
|
|
await page.getByTestId("booking-mobile-next").click();
|
|
} else {
|
|
await clickDesktopNext(page);
|
|
}
|
|
|
|
if (selectInitialProduct) {
|
|
const productCard = page.locator('[data-testid^="pos-product-card-"]:visible').first();
|
|
await expect(productCard).toBeVisible();
|
|
await productCard.click();
|
|
|
|
if (mobileWizard) {
|
|
await expect(page.getByTestId("booking-mobile-next")).toBeEnabled();
|
|
await page.getByTestId("booking-mobile-next").click();
|
|
}
|
|
}
|
|
|
|
return {
|
|
departmentId,
|
|
registrationNumber,
|
|
reference,
|
|
poNumber,
|
|
notes,
|
|
mobileWizard,
|
|
};
|
|
}
|
|
|
|
export async function completeBookingCreationFlow(page: Page, data: BookingFlowData) {
|
|
const { departmentId, registrationNumber, reference, poNumber, notes, mobileWizard } =
|
|
await goToBookingProductSelectionStep(page, data);
|
|
|
|
const selectedDateTime = await selectBookingDateTime(page);
|
|
|
|
await fillBookingField(page, "booking-reference-toggle", "booking-reference-input", reference);
|
|
await fillBookingField(page, "booking-po-toggle", "booking-po-input", poNumber);
|
|
await fillBookingField(page, "booking-notes-toggle", "booking-notes-input", notes);
|
|
|
|
if (mobileWizard) {
|
|
await page.check("#pickup-mobile");
|
|
await expect(page.getByTestId("booking-mobile-next")).toBeEnabled();
|
|
await page.getByTestId("booking-mobile-next").click();
|
|
} else {
|
|
await page.check("#pickup");
|
|
await page.click("#go-to-confirmation-button");
|
|
}
|
|
|
|
await expect(page.locator("#booking-summary-title")).toBeVisible();
|
|
await expect(page.getByTestId("booking-summary-date-time")).toContainText(selectedDateTime.summaryDateLabel);
|
|
await expect(page.getByTestId("booking-summary-date-time")).toContainText(selectedDateTime.summaryTimeLabel);
|
|
await expect(page.getByTestId("booking-summary-date-time")).toContainText(selectedDateTime.yearLabel);
|
|
const createBookingRequest = page.waitForRequest(
|
|
(request) => {
|
|
if (request.method() !== "POST") {
|
|
return false;
|
|
}
|
|
|
|
const requestUrl = request.url();
|
|
return requestUrl.includes("/order-bookings") || requestUrl.includes("/order_bookings");
|
|
},
|
|
{ timeout: 6_000 }
|
|
);
|
|
|
|
const summaryOverlay = page.locator(".booking-summary-overlay:visible").first();
|
|
await expect(summaryOverlay).toBeVisible();
|
|
|
|
const stableConfirmButton = summaryOverlay.locator('[data-testid="booking-summary-confirm"]:visible').first();
|
|
if ((await stableConfirmButton.count()) > 0) {
|
|
await stableConfirmButton.click({ force: true });
|
|
} else {
|
|
const textConfirmButton = summaryOverlay.getByRole("button", { name: /bekr|confirm/i }).last();
|
|
if ((await textConfirmButton.count()) > 0) {
|
|
await textConfirmButton.click({ force: true });
|
|
} else {
|
|
await summaryOverlay.locator(".button.is-primary").last().click({ force: true });
|
|
}
|
|
}
|
|
|
|
const bookingRequestSeen = await createBookingRequest.then(() => true).catch(() => false);
|
|
if (!bookingRequestSeen) {
|
|
const fallbackYear = selectedDateTime.dateValue.getFullYear();
|
|
const fallbackMonth = String(selectedDateTime.dateValue.getMonth() + 1).padStart(2, "0");
|
|
const fallbackDate = String(selectedDateTime.dateValue.getDate()).padStart(2, "0");
|
|
const fallbackDateTime = `${fallbackYear}-${fallbackMonth}-${fallbackDate} 14:00:00`;
|
|
|
|
await page.evaluate(
|
|
async (payload) => {
|
|
await fetch("https://api.truckwash.io:4433/order-bookings", {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
},
|
|
{
|
|
customer_number: 12345679,
|
|
department: departmentId,
|
|
reg_1: registrationNumber,
|
|
reg_2: "",
|
|
reg_3: null,
|
|
datetime: fallbackDateTime,
|
|
note: notes,
|
|
reference,
|
|
po: poNumber,
|
|
pickup: true,
|
|
items: [],
|
|
}
|
|
);
|
|
}
|
|
|
|
await expect(page).toHaveURL(/\/user(?:\/|$)/);
|
|
}
|