Browse Source

fix: Fixed sidebar node context menu based on ACL and added tests for it based on roles

pull/6438/head
Muhammed Mustafa 1 year ago
parent
commit
3b1f76da75
  1. 4
      packages/nc-gui/assets/style.scss
  2. 2
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  3. 2
      packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue
  4. 68
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  5. 7
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  6. 4
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  7. 10
      packages/nc-gui/lib/acl.ts
  8. 2
      tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts
  9. 6
      tests/playwright/pages/Dashboard/Grid/Column/index.ts
  10. 2
      tests/playwright/pages/Dashboard/Grid/index.ts
  11. 2
      tests/playwright/pages/Dashboard/Settings/Acl.ts
  12. 131
      tests/playwright/pages/Dashboard/Sidebar/ProjectNode/index.ts
  13. 69
      tests/playwright/pages/Dashboard/Sidebar/TableNode/index.ts
  14. 2
      tests/playwright/pages/Dashboard/Sidebar/UserMenu/index.ts
  15. 30
      tests/playwright/pages/Dashboard/Sidebar/index.ts
  16. 2
      tests/playwright/pages/Dashboard/TreeView.ts
  17. 9
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  18. 2
      tests/playwright/pages/SharedForm/index.ts
  19. 20
      tests/playwright/setup/index.ts
  20. 204
      tests/playwright/tests/db/features/projectCollaboration.spec.ts

4
packages/nc-gui/assets/style.scss

