Skip to content

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/test

Unit 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

  1. Test Widget Lifecycle: Test initialization, connection, and cleanup
  2. Mock External Dependencies: Use mock WebSocket servers for reliable tests
  3. Test Accessibility: Include keyboard navigation and screen reader tests
  4. Visual Regression: Consider screenshot testing for UI consistency
  5. Error Scenarios: Test connection failures and error handling
  6. 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

Built with ❤️ by CI&T