# 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: mfc-corporate-de.spec.ts >> content >> text-in-columns
- Location: visual/mfc-corporate-de.spec.ts:105:9

# Error details

```
Test timeout of 30000ms exceeded.
```

```
Error: page.waitForLoadState: Test timeout of 30000ms exceeded.
```

# Page snapshot

```yaml
- generic [ref=e2]:
  - link "Zum Inhalt springen" [ref=e3] [cursor=pointer]:
    - /url: "#page-content"
  - banner [ref=e4]:
    - generic [ref=e5]:
      - link "Marketing Factory Digital GmbH" [ref=e6] [cursor=pointer]:
        - /url: /
        - img "Marketing Factory Digital GmbH" [ref=e7]
      - link "Kontaktformular" [ref=e26] [cursor=pointer]:
        - /url: /formulare/kontaktformular/
      - button "Navigation" [expanded] [ref=e27] [cursor=pointer]
  - navigation "Brotkrumen-Navigation" [ref=e29]:
    - generic [ref=e30]:
      - paragraph [ref=e31]: "Sie sind here:"
      - list [ref=e32]:
        - listitem [ref=e33]:
          - link "Startseite" [ref=e34] [cursor=pointer]:
            - /url: /
            - img [ref=e35]
        - listitem [ref=e37]:
          - text: /
          - link "Inhaltselemente" [ref=e38] [cursor=pointer]:
            - /url: /inhaltselemente/
            - generic [ref=e39]: Inhaltselemente
        - listitem [ref=e40]:
          - text: /
          - generic [ref=e41]: Text in Columns
  - main [ref=e42]:
    - generic [ref=e44]:
      - heading "Text in Columns" [level=2] [ref=e46]
      - paragraph [ref=e48]: Weit hinten, hinter den Wortbergen, fern der Länder Vokalien und Konsonantien leben die Blindtexte. Abgeschieden wohnen sie in Buchstabhausen an der Küste des Semantik, eines großen Sprachozeans. Ein kleines Bächlein namens Duden fließt durch ihren Ort und versorgt sie mit den nötigen Regelialien. Es ist ein paradiesmatisches Land, in dem einem gebratene Satzteile in den Mund fliegen. Nicht einmal von der allmächtigen Interpunktion werden die Blindtexte beherrscht – ein geradezu unorthographisches Leben. Eines Tages aber beschloß eine kleine Zeile Blindtext, ihr Name war Lorem Ipsum, hinaus zu gehen in die weite Grammatik. Der große Oxmox riet ihr davon ab, da es dort wimmele von bösen Kommata, wilden Fragezeichen und hinterhältigen Semikoli, doch das Blindtextchen ließ sich nicht beirren. Es packte seine sieben Versalien, schob sich sein Initial in den Gürtel und machte sich auf den Weg. Als es die ersten Hügel des Kursivgebirges erklommen hatte, warf es einen letzten Blick zurück auf die Skyline seiner Heimatstadt Buchstabhausen, die Headline von Alphabetdorf und die Subline seiner eigenen Straße, der Zeilengasse. Wehmütig lief ihm eine rhetorische Frage über die Wange, dann setzte es seinen Weg fort. Unterwegs traf es eine Copy. Die Copy warnte das Blindtextchen, da, wo sie herkäme wäre sie zigmal umgeschrieben worden und alles, was von ihrem Ursprung noch übrig wäre, sei das Wort "und" und das Blindtextchen solle umkehren und wieder in sein eigenes, sicheres Land zurückkehren. Doch alles Gutzureden konnte es nicht überzeugen und so dauerte es nicht lange, bis ihm ein paar heimtückische Werbetexter auflauerten, es mit Longe und Parole betrunken machten und es dann in ihre Agentur schleppten, wo sie es für ihre Projekte wieder und wieder mißbrauchten. Und wenn es nicht umgeschrieben wurde, dann benutzen Sie es immernoch.
  - contentinfo [ref=e49]:
    - generic [ref=e52]:
      - navigation [ref=e53]:
        - list [ref=e54]:
          - listitem [ref=e55]:
            - button "Cookie-Einstellungen" [ref=e56] [cursor=pointer]
          - listitem [ref=e57]:
            - link "Datenschutzerklärung" [ref=e58] [cursor=pointer]:
              - /url: https://www.marketing-factory.de.renovate-develop-pnpm-10-x.review.mfc.gmbh/datenschutzerklaerung/
          - listitem [ref=e59]:
            - link "Impressum" [ref=e60] [cursor=pointer]:
              - /url: https://www.marketing-factory.de.renovate-develop-pnpm-10-x.review.mfc.gmbh/impressum/
      - generic [ref=e61]:
        - paragraph [ref=e62]: © Marketing Factory Digital GmbH
        - paragraph [ref=e63]:
          - text: Alternativtexte für Bilder sind teilweise
          - link "KI-generiert" [ref=e64] [cursor=pointer]:
            - /url: https://www.marketing-factory.de.renovate-develop-pnpm-10-x.review.mfc.gmbh/technologie/typo3/unsere-typo3-extensions/ai-filemetadata/
```

