# 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: tests/ui/sitemap.spec.ts >> sitemap URL validation >> states sitemap - sample URLs resolve
- Location: tests/ui/sitemap.spec.ts:260:7

# Error details

```
Error: Sitemap /states/sitemap-states.xml should load

expect(received).toBe(expected) // Object.is equality

Expected: 200
Received: 500
```

# Page snapshot

```yaml
- generic [active] [ref=e1]:
  - banner [ref=e2]:
    - heading "US Elections" [level=1] [ref=e3]
    - paragraph [ref=e4]: Election Data Explorer
  - generic [ref=e5]:
    - generic [ref=e6]:
      - generic [ref=e7]: Electoral
      - generic [ref=e8]:
        - link "🏛️ 290 Parties Political affiliations" [ref=e9] [cursor=pointer]:
          - /url: /parties/
          - generic [ref=e10]: 🏛️
          - generic [ref=e11]:
            - generic [ref=e12]: "290"
            - generic [ref=e13]: Parties
            - generic [ref=e14]: Political affiliations
        - link "🎖️ 746 Offices Federal positions" [ref=e15] [cursor=pointer]:
          - /url: /offices/
          - generic [ref=e16]: 🎖️
          - generic [ref=e17]:
            - generic [ref=e18]: "746"
            - generic [ref=e19]: Offices
            - generic [ref=e20]: Federal positions
        - link "👤 57,328 Politicians Candidates & office holders" [ref=e21] [cursor=pointer]:
          - /url: /politicians/
          - generic [ref=e22]: 👤
          - generic [ref=e23]:
            - generic [ref=e24]: 57,328
            - generic [ref=e25]: Politicians
            - generic [ref=e26]: Candidates & office holders
        - generic [ref=e27]:
          - generic [ref=e28]: 🗳️
          - generic [ref=e29]:
            - generic [ref=e30]: 196,257,874
            - generic [ref=e31]: Voters
            - generic [ref=e32]: Registered voter records
    - generic [ref=e33]:
      - generic [ref=e34]: Financial
      - generic [ref=e35]:
        - link "💼 89,712 Committees" [ref=e36] [cursor=pointer]:
          - /url: /committees/
          - generic [ref=e37]: 💼
          - generic [ref=e38]:
            - generic [ref=e39]: 89,712
            - generic [ref=e40]: Committees
        - link "🏢 1,398,441 Vendors" [ref=e41] [cursor=pointer]:
          - /url: /vendors/
          - generic [ref=e42]: 🏢
          - generic [ref=e43]:
            - generic [ref=e44]: 1,398,441
            - generic [ref=e45]: Vendors
        - link "🏭 5,711,245 Employers" [ref=e46] [cursor=pointer]:
          - /url: /employers/
          - generic [ref=e47]: 🏭
          - generic [ref=e48]:
            - generic [ref=e49]: 5,711,245
            - generic [ref=e50]: Employers
        - generic [ref=e51]:
          - generic [ref=e52]: 🧑
          - generic [ref=e53]:
            - generic [ref=e54]: 34,032,401
            - generic [ref=e55]: Individual Donors
    - generic [ref=e56]:
      - generic [ref=e57]: Geography
      - generic [ref=e58]:
        - link "🗺️ 52 States" [ref=e59] [cursor=pointer]:
          - /url: /states/
          - generic [ref=e60]: 🗺️
          - generic [ref=e61]: "52"
          - generic [ref=e62]: States
        - link "📍 444 Districts" [ref=e63] [cursor=pointer]:
          - /url: /districts/
          - generic [ref=e64]: 📍
          - generic [ref=e65]: "444"
          - generic [ref=e66]: Districts
        - link "🗂️ 3,222 Counties" [ref=e67] [cursor=pointer]:
          - /url: /counties/
          - generic [ref=e68]: 🗂️
          - generic [ref=e69]: 3,222
          - generic [ref=e70]: Counties
        - link "🏘️ 32,114 Places" [ref=e71] [cursor=pointer]:
          - /url: /places/
          - generic [ref=e72]: 🏘️
          - generic [ref=e73]: 32,114
          - generic [ref=e74]: Places
      - generic [ref=e75]:
        - link "📮 33,791 Postal Codes" [ref=e76] [cursor=pointer]:
          - /url: /postalcodes/
          - generic [ref=e77]: 📮
          - generic [ref=e78]: 33,791
          - generic [ref=e79]: Postal Codes
        - link "📐 85,382 Census Tracts" [ref=e80] [cursor=pointer]:
          - /url: /tracts/
          - generic [ref=e81]: 📐
          - generic [ref=e82]: 85,382
          - generic [ref=e83]: Census Tracts
        - link "🗳️ 163,242 Precincts" [ref=e84] [cursor=pointer]:
          - /url: /precincts/
          - generic [ref=e85]: 🗳️
          - generic [ref=e86]: 163,242
          - generic [ref=e87]: Precincts
        - generic [ref=e88]:
          - generic [ref=e89]: 🧱
          - generic [ref=e90]: 8,126,956
          - generic [ref=e91]: Census Blocks
    - generic [ref=e92]:
      - generic [ref=e93]: Other
      - generic [ref=e94]:
        - link "🗳️ 24 Elections" [ref=e95] [cursor=pointer]:
          - /url: /elections/
          - generic [ref=e96]: 🗳️
          - generic [ref=e97]: "24"
          - generic [ref=e98]: Elections
        - link "📊 - Data Sources" [ref=e99] [cursor=pointer]:
          - /url: /datasources/
          - generic [ref=e100]: 📊
          - generic [ref=e101]: "-"
          - generic [ref=e102]: Data Sources
  - contentinfo [ref=e103]:
    - link "elect.info" [ref=e104] [cursor=pointer]:
      - /url: https://www.elect.info
    - text: "|"
    - link "Trust Center" [ref=e105] [cursor=pointer]:
      - /url: https://trust.elect.info
    - text: "|"
    - link "Privacy Policy" [ref=e106] [cursor=pointer]:
      - /url: https://trust.elect.info/#privacy-policy
    - text: "|"
    - link "Terms of Use" [ref=e107] [cursor=pointer]:
      - /url: https://trust.elect.info/#terms
```

