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:
poduck
2025-12-16 11:56:01 -05:00
parent c333010620
commit cfb626b595
62 changed files with 4914 additions and 2413 deletions

View 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);
});
}
});