# Instructions - Following Playwright test failed. - Explain why, be concise, respect Playwright best practices. - Provide a snippet of code with the fix, if possible. # Test info - Name: src/manager/tests/e2e/booking/booking-adjust-move-in-date.spec.ts >> adjusts the move-in date of a scheduled booking to today - Location: src/manager/tests/e2e/booking/booking-adjust-move-in-date.spec.ts:30:7 # Error details ``` Error: expect(locator).toHaveCount(expected) failed Locator: getByTestId('app-page').getByTestId('app-page-content').locator('[data-test-id=app-details-card]:has([data-test-id=title]:has-text("Payments"))').locator('.ui-data-grid').locator('[data-test-id=ui-data-grid-table]:not(.disabled):not(.loading):not(.fetching)').getByTestId('ui-data-grid-table-with-data').locator('tr') Expected: 1 Received: 0 Timeout: 60000ms Call log: - Expect "toHaveCount" with timeout 60000ms - waiting for getByTestId('app-page').getByTestId('app-page-content').locator('[data-test-id=app-details-card]:has([data-test-id=title]:has-text("Payments"))').locator('.ui-data-grid').locator('[data-test-id=ui-data-grid-table]:not(.disabled):not(.loading):not(.fetching)').getByTestId('ui-data-grid-table-with-data').locator('tr') 123 × locator resolved to 0 elements - unexpected value "0" ``` # Page snapshot ```yaml - generic [active] [ref=f2e1]: - generic [ref=f2e6]: - navigation [ref=f2e7]: - generic [ref=f2e9]: - link [ref=f2e10] [cursor=pointer]: - /url: / - generic [ref=f2e11]: - generic [ref=f2e12]: KINNOVIS - generic [ref=f2e13]: Manager - button [ref=f2e15] [cursor=pointer] - list [ref=f2e19]: - generic [ref=f2e21]: Dashboard - link "Tasks" [ref=f2e23] [cursor=pointer]: - /url: /tasks - link "Facility maps" [ref=f2e30] [cursor=pointer]: - /url: /facility-map - generic [ref=f2e31]: 󰧾 - link "Analytics" [ref=f2e35] [cursor=pointer]: - /url: /dashboard - generic [ref=f2e36]: 󱖶 - generic [ref=f2e40]: Sales - link "Bookings" [ref=f2e42] [cursor=pointer]: - /url: /bookings - generic [ref=f2e43]: 󰇡 - link "Customers" [ref=f2e47] [cursor=pointer]: - /url: /customers - generic [ref=f2e48]: 󰀏 - link "Invoices" [ref=f2e52] [cursor=pointer]: - /url: /invoices - generic [ref=f2e53]: 󰷉 - link "Credit notes" [ref=f2e57] [cursor=pointer]: - /url: /credit-notes - link "Units" [ref=f2e64] [cursor=pointer]: - /url: /units - generic [ref=f2e65]: 󰍀 - generic [ref=f2e69]: Site management - link "Locations" [ref=f2e71] [cursor=pointer]: - /url: /locations - generic [ref=f2e72]: 󰟙 - link "Unit types" [ref=f2e76] [cursor=pointer]: - /url: /unit-types - generic [ref=f2e77]: 󰆧 - link "Protection Plans" [ref=f2e81] [cursor=pointer]: - /url: /insurances - generic [ref=f2e82]: 󰳌 - link "Deposits" [ref=f2e86] [cursor=pointer]: - /url: /deposits - generic [ref=f2e87]: 󱙆 - link "Products" [ref=f2e91] [cursor=pointer]: - /url: /products - generic [ref=f2e92]: 󰄑 - link "Discounts" [ref=f2e96] [cursor=pointer]: - /url: /discounts - generic [ref=f2e97]: 󰓼 - generic [ref=f2e101]: - option "Emails" [ref=f2e102] [cursor=pointer]: - generic [ref=f2e103]: 󰻨 - generic [ref=f2e107]: 󰅀 - text: 󱡰 󰁥 - link "Document templates" [ref=f2e111] [cursor=pointer]: - /url: /document-templates - generic [ref=f2e112]: 󰷉 - generic [ref=f2e116]: Admin - link "Integrations" [ref=f2e118] [cursor=pointer]: - /url: /connected-apps - generic [ref=f2e119]: 󱘖 - link "User & Roles" [ref=f2e123] [cursor=pointer]: - /url: /users - generic [ref=f2e124]: 󰭘 - generic [ref=f2e128]: - option "Booking Portal" [ref=f2e129] [cursor=pointer]: - generic [ref=f2e130]: 󱃁 - generic [ref=f2e134]: 󰅀 - text: 󰖟 󰟙 - generic [ref=f2e138]: - option "JaneAI" [ref=f2e139] [cursor=pointer]: - generic [ref=f2e140]: 󱙺 - generic [ref=f2e144]: 󰅀 - text: 󱜹 - generic [ref=f2e148]: Feedback - link "Voting Portal" [ref=f2e150] [cursor=pointer]: - /url: /voting-portal - generic [ref=f2e151]: 󰔔 - generic [ref=f2e157]: - button "AS Andreas Stadler andreas.stadler@storeroom.at" [ref=f2e160] [cursor=pointer]: - generic [ref=f2e161]: AS - generic [ref=f2e164]: - generic [ref=f2e165]: Andreas Stadler - generic "andreas.stadler@storeroom.at" [ref=f2e166] - generic [ref=f2e167]: 󰇙 - generic: 󰗊 󰅀 󰷖 󰍃 - button [ref=f2e168] [cursor=pointer]: - generic [ref=f2e169]: 󰋽 - main [ref=f2e171]: - generic [ref=f2e173]: - generic [ref=f2e175]: - generic [ref=f2e176]: - link "Bookings" [ref=f2e177] [cursor=pointer]: - /url: /bookings - generic [ref=f2e179]: 󰅂 - generic [ref=f2e180]: Booking 1466-3762-43 - generic [ref=f2e181]: Active - button [ref=f2e186] [cursor=pointer]: - generic [ref=f2e187]: - text: Actions - generic [ref=f2e188]: 󱨉 - generic: 󱕂 - generic [ref=f2e189]: - generic [ref=f2e190]: - tablist [ref=f2e191]: - generic [ref=f2e193]: - tab "Details" [selected] [ref=f2e194] [cursor=pointer] - tab "Timeline" [ref=f2e196] [cursor=pointer] - separator [ref=f2e198] - generic [ref=f2e199]: - generic [ref=f2e200]: - generic [ref=f2e202]: - heading "General" [level=3] [ref=f2e206] - table [ref=f2e209]: - rowgroup [ref=f2e210]: - row [ref=f2e211]: - rowheader "Customer" [ref=f2e212] - cell [ref=f2e213]: - link "Lauren Weimann" [ref=f2e216] [cursor=pointer]: - /url: /customers/21713904 - row [ref=f2e219]: - rowheader "Location" [ref=f2e220] - cell [ref=f2e221]: - link "Vienna South" [ref=f2e224] [cursor=pointer]: - /url: /locations/5000500050 - row [ref=f2e227]: - rowheader "Created at" [ref=f2e228] - cell "17 Aug 2026" [ref=f2e229] - row [ref=f2e232]: - rowheader "Move-in" [ref=f2e233] - cell "17 Aug 2026" [ref=f2e234] - row [ref=f2e237]: - rowheader "Move-out" [ref=f2e238] - cell "-" [ref=f2e239] - row [ref=f2e242]: - rowheader "Cancelled at" [ref=f2e243] - cell "-" [ref=f2e244] - row [ref=f2e247]: - rowheader "Deposit excl. VAT" [ref=f2e248] - cell "-" [ref=f2e249] - row [ref=f2e252]: - rowheader "Discount" [ref=f2e253] - cell "-" [ref=f2e254] - row [ref=f2e257]: - rowheader "Contract" [ref=f2e258] - cell "-" [ref=f2e259] - generic [ref=f2e262]: - generic [ref=f2e263]: - heading "Billing" [level=3] [ref=f2e267] - table [ref=f2e270]: - rowgroup [ref=f2e271]: - row [ref=f2e272]: - rowheader "Booking Plan" [ref=f2e273] - cell "Basic (1 week)" [ref=f2e274] - row [ref=f2e277]: - rowheader "Current period" [ref=f2e278] - cell "17 - 24 Aug 2026" [ref=f2e279] - row [ref=f2e282]: - rowheader "Amount excl. VAT" [ref=f2e283] - cell "€50.00" [ref=f2e284] - row [ref=f2e287]: - rowheader "Price adjustment" [ref=f2e288] - cell "-" [ref=f2e289] - row [ref=f2e292]: - rowheader "Next invoice" [ref=f2e293] - cell "24 Aug 2026" [ref=f2e294] - row [ref=f2e297]: - rowheader "Payment method" [ref=f2e298] - cell "-" [ref=f2e299] - row [ref=f2e302]: - rowheader "Payment option" [ref=f2e303] - cell "Automatic payment through Stripe" [ref=f2e304] - generic [ref=f2e307]: - generic [ref=f2e309]: - heading "Notes" [level=3] [ref=f2e311] - link [ref=f2e313] [cursor=pointer]: - /url: /bookings/1466376243/edit/notes - generic [ref=f2e314]: 󰲶 - table [ref=f2e318]: - rowgroup [ref=f2e319]: - row [ref=f2e320]: - rowheader "Note" [ref=f2e321] - cell "-" [ref=f2e322] - generic [ref=f2e325]: - heading "Items" [level=3] [ref=f2e329] - generic [ref=f2e332]: - generic [ref=f2e333]: - generic [ref=f2e334]: - link "Fantastic Box 4mvax2" [ref=f2e335] [cursor=pointer]: - /url: /unit-types/1287864011 - text: / - link "Unit Aj57YP" [ref=f2e338] [cursor=pointer]: - /url: /units/52768396 - generic [ref=f2e341]: €50.00 - generic [ref=f2e342]: - generic [ref=f2e343]: - strong [ref=f2e344]: Total excl. VAT - strong [ref=f2e345]: €50.00 - generic [ref=f2e346]: - generic [ref=f2e347]: VAT - generic [ref=f2e348]: €8.50 - generic [ref=f2e349]: - strong [ref=f2e350]: Total incl. VAT - strong [ref=f2e351]: €58.50 - generic [ref=f2e352]: - heading "Invoices" [level=3] [ref=f2e356] - generic [ref=f2e360]: - generic [ref=f2e362]: - generic [ref=f2e364]: - button "Select all" [ref=f2e372] [cursor=pointer]: - generic [ref=f2e373]: 󰄬 - generic [ref=f2e376]: - button "Mark as paid" [disabled] [ref=f2e379]: - generic [ref=f2e380]: 󰗡 - button "SEPA XML actions" [disabled] [ref=f2e385]: - generic [ref=f2e386]: 󱨉 - generic: 󰗡 󰅚 󰗖 - generic [ref=f2e391]: - button [ref=f2e392] [cursor=pointer] - generic: 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 - table [ref=f2e398]: - rowgroup [ref=f2e399]: - row [ref=f2e400]: - columnheader [ref=f2e401]: - generic [ref=f2e402]: - checkbox [ref=f2e403] - generic [ref=f2e404] [cursor=pointer]: 󰄱 - columnheader [ref=f2e406]: - button "Number" [ref=f2e407] [cursor=pointer]: - generic [ref=f2e410]: 󰁝 - columnheader [ref=f2e413]: - button "Status" [ref=f2e414] [cursor=pointer]: - generic [ref=f2e417]: 󰁝 - columnheader [ref=f2e420]: - button "Payment option" [ref=f2e421] [cursor=pointer]: - generic [ref=f2e424]: 󰁝 - columnheader [ref=f2e427]: - button "Due date" [ref=f2e428] [cursor=pointer]: - generic [ref=f2e431]: 󰁝 - columnheader [ref=f2e434]: - button "Unit" [ref=f2e435] [cursor=pointer]: - generic [ref=f2e438]: 󰁝 - columnheader "Discount" [ref=f2e441] - columnheader [ref=f2e445]: - button "Total (incl. VAT)" [ref=f2e446] [cursor=pointer]: - generic [ref=f2e447]: - generic [ref=f2e448]: Total (incl. VAT) - generic [ref=f2e449]: 󰋽 - generic [ref=f2e451]: 󰁝 - columnheader [ref=f2e454]: - button "Amount due" [ref=f2e455] [cursor=pointer]: - generic [ref=f2e458]: 󰁝 - columnheader [ref=f2e461]: - button "Issue date" [ref=f2e462] [cursor=pointer]: - generic [ref=f2e465]: 󰁝 - columnheader "Actions" [ref=f2e468] - row [ref=f2e472]: - columnheader [ref=f2e473] - rowgroup [ref=f2e474]: - row [ref=f2e475]: - cell [ref=f2e476]: - generic [ref=f2e477]: - checkbox [ref=f2e478] - generic [ref=f2e479] [cursor=pointer]: 󰄱 - cell [ref=f2e481]: - link "INV-2103417" [ref=f2e482] [cursor=pointer]: - /url: /invoices/1311731059 - cell [ref=f2e484]: - link "Open" [ref=f2e485] [cursor=pointer]: - /url: /invoices/1311731059 - cell [ref=f2e490]: - link "Manual" [ref=f2e491] [cursor=pointer]: - /url: /invoices/1311731059 - cell [ref=f2e493]: - link "19 Aug 2026" [ref=f2e494] [cursor=pointer]: - /url: /invoices/1311731059 - cell [ref=f2e496]: - link "Unit Aj57YP" [ref=f2e497] [cursor=pointer]: - /url: /invoices/1311731059 - cell "-" [ref=f2e499] - cell [ref=f2e501]: - link "€58.50" [ref=f2e502] [cursor=pointer]: - /url: /invoices/1311731059 - cell [ref=f2e504]: - link "€58.50" [ref=f2e505] [cursor=pointer]: - /url: /invoices/1311731059 - cell [ref=f2e507]: - link "17 Aug 2026" [ref=f2e508] [cursor=pointer]: - /url: /invoices/1311731059 - cell [ref=f2e510]: - button [ref=f2e513] [cursor=pointer]: - generic [ref=f2e514]: 󰇘 - generic: 󰇚 󰯝 - generic [ref=f2e518]: - generic [ref=f2e519]: - generic [ref=f2e520]: "Items per page:" - combobox [ref=f2e523]: - generic [ref=f2e525] [cursor=pointer]: - generic [ref=f2e526]: "10" - combobox "Items per page:": "10" - generic [ref=f2e528]: 󰍝 - generic [ref=f2e530]: 1-1 of 1 - generic [ref=f2e531]: - button [disabled]: - generic: 󰘀 - button [disabled]: - generic: 󰅁 - button [disabled]: - generic: 󰅂 - button [disabled]: - generic: 󰘁 - generic [ref=f2e532]: - heading "Payments" [level=3] [ref=f2e536] - generic [ref=f2e540]: - table [ref=f2e544]: - rowgroup [ref=f2e545]: - row [ref=f2e546]: - columnheader [ref=f2e547]: - button "Total" [ref=f2e548] [cursor=pointer]: - generic [ref=f2e551]: 󰁝 - columnheader [ref=f2e554]: - button "Status" [ref=f2e555] [cursor=pointer]: - generic [ref=f2e558]: 󰁝 - columnheader [ref=f2e561]: - button "Invoice no." [ref=f2e562] [cursor=pointer]: - generic [ref=f2e565]: 󰁝 - columnheader [ref=f2e568]: - button "Date" [ref=f2e569] [cursor=pointer]: - generic [ref=f2e572]: 󰁝 - columnheader [ref=f2e575]: - button "Payment method" [ref=f2e576] [cursor=pointer]: - generic [ref=f2e579]: 󰁝 - row [ref=f2e582]: - columnheader [ref=f2e583] - rowgroup [ref=f2e584]: - row [ref=f2e585]: - cell "No data yet" [ref=f2e586] - generic [ref=f2e590]: - generic [ref=f2e591]: - generic [ref=f2e592]: "Items per page:" - combobox [ref=f2e595]: - generic [ref=f2e597] [cursor=pointer]: - generic [ref=f2e598]: "10" - combobox "Items per page:": "10" - generic [ref=f2e600]: 󰍝 - generic [ref=f2e602]: 0-0 of 0 - generic [ref=f2e603]: - button [disabled]: - generic: 󰘀 - button [disabled]: - generic: 󰅁 - button [disabled]: - generic: 󰅂 - button [disabled]: - generic: 󰘁 - generic: - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip - tooltip ``` # Test source ```ts 1 | import { BookingItemsDetails, BookingPriceChangeDetails } from '@/manager/modules/booking/booking-types'; 2 | import { getBookingDepositInvoiceNumber } from '@/manager/modules/booking/booking-utils'; 3 | import { BookingDetailsItemsSection } from '@/manager/modules/booking/components/BookingDetailsItemsSection'; 4 | import { BookingDetailsPage } from '@/manager/modules/booking/views/BookingDetailsPage'; 5 | import { 6 | bookingPaymentTableColumnTestIds, 7 | invoiceTableColumnTestIds, 8 | } from '@/manager/modules/ui/data-table/data/data-table-column-test-ids'; 9 | import { expectSingleLocatorToHaveText } from '@/manager/shared/utils/expect-utils'; 10 | import { formatCurrency, formatDate } from '@/shared/utils/formatters'; 11 | import { expect } from '@/shared/utils/matchers'; 12 | 13 | export async function verifyBookingDetailsItemsSection( 14 | section: BookingDetailsItemsSection, 15 | data: BookingItemsDetails, 16 | currency: string 17 | ): Promise { 18 | await expectSingleLocatorToHaveText(section.unitTypeLabel, data.unitType.name); 19 | await expectSingleLocatorToHaveText(section.unitLabel, data.unitType.unitName); 20 | await expectSingleLocatorToHaveText( 21 | section.unitTypePriceExclVat, 22 | formatCurrency(data.unitType.priceExclVat, currency) 23 | ); 24 | 25 | if (data.insurance) { 26 | await expectSingleLocatorToHaveText(section.insuranceLabel, data.insurance.name); 27 | await expectSingleLocatorToHaveText( 28 | section.insurancePriceExclVat, 29 | formatCurrency(data.insurance.priceExclVat, currency) 30 | ); 31 | } else { 32 | await expect(section.insuranceLabel).toBeHidden(); 33 | await expect(section.insurancePriceExclVat).toBeHidden(); 34 | } 35 | 36 | // The SPA renders recurring product rows in the order they were added to the booking (the API 37 | // orders by the booking_has_recurring_products pivot id — see orderByPivot('id')). That insertion 38 | // order matches data.recurringProducts, so iterate it as-is; sorting by product id is wrong because 39 | // parallel test setup assigns product ids in a non-deterministic order. The per-piece price line is 40 | // only shown for multi-piece items, so it is tracked with its own running index. 41 | const recurringProducts = data.recurringProducts; 42 | let perPieceIndex = 0; 43 | 44 | for (const [index, product] of recurringProducts.entries()) { 45 | // The label drops the "{quantity} x" prefix when only a single piece is booked. 46 | const label = product.quantity > 1 ? `${product.quantity} x ${product.name}` : product.name; 47 | await expectSingleLocatorToHaveText(section.recurringProductLabel.nth(index), label); 48 | await expectSingleLocatorToHaveText( 49 | section.recurringProductPriceExclVat.nth(index), 50 | formatCurrency(product.priceExclVat, currency) 51 | ); 52 | 53 | if (product.quantity > 1) { 54 | await expectSingleLocatorToHaveText( 55 | section.recurringProductPricePerPiece.nth(perPieceIndex), 56 | `${formatCurrency(product.perPieceExclVat, currency)} per piece` 57 | ); 58 | perPieceIndex++; 59 | } 60 | } 61 | 62 | await expectSingleLocatorToHaveText(section.totalExclVatValue, formatCurrency(data.totals.exclVat, currency)); 63 | await expectSingleLocatorToHaveText(section.vatValue, formatCurrency(data.totals.vat, currency)); 64 | await expectSingleLocatorToHaveText(section.totalInclVatValue, formatCurrency(data.totals.inclVat, currency)); 65 | } 66 | 67 | // Invoices are issued by an asynchronous backend job after the booking is created, and the invoices 68 | // card polls for them on its own (~every 5s) — no reload needed. getRows() only resolves against a 69 | // settled (non-loading) grid, so toHaveCount retries across the card's poll cycles until the issued 70 | // invoices render. The window is deliberately bounded: if issuance doesn't land within it (e.g. the 71 | // billing job lagging under load), that is a real backend-latency signal — don't paper over it by 72 | // bumping the timeout. 73 | export async function verifyBookingInvoiceCount(detailsPage: BookingDetailsPage, expected: number): Promise { 74 | await expect(detailsPage.invoicesCard.dataTable.getRows()).toHaveCount(expected, { timeout: 60_000 }); 75 | } 76 | 77 | // Payments are recorded by the same asynchronous backend job that issues the invoices, and the payments 78 | // card polls for them on its own (~every 5s) — no reload needed. getRows() only resolves against a 79 | // settled (non-loading) grid, so toHaveCount retries across the card's poll cycles until the payments 80 | // render. The window is deliberately bounded: if the payment doesn't land within it (e.g. the billing 81 | // job lagging under load), that is a real backend-latency signal — don't paper over it by bumping the timeout. 82 | export async function verifyBookingPaymentCount(detailsPage: BookingDetailsPage, expected: number): Promise { > 83 | await expect(detailsPage.paymentsCard.dataTable.getRows()).toHaveCount(expected, { timeout: 60_000 }); | ^ Error: expect(locator).toHaveCount(expected) failed 84 | } 85 | 86 | // The whole price adjustment scene in the billing card: chip, new subscription amount, targets, and the 87 | // snapped apply-from date. The SPA polls the booking while has_pending_changes is set, so the section 88 | // appears without a reload once the backend has persisted the change. 89 | export async function verifyBookingPriceChangeSection( 90 | detailsPage: BookingDetailsPage, 91 | expected: BookingPriceChangeDetails, 92 | currency: string, 93 | timezone: string 94 | ): Promise { 95 | const section = detailsPage.billingCard.priceChangeSection; 96 | 97 | await expectSingleLocatorToHaveText(section.chip, expected.chip); 98 | await expectSingleLocatorToHaveText( 99 | section.newAmount, 100 | `New amount excl. VAT: ${formatCurrency(expected.newAmountExclVat, currency)}` 101 | ); 102 | await expectSingleLocatorToHaveText(section.appliedOn, expected.appliedOn); 103 | await expectSingleLocatorToHaveText( 104 | section.appliedFrom, 105 | `applied from ${formatDate(expected.appliedFrom, timezone)}` 106 | ); 107 | } 108 | 109 | // Asserts one invoice row in full. Pass `numberColumnText` to locate the row in the number column — an 110 | // invoice number for a regular invoice, or the "Deposit" chip label for the deposit invoice (whose number 111 | // cell also holds the chip, so it can't be matched by number) — needed when a booking has several invoices. 112 | // Omit it for a booking with a single invoice, where it falls back to the first row. Covers every column 113 | // the row carries besides the number itself. `unit` is optional — omit it for an invoice not tied to a 114 | // unit (e.g. the upfront-deposit invoice), where the unit cell asserts empty. 115 | export async function verifyBookingInvoice( 116 | detailsPage: BookingDetailsPage, 117 | expected: { 118 | status: string; 119 | paymentOption: string; 120 | unit?: string; 121 | amount: string; 122 | amountDue: string; 123 | issueDate: string; 124 | dueDate: string; 125 | }, 126 | numberColumnText?: string 127 | ): Promise { 128 | const invoices = detailsPage.invoicesCard.dataTable; 129 | const cell = (columnId: string) => 130 | numberColumnText 131 | ? invoices.getRowColumnBy([{ columnId: invoiceTableColumnTestIds.id, text: numberColumnText }], columnId) 132 | : invoices.getRowColumnByIndex(0, columnId); 133 | 134 | await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.status), expected.status); 135 | await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.paymentOption), expected.paymentOption); 136 | await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.unit), expected.unit); 137 | await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.amount), expected.amount); 138 | await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.amountDue), expected.amountDue); 139 | await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.issueDate), expected.issueDate); 140 | await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.dueDate), expected.dueDate); 141 | } 142 | 143 | // A booking has one payment row per invoice in the payments grid, keyed by invoice number. Pass an 144 | // invoiceNumber to scope the check to that invoice's payment — needed when a booking has several invoices 145 | // (e.g. an upfront deposit's invoice alongside the first-period one), each with its own payment; omit it 146 | // for a booking with a single payment. Asserts the row's gross amount, status, payment method, and date. 147 | // Omit paymentMethod for an unpaid payment (e.g. a manual invoice's "Not paid yet" row): no method has 148 | // been charged yet, so the cell is empty and the assertion checks it renders blank. 149 | export async function verifyBookingPayment( 150 | detailsPage: BookingDetailsPage, 151 | expected: { amount: string; status: string; paymentMethod?: string; date: string }, 152 | invoiceNumber?: string 153 | ): Promise { 154 | const payments = detailsPage.paymentsCard.dataTable; 155 | const cell = (columnId: string) => 156 | invoiceNumber 157 | ? payments.getRowColumnBy( 158 | [{ columnId: bookingPaymentTableColumnTestIds.invoiceId, text: invoiceNumber }], 159 | columnId 160 | ) 161 | : payments.getRowColumnByIndex(0, columnId); 162 | 163 | await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.amountInclVat), expected.amount); 164 | await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.status), expected.status); 165 | await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.paymentMethod), expected.paymentMethod); 166 | await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.date), expected.date); 167 | } 168 | 169 | // The deposit field renders its amount as a link to the deposit invoice, but the link is only hydrated 170 | // on a fresh load, not right after the booking is created. So grep the invoice's number from the grid, 171 | // reload to mount the link, then assert the field reads "{depositAmount} ({number})" with the number 172 | // rendered as the invoice link. 173 | export async function verifyBookingDepositInvoiceLink( 174 | detailsPage: BookingDetailsPage, 175 | depositAmount: string 176 | ): Promise { 177 | const invoiceNumber = await getBookingDepositInvoiceNumber(detailsPage.invoicesCard.dataTable); 178 | 179 | await detailsPage.reload(); 180 | 181 | await expectSingleLocatorToHaveText(detailsPage.generalCard.depositLink, invoiceNumber); 182 | await expectSingleLocatorToHaveText(detailsPage.generalCard.deposit, `${depositAmount} (${invoiceNumber})`); 183 | } ```