From 57aa34521147800f42c3be085e67c9e7b05bb644 Mon Sep 17 00:00:00 2001 From: Austin Turner Date: Wed, 19 Mar 2025 18:44:24 -0700 Subject: [PATCH] Add integration tests for manage permissions --- apps/jetstream-e2e/src/fixtures/fixtures.ts | 10 +- .../manager-permissions.spec.ts | 136 ++++++++++++++++++ .../src/ManagePermissionsSelection.tsx | 2 + .../utils/permission-manager-table-utils.tsx | 2 + libs/test/e2e-utils/src/index.ts | 1 + .../ManagePermissionPage.model.ts | 110 ++++++++++++++ .../lib/list/ListWithFilterMultiSelect.tsx | 4 +- .../ConnectedSobjectListMultiSelect.tsx | 6 +- 8 files changed, 265 insertions(+), 6 deletions(-) create mode 100644 apps/jetstream-e2e/src/tests/manager-permissions/manager-permissions.spec.ts create mode 100644 libs/test/e2e-utils/src/lib/pageObjectModels/ManagePermissionPage.model.ts diff --git a/apps/jetstream-e2e/src/fixtures/fixtures.ts b/apps/jetstream-e2e/src/fixtures/fixtures.ts index 3cde2582e..cf930be97 100644 --- a/apps/jetstream-e2e/src/fixtures/fixtures.ts +++ b/apps/jetstream-e2e/src/fixtures/fixtures.ts @@ -5,6 +5,7 @@ import { AuthenticationPage, LoadSingleObjectPage, LoadWithoutFilePage, + ManagePermissionPage, OrganizationsPage, PlatformEventPage, PlaywrightPage, @@ -40,6 +41,7 @@ type MyFixtures = { newUser: Awaited>; queryPage: QueryPage; loadSingleObjectPage: LoadSingleObjectPage; + managePermissionPage: ManagePermissionPage; organizationsPage: OrganizationsPage; loadWithoutFilePage: LoadWithoutFilePage; platformEventPage: PlatformEventPage; @@ -62,9 +64,9 @@ export const test = base.extend({ newUser: async ({ authenticationPage }, use) => { await use(await authenticationPage.signUpAndVerifyEmail()); }, - queryPage: async ({ page, apiRequestUtils, playwrightPage }, use) => { + queryPage: async ({ page, apiRequestUtils }, use) => { await apiRequestUtils.selectDefaultOrg(); - await use(new QueryPage(page, apiRequestUtils, playwrightPage)); + await use(new QueryPage(page, apiRequestUtils)); }, loadSingleObjectPage: async ({ page, apiRequestUtils, playwrightPage }, use) => { await apiRequestUtils.selectDefaultOrg(); @@ -74,6 +76,10 @@ export const test = base.extend({ await apiRequestUtils.selectDefaultOrg(); await use(new LoadWithoutFilePage(page, apiRequestUtils, playwrightPage)); }, + managePermissionPage: async ({ page, apiRequestUtils }, use) => { + await apiRequestUtils.selectDefaultOrg(); + await use(new ManagePermissionPage(page, apiRequestUtils)); + }, organizationsPage: async ({ page }, use) => { await use(new OrganizationsPage(page)); }, diff --git a/apps/jetstream-e2e/src/tests/manager-permissions/manager-permissions.spec.ts b/apps/jetstream-e2e/src/tests/manager-permissions/manager-permissions.spec.ts new file mode 100644 index 000000000..33f093514 --- /dev/null +++ b/apps/jetstream-e2e/src/tests/manager-permissions/manager-permissions.spec.ts @@ -0,0 +1,136 @@ +/* eslint-disable playwright/no-conditional-expect */ +/* eslint-disable playwright/no-conditional-in-test */ +import { expect, test } from '../../fixtures/fixtures'; + +test.beforeEach(async ({ page }) => { + await page.goto('/app'); +}); + +test.describe.configure({ mode: 'parallel' }); + +test.describe('MANAGE PERMISSIONS', () => { + const continueButtonTestCases = [ + { + profileNames: ['Standard User'], + permissionSetNames: [], + sobjectNames: [], + enabled: false, + }, + { + profileNames: ['Standard User'], + permissionSetNames: ['Jetstream'], + sobjectNames: [], + enabled: false, + }, + { + profileNames: [], + permissionSetNames: [], + sobjectNames: ['Account'], + enabled: false, + }, + { + profileNames: ['Standard User'], + permissionSetNames: [], + sobjectNames: ['Account'], + enabled: true, + }, + { + profileNames: [], + permissionSetNames: ['Jetstream'], + sobjectNames: ['Account'], + enabled: true, + }, + ]; + + for (const testCase of continueButtonTestCases) { + test(`Should test the continue button for profiles: ${testCase.profileNames.join()}, permSets: ${testCase.permissionSetNames.join( + ',' + )}, objects:${testCase.sobjectNames}`, async ({ page, managePermissionPage }) => { + await managePermissionPage.goto(); + await managePermissionPage.selectProfilesPermissionSetsAndObjects(testCase); + if (testCase.enabled) { + await expect(managePermissionPage.continueButton).toBeVisible(); + } else { + await expect(managePermissionPage.continueButtonDisabled).toBeVisible(); + await expect(managePermissionPage.continueButtonDisabled).toBeDisabled(); + } + }); + } + + test('Should allow selecting profiles, permission sets and objects', async ({ page, managePermissionPage }) => { + await managePermissionPage.goto(); + await managePermissionPage.selectProfilesPermissionSetsAndObjects({ + profileNames: ['Standard User'], + permissionSetNames: ['Jetstream'], + sobjectNames: ['Account', 'Contact', 'Opportunity'], + }); + + await expect(managePermissionPage.continueButton).toBeVisible(); + + await managePermissionPage.gotoEditor(); + + await test.step('Validate field dependencies work', async () => { + const read = managePermissionPage.getCheckboxLocator('Account', 'AccountNumber', 'profile', 'read'); + const edit = managePermissionPage.getCheckboxLocator('Account', 'AccountNumber', 'profile', 'edit'); + + await read.uncheck(); + await expect(read).toBeChecked({ checked: false }); + await expect(edit).toBeChecked({ checked: false }); + + await read.check(); + await expect(read).toBeChecked(); + await expect(edit).toBeChecked({ checked: false }); + + await read.uncheck(); + await edit.check(); + await expect(read).toBeChecked(); + await expect(edit).toBeChecked(); + }); + + await test.step('bulk edit row with dependencies', async () => { + await page.getByPlaceholder('Filter...').click(); + await page.getByPlaceholder('Filter...').fill('level__c'); + await page.getByTestId('row-action-button-Contact.Level__c').click(); + + await page.getByTestId('row-action-popover').locator('label').filter({ hasText: 'Edit' }).check(); + await expect(page.getByTestId('row-action-popover').locator('label').filter({ hasText: 'Read' })).toBeChecked(); + + await page.getByRole('button', { name: 'Apply to Row' }).click(); + await expect(page.locator('[id="Contact\\.Level__c-0PSDn000003RK82OAG-read"]')).toBeChecked(); + await expect(page.locator('[id="Contact\\.Level__c-0PSDn000003RK82OAG-edit"]')).toBeChecked(); + await expect(page.locator('[id="Contact\\.Level__c-0PSDn000001M6CFOA0-read"]')).toBeChecked(); + await expect(page.locator('[id="Contact\\.Level__c-0PSDn000001M6CFOA0-edit"]')).toBeChecked(); + + await page.getByTestId('row-action-popover').locator('label').filter({ hasText: 'Read' }).uncheck(); + await expect(page.getByTestId('row-action-popover').locator('label').filter({ hasText: 'Edit' })).toBeChecked({ checked: false }); + + await page.getByRole('button', { name: 'Apply to Row' }).click(); + await expect(page.locator('[id="Contact\\.Level__c-0PSDn000003RK82OAG-read"]')).toBeChecked({ checked: false }); + await expect(page.locator('[id="Contact\\.Level__c-0PSDn000003RK82OAG-edit"]')).toBeChecked({ checked: false }); + await expect(page.locator('[id="Contact\\.Level__c-0PSDn000001M6CFOA0-read"]')).toBeChecked({ checked: false }); + await expect(page.locator('[id="Contact\\.Level__c-0PSDn000001M6CFOA0-edit"]')).toBeChecked({ checked: false }); + + await page.getByTestId('row-action-popover').locator('label').filter({ hasText: 'Read' }).check(); + await expect(page.getByTestId('row-action-popover').locator('label').filter({ hasText: 'Edit' })).toBeChecked({ checked: false }); + + await page.getByRole('button', { name: 'Apply to Row' }).click(); + await expect(page.locator('[id="Contact\\.Level__c-0PSDn000003RK82OAG-read"]')).toBeChecked(); + await expect(page.locator('[id="Contact\\.Level__c-0PSDn000003RK82OAG-edit"]')).toBeChecked({ checked: false }); + await expect(page.locator('[id="Contact\\.Level__c-0PSDn000001M6CFOA0-read"]')).toBeChecked(); + await expect(page.locator('[id="Contact\\.Level__c-0PSDn000001M6CFOA0-edit"]')).toBeChecked({ checked: false }); + await page.getByRole('button', { name: 'Close dialog' }).click(); + }); + + await test.step('save', async () => { + await page.getByRole('button', { name: 'Save' }).click(); + await page.getByRole('button', { name: 'Save Changes' }).click(); + // TODO: validate saved changes + }); + }); + + // TODO: reset changes + + // TODO: tab visibility + + // TODO: object permissions +}); diff --git a/libs/features/manage-permissions/src/ManagePermissionsSelection.tsx b/libs/features/manage-permissions/src/ManagePermissionsSelection.tsx index ef38e520f..38740eaf4 100644 --- a/libs/features/manage-permissions/src/ManagePermissionsSelection.tsx +++ b/libs/features/manage-permissions/src/ManagePermissionsSelection.tsx @@ -139,6 +139,7 @@ export const ManagePermissionsSelection: FunctionComponent
diff --git a/libs/test/e2e-utils/src/index.ts b/libs/test/e2e-utils/src/index.ts index 885ecc58f..f0ce5efe3 100644 --- a/libs/test/e2e-utils/src/index.ts +++ b/libs/test/e2e-utils/src/index.ts @@ -3,6 +3,7 @@ export * from './lib/e2e-database-validation.utils'; export * from './lib/pageObjectModels/AuthenticationPage.model'; export * from './lib/pageObjectModels/LoadSingleObjectPage.model'; export * from './lib/pageObjectModels/LoadWithoutFilePage.model'; +export * from './lib/pageObjectModels/ManagePermissionPage.model'; export * from './lib/pageObjectModels/OrganizationsPage'; export * from './lib/pageObjectModels/PlatformEventPage.model'; export * from './lib/pageObjectModels/PlaywrightPage.model'; diff --git a/libs/test/e2e-utils/src/lib/pageObjectModels/ManagePermissionPage.model.ts b/libs/test/e2e-utils/src/lib/pageObjectModels/ManagePermissionPage.model.ts new file mode 100644 index 000000000..d366a4594 --- /dev/null +++ b/libs/test/e2e-utils/src/lib/pageObjectModels/ManagePermissionPage.model.ts @@ -0,0 +1,110 @@ +import { formatNumber } from '@jetstream/shared/ui-utils'; +import { pluralizeFromNumber } from '@jetstream/shared/utils'; +import { Locator, Page, expect } from '@playwright/test'; +import { ApiRequestUtils } from '../ApiRequestUtils'; + +function getSelectionText(length: number, identifier: string) { + return `${formatNumber(length)} ${pluralizeFromNumber(identifier, length)} selected`; +} + +export class ManagePermissionPage { + readonly apiRequestUtils: ApiRequestUtils; + readonly page: Page; + + readonly profileList: Locator; + readonly permissionSetList: Locator; + readonly sobjectList: Locator; + + readonly continueButton: Locator; + readonly continueButtonDisabled: Locator; + + readonly standardUserProfileId = '0PSDn000003RK82OAG'; + readonly jetstreamPermSetId = '0PSDn000001M6CFOA0'; + + constructor(page: Page, apiRequestUtils: ApiRequestUtils) { + this.apiRequestUtils = apiRequestUtils; + this.page = page; + + this.profileList = page.getByTestId('profiles-list'); + this.permissionSetList = page.getByTestId('permission-sets-list'); + this.sobjectList = page.getByTestId('sobject-list-multi-select'); + this.continueButton = page.getByRole('link', { name: 'Continue' }); + this.continueButtonDisabled = page.getByRole('button', { name: 'Continue' }); + } + + getCheckboxLocator(entity: string, field: string, type: 'profile' | 'permissionSet', which: 'read' | 'edit') { + // await page.locator('[id="Account\\.AccountNumber-0PSDn000003RK82OAG-edit"]').uncheck(); + return this.page.locator( + `[id="${entity}\\.${field}-${type === 'profile' ? this.standardUserProfileId : this.jetstreamPermSetId}-${which}"]` + ); + } + + isChecked(entity: string, field: string, type: 'profile' | 'permissionSet', which: 'read' | 'edit') { + return this.getCheckboxLocator(entity, field, type, which).isChecked(); + } + + async validateFieldDependency(entity: string, field: string, type: 'profile' | 'permissionSet') { + const read = this.getCheckboxLocator(entity, field, type, 'read'); + const edit = this.getCheckboxLocator(entity, field, type, 'edit'); + await edit.check(); + await expect(read).toBeChecked(); + await expect(edit).toBeChecked(); + + await read.uncheck(); + await expect(read).toBeChecked({ checked: false }); + await expect(edit).toBeChecked({ checked: false }); + } + + async modifyValue(entity: string, field: string, type: 'profile' | 'permissionSet') { + const read = this.getCheckboxLocator(entity, field, type, 'read'); + const edit = this.getCheckboxLocator(entity, field, type, 'edit'); + await edit.check(); + await expect(read).toBeChecked(); + await expect(edit).toBeChecked(); + + await read.uncheck(); + await expect(read).toBeChecked({ checked: false }); + await expect(edit).toBeChecked({ checked: false }); + } + + async goto() { + await this.page.getByRole('menuitem', { name: 'Manage Permissions' }).click(); + await this.page.waitForURL('**/permissions-manager'); + } + + async gotoEditor() { + await this.continueButton.click(); + await this.page.waitForURL('**/permissions-manager/editor'); + } + + async selectProfilesPermissionSetsAndObjects({ + profileNames, + permissionSetNames, + sobjectNames, + }: { + sobjectNames: string[]; + profileNames: string[]; + permissionSetNames: string[]; + }) { + for (const name of profileNames) { + await this.profileList.getByText(name, { exact: true }).first().click(); + } + if (profileNames.length > 0) { + await expect(this.profileList.getByRole('button', { name: getSelectionText(profileNames.length, 'item') })).toBeVisible(); + } + + for (const name of permissionSetNames) { + await this.permissionSetList.getByText(name, { exact: true }).first().click(); + } + if (permissionSetNames.length > 0) { + await expect(this.permissionSetList.getByRole('button', { name: getSelectionText(permissionSetNames.length, 'item') })).toBeVisible(); + } + + for (const name of sobjectNames) { + await this.sobjectList.getByText(name, { exact: true }).first().click(); + } + if (sobjectNames.length > 0) { + await expect(this.sobjectList.getByRole('button', { name: getSelectionText(sobjectNames.length, 'object') })).toBeVisible(); + } + } +} diff --git a/libs/ui/src/lib/list/ListWithFilterMultiSelect.tsx b/libs/ui/src/lib/list/ListWithFilterMultiSelect.tsx index 2df3c6e89..228a88185 100644 --- a/libs/ui/src/lib/list/ListWithFilterMultiSelect.tsx +++ b/libs/ui/src/lib/list/ListWithFilterMultiSelect.tsx @@ -17,6 +17,7 @@ import Tooltip from '../widgets/Tooltip'; import List from './List'; export interface ListWithFilterMultiSelectProps { + testId?: string; labels: { listHeading?: string; filter: string; // {Filter Items} @@ -43,6 +44,7 @@ export interface ListWithFilterMultiSelectProps { * This will extend to the full page height */ export const ListWithFilterMultiSelect: FunctionComponent = ({ + testId, labels, items, selectedItems = [], @@ -150,7 +152,7 @@ export const ListWithFilterMultiSelect: FunctionComponent
)} -
+
{hasError && (

There was an error loading {labels.descriptorPlural} for the selected org. diff --git a/libs/ui/src/lib/sobject-list/ConnectedSobjectListMultiSelect.tsx b/libs/ui/src/lib/sobject-list/ConnectedSobjectListMultiSelect.tsx index 7585004e5..fef82ba0e 100644 --- a/libs/ui/src/lib/sobject-list/ConnectedSobjectListMultiSelect.tsx +++ b/libs/ui/src/lib/sobject-list/ConnectedSobjectListMultiSelect.tsx @@ -5,7 +5,7 @@ import { DescribeGlobalSObjectResult, Maybe, RecentHistoryItemType, SalesforceOr import { recentHistoryItemsDb } from '@jetstream/ui/db'; import { formatRelative } from 'date-fns/formatRelative'; import { useLiveQuery } from 'dexie-react-hooks'; -import { Fragment, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'; import Grid from '../grid/Grid'; import Icon from '../widgets/Icon'; import Tooltip from '../widgets/Tooltip'; @@ -132,7 +132,7 @@ export const ConnectedSobjectListMultiSelect = forwardRef +

{label}

@@ -154,7 +154,7 @@ export const ConnectedSobjectListMultiSelect = forwardRef setErrorMessage(null)} /> - +
); } );