ci: Refactor e2e tests to be less flaky (no-changelog) (#9695)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-06-11 14:45:15 +02:00 committed by GitHub
parent bc35e8c33d
commit 3d0393c739
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 485 additions and 539 deletions

View file

@ -40,7 +40,7 @@ on:
containers:
description: 'Number of containers to run tests in.'
required: false
default: '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]'
default: '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]'
type: string
pr_number:
description: 'PR number to run tests for.'

View file

@ -4,10 +4,16 @@ const sharedOptions = require('@n8n_io/eslint-config/shared');
* @type {import('@types/eslint').ESLint.ConfigData}
*/
module.exports = {
extends: ['@n8n_io/eslint-config/base'],
extends: ['@n8n_io/eslint-config/base', 'plugin:cypress/recommended'],
...sharedOptions(__dirname),
plugins: ['cypress'],
env: {
'cypress/globals': true,
},
rules: {
// TODO: remove these rules
'@typescript-eslint/no-explicit-any': 'off',
@ -20,5 +26,9 @@ module.exports = {
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/promise-function-async': 'off',
'n8n-local-rules/no-uncaught-json-parse': 'off',
'cypress/no-assigning-return-values': 'warn',
'cypress/no-unnecessary-waiting': 'warn',
'cypress/unsafe-to-chain-command': 'warn',
},
};

View file

@ -59,7 +59,7 @@ export const AI_LANGUAGE_MODEL_OPENAI_CHAT_MODEL_NODE_NAME = 'OpenAI Chat Model'
export const AI_OUTPUT_PARSER_AUTO_FIXING_NODE_NAME = 'Auto-fixing Output Parser';
export const WEBHOOK_NODE_NAME = 'Webhook';
export const META_KEY = Cypress.platform === 'darwin' ? '{meta}' : '{ctrl}';
export const META_KEY = Cypress.platform === 'darwin' ? 'meta' : 'ctrl';
export const NEW_GOOGLE_ACCOUNT_NAME = 'Gmail account';
export const NEW_TRELLO_ACCOUNT_NAME = 'Trello account';

View file

@ -122,8 +122,7 @@ describe('Undo/Redo', () => {
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.get('body').type('{esc}');
cy.get('body').type('{esc}');
WorkflowPage.actions.selectAll();
cy.get('body').type('{backspace}');
WorkflowPage.actions.hitDeleteAllNodes();
WorkflowPage.getters.canvasNodes().should('have.have.length', 0);
WorkflowPage.actions.hitUndo();
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
@ -208,7 +207,7 @@ describe('Undo/Redo', () => {
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.get('body').type('{esc}');
cy.get('body').type('{esc}');
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 2);
WorkflowPage.actions.hitUndo();

View file

@ -199,7 +199,7 @@ describe('Canvas Actions', () => {
it('should copy selected nodes', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitCopy();
successToast().should('contain', 'Copied!');
@ -211,7 +211,7 @@ describe('Canvas Actions', () => {
it('should select/deselect all nodes', () => {
WorkflowPage.actions.addNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.getters.selectedNodes().should('have.length', 2);
WorkflowPage.actions.deselectAll();
WorkflowPage.getters.selectedNodes().should('have.length', 0);

View file

@ -164,8 +164,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.wait(500);
WorkflowPage.actions.selectAll();
cy.get('body').type('{backspace}');
WorkflowPage.actions.hitDeleteAllNodes();
WorkflowPage.getters.canvasNodes().should('have.length', 0);
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
@ -181,8 +180,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
cy.wait(500);
WorkflowPage.actions.selectAll();
cy.get('body').type('{backspace}');
WorkflowPage.actions.hitDeleteAllNodes();
WorkflowPage.getters.canvasNodes().should('have.length', 0);
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
@ -315,7 +313,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
cy.get('body').type('{esc}');
// Keyboard shortcut
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 2);
WorkflowPage.actions.hitDisableNodeShortcut();
@ -324,12 +322,12 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodeByName(MANUAL_TRIGGER_NODE_DISPLAY_NAME).click();
WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 1);
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitDisableNodeShortcut();
WorkflowPage.getters.disabledNodes().should('have.length', 2);
// Context menu
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.openContextMenu();
WorkflowPage.actions.contextMenuAction('toggle_activation');
WorkflowPage.getters.disabledNodes().should('have.length', 0);
@ -341,7 +339,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.actions.openContextMenu();
WorkflowPage.actions.contextMenuAction('toggle_activation');
WorkflowPage.getters.disabledNodes().should('have.length', 1);
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.openContextMenu();
WorkflowPage.actions.contextMenuAction('toggle_activation');
WorkflowPage.getters.disabledNodes().should('have.length', 2);
@ -383,8 +381,8 @@ describe('Canvas Node Manipulation and Navigation', () => {
WorkflowPage.getters.canvasNodes().should('have.length', 3);
WorkflowPage.getters.nodeConnections().should('have.length', 1);
WorkflowPage.actions.selectAll();
WorkflowPage.actions.hitDuplicateNodeShortcut();
WorkflowPage.actions.hitSelectAll();
WorkflowPage.actions.hitDuplicateNode();
WorkflowPage.getters.canvasNodes().should('have.length', 5);
});

View file

@ -34,7 +34,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
let workflowW2Url = '';
it('should create C1, W1, W2, share W1 with U3, as U2', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
@ -67,7 +67,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
});
it('should create C2, share C2 with U1 and U2, as U3', () => {
cy.signin(INSTANCE_MEMBERS[1]);
cy.signinAsMember(1);
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
@ -83,7 +83,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
});
it('should open W1, add node using C2 as U3', () => {
cy.signin(INSTANCE_MEMBERS[1]);
cy.signinAsMember(1);
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 1);
@ -99,7 +99,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
});
it('should open W1, add node using C2 as U2', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 2);
@ -119,7 +119,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
});
it('should not have access to W2, as U3', () => {
cy.signin(INSTANCE_MEMBERS[1]);
cy.signinAsMember(1);
cy.visit(workflowW2Url);
cy.waitForLoad();
@ -128,7 +128,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
});
it('should have access to W1, W2, as U1', () => {
cy.signin(INSTANCE_OWNER);
cy.signinAsOwner();
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 2);
@ -144,7 +144,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
});
it('should automatically test C2 when opened by U2 sharee', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(credentialsPage.url);
credentialsPage.getters.credentialCard('Credential C2').click();
@ -152,7 +152,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
});
it('should work for admin role on credentials created by others (also can share it with themselves)', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(credentialsPage.url);
credentialsPage.getters.createCredentialButton().click();
@ -164,7 +164,7 @@ describe('Sharing', { disableAutoLogin: true }, () => {
credentialsModal.actions.close();
cy.signout();
cy.signin(INSTANCE_ADMIN);
cy.signinAsAdmin();
cy.visit(credentialsPage.url);
credentialsPage.getters.credentialCard('Credential C3').click();
credentialsModal.getters.testSuccessTag().should('be.visible');

View file

@ -34,7 +34,7 @@ describe('User Management', { disableAutoLogin: true }, () => {
cy.enableFeature('sharing');
});
it.only('should login and logout', () => {
it('should login and logout', () => {
cy.visit('/');
cy.get('input[name="email"]').type(INSTANCE_OWNER.email);
cy.get('input[name="password"]').type(INSTANCE_OWNER.password);

View file

@ -34,15 +34,12 @@ describe('Canvas Actions', () => {
addDefaultSticky();
workflowPage.actions.deselectAll();
workflowPage.actions.addStickyFromContextMenu();
workflowPage.actions.hitAddStickyShortcut();
workflowPage.actions.hitAddSticky();
workflowPage.getters.stickies().should('have.length', 3);
// Should not add a sticky for ctrl+shift+s
cy.get('body')
.type(META_KEY, { delay: 500, release: false })
.type('{shift}', { release: false })
.type('s');
cy.get('body').type(`{${META_KEY}+shift+s}`);
workflowPage.getters.stickies().should('have.length', 3);
workflowPage.getters

View file

@ -6,13 +6,12 @@ import {
getPublicApiUpgradeCTA,
} from '../pages';
import planData from '../fixtures/Plan_data_opt_in_trial.json';
import { INSTANCE_OWNER } from '../constants';
const mainSidebar = new MainSidebar();
const bannerStack = new BannerStack();
const workflowPage = new WorkflowPage();
describe('Cloud', { disableAutoLogin: true }, () => {
describe('Cloud', () => {
before(() => {
const now = new Date();
const fiveDaysFromNow = new Date(now.getTime() + 5 * 24 * 60 * 60 * 1000);
@ -20,22 +19,12 @@ describe('Cloud', { disableAutoLogin: true }, () => {
});
beforeEach(() => {
cy.intercept('GET', '/rest/admin/cloud-plan', {
body: planData,
}).as('getPlanData');
cy.intercept('GET', '/rest/settings', (req) => {
req.on('response', (res) => {
res.send({
data: {
...res.body.data,
cy.overrideSettings({
deployment: { type: 'cloud' },
n8nMetadata: { userId: 1 },
},
n8nMetadata: { userId: '1' },
});
});
}).as('loadSettings');
cy.intercept('GET', '/rest/admin/cloud-plan', planData).as('getPlanData');
cy.intercept('GET', '/rest/cloud/proxy/user/me', {}).as('getCloudUserInfo');
cy.intercept('GET', new RegExp('/rest/projects*')).as('projects');
cy.intercept('GET', new RegExp('/rest/roles')).as('roles');
});
@ -49,8 +38,6 @@ describe('Cloud', { disableAutoLogin: true }, () => {
describe('BannerStack', () => {
it('should render trial banner for opt-in cloud user', () => {
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
visitWorkflowPage();
bannerStack.getters.banner().should('be.visible');
@ -58,21 +45,11 @@ describe('Cloud', { disableAutoLogin: true }, () => {
mainSidebar.actions.signout();
bannerStack.getters.banner().should('not.be.visible');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
visitWorkflowPage();
bannerStack.getters.banner().should('be.visible');
mainSidebar.actions.signout();
});
});
describe('Admin Home', () => {
it('Should show admin button', () => {
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
visitWorkflowPage();
mainSidebar.getters.adminPanel().should('be.visible');
@ -81,8 +58,6 @@ describe('Cloud', { disableAutoLogin: true }, () => {
describe('Public API', () => {
it('Should show upgrade CTA for Public API if user is trialing', () => {
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
visitPublicApiPage();
cy.wait(['@loadSettings', '@projects', '@roles', '@getPlanData']);

View file

@ -34,9 +34,8 @@ const signinPage = new SigninPage();
const personalSettingsPage = new PersonalSettingsPage();
const mainSidebar = new MainSidebar();
describe('Two-factor authentication', () => {
describe('Two-factor authentication', { disableAutoLogin: true }, () => {
beforeEach(() => {
void Cypress.session.clearAllSavedSessions();
cy.request('POST', `${BACKEND_BASE_URL}/rest/e2e/reset`, {
owner: user,
members: [],

View file

@ -1,7 +1,6 @@
import {
HTTP_REQUEST_NODE_NAME,
IF_NODE_NAME,
INSTANCE_OWNER,
MANUAL_TRIGGER_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
} from '../constants';
@ -21,7 +20,7 @@ describe('Debug', () => {
cy.intercept('GET', '/rest/executions/*').as('getExecution');
cy.intercept('POST', '/rest/workflows/**/run').as('postWorkflowRun');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.signinAsOwner();
workflowPage.actions.visit();

View file

@ -1,23 +1,25 @@
import { TemplatesPage } from '../pages/templates';
import { WorkflowPage } from '../pages/workflow';
import { WorkflowsPage } from '../pages/workflows';
import { MainSidebar } from '../pages/sidebar/main-sidebar';
import OnboardingWorkflow from '../fixtures/Onboarding_workflow.json';
import WorkflowTemplate from '../fixtures/Workflow_template_write_http_query.json';
const templatesPage = new TemplatesPage();
const workflowPage = new WorkflowPage();
const workflowsPage = new WorkflowsPage();
const mainSidebar = new MainSidebar();
describe('Workflow templates', () => {
beforeEach(() => {
cy.intercept('GET', '**/rest/settings', (req) => {
// Disable cache
delete req.headers['if-none-match'];
req.reply((res) => {
if (res.body.data) {
// Disable custom templates host if it has been overridden by another intercept
res.body.data.templates = { enabled: true, host: 'https://api.n8n.io/api/' };
}
const mockTemplateHost = (host: string) => {
cy.overrideSettings({
templates: { enabled: true, host },
});
}).as('settingsRequest');
};
describe('For api.n8n.io', () => {
beforeEach(() => {
mockTemplateHost('https://api.n8n.io/api/');
});
it('Opens website when clicking templates sidebar link', () => {
@ -47,9 +49,198 @@ describe('Workflow templates', () => {
});
it('Redirects to website when visiting templates page directly', () => {
cy.intercept(
{
hostname: 'n8n.io',
pathname: '/workflows',
},
'Mock Template Page',
).as('templatesPage');
cy.visit(templatesPage.url);
cy.origin('https://n8n.io', () => {
cy.url().should('include', 'https://n8n.io/workflows');
cy.wait('@templatesPage');
});
});
describe('For a custom template host', () => {
const hostname = 'random.domain';
const categories = [
{ id: 1, name: 'Engineering' },
{ id: 2, name: 'Finance' },
{ id: 3, name: 'Sales' },
];
const collections = [
{
id: 1,
name: 'Test Collection',
workflows: [{ id: 1 }],
nodes: [],
},
];
beforeEach(() => {
cy.intercept({ hostname, pathname: '/api/health' }, { status: 'OK' });
cy.intercept({ hostname, pathname: '/api/templates/categories' }, { categories });
cy.intercept(
{ hostname, pathname: '/api/templates/collections', query: { category: '**' } },
(req) => {
req.reply({ collections: req.query['category[]'] === '3' ? [] : collections });
},
);
cy.intercept(
{ hostname, pathname: '/api/templates/search', query: { category: '**' } },
(req) => {
const fixture =
req.query.category === 'Sales'
? 'templates_search/sales_templates_search_response.json'
: 'templates_search/all_templates_search_response.json';
req.reply({ statusCode: 200, fixture });
},
);
cy.intercept(
{ hostname, pathname: '/api/workflows/templates/1' },
{
statusCode: 200,
body: {
id: 1,
name: OnboardingWorkflow.name,
workflow: OnboardingWorkflow,
},
},
).as('getTemplate');
cy.intercept(
{ hostname, pathname: '/api/templates/workflows/1' },
{
statusCode: 200,
body: WorkflowTemplate,
},
).as('getTemplatePreview');
mockTemplateHost(`https://${hostname}/api`);
});
it('can open onboarding flow', () => {
templatesPage.actions.openOnboardingFlow();
cy.url().should('match', /.*\/workflow\/.*?onboardingId=1$/);
workflowPage.actions.shouldHaveWorkflowName('Demo: ' + OnboardingWorkflow.name);
workflowPage.getters.canvasNodes().should('have.length', 4);
workflowPage.getters.stickies().should('have.length', 1);
workflowPage.getters.canvasNodes().first().should('have.descendants', '.node-pin-data-icon');
});
it('can import template', () => {
templatesPage.actions.importTemplate();
cy.url().should('include', '/workflow/new?templateId=1');
workflowPage.getters.canvasNodes().should('have.length', 4);
workflowPage.getters.stickies().should('have.length', 1);
workflowPage.actions.shouldHaveWorkflowName(OnboardingWorkflow.name);
});
it('should save template id with the workflow', () => {
templatesPage.actions.importTemplate();
cy.visit(templatesPage.url);
cy.get('.el-skeleton.n8n-loading').should('not.exist');
templatesPage.getters.firstTemplateCard().should('exist');
templatesPage.getters.templatesLoadingContainer().should('not.exist');
templatesPage.getters.firstTemplateCard().click();
cy.url().should('include', '/templates/1');
cy.wait('@getTemplatePreview');
templatesPage.getters.useTemplateButton().click();
cy.url().should('include', '/workflow/new');
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.hitSelectAll();
workflowPage.actions.hitCopy();
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
// Check workflow JSON by copying it to clipboard
cy.readClipboard().then((workflowJSON) => {
expect(workflowJSON).to.contain('"templateId": "1"');
});
});
it('can open template with images and hides workflow screenshots', () => {
cy.visit(`${templatesPage.url}/1`);
cy.wait('@getTemplatePreview');
templatesPage.getters.description().find('img').should('have.length', 1);
});
it('renders search elements correctly', () => {
cy.visit(templatesPage.url);
templatesPage.getters.searchInput().should('exist');
templatesPage.getters.allCategoriesFilter().should('exist');
templatesPage.getters.categoryFilters().should('have.length.greaterThan', 1);
templatesPage.getters.templateCards().should('have.length.greaterThan', 0);
});
it('can filter templates by category', () => {
cy.visit(templatesPage.url);
templatesPage.getters.templatesLoadingContainer().should('not.exist');
templatesPage.getters.categoryFilter('sales').should('exist');
let initialTemplateCount = 0;
let initialCollectionCount = 0;
templatesPage.getters.templateCountLabel().then(($el) => {
initialTemplateCount = parseInt($el.text().replace(/\D/g, ''), 10);
templatesPage.getters.collectionCountLabel().then(($el1) => {
initialCollectionCount = parseInt($el1.text().replace(/\D/g, ''), 10);
templatesPage.getters.categoryFilter('sales').click();
templatesPage.getters.templatesLoadingContainer().should('not.exist');
// Should have less templates and collections after selecting a category
templatesPage.getters.templateCountLabel().should(($el2) => {
expect(parseInt($el2.text().replace(/\D/g, ''), 10)).to.be.lessThan(
initialTemplateCount,
);
});
templatesPage.getters.collectionCountLabel().should(($el2) => {
expect(parseInt($el2.text().replace(/\D/g, ''), 10)).to.be.lessThan(
initialCollectionCount,
);
});
});
});
});
it('should preserve search query in URL', () => {
cy.visit(templatesPage.url);
templatesPage.getters.templatesLoadingContainer().should('not.exist');
templatesPage.getters.categoryFilter('sales').should('exist');
templatesPage.getters.categoryFilter('sales').click();
templatesPage.getters.searchInput().type('auto');
cy.url().should('include', '?categories=');
cy.url().should('include', '&search=');
cy.reload();
// Should preserve search query in URL
cy.url().should('include', '?categories=');
cy.url().should('include', '&search=');
// Sales category should still be selected
templatesPage.getters
.categoryFilter('sales')
.find('label')
.should('have.class', 'is-checked');
// Search input should still have the search query
templatesPage.getters.searchInput().should('have.value', 'auto');
// Sales checkbox should be pushed to the top
templatesPage.getters
.categoryFilters()
.eq(1)
.then(($el) => {
expect($el.text()).to.equal('Sales');
});
});
});
});

View file

@ -2,7 +2,6 @@ import {
CODE_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
IF_NODE_NAME,
INSTANCE_OWNER,
SCHEDULE_TRIGGER_NODE_NAME,
} from '../constants';
import {
@ -125,7 +124,7 @@ describe('Editor actions should work', () => {
beforeEach(() => {
cy.enableFeature('debugInEditor');
cy.enableFeature('workflowHistory');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.signinAsOwner();
createNewWorkflowAndActivate();
});
@ -186,7 +185,7 @@ describe('Editor zoom should work after route changes', () => {
beforeEach(() => {
cy.enableFeature('debugInEditor');
cy.enableFeature('workflowHistory');
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.signinAsOwner();
workflowPage.actions.visit();
cy.createFixtureWorkflow('Lots_of_nodes.json', 'Lots of nodes');
workflowPage.actions.saveWorkflowOnButtonClick();

View file

@ -1,4 +1,3 @@
import { INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants';
import { WorkerViewPage } from '../pages';
const workerViewPage = new WorkerViewPage();
@ -10,13 +9,13 @@ describe('Worker View (unlicensed)', () => {
});
it('should not show up in the menu sidebar', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(workerViewPage.url);
workerViewPage.getters.menuItem().should('not.exist');
});
it('should show action box', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(workerViewPage.url);
workerViewPage.getters.workerViewUnlicensed().should('exist');
});
@ -29,14 +28,14 @@ describe('Worker View (licensed)', () => {
});
it('should show up in the menu sidebar', () => {
cy.signin(INSTANCE_OWNER);
cy.signinAsOwner();
cy.enableQueueMode();
cy.visit(workerViewPage.url);
workerViewPage.getters.menuItem().should('exist');
});
it('should show worker list view', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(workerViewPage.url);
workerViewPage.getters.workerViewLicensed().should('exist');
});

View file

@ -8,10 +8,19 @@ import { WorkflowPage } from '../pages/workflow';
import * as formStep from '../composables/setup-template-form-step';
import { getSetupWorkflowCredentialsButton } from '../composables/setup-workflow-credentials-button';
import * as setupCredsModal from '../composables/modals/workflow-credential-setup-modal';
import TestTemplate1 from '../fixtures/Test_Template_1.json';
import TestTemplate2 from '../fixtures/Test_Template_2.json';
const workflowPage = new WorkflowPage();
const testTemplate = templateCredentialsSetupPage.testData.simpleTemplate;
const testTemplate = {
id: 1205,
data: TestTemplate1,
};
const templateWithoutCredentials = {
id: 1344,
data: TestTemplate2,
};
// NodeView uses beforeunload listener that will show a browser
// native popup, which will block cypress from continuing / exiting.
@ -29,19 +38,19 @@ Cypress.on('window:before:load', (win) => {
describe('Template credentials setup', () => {
beforeEach(() => {
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${testTemplate.id}`, {
fixture: testTemplate.fixture,
cy.intercept(
'GET',
`https://api.n8n.io/api/templates/workflows/${testTemplate.id}`,
testTemplate.data,
).as('getTemplatePreview');
cy.intercept(
'GET',
`https://api.n8n.io/api/workflows/templates/${testTemplate.id}`,
testTemplate.data.workflow,
).as('getTemplate');
cy.overrideSettings({
templates: { enabled: true, host: 'https://api.n8n.io/api/' },
});
cy.intercept('GET', '**/rest/settings', (req) => {
// Disable cache
delete req.headers['if-none-match'];
req.reply((res) => {
if (res.body.data) {
// Disable custom templates host if it has been overridden by another intercept
res.body.data.templates = { enabled: true, host: 'https://api.n8n.io/api/' };
}
});
}).as('settingsRequest');
});
it('can be opened from template collection page', () => {
@ -108,7 +117,7 @@ describe('Template credentials setup', () => {
// Focus the canvas so the copy to clipboard works
workflowPage.getters.canvasNodes().eq(0).realClick();
workflowPage.actions.selectAll();
workflowPage.actions.hitSelectAll();
workflowPage.actions.hitCopy();
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');
@ -125,11 +134,9 @@ describe('Template credentials setup', () => {
});
it('should work with a template that has no credentials (ADO-1603)', () => {
const templateWithoutCreds = templateCredentialsSetupPage.testData.templateWithoutCredentials;
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${templateWithoutCreds.id}`, {
fixture: templateWithoutCreds.fixture,
});
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(templateWithoutCreds.id);
const { id, data } = templateWithoutCredentials;
cy.intercept('GET', `https://api.n8n.io/api/templates/workflows/${id}`, data);
templateCredentialsSetupPage.visitTemplateCredentialSetupPage(id);
const expectedAppNames = ['1. Email (IMAP)', '2. Nextcloud'];
const expectedAppDescriptions = [
@ -152,7 +159,7 @@ describe('Template credentials setup', () => {
workflowPage.getters.canvasNodes().should('have.length', 3);
});
describe('Credential setup from workflow editor', () => {
describe('Credential setup from workflow editor', { disableAutoLogin: true }, () => {
beforeEach(() => {
cy.resetDatabase();
cy.signinAsOwner();
@ -190,7 +197,7 @@ describe('Template credentials setup', () => {
// Focus the canvas so the copy to clipboard works
workflowPage.getters.canvasNodes().eq(0).realClick();
workflowPage.actions.selectAll();
workflowPage.actions.hitSelectAll();
workflowPage.actions.hitCopy();
cy.grantBrowserPermissions('clipboardReadWrite', 'clipboardSanitizedWrite');

View file

@ -1,11 +1,10 @@
import { INSTANCE_ADMIN, INSTANCE_OWNER } from '../constants';
import { SettingsPage } from '../pages/settings';
const settingsPage = new SettingsPage();
describe('Admin user', { disableAutoLogin: true }, () => {
it('should see same Settings sub menu items as instance owner', () => {
cy.signin(INSTANCE_OWNER);
cy.signinAsOwner();
cy.visit(settingsPage.url);
let ownerMenuItems = 0;
@ -15,7 +14,7 @@ describe('Admin user', { disableAutoLogin: true }, () => {
});
cy.signout();
cy.signin(INSTANCE_ADMIN);
cy.signinAsAdmin();
cy.visit(settingsPage.url);
settingsPage.getters.menuItems().should('have.length', ownerMenuItems);

View file

@ -1,4 +1,3 @@
import { INSTANCE_OWNER } from '../constants';
import { WorkflowsPage } from '../pages/workflows';
import {
closeVersionUpdatesPanel,
@ -11,11 +10,7 @@ const workflowsPage = new WorkflowsPage();
describe('Versions', () => {
it('should open updates panel', () => {
cy.intercept('GET', '/rest/settings', (req) => {
req.continue((res) => {
if (res.body.hasOwnProperty('data')) {
res.body.data = {
...res.body.data,
cy.overrideSettings({
releaseChannel: 'stable',
versionCli: '1.0.0',
versionNotifications: {
@ -23,40 +18,10 @@ describe('Versions', () => {
endpoint: 'https://api.n8n.io/api/versions/',
infoUrl: 'https://docs.n8n.io/getting-started/installation/updating.html',
},
};
}
});
}).as('settings');
cy.intercept('GET', 'https://api.n8n.io/api/versions/1.0.0', [
{
name: '1.3.1',
createdAt: '2023-08-18T11:53:12.857Z',
hasSecurityIssue: null,
hasSecurityFix: null,
securityIssueFixVersion: null,
hasBreakingChange: null,
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n131',
nodes: [],
description: 'Includes <strong>bug fixes</strong>',
},
{
name: '1.0.5',
createdAt: '2023-07-24T10:54:56.097Z',
hasSecurityIssue: false,
hasSecurityFix: null,
securityIssueFixVersion: null,
hasBreakingChange: true,
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n104',
nodes: [],
description: 'Includes <strong>core functionality</strong> and <strong>bug fixes</strong>',
},
]);
cy.signin(INSTANCE_OWNER);
cy.visit(workflowsPage.url);
cy.wait('@settings');
cy.wait('@loadSettings');
getVersionUpdatesPanelOpenButton().should('contain', '2 updates');
openVersionUpdatesPanel();

View file

@ -1,10 +1,4 @@
import {
INSTANCE_ADMIN,
INSTANCE_MEMBERS,
INSTANCE_OWNER,
MANUAL_TRIGGER_NODE_NAME,
NOTION_NODE_NAME,
} from '../constants';
import { INSTANCE_MEMBERS, MANUAL_TRIGGER_NODE_NAME, NOTION_NODE_NAME } from '../constants';
import {
WorkflowsPage,
WorkflowPage,
@ -23,7 +17,7 @@ const credentialsModal = new CredentialsModal();
const executionsTab = new WorkflowExecutionsTab();
const ndv = new NDV();
describe('Projects', () => {
describe('Projects', { disableAutoLogin: true }, () => {
before(() => {
cy.resetDatabase();
cy.enableFeature('sharing');
@ -34,7 +28,7 @@ describe('Projects', () => {
});
it('should handle workflows and credentials and menu items', () => {
cy.signin(INSTANCE_ADMIN);
cy.signinAsAdmin();
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('not.have.length');
@ -230,8 +224,7 @@ describe('Projects', () => {
});
it('should not show project add button and projects to a member if not invited to any project', () => {
cy.signout();
cy.signin(INSTANCE_MEMBERS[1]);
cy.signinAsMember(1);
cy.visit(workflowsPage.url);
projects.getAddProjectButton().should('not.exist');
@ -249,7 +242,7 @@ describe('Projects', () => {
});
it('should filter credentials by project ID when creating new workflow or hard reloading an opened workflow', () => {
cy.signin(INSTANCE_OWNER);
cy.signinAsOwner();
cy.visit(workflowsPage.url);
// Create a project and add a credential to it

View file

@ -345,7 +345,7 @@ describe('NDV', () => {
ndv.getters.parameterInput('remoteOptions').click();
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
ndv.actions.setInvalidExpression({ fieldName: 'fieldId', delay: 200 });
ndv.actions.setInvalidExpression({ fieldName: 'fieldId' });
ndv.getters.inputPanel().click(); // remove focus from input, hide expression preview
@ -363,7 +363,7 @@ describe('NDV', () => {
getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3);
ndv.getters.parameterInput('remoteOptions').click();
ndv.actions.setInvalidExpression({ fieldName: 'otherField', delay: 50 });
ndv.actions.setInvalidExpression({ fieldName: 'otherField' });
ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview

View file

@ -1,11 +1,8 @@
import {
CODE_NODE_NAME,
MANUAL_TRIGGER_NODE_NAME,
META_KEY,
SCHEDULE_TRIGGER_NODE_NAME,
EDIT_FIELDS_SET_NODE_NAME,
INSTANCE_MEMBERS,
INSTANCE_OWNER,
NOTION_NODE_NAME,
} from '../constants';
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
@ -136,13 +133,13 @@ describe('Workflow Actions', () => {
);
cy.reload();
cy.get('.el-loading-mask').should('exist');
cy.get('body').type(META_KEY, { release: false }).type('s');
cy.get('body').type(META_KEY, { release: false }).type('s');
cy.get('body').type(META_KEY, { release: false }).type('s');
WorkflowPage.actions.hitSaveWorkflow();
WorkflowPage.actions.hitSaveWorkflow();
WorkflowPage.actions.hitSaveWorkflow();
cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(0));
cy.waitForLoad();
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
cy.get('body').type(META_KEY, { release: false }).type('s');
WorkflowPage.actions.hitSaveWorkflow();
cy.wait('@saveWorkflow');
cy.wrap(null).then(() => expect(interceptCalledCount).to.eq(1));
});
@ -172,9 +169,10 @@ describe('Workflow Actions', () => {
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
cy.get('#node-creator').should('not.exist');
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('a');
WorkflowPage.actions.hitSelectAll();
cy.get('.jtk-drag-selected').should('have.length', 2);
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('c');
WorkflowPage.actions.hitCopy();
successToast().should('exist');
});
@ -338,33 +336,32 @@ describe('Workflow Actions', () => {
it('should run workflow using keyboard shortcut', () => {
WorkflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME);
WorkflowPage.actions.saveWorkflowOnButtonClick();
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('{enter}');
WorkflowPage.actions.hitExecuteWorkflow();
successToast().should('contain.text', 'Workflow executed successfully');
});
it('should not run empty workflows', () => {
// Clear the canvas
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('a');
cy.get('body').type('{backspace}');
WorkflowPage.actions.hitDeleteAllNodes();
WorkflowPage.getters.canvasNodes().should('have.length', 0);
// Button should be disabled
WorkflowPage.getters.executeWorkflowButton().should('be.disabled');
// Keyboard shortcut should not work
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('{enter}');
WorkflowPage.actions.hitExecuteWorkflow();
successToast().should('not.exist');
});
});
describe('Menu entry Push To Git', () => {
it('should not show up in the menu for members', () => {
cy.signin(INSTANCE_MEMBERS[0]);
cy.signinAsMember(0);
cy.visit(WorkflowPages.url);
WorkflowPage.actions.visit();
WorkflowPage.getters.workflowMenuItemGitPush().should('not.exist');
});
it('should show up for owners', () => {
cy.signin(INSTANCE_OWNER);
cy.signinAsOwner();
cy.visit(WorkflowPages.url);
WorkflowPage.actions.visit();
WorkflowPage.getters.workflowMenuItemGitPush().should('exist');

View file

@ -2,7 +2,7 @@
"totalWorkflows": 506,
"workflows": [
{
"id": 60,
"id": 1,
"name": "test1 test1",
"totalViews": 120000000,
"recentViews": 0,

View file

@ -1,19 +0,0 @@
{
"id": 60,
"name": "test1 test1",
"workflow": {
"nodes": [
{
"name": "Start",
"type": "n8n-nodes-base.start",
"position": [
250,
300
],
"parameters": {},
"typeVersion": 1
}
],
"connections": {}
}
}

View file

@ -1,150 +0,0 @@
{
"workflow": {
"id": 60,
"name": "test1 test1",
"views": 120000000,
"recentViews": 0,
"totalViews": 120000000,
"createdAt": "2019-08-30T16:39:31.362Z",
"description": "here is a description. here is a description. here is a description. \n\n![Screenshot from 20190806 091433.png](fileId:88)",
"workflow": {
"nodes": [
{
"name": "Start",
"type": "n8n-nodes-base.start",
"position": [
250,
300
],
"parameters": {},
"typeVersion": 1
}
],
"connections": {}
},
"lastUpdatedBy": null,
"workflowInfo": {
"nodeCount": 1,
"nodeTypes": {
"n8n-nodes-base.start": {
"count": 1
}
}
},
"user": {
"username": "admin"
},
"nodes": [
{
"id": 11,
"icon": "file:amqp.png",
"name": "n8n-nodes-base.amqpTrigger",
"defaults": {
"name": "AMQP Trigger"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6/NlyAAAB7UlEQVRoge2W4W3CMBCFj26QjkBHSEdIR4AR6Ah0BBgBRqAjhBFgBBghHaEVlV29PN0lDr+o9D7JEjhn+975bJ8JIYQQQgghhBBCCCGEEA9CY2bf0NaBW2uyu7UN2XSOzTyY60J2BzNbObbsH7eTmS2mhHJHE1wmCD7A93ngEAquHaHc2omCcysSXQW74g32BHfwfTEiuCoQm9vuDsEndPYpELxKjjBj0foCEXX6XdM3by3c7aOZPZvZzMzeaBzbIh9pzIuZXaG/RqNIMAq7Ur8XCHQ2kx3LC56DMQ39X4LI23zbAd88ruRHD09wTVF5p+/eBZI5g7O8w5FgXOvsZAI7PxRwS4HGIPbm8wRjBL/Sgp/QNyQYHWySmOxgJBgFeGnPfZHgDVyufET+YMEVCdo7gziCTBbGmRKlGQpCMXOnj+1L6B0JFsxndO3cjjZyjo6OnZeqGb5gqhTQS3qKeK1SwbesfB3IrF/awqu+g8Dgs5SLE37SciHiPUv8rLVp7k2wdl63tDDqgTs8lqpINWGXbSTKe9rlJgXME7C9I6V7oGAWsEzv2gzeN2TstkbCZyIJWBYKWUwtF4foKGU9TpRGdZDSdVDpDNXSVVBLt5TeucS9K6X/E3USX3rshBBCCCGEEEIIIYQQ4tExsx8PuuPnwhCIbgAAAABJRU5ErkJggg=="
},
"categories": [
{
"id": 5,
"name": "Development"
},
{
"id": 6,
"name": "Communication"
}
],
"displayName": "AMQP Trigger",
"typeVersion": 1
},
{
"id": 18,
"icon": "file:autopilot.svg",
"name": "n8n-nodes-base.autopilot",
"defaults": {
"name": "Autopilot"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjM4IDI2IDM1IDM1Ij48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgc3Ryb2tlPSIjMThkNGIyIiBzdHJva2Utd2lkdGg9IjMiIGZpbGw9IiMxOGQ0YjIiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNNDUuNCA0Mi42aDE5LjlsMy40LTQuOEg0MmwzLjQgNC44em0zLjEgOC4zaDEzLjFsMy40LTQuOEg0NS40bDMuMSA0Ljh6bTU0LS43Ii8+PC9zdmc+"
},
"categories": [
{
"id": 1,
"name": "Marketing"
}
],
"displayName": "Autopilot",
"typeVersion": 1
},
{
"id": 20,
"icon": "file:lambda.svg",
"name": "n8n-nodes-base.awsLambda",
"defaults": {
"name": "AWS Lambda"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjI1MDAiIHZpZXdCb3g9Ii0zLjAyMyAtMC4yMiA0MjAuOTIzIDQzMy41NCIgd2lkdGg9IjI0NDMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTIwOC40NSAyMjcuODljLTEuNTkgMi4yNi0yLjkzIDQuMTItNC4yMiA2cS0zMC44NiA0NS40Mi02MS43IDkwLjgzLTI4LjY5IDQyLjI0LTU3LjQ0IDg0LjQzYTMuODggMy44OCAwIDAxLTIuNzMgMS41OXEtNDAuNTktLjM1LTgxLjE2LS44OGMtLjMgMC0uNjEtLjA5LTEuMi0uMThhMTQuNDQgMTQuNDQgMCAwMS43Ni0xLjY1cTI4LjMxLTQzLjg5IDU2LjYyLTg3Ljc2IDI1LjExLTM4Ljg4IDUwLjI1LTc3Ljc0IDI3Ljg2LTQzLjE4IDU1LjY5LTg2LjQyYzIuNzQtNC4yNSA1LjU5LTguNDIgOC4xOS0xMi43NWE1LjI2IDUuMjYgMCAwMC41Ni0zLjgzYy01LTE1Ljk0LTEwLjEtMzEuODQtMTUuMTktNDcuNzQtMi4xOC02LjgxLTQuNDYtMTMuNTgtNi41LTIwLjQzLS42Ni0yLjItMS43NS0yLjg3LTQtMi44Ni0xNyAuMDctMzMuOS4wNS01MC44NS4wNS0zLjIyIDAtMy4yMyAwLTMuMjMtMy4xOCAwLTIwLjg0IDAtNDEuNjgtLjA2LTYyLjUyIDAtMi4zMi43Ni0yLjg0IDIuOTQtMi44NHE1MS4xOS4wOSAxMDIuNCAwYTMuMjkgMy4yOSAwIDAxMy42IDIuNDNxMjcgNjcuOTEgNTQgMTM1Ljc3IDMxLjUgNzkuMTQgNjMgMTU4LjNjNi41MiAxNi4zOCAxMy4wOSAzMi43NSAxOS41NCA0OS4xNy43NyAyIDEuNTcgMi4zOCAzLjU5IDEuNzYgMTcuODktNS41MyAzNS44Mi0xMC45MSA1My43LTE2LjQ1IDIuMjUtLjcgMy4wNy0uMjMgMy43NyAyIDYuMSAxOS4xNyAxMi4zMiAzOC4zIDE4LjUgNTcuNDUuMjEuNjYuMzcgMS4zMy42MiAyLjI1LTEuMjguNDctMi40OCAxLTMuNzEgMS4zNHEtNjEgMTkuMzMtMTIxLjkzIDM4LjY4Yy0xLjk0LjYxLTIuNTItLjA1LTMuMTctMS42OHEtMTguNjEtNDcuMTYtMzcuMzEtOTQuMjgtMTguMjktNDYuMTQtMzYuNi05Mi4yOGMtMS44My00LjYyLTMuNjMtOS4yNi01LjQ2LTEzLjg4LS4yOS0uNzktLjY5LTEuNDgtMS4yNy0yLjd6IiBmaWxsPSIjZmE3ZTE0Ii8+PC9zdmc+"
},
"categories": [
{
"id": 5,
"name": "Development"
}
],
"displayName": "AWS Lambda",
"typeVersion": 1
},
{
"id": 40,
"icon": "file:clearbit.svg",
"name": "n8n-nodes-base.clearbit",
"defaults": {
"name": "Clearbit"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI3MiIgaGVpZ2h0PSI3MiI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJhIiB4MT0iNTAlIiB4Mj0iMTAwJSIgeTE9IjAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI0RFRjJGRSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI0RCRjFGRSIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IGlkPSJiIiB4MT0iMCUiIHgyPSI1MCUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiM1N0JDRkQiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiM1MUI1RkQiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iYyIgeDE9IjM3LjUlIiB4Mj0iNjIuNSUiIHkxPSIwJSIgeTI9IjEwMCUiPjxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiMxQ0E3RkQiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiMxNDhDRkMiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGZpbGw9InVybCgjYSkiIGQ9Ik03MiAzNnYxNi43N2wtLjAwNC44NjhjLS4wNiA2LjAzNS0uNzUgOC4zNTMtMiAxMC42ODhhMTMuNjMgMTMuNjMgMCAwMS01LjY3IDUuNjdsLS4zMjYuMTcxQzYxLjY1OCA3MS4zNjQgNTkuMTYgNzIgNTIuNzcgNzJIMzZWMzZoMzZ6Ii8+PHBhdGggZmlsbD0idXJsKCNiKSIgZD0iTTY0LjMyNiAyLjAwM2ExMy42MyAxMy42MyAwIDAxNS42NyA1LjY3bC4xNzEuMzI3QzcxLjM2NCAxMC4zNDIgNzIgMTIuODQgNzIgMTkuMjNWMzZIMzZWMGgxNi43N2M2LjY4NyAwIDkuMTEyLjY5NiAxMS41NTYgMi4wMDN6Ii8+PHBhdGggZmlsbD0idXJsKCNjKSIgZD0iTTM2IDB2NzJIMTkuMjNsLS44NjgtLjAwNGMtNi4wMzUtLjA2LTguMzUzLS43NS0xMC42ODgtMmExMy42MyAxMy42MyAwIDAxLTUuNjctNS42N0wxLjgzMiA2NEMuNjM2IDYxLjY1OCAwIDU5LjE2IDAgNTIuNzdWMTkuMjNjMC02LjY4Ny42OTYtOS4xMTIgMi4wMDMtMTEuNTU2YTEzLjYzIDEzLjYzIDAgMDE1LjY3LTUuNjdMOCAxLjgzMkMxMC4zNDIuNjM2IDEyLjg0IDAgMTkuMjMgMEgzNnoiLz48L2c+PC9zdmc+"
},
"categories": [
{
"id": 2,
"name": "Sales"
}
],
"displayName": "Clearbit",
"typeVersion": 1
},
{
"id": 51,
"icon": "file:convertKit.svg",
"name": "n8n-nodes-base.convertKitTrigger",
"defaults": {
"name": "ConvertKit Trigger"
},
"iconData": {
"type": "file",
"fileBuffer": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTcyIiBoZWlnaHQ9IjE2MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNODIuNzIgMTI2LjMxNmMyOS43NyAwIDUyLjc4LTIyLjYyMiA1Mi43OC01MC41MjYgMC0yNi4xNDMtMjEuNjE3LTQyLjEwNi0zNS45MzUtNDIuMTA2LTE5Ljk0NSAwLTM1LjkzIDE0LjA4NC0zOC4xOTggMzQuOTg4LS40MTggMy44NTYtMy40NzYgNy4wOS03LjM1NSA3LjA2MS02LjQyMy0uMDQ2LTE1Ljc0Ni0uMS0yMS42NTgtLjA4LTIuNTU1LjAwOC00LjY2OS0yLjA2NS00LjU0My00LjYxOC44OS0xOC4xMjMgNi45MTQtMzUuMDcgMTguNDAyLTQ4LjA4N0M1OC45NzYgOC40ODggNzcuNTYxIDAgOTkuNTY1IDBjMzYuOTY5IDAgNzEuODY5IDMzLjc4NiA3MS44NjkgNzUuNzkgMCA0Ni41MDgtMzguMzEyIDg0LjIxLTg3LjkyNyA4NC4yMS0zNS4zODQgMC03MS4wMjEtMjMuMjU4LTgzLjQ2NC01NS43NzVhLjcwMi43MDIgMCAwMS0uMDMtLjM3N2MuMTY1LS45NjIuNDk0LTEuODQxLjgxOC0yLjcwNy40NzEtMS4yNTguOTMxLTIuNDg4Ljg2NC0zLjkwNmwtLjIxNS00LjUyOWE1LjUyMyA1LjUyMyAwIDAxMy4xOC01LjI2M2wxLjc5OC0uODQyYTYuOTgyIDYuOTgyIDAgMDAzLjkxMi01LjA3NSA2Ljk5MyA2Ljk5MyAwIDAxNi44ODctNS43MzZjNS4yODIgMCA5Ljg3NSAzLjUxNSAxMS41OSA4LjUxMiA4LjMwNyAyNC4yMTIgMjEuNTExIDQyLjAxNCA1My44NzMgNDIuMDE0eiIgZmlsbD0iI0ZCNjk3MCIvPjwvc3ZnPg=="
},
"categories": [
{
"id": 1,
"name": "Marketing"
},
{
"id": 2,
"name": "Sales"
}
],
"displayName": "ConvertKit Trigger",
"typeVersion": 1
}
],
"categories": [],
"image": []
}
}

View file

@ -10,19 +10,23 @@
"format": "prettier --write . --ignore-path ../.prettierignore",
"lint": "eslint . --quiet",
"lintfix": "eslint . --fix",
"develop": "cd ..; pnpm dev",
"start": "cd ..; pnpm start"
},
"devDependencies": {
"@types/lodash": "^4.14.195",
"@types/uuid": "^8.3.2",
"eslint-plugin-cypress": "^3.3.0",
"n8n-workflow": "workspace:*"
},
"dependencies": {
"@ngneat/falso": "^6.4.0",
"@ngneat/falso": "^7.2.0",
"@sinonjs/fake-timers": "^11.2.2",
"cross-env": "^7.0.3",
"cypress": "^13.6.2",
"cypress": "^13.11.0",
"cypress-otp": "^1.0.3",
"cypress-real-events": "^1.11.0",
"cypress-real-events": "^1.12.0",
"lodash": "4.17.21",
"start-server-and-test": "^2.0.3",
"uuid": "8.3.2"
}

View file

@ -158,9 +158,7 @@ export class NDV extends BasePage {
this.getters.pinnedDataEditor().click();
this.getters
.pinnedDataEditor()
.type(`{selectall}{backspace}${pinnedData.replace(new RegExp('{', 'g'), '{{}')}`, {
delay: 0,
});
.type(`{selectall}{backspace}${pinnedData.replace(new RegExp('{', 'g'), '{{}')}`);
this.actions.savePinnedData();
},
@ -168,10 +166,7 @@ export class NDV extends BasePage {
this.getters.editPinnedDataButton().click();
this.getters.pinnedDataEditor().click();
this.getters
.pinnedDataEditor()
.type('{selectall}{backspace}', { delay: 0 })
.paste(JSON.stringify(data));
this.getters.pinnedDataEditor().type('{selectall}{backspace}').paste(JSON.stringify(data));
this.actions.savePinnedData();
},
@ -181,7 +176,7 @@ export class NDV extends BasePage {
typeIntoParameterInput: (
parameterName: string,
content: string,
opts?: { parseSpecialCharSequences: boolean; delay?: number },
opts?: { parseSpecialCharSequences: boolean },
) => {
this.getters.parameterInput(parameterName).type(content, opts);
},
@ -272,16 +267,13 @@ export class NDV extends BasePage {
setInvalidExpression: ({
fieldName,
invalidExpression,
delay,
}: {
fieldName: string;
invalidExpression?: string;
delay?: number;
}) => {
this.actions.typeIntoParameterInput(fieldName, '=');
this.actions.typeIntoParameterInput(fieldName, invalidExpression ?? "{{ $('unknown')", {
parseSpecialCharSequences: false,
delay,
});
this.actions.validateExpressionPreview(fieldName, "node doesn't exist");
},

View file

@ -30,6 +30,7 @@ export class PersonalSettingsPage extends BasePage {
this.getters.themeSelector().click();
this.getters.selectOptionsVisible().should('have.length', 3);
this.getters.selectOptionsVisible().contains(theme).click();
this.getters.saveSettingsButton().realClick();
},
loginAndVisit: (email: string, password: string) => {
cy.signin({ email, password });

View file

@ -2,22 +2,6 @@ import * as formStep from '../composables/setup-template-form-step';
import { overrideFeatureFlag } from '../composables/featureFlags';
import { CredentialsModal, MessageBox } from './modals';
export type TemplateTestData = {
id: number;
fixture: string;
};
export const testData = {
simpleTemplate: {
id: 1205,
fixture: 'Test_Template_1.json',
},
templateWithoutCredentials: {
id: 1344,
fixture: 'Test_Template_2.json',
},
};
const credentialsModal = new CredentialsModal();
const messageBox = new MessageBox();

View file

@ -1,41 +0,0 @@
import { BasePage } from './base';
export class TemplateWorkflowPage extends BasePage {
url = '/templates';
getters = {
useTemplateButton: () => cy.get('[data-test-id="use-template-button"]'),
description: () => cy.get('[data-test-id="template-description"]'),
};
actions = {
visit: (templateId: number) => {
cy.visit(`${this.url}/${templateId}`);
},
clickUseThisWorkflowButton: () => {
this.getters.useTemplateButton().click();
},
openTemplate: (
template: {
workflow: {
id: number;
name: string;
description: string;
user: { username: string };
image: Array<{ id: number; url: string }>;
};
},
templateHost: string,
) => {
cy.intercept('GET', `${templateHost}/api/templates/workflows/${template.workflow.id}`, {
statusCode: 200,
body: template,
}).as('getTemplate');
this.actions.visit(template.workflow.id);
cy.wait('@getTemplate');
},
};
}

View file

@ -5,6 +5,7 @@ export class TemplatesPage extends BasePage {
getters = {
useTemplateButton: () => cy.getByTestId('use-template-button'),
description: () => cy.getByTestId('template-description'),
templateCards: () => cy.getByTestId('template-card'),
firstTemplateCard: () => this.getters.templateCards().first(),
allCategoriesFilter: () => cy.getByTestId('template-filter-all-categories'),
@ -14,50 +15,30 @@ export class TemplatesPage extends BasePage {
collectionCountLabel: () => cy.getByTestId('collection-count-label'),
templateCountLabel: () => cy.getByTestId('template-count-label'),
templatesLoadingContainer: () => cy.getByTestId('templates-loading-container'),
expandCategoriesButton: () => cy.getByTestId('expand-categories-button'),
};
actions = {
openSingleTemplateView: (templateId: number) => {
cy.visit(`${this.url}/${templateId}`);
cy.waitForLoad();
},
openOnboardingFlow: (id: number, name: string, workflow: object, templatesHost: string) => {
const apiResponse = {
id,
name,
workflow,
};
openOnboardingFlow: () => {
cy.intercept('POST', '/rest/workflows').as('createWorkflow');
cy.intercept('GET', `${templatesHost}/api/workflows/templates/${id}`, {
statusCode: 200,
body: apiResponse,
}).as('getTemplate');
cy.intercept('GET', 'rest/workflows/**').as('getWorkflow');
cy.visit(`/workflows/onboarding/${id}`);
cy.visit('/workflows/onboarding/1');
cy.window().then((win) => {
win.preventNodeViewBeforeUnload = true;
});
cy.wait('@getTemplate');
cy.wait(['@createWorkflow', '@getWorkflow']);
cy.wait(['@getTemplate', '@createWorkflow', '@getWorkflow']);
},
importTemplate: (id: number, name: string, workflow: object, templatesHost: string) => {
const apiResponse = {
id,
name,
workflow,
};
cy.intercept('GET', `${templatesHost}/api/workflows/templates/${id}`, {
statusCode: 200,
body: apiResponse,
}).as('getTemplate');
importTemplate: () => {
cy.intercept('GET', 'rest/workflows/**').as('getWorkflow');
cy.visit(`/workflows/templates/${id}`);
cy.visit('/workflows/templates/1');
cy.window().then((win) => {
win.preventNodeViewBeforeUnload = true;
});
cy.wait('@getTemplate');
cy.wait('@getWorkflow');
cy.wait(['@getTemplate', '@getWorkflow']);
},
};
}

View file

@ -283,7 +283,7 @@ export class WorkflowPage extends BasePage {
},
saveWorkflowUsingKeyboardShortcut: () => {
cy.intercept('POST', '/rest/workflows').as('createWorkflow');
cy.get('body').type(META_KEY, { release: false }).type('s');
this.actions.hitSaveWorkflow();
},
deleteNode: (name: string) => {
this.getters.canvasNodeByName(name).first().click();
@ -339,35 +339,43 @@ export class WorkflowPage extends BasePage {
});
});
},
/** Certain keyboard shortcuts are not possible on Cypress via a simple `.type`, and some delays are needed to emulate these events */
hitComboShortcut: (modifier: string, key: string) => {
cy.get('body').wait(100).type(modifier, { delay: 100, release: false }).type(key);
},
hitUndo: () => {
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('z');
this.actions.hitComboShortcut(`{${META_KEY}}`, 'z');
},
hitRedo: () => {
cy.get('body')
.type(META_KEY, { delay: 500, release: false })
.type('{shift}', { release: false })
.type('z');
cy.get('body').type(`{${META_KEY}+shift+z}`);
},
selectAll: () => {
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('a');
hitSelectAll: () => {
this.actions.hitComboShortcut(`{${META_KEY}}`, 'a');
},
hitDeleteAllNodes: () => {
this.actions.hitSelectAll();
cy.get('body').type('{backspace}');
},
hitDisableNodeShortcut: () => {
cy.get('body').type('d');
},
hitCopy: () => {
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('c');
this.actions.hitComboShortcut(`{${META_KEY}}`, 'c');
},
hitPinNodeShortcut: () => {
cy.get('body').type('p');
},
hitExecuteWorkflowShortcut: () => {
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('{enter}');
hitSaveWorkflow: () => {
cy.get('body').type(`{${META_KEY}+s}`);
},
hitDuplicateNodeShortcut: () => {
cy.get('body').type(META_KEY, { delay: 500, release: false }).type('d');
hitExecuteWorkflow: () => {
cy.get('body').type(`{${META_KEY}+enter}`);
},
hitAddStickyShortcut: () => {
cy.get('body').type('{shift}', { delay: 500, release: false }).type('S');
hitDuplicateNode: () => {
cy.get('body').type(`{${META_KEY}+d}`);
},
hitAddSticky: () => {
cy.get('body').type('{shift+S}');
},
executeWorkflow: () => {
this.getters.executeWorkflowButton().click();

View file

@ -50,7 +50,7 @@ switch (scenario) {
break;
case 'dev':
runTests({
startCommand: 'dev',
startCommand: 'develop',
url: 'http://localhost:8080/favicon.ico',
testCommand: 'cypress open',
customEnv: {

View file

@ -1,5 +1,6 @@
import 'cypress-real-events';
import FakeTimers from '@sinonjs/fake-timers';
import type { IN8nUISettings } from 'n8n-workflow';
import { WorkflowPage } from '../pages';
import {
BACKEND_BASE_URL,
@ -66,9 +67,9 @@ Cypress.Commands.add('signin', ({ email, password }) => {
);
});
Cypress.Commands.add('signinAsOwner', () => {
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
});
Cypress.Commands.add('signinAsOwner', () => cy.signin(INSTANCE_OWNER));
Cypress.Commands.add('signinAsAdmin', () => cy.signin(INSTANCE_ADMIN));
Cypress.Commands.add('signinAsMember', (index = 0) => cy.signin(INSTANCE_MEMBERS[index]));
Cypress.Commands.add('signout', () => {
cy.request({
@ -79,8 +80,9 @@ Cypress.Commands.add('signout', () => {
cy.getCookie(N8N_AUTH_COOKIE).should('not.exist');
});
Cypress.Commands.add('interceptREST', (method, url) => {
cy.intercept(method, `${BACKEND_BASE_URL}/rest${url}`);
export let settings: Partial<IN8nUISettings>;
Cypress.Commands.add('overrideSettings', (value: Partial<IN8nUISettings>) => {
settings = value;
});
const setFeature = (feature: string, enabled: boolean) =>

View file

@ -1,5 +1,6 @@
import { INSTANCE_OWNER } from '../constants';
import './commands';
import cloneDeep from 'lodash/cloneDeep';
import merge from 'lodash/merge';
import { settings } from './commands';
before(() => {
cy.resetDatabase();
@ -11,21 +12,54 @@ before(() => {
beforeEach(() => {
if (!cy.config('disableAutoLogin')) {
cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password });
cy.signinAsOwner();
}
cy.window().then((win): void => {
win.localStorage.setItem('N8N_THEME', 'light');
});
cy.intercept('GET', '/rest/settings').as('loadSettings');
cy.intercept('GET', '/rest/settings', (req) => {
// Disable cache
delete req.headers['if-none-match'];
req.on('response', (res) => {
const defaultSettings = res.body.data;
res.send({ data: merge(cloneDeep(defaultSettings), settings) });
});
}).as('loadSettings');
cy.intercept('GET', '/types/nodes.json').as('loadNodeTypes');
// Always intercept the request to test credentials and return a success
cy.intercept('POST', '/rest/credentials/test', {
statusCode: 200,
body: {
data: { status: 'success', message: 'Tested successfully' },
}).as('credentialTest');
cy.intercept('POST', '/rest/license/renew', {});
cy.intercept({ pathname: '/api/health' }, { status: 'OK' }).as('healthCheck');
cy.intercept({ pathname: '/api/versions/*' }, [
{
name: '1.45.1',
createdAt: '2023-08-18T11:53:12.857Z',
hasSecurityIssue: null,
hasSecurityFix: null,
securityIssueFixVersion: null,
hasBreakingChange: null,
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n131',
nodes: [],
description: 'Includes <strong>bug fixes</strong>',
},
});
{
name: '1.0.5',
createdAt: '2023-07-24T10:54:56.097Z',
hasSecurityIssue: false,
hasSecurityFix: null,
securityIssueFixVersion: null,
hasBreakingChange: true,
documentationUrl: 'https://docs.n8n.io/release-notes/#n8n104',
nodes: [],
description: 'Includes <strong>core functionality</strong> and <strong>bug fixes</strong>',
},
]).as('getVersions');
});

View file

@ -1,7 +1,11 @@
// Load type definitions that come with Cypress module
/// <reference types="cypress" />
import type { Interception } from 'cypress/types/net-stubbing';
import type { IN8nUISettings } from 'n8n-workflow';
Cypress.Keyboard.defaults({
keystrokeDelay: 0,
});
interface SigninPayload {
email: string;
@ -22,10 +26,13 @@ declare global {
): Chainable<JQuery<HTMLElement>>;
findChildByTestId(childTestId: string): Chainable<JQuery<HTMLElement>>;
createFixtureWorkflow(fixtureKey: string, workflowName: string): void;
/** @deprecated */
signin(payload: SigninPayload): void;
signinAsOwner(): void;
signinAsAdmin(): void;
signinAsMember(index?: number): void;
signout(): void;
interceptREST(method: string, url: string): Chainable<Interception>;
overrideSettings(value: Partial<IN8nUISettings>): void;
enableFeature(feature: string): void;
disableFeature(feature: string): void;
enableQueueMode(): void;

View file

@ -4,7 +4,7 @@
"sourceMap": false,
"declaration": false,
"lib": ["esnext", "dom"],
"types": ["cypress", "node"]
"types": ["cypress", "node", "cypress-real-events"]
},
"include": ["**/*.ts"],
"exclude": ["**/dist/**/*", "**/node_modules/**/*"],

View file

@ -467,7 +467,7 @@ const config = (module.exports = {
overrides: [
{
files: ['test/**/*.ts', '**/__tests__/*.ts'],
files: ['test/**/*.ts', '**/__tests__/*.ts', '**/*.cy.ts'],
rules: {
'n8n-local-rules/no-plain-errors': 'off',
'n8n-local-rules/no-skipped-tests':

View file

@ -104,8 +104,6 @@ const iconSource = computed<NodeIconSource>(() => {
// Otherwise, extract it from icon prop
if (nodeType.icon) {
const icon = getNodeIcon(nodeType, uiStore.appliedTheme);
console.log(nodeType.icon, icon);
if (icon) {
const [type, path] = icon.split(':');
if (type === 'file') {

View file

@ -123,8 +123,8 @@ importers:
cypress:
dependencies:
'@ngneat/falso':
specifier: ^6.4.0
version: 6.4.0
specifier: ^7.2.0
version: 7.2.0
'@sinonjs/fake-timers':
specifier: ^11.2.2
version: 11.2.2
@ -132,14 +132,17 @@ importers:
specifier: ^7.0.3
version: 7.0.3
cypress:
specifier: ^13.6.2
version: 13.6.2
specifier: ^13.11.0
version: 13.11.0
cypress-otp:
specifier: ^1.0.3
version: 1.0.3
cypress-real-events:
specifier: ^1.11.0
version: 1.11.0(cypress@13.6.2)
specifier: ^1.12.0
version: 1.12.0(cypress@13.11.0)
lodash:
specifier: 4.17.21
version: 4.17.21
start-server-and-test:
specifier: ^2.0.3
version: 2.0.3
@ -147,9 +150,15 @@ importers:
specifier: 8.3.2
version: 8.3.2
devDependencies:
'@types/lodash':
specifier: ^4.14.195
version: 4.14.195
'@types/uuid':
specifier: ^8.3.2
version: 8.3.4
eslint-plugin-cypress:
specifier: ^3.3.0
version: 3.3.0(eslint@8.57.0)
n8n-workflow:
specifier: workspace:*
version: link:../packages/workflow
@ -4118,8 +4127,8 @@ packages:
'@ndelangen/get-tarball@3.0.7':
resolution: {integrity: sha512-NqGfTZIZpRFef1GoVaShSSRwDC3vde3ThtTeqFdcYd6ipKqnfEVhjK2hUeHjCQUcptyZr2TONqcloFXM+5QBrQ==}
'@ngneat/falso@6.4.0':
resolution: {integrity: sha512-f6r036h2fX/AoHw1eV2t8+qWQwrbSrozs3zXMhhwoO7SJBc+DGMxRWEhFeYIinfwx0uhUH8ggx5+PDLzYESLOA==}
'@ngneat/falso@7.2.0':
resolution: {integrity: sha512-283EXBFd05kCbGuGSXgmvhCsQYEYzvD/eJaE7lxd05qRB0tgREvZX7TRlJ1KSp8nHxoK6Ws029G1Y30mt4IVAA==}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@ -7296,13 +7305,13 @@ packages:
cypress-otp@1.0.3:
resolution: {integrity: sha512-o7LssfI0HRHa+TkaOE5/Aukv6M9vsoZAtYESr9m7Ky2i+HRNb2p/IRelE7Z0wJ/UK2f+nXAGZIfXqraf9EPDqw==}
cypress-real-events@1.11.0:
resolution: {integrity: sha512-4LXVRsyq+xBh5TmlEyO1ojtBXtN7xw720Pwb9rEE9rkJuXmeH3VyoR1GGayMGr+Itqf11eEjfDewtDmcx6PWPQ==}
cypress-real-events@1.12.0:
resolution: {integrity: sha512-oiy+4kGKkzc2PT36k3GGQqkGxNiVypheWjMtfyi89iIk6bYmTzeqxapaLHS3pnhZOX1IEbTDUVxh8T4Nhs1tyQ==}
peerDependencies:
cypress: ^4.x || ^5.x || ^6.x || ^7.x || ^8.x || ^9.x || ^10.x || ^11.x || ^12.x || ^13.x
cypress@13.6.2:
resolution: {integrity: sha512-TW3bGdPU4BrfvMQYv1z3oMqj71YI4AlgJgnrycicmPZAXtvywVFZW9DAToshO65D97rCWfG/kqMFsYB6Kp91gQ==}
cypress@13.11.0:
resolution: {integrity: sha512-NXXogbAxVlVje4XHX+Cx5eMFZv4Dho/2rIcdBHg9CNPFUGZdM4cRdgIgM7USmNYsC12XY0bZENEQ+KBk72fl+A==}
engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
hasBin: true
@ -7900,6 +7909,11 @@ packages:
eslint-import-resolver-webpack:
optional: true
eslint-plugin-cypress@3.3.0:
resolution: {integrity: sha512-HPHMPzYBIshzJM8wqgKSKHG2p/8R0Gbg4Pb3tcdC9WrmkuqxiKxSKbjunUrajhV5l7gCIFrh1P7C7GuBqH6YuQ==}
peerDependencies:
eslint: '>=7'
eslint-plugin-import@2.29.1:
resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
engines: {node: '>=4'}
@ -17147,7 +17161,7 @@ snapshots:
pump: 3.0.0
tar-fs: 2.1.1
'@ngneat/falso@6.4.0':
'@ngneat/falso@7.2.0':
dependencies:
seedrandom: 3.0.5
uuid: 8.3.2
@ -21458,15 +21472,14 @@ snapshots:
dependencies:
otplib: 12.0.1
cypress-real-events@1.11.0(cypress@13.6.2):
cypress-real-events@1.12.0(cypress@13.11.0):
dependencies:
cypress: 13.6.2
cypress: 13.11.0
cypress@13.6.2:
cypress@13.11.0:
dependencies:
'@cypress/request': 3.0.1
'@cypress/xvfb': 1.2.4(supports-color@8.1.1)
'@types/node': 18.16.16
'@types/sinonjs__fake-timers': 8.1.1
'@types/sizzle': 2.3.3
arch: 2.2.0
@ -22223,6 +22236,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-plugin-cypress@3.3.0(eslint@8.57.0):
dependencies:
eslint: 8.57.0
globals: 13.20.0
eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0):
dependencies:
array-includes: 3.1.7