Appearance
Testing
Guide for testing applications that integrate the Infinity Widget, including unit tests, integration tests, and end-to-end testing strategies.
Testing Setup
Prerequisites
The Infinity Widget uses modern testing tools:
- Vitest for unit testing
- Playwright for end-to-end testing
- Testing Library for DOM testing utilities
Installing Testing Dependencies
bash
npm install --save-dev @testing-library/dom @testing-library/user-event vitest @playwright/testUnit Testing
Basic Widget Testing
javascript
// tests/infinity-widget.test.js
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { fireEvent, screen } from '@testing-library/dom';
import '@testing-library/jest-dom';
// Import your widget
import '../src/index.js';
describe('InfinityWidget', () => {
let widget;
beforeEach(async () => {
// Wait for custom element to be defined
await customElements.whenDefined('infinity-widget');
// Create widget instance
widget = document.createElement('infinity-widget');
widget.setAttribute('websocket-url', 'ws://localhost:8080');
document.body.appendChild(widget);
// Wait for widget to initialize
await new Promise(resolve => setTimeout(resolve, 100));
});
afterEach(() => {
document.body.innerHTML = '';
});
it('should render with default attributes', () => {
expect(widget).toBeInTheDocument();
expect(widget.getAttribute('theme')).toBe('light');
});
it('should handle theme changes', () => {
widget.setTheme('dark');
expect(widget.getTheme()).toBe('dark');
});
it('should emit events when messages are sent', async () => {
const messageHandler = vi.fn();
widget.addEventListener('infinity-widget-message-sent', messageHandler);
widget.sendMessage('Test message');
expect(messageHandler).toHaveBeenCalledWith(
expect.objectContaining({
detail: expect.objectContaining({
message: 'Test message'
})
})
);
});
});Testing Widget Attributes
javascript
// tests/widget-attributes.test.js
import { describe, it, expect, beforeEach } from 'vitest';
describe('Widget Attributes', () => {
let widget;
beforeEach(async () => {
await customElements.whenDefined('infinity-widget');
widget = document.createElement('infinity-widget');
document.body.appendChild(widget);
});
it('should update theme when attribute changes', () => {
widget.setAttribute('theme', 'dark');
expect(widget.getTheme()).toBe('dark');
});
it('should validate websocket URL', () => {
const consoleSpy = vi.spyOn(console, 'warn');
widget.setAttribute('websocket-url', 'invalid-url');
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Invalid WebSocket URL')
);
});
it('should handle boolean attributes correctly', () => {
widget.setAttribute('auto-scroll', 'true');
expect(widget.hasAttribute('auto-scroll')).toBe(true);
widget.removeAttribute('auto-scroll');
expect(widget.hasAttribute('auto-scroll')).toBe(false);
});
});Testing Events
javascript
// tests/widget-events.test.js
import { describe, it, expect, beforeEach, vi } from 'vitest';
describe('Widget Events', () => {
let widget;
beforeEach(async () => {
await customElements.whenDefined('infinity-widget');
widget = document.createElement('infinity-widget');
widget.setAttribute('websocket-url', 'ws://localhost:8080');
document.body.appendChild(widget);
});
it('should emit connection events', async () => {
const connectHandler = vi.fn();
const disconnectHandler = vi.fn();
widget.addEventListener('infinity-widget-connected', connectHandler);
widget.addEventListener('infinity-widget-disconnected', disconnectHandler);
// Mock WebSocket
const mockWebSocket = {
readyState: WebSocket.OPEN,
close: vi.fn()
};
// Simulate connection
widget.connect();
await new Promise(resolve => setTimeout(resolve, 100));
expect(connectHandler).toHaveBeenCalled();
});
it('should emit language change events', () => {
const languageHandler = vi.fn();
widget.addEventListener('infinity-widget-language-changed', languageHandler);
widget.setLanguage('es');
expect(languageHandler).toHaveBeenCalledWith(
expect.objectContaining({
detail: expect.objectContaining({
language: 'es',
previousLanguage: 'en'
})
})
);
});
});Integration Testing
Testing with Mock WebSocket
javascript
// tests/websocket-integration.test.js
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock WebSocket
class MockWebSocket {
constructor(url) {
this.url = url;
this.readyState = WebSocket.CONNECTING;
setTimeout(() => {
this.readyState = WebSocket.OPEN;
this.onopen?.();
}, 100);
}
send(data) {
// Simulate server response
setTimeout(() => {
this.onmessage?.({
data: JSON.stringify({
type: 'response',
message: 'Mock response to: ' + JSON.parse(data).message
})
});
}, 200);
}
close() {
this.readyState = WebSocket.CLOSED;
this.onclose?.();
}
}
global.WebSocket = MockWebSocket;
describe('WebSocket Integration', () => {
let widget;
beforeEach(async () => {
await customElements.whenDefined('infinity-widget');
widget = document.createElement('infinity-widget');
widget.setAttribute('websocket-url', 'ws://localhost:8080');
document.body.appendChild(widget);
});
it('should send and receive messages', async () => {
const messageReceived = vi.fn();
widget.addEventListener('infinity-widget-message-received', messageReceived);
// Connect and send message
widget.connect();
await new Promise(resolve => setTimeout(resolve, 150));
widget.sendMessage('Hello, bot!');
await new Promise(resolve => setTimeout(resolve, 250));
expect(messageReceived).toHaveBeenCalledWith(
expect.objectContaining({
detail: expect.objectContaining({
message: 'Mock response to: Hello, bot!'
})
})
);
});
});End-to-End Testing
Playwright E2E Tests
javascript
// tests/e2e/widget-interaction.spec.js
const { test, expect } = require('@playwright/test');
test.describe('Infinity Widget E2E', () => {
test.beforeEach(async ({ page }) => {
// Navigate to test page
await page.goto('/test-page.html');
// Wait for widget to load
await page.waitForSelector('infinity-widget');
});
test('should display and interact with widget', async ({ page }) => {
const widget = page.locator('infinity-widget');
// Check widget is visible
await expect(widget).toBeVisible();
// Type message and send
await widget.locator('input[type="text"]').fill('Hello, bot!');
await widget.locator('button[type="submit"]').click();
// Check message appears in chat
await expect(widget.locator('.message-user')).toContainText('Hello, bot!');
});
test('should change themes', async ({ page }) => {
const widget = page.locator('infinity-widget');
const themeToggle = widget.locator('.theme-toggle');
// Click theme toggle
await themeToggle.click();
// Check theme changed
await expect(widget).toHaveAttribute('theme', 'dark');
});
test('should work in floating mode', async ({ page }) => {
const widget = page.locator('infinity-widget');
// Set floating mode
await page.evaluate(() => {
document.querySelector('infinity-widget').setAttribute('display-mode', 'floating');
});
// Check floating bubble appears
await expect(widget.locator('.floating-bubble')).toBeVisible();
// Click to expand
await widget.locator('.floating-bubble').click();
await expect(widget.locator('.widget-container')).toBeVisible();
});
});Accessibility Testing
javascript
// tests/e2e/accessibility.spec.js
const { test, expect } = require('@playwright/test');
const { injectAxe, checkA11y } = require('axe-playwright');
test.describe('Accessibility Tests', () => {
test('should be accessible', async ({ page }) => {
await page.goto('/test-page.html');
await injectAxe(page);
// Check entire page
await checkA11y(page);
// Check widget specifically
await checkA11y(page, 'infinity-widget');
});
test('should support keyboard navigation', async ({ page }) => {
await page.goto('/test-page.html');
const widget = page.locator('infinity-widget');
const input = widget.locator('input[type="text"]');
// Tab to input
await page.keyboard.press('Tab');
await expect(input).toBeFocused();
// Type message
await page.keyboard.type('Hello via keyboard');
// Submit with Enter
await page.keyboard.press('Enter');
// Check message was sent
await expect(widget.locator('.message-user')).toContainText('Hello via keyboard');
});
});Testing Utilities
Custom Test Helpers
javascript
// tests/utils/widget-helpers.js
export async function createTestWidget(attributes = {}) {
await customElements.whenDefined('infinity-widget');
const widget = document.createElement('infinity-widget');
// Set default test attributes
widget.setAttribute('websocket-url', 'ws://localhost:8080');
// Apply custom attributes
Object.entries(attributes).forEach(([key, value]) => {
widget.setAttribute(key, value);
});
document.body.appendChild(widget);
// Wait for initialization
await new Promise(resolve => setTimeout(resolve, 100));
return widget;
}
export function mockWebSocketServer() {
const messages = [];
global.WebSocket = class MockWebSocket {
constructor(url) {
this.url = url;
this.readyState = WebSocket.OPEN;
setTimeout(() => this.onopen?.(), 0);
}
send(data) {
messages.push(JSON.parse(data));
// Echo back
setTimeout(() => {
this.onmessage?.({
data: JSON.stringify({
type: 'response',
message: 'Echo: ' + JSON.parse(data).message
})
});
}, 50);
}
close() {
this.readyState = WebSocket.CLOSED;
this.onclose?.();
}
};
return { messages };
}Running Tests
Package.json Scripts
Add these scripts to your package.json:
json
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test",
"test:all": "npm run test && npm run test:e2e"
}
}Test Configuration
javascript
// vitest.config.js
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'happy-dom',
setupFiles: ['./tests/setup.js'],
globals: true
}
});javascript
// playwright.config.js
module.exports = {
testDir: './tests/e2e',
use: {
baseURL: 'http://localhost:3000',
headless: true
},
webServer: {
command: 'npm run dev',
port: 3000,
reuseExistingServer: true
}
};Best Practices
- Test Widget Lifecycle: Test initialization, connection, and cleanup
- Mock External Dependencies: Use mock WebSocket servers for reliable tests
- Test Accessibility: Include keyboard navigation and screen reader tests
- Visual Regression: Consider screenshot testing for UI consistency
- Error Scenarios: Test connection failures and error handling
- Performance: Test with many messages and rapid interactions
Continuous Integration
Example GitHub Actions workflow:
yaml
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm run test
- run: npm run test:e2e