mirror of
https://github.com/cloud-shuttle/leptos-shadcn-ui.git
synced 2025-12-22 22:00:00 +00:00
feat: Initial release v0.1.0 - 52 Leptos ShadCN UI components
This commit is contained in:
314
tests/e2e/README.md
Normal file
314
tests/e2e/README.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# Enhanced Playwright E2E Testing Infrastructure
|
||||
|
||||
This directory contains comprehensive end-to-end tests for the Leptos shadcn/ui components using Playwright.
|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
Our E2E testing infrastructure provides:
|
||||
|
||||
- **Comprehensive Component Testing**: Tests all 46 working Leptos components
|
||||
- **Accessibility Testing**: WCAG compliance and screen reader support
|
||||
- **Performance Testing**: Load times, memory usage, and interaction responsiveness
|
||||
- **Integration Testing**: How components work together
|
||||
- **Cross-Browser Testing**: Chrome, Firefox, Safari, and mobile viewports
|
||||
- **Visual Regression Testing**: Screenshot comparison on failures
|
||||
|
||||
## 🏗️ Test Structure
|
||||
|
||||
### Core Test Files
|
||||
|
||||
- **`leptos-components.spec.ts`** - Comprehensive testing of all Leptos components
|
||||
- **`accessibility.spec.ts`** - Accessibility compliance and WCAG testing
|
||||
- **`performance.spec.ts`** - Performance metrics and optimization testing
|
||||
- **`component-integration.spec.ts`** - Component interaction and integration testing
|
||||
|
||||
### Test Categories
|
||||
|
||||
#### 1. Core UI Components
|
||||
- Button variants and interactions
|
||||
- Input validation and user interaction
|
||||
- Label accessibility and associations
|
||||
- Card structure and responsiveness
|
||||
- Badge display and variants
|
||||
- Checkbox state management
|
||||
|
||||
#### 2. Layout and Navigation
|
||||
- Separator visual separation
|
||||
- Navigation menu structure
|
||||
- Breadcrumb navigation paths
|
||||
- Pagination controls
|
||||
|
||||
#### 3. Interactive Components
|
||||
- Dialog modal functionality
|
||||
- Dropdown menu expansion
|
||||
- Select option selection
|
||||
- Combobox search and selection
|
||||
|
||||
#### 4. Form Components
|
||||
- Form structure and validation
|
||||
- Textarea multi-line input
|
||||
- OTP input functionality
|
||||
|
||||
#### 5. Data Display
|
||||
- Table data presentation
|
||||
- Calendar date display
|
||||
- Progress indicators
|
||||
|
||||
#### 6. Feedback Components
|
||||
- Alert notifications
|
||||
- Toast messages
|
||||
- Tooltip hover information
|
||||
|
||||
## 🚀 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
make install-deps
|
||||
|
||||
# Install Playwright
|
||||
make install-playwright
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all E2E tests
|
||||
make test-e2e
|
||||
|
||||
# Run tests with UI (interactive)
|
||||
make test-e2e-ui
|
||||
|
||||
# Run tests in debug mode
|
||||
make test-e2e-debug
|
||||
|
||||
# Run specific test file
|
||||
make test-e2e-specific FILE=tests/e2e/accessibility.spec.ts
|
||||
|
||||
# Run tests in specific browser
|
||||
make test-e2e-browser BROWSER=chromium
|
||||
|
||||
# Run tests in parallel
|
||||
make test-e2e-parallel
|
||||
|
||||
# Generate test report
|
||||
make test-e2e-report
|
||||
```
|
||||
|
||||
### Development Workflow
|
||||
|
||||
```bash
|
||||
# Start the Leptos development server
|
||||
cd book-examples/leptos && trunk serve
|
||||
|
||||
# In another terminal, run tests
|
||||
make test-e2e
|
||||
|
||||
# Generate new test code interactively
|
||||
make test-e2e-codegen
|
||||
```
|
||||
|
||||
## 🧪 Test Configuration
|
||||
|
||||
### Playwright Config
|
||||
|
||||
Located in `playwright.config.ts`:
|
||||
|
||||
- **Browsers**: Chrome, Firefox, Safari, Mobile Chrome, Mobile Safari
|
||||
- **Base URL**: `http://127.0.0.1:8080` (Leptos dev server)
|
||||
- **Test Directory**: `./tests/e2e`
|
||||
- **Output Directory**: `test-results/`
|
||||
- **Screenshots**: On failure
|
||||
- **Videos**: On failure
|
||||
- **Traces**: On retry
|
||||
|
||||
### Test Timeouts
|
||||
|
||||
- **Test Timeout**: 30 seconds
|
||||
- **Expect Timeout**: 5 seconds
|
||||
- **Web Server Timeout**: 120 seconds
|
||||
|
||||
## 📊 Test Coverage
|
||||
|
||||
### Component Coverage
|
||||
|
||||
Our E2E tests cover **100% of working Leptos components**:
|
||||
|
||||
- ✅ **46/46 Components** - Fully tested with E2E scenarios
|
||||
- ✅ **Accessibility** - WCAG compliance and screen reader support
|
||||
- ✅ **Performance** - Load times, memory usage, responsiveness
|
||||
- ✅ **Integration** - Component interaction and state management
|
||||
- ✅ **Responsive** - Mobile, tablet, and desktop viewports
|
||||
|
||||
### Test Metrics
|
||||
|
||||
- **Total Test Cases**: 100+ comprehensive scenarios
|
||||
- **Accessibility Tests**: 25+ WCAG compliance checks
|
||||
- **Performance Tests**: 15+ performance metrics
|
||||
- **Integration Tests**: 20+ component interaction scenarios
|
||||
- **Cross-Browser**: 5 browser environments
|
||||
- **Viewport Testing**: 4 responsive breakpoints
|
||||
|
||||
## 🔍 Test Categories
|
||||
|
||||
### Accessibility Testing
|
||||
|
||||
- **ARIA Compliance**: Proper labels, roles, and states
|
||||
- **Keyboard Navigation**: Tab order and focus management
|
||||
- **Screen Reader Support**: Alt text, landmarks, and announcements
|
||||
- **Color and Contrast**: Visual accessibility
|
||||
- **Mobile Accessibility**: Touch targets and gesture alternatives
|
||||
|
||||
### Performance Testing
|
||||
|
||||
- **Page Load Performance**: Initial load under 3 seconds
|
||||
- **Time to Interactive**: Interactive elements ready under 2 seconds
|
||||
- **Memory Usage**: Stable memory consumption
|
||||
- **Component Rendering**: Fast render times
|
||||
- **Network Optimization**: Efficient resource loading
|
||||
|
||||
### Integration Testing
|
||||
|
||||
- **Form Workflows**: Complete form validation and submission
|
||||
- **Navigation Integration**: Menu, breadcrumb, and pagination
|
||||
- **Modal Integration**: Dialog with forms and content
|
||||
- **Data Display**: Tables with pagination, calendars with pickers
|
||||
- **State Management**: Component communication and persistence
|
||||
|
||||
## 🛠️ Writing New Tests
|
||||
|
||||
### Test Structure
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Component Name', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('http://127.0.0.1:8080');
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('should perform specific action', async ({ page }) => {
|
||||
// Test implementation
|
||||
const element = page.locator('selector');
|
||||
await expect(element).toBeVisible();
|
||||
|
||||
// Test interaction
|
||||
await element.click();
|
||||
|
||||
// Verify result
|
||||
await expect(page.locator('result')).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
1. **Use Descriptive Test Names**: Clear, action-oriented descriptions
|
||||
2. **Test User Flows**: Focus on real user interactions
|
||||
3. **Check Accessibility**: Verify ARIA attributes and keyboard support
|
||||
4. **Test Responsiveness**: Verify mobile and desktop behavior
|
||||
5. **Handle Async Operations**: Use proper wait conditions
|
||||
6. **Clean Up State**: Reset state between tests when needed
|
||||
|
||||
### Locator Strategies
|
||||
|
||||
```typescript
|
||||
// Prefer semantic selectors
|
||||
page.locator('[role="button"]')
|
||||
page.locator('[aria-label="Close"]')
|
||||
page.locator('button[type="submit"]')
|
||||
|
||||
// Use class-based selectors as fallback
|
||||
page.locator('[class*="button"]')
|
||||
page.locator('.form-input')
|
||||
|
||||
// Avoid brittle selectors
|
||||
page.locator('div:nth-child(3)') // ❌ Fragile
|
||||
page.locator('button:first') // ❌ Fragile
|
||||
```
|
||||
|
||||
## 📈 Continuous Integration
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
```yaml
|
||||
# Example GitHub Actions workflow
|
||||
- name: Run E2E Tests
|
||||
run: |
|
||||
make install-playwright
|
||||
make test-e2e
|
||||
env:
|
||||
CI: true
|
||||
```
|
||||
|
||||
### Test Reports
|
||||
|
||||
- **HTML Reports**: Interactive test results
|
||||
- **JSON Reports**: Machine-readable test data
|
||||
- **JUnit Reports**: CI/CD integration
|
||||
- **Screenshots**: Visual failure documentation
|
||||
- **Videos**: Action replay for debugging
|
||||
|
||||
## 🐛 Debugging Tests
|
||||
|
||||
### Debug Mode
|
||||
|
||||
```bash
|
||||
# Run tests in debug mode
|
||||
make test-e2e-debug
|
||||
|
||||
# Run specific test in debug
|
||||
pnpm playwright test --debug accessibility.spec.ts
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Timing Issues**: Use `waitForLoadState` and `waitForSelector`
|
||||
2. **Selector Changes**: Update selectors when components change
|
||||
3. **Async Operations**: Wait for network idle and animations
|
||||
4. **Viewport Issues**: Test across different screen sizes
|
||||
|
||||
### Debugging Tools
|
||||
|
||||
- **Playwright Inspector**: Interactive test debugging
|
||||
- **Trace Viewer**: Step-by-step test execution
|
||||
- **Screenshots**: Visual failure analysis
|
||||
- **Console Logs**: Browser console output
|
||||
|
||||
## 🎯 Future Enhancements
|
||||
|
||||
### Planned Features
|
||||
|
||||
- **Visual Regression Testing**: Automated screenshot comparison
|
||||
- **Performance Budgets**: Enforce performance thresholds
|
||||
- **Accessibility Audits**: Automated WCAG compliance checks
|
||||
- **Cross-Framework Testing**: Extend to other Rust web frameworks
|
||||
- **Mobile Device Testing**: Real device testing with Appium
|
||||
|
||||
### Integration Opportunities
|
||||
|
||||
- **Storybook Integration**: Component story testing
|
||||
- **Design System Testing**: Visual consistency validation
|
||||
- **API Testing**: Backend integration testing
|
||||
- **Load Testing**: Performance under stress
|
||||
|
||||
## 📚 Resources
|
||||
|
||||
### Documentation
|
||||
|
||||
- [Playwright Documentation](https://playwright.dev/)
|
||||
- [Testing Best Practices](https://playwright.dev/docs/best-practices)
|
||||
- [Accessibility Testing](https://playwright.dev/docs/accessibility-testing)
|
||||
- [Performance Testing](https://playwright.dev/docs/performance-testing)
|
||||
|
||||
### Community
|
||||
|
||||
- [Playwright Discord](https://discord.gg/playwright)
|
||||
- [GitHub Discussions](https://github.com/microsoft/playwright/discussions)
|
||||
- [Stack Overflow](https://stackoverflow.com/questions/tagged/playwright)
|
||||
|
||||
---
|
||||
|
||||
**🎉 This testing infrastructure ensures our Leptos components are production-ready, accessible, and performant across all platforms!**
|
||||
347
tests/e2e/accessibility.spec.ts
Normal file
347
tests/e2e/accessibility.spec.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Accessibility Testing Suite', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to Leptos example app for accessibility testing
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test.describe('ARIA Compliance', () => {
|
||||
test('all interactive elements have proper ARIA labels', async ({ page }) => {
|
||||
// Test buttons
|
||||
const buttons = page.locator('button');
|
||||
for (let i = 0; i < await buttons.count(); i++) {
|
||||
const button = buttons.nth(i);
|
||||
const ariaLabel = await button.getAttribute('aria-label');
|
||||
const ariaLabelledby = await button.getAttribute('aria-labelledby');
|
||||
const textContent = await button.textContent();
|
||||
|
||||
// Button should have either aria-label, aria-labelledby, or meaningful text
|
||||
expect(ariaLabel || ariaLabelledby || (textContent && textContent.trim().length > 0)).toBeTruthy();
|
||||
}
|
||||
|
||||
// Test inputs
|
||||
const inputs = page.locator('input[type="text"], input[type="email"], input[type="password"]');
|
||||
for (let i = 0; i < await inputs.count(); i++) {
|
||||
const input = inputs.nth(i);
|
||||
const ariaLabel = await input.getAttribute('aria-label');
|
||||
const ariaLabelledby = await input.getAttribute('aria-labelledby');
|
||||
const placeholder = await input.getAttribute('placeholder');
|
||||
|
||||
// Input should have accessibility information
|
||||
expect(ariaLabel || ariaLabelledby || placeholder).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('form elements have proper associations', async ({ page }) => {
|
||||
const forms = page.locator('form');
|
||||
for (let i = 0; i < await forms.count(); i++) {
|
||||
const form = forms.nth(i);
|
||||
|
||||
// Check form inputs
|
||||
const inputs = form.locator('input, select, textarea');
|
||||
for (let j = 0; j < await inputs.count(); j++) {
|
||||
const input = inputs.nth(j);
|
||||
const id = await input.getAttribute('id');
|
||||
const ariaLabel = await input.getAttribute('aria-label');
|
||||
const ariaLabelledby = await input.getAttribute('aria-labelledby');
|
||||
|
||||
// Input should have accessibility information
|
||||
expect(id || ariaLabel || ariaLabelledby).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('navigation elements have proper roles', async ({ page }) => {
|
||||
// Test navigation menus
|
||||
const navs = page.locator('nav');
|
||||
for (let i = 0; i < await navs.count(); i++) {
|
||||
const nav = navs.nth(i);
|
||||
const role = await nav.getAttribute('role');
|
||||
const ariaLabel = await nav.getAttribute('aria-label');
|
||||
|
||||
// Navigation should have proper labeling
|
||||
expect(role === 'navigation' || ariaLabel).toBeTruthy();
|
||||
}
|
||||
|
||||
// Test menu items
|
||||
const menuItems = page.locator('[role="menuitem"]');
|
||||
for (let i = 0; i < await menuItems.count(); i++) {
|
||||
const item = menuItems.nth(i);
|
||||
const ariaLabel = await item.getAttribute('aria-label');
|
||||
const textContent = await item.textContent();
|
||||
|
||||
// Menu item should have accessible text
|
||||
expect(ariaLabel || (textContent && textContent.trim().length > 0)).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Keyboard Navigation', () => {
|
||||
test('tab order is logical and complete', async ({ page }) => {
|
||||
// Get all focusable elements
|
||||
const focusableElements = page.locator('button, input, select, textarea, a, [tabindex]:not([tabindex="-1"])');
|
||||
const count = await focusableElements.count();
|
||||
|
||||
if (count > 0) {
|
||||
// Test tab navigation through all elements
|
||||
for (let i = 0; i < Math.min(count, 10); i++) {
|
||||
await page.keyboard.press('Tab');
|
||||
const focusedElement = page.locator(':focus');
|
||||
|
||||
if (await focusedElement.count() > 0) {
|
||||
await expect(focusedElement.first()).toBeVisible();
|
||||
|
||||
// Verify element is actually focusable
|
||||
const tagName = await focusedElement.first().evaluate(el => el.tagName.toLowerCase());
|
||||
const tabIndex = await focusedElement.first().getAttribute('tabindex');
|
||||
|
||||
// Element should be focusable
|
||||
expect(['button', 'input', 'select', 'textarea', 'a'].includes(tagName) || tabIndex !== '-1').toBeTruthy();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('enter and space keys work on interactive elements', async ({ page }) => {
|
||||
// Test buttons
|
||||
const buttons = page.locator('button');
|
||||
if (await buttons.count() > 0) {
|
||||
const button = buttons.first();
|
||||
await button.focus();
|
||||
|
||||
// Test space key
|
||||
await page.keyboard.press('Space');
|
||||
// Button should still be focused
|
||||
await expect(button).toBeFocused();
|
||||
|
||||
// Test enter key
|
||||
await page.keyboard.press('Enter');
|
||||
// Button should still be focused
|
||||
await expect(button).toBeFocused();
|
||||
}
|
||||
});
|
||||
|
||||
test('escape key closes modals and dropdowns', async ({ page }) => {
|
||||
// Test dialog triggers
|
||||
const dialogTriggers = page.locator('button[aria-haspopup="dialog"]');
|
||||
if (await dialogTriggers.count() > 0) {
|
||||
const trigger = dialogTriggers.first();
|
||||
await trigger.click();
|
||||
|
||||
// Look for dialog
|
||||
const dialog = page.locator('[role="dialog"]');
|
||||
if (await dialog.count() > 0) {
|
||||
await expect(dialog.first()).toBeVisible();
|
||||
|
||||
// Press escape
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
// Dialog should be closed or hidden
|
||||
const isVisible = await dialog.first().isVisible();
|
||||
expect(isVisible).toBeFalsy();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Screen Reader Support', () => {
|
||||
test('images have alt text or are decorative', async ({ page }) => {
|
||||
const images = page.locator('img');
|
||||
for (let i = 0; i < await images.count(); i++) {
|
||||
const img = images.nth(i);
|
||||
const alt = await img.getAttribute('alt');
|
||||
const ariaHidden = await img.getAttribute('aria-hidden');
|
||||
const role = await img.getAttribute('role');
|
||||
|
||||
// Image should have alt text, be marked as decorative, or have presentation role
|
||||
expect(alt || ariaHidden === 'true' || role === 'presentation').toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('decorative elements are properly hidden', async ({ page }) => {
|
||||
const decorativeElements = page.locator('[aria-hidden="true"], [role="presentation"]');
|
||||
for (let i = 0; i < await decorativeElements.count(); i++) {
|
||||
const element = decorativeElements.nth(i);
|
||||
|
||||
// Decorative elements should not have meaningful content
|
||||
const textContent = await element.textContent();
|
||||
const ariaLabel = await element.getAttribute('aria-label');
|
||||
|
||||
// Should not have both aria-hidden and meaningful content
|
||||
expect(!(ariaLabel && ariaLabel.trim().length > 0) || !(textContent && textContent.trim().length > 0)).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('landmarks are properly defined', async ({ page }) => {
|
||||
// Test main landmark
|
||||
const main = page.locator('main, [role="main"]');
|
||||
if (await main.count() > 0) {
|
||||
await expect(main.first()).toBeVisible();
|
||||
}
|
||||
|
||||
// Test navigation landmarks
|
||||
const navs = page.locator('nav, [role="navigation"]');
|
||||
for (let i = 0; i < await navs.count(); i++) {
|
||||
const nav = navs.nth(i);
|
||||
const ariaLabel = await nav.getAttribute('aria-label');
|
||||
|
||||
// Navigation should have descriptive label
|
||||
expect(ariaLabel && ariaLabel.trim().length > 0).toBeTruthy();
|
||||
}
|
||||
|
||||
// Test complementary landmarks
|
||||
const complementary = page.locator('aside, [role="complementary"]');
|
||||
for (let i = 0; i < await complementary.count(); i++) {
|
||||
const comp = complementary.nth(i);
|
||||
const ariaLabel = await comp.getAttribute('aria-label');
|
||||
|
||||
// Complementary content should have descriptive label
|
||||
expect(ariaLabel && ariaLabel.trim().length > 0).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Color and Contrast', () => {
|
||||
test('text elements have sufficient contrast', async ({ page }) => {
|
||||
// This is a basic check - full contrast testing would require visual testing tools
|
||||
const textElements = page.locator('p, h1, h2, h3, h4, h5, h6, span, div, label');
|
||||
|
||||
if (await textElements.count() > 0) {
|
||||
// Check that text elements are visible
|
||||
for (let i = 0; i < Math.min(await textElements.count(), 5); i++) {
|
||||
const element = textElements.nth(i);
|
||||
await expect(element).toBeVisible();
|
||||
|
||||
// Check that text has content
|
||||
const text = await element.textContent();
|
||||
if (text && text.trim().length > 0) {
|
||||
expect(text.trim().length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('focus indicators are visible', async ({ page }) => {
|
||||
// Test focus visibility
|
||||
const focusableElements = page.locator('button, input, select, textarea, a');
|
||||
if (await focusableElements.count() > 0) {
|
||||
const element = focusableElements.first();
|
||||
await element.focus();
|
||||
|
||||
// Element should be focused
|
||||
await expect(element).toBeFocused();
|
||||
|
||||
// Check for focus indicator (outline, border, etc.)
|
||||
const outline = await element.evaluate(el => window.getComputedStyle(el).outline);
|
||||
const border = await element.evaluate(el => window.getComputedStyle(el).border);
|
||||
|
||||
// Should have some form of focus indicator
|
||||
expect(outline !== 'none' || border !== 'none').toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Dynamic Content', () => {
|
||||
test('loading states are announced to screen readers', async ({ page }) => {
|
||||
// Look for loading indicators
|
||||
const loadingElements = page.locator('[aria-busy="true"], [role="progressbar"], [aria-live="polite"]');
|
||||
|
||||
if (await loadingElements.count() > 0) {
|
||||
for (let i = 0; i < await loadingElements.count(); i++) {
|
||||
const element = loadingElements.nth(i);
|
||||
await expect(element).toBeVisible();
|
||||
|
||||
// Check for proper ARIA attributes
|
||||
const ariaBusy = await element.getAttribute('aria-busy');
|
||||
const ariaLive = await element.getAttribute('aria-live');
|
||||
const role = await element.getAttribute('role');
|
||||
|
||||
// Should have appropriate ARIA attributes
|
||||
expect(ariaBusy === 'true' || ariaLive || role === 'progressbar').toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('error messages are properly announced', async ({ page }) => {
|
||||
// Look for error elements
|
||||
const errorElements = page.locator('[role="alert"], [aria-invalid="true"], .error, [class*="error"]');
|
||||
|
||||
if (await errorElements.count() > 0) {
|
||||
for (let i = 0; i < await errorElements.count(); i++) {
|
||||
const element = errorElements.nth(i);
|
||||
await expect(element).toBeVisible();
|
||||
|
||||
// Check for proper error attributes
|
||||
const role = await element.getAttribute('role');
|
||||
const ariaInvalid = await element.getAttribute('aria-invalid');
|
||||
|
||||
// Should have error-related attributes
|
||||
expect(role === 'alert' || ariaInvalid === 'true').toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('status updates are announced', async ({ page }) => {
|
||||
// Look for status elements
|
||||
const statusElements = page.locator('[role="status"], [aria-live="polite"], [aria-live="assertive"]');
|
||||
|
||||
if (await statusElements.count() > 0) {
|
||||
for (let i = 0; i < await statusElements.count(); i++) {
|
||||
const element = statusElements.nth(i);
|
||||
await expect(element).toBeVisible();
|
||||
|
||||
// Check for proper status attributes
|
||||
const role = await element.getAttribute('role');
|
||||
const ariaLive = await element.getAttribute('aria-live');
|
||||
|
||||
// Should have status-related attributes
|
||||
expect(role === 'status' || ariaLive).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Mobile Accessibility', () => {
|
||||
test('touch targets are appropriately sized', async ({ page }) => {
|
||||
// Set mobile viewport
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
const interactiveElements = page.locator('button, input, select, textarea, a');
|
||||
if (await interactiveElements.count() > 0) {
|
||||
for (let i = 0; i < Math.min(await interactiveElements.count(), 5); i++) {
|
||||
const element = interactiveElements.nth(i);
|
||||
|
||||
// Get element dimensions
|
||||
const boundingBox = await element.boundingBox();
|
||||
if (boundingBox) {
|
||||
// Touch targets should be at least 44x44 pixels
|
||||
expect(boundingBox.width).toBeGreaterThanOrEqual(44);
|
||||
expect(boundingBox.height).toBeGreaterThanOrEqual(44);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('gesture alternatives are available', async ({ page }) => {
|
||||
// Set mobile viewport
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
// Look for elements that might require gestures
|
||||
const gestureElements = page.locator('[class*="swipe"], [class*="drag"], [class*="pinch"]');
|
||||
|
||||
if (await gestureElements.count() > 0) {
|
||||
for (let i = 0; i < await gestureElements.count(); i++) {
|
||||
const element = gestureElements.nth(i);
|
||||
|
||||
// Check for alternative controls
|
||||
const alternativeControls = element.locator('button, input, select');
|
||||
if (await alternativeControls.count() > 0) {
|
||||
await expect(alternativeControls.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
401
tests/e2e/bundle-optimization.spec.ts
Normal file
401
tests/e2e/bundle-optimization.spec.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Bundle Optimization & Performance - Comprehensive Testing', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to the enhanced lazy loading demo
|
||||
await page.goto('/');
|
||||
// Wait for the app to be fully loaded
|
||||
await page.waitForLoadState('networkidle');
|
||||
// Wait for WASM to initialize
|
||||
await page.waitForFunction(() => window.wasmBindings !== undefined);
|
||||
});
|
||||
|
||||
test.describe('Bundle Size Analysis', () => {
|
||||
test('should display accurate bundle size information', async ({ page }) => {
|
||||
const bundlePanel = page.locator('.panel.bundle-analysis');
|
||||
await expect(bundlePanel).toBeVisible();
|
||||
|
||||
// Bundle size should be displayed with proper units
|
||||
const sizeText = await bundlePanel.locator('text=/Bundle Size:.*/').textContent();
|
||||
expect(sizeText).toMatch(/Bundle Size:.*\d+\.\d+MB/);
|
||||
|
||||
// Should show reasonable bundle size (not 0 or extremely large)
|
||||
const sizeMatch = sizeText!.match(/Bundle Size: ([\d.]+)MB/);
|
||||
if (sizeMatch) {
|
||||
const sizeInMB = parseFloat(sizeMatch[1]);
|
||||
expect(sizeInMB).toBeGreaterThan(0.1); // At least 100KB
|
||||
expect(sizeInMB).toBeLessThan(10); // Less than 10MB
|
||||
}
|
||||
});
|
||||
|
||||
test('should show component count breakdown', async ({ page }) => {
|
||||
const bundlePanel = page.locator('.panel.bundle-analysis');
|
||||
|
||||
// Component count should be accurate
|
||||
const componentText = await bundlePanel.locator('text=/Components:.*/').textContent();
|
||||
expect(componentText).toMatch(/Components:.*\d+/);
|
||||
|
||||
// Should show the correct total (5 essential + 39 lazy + 5 dynamic = 49)
|
||||
const countMatch = componentText!.match(/Components: (\d+)/);
|
||||
if (countMatch) {
|
||||
const count = parseInt(countMatch[1]);
|
||||
expect(count).toBeGreaterThanOrEqual(40); // At least 40 components
|
||||
expect(count).toBeLessThanOrEqual(60); // Reasonable upper bound
|
||||
}
|
||||
});
|
||||
|
||||
test('should display optimization status', async ({ page }) => {
|
||||
const bundlePanel = page.locator('.panel.bundle-analysis');
|
||||
|
||||
// Optimization status should be visible
|
||||
await expect(bundlePanel.locator('text=/Optimization:/')).toBeVisible();
|
||||
|
||||
// Should show some optimization information
|
||||
const optimizationText = await bundlePanel.locator('text=/Optimization:.*/').textContent();
|
||||
expect(optimizationText).toBeTruthy();
|
||||
expect(optimizationText!.length).toBeGreaterThan(10);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('WASM Loading Performance', () => {
|
||||
test('should measure initial WASM load time', async ({ page }) => {
|
||||
// Navigate to page and measure load time
|
||||
const startTime = Date.now();
|
||||
await page.goto('/');
|
||||
await page.waitForFunction(() => window.wasmBindings !== undefined);
|
||||
const loadTime = Date.now() - startTime;
|
||||
|
||||
// Initial load should be reasonable (under 10 seconds)
|
||||
expect(loadTime).toBeLessThan(10000);
|
||||
|
||||
// Log the load time for monitoring
|
||||
console.log(`Initial WASM load time: ${loadTime}ms`);
|
||||
});
|
||||
|
||||
test('should handle WASM initialization gracefully', async ({ page }) => {
|
||||
// Check that WASM bindings are properly initialized
|
||||
const wasmBindings = await page.evaluate(() => window.wasmBindings);
|
||||
expect(wasmBindings).toBeDefined();
|
||||
|
||||
// Check that the app is interactive after WASM load
|
||||
await expect(page.locator('.load-component-btn').first()).toBeEnabled();
|
||||
|
||||
// No loading errors should be visible
|
||||
const errorElements = page.locator('.error:visible');
|
||||
await expect(errorElements).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should maintain performance during component loading', async ({ page }) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// Load multiple components simultaneously
|
||||
const components = page.locator('.dynamic-component-wrapper');
|
||||
const loadButtons = components.locator('.load-component-btn');
|
||||
|
||||
// Load first 3 components
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const loadBtn = loadButtons.nth(i);
|
||||
await loadBtn.click();
|
||||
}
|
||||
|
||||
// Wait for all to complete
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const component = components.nth(i);
|
||||
await expect(component.locator('.component-success')).toBeVisible({ timeout: 15000 });
|
||||
}
|
||||
|
||||
const totalTime = Date.now() - startTime;
|
||||
|
||||
// Loading 3 components should be reasonable (under 20 seconds)
|
||||
expect(totalTime).toBeLessThan(20000);
|
||||
|
||||
// Page should remain responsive
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
|
||||
console.log(`Loading 3 components took: ${totalTime}ms`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Memory Usage & Resource Management', () => {
|
||||
test('should not cause memory leaks during component loading', async ({ page }) => {
|
||||
// Load and unload components multiple times
|
||||
const components = page.locator('.dynamic-component-wrapper');
|
||||
const loadButtons = components.locator('.load-component-btn');
|
||||
|
||||
for (let cycle = 0; cycle < 3; cycle++) {
|
||||
// Load first component
|
||||
const loadBtn = loadButtons.first();
|
||||
await loadBtn.click();
|
||||
|
||||
// Wait for loading to complete
|
||||
const component = components.first();
|
||||
await expect(component.locator('.component-success')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify component is loaded
|
||||
await expect(component.locator('.component-content')).toBeVisible();
|
||||
|
||||
// Small delay to simulate user interaction
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Page should still be responsive after multiple load cycles
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
await expect(loadButtons.first()).toBeEnabled();
|
||||
});
|
||||
|
||||
test('should handle rapid component loading requests', async ({ page }) => {
|
||||
const components = page.locator('.dynamic-component-wrapper');
|
||||
const loadButtons = components.locator('.load-component-btn');
|
||||
|
||||
// Rapidly click multiple load buttons
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const loadBtn = loadButtons.nth(i);
|
||||
await loadBtn.click();
|
||||
await page.waitForTimeout(100); // Small delay between clicks
|
||||
}
|
||||
|
||||
// All components should eventually load successfully
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const component = components.nth(i);
|
||||
await expect(component.locator('.component-success')).toBeVisible({ timeout: 20000 });
|
||||
}
|
||||
|
||||
// Page should remain stable
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Bundle Optimization Features', () => {
|
||||
test('should implement proper code splitting', async ({ page }) => {
|
||||
// Check that not all components are loaded initially
|
||||
const lazyComponents = page.locator('.lazy-component-wrapper');
|
||||
const dynamicComponents = page.locator('.dynamic-component-wrapper');
|
||||
|
||||
// Lazy components should start in placeholder state
|
||||
for (let i = 0; i < await lazyComponents.count(); i++) {
|
||||
const component = lazyComponents.nth(i);
|
||||
await expect(component.locator('.component-placeholder')).toBeVisible();
|
||||
await expect(component.locator('.component-content')).not.toBeVisible();
|
||||
}
|
||||
|
||||
// Dynamic components should also start in placeholder state
|
||||
for (let i = 0; i < await dynamicComponents.count(); i++) {
|
||||
const component = dynamicComponents.nth(i);
|
||||
await expect(component.locator('.component-placeholder')).toBeVisible();
|
||||
await expect(component.locator('.component-content')).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should load components on demand', async ({ page }) => {
|
||||
const lazySection = page.locator('h3:has-text("Lazy Loaded Components")').locator('..');
|
||||
const firstComponent = lazySection.locator('.lazy-component-wrapper').first();
|
||||
|
||||
// Initially should show placeholder
|
||||
await expect(firstComponent.locator('.component-placeholder')).toBeVisible();
|
||||
|
||||
// Click load button
|
||||
const loadBtn = firstComponent.locator('.load-component-btn');
|
||||
await loadBtn.click();
|
||||
|
||||
// Should show loading state
|
||||
await expect(firstComponent.locator('.component-loading')).toBeVisible();
|
||||
|
||||
// Should eventually show component content
|
||||
await expect(firstComponent.locator('.component-content')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Placeholder should be hidden
|
||||
await expect(firstComponent.locator('.component-placeholder')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should maintain essential components always loaded', async ({ page }) => {
|
||||
const essentialSection = page.locator('h3:has-text("Essential Components")').locator('..');
|
||||
const essentialComponents = essentialSection.locator('.component-item');
|
||||
|
||||
// Essential components should be immediately visible
|
||||
for (let i = 0; i < await essentialComponents.count(); i++) {
|
||||
const component = essentialComponents.nth(i);
|
||||
await expect(component).toBeVisible();
|
||||
await expect(component).not.toHaveClass(/loading/);
|
||||
await expect(component).not.toHaveClass(/placeholder/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Performance Metrics & Monitoring', () => {
|
||||
test('should display real-time loading statistics', async ({ page }) => {
|
||||
const loaderPanel = page.locator('.panel.bundle-status');
|
||||
|
||||
// Should show initial stats
|
||||
await expect(loaderPanel.locator('text=Loaded: 0')).toBeVisible();
|
||||
await expect(loaderPanel.locator('text=Total Size: 0KB')).toBeVisible();
|
||||
|
||||
// Load a component and verify stats update
|
||||
const loadBtn = page.locator('.load-btn');
|
||||
await loadBtn.click();
|
||||
|
||||
// Should show loading progress
|
||||
await expect(loaderPanel.locator('.status-value.loading')).toBeVisible();
|
||||
|
||||
// Wait for completion and verify stats
|
||||
await expect(loaderPanel.locator('text=Loaded: 1')).toBeVisible({ timeout: 10000 });
|
||||
await expect(loaderPanel.locator('text=/Total Size:.*KB/')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should track component loading progress', async ({ page }) => {
|
||||
const loaderPanel = page.locator('.panel.bundle-status');
|
||||
|
||||
// Load test component
|
||||
const loadBtn = page.locator('.load-btn');
|
||||
await loadBtn.click();
|
||||
|
||||
// Should show progress indicator
|
||||
const progressElement = loaderPanel.locator('.status-value.loading');
|
||||
await expect(progressElement).toBeVisible();
|
||||
|
||||
// Progress should eventually complete
|
||||
await expect(loaderPanel.locator('text=Loaded: 1')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should provide detailed loading information', async ({ page }) => {
|
||||
const loaderPanel = page.locator('.panel.bundle-status');
|
||||
|
||||
// Toggle details to show more information
|
||||
const toggleBtn = page.locator('.toggle-btn');
|
||||
await toggleBtn.click();
|
||||
|
||||
// Details should be visible
|
||||
const detailsContent = loaderPanel.locator('.details-content');
|
||||
await expect(detailsContent).not.toHaveClass(/hidden/);
|
||||
|
||||
// Should show implementation details
|
||||
await expect(loaderPanel.locator('.implementation-note')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Error Handling & Resilience', () => {
|
||||
test('should handle component loading failures gracefully', async ({ page }) => {
|
||||
// This test verifies error handling infrastructure
|
||||
// Actual error simulation would require mocking
|
||||
|
||||
// Error display elements should be available
|
||||
const errorDisplay = page.locator('.error-display');
|
||||
await expect(errorDisplay).toBeAttached();
|
||||
|
||||
// Clear error button should be available
|
||||
const clearErrorBtn = page.locator('.clear-error-btn');
|
||||
await expect(clearErrorBtn).toBeAttached();
|
||||
});
|
||||
|
||||
test('should provide retry mechanisms', async ({ page }) => {
|
||||
// Retry buttons should be available on components
|
||||
const retryBtns = page.locator('.retry-btn');
|
||||
|
||||
// Initially no retry buttons should be visible (no errors)
|
||||
await expect(retryBtns.filter({ hasText: /retry/i })).toHaveCount(0);
|
||||
|
||||
// But retry infrastructure should be in place
|
||||
await expect(retryBtns).toBeAttached();
|
||||
});
|
||||
|
||||
test('should maintain system stability during errors', async ({ page }) => {
|
||||
// Load multiple components to stress test
|
||||
const components = page.locator('.dynamic-component-wrapper');
|
||||
const loadButtons = components.locator('.load-component-btn');
|
||||
|
||||
// Load several components
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const loadBtn = loadButtons.nth(i);
|
||||
await loadBtn.click();
|
||||
}
|
||||
|
||||
// Wait for completion
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const component = components.nth(i);
|
||||
await expect(component.locator('.component-success')).toBeVisible({ timeout: 15000 });
|
||||
}
|
||||
|
||||
// System should remain stable
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
await expect(loadButtons.first()).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Cross-Browser Compatibility', () => {
|
||||
test('should work consistently across different viewports', async ({ page }) => {
|
||||
// Test desktop viewport
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
await expect(page.locator('.panel.bundle-analysis')).toBeVisible();
|
||||
|
||||
// Test tablet viewport
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
await expect(page.locator('.panel.bundle-analysis')).toBeVisible();
|
||||
|
||||
// Test mobile viewport
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
await expect(page.locator('.panel.bundle-analysis')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should maintain functionality across viewport changes', async ({ page }) => {
|
||||
// Load a component in desktop view
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
const loadBtn = page.locator('.load-btn');
|
||||
await loadBtn.click();
|
||||
|
||||
// Switch to mobile view during loading
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
// Loading should continue and complete
|
||||
await expect(page.locator('text=Loaded: 1')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Component should be properly displayed in mobile view
|
||||
await expect(page.locator('.panel.bundle-status')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Integration Testing', () => {
|
||||
test('should integrate all optimization features seamlessly', async ({ page }) => {
|
||||
// Test bundle analysis
|
||||
const bundlePanel = page.locator('.panel.bundle-analysis');
|
||||
await expect(bundlePanel).toBeVisible();
|
||||
|
||||
// Test dynamic loader
|
||||
const loaderPanel = page.locator('.panel.bundle-status');
|
||||
await expect(loaderPanel).toBeVisible();
|
||||
|
||||
// Test essential components
|
||||
await expect(page.locator('h3:has-text("Essential Components")')).toBeVisible();
|
||||
|
||||
// Test lazy loading
|
||||
await expect(page.locator('h3:has-text("Lazy Loaded Components")')).toBeVisible();
|
||||
|
||||
// Test dynamic components
|
||||
await expect(page.locator('h3:has-text("Dynamic WASM Components")')).toBeVisible();
|
||||
|
||||
// All sections should work together
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should provide consistent user experience', async ({ page }) => {
|
||||
// Navigate through different sections
|
||||
const sections = [
|
||||
'Essential Components',
|
||||
'Lazy Loaded Components',
|
||||
'Dynamic WASM Components'
|
||||
];
|
||||
|
||||
for (const section of sections) {
|
||||
await expect(page.locator(`h3:has-text("${section}")`)).toBeVisible();
|
||||
|
||||
// Each section should be properly styled and functional
|
||||
const sectionElement = page.locator(`h3:has-text("${section}")`).locator('..');
|
||||
await expect(sectionElement).toBeVisible();
|
||||
}
|
||||
|
||||
// Overall layout should be consistent
|
||||
await expect(page.locator('.container')).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
494
tests/e2e/component-integration.spec.ts
Normal file
494
tests/e2e/component-integration.spec.ts
Normal file
@@ -0,0 +1,494 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Leptos Component Integration Testing Suite', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to Leptos example app
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test.describe('Form Component Integration', () => {
|
||||
test('complete form workflow with validation', async ({ page }) => {
|
||||
// Look for forms
|
||||
const forms = page.locator('form');
|
||||
|
||||
if (await forms.count() > 0) {
|
||||
const form = forms.first();
|
||||
|
||||
// Test form inputs
|
||||
const inputs = form.locator('input[type="text"], input[type="email"], input[type="password"]');
|
||||
if (await inputs.count() > 0) {
|
||||
// Fill out form
|
||||
for (let i = 0; i < await inputs.count(); i++) {
|
||||
const input = inputs.nth(i);
|
||||
const type = await input.getAttribute('type');
|
||||
|
||||
if (type === 'email') {
|
||||
await input.fill('test@example.com');
|
||||
} else if (type === 'password') {
|
||||
await input.fill('password123');
|
||||
} else {
|
||||
await input.fill(`Test input ${i + 1}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Test form submission
|
||||
const submitButton = form.locator('button[type="submit"], input[type="submit"]');
|
||||
if (await submitButton.count() > 0) {
|
||||
await expect(submitButton.first()).toBeVisible();
|
||||
await expect(submitButton.first()).toBeEnabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('form validation integration', async ({ page }) => {
|
||||
const inputs = page.locator('input[type="text"], input[type="email"]');
|
||||
|
||||
if (await inputs.count() > 0) {
|
||||
const input = inputs.first();
|
||||
|
||||
// Test invalid input
|
||||
await input.fill('invalid-email');
|
||||
await input.blur();
|
||||
|
||||
// Look for validation messages
|
||||
const validationMessages = page.locator('[role="alert"], .error, [class*="error"], [aria-invalid="true"]');
|
||||
if (await validationMessages.count() > 0) {
|
||||
await expect(validationMessages.first()).toBeVisible();
|
||||
}
|
||||
|
||||
// Test valid input
|
||||
await input.clear();
|
||||
await input.fill('valid@email.com');
|
||||
await input.blur();
|
||||
|
||||
// Validation messages should be cleared or hidden
|
||||
const remainingErrors = page.locator('[role="alert"], .error, [class*="error"]');
|
||||
if (await remainingErrors.count() > 0) {
|
||||
// Some validation might remain for other fields
|
||||
console.log(`Remaining validation messages: ${await remainingErrors.count()}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Navigation Component Integration', () => {
|
||||
test('navigation menu with dropdown integration', async ({ page }) => {
|
||||
// Test navigation structure
|
||||
const navs = page.locator('nav, [role="navigation"]');
|
||||
|
||||
if (await navs.count() > 0) {
|
||||
const nav = navs.first();
|
||||
|
||||
// Test navigation items
|
||||
const navItems = nav.locator('a, button, [role="menuitem"]');
|
||||
if (await navItems.count() > 0) {
|
||||
for (let i = 0; i < Math.min(await navItems.count(), 3); i++) {
|
||||
const item = navItems.nth(i);
|
||||
await expect(item).toBeVisible();
|
||||
|
||||
// Test if item has dropdown
|
||||
const hasDropdown = await item.getAttribute('aria-haspopup');
|
||||
if (hasDropdown === 'true') {
|
||||
await item.click();
|
||||
|
||||
// Look for dropdown content
|
||||
const dropdown = page.locator('[role="menu"], [data-state="open"]');
|
||||
if (await dropdown.count() > 0) {
|
||||
await expect(dropdown.first()).toBeVisible();
|
||||
|
||||
// Test dropdown items
|
||||
const dropdownItems = dropdown.locator('[role="menuitem"], a, button');
|
||||
if (await dropdownItems.count() > 0) {
|
||||
await expect(dropdownItems.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('breadcrumb navigation integration', async ({ page }) => {
|
||||
const breadcrumbs = page.locator('[class*="breadcrumb"], .breadcrumb, nav[aria-label*="breadcrumb"]');
|
||||
|
||||
if (await breadcrumbs.count() > 0) {
|
||||
const breadcrumb = breadcrumbs.first();
|
||||
|
||||
// Test breadcrumb structure
|
||||
const items = breadcrumb.locator('a, span, [class*="breadcrumb-item"]');
|
||||
if (await items.count() > 0) {
|
||||
// First item should be visible
|
||||
await expect(items.first()).toBeVisible();
|
||||
|
||||
// Test navigation through breadcrumbs
|
||||
const links = breadcrumb.locator('a');
|
||||
if (await links.count() > 0) {
|
||||
for (let i = 0; i < Math.min(await links.count(), 2); i++) {
|
||||
const link = links.nth(i);
|
||||
await expect(link).toBeVisible();
|
||||
|
||||
// Test link functionality
|
||||
const href = await link.getAttribute('href');
|
||||
if (href && href !== '#') {
|
||||
// This would navigate away, so we'll just verify the link exists
|
||||
expect(href).toBeTruthy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Modal and Dialog Integration', () => {
|
||||
test('modal with form integration', async ({ page }) => {
|
||||
const modalTriggers = page.locator('button[aria-haspopup="dialog"]');
|
||||
|
||||
if (await modalTriggers.count() > 0) {
|
||||
const trigger = modalTriggers.first();
|
||||
await trigger.click();
|
||||
|
||||
// Look for modal
|
||||
const modal = page.locator('[role="dialog"]');
|
||||
if (await modal.count() > 0) {
|
||||
await expect(modal.first()).toBeVisible();
|
||||
|
||||
// Test modal content
|
||||
const modalContent = modal.first();
|
||||
|
||||
// Look for forms in modal
|
||||
const modalForms = modalContent.locator('form');
|
||||
if (await modalForms.count() > 0) {
|
||||
const form = modalForms.first();
|
||||
|
||||
// Test form inputs in modal
|
||||
const inputs = form.locator('input, select, textarea');
|
||||
if (await inputs.count() > 0) {
|
||||
for (let i = 0; i < Math.min(await inputs.count(), 2); i++) {
|
||||
const input = inputs.nth(i);
|
||||
await expect(input).toBeVisible();
|
||||
|
||||
// Test input interaction
|
||||
if (await input.getAttribute('type') === 'text') {
|
||||
await input.fill('Modal test input');
|
||||
await expect(input).toHaveValue('Modal test input');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test modal close
|
||||
const closeButton = modalContent.locator('button[aria-label*="close"], button[aria-label*="Close"], [data-state="closed"]');
|
||||
if (await closeButton.count() > 0) {
|
||||
await closeButton.first().click();
|
||||
|
||||
// Modal should be closed
|
||||
const isVisible = await modal.first().isVisible();
|
||||
expect(isVisible).toBeFalsy();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('dropdown with search integration', async ({ page }) => {
|
||||
const dropdownTriggers = page.locator('button[aria-haspopup="true"]');
|
||||
|
||||
if (await dropdownTriggers.count() > 0) {
|
||||
const trigger = dropdownTriggers.first();
|
||||
await trigger.click();
|
||||
|
||||
// Look for dropdown
|
||||
const dropdown = page.locator('[role="menu"], [data-state="open"]');
|
||||
if (await dropdown.count() > 0) {
|
||||
await expect(dropdown.first()).toBeVisible();
|
||||
|
||||
// Test search functionality in dropdown
|
||||
const searchInput = dropdown.first().locator('input[type="search"], input[placeholder*="search"], input[placeholder*="Search"]');
|
||||
if (await searchInput.count() > 0) {
|
||||
const search = searchInput.first();
|
||||
await expect(search).toBeVisible();
|
||||
|
||||
// Test search input
|
||||
await search.fill('test search');
|
||||
await expect(search).toHaveValue('test search');
|
||||
}
|
||||
|
||||
// Test dropdown items
|
||||
const dropdownItems = dropdown.first().locator('[role="menuitem"], a, button');
|
||||
if (await dropdownItems.count() > 0) {
|
||||
await expect(dropdownItems.first()).toBeVisible();
|
||||
|
||||
// Test item selection
|
||||
await dropdownItems.first().click();
|
||||
|
||||
// Dropdown should close after selection
|
||||
const isVisible = await dropdown.first().isVisible();
|
||||
expect(isVisible).toBeFalsy();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Data Display Integration', () => {
|
||||
test('table with pagination integration', async ({ page }) => {
|
||||
const tables = page.locator('table, [class*="table"]');
|
||||
|
||||
if (await tables.count() > 0) {
|
||||
const table = tables.first();
|
||||
|
||||
// Test table structure
|
||||
const rows = table.locator('tr');
|
||||
if (await rows.count() > 0) {
|
||||
await expect(rows.first()).toBeVisible();
|
||||
|
||||
// Look for pagination
|
||||
const pagination = page.locator('[class*="pagination"], .pagination, nav[aria-label*="pagination"]');
|
||||
if (await pagination.count() > 0) {
|
||||
await expect(pagination.first()).toBeVisible();
|
||||
|
||||
// Test pagination controls
|
||||
const paginationControls = pagination.first().locator('button, a, [class*="page"]');
|
||||
if (await paginationControls.count() > 0) {
|
||||
await expect(paginationControls.first()).toBeVisible();
|
||||
|
||||
// Test pagination navigation
|
||||
const nextButton = pagination.first().locator('button[aria-label*="next"], button[aria-label*="Next"]');
|
||||
if (await nextButton.count() > 0) {
|
||||
await expect(nextButton.first()).toBeVisible();
|
||||
await expect(nextButton.first()).toBeEnabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('calendar with date picker integration', async ({ page }) => {
|
||||
const datePickers = page.locator('[class*="date-picker"], [class*="calendar"]');
|
||||
|
||||
if (await datePickers.count() > 0) {
|
||||
const datePicker = datePickers.first();
|
||||
|
||||
// Test date picker trigger
|
||||
const trigger = datePicker.locator('button, input[type="text"]');
|
||||
if (await trigger.count() > 0) {
|
||||
await trigger.first().click();
|
||||
|
||||
// Look for calendar
|
||||
const calendar = page.locator('[class*="calendar"], [role="grid"]');
|
||||
if (await calendar.count() > 0) {
|
||||
await expect(calendar.first()).toBeVisible();
|
||||
|
||||
// Test calendar navigation
|
||||
const prevButton = calendar.first().locator('button[aria-label*="previous"], button[aria-label*="Previous"]');
|
||||
const nextButton = calendar.first().locator('button[aria-label*="next"], button[aria-label*="Next"]');
|
||||
|
||||
if (await prevButton.count() > 0) {
|
||||
await expect(prevButton.first()).toBeVisible();
|
||||
}
|
||||
|
||||
if (await nextButton.count() > 0) {
|
||||
await expect(nextButton.first()).toBeVisible();
|
||||
}
|
||||
|
||||
// Test date selection
|
||||
const dateCells = calendar.first().locator('[class*="day"], [class*="date"], td[role="gridcell"]');
|
||||
if (await dateCells.count() > 0) {
|
||||
// Find a selectable date (not disabled)
|
||||
for (let i = 0; i < await dateCells.count(); i++) {
|
||||
const cell = dateCells.nth(i);
|
||||
const isDisabled = await cell.getAttribute('aria-disabled') === 'true' ||
|
||||
await cell.getAttribute('disabled') !== null;
|
||||
|
||||
if (!isDisabled) {
|
||||
await expect(cell).toBeVisible();
|
||||
await cell.click();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Interactive Component Integration', () => {
|
||||
test('accordion with content integration', async ({ page }) => {
|
||||
const accordions = page.locator('[class*="accordion"], [role="region"]');
|
||||
|
||||
if (await accordions.count() > 0) {
|
||||
const accordion = accordions.first();
|
||||
|
||||
// Test accordion triggers
|
||||
const triggers = accordion.locator('button[aria-expanded], [data-state="closed"]');
|
||||
if (await triggers.count() > 0) {
|
||||
const trigger = triggers.first();
|
||||
await expect(trigger).toBeVisible();
|
||||
|
||||
// Test accordion expansion
|
||||
await trigger.click();
|
||||
|
||||
// Check if content is expanded
|
||||
const isExpanded = await trigger.getAttribute('aria-expanded') === 'true' ||
|
||||
await trigger.getAttribute('data-state') === 'open';
|
||||
expect(isExpanded).toBeTruthy();
|
||||
|
||||
// Test accordion content
|
||||
const content = accordion.locator('[data-state="open"], [aria-hidden="false"]');
|
||||
if (await content.count() > 0) {
|
||||
await expect(content.first()).toBeVisible();
|
||||
|
||||
// Test content interaction
|
||||
const contentButtons = content.first().locator('button, input, select');
|
||||
if (await contentButtons.count() > 0) {
|
||||
await expect(contentButtons.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('tabs with content integration', async ({ page }) => {
|
||||
const tabContainers = page.locator('[role="tablist"], [class*="tabs"]');
|
||||
|
||||
if (await tabContainers.count() > 0) {
|
||||
const tabContainer = tabContainers.first();
|
||||
|
||||
// Test tab triggers
|
||||
const tabs = tabContainer.locator('[role="tab"], button[aria-selected]');
|
||||
if (await tabs.count() > 0) {
|
||||
// Test first tab
|
||||
const firstTab = tabs.first();
|
||||
await expect(firstTab).toBeVisible();
|
||||
|
||||
// Test tab selection
|
||||
await firstTab.click();
|
||||
|
||||
// Check if tab is selected
|
||||
const isSelected = await firstTab.getAttribute('aria-selected') === 'true';
|
||||
expect(isSelected).toBeTruthy();
|
||||
|
||||
// Test tab content
|
||||
const tabPanels = page.locator('[role="tabpanel"], [aria-labelledby]');
|
||||
if (await tabPanels.count() > 0) {
|
||||
await expect(tabPanels.first()).toBeVisible();
|
||||
|
||||
// Test content interaction
|
||||
const contentElements = tabPanels.first().locator('button, input, select, textarea');
|
||||
if (await contentElements.count() > 0) {
|
||||
await expect(contentElements.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('State Management Integration', () => {
|
||||
test('component state persistence across interactions', async ({ page }) => {
|
||||
// Test form state persistence
|
||||
const inputs = page.locator('input[type="text"], input[type="email"]');
|
||||
|
||||
if (await inputs.count() > 0) {
|
||||
const input = inputs.first();
|
||||
|
||||
// Fill input
|
||||
await input.fill('Persistent test value');
|
||||
await expect(input).toHaveValue('Persistent test value');
|
||||
|
||||
// Interact with other components
|
||||
const buttons = page.locator('button');
|
||||
if (await buttons.count() > 0) {
|
||||
await buttons.first().click();
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
|
||||
// Check if input value is maintained
|
||||
await expect(input).toHaveValue('Persistent test value');
|
||||
}
|
||||
});
|
||||
|
||||
test('component communication and updates', async ({ page }) => {
|
||||
// Test if components can communicate state changes
|
||||
const formInputs = page.locator('input[type="text"], input[type="email"]');
|
||||
const buttons = page.locator('button');
|
||||
|
||||
if (await formInputs.count() > 0 && await buttons.count() > 0) {
|
||||
const input = formInputs.first();
|
||||
const button = buttons.first();
|
||||
|
||||
// Fill input and click button
|
||||
await input.fill('Communication test');
|
||||
await button.click();
|
||||
|
||||
// Wait for potential state updates
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
// Check if input value is maintained
|
||||
await expect(input).toHaveValue('Communication test');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Responsive Integration', () => {
|
||||
test('components adapt to different viewports', async ({ page }) => {
|
||||
const viewports = [
|
||||
{ width: 375, height: 667, name: 'Mobile' },
|
||||
{ width: 768, height: 1024, name: 'Tablet' },
|
||||
{ width: 1280, height: 720, name: 'Desktop' }
|
||||
];
|
||||
|
||||
for (const viewport of viewports) {
|
||||
await page.setViewportSize(viewport);
|
||||
|
||||
// Test that main components are still accessible
|
||||
const mainComponents = page.locator('button, input, select, textarea, nav, form');
|
||||
if (await mainComponents.count() > 0) {
|
||||
await expect(mainComponents.first()).toBeVisible();
|
||||
}
|
||||
|
||||
// Test navigation adaptation
|
||||
const navs = page.locator('nav, [role="navigation"]');
|
||||
if (await navs.count() > 0) {
|
||||
await expect(navs.first()).toBeVisible();
|
||||
|
||||
// On mobile, look for mobile menu patterns
|
||||
if (viewport.width <= 768) {
|
||||
const mobileMenu = page.locator('[class*="mobile"], [class*="hamburger"], [aria-label*="menu"]');
|
||||
if (await mobileMenu.count() > 0) {
|
||||
await expect(mobileMenu.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('touch interactions work on mobile', async ({ page }) => {
|
||||
// Set mobile viewport
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
// Test touch-friendly interactions
|
||||
const touchElements = page.locator('button, input, select, textarea, a');
|
||||
if (await touchElements.count() > 0) {
|
||||
for (let i = 0; i < Math.min(await touchElements.count(), 3); i++) {
|
||||
const element = touchElements.nth(i);
|
||||
|
||||
// Check element size for touch
|
||||
const boundingBox = await element.boundingBox();
|
||||
if (boundingBox) {
|
||||
// Touch targets should be appropriately sized
|
||||
expect(boundingBox.width).toBeGreaterThanOrEqual(44);
|
||||
expect(boundingBox.height).toBeGreaterThanOrEqual(44);
|
||||
}
|
||||
|
||||
// Test element interaction
|
||||
await expect(element).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
467
tests/e2e/dynamic-loading.spec.ts
Normal file
467
tests/e2e/dynamic-loading.spec.ts
Normal file
@@ -0,0 +1,467 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Dynamic Loading System - Comprehensive E2E Testing', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to the enhanced lazy loading demo
|
||||
await page.goto('http://localhost:8082');
|
||||
// Wait for the app to be fully loaded
|
||||
await page.waitForLoadState('networkidle');
|
||||
// Wait for WASM to initialize
|
||||
await page.waitForFunction(() => (window as any).wasmBindings !== undefined);
|
||||
});
|
||||
|
||||
test.describe('Page Structure & Navigation', () => {
|
||||
test('should display main header and title', async ({ page }) => {
|
||||
const header = page.locator('h1');
|
||||
await expect(header).toBeVisible();
|
||||
await expect(header).toContainText('ShadCN UI - Leptos Bundle Optimization Demo');
|
||||
|
||||
const subtitle = page.locator('h2');
|
||||
await expect(subtitle).toBeVisible();
|
||||
await expect(subtitle).toContainText('Dynamic Lazy Loading with Essential Components');
|
||||
});
|
||||
|
||||
test('should display all main sections', async ({ page }) => {
|
||||
// Essential Components section
|
||||
await expect(page.locator('h3:has-text("Essential Components")')).toBeVisible();
|
||||
|
||||
// Lazy Loaded Components section
|
||||
await expect(page.locator('h3:has-text("Lazy Loaded Components")')).toBeVisible();
|
||||
|
||||
// Dynamic WASM Components section
|
||||
await expect(page.locator('h3:has-text("Dynamic WASM Components")')).toBeVisible();
|
||||
|
||||
// Bundle Analysis panel
|
||||
await expect(page.locator('.panel.bundle-analysis')).toBeVisible();
|
||||
|
||||
// Dynamic WASM Loader Status panel
|
||||
await expect(page.locator('.panel.bundle-status')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Bundle Analysis Panel', () => {
|
||||
test('should display bundle metrics correctly', async ({ page }) => {
|
||||
const bundlePanel = page.locator('.panel.bundle-analysis');
|
||||
await expect(bundlePanel).toBeVisible();
|
||||
|
||||
// Check for bundle size information
|
||||
await expect(bundlePanel.locator('text=Bundle Size:')).toBeVisible();
|
||||
await expect(bundlePanel.locator('text=Components:')).toBeVisible();
|
||||
await expect(bundlePanel.locator('text=Optimization:')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show accurate bundle statistics', async ({ page }) => {
|
||||
const bundlePanel = page.locator('.panel.bundle-analysis');
|
||||
|
||||
// Bundle size should be displayed
|
||||
const sizeText = await bundlePanel.locator('text=/Bundle Size:.*/').textContent();
|
||||
expect(sizeText).toMatch(/Bundle Size:.*\d+\.\d+MB/);
|
||||
|
||||
// Component count should be accurate
|
||||
const componentText = await bundlePanel.locator('text=/Components:.*/').textContent();
|
||||
expect(componentText).toMatch(/Components:.*\d+/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Dynamic WASM Loader Status Panel', () => {
|
||||
test('should display loader status information', async ({ page }) => {
|
||||
const loaderPanel = page.locator('.panel.bundle-status');
|
||||
await expect(loaderPanel).toBeVisible();
|
||||
|
||||
// Check for loader header
|
||||
await expect(loaderPanel.locator('.loader-header')).toBeVisible();
|
||||
await expect(loaderPanel.locator('text=Dynamic WASM Loader Status')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show initial loading state', async ({ page }) => {
|
||||
const loaderPanel = page.locator('.panel.bundle-status');
|
||||
|
||||
// Initial state should show 0 loaded components
|
||||
await expect(loaderPanel.locator('text=Loaded: 0')).toBeVisible();
|
||||
await expect(loaderPanel.locator('text=Total Size: 0KB')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have functional load test button', async ({ page }) => {
|
||||
const loadButton = page.locator('.load-btn');
|
||||
await expect(loadButton).toBeVisible();
|
||||
await expect(loadButton).toHaveText('Load Test Component');
|
||||
|
||||
// Button should be clickable
|
||||
await expect(loadButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test('should toggle details visibility', async ({ page }) => {
|
||||
const toggleBtn = page.locator('.toggle-btn');
|
||||
const detailsContent = page.locator('.details-content');
|
||||
|
||||
// Initially details should be hidden
|
||||
await expect(detailsContent).toHaveClass(/hidden/);
|
||||
|
||||
// Click toggle button
|
||||
await toggleBtn.click();
|
||||
|
||||
// Details should now be visible
|
||||
await expect(detailsContent).not.toHaveClass(/hidden/);
|
||||
|
||||
// Click again to hide
|
||||
await toggleBtn.click();
|
||||
await expect(detailsContent).toHaveClass(/hidden/);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Essential Components Section', () => {
|
||||
test('should display all 5 essential components', async ({ page }) => {
|
||||
const essentialSection = page.locator('h3:has-text("Essential Components")').locator('..');
|
||||
|
||||
// Should have 5 essential components
|
||||
const components = essentialSection.locator('.component-item');
|
||||
await expect(components).toHaveCount(5);
|
||||
|
||||
// Check component names
|
||||
const componentNames = ['Button', 'Input', 'Card', 'Badge', 'Label'];
|
||||
for (const name of componentNames) {
|
||||
await expect(essentialSection.locator(`text=${name}`)).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('essential components should be immediately visible', async ({ page }) => {
|
||||
const essentialSection = page.locator('h3:has-text("Essential Components")').locator('..');
|
||||
|
||||
// All essential components should be visible without loading
|
||||
const components = essentialSection.locator('.component-item');
|
||||
for (let i = 0; i < await components.count(); i++) {
|
||||
const component = components.nth(i);
|
||||
await expect(component).toBeVisible();
|
||||
await expect(component).not.toHaveClass(/loading/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Lazy Loaded Components Section', () => {
|
||||
test('should display all component categories', async ({ page }) => {
|
||||
const lazySection = page.locator('h3:has-text("Lazy Loaded Components")').locator('..');
|
||||
|
||||
// Check for all 4 categories
|
||||
const categories = ['Form & Input', 'Layout & Navigation', 'Overlay & Feedback', 'Data & Media'];
|
||||
for (const category of categories) {
|
||||
await expect(lazySection.locator(`text=${category}`)).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should show correct component counts per category', async ({ page }) => {
|
||||
const lazySection = page.locator('h3:has-text("Lazy Loaded Components")').locator('..');
|
||||
|
||||
// Form & Input: 12 components
|
||||
const formSection = lazySection.locator('h4:has-text("Form & Input")').locator('..');
|
||||
const formComponents = formSection.locator('.lazy-component-wrapper');
|
||||
await expect(formComponents).toHaveCount(12);
|
||||
|
||||
// Layout & Navigation: 8 components
|
||||
const layoutSection = lazySection.locator('h4:has-text("Layout & Navigation")').locator('..');
|
||||
const layoutComponents = layoutSection.locator('.lazy-component-wrapper');
|
||||
await expect(layoutComponents).toHaveCount(8);
|
||||
|
||||
// Overlay & Feedback: 10 components
|
||||
const overlaySection = lazySection.locator('h4:has-text("Overlay & Feedback")').locator('..');
|
||||
const overlayComponents = overlaySection.locator('.lazy-component-wrapper');
|
||||
await expect(overlayComponents).toHaveCount(10);
|
||||
|
||||
// Data & Media: 9 components
|
||||
const dataSection = lazySection.locator('h4:has-text("Data & Media")').locator('..');
|
||||
const dataComponents = dataSection.locator('.lazy-component-wrapper');
|
||||
await expect(dataComponents).toHaveCount(9);
|
||||
});
|
||||
|
||||
test('lazy components should start in placeholder state', async ({ page }) => {
|
||||
const lazySection = page.locator('h3:has-text("Lazy Loaded Components")').locator('..');
|
||||
const lazyComponents = lazySection.locator('.lazy-component-wrapper');
|
||||
|
||||
// All lazy components should start in placeholder state
|
||||
for (let i = 0; i < await lazyComponents.count(); i++) {
|
||||
const component = lazyComponents.nth(i);
|
||||
await expect(component.locator('.component-placeholder')).toBeVisible();
|
||||
await expect(component.locator('.component-content')).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Dynamic WASM Components Section', () => {
|
||||
test('should display all 5 dynamic components', async ({ page }) => {
|
||||
const dynamicSection = page.locator('h3:has-text("Dynamic WASM Components")').locator('..');
|
||||
|
||||
// Should have 5 dynamic components
|
||||
const components = dynamicSection.locator('.dynamic-component-wrapper');
|
||||
await expect(components).toHaveCount(5);
|
||||
|
||||
// Check component names
|
||||
const componentNames = ['Button', 'Input', 'Card', 'Modal', 'Table'];
|
||||
for (const name of componentNames) {
|
||||
await expect(dynamicSection.locator(`text=${name}`)).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('dynamic components should show correct metadata', async ({ page }) => {
|
||||
const dynamicSection = page.locator('h3:has-text("Dynamic WASM Components")').locator('..');
|
||||
const components = dynamicSection.locator('.dynamic-component-wrapper');
|
||||
|
||||
// Check first component (Button)
|
||||
const buttonComponent = components.first();
|
||||
await expect(buttonComponent.locator('.component-category')).toContainText('Form & Input');
|
||||
await expect(buttonComponent.locator('.component-size')).toContainText('15KB');
|
||||
await expect(buttonComponent.locator('.component-description')).toContainText('Interactive button component');
|
||||
});
|
||||
|
||||
test('dynamic components should start in placeholder state', async ({ page }) => {
|
||||
const dynamicSection = page.locator('h3:has-text("Dynamic WASM Components")').locator('..');
|
||||
const components = dynamicSection.locator('.dynamic-component-wrapper');
|
||||
|
||||
// All dynamic components should start in placeholder state
|
||||
for (let i = 0; i < await components.count(); i++) {
|
||||
const component = components.nth(i);
|
||||
await expect(component.locator('.component-placeholder')).toBeVisible();
|
||||
await expect(component.locator('.component-content')).not.toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Component Loading Functionality', () => {
|
||||
test('should load lazy components on demand', async ({ page }) => {
|
||||
const lazySection = page.locator('h3:has-text("Lazy Loaded Components")').locator('..');
|
||||
const firstComponent = lazySection.locator('.lazy-component-wrapper').first();
|
||||
|
||||
// Click load button
|
||||
const loadBtn = firstComponent.locator('.load-component-btn');
|
||||
await loadBtn.click();
|
||||
|
||||
// Should show loading state
|
||||
await expect(firstComponent.locator('.component-loading')).toBeVisible();
|
||||
|
||||
// Wait for loading to complete
|
||||
await expect(firstComponent.locator('.component-success')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Should show component content
|
||||
await expect(firstComponent.locator('.component-content')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should load dynamic components on demand', async ({ page }) => {
|
||||
const dynamicSection = page.locator('h3:has-text("Dynamic WASM Components")').locator('..');
|
||||
const firstComponent = dynamicSection.locator('.dynamic-component-wrapper').first();
|
||||
|
||||
// Click load button
|
||||
const loadBtn = firstComponent.locator('.load-component-btn');
|
||||
await loadBtn.click();
|
||||
|
||||
// Should show loading state
|
||||
await expect(firstComponent.locator('.component-loading')).toBeVisible();
|
||||
|
||||
// Wait for loading to complete
|
||||
await expect(firstComponent.locator('.component-success')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Should show component content
|
||||
await expect(firstComponent.locator('.component-content')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle multiple component loads simultaneously', async ({ page }) => {
|
||||
const dynamicSection = page.locator('h3:has-text("Dynamic WASM Components")').locator('..');
|
||||
const components = dynamicSection.locator('.dynamic-component-wrapper');
|
||||
|
||||
// Load first 3 components simultaneously
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const component = components.nth(i);
|
||||
const loadBtn = component.locator('.load-component-btn');
|
||||
await loadBtn.click();
|
||||
}
|
||||
|
||||
// All should show loading state
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const component = components.nth(i);
|
||||
await expect(component.locator('.component-loading')).toBeVisible();
|
||||
}
|
||||
|
||||
// Wait for all to complete
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const component = components.nth(i);
|
||||
await expect(component.locator('.component-success')).toBeVisible({ timeout: 15000 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Search and Filter Functionality', () => {
|
||||
test('should display search input and category filter', async ({ page }) => {
|
||||
const searchSection = page.locator('.search-filters');
|
||||
await expect(searchSection).toBeVisible();
|
||||
|
||||
// Search input
|
||||
await expect(searchSection.locator('input[placeholder*="search"]')).toBeVisible();
|
||||
|
||||
// Category filter
|
||||
await expect(searchSection.locator('select')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should filter components by category', async ({ page }) => {
|
||||
const categorySelect = page.locator('select');
|
||||
|
||||
// Select "Form & Input" category
|
||||
await categorySelect.selectOption('form-input');
|
||||
|
||||
// Should show only form components
|
||||
const visibleComponents = page.locator('.lazy-component-wrapper:visible');
|
||||
await expect(visibleComponents).toHaveCount(12);
|
||||
|
||||
// Should hide other categories
|
||||
await expect(page.locator('h4:has-text("Layout & Navigation")')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should search components by name', async ({ page }) => {
|
||||
const searchInput = page.locator('input[placeholder*="search"]');
|
||||
|
||||
// Search for "button"
|
||||
await searchInput.fill('button');
|
||||
|
||||
// Should show only button-related components
|
||||
const visibleComponents = page.locator('.lazy-component-wrapper:visible');
|
||||
await expect(visibleComponents.count()).toBeLessThan(39); // Less than total
|
||||
|
||||
// Should show button components
|
||||
await expect(page.locator('text=Button')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Favorites System', () => {
|
||||
test('should allow marking components as favorites', async ({ page }) => {
|
||||
const firstComponent = page.locator('.lazy-component-wrapper').first();
|
||||
const favoriteBtn = firstComponent.locator('.favorite-btn');
|
||||
|
||||
// Initially not favorited
|
||||
await expect(favoriteBtn).not.toHaveClass(/favorited/);
|
||||
|
||||
// Click to favorite
|
||||
await favoriteBtn.click();
|
||||
|
||||
// Should now be favorited
|
||||
await expect(favoriteBtn).toHaveClass(/favorited/);
|
||||
});
|
||||
|
||||
test('should filter by favorites', async ({ page }) => {
|
||||
// Mark a few components as favorites
|
||||
const components = page.locator('.lazy-component-wrapper');
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const component = components.nth(i);
|
||||
const favoriteBtn = component.locator('.favorite-btn');
|
||||
await favoriteBtn.click();
|
||||
}
|
||||
|
||||
// Click favorites filter
|
||||
const favoritesFilter = page.locator('.favorites-filter');
|
||||
await favoritesFilter.click();
|
||||
|
||||
// Should show only favorited components
|
||||
const visibleComponents = page.locator('.lazy-component-wrapper:visible');
|
||||
await expect(visibleComponents).toHaveCount(3);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Error Handling', () => {
|
||||
test('should handle component loading errors gracefully', async ({ page }) => {
|
||||
// This test would require mocking a failed component load
|
||||
// For now, we'll test that error states are properly styled
|
||||
const errorComponent = page.locator('.component-error');
|
||||
|
||||
// Error states should be properly styled when they occur
|
||||
// (This will be empty initially, but ensures error styling is available)
|
||||
await expect(errorComponent).toBeAttached();
|
||||
});
|
||||
|
||||
test('should provide retry functionality for failed loads', async ({ page }) => {
|
||||
// Test retry button functionality
|
||||
const retryBtn = page.locator('.retry-btn');
|
||||
|
||||
// Retry button should be available (though initially hidden)
|
||||
await expect(retryBtn).toBeAttached();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Performance and Responsiveness', () => {
|
||||
test('should maintain performance with many components', async ({ page }) => {
|
||||
// Load several components to test performance
|
||||
const components = page.locator('.lazy-component-wrapper');
|
||||
const loadButtons = components.locator('.load-component-btn');
|
||||
|
||||
// Load first 5 components
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const loadBtn = loadButtons.nth(i);
|
||||
await loadBtn.click();
|
||||
}
|
||||
|
||||
// Wait for all to complete
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const component = components.nth(i);
|
||||
await expect(component.locator('.component-success')).toBeVisible({ timeout: 20000 });
|
||||
}
|
||||
|
||||
// Page should remain responsive
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should be responsive on mobile devices', async ({ page }) => {
|
||||
// Set mobile viewport
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
// All sections should still be visible
|
||||
await expect(page.locator('h3:has-text("Essential Components")')).toBeVisible();
|
||||
await expect(page.locator('h3:has-text("Lazy Loaded Components")')).toBeVisible();
|
||||
await expect(page.locator('h3:has-text("Dynamic WASM Components")')).toBeVisible();
|
||||
|
||||
// Components should be properly stacked
|
||||
const components = page.locator('.lazy-component-wrapper');
|
||||
await expect(components.first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Accessibility', () => {
|
||||
test('should have proper ARIA labels and roles', async ({ page }) => {
|
||||
// Check for proper button labels
|
||||
const loadButtons = page.locator('.load-component-btn');
|
||||
for (let i = 0; i < await loadButtons.count(); i++) {
|
||||
const button = loadButtons.nth(i);
|
||||
await expect(button).toHaveAttribute('aria-label', /load.*component/i);
|
||||
}
|
||||
|
||||
// Check for proper form labels
|
||||
const searchInput = page.locator('input[placeholder*="search"]');
|
||||
await expect(searchInput).toHaveAttribute('aria-label', /search/i);
|
||||
});
|
||||
|
||||
test('should support keyboard navigation', async ({ page }) => {
|
||||
// Tab through interactive elements
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
// Should focus on first interactive element
|
||||
const focusedElement = page.locator(':focus');
|
||||
await expect(focusedElement).toBeVisible();
|
||||
|
||||
// Should be able to navigate with arrow keys
|
||||
await page.keyboard.press('ArrowDown');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Integration with WASM', () => {
|
||||
test('should properly initialize WASM bindings', async ({ page }) => {
|
||||
// Check that WASM bindings are available
|
||||
const wasmBindings = await page.evaluate(() => (window as any).wasmBindings);
|
||||
expect(wasmBindings).toBeDefined();
|
||||
|
||||
// Check that the app is properly mounted
|
||||
await expect(page.locator('h1')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle WASM loading states correctly', async ({ page }) => {
|
||||
// The app should be fully loaded and interactive
|
||||
await expect(page.locator('.load-component-btn').first()).toBeEnabled();
|
||||
|
||||
// No loading spinners should be visible initially
|
||||
const loadingSpinners = page.locator('.loading-spinner:visible');
|
||||
await expect(loadingSpinners).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
12
tests/e2e/global-setup.ts
Normal file
12
tests/e2e/global-setup.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { chromium, FullConfig } from '@playwright/test';
|
||||
|
||||
async function globalSetup(config: FullConfig) {
|
||||
console.log('🎭 Setting up Playwright test environment...');
|
||||
|
||||
// You can add global setup logic here
|
||||
// For example: seeding test data, starting additional services, etc.
|
||||
|
||||
console.log('✅ Global setup complete');
|
||||
}
|
||||
|
||||
export default globalSetup;
|
||||
16
tests/e2e/global-teardown.ts
Normal file
16
tests/e2e/global-teardown.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { FullConfig } from '@playwright/test';
|
||||
|
||||
async function globalTeardown(config: FullConfig) {
|
||||
console.log('🧹 Cleaning up Playwright test environment...');
|
||||
|
||||
// Force exit after tests complete to prevent hanging
|
||||
// This ensures the process doesn't wait for the HTML server
|
||||
setTimeout(() => {
|
||||
console.log('🚪 Auto-closing test environment...');
|
||||
process.exit(0);
|
||||
}, 1000);
|
||||
|
||||
console.log('✅ Global teardown complete');
|
||||
}
|
||||
|
||||
export default globalTeardown;
|
||||
488
tests/e2e/leptos-components.spec.ts
Normal file
488
tests/e2e/leptos-components.spec.ts
Normal file
@@ -0,0 +1,488 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Leptos Components - Comprehensive E2E Testing', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to Leptos example app
|
||||
await page.goto('/');
|
||||
// Wait for the app to be fully loaded
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test.describe('Core UI Components', () => {
|
||||
test('button component - basic functionality and variants', async ({ page }) => {
|
||||
// Test different button variants
|
||||
const buttons = page.locator('button');
|
||||
await expect(buttons).toHaveCount(await buttons.count());
|
||||
|
||||
// Test primary button
|
||||
const primaryButton = page.locator('button').filter({ hasText: /primary|default|submit/i }).first();
|
||||
if (await primaryButton.isVisible()) {
|
||||
await expect(primaryButton).toBeVisible();
|
||||
await primaryButton.click();
|
||||
// Verify button state changes
|
||||
await expect(primaryButton).toBeEnabled();
|
||||
}
|
||||
});
|
||||
|
||||
test('input component - user interaction and validation', async ({ page }) => {
|
||||
const inputs = page.locator('input[type="text"], input[type="email"], input[type="password"]');
|
||||
|
||||
if (await inputs.count() > 0) {
|
||||
const input = inputs.first();
|
||||
await expect(input).toBeVisible();
|
||||
|
||||
// Test typing
|
||||
await input.fill('Test input value');
|
||||
await expect(input).toHaveValue('Test input value');
|
||||
|
||||
// Test clearing
|
||||
await input.clear();
|
||||
await expect(input).toHaveValue('');
|
||||
}
|
||||
});
|
||||
|
||||
test('label component - accessibility and association', async ({ page }) => {
|
||||
const labels = page.locator('label');
|
||||
|
||||
if (await labels.count() > 0) {
|
||||
for (let i = 0; i < await labels.count(); i++) {
|
||||
const label = labels.nth(i);
|
||||
await expect(label).toBeVisible();
|
||||
|
||||
// Check if label has proper text content
|
||||
const text = await label.textContent();
|
||||
expect(text).toBeTruthy();
|
||||
expect(text!.trim().length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('card component - structure, styling, and responsiveness', async ({ page }) => {
|
||||
const cards = page.locator('[class*="card"], .card, [class*="rounded"], [class*="border"]');
|
||||
|
||||
if (await cards.count() > 0) {
|
||||
const card = cards.first();
|
||||
await expect(card).toBeVisible();
|
||||
|
||||
// Test responsive behavior
|
||||
await page.setViewportSize({ width: 320, height: 568 }); // Mobile
|
||||
await expect(card).toBeVisible();
|
||||
|
||||
await page.setViewportSize({ width: 1280, height: 720 }); // Desktop
|
||||
await expect(card).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('badge component - display and variants', async ({ page }) => {
|
||||
const badges = page.locator('[class*="badge"], .badge, [class*="inline-flex"], [class*="rounded-full"]');
|
||||
|
||||
if (await badges.count() > 0) {
|
||||
for (let i = 0; i < await badges.count(); i++) {
|
||||
const badge = badges.nth(i);
|
||||
await expect(badge).toBeVisible();
|
||||
|
||||
// Check badge content
|
||||
const text = await badge.textContent();
|
||||
expect(text).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('checkbox component - interaction and state', async ({ page }) => {
|
||||
const checkboxes = page.locator('input[type="checkbox"]');
|
||||
|
||||
if (await checkboxes.count() > 0) {
|
||||
const checkbox = checkboxes.first();
|
||||
await expect(checkbox).toBeVisible();
|
||||
|
||||
// Test checkbox interaction
|
||||
const initialChecked = await checkbox.isChecked();
|
||||
await checkbox.click();
|
||||
const afterClickChecked = await checkbox.isChecked();
|
||||
|
||||
// State should change
|
||||
expect(afterClickChecked).toBe(!initialChecked);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Layout and Navigation Components', () => {
|
||||
test('separator component - visual separation', async ({ page }) => {
|
||||
const separators = page.locator('hr, [class*="separator"], [class*="border-t"], [class*="border-b"]');
|
||||
|
||||
if (await separators.count() > 0) {
|
||||
const separator = separators.first();
|
||||
await expect(separator).toBeVisible();
|
||||
|
||||
// Check if separator has proper styling
|
||||
const classes = await separator.getAttribute('class');
|
||||
expect(classes).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('navigation-menu component - menu structure and interaction', async ({ page }) => {
|
||||
const navMenus = page.locator('nav, [class*="navigation"], [class*="menu"]');
|
||||
|
||||
if (await navMenus.count() > 0) {
|
||||
const nav = navMenus.first();
|
||||
await expect(nav).toBeVisible();
|
||||
|
||||
// Test navigation items
|
||||
const navItems = nav.locator('a, button, [role="menuitem"]');
|
||||
if (await navItems.count() > 0) {
|
||||
await expect(navItems.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('breadcrumb component - navigation path', async ({ page }) => {
|
||||
const breadcrumbs = page.locator('[class*="breadcrumb"], .breadcrumb, nav[aria-label*="breadcrumb"]');
|
||||
|
||||
if (await breadcrumbs.count() > 0) {
|
||||
const breadcrumb = breadcrumbs.first();
|
||||
await expect(breadcrumb).toBeVisible();
|
||||
|
||||
// Check breadcrumb items
|
||||
const items = breadcrumb.locator('a, span, [class*="breadcrumb-item"]');
|
||||
if (await items.count() > 0) {
|
||||
await expect(items.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('pagination component - page navigation', async ({ page }) => {
|
||||
const paginations = page.locator('[class*="pagination"], .pagination, nav[aria-label*="pagination"]');
|
||||
|
||||
if (await paginations.count() > 0) {
|
||||
const pagination = paginations.first();
|
||||
await expect(pagination).toBeVisible();
|
||||
|
||||
// Check pagination controls
|
||||
const controls = pagination.locator('button, a, [class*="page"]');
|
||||
if (await controls.count() > 0) {
|
||||
await expect(controls.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Interactive Components', () => {
|
||||
test('dialog component - modal functionality', async ({ page }) => {
|
||||
const dialogTriggers = page.locator('button[aria-haspopup="dialog"], [data-state="closed"]');
|
||||
|
||||
if (await dialogTriggers.count() > 0) {
|
||||
const trigger = dialogTriggers.first();
|
||||
await expect(trigger).toBeVisible();
|
||||
|
||||
// Test dialog opening
|
||||
await trigger.click();
|
||||
|
||||
// Look for dialog content
|
||||
const dialog = page.locator('[role="dialog"], [data-state="open"]');
|
||||
if (await dialog.count() > 0) {
|
||||
await expect(dialog.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('dropdown-menu component - menu expansion', async ({ page }) => {
|
||||
const dropdownTriggers = page.locator('button[aria-haspopup="true"], [data-state="closed"]');
|
||||
|
||||
if (await dropdownTriggers.count() > 0) {
|
||||
const trigger = dropdownTriggers.first();
|
||||
await expect(trigger).toBeVisible();
|
||||
|
||||
// Test dropdown opening
|
||||
await trigger.click();
|
||||
|
||||
// Look for dropdown content
|
||||
const dropdown = page.locator('[role="menu"], [data-state="open"]');
|
||||
if (await dropdown.count() > 0) {
|
||||
await expect(dropdown.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('select component - option selection', async ({ page }) => {
|
||||
const selects = page.locator('select, [role="combobox"], [class*="select"]');
|
||||
|
||||
if (await selects.count() > 0) {
|
||||
const select = selects.first();
|
||||
await expect(select).toBeVisible();
|
||||
|
||||
// Test select interaction
|
||||
await select.click();
|
||||
|
||||
// Look for options
|
||||
const options = page.locator('[role="option"], option');
|
||||
if (await options.count() > 0) {
|
||||
await expect(options.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('combobox component - search and selection', async ({ page }) => {
|
||||
const comboboxes = page.locator('[class*="combobox"], [role="combobox"]');
|
||||
|
||||
if (await comboboxes.count() > 0) {
|
||||
const combobox = comboboxes.first();
|
||||
await expect(combobox).toBeVisible();
|
||||
|
||||
// Test input functionality
|
||||
const input = combobox.locator('input');
|
||||
if (await input.count() > 0) {
|
||||
await input.first().fill('test');
|
||||
await expect(input.first()).toHaveValue('test');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Form Components', () => {
|
||||
test('form component - structure and validation', async ({ page }) => {
|
||||
const forms = page.locator('form, [class*="form"]');
|
||||
|
||||
if (await forms.count() > 0) {
|
||||
const form = forms.first();
|
||||
await expect(form).toBeVisible();
|
||||
|
||||
// Check form elements
|
||||
const formElements = form.locator('input, select, textarea, button');
|
||||
if (await formElements.count() > 0) {
|
||||
await expect(formElements.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('textarea component - multi-line input', async ({ page }) => {
|
||||
const textareas = page.locator('textarea, [class*="textarea"]');
|
||||
|
||||
if (await textareas.count() > 0) {
|
||||
const textarea = textareas.first();
|
||||
await expect(textarea).toBeVisible();
|
||||
|
||||
// Test textarea input
|
||||
await textarea.fill('Multi-line\ntest input');
|
||||
await expect(textarea).toHaveValue('Multi-line\ntest input');
|
||||
}
|
||||
});
|
||||
|
||||
test('input-otp component - one-time password input', async ({ page }) => {
|
||||
const otpInputs = page.locator('[class*="otp"], [class*="input-otp"], input[inputmode="numeric"]');
|
||||
|
||||
if (await otpInputs.count() > 0) {
|
||||
const otpInput = otpInputs.first();
|
||||
await expect(otpInput).toBeVisible();
|
||||
|
||||
// Test numeric input
|
||||
await otpInput.fill('1234');
|
||||
await expect(otpInput).toHaveValue('1234');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Data Display Components', () => {
|
||||
test('table component - data presentation', async ({ page }) => {
|
||||
const tables = page.locator('table, [class*="table"]');
|
||||
|
||||
if (await tables.count() > 0) {
|
||||
const table = tables.first();
|
||||
await expect(table).toBeVisible();
|
||||
|
||||
// Check table structure
|
||||
const rows = table.locator('tr');
|
||||
if (await rows.count() > 0) {
|
||||
await expect(rows.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('calendar component - date display', async ({ page }) => {
|
||||
const calendars = page.locator('[class*="calendar"], [class*="date-picker"]');
|
||||
|
||||
if (await calendars.count() > 0) {
|
||||
const calendar = calendars.first();
|
||||
await expect(calendar).toBeVisible();
|
||||
|
||||
// Check calendar structure
|
||||
const days = calendar.locator('[class*="day"], [class*="date"]');
|
||||
if (await days.count() > 0) {
|
||||
await expect(days.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('progress component - loading indicators', async ({ page }) => {
|
||||
const progressBars = page.locator('[class*="progress"], progress, [role="progressbar"]');
|
||||
|
||||
if (await progressBars.count() > 0) {
|
||||
const progress = progressBars.first();
|
||||
await expect(progress).toBeVisible();
|
||||
|
||||
// Check progress attributes
|
||||
const value = await progress.getAttribute('value');
|
||||
const max = await progress.getAttribute('max');
|
||||
expect(value || max).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Feedback Components', () => {
|
||||
test('alert component - notification display', async ({ page }) => {
|
||||
const alerts = page.locator('[class*="alert"], .alert, [role="alert"]');
|
||||
|
||||
if (await alerts.count() > 0) {
|
||||
const alert = alerts.first();
|
||||
await expect(alert).toBeVisible();
|
||||
|
||||
// Check alert content
|
||||
const text = await alert.textContent();
|
||||
expect(text).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('toast component - temporary notifications', async ({ page }) => {
|
||||
const toasts = page.locator('[class*="toast"], .toast, [role="status"]');
|
||||
|
||||
if (await toasts.count() > 0) {
|
||||
const toast = toasts.first();
|
||||
await expect(toast).toBeVisible();
|
||||
|
||||
// Check toast content
|
||||
const text = await toast.textContent();
|
||||
expect(text).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
||||
test('tooltip component - hover information', async ({ page }) => {
|
||||
const tooltipTriggers = page.locator('[data-tooltip], [title], [aria-describedby]');
|
||||
|
||||
if (await tooltipTriggers.count() > 0) {
|
||||
const trigger = tooltipTriggers.first();
|
||||
await expect(trigger).toBeVisible();
|
||||
|
||||
// Test tooltip hover
|
||||
await trigger.hover();
|
||||
|
||||
// Look for tooltip content
|
||||
const tooltip = page.locator('[role="tooltip"], [class*="tooltip"]');
|
||||
if (await tooltip.count() > 0) {
|
||||
await expect(tooltip.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Accessibility and Performance', () => {
|
||||
test('keyboard navigation - tab order and focus', async ({ page }) => {
|
||||
// Test tab navigation
|
||||
await page.keyboard.press('Tab');
|
||||
const focusedElement = page.locator(':focus');
|
||||
await expect(focusedElement).toBeVisible();
|
||||
|
||||
// Test multiple tab presses
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await page.keyboard.press('Tab');
|
||||
const newFocused = page.locator(':focus');
|
||||
if (await newFocused.count() > 0) {
|
||||
await expect(newFocused.first()).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('ARIA labels and semantic markup', async ({ page }) => {
|
||||
// Check for proper ARIA labels
|
||||
const elementsWithAriaLabel = page.locator('[aria-label]');
|
||||
const count = await elementsWithAriaLabel.count();
|
||||
|
||||
if (count > 0) {
|
||||
for (let i = 0; i < Math.min(count, 5); i++) {
|
||||
const element = elementsWithAriaLabel.nth(i);
|
||||
const ariaLabel = await element.getAttribute('aria-label');
|
||||
expect(ariaLabel).toBeTruthy();
|
||||
expect(ariaLabel!.length).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for proper roles
|
||||
const elementsWithRole = page.locator('[role]');
|
||||
const roleCount = await elementsWithRole.count();
|
||||
expect(roleCount).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
test('color contrast and visual accessibility', async ({ page }) => {
|
||||
// This would require visual testing tools
|
||||
// For now, we'll check that text elements have proper contrast
|
||||
const textElements = page.locator('p, h1, h2, h3, h4, h5, h6, span, div');
|
||||
if (await textElements.count() > 0) {
|
||||
await expect(textElements.first()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('component loading performance', async ({ page }) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const loadTime = Date.now() - startTime;
|
||||
|
||||
// Assert reasonable load time (adjust threshold as needed)
|
||||
expect(loadTime).toBeLessThan(5000); // 5 seconds
|
||||
});
|
||||
|
||||
test('responsive design across viewports', async ({ page }) => {
|
||||
const testViewports = [
|
||||
{ width: 320, height: 568, name: 'Mobile' },
|
||||
{ width: 768, height: 1024, name: 'Tablet' },
|
||||
{ width: 1280, height: 720, name: 'Desktop' },
|
||||
{ width: 1920, height: 1080, name: 'Large Desktop' }
|
||||
];
|
||||
|
||||
for (const viewport of testViewports) {
|
||||
await page.setViewportSize(viewport);
|
||||
await expect(page.locator('body')).toBeVisible();
|
||||
|
||||
// Check that main content is still accessible
|
||||
const mainContent = page.locator('main, [role="main"], body > *').first();
|
||||
await expect(mainContent).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Component Integration', () => {
|
||||
test('component composition - multiple components working together', async ({ page }) => {
|
||||
// Test that multiple components can coexist
|
||||
const interactiveElements = page.locator('button, input, select, textarea, a');
|
||||
const count = await interactiveElements.count();
|
||||
|
||||
if (count > 0) {
|
||||
// Test first few elements
|
||||
for (let i = 0; i < Math.min(count, 3); i++) {
|
||||
const element = interactiveElements.nth(i);
|
||||
await expect(element).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('state management - component state persistence', async ({ page }) => {
|
||||
// Test form state persistence
|
||||
const inputs = page.locator('input[type="text"]');
|
||||
|
||||
if (await inputs.count() > 0) {
|
||||
const input = inputs.first();
|
||||
await input.fill('Persistent test value');
|
||||
|
||||
// Navigate away and back
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check if state is maintained (this depends on implementation)
|
||||
const newInput = page.locator('input[type="text"]').first();
|
||||
if (await newInput.count() > 0) {
|
||||
await expect(newInput).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
393
tests/e2e/performance.spec.ts
Normal file
393
tests/e2e/performance.spec.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Leptos Performance Testing Suite', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to Leptos example app
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test.describe('Page Load Performance', () => {
|
||||
test('initial page load time is under 3 seconds', async ({ page }) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const loadTime = Date.now() - startTime;
|
||||
|
||||
// Initial load should be fast
|
||||
expect(loadTime).toBeLessThan(3000);
|
||||
|
||||
console.log(`📊 Initial page load time: ${loadTime}ms`);
|
||||
});
|
||||
|
||||
test('time to interactive is reasonable', async ({ page }) => {
|
||||
// Measure time to interactive (when buttons become clickable)
|
||||
const startTime = Date.now();
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
// Wait for interactive elements to be ready
|
||||
await page.waitForSelector('button, input, select', { state: 'visible' });
|
||||
|
||||
const tti = Date.now() - startTime;
|
||||
|
||||
// Time to interactive should be reasonable
|
||||
expect(tti).toBeLessThan(2000);
|
||||
|
||||
console.log(`⚡ Time to interactive: ${tti}ms`);
|
||||
});
|
||||
|
||||
test('memory usage is stable', async ({ page }) => {
|
||||
// Get initial memory info
|
||||
const initialMemory = await page.evaluate(() => {
|
||||
if ('memory' in performance) {
|
||||
return (performance as any).memory.usedJSHeapSize;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (initialMemory) {
|
||||
// Navigate and interact to trigger memory usage
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Perform some interactions
|
||||
const buttons = page.locator('button');
|
||||
if (await buttons.count() > 0) {
|
||||
for (let i = 0; i < Math.min(await buttons.count(), 3); i++) {
|
||||
await buttons.nth(i).click();
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
}
|
||||
|
||||
// Check memory after interactions
|
||||
const finalMemory = await page.evaluate(() => {
|
||||
if ('memory' in performance) {
|
||||
return (performance as any).memory.usedJSHeapSize;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
if (finalMemory) {
|
||||
const memoryIncrease = finalMemory - initialMemory;
|
||||
const memoryIncreaseMB = memoryIncrease / (1024 * 1024);
|
||||
|
||||
// Memory increase should be reasonable (less than 50MB)
|
||||
expect(memoryIncreaseMB).toBeLessThan(50);
|
||||
|
||||
console.log(`🧠 Memory increase: ${memoryIncreaseMB.toFixed(2)}MB`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Component Rendering Performance', () => {
|
||||
test('component render time is fast', async ({ page }) => {
|
||||
// Measure component rendering performance
|
||||
const renderStart = performance.now();
|
||||
|
||||
// Trigger a re-render by interacting with components
|
||||
const buttons = page.locator('button');
|
||||
if (await buttons.count() > 0) {
|
||||
await buttons.first().click();
|
||||
|
||||
// Wait for any animations or state changes
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
const renderEnd = performance.now();
|
||||
const renderTime = renderEnd - renderStart;
|
||||
|
||||
// Component render should be fast
|
||||
expect(renderTime).toBeLessThan(100);
|
||||
|
||||
console.log(`🎨 Component render time: ${renderTime.toFixed(2)}ms`);
|
||||
}
|
||||
});
|
||||
|
||||
test('large component lists render efficiently', async ({ page }) => {
|
||||
// Look for components that might render lists
|
||||
const listContainers = page.locator('[class*="list"], [class*="table"], [class*="grid"]');
|
||||
|
||||
if (await listContainers.count() > 0) {
|
||||
const container = listContainers.first();
|
||||
|
||||
// Measure render time for list components
|
||||
const startTime = performance.now();
|
||||
|
||||
// Wait for any dynamic content to load
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
const endTime = performance.now();
|
||||
const renderTime = endTime - startTime;
|
||||
|
||||
// List rendering should be efficient
|
||||
expect(renderTime).toBeLessThan(200);
|
||||
|
||||
console.log(`📋 List component render time: ${renderTime.toFixed(2)}ms`);
|
||||
}
|
||||
});
|
||||
|
||||
test('form validation is responsive', async ({ page }) => {
|
||||
const inputs = page.locator('input[type="text"], input[type="email"]');
|
||||
|
||||
if (await inputs.count() > 0) {
|
||||
const input = inputs.first();
|
||||
|
||||
// Measure validation response time
|
||||
const startTime = performance.now();
|
||||
|
||||
await input.fill('test@example.com');
|
||||
await input.blur();
|
||||
|
||||
// Wait for validation to complete
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
const endTime = performance.now();
|
||||
const validationTime = endTime - startTime;
|
||||
|
||||
// Validation should be responsive
|
||||
expect(validationTime).toBeLessThan(150);
|
||||
|
||||
console.log(`✅ Form validation time: ${validationTime.toFixed(2)}ms`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Interaction Performance', () => {
|
||||
test('button clicks are responsive', async ({ page }) => {
|
||||
const buttons = page.locator('button');
|
||||
|
||||
if (await buttons.count() > 0) {
|
||||
const button = buttons.first();
|
||||
|
||||
// Measure click response time
|
||||
const startTime = performance.now();
|
||||
|
||||
await button.click();
|
||||
|
||||
// Wait for any state changes
|
||||
await page.waitForTimeout(50);
|
||||
|
||||
const endTime = performance.now();
|
||||
const clickTime = endTime - startTime;
|
||||
|
||||
// Button clicks should be responsive
|
||||
expect(clickTime).toBeLessThan(100);
|
||||
|
||||
console.log(`🖱️ Button click response time: ${clickTime.toFixed(2)}ms`);
|
||||
}
|
||||
});
|
||||
|
||||
test('dropdown interactions are smooth', async ({ page }) => {
|
||||
const dropdownTriggers = page.locator('button[aria-haspopup="true"], [data-state="closed"]');
|
||||
|
||||
if (await dropdownTriggers.count() > 0) {
|
||||
const trigger = dropdownTriggers.first();
|
||||
|
||||
// Measure dropdown open time
|
||||
const startTime = performance.now();
|
||||
|
||||
await trigger.click();
|
||||
|
||||
// Wait for dropdown to appear
|
||||
const dropdown = page.locator('[role="menu"], [data-state="open"]');
|
||||
if (await dropdown.count() > 0) {
|
||||
await expect(dropdown.first()).toBeVisible();
|
||||
|
||||
const endTime = performance.now();
|
||||
const dropdownTime = endTime - startTime;
|
||||
|
||||
// Dropdown should open quickly
|
||||
expect(dropdownTime).toBeLessThan(150);
|
||||
|
||||
console.log(`📋 Dropdown open time: ${dropdownTime.toFixed(2)}ms`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('modal interactions are performant', async ({ page }) => {
|
||||
const modalTriggers = page.locator('button[aria-haspopup="dialog"]');
|
||||
|
||||
if (await modalTriggers.count() > 0) {
|
||||
const trigger = modalTriggers.first();
|
||||
|
||||
// Measure modal open time
|
||||
const startTime = performance.now();
|
||||
|
||||
await trigger.click();
|
||||
|
||||
// Wait for modal to appear
|
||||
const modal = page.locator('[role="dialog"]');
|
||||
if (await modal.count() > 0) {
|
||||
await expect(modal.first()).toBeVisible();
|
||||
|
||||
const endTime = performance.now();
|
||||
const modalTime = endTime - startTime;
|
||||
|
||||
// Modal should open quickly
|
||||
expect(modalTime).toBeLessThan(200);
|
||||
|
||||
console.log(`🪟 Modal open time: ${modalTime.toFixed(2)}ms`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Network and Resource Performance', () => {
|
||||
test('asset loading is optimized', async ({ page }) => {
|
||||
// Listen for network requests
|
||||
const requests: string[] = [];
|
||||
page.on('request', request => {
|
||||
requests.push(request.url());
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check for unnecessary requests
|
||||
const unnecessaryRequests = requests.filter(url =>
|
||||
url.includes('.woff') ||
|
||||
url.includes('.ttf') ||
|
||||
url.includes('analytics') ||
|
||||
url.includes('tracking')
|
||||
);
|
||||
|
||||
// Should not have unnecessary requests
|
||||
expect(unnecessaryRequests.length).toBeLessThan(5);
|
||||
|
||||
console.log(`🌐 Total network requests: ${requests.length}`);
|
||||
console.log(`❌ Unnecessary requests: ${unnecessaryRequests.length}`);
|
||||
});
|
||||
|
||||
test('CSS and JS bundle sizes are reasonable', async ({ page }) => {
|
||||
// Get resource timing information
|
||||
const resourceTimings = await page.evaluate(() => {
|
||||
return performance.getEntriesByType('resource').map(entry => ({
|
||||
name: entry.name,
|
||||
duration: entry.duration,
|
||||
transferSize: (entry as any).transferSize || 0
|
||||
}));
|
||||
});
|
||||
|
||||
// Check for large resources
|
||||
const largeResources = resourceTimings.filter(resource =>
|
||||
resource.transferSize > 100 * 1024 // 100KB
|
||||
);
|
||||
|
||||
// Should not have too many large resources
|
||||
expect(largeResources.length).toBeLessThan(10);
|
||||
|
||||
console.log(`📦 Large resources (>100KB): ${largeResources.length}`);
|
||||
|
||||
// Log resource details
|
||||
largeResources.forEach(resource => {
|
||||
const sizeKB = resource.transferSize / 1024;
|
||||
console.log(` - ${resource.name}: ${sizeKB.toFixed(2)}KB`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Responsive Performance', () => {
|
||||
test('mobile viewport performance is maintained', async ({ page }) => {
|
||||
// Set mobile viewport
|
||||
await page.setViewportSize({ width: 375, height: 667 });
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const mobileLoadTime = Date.now() - startTime;
|
||||
|
||||
// Mobile should load within reasonable time
|
||||
expect(mobileLoadTime).toBeLessThan(4000);
|
||||
|
||||
console.log(`📱 Mobile load time: ${mobileLoadTime}ms`);
|
||||
});
|
||||
|
||||
test('tablet viewport performance is maintained', async ({ page }) => {
|
||||
// Set tablet viewport
|
||||
await page.setViewportSize({ width: 768, height: 1024 });
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const tabletLoadTime = Date.now() - startTime;
|
||||
|
||||
// Tablet should load within reasonable time
|
||||
expect(tabletLoadTime).toBeLessThan(3500);
|
||||
|
||||
console.log(`📱 Tablet load time: ${tabletLoadTime}ms`);
|
||||
});
|
||||
|
||||
test('desktop viewport performance is optimal', async ({ page }) => {
|
||||
// Set desktop viewport
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const desktopLoadTime = Date.now() - startTime;
|
||||
|
||||
// Desktop should load quickly
|
||||
expect(desktopLoadTime).toBeLessThan(3000);
|
||||
|
||||
console.log(`🖥️ Desktop load time: ${desktopLoadTime}ms`);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Stress Testing', () => {
|
||||
test('rapid interactions remain responsive', async ({ page }) => {
|
||||
const buttons = page.locator('button');
|
||||
|
||||
if (await buttons.count() > 0) {
|
||||
const button = buttons.first();
|
||||
|
||||
// Perform rapid clicks
|
||||
const startTime = performance.now();
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await button.click();
|
||||
await page.waitForTimeout(10);
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
const totalTime = endTime - startTime;
|
||||
|
||||
// Should handle rapid interactions efficiently
|
||||
expect(totalTime).toBeLessThan(500);
|
||||
|
||||
console.log(`⚡ Rapid interaction time: ${totalTime.toFixed(2)}ms`);
|
||||
}
|
||||
});
|
||||
|
||||
test('large data sets render efficiently', async ({ page }) => {
|
||||
// Look for components that might handle large datasets
|
||||
const dataComponents = page.locator('[class*="table"], [class*="list"], [class*="grid"]');
|
||||
|
||||
if (await dataComponents.count() > 0) {
|
||||
const component = dataComponents.first();
|
||||
|
||||
// Measure render performance
|
||||
const startTime = performance.now();
|
||||
|
||||
// Wait for any dynamic content
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
const endTime = performance.now();
|
||||
const renderTime = endTime - startTime;
|
||||
|
||||
// Should render large datasets efficiently
|
||||
expect(renderTime).toBeLessThan(300);
|
||||
|
||||
console.log(`📊 Large dataset render time: ${renderTime.toFixed(2)}ms`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
409
tests/e2e/run-dynamic-loading-tests.sh
Executable file
409
tests/e2e/run-dynamic-loading-tests.sh
Executable file
@@ -0,0 +1,409 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Dynamic Loading System Test Runner
|
||||
# Comprehensive E2E testing for the enhanced lazy loading system
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Test configuration
|
||||
TEST_DIR="tests/e2e"
|
||||
REPORT_DIR="test-results"
|
||||
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
||||
|
||||
echo -e "${BLUE}🚀 Dynamic Loading System Test Runner${NC}"
|
||||
echo -e "${BLUE}=====================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Function to print colored output
|
||||
print_status() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
# Check if Playwright is installed
|
||||
check_playwright() {
|
||||
if ! command -v npx &> /dev/null; then
|
||||
print_error "npx not found. Please install Node.js and npm."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! npx playwright --version &> /dev/null; then
|
||||
print_warning "Playwright not found. Installing..."
|
||||
npm install -D @playwright/test
|
||||
npx playwright install
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if the development server is running
|
||||
check_server() {
|
||||
print_info "Checking if development server is running..."
|
||||
|
||||
if curl -s http://localhost:8080 > /dev/null 2>&1; then
|
||||
print_status "Development server is running on port 8080"
|
||||
return 0
|
||||
else
|
||||
print_warning "Development server not running on port 8080"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Start the development server
|
||||
start_server() {
|
||||
print_info "Starting development server..."
|
||||
|
||||
# Change to the correct directory
|
||||
cd book-examples/leptos
|
||||
|
||||
# Start trunk serve in background with output
|
||||
print_info "Running: trunk serve"
|
||||
trunk serve > /tmp/trunk.log 2>&1 &
|
||||
SERVER_PID=$!
|
||||
|
||||
# Store PID for cleanup
|
||||
echo $SERVER_PID > .server.pid
|
||||
|
||||
# Wait for server to start with progress indicator
|
||||
print_info "Waiting for server to start..."
|
||||
for i in {1..30}; do
|
||||
if curl -s http://localhost:8080 > /dev/null 2>&1; then
|
||||
print_status "Server started successfully on port 8080"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Show progress
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "" # New line after progress dots
|
||||
|
||||
if ! curl -s http://localhost:8080 > /dev/null 2>&1; then
|
||||
print_error "Failed to start development server after 30 seconds"
|
||||
print_error "Check /tmp/trunk.log for server errors"
|
||||
print_info "You can start the server manually with: cd book-examples/leptos && trunk serve"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Clean up server on exit
|
||||
cleanup() {
|
||||
if [ -f book-examples/leptos/.server.pid ]; then
|
||||
SERVER_PID=$(cat book-examples/leptos/.server.pid)
|
||||
if kill -0 $SERVER_PID 2>/dev/null; then
|
||||
print_info "Stopping development server (PID: $SERVER_PID)..."
|
||||
kill $SERVER_PID
|
||||
rm book-examples/leptos/.server.pid
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Set up trap for cleanup
|
||||
trap cleanup EXIT
|
||||
|
||||
# Create report directory
|
||||
setup_reports() {
|
||||
mkdir -p $REPORT_DIR
|
||||
print_status "Report directory created: $REPORT_DIR"
|
||||
}
|
||||
|
||||
# Run specific test suite
|
||||
run_test_suite() {
|
||||
local suite_name=$1
|
||||
local test_file=$2
|
||||
local browser=${3:-chromium}
|
||||
|
||||
echo ""
|
||||
print_info "Running $suite_name tests on $browser..."
|
||||
echo "Test file: $test_file"
|
||||
echo "Browser: $browser"
|
||||
echo ""
|
||||
|
||||
# Run the test with better output handling
|
||||
if npx playwright test $test_file --project=$browser --reporter=html,json,junit --timeout=30000; then
|
||||
print_status "$suite_name tests passed on $browser"
|
||||
return 0
|
||||
else
|
||||
print_error "$suite_name tests failed on $browser"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run all tests
|
||||
run_all_tests() {
|
||||
local browser=${1:-chromium}
|
||||
local failed_tests=0
|
||||
|
||||
echo ""
|
||||
print_info "Running all test suites on $browser..."
|
||||
echo ""
|
||||
|
||||
# Run dynamic loading tests
|
||||
if run_test_suite "Dynamic Loading System" "tests/e2e/dynamic-loading.spec.ts" $browser; then
|
||||
print_status "Dynamic Loading tests completed successfully"
|
||||
else
|
||||
failed_tests=$((failed_tests + 1))
|
||||
fi
|
||||
|
||||
# Run bundle optimization tests
|
||||
if run_test_suite "Bundle Optimization" "tests/e2e/bundle-optimization.spec.ts" $browser; then
|
||||
print_status "Bundle Optimization tests completed successfully"
|
||||
else
|
||||
failed_tests=$((failed_tests + 1))
|
||||
fi
|
||||
|
||||
# Run existing component tests
|
||||
if run_test_suite "Component Integration" "tests/e2e/component-integration.spec.ts" $browser; then
|
||||
print_status "Component Integration tests completed successfully"
|
||||
else
|
||||
failed_tests=$((failed_tests + 1))
|
||||
fi
|
||||
|
||||
# Run performance tests
|
||||
if run_test_suite "Performance" "tests/e2e/performance.spec.ts" $browser; then
|
||||
print_status "Performance tests completed successfully"
|
||||
else
|
||||
failed_tests=$((failed_tests + 1))
|
||||
fi
|
||||
|
||||
# Run accessibility tests
|
||||
if run_test_suite "Accessibility" "tests/e2e/accessibility.spec.ts" $browser; then
|
||||
print_status "Accessibility tests completed successfully"
|
||||
else
|
||||
failed_tests=$((failed_tests + 1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
if [ $failed_tests -eq 0 ]; then
|
||||
print_status "All test suites completed successfully! 🎉"
|
||||
else
|
||||
print_error "$failed_tests test suite(s) failed"
|
||||
fi
|
||||
|
||||
return $failed_tests
|
||||
}
|
||||
|
||||
# Run tests with specific configuration
|
||||
run_custom_tests() {
|
||||
local test_pattern=$1
|
||||
local browser=${2:-chromium}
|
||||
|
||||
echo ""
|
||||
print_info "Running custom tests matching: $test_pattern"
|
||||
print_info "Browser: $browser"
|
||||
echo ""
|
||||
|
||||
if npx playwright test --grep="$test_pattern" --project=$browser --reporter=html,json,junit --timeout=30000; then
|
||||
print_status "Custom tests completed successfully"
|
||||
return 0
|
||||
else
|
||||
print_error "Custom tests failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Generate test summary
|
||||
generate_summary() {
|
||||
echo ""
|
||||
print_info "Generating test summary..."
|
||||
|
||||
if [ -f "$REPORT_DIR/results.json" ]; then
|
||||
echo "Test results available in: $REPORT_DIR/results.json"
|
||||
echo "HTML report available in: $REPORT_DIR/playwright-report/index.html"
|
||||
echo "JUnit report available in: $REPORT_DIR/results.xml"
|
||||
fi
|
||||
|
||||
# Count test results
|
||||
if command -v jq &> /dev/null; then
|
||||
local total_tests=$(jq '.stats.total' "$REPORT_DIR/results.json" 2>/dev/null || echo "unknown")
|
||||
local passed_tests=$(jq '.stats.passed' "$REPORT_DIR/results.json" 2>/dev/null || echo "unknown")
|
||||
local failed_tests=$(jq '.stats.failed' "$REPORT_DIR/results.json" 2>/dev/null || echo "unknown")
|
||||
|
||||
echo ""
|
||||
echo "Test Summary:"
|
||||
echo " Total Tests: $total_tests"
|
||||
echo " Passed: $passed_tests"
|
||||
echo " Failed: $failed_tests"
|
||||
fi
|
||||
}
|
||||
|
||||
# Show help
|
||||
show_help() {
|
||||
echo "Usage: $0 [OPTIONS] [COMMAND]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " all Run all test suites (default)"
|
||||
echo " dynamic Run only dynamic loading tests"
|
||||
echo " bundle Run only bundle optimization tests"
|
||||
echo " components Run only component integration tests"
|
||||
echo " performance Run only performance tests"
|
||||
echo " accessibility Run only accessibility tests"
|
||||
echo " custom <pattern> Run tests matching pattern"
|
||||
echo " help Show this help message"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --browser <browser> Specify browser (chromium, firefox, webkit)"
|
||||
echo " --headless Run in headless mode"
|
||||
echo " --debug Run with debug output"
|
||||
echo " --report Generate detailed reports"
|
||||
echo " --no-server Don't start server (assume it's already running)"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 # Run all tests on chromium"
|
||||
echo " $0 dynamic # Run only dynamic loading tests"
|
||||
echo " $0 --browser firefox # Run all tests on Firefox"
|
||||
echo " $0 custom 'button' # Run tests containing 'button'"
|
||||
echo " $0 --no-server # Run tests without starting server"
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
local command="all"
|
||||
local browser="chromium"
|
||||
local headless=true
|
||||
local debug=false
|
||||
local generate_reports=true
|
||||
local start_server_flag=true
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--browser)
|
||||
browser="$2"
|
||||
shift 2
|
||||
;;
|
||||
--headless)
|
||||
headless=true
|
||||
shift
|
||||
;;
|
||||
--debug)
|
||||
debug=true
|
||||
shift
|
||||
;;
|
||||
--report)
|
||||
generate_reports=true
|
||||
shift
|
||||
;;
|
||||
--no-server)
|
||||
start_server_flag=false
|
||||
shift
|
||||
;;
|
||||
help|--help|-h)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
command="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Set environment variables
|
||||
if [ "$headless" = true ]; then
|
||||
export PLAYWRIGHT_HEADLESS=true
|
||||
else
|
||||
export PLAYWRIGHT_HEADLESS=false
|
||||
fi
|
||||
|
||||
if [ "$debug" = true ]; then
|
||||
export DEBUG=pw:api
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}Configuration:${NC}"
|
||||
echo " Command: $command"
|
||||
echo " Browser: $browser"
|
||||
echo " Headless: $headless"
|
||||
echo " Debug: $debug"
|
||||
echo " Reports: $generate_reports"
|
||||
echo " Start Server: $start_server_flag"
|
||||
echo ""
|
||||
|
||||
# Check prerequisites
|
||||
check_playwright
|
||||
|
||||
# Setup
|
||||
setup_reports
|
||||
|
||||
# Handle server startup
|
||||
if [ "$start_server_flag" = true ]; then
|
||||
if ! check_server; then
|
||||
if ! start_server; then
|
||||
print_error "Failed to start development server. Exiting."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if ! check_server; then
|
||||
print_error "Server not running and --no-server flag specified."
|
||||
print_info "Please start the server manually: cd book-examples/leptos && trunk serve"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Give server a moment to stabilize
|
||||
sleep 2
|
||||
|
||||
# Run tests based on command
|
||||
case $command in
|
||||
all)
|
||||
run_all_tests $browser
|
||||
;;
|
||||
dynamic)
|
||||
run_test_suite "Dynamic Loading System" "tests/e2e/dynamic-loading.spec.ts" $browser
|
||||
;;
|
||||
bundle)
|
||||
run_test_suite "Bundle Optimization" "tests/e2e/bundle-optimization.spec.ts" $browser
|
||||
;;
|
||||
components)
|
||||
run_test_suite "Component Integration" "tests/e2e/component-integration.spec.ts" $browser
|
||||
;;
|
||||
performance)
|
||||
run_test_suite "Performance" "tests/e2e/performance.spec.ts" $browser
|
||||
;;
|
||||
accessibility)
|
||||
run_test_suite "Accessibility" "tests/e2e/accessibility.spec.ts" $browser
|
||||
;;
|
||||
custom)
|
||||
if [ -z "$2" ]; then
|
||||
print_error "Custom pattern required. Usage: $0 custom <pattern>"
|
||||
exit 1
|
||||
fi
|
||||
run_custom_tests "$2" $browser
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown command: $command"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Generate summary if requested
|
||||
if [ "$generate_reports" = true ]; then
|
||||
generate_summary
|
||||
fi
|
||||
|
||||
echo ""
|
||||
print_info "Test execution completed!"
|
||||
print_info "Check $REPORT_DIR for detailed results"
|
||||
}
|
||||
|
||||
# Run main function with all arguments
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user