Rename plugins to automations and fix scheduler/payment bugs
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>
This commit is contained in:
232
frontend/tests/e2e/site-crawler.spec.ts
Normal file
232
frontend/tests/e2e/site-crawler.spec.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user