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
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:
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user