# 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/invoice/invoice-download.spec.ts >> downloads the invoice pdf from the list
- Location: src/manager/tests/e2e/invoice/invoice-download.spec.ts:11:5

# Error details

```
Test timeout of 120000ms exceeded.
```

```
Error: page.waitForEvent: Test timeout of 120000ms exceeded.
=========================== logs ===========================
waiting for event "download"
============================================================
```

# Page snapshot

```yaml
- generic [ref=f1e1]:
  - generic [ref=f1e6]:
    - navigation [ref=f1e7]:
      - generic [ref=f1e9]:
        - link [ref=f1e10] [cursor=pointer]:
          - /url: /
        - generic [ref=f1e11]:
          - generic [ref=f1e12]: KINNOVIS
          - generic [ref=f1e13]: Manager
        - button [ref=f1e15] [cursor=pointer]
      - list [ref=f1e19]:
        - generic [ref=f1e21]: Dashboard
        - link "Tasks" [ref=f1e23] [cursor=pointer]:
          - /url: /tasks
        - link "Facility maps" [ref=f1e30] [cursor=pointer]:
          - /url: /facility-map
          - generic [ref=f1e31]: 󰧾
        - link "Analytics" [ref=f1e35] [cursor=pointer]:
          - /url: /dashboard
          - generic [ref=f1e36]: 󱖶
        - generic [ref=f1e40]: Sales
        - link "Bookings" [ref=f1e42] [cursor=pointer]:
          - /url: /bookings
          - generic [ref=f1e43]: 󰇡
        - link "Customers" [ref=f1e47] [cursor=pointer]:
          - /url: /customers
          - generic [ref=f1e48]: 󰀏
        - link "Invoices" [ref=f1e52] [cursor=pointer]:
          - /url: /invoices
          - generic [ref=f1e53]: 󰷉
        - link "Credit notes" [ref=f1e57] [cursor=pointer]:
          - /url: /credit-notes
        - link "Units" [ref=f1e64] [cursor=pointer]:
          - /url: /units
          - generic [ref=f1e65]: 󰍀
        - generic [ref=f1e69]: Site management
        - link "Locations" [ref=f1e71] [cursor=pointer]:
          - /url: /locations
          - generic [ref=f1e72]: 󰟙
        - link "Unit types" [ref=f1e76] [cursor=pointer]:
          - /url: /unit-types
          - generic [ref=f1e77]: 󰆧
        - link "Protection Plans" [ref=f1e81] [cursor=pointer]:
          - /url: /insurances
          - generic [ref=f1e82]: 󰳌
        - link "Deposits" [ref=f1e86] [cursor=pointer]:
          - /url: /deposits
          - generic [ref=f1e87]: 󱙆
        - link "Products" [ref=f1e91] [cursor=pointer]:
          - /url: /products
          - generic [ref=f1e92]: 󰄑
        - link "Discounts" [ref=f1e96] [cursor=pointer]:
          - /url: /discounts
          - generic [ref=f1e97]: 󰓼
        - generic [ref=f1e101]:
          - option "Emails" [ref=f1e102] [cursor=pointer]:
            - generic [ref=f1e103]: 󰻨
            - generic [ref=f1e107]: 󰅀
          - text: 󱡰 󰁥
        - generic [ref=f1e111]: Admin
        - link "Integrations" [ref=f1e113] [cursor=pointer]:
          - /url: /connected-apps
          - generic [ref=f1e114]: 󱘖
        - link "User & Roles" [ref=f1e118] [cursor=pointer]:
          - /url: /users
          - generic [ref=f1e119]: 󰭘
        - generic [ref=f1e123]:
          - option "Booking Portal" [ref=f1e124] [cursor=pointer]:
            - generic [ref=f1e125]: 󱃁
            - generic [ref=f1e129]: 󰅀
          - text: 󰖟 󰟙
        - link "Public API" [ref=f1e133] [cursor=pointer]:
          - /url: /public-api
          - generic [ref=f1e134]: 󱂛
        - generic [ref=f1e138]: Feedback
        - link "Voting Portal" [ref=f1e140] [cursor=pointer]:
          - /url: /voting-portal
          - generic [ref=f1e141]: 󰔔
      - generic [ref=f1e147]:
        - button "AS Andreas Stadler andreas.stadler@storeroom.at" [ref=f1e150] [cursor=pointer]:
          - generic [ref=f1e151]: AS
          - generic [ref=f1e154]:
            - generic [ref=f1e155]: Andreas Stadler
            - generic "andreas.stadler@storeroom.at" [ref=f1e156]
          - generic [ref=f1e157]: 󰇙
          - generic: 󰗊 󰅀 󰷖 󰍃
        - button [ref=f1e158] [cursor=pointer]:
          - generic [ref=f1e159]: 󰋽
    - main [ref=f1e161]:
      - generic [ref=f1e163]:
        - generic [ref=f1e165]:
          - generic [ref=f1e166]: Invoices
          - generic [ref=f1e169]:
            - link [ref=f1e170] [cursor=pointer]:
              - /url: /invoices/create
              - generic [ref=f1e171]:
                - generic [ref=f1e172]: 󰐕
                - text: Create Invoice
            - button [ref=f1e173] [cursor=pointer]:
              - generic [ref=f1e174]: 󰍝
        - generic [ref=f1e179]:
          - generic [ref=f1e181]:
            - button [ref=f1e182] [cursor=pointer]:
              - generic [ref=f1e183]:
                - heading "Filters" [level=3] [ref=f1e186]
                - generic [ref=f1e188]:
                  - button [ref=f1e190]:
                    - generic [ref=f1e191]:
                      - generic [ref=f1e192]: 󰑐
                      - text: Reset filters
                  - generic [ref=f1e194]: 󰅀
            - generic [ref=f1e202]:
              - generic [ref=f1e207]:
                - generic [ref=f1e208]: 󰍉
                - textbox "Search" [ref=f1e211]: INV-2204207
                - button "Clear Search" [ref=f1e213] [cursor=pointer]: 󰅙
                - generic: Search
              - combobox [ref=f1e217]:
                - generic [ref=f1e218]:
                  - generic: Location
                  - combobox "Location" [ref=f1e220]
                - button [ref=f1e222] [cursor=pointer]: 󰍝
              - combobox [ref=f1e226]:
                - generic [ref=f1e227]:
                  - generic: Status
                  - combobox "Status" [ref=f1e229]
                - button [ref=f1e231] [cursor=pointer]: 󰍝
              - combobox [ref=f1e235]:
                - generic [ref=f1e236]:
                  - generic: Payment option
                  - combobox "Payment option" [ref=f1e238]
                - button [ref=f1e240] [cursor=pointer]: 󰍝
              - combobox [ref=f1e244]:
                - generic [ref=f1e245]:
                  - generic: Type
                  - combobox "Type" [ref=f1e247]
                - button [ref=f1e249] [cursor=pointer]: 󰍝
              - generic [ref=f1e254]:
                - generic [ref=f1e255]: 󰸗
                - generic [ref=f1e257]:
                  - generic: Created at
                  - textbox "Created at" [ref=f1e258]:
                    - generic: 󰅁 󰍝 󰅂 󰅁 󰍝 󰅂
                - text: 󰅙
          - generic [ref=f1e260]:
            - generic [ref=f1e262]:
              - generic [ref=f1e264]:
                - button "Select all" [ref=f1e272] [cursor=pointer]:
                  - generic [ref=f1e273]: 󰄬
                - generic [ref=f1e276]:
                  - button "Mark as paid" [disabled] [ref=f1e279]:
                    - generic [ref=f1e280]: 󰗡
                  - button "SEPA XML actions" [disabled] [ref=f1e285]:
                    - generic [ref=f1e286]: 󱨉
                    - generic: 󰗡 󰅚 󰗖
                  - generic [ref=f1e291]:
                    - button [ref=f1e292] [cursor=pointer]
                    - generic: 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲 󰄲
              - table [ref=f1e298]:
                - rowgroup [ref=f1e299]:
                  - row [ref=f1e300]:
                    - columnheader [ref=f1e301]:
                      - generic [ref=f1e302]:
                        - checkbox [ref=f1e303]
                        - generic [ref=f1e304] [cursor=pointer]: 󰄱
                    - columnheader [ref=f1e306]:
                      - button "Number" [ref=f1e307] [cursor=pointer]:
                        - generic [ref=f1e310]: 󰁝
                    - columnheader [ref=f1e313]:
                      - button "Location" [ref=f1e314] [cursor=pointer]:
                        - generic [ref=f1e317]: 󰁝
                    - columnheader [ref=f1e320]:
                      - button "Status" [ref=f1e321] [cursor=pointer]:
                        - generic [ref=f1e324]: 󰁝
                    - columnheader [ref=f1e327]:
                      - button "Payment option" [ref=f1e328] [cursor=pointer]:
                        - generic [ref=f1e331]: 󰁝
                    - columnheader [ref=f1e334]:
                      - button "Due date" [ref=f1e335] [cursor=pointer]:
                        - generic [ref=f1e338]: 󰁝
                    - columnheader [ref=f1e341]:
                      - button "Customer" [ref=f1e342] [cursor=pointer]:
                        - generic [ref=f1e345]: 󰁝
                    - columnheader [ref=f1e348]:
                      - button "Unit" [ref=f1e349] [cursor=pointer]:
                        - generic [ref=f1e352]: 󰁝
                    - columnheader "Discount" [ref=f1e355]
                    - columnheader [ref=f1e359]:
                      - button "Total (incl. VAT)" [ref=f1e360] [cursor=pointer]:
                        - generic [ref=f1e361]:
                          - generic [ref=f1e362]: Total (incl. VAT)
                          - generic [ref=f1e363]: 󰋽
                        - generic [ref=f1e365]: 󰁝
                    - columnheader [ref=f1e368]:
                      - button "Amount due" [ref=f1e369] [cursor=pointer]:
                        - generic [ref=f1e372]: 󰁝
                    - columnheader [ref=f1e375]:
                      - button "Issue date" [ref=f1e376] [cursor=pointer]:
                        - generic [ref=f1e379]: 󰁝
                    - columnheader "Actions" [ref=f1e382]
                  - row [ref=f1e386]:
                    - columnheader [ref=f1e387]
                - rowgroup [ref=f1e388]:
                  - row [ref=f1e389]:
                    - cell [ref=f1e390]:
                      - generic [ref=f1e391]:
                        - checkbox [ref=f1e392]
                        - generic [ref=f1e393] [cursor=pointer]: 󰄱
                    - cell [ref=f1e395]:
                      - link "INV-2204207" [ref=f1e396] [cursor=pointer]:
                        - /url: /invoices/764567039
                    - cell [ref=f1e398]:
                      - link "Vienna South" [ref=f1e399] [cursor=pointer]:
                        - /url: /invoices/764567039
                    - cell [ref=f1e401]:
                      - link "Open" [ref=f1e402] [cursor=pointer]:
                        - /url: /invoices/764567039
                    - cell [ref=f1e407]:
                      - link "Manual" [ref=f1e408] [cursor=pointer]:
                        - /url: /invoices/764567039
                    - cell [ref=f1e410]:
                      - link "8 Oct 2026" [ref=f1e411] [cursor=pointer]:
                        - /url: /invoices/764567039
                    - cell [ref=f1e413]:
                      - link "Savannah Fadel" [ref=f1e414] [cursor=pointer]:
                        - /url: /invoices/764567039
                    - cell [ref=f1e417]:
                      - link [ref=f1e418] [cursor=pointer]:
                        - /url: /invoices/764567039
                    - cell "-" [ref=f1e419]
                    - cell [ref=f1e421]:
                      - link "€1,018.80" [ref=f1e422] [cursor=pointer]:
                        - /url: /invoices/764567039
                    - cell [ref=f1e424]:
                      - link "€1,018.80" [ref=f1e425] [cursor=pointer]:
                        - /url: /invoices/764567039
                    - cell [ref=f1e427]:
                      - link "6 Oct 2026" [ref=f1e428] [cursor=pointer]:
                        - /url: /invoices/764567039
                    - cell [ref=f1e430]:
                      - button [expanded] [active] [ref=f1e433] [cursor=pointer]:
                        - generic [ref=f1e434]: 󰇘
                        - list [ref=f1e438]:
                          - generic [ref=f1e439]:
                            - listitem:
                              - generic:
                                - generic:
                                  - generic: 󰇚
                                  - generic: Download
                          - generic [ref=f1e440]:
                            - listitem:
                              - generic:
                                - generic:
                                  - generic: 󰯝
                                  - generic: Pay with balance
            - generic [ref=f1e443]:
              - generic [ref=f1e444]:
                - generic [ref=f1e445]: "Items per page:"
                - combobox [ref=f1e448]:
                  - generic [ref=f1e450] [cursor=pointer]:
                    - generic [ref=f1e451]: "10"
                    - combobox "Items per page:": "10"
                  - generic [ref=f1e453]: 󰍝
              - generic [ref=f1e455]: 1-1 of 1
              - generic [ref=f1e456]:
                - 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 "Download Pay with balance"
    - tooltip
    - tooltip
```