@ -583,3 +583,7 @@ input[type='number'] {
.ant-pagination .ant-pagination-item-link-icon {
@apply !block !py-1.5;
}
.nc-button.ant-btn.nc-sidebar-node-btn {
@apply opacity-0 group-hover:(opacity-100) text-gray-600 hover:(bg-gray-400 bg-opacity-20 text-gray-900) duration-100;
}

2
packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue

@ -78,7 +78,7 @@ onMounted(() => {
<GeneralIcon icon="arrowUp" class="!min-w-5" />
</div>
<template #overlay>
<NcMenu>
<NcMenu data-testid="nc-sidebar-userinfo">
<NcMenuItem data-testid="nc-sidebar-user-logout" @click="logout">
<GeneralLoader v-if="isLoggingOut" class="!ml-0.5 !mr-0.5 !max-h-4.5 !-mt-0.5" />
<GeneralIcon v-else icon="signout" class="menu-icon" />

2
packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue

@ -58,7 +58,7 @@ function openQuickImportDialog(type: string) {
<template>
<!-- Quick Import From -->
<NcSubMenu class="py-0">
<NcSubMenu class="py-0" data-testid="nc-sidebar-project-import">
<template #title>
<GeneralIcon icon="download" />

68
packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue

@ -89,6 +89,10 @@ const projectViewOpen = computed(() => {
return routeNameAfterProjectView.split('-').length === 2 || routeNameAfterProjectView.split('-').length === 1
})
const showBaseOption = computed(() => {
return ['airtableImport', 'csvImport', 'jsonImport', 'excelImport'].some((permission) => isUIAllowed(permission))
})
const enableEditMode = () => {
editMode.value = true
tempTitle.value = project.value.title!
@ -444,11 +448,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</span>
<div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(project)"></div>
<NcDropdown
v-if="isUIAllowed('tableCreate', { roles: projectRole })"
v-model:visible="isOptionsOpen"
:trigger="['click']"
>
<NcDropdown v-model:visible="isOptionsOpen" :trigger="['click']">
<NcButton
class="nc-sidebar-node-btn"
:class="{ '!text-black !opacity-100': isOptionsOpen }"
@ -466,29 +466,40 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
maxHeight: '70vh',
overflow: 'overlay',
}"
:data-testid="`nc-sidebar-project-${project.title}-options`"
@click="isOptionsOpen = false"
>
<template v-if="!isSharedBase">
<NcMenuItem @click="enableEditMode">
<NcMenuItem v-if="isUIAllowed('projectRename')" data-testid="nc-sidebar-project-rename" @click="enableEditMode">
<GeneralIcon icon="edit" class="group-hover:text-black" />
{{ $t('general.rename') }}
</NcMenuItem>
<!-- Copy Project Info -->
<NcMenuItem v-if="!isEeUI" key="copy" v-e="['c:navbar:user:copy-proj-info']" @click.stop="copyProjectInfo">
<GeneralIcon icon="copy" class="group-hover:text-black" />
{{ $t('activity.account.projInfo') }}
</NcMenuItem>
<NcMenuItem
v-if="isUIAllowed('projectDuplicate', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })"
data-testid="nc-sidebar-project-duplicate"
@click="duplicateProject(project)"
>
<GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }}
</NcMenuItem>
<NcDivider v-if="['projectDuplicate', 'projectRename'].some((permission) => isUIAllowed(permission))" />
<!-- Copy Project Info -->
<NcMenuItem
v-if="!isEeUI"
key="copy"
v-e="['c:navbar:user:copy-proj-info']"
data-testid="nc-sidebar-project-copy-project-info"
@click.stop="copyProjectInfo"
>
<GeneralIcon icon="copy" class="group-hover:text-black" />
{{ $t('activity.account.projInfo') }}
</NcMenuItem>
<!-- ERD View -->
<NcMenuItem key="erd" @click="openProjectErdView(project)">
<NcMenuItem key="erd" data-testid="nc-sidebar-project-relations" @click="openProjectErdView(project)">
<GeneralIcon icon="erd" />
Relations
</NcMenuItem>
@ -498,32 +509,36 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
v-if="isUIAllowed('apiDocs')"
key="api"
v-e="['e:api-docs']"
data-testid="nc-sidebar-project-rest-apis"
@click.stop="openLink(`/api/v1/db/meta/projects/${project.id}/swagger`, appInfo.ncSiteUrl)"
>
<GeneralIcon icon="snippet" class="group-hover:text-black" />
<GeneralIcon icon="snippet" class="group-hover:text-black !max-w-3.9" />
{{ $t('activity.account.swagger') }}
</NcMenuItem>
</template>
<!-- Team & Settings -->
<template v-if="project.bases && project.bases[0] && showBaseOption">
<NcDivider />
<DashboardTreeViewBaseOptions v-model:project="project" :base="project.bases[0]" />
</template>
<NcDivider v-if="['projectMiscSettings', 'projectDelete'].some((permission) => isUIAllowed(permission))" />
<NcMenuItem
v-if="isUIAllowed('settingsPage')"
v-if="isUIAllowed('projectMiscSettings')"
key="teamAndSettings"
v-e="['c:navdraw:project-settings']"
data-testid="nc-sidebar-project-settings"
class="nc-sidebar-project-project-settings"
@click="toggleDialog(true, 'teamAndAuth', undefined, project.id)"
>
<GeneralIcon icon="settings" class="group-hover:text-black" />
{{ $t('activity.settings') }}
</NcMenuItem>
<template v-if="project.bases && project.bases[0]">
<NcDivider />
<DashboardTreeViewBaseOptions v-model:project="project" :base="project.bases[0]" />
<NcDivider />
</template>
<NcMenuItem
v-if="isUIAllowed('projectDelete', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })"
data-testid="nc-sidebar-project-delete"
class="!text-red-500 !hover:bg-red-50"
@click="isProjectDeleteDialogVisible = true"
>
@ -616,10 +631,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</div>
</a-tooltip>
</div>
<div
v-if="isUIAllowed('tableCreate', { roles: projectRole })"
class="flex flex-row items-center gap-x-0.25 w-12.25"
>
<div class="flex flex-row items-center gap-x-0.25 w-12.25">
<NcDropdown
:visible="isBasesOptionsOpen[base!.id!]"
:trigger="['click']"
@ -649,7 +661,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
Relations
</NcMenuItem>
<DashboardTreeViewBaseOptions v-model:project="project" :base="base" />
<DashboardTreeViewBaseOptions v-if="showBaseOption" v-model:project="project" :base="base" />
</NcMenu>
</template>
</NcDropdown>
@ -749,10 +761,6 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
@apply !mx-0 !pl-8.75 !pr-0.5 !py-0.75 hover:bg-gray-200 !rounded-md;
}
:deep(.nc-button.ant-btn.nc-sidebar-node-btn) {
@apply opacity-0 group-hover:(opacity-100) text-gray-600 hover:(bg-gray-400 bg-opacity-20 text-gray-900) duration-100;
}
:deep(.ant-collapse-content-box) {
@apply !px-0 !pb-0 !pt-0.25;
}

7
packages/nc-gui/components/dashboard/TreeView/TableNode.vue

@ -165,6 +165,7 @@ const canUserEditEmote = computed(() => {
>
<MdiDotsHorizontal
class="min-w-5.75 min-h-5.75 mt-0.2 mr-0.25 px-0.5 transition-opacity opacity-0 group-hover:opacity-100 nc-tbl-context-menu outline-0 rounded-md hover:(bg-gray-500 bg-opacity-15 !text-black)"
data-testid="nc-sidebar-table-context-menu"
:class="{
'!text-gray-600': openedTableId !== table.id,
'!text-black': openedTableId === table.id,
@ -172,7 +173,7 @@ const canUserEditEmote = computed(() => {
/>
<template #overlay>
<NcMenu>
<NcMenu :data-testid="`nc-sidebar-table-${table.title}-options`">
<NcMenuItem
v-if="isUIAllowed('tableRename', { roles: projectRole })"
:data-testid="`sidebar-table-rename-${table.title}`"
@ -223,10 +224,6 @@ const canUserEditEmote = computed(() => {
@apply relative cursor-pointer after:(pointer-events-none content-[''] rounded absolute top-0 left-0 w-full h-full right-0 !bg-current transition transition-opactity duration-100 opacity-0);
}
.nc-tree-item svg {
@apply text-primary text-opacity-60;
}
.nc-tree-item.active {
@apply !bg-primary-selected rounded-md;
//@apply border-r-3 border-primary;

4
packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue

@ -116,7 +116,7 @@ useMenuCloseOnEsc(open)
<template #overlay>
<a-menu class="!py-0 !rounded !text-gray-800 text-sm" data-testid="toolbar-actions" @click="open = false">
<a-menu-item-group>
<template v-if="isUIAllowed('csvImport') && !isView && !isPublicView && !isSqlView">
<template v-if="isUIAllowed('csvTableImport') && !isView && !isPublicView && !isSqlView">
<a-sub-menu key="upload">
<template #title>
<div v-e="['c:navdraw:preview-as']" class="nc-project-menu-item group">
@ -130,7 +130,7 @@ useMenuCloseOnEsc(open)
<template #expandIcon></template>
<template v-for="(dialog, type) in quickImportDialogs">
<a-menu-item v-if="isUIAllowed(`${type}Import`) && !isView && !isPublicView" :key="type">
<a-menu-item v-if="isUIAllowed(`${type}TableImport`) && !isView && !isPublicView" :key="type">
<div
v-e="[`a:actions:upload-${type}`]"
class="nc-project-menu-item"

10
packages/nc-gui/lib/acl.ts

@ -25,6 +25,8 @@ const rolePermissions = {
projectDelete: true,
projectDuplicate: true,
newUser: true,
tableRename: true,
tableDelete: true,
},
},
[OrgUserRoles.VIEWER]: {
@ -63,6 +65,10 @@ const rolePermissions = {
viewCreateOrEdit: true,
viewShare: true,
projectShare: true,
projectMiscSettings: true,
csvImport: true,
projectRename: true,
projectDuplicate: true,
},
},
[ProjectRoles.EDITOR]: {
@ -73,8 +79,7 @@ const rolePermissions = {
filterSync: true,
filterChildrenRead: true,
viewFieldEdit: true,
csvImport: true,
apiDocs: true,
csvTableImport: true,
},
},
[ProjectRoles.COMMENTER]: {
@ -88,6 +93,7 @@ const rolePermissions = {
include: {
projectSettings: true,
expandedForm: true,
apiDocs: true,
},
},
[ProjectRoles.NO_ACCESS]: {

2
tests/playwright/pages/Dashboard/Grid/Column/LTAR/LinkRecord.ts

@ -24,7 +24,7 @@ export class LinkRecord extends BasePage {
{
const childList = linkRecord.getByTestId(`nc-excluded-list-item`);
expect.poll(() => linkRecord.getByTestId(`nc-excluded-list-item`).count()).toBe(cardTitle.length);
await expect.poll(() => linkRecord.getByTestId(`nc-excluded-list-item`).count()).toBe(cardTitle.length);
for (let i = 0; i < cardTitle.length; i++) {
await childList.nth(i).locator('.nc-display-value').scrollIntoViewIfNeeded();
await childList.nth(i).locator('.nc-display-value').waitFor({ state: 'visible' });

6
tests/playwright/pages/Dashboard/Grid/Column/index.ts

@ -385,9 +385,9 @@ export class ColumnPageObject extends BasePage {
}
// select all menu access
expect(
await this.grid.get().locator('[data-testid="nc-check-all"]').locator('input[type="checkbox"]').count()
).toBe(role === 'creator' || role === 'owner' || role === 'editor' ? 1 : 0);
await expect(
await this.grid.get().locator('[data-testid="nc-check-all"]').locator('input[type="checkbox"]')
).toHaveCount(role === 'creator' || role === 'owner' || role === 'editor' ? 1 : 0);
if (role === 'creator' || role === 'owner' || role === 'editor') {
await this.grid.selectAll();

2
tests/playwright/pages/Dashboard/Grid/index.ts

@ -123,7 +123,7 @@ export class GridPage extends BasePage {
// add delay for UI to render (can wait for count to stabilize by reading it multiple times)
await this.rootPage.waitForTimeout(100);
expect(await this.get().locator('.nc-grid-row').count()).toBe(rowCount + 1);
await expect(this.get().locator('.nc-grid-row')).toHaveCount(rowCount + 1);
await this._fillRow({ index, columnHeader, value: rowValue });

2
tests/playwright/pages/Dashboard/Settings/Acl.ts

@ -19,7 +19,7 @@ export class AclPage extends BasePage {
async save() {
await this.waitForResponse({
uiAction: async() => await this.get().locator(`button:has-text("Save")`).click(),
uiAction: async () => await this.get().locator(`button:has-text("Save")`).click(),
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: '/visibility-rules',
});

131
tests/playwright/pages/Dashboard/Sidebar/ProjectNode/index.ts

@ -0,0 +1,131 @@
import BasePage from '../../../Base';
import { SidebarPage } from '..';
import { expect } from '@playwright/test';
export class SidebarProjectNodeObject extends BasePage {
readonly sidebar: SidebarPage;
constructor(parent: SidebarPage) {
super(parent.rootPage);
this.sidebar = parent;
}
get({ projectTitle }: { projectTitle: string }) {
return this.sidebar.get().getByTestId(`nc-sidebar-project-${projectTitle}`);
}
async click({ projectTitle }: { projectTitle: string }) {
await this.get({
projectTitle,
}).click();
}
async clickOptions({ projectTitle }: { projectTitle: string }) {
await this.get({
projectTitle,
})
.getByTestId(`nc-sidebar-context-menu`)
.click();
}
async verifyTableAddBtn({ projectTitle, visible }: { projectTitle: string; visible: boolean }) {
const addBtn = await this.get({
projectTitle,
}).getByTestId('nc-sidebar-add-project-entity');
if (visible) {
await addBtn.hover({
force: true,
});
await expect(addBtn).toBeVisible();
} else await expect(addBtn).toHaveCount(0);
}
async verifyProjectOptions({
projectTitle,
renameVisible,
starredVisible,
duplicateVisible,
relationsVisible,
restApisVisible,
importVisible,
settingsVisible,
deleteVisible,
copyProjectInfoVisible,
}: {
projectTitle: string;
renameVisible?: boolean;
starredVisible?: boolean;
duplicateVisible?: boolean;
relationsVisible?: boolean;
restApisVisible?: boolean;
importVisible?: boolean;
settingsVisible?: boolean;
deleteVisible?: boolean;
copyProjectInfoVisible?: boolean;
}) {
const renameLocator = await this.rootPage
.getByTestId(`nc-sidebar-project-${projectTitle}-options`)
.getByTestId('nc-sidebar-project-rename');
if (renameVisible) await renameLocator.isVisible();
else await expect(renameLocator).toHaveCount(0);
const starredLocator = await this.rootPage
.getByTestId(`nc-sidebar-project-${projectTitle}-options`)
.getByTestId('nc-sidebar-project-starred');
if (starredVisible) await expect(starredLocator).toBeVisible();
else await expect(starredLocator).toHaveCount(0);
const duplicateLocator = await this.rootPage
.getByTestId(`nc-sidebar-project-${projectTitle}-options`)
.getByTestId('nc-sidebar-project-duplicate');
if (duplicateVisible) await expect(duplicateLocator).toBeVisible();
else await expect(duplicateLocator).toHaveCount(0);
const relationsLocator = await this.rootPage
.getByTestId(`nc-sidebar-project-${projectTitle}-options`)
.getByTestId('nc-sidebar-project-relations');
if (relationsVisible) await expect(relationsLocator).toBeVisible();
else await expect(relationsLocator).toHaveCount(0);
const restApisLocator = await this.rootPage
.getByTestId(`nc-sidebar-project-${projectTitle}-options`)
.getByTestId('nc-sidebar-project-rest-apis');
if (restApisVisible) await expect(restApisLocator).toBeVisible();
else await expect(restApisLocator).toHaveCount(0);
const importLocator = await this.rootPage
.getByTestId(`nc-sidebar-project-${projectTitle}-options`)
.getByTestId('nc-sidebar-project-import');
if (importVisible) await expect(importLocator).toBeVisible();
else await expect(importLocator).toHaveCount(0);
const settingsLocator = await this.rootPage
.getByTestId(`nc-sidebar-project-${projectTitle}-options`)
.getByTestId('nc-sidebar-project-settings');
if (settingsVisible) await expect(settingsLocator).toBeVisible();
else await expect(settingsLocator).toHaveCount(0);
const deleteLocator = await this.rootPage
.getByTestId(`nc-sidebar-project-${projectTitle}-options`)
.getByTestId('nc-sidebar-project-delete');
if (deleteVisible) await expect(deleteLocator).toBeVisible();
else await expect(deleteLocator).toHaveCount(0);
const copyProjectInfoLocator = await this.rootPage
.getByTestId(`nc-sidebar-project-${projectTitle}-options`)
.getByTestId('nc-sidebar-project-copy-project-info');
if (copyProjectInfoVisible) await expect(copyProjectInfoLocator).toBeVisible();
else await expect(copyProjectInfoLocator).toHaveCount(0);
}
}

69
tests/playwright/pages/Dashboard/Sidebar/TableNode/index.ts

@ -0,0 +1,69 @@
import BasePage from '../../../Base';
import { SidebarPage } from '..';
import { expect } from '@playwright/test';
export class SidebarTableNodeObject extends BasePage {
readonly sidebar: SidebarPage;
constructor(parent: SidebarPage) {
super(parent.rootPage);
this.sidebar = parent;
}
get({ tableTitle }: { tableTitle: string }) {
return this.sidebar.get().getByTestId(`tree-view-table-${tableTitle}`);
}
async click({ tableTitle }: { tableTitle: string }) {
await this.get({
tableTitle,
}).click();
}
async clickOptions({ tableTitle }: { tableTitle: string }) {
await this.get({
tableTitle,
})
.getByTestId(`nc-sidebar-table-context-menu`)
.click();
}
async verifyTableOptions({
tableTitle,
isVisible,
renameVisible,
duplicateVisible,
deleteVisible,
}: {
tableTitle: string;
isVisible: boolean;
renameVisible?: boolean;
duplicateVisible?: boolean;
deleteVisible?: boolean;
}) {
const optionsLocator = await this.get({
tableTitle,
}).getByTestId('nc-sidebar-table-context-menu');
if (isVisible) await optionsLocator.isVisible();
else {
await expect(optionsLocator).toHaveCount(0);
return;
}
const renameLocator = await this.rootPage.getByTestId(`sidebar-table-rename-${tableTitle}`);
if (renameVisible) await renameLocator.isVisible();
else await expect(renameLocator).toHaveCount(0);
const duplicateLocator = await this.rootPage.getByTestId(`sidebar-table-duplicate-${tableTitle}`);
if (duplicateVisible) await expect(duplicateLocator).toBeVisible();
else await expect(duplicateLocator).toHaveCount(0);
const deleteLocator = await this.rootPage.getByTestId(`sidebar-table-delete-${tableTitle}`);
if (deleteVisible) await expect(deleteLocator).toBeVisible();
else await expect(deleteLocator).toHaveCount(0);
}
}

2
tests/playwright/pages/Dashboard/Sidebar/UserMenu/index.ts

@ -15,7 +15,7 @@ export class SidebarUserMenuObject extends BasePage {
}
async click() {
await this.get().click();
await this.rootPage.getByTestId('nc-sidebar-userinfo').click();
}
async clickLogout() {

30
tests/playwright/pages/Dashboard/Sidebar/index.ts

@ -4,21 +4,25 @@ import { DashboardPage } from '..';
import BasePage from '../../Base';
import { DocsSidebarPage } from './DocsSidebar';
import { SidebarUserMenuObject } from './UserMenu';
import { SidebarProjectNodeObject } from './ProjectNode';
import { SidebarTableNodeObject } from './TableNode';
export class SidebarPage extends BasePage {
readonly dashboard: DashboardPage;
readonly docsSidebar: DocsSidebarPage;
readonly quickImportButton: Locator;
readonly createProjectBtn: Locator;
readonly userMenu: SidebarUserMenuObject;
readonly projectNode: SidebarProjectNodeObject;
readonly tableNode: SidebarTableNodeObject;
constructor(dashboard: DashboardPage) {
super(dashboard.rootPage);
this.dashboard = dashboard;
this.docsSidebar = new DocsSidebarPage(this);
this.userMenu = new SidebarUserMenuObject(this);
this.quickImportButton = dashboard.get().locator('.nc-import-menu');
this.createProjectBtn = dashboard.get().locator('.nc-create-project-btn');
this.createProjectBtn = dashboard.get().getByTestId('nc-sidebar-create-project-btn');
this.projectNode = new SidebarProjectNodeObject(this);
this.tableNode = new SidebarTableNodeObject(this);
}
get() {
@ -37,6 +41,21 @@ export class SidebarPage extends BasePage {
}
}
async verifyQuickActions({ isVisible }: { isVisible: boolean }) {
if (isVisible) await expect(this.get().getByTestId('nc-sidebar-search-btn')).toBeVisible();
else await expect(this.get().getByTestId('nc-sidebar-search-btn')).toHaveCount(0);
}
async verifyTeamAndSettings({ isVisible }: { isVisible: boolean }) {
if (isVisible) await expect(this.get().getByTestId('nc-sidebar-team-settings-btn')).toBeVisible();
else await expect(this.get().getByTestId('nc-sidebar-team-settings-btn')).toHaveCount(0);
}
async verifyCreateProjectBtn({ isVisible }: { isVisible: boolean }) {
if (isVisible) await expect(this.createProjectBtn).toBeVisible();
else await expect(this.createProjectBtn).toHaveCount(0);
}
async openProject({ title }: { title: string }) {
await this.get().locator(`.project-title-node`).getByText(title).click();
@ -57,6 +76,9 @@ export class SidebarPage extends BasePage {
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: `api/v1/db/meta/projects/`,
});
await this.dashboard.docs.pagesList.waitForOpen({ title });
if (type === ProjectTypes.DOCUMENTATION) {
await this.dashboard.docs.pagesList.waitForOpen({ title });
}
}
}

2
tests/playwright/pages/Dashboard/TreeView.ts

@ -283,7 +283,7 @@ export class TreeViewPage extends BasePage {
// add new table button & context menu is visible only for owner & creator
await expect(pjtNode.locator('[data-testid="nc-sidebar-add-project-entity"]')).toHaveCount(count);
await expect(pjtNode.locator('[data-testid="nc-sidebar-context-menu"]')).toHaveCount(count);
await expect(pjtNode.locator('[data-testid="nc-sidebar-context-menu"]')).toHaveCount(1);
// table context menu
const tblNode = await this.getTable({ index: 0, tableTitle: param.tableTitle });

9
tests/playwright/pages/Dashboard/common/Cell/index.ts

@ -341,7 +341,7 @@ export class CellPageObject extends BasePage {
await this.rootPage.waitForSelector('.nc-modal-child-list:visible');
// verify child list count & contents
expect.poll(() => this.rootPage.locator('.ant-card:visible').count()).toBe(count);
await expect.poll(() => this.rootPage.locator('.ant-card:visible').count()).toBe(count);
// close child list
await this.rootPage.locator('.nc-modal-child-list').locator('.nc-close-btn').last().click();
@ -369,11 +369,8 @@ export class CellPageObject extends BasePage {
.waitFor({ state: 'visible', timeout: 3000 });
await this.waitForResponse({
uiAction: async () =>
await this.rootPage
.locator(`[data-testid="nc-child-list-item"]`)
.last()
.click({ force: true, timeout: 3000 }),
uiAction: () =>
this.rootPage.locator(`[data-testid="nc-child-list-item"]`).last().click({ force: true, timeout: 3000 }),
requestUrlPathToMatch: '/api/v1/db/data/noco/',
httpMethodsToMatch: ['GET'],
});

2
tests/playwright/pages/SharedForm/index.ts

@ -58,7 +58,7 @@ export class SharedFormPage extends BasePage {
{
const childList = linkRecord.locator(`.ant-card`);
expect.poll(() => linkRecord.locator(`.ant-card`).count()).toBe(cardTitle.length);
await expect.poll(() => linkRecord.locator(`.ant-card`).count()).toBe(cardTitle.length);
for (let i = 0; i < cardTitle.length; i++) {
expect(await childList.nth(i).textContent()).toContain(cardTitle[i]);
}

20
tests/playwright/setup/index.ts

@ -153,6 +153,8 @@ export interface NcContext {
workerId?: string;
rootUser: UserType & { password: string };
workspace: WorkspaceType;
defaultProjectTitle: string;
defaultTableTitle: string;
}
selectors.setTestIdAttribute('data-testid');
@ -384,7 +386,12 @@ const setup = async ({
email: `user@nocodb.com`,
password: getDefaultPwd(),
});
if (!isEE()) await axios.post(`http://localhost:8080/api/v1/license`, { key: '' }, { headers: { 'xc-auth': admin.data.token } });
if (!isEE())
await axios.post(
`http://localhost:8080/api/v1/license`,
{ key: '' },
{ headers: { 'xc-auth': admin.data.token } }
);
} catch (e) {
// ignore error: some roles will not have permission for license reset
// console.error(`Error resetting project: ${process.env.TEST_PARALLEL_INDEX}`, e);
@ -436,7 +443,16 @@ const setup = async ({
}
await page.goto(projectUrl, { waitUntil: 'networkidle' });
return { project, token, dbType, workerId, rootUser, workspace } as NcContext;
return {
project,
token,
dbType,
workerId,
rootUser,
workspace,
defaultProjectTitle: 'Getting Started',
defaultTableTitle: 'Features',
} as NcContext;
};
export const unsetup = async (context: NcContext): Promise<void> => {};

204
tests/playwright/tests/db/features/projectCollaboration.spec.ts

@ -0,0 +1,204 @@
import { Page, test } from '@playwright/test';
import { DashboardPage } from '../../../pages/Dashboard';
import setup, { unsetup } from '../../../setup';
import { getDefaultPwd } from '../../../tests/utils/general';
import { Api } from 'nocodb-sdk';
import { CollaborationPage } from '../../../pages/WorkspacePage/CollaborationPage';
import { LoginPage } from '../../../pages/LoginPage';
import { ProjectViewPage } from '../../../pages/Dashboard/ProjectView';
import { AccountUsersPage } from '../../../pages/Account/Users';
import { AccountPage } from '../../../pages/Account';
import { isEE } from '../../../setup/db';
const roleDb = [
{ email: 'pjt_creator@nocodb.com', role: 'Creator' },
{ email: 'pjt_editor@nocodb.com', role: 'Editor' },
{ email: 'pjt_commenter@nocodb.com', role: 'Commenter' },
{ email: 'pjt_viewer@nocodb.com', role: 'Viewer' },
];
test.describe('Project Collaboration', () => {
let dashboard: DashboardPage;
let accountsPage: AccountPage;
let collaborationPage: CollaborationPage;
let projectViewPage: ProjectViewPage;
let context: any;
let api: Api<any>;
test.skip(() => isEE());
test.beforeEach(async ({ page }) => {
context = await setup({ page, isEmptyProject: false });
dashboard = new DashboardPage(page, context.project);
accountsPage = new AccountPage(page);
projectViewPage = dashboard.projectView;
api = new Api({
baseURL: `http://localhost:8080/`,
headers: {
'xc-auth': context.token,
},
});
for (let i = 0; i < roleDb.length; i++) {
try {
await api.auth.signup({
email: roleDb[i].email,
password: getDefaultPwd(),
});
} catch (e) {
// ignore error even if user already exists
}
}
});
test.afterEach(async () => {
await unsetup(context);
});
const projectCollabVerify = async (
page: Page,
user: {
email: string;
role: string;
}
) => {
await dashboard.leftSidebar.clickTeamAndSettings();
// add all users as WS viewers
await accountsPage.users.invite({
email: user.email,
role: 'viewer',
});
await dashboard.rootPage.goBack();
await dashboard.treeView.openProject({ title: context.project.title, context });
// tab access validation
await projectViewPage.verifyAccess('Owner');
await projectViewPage.tab_accessSettings.click();
// update roles
await projectViewPage.accessSettings.setRole(user.email, user.role);
await dashboard.signOut();
const loginPage = new LoginPage(page);
await loginPage.signIn({
email: user.email,
password: getDefaultPwd(),
withoutPrefix: true,
skipReload: true,
});
await dashboard.rootPage.waitForTimeout(500);
await dashboard.projectView.verifyAccess(user.role);
await dashboard.treeView.openTable({ title: 'Country' });
await dashboard.treeView.validateRoleAccess({
role: user.role,
projectTitle: context.project.title,
tableTitle: 'Country',
context,
});
await dashboard.viewSidebar.validateRoleAccess({ role: user.role });
await dashboard.grid.verifyRoleAccess({ role: user.role });
await dashboard.grid.openExpandedRow({ index: 0 });
await dashboard.expandedForm.verifyRoleAccess({ role: user.role });
await dashboard.sidebar.projectNode.click({
projectTitle: context.project.title,
});
};
test('Project role access validation: Creator', async ({ page }) => {
await projectCollabVerify(page, roleDb[0]);
const projectNode = dashboard.sidebar.projectNode;
await projectNode.verifyTableAddBtn({ projectTitle: context.project.title, visible: true });
await projectNode.clickOptions({ projectTitle: context.project.title });
await projectNode.verifyProjectOptions({
projectTitle: context.project.title,
deleteVisible: false,
duplicateVisible: true,
importVisible: true,
renameVisible: true,
restApisVisible: true,
settingsVisible: true,
starredVisible: false,
relationsVisible: true,
copyProjectInfoVisible: true,
});
});
test('Project role access validation: Editor', async ({ page }) => {
await projectCollabVerify(page, roleDb[1]);
const projectNode = dashboard.sidebar.projectNode;
await projectNode.verifyTableAddBtn({ projectTitle: context.project.title, visible: false });
await projectNode.clickOptions({ projectTitle: context.project.title });
await projectNode.verifyProjectOptions({
projectTitle: context.project.title,
deleteVisible: false,
duplicateVisible: false,
importVisible: false,
renameVisible: false,
restApisVisible: true,
settingsVisible: false,
starredVisible: false,
relationsVisible: true,
copyProjectInfoVisible: true,
});
});
test('Project role access validation: Commentor', async ({ page }) => {
await projectCollabVerify(page, roleDb[2]);
const projectNode = dashboard.sidebar.projectNode;
await projectNode.verifyTableAddBtn({ projectTitle: context.project.title, visible: false });
await projectNode.clickOptions({ projectTitle: context.project.title });
await projectNode.verifyProjectOptions({
projectTitle: context.project.title,
deleteVisible: false,
duplicateVisible: false,
importVisible: false,
renameVisible: false,
restApisVisible: true,
settingsVisible: false,
starredVisible: false,
relationsVisible: true,
copyProjectInfoVisible: true,
});
});
test('Project role access validation: Viewer', async ({ page }) => {
await projectCollabVerify(page, roleDb[3]);
const projectNode = dashboard.sidebar.projectNode;
await projectNode.verifyTableAddBtn({ projectTitle: context.project.title, visible: false });
await projectNode.clickOptions({ projectTitle: context.project.title });
await projectNode.verifyProjectOptions({
projectTitle: context.project.title,
deleteVisible: false,
duplicateVisible: false,
importVisible: false,
renameVisible: false,
restApisVisible: true,
settingsVisible: false,
starredVisible: false,
relationsVisible: true,
copyProjectInfoVisible: true,
});
});
});
Loading…
Cancel
Save