Files
smoothschedule/frontend/tests/e2e/site-crawler.spec.ts
poduck 94e37a2522 Add Site Builder help docs and fix FloatingHelpButton paths
- Add HelpSiteBuilder.tsx with comprehensive documentation for the
  drag-and-drop page editor (components, publishing, settings)
- Fix FloatingHelpButton to use /dashboard/help/* paths on tenant sites
- Update HelpComprehensive and HelpAutomations to rename plugins to automations
- Add site-crawler utility with cross-subdomain redirect detection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 22:42:46 -05:00

247 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
});
}
});