Major changes: - Rename "plugins" to "automations" throughout codebase - Move automation system to dedicated app (scheduling/automations/) - Add new automation marketplace, creation, and management pages Bug fixes: - Fix payment endpoint 500 errors (use has_feature() instead of attribute) - Fix scheduler showing "0 AM" instead of "12 AM" - Fix scheduler business hours double-inversion display issue - Fix scheduler scroll-to-current-time when switching views - Fix week view centering on wrong day (use Sunday-based indexing) - Fix capacity widget overflow with many resources - Fix Recharts minWidth/minHeight console warnings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
233 lines
7.3 KiB
TypeScript
233 lines
7.3 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);
|
|
});
|
|
});
|
|
|
|
// Uncomment and configure when tenant test users are set up
|
|
// test.describe('Site Crawler - Tenant Dashboard', () => {
|
|
// test('should crawl tenant dashboard without errors', 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);
|
|
// const report = await crawler.crawl('http://demo.lvh.me:5173');
|
|
//
|
|
// 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;
|
|
// if (e.message.includes('WebSocket')) return false;
|
|
// return true;
|
|
// })
|
|
// );
|
|
//
|
|
// 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);
|
|
});
|
|
}
|
|
});
|