mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
refactor(editor): Update project settings to use new table component and role selector (#19152)
This commit is contained in:
@@ -343,17 +343,17 @@ test('should create workflow via API, activate it, trigger webhook externally @a
|
||||
const workflowDefinition = JSON.parse(
|
||||
readFileSync(resolveFromRoot('workflows', 'simple-webhook-test.json'), 'utf8'),
|
||||
);
|
||||
|
||||
|
||||
const createdWorkflow = await api.workflowApi.createWorkflow(workflowDefinition);
|
||||
await api.workflowApi.setActive(createdWorkflow.id, true);
|
||||
|
||||
|
||||
const testPayload = { message: 'Hello from Playwright test' };
|
||||
const webhookResponse = await api.workflowApi.triggerWebhook('test-webhook', { data: testPayload });
|
||||
expect(webhookResponse.ok()).toBe(true);
|
||||
|
||||
|
||||
const execution = await api.workflowApi.waitForExecution(createdWorkflow.id, 10000);
|
||||
expect(execution.status).toBe('success');
|
||||
|
||||
|
||||
const executionDetails = await api.workflowApi.getExecution(execution.id);
|
||||
expect(executionDetails.data).toContain('Hello from Playwright test');
|
||||
});
|
||||
@@ -525,7 +525,8 @@ Here's a complete example from our codebase showing all layers:
|
||||
export class ProjectSettingsPage extends BasePage {
|
||||
// Simple action methods only
|
||||
async fillProjectName(name: string) {
|
||||
await this.page.getByTestId('project-settings-name-input').locator('input').fill(name);
|
||||
// Prefer stable ID selectors on the wrapper element and then target the inner control
|
||||
await this.page.locator('#projectName input').fill(name);
|
||||
}
|
||||
|
||||
async clickSaveButton() {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { BasePage } from './BasePage';
|
||||
|
||||
export class ProjectSettingsPage extends BasePage {
|
||||
@@ -5,7 +7,73 @@ export class ProjectSettingsPage extends BasePage {
|
||||
await this.page.getByTestId('project-settings-name-input').locator('input').fill(name);
|
||||
}
|
||||
|
||||
async fillProjectDescription(description: string) {
|
||||
await this.page
|
||||
.getByTestId('project-settings-description-input')
|
||||
.locator('textarea')
|
||||
.fill(description);
|
||||
}
|
||||
|
||||
async clickSaveButton() {
|
||||
await this.clickButtonByName('Save');
|
||||
}
|
||||
|
||||
async clickCancelButton() {
|
||||
await this.page.getByTestId('project-settings-cancel-button').click();
|
||||
}
|
||||
|
||||
async clearMemberSearch() {
|
||||
const searchInput = this.page.getByTestId('project-members-search');
|
||||
const clearButton = searchInput.locator('+ span');
|
||||
if (await clearButton.isVisible()) {
|
||||
await clearButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
getMembersTable() {
|
||||
return this.page.getByTestId('project-members-table');
|
||||
}
|
||||
|
||||
async getMemberRowCount() {
|
||||
const table = this.getMembersTable();
|
||||
const rows = table.locator('tbody tr');
|
||||
return await rows.count();
|
||||
}
|
||||
|
||||
async expectTableHasMemberCount(expectedCount: number) {
|
||||
const actualCount = await this.getMemberRowCount();
|
||||
expect(actualCount).toBe(expectedCount);
|
||||
}
|
||||
|
||||
async expectSearchInputValue(expectedValue: string) {
|
||||
const searchInput = this.page.getByTestId('project-members-search').locator('input');
|
||||
await expect(searchInput).toHaveValue(expectedValue);
|
||||
}
|
||||
|
||||
// Robust value assertions on inner form controls
|
||||
getNameInput() {
|
||||
return this.page.locator('#projectName input');
|
||||
}
|
||||
|
||||
getDescriptionTextarea() {
|
||||
return this.page.locator('#projectDescription textarea');
|
||||
}
|
||||
|
||||
async expectProjectNameValue(value: string) {
|
||||
await expect(this.getNameInput()).toHaveValue(value);
|
||||
}
|
||||
|
||||
async expectProjectDescriptionValue(value: string) {
|
||||
await expect(this.getDescriptionTextarea()).toHaveValue(value);
|
||||
}
|
||||
|
||||
async expectTableIsVisible() {
|
||||
const table = this.getMembersTable();
|
||||
await expect(table).toBeVisible();
|
||||
}
|
||||
|
||||
async expectMembersSelectIsVisible() {
|
||||
const select = this.page.getByTestId('project-members-select');
|
||||
await expect(select).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,4 +102,237 @@ test.describe('Projects', () => {
|
||||
await expect(subn8n.page.locator('[data-test-id="resources-list-item"]')).toHaveCount(1);
|
||||
await expect(subn8n.page.getByRole('heading', { name: 'Notion account' })).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe('Project Settings - Member Management', () => {
|
||||
test('should display project settings page with correct layout @auth:owner', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
// Create a new project
|
||||
const { projectId } = await n8n.projectComposer.createProject('UI Test Project');
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.page.goto(`/projects/${projectId}/settings`);
|
||||
await n8n.page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Verify basic project settings form elements are visible (inner controls)
|
||||
await expect(n8n.projectSettings.getNameInput()).toBeVisible();
|
||||
await expect(n8n.projectSettings.getDescriptionTextarea()).toBeVisible();
|
||||
await n8n.projectSettings.expectMembersSelectIsVisible();
|
||||
|
||||
// Verify members table is visible when there are members
|
||||
await n8n.projectSettings.expectTableIsVisible();
|
||||
|
||||
// Initially should have only the owner (current user)
|
||||
await n8n.projectSettings.expectTableHasMemberCount(1);
|
||||
|
||||
// Verify project settings action buttons are present
|
||||
await expect(n8n.page.getByTestId('project-settings-save-button')).toBeVisible();
|
||||
await expect(n8n.page.getByTestId('project-settings-cancel-button')).toBeVisible();
|
||||
await expect(n8n.page.getByTestId('project-settings-delete-button')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should allow editing project name and description @auth:owner', async ({ n8n }) => {
|
||||
// Create a new project
|
||||
const { projectId } = await n8n.projectComposer.createProject('Edit Test Project');
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.page.goto(`/projects/${projectId}/settings`);
|
||||
await n8n.page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Update project name
|
||||
const newName = 'Updated Project Name';
|
||||
await n8n.projectSettings.fillProjectName(newName);
|
||||
|
||||
// Update project description
|
||||
const newDescription = 'This is an updated project description.';
|
||||
await n8n.projectSettings.fillProjectDescription(newDescription);
|
||||
|
||||
// Save changes
|
||||
await n8n.projectSettings.clickSaveButton();
|
||||
|
||||
// Wait for success notification
|
||||
await expect(
|
||||
n8n.page.getByText('Project Updated Project Name saved successfully', { exact: false }),
|
||||
).toBeVisible();
|
||||
|
||||
// Verify the form shows the updated values
|
||||
await n8n.projectSettings.expectProjectNameValue(newName);
|
||||
await n8n.projectSettings.expectProjectDescriptionValue(newDescription);
|
||||
});
|
||||
|
||||
test('should display members table with correct structure @auth:owner', async ({ n8n }) => {
|
||||
// Create a new project
|
||||
const { projectId } = await n8n.projectComposer.createProject('Table Structure Test');
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.page.goto(`/projects/${projectId}/settings`);
|
||||
await n8n.page.waitForLoadState('domcontentloaded');
|
||||
|
||||
const table = n8n.projectSettings.getMembersTable();
|
||||
|
||||
// Verify table headers are present
|
||||
await expect(table.getByText('User')).toBeVisible();
|
||||
await expect(table.getByText('Role')).toBeVisible();
|
||||
|
||||
// Verify the owner is displayed in the table
|
||||
const memberRows = table.locator('tbody tr');
|
||||
await expect(memberRows).toHaveCount(1);
|
||||
|
||||
// Verify owner cannot change their own role
|
||||
const ownerRow = memberRows.first();
|
||||
const roleDropdown = ownerRow.getByTestId('project-member-role-dropdown');
|
||||
await expect(roleDropdown).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should display role dropdown for members but not for current user @auth:owner', async ({
|
||||
n8n,
|
||||
}) => {
|
||||
// Create a new project
|
||||
const { projectId } = await n8n.projectComposer.createProject('Role Dropdown Test');
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.page.goto(`/projects/${projectId}/settings`);
|
||||
await n8n.page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Current user (owner) should not have a role dropdown
|
||||
const currentUserRow = n8n.page.locator('tbody tr').first();
|
||||
await expect(currentUserRow.getByTestId('project-member-role-dropdown')).not.toBeVisible();
|
||||
|
||||
// The role should be displayed as static text for the current user
|
||||
await expect(currentUserRow.getByText('Admin')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle member search functionality when search input is used', async ({ n8n }) => {
|
||||
// Create a new project
|
||||
const { projectId } = await n8n.projectComposer.createProject('Search Test Project');
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.page.goto(`/projects/${projectId}/settings`);
|
||||
await n8n.page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Verify search input is visible
|
||||
const searchInput = n8n.page.getByTestId('project-members-search');
|
||||
await expect(searchInput).toBeVisible();
|
||||
|
||||
// Test search functionality - enter search term
|
||||
await searchInput.fill('nonexistent');
|
||||
|
||||
// Since we only have the owner, searching for nonexistent should show no filtered results
|
||||
// But the table structure should still be present
|
||||
await expect(searchInput).toHaveValue('nonexistent');
|
||||
|
||||
// Clear search
|
||||
await n8n.projectSettings.clearMemberSearch();
|
||||
await expect(searchInput).toHaveValue('');
|
||||
});
|
||||
|
||||
test('should show project settings form validation @auth:owner', async ({ n8n }) => {
|
||||
// Create a new project
|
||||
const { projectId } = await n8n.projectComposer.createProject('Validation Test');
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.page.goto(`/projects/${projectId}/settings`);
|
||||
await n8n.page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Clear the project name (required field)
|
||||
await n8n.projectSettings.fillProjectName('');
|
||||
|
||||
// Save button should be disabled when required field is empty
|
||||
const saveButton = n8n.page.getByTestId('project-settings-save-button');
|
||||
await expect(saveButton).toBeDisabled();
|
||||
|
||||
// Fill in a valid name
|
||||
await n8n.projectSettings.fillProjectName('Valid Project Name');
|
||||
|
||||
// Save button should now be enabled
|
||||
await expect(saveButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
test('should handle unsaved changes state @auth:owner', async ({ n8n }) => {
|
||||
// Create a new project
|
||||
const { projectId } = await n8n.projectComposer.createProject('Unsaved Changes Test');
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.page.goto(`/projects/${projectId}/settings`);
|
||||
await n8n.page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Initially, save and cancel buttons should be disabled (no changes)
|
||||
await expect(n8n.page.getByTestId('project-settings-save-button')).toBeDisabled();
|
||||
await expect(n8n.page.getByTestId('project-settings-cancel-button')).toBeDisabled();
|
||||
|
||||
// Make a change to the project name
|
||||
await n8n.projectSettings.fillProjectName('Modified Name');
|
||||
|
||||
// Save and cancel buttons should now be enabled
|
||||
await expect(n8n.page.getByTestId('project-settings-save-button')).not.toBeDisabled();
|
||||
await expect(n8n.page.getByTestId('project-settings-cancel-button')).not.toBeDisabled();
|
||||
|
||||
// Unsaved changes message should be visible
|
||||
await expect(n8n.page.getByText('You have unsaved changes')).toBeVisible();
|
||||
|
||||
// Cancel changes
|
||||
await n8n.projectSettings.clickCancelButton();
|
||||
|
||||
// Buttons should be disabled again
|
||||
await expect(n8n.page.getByTestId('project-settings-save-button')).toBeDisabled();
|
||||
await expect(n8n.page.getByTestId('project-settings-cancel-button')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('should display delete project section with warning @auth:owner', async ({ n8n }) => {
|
||||
// Create a new project
|
||||
const { projectId } = await n8n.projectComposer.createProject('Delete Test Project');
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.page.goto(`/projects/${projectId}/settings`);
|
||||
await n8n.page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Scroll to bottom to see delete section
|
||||
await n8n.page
|
||||
.locator('[data-test-id="project-settings-delete-button"]')
|
||||
.scrollIntoViewIfNeeded();
|
||||
|
||||
// Verify danger section is visible with warning
|
||||
// Copy was updated in UI to use sentence case and expanded description
|
||||
await expect(n8n.page.getByText('Danger zone')).toBeVisible();
|
||||
await expect(
|
||||
n8n.page.getByText(
|
||||
'When deleting a project, you can also choose to move all workflows and credentials to another project.',
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(n8n.page.getByTestId('project-settings-delete-button')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should persist settings after page reload @auth:owner', async ({ n8n }) => {
|
||||
// Create a new project
|
||||
const { projectId } = await n8n.projectComposer.createProject('Persistence Test');
|
||||
|
||||
// Navigate to project settings
|
||||
await n8n.page.goto(`/projects/${projectId}/settings`);
|
||||
await n8n.page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Update project details
|
||||
const projectName = 'Persisted Project Name';
|
||||
const projectDescription = 'This description should persist after reload';
|
||||
|
||||
await n8n.projectSettings.fillProjectName(projectName);
|
||||
await n8n.projectSettings.fillProjectDescription(projectDescription);
|
||||
await n8n.projectSettings.clickSaveButton();
|
||||
|
||||
// Wait for save confirmation (partial match to include project name)
|
||||
await expect(
|
||||
n8n.page.getByText('Project Persisted Project Name saved successfully', { exact: false }),
|
||||
).toBeVisible();
|
||||
|
||||
// Reload the page
|
||||
await n8n.page.reload();
|
||||
await n8n.page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Verify data persisted
|
||||
await n8n.projectSettings.expectProjectNameValue(projectName);
|
||||
await n8n.projectSettings.expectProjectDescriptionValue(projectDescription);
|
||||
|
||||
// Verify table still shows the owner
|
||||
await n8n.projectSettings.expectTableHasMemberCount(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user