Skip to content

Home > Documentation > Testing Patterns

ParleyJS Testing Patterns ​

Testing strategies for postMessage-based code and ParleyJS framework components.

Overview ​

Testing postMessage communication requires special patterns because:

  • Windows cannot be directly instantiated in tests
  • Events are asynchronous
  • Timing is critical
  • Origin validation must be maintained

For general testing documentation, see TESTING.md.

Table of Contents ​

  1. Test Structure
  2. Unit Testing Patterns
  3. Integration Testing
  4. Mocking Strategies
  5. Coverage Requirements

Test Structure ​

tests/
├── unit/
│   ├── parley.test.ts           # Core Parley functionality
│   ├── message-handler.test.ts  # Handler functionality
│   ├── origin-validation.test.ts # Security tests
│   └── error-handling.test.ts   # Error scenarios
├── integration/
│   ├── parent-child.test.ts     # iFrame communication
│   ├── popup.test.ts            # Popup window communication
│   └── multi-window.test.ts     # Multiple window coordination
└── fixtures/
    ├── mock-helpers.ts          # Test utilities
    └── sample-data.ts           # Test data

Unit Testing Patterns ​

Testing Message Handlers ​

typescript
describe('message handlers', () => {
    let parley: Parley;
    let mockWindow: Window;

    beforeEach(() => {
        mockWindow = createMockWindow('https://example.com');
        parley = Parley.create({
            allowedOrigins: ['https://example.com'],
        });
    });

    afterEach(() => {
        parley.destroy();
    });

    it('should call handler for matching message type', async () => {
        const handler = vi.fn().mockResolvedValue({ success: true });
        parley.on('test-message', handler);

        await simulateIncomingMessage(parley, {
            type: 'test-message',
            data: { value: 42 },
        });

        expect(handler).toHaveBeenCalledWith(
            { value: 42 },
            expect.any(Function),
            expect.objectContaining({ type: 'test-message' })
        );
    });

    it('should not call handler for non-matching types', async () => {
        const handler = vi.fn();
        parley.on('type-a', handler);

        await simulateIncomingMessage(parley, {
            type: 'type-b',
            data: {},
        });

        expect(handler).not.toHaveBeenCalled();
    });
});

Testing Origin Validation ​

typescript
describe('origin validation', () => {
    it('should reject messages from wrong origin', async () => {
        const parley = Parley.create({
            allowedOrigins: ['https://trusted.com'],
        });

        const handler = vi.fn();
        parley.on('message', handler);

        // Try to send from wrong origin
        await simulateIncomingMessage(parley, {
            type: 'message',
            data: { evil: 'payload' },
            origin: 'https://evil.com',
        });

        expect(handler).not.toHaveBeenCalled();
    });

    it('should validate exact origin match', async () => {
        const parley = Parley.create({
            allowedOrigins: ['https://example.com:443'],
        });

        const handler = vi.fn();
        parley.on('test', handler);

        // These should NOT match
        const wrongOrigins = [
            'https://example.com:8443', // Wrong port
            'https://example.com/', // Has trailing slash
            'http://example.com', // Wrong protocol
            'https://sub.example.com', // Subdomain
        ];

        for (const origin of wrongOrigins) {
            await simulateIncomingMessage(parley, {
                type: 'test',
                data: {},
                origin,
            });

            expect(handler).not.toHaveBeenCalled();
        }
    });
});

Testing Request-Response ​

typescript
describe('request-response', () => {
    let parley: Parley;

    beforeEach(() => {
        parley = Parley.create({
            allowedOrigins: ['https://example.com'],
        });
    });

    afterEach(() => {
        parley.destroy();
    });

    it('should match response to request', async () => {
        parley.on('calculate', (payload, respond) => {
            respond({ result: payload.x + payload.y });
        });

        const iframe = createMockIframe('https://example.com');
        await parley.connect(iframe, 'child');

        const response = await parley.send(
            'calculate',
            { x: 5, y: 3 },
            {
                targetId: 'child',
            }
        );

        expect(response.result).toBe(8);
    });

    it('should timeout if response not received', async () => {
        const iframe = createMockIframe('https://example.com');
        await parley.connect(iframe, 'child');

        await expect(
            parley.send(
                'slow-operation',
                {},
                {
                    targetId: 'child',
                    timeout: 100,
                }
            )
        ).rejects.toThrow(TimeoutError);
    });
});

Integration Testing ​

Testing iFrame Communication ​

