# 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: 󱡰 󰁥 - generic [ref=f2e111]: Admin - link "Integrations" [ref=f2e113] [cursor=pointer]: - /url: /connected-apps - generic [ref=f2e114]: 󱘖 - link "User & Roles" [ref=f2e118] [cursor=pointer]: - /url: /users - generic [ref=f2e119]: 󰭘 - generic [ref=f2e123]: - option "Booking Portal" [ref=f2e124] [cursor=pointer]: - generic [ref=f2e125]: 󱃁 - generic [ref=f2e129]: 󰅀 - text: 󰖟 󰟙 - link "Public API" [ref=f2e133] [cursor=pointer]: - /url: /public-api - generic [ref=f2e134]: 󱂛 - generic [ref=f2e138]: Feedback - link "Voting Portal" [ref=f2e140] [cursor=pointer]: - /url: /voting-portal - generic [ref=f2e141]: 󰔔 - generic [ref=f2e147]: - button "AS Andreas Stadler andreas.stadler@storeroom.at" [ref=f2e150] [cursor=pointer]: - generic [ref=f2e151]: AS - generic [ref=f2e154]: - generic [ref=f2e155]: Andreas Stadler - generic "andreas.stadler@storeroom.at" [ref=f2e156] - generic [ref=f2e157]: 󰇙 - generic: 󰗊 󰅀 󰷖 󰍃 - button [ref=f2e158] [cursor=pointer]: - generic [ref=f2e159]: 󰋽 - main [ref=f2e161]: - generic [ref=f2e163]: - generic [ref=f2e165]: - generic [ref=f2e166]: - link "Bookings" [ref=f2e167] [cursor=pointer]: - /url: /bookings - generic [ref=f2e169]: 󰅂 - generic [ref=f2e170]: Booking 1697-4451-1973 - generic [ref=f2e171]: Active - button [ref=f2e176] [cursor=pointer]: - generic [ref=f2e177]: - text: Actions - generic [ref=f2e178]: 󱨉 - generic: 󱕂 - generic [ref=f2e180]: - generic [ref=f2e181]: - generic [ref=f2e183]: - heading "General" [level=3] [ref=f2e187] - table [ref=f2e190]: - rowgroup [ref=f2e191]: - row [ref=f2e192]: - rowheader "Customer" [ref=f2e193] - cell [ref=f2e194]: - link "Margie Bartoletti" [ref=f2e197] [cursor=pointer]: - /url: /customers/1661166278 - row [ref=f2e200]: - rowheader "Location" [ref=f2e201] - cell [ref=f2e202]: - link "Vienna South" [ref=f2e205] [cursor=pointer]: - /url: /locations/5000500050 - row [ref=f2e208]: - rowheader "Created at" [ref=f2e209] - cell "12 Oct 2026" [ref=f2e210] - row [ref=f2e213]: - rowheader "Move-in" [ref=f2e214] - cell "12 Oct 2026" [ref=f2e215] - row [ref=f2e218]: - rowheader "Move-out" [ref=f2e219] - cell "-" [ref=f2e220] - row [ref=f2e223]: - rowheader "Cancelled at" [ref=f2e224] - cell "-" [ref=f2e225] - row [ref=f2e228]: - rowheader "Deposit excl. VAT" [ref=f2e229] - cell "-" [ref=f2e230] - row [ref=f2e233]: - rowheader "Discount" [ref=f2e234] - cell "-" [ref=f2e235] - row [ref=f2e238]: - rowheader "Contract" [ref=f2e239] - cell "-" [ref=f2e240] - generic [ref=f2e243]: - generic [ref=f2e244]: - heading "Billing" [level=3] [ref=f2e248] - table [ref=f2e251]: - rowgroup [ref=f2e252]: - row [ref=f2e253]: - rowheader "Booking Plan" [ref=f2e254] - cell "Basic (1 week)" [ref=f2e255] - row [ref=f2e258]: - rowheader "Current period" [ref=f2e259] - cell "12 - 19 Oct 2026" [ref=f2e260] - row [ref=f2e263]: - rowheader "Amount excl. VAT" [ref=f2e264] - cell "€50.00" [ref=f2e265] - row [ref=f2e268]: - rowheader "Price adjustment" [ref=f2e269] - cell "-" [ref=f2e270] - row [ref=f2e273]: - rowheader "Next invoice" [ref=f2e274] - cell "19 Oct 2026 Show preview" [ref=f2e275]: - generic [ref=f2e278]: - generic [ref=f2e279]: 19 Oct 2026 - link "Show preview" [ref=f2e280] [cursor=pointer]: - /url: /bookings/169744511973/upcoming-invoice - generic [ref=f2e281]: 󰛐 - row [ref=f2e284]: - rowheader "Payment method" [ref=f2e285] - cell "-" [ref=f2e286] - row [ref=f2e289]: - rowheader "Payment option" [ref=f2e290] - cell "Automatic payment through Stripe" [ref=f2e291] - generic [ref=f2e294]: - generic [ref=f2e296]: - heading "Notes" [level=3] [ref=f2e298] - link [ref=f2e300] [cursor=pointer]: - /url: /bookings/169744511973/edit/notes - generic [ref=f2e301]: 󰲶 - table [ref=f2e305]: - rowgroup [ref=f2e306]: - row [ref=f2e307]: - rowheader "Note" [ref=f2e308] - cell "-" [ref=f2e309] - generic [ref=f2e312]: - heading "Items" [level=3] [ref=f2e316] - generic [ref=f2e319]: - generic [ref=f2e320]: - generic [ref=f2e321]: - link "Recycled Box woGbYB" [ref=f2e322] [cursor=pointer]: - /url: /unit-types/1411800365 - text: / - link "Unit Wjkt27" [ref=f2e325] [cursor=pointer]: - /url: /units/1726038134 - generic [ref=f2e328]: €50.00 - generic [ref=f2e329]: - generic [ref=f2e330]: - strong [ref=f2e331]: Total excl. VAT - strong [ref=f2e332]: €50.00 - generic [ref=f2e333]: - generic [ref=f2e334]: VAT - generic [ref=f2e335]: €8.50 - generic [ref=f2e336]: - strong [ref=f2e337]: Total incl. VAT - strong [ref=f2e338]: €58.50 - generic [ref=f2e339]: - heading "Invoices" [level=3] [ref=f2e343] - generic [ref=f2e347]: - generic [ref=f2e349]: - generic [ref=f2e351]: - button "Select all" [ref=f2e359] [cursor=pointer]: - generic [ref=f2e360]: 󰄬 - generic [ref=f2e363]: - button "Mark as paid" [disabled] [ref=f2e366]: - generic [ref=f2e367]: 󰗡 - button "SEPA XML actions" [disabled] [ref=f2e372]: - generic [ref=f2e373]: 󱨉 - generic: 󰗡 󰅚 󰗖 - generic [ref=f2e378]: - button [ref=f2e379] [cursor=pointer] - generic: 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 - table [ref=f2e385]: - rowgroup [ref=f2e386]: - row [ref=f2e387]: - columnheader [ref=f2e388]: - generic [ref=f2e389]: - checkbox [ref=f2e390] - generic [ref=f2e391] [cursor=pointer]: 󰄱 - columnheader [ref=f2e393]: - button "Number" [ref=f2e394] [cursor=pointer]: - generic [ref=f2e397]: 󰁝 - columnheader [ref=f2e400]: - button "Status" [ref=f2e401] [cursor=pointer]: - generic [ref=f2e404]: 󰁝 - columnheader [ref=f2e407]: - button "Payment option" [ref=f2e408] [cursor=pointer]: - generic [ref=f2e411]: 󰁝 - columnheader [ref=f2e414]: - button "Due date" [ref=f2e415] [cursor=pointer]: - generic [ref=f2e418]: 󰁝 - columnheader [ref=f2e421]: - button "Unit" [ref=f2e422] [cursor=pointer]: - generic [ref=f2e425]: 󰁝 - columnheader "Discount" [ref=f2e428] - columnheader [ref=f2e432]: - button "Total (incl. VAT)" [ref=f2e433] [cursor=pointer]: - generic [ref=f2e434]: - generic [ref=f2e435]: Total (incl. VAT) - generic [ref=f2e436]: 󰋽 - generic [ref=f2e438]: 󰁝 - columnheader [ref=f2e441]: - button "Amount due" [ref=f2e442] [cursor=pointer]: - generic [ref=f2e445]: 󰁝 - columnheader [ref=f2e448]: - button "Issue date" [ref=f2e449] [cursor=pointer]: - generic [ref=f2e452]: 󰁝 - columnheader "Actions" [ref=f2e455] - row [ref=f2e459]: - columnheader [ref=f2e460] - rowgroup [ref=f2e461]: - row [ref=f2e462]: - cell [ref=f2e463]: - generic [ref=f2e464]: - checkbox [ref=f2e465] - generic [ref=f2e466] [cursor=pointer]: 󰄱 - cell [ref=f2e468]: - link "INV-2218822" [ref=f2e469] [cursor=pointer]: - /url: /invoices/314384465382 - cell [ref=f2e471]: - link "Open" [ref=f2e472] [cursor=pointer]: - /url: /invoices/314384465382 - cell [ref=f2e477]: - link "Manual" [ref=f2e478] [cursor=pointer]: - /url: /invoices/314384465382 - cell [ref=f2e480]: - link "14 Oct 2026" [ref=f2e481] [cursor=pointer]: - /url: /invoices/314384465382 - cell [ref=f2e483]: - link "Unit Wjkt27" [ref=f2e484] [cursor=pointer]: - /url: /invoices/314384465382 - cell "-" [ref=f2e486] - cell [ref=f2e488]: - link "€58.50" [ref=f2e489] [cursor=pointer]: - /url: /invoices/314384465382 - cell [ref=f2e491]: - link "€58.50" [ref=f2e492] [cursor=pointer]: - /url: /invoices/314384465382 - cell [ref=f2e494]: - link "12 Oct 2026" [ref=f2e495] [cursor=pointer]: - /url: /invoices/314384465382 - cell [ref=f2e497]: - button [ref=f2e500] [cursor=pointer]: - generic [ref=f2e501]: 󰇘 - generic: 󰇚 󰯝 - generic [ref=f2e505]: - generic [ref=f2e506]: - generic [ref=f2e507]: "Items per page:" - combobox [ref=f2e510]: - generic [ref=f2e512] [cursor=pointer]: - generic [ref=f2e513]: "10" - combobox "Items per page:": "10" - generic [ref=f2e515]: 󰍝 - generic [ref=f2e517]: 1-1 of 1 - generic [ref=f2e518]: - button [disabled]: - generic: 󰘀 - button [disabled]: - generic: 󰅁 - button [disabled]: - generic: 󰅂 - button [disabled]: - generic: 󰘁 - generic [ref=f2e519]: - heading "Payments" [level=3] [ref=f2e523] - generic [ref=f2e527]: - table [ref=f2e531]: - rowgroup [ref=f2e532]: - row [ref=f2e533]: - columnheader [ref=f2e534]: - button "Total" [ref=f2e535] [cursor=pointer]: - generic [ref=f2e538]: 󰁝 - columnheader [ref=f2e541]: - button "Status" [ref=f2e542] [cursor=pointer]: - generic [ref=f2e545]: 󰁝 - columnheader [ref=f2e548]: - button "Invoice no." [ref=f2e549] [cursor=pointer]: - generic [ref=f2e552]: 󰁝 - columnheader [ref=f2e555]: - button "Date" [ref=f2e556] [cursor=pointer]: - generic [ref=f2e559]: 󰁝 - columnheader [ref=f2e562]: - button "Payment method" [ref=f2e563] [cursor=pointer]: - generic [ref=f2e566]: 󰁝 - row [ref=f2e569]: - columnheader [ref=f2e570] - rowgroup [ref=f2e571]: - row [ref=f2e572]: - cell "No data yet" [ref=f2e573] - generic [ref=f2e577]: - generic [ref=f2e578]: - generic [ref=f2e579]: "Items per page:" - combobox [ref=f2e582]: - generic [ref=f2e584] [cursor=pointer]: - generic [ref=f2e585]: "10" - combobox "Items per page:": "10" - generic [ref=f2e587]: 󰍝 - generic [ref=f2e589]: 0-0 of 0 - generic [ref=f2e590]: - 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 ``` # Test source ```ts 6 | import { BookingDetailsPage } from '@/manager/modules/booking/views/BookingDetailsPage'; 7 | import { 8 | bookingPaymentTableColumnTestIds, 9 | invoiceTableColumnTestIds, 10 | } from '@/manager/modules/ui/data-table/data/data-table-column-test-ids'; 11 | import { expectSingleLocatorToHaveText } from '@/manager/shared/utils/expect-utils'; 12 | import { formatCurrency, formatDate } from '@/shared/utils/formatters'; 13 | import { expect } from '@/shared/utils/matchers'; 14 | 15 | export async function verifyBookingDetailsItemsSection( 16 | section: BookingDetailsItemsSection, 17 | data: BookingItemsDetails, 18 | currency: string 19 | ): Promise { 20 | await expectSingleLocatorToHaveText(section.unitTypeLabel, data.unitType.name); 21 | await expectSingleLocatorToHaveText(section.unitLabel, data.unitType.unitName); 22 | await expectSingleLocatorToHaveText( 23 | section.unitTypePriceExclVat, 24 | formatCurrency(data.unitType.priceExclVat, currency) 25 | ); 26 | 27 | if (data.insurance) { 28 | await expectSingleLocatorToHaveText(section.insuranceLabel, data.insurance.name); 29 | await expectSingleLocatorToHaveText( 30 | section.insurancePriceExclVat, 31 | formatCurrency(data.insurance.priceExclVat, currency) 32 | ); 33 | } else { 34 | await expect(section.insuranceLabel).toBeHidden(); 35 | await expect(section.insurancePriceExclVat).toBeHidden(); 36 | } 37 | 38 | // Rows follow the booking's pivot insertion order, which a billing-api adjustment rewrites (changed and 39 | // added items move), so each product is matched by name instead of by position. 40 | await expect(section.recurringProductRows).toHaveCount(data.recurringProducts.length); 41 | 42 | for (const product of data.recurringProducts) { 43 | const row = section.getRecurringProductRow(product.name); 44 | // The label drops the "{quantity} x" prefix when only a single piece is booked. 45 | const label = product.quantity > 1 ? `${product.quantity} x ${product.name}` : product.name; 46 | await expectSingleLocatorToHaveText(row.label, label); 47 | await expectSingleLocatorToHaveText(row.priceExclVat, formatCurrency(product.priceExclVat, currency)); 48 | 49 | if (product.quantity > 1) { 50 | await expectSingleLocatorToHaveText( 51 | row.pricePerPiece, 52 | `${formatCurrency(product.perPieceExclVat, currency)} per piece` 53 | ); 54 | } 55 | } 56 | 57 | await expectSingleLocatorToHaveText(section.totalExclVatValue, formatCurrency(data.totals.exclVat, currency)); 58 | await expectSingleLocatorToHaveText(section.vatValue, formatCurrency(data.totals.vat, currency)); 59 | await expectSingleLocatorToHaveText(section.totalInclVatValue, formatCurrency(data.totals.inclVat, currency)); 60 | } 61 | 62 | export async function verifyBookingAdjustmentItemsTable( 63 | table: BookingAdjustmentCurrentItemsTable | BookingAdjustmentAdjustedItemsTable, 64 | data: BookingItemsDetails, 65 | currency: string 66 | ): Promise { 67 | await expect(table.unitRow).toContainText(data.unitType.name); 68 | await expect(table.unitRow).toContainText(data.unitType.unitName); 69 | await expectSingleLocatorToHaveText(table.unitPriceValue, formatCurrency(data.unitType.priceExclVat, currency)); 70 | 71 | if (data.insurance) { 72 | await expect(table.insuranceRow).toContainText(data.insurance.name); 73 | await expectSingleLocatorToHaveText( 74 | table.insurancePriceValue, 75 | formatCurrency(data.insurance.priceExclVat, currency) 76 | ); 77 | } else { 78 | await expect(table.insuranceRow).toBeHidden(); 79 | } 80 | 81 | await expect(table.recurringProductRows).toHaveCount(data.recurringProducts.length); 82 | 83 | for (const product of data.recurringProducts) { 84 | await expect(table.recurringProductRows.filter({ hasText: product.name })).toContainText( 85 | formatCurrency(product.priceExclVat, currency) 86 | ); 87 | } 88 | } 89 | 90 | // Invoices are issued by an asynchronous backend job after the booking is created, and the invoices 91 | // card polls for them on its own (~every 5s) — no reload needed. getRows() only resolves against a 92 | // settled (non-loading) grid, so toHaveCount retries across the card's poll cycles until the issued 93 | // invoices render. The window is deliberately bounded: if issuance doesn't land within it (e.g. the 94 | // billing job lagging under load), that is a real backend-latency signal — don't paper over it by 95 | // bumping the timeout. 96 | export async function verifyBookingInvoiceCount(detailsPage: BookingDetailsPage, expected: number): Promise { 97 | await expect(detailsPage.invoicesCard.dataTable.getRows()).toHaveCount(expected, { timeout: 60_000 }); 98 | } 99 | 100 | // Payments are recorded by the same asynchronous backend job that issues the invoices, and the payments 101 | // card polls for them on its own (~every 5s) — no reload needed. getRows() only resolves against a 102 | // settled (non-loading) grid, so toHaveCount retries across the card's poll cycles until the payments 103 | // render. The window is deliberately bounded: if the payment doesn't land within it (e.g. the billing 104 | // job lagging under load), that is a real backend-latency signal — don't paper over it by bumping the timeout. 105 | export async function verifyBookingPaymentCount(detailsPage: BookingDetailsPage, expected: number): Promise { > 106 | await expect(detailsPage.paymentsCard.dataTable.getRows()).toHaveCount(expected, { timeout: 60_000 }); | ^ Error: expect(locator).toHaveCount(expected) failed 107 | } 108 | 109 | // The whole price adjustment scene in the billing card: chip, new subscription amount, targets, and the 110 | // snapped apply-from date. The SPA polls the booking while has_pending_changes is set, so the section 111 | // appears without a reload once the backend has persisted the change. 112 | export async function verifyBookingPriceChangeSection( 113 | detailsPage: BookingDetailsPage, 114 | expected: BookingPriceChangeDetails, 115 | currency: string, 116 | timezone: string 117 | ): Promise { 118 | const section = detailsPage.billingCard.priceChangeSection; 119 | 120 | await expectSingleLocatorToHaveText(section.chip, expected.chip); 121 | await expectSingleLocatorToHaveText( 122 | section.newAmount, 123 | `New amount excl. VAT: ${formatCurrency(expected.newAmountExclVat, currency)}` 124 | ); 125 | await expectSingleLocatorToHaveText(section.appliedOn, expected.appliedOn); 126 | await expectSingleLocatorToHaveText( 127 | section.appliedFrom, 128 | `applied from ${formatDate(expected.appliedFrom, timezone)}` 129 | ); 130 | } 131 | 132 | // Asserts one invoice row in full. Pass `numberColumnText` to locate the row in the number column — an 133 | // invoice number for a regular invoice, or the "Deposit" chip label for the deposit invoice (whose number 134 | // cell also holds the chip, so it can't be matched by number) — needed when a booking has several invoices. 135 | // Omit it for a booking with a single invoice, where it falls back to the first row. Covers every column 136 | // the row carries besides the number itself. `unit` is optional — omit it for an invoice not tied to a 137 | // unit (e.g. the upfront-deposit invoice), where the unit cell asserts empty. 138 | export async function verifyBookingInvoice( 139 | detailsPage: BookingDetailsPage, 140 | expected: { 141 | status: string; 142 | paymentOption: string; 143 | unit?: string; 144 | amount: string; 145 | amountDue: string; 146 | issueDate: string; 147 | dueDate: string; 148 | }, 149 | numberColumnText?: string 150 | ): Promise { 151 | const invoices = detailsPage.invoicesCard.dataTable; 152 | const cell = (columnId: string) => 153 | numberColumnText 154 | ? invoices.getRowColumnBy([{ columnId: invoiceTableColumnTestIds.id, text: numberColumnText }], columnId) 155 | : invoices.getRowColumnByIndex(0, columnId); 156 | 157 | await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.status), expected.status); 158 | await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.paymentOption), expected.paymentOption); 159 | await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.unit), expected.unit); 160 | await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.amount), expected.amount); 161 | await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.amountDue), expected.amountDue); 162 | await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.issueDate), expected.issueDate); 163 | await expectSingleLocatorToHaveText(cell(invoiceTableColumnTestIds.dueDate), expected.dueDate); 164 | } 165 | 166 | // A booking has one payment row per invoice in the payments grid, keyed by invoice number. Pass an 167 | // invoiceNumber to scope the check to that invoice's payment — needed when a booking has several invoices 168 | // (e.g. an upfront deposit's invoice alongside the first-period one), each with its own payment; omit it 169 | // for a booking with a single payment. Asserts the row's gross amount, status, payment method, and date. 170 | // Omit paymentMethod for an unpaid payment (e.g. a manual invoice's "Not paid yet" row): no method has 171 | // been charged yet, so the cell is empty and the assertion checks it renders blank. 172 | export async function verifyBookingPayment( 173 | detailsPage: BookingDetailsPage, 174 | expected: { amount: string; status: string; paymentMethod?: string; date: string }, 175 | invoiceNumber?: string 176 | ): Promise { 177 | const payments = detailsPage.paymentsCard.dataTable; 178 | const cell = (columnId: string) => 179 | invoiceNumber 180 | ? payments.getRowColumnBy( 181 | [{ columnId: bookingPaymentTableColumnTestIds.invoiceId, text: invoiceNumber }], 182 | columnId 183 | ) 184 | : payments.getRowColumnByIndex(0, columnId); 185 | 186 | await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.amountInclVat), expected.amount); 187 | await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.status), expected.status); 188 | await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.paymentMethod), expected.paymentMethod); 189 | await expectSingleLocatorToHaveText(cell(bookingPaymentTableColumnTestIds.date), expected.date); 190 | } 191 | 192 | // The deposit field renders its amount as a link to the deposit invoice, but the link is only hydrated 193 | // on a fresh load, not right after the booking is created. So grep the invoice's number from the grid, 194 | // reload to mount the link, then assert the field reads "{depositAmount} ({number})" with the number 195 | // rendered as the invoice link. 196 | export async function verifyBookingDepositInvoiceLink( 197 | detailsPage: BookingDetailsPage, 198 | depositAmount: string 199 | ): Promise { 200 | const invoiceNumber = await getBookingDepositInvoiceNumber(detailsPage.invoicesCard.dataTable); 201 | 202 | await detailsPage.reload(); 203 | 204 | await expectSingleLocatorToHaveText(detailsPage.generalCard.depositLink, invoiceNumber); 205 | await expectSingleLocatorToHaveText(detailsPage.generalCard.deposit, `${depositAmount} (${invoiceNumber})`); 206 | } ```