mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 21:49:36 +02:00
Merge branch 'master' into ado-4519-bug-update-0-versions-behind
This commit is contained in:
commit
2f242799fa
|
|
@ -60,6 +60,7 @@
|
|||
"@types/jest": "^29.5.3",
|
||||
"@types/node": "*",
|
||||
"@types/supertest": "^6.0.3",
|
||||
"@babel/preset-env": "^7.26.0",
|
||||
"babel-plugin-transform-import-meta": "^2.3.2",
|
||||
"bundlemon": "^3.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ describe('ToolSerpApi', () => {
|
|||
const node = new ToolSerpApi();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'artificial intelligence news' },
|
||||
json: { input: 'artificial intelligence news' },
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -69,17 +69,17 @@ describe('ToolSerpApi', () => {
|
|||
},
|
||||
],
|
||||
]);
|
||||
expect(SerpAPI.prototype.invoke).toHaveBeenCalledWith(inputData[0]);
|
||||
expect(SerpAPI.prototype.invoke).toHaveBeenCalledWith(inputData[0].json);
|
||||
});
|
||||
|
||||
it('should handle multiple input items', async () => {
|
||||
const node = new ToolSerpApi();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'machine learning' },
|
||||
json: { input: 'machine learning' },
|
||||
},
|
||||
{
|
||||
json: { query: 'deep learning' },
|
||||
json: { input: 'deep learning' },
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ describe('ToolSerpApi', () => {
|
|||
const node = new ToolSerpApi();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: { query: 'test query' },
|
||||
json: { input: 'test query' },
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -144,5 +144,25 @@ describe('ToolSerpApi', () => {
|
|||
expect(mockExecute.getCredentials).toHaveBeenCalledWith('serpApi');
|
||||
expect(mockExecute.getNodeParameter).toHaveBeenCalledWith('options', 0);
|
||||
});
|
||||
|
||||
it('should fail gracefully if input is missing', async () => {
|
||||
const node = new ToolSerpApi();
|
||||
const inputData: INodeExecutionData[] = [
|
||||
{
|
||||
json: {},
|
||||
},
|
||||
];
|
||||
|
||||
const mockExecute = mock<IExecuteFunctions>({
|
||||
getInputData: jest.fn(() => inputData),
|
||||
getNode: jest.fn(() => mock<INode>({ name: 'test serpapi' })),
|
||||
getCredentials: jest.fn().mockResolvedValue({ apiKey: 'test-api-key' }),
|
||||
getNodeParameter: jest.fn().mockReturnValue({}),
|
||||
});
|
||||
|
||||
await expect(node.execute.call(mockExecute)).rejects.toThrow(
|
||||
'Missing search query input at itemIndex 0',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { SerpAPI } from '@langchain/community/tools/serpapi';
|
||||
import { logWrapper } from '@utils/logWrapper';
|
||||
import { getConnectionHintNoticeField } from '@utils/sharedFields';
|
||||
import {
|
||||
type IExecuteFunctions,
|
||||
NodeConnectionTypes,
|
||||
|
|
@ -7,10 +9,9 @@ import {
|
|||
type ISupplyDataFunctions,
|
||||
type SupplyData,
|
||||
type INodeExecutionData,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { logWrapper } from '@utils/logWrapper';
|
||||
import { getConnectionHintNoticeField } from '@utils/sharedFields';
|
||||
async function getTool(ctx: ISupplyDataFunctions | IExecuteFunctions, itemIndex: number) {
|
||||
const credentials = await ctx.getCredentials('serpApi');
|
||||
|
||||
|
|
@ -133,8 +134,17 @@ export class ToolSerpApi implements INodeType {
|
|||
const returnData: INodeExecutionData[] = [];
|
||||
for (let itemIndex = 0; itemIndex < inputData.length; itemIndex++) {
|
||||
const tool = await getTool(this, itemIndex);
|
||||
const query = inputData[itemIndex];
|
||||
const result = await tool.invoke(query);
|
||||
const item = inputData[itemIndex].json;
|
||||
|
||||
if (typeof item.input !== 'string' || !item.input) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`Missing search query input at itemIndex ${itemIndex}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result = (await tool.invoke(item)) as string;
|
||||
|
||||
returnData.push({
|
||||
json: {
|
||||
response: result,
|
||||
|
|
|
|||
|
|
@ -134,16 +134,6 @@ export class ChatHubController {
|
|||
res: Response,
|
||||
@Body payload: ChatHubSendMessageRequest,
|
||||
) {
|
||||
this.logger.debug(
|
||||
`Chat send request received: ${jsonStringify({
|
||||
...payload,
|
||||
attachments: payload.attachments?.map((a) => ({
|
||||
fileName: a.fileName,
|
||||
data: `${a.data.length} characters (Base64)`,
|
||||
})),
|
||||
})}`,
|
||||
);
|
||||
|
||||
try {
|
||||
await this.chatService.sendHumanMessage(res, req.user, {
|
||||
...payload,
|
||||
|
|
@ -186,8 +176,6 @@ export class ChatHubController {
|
|||
@Param('messageId') editId: ChatMessageId,
|
||||
@Body payload: ChatHubEditMessageRequest,
|
||||
) {
|
||||
this.logger.debug(`Chat edit request received: ${jsonStringify(payload)}`);
|
||||
|
||||
try {
|
||||
await this.chatService.editMessage(res, req.user, {
|
||||
...payload,
|
||||
|
|
@ -232,8 +220,6 @@ export class ChatHubController {
|
|||
@Param('messageId') retryId: ChatMessageId,
|
||||
@Body payload: ChatHubRegenerateMessageRequest,
|
||||
) {
|
||||
this.logger.debug(`Chat retry request received: ${jsonStringify(payload)}`);
|
||||
|
||||
try {
|
||||
await this.chatService.regenerateAIMessage(res, req.user, {
|
||||
...payload,
|
||||
|
|
@ -277,8 +263,6 @@ export class ChatHubController {
|
|||
@Param('sessionId') sessionId: ChatSessionId,
|
||||
@Param('messageId') messageId: ChatMessageId,
|
||||
) {
|
||||
this.logger.debug(`Chat stop request received: ${jsonStringify({ sessionId, messageId })}`);
|
||||
|
||||
await this.chatService.stopGeneration(req.user, sessionId, messageId);
|
||||
res.status(204).send();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,14 +107,6 @@ describe('TestWebhookRegistrationsService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('deregisterAll()', () => {
|
||||
test('should deregister all test webhook registrations', async () => {
|
||||
await registrations.deregisterAll();
|
||||
|
||||
expect(cacheService.delete).toHaveBeenCalledWith(cacheKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toKey()', () => {
|
||||
test('should convert a test webhook registration to a key', () => {
|
||||
const result = registrations.toKey(registration.webhook);
|
||||
|
|
|
|||
|
|
@ -113,10 +113,6 @@ export class TestWebhookRegistrationsService {
|
|||
return val;
|
||||
}
|
||||
|
||||
async deregisterAll() {
|
||||
await this.cacheService.delete(this.cacheKey);
|
||||
}
|
||||
|
||||
toKey(webhook: Pick<IWebhookData, 'webhookId' | 'httpMethod' | 'path'>) {
|
||||
const { webhookId, httpMethod, path: webhookPath } = webhook;
|
||||
|
||||
|
|
|
|||
|
|
@ -500,9 +500,10 @@ export class TestWebhooks implements IWebhookManager {
|
|||
if (staticData) workflow.staticData = staticData;
|
||||
|
||||
await this.webhookService.deleteWebhook(workflow, webhook, 'internal', 'update');
|
||||
}
|
||||
|
||||
await this.registrations.deregisterAll();
|
||||
// Deregister only this webhook, not all webhooks from other running workflows
|
||||
await this.registrations.deregister(webhook);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -462,4 +462,121 @@ describe('NodeExecutionContext', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nodeFeatures', () => {
|
||||
it('should return empty object when features are not defined', () => {
|
||||
node.typeVersion = 2.4;
|
||||
nodeType.description.features = undefined;
|
||||
|
||||
const result = testContext['nodeFeatures'];
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
|
||||
it('should return enabled features based on node version', () => {
|
||||
node.typeVersion = 2.4;
|
||||
nodeType.description.features = {
|
||||
useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] },
|
||||
useFeatureB: { '@version': [{ _cnd: { lte: 2.1 } }] },
|
||||
useFeatureC: { '@version': [{ _cnd: { gte: 2.2 } }] },
|
||||
};
|
||||
|
||||
const result = testContext['nodeFeatures'];
|
||||
|
||||
expect(result).toEqual({
|
||||
useFeatureA: true,
|
||||
useFeatureB: false,
|
||||
useFeatureC: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return correct features for version 2.1', () => {
|
||||
node.typeVersion = 2.1;
|
||||
nodeType.description.features = {
|
||||
useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] },
|
||||
useFeatureB: { '@version': [{ _cnd: { lte: 2.1 } }] },
|
||||
useFeatureC: { '@version': [{ _cnd: { gte: 2.2 } }] },
|
||||
};
|
||||
|
||||
const result = testContext['nodeFeatures'];
|
||||
|
||||
expect(result).toEqual({
|
||||
useFeatureA: false,
|
||||
useFeatureB: true,
|
||||
useFeatureC: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle simple version number conditions', () => {
|
||||
node.typeVersion = 2;
|
||||
nodeType.description.features = {
|
||||
useFeatureD: { '@version': [2] },
|
||||
useFeatureE: { '@version': [{ _cnd: { lt: 2.3 } }] },
|
||||
};
|
||||
|
||||
const result = testContext['nodeFeatures'];
|
||||
|
||||
expect(result).toEqual({
|
||||
useFeatureD: true,
|
||||
useFeatureE: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNodeFeatureEnabled', () => {
|
||||
it('should return true when feature is enabled', () => {
|
||||
node.typeVersion = 2.4;
|
||||
nodeType.description.features = {
|
||||
useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] },
|
||||
};
|
||||
|
||||
const result = testContext.isNodeFeatureEnabled('useFeatureA');
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when feature is disabled', () => {
|
||||
node.typeVersion = 2.3;
|
||||
nodeType.description.features = {
|
||||
useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] },
|
||||
};
|
||||
|
||||
const result = testContext.isNodeFeatureEnabled('useFeatureA');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when feature does not exist', () => {
|
||||
node.typeVersion = 2.4;
|
||||
nodeType.description.features = {
|
||||
useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] },
|
||||
};
|
||||
|
||||
const result = testContext.isNodeFeatureEnabled('nonExistentFeature');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when features are not defined', () => {
|
||||
node.typeVersion = 2.4;
|
||||
nodeType.description.features = undefined;
|
||||
|
||||
const result = testContext.isNodeFeatureEnabled('useFeatureA');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle multiple features correctly', () => {
|
||||
node.typeVersion = 2.4;
|
||||
nodeType.description.features = {
|
||||
useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] },
|
||||
useFeatureB: { '@version': [{ _cnd: { lte: 2.1 } }] },
|
||||
useFeatureC: { '@version': [{ _cnd: { gte: 2.2 } }] },
|
||||
};
|
||||
|
||||
expect(testContext.isNodeFeatureEnabled('useFeatureA')).toBe(true);
|
||||
expect(testContext.isNodeFeatureEnabled('useFeatureB')).toBe(false);
|
||||
expect(testContext.isNodeFeatureEnabled('useFeatureC')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import type {
|
|||
IRunExecutionData,
|
||||
IWorkflowExecuteAdditionalData,
|
||||
NodeConnectionType,
|
||||
NodeFeatures,
|
||||
NodeInputConnections,
|
||||
NodeParameterValueType,
|
||||
NodeTypeAndVersion,
|
||||
|
|
@ -168,6 +169,26 @@ export abstract class NodeExecutionContext implements Omit<FunctionsBase, 'getCr
|
|||
return this.workflow.nodeTypes.getByNameAndVersion(type, typeVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the feature flags for the current node version.
|
||||
* Uses declarative features from the node type description.
|
||||
* @private
|
||||
*/
|
||||
@Memoized
|
||||
private get nodeFeatures(): NodeFeatures {
|
||||
const description = this.nodeType.description;
|
||||
return NodeHelpers.getNodeFeatures(description.features, this.node.typeVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a feature is enabled for the current node version.
|
||||
* @param featureName - The name of the feature to check
|
||||
* @returns true if the feature is enabled, false otherwise
|
||||
*/
|
||||
isNodeFeatureEnabled(featureName: string): boolean {
|
||||
return this.nodeFeatures[featureName] ?? false;
|
||||
}
|
||||
|
||||
@Memoized
|
||||
get nodeInputs() {
|
||||
return NodeHelpers.getNodeInputs(this.workflow, this.node, this.nodeType.description).map(
|
||||
|
|
|
|||
|
|
@ -3068,7 +3068,6 @@
|
|||
"workflowActivator.thisWorkflowIsArchived": "This workflow is archived so it cannot be activated",
|
||||
"workflowActivator.thisWorkflowHasOnlyOneExecuteWorkflowTriggerNode": "'Execute Workflow Trigger' doesn't require activation as it is triggered by another workflow",
|
||||
"workflowDetails.share": "Share",
|
||||
"workflowDetails.active": "Active",
|
||||
"workflowDetails.addTag": "Add tag",
|
||||
"workflowDetails.chooseOrCreateATag": "Choose or create a tag",
|
||||
"workflowProductionChecklist.title": "Production Checklist",
|
||||
|
|
@ -3099,8 +3098,6 @@
|
|||
"workflowExtraction.modal.title": "Convert to sub-workflow",
|
||||
"workflowExtraction.modal.description": "Convert {nodeCount} node to a new sub-workflow | Convert {nodeCount} nodes to a new sub-workflow",
|
||||
"workflowHelpers.showMessage.title": "Problem saving workflow",
|
||||
"workflowOpen.active": "Active",
|
||||
"workflowOpen.couldNotLoadActiveWorkflows": "Could not load active workflows",
|
||||
"workflowOpen.created": "Created",
|
||||
"workflowOpen.filterWorkflows": "Filter by tags",
|
||||
"workflowOpen.name": "@:_reusableBaseText.name",
|
||||
|
|
@ -3313,8 +3310,8 @@
|
|||
"workflows.filters.tags": "Tags",
|
||||
"workflows.filters.status": "Status",
|
||||
"workflows.filters.status.all": "All",
|
||||
"workflows.filters.status.active": "Active",
|
||||
"workflows.filters.status.deactivated": "Deactivated",
|
||||
"workflows.filters.status.active": "Published",
|
||||
"workflows.filters.status.deactivated": "Unpublished",
|
||||
"workflows.filters.showArchived": "Show archived workflows",
|
||||
"workflows.filters.ownedBy": "Owned by",
|
||||
"workflows.filters.sharedWith": "Shared with",
|
||||
|
|
|
|||
|
|
@ -381,7 +381,7 @@ watch(
|
|||
{ immediate: true },
|
||||
);
|
||||
|
||||
function onSubmit(message: string, attachments: File[]) {
|
||||
async function onSubmit(message: string, attachments: File[]) {
|
||||
if (
|
||||
!message.trim() ||
|
||||
isResponding.value ||
|
||||
|
|
@ -394,7 +394,7 @@ function onSubmit(message: string, attachments: File[]) {
|
|||
didSubmitInCurrentSession.value = true;
|
||||
editingMessageId.value = undefined;
|
||||
|
||||
void chatStore.sendMessage(
|
||||
await chatStore.sendMessage(
|
||||
sessionId.value,
|
||||
message,
|
||||
selectedModel.value.model,
|
||||
|
|
|
|||
|
|
@ -125,9 +125,13 @@ const plugins: UserConfig['plugins'] = [
|
|||
],
|
||||
},
|
||||
}),
|
||||
legacy({
|
||||
modernTargets: browsers,
|
||||
}),
|
||||
...(release
|
||||
? [
|
||||
legacy({
|
||||
modernTargets: browsers,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: 'Insert config script',
|
||||
transformIndexHtml: (html, ctx) => {
|
||||
|
|
|
|||
|
|
@ -280,7 +280,7 @@ const getAllOperation: INodeProperties[] = [
|
|||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Return Only Active Workflows',
|
||||
displayName: 'Return Only Published Workflows',
|
||||
name: 'activeWorkflows',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ export class NodeParameterHelper {
|
|||
break;
|
||||
case 'text':
|
||||
await this.ndv.setParameterInput(parameterName, value);
|
||||
await this.ndv.waitForDebounce();
|
||||
break;
|
||||
case 'switch':
|
||||
await this.ndv.setParameterSwitch(parameterName, value === 'true');
|
||||
|
|
@ -80,10 +81,11 @@ export class NodeParameterHelper {
|
|||
await this.setParameter('responseMode', config.responseMode, 'dropdown');
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplified HTTP Request node parameter configuration
|
||||
* @param config - Configuration object with parameter values
|
||||
*/
|
||||
async getWebhookPath(): Promise<string> {
|
||||
const input = this.ndv.getParameterInputField('path');
|
||||
return await input.inputValue();
|
||||
}
|
||||
|
||||
async httpRequest(config: {
|
||||
method?: string;
|
||||
url?: string;
|
||||
|
|
|
|||
|
|
@ -626,6 +626,12 @@ export class NodeDetailsViewPage extends BasePage {
|
|||
await input.fill(value);
|
||||
}
|
||||
|
||||
/** Waits for parameter input debounce (100ms) to flush. */
|
||||
async waitForDebounce(): Promise<void> {
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await this.page.waitForTimeout(150);
|
||||
}
|
||||
|
||||
async clickGetBackToCanvas(): Promise<void> {
|
||||
await this.clickBackToCanvasButton();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { RoleApiHelper } from './role-api-helper';
|
|||
import { TagApiHelper } from './tag-api-helper';
|
||||
import { UserApiHelper } from './user-api-helper';
|
||||
import { VariablesApiHelper } from './variables-api-helper';
|
||||
import { WebhookApiHelper } from './webhook-api-helper';
|
||||
import { WorkflowApiHelper } from './workflow-api-helper';
|
||||
|
||||
export interface LoginResponseData {
|
||||
|
|
@ -39,6 +40,7 @@ const DB_TAGS = {
|
|||
export class ApiHelpers {
|
||||
request: APIRequestContext;
|
||||
workflows: WorkflowApiHelper;
|
||||
webhooks: WebhookApiHelper;
|
||||
projects: ProjectApiHelper;
|
||||
credentials: CredentialApiHelper;
|
||||
variables: VariablesApiHelper;
|
||||
|
|
@ -49,6 +51,7 @@ export class ApiHelpers {
|
|||
constructor(requestContext: APIRequestContext) {
|
||||
this.request = requestContext;
|
||||
this.workflows = new WorkflowApiHelper(this);
|
||||
this.webhooks = new WebhookApiHelper(this);
|
||||
this.projects = new ProjectApiHelper(this);
|
||||
this.credentials = new CredentialApiHelper(this);
|
||||
this.variables = new VariablesApiHelper(this);
|
||||
|
|
|
|||
44
packages/testing/playwright/services/webhook-api-helper.ts
Normal file
44
packages/testing/playwright/services/webhook-api-helper.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import type { APIResponse } from '@playwright/test';
|
||||
|
||||
import type { ApiHelpers } from './api-helper';
|
||||
|
||||
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD';
|
||||
|
||||
interface TriggerOptions {
|
||||
method?: HttpMethod;
|
||||
headers?: Record<string, string>;
|
||||
data?: unknown;
|
||||
/** Max retries for 404 (webhook not yet registered). Default: 3 */
|
||||
maxNotFoundRetries?: number;
|
||||
/** Delay between 404 retries in ms. Default: 250 */
|
||||
notFoundRetryDelayMs?: number;
|
||||
}
|
||||
|
||||
/** Triggers webhooks with retry for 404s (async registration) and connection errors. */
|
||||
export class WebhookApiHelper {
|
||||
constructor(private readonly api: ApiHelpers) {}
|
||||
|
||||
async trigger(path: string, options?: TriggerOptions): Promise<APIResponse> {
|
||||
const maxNotFoundRetries = options?.maxNotFoundRetries ?? 3;
|
||||
const notFoundRetryDelayMs = options?.notFoundRetryDelayMs ?? 250;
|
||||
|
||||
let lastResponse: APIResponse | undefined;
|
||||
|
||||
for (let attempt = 0; attempt <= maxNotFoundRetries; attempt++) {
|
||||
lastResponse = await this.api.request.fetch(path, {
|
||||
method: options?.method ?? 'GET',
|
||||
headers: options?.headers,
|
||||
data: options?.data,
|
||||
maxRetries: 3, // Playwright retry for connection errors
|
||||
});
|
||||
|
||||
if (lastResponse.status() !== 404 || attempt === maxNotFoundRetries) {
|
||||
return lastResponse;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, notFoundRetryDelayMs));
|
||||
}
|
||||
|
||||
return lastResponse!;
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ type WorkflowImportResult = {
|
|||
createdWorkflow: IWorkflowBase;
|
||||
webhookPath?: string;
|
||||
webhookId?: string;
|
||||
webhookMethod?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD';
|
||||
};
|
||||
|
||||
export class WorkflowApiHelper {
|
||||
|
|
@ -35,14 +36,7 @@ export class WorkflowApiHelper {
|
|||
return result.data ?? result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a workflow in a project with optional folder placement (Uses Internal API not public API)
|
||||
* @param project - Required project ID where the workflow will be created
|
||||
* @param options - Optional configuration for workflow creation
|
||||
* @param options.folder - Optional folder ID to place the workflow in
|
||||
* @param options.name - Optional workflow name. If not provided, generates a unique name using nanoid
|
||||
* @returns Object containing the name, ID, and versionId of the created workflow
|
||||
*/
|
||||
/** Creates a workflow in a project with optional folder placement. */
|
||||
async createInProject(
|
||||
project: string,
|
||||
options?: {
|
||||
|
|
@ -96,10 +90,7 @@ export class WorkflowApiHelper {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make workflow unique by updating name, IDs, and webhook paths if present.
|
||||
* This ensures no conflicts when importing workflows for testing.
|
||||
*/
|
||||
/** Makes workflow unique by updating name, IDs, and webhook paths. */
|
||||
private makeWorkflowUnique(
|
||||
workflow: Partial<IWorkflowBase>,
|
||||
options?: { webhookPrefix?: string; idLength?: number },
|
||||
|
|
@ -121,6 +112,7 @@ export class WorkflowApiHelper {
|
|||
// Check if workflow has webhook nodes and process them
|
||||
let webhookId: string | undefined;
|
||||
let webhookPath: string | undefined;
|
||||
let webhookMethod: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | undefined;
|
||||
|
||||
if (workflow.nodes) {
|
||||
for (const node of workflow.nodes) {
|
||||
|
|
@ -129,25 +121,24 @@ export class WorkflowApiHelper {
|
|||
webhookPath = `${webhookPrefix}-${webhookId}`;
|
||||
node.webhookId = webhookId;
|
||||
node.parameters.path = webhookPath;
|
||||
// Extract HTTP method from webhook node, default to GET
|
||||
webhookMethod = (node.parameters.httpMethod as typeof webhookMethod) ?? 'GET';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { webhookId, webhookPath, workflow };
|
||||
return { webhookId, webhookPath, webhookMethod, workflow };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a workflow from an in-memory definition, making it unique for testing.
|
||||
* Returns detailed information about what was created.
|
||||
*/
|
||||
/** Creates a workflow from definition, making it unique for testing. */
|
||||
async createWorkflowFromDefinition(
|
||||
workflow: Partial<IWorkflowBase>,
|
||||
options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean },
|
||||
): Promise<WorkflowImportResult> {
|
||||
const { makeUnique = true, ...rest } = options ?? {};
|
||||
const { webhookPath, webhookId } = makeUnique
|
||||
const { webhookPath, webhookId, webhookMethod } = makeUnique
|
||||
? this.makeWorkflowUnique(workflow, rest)
|
||||
: { webhookPath: undefined, webhookId: undefined };
|
||||
: { webhookPath: undefined, webhookId: undefined, webhookMethod: undefined };
|
||||
const createdWorkflow = await this.createWorkflow(workflow);
|
||||
const workflowId: string = String(createdWorkflow.id);
|
||||
|
||||
|
|
@ -156,14 +147,11 @@ export class WorkflowApiHelper {
|
|||
createdWorkflow,
|
||||
webhookPath,
|
||||
webhookId,
|
||||
webhookMethod,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a workflow from file and make it unique for testing.
|
||||
* The workflow will be created with its original active state from the JSON file.
|
||||
* Returns detailed information about what was imported, including webhook info if present.
|
||||
*/
|
||||
/** Imports a workflow from file, making it unique for testing. */
|
||||
async importWorkflowFromFile(
|
||||
fileName: string,
|
||||
options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean },
|
||||
|
|
@ -179,7 +167,6 @@ export class WorkflowApiHelper {
|
|||
workflowDefinition: Partial<IWorkflowBase>,
|
||||
options?: { webhookPrefix?: string; idLength?: number; makeUnique?: boolean },
|
||||
): Promise<WorkflowImportResult> {
|
||||
// Store original active state
|
||||
const result = await this.createWorkflowFromDefinition(workflowDefinition, options);
|
||||
|
||||
if (workflowDefinition.active) {
|
||||
|
|
@ -188,7 +175,6 @@ export class WorkflowApiHelper {
|
|||
return result;
|
||||
}
|
||||
|
||||
// TODO: workflowId is being ignored
|
||||
async getExecutions(workflowId?: string, limit = 20): Promise<ExecutionListResponse[]> {
|
||||
const params = new URLSearchParams();
|
||||
if (workflowId) params.set('workflowId', workflowId);
|
||||
|
|
@ -229,7 +215,9 @@ export class WorkflowApiHelper {
|
|||
|
||||
if (executions.length > initialCount) {
|
||||
for (const execution of executions.slice(0, executions.length - initialCount)) {
|
||||
if (execution.status === 'success' || execution.status === 'error') {
|
||||
const isCompleted = execution.status === 'success' || execution.status === 'error';
|
||||
const isCorrectWorkflow = execution.workflowId === workflowId;
|
||||
if (isCompleted && isCorrectWorkflow) {
|
||||
return execution;
|
||||
}
|
||||
}
|
||||
|
|
@ -237,7 +225,8 @@ export class WorkflowApiHelper {
|
|||
|
||||
for (const execution of executions) {
|
||||
const isCompleted = execution.status === 'success' || execution.status === 'error';
|
||||
if (isCompleted && execution.mode === 'webhook') {
|
||||
const isCorrectWorkflow = execution.workflowId === workflowId;
|
||||
if (isCompleted && isCorrectWorkflow && execution.mode === 'webhook') {
|
||||
const executionTime = new Date(
|
||||
execution.startedAt ?? execution.createdAt ?? Date.now(),
|
||||
).getTime();
|
||||
|
|
@ -253,14 +242,7 @@ export class WorkflowApiHelper {
|
|||
throw new TestError(`Execution did not complete within ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a workflow execution to reach a specific status
|
||||
* @param workflowId - The workflow ID to check
|
||||
* @param expectedStatus - The expected status (e.g., 'waiting', 'success', 'error')
|
||||
* @param timeoutMs - Maximum time to wait in milliseconds
|
||||
* @returns The execution once it reaches the expected status
|
||||
* @throws TestError if execution doesn't reach the expected status within timeout
|
||||
*/
|
||||
/** Waits for a workflow execution to reach a specific status. */
|
||||
async waitForWorkflowStatus(
|
||||
workflowId: string,
|
||||
expectedStatus: string,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ test.describe('External Webhook Triggering', () => {
|
|||
|
||||
const testPayload = { message: 'Hello from Playwright test' };
|
||||
|
||||
const webhookResponse = await api.request.post(`/webhook/${webhookPath}`, {
|
||||
const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`, {
|
||||
method: 'POST',
|
||||
data: testPayload,
|
||||
});
|
||||
|
||||
|
|
@ -28,7 +29,7 @@ test.describe('External Webhook Triggering', () => {
|
|||
'webhook-misconfiguration-test.json',
|
||||
);
|
||||
|
||||
const webhookResponse = await api.request.get(`/webhook/${webhookPath}`);
|
||||
const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`);
|
||||
|
||||
expect(webhookResponse.ok()).toBe(false);
|
||||
expect(await webhookResponse.text()).toContain('Unused Respond to Webhook node');
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ test.describe('Webhook Origin Isolation', () => {
|
|||
test(`Webhook responses should include the correct response headers for ${webhookPath}`, async ({
|
||||
api,
|
||||
}) => {
|
||||
const webhookResponse = await api.request.get(`/webhook/${webhookPath}`);
|
||||
const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`);
|
||||
expect(webhookResponse.ok()).toBe(true);
|
||||
|
||||
const headers = webhookResponse.headers();
|
||||
|
|
|
|||
|
|
@ -7,46 +7,47 @@ import { EditFieldsNode } from '../../../pages/nodes/EditFieldsNode';
|
|||
const cowBase64 =
|
||||
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k=';
|
||||
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests -- Flaky in multi-main mode due to webhook registration timing issues
|
||||
test.describe.skip('Webhook Trigger node', () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
test.describe('Webhook Trigger node', () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.start.fromBlankCanvas();
|
||||
});
|
||||
|
||||
const HTTP_METHODS = ['GET', 'POST', 'DELETE', 'HEAD', 'PATCH', 'PUT'];
|
||||
for (const httpMethod of HTTP_METHODS) {
|
||||
test(`should listen for a ${httpMethod} request`, async ({ n8n }) => {
|
||||
const webhookPath = nanoid();
|
||||
await n8n.canvas.addNode('Webhook');
|
||||
await n8n.ndv.setupHelper.webhook({ httpMethod, path: webhookPath });
|
||||
test('should listen for all HTTP methods (GET, POST, DELETE, HEAD, PATCH, PUT)', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.canvas.addNode('Webhook');
|
||||
const webhookPath = await n8n.ndv.setupHelper.getWebhookPath();
|
||||
|
||||
const methods = ['GET', 'POST', 'DELETE', 'HEAD', 'PATCH', 'PUT'] as const;
|
||||
for (const method of methods) {
|
||||
await n8n.ndv.setupHelper.webhook({ httpMethod: method });
|
||||
await n8n.ndv.execute();
|
||||
await expect(n8n.ndv.getWebhookTestEvent()).toBeVisible();
|
||||
const response = await n8n.api.request.fetch(`/webhook-test/${webhookPath}`, {
|
||||
method: httpMethod,
|
||||
});
|
||||
expect(response.ok()).toBe(true);
|
||||
});
|
||||
}
|
||||
|
||||
const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`, { method });
|
||||
expect(response.ok(), `${method} request should succeed`).toBe(true);
|
||||
|
||||
// Wait for output to appear (confirms execution completed)
|
||||
await expect(n8n.ndv.outputPanel.getDataContainer()).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('should listen for a GET request and respond with Respond to Webhook node', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
const webhookPath = nanoid();
|
||||
|
||||
await n8n.canvas.addNode('Webhook');
|
||||
await n8n.ndv.setupHelper.webhook({
|
||||
httpMethod: 'GET',
|
||||
path: webhookPath,
|
||||
responseMode: "Using 'Respond to Webhook' Node",
|
||||
});
|
||||
const webhookPath = await n8n.ndv.setupHelper.getWebhookPath();
|
||||
await n8n.ndv.close();
|
||||
await addEditFieldsNode(n8n);
|
||||
await n8n.canvas.addNode('Respond to Webhook', { closeNDV: true });
|
||||
|
||||
await n8n.canvas.clickExecuteWorkflowButton();
|
||||
await expect(n8n.canvas.waitingForTriggerEvent()).toBeVisible();
|
||||
const response = await n8n.api.request.get(`/webhook-test/${webhookPath}`);
|
||||
const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`);
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
const responseData = await response.json();
|
||||
|
|
@ -56,28 +57,27 @@ test.describe.skip('Webhook Trigger node', () => {
|
|||
test('should listen for a GET request and respond with custom status code 201', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
const webhookPath = nanoid();
|
||||
|
||||
await n8n.canvas.addNode('Webhook');
|
||||
await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET', path: webhookPath });
|
||||
await n8n.ndv.setupHelper.webhook({ httpMethod: 'GET' });
|
||||
const webhookPath = await n8n.ndv.setupHelper.getWebhookPath();
|
||||
|
||||
await n8n.ndv.setOptionalParameter('Response Code', 'responseCode', '201');
|
||||
// Extra wait to ensure parameter is persisted before webhook registration
|
||||
await n8n.ndv.waitForDebounce();
|
||||
await n8n.ndv.execute();
|
||||
await expect(n8n.ndv.getWebhookTestEvent()).toBeVisible();
|
||||
|
||||
const response = await n8n.api.request.get(`/webhook-test/${webhookPath}`);
|
||||
const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`);
|
||||
expect(response.status()).toBe(201);
|
||||
});
|
||||
|
||||
test('should listen for a GET request and respond with last node', async ({ n8n }) => {
|
||||
const webhookPath = nanoid();
|
||||
|
||||
await n8n.canvas.addNode('Webhook');
|
||||
await n8n.ndv.setupHelper.webhook({
|
||||
httpMethod: 'GET',
|
||||
path: webhookPath,
|
||||
responseMode: 'When Last Node Finishes',
|
||||
});
|
||||
const webhookPath = await n8n.ndv.setupHelper.getWebhookPath();
|
||||
await n8n.ndv.close();
|
||||
|
||||
await addEditFieldsNode(n8n);
|
||||
|
|
@ -86,7 +86,7 @@ test.describe.skip('Webhook Trigger node', () => {
|
|||
|
||||
await expect(n8n.canvas.waitingForTriggerEvent()).toBeVisible();
|
||||
|
||||
const response = await n8n.api.request.get(`/webhook-test/${webhookPath}`);
|
||||
const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`);
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
const responseData = await response.json();
|
||||
|
|
@ -96,14 +96,12 @@ test.describe.skip('Webhook Trigger node', () => {
|
|||
test('should listen for a GET request and respond with last node binary data', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
const webhookPath = nanoid();
|
||||
|
||||
await n8n.canvas.addNode('Webhook');
|
||||
await n8n.ndv.setupHelper.webhook({
|
||||
httpMethod: 'GET',
|
||||
path: webhookPath,
|
||||
responseMode: 'When Last Node Finishes',
|
||||
});
|
||||
const webhookPath = await n8n.ndv.setupHelper.getWebhookPath();
|
||||
await n8n.ndv.selectOptionInParameterDropdown('responseData', 'First Entry Binary');
|
||||
await n8n.ndv.close();
|
||||
|
||||
|
|
@ -120,7 +118,7 @@ test.describe.skip('Webhook Trigger node', () => {
|
|||
|
||||
await expect(n8n.canvas.waitingForTriggerEvent()).toBeVisible();
|
||||
|
||||
const response = await n8n.api.request.get(`/webhook-test/${webhookPath}`);
|
||||
const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`);
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
const responseData = await response.json();
|
||||
|
|
@ -128,19 +126,17 @@ test.describe.skip('Webhook Trigger node', () => {
|
|||
});
|
||||
|
||||
test('should listen for a GET request and respond with an empty body', async ({ n8n }) => {
|
||||
const webhookPath = nanoid();
|
||||
|
||||
await n8n.canvas.addNode('Webhook');
|
||||
await n8n.ndv.setupHelper.webhook({
|
||||
httpMethod: 'GET',
|
||||
path: webhookPath,
|
||||
responseMode: 'When Last Node Finishes',
|
||||
});
|
||||
const webhookPath = await n8n.ndv.setupHelper.getWebhookPath();
|
||||
await n8n.ndv.selectOptionInParameterDropdown('responseData', 'No Response Body');
|
||||
await n8n.ndv.execute();
|
||||
await expect(n8n.ndv.getWebhookTestEvent()).toBeVisible();
|
||||
|
||||
const response = await n8n.api.request.get(`/webhook-test/${webhookPath}`);
|
||||
const response = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`);
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
const responseData = await response.text();
|
||||
|
|
@ -148,7 +144,6 @@ test.describe.skip('Webhook Trigger node', () => {
|
|||
});
|
||||
|
||||
test('should listen for a GET request with Basic Authentication', async ({ n8n }) => {
|
||||
const webhookPath = nanoid();
|
||||
const credentialName = `test-${nanoid()}`;
|
||||
const user = `test-${nanoid()}`;
|
||||
const password = `test-${nanoid()}`;
|
||||
|
|
@ -164,21 +159,21 @@ test.describe.skip('Webhook Trigger node', () => {
|
|||
await n8n.canvas.addNode('Webhook');
|
||||
await n8n.ndv.setupHelper.webhook({
|
||||
httpMethod: 'GET',
|
||||
path: webhookPath,
|
||||
authentication: 'Basic Auth',
|
||||
});
|
||||
const webhookPath = await n8n.ndv.setupHelper.getWebhookPath();
|
||||
|
||||
await n8n.ndv.execute();
|
||||
await expect(n8n.ndv.getWebhookTestEvent()).toBeVisible();
|
||||
|
||||
const failResponse = await n8n.api.request.get(`/webhook-test/${webhookPath}`, {
|
||||
const failResponse = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`, {
|
||||
headers: {
|
||||
Authorization: 'Basic ' + Buffer.from('wrong:wrong').toString('base64'),
|
||||
},
|
||||
});
|
||||
expect(failResponse.status()).toBe(403);
|
||||
|
||||
const successResponse = await n8n.api.request.get(`/webhook-test/${webhookPath}`, {
|
||||
const successResponse = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`, {
|
||||
headers: {
|
||||
Authorization: 'Basic ' + Buffer.from(`${user}:${password}`).toString('base64'),
|
||||
},
|
||||
|
|
@ -186,8 +181,7 @@ test.describe.skip('Webhook Trigger node', () => {
|
|||
expect(successResponse.ok()).toBe(true);
|
||||
});
|
||||
|
||||
test('should listen for a GET request with Header Authentication', async ({ n8n, api }) => {
|
||||
const webhookPath = nanoid();
|
||||
test('should listen for a GET request with Header Authentication', async ({ n8n }) => {
|
||||
const credentialName = `test-${nanoid()}`;
|
||||
const name = `test-${nanoid()}`;
|
||||
const value = `test-${nanoid()}`;
|
||||
|
|
@ -203,14 +197,14 @@ test.describe.skip('Webhook Trigger node', () => {
|
|||
await n8n.canvas.addNode('Webhook');
|
||||
await n8n.ndv.setupHelper.webhook({
|
||||
httpMethod: 'GET',
|
||||
path: webhookPath,
|
||||
authentication: 'Header Auth',
|
||||
});
|
||||
const webhookPath = await n8n.ndv.setupHelper.getWebhookPath();
|
||||
|
||||
await n8n.ndv.execute();
|
||||
await expect(n8n.ndv.getWebhookTestEvent()).toBeVisible();
|
||||
|
||||
const failResponse = await api.request.get(`/webhook-test/${webhookPath}`, {
|
||||
const failResponse = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`, {
|
||||
headers: {
|
||||
test: 'wrong',
|
||||
},
|
||||
|
|
@ -218,7 +212,7 @@ test.describe.skip('Webhook Trigger node', () => {
|
|||
|
||||
expect(failResponse.status()).toBe(403);
|
||||
|
||||
const successResponse = await api.request.get(`/webhook-test/${webhookPath}`, {
|
||||
const successResponse = await n8n.api.webhooks.trigger(`/webhook-test/${webhookPath}`, {
|
||||
headers: {
|
||||
[name]: value,
|
||||
},
|
||||
|
|
@ -227,13 +221,10 @@ test.describe.skip('Webhook Trigger node', () => {
|
|||
});
|
||||
|
||||
test('CAT-1253-bug-cant-run-workflow-when-unconnected-nodes-have-errors', async ({ n8n }) => {
|
||||
const webhookPath = nanoid();
|
||||
|
||||
// Add Webhook node
|
||||
await n8n.canvas.addNode('Webhook');
|
||||
await n8n.ndv.setupHelper.webhook({
|
||||
httpMethod: 'GET',
|
||||
path: webhookPath,
|
||||
});
|
||||
await n8n.ndv.close();
|
||||
|
||||
|
|
@ -123,8 +123,7 @@ test.describe('Execution', () => {
|
|||
await expect(n8n.canvas.clearExecutionDataButton()).toBeHidden();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests -- Flaky in multi-main mode due to webhook registration timing issues
|
||||
test.skip('should test webhook workflow', async ({ n8n }) => {
|
||||
test('should test webhook workflow', async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('Webhook_wait_set.json');
|
||||
|
||||
await expect(n8n.canvas.getExecuteWorkflowButton()).toBeVisible();
|
||||
|
|
|
|||
|
|
@ -231,10 +231,7 @@ test.describe('Logs', () => {
|
|||
await expect(n8n.executions.logsPanel.getLogEntries().nth(2)).toContainText('E2E Chat Model');
|
||||
});
|
||||
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests -- Flaky in multi-main mode due to webhook registration timing issues
|
||||
test.skip('should show logs for a workflow with a node that waits for webhook', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
test('should show logs for a workflow with a node that waits for webhook', async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('Workflow_wait_for_webhook.json');
|
||||
await n8n.canvas.deselectAll();
|
||||
await n8n.canvas.logsPanel.open();
|
||||
|
|
@ -270,10 +267,7 @@ test.describe('Logs', () => {
|
|||
await expect(n8n.canvas.logsPanel.getLogEntries().nth(1)).toContainText('Success');
|
||||
});
|
||||
|
||||
// eslint-disable-next-line n8n-local-rules/no-skipped-tests -- Flaky in multi-main mode due to webhook registration timing issues
|
||||
test.skip('should allow to cancel a workflow with a node that waits for webhook', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
test('should allow to cancel a workflow with a node that waits for webhook', async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('Workflow_wait_for_webhook.json');
|
||||
await n8n.canvas.deselectAll();
|
||||
await n8n.canvas.logsPanel.open();
|
||||
|
|
|
|||
|
|
@ -31,20 +31,6 @@ test.describe('NDV', () => {
|
|||
await expect(n8n.ndv.inputPanel.get()).toContainText('No input connected');
|
||||
});
|
||||
|
||||
test('should test webhook node', async ({ n8n }) => {
|
||||
await n8n.canvas.addNode('Webhook', { closeNDV: false });
|
||||
|
||||
await n8n.ndv.execute();
|
||||
|
||||
const webhookUrl = await n8n.ndv.getWebhookUrl();
|
||||
await expect(n8n.ndv.getWebhookTriggerListening()).toBeVisible();
|
||||
const response = await n8n.ndv.makeWebhookRequest(webhookUrl as string);
|
||||
expect(response.status()).toBe(200);
|
||||
|
||||
await expect(n8n.ndv.outputPanel.get()).toBeVisible();
|
||||
await expect(n8n.ndv.outputPanel.getDataContainer()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should change input and go back to canvas', async ({ n8n }) => {
|
||||
await n8n.start.fromImportedWorkflow('NDV-test-select-input.json');
|
||||
await n8n.canvas.clickZoomToFitButton();
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ test.describe('Parent that does not wait for sub-workflow', () => {
|
|||
const { webhookPath, workflowId } =
|
||||
await api.workflows.importWorkflowFromDefinition(workflowDefinition);
|
||||
|
||||
const response = await api.request.get(`/webhook/${webhookPath}`);
|
||||
const response = await api.webhooks.trigger(`/webhook/${webhookPath}`);
|
||||
expect(response.ok()).toBe(true);
|
||||
const execution = await api.workflows.waitForExecution(workflowId, 5000);
|
||||
expect(execution.status).toBe('success');
|
||||
|
|
@ -57,7 +57,7 @@ test.describe('Parent that does not wait for sub-workflow', () => {
|
|||
const { webhookPath, workflowId } =
|
||||
await api.workflows.importWorkflowFromDefinition(workflowDefinition);
|
||||
|
||||
const response = await api.request.get(`/webhook/${webhookPath}`);
|
||||
const response = await api.webhooks.trigger(`/webhook/${webhookPath}`);
|
||||
expect(response.ok()).toBe(true);
|
||||
|
||||
// First, wait for the child to finish.
|
||||
|
|
@ -105,7 +105,7 @@ test.describe('CAT-1801: Parent receives correct data from child with wait node'
|
|||
await api.workflows.activate(parentWorkflowId, versionId!);
|
||||
|
||||
// Trigger parent workflow via webhook
|
||||
const webhookResponse = await api.request.get(`/webhook/${webhookPath}`);
|
||||
const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`);
|
||||
expect(webhookResponse.ok()).toBe(true);
|
||||
|
||||
// Wait for child execution to appear and enter waiting state
|
||||
|
|
@ -120,7 +120,9 @@ test.describe('CAT-1801: Parent receives correct data from child with wait node'
|
|||
);
|
||||
|
||||
// Trigger the wait webhook to resume child using child execution ID
|
||||
const waitWebhookResponse = await api.request.get(`/webhook-waiting/${childExecution!.id}`);
|
||||
const waitWebhookResponse = await api.webhooks.trigger(
|
||||
`/webhook-waiting/${childExecution!.id}`,
|
||||
);
|
||||
expect(waitWebhookResponse.ok()).toBe(true);
|
||||
|
||||
// Wait for parent to complete
|
||||
|
|
@ -169,7 +171,7 @@ test.describe('CAT-1929: Parent should not resume until child with multiple wait
|
|||
await api.workflows.activate(parentWorkflowId, versionId!);
|
||||
|
||||
// Trigger parent workflow via webhook
|
||||
const webhookResponse = await api.request.get(`/webhook/${webhookPath}`);
|
||||
const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`);
|
||||
expect(webhookResponse.ok()).toBe(true);
|
||||
|
||||
// Wait for child execution to appear and enter waiting state (first wait node)
|
||||
|
|
@ -179,7 +181,7 @@ test.describe('CAT-1929: Parent should not resume until child with multiple wait
|
|||
await api.workflows.waitForWorkflowStatus(parentWorkflowId, 'waiting');
|
||||
|
||||
// Resume first wait node
|
||||
const firstWaitResponse = await api.request.get(`/webhook-waiting/${childExecution.id}`);
|
||||
const firstWaitResponse = await api.webhooks.trigger(`/webhook-waiting/${childExecution.id}`);
|
||||
expect(firstWaitResponse.ok()).toBe(true);
|
||||
|
||||
// Wait for child to reach the second wait node
|
||||
|
|
@ -193,7 +195,7 @@ test.describe('CAT-1929: Parent should not resume until child with multiple wait
|
|||
expect(parentExecAfterFirstWait.status).toBe('waiting');
|
||||
|
||||
// Resume second wait node
|
||||
const secondWaitResponse = await api.request.get(`/webhook-waiting/${childExecution.id}`);
|
||||
const secondWaitResponse = await api.webhooks.trigger(`/webhook-waiting/${childExecution.id}`);
|
||||
expect(secondWaitResponse.ok()).toBe(true);
|
||||
|
||||
// Now parent should complete
|
||||
|
|
|
|||
|
|
@ -951,7 +951,7 @@ export interface FunctionsBase {
|
|||
getMode?: () => WorkflowExecuteMode;
|
||||
getActivationMode?: () => WorkflowActivateMode;
|
||||
getChatTrigger: () => INode | null;
|
||||
|
||||
isNodeFeatureEnabled(featureName: string): boolean;
|
||||
getExecutionContext: () => IExecutionContext | undefined;
|
||||
|
||||
/** @deprecated */
|
||||
|
|
@ -1549,12 +1549,17 @@ export type DisplayCondition =
|
|||
| { _cnd: { regex: string } }
|
||||
| { _cnd: { exists: true } };
|
||||
|
||||
export type NodeFeatures = Record<string, boolean>;
|
||||
export type FeatureCondition = { '@version': Array<number | DisplayCondition> };
|
||||
export type NodeFeaturesDefinition = Record<string, FeatureCondition>;
|
||||
|
||||
export interface IDisplayOptions {
|
||||
hide?: {
|
||||
[key: string]: Array<NodeParameterValue | DisplayCondition> | undefined;
|
||||
};
|
||||
show?: {
|
||||
'@version'?: Array<number | DisplayCondition>;
|
||||
'@feature'?: Array<string | DisplayCondition>;
|
||||
'@tool'?: boolean[];
|
||||
[key: string]: Array<NodeParameterValue | DisplayCondition> | undefined;
|
||||
};
|
||||
|
|
@ -2241,6 +2246,7 @@ export interface INodeTypeDescription extends INodeTypeBaseDescription {
|
|||
communityNodePackageVersion?: string;
|
||||
waitingNodeTooltip?: string;
|
||||
__loadOptionsMethods?: string[]; // only for validation during build
|
||||
features?: NodeFeaturesDefinition;
|
||||
}
|
||||
|
||||
export type TriggerPanelDefinition = {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ import type {
|
|||
DisplayCondition,
|
||||
NodeConnectionType,
|
||||
ICredentialDataDecryptedObject,
|
||||
FeatureCondition,
|
||||
NodeFeaturesDefinition,
|
||||
NodeFeatures,
|
||||
} from './interfaces';
|
||||
import { validateFilterParameter } from './node-parameters/filter-parameter';
|
||||
import type { IRunExecutionData } from './run-execution-data/run-execution-data';
|
||||
|
|
@ -252,6 +255,37 @@ export function isSubNodeType(
|
|||
: false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a feature condition against a node version.
|
||||
* @param featureDef The feature condition definition
|
||||
* @param nodeVersion The node version to evaluate against
|
||||
* @returns true if the feature is enabled, false otherwise
|
||||
*/
|
||||
function evaluateFeature(featureDef: FeatureCondition, nodeVersion: number): boolean {
|
||||
return checkConditions(featureDef['@version'], [nodeVersion]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates all feature definitions for a node type and returns the computed features.
|
||||
* @param featuresDef The feature definitions from the node type description
|
||||
* @param nodeVersion The node version to evaluate against
|
||||
* @returns A record of feature names to their enabled state
|
||||
*/
|
||||
export function getNodeFeatures(
|
||||
featuresDef: NodeFeaturesDefinition | undefined,
|
||||
nodeVersion: number,
|
||||
): NodeFeatures {
|
||||
if (!featuresDef) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const features: NodeFeatures = {};
|
||||
for (const [featureName, condition] of Object.entries(featuresDef)) {
|
||||
features[featureName] = evaluateFeature(condition, nodeVersion);
|
||||
}
|
||||
return features;
|
||||
}
|
||||
|
||||
const getPropertyValues = (
|
||||
nodeValues: INodeParameters | ICredentialDataDecryptedObject,
|
||||
propertyName: string,
|
||||
|
|
@ -267,6 +301,15 @@ const getPropertyValues = (
|
|||
value = node?.typeVersion || 0;
|
||||
} else if (propertyName === '@tool') {
|
||||
value = nodeTypeDescription?.name.endsWith('Tool') ?? false;
|
||||
} else if (propertyName === '@feature') {
|
||||
if (!nodeTypeDescription?.features || !node?.typeVersion) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const features = getNodeFeatures(nodeTypeDescription.features, node.typeVersion);
|
||||
return Object.entries(features)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([name]) => name);
|
||||
} else {
|
||||
// Get the value from current level
|
||||
value = get(nodeValues, propertyName);
|
||||
|
|
@ -296,6 +339,12 @@ const checkConditions = (
|
|||
) {
|
||||
const [key, targetValue] = Object.entries(condition._cnd)[0];
|
||||
|
||||
// Special case: empty array handling
|
||||
if (actualValues.length === 0) {
|
||||
if (key === 'not') return true; // Value is not present, so 'not' is true
|
||||
return false; // For all other keys, empty array means condition is not met
|
||||
}
|
||||
|
||||
return actualValues.every((propertyValue) => {
|
||||
if (key === 'eq') {
|
||||
return isEqual(propertyValue, targetValue);
|
||||
|
|
@ -381,7 +430,7 @@ export function displayParameter(
|
|||
return true;
|
||||
}
|
||||
|
||||
if (values.length === 0 || !checkConditions(show[propertyName]!, values)) {
|
||||
if (!checkConditions(show[propertyName]!, values)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -302,6 +302,7 @@ export const IDisplayOptionsSchema: z.ZodType<IDisplayOptions> = z.object({
|
|||
show: z
|
||||
.object({
|
||||
'@version': z.array(z.union([z.number(), DisplayConditionSchema])).optional(),
|
||||
'@feature': z.array(z.union([z.string(), DisplayConditionSchema])).optional(),
|
||||
'@tool': z.array(z.boolean()).optional(),
|
||||
})
|
||||
.catchall(
|
||||
|
|
|
|||
|
|
@ -4866,6 +4866,205 @@ describe('NodeHelpers', () => {
|
|||
},
|
||||
false,
|
||||
],
|
||||
[
|
||||
'Should return true if @feature is enabled (simple string array)',
|
||||
{
|
||||
...defaultTestInput,
|
||||
node: {
|
||||
...testNode,
|
||||
typeVersion: 2.4,
|
||||
},
|
||||
nodeTypeDescription: {
|
||||
...testNodeType,
|
||||
features: {
|
||||
useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] },
|
||||
},
|
||||
},
|
||||
parameter: {
|
||||
...defaultTestInput.parameter,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@feature': ['useFeatureA'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
],
|
||||
[
|
||||
'Should return false if @feature is not enabled (simple string array)',
|
||||
{
|
||||
...defaultTestInput,
|
||||
node: {
|
||||
...testNode,
|
||||
typeVersion: 2.3,
|
||||
},
|
||||
nodeTypeDescription: {
|
||||
...testNodeType,
|
||||
features: {
|
||||
useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] },
|
||||
},
|
||||
},
|
||||
parameter: {
|
||||
...defaultTestInput.parameter,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@feature': ['useFeatureA'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
],
|
||||
[
|
||||
'Should return true if @feature condition eq is met',
|
||||
{
|
||||
...defaultTestInput,
|
||||
node: {
|
||||
...testNode,
|
||||
typeVersion: 2.4,
|
||||
},
|
||||
nodeTypeDescription: {
|
||||
...testNodeType,
|
||||
features: {
|
||||
useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] },
|
||||
},
|
||||
},
|
||||
parameter: {
|
||||
...defaultTestInput.parameter,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@feature': [{ _cnd: { eq: 'useFeatureA' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
],
|
||||
[
|
||||
'Should return false if @feature condition eq is not met',
|
||||
{
|
||||
...defaultTestInput,
|
||||
node: {
|
||||
...testNode,
|
||||
typeVersion: 2.3,
|
||||
},
|
||||
nodeTypeDescription: {
|
||||
...testNodeType,
|
||||
features: {
|
||||
useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] },
|
||||
},
|
||||
},
|
||||
parameter: {
|
||||
...defaultTestInput.parameter,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@feature': [{ _cnd: { eq: 'useFeatureA' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
],
|
||||
[
|
||||
'Should return true if @feature condition not is met (feature disabled)',
|
||||
{
|
||||
...defaultTestInput,
|
||||
node: {
|
||||
...testNode,
|
||||
typeVersion: 2.3,
|
||||
},
|
||||
nodeTypeDescription: {
|
||||
...testNodeType,
|
||||
features: {
|
||||
useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] },
|
||||
},
|
||||
},
|
||||
parameter: {
|
||||
...defaultTestInput.parameter,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@feature': [{ _cnd: { not: 'useFeatureA' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
],
|
||||
[
|
||||
'Should return false if @feature condition not is not met (feature enabled)',
|
||||
{
|
||||
...defaultTestInput,
|
||||
node: {
|
||||
...testNode,
|
||||
typeVersion: 2.4,
|
||||
},
|
||||
nodeTypeDescription: {
|
||||
...testNodeType,
|
||||
features: {
|
||||
useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] },
|
||||
},
|
||||
},
|
||||
parameter: {
|
||||
...defaultTestInput.parameter,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@feature': [{ _cnd: { not: 'useFeatureA' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
],
|
||||
[
|
||||
'Should return true if @feature with multiple conditions (OR logic)',
|
||||
{
|
||||
...defaultTestInput,
|
||||
node: {
|
||||
...testNode,
|
||||
typeVersion: 2.4,
|
||||
},
|
||||
nodeTypeDescription: {
|
||||
...testNodeType,
|
||||
features: {
|
||||
useFeatureA: { '@version': [{ _cnd: { gte: 2.4 } }] },
|
||||
useFeatureB: { '@version': [{ _cnd: { lte: 2.1 } }] },
|
||||
},
|
||||
},
|
||||
parameter: {
|
||||
...defaultTestInput.parameter,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@feature': [{ _cnd: { eq: 'useFeatureA' } }, { _cnd: { eq: 'useFeatureB' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
true,
|
||||
],
|
||||
[
|
||||
'Should return false if @feature is checked but node has no features',
|
||||
{
|
||||
...defaultTestInput,
|
||||
node: {
|
||||
...testNode,
|
||||
typeVersion: 2.4,
|
||||
},
|
||||
nodeTypeDescription: {
|
||||
...testNodeType,
|
||||
features: undefined,
|
||||
},
|
||||
parameter: {
|
||||
...defaultTestInput.parameter,
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@feature': ['useFeatureA'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
false,
|
||||
],
|
||||
];
|
||||
|
||||
for (const [description, input, expected] of tests) {
|
||||
|
|
|
|||
153
pnpm-lock.yaml
153
pnpm-lock.yaml
|
|
@ -279,6 +279,9 @@ importers:
|
|||
|
||||
.:
|
||||
devDependencies:
|
||||
'@babel/preset-env':
|
||||
specifier: ^7.26.0
|
||||
version: 7.28.5(@babel/core@7.28.4)
|
||||
'@biomejs/biome':
|
||||
specifier: ^1.9.0
|
||||
version: 1.9.0
|
||||
|
|
@ -555,7 +558,7 @@ importers:
|
|||
version: 4.0.7
|
||||
axios:
|
||||
specifier: 1.12.0
|
||||
version: 1.12.0(debug@4.4.3)
|
||||
version: 1.12.0(debug@4.4.1)
|
||||
dotenv:
|
||||
specifier: 17.2.3
|
||||
version: 17.2.3
|
||||
|
|
@ -580,7 +583,7 @@ importers:
|
|||
dependencies:
|
||||
axios:
|
||||
specifier: 1.12.0
|
||||
version: 1.12.0(debug@4.4.3)
|
||||
version: 1.12.0(debug@4.4.1)
|
||||
devDependencies:
|
||||
'@n8n/typescript-config':
|
||||
specifier: workspace:*
|
||||
|
|
@ -1340,7 +1343,7 @@ importers:
|
|||
version: link:../eslint-plugin-community-nodes
|
||||
axios:
|
||||
specifier: 1.12.0
|
||||
version: 1.12.0(debug@4.4.3)
|
||||
version: 1.12.0(debug@4.4.1)
|
||||
eslint:
|
||||
specifier: 'catalog:'
|
||||
version: 9.29.0(jiti@2.6.1)
|
||||
|
|
@ -1554,7 +1557,7 @@ importers:
|
|||
version: 1.11.0
|
||||
axios:
|
||||
specifier: 1.12.0
|
||||
version: 1.12.0(debug@4.4.3)
|
||||
version: 1.12.0(debug@4.4.1)
|
||||
bcryptjs:
|
||||
specifier: 2.4.3
|
||||
version: 2.4.3
|
||||
|
|
@ -1909,7 +1912,7 @@ importers:
|
|||
version: 9.42.1
|
||||
axios:
|
||||
specifier: 1.12.0
|
||||
version: 1.12.0(debug@4.4.3)
|
||||
version: 1.12.0(debug@4.4.1)
|
||||
callsites:
|
||||
specifier: 'catalog:'
|
||||
version: 3.1.0
|
||||
|
|
@ -2376,7 +2379,7 @@ importers:
|
|||
version: link:../../../@n8n/utils
|
||||
axios:
|
||||
specifier: 1.12.0
|
||||
version: 1.12.0(debug@4.4.3)
|
||||
version: 1.12.0(debug@4.4.1)
|
||||
flatted:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.7
|
||||
|
|
@ -2633,7 +2636,7 @@ importers:
|
|||
version: 1.1.4
|
||||
axios:
|
||||
specifier: 1.12.0
|
||||
version: 1.12.0(debug@4.4.3)
|
||||
version: 1.12.0(debug@4.4.1)
|
||||
bowser:
|
||||
specifier: 2.11.0
|
||||
version: 2.11.0
|
||||
|
|
@ -3963,10 +3966,6 @@ packages:
|
|||
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/compat-data@7.27.5':
|
||||
resolution: {integrity: sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/compat-data@7.28.5':
|
||||
resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -3983,10 +3982,6 @@ packages:
|
|||
resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-annotate-as-pure@7.25.9':
|
||||
resolution: {integrity: sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/helper-annotate-as-pure@7.27.3':
|
||||
resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -4001,12 +3996,6 @@ packages:
|
|||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
|
||||
'@babel/helper-create-regexp-features-plugin@7.26.3':
|
||||
resolution: {integrity: sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
peerDependencies:
|
||||
'@babel/core': ^7.0.0
|
||||
|
||||
'@babel/helper-create-regexp-features-plugin@7.28.5':
|
||||
resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -4578,10 +4567,6 @@ packages:
|
|||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/traverse@7.28.4':
|
||||
resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/traverse@7.28.5':
|
||||
resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
|
@ -15900,10 +15885,6 @@ packages:
|
|||
reftools@1.1.9:
|
||||
resolution: {integrity: sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w==}
|
||||
|
||||
regenerate-unicode-properties@10.2.0:
|
||||
resolution: {integrity: sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
regenerate-unicode-properties@10.2.2:
|
||||
resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -15930,10 +15911,6 @@ packages:
|
|||
resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
regexpu-core@6.2.0:
|
||||
resolution: {integrity: sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
regexpu-core@6.4.0:
|
||||
resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -17545,10 +17522,6 @@ packages:
|
|||
resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
unicode-match-property-value-ecmascript@2.2.0:
|
||||
resolution: {integrity: sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
unicode-match-property-value-ecmascript@2.2.1:
|
||||
resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -17941,8 +17914,8 @@ packages:
|
|||
vue-component-type-helpers@2.2.12:
|
||||
resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
|
||||
|
||||
vue-component-type-helpers@3.1.7:
|
||||
resolution: {integrity: sha512-SxZOijzRU0LiTYARVZNYf//21bZRLJ26P2prcA6a+FfDQxJtan4tEnsryIY7aiLh2LVoet0ci2G+6RrArQ+7tA==}
|
||||
vue-component-type-helpers@3.1.8:
|
||||
resolution: {integrity: sha512-oaowlmEM6BaYY+8o+9D9cuzxpWQWHqHTMKakMxXu0E+UCIOMTljyIPO15jcnaCwJtZu/zWDotK7mOIHvWD9mcw==}
|
||||
|
||||
vue-demi@0.14.10:
|
||||
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
|
||||
|
|
@ -20273,8 +20246,6 @@ snapshots:
|
|||
js-tokens: 4.0.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
'@babel/compat-data@7.27.5': {}
|
||||
|
||||
'@babel/compat-data@7.28.5': {}
|
||||
|
||||
'@babel/core@7.28.4':
|
||||
|
|
@ -20286,7 +20257,7 @@ snapshots:
|
|||
'@babel/helpers': 7.28.4
|
||||
'@babel/parser': 7.28.5
|
||||
'@babel/template': 7.27.2
|
||||
'@babel/traverse': 7.28.4
|
||||
'@babel/traverse': 7.28.5
|
||||
'@babel/types': 7.28.5
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
convert-source-map: 2.0.0
|
||||
|
|
@ -20313,17 +20284,13 @@ snapshots:
|
|||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jsesc: 3.1.0
|
||||
|
||||
'@babel/helper-annotate-as-pure@7.25.9':
|
||||
dependencies:
|
||||
'@babel/types': 7.28.5
|
||||
|
||||
'@babel/helper-annotate-as-pure@7.27.3':
|
||||
dependencies:
|
||||
'@babel/types': 7.28.5
|
||||
|
||||
'@babel/helper-compilation-targets@7.27.2':
|
||||
dependencies:
|
||||
'@babel/compat-data': 7.27.5
|
||||
'@babel/compat-data': 7.28.5
|
||||
'@babel/helper-validator-option': 7.27.1
|
||||
browserslist: 4.27.0
|
||||
lru-cache: 5.1.1
|
||||
|
|
@ -20342,13 +20309,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-create-regexp-features-plugin@7.26.3(@babel/core@7.28.4)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/helper-annotate-as-pure': 7.25.9
|
||||
regexpu-core: 6.2.0
|
||||
semver: 7.7.3
|
||||
|
||||
'@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.28.4)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.4
|
||||
|
|
@ -20403,7 +20363,7 @@ snapshots:
|
|||
'@babel/core': 7.28.4
|
||||
'@babel/helper-annotate-as-pure': 7.27.3
|
||||
'@babel/helper-wrap-function': 7.28.3
|
||||
'@babel/traverse': 7.28.4
|
||||
'@babel/traverse': 7.28.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -20412,13 +20372,13 @@ snapshots:
|
|||
'@babel/core': 7.28.4
|
||||
'@babel/helper-member-expression-to-functions': 7.28.5
|
||||
'@babel/helper-optimise-call-expression': 7.27.1
|
||||
'@babel/traverse': 7.28.4
|
||||
'@babel/traverse': 7.28.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/helper-skip-transparent-expression-wrappers@7.27.1':
|
||||
dependencies:
|
||||
'@babel/traverse': 7.28.4
|
||||
'@babel/traverse': 7.28.5
|
||||
'@babel/types': 7.28.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -20434,7 +20394,7 @@ snapshots:
|
|||
'@babel/helper-wrap-function@7.28.3':
|
||||
dependencies:
|
||||
'@babel/template': 7.27.2
|
||||
'@babel/traverse': 7.28.4
|
||||
'@babel/traverse': 7.28.5
|
||||
'@babel/types': 7.28.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -20483,7 +20443,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
'@babel/traverse': 7.28.4
|
||||
'@babel/traverse': 7.28.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -20594,7 +20554,7 @@ snapshots:
|
|||
'@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.28.4)':
|
||||
dependencies:
|
||||
'@babel/core': 7.28.4
|
||||
'@babel/helper-create-regexp-features-plugin': 7.26.3(@babel/core@7.28.4)
|
||||
'@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.28.4)
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
|
||||
'@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.28.4)':
|
||||
|
|
@ -20607,7 +20567,7 @@ snapshots:
|
|||
'@babel/core': 7.28.4
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
'@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.28.4)
|
||||
'@babel/traverse': 7.28.4
|
||||
'@babel/traverse': 7.28.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -20654,7 +20614,7 @@ snapshots:
|
|||
'@babel/helper-globals': 7.28.0
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
'@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4)
|
||||
'@babel/traverse': 7.28.4
|
||||
'@babel/traverse': 7.28.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -20725,7 +20685,7 @@ snapshots:
|
|||
'@babel/core': 7.28.4
|
||||
'@babel/helper-compilation-targets': 7.27.2
|
||||
'@babel/helper-plugin-utils': 7.27.1
|
||||
'@babel/traverse': 7.28.4
|
||||
'@babel/traverse': 7.28.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -20811,7 +20771,7 @@ snapshots:
|
|||
'@babel/helper-plugin-utils': 7.27.1
|
||||
'@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.28.4)
|
||||
'@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.28.4)
|
||||
'@babel/traverse': 7.28.4
|
||||
'@babel/traverse': 7.28.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -21031,18 +20991,6 @@ snapshots:
|
|||
'@babel/parser': 7.28.5
|
||||
'@babel/types': 7.28.5
|
||||
|
||||
'@babel/traverse@7.28.4':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
'@babel/generator': 7.28.5
|
||||
'@babel/helper-globals': 7.28.0
|
||||
'@babel/parser': 7.28.5
|
||||
'@babel/template': 7.27.2
|
||||
'@babel/types': 7.28.5
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@babel/traverse@7.28.5':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
|
|
@ -23628,7 +23576,7 @@ snapshots:
|
|||
|
||||
'@rudderstack/rudder-sdk-node@2.1.4(tslib@2.8.1)':
|
||||
dependencies:
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
axios-retry: 4.5.0(axios@1.12.0)
|
||||
component-type: 2.0.0
|
||||
join-component: 1.1.0
|
||||
|
|
@ -24930,7 +24878,7 @@ snapshots:
|
|||
storybook: 9.1.7(@testing-library/dom@10.4.0)(bufferutil@4.0.9)(prettier@3.6.2)(utf-8-validate@5.0.10)(vite@7.0.0(@types/node@20.19.21)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.89.2)(terser@5.16.1)(tsx@4.19.3))
|
||||
type-fest: 2.19.0
|
||||
vue: 3.5.13(typescript@5.9.2)
|
||||
vue-component-type-helpers: 3.1.7
|
||||
vue-component-type-helpers: 3.1.8
|
||||
|
||||
'@stylistic/eslint-plugin@5.0.0(eslint@9.29.0(jiti@2.6.1))':
|
||||
dependencies:
|
||||
|
|
@ -26719,7 +26667,7 @@ snapshots:
|
|||
|
||||
axios-retry@4.5.0(axios@1.12.0):
|
||||
dependencies:
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
is-retry-allowed: 2.2.0
|
||||
|
||||
axios@1.12.0(debug@4.3.6):
|
||||
|
|
@ -26738,14 +26686,6 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
axios@1.12.0(debug@4.4.3):
|
||||
dependencies:
|
||||
follow-redirects: 1.15.11(debug@4.4.1)
|
||||
form-data: 4.0.4
|
||||
proxy-from-env: 1.1.0
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
||||
b4a@1.6.7: {}
|
||||
|
||||
babel-jest@29.6.2(@babel/core@7.28.4):
|
||||
|
|
@ -27049,10 +26989,10 @@ snapshots:
|
|||
|
||||
browserslist@4.25.0:
|
||||
dependencies:
|
||||
caniuse-lite: 1.0.30001724
|
||||
electron-to-chromium: 1.5.173
|
||||
node-releases: 2.0.19
|
||||
update-browserslist-db: 1.1.3(browserslist@4.25.0)
|
||||
caniuse-lite: 1.0.30001752
|
||||
electron-to-chromium: 1.5.244
|
||||
node-releases: 2.0.27
|
||||
update-browserslist-db: 1.1.4(browserslist@4.25.0)
|
||||
|
||||
browserslist@4.27.0:
|
||||
dependencies:
|
||||
|
|
@ -27132,7 +27072,7 @@ snapshots:
|
|||
|
||||
bundlemon@3.1.0(typescript@5.9.2):
|
||||
dependencies:
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
axios-retry: 4.5.0(axios@1.12.0)
|
||||
brotli-size: 4.0.0
|
||||
bundlemon-utils: 2.0.1
|
||||
|
|
@ -30235,7 +30175,7 @@ snapshots:
|
|||
'@types/debug': 4.1.12
|
||||
'@types/node': 20.19.21
|
||||
'@types/tough-cookie': 4.0.5
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
camelcase: 6.3.0
|
||||
debug: 4.4.3
|
||||
dotenv: 16.6.1
|
||||
|
|
@ -30323,7 +30263,7 @@ snapshots:
|
|||
|
||||
infisical-node@1.3.0:
|
||||
dependencies:
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
dotenv: 16.6.1
|
||||
tweetnacl: 1.0.3
|
||||
tweetnacl-util: 0.15.1
|
||||
|
|
@ -33903,7 +33843,7 @@ snapshots:
|
|||
|
||||
posthog-node@3.2.1:
|
||||
dependencies:
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
rusha: 0.8.14
|
||||
transitivePeerDependencies:
|
||||
- debug
|
||||
|
|
@ -34470,10 +34410,6 @@ snapshots:
|
|||
|
||||
reftools@1.1.9: {}
|
||||
|
||||
regenerate-unicode-properties@10.2.0:
|
||||
dependencies:
|
||||
regenerate: 1.4.2
|
||||
|
||||
regenerate-unicode-properties@10.2.2:
|
||||
dependencies:
|
||||
regenerate: 1.4.2
|
||||
|
|
@ -34509,15 +34445,6 @@ snapshots:
|
|||
gopd: 1.2.0
|
||||
set-function-name: 2.0.2
|
||||
|
||||
regexpu-core@6.2.0:
|
||||
dependencies:
|
||||
regenerate: 1.4.2
|
||||
regenerate-unicode-properties: 10.2.0
|
||||
regjsgen: 0.8.0
|
||||
regjsparser: 0.12.0
|
||||
unicode-match-property-ecmascript: 2.0.0
|
||||
unicode-match-property-value-ecmascript: 2.2.0
|
||||
|
||||
regexpu-core@6.4.0:
|
||||
dependencies:
|
||||
regenerate: 1.4.2
|
||||
|
|
@ -34628,7 +34555,7 @@ snapshots:
|
|||
|
||||
retry-axios@2.6.0(axios@1.12.0(debug@4.4.3)):
|
||||
dependencies:
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
|
||||
retry-request@7.0.2(encoding@0.1.13):
|
||||
dependencies:
|
||||
|
|
@ -35234,7 +35161,7 @@ snapshots:
|
|||
asn1.js: 5.4.1
|
||||
asn1.js-rfc2560: 5.0.1(asn1.js@5.4.1)
|
||||
asn1.js-rfc5280: 3.0.0
|
||||
axios: 1.12.0(debug@4.4.3)
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
big-integer: 1.6.52
|
||||
bignumber.js: 9.1.2
|
||||
binascii: 0.0.2
|
||||
|
|
@ -36565,8 +36492,6 @@ snapshots:
|
|||
unicode-canonical-property-names-ecmascript: 2.0.1
|
||||
unicode-property-aliases-ecmascript: 2.1.0
|
||||
|
||||
unicode-match-property-value-ecmascript@2.2.0: {}
|
||||
|
||||
unicode-match-property-value-ecmascript@2.2.1: {}
|
||||
|
||||
unicode-property-aliases-ecmascript@2.1.0: {}
|
||||
|
|
@ -36652,7 +36577,7 @@ snapshots:
|
|||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
update-browserslist-db@1.1.3(browserslist@4.25.0):
|
||||
update-browserslist-db@1.1.4(browserslist@4.25.0):
|
||||
dependencies:
|
||||
browserslist: 4.25.0
|
||||
escalade: 3.2.0
|
||||
|
|
@ -36962,7 +36887,7 @@ snapshots:
|
|||
|
||||
vue-component-type-helpers@2.2.12: {}
|
||||
|
||||
vue-component-type-helpers@3.1.7: {}
|
||||
vue-component-type-helpers@3.1.8: {}
|
||||
|
||||
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
|
||||
dependencies:
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user