typescript
describe('iFrame communication', () => {
    let parentParley: Parley;
    let childParley: Parley;
    let iframe: HTMLIFrameElement;

    beforeEach(async () => {
        // Create mock iframe
        iframe = createMockIframe('https://child.example.com');

        // Create parent parley
        parentParley = Parley.create({
            allowedOrigins: ['https://child.example.com'],
        });

        // Create child parley
        childParley = Parley.create({
            allowedOrigins: ['https://parent.example.com'],
        });

        // Connect
        await parentParley.connect(iframe, 'child');
        await childParley.connect(window.parent, 'parent');
    });

    afterEach(() => {
        parentParley.destroy();
        childParley.destroy();
    });

    it('should establish bidirectional communication', async () => {
        childParley.on('greeting', (payload, respond) => {
            respond({ message: 'Hello from child!' });
        });

        const response = await parentParley.send(
            'greeting',
            {
                message: 'Hello from parent',
            },
            {
                targetId: 'child',
            }
        );

        expect(response.message).toBe('Hello from child!');
    });
});

Testing Error Scenarios ​

typescript
describe('error handling in integration', () => {
    it('should handle handler exceptions', async () => {
        parley.on('failing-operation', () => {
            throw new Error('Something went wrong');
        });

        const iframe = createMockIframe('https://example.com');
        await parley.connect(iframe, 'child');

        await expect(
            parley.send(
                'failing-operation',
                {},
                {
                    targetId: 'child',
                }
            )
        ).rejects.toThrow('Something went wrong');
    });
});

Mocking Strategies ​

Mock Window Helper ​

typescript
// tests/fixtures/mock-helpers.ts
export function createMockWindow(origin = 'https://example.com'): Window {
    const listeners = new Map<string, Set<EventListener>>();

    return {
        postMessage: vi.fn((message, targetOrigin) => {
            // Simulate postMessage behavior
            setTimeout(() => {
                const messageEvent = new MessageEvent('message', {
                    data: message,
                    origin: origin,
                    source: mockWindow as any,
                });

                if (listeners.has('message')) {
                    listeners.get('message')!.forEach((listener) => {
                        listener(messageEvent);
                    });
                }
            }, 0);
        }),

        addEventListener: vi.fn((type: string, listener: EventListener) => {
            if (!listeners.has(type)) {
                listeners.set(type, new Set());
            }
            listeners.get(type)!.add(listener);
        }),

        removeEventListener: vi.fn((type: string, listener: EventListener) => {
            if (listeners.has(type)) {
                listeners.get(type)!.delete(listener);
            }
        }),

        location: { origin } as Location,
    } as unknown as Window;
}

Simulation Helpers ​

typescript
// tests/fixtures/mock-helpers.ts
export async function simulateIncomingMessage(
    parley: Parley,
    messageData: { type: string; data: any; origin?: string }
) {
    const messageEvent = new MessageEvent('message', {
        data: {
            _v: '1.0.0',
            _id: crypto.randomUUID(),
            _type: messageData.type,
            _timestamp: Date.now(),
            _origin: messageData.origin || parley.allowedOrigins[0],
            _expectsResponse: false,
            payload: messageData.data,
        },
        origin: messageData.origin || parley.allowedOrigins[0],
        source: window as any,
    });

    window.dispatchEvent(messageEvent);
    await new Promise((resolve) => setImmediate(resolve));
}

Coverage Requirements ​

Minimum Coverage Thresholds ​

  • Statements: 85%
  • Branches: 80%
  • Functions: 85%
  • Lines: 85%

Critical Code (95%+ Coverage Required) ​

  • Origin validation logic
  • Message security checks
  • Error handling paths
  • Resource cleanup
  • Connection lifecycle management

Testing Checklist ​

For each feature:

  • [ ] Happy path test
  • [ ] Error path test
  • [ ] Edge case test
  • [ ] Security validation test
  • [ ] Integration test
  • [ ] Performance test (if performance-critical)

Running Tests ​

bash
# Run all tests
npm test

# Run with coverage
npm run test:coverage

# Run specific test file
npm test -- tests/unit/parley.test.ts

# Run in watch mode
npm run test:watch

# Run tests matching pattern
npm test -- --testNamePattern="origin validation"

Best Practices ​

  1. Isolate Tests: Each test should be independent
  2. Clean Up: Always destroy parley instances in afterEach
  3. Meaningful Names: Test names should describe what is being tested
  4. Arrange-Act-Assert: Structure tests clearly
  5. Mock External Dependencies: Don't test external libraries
  6. Test Error Cases First: Error handling is critical
  7. Use Realistic Data: Test with production-like data

Related Documentation:

Back to: Documentation Home

Released under the MIT License.