# 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: applications/bwl/suite.spec.ts >> Imports for bwl >> generate-fakes "Download CSV" delivers a CSV with rows for the selected period
- Location: src/framework/specs/imports.spec.ts:223:11

# Error details

```
Error: expect(locator).toBeVisible() failed

Locator: locator('.p-dialog-mask .p-dialog').first().locator('[data-testid="fakes-period-select"]')
Expected: visible
Timeout: 5000ms
Error: element(s) not found

Call log:
  - Expect "toBeVisible" with timeout 5000ms
  - waiting for locator('.p-dialog-mask .p-dialog').first().locator('[data-testid="fakes-period-select"]')

```

# Page snapshot

```yaml
- generic [ref=e1]:
  - generic [ref=e2]:
    - generic [ref=e3]:
      - generic [ref=e5]:
        - generic [ref=e8]:
          - link "MasterEV Home" [ref=e9] [cursor=pointer]:
            - /url: /admin/applications/bwl
            - img [ref=e12]
          - generic [ref=e17]:
            - link "BMT München" [ref=e18] [cursor=pointer]:
              - /url: /admin/applications/bwl
            - button "E2E" [ref=e20] [cursor=pointer]:
              - generic [ref=e24]:
                - generic [ref=e25]: 
                - generic [ref=e26]: E2E
                - generic [ref=e27]: 
              - generic [ref=e28]: 
        - menubar [ref=e29]:
          - menuitem "Bewerbungen" [ref=e30]:
            - link "Bewerbungen" [ref=e32] [cursor=pointer]:
              - /url: /admin/applications/bwl/list
              - generic [ref=e33]: Bewerbungen
              - img [ref=e34]
          - menuitem "Stage 1" [ref=e36]:
            - link "Stage 1" [ref=e38] [cursor=pointer]:
              - /url: /admin/applications/bwl/process/stage1
              - generic [ref=e39]: Stage 1
              - img [ref=e40]
          - menuitem "Stage 2" [ref=e42]:
            - link "Stage 2" [ref=e44] [cursor=pointer]:
              - /url: /admin/applications/bwl/process/stage2
              - generic [ref=e45]: Stage 2
              - img [ref=e46]
        - generic [ref=e49]:
          - combobox "Suche in BMT München" [ref=e51]
          - generic [ref=e55] [cursor=pointer]:
            - generic [ref=e56]: 
            - generic [ref=e59]: E2E ADMIN applications-bwl-suite-spec-ts
            - generic [ref=e60]: 
      - generic [ref=e63]:
        - generic [ref=e67]:
          - heading "Bewerbungsverwaltung" [level=1] [ref=e68]
          - paragraph [ref=e69]: Importe, Bewerbungszeiträume und historische Daten verwalten
        - generic [ref=e73]:
          - tablist [ref=e76]:
            - tab "Imports" [selected] [ref=e78] [cursor=pointer]
            - tab "Application Periods" [ref=e80] [cursor=pointer]
            - tab "Historic Data" [ref=e82] [cursor=pointer]
          - generic [ref=e86]:
            - generic [ref=e88]:
              - generic [ref=e91]:
                - generic [ref=e92]: 
                - textbox "Suchen" [ref=e93]
              - button " Testdaten generieren" [ref=e95] [cursor=pointer]:
                - generic [ref=e96]: 
                - generic [ref=e97]: Testdaten generieren
              - button " Importieren" [ref=e99] [cursor=pointer]:
                - generic [ref=e100]: 
                - generic [ref=e101]: Importieren
            - generic [ref=e102]:
              - generic [ref=e104]:
                - generic [ref=e106] [cursor=pointer]:
                  - text: Importdatum
                  - generic [ref=e107]: 
                - generic [ref=e109]: Datei
                - generic [ref=e111]: Bewerbungszeiträume
                - generic [ref=e113] [cursor=pointer]:
                  - text: Ergebnis
                  - generic [ref=e114]: 
                - generic [ref=e116] [cursor=pointer]:
                  - text: Benutzer:in
                  - generic [ref=e117]: 
              - generic [ref=e122]: Keine Ergebnisse gefunden
      - generic [ref=e125]:
        - link " Impressum" [ref=e128] [cursor=pointer]:
          - /url: /pages/sitenotice
          - generic [ref=e129]: 
          - generic [ref=e130]: Impressum
        - link " Datenschutz" [ref=e133] [cursor=pointer]:
          - /url: /pages/privacy
          - generic [ref=e134]: 
          - generic [ref=e135]: Datenschutz
        - generic [ref=e139] [cursor=pointer]: 
    - generic:
      - generic:
        - alertdialog
  - dialog "Testdatengenerierung" [ref=e141]:
    - generic [ref=e145]: Testdatengenerierung
    - generic [ref=e147]:
      - generic [ref=e148]:
        - paragraph [ref=e149]: Hiermit können Sie jederzeit Testdaten (sog. Fake-Daten) zum Testen des Prozesses generieren. Die Datensätze können entweder direkt importiert oder als CSV-Datei heruntergeladen werden.
        - paragraph [ref=e150]: Die Daten können nach dem Testen einfach wieder gelöscht werden, indem der entsprechende Import revidiert wird.
      - alert [ref=e151]:
        - generic [ref=e154]: Keine Länderdaten geladen. Die Länderliste muss vor der Testdatengenerierung eingespielt werden.
      - generic [ref=e155]:
        - button " Abbrechen" [active] [ref=e157] [cursor=pointer]:
          - generic [ref=e158]: 
          - generic [ref=e159]: Abbrechen
        - button " Direkt Importieren" [disabled] [ref=e161]:
          - generic [ref=e162]: 
          - generic [ref=e163]: Direkt Importieren
        - button " CSV herunterladen" [disabled] [ref=e165]:
          - generic [ref=e166]: 
          - generic [ref=e167]: CSV herunterladen
```

