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:
+74
@@ -0,0 +1,74 @@
|
||||
import { test, expect } from '../../../../fixtures/base';
|
||||
|
||||
const MOCK_PACKAGE = {
|
||||
createdAt: '2024-07-22T19:08:06.505Z',
|
||||
updatedAt: '2024-07-22T19:08:06.505Z',
|
||||
packageName: 'n8n-nodes-chatwork',
|
||||
installedVersion: '1.0.0',
|
||||
authorName: null,
|
||||
authorEmail: null,
|
||||
installedNodes: [
|
||||
{
|
||||
name: 'Chatwork',
|
||||
type: 'n8n-nodes-chatwork.chatwork',
|
||||
latestVersion: 1,
|
||||
},
|
||||
],
|
||||
updateAvailable: '1.1.2',
|
||||
};
|
||||
|
||||
test.describe('Community nodes management', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'NODES' },
|
||||
],
|
||||
}, () => {
|
||||
test('can install, update and uninstall community nodes', async ({ n8n }) => {
|
||||
await n8n.page.route('**/api.npms.io/v2/search*', async (route) => {
|
||||
await route.fulfill({ status: 200, json: {} });
|
||||
});
|
||||
|
||||
await n8n.page.route('/rest/community-packages', async (route) => {
|
||||
if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: { data: [] } });
|
||||
}
|
||||
});
|
||||
|
||||
await n8n.navigate.toCommunityNodes();
|
||||
|
||||
await n8n.page.route('/rest/community-packages', async (route) => {
|
||||
if (route.request().method() === 'POST') {
|
||||
await route.fulfill({ status: 200, json: { data: MOCK_PACKAGE } });
|
||||
} else if (route.request().method() === 'GET') {
|
||||
await route.fulfill({ status: 200, json: { data: [MOCK_PACKAGE] } });
|
||||
}
|
||||
});
|
||||
|
||||
await n8n.communityNodes.installPackage('n8n-nodes-chatwork@1.0.0');
|
||||
await expect(n8n.communityNodes.getCommunityCards()).toHaveCount(1);
|
||||
await expect(n8n.communityNodes.getCommunityCards().first()).toContainText('v1.0.0');
|
||||
|
||||
const updatedPackage = {
|
||||
...MOCK_PACKAGE,
|
||||
installedVersion: '1.2.0',
|
||||
updateAvailable: undefined,
|
||||
};
|
||||
await n8n.page.route('/rest/community-packages', async (route) => {
|
||||
if (route.request().method() === 'PATCH') {
|
||||
await route.fulfill({ status: 200, json: { data: updatedPackage } });
|
||||
}
|
||||
});
|
||||
|
||||
await n8n.communityNodes.updatePackage();
|
||||
await expect(n8n.communityNodes.getCommunityCards()).toHaveCount(1);
|
||||
await expect(n8n.communityNodes.getCommunityCards().first()).not.toContainText('v1.0.0');
|
||||
|
||||
await n8n.page.route('/rest/community-packages*', async (route) => {
|
||||
if (route.request().method() === 'DELETE') {
|
||||
await route.fulfill({ status: 204 });
|
||||
}
|
||||
});
|
||||
|
||||
await n8n.communityNodes.uninstallPackage();
|
||||
await expect(n8n.communityNodes.getActionBox()).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,145 @@
|
||||
import { expect, test } from '../../../../fixtures/base';
|
||||
import type { n8nPage } from '../../../../pages/n8nPage';
|
||||
import {
|
||||
buildRepoUrl,
|
||||
generateUniqueRepoName,
|
||||
initSourceControl,
|
||||
} from '../../../../utils/source-control-helper';
|
||||
|
||||
test.use({ capability: 'source-control' });
|
||||
|
||||
async function saveSettings(n8n: n8nPage) {
|
||||
await n8n.settingsEnvironment.getSaveButton().click();
|
||||
await n8n.page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/rest/source-control/preferences') &&
|
||||
response.request().method() === 'PATCH',
|
||||
);
|
||||
}
|
||||
|
||||
// Skipped: These tests are flaky. Re-enable when PAY-4365 is resolved.
|
||||
// https://linear.app/n8n/issue/PAY-4365/bug-source-control-operations-fail-in-multi-main-deployment
|
||||
test.describe(
|
||||
'Source Control Settings @capability:source-control',
|
||||
{
|
||||
annotation: [{ type: 'owner', description: 'Lifecycle & Governance' }],
|
||||
},
|
||||
() => {
|
||||
test.fixme();
|
||||
|
||||
let repoUrl: string;
|
||||
let repoName: string;
|
||||
|
||||
test.beforeEach(async ({ n8n, services }) => {
|
||||
await n8n.api.enableFeature('sourceControl');
|
||||
const gitea = services.gitea;
|
||||
await initSourceControl({ n8n, gitea });
|
||||
|
||||
// Create unique repo with branches via API (not UI)
|
||||
repoName = generateUniqueRepoName();
|
||||
await gitea.createRepo(repoName);
|
||||
|
||||
repoUrl = buildRepoUrl(repoName);
|
||||
});
|
||||
|
||||
test('should connect to Git repository using SSH', async ({ n8n }) => {
|
||||
// Test UI connection flow with unique repo
|
||||
await n8n.navigate.toEnvironments();
|
||||
|
||||
await n8n.settingsEnvironment.fillRepoUrl(repoUrl);
|
||||
await expect(n8n.settingsEnvironment.getConnectButton()).toBeEnabled();
|
||||
await n8n.settingsEnvironment.getConnectButton().click();
|
||||
|
||||
await expect(n8n.settingsEnvironment.getDisconnectButton()).toBeVisible();
|
||||
await expect(n8n.settingsEnvironment.getBranchSelect()).toBeVisible();
|
||||
|
||||
await n8n.settingsEnvironment.getBranchSelect().click();
|
||||
await expect(n8n.page.getByRole('option', { name: 'main' })).toBeVisible();
|
||||
|
||||
// Verify source control connected indicator is visible
|
||||
await n8n.navigate.toHome();
|
||||
await expect(n8n.sideBar.getSourceControlConnectedIndicator()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should switch between branches', async ({ n8n, services }) => {
|
||||
const gitea = services.gitea;
|
||||
await gitea.createBranch(repoName, 'development');
|
||||
await gitea.createBranch(repoName, 'staging');
|
||||
await gitea.createBranch(repoName, 'production');
|
||||
|
||||
await n8n.api.sourceControl.connect({ repositoryUrl: repoUrl });
|
||||
|
||||
await n8n.navigate.toEnvironments();
|
||||
|
||||
// Switch to 'development' branch
|
||||
await n8n.settingsEnvironment.getBranchSelect().click();
|
||||
await expect(n8n.page.getByRole('option', { name: 'main' })).toBeVisible();
|
||||
await expect(n8n.page.getByRole('option', { name: 'development' })).toBeVisible();
|
||||
await expect(n8n.page.getByRole('option', { name: 'staging' })).toBeVisible();
|
||||
await expect(n8n.page.getByRole('option', { name: 'production' })).toBeVisible();
|
||||
await n8n.page.getByRole('option', { name: 'development' }).click();
|
||||
await saveSettings(n8n);
|
||||
|
||||
// Verify branch switched by checking preferences
|
||||
let preferencesResponse = await n8n.page.request.get('/rest/source-control/preferences');
|
||||
let preferences = await preferencesResponse.json();
|
||||
expect(preferences.data.branchName).toBe('development');
|
||||
|
||||
// Switch back to 'main'
|
||||
await n8n.settingsEnvironment.selectBranch('main');
|
||||
await saveSettings(n8n);
|
||||
|
||||
// Verify switched back
|
||||
preferencesResponse = await n8n.page.request.get('/rest/source-control/preferences');
|
||||
preferences = await preferencesResponse.json();
|
||||
expect(preferences.data.branchName).toBe('main');
|
||||
});
|
||||
|
||||
test('should enable read-only mode and restrict operations', async ({ n8n }) => {
|
||||
await n8n.api.sourceControl.connect({ repositoryUrl: repoUrl });
|
||||
|
||||
await n8n.navigate.toEnvironments();
|
||||
|
||||
await n8n.settingsEnvironment.enableReadOnlyMode();
|
||||
await saveSettings(n8n);
|
||||
|
||||
// Verify push button is disabled in read-only mode
|
||||
await n8n.navigate.toHome();
|
||||
await expect(n8n.sideBar.getSourceControlPushButton()).toBeDisabled();
|
||||
await expect(n8n.sideBar.getSourceControlPullButton()).toBeEnabled();
|
||||
|
||||
await n8n.navigate.toEnvironments();
|
||||
await n8n.settingsEnvironment.disableReadOnlyMode();
|
||||
await saveSettings(n8n);
|
||||
|
||||
// Verify push button is enabled again
|
||||
await n8n.navigate.toHome();
|
||||
await expect(n8n.sideBar.getSourceControlPushButton()).toBeEnabled();
|
||||
await expect(n8n.sideBar.getSourceControlPullButton()).toBeEnabled();
|
||||
});
|
||||
|
||||
test('should disconnect and reconnect with existing keys', async ({ n8n }) => {
|
||||
await n8n.api.sourceControl.connect({ repositoryUrl: repoUrl });
|
||||
|
||||
await n8n.navigate.toEnvironments();
|
||||
await n8n.settingsEnvironment.disconnect();
|
||||
|
||||
// check that source control is disconnected
|
||||
await n8n.navigate.toHome();
|
||||
await expect(n8n.sideBar.getSourceControlConnectedIndicator()).toBeHidden();
|
||||
|
||||
// Reconnect
|
||||
await n8n.navigate.toEnvironments();
|
||||
await n8n.settingsEnvironment.fillRepoUrl(repoUrl);
|
||||
await expect(n8n.settingsEnvironment.getConnectButton()).toBeEnabled();
|
||||
await n8n.settingsEnvironment.getConnectButton().click();
|
||||
|
||||
await expect(n8n.settingsEnvironment.getDisconnectButton()).toBeVisible();
|
||||
await expect(n8n.settingsEnvironment.getBranchSelect()).toBeVisible();
|
||||
|
||||
// check that source control is connected
|
||||
await n8n.navigate.toHome();
|
||||
await expect(n8n.sideBar.getSourceControlConnectedIndicator()).toBeVisible();
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,157 @@
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
import { test, expect } from '../../../../fixtures/base';
|
||||
|
||||
const generateValidId = customAlphabet(
|
||||
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_',
|
||||
8,
|
||||
);
|
||||
|
||||
test.describe('Variables', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Lifecycle & Governance' },
|
||||
],
|
||||
}, () => {
|
||||
// These tests are serial since it's at an instance level and they interact with the same variables
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
test.describe('unlicensed', () => {
|
||||
test('should show the unlicensed action box when the feature is disabled', async ({ n8n }) => {
|
||||
await n8n.api.disableFeature('variables');
|
||||
await n8n.navigate.toVariables();
|
||||
await expect(n8n.variables.getUnavailableResourcesList()).toBeVisible();
|
||||
await expect(n8n.variables.getResourcesList()).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('licensed', () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.api.enableFeature('variables');
|
||||
await n8n.api.variables.deleteAllVariables();
|
||||
await n8n.navigate.toVariables();
|
||||
});
|
||||
|
||||
test('should create a new variable using empty state', async ({ n8n }) => {
|
||||
const key = `ENV_VAR_${generateValidId()}`;
|
||||
const value = 'test_value';
|
||||
|
||||
await n8n.variables.createVariableFromEmptyState(key, value);
|
||||
|
||||
const variableRow = n8n.variables.getVariableRow(key);
|
||||
await expect(variableRow).toContainText(value);
|
||||
await expect(variableRow).toBeVisible();
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should create multiple variables', async ({ n8n }) => {
|
||||
const key1 = `ENV_VAR_NEW_${generateValidId()}`;
|
||||
const value1 = 'test_value_1';
|
||||
await n8n.variables.createVariableFromEmptyState(key1, value1);
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(1);
|
||||
|
||||
const key2 = `ENV_EXAMPLE_${generateValidId()}`;
|
||||
const value2 = 'test_value_2';
|
||||
await n8n.variables.createVariable(key2, value2);
|
||||
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(2);
|
||||
|
||||
const variableRow1 = n8n.variables.getVariableRow(key1);
|
||||
await expect(variableRow1).toContainText(value1);
|
||||
await expect(variableRow1).toBeVisible();
|
||||
|
||||
const variableRow2 = n8n.variables.getVariableRow(key2);
|
||||
await expect(variableRow2).toContainText(value2);
|
||||
await expect(variableRow2).toBeVisible();
|
||||
});
|
||||
|
||||
test('should get validation errors and cancel variable creation', async ({ n8n }) => {
|
||||
await n8n.variables.createVariableFromEmptyState(
|
||||
`ENV_BASE_${generateValidId()}`,
|
||||
'base_value',
|
||||
);
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(1);
|
||||
const initialCount = await n8n.variables.getVariablesRows().count();
|
||||
|
||||
const key = `ENV_VAR_INVALID_${generateValidId()}$`; // Invalid key with special character
|
||||
const value = 'test_value';
|
||||
|
||||
await n8n.variables.createVariable(key, value, { shouldSave: false });
|
||||
const saveButton = n8n.variables.variableModal.getSaveButton();
|
||||
await expect(saveButton).toBeDisabled();
|
||||
await n8n.variables.variableModal.close();
|
||||
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(initialCount);
|
||||
});
|
||||
|
||||
test('should edit a variable', async ({ n8n }) => {
|
||||
const key = `ENV_VAR_EDIT_${generateValidId()}`;
|
||||
const initialValue = 'initial_value';
|
||||
await n8n.variables.createVariableFromEmptyState(key, initialValue);
|
||||
|
||||
const newValue = 'updated_value';
|
||||
|
||||
await n8n.variables.editVariable(key, newValue, { shouldSave: true });
|
||||
|
||||
const variableRow = n8n.variables.getVariableRow(key);
|
||||
await expect(variableRow).toContainText(newValue);
|
||||
await expect(variableRow).toBeVisible();
|
||||
});
|
||||
|
||||
test('should delete a variable', async ({ n8n }) => {
|
||||
const key = `TO_DELETE_${generateValidId()}`;
|
||||
const value = 'delete_test_value';
|
||||
|
||||
await n8n.variables.createVariableFromEmptyState(key, value);
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(1);
|
||||
const initialCount = await n8n.variables.getVariablesRows().count();
|
||||
|
||||
await n8n.variables.deleteVariable(key);
|
||||
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(initialCount - 1);
|
||||
|
||||
await expect(n8n.variables.getVariableRow(key)).toBeHidden();
|
||||
});
|
||||
|
||||
test('should search for a variable', async ({ n8n }) => {
|
||||
const uniqueId = generateValidId();
|
||||
|
||||
const key1 = `SEARCH_VAR_${uniqueId}`;
|
||||
const key2 = `SEARCH_VAR_NEW_${uniqueId}`;
|
||||
const key3 = `SEARCH_EXAMPLE_${uniqueId}`;
|
||||
|
||||
await n8n.variables.createVariableFromEmptyState(key1, 'search_value_1');
|
||||
await n8n.variables.createVariable(key2, 'search_value_2');
|
||||
await n8n.variables.createVariable(key3, 'search_value_3');
|
||||
|
||||
await n8n.variables.getSearchBar().fill('NEW_');
|
||||
await n8n.variables.getSearchBar().press('Enter');
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(1);
|
||||
await expect(n8n.variables.getVariableRow(key2)).toBeVisible();
|
||||
await expect(n8n.page).toHaveURL(new RegExp('search=NEW_'));
|
||||
|
||||
await n8n.variables.getSearchBar().clear();
|
||||
await n8n.variables.getSearchBar().fill('SEARCH_VAR_');
|
||||
await n8n.variables.getSearchBar().press('Enter');
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(2);
|
||||
await expect(n8n.variables.getVariableRow(key1)).toBeVisible();
|
||||
await expect(n8n.variables.getVariableRow(key2)).toBeVisible();
|
||||
await expect(n8n.page).toHaveURL(new RegExp('search=SEARCH_VAR_'));
|
||||
|
||||
await n8n.variables.getSearchBar().clear();
|
||||
await n8n.variables.getSearchBar().fill('SEARCH_');
|
||||
await n8n.variables.getSearchBar().press('Enter');
|
||||
await expect(n8n.variables.getVariablesRows()).toHaveCount(3);
|
||||
await expect(n8n.variables.getVariableRow(key1)).toBeVisible();
|
||||
await expect(n8n.variables.getVariableRow(key2)).toBeVisible();
|
||||
await expect(n8n.variables.getVariableRow(key3)).toBeVisible();
|
||||
await expect(n8n.page).toHaveURL(new RegExp('search=SEARCH_'));
|
||||
|
||||
await n8n.variables.getSearchBar().clear();
|
||||
await n8n.variables.getSearchBar().fill(`NonExistent_${generateValidId()}`);
|
||||
await n8n.variables.getSearchBar().press('Enter');
|
||||
await expect(n8n.variables.getVariablesRows()).toBeHidden();
|
||||
await expect(n8n.page).toHaveURL(/search=NonExistent_/);
|
||||
|
||||
await expect(n8n.variables.getNoVariablesFoundMessage()).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { expect, test } from '../../../../fixtures/base';
|
||||
|
||||
test.use({ capability: 'external-secrets' });
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test.describe(
|
||||
'AWS Secrets Manager with LocalStack @capability:external-secrets @licensed',
|
||||
{
|
||||
annotation: [{ type: 'owner', description: 'Lifecycle & Governance' }],
|
||||
},
|
||||
() => {
|
||||
const PROVIDER_NAME = 'awsSecretsManager';
|
||||
const PROVIDER_SETTINGS = {
|
||||
region: 'us-east-1',
|
||||
authMethod: 'iamUser',
|
||||
accessKeyId: 'test',
|
||||
secretAccessKey: 'test',
|
||||
};
|
||||
|
||||
test.beforeEach(async ({ n8n, services }) => {
|
||||
await services.localstack.secretsManager.clear();
|
||||
await n8n.api.enableFeature('externalSecrets');
|
||||
});
|
||||
|
||||
test('can configure, connect, and sync secrets from LocalStack', async ({ n8n, services }) => {
|
||||
const { secretsManager } = services.localstack;
|
||||
|
||||
await secretsManager.createSecret('api-key', 'secret-123');
|
||||
|
||||
await n8n.api.externalSecrets.saveProviderSettings(PROVIDER_NAME, PROVIDER_SETTINGS);
|
||||
await n8n.api.externalSecrets.testProvider(PROVIDER_NAME, PROVIDER_SETTINGS);
|
||||
await n8n.api.externalSecrets.connectProvider(PROVIDER_NAME);
|
||||
await n8n.api.externalSecrets.updateProvider(PROVIDER_NAME);
|
||||
|
||||
expect(await n8n.api.externalSecrets.getSecrets(PROVIDER_NAME)).toContain('api-key');
|
||||
|
||||
await secretsManager.createSecret('new-secret', 'value-2');
|
||||
await n8n.api.externalSecrets.updateProvider(PROVIDER_NAME);
|
||||
|
||||
const secrets = await n8n.api.externalSecrets.getSecrets(PROVIDER_NAME);
|
||||
expect(secrets).toContain('api-key');
|
||||
expect(secrets).toContain('new-secret');
|
||||
});
|
||||
},
|
||||
);
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
import { expect, test } from '../../../../fixtures/base';
|
||||
|
||||
test.use({ capability: 'external-secrets' });
|
||||
|
||||
// LocalStack can take time to start up
|
||||
test.setTimeout(180_000);
|
||||
|
||||
test.describe(
|
||||
'Secret Providers Connections with LocalStack @capability:external-secrets @licensed',
|
||||
{
|
||||
annotation: [{ type: 'owner', description: 'Lifecycle & Governance' }],
|
||||
},
|
||||
() => {
|
||||
const PROVIDER_KEY = 'aws-localstack-e2e';
|
||||
const PROVIDER_TYPE = 'awsSecretsManager';
|
||||
|
||||
test.beforeEach(async ({ n8n, services }) => {
|
||||
// N8N_ENV_FEAT_EXTERNAL_SECRETS_FOR_PROJECTS is set at container startup
|
||||
// via the external-secrets capability config
|
||||
|
||||
// Enable the external secrets license feature
|
||||
await n8n.api.enableFeature('externalSecrets');
|
||||
|
||||
// Clear any existing secrets from previous tests
|
||||
await services.localstack.secretsManager.clear();
|
||||
});
|
||||
|
||||
test.afterEach(async ({ n8n }) => {
|
||||
// Clean up: delete the test connection if it exists
|
||||
try {
|
||||
await n8n.api.externalSecrets.deleteConnection(PROVIDER_KEY);
|
||||
} catch {
|
||||
// Ignore errors if connection doesn't exist
|
||||
}
|
||||
});
|
||||
|
||||
test('can create a connection pointing to LocalStack', async ({ n8n, services }) => {
|
||||
// Arrange: Seed secrets in LocalStack
|
||||
await services.localstack.secretsManager.createSecret('e2e-api-key', 'secret-123');
|
||||
await services.localstack.secretsManager.createSecret(
|
||||
'e2e-db-credentials',
|
||||
JSON.stringify({ username: 'admin', password: 'hunter2' }),
|
||||
);
|
||||
|
||||
// Verify secrets exist in LocalStack
|
||||
const secrets = await services.localstack.secretsManager.listSecrets();
|
||||
expect(secrets).toContain('e2e-api-key');
|
||||
expect(secrets).toContain('e2e-db-credentials');
|
||||
|
||||
// Act: Create a connection with settings that would work with LocalStack
|
||||
// (n8n container has AWS_ENDPOINT_URL set to point to LocalStack)
|
||||
const created = await n8n.api.externalSecrets.createConnection({
|
||||
providerKey: PROVIDER_KEY,
|
||||
type: PROVIDER_TYPE,
|
||||
projectIds: [],
|
||||
settings: {
|
||||
region: 'us-east-1',
|
||||
authMethod: 'iamUser',
|
||||
accessKeyId: 'test',
|
||||
secretAccessKey: 'test',
|
||||
},
|
||||
});
|
||||
|
||||
// Assert: Connection created successfully
|
||||
expect(created.name).toBe(PROVIDER_KEY);
|
||||
expect(created.type).toBe(PROVIDER_TYPE);
|
||||
|
||||
// TODO - this test should verify that the secrets are loaded - but that functionality is not there yet
|
||||
});
|
||||
},
|
||||
);
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* E2E tests for log streaming to VictoriaLogs via syslog.
|
||||
*
|
||||
* These tests verify that n8n log streaming events are correctly
|
||||
* sent to VictoriaLogs and can be queried using LogsQL.
|
||||
*
|
||||
* Prerequisites:
|
||||
* - Log streaming feature enabled (enterprise license)
|
||||
* - @capability:observability tag to bring up VictoriaLogs
|
||||
*/
|
||||
|
||||
import { test, expect } from '../../../../fixtures/base';
|
||||
|
||||
// Worker-scoped fixtures must be at top level
|
||||
test.use({ capability: 'observability' });
|
||||
|
||||
test.describe('Log Streaming to VictoriaLogs @capability:observability', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Lifecycle & Governance' },
|
||||
],
|
||||
}, () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
// Enable log streaming feature for the test
|
||||
await n8n.api.enableFeature('logStreaming');
|
||||
});
|
||||
|
||||
test('should configure syslog destination and send test message', async ({ api, services }) => {
|
||||
const obs = services.observability;
|
||||
|
||||
// Configure syslog destination pointing to VictoriaLogs
|
||||
// syslog contains: host, port, protocol, facility, appName
|
||||
const destination = await api.createSyslogDestination({
|
||||
host: obs.syslog.host,
|
||||
port: obs.syslog.port,
|
||||
protocol: obs.syslog.protocol,
|
||||
facility: obs.syslog.facility,
|
||||
app_name: obs.syslog.appName,
|
||||
label: 'VictoriaLogs Test Destination',
|
||||
});
|
||||
|
||||
expect(destination.id).toBeDefined();
|
||||
console.log(`Created syslog destination with ID: ${destination.id}`);
|
||||
|
||||
// Send test message to the destination
|
||||
const testResult = await api.testLogStreamingDestination(destination.id);
|
||||
expect(testResult).toBe(true);
|
||||
|
||||
// Wait for the test message to appear in VictoriaLogs
|
||||
// Use wildcard - LogsQL interprets dots as word separators
|
||||
const logEntry = await obs.logs.waitForLog('*destination.test*', {
|
||||
timeoutMs: 30000,
|
||||
start: '-1m',
|
||||
});
|
||||
|
||||
expect(logEntry).toBeTruthy();
|
||||
|
||||
// Clean up - delete the destination
|
||||
await api.deleteLogStreamingDestination(destination.id);
|
||||
});
|
||||
|
||||
test('should query metrics from VictoriaMetrics', async ({ api, services }) => {
|
||||
const obs = services.observability;
|
||||
|
||||
// Import and activate a webhook workflow to generate metrics
|
||||
const { webhookPath, workflowId } = await api.workflows.importWorkflowFromFile(
|
||||
'simple-webhook-test.json',
|
||||
);
|
||||
|
||||
// Trigger the workflow via webhook to generate metrics
|
||||
const webhookResponse = await api.webhooks.trigger(`/webhook/${webhookPath}`, {
|
||||
method: 'POST',
|
||||
data: { test: 'metrics' },
|
||||
});
|
||||
expect(webhookResponse.ok()).toBe(true);
|
||||
|
||||
// Wait for workflow execution to complete
|
||||
const execution = await api.workflows.waitForExecution(workflowId, 10000);
|
||||
expect(execution.status).toBe('success');
|
||||
|
||||
// Wait for metrics to be scraped (VictoriaMetrics scrapes every 5s)
|
||||
// Query for n8n version info metric (always present)
|
||||
const versionMetric = await obs.metrics.waitForMetric('n8n_version_info', {
|
||||
timeoutMs: 30000,
|
||||
});
|
||||
|
||||
expect(versionMetric).toBeTruthy();
|
||||
console.log('n8n version metric:', versionMetric?.labels);
|
||||
});
|
||||
});
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* End-to-end UI test for log streaming feature.
|
||||
*
|
||||
* This test verifies:
|
||||
* 1. Log streaming can be configured via the UI
|
||||
* 2. Test events are streamed to VictoriaLogs via syslog
|
||||
* 3. Events can be queried from VictoriaLogs
|
||||
*/
|
||||
|
||||
import { test, expect } from '../../../../fixtures/base';
|
||||
|
||||
test.use({ capability: 'observability' });
|
||||
|
||||
test.describe('Log Streaming UI E2E @capability:observability', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Lifecycle & Governance' },
|
||||
],
|
||||
}, () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.api.enableFeature('logStreaming');
|
||||
});
|
||||
|
||||
test('should configure syslog destination via UI and send test event', async ({
|
||||
n8n,
|
||||
services,
|
||||
}) => {
|
||||
const obs = services.observability;
|
||||
|
||||
// ========== STEP 1: Configure Log Streaming via UI ==========
|
||||
await n8n.navigate.toLogStreaming();
|
||||
await expect(n8n.settingsLogStreaming.getActionBoxLicensed()).toBeVisible();
|
||||
|
||||
// Create syslog destination pointing to VictoriaLogs
|
||||
await n8n.settingsLogStreaming.createSyslogDestination({
|
||||
name: 'VictoriaLogs E2E Test',
|
||||
host: obs.syslog.host,
|
||||
port: obs.syslog.port,
|
||||
});
|
||||
// Send test event
|
||||
await n8n.settingsLogStreaming.sendTestEvent();
|
||||
|
||||
// ========== STEP 3: Verify Event in VictoriaLogs ==========
|
||||
// Use wildcard search - LogsQL interprets dots as word separators
|
||||
const testEvent = await obs.logs.waitForLog('*destination.test*', {
|
||||
timeoutMs: 30000,
|
||||
start: '-2m',
|
||||
});
|
||||
|
||||
expect(testEvent).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { test, expect } from '../../../../fixtures/base';
|
||||
|
||||
const DESTINATION_NAMES = {
|
||||
FIRST: 'Destination 0',
|
||||
SECOND: 'Destination 1',
|
||||
} as const;
|
||||
|
||||
const MODAL_MAX_WIDTH = 500;
|
||||
|
||||
test.describe('Log Streaming Settings', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Lifecycle & Governance' },
|
||||
],
|
||||
}, () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
test.describe('unlicensed', () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.api.disableFeature('logStreaming');
|
||||
});
|
||||
|
||||
test('should show the unlicensed view when the feature is disabled', async ({ n8n }) => {
|
||||
await n8n.navigate.toLogStreaming();
|
||||
await expect(n8n.settingsLogStreaming.getActionBoxUnlicensed()).toBeVisible();
|
||||
await expect(n8n.settingsLogStreaming.getContactUsButton()).toBeVisible();
|
||||
await expect(n8n.settingsLogStreaming.getActionBoxLicensed()).not.toBeAttached();
|
||||
});
|
||||
});
|
||||
|
||||
// @licensed - requires enterprise license (module routes only exist with license at startup)
|
||||
test.describe('licensed @licensed', () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.api.enableFeature('logStreaming');
|
||||
await n8n.api.deleteAllLogStreamingDestinations();
|
||||
await n8n.navigate.toLogStreaming();
|
||||
});
|
||||
|
||||
test('should show the licensed view when the feature is enabled', async ({ n8n }) => {
|
||||
await expect(n8n.settingsLogStreaming.getActionBoxLicensed()).toBeVisible();
|
||||
await expect(n8n.settingsLogStreaming.getAddFirstDestinationButton()).toBeVisible();
|
||||
await expect(n8n.settingsLogStreaming.getActionBoxUnlicensed()).not.toBeAttached();
|
||||
});
|
||||
|
||||
test('should show the add destination modal', async ({ n8n }) => {
|
||||
await n8n.settingsLogStreaming.addDestination();
|
||||
await expect(n8n.settingsLogStreaming.getDestinationModal()).toBeVisible();
|
||||
await expect(n8n.settingsLogStreaming.getSelectDestinationType()).toBeVisible();
|
||||
await expect(n8n.settingsLogStreaming.getSelectDestinationButton()).toBeVisible();
|
||||
await expect(n8n.settingsLogStreaming.getSelectDestinationButton()).toBeDisabled();
|
||||
|
||||
const modal = n8n.settingsLogStreaming.getDestinationModal();
|
||||
const width = await modal.evaluate((element) => {
|
||||
return parseInt(window.getComputedStyle(element).width.replace('px', ''));
|
||||
});
|
||||
expect(width).toBeLessThan(MODAL_MAX_WIDTH);
|
||||
|
||||
await n8n.settingsLogStreaming.clickSelectDestinationType();
|
||||
await n8n.settingsLogStreaming.selectDestinationType(0);
|
||||
await expect(n8n.settingsLogStreaming.getSelectDestinationButton()).toBeEnabled();
|
||||
await n8n.settingsLogStreaming.closeModalByClickingOverlay();
|
||||
await expect(n8n.settingsLogStreaming.getDestinationModal()).not.toBeAttached();
|
||||
});
|
||||
|
||||
test('should create a destination and delete it', async ({ n8n }) => {
|
||||
await n8n.settingsLogStreaming.createDestination(DESTINATION_NAMES.FIRST);
|
||||
await n8n.page.reload();
|
||||
await n8n.settingsLogStreaming.clickDestinationCard(0);
|
||||
await expect(n8n.settingsLogStreaming.getDestinationDeleteButton()).toBeVisible();
|
||||
await n8n.settingsLogStreaming.deleteDestination();
|
||||
await expect(n8n.settingsLogStreaming.getConfirmationDialog()).toBeVisible();
|
||||
await n8n.settingsLogStreaming.cancelDialog();
|
||||
await n8n.settingsLogStreaming.deleteDestination();
|
||||
await expect(n8n.settingsLogStreaming.getConfirmationDialog()).toBeVisible();
|
||||
await n8n.settingsLogStreaming.confirmDialog();
|
||||
});
|
||||
|
||||
test('should create a destination and delete it via card actions', async ({ n8n }) => {
|
||||
await n8n.settingsLogStreaming.createDestination(DESTINATION_NAMES.SECOND);
|
||||
await n8n.page.reload();
|
||||
|
||||
await n8n.settingsLogStreaming.clickDestinationCardDropdown(0);
|
||||
await n8n.settingsLogStreaming.clickDropdownMenuItem(0);
|
||||
await expect(n8n.settingsLogStreaming.getDestinationSaveButton()).not.toBeAttached();
|
||||
await n8n.settingsLogStreaming.closeModalByClickingOverlay();
|
||||
|
||||
await n8n.settingsLogStreaming.clickDestinationCardDropdown(0);
|
||||
await n8n.settingsLogStreaming.clickDropdownMenuItem(1);
|
||||
await expect(n8n.settingsLogStreaming.getConfirmationDialog()).toBeVisible();
|
||||
await n8n.settingsLogStreaming.confirmDialog();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { test, expect } from '../../../../fixtures/base';
|
||||
|
||||
const INVALID_NAMES = [
|
||||
'https://n8n.io',
|
||||
'http://n8n.io',
|
||||
'www.n8n.io',
|
||||
'n8n.io',
|
||||
'n8n.бг',
|
||||
'n8n.io/home',
|
||||
'n8n.io/home?send=true',
|
||||
'<a href="#">Jack</a>',
|
||||
'<script>alert("Hello")</script>',
|
||||
];
|
||||
|
||||
const VALID_NAMES = [
|
||||
['a', 'a'],
|
||||
['alice', 'alice'],
|
||||
['Robert', 'Downey Jr.'],
|
||||
['Mia', 'Mia-Downey'],
|
||||
['Mark', "O'neil"],
|
||||
['Thomas', 'Müler'],
|
||||
['ßáçøñ', 'ßáçøñ'],
|
||||
['أحمد', 'فلسطين'],
|
||||
['Милорад', 'Филиповић'],
|
||||
];
|
||||
|
||||
test.describe(
|
||||
'Personal Settings',
|
||||
{
|
||||
annotation: [{ type: 'owner', description: 'Identity & Access' }],
|
||||
},
|
||||
() => {
|
||||
test('should allow to change first and last name', async ({ n8n }) => {
|
||||
await n8n.settingsPersonal.goto();
|
||||
|
||||
for (const name of VALID_NAMES) {
|
||||
await n8n.settingsPersonal.fillPersonalData(name[0], name[1]);
|
||||
await n8n.settingsPersonal.saveSettings();
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Personal details updated'),
|
||||
).toBeVisible();
|
||||
await n8n.notifications.closeNotificationByText('Personal details updated');
|
||||
}
|
||||
});
|
||||
|
||||
test('should not allow malicious values for personal data', async ({ n8n }) => {
|
||||
await n8n.settingsPersonal.goto();
|
||||
|
||||
for (const name of INVALID_NAMES) {
|
||||
await n8n.settingsPersonal.fillPersonalData(name, name);
|
||||
await n8n.settingsPersonal.saveSettings();
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Problem updating your details'),
|
||||
).toBeVisible();
|
||||
await n8n.notifications.closeNotificationByText('Problem updating your details');
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
import { authenticator } from 'otplib';
|
||||
|
||||
import { INSTANCE_OWNER_CREDENTIALS } from '../../../../config/test-users';
|
||||
import { test, expect } from '../../../../fixtures/base';
|
||||
|
||||
test.use({ capability: { env: { TEST_ISOLATION: 'two-factor-auth' } } });
|
||||
|
||||
const TEST_DATA = {
|
||||
NEW_EMAIL: 'newemail@test.com',
|
||||
NEW_FIRST_NAME: 'newFirstName',
|
||||
NEW_LAST_NAME: 'newLastName',
|
||||
};
|
||||
|
||||
const NOTIFICATIONS = {
|
||||
PERSONAL_DETAILS_UPDATED: 'Personal details updated',
|
||||
};
|
||||
|
||||
const { email, password, mfaSecret, mfaRecoveryCodes } = INSTANCE_OWNER_CREDENTIALS;
|
||||
const RECOVERY_CODE = mfaRecoveryCodes![0];
|
||||
|
||||
test.describe(
|
||||
'Two-factor authentication @auth:none @db:reset',
|
||||
{
|
||||
annotation: [{ type: 'owner', description: 'Identity & Access' }],
|
||||
},
|
||||
() => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test('Should be able to login with MFA code', async ({ n8n }) => {
|
||||
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
|
||||
await n8n.sideBar.signOutFromWorkflows();
|
||||
|
||||
await n8n.mfaComposer.loginWithMfaCode(email, password, mfaSecret!);
|
||||
|
||||
await expect(n8n.page).toHaveURL(/workflows/);
|
||||
});
|
||||
|
||||
test('Should be able to login with MFA recovery code', async ({ n8n }) => {
|
||||
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
|
||||
await n8n.sideBar.signOutFromWorkflows();
|
||||
|
||||
await n8n.mfaComposer.loginWithMfaRecoveryCode(email, password, RECOVERY_CODE);
|
||||
|
||||
await expect(n8n.page).toHaveURL(/workflows/);
|
||||
});
|
||||
|
||||
test('Should be able to disable MFA in account with MFA code', async ({ n8n }) => {
|
||||
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
|
||||
await n8n.sideBar.signOutFromWorkflows();
|
||||
|
||||
await n8n.mfaComposer.loginWithMfaCode(email, password, mfaSecret!);
|
||||
|
||||
const disableToken = authenticator.generate(mfaSecret!);
|
||||
await n8n.settingsPersonal.triggerDisableMfa();
|
||||
await n8n.settingsPersonal.fillMfaCodeAndSave(disableToken);
|
||||
|
||||
await expect(n8n.settingsPersonal.getEnableMfaButton()).toBeVisible();
|
||||
});
|
||||
|
||||
test('Should prompt for MFA code when email changes', async ({ n8n }) => {
|
||||
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
|
||||
|
||||
await n8n.settingsPersonal.goto();
|
||||
await n8n.settingsPersonal.fillEmail(TEST_DATA.NEW_EMAIL);
|
||||
await n8n.settingsPersonal.pressEnterOnEmail();
|
||||
|
||||
const mfaCode = authenticator.generate(mfaSecret!);
|
||||
await n8n.settingsPersonal.fillMfaCodeAndSave(mfaCode);
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent(NOTIFICATIONS.PERSONAL_DETAILS_UPDATED),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Should prompt for MFA recovery code when email changes', async ({ n8n }) => {
|
||||
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
|
||||
|
||||
await n8n.settingsPersonal.goto();
|
||||
await n8n.settingsPersonal.fillEmail(TEST_DATA.NEW_EMAIL);
|
||||
await n8n.settingsPersonal.pressEnterOnEmail();
|
||||
|
||||
await expect(n8n.settingsPersonal.getMfaCodeOrRecoveryCodeInput()).toBeVisible();
|
||||
});
|
||||
|
||||
test('Should not prompt for MFA code or recovery code when first name or last name changes', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
|
||||
|
||||
await n8n.settingsPersonal.updateFirstAndLastName(
|
||||
TEST_DATA.NEW_FIRST_NAME,
|
||||
TEST_DATA.NEW_LAST_NAME,
|
||||
);
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent(NOTIFICATIONS.PERSONAL_DETAILS_UPDATED),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Should be able to disable MFA in account with recovery code', async ({ n8n }) => {
|
||||
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
|
||||
await n8n.sideBar.signOutFromWorkflows();
|
||||
|
||||
await n8n.mfaComposer.loginWithMfaCode(email, password, mfaSecret!);
|
||||
|
||||
await n8n.settingsPersonal.triggerDisableMfa();
|
||||
await n8n.settingsPersonal.fillMfaCodeAndSave(RECOVERY_CODE);
|
||||
|
||||
await expect(n8n.settingsPersonal.getEnableMfaButton()).toBeVisible();
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
import { INSTANCE_OWNER_CREDENTIALS } from '../../../../config/test-users';
|
||||
import { test, expect } from '../../../../fixtures/base';
|
||||
|
||||
test.describe('Users Settings', {
|
||||
annotation: [
|
||||
{ type: 'owner', description: 'Identity & Access' },
|
||||
],
|
||||
}, () => {
|
||||
test('should prevent non-owners to access UM settings', async ({ n8n }) => {
|
||||
// This creates a new user in the same context, so the cookies are refreshed and owner is no longer logged in
|
||||
await n8n.api.users.create();
|
||||
await n8n.navigate.toUsers();
|
||||
await expect.poll(() => n8n.page.url()).not.toContain('/settings/users');
|
||||
});
|
||||
|
||||
test('should allow instance owner to access UM settings', async ({ n8n }) => {
|
||||
await n8n.navigate.toUsers();
|
||||
expect(n8n.page.url()).toContain('/settings/users');
|
||||
});
|
||||
|
||||
test('should be able to change user role to Admin and back', async ({ n8n, api }) => {
|
||||
const user = await api.users.create();
|
||||
await n8n.navigate.toUsers();
|
||||
await n8n.settingsUsers.search(user.email);
|
||||
await n8n.settingsUsers.selectAccountType(user.email, 'Admin');
|
||||
await expect(n8n.settingsUsers.getAccountType(user.email)).toHaveText('Admin');
|
||||
await n8n.settingsUsers.selectAccountType(user.email, 'Member');
|
||||
await expect(n8n.settingsUsers.getAccountType(user.email)).toHaveText('Member');
|
||||
});
|
||||
|
||||
test('should delete user and their data', async ({ n8n, api }) => {
|
||||
const user = await api.users.create();
|
||||
await n8n.navigate.toUsers();
|
||||
await n8n.page.reload();
|
||||
|
||||
await n8n.settingsUsers.search(user.email);
|
||||
await expect(n8n.settingsUsers.getRow(user.email)).toBeVisible();
|
||||
|
||||
await n8n.settingsUsers.clickDeleteUser(user.email);
|
||||
await n8n.settingsUsers.deleteData();
|
||||
await expect(n8n.notifications.getNotificationByTitleOrContent('User deleted')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should delete user and transfer their data', async ({ n8n, api }) => {
|
||||
const ownerEmail = INSTANCE_OWNER_CREDENTIALS.email;
|
||||
const user = await api.users.create();
|
||||
await n8n.navigate.toUsers();
|
||||
await n8n.page.reload();
|
||||
|
||||
await n8n.settingsUsers.search(user.email);
|
||||
await n8n.settingsUsers.getRow(user.email).isVisible();
|
||||
|
||||
await n8n.settingsUsers.clickDeleteUser(user.email);
|
||||
await n8n.settingsUsers.transferData(ownerEmail);
|
||||
await expect(n8n.notifications.getNotificationByTitleOrContent('User deleted')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { test, expect } from '../../../../fixtures/base';
|
||||
|
||||
test.describe
|
||||
.serial('Worker View', () => {
|
||||
test.describe(
|
||||
'unlicensed',
|
||||
{
|
||||
annotation: [{ type: 'owner', description: 'Catalysts' }],
|
||||
},
|
||||
() => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.api.disableFeature('workerView');
|
||||
await n8n.api.disableFeature('workerView');
|
||||
await n8n.api.setQueueMode(false);
|
||||
});
|
||||
|
||||
test('should not show up in the menu sidebar', async ({ n8n }) => {
|
||||
await n8n.workerView.goto();
|
||||
await expect(n8n.workerView.getWorkerMenuItem()).toBeHidden();
|
||||
});
|
||||
|
||||
test('should show action box', async ({ n8n }) => {
|
||||
await n8n.workerView.goto();
|
||||
await expect(n8n.workerView.getWorkerViewUnlicensed()).toBeVisible();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
test.describe('licensed', () => {
|
||||
test.beforeEach(async ({ n8n }) => {
|
||||
await n8n.api.enableFeature('workerView');
|
||||
await n8n.api.setQueueMode(true);
|
||||
});
|
||||
|
||||
test('should show up in the menu sidebar', async ({ n8n }) => {
|
||||
await n8n.goHome();
|
||||
await n8n.workerView.goto();
|
||||
await expect(n8n.workerView.getWorkerMenuItem()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show worker list view', async ({ n8n }) => {
|
||||
await n8n.workerView.goto();
|
||||
await expect(n8n.workerView.getWorkerViewLicensed()).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user