# 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<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 | }
```