# 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 9166-3667-3828 - generic [ref=f2e171]: Active - button [ref=f2e176] [cursor=pointer]: - generic [ref=f2e177]: - text: Actions - generic [ref=f2e178]: 󱨉 - generic: 󱕂 - generic [ref=f2e179]: - generic [ref=f2e180]: - tablist [ref=f2e181]: - generic [ref=f2e183]: - tab "Details" [selected] [ref=f2e184] [cursor=pointer] - tab "Timeline" [ref=f2e186] [cursor=pointer] - separator [ref=f2e188] - generic [ref=f2e189]: - generic [ref=f2e190]: - generic [ref=f2e192]: - heading "General" [level=3] [ref=f2e196] - table [ref=f2e199]: - rowgroup [ref=f2e200]: - row [ref=f2e201]: - rowheader "Customer" [ref=f2e202] - cell [ref=f2e203]: - link "Dandre Harris" [ref=f2e206] [cursor=pointer]: - /url: /customers/1661166278 - row [ref=f2e209]: - rowheader "Location" [ref=f2e210] - cell [ref=f2e211]: - link "Vienna South" [ref=f2e214] [cursor=pointer]: - /url: /locations/5000500050 - row [ref=f2e217]: - rowheader "Created at" [ref=f2e218] - cell "9 Oct 2026" [ref=f2e219] - row [ref=f2e222]: - rowheader "Move-in" [ref=f2e223] - cell "9 Oct 2026" [ref=f2e224] - row [ref=f2e227]: - rowheader "Move-out" [ref=f2e228] - cell "-" [ref=f2e229] - row [ref=f2e232]: - rowheader "Cancelled at" [ref=f2e233] - cell "-" [ref=f2e234] - row [ref=f2e237]: - rowheader "Deposit excl. VAT" [ref=f2e238] - cell "-" [ref=f2e239] - row [ref=f2e242]: - rowheader "Discount" [ref=f2e243] - cell "-" [ref=f2e244] - row [ref=f2e247]: - rowheader "Contract" [ref=f2e248] - cell "-" [ref=f2e249] - generic [ref=f2e252]: - generic [ref=f2e253]: - heading "Billing" [level=3] [ref=f2e257] - table [ref=f2e260]: - rowgroup [ref=f2e261]: - row [ref=f2e262]: - rowheader "Booking Plan" [ref=f2e263] - cell "Basic (1 week)" [ref=f2e264] - row [ref=f2e267]: - rowheader "Current period" [ref=f2e268] - cell "9 - 16 Oct 2026" [ref=f2e269] - row [ref=f2e272]: - rowheader "Amount excl. VAT" [ref=f2e273] - cell "€50.00" [ref=f2e274] - row [ref=f2e277]: - rowheader "Price adjustment" [ref=f2e278] - cell "-" [ref=f2e279] - row [ref=f2e282]: - rowheader "Next invoice" [ref=f2e283] - cell "16 Oct 2026 Show preview" [ref=f2e284]: - generic [ref=f2e287]: - generic [ref=f2e288]: 16 Oct 2026 - link "Show preview" [ref=f2e289] [cursor=pointer]: - /url: /bookings/916636673828/upcoming-invoice - generic [ref=f2e290]: 󰛐 - row [ref=f2e293]: - rowheader "Payment method" [ref=f2e294] - cell "-" [ref=f2e295] - row [ref=f2e298]: - rowheader "Payment option" [ref=f2e299] - cell "Automatic payment through Stripe" [ref=f2e300] - generic [ref=f2e303]: - generic [ref=f2e305]: - heading "Notes" [level=3] [ref=f2e307] - link [ref=f2e309] [cursor=pointer]: - /url: /bookings/916636673828/edit/notes - generic [ref=f2e310]: 󰲶 - table [ref=f2e314]: - rowgroup [ref=f2e315]: - row [ref=f2e316]: - rowheader "Note" [ref=f2e317] - cell "-" [ref=f2e318] - generic [ref=f2e321]: - heading "Items" [level=3] [ref=f2e325] - generic [ref=f2e328]: - generic [ref=f2e329]: - generic [ref=f2e330]: - link "Tasty Box AnyD8T" [ref=f2e331] [cursor=pointer]: - /url: /unit-types/1411800365 - text: / - link "Unit NBAkQ4" [ref=f2e334] [cursor=pointer]: - /url: /units/1726038134 - generic [ref=f2e337]: €50.00 - generic [ref=f2e338]: - generic [ref=f2e339]: - strong [ref=f2e340]: Total excl. VAT - strong [ref=f2e341]: €50.00 - generic [ref=f2e342]: - generic [ref=f2e343]: VAT - generic [ref=f2e344]: €8.50 - generic [ref=f2e345]: - strong [ref=f2e346]: Total incl. VAT - strong [ref=f2e347]: €58.50 - generic [ref=f2e348]: - heading "Invoices" [level=3] [ref=f2e352] - generic [ref=f2e356]: - generic [ref=f2e358]: - generic [ref=f2e360]: - button "Select all" [ref=f2e368] [cursor=pointer]: - generic [ref=f2e369]: 󰄬 - generic [ref=f2e372]: - button "Mark as paid" [disabled] [ref=f2e375]: - generic [ref=f2e376]: 󰗡 - button "SEPA XML actions" [disabled] [ref=f2e381]: - generic [ref=f2e382]: 󱨉 - generic: 󰗡 󰅚 󰗖 - generic [ref=f2e387]: - button [ref=f2e388] [cursor=pointer] - generic: 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 - table [ref=f2e394]: - rowgroup [ref=f2e395]: - row [ref=f2e396]: - columnheader [ref=f2e397]: - generic [ref=f2e398]: - checkbox [ref=f2e399] - generic [ref=f2e400] [cursor=pointer]: 󰄱 - columnheader [ref=f2e402]: - button "Number" [ref=f2e403] [cursor=pointer]: - generic [ref=f2e406]: 󰁝 - columnheader [ref=f2e409]: - button "Status" [ref=f2e410] [cursor=pointer]: - generic [ref=f2e413]: 󰁝 - columnheader [ref=f2e416]: - button "Payment option" [ref=f2e417] [cursor=pointer]: - generic [ref=f2e420]: 󰁝 - columnheader [ref=f2e423]: - button "Due date" [ref=f2e424] [cursor=pointer]: - generic [ref=f2e427]: 󰁝 - columnheader [ref=f2e430]: - button "Unit" [ref=f2e431] [cursor=pointer]: - generic [ref=f2e434]: 󰁝 - columnheader "Discount" [ref=f2e437] - columnheader [ref=f2e441]: - button "Total (incl. VAT)" [ref=f2e442] [cursor=pointer]: - generic [ref=f2e443]: - generic [ref=f2e444]: Total (incl. VAT) - generic [ref=f2e445]: 󰋽 - generic [ref=f2e447]: 󰁝 - columnheader [ref=f2e450]: - button "Amount due" [ref=f2e451] [cursor=pointer]: - generic [ref=f2e454]: 󰁝 - columnheader [ref=f2e457]: - button "Issue date" [ref=f2e458] [cursor=pointer]: - generic [ref=f2e461]: 󰁝 - columnheader "Actions" [ref=f2e464] - row [ref=f2e468]: - columnheader [ref=f2e469] - rowgroup [ref=f2e470]: - row [ref=f2e471]: - cell [ref=f2e472]: - generic [ref=f2e473]: - checkbox [ref=f2e474] - generic [ref=f2e475] [cursor=pointer]: 󰄱 - cell [ref=f2e477]: - link "INV-2212152" [ref=f2e478] [cursor=pointer]: - /url: /invoices/188867294786 - cell [ref=f2e480]: - link "Open" [ref=f2e481] [cursor=pointer]: - /url: /invoices/188867294786 - cell [ref=f2e486]: - link "Manual" [ref=f2e487] [cursor=pointer]: - /url: /invoices/188867294786 - cell [ref=f2e489]: - link "11 Oct 2026" [ref=f2e490] [cursor=pointer]: - /url: /invoices/188867294786 - cell [ref=f2e492]: - link "Unit NBAkQ4" [ref=f2e493] [cursor=pointer]: - /url: /invoices/188867294786 - cell "-" [ref=f2e495] - cell [ref=f2e497]: - link "€58.50" [ref=f2e498] [cursor=pointer]: - /url: /invoices/188867294786 - cell [ref=f2e500]: - link "€58.50" [ref=f2e501] [cursor=pointer]: - /url: /invoices/188867294786 - cell [ref=f2e503]: - link "9 Oct 2026" [ref=f2e504] [cursor=pointer]: - /url: /invoices/188867294786 - cell [ref=f2e506]: - button [ref=f2e509] [cursor=pointer]: - generic [ref=f2e510]: 󰇘 - generic: 󰇚 󰯝 - generic [ref=f2e514]: - generic [ref=f2e515]: - generic [ref=f2e516]: "Items per page:" - combobox [ref=f2e519]: - generic [ref=f2e521] [cursor=pointer]: - generic [ref=f2e522]: "10" - combobox "Items per page:": "10" - generic [ref=f2e524]: 󰍝 - generic [ref=f2e526]: 1-1 of 1 - generic [ref=f2e527]: - button [disabled]: - generic: 󰘀 - button [disabled]: - generic: 󰅁 - button [disabled]: - generic: 󰅂 - button [disabled]: - generic: 󰘁 - generic [ref=f2e528]: - heading "Payments" [level=3] [ref=f2e532] - generic [ref=f2e536]: - table [ref=f2e540]: - rowgroup [ref=f2e541]: - row [ref=f2e542]: - columnheader [ref=f2e543]: - button "Total" [ref=f2e544] [cursor=pointer]: - generic [ref=f2e547]: 󰁝 - columnheader [ref=f2e550]: - button "Status" [ref=f2e551] [cursor=pointer]: - generic [ref=f2e554]: 󰁝 - columnheader [ref=f2e557]: - button "Invoice no." [ref=f2e558] [cursor=pointer]: - generic [ref=f2e561]: 󰁝 - columnheader [ref=f2e564]: - button "Date" [ref=f2e565] [cursor=pointer]: - generic [ref=f2e568]: 󰁝 - columnheader [ref=f2e571]: - button "Payment method" [ref=f2e572] [cursor=pointer]: - generic [ref=f2e575]: 󰁝 - row [ref=f2e578]: - columnheader [ref=f2e579] - rowgroup [ref=f2e580]: - row [ref=f2e581]: - cell "No data yet" [ref=f2e582] - generic [ref=f2e586]: - generic [ref=f2e587]: - generic [ref=f2e588]: "Items per page:" - combobox [ref=f2e591]: - generic [ref=f2e593] [cursor=pointer]: - generic [ref=f2e594]: "10" - combobox "Items per page:": "10" - generic [ref=f2e596]: 󰍝 - generic [ref=f2e598]: 0-0 of 0 - generic [ref=f2e599]: - 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 | } ```