Testing StickerNest widgets and components. Use when the user asks to test widgets, write tests, add tests, create test cases, run tests, debug tests, or verify widget functionality...
This skill covers testing patterns for StickerNest widgets and components using Playwright (E2E) and Vitest (unit tests).
| Tool | Purpose | Location |
|---|---|---|
| Playwright | E2E browser tests | tests/*.spec.ts |
| Vitest | Unit tests | src/**/*.test.ts |
# E2E Tests (Playwright)
npm run test # Run all E2E tests
npm run test:ui # Interactive UI mode
npm run test:headed # Tests with visible browser
# Unit Tests (Vitest)
npm run test:unit # Run all unit tests
npm run test:unit:watch # Watch mode
// tests/my-widget.spec.ts
import { test, expect } from '@playwright/test';
test.describe('My Widget', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
// Wait for app to load
await page.waitForSelector('[data-testid="canvas"]');
});
test('should load and display correctly', async ({ page }) => {
// Test implementation
});
});
test('should add widget to canvas', async ({ page }) => {
// Open widget library
await page.click('[data-testid="library-tab"]');
// Search for widget
await page.fill('[data-testid="widget-search"]', 'my-widget');
// Click to add
await page.click('[data-testid="widget-card-my-widget"]');
// Verify widget appears on canvas
await expect(page.locator('[data-widget-id*="my-widget"]')).toBeVisible();
});
test('should respond to clicks', async ({ page }) => {
// Add widget first...
// Get widget iframe
const widgetFrame = page.frameLocator('[data-widget-id*="my-widget"] iframe');
// Click element inside widget
await widgetFrame.locator('#button').click();
// Verify result
await expect(widgetFrame.locator('#count')).toHaveText('1');
});
test('should emit events to connected widgets', async ({ page }) => {
// Add source widget
await addWidget(page, 'counter');
// Add target widget
await addWidget(page, 'display');
// Create connection (via UI or programmatically)
await createConnection(page, 'counter', 'valueChanged', 'display', 'data.set');
// Interact with source
const counterFrame = page.frameLocator('[data-widget-id*="counter"] iframe');
await counterFrame.locator('#increment').click();
// Verify target received data
const displayFrame = page.frameLocator('[data-widget-id*="display"] iframe');
await expect(displayFrame.locator('#value')).toHaveText('1');
});
test('should persist state across reload', async ({ page }) => {
// Add widget and modify state
const widgetFrame = page.frameLocator('[data-widget-id*="counter"] iframe');
await widgetFrame.locator('#increment').click();
await widgetFrame.locator('#increment').click();
// Save canvas
await page.click('[data-testid="save-canvas"]');
// Reload page
await page.reload();
await page.waitForSelector('[data-testid="canvas"]');
// Verify state persisted
const reloadedFrame = page.frameLocator('[data-widget-id*="counter"] iframe');
await expect(reloadedFrame.locator('#count')).toHaveText('2');
});
Create reusable helpers in tests/helpers.ts:
import { Page, expect } from '@playwright/test';
export async function addWidget(page: Page, widgetId: string) {
await page.click('[data-testid="library-tab"]');
await page.fill('[data-testid="widget-search"]', widgetId);
await page.click(`[data-testid="widget-card-${widgetId}"]`);
await page.waitForSelector(`[data-widget-id*="${widgetId}"]`);
}
export async function getWidgetFrame(page: Page, widgetId: string) {
return page.frameLocator(`[data-widget-id*="${widgetId}"] iframe`);
}
export async function waitForWidgetReady(page: Page, widgetId: string) {
await page.waitForFunction(
(id) => {
const widget = document.querySelector(`[data-widget-id*="${id}"]`);
return widget?.getAttribute('data-lifecycle') === 'ready';
},
widgetId,
{ timeout: 5000 }
);
}
export async function createConnection(
page: Page,
sourceId: string,
sourcePort: string,
targetId: string,
targetPort: string
) {
// Implementation depends on your connection UI
// This is a placeholder for programmatic connection
}
// src/widgets/builtin/MyWidget.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { MyWidgetManifest } from './MyWidget';
describe('MyWidget', () => {
describe('Manifest', () => {
it('should have valid id format', () => {
expect(MyWidgetManifest.id).toMatch(/^stickernest\.[a-z-]+$/);
});
it('should have required fields', () => {
expect(MyWidgetManifest.id).toBeDefined();
expect(MyWidgetManifest.name).toBeDefined();
expect(MyWidgetManifest.version).toBeDefined();
expect(MyWidgetManifest.entry).toBeDefined();
});
it('should have valid version format', () => {
expect(MyWidgetManifest.version).toMatch(/^\d+\.\d+\.\d+$/);
});
});
});
import { validateManifest } from '../../runtime/ManifestValidator';
describe('Manifest Validation', () => {
it('should pass validation', () => {
const result = validateManifest(MyWidgetManifest);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should have matching input/output declarations', () => {
const { inputs, outputs, io } = MyWidgetManifest;
// Check io.inputs have corresponding input ports
if (io?.inputs) {
io.inputs.forEach(inputId => {
const baseId = inputId.split('.')[0];
expect(inputs).toHaveProperty(baseId);
});
}
// Check io.outputs have corresponding output ports
if (io?.outputs) {
io.outputs.forEach(outputId => {
const baseId = outputId.split('.')[0];
expect(outputs).toHaveProperty(baseId);
});
}
});
});
import { isPortCompatible } from '../../runtime/PortCompatibility';
describe('Port Compatibility', () => {
it('should allow same types', () => {
expect(isPortCompatible('string', 'string')).toBe(true);
expect(isPortCompatible('number', 'number')).toBe(true);
});
it('should allow any type to receive all', () => {
expect(isPortCompatible('string', 'any')).toBe(true);
expect(isPortCompatible('number', 'any')).toBe(true);
expect(isPortCompatible('object', 'any')).toBe(true);
});
it('should reject incompatible types', () => {
expect(isPortCompatible('string', 'number')).toBe(false);
expect(isPortCompatible('trigger', 'string')).toBe(false);
});
});
import { PipelineRuntime } from '../../runtime/PipelineRuntime';
import { EventBus } from '../../runtime/EventBus';
describe('PipelineRuntime', () => {
let runtime: PipelineRuntime;
let eventBus: EventBus;
beforeEach(() => {
eventBus = new EventBus();
runtime = new PipelineRuntime({ eventBus });
});
it('should route events between connected widgets', async () => {
const received: any[] = [];
// Subscribe to target
eventBus.on('widget:input', (event) => {
if (event.targetWidgetId === 'widget-b') {
received.push(event);
}
});
// Create connection
runtime.addConnection({
id: 'conn-1',
sourceNodeId: 'widget-a',
sourcePortId: 'output',
targetNodeId: 'widget-b',
targetPortId: 'input',
});
// Emit from source
runtime.handleOutput('widget-a', 'output', { value: 42 });
expect(received).toHaveLength(1);
expect(received[0].payload.value).toBe(42);
});
});
import { vi } from 'vitest';
function createMockWidgetAPI() {
return {
onMount: vi.fn(),
onDestroy: vi.fn(),
onInput: vi.fn(),
onStateChange: vi.fn(),
setState: vi.fn(),
emitOutput: vi.fn(),
emit: vi.fn(),
on: vi.fn(),
log: vi.fn(),
getAssetUrl: vi.fn((path) => `/assets/${path}`),
};
}
describe('Widget HTML Logic', () => {
let mockAPI: ReturnType<typeof createMockWidgetAPI>;
beforeEach(() => {
mockAPI = createMockWidgetAPI();
(global as any).window = { WidgetAPI: mockAPI };
});
it('should register mount handler', () => {
// Execute widget script logic
// ...
expect(mockAPI.onMount).toHaveBeenCalled();
});
});
test('should update display when content changes', async ({ page }) => {
const frame = await getWidgetFrame(page, 'basic-text');
// Send input via debug panel or programmatically
await sendWidgetInput(page, 'basic-text', 'text.set', 'New Content');
await expect(frame.locator('#text')).toHaveText('New Content');
});
test('should handle user interaction', async ({ page }) => {
const frame = await getWidgetFrame(page, 'counter');
// Initial state
await expect(frame.locator('#count')).toHaveText('0');
// Click increment
await frame.locator('#increment').click();
// Verify update
await expect(frame.locator('#count')).toHaveText('1');
});
test('should count down correctly', async ({ page }) => {
const frame = await getWidgetFrame(page, 'timer');
// Start timer
await frame.locator('#start').click();
// Wait for countdown
await page.waitForTimeout(1100); // 1 second + buffer
// Verify time decreased
const timeText = await frame.locator('#time').textContent();
expect(parseInt(timeText || '60')).toBeLessThan(60);
});
| File | Tests |
|---|---|
tests/widget-basics.spec.ts |
Widget loading, lifecycle |
tests/widget-integration.spec.ts |
Cross-widget communication |
tests/pipeline-routing.spec.ts |
Pipeline connections |
tests/pipeline-creation.spec.ts |
Pipeline UI |
tests/ai-pipeline-integration.spec.ts |
AI widget generation |
tests/widget-generation.spec.ts |
Widget creation |
src/runtime/PipelineRuntime.test.ts |
Pipeline unit tests |
src/runtime/PortCompatibility.test.ts |
Port compatibility |
src/runtime/EventBus.test.ts |
Event bus unit tests |
src/state/usePanelsStore.test.ts |
Store tests |
# Run with debug UI
npx playwright test --debug
# Run specific test file
npx playwright test tests/my-widget.spec.ts
# Run with trace on failure
npx playwright test --trace on
# Run specific test file
npx vitest run src/widgets/builtin/MyWidget.test.ts
# Watch specific file
npx vitest watch src/widgets/builtin/MyWidget.test.ts
test('debug widget', async ({ page }) => {
// Capture console logs
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
// Capture errors
page.on('pageerror', err => console.error('PAGE ERROR:', err));
// Your test...
});
Located at playwright.config.ts:
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
},
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
});
Located in vite.config.ts:
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
include: ['src/**/*.test.ts'],
},
});