Complete Phase 1 critical test coverage expansion and begin Phase 2

Phase 1 Achievements (47 new test scenarios):
• Modern Framework Integration Suite (20 scenarios)
  - React 18 with hooks, state management, component interactions
  - Vue 3 with Composition API, reactivity system, watchers
  - Angular 17 with services, RxJS observables, reactive forms
  - Cross-framework compatibility and performance comparison

• Mobile Browser Compatibility Suite (15 scenarios)
  - iPhone 13/SE, Android Pixel/Galaxy, iPad Air configurations
  - Touch events, gesture support, viewport adaptation
  - Mobile-specific APIs (orientation, battery, network)
  - Safari/Chrome mobile quirks and optimizations

• Advanced User Interaction Suite (12 scenarios)
  - Multi-step form workflows with validation
  - Drag-and-drop file handling and complex interactions
  - Keyboard navigation and ARIA accessibility
  - Multi-page e-commerce workflow simulation

Phase 2 Started - Production Network Resilience:
• Enterprise proxy/firewall scenarios with content filtering
• CDN failover strategies with geographic load balancing
• HTTP connection pooling optimization
• DNS failure recovery mechanisms

Infrastructure Enhancements:
• Local test server with React/Vue/Angular demo applications
• Production-like SPAs with complex state management
• Cross-platform mobile/tablet/desktop configurations
• Network resilience testing framework

Coverage Impact:
• Before: ~70% production coverage (280+ scenarios)
• After Phase 1: ~85% production coverage (327+ scenarios)
• Target Phase 2: ~92% production coverage (357+ scenarios)

Critical gaps closed for modern framework support (90% of websites)
and mobile browser compatibility (60% of traffic).
This commit is contained in:
Crawailer Developer 2025-09-18 09:35:31 -06:00
parent d35dcbb494
commit fd836c90cf
39 changed files with 21772 additions and 0 deletions

View file

