/** * Site Crawler Tests * * Crawls the site looking for broken links, console errors, and network failures. * Run with: npx playwright test site-crawler.spec.ts * * These tests are designed to be run in CI to catch issues before deployment. */ import { test, expect } from '@playwright/test'; import { SiteCrawler, formatReport, loginAsUser, TEST_USERS, CrawlerOptions, } from './utils/crawler'; // Increase timeout for crawl tests since they visit many pages test.setTimeout(300000); // 5 minutes const CRAWLER_OPTIONS: CrawlerOptions = { maxPages: 30, // Limit for CI to keep tests reasonable verbose: false, screenshotOnError: true, screenshotDir: 'test-results/crawler-screenshots', timeout: 15000, waitForNetworkIdle: true, excludePatterns: [ /\.(pdf|zip|tar|gz|exe|dmg|pkg)$/i, /^mailto:/i, /^tel:/i, /^javascript:/i, /logout/i, /sign-out/i, // Exclude external links for faster tests /^https?:\/\/(?!.*lvh\.me)/i, ], }; test.describe('Site Crawler - Public Pages', () => { test('should crawl public marketing site without errors', async ({ page, context }) => { const crawler = new SiteCrawler(page, context, { ...CRAWLER_OPTIONS, maxPages: 20, // Fewer pages for public site }); const report = await crawler.crawl('http://lvh.me:5173'); // Print the report console.log(formatReport(report)); // Assertions expect(report.totalPages).toBeGreaterThan(0); // Filter out minor warnings that aren't real issues const criticalErrors = report.results.flatMap(r => r.errors.filter(e => { // Ignore React DevTools suggestion if (e.message.includes('React DevTools')) return false; // Ignore favicon 404 if (e.message.includes('favicon.ico')) return false; // Ignore hot module replacement messages if (e.message.includes('hot-update') || e.message.includes('hmr')) return false; return true; }) ); // Fail if there are critical errors if (criticalErrors.length > 0) { console.error('\n❌ Critical errors found:'); criticalErrors.forEach(e => { console.error(` - [${e.type}] ${e.url}: ${e.message}`); }); } expect(criticalErrors.length).toBe(0); }); }); test.describe('Site Crawler - Platform Dashboard', () => { test('should crawl platform dashboard without errors', async ({ page, context }) => { // Login first const user = TEST_USERS.platformSuperuser; const loggedIn = await loginAsUser(page, user); expect(loggedIn).toBe(true); const crawler = new SiteCrawler(page, context, CRAWLER_OPTIONS); const report = await crawler.crawl('http://platform.lvh.me:5173'); // Print the report console.log(formatReport(report)); // Assertions expect(report.totalPages).toBeGreaterThan(0); // Filter out minor warnings const criticalErrors = report.results.flatMap(r => r.errors.filter(e => { if (e.message.includes('React DevTools')) return false; if (e.message.includes('favicon.ico')) return false; if (e.message.includes('hot-update') || e.message.includes('hmr')) return false; // Ignore WebSocket connection issues in dev if (e.message.includes('WebSocket')) return false; return true; }) ); if (criticalErrors.length > 0) { console.error('\n❌ Critical errors found:'); criticalErrors.forEach(e => { console.error(` - [${e.type}] ${e.url}: ${e.message}`); }); } expect(criticalErrors.length).toBe(0); }); }); test.describe('Site Crawler - Tenant Dashboard', () => { test('should crawl tenant dashboard without cross-subdomain redirects', async ({ page, context }) => { const user = TEST_USERS.businessOwner; const loggedIn = await loginAsUser(page, user); expect(loggedIn).toBe(true); const crawler = new SiteCrawler(page, context, { ...CRAWLER_OPTIONS, detectCrossSubdomainRedirects: true, // Detect redirects to public site }); const report = await crawler.crawl('http://demo.lvh.me:5173'); console.log(formatReport(report)); expect(report.totalPages).toBeGreaterThan(0); // Filter critical errors (excluding minor dev warnings) const criticalErrors = report.results.flatMap(r => r.errors.filter(e => { if (e.message.includes('React DevTools')) return false; if (e.message.includes('favicon.ico')) return false; if (e.message.includes('hot-update')) return false; if (e.message.includes('WebSocket')) return false; return true; }) ); // Log redirect issues separately for visibility const redirectErrors = criticalErrors.filter(e => e.type === 'redirect'); if (redirectErrors.length > 0) { console.error('\n⚠️ CROSS-SUBDOMAIN REDIRECTS DETECTED:'); redirectErrors.forEach(e => { console.error(` - ${e.url}`); console.error(` ${e.message}`); if (e.details) console.error(` ${e.details}`); }); } expect(criticalErrors.length).toBe(0); }); }); // test.describe('Site Crawler - Customer Portal', () => { // test('should crawl customer booking portal without errors', async ({ page, context }) => { // // Customer portal might not require auth for the booking pages // const crawler = new SiteCrawler(page, context, { // ...CRAWLER_OPTIONS, // maxPages: 15, // }); // // const report = await crawler.crawl('http://demo.lvh.me:5173/book'); // // console.log(formatReport(report)); // // expect(report.totalPages).toBeGreaterThan(0); // // const criticalErrors = report.results.flatMap(r => // r.errors.filter(e => { // if (e.message.includes('React DevTools')) return false; // if (e.message.includes('favicon.ico')) return false; // if (e.message.includes('hot-update')) return false; // return true; // }) // ); // // expect(criticalErrors.length).toBe(0); // }); // }); // Quick smoke test that just checks key pages load test.describe('Site Crawler - Quick Smoke Test', () => { const keyPages = [ { name: 'Home', url: 'http://lvh.me:5173/' }, { name: 'About', url: 'http://lvh.me:5173/about' }, { name: 'Pricing', url: 'http://lvh.me:5173/pricing' }, { name: 'Privacy', url: 'http://lvh.me:5173/privacy' }, { name: 'Terms', url: 'http://lvh.me:5173/terms' }, { name: 'Contact', url: 'http://lvh.me:5173/contact' }, ]; for (const { name, url } of keyPages) { test(`${name} page should load without errors`, async ({ page }) => { const errors: string[] = []; // Capture console errors page.on('console', msg => { if (msg.type() === 'error') { const text = msg.text(); // Filter out non-critical if (!text.includes('React DevTools') && !text.includes('favicon')) { errors.push(`Console: ${text}`); } } }); // Capture page errors page.on('pageerror', error => { errors.push(`Page error: ${error.message}`); }); // Capture failed requests page.on('requestfailed', request => { const failedUrl = request.url(); if (!failedUrl.includes('favicon') && !failedUrl.includes('hot-update')) { errors.push(`Request failed: ${failedUrl}`); } }); await page.goto(url, { waitUntil: 'networkidle' }); // Wait a bit for React to fully render await page.waitForTimeout(1000); // Check that page rendered something const bodyContent = await page.locator('body').textContent(); expect(bodyContent?.length).toBeGreaterThan(100); // Report and fail if errors if (errors.length > 0) { console.error(`\n❌ Errors on ${name} page (${url}):`); errors.forEach(e => console.error(` - ${e}`)); } expect(errors.length).toBe(0); }); } });