- 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>
247 lines
7.8 KiB
TypeScript
247 lines
7.8 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
}
|
||
});
|