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>
This commit is contained in:
@@ -117,33 +117,47 @@ test.describe('Site Crawler - Platform Dashboard', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// 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 - 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 }) => {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Page, BrowserContext } from '@playwright/test';
|
||||
|
||||
export interface CrawlError {
|
||||
url: string;
|
||||
type: 'console' | 'network' | 'broken-link' | 'page-error';
|
||||
type: 'console' | 'network' | 'broken-link' | 'page-error' | 'redirect';
|
||||
message: string;
|
||||
details?: string;
|
||||
timestamp: Date;
|
||||
@@ -33,6 +33,7 @@ export interface CrawlReport {
|
||||
networkErrors: number;
|
||||
brokenLinks: number;
|
||||
pageErrors: number;
|
||||
redirects: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,6 +46,8 @@ export interface CrawlerOptions {
|
||||
screenshotOnError?: boolean;
|
||||
screenshotDir?: string;
|
||||
verbose?: boolean;
|
||||
/** Detect when page redirects to a different subdomain (e.g., tenant to public) */
|
||||
detectCrossSubdomainRedirects?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_OPTIONS: CrawlerOptions = {
|
||||
@@ -63,6 +66,7 @@ const DEFAULT_OPTIONS: CrawlerOptions = {
|
||||
screenshotOnError: false,
|
||||
screenshotDir: 'test-results/crawler-screenshots',
|
||||
verbose: false,
|
||||
detectCrossSubdomainRedirects: false,
|
||||
};
|
||||
|
||||
export class SiteCrawler {
|
||||
@@ -114,6 +118,24 @@ export class SiteCrawler {
|
||||
}
|
||||
}
|
||||
|
||||
private getSubdomain(url: string): string | null {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const hostname = parsed.hostname;
|
||||
// Extract subdomain from *.lvh.me
|
||||
if (hostname.endsWith('.lvh.me')) {
|
||||
const parts = hostname.split('.');
|
||||
if (parts.length >= 3) {
|
||||
return parts[0];
|
||||
}
|
||||
}
|
||||
// No subdomain (e.g., lvh.me itself)
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private shouldCrawl(url: string): boolean {
|
||||
// Skip if already visited
|
||||
if (this.visited.has(url)) {
|
||||
@@ -280,6 +302,23 @@ export class SiteCrawler {
|
||||
});
|
||||
}
|
||||
|
||||
// Check for cross-subdomain redirects
|
||||
if (this.options.detectCrossSubdomainRedirects) {
|
||||
const finalUrl = this.page.url();
|
||||
const requestedSubdomain = this.getSubdomain(url);
|
||||
const finalSubdomain = this.getSubdomain(finalUrl);
|
||||
|
||||
if (requestedSubdomain && requestedSubdomain !== finalSubdomain) {
|
||||
errors.push({
|
||||
url,
|
||||
type: 'redirect',
|
||||
message: `Redirected from ${requestedSubdomain}.lvh.me to ${finalSubdomain || 'root'}.lvh.me`,
|
||||
details: `Final URL: ${finalUrl}`,
|
||||
timestamp: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Wait a bit for React to render and any async operations
|
||||
await this.page.waitForTimeout(500);
|
||||
|
||||
@@ -378,6 +417,7 @@ export class SiteCrawler {
|
||||
networkErrors: 0,
|
||||
brokenLinks: 0,
|
||||
pageErrors: 0,
|
||||
redirects: 0,
|
||||
};
|
||||
|
||||
for (const result of this.results) {
|
||||
@@ -395,6 +435,9 @@ export class SiteCrawler {
|
||||
case 'page-error':
|
||||
summary.pageErrors++;
|
||||
break;
|
||||
case 'redirect':
|
||||
summary.redirects++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -427,7 +470,8 @@ export function formatReport(report: CrawlReport): string {
|
||||
output += ` Console errors: ${report.summary.consoleErrors}\n`;
|
||||
output += ` Network errors: ${report.summary.networkErrors}\n`;
|
||||
output += ` Broken links: ${report.summary.brokenLinks}\n`;
|
||||
output += ` Page errors: ${report.summary.pageErrors}\n\n`;
|
||||
output += ` Page errors: ${report.summary.pageErrors}\n`;
|
||||
output += ` Redirects: ${report.summary.redirects}\n\n`;
|
||||
|
||||
// List pages with errors
|
||||
const pagesWithErrors = report.results.filter(r => r.errors.length > 0);
|
||||
@@ -443,7 +487,8 @@ export function formatReport(report: CrawlReport): string {
|
||||
for (const error of result.errors) {
|
||||
const icon = error.type === 'console' ? '⚠️' :
|
||||
error.type === 'network' ? '🌐' :
|
||||
error.type === 'broken-link' ? '🔴' : '💥';
|
||||
error.type === 'broken-link' ? '🔴' :
|
||||
error.type === 'redirect' ? '↪️' : '💥';
|
||||
output += ` ${icon} [${error.type.toUpperCase()}] ${error.message}\n`;
|
||||
if (error.details) {
|
||||
output += ` Details: ${error.details.substring(0, 200)}\n`;
|
||||
|
||||
Reference in New Issue
Block a user