# 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 1925-7885-9227
            - 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 "Colton Abbott" [ref=f2e206] [cursor=pointer]:
                          - /url: /customers/21713904
                    - 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 "5 Oct 2026" [ref=f2e219]
                    - row [ref=f2e222]:
                      - rowheader "Move-in" [ref=f2e223]
                      - cell "5 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 "5 - 12 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 "12 Oct 2026 Show preview" [ref=f2e284]:
                          - generic [ref=f2e287]:
                            - generic [ref=f2e288]: 12 Oct 2026
                            - link "Show preview" [ref=f2e289] [cursor=pointer]:
                              - /url: /bookings/192578859227/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/192578859227/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 "Fresh Box uC2H0S" [ref=f2e331] [cursor=pointer]:
                      - /url: /unit-types/1411800365
                    - text: /
                    - link "Unit pEhH7x" [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-2202162" [ref=f2e478] [cursor=pointer]:
                            - /url: /invoices/671188811589
                        - cell [ref=f2e480]:
                          - link "Open" [ref=f2e481] [cursor=pointer]:
                            - /url: /invoices/671188811589
                        - cell [ref=f2e486]:
                          - link "Manual" [ref=f2e487] [cursor=pointer]:
                            - /url: /invoices/671188811589
                        - cell [ref=f2e489]:
                          - link "7 Oct 2026" [ref=f2e490] [cursor=pointer]:
                            - /url: /invoices/671188811589
                        - cell [ref=f2e492]:
                          - link "Unit pEhH7x" [ref=f2e493] [cursor=pointer]:
                            - /url: /invoices/671188811589
                        - cell "-" [ref=f2e495]
                        - cell [ref=f2e497]:
                          - link "€58.50" [ref=f2e498] [cursor=pointer]:
                            - /url: /invoices/671188811589
                        - cell [ref=f2e500]:
                          - link "€58.50" [ref=f2e501] [cursor=pointer]:
                            - /url: /invoices/671188811589
                        - cell [ref=f2e503]:
                          - link "5 Oct 2026" [ref=f2e504] [cursor=pointer]:
                            - /url: /invoices/671188811589
                        - 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<void> {
  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<void> {
  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<void> {
  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<void> {
> 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<void> {
  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<void> {
  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<void> {
  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<void> {
  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 | }
```