- Migrate postinstall script to `postinstall-sync-playwright-root-links.mjs` for streamlined path resolution. - Replace `axios` v1.15.0 with v1.13.5 and downgrade `vite` from v8.0.5 to v7.1.11. - Update testing code for consistent formatting and enhanced readability (e.g., `poll` and `catch` calls). - Remove unused or redundant dependency flags and align `package-lock.json` with new configuration.
223 lines
7.1 KiB
TypeScript
223 lines
7.1 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 {
|
|
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 completeBookingCreationFlow(page: Page, data: BookingFlowData) {
|
|
const { departmentId, registrationNumber, reference, poNumber, notes } = data;
|
|
|
|
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);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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);
|
|
await page.click("#confirm-booking-button", { force: true });
|
|
await expect(page).toHaveURL(/\/user/);
|
|
}
|