mirror of
https://github.com/n8n-io/n8n.git
synced 2025-11-20 17:46:34 +00:00
test: Migrate cypress batch to playwright (#19854)
This commit is contained in:
parent
2d7990920d
commit
ded6db694a
@ -1,128 +0,0 @@
|
||||
import generateOTPToken from 'cypress-otp';
|
||||
|
||||
import { INSTANCE_OWNER, INSTANCE_ADMIN, BACKEND_BASE_URL } from '../../constants';
|
||||
import { SigninPage } from '../../pages';
|
||||
import { MfaLoginPage } from '../../pages/mfa-login';
|
||||
import { successToast } from '../../pages/notifications';
|
||||
import { PersonalSettingsPage } from '../../pages/settings-personal';
|
||||
import { MainSidebar } from './../../pages/sidebar/main-sidebar';
|
||||
|
||||
const MFA_SECRET = 'KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD';
|
||||
|
||||
const RECOVERY_CODE = 'd04ea17f-e8b2-4afa-a9aa-57a2c735b30e';
|
||||
|
||||
const user = {
|
||||
email: INSTANCE_OWNER.email,
|
||||
password: INSTANCE_OWNER.password,
|
||||
firstName: 'User',
|
||||
lastName: 'A',
|
||||
mfaEnabled: false,
|
||||
mfaSecret: MFA_SECRET,
|
||||
mfaRecoveryCodes: [RECOVERY_CODE],
|
||||
};
|
||||
|
||||
const admin = {
|
||||
email: INSTANCE_ADMIN.email,
|
||||
password: INSTANCE_ADMIN.password,
|
||||
firstName: 'Admin',
|
||||
lastName: 'B',
|
||||
mfaEnabled: false,
|
||||
mfaSecret: MFA_SECRET,
|
||||
mfaRecoveryCodes: [RECOVERY_CODE],
|
||||
};
|
||||
|
||||
const mfaLoginPage = new MfaLoginPage();
|
||||
const signinPage = new SigninPage();
|
||||
const personalSettingsPage = new PersonalSettingsPage();
|
||||
const mainSidebar = new MainSidebar();
|
||||
|
||||
describe('Two-factor authentication', { disableAutoLogin: true }, () => {
|
||||
beforeEach(() => {
|
||||
cy.request('POST', `${BACKEND_BASE_URL}/rest/e2e/reset`, {
|
||||
owner: user,
|
||||
members: [],
|
||||
admin,
|
||||
});
|
||||
cy.on('uncaught:exception', (error) => {
|
||||
expect(error.message).to.include('Not logged in');
|
||||
return false;
|
||||
});
|
||||
cy.intercept('GET', '/rest/mfa/qr').as('getMfaQrCode');
|
||||
});
|
||||
|
||||
it('Should be able to login with MFA code', () => {
|
||||
const { email, password } = user;
|
||||
signinPage.actions.loginWithEmailAndPassword(email, password);
|
||||
personalSettingsPage.actions.enableMfa();
|
||||
mainSidebar.actions.signout();
|
||||
const mfaCode = generateOTPToken(user.mfaSecret);
|
||||
mfaLoginPage.actions.loginWithMfaCode(email, password, mfaCode);
|
||||
mainSidebar.actions.signout();
|
||||
});
|
||||
|
||||
it('Should be able to login with MFA recovery code', () => {
|
||||
const { email, password } = user;
|
||||
signinPage.actions.loginWithEmailAndPassword(email, password);
|
||||
personalSettingsPage.actions.enableMfa();
|
||||
mainSidebar.actions.signout();
|
||||
mfaLoginPage.actions.loginWithMfaRecoveryCode(email, password, user.mfaRecoveryCodes[0]);
|
||||
mainSidebar.actions.signout();
|
||||
});
|
||||
|
||||
it('Should be able to disable MFA in account with MFA code', () => {
|
||||
const { email, password } = user;
|
||||
signinPage.actions.loginWithEmailAndPassword(email, password);
|
||||
personalSettingsPage.actions.enableMfa();
|
||||
mainSidebar.actions.signout();
|
||||
const mfaCode = generateOTPToken(user.mfaSecret);
|
||||
mfaLoginPage.actions.loginWithMfaCode(email, password, mfaCode);
|
||||
const disableToken = generateOTPToken(user.mfaSecret);
|
||||
personalSettingsPage.actions.disableMfa(disableToken);
|
||||
personalSettingsPage.getters.enableMfaButton().should('exist');
|
||||
mainSidebar.actions.signout();
|
||||
});
|
||||
|
||||
it('Should prompt for MFA code when email changes', () => {
|
||||
const { email, password } = user;
|
||||
signinPage.actions.loginWithEmailAndPassword(email, password);
|
||||
personalSettingsPage.actions.enableMfa();
|
||||
personalSettingsPage.actions.updateEmail('newemail@test.com');
|
||||
const mfaCode = generateOTPToken(user.mfaSecret);
|
||||
personalSettingsPage.getters.mfaCodeOrMfaRecoveryCodeInput().type(mfaCode);
|
||||
personalSettingsPage.getters.mfaSaveButton().click();
|
||||
successToast().should('exist');
|
||||
mainSidebar.actions.signout();
|
||||
});
|
||||
|
||||
it('Should prompt for MFA recovery code when email changes', () => {
|
||||
const { email, password } = user;
|
||||
signinPage.actions.loginWithEmailAndPassword(email, password);
|
||||
personalSettingsPage.actions.enableMfa();
|
||||
personalSettingsPage.actions.updateEmail('newemail@test.com');
|
||||
personalSettingsPage.getters.mfaCodeOrMfaRecoveryCodeInput().type(RECOVERY_CODE);
|
||||
personalSettingsPage.getters.mfaSaveButton().click();
|
||||
successToast().should('exist');
|
||||
mainSidebar.actions.signout();
|
||||
});
|
||||
|
||||
it('Should not prompt for MFA code or recovery code when first name or last name changes', () => {
|
||||
const { email, password } = user;
|
||||
signinPage.actions.loginWithEmailAndPassword(email, password);
|
||||
personalSettingsPage.actions.enableMfa();
|
||||
personalSettingsPage.actions.updateFirstAndLastName('newFirstName', 'newLastName');
|
||||
successToast().should('exist');
|
||||
mainSidebar.actions.signout();
|
||||
});
|
||||
|
||||
it('Should be able to disable MFA in account with recovery code', () => {
|
||||
const { email, password } = user;
|
||||
signinPage.actions.loginWithEmailAndPassword(email, password);
|
||||
personalSettingsPage.actions.enableMfa();
|
||||
mainSidebar.actions.signout();
|
||||
const mfaCode = generateOTPToken(user.mfaSecret);
|
||||
mfaLoginPage.actions.loginWithMfaCode(email, password, mfaCode);
|
||||
personalSettingsPage.actions.disableMfa(user.mfaRecoveryCodes[0]);
|
||||
personalSettingsPage.getters.enableMfaButton().should('exist');
|
||||
mainSidebar.actions.signout();
|
||||
});
|
||||
});
|
||||
@ -1,116 +0,0 @@
|
||||
import {
|
||||
getCancelSaveChangesButton,
|
||||
getCloseSaveChangesButton,
|
||||
getSaveChangesModal,
|
||||
} from '../../composables/modals/save-changes-modal';
|
||||
import { getNdvContainer } from '../../composables/ndv';
|
||||
import { getHomeButton } from '../../composables/projects';
|
||||
import { addNodeToCanvas, saveWorkflowOnButtonClick } from '../../composables/workflow';
|
||||
import {
|
||||
getCreateWorkflowButton,
|
||||
getNewWorkflowCardButton,
|
||||
getWorkflowsPageUrl,
|
||||
visitWorkflowsPage,
|
||||
} from '../../composables/workflowsPage';
|
||||
import { EDIT_FIELDS_SET_NODE_NAME } from '../../constants';
|
||||
import { warningToast } from '../../pages/notifications';
|
||||
|
||||
describe('Workflows', () => {
|
||||
beforeEach(() => {
|
||||
visitWorkflowsPage();
|
||||
});
|
||||
|
||||
it('should ask to save unsaved changes before leaving route', () => {
|
||||
getNewWorkflowCardButton().should('be.visible');
|
||||
getNewWorkflowCardButton().click();
|
||||
|
||||
cy.createFixtureWorkflow('Test_workflow_1.json');
|
||||
|
||||
addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||
|
||||
getHomeButton().click();
|
||||
|
||||
// We expect to still be on the workflow route here
|
||||
cy.url().should('include', '/workflow/');
|
||||
|
||||
getSaveChangesModal().should('be.visible');
|
||||
getCancelSaveChangesButton().click();
|
||||
|
||||
// Only now do we switch
|
||||
cy.url().should('include', getWorkflowsPageUrl());
|
||||
});
|
||||
|
||||
it('should correct route after cancelling saveChangesModal', () => {
|
||||
getCreateWorkflowButton().click();
|
||||
|
||||
cy.createFixtureWorkflow('Test_workflow_1.json');
|
||||
addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
|
||||
|
||||
// Here we go back via browser rather than the home button
|
||||
// As this already updates the route
|
||||
cy.go(-1);
|
||||
|
||||
cy.url().should('include', getWorkflowsPageUrl());
|
||||
|
||||
getSaveChangesModal().should('be.visible');
|
||||
getCloseSaveChangesButton().click();
|
||||
|
||||
// Confirm the url is back to the workflow
|
||||
cy.url().should('include', '/workflow/');
|
||||
});
|
||||
|
||||
it('should correct route when opening and closing NDV', () => {
|
||||
getCreateWorkflowButton().click();
|
||||
saveWorkflowOnButtonClick();
|
||||
cy.url().then((startUrl) => {
|
||||
cy.createFixtureWorkflow('Test_workflow_1.json');
|
||||
cy.url().should('equal', startUrl);
|
||||
|
||||
addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
|
||||
|
||||
// Getting the generated nodeId is awkward, so we just ensure the URL changed
|
||||
cy.url().should('not.equal', startUrl);
|
||||
|
||||
cy.get('body').type('{esc}');
|
||||
|
||||
cy.url().should('equal', startUrl);
|
||||
});
|
||||
});
|
||||
|
||||
it('should open ndv via URL', () => {
|
||||
getCreateWorkflowButton().click();
|
||||
saveWorkflowOnButtonClick();
|
||||
cy.createFixtureWorkflow('Test_workflow_1.json');
|
||||
|
||||
addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
|
||||
cy.url().then((ndvUrl) => {
|
||||
cy.get('body').type('{esc}');
|
||||
saveWorkflowOnButtonClick();
|
||||
|
||||
getNdvContainer().should('not.be.visible');
|
||||
|
||||
cy.visit(ndvUrl);
|
||||
|
||||
getNdvContainer().should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
it('should open show warning and drop nodeId from URL if it contained an unknown nodeId', () => {
|
||||
getCreateWorkflowButton().click();
|
||||
saveWorkflowOnButtonClick();
|
||||
cy.createFixtureWorkflow('Test_workflow_1.json');
|
||||
|
||||
addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
|
||||
cy.url().then((ndvUrl) => {
|
||||
cy.get('body').type('{esc}');
|
||||
saveWorkflowOnButtonClick();
|
||||
|
||||
getNdvContainer().should('not.be.visible');
|
||||
|
||||
cy.visit(ndvUrl + 'thisMessesUpTheNodeId');
|
||||
|
||||
warningToast().should('be.visible');
|
||||
cy.url().should('equal', ndvUrl.split('/').slice(0, -1).join('/'));
|
||||
});
|
||||
});
|
||||
});
|
||||
69
packages/testing/playwright/composables/MfaComposer.ts
Normal file
69
packages/testing/playwright/composables/MfaComposer.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import { authenticator } from 'otplib';
|
||||
|
||||
import type { n8nPage } from '../pages/n8nPage';
|
||||
|
||||
export class MfaComposer {
|
||||
constructor(private readonly n8n: n8nPage) {}
|
||||
|
||||
/**
|
||||
* Enable MFA for a user using predefined secret
|
||||
* @param email - User email
|
||||
* @param password - User password
|
||||
* @param mfaSecret - Known MFA secret to use for token generation
|
||||
*/
|
||||
async enableMfa(email: string, password: string, mfaSecret: string): Promise<void> {
|
||||
await this.n8n.signIn.loginWithEmailAndPassword(email, password, true);
|
||||
await this.n8n.settings.goToPersonalSettings();
|
||||
|
||||
await this.n8n.settings.clickEnableMfa();
|
||||
|
||||
await this.n8n.mfaSetupModal.getModalContainer().waitFor({ state: 'visible' });
|
||||
|
||||
await this.n8n.mfaSetupModal.clickCopySecretToClipboard();
|
||||
|
||||
const token = authenticator.generate(mfaSecret);
|
||||
await this.n8n.mfaSetupModal.fillToken(token);
|
||||
await expect(this.n8n.mfaSetupModal.getDownloadRecoveryCodesButton()).toBeVisible();
|
||||
await this.n8n.mfaSetupModal.clickDownloadRecoveryCodes();
|
||||
await this.n8n.mfaSetupModal.clickSave();
|
||||
await this.n8n.mfaSetupModal.waitForHidden();
|
||||
}
|
||||
|
||||
/**
|
||||
* Login with MFA code
|
||||
* @param email - User email
|
||||
* @param password - User password
|
||||
* @param mfaSecret - Known MFA secret for token generation
|
||||
*/
|
||||
async loginWithMfaCode(email: string, password: string, mfaSecret: string): Promise<void> {
|
||||
await this.n8n.signIn.fillEmail(email);
|
||||
await this.n8n.signIn.fillPassword(password);
|
||||
await this.n8n.signIn.clickSubmit();
|
||||
|
||||
await expect(this.n8n.mfaLogin.getForm()).toBeVisible();
|
||||
const loginMfaCode = authenticator.generate(mfaSecret);
|
||||
await this.n8n.mfaLogin.submitMfaCode(loginMfaCode);
|
||||
await expect(this.n8n.page).toHaveURL(/workflows/);
|
||||
}
|
||||
|
||||
/**
|
||||
* Login with MFA recovery code
|
||||
* @param email - User email
|
||||
* @param password - User password
|
||||
* @param recoveryCode - Known recovery code
|
||||
*/
|
||||
async loginWithMfaRecoveryCode(
|
||||
email: string,
|
||||
password: string,
|
||||
recoveryCode: string,
|
||||
): Promise<void> {
|
||||
await this.n8n.signIn.fillEmail(email);
|
||||
await this.n8n.signIn.fillPassword(password);
|
||||
await this.n8n.signIn.clickSubmit();
|
||||
|
||||
await expect(this.n8n.mfaLogin.getForm()).toBeVisible();
|
||||
await this.n8n.mfaLogin.submitMfaRecoveryCode(recoveryCode);
|
||||
await expect(this.n8n.page).toHaveURL(/workflows/);
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,9 @@ export interface UserCredentials {
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
mfaEnabled?: boolean;
|
||||
mfaSecret?: string;
|
||||
mfaRecoveryCodes?: string[];
|
||||
}
|
||||
|
||||
// Simple name generators
|
||||
@ -58,6 +61,9 @@ export const INSTANCE_OWNER_CREDENTIALS: UserCredentials = {
|
||||
password: DEFAULT_USER_PASSWORD,
|
||||
firstName: randFirstName(),
|
||||
lastName: randLastName(),
|
||||
mfaEnabled: false,
|
||||
mfaSecret: 'KVKFKRCPNZQUYMLXOVYDSQKJKZDTSRLD',
|
||||
mfaRecoveryCodes: ['d04ea17f-e8b2-4afa-a9aa-57a2c735b30e'],
|
||||
};
|
||||
|
||||
export const INSTANCE_ADMIN_CREDENTIALS: UserCredentials = {
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
"n8n-workflow": "workspace:*",
|
||||
"flatted": "catalog:",
|
||||
"nanoid": "catalog:",
|
||||
"otplib": "^12.0.1",
|
||||
"tsx": "catalog:",
|
||||
"mockserver-client": "^5.15.0",
|
||||
"zod": "catalog:"
|
||||
|
||||
@ -8,6 +8,7 @@ import { CredentialModal } from './components/CredentialModal';
|
||||
import { FocusPanel } from './components/FocusPanel';
|
||||
import { LogsPanel } from './components/LogsPanel';
|
||||
import { NodeCreator } from './components/NodeCreator';
|
||||
import { SaveChangesModal } from './components/SaveChangesModal';
|
||||
import { StickyComponent } from './components/StickyComponent';
|
||||
|
||||
export class CanvasPage extends BasePage {
|
||||
@ -16,6 +17,7 @@ export class CanvasPage extends BasePage {
|
||||
readonly focusPanel = new FocusPanel(this.page.getByTestId('focus-panel'));
|
||||
readonly credentialModal = new CredentialModal(this.page.getByTestId('editCredential-modal'));
|
||||
readonly nodeCreator = new NodeCreator(this.page);
|
||||
readonly saveChangesModal = new SaveChangesModal(this.page.locator('.el-overlay'));
|
||||
|
||||
saveWorkflowButton(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Save' });
|
||||
|
||||
67
packages/testing/playwright/pages/MfaLoginPage.ts
Normal file
67
packages/testing/playwright/pages/MfaLoginPage.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
/**
|
||||
* Page object for the MFA login page that appears after entering email/password when MFA is enabled.
|
||||
*/
|
||||
export class MfaLoginPage extends BasePage {
|
||||
getForm(): Locator {
|
||||
return this.page.getByTestId('mfa-login-form');
|
||||
}
|
||||
|
||||
getMfaCodeField(): Locator {
|
||||
return this.getForm().locator('input[name="mfaCode"]');
|
||||
}
|
||||
|
||||
getMfaRecoveryCodeField(): Locator {
|
||||
return this.getForm().locator('input[name="mfaRecoveryCode"]');
|
||||
}
|
||||
|
||||
getEnterRecoveryCodeButton(): Locator {
|
||||
return this.page.getByTestId('mfa-enter-recovery-code-button');
|
||||
}
|
||||
|
||||
getSubmitButton(): Locator {
|
||||
return this.page.getByRole('button', { name: /^(Continue|Verify)$/ });
|
||||
}
|
||||
|
||||
async goToMfaLogin(): Promise<void> {
|
||||
await this.page.goto('/mfa');
|
||||
}
|
||||
|
||||
async fillMfaCode(code: string): Promise<void> {
|
||||
await this.getMfaCodeField().fill(code);
|
||||
}
|
||||
|
||||
async fillMfaRecoveryCode(recoveryCode: string): Promise<void> {
|
||||
await this.getMfaRecoveryCodeField().fill(recoveryCode);
|
||||
}
|
||||
|
||||
async clickEnterRecoveryCode(): Promise<void> {
|
||||
await this.clickByTestId('mfa-enter-recovery-code-button');
|
||||
}
|
||||
|
||||
async clickSubmit(): Promise<void> {
|
||||
await this.getSubmitButton().click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill MFA code and submit the form
|
||||
* @param code - The MFA token to submit
|
||||
*/
|
||||
async submitMfaCode(code: string): Promise<void> {
|
||||
await this.fillMfaCode(code);
|
||||
// Form auto-submits
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to recovery code mode, fill recovery code and submit
|
||||
* @param recoveryCode - The recovery code to submit
|
||||
*/
|
||||
async submitMfaRecoveryCode(recoveryCode: string): Promise<void> {
|
||||
await this.clickEnterRecoveryCode();
|
||||
await this.fillMfaRecoveryCode(recoveryCode);
|
||||
// Form auto-submits
|
||||
}
|
||||
}
|
||||
52
packages/testing/playwright/pages/MfaSetupModal.ts
Normal file
52
packages/testing/playwright/pages/MfaSetupModal.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
/**
|
||||
* Page object for the MFA setup modal that appears when enabling two-factor authentication.
|
||||
*/
|
||||
export class MfaSetupModal extends BasePage {
|
||||
getModalContainer(): Locator {
|
||||
return this.page.getByTestId('mfaSetup-modal');
|
||||
}
|
||||
|
||||
getTokenInput(): Locator {
|
||||
return this.page.getByTestId('mfa-token-input');
|
||||
}
|
||||
|
||||
getCopySecretToClipboardButton(): Locator {
|
||||
return this.page.getByTestId('mfa-secret-button');
|
||||
}
|
||||
|
||||
getDownloadRecoveryCodesButton(): Locator {
|
||||
return this.page.getByTestId('mfa-recovery-codes-button');
|
||||
}
|
||||
|
||||
getSaveButton(): Locator {
|
||||
return this.page.getByTestId('mfa-save-button');
|
||||
}
|
||||
|
||||
async fillToken(token: string): Promise<void> {
|
||||
await this.getTokenInput().fill(token);
|
||||
}
|
||||
|
||||
async clickCopySecretToClipboard(): Promise<void> {
|
||||
await this.clickByTestId('mfa-secret-button');
|
||||
}
|
||||
|
||||
async clickDownloadRecoveryCodes(): Promise<void> {
|
||||
await this.clickByTestId('mfa-recovery-codes-button');
|
||||
}
|
||||
|
||||
async clickSave(): Promise<void> {
|
||||
await this.getModalContainer().getByTestId('mfa-save-button').click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the MFA setup modal to be hidden from view
|
||||
*/
|
||||
async waitForHidden(): Promise<void> {
|
||||
await expect(this.getModalContainer()).toBeHidden();
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,10 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
/**
|
||||
* Page object for Settings including Personal Settings where users can update their profile and manage MFA.
|
||||
*/
|
||||
export class SettingsPage extends BasePage {
|
||||
getMenuItems() {
|
||||
return this.page.getByTestId('menu-item');
|
||||
@ -17,32 +22,106 @@ export class SettingsPage extends BasePage {
|
||||
await this.page.goto('/settings');
|
||||
}
|
||||
|
||||
async goToPersonalSettings() {
|
||||
async goToPersonalSettings(): Promise<void> {
|
||||
await this.page.goto('/settings/personal');
|
||||
}
|
||||
|
||||
getPersonalDataForm() {
|
||||
getPersonalDataForm(): Locator {
|
||||
return this.page.getByTestId('personal-data-form');
|
||||
}
|
||||
|
||||
getFirstNameField() {
|
||||
getFirstNameField(): Locator {
|
||||
return this.getPersonalDataForm().locator('input[name="firstName"]');
|
||||
}
|
||||
|
||||
getLastNameField() {
|
||||
getLastNameField(): Locator {
|
||||
return this.getPersonalDataForm().locator('input[name="lastName"]');
|
||||
}
|
||||
|
||||
getSaveSettingsButton() {
|
||||
getEmailField(): Locator {
|
||||
return this.getPersonalDataForm().locator('input[name="email"]');
|
||||
}
|
||||
|
||||
getSaveSettingsButton(): Locator {
|
||||
return this.page.getByTestId('save-settings-button');
|
||||
}
|
||||
|
||||
async fillPersonalData(firstName: string, lastName: string) {
|
||||
async fillPersonalData(firstName: string, lastName: string): Promise<void> {
|
||||
await this.getFirstNameField().fill(firstName);
|
||||
await this.getLastNameField().fill(lastName);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
async fillEmail(email: string): Promise<void> {
|
||||
await this.getEmailField().fill(email);
|
||||
}
|
||||
|
||||
async pressEnterOnEmail(): Promise<void> {
|
||||
await this.getEmailField().press('Enter');
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
await this.getSaveSettingsButton().click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete workflow to update user's email address
|
||||
* @param newEmail - The new email address to set
|
||||
*/
|
||||
async updateEmail(newEmail: string): Promise<void> {
|
||||
await this.goToPersonalSettings();
|
||||
await this.fillEmail(newEmail);
|
||||
await this.saveSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete workflow to update user's first and last name
|
||||
* @param firstName - The new first name
|
||||
* @param lastName - The new last name
|
||||
*/
|
||||
async updateFirstAndLastName(firstName: string, lastName: string): Promise<void> {
|
||||
await this.goToPersonalSettings();
|
||||
await this.fillPersonalData(firstName, lastName);
|
||||
await this.saveSettings();
|
||||
}
|
||||
|
||||
getEnableMfaButton(): Locator {
|
||||
return this.page.getByTestId('enable-mfa-button');
|
||||
}
|
||||
|
||||
getDisableMfaButton(): Locator {
|
||||
return this.page.getByTestId('disable-mfa-button');
|
||||
}
|
||||
|
||||
getMfaCodeOrRecoveryCodeInput(): Locator {
|
||||
return this.page.locator('input[name="mfaCodeOrMfaRecoveryCode"]');
|
||||
}
|
||||
|
||||
getMfaSaveButton(): Locator {
|
||||
return this.page.getByTestId('mfa-save-button');
|
||||
}
|
||||
|
||||
async clickEnableMfa(): Promise<void> {
|
||||
await this.clickByTestId('enable-mfa-button');
|
||||
}
|
||||
|
||||
async clickDisableMfa(): Promise<void> {
|
||||
await this.getDisableMfaButton().click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to personal settings and initiate MFA disable workflow
|
||||
*/
|
||||
async triggerDisableMfa(): Promise<void> {
|
||||
await this.goToPersonalSettings();
|
||||
await this.clickDisableMfa();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill in MFA code or recovery code and save the form
|
||||
* @param code - MFA token or recovery code
|
||||
*/
|
||||
async fillMfaCodeAndSave(code: string): Promise<void> {
|
||||
await this.getMfaCodeOrRecoveryCodeInput().fill(code);
|
||||
await this.getMfaSaveButton().click();
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,10 @@ export class SidebarPage {
|
||||
await this.page.getByTestId('project-plus-button').click();
|
||||
}
|
||||
|
||||
async clickHomeButton() {
|
||||
await this.page.getByTestId('project-home-menu-item').click();
|
||||
}
|
||||
|
||||
async universalAdd() {
|
||||
await this.page.getByTestId('universal-add').click();
|
||||
}
|
||||
@ -44,6 +48,33 @@ export class SidebarPage {
|
||||
return this.page.getByTestId('add-first-project-button');
|
||||
}
|
||||
|
||||
getUserMenu(): Locator {
|
||||
return this.page.getByTestId('user-menu');
|
||||
}
|
||||
|
||||
getLogoutMenuItem(): Locator {
|
||||
return this.page.getByTestId('user-menu-item-logout');
|
||||
}
|
||||
|
||||
async openUserMenu(): Promise<void> {
|
||||
await this.getUserMenu().click();
|
||||
}
|
||||
|
||||
async clickSignout(): Promise<void> {
|
||||
await this.expand();
|
||||
await this.openUserMenu();
|
||||
await this.getLogoutMenuItem().click();
|
||||
}
|
||||
|
||||
async signOutFromWorkflows(): Promise<void> {
|
||||
await this.page.goto('/workflows');
|
||||
await this.clickSignout();
|
||||
}
|
||||
|
||||
async goToWorkflows(): Promise<void> {
|
||||
await this.page.goto('/workflows');
|
||||
}
|
||||
|
||||
async expand() {
|
||||
const collapseButton = this.page.locator('#collapse-change-button');
|
||||
const chevronRight = this.page.locator(
|
||||
|
||||
58
packages/testing/playwright/pages/SignInPage.ts
Normal file
58
packages/testing/playwright/pages/SignInPage.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class SignInPage extends BasePage {
|
||||
getForm(): Locator {
|
||||
return this.page.getByTestId('auth-form');
|
||||
}
|
||||
|
||||
getEmailField(): Locator {
|
||||
return this.page.getByTestId('emailOrLdapLoginId').locator('input');
|
||||
}
|
||||
|
||||
getPasswordField(): Locator {
|
||||
return this.page.getByTestId('password').locator('input');
|
||||
}
|
||||
|
||||
getSubmitButton(): Locator {
|
||||
return this.page.getByTestId('form-submit-button');
|
||||
}
|
||||
|
||||
async goToSignIn(): Promise<void> {
|
||||
await this.page.goto('/signin');
|
||||
}
|
||||
|
||||
async fillEmail(email: string): Promise<void> {
|
||||
await this.getEmailField().fill(email);
|
||||
}
|
||||
|
||||
async fillPassword(password: string): Promise<void> {
|
||||
await this.getPasswordField().fill(password);
|
||||
}
|
||||
|
||||
async clickSubmit(): Promise<void> {
|
||||
await this.getSubmitButton().click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete login flow with email and password
|
||||
* @param email - User email
|
||||
* @param password - User password
|
||||
* @param waitForWorkflow - Whether to wait for redirect to workflow page after login
|
||||
*/
|
||||
async loginWithEmailAndPassword(
|
||||
email: string,
|
||||
password: string,
|
||||
waitForWorkflow = false,
|
||||
): Promise<void> {
|
||||
await this.goToSignIn();
|
||||
await this.fillEmail(email);
|
||||
await this.fillPassword(password);
|
||||
await this.clickSubmit();
|
||||
|
||||
if (waitForWorkflow) {
|
||||
await this.page.waitForURL(/workflows/);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Save Changes Modal component for handling unsaved changes dialogs.
|
||||
* Appears when navigating away from workflow with unsaved changes.
|
||||
*/
|
||||
export class SaveChangesModal {
|
||||
constructor(private root: Locator) {}
|
||||
|
||||
getModal(): Locator {
|
||||
return this.root.filter({ hasText: 'Save changes before leaving?' });
|
||||
}
|
||||
|
||||
getCancelButton(): Locator {
|
||||
return this.root.locator('.btn--cancel');
|
||||
}
|
||||
|
||||
getCloseButton(): Locator {
|
||||
return this.root.locator('.el-message-box__headerbtn');
|
||||
}
|
||||
|
||||
getSaveButton(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Save' });
|
||||
}
|
||||
|
||||
async clickCancel(): Promise<void> {
|
||||
await this.getCancelButton().click();
|
||||
}
|
||||
|
||||
async clickClose(): Promise<void> {
|
||||
await this.getCloseButton().click();
|
||||
}
|
||||
|
||||
async clickSave(): Promise<void> {
|
||||
await this.getSaveButton().click();
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,8 @@ import { DemoPage } from './DemoPage';
|
||||
import { ExecutionsPage } from './ExecutionsPage';
|
||||
import { IframePage } from './IframePage';
|
||||
import { InteractionsPage } from './InteractionsPage';
|
||||
import { MfaLoginPage } from './MfaLoginPage';
|
||||
import { MfaSetupModal } from './MfaSetupModal';
|
||||
import { NodeDetailsViewPage } from './NodeDetailsViewPage';
|
||||
import { NotificationsPage } from './NotificationsPage';
|
||||
import { NpsSurveyPage } from './NpsSurveyPage';
|
||||
@ -16,6 +18,7 @@ import { ProjectSettingsPage } from './ProjectSettingsPage';
|
||||
import { SettingsLogStreamingPage } from './SettingsLogStreamingPage';
|
||||
import { SettingsPage } from './SettingsPage';
|
||||
import { SidebarPage } from './SidebarPage';
|
||||
import { SignInPage } from './SignInPage';
|
||||
import { VariablesPage } from './VariablesPage';
|
||||
import { VersionsPage } from './VersionsPage';
|
||||
import { WorkerViewPage } from './WorkerViewPage';
|
||||
@ -25,6 +28,7 @@ import { WorkflowSharingModal } from './WorkflowSharingModal';
|
||||
import { WorkflowsPage } from './WorkflowsPage';
|
||||
import { CanvasComposer } from '../composables/CanvasComposer';
|
||||
import { CredentialsComposer } from '../composables/CredentialsComposer';
|
||||
import { MfaComposer } from '../composables/MfaComposer';
|
||||
import { PartialExecutionComposer } from '../composables/PartialExecutionComposer';
|
||||
import { ProjectComposer } from '../composables/ProjectComposer';
|
||||
import { TestEntryComposer } from '../composables/TestEntryComposer';
|
||||
@ -45,6 +49,7 @@ export class n8nPage {
|
||||
readonly demo: DemoPage;
|
||||
readonly iframe: IframePage;
|
||||
readonly interactions: InteractionsPage;
|
||||
readonly mfaLogin: MfaLoginPage;
|
||||
readonly ndv: NodeDetailsViewPage;
|
||||
readonly npsSurvey: NpsSurveyPage;
|
||||
readonly projectSettings: ProjectSettingsPage;
|
||||
@ -58,17 +63,20 @@ export class n8nPage {
|
||||
readonly credentials: CredentialsPage;
|
||||
readonly executions: ExecutionsPage;
|
||||
readonly sideBar: SidebarPage;
|
||||
readonly signIn: SignInPage;
|
||||
|
||||
// Modals
|
||||
readonly workflowActivationModal: WorkflowActivationModal;
|
||||
readonly workflowSettingsModal: WorkflowSettingsModal;
|
||||
readonly workflowSharingModal: WorkflowSharingModal;
|
||||
readonly mfaSetupModal: MfaSetupModal;
|
||||
|
||||
// Composables
|
||||
readonly workflowComposer: WorkflowComposer;
|
||||
readonly projectComposer: ProjectComposer;
|
||||
readonly canvasComposer: CanvasComposer;
|
||||
readonly credentialsComposer: CredentialsComposer;
|
||||
readonly mfaComposer: MfaComposer;
|
||||
readonly partialExecutionComposer: PartialExecutionComposer;
|
||||
readonly start: TestEntryComposer;
|
||||
|
||||
@ -87,6 +95,7 @@ export class n8nPage {
|
||||
this.demo = new DemoPage(page);
|
||||
this.iframe = new IframePage(page);
|
||||
this.interactions = new InteractionsPage(page);
|
||||
this.mfaLogin = new MfaLoginPage(page);
|
||||
this.ndv = new NodeDetailsViewPage(page);
|
||||
this.npsSurvey = new NpsSurveyPage(page);
|
||||
this.projectSettings = new ProjectSettingsPage(page);
|
||||
@ -100,17 +109,20 @@ export class n8nPage {
|
||||
this.credentials = new CredentialsPage(page);
|
||||
this.executions = new ExecutionsPage(page);
|
||||
this.sideBar = new SidebarPage(page);
|
||||
this.signIn = new SignInPage(page);
|
||||
this.workflowSharingModal = new WorkflowSharingModal(page);
|
||||
|
||||
// Modals
|
||||
this.workflowActivationModal = new WorkflowActivationModal(page);
|
||||
this.workflowSettingsModal = new WorkflowSettingsModal(page);
|
||||
this.mfaSetupModal = new MfaSetupModal(page);
|
||||
|
||||
// Composables
|
||||
this.workflowComposer = new WorkflowComposer(this);
|
||||
this.projectComposer = new ProjectComposer(this);
|
||||
this.canvasComposer = new CanvasComposer(this);
|
||||
this.credentialsComposer = new CredentialsComposer(this);
|
||||
this.mfaComposer = new MfaComposer(this);
|
||||
this.partialExecutionComposer = new PartialExecutionComposer(this);
|
||||
this.start = new TestEntryComposer(this);
|
||||
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
import { authenticator } from 'otplib';
|
||||
|
||||
import { INSTANCE_OWNER_CREDENTIALS } from '../../config/test-users';
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
|
||||
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', () => {
|
||||
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.settings.triggerDisableMfa();
|
||||
await n8n.settings.fillMfaCodeAndSave(disableToken);
|
||||
|
||||
await expect(n8n.settings.getEnableMfaButton()).toBeVisible();
|
||||
});
|
||||
|
||||
test('Should prompt for MFA code when email changes', async ({ n8n }) => {
|
||||
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
|
||||
|
||||
await n8n.settings.goToPersonalSettings();
|
||||
await n8n.settings.fillEmail(TEST_DATA.NEW_EMAIL);
|
||||
await n8n.settings.pressEnterOnEmail();
|
||||
|
||||
const mfaCode = authenticator.generate(mfaSecret!);
|
||||
await n8n.settings.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.settings.goToPersonalSettings();
|
||||
await n8n.settings.fillEmail(TEST_DATA.NEW_EMAIL);
|
||||
await n8n.settings.pressEnterOnEmail();
|
||||
|
||||
await expect(n8n.settings.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.settings.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.settings.triggerDisableMfa();
|
||||
await n8n.settings.fillMfaCodeAndSave(RECOVERY_CODE);
|
||||
|
||||
await expect(n8n.settings.getEnableMfaButton()).toBeVisible();
|
||||
});
|
||||
});
|
||||
100
packages/testing/playwright/tests/ui/44-routing.spec.ts
Normal file
100
packages/testing/playwright/tests/ui/44-routing.spec.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { EDIT_FIELDS_SET_NODE_NAME } from '../../config/constants';
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
|
||||
test.describe('Routing @db:reset', () => {
|
||||
test('should ask to save unsaved changes before leaving route', async ({ n8n }) => {
|
||||
await n8n.goHome();
|
||||
await expect(n8n.workflows.getNewWorkflowCard()).toBeVisible();
|
||||
await n8n.workflows.clickNewWorkflowCard();
|
||||
|
||||
await n8n.canvas.importWorkflow('Test_workflow_1.json', 'Test Workflow');
|
||||
|
||||
await n8n.canvas.addNode(EDIT_FIELDS_SET_NODE_NAME, { closeNDV: true });
|
||||
|
||||
await n8n.sideBar.clickHomeButton();
|
||||
|
||||
await expect(n8n.page).toHaveURL(/workflow/);
|
||||
|
||||
await expect(n8n.canvas.saveChangesModal.getModal()).toBeVisible();
|
||||
await n8n.canvas.saveChangesModal.clickCancel();
|
||||
|
||||
await expect(n8n.page).toHaveURL(/home\/workflows/);
|
||||
});
|
||||
|
||||
test('should correct route after cancelling saveChangesModal', async ({ n8n }) => {
|
||||
await n8n.start.fromBlankCanvas();
|
||||
|
||||
await n8n.canvas.importWorkflow('Test_workflow_1.json', 'Test Workflow');
|
||||
|
||||
await n8n.canvas.addNode(EDIT_FIELDS_SET_NODE_NAME, { closeNDV: false });
|
||||
|
||||
await n8n.page.goBack();
|
||||
|
||||
await expect(n8n.page).toHaveURL(/home\/workflows/);
|
||||
|
||||
await expect(n8n.canvas.saveChangesModal.getModal()).toBeVisible();
|
||||
await n8n.canvas.saveChangesModal.clickClose();
|
||||
|
||||
await expect(n8n.page).toHaveURL(/workflow/);
|
||||
});
|
||||
|
||||
test('should correct route when opening and closing NDV', async ({ n8n }) => {
|
||||
await n8n.start.fromBlankCanvas();
|
||||
await n8n.canvas.clickSaveWorkflowButton();
|
||||
|
||||
await n8n.canvas.importWorkflow('Test_workflow_1.json', 'Test Workflow');
|
||||
|
||||
const baselineUrl = n8n.page.url();
|
||||
|
||||
await n8n.canvas.addNode(EDIT_FIELDS_SET_NODE_NAME, { closeNDV: false });
|
||||
|
||||
expect(n8n.page.url()).not.toBe(baselineUrl);
|
||||
|
||||
await n8n.page.keyboard.press('Escape');
|
||||
|
||||
expect(n8n.page.url()).toBe(baselineUrl);
|
||||
});
|
||||
|
||||
test('should open ndv via URL', async ({ n8n }) => {
|
||||
await n8n.start.fromBlankCanvas();
|
||||
await n8n.canvas.clickSaveWorkflowButton();
|
||||
|
||||
await n8n.canvas.importWorkflow('Test_workflow_1.json', 'Test Workflow');
|
||||
|
||||
await n8n.canvas.addNode(EDIT_FIELDS_SET_NODE_NAME, { closeNDV: false });
|
||||
const ndvUrl = n8n.page.url();
|
||||
|
||||
await n8n.page.keyboard.press('Escape');
|
||||
await n8n.canvas.clickSaveWorkflowButton();
|
||||
|
||||
await expect(n8n.ndv.getContainer()).toBeHidden();
|
||||
|
||||
await n8n.page.goto(ndvUrl);
|
||||
|
||||
await expect(n8n.ndv.getContainer()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should open show warning and drop nodeId from URL if it contained an unknown nodeId', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
await n8n.start.fromBlankCanvas();
|
||||
await n8n.canvas.clickSaveWorkflowButton();
|
||||
|
||||
await n8n.canvas.importWorkflow('Test_workflow_1.json', 'Test Workflow');
|
||||
|
||||
await n8n.canvas.addNode(EDIT_FIELDS_SET_NODE_NAME, { closeNDV: false });
|
||||
const ndvUrl = n8n.page.url();
|
||||
|
||||
await n8n.page.keyboard.press('Escape');
|
||||
await n8n.canvas.clickSaveWorkflowButton();
|
||||
|
||||
await expect(n8n.ndv.getContainer()).toBeHidden();
|
||||
|
||||
await n8n.page.goto(ndvUrl + 'thisMessesUpTheNodeId');
|
||||
|
||||
await expect(n8n.notifications.getWarningNotifications()).toBeVisible();
|
||||
|
||||
const urlWithoutNodeId = ndvUrl.split('/').slice(0, -1).join('/');
|
||||
expect(n8n.page.url()).toBe(urlWithoutNodeId);
|
||||
});
|
||||
});
|
||||
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@ -1044,7 +1044,7 @@ importers:
|
||||
version: 4.3.0
|
||||
'@getzep/zep-cloud':
|
||||
specifier: 1.0.12
|
||||
version: 1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.33(e94cf81b5fa4aa911673e0503f662b2e))
|
||||
version: 1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.33(f461b118585bdb288345da9017188aa6))
|
||||
'@getzep/zep-js':
|
||||
specifier: 0.9.0
|
||||
version: 0.9.0
|
||||
@ -1071,7 +1071,7 @@ importers:
|
||||
version: 0.3.4(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)
|
||||
'@langchain/community':
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.50(8ac6ecc2064042e5620199e694862b5d)
|
||||
version: 0.3.50(f853e1a1cbd27719f8eb2bfe941d126d)
|
||||
'@langchain/core':
|
||||
specifier: 'catalog:'
|
||||
version: 0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))
|
||||
@ -1194,7 +1194,7 @@ importers:
|
||||
version: 23.0.1(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||
langchain:
|
||||
specifier: 0.3.33
|
||||
version: 0.3.33(e94cf81b5fa4aa911673e0503f662b2e)
|
||||
version: 0.3.33(f461b118585bdb288345da9017188aa6)
|
||||
lodash:
|
||||
specifier: 'catalog:'
|
||||
version: 4.17.21
|
||||
@ -3228,6 +3228,9 @@ importers:
|
||||
nanoid:
|
||||
specifier: 'catalog:'
|
||||
version: 3.3.8
|
||||
otplib:
|
||||
specifier: ^12.0.1
|
||||
version: 12.0.1
|
||||
tsx:
|
||||
specifier: 'catalog:'
|
||||
version: 4.19.3
|
||||
@ -19285,7 +19288,7 @@ snapshots:
|
||||
'@gar/promisify@1.1.3':
|
||||
optional: true
|
||||
|
||||
'@getzep/zep-cloud@1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.33(e94cf81b5fa4aa911673e0503f662b2e))':
|
||||
'@getzep/zep-cloud@1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.33(f461b118585bdb288345da9017188aa6))':
|
||||
dependencies:
|
||||
form-data: 4.0.4
|
||||
node-fetch: 2.7.0(encoding@0.1.13)
|
||||
@ -19294,7 +19297,7 @@ snapshots:
|
||||
zod: 3.25.67
|
||||
optionalDependencies:
|
||||
'@langchain/core': 0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))
|
||||
langchain: 0.3.33(e94cf81b5fa4aa911673e0503f662b2e)
|
||||
langchain: 0.3.33(f461b118585bdb288345da9017188aa6)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
@ -19861,7 +19864,7 @@ snapshots:
|
||||
- aws-crt
|
||||
- encoding
|
||||
|
||||
'@langchain/community@0.3.50(8ac6ecc2064042e5620199e694862b5d)':
|
||||
'@langchain/community@0.3.50(f853e1a1cbd27719f8eb2bfe941d126d)':
|
||||
dependencies:
|
||||
'@browserbasehq/stagehand': 1.9.0(@playwright/test@1.54.2)(bufferutil@4.0.9)(deepmerge@4.3.1)(dotenv@16.6.1)(encoding@0.1.13)(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))(utf-8-validate@5.0.10)(zod@3.25.67)
|
||||
'@ibm-cloud/watsonx-ai': 1.1.2
|
||||
@ -19873,7 +19876,7 @@ snapshots:
|
||||
flat: 5.0.2
|
||||
ibm-cloud-sdk-core: 5.3.2
|
||||
js-yaml: 4.1.0
|
||||
langchain: 0.3.33(e94cf81b5fa4aa911673e0503f662b2e)
|
||||
langchain: 0.3.33(f461b118585bdb288345da9017188aa6)
|
||||
langsmith: 0.3.55(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))
|
||||
openai: 5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)
|
||||
uuid: 10.0.0
|
||||
@ -19887,7 +19890,7 @@ snapshots:
|
||||
'@aws-sdk/credential-provider-node': 3.808.0
|
||||
'@azure/storage-blob': 12.26.0
|
||||
'@browserbasehq/sdk': 2.6.0(encoding@0.1.13)
|
||||
'@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.33(e94cf81b5fa4aa911673e0503f662b2e))
|
||||
'@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(encoding@0.1.13)(langchain@0.3.33(f461b118585bdb288345da9017188aa6))
|
||||
'@getzep/zep-js': 0.9.0
|
||||
'@google-ai/generativelanguage': 3.4.0(encoding@0.1.13)
|
||||
'@google-cloud/storage': 7.12.1(encoding@0.1.13)
|
||||
@ -27020,7 +27023,7 @@ snapshots:
|
||||
'@types/debug': 4.1.12
|
||||
'@types/node': 20.19.11
|
||||
'@types/tough-cookie': 4.0.5
|
||||
axios: 1.12.0(debug@4.3.6)
|
||||
axios: 1.12.0(debug@4.4.1)
|
||||
camelcase: 6.3.0
|
||||
debug: 4.4.1(supports-color@8.1.1)
|
||||
dotenv: 16.6.1
|
||||
@ -27030,7 +27033,7 @@ snapshots:
|
||||
isstream: 0.1.2
|
||||
jsonwebtoken: 9.0.2
|
||||
mime-types: 2.1.35
|
||||
retry-axios: 2.6.0(axios@1.12.0(debug@4.4.1))
|
||||
retry-axios: 2.6.0(axios@1.12.0)
|
||||
tough-cookie: 4.1.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@ -28288,7 +28291,7 @@ snapshots:
|
||||
|
||||
kuler@2.0.0: {}
|
||||
|
||||
langchain@0.3.33(e94cf81b5fa4aa911673e0503f662b2e):
|
||||
langchain@0.3.33(f461b118585bdb288345da9017188aa6):
|
||||
dependencies:
|
||||
'@langchain/core': 0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67))
|
||||
'@langchain/openai': 0.6.7(@langchain/core@0.3.68(@opentelemetry/api@1.9.0)(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(openai@5.12.2(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@3.25.67)))(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10))
|
||||
@ -31101,7 +31104,7 @@ snapshots:
|
||||
onetime: 5.1.2
|
||||
signal-exit: 3.0.7
|
||||
|
||||
retry-axios@2.6.0(axios@1.12.0(debug@4.4.1)):
|
||||
retry-axios@2.6.0(axios@1.12.0):
|
||||
dependencies:
|
||||
axios: 1.12.0(debug@4.3.6)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user