@ -0,0 +1,942 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Angular Test Application - Crawailer Testing</title>
<script src="https://unpkg.com/@angular/core@17/bundles/core.umd.js"></script>
<script src="https://unpkg.com/@angular/common@17/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/forms@17/bundles/forms.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@17/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@17/bundles/platform-browser-dynamic.umd.js"></script>
<script src="https://unpkg.com/rxjs@7/dist/bundles/rxjs.umd.min.js"></script>
<script src="https://unpkg.com/zone.js@0.14.2/bundles/zone.umd.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: linear-gradient(135deg, #dd0031 0%, #c3002f 100%);
min-height: 100vh;
color: #333;
}
.app-container {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
h1 {
color: #dd0031;
text-align: center;
margin-bottom: 30px;
font-size: 2.5rem;
}
.section {
margin: 30px 0;
padding: 20px;
border: 2px solid #e9ecef;
border-radius: 8px;
background: #f8f9fa;
}
.section h2 {
color: #dd0031;
margin-top: 0;
}
.controls {
display: flex;
gap: 10px;
margin: 15px 0;
flex-wrap: wrap;
}
button {
background: #dd0031;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
button:hover {
background: #c3002f;
transform: translateY(-2px);
}
button:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
input, textarea, select {
padding: 10px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 14px;
margin: 5px;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: #dd0031;
}
.todo-item {
display: flex;
align-items: center;
padding: 10px;
margin: 5px 0;
background: white;
border-radius: 5px;
border-left: 4px solid #dd0031;
transition: all 0.3s ease;
}
.todo-item:hover {
transform: translateX(5px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.todo-item.completed {
opacity: 0.7;
border-left-color: #28a745;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 8px;
text-align: center;
border: 2px solid #dd0031;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #dd0031;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 5px;
color: white;
font-weight: bold;
z-index: 1000;
transform: translateX(400px);
transition: transform 0.3s ease;
}
.notification.show {
transform: translateX(0);
}
.notification.success { background: #28a745; }
.notification.warning { background: #ffc107; color: #333; }
.notification.error { background: #dc3545; }
.form-group {
margin: 15px 0;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.reactive-demo {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin: 20px 0;
}
.observable-demo {
background: #fff3cd;
padding: 15px;
border-radius: 8px;
margin: 10px 0;
}
.service-status {
background: #d4edda;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
}
@media (max-width: 768px) {
.reactive-demo {
grid-template-columns: 1fr;
}
.controls {
flex-direction: column;
}
}
</style>
</head>
<body>
<div id="app">
<div class="app-container">
<h1>🅰️ Angular TypeScript Testing App</h1>
<div class="section">
<h2>Loading...</h2>
<p>Please wait while Angular application initializes...</p>
</div>
</div>
</div>
<script>
// Angular application setup
const { Component, NgModule, Injectable, Input, Output, EventEmitter, OnInit, OnDestroy } = ng.core;
const { CommonModule } = ng.common;
const { ReactiveFormsModule, FormBuilder, FormGroup, Validators } = ng.forms;
const { BrowserModule } = ng.platformBrowser;
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
const { BehaviorSubject, Observable, Subject, interval } = rxjs;
const { map, takeUntil, debounceTime, distinctUntilChanged } = rxjs.operators;
// Data models (TypeScript-like)
class Todo {
constructor(id, text, completed = false, priority = 'medium') {
this.id = id;
this.text = text;
this.completed = completed;
this.priority = priority;
this.createdAt = new Date();
}
}
class User {
constructor(name = '', email = '', preferences = {}) {
this.name = name;
this.email = email;
this.preferences = preferences;
}
}
// Services
@Injectable({ providedIn: 'root' })
class TodoService {
constructor() {
this.todos$ = new BehaviorSubject([
new Todo(1, 'Learn Angular 17 Standalone Components', true, 'high'),
new Todo(2, 'Implement RxJS Observables', false, 'high'),
new Todo(3, 'Test with Crawailer JavaScript API', false, 'medium')
]);
this.nextId = 4;
}
getTodos() {
return this.todos$.asObservable();
}
addTodo(text, priority = 'medium') {
const currentTodos = this.todos$.value;
const newTodo = new Todo(this.nextId++, text, false, priority);
this.todos$.next([...currentTodos, newTodo]);
return newTodo;
}
toggleTodo(id) {
const currentTodos = this.todos$.value;
const updatedTodos = currentTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
);
this.todos$.next(updatedTodos);
}
removeTodo(id) {
const currentTodos = this.todos$.value;
const filteredTodos = currentTodos.filter(todo => todo.id !== id);
this.todos$.next(filteredTodos);
}
clearCompleted() {
const currentTodos = this.todos$.value;
const activeTodos = currentTodos.filter(todo => !todo.completed);
this.todos$.next(activeTodos);
}
}
@Injectable({ providedIn: 'root' })
class NotificationService {
constructor() {
this.notifications$ = new Subject();
}
show(message, type = 'success') {
this.notifications$.next({ message, type, show: true });
setTimeout(() => {
this.notifications$.next({ message, type, show: false });
}, 3000);
}
}
@Injectable({ providedIn: 'root' })
class TimerService {
constructor() {
this.timer$ = interval(1000);
this.elapsed$ = new BehaviorSubject(0);
this.isRunning$ = new BehaviorSubject(false);
}
start() {
if (!this.isRunning$.value) {
this.isRunning$.next(true);
this.subscription = this.timer$.subscribe(() => {
this.elapsed$.next(this.elapsed$.value + 1);
});
}
}
stop() {
if (this.subscription) {
this.subscription.unsubscribe();
this.isRunning$.next(false);
}
}
reset() {
this.stop();
this.elapsed$.next(0);
}
}
// Components
@Component({
selector: 'app-root',
template: `
<div class="app-container">
<h1>🅰️ Angular TypeScript Testing App</h1>
<!-- Reactive Forms Section -->
<div class="section">
<h2>📋 Reactive Forms & Validation</h2>
<form [formGroup]="userForm" (ngSubmit)="onSubmitForm()">
<div class="reactive-demo">
<div>
<div class="form-group">
<label>Name:</label>
<input
formControlName="name"
placeholder="Enter your name"
data-testid="name-input">
<div *ngIf="userForm.get('name')?.invalid && userForm.get('name')?.touched"
style="color: red; font-size: 12px;">
Name is required (min 2 characters)
</div>
</div>
<div class="form-group">
<label>Email:</label>
<input
formControlName="email"
type="email"
placeholder="Enter your email"
data-testid="email-input">
<div *ngIf="userForm.get('email')?.invalid && userForm.get('email')?.touched"
style="color: red; font-size: 12px;">
Valid email is required
</div>
</div>
<div class="form-group">
<label>Role:</label>
<select formControlName="role" data-testid="role-select">
<option value="user">User</option>
<option value="admin">Administrator</option>
<option value="developer">Developer</option>
</select>
</div>
</div>
<div>
<h3>Form Status:</h3>
<p><strong>Valid:</strong> {{ userForm.valid ? '✅' : '❌' }}</p>
<p><strong>Touched:</strong> {{ userForm.touched ? '✅' : '❌' }}</p>
<p><strong>Dirty:</strong> {{ userForm.dirty ? '✅' : '❌' }}</p>
<p><strong>Name Value:</strong> {{ userForm.get('name')?.value || 'Empty' }}</p>
<p><strong>Email Value:</strong> {{ userForm.get('email')?.value || 'Empty' }}</p>
</div>
</div>
<button type="submit" [disabled]="!userForm.valid" data-testid="submit-form-btn">
Submit Form
</button>
</form>
</div>
<!-- Observable Streams Section -->
<div class="section">
<h2>🌊 Observable Streams & RxJS</h2>
<div class="observable-demo">
<p><strong>Timer Status:</strong> {{ (timerService.isRunning$ | async) ? 'Running' : 'Stopped' }}</p>
<p><strong>Elapsed Time:</strong> {{ timerService.elapsed$ | async }} seconds</p>
<div class="controls">
<button (click)="timerService.start()" data-testid="start-timer-btn">Start Timer</button>
<button (click)="timerService.stop()" data-testid="stop-timer-btn">Stop Timer</button>
<button (click)="timerService.reset()" data-testid="reset-timer-btn">Reset Timer</button>
</div>
</div>
<div class="observable-demo">
<p><strong>Search Results:</strong> {{ searchResults.length }} items</p>
<input
[(ngModel)]="searchTerm"
placeholder="Search todos (debounced)..."
data-testid="search-input">
<div *ngFor="let result of searchResults" class="todo-item">
{{ result.text }} (Priority: {{ result.priority }})
</div>
</div>
</div>
<!-- Todo Management Section -->
<div class="section">
<h2>📝 Todo Management with Services</h2>
<div class="controls">
<input
[(ngModel)]="newTodoText"
(keyup.enter)="addTodo()"
placeholder="Add a new todo..."
data-testid="todo-input">
<select [(ngModel)]="newTodoPriority" data-testid="priority-select">
<option value="low">Low Priority</option>
<option value="medium">Medium Priority</option>
<option value="high">High Priority</option>
</select>
<button (click)="addTodo()" [disabled]="!newTodoText.trim()" data-testid="add-todo-btn">
Add Todo
</button>
<button (click)="clearCompleted()" data-testid="clear-completed-btn">
Clear Completed ({{ completedCount$ | async }})
</button>
</div>
<div class="todo-list" data-testid="todo-list">
<div
*ngFor="let todo of filteredTodos$ | async; trackBy: trackByTodoId"
[class]="'todo-item ' + (todo.completed ? 'completed' : '')"
[attr.data-testid]="'todo-' + todo.id">
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggleTodo(todo.id)"
[attr.data-testid]="'todo-checkbox-' + todo.id">
<span class="todo-text">{{ todo.text }}</span>
<span style="margin-left: auto; padding: 0 10px; font-size: 12px;">
{{ todo.priority.toUpperCase() }}
</span>
<button (click)="removeTodo(todo.id)" [attr.data-testid]="'remove-todo-' + todo.id">
</button>
</div>
</div>
<div class="controls">
<button
*ngFor="let filter of ['all', 'active', 'completed']"
(click)="currentFilter = filter"
[style.background]="currentFilter === filter ? '#dd0031' : '#ccc'"
[attr.data-testid]="'filter-' + filter">
{{ filter | titlecase }}
</button>
</div>
</div>
<!-- Statistics & State Section -->
<div class="section">
<h2>📊 Live Statistics & Computed Values</h2>
<div class="stats">
<div class="stat-card">
<div class="stat-number">{{ (totalTodos$ | async) || 0 }}</div>
<div>Total Todos</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ (completedCount$ | async) || 0 }}</div>
<div>Completed</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ (activeCount$ | async) || 0 }}</div>
<div>Active</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ userForm.get('name')?.value?.length || 0 }}</div>
<div>Name Length</div>
</div>
</div>
</div>
<!-- Service Status Section -->
<div class="section">
<h2>🔧 Service Status & Dependency Injection</h2>
<div class="service-status">
<p><strong>TodoService:</strong> ✅ Active ({{ (totalTodos$ | async) || 0 }} todos managed)</p>
<p><strong>NotificationService:</strong> ✅ Active</p>
<p><strong>TimerService:</strong> {{ (timerService.isRunning$ | async) ? '🟢 Running' : '🔴 Stopped' }}</p>
<p><strong>Change Detection:</strong> {{ changeDetectionCount }} runs</p>
</div>
<div class="controls">
<button (click)="triggerChangeDetection()" data-testid="trigger-cd-btn">
Trigger Change Detection
</button>
<button (click)="simulateAsyncOperation()" [disabled]="isLoading" data-testid="async-operation-btn">
{{ isLoading ? 'Loading...' : 'Simulate Async Operation' }}
</button>
</div>
</div>
</div>
<!-- Notification Component -->
<div
*ngIf="notification$ | async as notification"
[class]="'notification ' + notification.type + (notification.show ? ' show' : '')"
data-testid="notification">
{{ notification.message }}
</div>
`
})
class AppComponent {
constructor(fb, todoService, notificationService, timerService, cdr) {
this.fb = fb;
this.todoService = todoService;
this.notificationService = notificationService;
this.timerService = timerService;
this.cdr = cdr;
this.destroy$ = new Subject();
this.changeDetectionCount = 0;
this.isLoading = false;
// Form setup
this.userForm = this.fb.group({
name: ['', [Validators.required, Validators.minLength(2)]],
email: ['', [Validators.required, Validators.email]],
role: ['user']
});
// Todo management
this.newTodoText = '';
this.newTodoPriority = 'medium';
this.currentFilter = 'all';
this.searchTerm = '';
this.searchResults = [];
// Observables
this.todos$ = this.todoService.getTodos();
this.notification$ = this.notificationService.notifications$;
this.totalTodos$ = this.todos$.pipe(
map(todos => todos.length)
);
this.completedCount$ = this.todos$.pipe(
map(todos => todos.filter(todo => todo.completed).length)
);
this.activeCount$ = this.todos$.pipe(
map(todos => todos.filter(todo => !todo.completed).length)
);
this.filteredTodos$ = this.todos$.pipe(
map(todos => {
switch (this.currentFilter) {
case 'active':
return todos.filter(todo => !todo.completed);
case 'completed':
return todos.filter(todo => todo.completed);
default:
return todos;
}
})
);
}
ngOnInit() {
// Search functionality with debounce
this.searchSubject = new BehaviorSubject('');
this.searchSubject.pipe(
debounceTime(300),
distinctUntilChanged(),
takeUntil(this.destroy$)
).subscribe(searchTerm => {
this.todos$.pipe(
map(todos => todos.filter(todo =>
todo.text.toLowerCase().includes(searchTerm.toLowerCase())
))
).subscribe(results => {
this.searchResults = results;
});
});
// Monitor search term changes
Object.defineProperty(this, 'searchTerm', {
get: () => this._searchTerm,
set: (value) => {
this._searchTerm = value;
this.searchSubject.next(value);
}
});
this._searchTerm = '';
console.log('Angular component initialized');
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
ngAfterViewChecked() {
this.changeDetectionCount++;
}
onSubmitForm() {
if (this.userForm.valid) {
const formData = this.userForm.value;
this.notificationService.show(`Form submitted: ${formData.name} (${formData.role})`, 'success');
console.log('Form submitted:', formData);
}
}
addTodo() {
if (this.newTodoText.trim()) {
const todo = this.todoService.addTodo(this.newTodoText.trim(), this.newTodoPriority);
this.newTodoText = '';
this.notificationService.show(`Todo added: ${todo.text}`, 'success');
}
}
toggleTodo(id) {
this.todoService.toggleTodo(id);
this.notificationService.show('Todo status updated', 'success');
}
removeTodo(id) {
this.todoService.removeTodo(id);
this.notificationService.show('Todo removed', 'warning');
}
clearCompleted() {
this.todoService.clearCompleted();
this.notificationService.show('Completed todos cleared', 'success');
}
trackByTodoId(index, todo) {
return todo.id;
}
triggerChangeDetection() {
this.cdr.detectChanges();
this.notificationService.show('Change detection triggered', 'success');
}
async simulateAsyncOperation() {
this.isLoading = true;
this.notificationService.show('Starting async operation...', 'success');
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
this.isLoading = false;
this.notificationService.show('Async operation completed!', 'success');
}
}
// Module definition
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, CommonModule, ReactiveFormsModule],
providers: [TodoService, NotificationService, TimerService],
bootstrap: [AppComponent]
})
class AppModule {}
// Bootstrap the application
platformBrowserDynamic().bootstrapModule(AppModule).then(() => {
console.log('Angular application bootstrapped successfully');
// Global test data for Crawailer JavaScript API testing
window.testData = {
framework: 'angular',
version: ng.VERSION?.full || 'Unknown',
// Component analysis
getComponentInfo: () => {
const app = document.querySelector('app-root');
const inputs = document.querySelectorAll('input');
const buttons = document.querySelectorAll('button');
const testableElements = document.querySelectorAll('[data-testid]');
return {
totalInputs: inputs.length,
totalButtons: buttons.length,
testableElements: testableElements.length,
hasAngularDevtools: typeof window.ng !== 'undefined',
componentInstance: !!app
};
},
// Get application state
getAppState: () => {
try {
const appElement = document.querySelector('app-root');
const componentRef = ng.getComponent(appElement);
if (componentRef) {
return {
formValue: componentRef.userForm?.value,
formValid: componentRef.userForm?.valid,
isLoading: componentRef.isLoading,
currentFilter: componentRef.currentFilter,
changeDetectionCount: componentRef.changeDetectionCount,
searchTerm: componentRef.searchTerm
};
}
return { error: 'Could not access Angular component state' };
} catch (error) {
return { error: error.message };
}
},
// Get service data
getServiceData: () => {
try {
const appElement = document.querySelector('app-root');
const componentRef = ng.getComponent(appElement);
if (componentRef && componentRef.todoService) {
const todos = componentRef.todoService.todos$.value;
return {
totalTodos: todos.length,
completedTodos: todos.filter(t => t.completed).length,
activeTodos: todos.filter(t => !t.completed).length,
timerRunning: componentRef.timerService.isRunning$.value,
timerElapsed: componentRef.timerService.elapsed$.value
};
}
return { error: 'Could not access Angular services' };
} catch (error) {
return { error: error.message };
}
},
// User interaction simulation
simulateUserAction: async (action) => {
const actions = {
'fill-form': () => {
const nameInput = document.querySelector('[data-testid="name-input"]');
const emailInput = document.querySelector('[data-testid="email-input"]');
const roleSelect = document.querySelector('[data-testid="role-select"]');
nameInput.value = 'Test User';
emailInput.value = 'test@example.com';
roleSelect.value = 'developer';
nameInput.dispatchEvent(new Event('input'));
emailInput.dispatchEvent(new Event('input'));
roleSelect.dispatchEvent(new Event('change'));
return 'Form filled';
},
'submit-form': () => {
const submitBtn = document.querySelector('[data-testid="submit-form-btn"]');
if (!submitBtn.disabled) {
submitBtn.click();
return 'Form submitted';
}
return 'Form invalid, cannot submit';
},
'add-todo': () => {
const input = document.querySelector('[data-testid="todo-input"]');
const button = document.querySelector('[data-testid="add-todo-btn"]');
input.value = `Angular todo ${Date.now()}`;
input.dispatchEvent(new Event('input'));
button.click();
return 'Todo added';
},
'start-timer': () => {
document.querySelector('[data-testid="start-timer-btn"]').click();
return 'Timer started';
},
'search-todos': () => {
const searchInput = document.querySelector('[data-testid="search-input"]');
searchInput.value = 'Angular';
searchInput.dispatchEvent(new Event('input'));
return 'Search performed';
},
'async-operation': async () => {
document.querySelector('[data-testid="async-operation-btn"]').click();
// Wait for operation to complete
await new Promise(resolve => {
const checkComplete = () => {
const appElement = document.querySelector('app-root');
const componentRef = ng.getComponent(appElement);
if (!componentRef.isLoading) {
resolve();
} else {
setTimeout(checkComplete, 100);
}
};
checkComplete();
});
return 'Async operation completed';
}
};
if (actions[action]) {
return await actions[action]();
}
throw new Error(`Unknown action: ${action}`);
},
// Detect Angular-specific features
detectAngularFeatures: () => {
return {
hasAngular: typeof ng !== 'undefined',
hasRxJS: typeof rxjs !== 'undefined',
hasReactiveForms: typeof ng.forms?.ReactiveFormsModule !== 'undefined',
hasCommonModule: typeof ng.common?.CommonModule !== 'undefined',
hasServices: true, // We have injectable services
hasObservables: typeof rxjs.Observable !== 'undefined',
hasChangeDetection: true,
angularVersion: ng.VERSION?.full || 'Unknown',
hasDevtools: typeof window.ng !== 'undefined',
hasZoneJS: typeof Zone !== 'undefined'
};
},
// Observable monitoring
monitorObservables: () => {
const appElement = document.querySelector('app-root');
const componentRef = ng.getComponent(appElement);
if (componentRef) {
return {
todosObservable: componentRef.todos$ !== undefined,
notificationObservable: componentRef.notification$ !== undefined,
timerObservable: componentRef.timerService.timer$ !== undefined,
hasSubscriptions: componentRef.destroy$ !== undefined
};
}
return { error: 'Cannot access observables' };
},
// Performance measurement
measureChangeDetection: () => {
const start = performance.now();
const appElement = document.querySelector('app-root');
const componentRef = ng.getComponent(appElement);
// Trigger multiple change detection cycles
for (let i = 0; i < 10; i++) {
componentRef.cdr.detectChanges();
}
const end = performance.now();
return {
detectionTime: end - start,
cyclesPerSecond: 10 / ((end - start) / 1000)
};
},
// Complex workflow simulation
simulateComplexWorkflow: async () => {
const steps = [];
// Step 1: Fill and submit form
await window.testData.simulateUserAction('fill-form');
steps.push('Form filled');
await window.testData.simulateUserAction('submit-form');
steps.push('Form submitted');
// Step 2: Add multiple todos
for (let i = 1; i <= 3; i++) {
await window.testData.simulateUserAction('add-todo');
}
steps.push('Multiple todos added');
// Step 3: Start timer
await window.testData.simulateUserAction('start-timer');
steps.push('Timer started');
// Step 4: Search todos
await window.testData.simulateUserAction('search-todos');
steps.push('Search performed');
// Step 5: Run async operation
await window.testData.simulateUserAction('async-operation');
steps.push('Async operation completed');
return {
stepsCompleted: steps,
finalState: window.testData.getAppState(),
serviceData: window.testData.getServiceData()
};
}
};
// Global error handler for testing
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
window.lastError = {
message: event.error.message,
stack: event.error.stack,
timestamp: new Date().toISOString()
};
});
console.log('Available test methods:', Object.keys(window.testData));
console.log('Angular version:', ng.VERSION?.full);
}).catch(err => {
console.error('Error bootstrapping Angular application:', err);
// Fallback content
document.getElementById('app').innerHTML = `
<div class="app-container">
<h1>🅰️ Angular Test Application</h1>
<div class="section">
<h2>❌ Bootstrap Error</h2>
<p>Angular application failed to bootstrap. Error: ${err.message}</p>
<p>This may be due to CDN loading issues or compatibility problems.</p>
</div>
</div>
`;
// Basic test data even if Angular fails
window.testData = {
framework: 'angular',
version: 'failed-to-load',
error: err.message,
getComponentInfo: () => ({ error: 'Angular failed to load' }),
getAppState: () => ({ error: 'Angular failed to load' }),
detectAngularFeatures: () => ({ hasAngular: false, error: err.message })
};
});
</script>
</body>
</html>

View file

@ -0,0 +1,851 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DevDocs - Comprehensive API Documentation</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'SF Pro Text', system-ui, sans-serif;
line-height: 1.6;
color: #24292e;
background: #fafbfc;
}
.layout {
display: flex;
min-height: 100vh;
}
/* Sidebar */
.sidebar {
width: 280px;
background: white;
border-right: 1px solid #e1e4e8;
position: fixed;
height: 100vh;
overflow-y: auto;
z-index: 100;
}
.sidebar-header {
padding: 1.5rem;
border-bottom: 1px solid #e1e4e8;
background: #f6f8fa;
}
.logo {
font-size: 1.3rem;
font-weight: 700;
color: #0366d6;
margin-bottom: 0.5rem;
}
.version {
font-size: 0.8rem;
color: #6a737d;
background: #e1e4e8;
padding: 0.25rem 0.5rem;
border-radius: 12px;
display: inline-block;
}
.search-box {
padding: 1rem;
border-bottom: 1px solid #e1e4e8;
}
.search-input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5da;
border-radius: 6px;
font-size: 0.9rem;
background: white;
}
.search-input:focus {
outline: none;
border-color: #0366d6;
box-shadow: 0 0 0 3px rgba(3, 102, 214, 0.1);
}
.nav-section {
padding: 1rem 0;
}
.nav-title {
padding: 0 1rem 0.5rem 1rem;
font-size: 0.8rem;
font-weight: 600;
color: #6a737d;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.nav-item {
display: block;
padding: 0.5rem 1rem;
color: #586069;
text-decoration: none;
border-left: 3px solid transparent;
transition: all 0.2s ease;
}
.nav-item:hover {
background: #f6f8fa;
color: #0366d6;
}
.nav-item.active {
background: #f1f8ff;
color: #0366d6;
border-left-color: #0366d6;
font-weight: 500;
}
.nav-item.sub-item {
padding-left: 2rem;
font-size: 0.9rem;
}
/* Main Content */
.main-content {
flex: 1;
margin-left: 280px;
min-height: 100vh;
}
.header {
background: white;
border-bottom: 1px solid #e1e4e8;
padding: 1rem 2rem;
position: sticky;
top: 0;
z-index: 50;
}
.breadcrumb {
font-size: 0.9rem;
color: #6a737d;
}
.breadcrumb a {
color: #0366d6;
text-decoration: none;
}
.content {
padding: 2rem;
max-width: 900px;
}
.page-title {
font-size: 2.5rem;
font-weight: 600;
margin-bottom: 1rem;
color: #24292e;
}
.page-description {
font-size: 1.1rem;
color: #586069;
margin-bottom: 2rem;
line-height: 1.7;
}
.content h2 {
font-size: 1.5rem;
margin: 2rem 0 1rem 0;
color: #24292e;
border-bottom: 1px solid #e1e4e8;
padding-bottom: 0.5rem;
}
.content h3 {
font-size: 1.2rem;
margin: 1.5rem 0 0.75rem 0;
color: #24292e;
}
.content p {
margin-bottom: 1rem;
color: #586069;
line-height: 1.7;
}
.content ul, .content ol {
margin-bottom: 1rem;
padding-left: 2rem;
}
.content li {
margin-bottom: 0.5rem;
color: #586069;
}
/* Code blocks */
.code-block {
background: #f6f8fa;
border: 1px solid #e1e4e8;
border-radius: 6px;
padding: 1rem;
margin: 1rem 0;
overflow-x: auto;
font-family: 'SF Mono', Consolas, monospace;
font-size: 0.9rem;
line-height: 1.4;
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
background: #f1f3f4;
padding: 0.5rem 1rem;
border-bottom: 1px solid #e1e4e8;
font-size: 0.8rem;
color: #6a737d;
}
.copy-btn {
background: #fafbfc;
border: 1px solid #d1d5da;
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
.copy-btn:hover {
background: #f3f4f6;
}
/* API Reference Cards */
.api-card {
background: white;
border: 1px solid #e1e4e8;
border-radius: 8px;
margin: 1.5rem 0;
overflow: hidden;
}
.api-header {
background: #f6f8fa;
padding: 1rem;
border-bottom: 1px solid #e1e4e8;
}
.api-method {
display: inline-block;
background: #28a745;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
margin-right: 0.5rem;
}
.api-method.post { background: #fd7e14; }
.api-method.put { background: #6f42c1; }
.api-method.delete { background: #dc3545; }
.api-endpoint {
font-family: 'SF Mono', Consolas, monospace;
font-size: 1rem;
color: #24292e;
}
.api-content {
padding: 1rem;
}
.param-table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
}
.param-table th,
.param-table td {
text-align: left;
padding: 0.75rem;
border-bottom: 1px solid #e1e4e8;
}
.param-table th {
background: #f6f8fa;
font-weight: 600;
color: #24292e;
}
.param-name {
font-family: 'SF Mono', Consolas, monospace;
font-size: 0.9rem;
color: #0366d6;
}
.param-type {
color: #6a737d;
font-size: 0.8rem;
}
.response-example {
background: #f8f9fa;
border-left: 4px solid #28a745;
padding: 1rem;
margin: 1rem 0;
}
/* Interactive elements */
.try-it-btn {
background: #0366d6;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
margin-top: 1rem;
}
.try-it-btn:hover {
background: #0256cc;
}
/* Status indicators */
.status-badge {
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 500;
}
.status-stable {
background: #d4edda;
color: #155724;
}
.status-beta {
background: #fff3cd;
color: #856404;
}
.status-deprecated {
background: #f8d7da;
color: #721c24;
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0);
}
.main-content {
margin-left: 0;
}
.content {
padding: 1rem;
}
.page-title {
font-size: 2rem;
}
}
/* Syntax highlighting simulation */
.keyword { color: #d73a49; }
.string { color: #032f62; }
.comment { color: #6a737d; }
.number { color: #005cc5; }
.function { color: #6f42c1; }
</style>
</head>
<body>
<div class="layout">
<!-- Sidebar -->
<nav class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="logo">DevDocs</div>
<span class="version">v2.1.0</span>
</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search documentation..." id="doc-search">
</div>
<div class="nav-section">
<div class="nav-title">Getting Started</div>
<a href="#overview" class="nav-item active">Overview</a>
<a href="#installation" class="nav-item">Installation</a>
<a href="#quick-start" class="nav-item">Quick Start</a>
<a href="#authentication" class="nav-item">Authentication</a>
</div>
<div class="nav-section">
<div class="nav-title">API Reference</div>
<a href="#users" class="nav-item">Users</a>
<a href="#users-create" class="nav-item sub-item">Create User</a>
<a href="#users-list" class="nav-item sub-item">List Users</a>
<a href="#users-get" class="nav-item sub-item">Get User</a>
<a href="#products" class="nav-item">Products</a>
<a href="#products-list" class="nav-item sub-item">List Products</a>
<a href="#products-search" class="nav-item sub-item">Search Products</a>
<a href="#orders" class="nav-item">Orders</a>
<a href="#analytics" class="nav-item">Analytics</a>
</div>
<div class="nav-section">
<div class="nav-title">Advanced</div>
<a href="#webhooks" class="nav-item">Webhooks</a>
<a href="#rate-limiting" class="nav-item">Rate Limiting</a>
<a href="#errors" class="nav-item">Error Handling</a>
<a href="#sdks" class="nav-item">SDKs</a>
</div>
<div class="nav-section">
<div class="nav-title">Resources</div>
<a href="#examples" class="nav-item">Examples</a>
<a href="#changelog" class="nav-item">Changelog</a>
<a href="#support" class="nav-item">Support</a>
</div>
</nav>
<!-- Main Content -->
<main class="main-content">
<header class="header">
<div class="breadcrumb">
<a href="/">Home</a> / <a href="/docs">Documentation</a> / <span id="current-section">Overview</span>
</div>
</header>
<div class="content">
<section id="overview" class="doc-section">
<h1 class="page-title">API Documentation</h1>
<p class="page-description">
Welcome to our comprehensive API documentation. This guide will help you integrate our services
into your applications with ease. Our RESTful API provides access to user management,
product catalog, order processing, and analytics data.
</p>
<h2>Key Features</h2>
<ul>
<li>RESTful API design with JSON responses</li>
<li>OAuth 2.0 authentication</li>
<li>Comprehensive error handling</li>
<li>Rate limiting and throttling</li>
<li>Real-time webhooks</li>
<li>Extensive filtering and pagination</li>
</ul>
<h2>Base URL</h2>
<div class="code-block">
<div class="code-header">
<span>Production</span>
<button class="copy-btn" onclick="copyToClipboard('https://api.example.com/v1')">Copy</button>
</div>
https://api.example.com/v1
</div>
<h2>Content Type</h2>
<p>All API requests should include the following headers:</p>
<div class="code-block">
<div class="code-header">
<span>Headers</span>
<button class="copy-btn" onclick="copyToClipboard('Content-Type: application/json\\nAccept: application/json')">Copy</button>
</div>
Content-Type: application/json
Accept: application/json
</div>
</section>
<section id="users" class="doc-section" style="display: none;">
<h1 class="page-title">Users API</h1>
<p class="page-description">
Manage user accounts, profiles, and authentication. The Users API provides endpoints
for creating, updating, and retrieving user information.
</p>
<div class="api-card">
<div class="api-header">
<span class="api-method">GET</span>
<span class="api-endpoint">/users</span>
<span class="status-badge status-stable">Stable</span>
</div>
<div class="api-content">
<p>Retrieve a paginated list of users.</p>
<h3>Query Parameters</h3>
<table class="param-table">
<thead>
<tr>
<th>Parameter</th>
<th>Type</th>
<th>Description</th>
<th>Required</th>
</tr>
</thead>
<tbody>
<tr>
<td class="param-name">page</td>
<td class="param-type">integer</td>
<td>Page number (default: 1)</td>
<td>No</td>
</tr>
<tr>
<td class="param-name">limit</td>
<td class="param-type">integer</td>
<td>Items per page (default: 20, max: 100)</td>
<td>No</td>
</tr>
<tr>
<td class="param-name">search</td>
<td class="param-type">string</td>
<td>Search users by name or email</td>
<td>No</td>
</tr>
</tbody>
</table>
<h3>Example Response</h3>
<div class="response-example">
<div class="code-block">
{
<span class="string">"users"</span>: [
{
<span class="string">"id"</span>: <span class="number">1</span>,
<span class="string">"name"</span>: <span class="string">"John Doe"</span>,
<span class="string">"email"</span>: <span class="string">"john@example.com"</span>,
<span class="string">"created_at"</span>: <span class="string">"2023-01-15T10:30:00Z"</span>,
<span class="string">"status"</span>: <span class="string">"active"</span>
}
],
<span class="string">"pagination"</span>: {
<span class="string">"current_page"</span>: <span class="number">1</span>,
<span class="string">"total_pages"</span>: <span class="number">10</span>,
<span class="string">"total_items"</span>: <span class="number">200</span>
}
}
</div>
</div>
<button class="try-it-btn" onclick="tryApiCall('/users')">Try it out</button>
</div>
</div>
<div class="api-card">
<div class="api-header">
<span class="api-method post">POST</span>
<span class="api-endpoint">/users</span>
<span class="status-badge status-stable">Stable</span>
</div>
<div class="api-content">
<p>Create a new user account.</p>
<h3>Request Body</h3>
<div class="code-block">
{
<span class="string">"name"</span>: <span class="string">"Jane Smith"</span>,
<span class="string">"email"</span>: <span class="string">"jane@example.com"</span>,
<span class="string">"password"</span>: <span class="string">"securepassword123"</span>
}
</div>
<button class="try-it-btn" onclick="tryApiCall('/users', 'POST')">Try it out</button>
</div>
</div>
</section>
<section id="products" class="doc-section" style="display: none;">
<h1 class="page-title">Products API</h1>
<p class="page-description">
Access and manage product catalog data. Search, filter, and retrieve detailed
product information including pricing, inventory, and specifications.
</p>
<div class="api-card">
<div class="api-header">
<span class="api-method">GET</span>
<span class="api-endpoint">/products</span>
<span class="status-badge status-stable">Stable</span>
</div>
<div class="api-content">
<p>Retrieve a list of products with filtering and search capabilities.</p>
<h3>Query Parameters</h3>
<table class="param-table">
<thead>
<tr>
<th>Parameter</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td class="param-name">category</td>
<td class="param-type">string</td>
<td>Filter by product category</td>
</tr>
<tr>
<td class="param-name">price_min</td>
<td class="param-type">number</td>
<td>Minimum price filter</td>
</tr>
<tr>
<td class="param-name">price_max</td>
<td class="param-type">number</td>
<td>Maximum price filter</td>
</tr>
<tr>
<td class="param-name">in_stock</td>
<td class="param-type">boolean</td>
<td>Filter by availability</td>
</tr>
</tbody>
</table>
<button class="try-it-btn" onclick="tryApiCall('/products')">Try it out</button>
</div>
</div>
</section>
</div>
</main>
</div>
<script>
// Documentation Site JavaScript
class DocsSite {
constructor() {
this.currentSection = 'overview';
this.searchIndex = [];
this.init();
}
init() {
this.setupNavigation();
this.setupSearch();
this.generateSearchIndex();
this.simulateApiStatus();
// Update page views for testing
setInterval(() => this.updateMetrics(), 5000);
}
setupNavigation() {
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const sectionId = item.getAttribute('href').substring(1);
this.navigateToSection(sectionId);
});
});
// Handle hash changes
window.addEventListener('hashchange', () => {
const hash = window.location.hash.substring(1);
if (hash) this.navigateToSection(hash);
});
// Set initial section from URL
const initialHash = window.location.hash.substring(1);
if (initialHash) this.navigateToSection(initialHash);
}
navigateToSection(sectionId) {
// Hide current section
const currentEl = document.querySelector('.doc-section:not([style*="display: none"])');
if (currentEl) currentEl.style.display = 'none';
// Show new section
const newSection = document.getElementById(sectionId);
if (newSection) {
newSection.style.display = 'block';
this.currentSection = sectionId;
// Update navigation
document.querySelector('.nav-item.active').classList.remove('active');
document.querySelector(`[href="#${sectionId}"]`).classList.add('active');
// Update breadcrumb
document.getElementById('current-section').textContent =
newSection.querySelector('h1').textContent;
// Update URL
history.pushState(null, '', `#${sectionId}`);
}
}
setupSearch() {
const searchInput = document.getElementById('doc-search');
let searchTimeout;
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
this.performSearch(e.target.value);
}, 300);
});
}
generateSearchIndex() {
// Generate search index from documentation content
document.querySelectorAll('.doc-section').forEach(section => {
const title = section.querySelector('h1')?.textContent || '';
const content = section.textContent || '';
this.searchIndex.push({
id: section.id,
title,
content: content.toLowerCase(),
keywords: this.extractKeywords(content)
});
});
}
extractKeywords(text) {
return text.toLowerCase()
.split(/\W+/)
.filter(word => word.length > 3)
.slice(0, 20);
}
performSearch(query) {
if (!query || query.length < 2) return;
const results = this.searchIndex.filter(item =>
item.title.toLowerCase().includes(query.toLowerCase()) ||
item.content.includes(query.toLowerCase()) ||
item.keywords.some(keyword => keyword.includes(query.toLowerCase()))
);
console.log(`Search for "${query}":`, results);
// In a real implementation, you'd show search results
}
simulateApiStatus() {
// Simulate API status updates
const statusChecks = [
{ endpoint: '/users', status: 'healthy', responseTime: 45 },
{ endpoint: '/products', status: 'healthy', responseTime: 62 },
{ endpoint: '/orders', status: 'degraded', responseTime: 234 },
{ endpoint: '/analytics', status: 'healthy', responseTime: 89 }
];
window.apiStatus = statusChecks;
console.log('API Status:', statusChecks);
}
updateMetrics() {
// Simulate real-time metrics
const metrics = {
pageViews: Math.floor(Math.random() * 1000) + 500,
activeUsers: Math.floor(Math.random() * 50) + 10,
apiCalls: Math.floor(Math.random() * 10000) + 5000,
uptime: '99.9%'
};
window.liveMetrics = metrics;
}
}
// Utility functions
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(() => {
showNotification('Copied to clipboard!');
});
}
function showNotification(message) {
// Create temporary notification
const notification = document.createElement('div');
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #28a745;
color: white;
padding: 0.75rem 1rem;
border-radius: 6px;
z-index: 1000;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
}
function tryApiCall(endpoint, method = 'GET') {
// Simulate API call
const baseUrl = 'https://api.example.com/v1';
const fullUrl = baseUrl + endpoint;
console.log(`Simulating ${method} ${fullUrl}`);
// Show loading state
const button = event.target;
const originalText = button.textContent;
button.textContent = 'Testing...';
button.disabled = true;
// Simulate API response
setTimeout(() => {
const response = {
method,
url: fullUrl,
status: 200,
timestamp: new Date().toISOString(),
responseTime: Math.floor(Math.random() * 200) + 50
};
console.log('API Response:', response);
showNotification(`${method} ${endpoint} - ${response.status} (${response.responseTime}ms)`);
button.textContent = originalText;
button.disabled = false;
}, 1000);
}
// Initialize documentation site
const docsApp = new DocsSite();
// Global test data
window.testData = {
siteName: 'DevDocs',
version: '2.1.0',
currentSection: () => docsApp.currentSection,
searchIndex: () => docsApp.searchIndex,
navigationItems: () => document.querySelectorAll('.nav-item').length,
apiEndpoints: [
{ method: 'GET', path: '/users', description: 'List users' },
{ method: 'POST', path: '/users', description: 'Create user' },
{ method: 'GET', path: '/products', description: 'List products' },
{ method: 'GET', path: '/orders', description: 'List orders' },
{ method: 'GET', path: '/analytics', description: 'Get analytics' }
],
getApiStatus: () => window.apiStatus,
getLiveMetrics: () => window.liveMetrics,
generateTimestamp: () => new Date().toISOString()
};
console.log('DevDocs initialized');
console.log('Test data available at window.testData');
console.log('Current section:', docsApp.currentSection);
</script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,257 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Crawailer Test Suite Hub</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
min-height: 100vh;
}
.container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37);
border: 1px solid rgba(255, 255, 255, 0.18);
}
h1 {
text-align: center;
margin-bottom: 2rem;
font-size: 2.5rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.card {
background: rgba(255, 255, 255, 0.2);
border-radius: 15px;
padding: 1.5rem;
border: 1px solid rgba(255, 255, 255, 0.3);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 40px rgba(31, 38, 135, 0.5);
}
.card h3 {
color: #fff;
margin-bottom: 1rem;
font-size: 1.3rem;
}
.card p {
opacity: 0.9;
line-height: 1.6;
margin-bottom: 1rem;
}
.card a {
color: #FFD700;
text-decoration: none;
font-weight: bold;
display: inline-block;
margin-top: 0.5rem;
transition: color 0.3s ease;
}
.card a:hover {
color: #FFF;
text-shadow: 0 0 10px #FFD700;
}
.stats {
display: flex;
justify-content: space-around;
margin: 2rem 0;
text-align: center;
}
.stat {
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
padding: 1rem;
min-width: 100px;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #FFD700;
}
.nav-links {
text-align: center;
margin-top: 2rem;
}
.nav-links a {
color: #FFD700;
text-decoration: none;
margin: 0 1rem;
padding: 0.5rem 1rem;
border: 1px solid rgba(255, 215, 0, 0.5);
border-radius: 25px;
transition: all 0.3s ease;
display: inline-block;
}
.nav-links a:hover {
background: rgba(255, 215, 0, 0.2);
transform: scale(1.05);
}
</style>
</head>
<body>
<div class="container">
<h1>🕷️ Crawailer Test Suite Hub</h1>
<div class="stats">
<div class="stat">
<div class="stat-number" id="site-count">8</div>
<div>Test Sites</div>
</div>
<div class="stat">
<div class="stat-number" id="api-count">12</div>
<div>API Endpoints</div>
</div>
<div class="stat">
<div class="stat-number" id="test-count">280+</div>
<div>Test Scenarios</div>
</div>
</div>
<div class="grid">
<div class="card">
<h3>🛍️ E-commerce Demo</h3>
<p>Complete online store with dynamic pricing, cart functionality, and product filtering. Perfect for testing JavaScript-heavy commerce sites.</p>
<a href="/shop/">Visit E-commerce →</a>
<br><a href="http://ecommerce.test.crawailer.local:8080">Subdomain Version →</a>
</div>
<div class="card">
<h3>⚛️ Single Page Application</h3>
<p>React-style SPA with client-side routing, dynamic content loading, and modern JavaScript frameworks simulation.</p>
<a href="/spa/">Visit SPA →</a>
<br><a href="http://spa.test.crawailer.local:8080">Subdomain Version →</a>
</div>
<div class="card">
<h3>📰 News & Blog Platform</h3>
<p>Content-heavy site with infinite scroll, comment systems, and dynamic article loading for content extraction testing.</p>
<a href="/news/">Visit News Site →</a>
</div>
<div class="card">
<h3>📚 Documentation Site</h3>
<p>Technical documentation with search, navigation, and code examples. Tests structured content extraction.</p>
<a href="/docs/">Visit Docs →</a>
<br><a href="http://docs.test.crawailer.local:8080">Subdomain Version →</a>
</div>
<div class="card">
<h3>🔌 REST API Endpoints</h3>
<p>Various API endpoints with different response times, error scenarios, and data formats for comprehensive testing.</p>
<a href="/api/users">Users API →</a>
<br><a href="http://api.test.crawailer.local:8080/v1/users">V1 API →</a>
</div>
<div class="card">
<h3>📁 Static Assets</h3>
<p>Collection of images, documents, and files for testing download capabilities and file handling.</p>
<a href="/static/">Browse Files →</a>
</div>
<div class="card">
<h3>⚡ Performance Testing</h3>
<p>Pages designed to test various performance scenarios including slow loading, large content, and resource-heavy operations.</p>
<a href="/api/slow">Slow Response →</a>
<br><a href="/api/error">Error Simulation →</a>
</div>
<div class="card">
<h3>🔍 JavaScript Scenarios</h3>
<p>Specialized pages for testing JavaScript execution, DOM manipulation, and dynamic content generation.</p>
<a href="/spa/dynamic-content">Dynamic Content →</a>
<br><a href="/shop/cart">Interactive Cart →</a>
</div>
</div>
<div class="nav-links">
<a href="/health">Health Check</a>
<a href="/api/users">API Status</a>
<a href="https://github.com/anthropics/crawailer">GitHub Repo</a>
</div>
</div>
<script>
// Add some dynamic behavior for testing
document.addEventListener('DOMContentLoaded', function() {
// Animate counters
function animateCounter(element, target) {
let current = 0;
const increment = target / 50;
const timer = setInterval(() => {
current += increment;
if (current >= target) {
element.textContent = target;
clearInterval(timer);
} else {
element.textContent = Math.floor(current);
}
}, 20);
}
// Get current time for dynamic timestamps
const now = new Date();
const timeStamp = now.toISOString();
// Add timestamp to page for testing
const timestampEl = document.createElement('div');
timestampEl.style.position = 'fixed';
timestampEl.style.bottom = '10px';
timestampEl.style.right = '10px';
timestampEl.style.background = 'rgba(0,0,0,0.5)';
timestampEl.style.color = 'white';
timestampEl.style.padding = '5px 10px';
timestampEl.style.borderRadius = '5px';
timestampEl.style.fontSize = '12px';
timestampEl.textContent = `Generated: ${timeStamp}`;
document.body.appendChild(timestampEl);
// Add click tracking for testing
let clickCount = 0;
document.addEventListener('click', function(e) {
clickCount++;
console.log(`Click ${clickCount} on:`, e.target.tagName);
});
// Simulate some async data loading
setTimeout(() => {
const siteCount = document.getElementById('site-count');
const apiCount = document.getElementById('api-count');
const testCount = document.getElementById('test-count');
if (siteCount) animateCounter(siteCount, 8);
if (apiCount) animateCounter(apiCount, 12);
if (testCount) testCount.textContent = '280+';
}, 500);
});
// Add global test data
window.testData = {
hubVersion: '1.0.0',
generatedAt: new Date().toISOString(),
testSites: [
'ecommerce', 'spa', 'news', 'docs', 'api', 'static'
],
apiEndpoints: [
'/api/users', '/api/products', '/api/slow', '/api/error',
'/api/analytics', '/health'
]
};
</script>
</body>
</html>

View file

@ -0,0 +1,697 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TechNews Today - Latest Technology Updates</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Georgia', serif;
line-height: 1.6;
color: #2c3e50;
background: #fff;
}
.header {
background: #1a202c;
color: white;
padding: 1rem 0;
position: sticky;
top: 0;
z-index: 100;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.8rem;
font-weight: bold;
color: #4a90e2;
}
.nav-menu {
display: flex;
list-style: none;
gap: 2rem;
}
.nav-item {
color: white;
text-decoration: none;
font-weight: 500;
transition: color 0.3s ease;
}
.nav-item:hover {
color: #4a90e2;
}
.hero {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 3rem 0;
text-align: center;
}
.hero h1 {
font-size: 3rem;
margin-bottom: 1rem;
font-weight: 300;
}
.hero p {
font-size: 1.2rem;
opacity: 0.9;
}
.main-content {
padding: 2rem 0;
}
.content-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2rem;
margin-top: 2rem;
}
.articles-section h2 {
font-size: 2rem;
margin-bottom: 1.5rem;
color: #1a202c;
border-bottom: 3px solid #4a90e2;
padding-bottom: 0.5rem;
}
.article-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 2rem;
overflow: hidden;
transition: transform 0.3s ease;
}
.article-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
}
.article-image {
height: 200px;
background: linear-gradient(45deg, #f0f2f5, #e1e5e9);
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: #6c757d;
}
.article-content {
padding: 1.5rem;
}
.article-meta {
display: flex;
gap: 1rem;
font-size: 0.9rem;
color: #6c757d;
margin-bottom: 1rem;
}
.article-category {
background: #4a90e2;
color: white;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.8rem;
font-weight: 500;
}
.article-title {
font-size: 1.4rem;
margin-bottom: 1rem;
color: #1a202c;
font-weight: 600;
}
.article-excerpt {
color: #4a5568;
line-height: 1.6;
margin-bottom: 1rem;
}
.read-more {
color: #4a90e2;
text-decoration: none;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.read-more:hover {
text-decoration: underline;
}
.sidebar {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
height: fit-content;
}
.sidebar h3 {
margin-bottom: 1rem;
color: #1a202c;
}
.trending-list {
list-style: none;
}
.trending-item {
padding: 0.75rem 0;
border-bottom: 1px solid #e2e8f0;
}
.trending-item:last-child {
border-bottom: none;
}
.trending-link {
color: #2d3748;
text-decoration: none;
font-size: 0.9rem;
line-height: 1.4;
}
.trending-link:hover {
color: #4a90e2;
}
.load-more {
text-align: center;
padding: 2rem 0;
}
.load-more-btn {
background: #4a90e2;
color: white;
padding: 0.75rem 2rem;
border: none;
border-radius: 6px;
font-size: 1rem;
cursor: pointer;
transition: background 0.3s ease;
}
.load-more-btn:hover {
background: #357abd;
}
.load-more-btn:disabled {
background: #a0aec0;
cursor: not-allowed;
}
.newsletter {
background: #1a202c;
color: white;
padding: 2rem;
border-radius: 8px;
margin-top: 2rem;
text-align: center;
}
.newsletter h3 {
margin-bottom: 1rem;
}
.newsletter-form {
display: flex;
gap: 0.5rem;
max-width: 400px;
margin: 0 auto;
}
.newsletter-input {
flex: 1;
padding: 0.75rem;
border: none;
border-radius: 4px;
}
.newsletter-btn {
background: #4a90e2;
color: white;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
}
/* Loading animation */
.loading {
text-align: center;
padding: 2rem;
color: #6c757d;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid #f3f3f3;
border-top: 3px solid #4a90e2;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Comments section */
.comments-section {
background: #f8f9fa;
padding: 1.5rem;
border-radius: 8px;
margin-top: 1rem;
}
.comment {
background: white;
padding: 1rem;
border-radius: 6px;
margin-bottom: 1rem;
border-left: 3px solid #4a90e2;
}
.comment-author {
font-weight: 600;
color: #1a202c;
margin-bottom: 0.5rem;
}
.comment-time {
font-size: 0.8rem;
color: #6c757d;
}
/* Responsive */
@media (max-width: 768px) {
.content-grid {
grid-template-columns: 1fr;
}
.hero h1 {
font-size: 2rem;
}
.nav-menu {
flex-direction: column;
gap: 1rem;
}
.newsletter-form {
flex-direction: column;
}
}
</style>
</head>
<body>
<header class="header">
<div class="container">
<div class="header-content">
<div class="logo">TechNews Today</div>
<nav>
<ul class="nav-menu">
<li><a href="#home" class="nav-item">Home</a></li>
<li><a href="#technology" class="nav-item">Technology</a></li>
<li><a href="#ai" class="nav-item">AI & ML</a></li>
<li><a href="#startups" class="nav-item">Startups</a></li>
<li><a href="#reviews" class="nav-item">Reviews</a></li>
</ul>
</nav>
</div>
</div>
</header>
<section class="hero">
<div class="container">
<h1>Latest in Technology</h1>
<p>Stay updated with breaking tech news, in-depth analysis, and expert insights</p>
</div>
</section>
<main class="main-content">
<div class="container">
<div class="content-grid">
<div class="articles-section">
<h2>Latest Articles</h2>
<div id="articles-container">
<!-- Articles will be loaded dynamically -->
</div>
<div class="load-more">
<button class="load-more-btn" onclick="loadMoreArticles()" id="load-more-btn">
Load More Articles
</button>
</div>
</div>
<aside class="sidebar">
<h3>🔥 Trending Now</h3>
<ul class="trending-list" id="trending-list">
<!-- Trending articles will be loaded -->
</ul>
<div class="newsletter">
<h3>📧 Newsletter</h3>
<p>Get the latest tech news delivered to your inbox</p>
<form class="newsletter-form" onsubmit="subscribeNewsletter(event)">
<input type="email" class="newsletter-input" placeholder="Enter your email" required>
<button type="submit" class="newsletter-btn">Subscribe</button>
</form>
</div>
</aside>
</div>
</div>
</main>
<script>
// News Site Application
class NewsApp {
constructor() {
this.articles = [];
this.currentPage = 1;
this.articlesPerPage = 5;
this.totalArticles = 50; // Simulate large dataset
this.categories = ['Technology', 'AI & ML', 'Startups', 'Reviews', 'Security'];
this.init();
}
init() {
this.generateArticles();
this.renderArticles();
this.loadTrendingArticles();
this.setupInfiniteScroll();
this.simulateRealTimeUpdates();
}
generateArticles() {
const sampleTitles = [
"Revolutionary AI Model Achieves Human-Level Performance in Complex Reasoning",
"Quantum Computing Breakthrough: New Algorithm Solves Previously Impossible Problems",
"The Rise of Edge Computing: How It's Transforming Data Processing",
"Cybersecurity in 2024: New Threats and Defense Strategies",
"Sustainable Technology: Green Innovation in the Digital Age",
"5G Networks: Enabling the Internet of Things Revolution",
"Blockchain Beyond Cryptocurrency: Real-World Applications",
"Augmented Reality in Healthcare: Transforming Medical Training",
"The Future of Work: AI and Automation in the Workplace",
"Space Technology: Private Companies Leading the New Space Race"
];
const sampleExcerpts = [
"Researchers have developed a groundbreaking AI system that demonstrates human-level performance across multiple cognitive tasks...",
"Scientists at leading quantum computing laboratories have announced a major breakthrough that could revolutionize computing...",
"Edge computing is rapidly becoming a critical component of modern IT infrastructure, bringing processing power closer to data sources...",
"As cyber threats evolve, organizations must adapt their security strategies to protect against sophisticated attacks...",
"The technology industry is increasingly focusing on sustainable practices and environmentally friendly innovations...",
"The widespread deployment of 5G networks is enabling new possibilities for connected devices and smart cities...",
"Beyond digital currencies, blockchain technology is finding applications in supply chain management, healthcare, and more...",
"Medical professionals are using AR technology to enhance surgical procedures and improve patient outcomes...",
"The integration of AI and automation is reshaping job markets and creating new opportunities for human-AI collaboration...",
"Private space companies are achieving remarkable milestones in space exploration and satellite technology..."
];
for (let i = 0; i < this.totalArticles; i++) {
const title = sampleTitles[i % sampleTitles.length];
const excerpt = sampleExcerpts[i % sampleExcerpts.length];
const category = this.categories[i % this.categories.length];
this.articles.push({
id: i + 1,
title: `${title} ${i > 9 ? `(Part ${Math.floor(i/10) + 1})` : ''}`,
excerpt,
category,
author: ['John Smith', 'Sarah Johnson', 'Mike Chen', 'Emily Davis'][i % 4],
publishDate: new Date(Date.now() - (i * 24 * 60 * 60 * 1000)).toISOString().split('T')[0],
readTime: Math.floor(Math.random() * 10) + 3,
views: Math.floor(Math.random() * 5000) + 500,
comments: Math.floor(Math.random() * 50) + 5,
image: ['🚀', '🔬', '💻', '🤖', '🌐', '📱', '⚡'][i % 7]
});
}
// Sort by most recent
this.articles.sort((a, b) => new Date(b.publishDate) - new Date(a.publishDate));
}
renderArticles() {
const container = document.getElementById('articles-container');
const startIndex = (this.currentPage - 1) * this.articlesPerPage;
const endIndex = startIndex + this.articlesPerPage;
const articlesToShow = this.articles.slice(0, endIndex);
container.innerHTML = articlesToShow.map(article => `
<article class="article-card" onclick="readArticle(${article.id})">
<div class="article-image">${article.image}</div>
<div class="article-content">
<div class="article-meta">
<span class="article-category">${article.category}</span>
<span>By ${article.author}</span>
<span>${article.publishDate}</span>
<span>${article.readTime} min read</span>
</div>
<h3 class="article-title">${article.title}</h3>
<p class="article-excerpt">${article.excerpt}</p>
<a href="#" class="read-more">
Read More →
</a>
<div style="margin-top: 1rem; display: flex; gap: 1rem; font-size: 0.9rem; color: #6c757d;">
<span>👁️ ${article.views}</span>
<span>💬 ${article.comments}</span>
</div>
</div>
</article>
`).join('');
// Update load more button
const loadMoreBtn = document.getElementById('load-more-btn');
if (endIndex >= this.totalArticles) {
loadMoreBtn.style.display = 'none';
} else {
loadMoreBtn.style.display = 'inline-block';
}
}
loadTrendingArticles() {
const trendingContainer = document.getElementById('trending-list');
const trending = this.articles
.sort((a, b) => b.views - a.views)
.slice(0, 8);
trendingContainer.innerHTML = trending.map(article => `
<li class="trending-item">
<a href="#" class="trending-link" onclick="readArticle(${article.id})">
${article.title.length > 60 ? article.title.substring(0, 57) + '...' : article.title}
</a>
<div style="font-size: 0.8rem; color: #6c757d; margin-top: 0.25rem;">
${article.views} views
</div>
</li>
`).join('');
}
loadMoreArticles() {
this.currentPage++;
this.renderArticles();
// Smooth scroll to new content
setTimeout(() => {
const newArticles = document.querySelectorAll('.article-card');
const lastVisible = newArticles[Math.min(this.currentPage * this.articlesPerPage - this.articlesPerPage - 1, newArticles.length - 1)];
if (lastVisible) {
lastVisible.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 100);
}
setupInfiniteScroll() {
let isLoading = false;
window.addEventListener('scroll', () => {
if (isLoading) return;
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 1000) {
const loadMoreBtn = document.getElementById('load-more-btn');
if (loadMoreBtn.style.display !== 'none') {
isLoading = true;
this.loadMoreArticles();
setTimeout(() => { isLoading = false; }, 1000);
}
}
});
}
simulateRealTimeUpdates() {
setInterval(() => {
// Simulate view count updates
this.articles.forEach(article => {
if (Math.random() < 0.1) { // 10% chance
article.views += Math.floor(Math.random() * 10) + 1;
}
});
// Update trending articles occasionally
if (Math.random() < 0.2) { // 20% chance
this.loadTrendingArticles();
}
}, 5000);
// Simulate new articles being published
setInterval(() => {
if (Math.random() < 0.3) { // 30% chance
this.addNewArticle();
}
}, 30000);
}
addNewArticle() {
const newTitles = [
"Breaking: Major Tech Company Announces Revolutionary Product",
"Latest Research: AI Breakthrough in Natural Language Processing",
"Market Update: Tech Stocks Surge on Innovation News",
"Industry Analysis: The Impact of Emerging Technologies"
];
const newArticle = {
id: Date.now(),
title: newTitles[Math.floor(Math.random() * newTitles.length)],
excerpt: "This is a breaking news story that just came in. Our team is gathering more details and will provide updates as they become available...",
category: this.categories[Math.floor(Math.random() * this.categories.length)],
author: "Breaking News Team",
publishDate: new Date().toISOString().split('T')[0],
readTime: 2,
views: Math.floor(Math.random() * 100) + 10,
comments: 0,
image: "🚨"
};
this.articles.unshift(newArticle);
this.totalArticles++;
// Show notification
this.showNotification("New article published!");
// Re-render if on first page
if (this.currentPage === 1) {
this.renderArticles();
}
}
showNotification(message) {
const notification = document.createElement('div');
notification.textContent = message;
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4a90e2;
color: white;
padding: 1rem 1.5rem;
border-radius: 6px;
z-index: 1000;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
animation: slideIn 0.3s ease;
`;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 4000);
}
searchArticles(query) {
return this.articles.filter(article =>
article.title.toLowerCase().includes(query.toLowerCase()) ||
article.excerpt.toLowerCase().includes(query.toLowerCase()) ||
article.category.toLowerCase().includes(query.toLowerCase())
);
}
}
// Global functions
function loadMoreArticles() {
newsApp.loadMoreArticles();
}
function readArticle(id) {
const article = newsApp.articles.find(a => a.id === id);
if (article) {
// Simulate reading article
article.views++;
alert(`Reading: ${article.title}\n\nBy ${article.author}\nPublished: ${article.publishDate}\n\n${article.excerpt}`);
}
}
function subscribeNewsletter(event) {
event.preventDefault();
const email = event.target.querySelector('input').value;
alert(`Thank you for subscribing with email: ${email}`);
event.target.reset();
}
// Initialize news app
const newsApp = new NewsApp();
// Global test data
window.testData = {
siteName: 'TechNews Today',
version: '1.4.2',
totalArticles: () => newsApp.totalArticles,
currentPage: () => newsApp.currentPage,
articlesLoaded: () => newsApp.currentPage * newsApp.articlesPerPage,
categories: () => newsApp.categories,
searchArticles: (query) => newsApp.searchArticles(query),
getArticleById: (id) => newsApp.articles.find(a => a.id === id),
getTrendingArticles: () => newsApp.articles.sort((a, b) => b.views - a.views).slice(0, 5),
generateTimestamp: () => new Date().toISOString()
};
console.log('TechNews Today initialized');
console.log('Test data available at window.testData');
// Add CSS animation for notifications
const style = document.createElement('style');
style.textContent = `
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
`;
document.head.appendChild(style);
</script>
</body>
</html>

View file

@ -0,0 +1,662 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ReactFlow - Modern React Demo</title>
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f0f2f5;
color: #1c1e21;
}
.app {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem;
border-radius: 12px;
margin-bottom: 2rem;
text-align: center;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.header p {
opacity: 0.9;
font-size: 1.1rem;
}
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e4e6ea;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.card-header {
display: flex;
justify-content: between;
align-items: center;
margin-bottom: 1rem;
}
.card-title {
font-size: 1.2rem;
font-weight: 600;
color: #1c1e21;
}
.metric {
font-size: 2rem;
font-weight: bold;
color: #1877f2;
margin-bottom: 0.5rem;
}
.metric-label {
color: #65676b;
font-size: 0.9rem;
}
.controls {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.button {
background: #1877f2;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 8px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
margin: 0.25rem;
transition: background 0.2s ease;
}
.button:hover {
background: #166fe5;
}
.button:disabled {
background: #e4e6ea;
color: #8a8d91;
cursor: not-allowed;
}
.button.secondary {
background: #42b883;
}
.button.secondary:hover {
background: #369870;
}
.button.danger {
background: #e41e3f;
}
.button.danger:hover {
background: #d91b42;
}
.input-group {
margin-bottom: 1rem;
}
.input-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #1c1e21;
}
.input-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid #dddfe2;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s ease;
}
.input-group input:focus {
outline: none;
border-color: #1877f2;
box-shadow: 0 0 0 2px rgba(24, 119, 242, 0.2);
}
.todo-list {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.todo-item {
display: flex;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #e4e6ea;
transition: background 0.2s ease;
}
.todo-item:last-child {
border-bottom: none;
}
.todo-item:hover {
background: #f7f8fa;
}
.todo-item.completed {
opacity: 0.6;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
}
.todo-checkbox {
margin-right: 1rem;
width: 20px;
height: 20px;
}
.todo-text {
flex: 1;
font-size: 1rem;
}
.todo-delete {
background: #e41e3f;
color: white;
border: none;
padding: 0.5rem;
border-radius: 6px;
cursor: pointer;
font-size: 0.8rem;
}
.loading {
text-align: center;
padding: 2rem;
color: #65676b;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e4e6ea;
border-top: 4px solid #1877f2;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.notification {
position: fixed;
top: 20px;
right: 20px;
background: #42b883;
color: white;
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(66, 184, 131, 0.3);
transform: translateX(400px);
transition: transform 0.3s ease;
z-index: 1000;
}
.notification.show {
transform: translateX(0);
}
.react-component {
border: 2px dashed #1877f2;
border-radius: 8px;
padding: 1rem;
margin: 1rem 0;
background: rgba(24, 119, 242, 0.05);
}
.component-label {
font-size: 0.8rem;
color: #1877f2;
font-weight: 600;
margin-bottom: 0.5rem;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect, useRef, useCallback, useMemo } = React;
// Dashboard Component
function Dashboard({ metrics, onRefresh }) {
return (
<div className="dashboard">
<div className="card">
<div className="card-header">
<h3 className="card-title">Active Users</h3>
</div>
<div className="metric">{metrics.activeUsers}</div>
<div className="metric-label">Currently online</div>
</div>
<div className="card">
<div className="card-header">
<h3 className="card-title">Total Tasks</h3>
</div>
<div className="metric">{metrics.totalTasks}</div>
<div className="metric-label">Tasks created</div>
</div>
<div className="card">
<div className="card-header">
<h3 className="card-title">Completion Rate</h3>
</div>
<div className="metric">{metrics.completionRate}%</div>
<div className="metric-label">Tasks completed</div>
</div>
<div className="card">
<div className="card-header">
<h3 className="card-title">Performance Score</h3>
</div>
<div className="metric">{metrics.performanceScore}</div>
<div className="metric-label">Overall system health</div>
</div>
</div>
);
}
// Todo Item Component
function TodoItem({ todo, onToggle, onDelete }) {
return (
<div className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
className="todo-checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span className="todo-text">{todo.text}</span>
<button
className="todo-delete"
onClick={() => onDelete(todo.id)}
>
Delete
</button>
</div>
);
}
// Todo List Component
function TodoList({ todos, onToggle, onDelete, onAdd }) {
const [newTodo, setNewTodo] = useState('');
const inputRef = useRef(null);
const handleSubmit = useCallback((e) => {
e.preventDefault();
if (newTodo.trim()) {
onAdd(newTodo.trim());
setNewTodo('');
inputRef.current?.focus();
}
}, [newTodo, onAdd]);
const completedCount = useMemo(() =>
todos.filter(todo => todo.completed).length, [todos]
);
return (
<div className="todo-list">
<div className="react-component">
<div className="component-label">React Component: TodoList</div>
<h3>Task Manager ({completedCount}/{todos.length} completed)</h3>
<form onSubmit={handleSubmit} style={{ marginBottom: '1.5rem' }}>
<div className="input-group">
<label htmlFor="new-todo">Add New Task:</label>
<input
ref={inputRef}
id="new-todo"
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Enter a new task..."
/>
</div>
<button type="submit" className="button">Add Task</button>
</form>
{todos.length === 0 ? (
<div className="loading">
<p>No tasks yet. Add one above!</p>
</div>
) : (
todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))
)}
</div>
</div>
);
}
// Controls Component
function Controls({ onAction, loading }) {
return (
<div className="controls">
<div className="react-component">
<div className="component-label">React Component: Controls</div>
<h3>Actions</h3>
<div style={{ marginTop: '1rem' }}>
<button
className="button"
onClick={() => onAction('refresh')}
disabled={loading}
>
{loading ? 'Loading...' : 'Refresh Data'}
</button>
<button
className="button secondary"
onClick={() => onAction('simulate')}
disabled={loading}
>
Simulate Activity
</button>
<button
className="button danger"
onClick={() => onAction('reset')}
disabled={loading}
>
Reset All Data
</button>
</div>
</div>
</div>
);
}
// Notification Component
function Notification({ message, show, onClose }) {
useEffect(() => {
if (show) {
const timer = setTimeout(onClose, 3000);
return () => clearTimeout(timer);
}
}, [show, onClose]);
return (
<div className={`notification ${show ? 'show' : ''}`}>
{message}
</div>
);
}
// Main App Component
function App() {
const [metrics, setMetrics] = useState({
activeUsers: 0,
totalTasks: 0,
completionRate: 0,
performanceScore: 0
});
const [todos, setTodos] = useState([
{ id: 1, text: 'Setup React development environment', completed: true },
{ id: 2, text: 'Create component architecture', completed: true },
{ id: 3, text: 'Implement state management', completed: false },
{ id: 4, text: 'Add user interactions', completed: false },
{ id: 5, text: 'Write comprehensive tests', completed: false }
]);
const [loading, setLoading] = useState(false);
const [notification, setNotification] = useState({ message: '', show: false });
const [nextId, setNextId] = useState(6);
// Initialize metrics
useEffect(() => {
const initializeMetrics = () => {
setMetrics({
activeUsers: Math.floor(Math.random() * 100) + 50,
totalTasks: todos.length,
completionRate: Math.round((todos.filter(t => t.completed).length / todos.length) * 100),
performanceScore: Math.floor(Math.random() * 20) + 80
});
};
initializeMetrics();
const interval = setInterval(initializeMetrics, 5000);
return () => clearInterval(interval);
}, [todos]);
const showNotification = useCallback((message) => {
setNotification({ message, show: true });
}, []);
const hideNotification = useCallback(() => {
setNotification(prev => ({ ...prev, show: false }));
}, []);
const handleAction = useCallback(async (action) => {
setLoading(true);
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 1000));
switch (action) {
case 'refresh':
setMetrics(prev => ({
...prev,
activeUsers: Math.floor(Math.random() * 100) + 50,
performanceScore: Math.floor(Math.random() * 20) + 80
}));
showNotification('Data refreshed successfully!');
break;
case 'simulate':
setMetrics(prev => ({
...prev,
activeUsers: prev.activeUsers + Math.floor(Math.random() * 20),
performanceScore: Math.min(100, prev.performanceScore + Math.floor(Math.random() * 10))
}));
showNotification('Activity simulation completed!');
break;
case 'reset':
setTodos([]);
setMetrics({ activeUsers: 0, totalTasks: 0, completionRate: 0, performanceScore: 0 });
showNotification('All data has been reset!');
break;
}
setLoading(false);
}, [showNotification]);
const addTodo = useCallback((text) => {
const newTodo = { id: nextId, text, completed: false };
setTodos(prev => [...prev, newTodo]);
setNextId(prev => prev + 1);
showNotification(`Task "${text}" added successfully!`);
}, [nextId, showNotification]);
const toggleTodo = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
showNotification('Task status updated!');
}, [showNotification]);
const deleteTodo = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
showNotification('Task deleted!');
}, [showNotification]);
return (
<div className="app">
<div className="header">
<h1>ReactFlow Dashboard</h1>
<p>Modern React application with hooks, state management, and component interactions</p>
</div>
<Dashboard metrics={metrics} onRefresh={() => handleAction('refresh')} />
<Controls onAction={handleAction} loading={loading} />
<TodoList
todos={todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
onAdd={addTodo}
/>
<Notification
message={notification.message}
show={notification.show}
onClose={hideNotification}
/>
</div>
);
}
// Render the app
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
// Global test data for Crawailer testing
window.testData = {
framework: 'react',
version: React.version,
hasReactDOM: typeof ReactDOM !== 'undefined',
componentCount: () => {
const reactRoot = document.querySelector('#root');
return reactRoot ? reactRoot.querySelectorAll('[data-reactroot] *').length : 0;
},
getAppState: () => {
// Access React DevTools if available
if (window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
return { hasDevTools: true, fiberVersion: React.version };
}
return { hasDevTools: false };
},
getTodoCount: () => {
return document.querySelectorAll('.todo-item').length;
},
getCompletedTodos: () => {
return document.querySelectorAll('.todo-item.completed').length;
},
simulateUserAction: (action) => {
switch (action) {
case 'add-todo':
const input = document.querySelector('#new-todo');
const form = input.closest('form');
if (input && form) {
input.value = 'Test task from JavaScript';
input.dispatchEvent(new Event('change', { bubbles: true }));
form.dispatchEvent(new Event('submit', { bubbles: true }));
return { success: true, action: 'Todo added via JavaScript' };
}
return { success: false, error: 'Form elements not found' };
case 'toggle-first-todo':
const firstCheckbox = document.querySelector('.todo-checkbox');
if (firstCheckbox) {
firstCheckbox.click();
return { success: true, action: 'First todo toggled' };
}
return { success: false, error: 'No todos found' };
case 'refresh-data':
const refreshBtn = document.querySelector('.button');
if (refreshBtn && refreshBtn.textContent.includes('Refresh')) {
refreshBtn.click();
return { success: true, action: 'Data refresh triggered' };
}
return { success: false, error: 'Refresh button not found' };
default:
return { success: false, error: 'Unknown action' };
}
},
getMetrics: () => {
const metricElements = document.querySelectorAll('.metric');
const metrics = {};
metricElements.forEach((el, index) => {
const label = el.parentNode.querySelector('.metric-label')?.textContent || `metric${index}`;
metrics[label.replace(/\s+/g, '_')] = el.textContent;
});
return metrics;
},
generateTimestamp: () => new Date().toISOString(),
detectReactFeatures: () => {
return {
hasHooks: typeof React.useState !== 'undefined',
hasEffects: typeof React.useEffect !== 'undefined',
hasContext: typeof React.createContext !== 'undefined',
hasSuspense: typeof React.Suspense !== 'undefined',
hasFragments: typeof React.Fragment !== 'undefined',
reactVersion: React.version
};
}
};
// Console logging for debugging
console.log('ReactFlow app initialized');
console.log('React version:', React.version);
console.log('Test data available at window.testData');
</script>
</body>
</html>

View file

@ -0,0 +1,807 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TaskFlow - Modern SPA Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f8fafc;
color: #334155;
line-height: 1.6;
}
.app-container {
max-width: 1200px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.header {
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
color: white;
padding: 1rem 2rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.nav {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
}
.nav-menu {
display: flex;
list-style: none;
gap: 2rem;
}
.nav-item {
cursor: pointer;
padding: 0.5rem 1rem;
border-radius: 6px;
transition: background 0.3s ease;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.2);
}
.nav-item.active {
background: rgba(255, 255, 255, 0.3);
font-weight: bold;
}
/* Main Content */
.main-content {
flex: 1;
padding: 2rem;
}
.page {
display: none;
animation: fadeIn 0.3s ease-in;
}
.page.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
/* Dashboard Page */
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e2e8f0;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.card-header {
display: flex;
justify-content: between;
align-items: center;
margin-bottom: 1rem;
}
.card-title {
font-size: 1.1rem;
font-weight: 600;
color: #1e293b;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #3b82f6;
}
/* Tasks Page */
.task-container {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.task-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.btn {
background: #3b82f6;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
transition: background 0.2s ease;
}
.btn:hover {
background: #2563eb;
}
.btn-secondary {
background: #6b7280;
}
.btn-secondary:hover {
background: #4b5563;
}
.task-list {
list-style: none;
}
.task-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
margin-bottom: 0.5rem;
transition: background 0.2s ease;
}
.task-item:hover {
background: #f8fafc;
}
.task-checkbox {
width: 18px;
height: 18px;
}
.task-text {
flex: 1;
}
.task-completed {
text-decoration: line-through;
opacity: 0.6;
}
.task-delete {
background: #ef4444;
color: white;
border: none;
padding: 0.25rem 0.5rem;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
.task-delete:hover {
background: #dc2626;
}
/* Analytics Page */
.chart-container {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 1.5rem;
}
.chart {
height: 300px;
background: linear-gradient(45deg, #f1f5f9, #e2e8f0);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
color: #64748b;
font-size: 1.1rem;
position: relative;
overflow: hidden;
}
.chart-bars {
display: flex;
align-items: end;
gap: 1rem;
height: 200px;
}
.chart-bar {
background: linear-gradient(to top, #3b82f6, #60a5fa);
width: 40px;
border-radius: 4px 4px 0 0;
transition: transform 0.3s ease;
}
.chart-bar:hover {
transform: scaleY(1.1);
}
/* Loading States */
.loading {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
color: #6b7280;
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid #e2e8f0;
border-top: 2px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Responsive */
@media (max-width: 768px) {
.nav-menu {
gap: 1rem;
}
.dashboard-grid {
grid-template-columns: 1fr;
}
.main-content {
padding: 1rem;
}
}
/* Form Styles */
.form-group {
margin-bottom: 1rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #374151;
}
.form-input {
width: 100%;
padding: 0.75rem;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s ease;
}
.form-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.modal.active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
border-radius: 12px;
padding: 2rem;
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #e2e8f0;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #6b7280;
}
.modal-close:hover {
color: #374151;
}
</style>
</head>
<body>
<div class="app-container">
<header class="header">
<nav class="nav">
<div class="logo">TaskFlow</div>
<ul class="nav-menu">
<li class="nav-item active" data-page="dashboard">Dashboard</li>
<li class="nav-item" data-page="tasks">Tasks</li>
<li class="nav-item" data-page="analytics">Analytics</li>
<li class="nav-item" data-page="settings">Settings</li>
</ul>
</nav>
</header>
<main class="main-content">
<!-- Dashboard Page -->
<div id="dashboard" class="page active">
<h1>Dashboard</h1>
<div class="dashboard-grid">
<div class="card">
<div class="card-header">
<h3 class="card-title">Total Tasks</h3>
</div>
<div class="stat-number" id="total-tasks">--</div>
<p class="text-gray-600">Tasks in your workspace</p>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Completed Today</h3>
</div>
<div class="stat-number" id="completed-today">--</div>
<p class="text-gray-600">Tasks completed today</p>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Active Projects</h3>
</div>
<div class="stat-number" id="active-projects">--</div>
<p class="text-gray-600">Projects in progress</p>
</div>
<div class="card">
<div class="card-header">
<h3 class="card-title">Team Members</h3>
</div>
<div class="stat-number" id="team-members">--</div>
<p class="text-gray-600">Active team members</p>
</div>
</div>
<div class="card">
<h3 class="card-title">Recent Activity</h3>
<div id="recent-activity" class="loading">
<div class="spinner"></div>
Loading recent activity...
</div>
</div>
</div>
<!-- Tasks Page -->
<div id="tasks" class="page">
<div class="task-container">
<div class="task-header">
<h1>Tasks</h1>
<button class="btn" onclick="openAddTaskModal()">Add Task</button>
</div>
<div class="form-group">
<input type="text" id="task-filter" class="form-input" placeholder="Filter tasks...">
</div>
<ul id="task-list" class="task-list">
<!-- Tasks will be dynamically loaded -->
</ul>
</div>
</div>
<!-- Analytics Page -->
<div id="analytics" class="page">
<h1>Analytics</h1>
<div class="chart-container">
<h3>Task Completion Over Time</h3>
<div class="chart">
<div class="chart-bars" id="completion-chart">
<!-- Chart bars will be generated -->
</div>
</div>
</div>
<div class="dashboard-grid">
<div class="card">
<h3 class="card-title">Average Completion Time</h3>
<div class="stat-number">2.4h</div>
</div>
<div class="card">
<h3 class="card-title">Productivity Score</h3>
<div class="stat-number">87%</div>
</div>
</div>
</div>
<!-- Settings Page -->
<div id="settings" class="page">
<h1>Settings</h1>
<div class="card">
<h3 class="card-title">User Preferences</h3>
<div class="form-group">
<label class="form-label">Theme</label>
<select class="form-input" id="theme-select">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="auto">Auto</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Notifications</label>
<input type="checkbox" id="notifications-enabled" checked> Enable notifications
</div>
<button class="btn" onclick="saveSettings()">Save Settings</button>
</div>
</div>
</main>
</div>
<!-- Add Task Modal -->
<div id="add-task-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Add New Task</h3>
<button class="modal-close" onclick="closeAddTaskModal()">&times;</button>
</div>
<form id="add-task-form">
<div class="form-group">
<label class="form-label">Task Title</label>
<input type="text" id="task-title" class="form-input" required>
</div>
<div class="form-group">
<label class="form-label">Description</label>
<textarea id="task-description" class="form-input" rows="3"></textarea>
</div>
<div class="form-group">
<label class="form-label">Priority</label>
<select id="task-priority" class="form-input">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
<div style="display: flex; gap: 1rem; justify-content: end;">
<button type="button" class="btn btn-secondary" onclick="closeAddTaskModal()">Cancel</button>
<button type="submit" class="btn">Add Task</button>
</div>
</form>
</div>
</div>
<script>
// SPA Router and State Management
class TaskFlowApp {
constructor() {
this.currentPage = 'dashboard';
this.tasks = [
{ id: 1, title: 'Setup development environment', completed: true, priority: 'high' },
{ id: 2, title: 'Design user interface mockups', completed: false, priority: 'medium' },
{ id: 3, title: 'Implement authentication system', completed: false, priority: 'high' },
{ id: 4, title: 'Write unit tests', completed: false, priority: 'medium' },
{ id: 5, title: 'Deploy to staging', completed: false, priority: 'low' }
];
this.settings = {
theme: 'light',
notifications: true
};
this.init();
}
init() {
this.setupNavigation();
this.loadDashboardData();
this.renderTasks();
this.generateChart();
this.setupTaskFilter();
this.loadSettings();
// Simulate real-time updates
setInterval(() => this.updateRealtimeData(), 5000);
}
setupNavigation() {
document.querySelectorAll('.nav-item').forEach(item => {
item.addEventListener('click', (e) => {
const page = e.target.dataset.page;
this.navigateToPage(page);
});
});
// Handle browser back/forward
window.addEventListener('popstate', (e) => {
const page = e.state?.page || 'dashboard';
this.navigateToPage(page, false);
});
// Set initial URL
history.replaceState({ page: 'dashboard' }, '', '/spa/dashboard');
}
navigateToPage(page, pushState = true) {
// Hide current page
document.querySelector('.page.active').classList.remove('active');
document.querySelector('.nav-item.active').classList.remove('active');
// Show new page
document.getElementById(page).classList.add('active');
document.querySelector(`[data-page="${page}"]`).classList.add('active');
this.currentPage = page;
// Update URL
if (pushState) {
history.pushState({ page }, '', `/spa/${page}`);
}
// Load page-specific data
this.loadPageData(page);
}
loadPageData(page) {
switch (page) {
case 'dashboard':
this.loadDashboardData();
break;
case 'tasks':
this.renderTasks();
break;
case 'analytics':
this.generateChart();
break;
case 'settings':
this.loadSettings();
break;
}
}
loadDashboardData() {
// Simulate API loading
setTimeout(() => {
document.getElementById('total-tasks').textContent = this.tasks.length;
document.getElementById('completed-today').textContent =
this.tasks.filter(t => t.completed).length;
document.getElementById('active-projects').textContent = '3';
document.getElementById('team-members').textContent = '12';
// Load recent activity
const activityEl = document.getElementById('recent-activity');
activityEl.innerHTML = `
<div style="space-y: 0.5rem;">
<div>✅ Task "Setup development environment" completed</div>
<div>📝 New task "Design user interface mockups" created</div>
<div>👥 Team member John joined the project</div>
<div>🚀 Project "Web Application" moved to review</div>
</div>
`;
}, 1000);
}
renderTasks() {
const taskList = document.getElementById('task-list');
taskList.innerHTML = this.tasks.map(task => `
<li class="task-item">
<input type="checkbox" class="task-checkbox"
${task.completed ? 'checked' : ''}
onchange="app.toggleTask(${task.id})">
<span class="task-text ${task.completed ? 'task-completed' : ''}">
${task.title}
</span>
<span class="task-priority" style="
color: ${task.priority === 'high' ? '#ef4444' :
task.priority === 'medium' ? '#f59e0b' : '#6b7280'};
font-size: 0.8rem;
font-weight: 500;
">${task.priority.toUpperCase()}</span>
<button class="task-delete" onclick="app.deleteTask(${task.id})">Delete</button>
</li>
`).join('');
}
setupTaskFilter() {
const filterInput = document.getElementById('task-filter');
if (filterInput) {
filterInput.addEventListener('input', (e) => {
const filter = e.target.value.toLowerCase();
const taskItems = document.querySelectorAll('.task-item');
taskItems.forEach(item => {
const text = item.querySelector('.task-text').textContent.toLowerCase();
item.style.display = text.includes(filter) ? 'flex' : 'none';
});
});
}
}
toggleTask(id) {
const task = this.tasks.find(t => t.id === id);
if (task) {
task.completed = !task.completed;
this.renderTasks();
this.loadDashboardData(); // Update dashboard stats
}
}
deleteTask(id) {
this.tasks = this.tasks.filter(t => t.id !== id);
this.renderTasks();
this.loadDashboardData();
}
addTask(title, description, priority) {
const newTask = {
id: Date.now(),
title,
description,
priority,
completed: false
};
this.tasks.push(newTask);
this.renderTasks();
this.loadDashboardData();
}
generateChart() {
const chartContainer = document.getElementById('completion-chart');
if (!chartContainer) return;
// Generate random chart data
const data = Array.from({ length: 7 }, () => Math.floor(Math.random() * 100) + 20);
chartContainer.innerHTML = data.map(value => `
<div class="chart-bar" style="height: ${value}%;" title="${value}%"></div>
`).join('');
}
loadSettings() {
const themeSelect = document.getElementById('theme-select');
const notificationsCheck = document.getElementById('notifications-enabled');
if (themeSelect) themeSelect.value = this.settings.theme;
if (notificationsCheck) notificationsCheck.checked = this.settings.notifications;
}
saveSettings() {
const themeSelect = document.getElementById('theme-select');
const notificationsCheck = document.getElementById('notifications-enabled');
this.settings.theme = themeSelect.value;
this.settings.notifications = notificationsCheck.checked;
// Simulate save to server
alert('Settings saved successfully!');
}
updateRealtimeData() {
// Simulate real-time updates
const now = new Date();
const timeElement = document.querySelector('.timestamp');
if (timeElement) {
timeElement.textContent = now.toLocaleTimeString();
}
// Add random activity
if (Math.random() < 0.3 && this.currentPage === 'dashboard') {
this.loadDashboardData();
}
}
}
// Modal functions
function openAddTaskModal() {
document.getElementById('add-task-modal').classList.add('active');
}
function closeAddTaskModal() {
document.getElementById('add-task-modal').classList.remove('active');
document.getElementById('add-task-form').reset();
}
function saveSettings() {
app.saveSettings();
}
// Handle form submission
document.getElementById('add-task-form').addEventListener('submit', (e) => {
e.preventDefault();
const title = document.getElementById('task-title').value;
const description = document.getElementById('task-description').value;
const priority = document.getElementById('task-priority').value;
app.addTask(title, description, priority);
closeAddTaskModal();
});
// Initialize app
const app = new TaskFlowApp();
// Global test data for Crawailer testing
window.testData = {
appName: 'TaskFlow',
version: '2.1.0',
framework: 'Vanilla JS SPA',
routes: ['dashboard', 'tasks', 'analytics', 'settings'],
features: ['routing', 'state-management', 'real-time-updates', 'modals'],
totalTasks: () => app.tasks.length,
completedTasks: () => app.tasks.filter(t => t.completed).length,
getCurrentPage: () => app.currentPage,
getSettings: () => app.settings,
generateTimestamp: () => new Date().toISOString()
};
// Console logging for testing
console.log('TaskFlow SPA initialized');
console.log('Test data available at window.testData');
console.log('Current route:', window.location.pathname);
</script>
</body>
</html>

View file

@ -0,0 +1,21 @@
id,name,email,signup_date,status,plan,monthly_spend
1,John Smith,john.smith@example.com,2023-01-15,active,premium,99.99
2,Sarah Johnson,sarah.j@company.com,2023-02-03,active,basic,29.99
3,Mike Chen,mike.chen@startup.io,2023-01-28,inactive,premium,99.99
4,Emily Davis,emily.davis@tech.org,2023-03-12,active,enterprise,299.99
5,Robert Wilson,r.wilson@business.net,2023-02-18,active,basic,29.99
6,Lisa Brown,lisa.brown@design.co,2023-01-09,active,premium,99.99
7,David Lee,david.lee@dev.com,2023-03-05,pending,basic,0.00
8,Amanda Taylor,a.taylor@marketing.io,2023-02-25,active,premium,99.99
9,Chris Anderson,chris@analytics.com,2023-01-31,active,enterprise,299.99
10,Jessica White,jess.white@creative.org,2023-03-08,active,basic,29.99
11,Tom Martinez,tom.m@consulting.biz,2023-02-14,inactive,premium,99.99
12,Rachel Green,rachel.g@nonprofit.org,2023-01-22,active,basic,29.99
13,Kevin Thompson,kevin.t@fintech.io,2023-03-01,active,enterprise,299.99
14,Nicole Adams,n.adams@health.com,2023-02-09,active,premium,99.99
15,Daniel Clark,dan.clark@edu.org,2023-01-17,pending,basic,0.00
16,Stephanie Lewis,steph.l@retail.com,2023-02-28,active,premium,99.99
17,Mark Rodriguez,mark.r@logistics.co,2023-01-24,active,basic,29.99
18,Jennifer Hall,jen.hall@media.io,2023-03-14,active,enterprise,299.99
19,Andrew Young,andrew.y@travel.com,2023-02-11,inactive,premium,99.99
20,Michelle King,michelle.k@legal.org,2023-01-29,active,basic,29.99
1 id name email signup_date status plan monthly_spend
2 1 John Smith john.smith@example.com 2023-01-15 active premium 99.99
3 2 Sarah Johnson sarah.j@company.com 2023-02-03 active basic 29.99
4 3 Mike Chen mike.chen@startup.io 2023-01-28 inactive premium 99.99
5 4 Emily Davis emily.davis@tech.org 2023-03-12 active enterprise 299.99
6 5 Robert Wilson r.wilson@business.net 2023-02-18 active basic 29.99
7 6 Lisa Brown lisa.brown@design.co 2023-01-09 active premium 99.99
8 7 David Lee david.lee@dev.com 2023-03-05 pending basic 0.00
9 8 Amanda Taylor a.taylor@marketing.io 2023-02-25 active premium 99.99
10 9 Chris Anderson chris@analytics.com 2023-01-31 active enterprise 299.99
11 10 Jessica White jess.white@creative.org 2023-03-08 active basic 29.99
12 11 Tom Martinez tom.m@consulting.biz 2023-02-14 inactive premium 99.99
13 12 Rachel Green rachel.g@nonprofit.org 2023-01-22 active basic 29.99
14 13 Kevin Thompson kevin.t@fintech.io 2023-03-01 active enterprise 299.99
15 14 Nicole Adams n.adams@health.com 2023-02-09 active premium 99.99
16 15 Daniel Clark dan.clark@edu.org 2023-01-17 pending basic 0.00
17 16 Stephanie Lewis steph.l@retail.com 2023-02-28 active premium 99.99
18 17 Mark Rodriguez mark.r@logistics.co 2023-01-24 active basic 29.99
19 18 Jennifer Hall jen.hall@media.io 2023-03-14 active enterprise 299.99
20 19 Andrew Young andrew.y@travel.com 2023-02-11 inactive premium 99.99
21 20 Michelle King michelle.k@legal.org 2023-01-29 active basic 29.99

View file

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Static Files Server</title>
<style>
body {
font-family: system-ui, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 2rem;
background: #f5f5f5;
}
.container {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h1 { color: #333; }
.file-list {
list-style: none;
padding: 0;
}
.file-item {
padding: 1rem;
margin: 0.5rem 0;
background: #f8f9fa;
border-radius: 6px;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-name {
font-weight: 500;
}
.file-size {
color: #666;
font-size: 0.9rem;
}
.download-btn {
background: #007bff;
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
}
</style>
</head>
<body>
<div class="container">
<h1>📁 Static Files Directory</h1>
<p>Collection of test files for download and processing scenarios.</p>
<ul class="file-list">
<li class="file-item">
<div>
<div class="file-name">📄 sample-document.pdf</div>
<div class="file-size">2.3 MB</div>
</div>
<a href="/static/files/sample-document.pdf" class="download-btn">Download</a>
</li>
<li class="file-item">
<div>
<div class="file-name">🖼️ test-image.jpg</div>
<div class="file-size">856 KB</div>
</div>
<a href="/static/files/test-image.jpg" class="download-btn">Download</a>
</li>
<li class="file-item">
<div>
<div class="file-name">📊 data-export.csv</div>
<div class="file-size">143 KB</div>
</div>
<a href="/static/files/data-export.csv" class="download-btn">Download</a>
</li>
<li class="file-item">
<div>
<div class="file-name">🎵 audio-sample.mp3</div>
<div class="file-size">4.2 MB</div>
</div>
<a href="/static/files/audio-sample.mp3" class="download-btn">Download</a>
</li>
<li class="file-item">
<div>
<div class="file-name">📦 archive.zip</div>
<div class="file-size">1.8 MB</div>
</div>
<a href="/static/files/archive.zip" class="download-btn">Download</a>
</li>
</ul>
</div>
<script>
window.testData = {
fileCount: 5,
totalSize: '9.3 MB',
fileTypes: ['pdf', 'jpg', 'csv', 'mp3', 'zip'],
generateTimestamp: () => new Date().toISOString()
};
</script>
</body>
</html>

View file

@ -0,0 +1,747 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue.js Test Application - Crawailer Testing</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.app-container {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
}
h1 {
color: #4FC08D;
text-align: center;
margin-bottom: 30px;
font-size: 2.5rem;
}
.section {
margin: 30px 0;
padding: 20px;
border: 2px solid #e9ecef;
border-radius: 8px;
background: #f8f9fa;
}
.section h2 {
color: #4FC08D;
margin-top: 0;
}
.controls {
display: flex;
gap: 10px;
margin: 15px 0;
flex-wrap: wrap;
}
button {
background: #4FC08D;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
button:hover {
background: #369870;
transform: translateY(-2px);
}
button:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
}
input, textarea, select {
padding: 10px;
border: 2px solid #ddd;
border-radius: 5px;
font-size: 14px;
margin: 5px;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: #4FC08D;
}
.todo-item {
display: flex;
align-items: center;
padding: 10px;
margin: 5px 0;
background: white;
border-radius: 5px;
border-left: 4px solid #4FC08D;
transition: all 0.3s ease;
}
.todo-item:hover {
transform: translateX(5px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.todo-item.completed {
opacity: 0.7;
border-left-color: #28a745;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin: 20px 0;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 8px;
text-align: center;
border: 2px solid #4FC08D;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #4FC08D;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 5px;
color: white;
font-weight: bold;
z-index: 1000;
transform: translateX(400px);
transition: transform 0.3s ease;
}
.notification.show {
transform: translateX(0);
}
.notification.success { background: #28a745; }
.notification.warning { background: #ffc107; color: #333; }
.notification.error { background: #dc3545; }
.form-group {
margin: 15px 0;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.reactive-demo {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin: 20px 0;
}
@media (max-width: 768px) {
.reactive-demo {
grid-template-columns: 1fr;
}
.controls {
flex-direction: column;
}
}
</style>
</head>
<body>
<div id="app">
<div class="app-container">
<h1>🌿 Vue.js 3 Reactive Testing App</h1>
<!-- Real-time Data Binding Section -->
<div class="section">
<h2>📊 Real-time Data Binding & Reactivity</h2>
<div class="reactive-demo">
<div>
<div class="form-group">
<label>Your Name:</label>
<input v-model="user.name" placeholder="Enter your name" data-testid="name-input">
</div>
<div class="form-group">
<label>Your Email:</label>
<input v-model="user.email" type="email" placeholder="Enter your email" data-testid="email-input">
</div>
<div class="form-group">
<label>Theme:</label>
<select v-model="settings.theme" data-testid="theme-select">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="auto">Auto</option>
</select>
</div>
</div>
<div>
<h3>Live Preview:</h3>
<p><strong>Name:</strong> {{ user.name || 'Anonymous' }}</p>
<p><strong>Email:</strong> {{ user.email || 'Not provided' }}</p>
<p><strong>Theme:</strong> {{ settings.theme }}</p>
<p><strong>Character Count:</strong> {{ totalCharacters }}</p>
<p><strong>Valid Email:</strong> {{ isValidEmail ? '✅' : '❌' }}</p>
</div>
</div>
</div>
<!-- Todo List with Advanced State -->
<div class="section">
<h2>📝 Advanced Todo List (Vuex-style State)</h2>
<div class="controls">
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="Add a new todo..."
data-testid="todo-input">
<button @click="addTodo" :disabled="!newTodo.trim()" data-testid="add-todo-btn">
Add Todo
</button>
<button @click="clearCompleted" :disabled="!hasCompletedTodos" data-testid="clear-completed-btn">
Clear Completed ({{ completedCount }})
</button>
<button @click="toggleAllTodos" data-testid="toggle-all-btn">
{{ allCompleted ? 'Mark All Incomplete' : 'Mark All Complete' }}
</button>
</div>
<div class="todo-list" data-testid="todo-list">
<div
v-for="todo in filteredTodos"
:key="todo.id"
:class="['todo-item', { completed: todo.completed }]"
:data-testid="`todo-${todo.id}`">
<input
type="checkbox"
v-model="todo.completed"
:data-testid="`todo-checkbox-${todo.id}`">
<span class="todo-text">{{ todo.text }}</span>
<button @click="removeTodo(todo.id)" :data-testid="`remove-todo-${todo.id}`"></button>
</div>
</div>
<div class="controls">
<button
v-for="filter in ['all', 'active', 'completed']"
:key="filter"
@click="currentFilter = filter"
:class="{ active: currentFilter === filter }"
:data-testid="`filter-${filter}`">
{{ filter.charAt(0).toUpperCase() + filter.slice(1) }}
</button>
</div>
</div>
<!-- Dynamic Components & Advanced Interactions -->
<div class="section">
<h2>🎛️ Dynamic Components & Interactions</h2>
<div class="controls">
<button @click="incrementCounter" data-testid="increment-btn">
Increment ({{ counter }})
</button>
<button @click="decrementCounter" data-testid="decrement-btn">
Decrement
</button>
<button @click="resetCounter" data-testid="reset-btn">
Reset
</button>
<button @click="simulateAsyncOperation" :disabled="isLoading" data-testid="async-btn">
{{ isLoading ? 'Loading...' : 'Async Operation' }}
</button>
</div>
<div class="stats">
<div class="stat-card">
<div class="stat-number">{{ counter }}</div>
<div>Counter Value</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ todos.length }}</div>
<div>Total Todos</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ completedCount }}</div>
<div>Completed</div>
</div>
<div class="stat-card">
<div class="stat-number">{{ user.name.length }}</div>
<div>Name Length</div>
</div>
</div>
</div>
<!-- Watchers & Lifecycle Demo -->
<div class="section">
<h2>🔄 Watchers & Lifecycle</h2>
<p><strong>Component Mounted:</strong> {{ mountTime }}</p>
<p><strong>Updates Count:</strong> {{ updateCount }}</p>
<p><strong>Last Action:</strong> {{ lastAction }}</p>
<p><strong>Deep Watch Demo:</strong> {{ JSON.stringify(watchedData) }}</p>
<button @click="triggerDeepChange" data-testid="deep-change-btn">
Trigger Deep Change
</button>
</div>
</div>
<!-- Notification System -->
<div
v-if="notification.show"
:class="['notification', notification.type, { show: notification.show }]"
data-testid="notification">
{{ notification.message }}
</div>
</div>
<script>
const { createApp, ref, computed, watch, onMounted, onUpdated, nextTick } = Vue;
createApp({
setup() {
// Reactive data
const user = ref({
name: '',
email: ''
});
const settings = ref({
theme: 'light'
});
const todos = ref([
{ id: 1, text: 'Learn Vue.js 3 Composition API', completed: true },
{ id: 2, text: 'Build reactive components', completed: false },
{ id: 3, text: 'Test with Crawailer', completed: false }
]);
const newTodo = ref('');
const currentFilter = ref('all');
const counter = ref(0);
const isLoading = ref(false);
const updateCount = ref(0);
const mountTime = ref('');
const lastAction = ref('Initial load');
const watchedData = ref({
nested: {
value: 'initial',
count: 0
}
});
const notification = ref({
show: false,
message: '',
type: 'success'
});
// Computed properties
const totalCharacters = computed(() => {
return user.value.name.length + user.value.email.length;
});
const isValidEmail = computed(() => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(user.value.email);
});
const completedCount = computed(() => {
return todos.value.filter(todo => todo.completed).length;
});
const hasCompletedTodos = computed(() => completedCount.value > 0);
const allCompleted = computed(() => {
return todos.value.length > 0 && todos.value.every(todo => todo.completed);
});
const filteredTodos = computed(() => {
switch (currentFilter.value) {
case 'active':
return todos.value.filter(todo => !todo.completed);
case 'completed':
return todos.value.filter(todo => todo.completed);
default:
return todos.value;
}
});
// Methods
const addTodo = () => {
if (newTodo.value.trim()) {
const newId = Math.max(...todos.value.map(t => t.id), 0) + 1;
todos.value.push({
id: newId,
text: newTodo.value.trim(),
completed: false
});
newTodo.value = '';
lastAction.value = 'Added todo';
showNotification('Todo added successfully!', 'success');
}
};
const removeTodo = (id) => {
const index = todos.value.findIndex(todo => todo.id === id);
if (index > -1) {
todos.value.splice(index, 1);
lastAction.value = 'Removed todo';
showNotification('Todo removed!', 'warning');
}
};
const clearCompleted = () => {
const beforeCount = todos.value.length;
todos.value = todos.value.filter(todo => !todo.completed);
const removedCount = beforeCount - todos.value.length;
lastAction.value = `Cleared ${removedCount} completed todos`;
showNotification(`Cleared ${removedCount} completed todos`, 'success');
};
const toggleAllTodos = () => {
const newStatus = !allCompleted.value;
todos.value.forEach(todo => {
todo.completed = newStatus;
});
lastAction.value = newStatus ? 'Marked all complete' : 'Marked all incomplete';
showNotification(lastAction.value, 'success');
};
const incrementCounter = () => {
counter.value++;
lastAction.value = 'Incremented counter';
};
const decrementCounter = () => {
counter.value--;
lastAction.value = 'Decremented counter';
};
const resetCounter = () => {
counter.value = 0;
lastAction.value = 'Reset counter';
showNotification('Counter reset!', 'success');
};
const simulateAsyncOperation = async () => {
isLoading.value = true;
lastAction.value = 'Started async operation';
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
isLoading.value = false;
lastAction.value = 'Completed async operation';
showNotification('Async operation completed!', 'success');
};
const triggerDeepChange = () => {
watchedData.value.nested.count++;
watchedData.value.nested.value = `Updated ${watchedData.value.nested.count} times`;
lastAction.value = 'Triggered deep change';
};
const showNotification = (message, type = 'success') => {
notification.value = {
show: true,
message,
type
};
setTimeout(() => {
notification.value.show = false;
}, 3000);
};
// Watchers
watch(user, (newUser, oldUser) => {
console.log('User changed:', { newUser, oldUser });
}, { deep: true });
watch(counter, (newVal, oldVal) => {
console.log(`Counter changed from ${oldVal} to ${newVal}`);
});
watch(watchedData, (newData) => {
console.log('Deep watched data changed:', newData);
}, { deep: true });
// Lifecycle hooks
onMounted(() => {
mountTime.value = new Date().toLocaleTimeString();
console.log('Vue component mounted');
// Simulate initial data load
setTimeout(() => {
showNotification('Vue app loaded successfully!', 'success');
}, 500);
});
onUpdated(() => {
updateCount.value++;
});
return {
user,
settings,
todos,
newTodo,
currentFilter,
counter,
isLoading,
updateCount,
mountTime,
lastAction,
watchedData,
notification,
totalCharacters,
isValidEmail,
completedCount,
hasCompletedTodos,
allCompleted,
filteredTodos,
addTodo,
removeTodo,
clearCompleted,
toggleAllTodos,
incrementCounter,
decrementCounter,
resetCounter,
simulateAsyncOperation,
triggerDeepChange,
showNotification
};
}
}).mount('#app');
// Global test data for Crawailer JavaScript API testing
window.testData = {
framework: 'vue',
version: Vue.version,
// Component analysis
getComponentInfo: () => {
const app = document.querySelector('#app');
const inputs = app.querySelectorAll('input');
const buttons = app.querySelectorAll('button');
const reactiveElements = app.querySelectorAll('[data-testid]');
return {
totalInputs: inputs.length,
totalButtons: buttons.length,
testableElements: reactiveElements.length,
hasVueDevtools: typeof window.__VUE_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined'
};
},
// State access
getAppState: () => {
// Access Vue app instance data
const appInstance = document.querySelector('#app').__vueParentComponent;
if (appInstance) {
return {
userState: appInstance.setupState?.user,
todosCount: appInstance.setupState?.todos?.length || 0,
counterValue: appInstance.setupState?.counter || 0,
isLoading: appInstance.setupState?.isLoading || false
};
}
return { error: 'Could not access Vue app state' };
},
// User interaction simulation
simulateUserAction: async (action) => {
const actions = {
'add-todo': () => {
const input = document.querySelector('[data-testid="todo-input"]');
const button = document.querySelector('[data-testid="add-todo-btn"]');
input.value = `Test todo ${Date.now()}`;
input.dispatchEvent(new Event('input'));
button.click();
return 'Todo added';
},
'increment-counter': () => {
document.querySelector('[data-testid="increment-btn"]').click();
return 'Counter incremented';
},
'change-theme': () => {
const select = document.querySelector('[data-testid="theme-select"]');
select.value = 'dark';
select.dispatchEvent(new Event('change'));
return 'Theme changed to dark';
},
'fill-form': () => {
const nameInput = document.querySelector('[data-testid="name-input"]');
const emailInput = document.querySelector('[data-testid="email-input"]');
nameInput.value = 'Test User';
emailInput.value = 'test@example.com';
nameInput.dispatchEvent(new Event('input'));
emailInput.dispatchEvent(new Event('input'));
return 'Form filled';
},
'async-operation': async () => {
document.querySelector('[data-testid="async-btn"]').click();
// Wait for operation to complete
await new Promise(resolve => {
const checkComplete = () => {
const btn = document.querySelector('[data-testid="async-btn"]');
if (!btn.disabled) {
resolve();
} else {
setTimeout(checkComplete, 100);
}
};
checkComplete();
});
return 'Async operation completed';
}
};
if (actions[action]) {
return await actions[action]();
}
throw new Error(`Unknown action: ${action}`);
},
// Wait for Vue reactivity updates
waitForUpdate: async () => {
await Vue.nextTick();
return 'Vue reactivity updated';
},
// Get reactive data
getReactiveData: () => {
return {
totalCharacters: document.querySelector('#app').__vueParentComponent?.setupState?.totalCharacters || 0,
isValidEmail: document.querySelector('#app').__vueParentComponent?.setupState?.isValidEmail || false,
completedCount: document.querySelector('#app').__vueParentComponent?.setupState?.completedCount || 0,
filteredTodosCount: document.querySelector('#app').__vueParentComponent?.setupState?.filteredTodos?.length || 0
};
},
// Detect Vue-specific features
detectVueFeatures: () => {
return {
hasCompositionAPI: typeof Vue.ref !== 'undefined',
hasReactivity: typeof Vue.reactive !== 'undefined',
hasWatchers: typeof Vue.watch !== 'undefined',
hasComputed: typeof Vue.computed !== 'undefined',
hasLifecycleHooks: typeof Vue.onMounted !== 'undefined',
vueVersion: Vue.version,
isVue3: Vue.version.startsWith('3'),
hasDevtools: typeof window.__VUE_DEVTOOLS_GLOBAL_HOOK__ !== 'undefined'
};
},
// Performance measurement
measureReactivity: async () => {
const start = performance.now();
// Trigger multiple reactive updates
for (let i = 0; i < 100; i++) {
document.querySelector('[data-testid="increment-btn"]').click();
}
await Vue.nextTick();
const end = performance.now();
return {
updateTime: end - start,
updatesPerSecond: 100 / ((end - start) / 1000)
};
},
// Complex workflow simulation
simulateComplexWorkflow: async () => {
const steps = [];
// Step 1: Fill form
const nameInput = document.querySelector('[data-testid="name-input"]');
nameInput.value = 'Workflow Test User';
nameInput.dispatchEvent(new Event('input'));
steps.push('Form filled');
// Step 2: Add multiple todos
for (let i = 1; i <= 3; i++) {
const input = document.querySelector('[data-testid="todo-input"]');
input.value = `Workflow Task ${i}`;
input.dispatchEvent(new Event('input'));
document.querySelector('[data-testid="add-todo-btn"]').click();
}
steps.push('Multiple todos added');
// Step 3: Complete first todo
await Vue.nextTick();
const firstCheckbox = document.querySelector('[data-testid="todo-checkbox-4"]');
if (firstCheckbox) {
firstCheckbox.click();
steps.push('First todo completed');
}
// Step 4: Increment counter
for (let i = 0; i < 5; i++) {
document.querySelector('[data-testid="increment-btn"]').click();
}
steps.push('Counter incremented 5 times');
// Step 5: Change filter
document.querySelector('[data-testid="filter-completed"]').click();
steps.push('Filter changed to completed');
await Vue.nextTick();
return {
stepsCompleted: steps,
finalState: window.testData.getAppState()
};
}
};
// Global error handler for testing
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
window.lastError = {
message: event.error.message,
stack: event.error.stack,
timestamp: new Date().toISOString()
};
});
// Console log for debugging
console.log('Vue.js 3 Test Application loaded successfully');
console.log('Available test methods:', Object.keys(window.testData));
console.log('Vue version:', Vue.version);
</script>
</body>
</html>