# Test source

```ts
  144 |       //
  145 |       // The prerequisites (>=1 applicationPeriod, >=1 country) are
  146 |       // seeded idempotently via `e2e:ensureFakesPrerequisites`. On
  147 |       // staging the BMT customer can land in a no-periods state after
  148 |       // a semester reset, which would otherwise make these tests
  149 |       // assert against the empty-state error instead of the select.
  150 |       // ==========================================================
  151 |       const fakesPeriodShort = 'E2EFAKES';
  152 | 
  153 |       test('generate-fakes dialog opens with real application periods', async ({
  154 |         adminPage,
  155 |       }) => {
  156 |         await convexRun('e2e:ensureFakesPrerequisites', {
  157 |           customer: actions.customer,
  158 |           process: actions.process,
  159 |           periodShort: fakesPeriodShort,
  160 |         });
  161 | 
  162 |         await adminPage.goto(importsPath);
  163 |         await expect(
  164 |           adminPage.locator('masterev-infinite-data-table')
  165 |         ).toBeVisible({ timeout: 10_000 });
  166 | 
  167 |         await adminPage
  168 |           .locator('[data-testid="generate-fakes-button"]')
  169 |           .click();
  170 | 
  171 |         const dialog = adminPage.locator(DIALOG).first();
  172 |         await expect(dialog).toBeVisible({ timeout: 5_000 });
  173 |         await expect(
  174 |           dialog.locator('masterev-data-imports-generate-fakes-dialog')
  175 |         ).toBeVisible({ timeout: 5_000 });
  176 | 
  177 |         // Period select (Convex path) must be visible — neither empty
  178 |         // state should be active in the BMT test customer.
  179 |         const periodSelect = dialog.locator(
  180 |           '[data-testid="fakes-period-select"]'
  181 |         );
  182 |         await expect(periodSelect).toBeVisible({ timeout: 5_000 });
  183 |         await expect(
  184 |           dialog.locator('[data-testid="fakes-no-periods-error"]')
  185 |         ).toHaveCount(0);
  186 |         await expect(
  187 |           dialog.locator('[data-testid="fakes-no-countries-error"]')
  188 |         ).toHaveCount(0);
  189 | 
  190 |         // The selected option carries the period-tag component (same
  191 |         // visual language as the header period switcher). The tag is
  192 |         // rendered inside the p-select's "selectedItem" slot.
  193 |         await expect(
  194 |           periodSelect.locator('masterev-application-period-tag').first()
  195 |         ).toBeVisible({ timeout: 5_000 });
  196 | 
  197 |         // Open the dropdown panel and assert at least one option is
  198 |         // rendered with a period tag. `appendTo="body"` puts the panel
  199 |         // outside the dialog, so locate it on `adminPage`.
  200 |         await periodSelect.locator('p-select').click();
  201 |         const panel = adminPage.locator('.p-select-overlay');
  202 |         await expect(panel).toBeVisible({ timeout: 5_000 });
  203 |         await expect(
  204 |           panel.locator('masterev-application-period-tag').first()
  205 |         ).toBeVisible({ timeout: 5_000 });
  206 |         // Close the panel before cancelling so PrimeNG cleans up the
  207 |         // overlay before we assert the dialog disappears.
  208 |         await adminPage.keyboard.press('Escape');
  209 | 
  210 |         // Both action buttons are reachable and enabled (countries +
  211 |         // periods present).
  212 |         const importBtn = dialog.locator('[data-testid="fakes-import-button"]');
  213 |         const downloadBtn = dialog.locator(
  214 |           '[data-testid="fakes-download-button"]'
  215 |         );
  216 |         await expect(importBtn).toBeEnabled();
  217 |         await expect(downloadBtn).toBeEnabled();
  218 | 
  219 |         await dialog.locator('[data-testid="fakes-cancel-button"]').click();
  220 |         await expect(dialog).not.toBeVisible({ timeout: 5_000 });
  221 |       });
  222 | 
  223 |       test('generate-fakes "Download CSV" delivers a CSV with rows for the selected period', async ({
  224 |         adminPage,
  225 |       }) => {
  226 |         await convexRun('e2e:ensureFakesPrerequisites', {
  227 |           customer: actions.customer,
  228 |           process: actions.process,
  229 |           periodShort: fakesPeriodShort,
  230 |         });
  231 | 
  232 |         await adminPage.goto(importsPath);
  233 |         await expect(
  234 |           adminPage.locator('masterev-infinite-data-table')
  235 |         ).toBeVisible({ timeout: 10_000 });
  236 | 
  237 |         await adminPage
  238 |           .locator('[data-testid="generate-fakes-button"]')
  239 |           .click();
  240 |         const dialog = adminPage.locator(DIALOG).first();
  241 |         await expect(dialog).toBeVisible({ timeout: 5_000 });
  242 |         await expect(
  243 |           dialog.locator('[data-testid="fakes-period-select"]')
> 244 |         ).toBeVisible({ timeout: 5_000 });
      |           ^ Error: expect(locator).toBeVisible() failed
  245 | 
  246 |         // Read the period code from the selected-item period tag. The
  247 |         // CSV contains this as a row value (bewerbungszeitraum column);
  248 |         // the dialog passes `period.short` straight through to the
  249 |         // generateFakes action.
  250 |         const selectedShort = (
  251 |           await dialog
  252 |             .locator(
  253 |               '[data-testid="fakes-period-select"] masterev-application-period-tag'
  254 |             )
  255 |             .first()
  256 |             .innerText()
  257 |         ).trim();
  258 |         expect(selectedShort.length).toBeGreaterThan(0);
  259 | 
  260 |         const downloadPromise = adminPage.waitForEvent('download', {
  261 |           timeout: 30_000,
  262 |         });
  263 |         await dialog.locator('[data-testid="fakes-download-button"]').click();
  264 |         const download = await downloadPromise;
  265 | 
  266 |         // The backend's `fakeCsvFilename` shape is
  267 |         // `CAMPUSonline_<customer>_<process>_fakes.csv` and the period
  268 |         // is encoded inside rows, not the filename. Assert the shape
  269 |         // here so a future filename change is caught explicitly.
  270 |         const filename = download.suggestedFilename();
  271 |         expect(filename).toMatch(
  272 |           new RegExp(`^CAMPUSonline_.*${actions.process}.*_fakes\\.csv$`, 'i')
  273 |         );
  274 | 
  275 |         // Read the CSV body and assert the selected period appears in
  276 |         // at least one data row. Streaming-read via the download stream
  277 |         // keeps the file under control across all customers.
  278 |         const stream = await download.createReadStream();
  279 |         const chunks: Buffer[] = [];
  280 |         await new Promise<void>((resolve, reject) => {
  281 |           stream.on('data', (c) => chunks.push(Buffer.from(c)));
  282 |           stream.on('end', () => resolve());
  283 |           stream.on('error', reject);
  284 |         });
  285 |         const csv = Buffer.concat(chunks).toString('utf8');
  286 |         expect(csv.length).toBeGreaterThan(0);
  287 |         expect(csv).toContain(selectedShort);
  288 | 
  289 |         // Dialog closes itself on success.
  290 |         await expect(dialog).not.toBeVisible({ timeout: 10_000 });
  291 |       });
  292 | 
  293 |       // ==========================================================
  294 |       // Revert dialog: opens with filename, cancel is non-destructive.
  295 |       // The end-to-end revert (generate -> revert -> verify gone) is
  296 |       // intentionally NOT here — it would mutate shared dev state and
  297 |       // race with parallel workers. The chunked-revert mutation itself
  298 |       // is covered by `apps/backend/convex-test/applicationImports.test.ts`.
  299 |       // ==========================================================
  300 |       test('revert dialog opens for an existing import and cancel keeps the row', async ({
  301 |         adminPage,
  302 |       }) => {
  303 |         await adminPage.goto(importsPath);
  304 |         const table = adminPage.locator('masterev-infinite-data-table');
  305 |         await expect(table).toBeVisible({ timeout: 10_000 });
  306 | 
  307 |         const revertButtons = adminPage.locator(
  308 |           '[data-testid="revert-button"]'
  309 |         );
  310 |         const count = await revertButtons.count();
  311 |         test.skip(
  312 |           count === 0,
  313 |           'No existing import rows to revert against — seed BMT fakes first'
  314 |         );
  315 | 
  316 |         // Snapshot the current row count via the revert-button locator
  317 |         // (1:1 with imports) so we can assert "nothing was deleted"
  318 |         // after cancelling.
  319 |         const before = count;
  320 |         await revertButtons.first().click();
  321 | 
  322 |         const dialog = adminPage.locator(DIALOG).first();
  323 |         await expect(dialog).toBeVisible({ timeout: 5_000 });
  324 |         await expect(
  325 |           dialog.locator('masterev-data-imports-revert-dialog')
  326 |         ).toBeVisible({ timeout: 5_000 });
  327 |         await expect(
  328 |           dialog.locator('[data-testid="revert-idle"]')
  329 |         ).toBeVisible();
  330 |         // Filename slot is populated from the row data.
  331 |         await expect(
  332 |           dialog.locator('[data-testid="revert-filename"]')
  333 |         ).not.toBeEmpty();
  334 |         await expect(
  335 |           dialog.locator('[data-testid="revert-confirm-button"]')
  336 |         ).toBeEnabled();
  337 | 
  338 |         await dialog.locator('[data-testid="revert-cancel-button"]').click();
  339 |         await expect(dialog).not.toBeVisible({ timeout: 5_000 });
  340 | 
  341 |         // The row must still be there — cancel is non-destructive.
  342 |         await expect(revertButtons).toHaveCount(before);
  343 |       });
  344 |     }
```