# Test source

```ts
  4   |  * Sitemap-based URL validation tests
  5   |  *
  6   |  * These tests fetch URLs directly from the sitemaps and verify they resolve.
  7   |  * This catches issues where:
  8   |  * - URLs in sitemaps point to non-existent pages
  9   |  * - nginx routing doesn't serve mounted content correctly
  10  |  * - Static page generation produced broken links
  11  |  *
  12  |  * Handles nested sitemaps (sitemap indexes containing child sitemaps).
  13  |  *
  14  |  * Large sitemaps (candidates: 50K+ URLs, committees: 30K+) are fetched via
  15  |  * the fetch API instead of page.goto to avoid browser rendering timeouts
  16  |  * on multi-megabyte XML documents.
  17  |  */
  18  | 
  19  | // Number of random URLs to sample from each sitemap
  20  | const SAMPLE_SIZE = 5;
  21  | 
  22  | // Number of child sitemaps to sample when encountering a sitemap index
  23  | const CHILD_SITEMAP_SAMPLE_SIZE = 2;
  24  | 
  25  | // Per-page navigation timeout for entity pages (some generated pages are slow)
  26  | const PAGE_TIMEOUT = 15000;
  27  | 
  28  | // Helper to get random items from array
  29  | function getRandomItems<T>(array: T[], count: number): T[] {
  30  |   const shuffled = [...array].sort(() => 0.5 - Math.random());
  31  |   return shuffled.slice(0, Math.min(count, array.length));
  32  | }
  33  | 
  34  | // Parse URLs from sitemap XML (works for both <url><loc> and <sitemap><loc>)
  35  | function parseUrlsFromSitemap(xml: string): string[] {
  36  |   const urls: string[] = [];
  37  |   const locRegex = /<loc>([^<]+)<\/loc>/g;
  38  |   let match;
  39  |   while ((match = locRegex.exec(xml)) !== null) {
  40  |     urls.push(match[1]);
  41  |   }
  42  |   return urls;
  43  | }
  44  | 
  45  | // Check if XML is a sitemap index (contains <sitemapindex> or <sitemap> tags)
  46  | function isSitemapIndex(xml: string): boolean {
  47  |   return xml.includes('<sitemapindex') || (xml.includes('<sitemap>') && !xml.includes('<url>'));
  48  | }
  49  | 
  50  | // Check if URL looks like a sitemap (ends in .xml)
  51  | function isSitemapUrl(url: string): boolean {
  52  |   return url.endsWith('.xml');
  53  | }
  54  | 
  55  | // Convert absolute URL to relative path for test requests
  56  | function toRelativePath(url: string): string {
  57  |   try {
  58  |     const parsed = new URL(url);
  59  |     return parsed.pathname;
  60  |   } catch {
  61  |     return url;
  62  |   }
  63  | }
  64  | 
  65  | /**
  66  |  * Fetch sitemap XML via the fetch API (inside the browser context).
  67  |  * This avoids Playwright's page.goto rendering pipeline which times out
  68  |  * on very large XML documents (50K+ URLs).
  69  |  */
  70  | async function fetchSitemapXml(page: Page, baseUrl: string, sitemapPath: string): Promise<{ status: number; xml: string }> {
  71  |   // Use page.evaluate to fetch XML via browser fetch API.
  72  |   // Large sitemaps (50K+ URLs, multi-MB XML) need extra time to download.
  73  |   return await page.evaluate(async ({ base, path }) => {
  74  |     const res = await fetch(`${base}${path}`);
  75  |     const text = await res.text();
  76  |     return { status: res.status, xml: text };
  77  |   }, { base: baseUrl, path: sitemapPath });
  78  | }
  79  | 
  80  | /**
  81  |  * Recursively sample and test URLs from a sitemap.
  82  |  * If the sitemap is an index, sample child sitemaps and recurse.
  83  |  * If the sitemap contains page URLs, sample and verify them.
  84  |  *
  85  |  * Uses fetch API for sitemap XML retrieval (fast, no rendering),
  86  |  * and page.goto only for testing individual page URLs.
  87  |  */
  88  | async function testSitemapUrls(
  89  |   page: Page,
  90  |   baseUrl: string,
  91  |   sitemapPath: string,
  92  |   depth: number = 0
  93  | ): Promise<{ tested: number; errors: string[] }> {
  94  |   const indent = '  '.repeat(depth);
  95  |   const result = { tested: 0, errors: [] as string[] };
  96  | 
  97  |   const { status, xml } = await fetchSitemapXml(page, baseUrl, sitemapPath);
  98  | 
  99  |   if (status === 404) {
  100 |     console.log(`${indent}Sitemap not found: ${sitemapPath} (skipping)`);
  101 |     return result;
  102 |   }
  103 | 
> 104 |   expect(status, `Sitemap ${sitemapPath} should load`).toBe(200);
      |                                                        ^ Error: Sitemap /states/sitemap-states.xml should load
  105 | 
  106 |   const urls = parseUrlsFromSitemap(xml);
  107 | 
  108 |   if (urls.length === 0) {
  109 |     console.log(`${indent}No URLs found in ${sitemapPath}`);
  110 |     return result;
  111 |   }
  112 | 
  113 |   console.log(`${indent}Found ${urls.length} URLs in ${sitemapPath}`);
  114 | 
  115 |   if (isSitemapIndex(xml)) {
  116 |     // This is a sitemap index - sample child sitemaps and recurse
  117 |     console.log(`${indent}Detected sitemap index, sampling ${CHILD_SITEMAP_SAMPLE_SIZE} child sitemaps`);
  118 | 
  119 |     const childSitemaps = getRandomItems(urls, CHILD_SITEMAP_SAMPLE_SIZE);
  120 |     for (const childUrl of childSitemaps) {
  121 |       const childPath = toRelativePath(childUrl);
  122 |       console.log(`${indent}Processing child sitemap: ${childPath}`);
  123 | 
  124 |       const childResult = await testSitemapUrls(page, baseUrl, childPath, depth + 1);
  125 |       result.tested += childResult.tested;
  126 |       result.errors.push(...childResult.errors);
  127 |     }
  128 |   } else {
  129 |     // This is a regular sitemap with page URLs - sample and test
  130 |     const sampleUrls = getRandomItems(urls, SAMPLE_SIZE);
  131 | 
  132 |     for (const url of sampleUrls) {
  133 |       const path = toRelativePath(url);
  134 | 
  135 |       // Check if this URL is itself a sitemap (nested sitemap within urlset)
  136 |       if (isSitemapUrl(url)) {
  137 |         console.log(`${indent}Found nested sitemap URL: ${path}`);
  138 |         const nestedResult = await testSitemapUrls(page, baseUrl, path, depth + 1);
  139 |         result.tested += nestedResult.tested;
  140 |         result.errors.push(...nestedResult.errors);
  141 |       } else {
  142 |         // Regular page URL - verify it loads
  143 |         console.log(`${indent}Testing page: ${path}`);
  144 |         const pageResponse = await page.goto(path, { timeout: PAGE_TIMEOUT });
  145 |         result.tested++;
  146 | 
  147 |         if (pageResponse?.status() !== 200) {
  148 |           const error = `Expected 200 for ${path}, got ${pageResponse?.status()}`;
  149 |           console.log(`${indent}  ERROR: ${error}`);
  150 |           result.errors.push(error);
  151 |         } else {
  152 |           // Verify page has content
  153 |           const h1Visible = await page.locator('h1').isVisible().catch(() => false);
  154 |           if (!h1Visible) {
  155 |             const error = `Page ${path} loaded but has no visible h1`;
  156 |             console.log(`${indent}  WARNING: ${error}`);
  157 |             // Don't treat missing h1 as hard failure, just log it
  158 |           }
  159 |         }
  160 |       }
  161 |     }
  162 |   }
  163 | 
  164 |   console.log(`${indent}Tested ${result.tested} URLs from ${sitemapPath}`);
  165 |   return result;
  166 | }
  167 | 
  168 | test.describe('sitemap URL validation', () => {
  169 | 
  170 |   test('sitemap index loads and contains child sitemaps', async ({ page }) => {
  171 |     const response = await page.goto('/sitemap.xml');
  172 |     expect(response?.status()).toBe(200);
  173 | 
  174 |     const content = await page.content();
  175 |     expect(content).toContain('sitemapindex');
  176 |     expect(content).toContain('sitemap-candidates.xml');
  177 |   });
  178 | 
  179 |   test('candidates sitemap - sample URLs resolve', async ({ page }) => {
  180 |     test.setTimeout(120000);
  181 | 
  182 |     // Navigate to a simple page first so we have a page context for fetch
  183 |     await page.goto('/');
  184 |     const baseUrl = new URL(page.url()).origin;
  185 | 
  186 |     const result = await testSitemapUrls(page, baseUrl, '/candidates/sitemap-candidates.xml');
  187 | 
  188 |     if (result.tested === 0) {
  189 |       test.skip();
  190 |       return;
  191 |     }
  192 | 
  193 |     console.log(`Tested ${result.tested} URLs from candidates sitemap`);
  194 |     expect(result.errors, `Errors: ${result.errors.join(', ')}`).toHaveLength(0);
  195 |   });
  196 | 
  197 |   test('committees sitemap - sample URLs resolve', async ({ page }) => {
  198 |     test.setTimeout(120000);
  199 | 
  200 |     // Navigate to a simple page first so we have a page context for fetch
  201 |     await page.goto('/');
  202 |     const baseUrl = new URL(page.url()).origin;
  203 | 
  204 |     const result = await testSitemapUrls(page, baseUrl, '/committees/sitemap-committees.xml');
```