# Test source

```ts
  1   | import { Page } from '@playwright/test';
  2   | 
  3   | const BLOCKED_DOMAINS = [
  4   |     'google-analytics.com',
  5   |     'googletagmanager.com',
  6   |     'analytics.google.com',
  7   |     'stats.g.doubleclick.net',
  8   |     'googleads.g.doubleclick.net',
  9   |     'sentry.marketing-factory.de',
  10  | ];
  11  | 
  12  | const BLOCKED_PATH_PARTS = [
  13  |     '/sentry-init',
  14  |     '/ga4Integration'
  15  | ];
  16  | 
  17  | export async function blockDomains(page: Page, domains: string[] = BLOCKED_DOMAINS, pathParts: string[] = BLOCKED_PATH_PARTS): Promise<void> {
  18  |     await page.route('**', route => {
  19  |         const url = new URL(route.request().url());
  20  |         const hostname = url.hostname;
  21  |         const path = url.pathname;
  22  | 
  23  |         if (pathParts.some(p => path.includes(p))) {
  24  |             route.abort();
  25  |             return;
  26  |         }
  27  | 
  28  |         if (domains.some(d => hostname === d || hostname.endsWith(`.${d}`))) {
  29  |             route.abort();
  30  |         } else {
  31  |             route.continue();
  32  |         }
  33  |     });
  34  | }
  35  | 
  36  | const CSS_OVERRIDES = [
  37  |     '.c-header-content { transition: unset; }',
  38  |     '.c-header-search__form { display: none; transition: unset; }',
  39  |     '.c-captcha__img { filter: brightness(0); }',
  40  |     '.o-radio-switch__label:first-of-type::before, .c-comparison__slide-bg, .c-comparison__content { transition: unset; }',
  41  |     '.c-btn { transition: unset !important; }',
  42  |     '.c-header-content { transition: none !important; }',
  43  |     '.c-comparison__switch { position: initial !important; }',
  44  |     'figure.video { visibility: hidden !important; }',
  45  |     '::file-selector-button { transition: none !important; }',
  46  | ].join(' ');
  47  | 
  48  | export async function overrideCSS(page: Page): Promise<void> {
  49  |     await page.addStyleTag({ content: CSS_OVERRIDES });
  50  | }
  51  | 
  52  | export async function loadLazyImages(page: Page): Promise<void> {
  53  |     await page.evaluate(() => {
  54  |         document.querySelectorAll<HTMLImageElement>('[loading="lazy"]').forEach(el => { el.loading = 'eager'; });
  55  |         document.querySelectorAll<HTMLImageElement>('[decoding="async"]').forEach(el => { el.decoding = 'sync'; });
  56  |     });
  57  | }
  58  | 
  59  | export async function waitForFontsAndImages(page: Page): Promise<void> {
  60  |     await page.evaluate(async () => {
  61  |         await document.fonts.ready;
  62  |         await Promise.all(Array.from(document.images).map(img => img.decode().catch(() => undefined)));
  63  |     });
  64  | }
  65  | 
  66  | export async function scrollToBottom(page: Page): Promise<void> {
  67  |     await page.evaluate(async () => {
  68  |         await new Promise<void>(resolve => {
  69  |             let totalHeight = 0;
  70  |             const distance = 500;
  71  |             const timer = setInterval(() => {
  72  |                 const scrollHeight = document.body.scrollHeight;
  73  |                 window.scrollBy(0, distance);
  74  |                 totalHeight += distance;
  75  |                 if (totalHeight >= scrollHeight) {
  76  |                     clearInterval(timer);
  77  |                     resolve();
  78  |                 }
  79  |             }, 100);
  80  |         });
  81  |     });
  82  |     await page.waitForLoadState('networkidle');
  83  |     await waitForFontsAndImages(page);
  84  | }
  85  | 
  86  | export async function setDarkTheme(page: Page): Promise<void> {
  87  |     await page.evaluate(() => {
  88  |         document.documentElement.dataset.colorMode = 'dark';
  89  |     });
  90  | }
  91  | 
  92  | export async function waitUntilLoaded(page: Page): Promise<void> {
> 93  |     await page.waitForLoadState("networkidle");
      |                ^ Error: page.waitForLoadState: Test timeout of 30000ms exceeded.
  94  | }
  95  | 
  96  | export async function setup(page: Page, url: string): Promise<void> {
  97  |     await blockDomains(page);
  98  |     await page.goto(url, { waitUntil: 'load' });
  99  |     await overrideCSS(page);
  100 |     await loadLazyImages(page);
  101 |     await waitForFontsAndImages(page);
  102 |     await waitUntilLoaded(page);
  103 | }
  104 | 
  105 | export async function hideElements(page: Page, selectors: string[]): Promise<void> {
  106 |     if (selectors.length === 0) return;
  107 |     const css = selectors.map(s => `${s} { display: none !important; }`).join(' ');
  108 |     await page.addStyleTag({ content: css });
  109 | }
  110 | 
```