first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,463 @@
import type { ILoadOptionsFunctions } from 'n8n-workflow';
import * as run from '../../../actions/agent/run.operation';
import { ERROR_MESSAGES, BASE_URL_V2, AIRTOP_HOOKS_BASE_URL } from '../../../constants';
import * as methods from '../../../methods';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const AGENTS_ENDPOINT = `${BASE_URL_V2}/agents`;
const AGENTS_HOOKS_ENDPOINT = `${AIRTOP_HOOKS_BASE_URL}/agents`;
const baseNodeParameters = {
resource: 'agent',
operation: 'run',
webhookUrl: 'https://api.airtop.ai/api/hooks/agents/test-agent-123/webhooks/test-webhook',
agentParameters: '{"key": "value"}',
awaitExecution: true,
timeout: 600,
};
const mockInvocationResponse = {
invocationId: 'invocation-123',
};
const mockAgentStatusResponseWithOutput = {
status: 'Completed' as const,
output: {
result: 'success',
data: { test: 'data' },
},
};
const mockAgentStatusResponseRunning = {
status: 'Running' as const,
};
const mockAgentsListResponse = {
agents: [
{ id: 'agent-1', name: 'Test Agent 1', enabled: true },
{ id: 'agent-2', name: 'Test Agent 2', enabled: true },
{ id: 'agent-3', name: 'Another Agent', enabled: true },
],
};
const mockAgentDetailsResponse = {
id: 'test-agent-123',
name: 'Test Agent',
enabled: true,
publishedVersion: 1,
webhookId: 'test-webhook',
versionData: {
configVarsSchema: {
properties: {
url: { type: 'string', description: 'The URL to process' },
maxResults: { type: 'number', description: 'Maximum results to return' },
includeMetadata: { type: 'boolean', description: 'Include metadata in response' },
},
required: ['url'],
},
},
};
const createMockLoadOptionsFunction = (
nodeParameters: Record<string, unknown> = {},
): ILoadOptionsFunctions => {
return {
getCurrentNodeParameter(parameterName: string) {
return nodeParameters[parameterName];
},
getCredentials: jest.fn(),
getNode: () => ({
id: '1',
name: 'Airtop node',
typeVersion: 1,
type: 'n8n-nodes-base.airtop',
position: [10, 10],
parameters: {},
}),
} as unknown as ILoadOptionsFunctions;
};
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(),
};
});
describe('Test Airtop, agent run operation', () => {
afterAll(() => {
jest.unmock('../../../transport');
});
afterEach(() => {
jest.resetAllMocks();
});
it('should list available agents', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValueOnce(mockAgentsListResponse);
const mockLoadOptions = createMockLoadOptionsFunction();
const result = await methods.listSearchAgents.call(mockLoadOptions, '');
expect(apiRequestMock).toHaveBeenCalledTimes(1);
expect(apiRequestMock).toHaveBeenCalledWith(
'GET',
AGENTS_ENDPOINT,
{},
{ createdByMe: true, limit: 50, enabled: true, published: true, name: '' },
);
expect(result).toEqual({
results: [
{ name: 'Another Agent', value: 'agent-3' },
{ name: 'Test Agent 1', value: 'agent-1' },
{ name: 'Test Agent 2', value: 'agent-2' },
],
});
});
it('should get agent input parameters schema for selected agent ID', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValueOnce(mockAgentDetailsResponse);
const mockLoadOptions = createMockLoadOptionsFunction({
agentId: { mode: 'list', value: 'test-agent-123' },
});
const result = await methods.agentsResourceMapping.call(mockLoadOptions);
expect(apiRequestMock).toHaveBeenCalledTimes(1);
expect(apiRequestMock).toHaveBeenCalledWith('GET', `${AGENTS_ENDPOINT}/test-agent-123`);
expect(result).toEqual({
fields: [
{
id: 'includeMetadata',
displayName: 'includeMetadata',
defaultMatch: false,
display: true,
type: 'boolean',
required: false,
},
{
id: 'maxResults',
displayName: 'maxResults',
defaultMatch: false,
display: true,
type: 'number',
required: false,
},
{
id: 'url',
displayName: 'url (required)',
defaultMatch: false,
display: true,
type: 'string',
required: true,
},
],
});
});
it('should validate required agent parameters', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValueOnce(mockAgentDetailsResponse);
const nodeParameters = {
...baseNodeParameters,
agentId: {
mode: 'id',
value: 'test-agent-123',
},
agentParameters: {
mappingMode: 'defineBelow',
value: {
maxResults: 10, // url is required but missing
},
schema: [
{ id: 'url', displayName: 'url (required)', type: 'string', required: true },
{ id: 'maxResults', displayName: 'maxResults', type: 'number', required: false },
],
},
};
await expect(run.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
'Missing required parameters: url',
);
});
it('should return invocationId without waiting for agent completion', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
// First call: getAgentDetails
apiRequestMock.mockResolvedValueOnce(mockAgentDetailsResponse);
// Second call: invoke agent webhook
apiRequestMock.mockResolvedValueOnce(mockInvocationResponse);
const nodeParameters = {
...baseNodeParameters,
agentId: {
mode: 'id',
value: 'test-agent-123',
},
agentParameters: {
mappingMode: 'defineBelow',
value: {},
schema: [],
},
awaitExecution: false,
};
const result = await run.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(apiRequestMock).toHaveBeenCalledTimes(2);
// First call should be getAgentDetails
expect(apiRequestMock).toHaveBeenNthCalledWith(1, 'GET', `${AGENTS_ENDPOINT}/test-agent-123`);
// Second call should be the invocation
expect(apiRequestMock).toHaveBeenNthCalledWith(
2,
'POST',
`${AGENTS_HOOKS_ENDPOINT}/test-agent-123/webhooks/test-webhook`,
{ configVars: {} },
);
expect(result).toEqual([
{
json: {
invocationId: 'invocation-123',
},
},
]);
});
it('should wait for agent until response contains an output', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
// Mock getAgentDetails
apiRequestMock.mockResolvedValueOnce(mockAgentDetailsResponse);
// Mock the initial invocation request
apiRequestMock.mockResolvedValueOnce(mockInvocationResponse);
// Mock the first status check (still running, no output)
apiRequestMock.mockResolvedValueOnce(mockAgentStatusResponseRunning);
// Mock the second status check (completed with output)
apiRequestMock.mockResolvedValueOnce(mockAgentStatusResponseWithOutput);
const nodeParameters = {
...baseNodeParameters,
agentId: {
mode: 'id',
value: 'test-agent-123',
},
agentParameters: {
mappingMode: 'defineBelow',
value: {},
schema: [],
},
awaitExecution: true,
timeout: 600,
};
const result = await run.execute.call(createMockExecuteFunction(nodeParameters), 0);
// Should have called apiRequest 4 times: 1 for getAgentDetails + 1 for invocation + 2 for status checks
expect(apiRequestMock).toHaveBeenCalledTimes(4);
// First call should be getAgentDetails
expect(apiRequestMock).toHaveBeenNthCalledWith(1, 'GET', `${AGENTS_ENDPOINT}/test-agent-123`);
// Second call should be the invocation
expect(apiRequestMock).toHaveBeenNthCalledWith(
2,
'POST',
`${AGENTS_HOOKS_ENDPOINT}/test-agent-123/webhooks/test-webhook`,
{ configVars: {} },
);
// Third and fourth calls should be status checks
expect(apiRequestMock).toHaveBeenNthCalledWith(
3,
'GET',
`${AGENTS_HOOKS_ENDPOINT}/test-agent-123/invocations/invocation-123/result`,
);
expect(apiRequestMock).toHaveBeenNthCalledWith(
4,
'GET',
`${AGENTS_HOOKS_ENDPOINT}/test-agent-123/invocations/invocation-123/result`,
);
expect(result).toEqual([
{
json: {
invocationId: 'invocation-123',
status: 'Completed',
output: {
result: 'success',
data: { test: 'data' },
},
},
},
]);
});
it('should throw an error if timeout is less than 10 seconds', async () => {
const nodeParameters = {
...baseNodeParameters,
timeout: 5, // Less than 10 seconds
};
await expect(run.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.AGENT_TIMEOUT_INVALID,
);
});
it('should return empty results when no agents are available', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValueOnce({ agents: [] });
const mockLoadOptions = createMockLoadOptionsFunction();
const result = await methods.listSearchAgents.call(mockLoadOptions, '');
expect(result).toEqual({ results: [] });
});
it('should return empty fields when agent has no parameters schema', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValueOnce({
id: 'test-agent-123',
name: 'Test Agent',
enabled: true,
publishedVersion: 1,
webhookId: 'test-webhook',
versionData: {},
});
const mockLoadOptions = createMockLoadOptionsFunction({
agentId: { mode: 'list', value: 'test-agent-123' },
});
const result = await methods.agentsResourceMapping.call(mockLoadOptions);
expect(result).toEqual({ fields: [] });
});
it('should filter agents by name when search filter is provided', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValueOnce(mockAgentsListResponse);
const mockLoadOptions = createMockLoadOptionsFunction();
await methods.listSearchAgents.call(mockLoadOptions, 'Test');
expect(apiRequestMock).toHaveBeenCalledWith(
'GET',
AGENTS_ENDPOINT,
{},
{ createdByMe: true, limit: 50, enabled: true, published: true, name: 'Test' },
);
});
it('should wrap agent parameters in configVars when executing', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
// First call: getAgentDetails
apiRequestMock.mockResolvedValueOnce(mockAgentDetailsResponse);
// Second call: invoke agent webhook
apiRequestMock.mockResolvedValueOnce(mockInvocationResponse);
const nodeParameters = {
...baseNodeParameters,
agentId: {
mode: 'id',
value: 'test-agent-123',
},
agentParameters: {
mappingMode: 'defineBelow',
value: {
url: 'https://example.com',
maxResults: 10,
},
schema: [
{ id: 'url', displayName: 'url (required)', type: 'string', required: true },
{ id: 'maxResults', displayName: 'maxResults', type: 'number', required: false },
],
},
awaitExecution: false,
};
await run.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(apiRequestMock).toHaveBeenCalledTimes(2);
// First call should be getAgentDetails
expect(apiRequestMock).toHaveBeenNthCalledWith(1, 'GET', `${AGENTS_ENDPOINT}/test-agent-123`);
// Second call should be the invocation with configVars
expect(apiRequestMock).toHaveBeenNthCalledWith(
2,
'POST',
`${AGENTS_HOOKS_ENDPOINT}/test-agent-123/webhooks/test-webhook`,
{
configVars: {
url: 'https://example.com',
maxResults: 10,
},
},
);
});
it('should pass all required parameters successfully', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValueOnce(mockAgentDetailsResponse);
apiRequestMock.mockResolvedValueOnce(mockInvocationResponse);
apiRequestMock.mockResolvedValueOnce(mockAgentStatusResponseWithOutput);
const nodeParameters = {
...baseNodeParameters,
agentId: {
mode: 'id',
value: 'test-agent-123',
},
agentParameters: {
mappingMode: 'defineBelow',
value: {
url: 'https://example.com',
maxResults: 10,
includeMetadata: true,
},
schema: [
{ id: 'url', displayName: 'url (required)', type: 'string', required: true },
{ id: 'maxResults', displayName: 'maxResults', type: 'number', required: false },
{
id: 'includeMetadata',
displayName: 'includeMetadata',
type: 'boolean',
required: false,
},
],
},
awaitExecution: true,
};
const result = await run.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(result).toEqual([
{
json: {
invocationId: 'invocation-123',
status: 'Completed',
output: {
result: 'success',
data: { test: 'data' },
},
},
},
]);
});
});
@@ -0,0 +1,280 @@
import type { IExecuteFunctions } from 'n8n-workflow';
import nock from 'nock';
import * as getPaginated from '../../../actions/extraction/getPaginated.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as GenericFunctions from '../../../GenericFunctions';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const baseNodeParameters = {
resource: 'extraction',
operation: 'getPaginated',
sessionId: 'test-session-123',
windowId: 'win-123',
sessionMode: 'existing',
additionalFields: {},
};
const mockResponse = {
data: {
modelResponse:
'{"items": [{"title": "Item 1", "price": "$10.99"}, {"title": "Item 2", "price": "$20.99"}]}',
},
};
const mockJsonSchema =
'{"type":"object","properties":{"title":{"type":"string"},"price":{"type":"string"}}}';
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(async (method: string, endpoint: string) => {
// For paginated extraction requests
if (endpoint.includes('/paginated-extraction')) {
return mockResponse;
}
// For session deletion
if (method === 'DELETE' && endpoint.includes('/sessions/')) {
return { status: 'success' };
}
return { success: true };
}),
};
});
jest.mock('../../../GenericFunctions', () => {
const originalModule = jest.requireActual<typeof GenericFunctions>('../../../GenericFunctions');
return {
...originalModule,
createSessionAndWindow: jest.fn().mockImplementation(async () => {
return {
sessionId: 'new-session-123',
windowId: 'new-window-123',
};
}),
shouldCreateNewSession: jest.fn().mockImplementation(function (
this: IExecuteFunctions,
index: number,
) {
const sessionMode = this.getNodeParameter('sessionMode', index) as string;
return sessionMode === 'new';
}),
validateAirtopApiResponse: jest.fn(),
};
});
describe('Test Airtop, getPaginated operation', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../transport');
jest.unmock('../../../GenericFunctions');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should extract data with minimal parameters', async () => {
const nodeParameters = {
...baseNodeParameters,
prompt: 'Extract all product titles and prices',
};
const result = await getPaginated.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(GenericFunctions.shouldCreateNewSession).toHaveBeenCalledTimes(1);
expect(GenericFunctions.createSessionAndWindow).not.toHaveBeenCalled();
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/paginated-extraction',
{
prompt: 'Extract all product titles and prices',
configuration: {},
},
);
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
windowId: 'win-123',
data: mockResponse.data,
},
},
]);
});
it('should extract data with output schema', async () => {
const nodeParameters = {
...baseNodeParameters,
prompt: 'Extract all product titles and prices',
additionalFields: {
outputSchema: mockJsonSchema,
},
};
const result = await getPaginated.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(GenericFunctions.shouldCreateNewSession).toHaveBeenCalledTimes(1);
expect(GenericFunctions.createSessionAndWindow).not.toHaveBeenCalled();
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/paginated-extraction',
{
prompt: 'Extract all product titles and prices',
configuration: {
outputSchema: mockJsonSchema,
},
},
);
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
windowId: 'win-123',
data: mockResponse.data,
},
},
]);
});
['auto', 'accurate', 'cost-efficient'].forEach((interactionMode) => {
it(`interactionMode > Should extract data with '${interactionMode}' mode`, async () => {
const nodeParameters = {
...baseNodeParameters,
prompt: 'Extract all product titles and prices',
additionalFields: {
interactionMode,
},
};
const result = await getPaginated.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/paginated-extraction',
{
prompt: 'Extract all product titles and prices',
configuration: {
interactionMode,
},
},
);
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
windowId: 'win-123',
data: mockResponse.data,
},
},
]);
});
});
['auto', 'paginated', 'infinite-scroll'].forEach((paginationMode) => {
it(`paginationMode > Should extract data with '${paginationMode}' mode`, async () => {
const nodeParameters = {
...baseNodeParameters,
prompt: 'Extract all product titles and prices',
additionalFields: {
paginationMode,
},
};
const result = await getPaginated.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/paginated-extraction',
{
prompt: 'Extract all product titles and prices',
configuration: {
paginationMode,
},
},
);
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
windowId: 'win-123',
data: mockResponse.data,
},
},
]);
});
});
it('should extract data using a new session', async () => {
const nodeParameters = {
...baseNodeParameters,
sessionMode: 'new',
autoTerminateSession: true,
url: 'https://example.com',
prompt: 'Extract all product titles and prices',
};
const result = await getPaginated.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(GenericFunctions.shouldCreateNewSession).toHaveBeenCalledTimes(1);
expect(GenericFunctions.createSessionAndWindow).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledTimes(2); // One for extraction, one for session deletion
expect(transport.apiRequest).toHaveBeenNthCalledWith(
1,
'POST',
'/sessions/new-session-123/windows/new-window-123/paginated-extraction',
{
prompt: 'Extract all product titles and prices',
configuration: {},
},
);
expect(result).toEqual([
{
json: {
data: mockResponse.data,
},
},
]);
});
it("should throw error when 'sessionId' is empty and session mode is 'existing'", async () => {
const nodeParameters = {
...baseNodeParameters,
sessionId: '',
prompt: 'Extract data',
};
await expect(
getPaginated.execute.call(createMockExecuteFunction(nodeParameters), 0),
).rejects.toThrow(ERROR_MESSAGES.SESSION_ID_REQUIRED);
});
it("should throw error when 'windowId' is empty and session mode is 'existing'", async () => {
const nodeParameters = {
...baseNodeParameters,
windowId: '',
prompt: 'Extract data',
};
await expect(
getPaginated.execute.call(createMockExecuteFunction(nodeParameters), 0),
).rejects.toThrow(ERROR_MESSAGES.WINDOW_ID_REQUIRED);
});
});
@@ -0,0 +1,283 @@
import nock from 'nock';
import * as query from '../../../actions/extraction/query.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as GenericFunctions from '../../../GenericFunctions';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const baseNodeParameters = {
resource: 'extraction',
operation: 'query',
sessionId: 'test-session-123',
windowId: 'win-123',
sessionMode: 'existing',
};
const mockResponse = {
data: {
modelResponse: {
answer: 'The page contains 5 products with prices ranging from $10.99 to $50.99',
},
},
};
const mockJsonSchema =
'{"type":"object","properties":{"productCount":{"type":"number"},"priceRange":{"type":"object"}}}';
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(async function (method: string, endpoint: string) {
if (method === 'DELETE' && endpoint.includes('/sessions/')) {
return { status: 'success' };
}
return mockResponse;
}),
};
});
jest.mock('../../../GenericFunctions', () => {
const originalModule = jest.requireActual<typeof GenericFunctions>('../../../GenericFunctions');
return {
...originalModule,
createSessionAndWindow: jest.fn().mockImplementation(async () => {
return {
sessionId: 'new-session-456',
windowId: 'new-win-456',
};
}),
shouldCreateNewSession: jest.fn().mockImplementation(function (this: any) {
const sessionMode = this.getNodeParameter('sessionMode', 0);
return sessionMode === 'new';
}),
};
});
describe('Test Airtop, query page operation', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../transport');
jest.unmock('../../../GenericFunctions');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should query the page with minimal parameters using existing session', async () => {
const nodeParameters = {
...baseNodeParameters,
prompt: 'How many products are on the page and what is their price range?',
};
const result = await query.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(GenericFunctions.shouldCreateNewSession).toHaveBeenCalledTimes(1);
expect(GenericFunctions.createSessionAndWindow).not.toHaveBeenCalled();
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/page-query',
{
prompt: 'How many products are on the page and what is their price range?',
configuration: {
experimental: {
includeVisualAnalysis: 'disabled',
},
},
},
);
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
windowId: 'win-123',
data: mockResponse.data,
},
},
]);
});
it('should query the page with output schema using existing session', async () => {
const nodeParameters = {
...baseNodeParameters,
prompt: 'How many products are on the page and what is their price range?',
additionalFields: {
outputSchema: mockJsonSchema,
},
};
const result = await query.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(GenericFunctions.shouldCreateNewSession).toHaveBeenCalledTimes(1);
expect(GenericFunctions.createSessionAndWindow).not.toHaveBeenCalled();
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/page-query',
{
prompt: 'How many products are on the page and what is their price range?',
configuration: {
outputSchema: mockJsonSchema,
experimental: {
includeVisualAnalysis: 'disabled',
},
},
},
);
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
windowId: 'win-123',
data: mockResponse.data,
},
},
]);
});
it('should query the page using a new session', async () => {
const nodeParameters = {
...baseNodeParameters,
sessionMode: 'new',
url: 'https://example.com',
prompt: 'How many products are on the page and what is their price range?',
autoTerminateSession: true,
};
const result = await query.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(GenericFunctions.shouldCreateNewSession).toHaveBeenCalledTimes(1);
expect(GenericFunctions.createSessionAndWindow).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledTimes(2); // One for query, one for session deletion
expect(transport.apiRequest).toHaveBeenNthCalledWith(
1,
'POST',
'/sessions/new-session-456/windows/new-win-456/page-query',
{
prompt: 'How many products are on the page and what is their price range?',
configuration: {
experimental: {
includeVisualAnalysis: 'disabled',
},
},
},
);
expect(result).toEqual([
{
json: {
data: mockResponse.data,
},
},
]);
});
it("should throw error when 'sessionId' is empty in 'existing' session mode", async () => {
const nodeParameters = {
...baseNodeParameters,
sessionId: '',
prompt: 'Query data',
};
await expect(query.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.SESSION_ID_REQUIRED,
);
});
it("should throw error when 'windowId' is empty in 'existing' session mode", async () => {
const nodeParameters = {
...baseNodeParameters,
windowId: '',
prompt: 'Query data',
};
await expect(query.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.WINDOW_ID_REQUIRED,
);
});
it("should query the page with 'includeVisualAnalysis' enabled", async () => {
const nodeParameters = {
...baseNodeParameters,
prompt: 'List the colors of the products on the page',
additionalFields: {
includeVisualAnalysis: true,
},
};
const result = await query.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(GenericFunctions.shouldCreateNewSession).toHaveBeenCalledTimes(1);
expect(GenericFunctions.createSessionAndWindow).not.toHaveBeenCalled();
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/page-query',
{
prompt: 'List the colors of the products on the page',
configuration: {
experimental: {
includeVisualAnalysis: 'enabled',
},
},
},
);
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
windowId: 'win-123',
data: mockResponse.data,
},
},
]);
});
it("should query the page with 'includeVisualAnalysis' disabled", async () => {
const nodeParameters = {
...baseNodeParameters,
prompt: 'How many products are on the page and what is their price range?',
additionalFields: {
includeVisualAnalysis: false,
},
};
const result = await query.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(GenericFunctions.shouldCreateNewSession).toHaveBeenCalledTimes(1);
expect(GenericFunctions.createSessionAndWindow).not.toHaveBeenCalled();
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/page-query',
{
prompt: 'How many products are on the page and what is their price range?',
configuration: {
experimental: {
includeVisualAnalysis: 'disabled',
},
},
},
);
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
windowId: 'win-123',
data: mockResponse.data,
},
},
]);
});
});
@@ -0,0 +1,172 @@
import nock from 'nock';
import * as scrape from '../../../actions/extraction/scrape.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as GenericFunctions from '../../../GenericFunctions';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const baseNodeParameters = {
resource: 'extraction',
operation: 'scrape',
sessionId: 'test-session-123',
windowId: 'win-123',
sessionMode: 'existing',
};
const mockResponse = {
data: {
content: '<html><body>Scraped content</body></html>',
},
};
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(async function (method: string, endpoint: string) {
if (method === 'DELETE' && endpoint.includes('/sessions/')) {
return { status: 'success' };
}
return mockResponse;
}),
};
});
jest.mock('../../../GenericFunctions', () => {
const originalModule = jest.requireActual<typeof GenericFunctions>('../../../GenericFunctions');
return {
...originalModule,
createSessionAndWindow: jest.fn().mockImplementation(async () => {
return {
sessionId: 'new-session-456',
windowId: 'new-win-456',
};
}),
shouldCreateNewSession: jest.fn().mockImplementation(function (this: any) {
const sessionMode = this.getNodeParameter('sessionMode', 0);
return sessionMode === 'new';
}),
};
});
describe('Test Airtop, scrape operation', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../transport');
jest.unmock('../../../GenericFunctions');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should scrape content with minimal parameters using existing session', async () => {
const result = await scrape.execute.call(createMockExecuteFunction(baseNodeParameters), 0);
expect(GenericFunctions.shouldCreateNewSession).toHaveBeenCalledTimes(1);
expect(GenericFunctions.createSessionAndWindow).not.toHaveBeenCalled();
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/scrape-content',
{},
);
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
windowId: 'win-123',
data: mockResponse.data,
},
},
]);
});
it('should scrape content with additional parameters using existing session', async () => {
const nodeParameters = {
...baseNodeParameters,
additionalFields: {
waitForSelector: '.product-list',
waitForTimeout: 5000,
},
};
const result = await scrape.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(GenericFunctions.shouldCreateNewSession).toHaveBeenCalledTimes(1);
expect(GenericFunctions.createSessionAndWindow).not.toHaveBeenCalled();
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/scrape-content',
{},
);
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
windowId: 'win-123',
data: mockResponse.data,
},
},
]);
});
it('should scrape content using a new session', async () => {
const nodeParameters = {
...baseNodeParameters,
sessionMode: 'new',
url: 'https://example.com',
autoTerminateSession: true,
};
const result = await scrape.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(GenericFunctions.shouldCreateNewSession).toHaveBeenCalledTimes(1);
expect(GenericFunctions.createSessionAndWindow).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledTimes(2); // One for scrape, one for session deletion
expect(transport.apiRequest).toHaveBeenNthCalledWith(
1,
'POST',
'/sessions/new-session-456/windows/new-win-456/scrape-content',
{},
);
expect(result).toEqual([
{
json: {
data: mockResponse.data,
},
},
]);
});
it("should throw error when sessionId is empty in 'existing' session mode", async () => {
const nodeParameters = {
...baseNodeParameters,
sessionId: '',
};
await expect(scrape.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.SESSION_ID_REQUIRED,
);
});
it("should throw error when windowId is empty in 'existing' session mode", async () => {
const nodeParameters = {
...baseNodeParameters,
windowId: '',
};
await expect(scrape.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.WINDOW_ID_REQUIRED,
);
});
});
@@ -0,0 +1,55 @@
import * as deleteFile from '../../../actions/file/delete.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const baseNodeParameters = {
resource: 'file',
operation: 'deleteFile',
sessionId: 'test-session-123',
fileId: 'file-123',
};
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn().mockResolvedValue({}),
};
});
describe('Test Airtop, delete file operation', () => {
afterAll(() => {
jest.unmock('../../../transport');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should delete file successfully', async () => {
const result = await deleteFile.execute.call(createMockExecuteFunction(baseNodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledWith('DELETE', '/files/file-123');
expect(result).toEqual([
{
json: {
data: {
message: 'File deleted successfully',
},
},
},
]);
});
it('should throw error when fileId is empty', async () => {
const nodeParameters = {
...baseNodeParameters,
fileId: '',
};
await expect(
deleteFile.execute.call(createMockExecuteFunction(nodeParameters), 0),
).rejects.toThrow(ERROR_MESSAGES.REQUIRED_PARAMETER.replace('{{field}}', 'File ID'));
});
});
@@ -0,0 +1,105 @@
import * as get from '../../../actions/file/get.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const baseNodeParameters = {
resource: 'file',
operation: 'get',
sessionId: 'test-session-123',
fileId: 'file-123',
};
const mockFileResponse = {
data: {
id: 'file-123',
fileName: 'test-file.pdf',
status: 'available',
downloadUrl: 'https://api.airtop.com/files/file-123/download',
},
};
const mockBinaryBuffer = Buffer.from('mock-binary-data');
const mockPreparedBinaryData = {
mimeType: 'application/pdf',
fileType: 'pdf',
fileName: 'test-file.pdf',
data: 'mock-base64-data',
};
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(),
};
});
describe('Test Airtop, get file operation', () => {
afterAll(() => {
jest.unmock('../../../transport');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should get file details successfully', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValueOnce(mockFileResponse);
const result = await get.execute.call(createMockExecuteFunction(baseNodeParameters), 0);
expect(apiRequestMock).toHaveBeenCalledWith('GET', '/files/file-123');
expect(result).toEqual([
{
json: {
...mockFileResponse,
},
},
]);
});
it('should output file with binary data when specified', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValueOnce(mockFileResponse);
const nodeParameters = {
...baseNodeParameters,
outputBinaryFile: true,
};
const mockExecuteFunction = createMockExecuteFunction(nodeParameters);
mockExecuteFunction.helpers.httpRequest = jest.fn().mockResolvedValue(mockBinaryBuffer);
mockExecuteFunction.helpers.prepareBinaryData = jest
.fn()
.mockResolvedValue(mockPreparedBinaryData);
const result = await get.execute.call(mockExecuteFunction, 0);
expect(apiRequestMock).toHaveBeenCalledWith('GET', '/files/file-123');
expect(result).toEqual([
{
json: {
...mockFileResponse,
},
binary: { data: mockPreparedBinaryData },
},
]);
});
it('should throw error when fileId is empty', async () => {
const nodeParameters = {
...baseNodeParameters,
fileId: '',
};
await expect(get.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.REQUIRED_PARAMETER.replace('{{field}}', 'File ID'),
);
});
});
@@ -0,0 +1,119 @@
import * as getMany from '../../../actions/file/getMany.operation';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const baseNodeParameters = {
resource: 'file',
operation: 'getMany',
sessionId: 'test-session-123',
returnAll: true,
outputSingleItem: true,
};
const mockFilesResponse = {
data: {
files: [
{
id: 'file-123',
name: 'document1.pdf',
size: 12345,
contentType: 'application/pdf',
createdAt: '2023-06-15T10:30:00Z',
},
{
id: 'file-456',
name: 'image1.jpg',
size: 54321,
contentType: 'image/jpeg',
createdAt: '2023-06-16T11:45:00Z',
},
],
pagination: {
hasMore: false,
},
},
};
const mockPaginatedResponse = {
data: {
files: [mockFilesResponse.data.files[0]],
pagination: {
hasMore: true,
},
},
};
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(),
};
});
describe('Test Airtop, get many files operation', () => {
afterAll(() => {
jest.unmock('../../../transport');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should get all files successfully', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValueOnce(mockFilesResponse);
const result = await getMany.execute.call(createMockExecuteFunction(baseNodeParameters), 0);
expect(apiRequestMock).toHaveBeenCalledWith(
'GET',
'/files',
{},
{
limit: 100,
offset: 0,
sessionIds: '',
},
);
expect(result).toEqual([
{
json: {
...mockFilesResponse,
},
},
]);
});
it('should handle limited results', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValueOnce(mockPaginatedResponse);
const nodeParameters = {
...baseNodeParameters,
returnAll: false,
limit: 1,
};
const result = await getMany.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(apiRequestMock).toHaveBeenCalledWith(
'GET',
'/files',
{},
{
limit: 1,
sessionIds: '',
},
);
expect(result).toEqual([
{
json: {
...mockPaginatedResponse,
},
},
]);
});
});
@@ -0,0 +1,389 @@
import * as helpers from '../../../actions/file/helpers';
import * as GenericFunctions from '../../../GenericFunctions';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const mockFileCreateResponse = {
data: {
id: 'file-123',
uploadUrl: 'https://upload.example.com/url',
},
};
// Mock the transport and other dependencies
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(async () => {}),
};
});
jest.mock('../../../GenericFunctions', () => {
const originalModule = jest.requireActual<typeof GenericFunctions>('../../../GenericFunctions');
return {
...originalModule,
waitForSessionEvent: jest.fn(),
};
});
describe('Test Airtop file helpers', () => {
afterAll(() => {
jest.unmock('../../../transport');
jest.unmock('../../../GenericFunctions');
});
afterEach(() => {
jest.clearAllMocks();
(transport.apiRequest as jest.Mock).mockReset();
(GenericFunctions.waitForSessionEvent as jest.Mock).mockReset();
});
describe('requestAllFiles', () => {
it('should request all files with pagination', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
const mockFilesResponse1 = {
data: {
files: [{ id: 'file-1' }, { id: 'file-2' }],
pagination: { hasMore: true },
},
};
const mockFilesResponse2 = {
data: {
files: [{ id: 'file-3' }],
pagination: { hasMore: false },
},
};
apiRequestMock
.mockResolvedValueOnce(mockFilesResponse1)
.mockResolvedValueOnce(mockFilesResponse2);
const result = await helpers.requestAllFiles.call(
createMockExecuteFunction({}),
'session-123',
);
expect(apiRequestMock).toHaveBeenCalledTimes(2);
expect(apiRequestMock).toHaveBeenNthCalledWith(
1,
'GET',
'/files',
{},
{ offset: 0, limit: 100, sessionIds: 'session-123' },
);
expect(apiRequestMock).toHaveBeenNthCalledWith(
2,
'GET',
'/files',
{},
{ offset: 100, limit: 100, sessionIds: 'session-123' },
);
expect(result).toEqual({
data: {
files: [{ id: 'file-1' }, { id: 'file-2' }, { id: 'file-3' }],
pagination: { hasMore: false },
},
});
});
it('should handle empty response', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
const mockEmptyResponse = {
data: {
files: [],
pagination: { hasMore: false },
},
};
apiRequestMock.mockResolvedValueOnce(mockEmptyResponse);
const result = await helpers.requestAllFiles.call(
createMockExecuteFunction({}),
'session-123',
);
expect(apiRequestMock).toHaveBeenCalledTimes(1);
expect(result).toEqual({
data: {
files: [],
pagination: { hasMore: false },
},
});
});
});
describe('pollFileUntilAvailable', () => {
it('should poll until file is available', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock
.mockResolvedValueOnce({ data: { status: 'uploading' } })
.mockResolvedValueOnce({ data: { status: 'available' } });
const pollPromise = helpers.pollFileUntilAvailable.call(
createMockExecuteFunction({}),
'file-123',
1000,
0,
);
const result = await pollPromise;
expect(apiRequestMock).toHaveBeenCalledTimes(2);
expect(apiRequestMock).toHaveBeenCalledWith('GET', '/files/file-123');
expect(result).toBe('file-123');
});
it('should throw timeout error if file never becomes available', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValue({ data: { status: 'processing' } });
const promise = helpers.pollFileUntilAvailable.call(
createMockExecuteFunction({}),
'file-123',
0,
);
await expect(promise).rejects.toThrow();
});
});
describe('createAndUploadFile', () => {
it('should create file entry, upload file, and poll until available', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock
.mockResolvedValueOnce(mockFileCreateResponse)
.mockResolvedValueOnce({ data: { status: 'available' } });
const mockExecuteFunction = createMockExecuteFunction({});
const mockHttpRequest = jest.fn().mockResolvedValueOnce({});
mockExecuteFunction.helpers.httpRequest = mockHttpRequest;
const pollingFunctionMock = jest.fn().mockResolvedValueOnce(mockFileCreateResponse.data.id);
const result = await helpers.createAndUploadFile.call(
mockExecuteFunction,
'test.png',
Buffer.from('test'),
'customer_upload',
pollingFunctionMock,
);
expect(apiRequestMock).toHaveBeenCalledWith('POST', '/files', {
fileName: 'test.png',
fileType: 'customer_upload',
});
expect(mockHttpRequest).toHaveBeenCalledWith({
method: 'PUT',
url: mockFileCreateResponse.data.uploadUrl,
body: Buffer.from('test'),
headers: {
'Content-Type': 'application/octet-stream',
},
});
expect(pollingFunctionMock).toHaveBeenCalledWith(mockFileCreateResponse.data.id);
expect(result).toBe(mockFileCreateResponse.data.id);
});
it('should throw error if file creation response is missing id or upload URL', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValueOnce({});
await expect(
helpers.createAndUploadFile.call(
createMockExecuteFunction({}),
'test.pdf',
Buffer.from('test'),
'customer_upload',
),
).rejects.toThrow();
});
});
describe('waitForFileInSession', () => {
it('should resolve when file_upload_status event with available status is received', async () => {
const waitForSessionEventMock = GenericFunctions.waitForSessionEvent as jest.Mock;
const mockEvent = {
event: 'file_upload_status',
status: 'available',
fileId: 'file-123',
};
waitForSessionEventMock.mockResolvedValueOnce(mockEvent);
const mockExecuteFunction = createMockExecuteFunction({});
await helpers.waitForFileInSession.call(mockExecuteFunction, 'session-123', 'file-123', 1000);
expect(waitForSessionEventMock).toHaveBeenCalledTimes(1);
expect(waitForSessionEventMock).toHaveBeenCalledWith(
'session-123',
expect.any(Function),
1000,
);
});
it('should throw error when uploading a file with invalid file format', async () => {
const waitForSessionEventMock = GenericFunctions.waitForSessionEvent as jest.Mock;
const mockEvent = {
event: 'file_upload_status',
status: 'upload_failed',
fileId: 'file-123',
eventData: {
error: 'Upload failed due to invalid file format',
},
};
waitForSessionEventMock.mockResolvedValueOnce(mockEvent);
const mockExecuteFunction = createMockExecuteFunction({});
await expect(
helpers.waitForFileInSession.call(mockExecuteFunction, 'session-123', 'file-123', 1000),
).rejects.toMatchObject({ description: 'Upload failed due to invalid file format' });
});
it('should throw error when upload_failed status is received', async () => {
const waitForSessionEventMock = GenericFunctions.waitForSessionEvent as jest.Mock;
const mockEvent = {
fileId: 'file-123',
event: 'file_upload_status',
status: 'upload_failed',
eventData: {
error: 'Upload failed for File ID: file-123',
},
};
waitForSessionEventMock.mockResolvedValueOnce(mockEvent);
const mockExecuteFunction = createMockExecuteFunction({});
// the service should throw an error description 'Upload failed for File ID: file-123'
await expect(
helpers.waitForFileInSession.call(mockExecuteFunction, 'session-123', 'file-123', 1000),
).rejects.toMatchObject({ description: 'Upload failed for File ID: file-123' });
});
it('should timeout if no matching event is received', async () => {
const waitForSessionEventMock = GenericFunctions.waitForSessionEvent as jest.Mock;
waitForSessionEventMock.mockRejectedValueOnce(new Error('Timeout reached'));
const mockExecuteFunction = createMockExecuteFunction({});
await expect(
helpers.waitForFileInSession.call(mockExecuteFunction, 'session-123', 'file-123', 100),
).rejects.toThrow('Timeout reached');
});
});
describe('pushFileToSession', () => {
it('should push file to session and wait', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
const mockFileId = 'file-123';
const mockSessionId = 'session-123';
apiRequestMock.mockResolvedValueOnce({});
// Mock waitForFileInSession
const waitForFileInSessionMock = jest.fn().mockResolvedValueOnce({});
// Call the function
await helpers.pushFileToSession.call(
createMockExecuteFunction({}),
mockFileId,
mockSessionId,
waitForFileInSessionMock,
);
expect(apiRequestMock).toHaveBeenCalledWith('POST', `/files/${mockFileId}/push`, {
sessionIds: [mockSessionId],
});
expect(waitForFileInSessionMock).toHaveBeenCalledWith(mockSessionId, mockFileId);
});
});
describe('triggerFileInput', () => {
it('should trigger file input in window', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValueOnce({});
const mockFileId = 'file-123';
const mockWindowId = 'window-123';
const mockSessionId = 'session-123';
await helpers.triggerFileInput.call(createMockExecuteFunction({}), {
fileId: mockFileId,
windowId: mockWindowId,
sessionId: mockSessionId,
elementDescription: 'test',
includeHiddenElements: false,
});
expect(apiRequestMock).toHaveBeenCalledWith(
'POST',
`/sessions/${mockSessionId}/windows/${mockWindowId}/file-input`,
{
fileId: mockFileId,
elementDescription: 'test',
includeHiddenElements: false,
},
);
});
});
describe('createFileBuffer', () => {
it('should create buffer from URL', async () => {
const mockUrl = 'https://example.com/file.pdf';
const mockBuffer = [1, 2, 3];
// Mock http request
const mockHttpRequest = jest.fn().mockResolvedValueOnce(mockBuffer);
// Create mock execute function with http request helper
const mockExecuteFunction = createMockExecuteFunction({});
mockExecuteFunction.helpers.httpRequest = mockHttpRequest;
const result = await helpers.createFileBuffer.call(mockExecuteFunction, 'url', mockUrl, 0);
expect(mockHttpRequest).toHaveBeenCalledWith({
url: mockUrl,
json: false,
encoding: 'arraybuffer',
});
expect(result).toBe(mockBuffer);
});
it('should create buffer from binary data', async () => {
const mockBinaryPropertyName = 'data';
const mockBuffer = [1, 2, 3];
// Mock getBinaryDataBuffer
const mockGetBinaryDataBuffer = jest.fn().mockResolvedValue(mockBuffer);
// Create mock execute function with getBinaryDataBuffer helper
const mockExecuteFunction = createMockExecuteFunction({});
mockExecuteFunction.helpers.getBinaryDataBuffer = mockGetBinaryDataBuffer;
const result = await helpers.createFileBuffer.call(
mockExecuteFunction,
'binary',
mockBinaryPropertyName,
0,
);
expect(mockGetBinaryDataBuffer).toHaveBeenCalledWith(0, mockBinaryPropertyName);
expect(result).toBe(mockBuffer);
});
it('should throw error for unsupported source type', async () => {
await expect(
helpers.createFileBuffer.call(
createMockExecuteFunction({}),
'invalid-source',
'test-value',
0,
),
).rejects.toThrow();
});
});
});
@@ -0,0 +1,57 @@
import get from 'lodash/get';
import { constructExecutionMetaData } from 'n8n-core';
import type {
IDataObject,
IExecuteFunctions,
IGetNodeParameterOptions,
INode,
INodeExecutionData,
} from 'n8n-workflow';
export const node: INode = {
id: '1',
name: 'Airtop node',
typeVersion: 1,
type: 'n8n-nodes-base.airtop',
position: [10, 10],
parameters: {},
};
export const createMockExecuteFunction = (nodeParameters: IDataObject) => {
const fakeExecuteFunction = {
getInputData(): INodeExecutionData[] {
return [{ json: {} }];
},
getNodeParameter(
parameterName: string,
_itemIndex: number,
fallbackValue?: IDataObject,
options?: IGetNodeParameterOptions,
) {
const parameter = options?.extractValue ? `${parameterName}.value` : parameterName;
return get(nodeParameters, parameter, fallbackValue);
},
getNode() {
return node;
},
helpers: {
constructExecutionMetaData,
returnJsonArray: (data: IDataObject | IDataObject[]) => {
return [{ json: data }] as INodeExecutionData[];
},
prepareBinaryData: async (data: Buffer) => {
return {
mimeType: 'image/jpeg',
fileType: 'jpg',
fileName: 'screenshot.jpg',
data: data.toString('base64'),
};
},
},
continueOnFail: () => false,
logger: {
info: () => {},
},
} as unknown as IExecuteFunctions;
return fakeExecuteFunction;
};
@@ -0,0 +1,182 @@
import nock from 'nock';
import * as click from '../../../actions/interaction/click.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const baseNodeParameters = {
resource: 'interaction',
operation: 'click',
sessionId: 'test-session-123',
windowId: 'win-123',
elementDescription: 'the login button',
clickType: 'click',
additionalFields: {},
};
const mockResponse = {
success: true,
message: 'Click executed successfully',
};
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(async function () {
return {
status: 'success',
data: mockResponse,
};
}),
};
});
describe('Test Airtop, click operation', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../transport');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should execute click with minimal parameters', async () => {
const result = await click.execute.call(createMockExecuteFunction(baseNodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/click',
{
elementDescription: 'the login button',
configuration: {
clickType: 'click',
},
},
);
expect(result).toEqual([
{
json: {
sessionId: baseNodeParameters.sessionId,
windowId: baseNodeParameters.windowId,
status: 'success',
data: mockResponse,
},
},
]);
});
it("should throw error when 'elementDescription' parameter is empty", async () => {
const nodeParameters = {
...baseNodeParameters,
elementDescription: '',
};
const errorMessage = ERROR_MESSAGES.REQUIRED_PARAMETER.replace(
'{{field}}',
'Element Description',
);
await expect(click.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
errorMessage,
);
});
it("should include 'visualScope' parameter when specified", async () => {
const nodeParameters = {
...baseNodeParameters,
additionalFields: {
visualScope: 'viewport',
},
};
await click.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/click',
{
configuration: {
visualAnalysis: {
scope: 'viewport',
},
clickType: 'click',
},
elementDescription: 'the login button',
},
);
});
it("should include 'waitForNavigation' parameter when specified", async () => {
const nodeParameters = {
...baseNodeParameters,
additionalFields: {
waitForNavigation: 'load',
},
};
await click.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/click',
{
configuration: {
clickType: 'click',
waitForNavigationConfig: {
waitUntil: 'load',
},
},
waitForNavigation: true,
elementDescription: 'the login button',
},
);
});
it("should execute double click when 'clickType' is 'doubleClick'", async () => {
const nodeParameters = {
...baseNodeParameters,
clickType: 'doubleClick',
};
await click.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/click',
{
elementDescription: 'the login button',
configuration: {
clickType: 'doubleClick',
},
},
);
});
it("should execute right click when 'clickType' is 'rightClick'", async () => {
const nodeParameters = {
...baseNodeParameters,
clickType: 'rightClick',
};
await click.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/click',
{
elementDescription: 'the login button',
configuration: {
clickType: 'rightClick',
},
},
);
});
});
@@ -0,0 +1,147 @@
import * as fill from '../../../actions/interaction/fill.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const baseNodeParameters = {
resource: 'interaction',
operation: 'fill',
sessionId: 'test-session-123',
windowId: 'win-123',
formData: 'Name: John Doe, Email: john@example.com',
};
const mockAsyncResponse = {
requestId: 'req-123',
status: 'pending',
};
const mockCompletedResponse = {
status: 'completed',
data: {
success: true,
message: 'Form filled successfully',
},
};
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(),
};
});
describe('Test Airtop, fill form operation', () => {
afterAll(() => {
jest.unmock('../../../transport');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should execute fill operation successfully', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
// Mock the initial async request
apiRequestMock.mockResolvedValueOnce(mockAsyncResponse);
// Mock the status check to return completed after first pending
apiRequestMock
.mockResolvedValueOnce({ ...mockAsyncResponse })
.mockResolvedValueOnce(mockCompletedResponse);
const result = await fill.execute.call(createMockExecuteFunction(baseNodeParameters), 0);
expect(apiRequestMock).toHaveBeenCalledWith(
'POST',
'/async/sessions/test-session-123/windows/win-123/execute-automation',
{
automationId: 'auto',
parameters: {
customData: 'Name: John Doe, Email: john@example.com',
},
},
);
expect(apiRequestMock).toHaveBeenCalledWith('GET', '/requests/req-123/status');
expect(result).toEqual([
{
json: {
sessionId: baseNodeParameters.sessionId,
windowId: baseNodeParameters.windowId,
status: 'completed',
data: {
success: true,
message: 'Form filled successfully',
},
},
},
]);
});
it("should throw error when 'formData' parameter is empty", async () => {
const nodeParameters = {
...baseNodeParameters,
formData: '',
};
await expect(fill.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.REQUIRED_PARAMETER.replace('{{field}}', 'Form Data'),
);
});
it('should throw error when operation times out after 2 sec', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
const nodeParameters = {
...baseNodeParameters,
};
const timeout = 2000;
// Mock the initial async request
apiRequestMock.mockResolvedValueOnce(mockAsyncResponse);
// Return pending on all requests
apiRequestMock.mockResolvedValue({ ...mockAsyncResponse });
// should throw NodeApiError
await expect(
fill.execute.call(createMockExecuteFunction(nodeParameters), 0, timeout),
).rejects.toThrow('The service was not able to process your request');
});
it('should handle error status in response', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
const errorResponse = {
status: 'error',
error: {
message: 'Failed to fill form',
},
};
// Mock the initial async request
apiRequestMock.mockResolvedValueOnce(mockAsyncResponse);
// Mock the status check to return error
apiRequestMock
.mockResolvedValueOnce({ ...mockAsyncResponse })
.mockResolvedValueOnce(errorResponse);
const result = await fill.execute.call(createMockExecuteFunction(baseNodeParameters), 0);
expect(result).toEqual([
{
json: {
sessionId: baseNodeParameters.sessionId,
windowId: baseNodeParameters.windowId,
status: 'error',
error: {
message: 'Failed to fill form',
},
},
},
]);
});
});
@@ -0,0 +1,77 @@
import { constructInteractionRequest } from '../../../actions/interaction/helpers';
import { createMockExecuteFunction } from '../helpers';
describe('Test Airtop interaction helpers', () => {
describe('constructInteractionRequest', () => {
it('should construct basic request with default values', () => {
const mockExecute = createMockExecuteFunction({
additionalFields: {},
});
const request = constructInteractionRequest.call(mockExecute, 0);
expect(request).toEqual({
configuration: {},
});
});
it("should include 'visualScope' parameter when specified", () => {
const mockExecute = createMockExecuteFunction({
additionalFields: {
visualScope: 'viewport',
},
});
const request = constructInteractionRequest.call(mockExecute, 0);
expect(request).toEqual({
configuration: {
visualAnalysis: {
scope: 'viewport',
},
},
});
});
it("should include 'waitForNavigation' parameter when specified", () => {
const mockExecute = createMockExecuteFunction({
additionalFields: {
waitForNavigation: 'load',
},
});
const request = constructInteractionRequest.call(mockExecute, 0);
expect(request).toEqual({
configuration: {
waitForNavigationConfig: {
waitUntil: 'load',
},
},
waitForNavigation: true,
});
});
it('should merge additional parameters', () => {
const mockExecute = createMockExecuteFunction({
additionalFields: {
waitForNavigation: 'load',
},
});
const request = constructInteractionRequest.call(mockExecute, 0, {
elementDescription: 'test element',
});
expect(request).toEqual({
configuration: {
waitForNavigationConfig: {
waitUntil: 'load',
},
},
waitForNavigation: true,
elementDescription: 'test element',
});
});
});
});
@@ -0,0 +1,137 @@
import nock from 'nock';
import * as hover from '../../../actions/interaction/hover.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const baseNodeParameters = {
resource: 'interaction',
operation: 'hover',
sessionId: 'test-session-123',
windowId: 'win-123',
elementDescription: 'the user profile image',
additionalFields: {},
};
const mockResponse = {
success: true,
message: 'Hover interaction executed successfully',
};
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(async function () {
return {
status: 'success',
data: mockResponse,
};
}),
};
});
describe('Test Airtop, hover operation', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../transport');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should execute hover with minimal parameters', async () => {
const result = await hover.execute.call(createMockExecuteFunction(baseNodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/hover',
{
configuration: {},
elementDescription: 'the user profile image',
},
);
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
windowId: 'win-123',
status: 'success',
data: mockResponse,
},
},
]);
});
it("should throw error when 'elementDescription' parameter is empty", async () => {
const nodeParameters = {
...baseNodeParameters,
elementDescription: '',
};
const errorMessage = ERROR_MESSAGES.REQUIRED_PARAMETER.replace(
'{{field}}',
'Element Description',
);
await expect(hover.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
errorMessage,
);
});
it("should include 'visualScope' parameter when specified", async () => {
const nodeParameters = {
...baseNodeParameters,
additionalFields: {
visualScope: 'viewport',
},
};
await hover.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/hover',
{
configuration: {
visualAnalysis: {
scope: 'viewport',
},
},
elementDescription: 'the user profile image',
},
);
});
it("should include 'waitForNavigation' parameter when specified", async () => {
const nodeParameters = {
...baseNodeParameters,
additionalFields: {
waitForNavigation: 'load',
},
};
await hover.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/hover',
{
configuration: {
waitForNavigationConfig: {
waitUntil: 'load',
},
},
waitForNavigation: true,
elementDescription: 'the user profile image',
},
);
});
});
@@ -0,0 +1,170 @@
import * as scroll from '../../../actions/interaction/scroll.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const baseNodeParameters = {
resource: 'interaction',
operation: 'scroll',
sessionId: 'test-session-123',
windowId: 'win-123',
additionalFields: {},
};
const baseAutomaticNodeParameters = {
...baseNodeParameters,
scrollingMode: 'automatic',
scrollToElement: 'the bottom of the page',
scrollWithin: '',
};
const baseManualNodeParameters = {
...baseNodeParameters,
scrollingMode: 'manual',
scrollToEdge: {
edgeValues: {
yAxis: 'bottom',
xAxis: '',
},
},
scrollBy: {
scrollValues: {
yAxis: '200px',
xAxis: '',
},
},
};
const mockResponse = {
success: true,
message: 'Scrolled successfully',
};
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(),
};
});
describe('Test Airtop, scroll operation', () => {
afterAll(() => {
jest.unmock('../../../transport');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should execute automatic scroll operation successfully', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValueOnce(mockResponse);
const result = await scroll.execute.call(
createMockExecuteFunction(baseAutomaticNodeParameters),
0,
);
expect(apiRequestMock).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/scroll',
{
scrollToElement: 'the bottom of the page',
configuration: {},
},
);
expect(result).toEqual([
{
json: {
sessionId: baseAutomaticNodeParameters.sessionId,
windowId: baseAutomaticNodeParameters.windowId,
success: true,
message: 'Scrolled successfully',
},
},
]);
});
it('should execute manual scroll operation successfully', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
apiRequestMock.mockResolvedValueOnce(mockResponse);
const result = await scroll.execute.call(
createMockExecuteFunction(baseManualNodeParameters),
0,
);
expect(apiRequestMock).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/scroll',
{
configuration: {},
scrollToEdge: {
yAxis: 'bottom',
xAxis: '',
},
scrollBy: {
yAxis: '200px',
xAxis: '',
},
},
);
expect(result).toEqual([
{
json: {
sessionId: baseManualNodeParameters.sessionId,
windowId: baseManualNodeParameters.windowId,
success: true,
message: 'Scrolled successfully',
},
},
]);
});
it("should throw error when scrollingMode is 'automatic' and 'scrollToElement' parameter is empty", async () => {
const nodeParameters = {
...baseAutomaticNodeParameters,
scrollToElement: '',
};
await expect(scroll.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.REQUIRED_PARAMETER.replace('{{field}}', 'Element Description'),
);
});
it("should validate scroll amount formats when scrollingMode is 'manual'", async () => {
const invalidNodeParameters = {
...baseManualNodeParameters,
scrollBy: {
scrollValues: {
yAxis: 'one hundred pixels',
xAxis: '',
},
},
};
await expect(
scroll.execute.call(createMockExecuteFunction(invalidNodeParameters), 0),
).rejects.toThrow(ERROR_MESSAGES.SCROLL_BY_AMOUNT_INVALID);
});
it('should throw an error when the API returns an error response', async () => {
const apiRequestMock = transport.apiRequest as jest.Mock;
const errorResponse = {
errors: [
{
message: 'Failed to scroll',
},
],
};
apiRequestMock.mockResolvedValueOnce(errorResponse);
await expect(
scroll.execute.call(createMockExecuteFunction(baseAutomaticNodeParameters), 0),
).rejects.toThrow('Failed to scroll');
});
});
@@ -0,0 +1,181 @@
import nock from 'nock';
import * as type from '../../../actions/interaction/type.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const baseNodeParameters = {
resource: 'interaction',
operation: 'type',
sessionId: 'test-session-123',
windowId: 'win-123',
text: 'Hello World',
pressEnterKey: false,
elementDescription: '',
additionalFields: {},
};
const mockResponse = {
success: true,
message: 'Text typed successfully',
};
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(async function () {
return {
status: 'success',
data: mockResponse,
};
}),
};
});
describe('Test Airtop, type operation', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../transport');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should execute type with minimal parameters', async () => {
const result = await type.execute.call(createMockExecuteFunction(baseNodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/type',
{
configuration: {},
text: 'Hello World',
pressEnterKey: false,
elementDescription: '',
},
);
expect(result).toEqual([
{
json: {
sessionId: baseNodeParameters.sessionId,
windowId: baseNodeParameters.windowId,
status: 'success',
data: mockResponse,
},
},
]);
});
it("should throw error when 'text' parameter is empty", async () => {
const nodeParameters = {
...baseNodeParameters,
text: '',
};
await expect(type.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.REQUIRED_PARAMETER.replace('{{field}}', 'Text'),
);
});
it("should include 'elementDescription' parameter when specified", async () => {
const nodeParameters = {
...baseNodeParameters,
elementDescription: 'the search box',
};
await type.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/type',
{
configuration: {},
text: 'Hello World',
pressEnterKey: false,
elementDescription: 'the search box',
},
);
});
it("should include 'pressEnterKey' parameter when specified", async () => {
const nodeParameters = {
...baseNodeParameters,
pressEnterKey: true,
};
await type.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/type',
{
configuration: {},
text: 'Hello World',
pressEnterKey: true,
elementDescription: '',
},
);
});
it("should include 'waitForNavigation' parameter when specified", async () => {
const nodeParameters = {
...baseNodeParameters,
additionalFields: {
waitForNavigation: 'load',
},
};
await type.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/type',
{
configuration: {
waitForNavigationConfig: {
waitUntil: 'load',
},
},
waitForNavigation: true,
text: 'Hello World',
pressEnterKey: false,
elementDescription: '',
},
);
});
it("should include 'visualScope' parameter when specified", async () => {
const nodeParameters = {
...baseNodeParameters,
additionalFields: {
visualScope: 'viewport',
},
};
await type.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/type',
{
configuration: {
visualAnalysis: {
scope: 'viewport',
},
},
text: 'Hello World',
pressEnterKey: false,
elementDescription: '',
},
);
});
});
@@ -0,0 +1,310 @@
import * as create from '../../../actions/session/create.operation';
import { ERROR_MESSAGES, SESSION_STATUS } from '../../../constants';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const mockCreatedSession = {
data: { id: 'test-session-123', status: SESSION_STATUS.RUNNING },
};
const baseNodeParameters = {
resource: 'session',
operation: 'create',
profileName: 'test-profile',
record: false,
timeoutMinutes: 10,
saveProfileOnTermination: false,
};
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(async function () {
return {
...mockCreatedSession,
};
}),
};
});
describe('Test Airtop, session create operation', () => {
afterAll(() => {
jest.unmock('../../../transport');
});
afterEach(() => {
jest.clearAllMocks();
});
/**
* Minimal parameters
*/
it('should create a session with minimal parameters', async () => {
const nodeParameters = {
...baseNodeParameters,
proxy: 'none',
};
const result = await create.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/sessions', {
configuration: {
profileName: 'test-profile',
solveCaptcha: false,
timeoutMinutes: 10,
record: false,
proxy: false,
},
});
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
data: { ...mockCreatedSession.data },
},
},
]);
});
/**
* Profiles
*/
it('should create a session with save profile enabled', async () => {
const nodeParameters = {
...baseNodeParameters,
saveProfileOnTermination: true,
proxy: 'none',
};
const result = await create.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(2);
expect(transport.apiRequest).toHaveBeenNthCalledWith(1, 'POST', '/sessions', {
configuration: {
profileName: 'test-profile',
solveCaptcha: false,
timeoutMinutes: 10,
record: false,
proxy: false,
},
});
expect(transport.apiRequest).toHaveBeenNthCalledWith(
2,
'PUT',
'/sessions/test-session-123/save-profile-on-termination/test-profile',
);
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
data: { ...mockCreatedSession.data },
},
},
]);
});
/**
* Proxy
*/
it('should create a session with integrated proxy and empty config', async () => {
const nodeParameters = {
...baseNodeParameters,
proxy: 'integrated',
proxyConfig: {}, // simulate integrated proxy with empty config
};
const result = await create.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/sessions', {
configuration: {
profileName: 'test-profile',
solveCaptcha: false,
timeoutMinutes: 10,
record: false,
proxy: true,
},
});
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
data: { ...mockCreatedSession.data },
},
},
]);
});
it('should create a session with integrated proxy and proxy configuration', async () => {
const nodeParameters = {
...baseNodeParameters,
proxy: 'integrated',
proxyConfig: { country: 'US', sticky: true },
};
const result = await create.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/sessions', {
configuration: {
profileName: 'test-profile',
solveCaptcha: false,
timeoutMinutes: 10,
record: false,
proxy: { country: 'US', sticky: true },
},
});
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
data: { ...mockCreatedSession.data },
},
},
]);
});
it('should create a session with proxy URL', async () => {
const nodeParameters = {
...baseNodeParameters,
proxy: 'proxyUrl',
proxyUrl: 'http://proxy.example.com:8080',
};
const result = await create.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/sessions', {
configuration: {
profileName: 'test-profile',
solveCaptcha: false,
timeoutMinutes: 10,
record: false,
proxy: 'http://proxy.example.com:8080',
},
});
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
data: { ...mockCreatedSession.data },
},
},
]);
});
it('should throw error when custom proxy URL is empty', async () => {
const nodeParameters = {
...baseNodeParameters,
proxy: 'proxyUrl',
proxyUrl: '',
};
await expect(create.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.PROXY_URL_REQUIRED,
);
});
/**
* Auto solve captcha
*/
it('should create a session with auto solve captcha enabled', async () => {
const nodeParameters = {
...baseNodeParameters,
additionalFields: {
solveCaptcha: true,
},
};
const result = await create.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/sessions', {
configuration: {
profileName: 'test-profile',
solveCaptcha: true,
timeoutMinutes: 10,
record: false,
proxy: false,
},
});
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
data: { ...mockCreatedSession.data },
},
},
]);
});
/**
* Chrome extensions
*/
it('should create a session with chrome extensions enabled', async () => {
const nodeParameters = {
...baseNodeParameters,
additionalFields: {
extensionIds: 'extId1, extId2',
},
};
const result = await create.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/sessions', {
configuration: {
profileName: 'test-profile',
solveCaptcha: false,
timeoutMinutes: 10,
record: false,
proxy: false,
extensionIds: ['extId1', 'extId2'],
},
});
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
data: { ...mockCreatedSession.data },
},
},
]);
});
/**
* Session recording
*/
it('should create a session with recording enabled', async () => {
const nodeParameters = {
...baseNodeParameters,
record: true,
proxy: 'none',
};
const result = await create.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith('POST', '/sessions', {
configuration: {
profileName: 'test-profile',
solveCaptcha: false,
timeoutMinutes: 10,
record: true,
proxy: false,
},
});
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
data: { ...mockCreatedSession.data },
},
},
]);
});
});
@@ -0,0 +1,103 @@
import * as save from '../../../actions/session/save.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(async function () {
return {
status: 'success',
message: 'Profile will be saved on session termination',
};
}),
};
});
const baseParameters = {
resource: 'session',
operation: 'save',
sessionId: 'test-session-123',
profileName: 'test-profile',
};
describe('Test Airtop, session save operation', () => {
afterAll(() => {
jest.unmock('../../../transport');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should save a profile on session termination successfully', async () => {
const nodeParameters = {
...baseParameters,
};
const result = await save.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'PUT',
'/sessions/test-session-123/save-profile-on-termination/test-profile',
);
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
profileName: 'test-profile',
status: 'success',
message: 'Profile will be saved on session termination',
},
},
]);
});
it('should throw error when sessionId is empty', async () => {
const nodeParameters = {
...baseParameters,
sessionId: '',
};
await expect(save.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.SESSION_ID_REQUIRED,
);
});
it('should throw error when sessionId is whitespace', async () => {
const nodeParameters = {
...baseParameters,
sessionId: ' ',
};
await expect(save.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.SESSION_ID_REQUIRED,
);
});
it('should throw error when profileName is empty', async () => {
const nodeParameters = {
...baseParameters,
profileName: '',
};
await expect(save.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
"Please fill the 'Profile Name' parameter",
);
});
it('should throw error when profileName is whitespace', async () => {
const nodeParameters = {
...baseParameters,
profileName: ' ',
};
await expect(save.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
"Please fill the 'Profile Name' parameter",
);
});
});
@@ -0,0 +1,71 @@
import * as terminate from '../../../actions/session/terminate.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(async function () {
return {
status: 'success',
};
}),
};
});
describe('Test Airtop, session terminate operation', () => {
afterAll(() => {
jest.unmock('../../../transport');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should terminate a session successfully', async () => {
const nodeParameters = {
resource: 'session',
operation: 'terminate',
sessionId: 'test-session-123',
};
const result = await terminate.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith('DELETE', '/sessions/test-session-123');
expect(result).toEqual([
{
json: {
success: true,
},
},
]);
});
it('should throw error when sessionId is empty', async () => {
const nodeParameters = {
resource: 'session',
operation: 'terminate',
sessionId: '',
};
await expect(
terminate.execute.call(createMockExecuteFunction(nodeParameters), 0),
).rejects.toThrow(ERROR_MESSAGES.SESSION_ID_REQUIRED);
});
it('should throw error when sessionId is whitespace', async () => {
const nodeParameters = {
resource: 'session',
operation: 'terminate',
sessionId: ' ',
};
await expect(
terminate.execute.call(createMockExecuteFunction(nodeParameters), 0),
).rejects.toThrow(ERROR_MESSAGES.SESSION_ID_REQUIRED);
});
});
@@ -0,0 +1,85 @@
import * as waitForDownload from '../../../actions/session/waitForDownload.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as GenericFunctions from '../../../GenericFunctions';
import { createMockExecuteFunction } from '../helpers';
jest.mock('../../../GenericFunctions', () => {
const originalModule = jest.requireActual<typeof GenericFunctions>('../../../GenericFunctions');
return {
...originalModule,
waitForSessionEvent: jest.fn(),
};
});
describe('Test Airtop, session waitForDownload operation', () => {
afterAll(() => {
jest.unmock('../../../GenericFunctions');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should wait for download successfully', async () => {
const mockEvent = {
event: 'file_status',
status: 'available',
fileId: 'test-file-123',
downloadUrl: 'https://example.com/download/test-file-123',
};
(GenericFunctions.waitForSessionEvent as jest.Mock).mockResolvedValue(mockEvent);
const nodeParameters = {
resource: 'session',
operation: 'waitForDownload',
sessionId: 'test-session-123',
timeout: 1,
};
const result = await waitForDownload.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(GenericFunctions.waitForSessionEvent).toHaveBeenCalledTimes(1);
expect(GenericFunctions.waitForSessionEvent).toHaveBeenCalledWith(
'test-session-123',
expect.any(Function),
1,
);
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
data: {
fileId: 'test-file-123',
downloadUrl: 'https://example.com/download/test-file-123',
},
},
},
]);
});
it('should throw error when sessionId is empty', async () => {
const nodeParameters = {
resource: 'session',
operation: 'waitForDownload',
sessionId: '',
};
await expect(
waitForDownload.execute.call(createMockExecuteFunction(nodeParameters), 0),
).rejects.toThrow(ERROR_MESSAGES.SESSION_ID_REQUIRED);
});
it('should throw error when sessionId is whitespace', async () => {
const nodeParameters = {
resource: 'session',
operation: 'waitForDownload',
sessionId: ' ',
};
await expect(
waitForDownload.execute.call(createMockExecuteFunction(nodeParameters), 0),
).rejects.toThrow(ERROR_MESSAGES.SESSION_ID_REQUIRED);
});
});
@@ -0,0 +1,92 @@
import nock from 'nock';
import * as close from '../../../actions/window/close.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(async function () {
return {
status: 'success',
data: {
closed: true,
},
};
}),
};
});
describe('Test Airtop, window close operation', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../transport');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should close a window successfully', async () => {
const nodeParameters = {
resource: 'window',
operation: 'close',
sessionId: 'test-session-123',
windowId: 'win-123',
};
const result = await close.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'DELETE',
'/sessions/test-session-123/windows/win-123',
);
expect(result).toEqual([
{
json: {
sessionId: 'test-session-123',
windowId: 'win-123',
status: 'success',
data: {
closed: true,
},
},
},
]);
});
it('should throw error when sessionId is empty', async () => {
const nodeParameters = {
resource: 'window',
operation: 'close',
sessionId: '',
windowId: 'win-123',
};
await expect(close.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.SESSION_ID_REQUIRED,
);
});
it('should throw error when windowId is empty', async () => {
const nodeParameters = {
resource: 'window',
operation: 'close',
sessionId: 'test-session-123',
windowId: '',
};
await expect(close.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.WINDOW_ID_REQUIRED,
);
});
});
@@ -0,0 +1,308 @@
import nock from 'nock';
import * as create from '../../../actions/window/create.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const baseNodeParameters = {
resource: 'window',
operation: 'create',
sessionId: 'test-session-123',
url: 'https://example.com',
getLiveView: false,
disableResize: false,
additionalFields: {},
};
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(async function (method: string) {
if (method === 'GET') {
return {
status: 'success',
data: {
liveViewUrl: 'https://live.airtop.ai/123-abcd',
},
};
}
return {
status: 'success',
data: {
windowId: 'win-123',
},
};
}),
};
});
describe('Test Airtop, window create operation', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../transport');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should create a window with minimal parameters', async () => {
const result = await create.execute.call(createMockExecuteFunction(baseNodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows',
{
url: 'https://example.com',
},
);
expect(result).toEqual([
{
json: {
sessionId: baseNodeParameters.sessionId,
windowId: 'win-123',
status: 'success',
data: {
windowId: 'win-123',
},
},
},
]);
});
it('should create a window with live view', async () => {
const nodeParameters = {
...baseNodeParameters,
getLiveView: true,
};
const result = await create.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(2);
expect(transport.apiRequest).toHaveBeenNthCalledWith(
1,
'POST',
'/sessions/test-session-123/windows',
{
url: 'https://example.com',
},
);
expect(transport.apiRequest).toHaveBeenNthCalledWith(
2,
'GET',
'/sessions/test-session-123/windows/win-123',
undefined,
{},
);
expect(result).toEqual([
{
json: {
sessionId: baseNodeParameters.sessionId,
windowId: 'win-123',
status: 'success',
data: {
liveViewUrl: 'https://live.airtop.ai/123-abcd',
},
},
},
]);
});
it('should create a window with live view and disabled resize', async () => {
const nodeParameters = {
...baseNodeParameters,
getLiveView: true,
disableResize: true,
};
const result = await create.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(2);
expect(transport.apiRequest).toHaveBeenNthCalledWith(
1,
'POST',
'/sessions/test-session-123/windows',
{
url: 'https://example.com',
},
);
expect(transport.apiRequest).toHaveBeenNthCalledWith(
2,
'GET',
'/sessions/test-session-123/windows/win-123',
undefined,
{ disableResize: true },
);
expect(result).toEqual([
{
json: {
sessionId: baseNodeParameters.sessionId,
windowId: 'win-123',
status: 'success',
data: {
liveViewUrl: 'https://live.airtop.ai/123-abcd',
},
},
},
]);
});
it('should create a window with live view and navigation bar', async () => {
const nodeParameters = {
...baseNodeParameters,
getLiveView: true,
includeNavigationBar: true,
};
const result = await create.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(2);
expect(transport.apiRequest).toHaveBeenNthCalledWith(
1,
'POST',
'/sessions/test-session-123/windows',
{
url: 'https://example.com',
},
);
expect(transport.apiRequest).toHaveBeenNthCalledWith(
2,
'GET',
'/sessions/test-session-123/windows/win-123',
undefined,
{ includeNavigationBar: true },
);
expect(result).toEqual([
{
json: {
sessionId: baseNodeParameters.sessionId,
windowId: 'win-123',
status: 'success',
data: {
liveViewUrl: 'https://live.airtop.ai/123-abcd',
},
},
},
]);
});
it('should create a window with live view and screen resolution', async () => {
const nodeParameters = {
...baseNodeParameters,
getLiveView: true,
screenResolution: '1280x720',
};
const result = await create.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(2);
expect(transport.apiRequest).toHaveBeenNthCalledWith(
1,
'POST',
'/sessions/test-session-123/windows',
{
url: 'https://example.com',
},
);
expect(transport.apiRequest).toHaveBeenNthCalledWith(
2,
'GET',
'/sessions/test-session-123/windows/win-123',
undefined,
{ screenResolution: '1280x720' },
);
expect(result).toEqual([
{
json: {
sessionId: baseNodeParameters.sessionId,
windowId: 'win-123',
status: 'success',
data: {
liveViewUrl: 'https://live.airtop.ai/123-abcd',
},
},
},
]);
});
it('should create a window with all live view options', async () => {
const nodeParameters = {
...baseNodeParameters,
getLiveView: true,
includeNavigationBar: true,
screenResolution: '1920x1080',
disableResize: true,
};
const result = await create.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(2);
expect(transport.apiRequest).toHaveBeenNthCalledWith(
1,
'POST',
'/sessions/test-session-123/windows',
{
url: 'https://example.com',
},
);
expect(transport.apiRequest).toHaveBeenNthCalledWith(
2,
'GET',
'/sessions/test-session-123/windows/win-123',
undefined,
{
includeNavigationBar: true,
screenResolution: '1920x1080',
disableResize: true,
},
);
expect(result).toEqual([
{
json: {
sessionId: baseNodeParameters.sessionId,
windowId: 'win-123',
status: 'success',
data: {
liveViewUrl: 'https://live.airtop.ai/123-abcd',
},
},
},
]);
});
it("should throw error when 'sessionId' parameter is empty", async () => {
const nodeParameters = {
...baseNodeParameters,
sessionId: '',
};
await expect(create.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.SESSION_ID_REQUIRED,
);
});
it('should throw error when screen resolution format is invalid', async () => {
const nodeParameters = {
...baseNodeParameters,
getLiveView: true,
screenResolution: 'invalid-format',
};
await expect(create.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.SCREEN_RESOLUTION_INVALID,
);
});
});
@@ -0,0 +1,115 @@
import nock from 'nock';
import * as load from '../../../actions/window/load.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const baseNodeParameters = {
resource: 'window',
operation: 'load',
sessionId: 'test-session-123',
windowId: 'win-123',
url: 'https://example.com',
additionalFields: {},
};
const mockResponse = {
success: true,
message: 'Page loaded successfully',
};
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(async function () {
return {
status: 'success',
data: mockResponse,
};
}),
};
});
describe('Test Airtop, window load operation', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../transport');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should load URL with minimal parameters', async () => {
const result = await load.execute.call(createMockExecuteFunction(baseNodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123',
{
url: 'https://example.com',
},
);
expect(result).toEqual([
{
json: {
sessionId: baseNodeParameters.sessionId,
windowId: baseNodeParameters.windowId,
status: 'success',
data: mockResponse,
},
},
]);
});
it('should throw error when URL is empty', async () => {
const nodeParameters = {
...baseNodeParameters,
url: '',
};
const errorMessage = ERROR_MESSAGES.REQUIRED_PARAMETER.replace('{{field}}', 'URL');
await expect(load.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
errorMessage,
);
});
it('should throw error when URL is invalid', async () => {
const nodeParameters = {
...baseNodeParameters,
url: 'not-a-valid-url',
};
await expect(load.execute.call(createMockExecuteFunction(nodeParameters), 0)).rejects.toThrow(
ERROR_MESSAGES.URL_INVALID,
);
});
it("should include 'waitUntil' parameter when specified", async () => {
const nodeParameters = {
...baseNodeParameters,
additionalFields: {
waitUntil: 'domContentLoaded',
},
};
await load.execute.call(createMockExecuteFunction(nodeParameters), 0);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123',
{
url: 'https://example.com',
waitUntil: 'domContentLoaded',
},
);
});
});
@@ -0,0 +1,135 @@
import * as takeScreenshot from '../../../actions/window/takeScreenshot.operation';
import { ERROR_MESSAGES } from '../../../constants';
import * as GenericFunctions from '../../../GenericFunctions';
import * as transport from '../../../transport';
import { createMockExecuteFunction } from '../helpers';
const baseNodeParameters = {
resource: 'window',
operation: 'takeScreenshot',
sessionId: 'test-session-123',
windowId: 'win-123',
};
const mockResponse = {
meta: {
screenshots: [{ dataUrl: 'base64-encoded-image-data' }],
},
};
const mockBinaryBuffer = Buffer.from('mock-binary-data');
const expectedJsonResult = {
json: {
sessionId: 'test-session-123',
windowId: 'win-123',
image: 'base64-encoded-image-data',
},
};
const expectedBinaryResult = {
binary: {
data: {
mimeType: 'image/jpeg',
fileType: 'jpg',
fileName: 'screenshot.jpg',
data: mockBinaryBuffer.toString('base64'),
},
},
};
jest.mock('../../../transport', () => {
const originalModule = jest.requireActual<typeof transport>('../../../transport');
return {
...originalModule,
apiRequest: jest.fn(async function () {
return {
status: 'success',
...mockResponse,
};
}),
};
});
jest.mock('../../../GenericFunctions', () => {
const originalModule = jest.requireActual<typeof GenericFunctions>('../../../GenericFunctions');
return {
...originalModule,
convertScreenshotToBinary: jest.fn(() => mockBinaryBuffer),
};
});
describe('Test Airtop, take screenshot operation', () => {
afterAll(() => {
jest.unmock('../../../transport');
jest.unmock('../../../GenericFunctions');
});
afterEach(() => {
jest.clearAllMocks();
});
it('should take screenshot in base64 format', async () => {
const result = await takeScreenshot.execute.call(
createMockExecuteFunction({ ...baseNodeParameters }),
0,
);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/screenshot',
);
expect(result).toEqual([{ ...expectedJsonResult }]);
});
it('should take screenshot in binary format', async () => {
const result = await takeScreenshot.execute.call(
createMockExecuteFunction({
...baseNodeParameters,
outputImageAsBinary: true,
}),
0,
);
expect(transport.apiRequest).toHaveBeenCalledTimes(1);
expect(transport.apiRequest).toHaveBeenCalledWith(
'POST',
'/sessions/test-session-123/windows/win-123/screenshot',
);
expect(GenericFunctions.convertScreenshotToBinary).toHaveBeenCalledWith(
mockResponse.meta.screenshots[0],
);
expect(result).toEqual([
{
json: { ...expectedJsonResult.json, image: '' },
...expectedBinaryResult,
},
]);
});
it('should throw error when sessionId is empty', async () => {
const nodeParameters = {
...baseNodeParameters,
sessionId: '',
};
await expect(
takeScreenshot.execute.call(createMockExecuteFunction(nodeParameters), 0),
).rejects.toThrow(ERROR_MESSAGES.SESSION_ID_REQUIRED);
});
it('should throw error when windowId is empty', async () => {
const nodeParameters = {
...baseNodeParameters,
windowId: '',
};
await expect(
takeScreenshot.execute.call(createMockExecuteFunction(nodeParameters), 0),
).rejects.toThrow(ERROR_MESSAGES.WINDOW_ID_REQUIRED);
});
});