test: add comprehensive SDK unit tests (Node.js + Python)
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 9m29s
All checks were successful
Build & Deploy to Staging / Build & Deploy to Staging (push) Successful in 9m29s
This commit is contained in:
parent
2eca4e700b
commit
dfd410f842
5 changed files with 2212 additions and 4 deletions
323
sdk/node/test/snapapi.test.ts
Normal file
323
sdk/node/test/snapapi.test.ts
Normal file
|
|
@ -0,0 +1,323 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { SnapAPI, SnapAPIError, ScreenshotOptions } from '../src/index.js';
|
||||
|
||||
// Mock fetch globally
|
||||
const mockFetch = vi.fn();
|
||||
vi.stubGlobal('fetch', mockFetch);
|
||||
|
||||
// Mock AbortController and AbortSignal
|
||||
const mockAbort = vi.fn();
|
||||
const mockAbortController = {
|
||||
abort: mockAbort,
|
||||
signal: {
|
||||
aborted: false,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
} as AbortSignal,
|
||||
};
|
||||
|
||||
// Properly mock AbortController as a constructor
|
||||
class MockAbortController {
|
||||
abort = mockAbort;
|
||||
signal = mockAbortController.signal;
|
||||
}
|
||||
|
||||
vi.stubGlobal('AbortController', MockAbortController);
|
||||
vi.stubGlobal('setTimeout', vi.fn((fn: () => void, delay: number) => {
|
||||
// Return a timer ID that can be cleared
|
||||
return 123;
|
||||
}));
|
||||
vi.stubGlobal('clearTimeout', vi.fn());
|
||||
|
||||
describe('SnapAPI', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockAbort.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Constructor', () => {
|
||||
it('throws if no apiKey', () => {
|
||||
expect(() => new SnapAPI('')).toThrow('SnapAPI: apiKey is required');
|
||||
expect(() => new SnapAPI(null as any)).toThrow('SnapAPI: apiKey is required');
|
||||
expect(() => new SnapAPI(undefined as any)).toThrow('SnapAPI: apiKey is required');
|
||||
});
|
||||
|
||||
it('sets defaults (baseUrl, timeout)', () => {
|
||||
const snap = new SnapAPI('test-key');
|
||||
expect(snap['baseUrl']).toBe('https://snapapi.eu');
|
||||
expect(snap['timeout']).toBe(30000);
|
||||
});
|
||||
|
||||
it('accepts custom config', () => {
|
||||
const snap = new SnapAPI('test-key', {
|
||||
baseUrl: 'https://custom.snapapi.com',
|
||||
timeout: 60000,
|
||||
});
|
||||
expect(snap['baseUrl']).toBe('https://custom.snapapi.com');
|
||||
expect(snap['timeout']).toBe(60000);
|
||||
});
|
||||
|
||||
it('strips trailing slash from baseUrl', () => {
|
||||
const snap = new SnapAPI('test-key', {
|
||||
baseUrl: 'https://custom.snapapi.com/',
|
||||
});
|
||||
expect(snap['baseUrl']).toBe('https://custom.snapapi.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('capture()', () => {
|
||||
let snap: SnapAPI;
|
||||
|
||||
beforeEach(() => {
|
||||
snap = new SnapAPI('test-api-key');
|
||||
});
|
||||
|
||||
it('sends correct POST with Bearer auth', async () => {
|
||||
const mockArrayBuffer = new ArrayBuffer(100);
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
arrayBuffer: () => Promise.resolve(mockArrayBuffer),
|
||||
});
|
||||
|
||||
await snap.capture('https://example.com');
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://snapapi.eu/v1/screenshot',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer test-api-key',
|
||||
},
|
||||
body: JSON.stringify({ url: 'https://example.com' }),
|
||||
signal: mockAbortController.signal,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('sends all options in body', async () => {
|
||||
const mockArrayBuffer = new ArrayBuffer(100);
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
arrayBuffer: () => Promise.resolve(mockArrayBuffer),
|
||||
});
|
||||
|
||||
await snap.capture('https://example.com', {
|
||||
format: 'jpeg',
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
quality: 90,
|
||||
fullPage: true,
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://snapapi.eu/v1/screenshot',
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({
|
||||
url: 'https://example.com',
|
||||
format: 'jpeg',
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
quality: 90,
|
||||
fullPage: true,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('works with ScreenshotOptions object form', async () => {
|
||||
const mockArrayBuffer = new ArrayBuffer(100);
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
arrayBuffer: () => Promise.resolve(mockArrayBuffer),
|
||||
});
|
||||
|
||||
const options: ScreenshotOptions = {
|
||||
url: 'https://example.com',
|
||||
format: 'png',
|
||||
width: 1280,
|
||||
height: 800,
|
||||
deviceScale: 2,
|
||||
waitForSelector: '.content',
|
||||
};
|
||||
|
||||
await snap.capture(options);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'https://snapapi.eu/v1/screenshot',
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify(options),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('throws if no url', async () => {
|
||||
await expect(snap.capture('')).rejects.toThrow('SnapAPI: url is required');
|
||||
|
||||
const optionsWithoutUrl = {} as ScreenshotOptions;
|
||||
await expect(snap.capture(optionsWithoutUrl)).rejects.toThrow('SnapAPI: url is required');
|
||||
});
|
||||
|
||||
it('throws SnapAPIError on 401/403/429 with error detail', async () => {
|
||||
// Test 401
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: () => Promise.resolve({ error: 'Invalid API key' }),
|
||||
});
|
||||
|
||||
await expect(snap.capture('https://example.com')).rejects.toMatchObject({
|
||||
name: 'SnapAPIError',
|
||||
status: 401,
|
||||
detail: 'Invalid API key',
|
||||
message: 'SnapAPI error 401: Invalid API key',
|
||||
});
|
||||
|
||||
// Test 403
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 403,
|
||||
json: () => Promise.resolve({ error: 'Forbidden' }),
|
||||
});
|
||||
|
||||
await expect(snap.capture('https://example.com')).rejects.toMatchObject({
|
||||
name: 'SnapAPIError',
|
||||
status: 403,
|
||||
detail: 'Forbidden',
|
||||
});
|
||||
|
||||
// Test 429
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 429,
|
||||
json: () => Promise.resolve({ error: 'Rate limit exceeded' }),
|
||||
});
|
||||
|
||||
await expect(snap.capture('https://example.com')).rejects.toMatchObject({
|
||||
name: 'SnapAPIError',
|
||||
status: 429,
|
||||
detail: 'Rate limit exceeded',
|
||||
});
|
||||
});
|
||||
|
||||
it('throws SnapAPIError with fallback message when error JSON fails', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 500,
|
||||
json: () => Promise.reject(new Error('Invalid JSON')),
|
||||
});
|
||||
|
||||
await expect(snap.capture('https://example.com')).rejects.toMatchObject({
|
||||
name: 'SnapAPIError',
|
||||
status: 500,
|
||||
detail: 'HTTP 500',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns Buffer on success', async () => {
|
||||
const mockArrayBuffer = new ArrayBuffer(100);
|
||||
// Fill with some test data
|
||||
const view = new Uint8Array(mockArrayBuffer);
|
||||
view.fill(0xFF);
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
arrayBuffer: () => Promise.resolve(mockArrayBuffer),
|
||||
});
|
||||
|
||||
const result = await snap.capture('https://example.com');
|
||||
|
||||
expect(result).toBeInstanceOf(Buffer);
|
||||
expect(result.length).toBe(100);
|
||||
expect(result[0]).toBe(0xFF);
|
||||
});
|
||||
|
||||
it('sets up AbortController correctly for timeout', async () => {
|
||||
const mockArrayBuffer = new ArrayBuffer(100);
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
arrayBuffer: () => Promise.resolve(mockArrayBuffer),
|
||||
});
|
||||
|
||||
const customSnap = new SnapAPI('test-key', { timeout: 15000 });
|
||||
await customSnap.capture('https://example.com');
|
||||
|
||||
// Verify setTimeout was called with correct timeout
|
||||
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 15000);
|
||||
|
||||
// Verify clearTimeout was called (cleanup)
|
||||
expect(clearTimeout).toHaveBeenCalledWith(123);
|
||||
|
||||
// Verify fetch was called with the abort signal
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
signal: mockAbortController.signal,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('health()', () => {
|
||||
let snap: SnapAPI;
|
||||
|
||||
beforeEach(() => {
|
||||
snap = new SnapAPI('test-api-key');
|
||||
});
|
||||
|
||||
it('calls GET /health and returns parsed JSON', async () => {
|
||||
const mockHealthResponse = {
|
||||
status: 'ok',
|
||||
version: '1.0.0',
|
||||
uptime: 12345,
|
||||
browser: {
|
||||
browsers: 2,
|
||||
totalPages: 10,
|
||||
availablePages: 8,
|
||||
queueDepth: 0,
|
||||
},
|
||||
};
|
||||
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(mockHealthResponse),
|
||||
});
|
||||
|
||||
const result = await snap.health();
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://snapapi.eu/health');
|
||||
expect(result).toEqual(mockHealthResponse);
|
||||
});
|
||||
|
||||
it('throws SnapAPIError on failure', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 503,
|
||||
});
|
||||
|
||||
await expect(snap.health()).rejects.toMatchObject({
|
||||
name: 'SnapAPIError',
|
||||
status: 503,
|
||||
detail: 'Health check failed',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SnapAPIError', () => {
|
||||
it('creates proper error with status and detail', () => {
|
||||
const error = new SnapAPIError(400, 'Bad Request');
|
||||
|
||||
expect(error.name).toBe('SnapAPIError');
|
||||
expect(error.status).toBe(400);
|
||||
expect(error.detail).toBe('Bad Request');
|
||||
expect(error.message).toBe('SnapAPI error 400: Bad Request');
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error).toBeInstanceOf(SnapAPIError);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue