mirror of
https://github.com/n8n-io/n8n.git
synced 2025-11-20 17:46:34 +00:00
test: Migrate user management tests to Playwright (#20210)
This commit is contained in:
parent
3d30f959c2
commit
96aba18605
2
.github/workflows/e2e-reusable.yml
vendored
2
.github/workflows/e2e-reusable.yml
vendored
@ -25,7 +25,7 @@ on:
|
||||
containers:
|
||||
description: 'Number of containers to run tests in.'
|
||||
required: false
|
||||
default: '[1, 2, 3, 4, 5, 6]'
|
||||
default: '[1, 2, 3, 4, 5]'
|
||||
type: string
|
||||
pr_number:
|
||||
description: 'PR number to run tests for.'
|
||||
|
||||
@ -16,7 +16,7 @@ on:
|
||||
shards:
|
||||
description: 'Shards for parallel execution'
|
||||
required: false
|
||||
default: '[1, 2, 3, 4, 5, 6]'
|
||||
default: '[1, 2, 3, 4, 5, 6, 7]'
|
||||
type: string
|
||||
docker-image:
|
||||
description: 'Docker image to use (for docker-pull mode)'
|
||||
@ -52,7 +52,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: ${{ fromJSON(inputs.shards || '[1, 2, 3, 4, 5, 6]') }}
|
||||
shard: ${{ fromJSON(inputs.shards || '[1, 2, 3, 4, 5, 6, 7]') }}
|
||||
name: Test (Shard ${{ matrix.shard }}/${{ strategy.job-total }})
|
||||
|
||||
steps:
|
||||
|
||||
@ -1,251 +0,0 @@
|
||||
import { expandSidebar } from '../../composables/sidebar';
|
||||
import { INSTANCE_MEMBERS, INSTANCE_OWNER, INSTANCE_ADMIN } from '../../constants';
|
||||
import { MainSidebar, SettingsSidebar, SettingsUsersPage } from '../../pages';
|
||||
import { errorToast, successToast } from '../../pages/notifications';
|
||||
import { PersonalSettingsPage } from '../../pages/settings-personal';
|
||||
import { getVisiblePopper } from '../../utils';
|
||||
|
||||
/**
|
||||
* User A - Instance owner
|
||||
* User B - User, owns C1, W1, W2
|
||||
* User C - User, owns C2
|
||||
*
|
||||
* W1 - Workflow owned by User B, shared with User C
|
||||
* W2 - Workflow owned by User B
|
||||
*
|
||||
* C1 - Credential owned by User B
|
||||
* C2 - Credential owned by User C, shared with User A and User B
|
||||
*/
|
||||
|
||||
const updatedPersonalData = {
|
||||
newFirstName: 'Something',
|
||||
newLastName: 'Else',
|
||||
newEmail: 'something_else@acme.corp',
|
||||
newPassword: 'Keybo4rd',
|
||||
invalidPasswords: ['abc', 'longEnough', 'longenough123'],
|
||||
};
|
||||
|
||||
const usersSettingsPage = new SettingsUsersPage();
|
||||
const personalSettingsPage = new PersonalSettingsPage();
|
||||
const settingsSidebar = new SettingsSidebar();
|
||||
const mainSidebar = new MainSidebar();
|
||||
|
||||
describe('User Management', { disableAutoLogin: true }, () => {
|
||||
before(() => {
|
||||
cy.enableFeature('sharing');
|
||||
});
|
||||
|
||||
it('should login and logout', () => {
|
||||
cy.visit('/');
|
||||
cy.get('input[name="emailOrLdapLoginId"]').type(INSTANCE_OWNER.email);
|
||||
cy.get('input[name="password"]').type(INSTANCE_OWNER.password);
|
||||
cy.getByTestId('form-submit-button').click();
|
||||
mainSidebar.getters.logo().should('be.visible');
|
||||
expandSidebar();
|
||||
mainSidebar.actions.goToSettings();
|
||||
settingsSidebar.getters.users().should('be.visible');
|
||||
|
||||
mainSidebar.actions.closeSettings();
|
||||
mainSidebar.actions.openUserMenu();
|
||||
cy.getByTestId('user-menu-item-logout').click();
|
||||
|
||||
cy.get('input[name="emailOrLdapLoginId"]').type(INSTANCE_MEMBERS[0].email);
|
||||
cy.get('input[name="password"]').type(INSTANCE_MEMBERS[0].password);
|
||||
cy.getByTestId('form-submit-button').click();
|
||||
mainSidebar.getters.logo().should('be.visible');
|
||||
mainSidebar.actions.goToSettings();
|
||||
cy.getByTestId('menu-item').filter('#settings-users').should('not.exist');
|
||||
});
|
||||
|
||||
it('should prevent non-owners to access UM settings', () => {
|
||||
usersSettingsPage.actions.loginAndVisit(
|
||||
INSTANCE_MEMBERS[0].email,
|
||||
INSTANCE_MEMBERS[0].password,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow instance owner to access UM settings', () => {
|
||||
usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
|
||||
});
|
||||
|
||||
it('should properly render UM settings page for instance owners', () => {
|
||||
usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
|
||||
// All items in user list should be there
|
||||
usersSettingsPage.getters.userListItems().should('have.length', 4);
|
||||
// List item for current user should have the `Owner` badge
|
||||
usersSettingsPage.getters
|
||||
.userItem(INSTANCE_OWNER.email)
|
||||
.find('td:contains("Owner")')
|
||||
.should('be.visible');
|
||||
// Other users list items should contain action pop-up list
|
||||
usersSettingsPage.getters.userActionsToggle(INSTANCE_MEMBERS[0].email).should('exist');
|
||||
usersSettingsPage.getters.userActionsToggle(INSTANCE_MEMBERS[1].email).should('exist');
|
||||
usersSettingsPage.getters.userActionsToggle(INSTANCE_ADMIN.email).should('exist');
|
||||
});
|
||||
|
||||
it('should be able to change user role to Admin and back', () => {
|
||||
cy.enableFeature('advancedPermissions');
|
||||
|
||||
usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
|
||||
|
||||
// Change role from Member to Admin
|
||||
usersSettingsPage.getters
|
||||
.userRoleSelect(INSTANCE_MEMBERS[0].email)
|
||||
.find('button:contains("Member")')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
getVisiblePopper().find('label').contains('Admin').click();
|
||||
usersSettingsPage.getters
|
||||
.userRoleSelect(INSTANCE_MEMBERS[0].email)
|
||||
.find('button:contains("Admin")')
|
||||
.should('be.visible');
|
||||
|
||||
usersSettingsPage.actions.loginAndVisit(
|
||||
INSTANCE_MEMBERS[0].email,
|
||||
INSTANCE_MEMBERS[0].password,
|
||||
true,
|
||||
);
|
||||
|
||||
// Change role from Admin to Member, then back to Admin
|
||||
usersSettingsPage.getters
|
||||
.userRoleSelect(INSTANCE_ADMIN.email)
|
||||
.find('button:contains("Admin")')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
getVisiblePopper().find('label').contains('Member').click();
|
||||
usersSettingsPage.getters
|
||||
.userRoleSelect(INSTANCE_ADMIN.email)
|
||||
.find('button:contains("Member")')
|
||||
.should('be.visible');
|
||||
|
||||
usersSettingsPage.actions.loginAndVisit(INSTANCE_ADMIN.email, INSTANCE_ADMIN.password, false);
|
||||
usersSettingsPage.actions.loginAndVisit(
|
||||
INSTANCE_MEMBERS[0].email,
|
||||
INSTANCE_MEMBERS[0].password,
|
||||
true,
|
||||
);
|
||||
|
||||
usersSettingsPage.getters
|
||||
.userRoleSelect(INSTANCE_ADMIN.email)
|
||||
.find('button:contains("Member")')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
getVisiblePopper().find('label').contains('Admin').click();
|
||||
usersSettingsPage.getters
|
||||
.userRoleSelect(INSTANCE_ADMIN.email)
|
||||
.find('button:contains("Admin")')
|
||||
.should('be.visible');
|
||||
|
||||
usersSettingsPage.actions.loginAndVisit(INSTANCE_ADMIN.email, INSTANCE_ADMIN.password, true);
|
||||
usersSettingsPage.getters
|
||||
.userRoleSelect(INSTANCE_MEMBERS[0].email)
|
||||
.find('button:contains("Admin")')
|
||||
.should('be.visible')
|
||||
.click();
|
||||
getVisiblePopper().find('label').contains('Member').click();
|
||||
usersSettingsPage.getters
|
||||
.userRoleSelect(INSTANCE_MEMBERS[0].email)
|
||||
.find('button:contains("Member")')
|
||||
.should('be.visible');
|
||||
|
||||
cy.disableFeature('advancedPermissions');
|
||||
});
|
||||
|
||||
it('should be able to change theme', () => {
|
||||
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
|
||||
|
||||
personalSettingsPage.actions.changeTheme('Dark');
|
||||
cy.get('body').should('have.attr', 'data-theme', 'dark');
|
||||
|
||||
personalSettingsPage.actions.changeTheme('Light');
|
||||
cy.get('body').should('have.attr', 'data-theme', 'light');
|
||||
});
|
||||
|
||||
it('should delete user and their data', () => {
|
||||
usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
|
||||
usersSettingsPage.actions.opedDeleteDialog(INSTANCE_MEMBERS[0].email);
|
||||
usersSettingsPage.getters.deleteDataRadioButton().click();
|
||||
usersSettingsPage.getters.deleteDataInput().type('delete all data');
|
||||
usersSettingsPage.getters.deleteUserButton().click();
|
||||
successToast().should('contain', 'User deleted');
|
||||
});
|
||||
|
||||
it('should delete user and transfer their data', () => {
|
||||
usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true);
|
||||
usersSettingsPage.actions.opedDeleteDialog(INSTANCE_MEMBERS[1].email);
|
||||
usersSettingsPage.getters.transferDataRadioButton().click();
|
||||
usersSettingsPage.getters.userSelectDropDown().click();
|
||||
usersSettingsPage.getters.userSelectOptions().first().click();
|
||||
usersSettingsPage.getters.deleteUserButton().click();
|
||||
successToast().should('contain', 'User deleted');
|
||||
});
|
||||
|
||||
it('should allow user to change their personal data', () => {
|
||||
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
|
||||
personalSettingsPage.actions.updateFirstAndLastName(
|
||||
updatedPersonalData.newFirstName,
|
||||
updatedPersonalData.newLastName,
|
||||
);
|
||||
personalSettingsPage.getters
|
||||
.currentUserName()
|
||||
.should('contain', `${updatedPersonalData.newFirstName} ${updatedPersonalData.newLastName}`);
|
||||
successToast().should('contain', 'Personal details updated');
|
||||
});
|
||||
|
||||
it("shouldn't allow user to set weak password", () => {
|
||||
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
|
||||
personalSettingsPage.getters.changePasswordLink().click();
|
||||
for (const weakPass of updatedPersonalData.invalidPasswords) {
|
||||
personalSettingsPage.actions.tryToSetWeakPassword(INSTANCE_OWNER.password, weakPass);
|
||||
}
|
||||
});
|
||||
|
||||
it("shouldn't allow user to change password if old password is wrong", () => {
|
||||
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
|
||||
personalSettingsPage.getters.changePasswordLink().click();
|
||||
personalSettingsPage.actions.updatePassword('iCannotRemember', updatedPersonalData.newPassword);
|
||||
errorToast().closest('div').should('contain', 'Provided current password is incorrect.');
|
||||
});
|
||||
|
||||
it('should change current user password', () => {
|
||||
personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password);
|
||||
personalSettingsPage.getters.changePasswordLink().click();
|
||||
personalSettingsPage.actions.updatePassword(
|
||||
INSTANCE_OWNER.password,
|
||||
updatedPersonalData.newPassword,
|
||||
);
|
||||
successToast().should('contain', 'Password updated');
|
||||
personalSettingsPage.actions.loginWithNewData(
|
||||
INSTANCE_OWNER.email,
|
||||
updatedPersonalData.newPassword,
|
||||
);
|
||||
});
|
||||
|
||||
it("shouldn't allow users to set invalid email", () => {
|
||||
personalSettingsPage.actions.loginAndVisit(
|
||||
INSTANCE_OWNER.email,
|
||||
updatedPersonalData.newPassword,
|
||||
);
|
||||
// try without @ part
|
||||
personalSettingsPage.actions.tryToSetInvalidEmail(updatedPersonalData.newEmail.split('@')[0]);
|
||||
// try without domain
|
||||
personalSettingsPage.actions.tryToSetInvalidEmail(updatedPersonalData.newEmail.split('.')[0]);
|
||||
});
|
||||
|
||||
it('should change user email', () => {
|
||||
personalSettingsPage.actions.loginAndVisit(
|
||||
INSTANCE_OWNER.email,
|
||||
updatedPersonalData.newPassword,
|
||||
);
|
||||
personalSettingsPage.actions.updateEmail(
|
||||
updatedPersonalData.newEmail,
|
||||
updatedPersonalData.newPassword,
|
||||
);
|
||||
successToast().should('contain', 'Personal details updated');
|
||||
personalSettingsPage.actions.loginWithNewData(
|
||||
updatedPersonalData.newEmail,
|
||||
updatedPersonalData.newPassword,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -14,9 +14,9 @@ export class MfaComposer {
|
||||
*/
|
||||
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.settingsPersonal.goToPersonalSettings();
|
||||
|
||||
await this.n8n.settings.clickEnableMfa();
|
||||
await this.n8n.settingsPersonal.clickEnableMfa();
|
||||
|
||||
await this.n8n.mfaSetupModal.getModalContainer().waitFor({ state: 'visible' });
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import type { n8nPage } from '../pages/n8nPage';
|
||||
import type { TestUser } from '../services/user-api-helper';
|
||||
|
||||
/**
|
||||
* Composer for UI test entry points. All methods in this class navigate to or verify UI state.
|
||||
@ -89,4 +90,18 @@ export class TestEntryComposer {
|
||||
await this.n8n.api.enableFeature('projectRole:editor');
|
||||
await this.n8n.api.setMaxTeamProjectsQuota(-1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new isolated user context with fresh page and authentication
|
||||
* @param user - User with email and password
|
||||
* @returns Fresh n8nPage instance with user authentication
|
||||
*/
|
||||
async withUser(user: Pick<TestUser, 'email' | 'password'>): Promise<n8nPage> {
|
||||
const browser = this.n8n.page.context().browser()!;
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
const newN8n = new (this.n8n.constructor as new (page: Page) => n8nPage)(page);
|
||||
await newN8n.api.login({ email: user.email, password: user.password });
|
||||
return newN8n;
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,11 @@ 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 {
|
||||
export class SettingsPersonalPage extends BasePage {
|
||||
getChangePasswordLink(): Locator {
|
||||
return this.page.getByTestId('change-password-link');
|
||||
}
|
||||
|
||||
getMenuItems() {
|
||||
return this.page.getByTestId('menu-item');
|
||||
}
|
||||
@ -22,6 +26,10 @@ export class SettingsPage extends BasePage {
|
||||
await this.page.goto('/settings');
|
||||
}
|
||||
|
||||
getUserRole(): Locator {
|
||||
return this.page.getByTestId('current-user-role');
|
||||
}
|
||||
|
||||
async goToPersonalSettings(): Promise<void> {
|
||||
await this.page.goto('/settings/personal');
|
||||
}
|
||||
@ -128,4 +136,34 @@ export class SettingsPage extends BasePage {
|
||||
getUpgradeCta(): Locator {
|
||||
return this.page.getByTestId('public-api-upgrade-cta');
|
||||
}
|
||||
|
||||
async changeTheme(theme: 'System default' | 'Light theme' | 'Dark theme') {
|
||||
await this.page.getByTestId('theme-select').click();
|
||||
await this.page.getByRole('option', { name: theme }).click();
|
||||
await this.getSaveSettingsButton().click();
|
||||
}
|
||||
|
||||
currentPassword(): Locator {
|
||||
return this.page.locator('input[name="currentPassword"]');
|
||||
}
|
||||
|
||||
newPassword(): Locator {
|
||||
return this.page.locator('input[name="password"]');
|
||||
}
|
||||
|
||||
repeatPassword(): Locator {
|
||||
return this.page.locator('input[name="password2"]');
|
||||
}
|
||||
|
||||
changePasswordModal(): Locator {
|
||||
return this.page.getByTestId('changePassword-modal');
|
||||
}
|
||||
|
||||
changePasswordButton(): Locator {
|
||||
return this.changePasswordModal().getByRole('button', { name: 'Change password' });
|
||||
}
|
||||
|
||||
emailBox(): Locator {
|
||||
return this.page.getByTestId('email');
|
||||
}
|
||||
}
|
||||
72
packages/testing/playwright/pages/SettingsUsersPage.ts
Normal file
72
packages/testing/playwright/pages/SettingsUsersPage.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class SettingsUsersPage extends BasePage {
|
||||
getSearchInput(): Locator {
|
||||
return this.page.getByTestId('users-list-search');
|
||||
}
|
||||
|
||||
getRow(email: string): Locator {
|
||||
return this.page.getByRole('row', { name: email });
|
||||
}
|
||||
|
||||
getAccountType(email: string) {
|
||||
return this.getRow(email).getByTestId('user-role-dropdown');
|
||||
}
|
||||
|
||||
clickAccountType(email: string) {
|
||||
return this.getRow(email).getByTestId('user-role-dropdown').getByRole('button').click();
|
||||
}
|
||||
|
||||
async search(email: string) {
|
||||
const searchInput = this.getSearchInput();
|
||||
await searchInput.click();
|
||||
await searchInput.fill(email);
|
||||
}
|
||||
|
||||
async clickTransferUser(email: string) {
|
||||
await this.openActions(email);
|
||||
await this.page.getByTestId('action-transfer').click();
|
||||
}
|
||||
|
||||
async transferData(email: string) {
|
||||
await this.page
|
||||
.getByRole('radio', {
|
||||
name: 'Transfer their workflows and credentials to another user or project',
|
||||
})
|
||||
// This doesn't work without force: true
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
.click({ force: true });
|
||||
|
||||
await this.page.getByPlaceholder('Select project or user').click();
|
||||
await this.page.getByTestId('project-sharing-info').filter({ hasText: email }).click();
|
||||
await this.page.getByRole('button', { name: 'Delete' }).click();
|
||||
}
|
||||
|
||||
async deleteData() {
|
||||
await this.page
|
||||
.getByRole('radio', {
|
||||
name: 'Delete their workflows and credentials',
|
||||
})
|
||||
// This doesn't work without force: true
|
||||
// eslint-disable-next-line playwright/no-force-option
|
||||
.check({ force: true });
|
||||
await this.page.getByPlaceholder('delete all data').fill('delete all data');
|
||||
await this.page.getByRole('button', { name: 'Delete' }).click();
|
||||
}
|
||||
|
||||
async selectAccountType(email: string, type: 'Admin' | 'Member') {
|
||||
await this.clickAccountType(email);
|
||||
await this.page.getByRole('menuitem', { name: type }).click();
|
||||
}
|
||||
|
||||
async openActions(email: string) {
|
||||
await this.getRow(email).getByTestId('action-toggle').click();
|
||||
}
|
||||
|
||||
async clickDeleteUser(email: string) {
|
||||
await this.openActions(email);
|
||||
await this.page.getByTestId('action-delete').filter({ visible: true }).click();
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,7 @@ import { NotificationsPage } from './NotificationsPage';
|
||||
import { NpsSurveyPage } from './NpsSurveyPage';
|
||||
import { ProjectSettingsPage } from './ProjectSettingsPage';
|
||||
import { SettingsLogStreamingPage } from './SettingsLogStreamingPage';
|
||||
import { SettingsPage } from './SettingsPage';
|
||||
import { SettingsPersonalPage } from './SettingsPersonalPage';
|
||||
import { SidebarPage } from './SidebarPage';
|
||||
import { SignInPage } from './SignInPage';
|
||||
import { VariablesPage } from './VariablesPage';
|
||||
@ -38,6 +38,7 @@ import { NavigationHelper } from '../helpers/NavigationHelper';
|
||||
import { ApiHelpers } from '../services/api-helper';
|
||||
import { BaseModal } from './components/BaseModal';
|
||||
import { Breadcrumbs } from './components/Breadcrumbs';
|
||||
import { SettingsUsersPage } from './SettingsUsersPage';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export class n8nPage {
|
||||
@ -56,7 +57,7 @@ export class n8nPage {
|
||||
readonly ndv: NodeDetailsViewPage;
|
||||
readonly npsSurvey: NpsSurveyPage;
|
||||
readonly projectSettings: ProjectSettingsPage;
|
||||
readonly settings: SettingsPage;
|
||||
readonly settingsPersonal: SettingsPersonalPage;
|
||||
readonly settingsLogStreaming: SettingsLogStreamingPage;
|
||||
readonly variables: VariablesPage;
|
||||
readonly versions: VersionsPage;
|
||||
@ -67,7 +68,7 @@ export class n8nPage {
|
||||
readonly executions: ExecutionsPage;
|
||||
readonly sideBar: SidebarPage;
|
||||
readonly signIn: SignInPage;
|
||||
|
||||
readonly settingsUsers: SettingsUsersPage;
|
||||
// Modals
|
||||
readonly workflowActivationModal: WorkflowActivationModal;
|
||||
readonly workflowSettingsModal: WorkflowSettingsModal;
|
||||
@ -105,7 +106,7 @@ export class n8nPage {
|
||||
this.ndv = new NodeDetailsViewPage(page);
|
||||
this.npsSurvey = new NpsSurveyPage(page);
|
||||
this.projectSettings = new ProjectSettingsPage(page);
|
||||
this.settings = new SettingsPage(page);
|
||||
this.settingsPersonal = new SettingsPersonalPage(page);
|
||||
this.settingsLogStreaming = new SettingsLogStreamingPage(page);
|
||||
this.variables = new VariablesPage(page);
|
||||
this.versions = new VersionsPage(page);
|
||||
@ -117,6 +118,7 @@ export class n8nPage {
|
||||
this.sideBar = new SidebarPage(page);
|
||||
this.signIn = new SignInPage(page);
|
||||
this.workflowSharingModal = new WorkflowSharingModal(page);
|
||||
this.settingsUsers = new SettingsUsersPage(page);
|
||||
// Modals
|
||||
this.workflowActivationModal = new WorkflowActivationModal(page);
|
||||
this.workflowSettingsModal = new WorkflowSettingsModal(page);
|
||||
|
||||
@ -11,6 +11,7 @@ import {
|
||||
import { TestError } from '../Types';
|
||||
import { CredentialApiHelper } from './credential-api-helper';
|
||||
import { ProjectApiHelper } from './project-api-helper';
|
||||
import { UserApiHelper } from './user-api-helper';
|
||||
import { VariablesApiHelper } from './variables-api-helper';
|
||||
import { WorkflowApiHelper } from './workflow-api-helper';
|
||||
|
||||
@ -39,6 +40,7 @@ export class ApiHelpers {
|
||||
projects: ProjectApiHelper;
|
||||
credentials: CredentialApiHelper;
|
||||
variables: VariablesApiHelper;
|
||||
users: UserApiHelper;
|
||||
|
||||
constructor(requestContext: APIRequestContext) {
|
||||
this.request = requestContext;
|
||||
@ -46,6 +48,7 @@ export class ApiHelpers {
|
||||
this.projects = new ProjectApiHelper(this);
|
||||
this.credentials = new CredentialApiHelper(this);
|
||||
this.variables = new VariablesApiHelper(this);
|
||||
this.users = new UserApiHelper(this);
|
||||
}
|
||||
|
||||
// ===== MAIN SETUP METHODS =====
|
||||
@ -136,6 +139,10 @@ export class ApiHelpers {
|
||||
return await this.loginAndSetCookies(credentials);
|
||||
}
|
||||
|
||||
async login(credentials: { email: string; password: string }): Promise<LoginResponseData> {
|
||||
return await this.loginAndSetCookies(credentials);
|
||||
}
|
||||
|
||||
// ===== CONFIGURATION METHODS =====
|
||||
|
||||
async setFeature(feature: string, enabled: boolean): Promise<void> {
|
||||
|
||||
71
packages/testing/playwright/services/user-api-helper.ts
Normal file
71
packages/testing/playwright/services/user-api-helper.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
import type { ApiHelpers } from './api-helper';
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789', 8);
|
||||
|
||||
export interface TestUser {
|
||||
id: string;
|
||||
email: string;
|
||||
password: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
role: 'global:owner' | 'global:admin' | 'global:member';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates test users via n8n's invitation API.
|
||||
* Note: Using this with n8n.api will affect browser cookies. Use with the isolated api fixture instead unless you want to overwrite the existing user
|
||||
*/
|
||||
export class UserApiHelper {
|
||||
constructor(private api: ApiHelpers) {}
|
||||
|
||||
/**
|
||||
* Create and activate a test user
|
||||
*/
|
||||
async create(options: Partial<TestUser> = {}): Promise<TestUser> {
|
||||
const user = {
|
||||
email: options.email?.toLowerCase() ?? `testuser${nanoid()}@test.com`,
|
||||
password: options.password ?? 'PlaywrightTest123',
|
||||
firstName: options.firstName ?? 'Test',
|
||||
lastName: options.lastName ?? `User${nanoid()}`,
|
||||
role: options.role ?? 'global:member',
|
||||
};
|
||||
|
||||
// Invite user
|
||||
const inviteResponse = await this.api.request.post('/rest/invitations', {
|
||||
data: [{ email: user.email, role: user.role }],
|
||||
});
|
||||
if (!inviteResponse.ok()) {
|
||||
throw new Error(`Failed to invite user: ${inviteResponse.status()}`);
|
||||
}
|
||||
const inviteData = await inviteResponse.json();
|
||||
const { id, inviteAcceptUrl } = inviteData.data[0].user;
|
||||
|
||||
// Accept invitation
|
||||
const url = new URL(inviteAcceptUrl);
|
||||
const inviterId = url.searchParams.get('inviterId');
|
||||
const inviteeId = url.searchParams.get('inviteeId');
|
||||
|
||||
const acceptResponse = await this.api.request.post(`/rest/invitations/${inviteeId}/accept`, {
|
||||
data: {
|
||||
inviterId,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
password: user.password,
|
||||
},
|
||||
});
|
||||
if (!acceptResponse.ok()) {
|
||||
throw new Error(`Failed to accept invitation: ${acceptResponse.status()}`);
|
||||
}
|
||||
|
||||
return { id, ...user };
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a user
|
||||
*/
|
||||
async delete(userId: string): Promise<void> {
|
||||
await this.api.request.delete(`/rest/users/${userId}`);
|
||||
}
|
||||
}
|
||||
158
packages/testing/playwright/tests/ui/18-user-management.spec.ts
Normal file
158
packages/testing/playwright/tests/ui/18-user-management.spec.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
import { INSTANCE_OWNER_CREDENTIALS } from '../../config/test-users';
|
||||
import { test, expect } from '../../fixtures/base';
|
||||
|
||||
test.describe('User Management', () => {
|
||||
test('should login and logout @auth:none', async ({ n8n }) => {
|
||||
await n8n.goHome();
|
||||
await n8n.signIn.goToSignIn();
|
||||
await n8n.signIn.loginWithEmailAndPassword(
|
||||
INSTANCE_OWNER_CREDENTIALS.email,
|
||||
INSTANCE_OWNER_CREDENTIALS.password,
|
||||
);
|
||||
await expect(n8n.workflows.getProjectName()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should prevent non-owners to access UM settings', async ({ n8n }) => {
|
||||
// This creates a new user in the same context, so the cookies are refreshed and owner is no longer logged in
|
||||
await n8n.api.users.create();
|
||||
await n8n.navigate.toUsers();
|
||||
await expect(n8n.workflows.getProjectName()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should allow instance owner to access UM settings', async ({ n8n }) => {
|
||||
await n8n.navigate.toUsers();
|
||||
expect(n8n.page.url()).toContain('/settings/users');
|
||||
});
|
||||
|
||||
test('should be able to change user role to Admin and back', async ({ n8n, api }) => {
|
||||
const user = await api.users.create();
|
||||
await n8n.navigate.toUsers();
|
||||
await n8n.settingsUsers.search(user.email);
|
||||
await n8n.settingsUsers.selectAccountType(user.email, 'Admin');
|
||||
await expect(n8n.settingsUsers.getAccountType(user.email)).toHaveText('Admin');
|
||||
await n8n.settingsUsers.selectAccountType(user.email, 'Member');
|
||||
await expect(n8n.settingsUsers.getAccountType(user.email)).toHaveText('Member');
|
||||
});
|
||||
|
||||
test('should be able to change theme', async ({ n8n }) => {
|
||||
await n8n.navigate.toPersonalSettings();
|
||||
await n8n.settingsPersonal.changeTheme('Dark theme');
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Personal details updated'),
|
||||
).toBeVisible();
|
||||
await expect(n8n.page.locator('body')).toHaveAttribute('data-theme', 'dark');
|
||||
});
|
||||
|
||||
test('should delete user and their data', async ({ n8n, api }) => {
|
||||
const user = await api.users.create();
|
||||
await n8n.navigate.toUsers();
|
||||
await n8n.page.reload();
|
||||
|
||||
await n8n.settingsUsers.search(user.email);
|
||||
await expect(n8n.settingsUsers.getRow(user.email)).toBeVisible();
|
||||
|
||||
await n8n.settingsUsers.clickDeleteUser(user.email);
|
||||
await n8n.settingsUsers.deleteData();
|
||||
await expect(n8n.notifications.getNotificationByTitleOrContent('User deleted')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should delete user and transfer their data', async ({ n8n, api }) => {
|
||||
const ownerEmail = INSTANCE_OWNER_CREDENTIALS.email;
|
||||
const user = await api.users.create();
|
||||
await n8n.navigate.toUsers();
|
||||
await n8n.page.reload();
|
||||
|
||||
await n8n.settingsUsers.search(user.email);
|
||||
await n8n.settingsUsers.getRow(user.email).isVisible();
|
||||
|
||||
await n8n.settingsUsers.clickDeleteUser(user.email);
|
||||
await n8n.settingsUsers.transferData(ownerEmail);
|
||||
await expect(n8n.notifications.getNotificationByTitleOrContent('User deleted')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should allow user to change their personal data', async ({ n8n }) => {
|
||||
await n8n.api.users.create();
|
||||
await n8n.navigate.toPersonalSettings();
|
||||
|
||||
await n8n.settingsPersonal.fillPersonalData('Something', 'Else');
|
||||
await n8n.settingsPersonal.saveSettings();
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Personal details updated'),
|
||||
).toBeVisible();
|
||||
|
||||
await n8n.page.reload();
|
||||
await expect(n8n.settingsPersonal.getFirstNameField()).toHaveValue('Something');
|
||||
await expect(n8n.settingsPersonal.getLastNameField()).toHaveValue('Else');
|
||||
});
|
||||
|
||||
test("shouldn't allow user to set weak password", async ({ n8n }) => {
|
||||
const user = await n8n.api.users.create();
|
||||
await n8n.navigate.toPersonalSettings();
|
||||
await n8n.settingsPersonal.getChangePasswordLink().click();
|
||||
|
||||
await n8n.settingsPersonal.currentPassword().fill(user.password);
|
||||
await n8n.settingsPersonal.newPassword().fill('abc');
|
||||
await n8n.settingsPersonal.repeatPassword().fill('abc');
|
||||
await expect(
|
||||
n8n.settingsPersonal
|
||||
.changePasswordModal()
|
||||
.getByText('8+ characters, at least 1 number and 1 capital letter'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("shouldn't allow user to change password if old password is wrong", async ({ n8n }) => {
|
||||
await n8n.navigate.toPersonalSettings();
|
||||
await n8n.settingsPersonal.getChangePasswordLink().click();
|
||||
await n8n.settingsPersonal.currentPassword().fill('wrong');
|
||||
await n8n.settingsPersonal.newPassword().fill('Keybo4rd');
|
||||
await n8n.settingsPersonal.repeatPassword().fill('Keybo4rd');
|
||||
await n8n.settingsPersonal.changePasswordButton().click();
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Provided current password is incorrect.'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('should change current user password', async ({ n8n }) => {
|
||||
const user = await n8n.api.users.create();
|
||||
await n8n.navigate.toPersonalSettings();
|
||||
await n8n.settingsPersonal.getChangePasswordLink().click();
|
||||
await n8n.settingsPersonal.currentPassword().fill(user.password);
|
||||
await n8n.settingsPersonal.newPassword().fill('Keybo4rd');
|
||||
await n8n.settingsPersonal.repeatPassword().fill('Keybo4rd');
|
||||
await n8n.settingsPersonal.changePasswordButton().click();
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Password updated'),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("shouldn't allow users to set invalid email", async ({ n8n }) => {
|
||||
await n8n.api.users.create();
|
||||
await n8n.navigate.toPersonalSettings();
|
||||
await n8n.settingsPersonal.fillEmail('something_else');
|
||||
await expect(n8n.settingsPersonal.getSaveSettingsButton()).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should change user email', async ({ n8n }) => {
|
||||
const user = await n8n.api.users.create();
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz0123456789', 8);
|
||||
const newEmail = `something${nanoid()}@acme.corp`;
|
||||
await n8n.navigate.toPersonalSettings();
|
||||
await n8n.settingsPersonal.fillEmail(newEmail);
|
||||
await n8n.settingsPersonal.saveSettings();
|
||||
await n8n.settingsPersonal.currentPassword().fill(user.password);
|
||||
await n8n.modal.clickButton('Confirm');
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Personal details updated'),
|
||||
).toBeVisible();
|
||||
|
||||
const newTestUser = {
|
||||
email: newEmail,
|
||||
password: user.password,
|
||||
};
|
||||
const secondBrowser = await n8n.start.withUser(newTestUser);
|
||||
await secondBrowser.navigate.toPersonalSettings();
|
||||
await expect(secondBrowser.settingsPersonal.getEmailField()).toHaveValue(newEmail);
|
||||
});
|
||||
});
|
||||
60
packages/testing/playwright/tests/ui/18-user-service.spec.ts
Normal file
60
packages/testing/playwright/tests/ui/18-user-service.spec.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { expect, test } from '../../fixtures/base';
|
||||
|
||||
test.describe('User API Service', () => {
|
||||
test('should create a user with default values', async ({ api }) => {
|
||||
const user = await api.users.create();
|
||||
|
||||
expect(user.email).toContain('testuser');
|
||||
expect(user.email).toContain('@test.com');
|
||||
expect(user.firstName).toBe('Test');
|
||||
expect(user.lastName).toContain('User');
|
||||
expect(user.role).toContain('member');
|
||||
});
|
||||
|
||||
test('should create a user with custom values', async ({ api }) => {
|
||||
const customEmail = `custom-${Date.now()}@test.com`;
|
||||
const customPassword = 'CustomPass123!';
|
||||
|
||||
const user = await api.users.create({
|
||||
email: customEmail,
|
||||
password: customPassword,
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
role: 'global:member',
|
||||
});
|
||||
|
||||
expect(user.email.toLowerCase()).toBe(customEmail.toLowerCase());
|
||||
expect(user.firstName).toBe('John');
|
||||
expect(user.lastName).toBe('Doe');
|
||||
expect(user.role).toContain('member');
|
||||
});
|
||||
|
||||
test('should create a member user by default', async ({ api }) => {
|
||||
const user = await api.users.create();
|
||||
|
||||
expect(user.role).toContain('member');
|
||||
expect(user.role).toBe('global:member');
|
||||
});
|
||||
|
||||
test('should maintain separate sessions for multiple users', async ({ n8n, api }) => {
|
||||
await n8n.navigate.toPersonalSettings();
|
||||
const user = await api.users.create();
|
||||
|
||||
await n8n.page.reload();
|
||||
await expect(n8n.settingsPersonal.getUserRole()).toHaveText('Owner');
|
||||
|
||||
// New user page should have test name
|
||||
const memberN8n = await n8n.start.withUser(user);
|
||||
|
||||
await memberN8n.navigate.toPersonalSettings();
|
||||
await expect(memberN8n.settingsPersonal.getUserRole()).toHaveText('Member');
|
||||
|
||||
// n8n main should still have owner context
|
||||
await n8n.page.reload();
|
||||
await expect(n8n.settingsPersonal.getUserRole()).toHaveText('Owner');
|
||||
|
||||
// user page should still have member role
|
||||
await memberN8n.page.reload();
|
||||
await expect(memberN8n.settingsPersonal.getUserRole()).toHaveText('Member');
|
||||
});
|
||||
});
|
||||
@ -89,7 +89,7 @@ test.describe('Cloud @db:reset @auth:owner', () => {
|
||||
|
||||
await n8n.page.waitForLoadState();
|
||||
|
||||
await expect(n8n.settings.getUpgradeCta()).toBeVisible();
|
||||
await expect(n8n.settingsPersonal.getUpgradeCta()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -42,21 +42,21 @@ test.describe('Two-factor authentication @auth:none @db:reset', () => {
|
||||
await n8n.mfaComposer.loginWithMfaCode(email, password, mfaSecret!);
|
||||
|
||||
const disableToken = authenticator.generate(mfaSecret!);
|
||||
await n8n.settings.triggerDisableMfa();
|
||||
await n8n.settings.fillMfaCodeAndSave(disableToken);
|
||||
await n8n.settingsPersonal.triggerDisableMfa();
|
||||
await n8n.settingsPersonal.fillMfaCodeAndSave(disableToken);
|
||||
|
||||
await expect(n8n.settings.getEnableMfaButton()).toBeVisible();
|
||||
await expect(n8n.settingsPersonal.getEnableMfaButton()).toBeVisible();
|
||||
});
|
||||
|
||||
test('Should prompt for MFA code when email changes', async ({ n8n }) => {
|
||||
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
|
||||
|
||||
await n8n.settings.goToPersonalSettings();
|
||||
await n8n.settings.fillEmail(TEST_DATA.NEW_EMAIL);
|
||||
await n8n.settings.pressEnterOnEmail();
|
||||
await n8n.settingsPersonal.goToPersonalSettings();
|
||||
await n8n.settingsPersonal.fillEmail(TEST_DATA.NEW_EMAIL);
|
||||
await n8n.settingsPersonal.pressEnterOnEmail();
|
||||
|
||||
const mfaCode = authenticator.generate(mfaSecret!);
|
||||
await n8n.settings.fillMfaCodeAndSave(mfaCode);
|
||||
await n8n.settingsPersonal.fillMfaCodeAndSave(mfaCode);
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent(NOTIFICATIONS.PERSONAL_DETAILS_UPDATED),
|
||||
@ -66,11 +66,11 @@ test.describe('Two-factor authentication @auth:none @db:reset', () => {
|
||||
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 n8n.settingsPersonal.goToPersonalSettings();
|
||||
await n8n.settingsPersonal.fillEmail(TEST_DATA.NEW_EMAIL);
|
||||
await n8n.settingsPersonal.pressEnterOnEmail();
|
||||
|
||||
await expect(n8n.settings.getMfaCodeOrRecoveryCodeInput()).toBeVisible();
|
||||
await expect(n8n.settingsPersonal.getMfaCodeOrRecoveryCodeInput()).toBeVisible();
|
||||
});
|
||||
|
||||
test('Should not prompt for MFA code or recovery code when first name or last name changes', async ({
|
||||
@ -78,7 +78,10 @@ test.describe('Two-factor authentication @auth:none @db:reset', () => {
|
||||
}) => {
|
||||
await n8n.mfaComposer.enableMfa(email, password, mfaSecret!);
|
||||
|
||||
await n8n.settings.updateFirstAndLastName(TEST_DATA.NEW_FIRST_NAME, TEST_DATA.NEW_LAST_NAME);
|
||||
await n8n.settingsPersonal.updateFirstAndLastName(
|
||||
TEST_DATA.NEW_FIRST_NAME,
|
||||
TEST_DATA.NEW_LAST_NAME,
|
||||
);
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent(NOTIFICATIONS.PERSONAL_DETAILS_UPDATED),
|
||||
@ -91,9 +94,9 @@ test.describe('Two-factor authentication @auth:none @db:reset', () => {
|
||||
|
||||
await n8n.mfaComposer.loginWithMfaCode(email, password, mfaSecret!);
|
||||
|
||||
await n8n.settings.triggerDisableMfa();
|
||||
await n8n.settings.fillMfaCodeAndSave(RECOVERY_CODE);
|
||||
await n8n.settingsPersonal.triggerDisableMfa();
|
||||
await n8n.settingsPersonal.fillMfaCodeAndSave(RECOVERY_CODE);
|
||||
|
||||
await expect(n8n.settings.getEnableMfaButton()).toBeVisible();
|
||||
await expect(n8n.settingsPersonal.getEnableMfaButton()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@ -26,11 +26,11 @@ const VALID_NAMES = [
|
||||
|
||||
test.describe('Personal Settings', () => {
|
||||
test('should allow to change first and last name', async ({ n8n }) => {
|
||||
await n8n.settings.goToPersonalSettings();
|
||||
await n8n.settingsPersonal.goToPersonalSettings();
|
||||
|
||||
for (const name of VALID_NAMES) {
|
||||
await n8n.settings.fillPersonalData(name[0], name[1]);
|
||||
await n8n.settings.saveSettings();
|
||||
await n8n.settingsPersonal.fillPersonalData(name[0], name[1]);
|
||||
await n8n.settingsPersonal.saveSettings();
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Personal details updated'),
|
||||
@ -40,11 +40,11 @@ test.describe('Personal Settings', () => {
|
||||
});
|
||||
|
||||
test('should not allow malicious values for personal data', async ({ n8n }) => {
|
||||
await n8n.settings.goToPersonalSettings();
|
||||
await n8n.settingsPersonal.goToPersonalSettings();
|
||||
|
||||
for (const name of INVALID_NAMES) {
|
||||
await n8n.settings.fillPersonalData(name, name);
|
||||
await n8n.settings.saveSettings();
|
||||
await n8n.settingsPersonal.fillPersonalData(name, name);
|
||||
await n8n.settingsPersonal.saveSettings();
|
||||
|
||||
await expect(
|
||||
n8n.notifications.getNotificationByTitleOrContent('Problem updating your details'),
|
||||
|
||||
@ -3,13 +3,13 @@ import { test, expect } from '../../fixtures/base';
|
||||
test.describe('Admin user', () => {
|
||||
test('should see same Settings sub menu items as instance owner', async ({ n8n }) => {
|
||||
await n8n.api.setupTest('signin-only', 'owner');
|
||||
await n8n.settings.goToSettings();
|
||||
await n8n.settingsPersonal.goToSettings();
|
||||
|
||||
const ownerMenuItems = await n8n.settings.getMenuItems().count();
|
||||
const ownerMenuItems = await n8n.settingsPersonal.getMenuItems().count();
|
||||
|
||||
await n8n.api.setupTest('signin-only', 'admin');
|
||||
await n8n.settings.goToSettings();
|
||||
await n8n.settingsPersonal.goToSettings();
|
||||
|
||||
await expect(n8n.settings.getMenuItems()).toHaveCount(ownerMenuItems);
|
||||
await expect(n8n.settingsPersonal.getMenuItems()).toHaveCount(ownerMenuItems);
|
||||
});
|
||||
});
|
||||
|
||||
@ -116,8 +116,8 @@ test.describe('Projects', () => {
|
||||
await n8n.projectSettings.expectTableHasMemberCount(1);
|
||||
|
||||
// Verify save/cancel buttons are not visible initially (no changes)
|
||||
await expect(n8n.page.getByTestId('project-settings-save-button')).not.toBeVisible();
|
||||
await expect(n8n.page.getByTestId('project-settings-cancel-button')).not.toBeVisible();
|
||||
await expect(n8n.page.getByTestId('project-settings-save-button')).toBeHidden();
|
||||
await expect(n8n.page.getByTestId('project-settings-cancel-button')).toBeHidden();
|
||||
|
||||
// Delete button should always be visible
|
||||
await expect(n8n.page.getByTestId('project-settings-delete-button')).toBeVisible();
|
||||
@ -249,8 +249,8 @@ test.describe('Projects', () => {
|
||||
await expect(n8n.projectSettings.getTitle()).toHaveText('Unsaved Changes Test');
|
||||
|
||||
// Initially, save and cancel buttons should not be visible (no changes)
|
||||
await expect(n8n.page.getByTestId('project-settings-save-button')).not.toBeVisible();
|
||||
await expect(n8n.page.getByTestId('project-settings-cancel-button')).not.toBeVisible();
|
||||
await expect(n8n.page.getByTestId('project-settings-save-button')).toBeHidden();
|
||||
await expect(n8n.page.getByTestId('project-settings-cancel-button')).toBeHidden();
|
||||
|
||||
// Make a change to the project name
|
||||
await n8n.projectSettings.fillProjectName('Modified Name');
|
||||
@ -266,8 +266,8 @@ test.describe('Projects', () => {
|
||||
await n8n.projectSettings.clickCancelButton();
|
||||
|
||||
// Buttons should not be visible again (no changes)
|
||||
await expect(n8n.page.getByTestId('project-settings-save-button')).not.toBeVisible();
|
||||
await expect(n8n.page.getByTestId('project-settings-cancel-button')).not.toBeVisible();
|
||||
await expect(n8n.page.getByTestId('project-settings-save-button')).toBeHidden();
|
||||
await expect(n8n.page.getByTestId('project-settings-cancel-button')).toBeHidden();
|
||||
});
|
||||
|
||||
test('should display delete project section with warning @auth:owner', async ({ n8n }) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user