# Test source

```ts
  1  | import { test } from '@/manager/modules/invoice/invoice-fixtures';
  2  | import { getCustomerDisplayName } from '@/manager/modules/customer/customer-utils';
  3  | import { toInvoiceCreateTotalsSummary } from '@/manager/modules/invoice/invoice-mappers';
  4  | import { formatCurrency } from '@/shared/utils/formatters';
  5  | import { expect } from '@/shared/utils/matchers';
  6  | import { readPdfText, toPdfText } from '@/shared/utils/pdf-utils';
  7  | import { pollOrSkip } from '@/shared/utils/poll-utils';
  8  | 
  9  | test.describe.configure({ mode: 'default', timeout: 120_000 });
  10 | 
  11 | test('downloads the invoice pdf from the list', async ({
  12 |   page,
  13 |   setupInvoiceCreateData,
  14 |   createInvoice,
  15 |   invoiceListPage,
  16 | }) => {
  17 |   const data = await setupInvoiceCreateData({ numberOfProducts: 1 });
  18 |   const customerName = getCustomerDisplayName(data.customer);
  19 |   // The storeroom Stripe account renders its documents in German, so the PDF total is German-formatted.
  20 |   const amount = formatCurrency(toInvoiceCreateTotalsSummary(data).totalInclVat, data.location.currency, 'de-DE');
  21 | 
  22 |   const invoice = await createInvoice(data);
  23 | 
  24 |   await invoiceListPage.goto();
  25 | 
  26 |   await test.step('apply filter', () => invoiceListPage.searchTextField.fill(invoice.number));
  27 | 
  28 |   // Unlike the credit-note list there is no pdf-readiness indicator to gate on (the download action is always
  29 |   // in the row menu), so the poll waits for the invoice row itself. The applied filter persists across reloads
  30 |   // (grid filters live in localStorage), so the row under test stays the only one.
  31 |   await test.step('wait for the invoice to appear in the list', () =>
  32 |     pollOrSkip(
  33 |       async () => {
  34 |         await invoiceListPage.reload();
  35 |         await expect(invoiceListPage.dataTable.getRows()).toHaveCount(1);
  36 |       },
  37 |       { timeout: 60_000, reason: 'invoice did not appear in the list within 60s — test-env queue backlog (KIN-4692)' }
  38 |     ));
  39 | 
  40 |   const download = await test.step('download invoice pdf', async () => {
  41 |     // The action fetches a signed Kinnovis storage link and opens it in a new tab that immediately turns into a
  42 |     // file download and closes itself — Chromium attributes that download to the opener page, so listen there.
  43 |     const [invoicePdfDownload] = await Promise.all([
> 44 |       page.waitForEvent('download'),
     |            ^ Error: page.waitForEvent: Test timeout of 120000ms exceeded.
  45 |       invoiceListPage.clickDownloadInvoicePdfAction(0),
  46 |     ]);
  47 | 
  48 |     return invoicePdfDownload;
  49 |   });
  50 | 
  51 |   await test.step('verify invoice pdf contents', async () => {
  52 |     const pdfText = await readPdfText(await download.path());
  53 | 
  54 |     expect(pdfText).toContain(toPdfText(invoice.number));
  55 |     expect(pdfText).toContain(toPdfText(customerName));
  56 |     expect(pdfText).toContain(toPdfText(amount));
  57 |   });
  58 | });
  59 | 
  60 | test('downloads the invoice pdf from the details page', async ({
  61 |   page,
  62 |   setupInvoiceCreateData,
  63 |   createInvoice,
  64 |   invoiceDetailsPage,
  65 | }) => {
  66 |   const data = await setupInvoiceCreateData({ numberOfProducts: 1 });
  67 |   const customerName = getCustomerDisplayName(data.customer);
  68 |   // The storeroom Stripe account renders its documents in German, so the PDF total is German-formatted.
  69 |   const amount = formatCurrency(toInvoiceCreateTotalsSummary(data).totalInclVat, data.location.currency, 'de-DE');
  70 | 
  71 |   const invoice = await createInvoice(data);
  72 | 
  73 |   await invoiceDetailsPage.goto(invoice.id);
  74 | 
  75 |   const download = await test.step('download invoice pdf', async () => {
  76 |     // The action fetches a signed Kinnovis storage link and opens it in a new tab that immediately turns into a
  77 |     // file download and closes itself — Chromium attributes that download to the opener page, so listen there.
  78 |     const [invoicePdfDownload] = await Promise.all([
  79 |       page.waitForEvent('download'),
  80 |       invoiceDetailsPage.clickDownloadInvoicePdfAction(),
  81 |     ]);
  82 | 
  83 |     return invoicePdfDownload;
  84 |   });
  85 | 
  86 |   await test.step('verify invoice pdf contents', async () => {
  87 |     const pdfText = await readPdfText(await download.path());
  88 | 
  89 |     expect(pdfText).toContain(toPdfText(invoice.number));
  90 |     expect(pdfText).toContain(toPdfText(customerName));
  91 |     expect(pdfText).toContain(toPdfText(amount));
  92 |   });
  93 | });
  94 | 
```