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

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,148 @@
import type { INodeProperties } from 'n8n-workflow';
import * as click from './click.operation';
import * as fill from './fill.operation';
import * as hover from './hover.operation';
import * as scroll from './scroll.operation';
import * as type from './type.operation';
import { sessionIdField, windowIdField } from '../common/fields';
export { click, fill, hover, scroll, type };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['interaction'],
},
},
options: [
{
name: 'Click an Element',
value: 'click',
description: 'Execute a click on an element given a description',
action: 'Click an element',
},
{
name: 'Fill Form',
value: 'fill',
description: 'Fill a form with the provided information',
action: 'Fill form',
},
{
name: 'Hover on an Element',
value: 'hover',
description: 'Execute a hover action on an element given a description',
action: 'Hover on an element',
},
{
name: 'Scroll',
value: 'scroll',
description: 'Execute a scroll action on the page',
action: 'Scroll on page',
},
{
name: 'Type',
value: 'type',
description: 'Execute a Type action on an element given a description',
action: 'Type text',
},
],
default: 'click',
},
{
...sessionIdField,
displayOptions: {
show: {
resource: ['interaction'],
},
},
},
{
...windowIdField,
displayOptions: {
show: {
resource: ['interaction'],
},
},
},
...click.description,
...fill.description,
...hover.description,
...scroll.description,
...type.description,
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['interaction'],
operation: ['click', 'hover', 'type', 'scroll'],
},
},
options: [
{
displayName: 'Visual Scope',
name: 'visualScope',
type: 'options',
default: 'auto',
description: 'Defines the strategy for visual analysis of the current window',
options: [
{
name: 'Auto',
description: 'Provides the simplest out-of-the-box experience for most web pages',
value: 'auto',
},
{
name: 'Viewport',
description: 'For analysis of the current browser view only',
value: 'viewport',
},
{
name: 'Page',
description: 'For analysis of the entire page',
value: 'page',
},
{
name: 'Scan',
description:
"For a full page analysis on sites that have compatibility issues with 'Page' mode",
value: 'scan',
},
],
},
{
displayName: 'Wait Until Event After Navigation',
name: 'waitForNavigation',
type: 'options',
default: 'load',
description:
"The condition to wait for the navigation to complete after an interaction (click, type or hover). Defaults to 'Fully Loaded'.",
options: [
{
name: 'Fully Loaded (Slower)',
value: 'load',
},
{
name: 'DOM Only Loaded (Faster)',
value: 'domcontentloaded',
},
{
name: 'All Network Activity Has Stopped',
value: 'networkidle0',
},
{
name: 'Most Network Activity Has Stopped',
value: 'networkidle2',
},
],
},
],
},
];
@@ -0,0 +1,88 @@
import {
type IExecuteFunctions,
type INodeExecutionData,
type INodeProperties,
} from 'n8n-workflow';
import { constructInteractionRequest } from './helpers';
import {
validateRequiredStringField,
validateSessionAndWindowId,
validateAirtopApiResponse,
} from '../../GenericFunctions';
import { apiRequest } from '../../transport';
import { elementDescriptionField } from '../common/fields';
export const description: INodeProperties[] = [
{
...elementDescriptionField,
placeholder: 'e.g. the green "save" button at the top of the page',
required: true,
displayOptions: {
show: {
resource: ['interaction'],
operation: ['click'],
},
},
},
{
displayName: 'Click Type',
name: 'clickType',
type: 'options',
default: 'click',
description: 'The type of click to perform. Defaults to left click.',
options: [
{
name: 'Left Click',
value: 'click',
},
{
name: 'Double Click',
value: 'doubleClick',
},
{
name: 'Right Click',
value: 'rightClick',
},
],
displayOptions: {
show: {
resource: ['interaction'],
operation: ['click'],
},
},
},
];
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const { sessionId, windowId } = validateSessionAndWindowId.call(this, index);
const elementDescription = validateRequiredStringField.call(
this,
index,
'elementDescription',
'Element Description',
);
const clickType = validateRequiredStringField.call(this, index, 'clickType', 'Click Type');
const request = constructInteractionRequest.call(this, index, {
elementDescription,
configuration: {
clickType,
},
});
const response = await apiRequest.call(
this,
'POST',
`/sessions/${sessionId}/windows/${windowId}/click`,
request,
);
validateAirtopApiResponse(this.getNode(), response);
return this.helpers.returnJsonArray({ sessionId, windowId, ...response });
}
@@ -0,0 +1,88 @@
import {
type IExecuteFunctions,
type INodeExecutionData,
type INodeProperties,
NodeApiError,
} from 'n8n-workflow';
import { ERROR_MESSAGES, OPERATION_TIMEOUT } from '../../constants';
import {
validateRequiredStringField,
validateSessionAndWindowId,
validateAirtopApiResponse,
} from '../../GenericFunctions';
import { apiRequest } from '../../transport';
import type { IAirtopResponse } from '../../transport/types';
export const description: INodeProperties[] = [
{
displayName: 'Form Data',
name: 'formData',
type: 'string',
typeOptions: {
rows: 4,
},
required: true,
default: '',
displayOptions: {
show: {
resource: ['interaction'],
operation: ['fill'],
},
},
description: 'The information to fill into the form written in natural language',
placeholder: 'e.g. "Name: John Doe, Email: john.doe@example.com"',
},
];
export async function execute(
this: IExecuteFunctions,
index: number,
timeout = OPERATION_TIMEOUT,
): Promise<INodeExecutionData[]> {
const { sessionId, windowId } = validateSessionAndWindowId.call(this, index);
const formData = validateRequiredStringField.call(this, index, 'formData', 'Form Data');
// run automation
const asyncAutomationResponse = await apiRequest.call(
this,
'POST',
`/async/sessions/${sessionId}/windows/${windowId}/execute-automation`,
{
automationId: 'auto',
parameters: {
customData: formData,
},
},
);
const reqId = asyncAutomationResponse.requestId as string;
// Poll status every second until it's completed or timeout is reached
const startTime = Date.now();
let automationStatusResponse: IAirtopResponse;
while (true) {
automationStatusResponse = await apiRequest.call(this, 'GET', `/requests/${reqId}/status`);
const status = automationStatusResponse?.status ?? '';
validateAirtopApiResponse(this.getNode(), automationStatusResponse);
if (status === 'completed' || status === 'error') {
break;
}
const elapsedTime = Date.now() - startTime;
if (elapsedTime >= timeout) {
throw new NodeApiError(this.getNode(), {
message: ERROR_MESSAGES.TIMEOUT_REACHED,
code: 500,
});
}
// Wait one second
await new Promise((resolve) => setTimeout(resolve, 1000));
}
return this.helpers.returnJsonArray({ sessionId, windowId, ...automationStatusResponse });
}
@@ -0,0 +1,32 @@
import type { IExecuteFunctions } from 'n8n-workflow';
import type { IAirtopInteractionRequest } from '../../transport/types';
export function constructInteractionRequest(
this: IExecuteFunctions,
index: number,
parameters: Partial<IAirtopInteractionRequest> = {},
): IAirtopInteractionRequest {
const additionalFields = this.getNodeParameter('additionalFields', index);
const request: IAirtopInteractionRequest = {
...parameters,
configuration: {
...(parameters.configuration ?? {}),
},
};
if (additionalFields.visualScope) {
request.configuration.visualAnalysis = {
scope: additionalFields.visualScope as string,
};
}
if (additionalFields.waitForNavigation) {
request.waitForNavigation = true;
request.configuration.waitForNavigationConfig = {
waitUntil: additionalFields.waitForNavigation as string,
};
}
return request;
}
@@ -0,0 +1,56 @@
import {
type IExecuteFunctions,
type INodeExecutionData,
type INodeProperties,
} from 'n8n-workflow';
import { constructInteractionRequest } from './helpers';
import {
validateRequiredStringField,
validateSessionAndWindowId,
validateAirtopApiResponse,
} from '../../GenericFunctions';
import { apiRequest } from '../../transport';
import { elementDescriptionField } from '../common/fields';
export const description: INodeProperties[] = [
{
...elementDescriptionField,
required: true,
placeholder: 'e.g. the rounded user profile image at the top right of the page',
displayOptions: {
show: {
resource: ['interaction'],
operation: ['hover'],
},
},
},
];
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const { sessionId, windowId } = validateSessionAndWindowId.call(this, index);
const elementDescription = validateRequiredStringField.call(
this,
index,
'elementDescription',
'Element Description',
);
const request = constructInteractionRequest.call(this, index, {
elementDescription,
});
const response = await apiRequest.call(
this,
'POST',
`/sessions/${sessionId}/windows/${windowId}/hover`,
request,
);
validateAirtopApiResponse(this.getNode(), response);
return this.helpers.returnJsonArray({ sessionId, windowId, ...response });
}
@@ -0,0 +1,224 @@
import type {
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { constructInteractionRequest } from './helpers';
import {
validateRequiredStringField,
validateSessionAndWindowId,
validateAirtopApiResponse,
validateScrollByAmount,
validateScrollingMode,
} from '../../GenericFunctions';
import { apiRequest } from '../../transport';
export const description: INodeProperties[] = [
{
displayName: 'Scroll Mode',
name: 'scrollingMode',
type: 'options',
description: 'Choose the mode of scrolling',
options: [
{
name: 'Automatic',
value: 'automatic',
description: 'Describe with natural language the element to scroll to',
},
{
name: 'Manual',
value: 'manual',
description: 'Define the direction and amount to scroll by',
},
],
default: 'automatic',
required: true,
displayOptions: {
show: {
resource: ['interaction'],
operation: ['scroll'],
},
},
},
{
displayName: 'Element Description',
default: '',
description: 'A natural language description of the element to scroll to',
name: 'scrollToElement',
type: 'string',
placeholder: 'e.g. the page section titled "Contact Us"',
required: true,
displayOptions: {
show: {
resource: ['interaction'],
operation: ['scroll'],
scrollingMode: ['automatic'],
},
},
},
{
displayName: 'Scroll To Edge',
name: 'scrollToEdge',
type: 'fixedCollection',
default: {},
placeholder: 'Add Edge Direction',
description:
"The direction to scroll to. When 'Scroll By' is defined, 'Scroll To Edge' action will be executed first, then 'Scroll By' action.",
displayOptions: {
show: {
resource: ['interaction'],
operation: ['scroll'],
scrollingMode: ['manual'],
},
},
options: [
{
displayName: 'Page Edges',
name: 'edgeValues',
values: [
{
displayName: 'Vertically',
name: 'yAxis',
type: 'options',
default: '',
options: [
{
name: 'Empty',
value: '',
},
{
name: 'Top',
value: 'top',
},
{
name: 'Bottom',
value: 'bottom',
},
],
},
{
displayName: 'Horizontally',
name: 'xAxis',
type: 'options',
default: '',
options: [
{
name: 'Empty',
value: '',
},
{
name: 'Left',
value: 'left',
},
{
name: 'Right',
value: 'right',
},
],
},
],
},
],
},
{
displayName: 'Scroll By',
name: 'scrollBy',
type: 'fixedCollection',
default: {},
description:
"The amount to scroll by. When 'Scroll To Edge' is defined, 'Scroll By' action will be executed after 'Scroll To Edge'.",
placeholder: 'Add Scroll Amount',
displayOptions: {
show: {
resource: ['interaction'],
operation: ['scroll'],
scrollingMode: ['manual'],
},
},
options: [
{
name: 'scrollValues',
displayName: 'Scroll Values',
description: 'The amount in pixels or percentage to scroll by',
values: [
{
displayName: 'Vertically',
name: 'yAxis',
type: 'string',
default: '',
placeholder: 'e.g. 200px, 50%, -100px',
},
{
displayName: 'Horizontally',
name: 'xAxis',
type: 'string',
default: '',
placeholder: 'e.g. 50px, 10%, -200px',
},
],
},
],
},
{
displayName: 'Scrollable Area',
name: 'scrollWithin',
type: 'string',
default: '',
description: 'Scroll within an element on the page',
placeholder: 'e.g. the left sidebar',
displayOptions: {
show: {
resource: ['interaction'],
operation: ['scroll'],
},
},
},
];
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const { sessionId, windowId } = validateSessionAndWindowId.call(this, index);
const scrollingMode = validateScrollingMode.call(this, index);
const isAutomatic = scrollingMode === 'automatic';
const scrollToElement = isAutomatic
? validateRequiredStringField.call(this, index, 'scrollToElement', 'Element Description')
: '';
const scrollToEdge = this.getNodeParameter('scrollToEdge.edgeValues', index, {}) as {
xAxis?: string;
yAxis?: string;
};
const scrollBy = validateScrollByAmount.call(this, index, 'scrollBy.scrollValues');
const scrollWithin = this.getNodeParameter('scrollWithin', index, '') as string;
const request: IDataObject = {
// when scrollingMode is 'Manual'
...(!isAutomatic ? { scrollToEdge } : {}),
...(!isAutomatic ? { scrollBy } : {}),
// when scrollingMode is 'Automatic'
...(isAutomatic ? { scrollToElement } : {}),
// when scrollWithin is defined
...(scrollWithin ? { scrollWithin } : {}),
};
const fullRequest = constructInteractionRequest.call(this, index, request);
const response = await apiRequest.call(
this,
'POST',
`/sessions/${sessionId}/windows/${windowId}/scroll`,
fullRequest,
);
validateAirtopApiResponse(this.getNode(), response);
return this.helpers.returnJsonArray({ sessionId, windowId, ...response });
}
@@ -0,0 +1,81 @@
import {
type IExecuteFunctions,
type INodeExecutionData,
type INodeProperties,
} from 'n8n-workflow';
import { constructInteractionRequest } from './helpers';
import {
validateRequiredStringField,
validateSessionAndWindowId,
validateAirtopApiResponse,
} from '../../GenericFunctions';
import { apiRequest } from '../../transport';
import { elementDescriptionField } from '../common/fields';
export const description: INodeProperties[] = [
{
displayName: 'Text',
name: 'text',
type: 'string',
required: true,
default: '',
displayOptions: {
show: {
resource: ['interaction'],
operation: ['type'],
},
},
description: 'The text to type into the browser window',
placeholder: 'e.g. email@example.com',
},
{
displayName: 'Press Enter Key',
name: 'pressEnterKey',
type: 'boolean',
default: false,
description: 'Whether to press the Enter key after typing the text',
displayOptions: {
show: {
resource: ['interaction'],
operation: ['type'],
},
},
},
{
...elementDescriptionField,
displayOptions: {
show: {
resource: ['interaction'],
operation: ['type'],
},
},
},
];
export async function execute(
this: IExecuteFunctions,
index: number,
): Promise<INodeExecutionData[]> {
const { sessionId, windowId } = validateSessionAndWindowId.call(this, index);
const text = validateRequiredStringField.call(this, index, 'text', 'Text');
const pressEnterKey = this.getNodeParameter('pressEnterKey', index) as boolean;
const elementDescription = this.getNodeParameter('elementDescription', index) as string;
const request = constructInteractionRequest.call(this, index, {
text,
pressEnterKey,
elementDescription,
});
const response = await apiRequest.call(
this,
'POST',
`/sessions/${sessionId}/windows/${windowId}/type`,
request,
);
validateAirtopApiResponse(this.getNode(), response);
return this.helpers.returnJsonArray({ sessionId, windowId, ...response });
}