Browse Source

Merge pull request #6438 from nocodb/fix/sidebar-node-context-menu-acl-fix

Fixed sidebar node context menu based on ACL and added tests for  it based on roles
pull/6447/head
Muhammed Mustafa 1 year ago committed by GitHub
parent
commit
f5ea2e4e78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .github/workflows/ci-cd.yml
  2. 2
      .github/workflows/playwright-test-workflow.yml
  3. 2
      packages/nc-gui/components/dashboard/Sidebar/UserInfo.vue
  4. 2
      packages/nc-gui/components/dashboard/TreeView/BaseOptions.vue
  5. 59
      packages/nc-gui/components/dashboard/TreeView/ProjectNode.vue
  6. 1
      packages/nc-gui/components/dashboard/TreeView/TableNode.vue
  7. 4
      packages/nc-gui/components/smartsheet/toolbar/ViewActions.vue
  8. 10
      packages/nc-gui/lib/acl.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. 73
      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. 7
      tests/playwright/pages/Dashboard/common/Cell/index.ts
  18. 20
      tests/playwright/setup/index.ts

2
.github/workflows/ci-cd.yml

@ -91,8 +91,6 @@ jobs:
${{ runner.os }}-pnpm-store- ${{ runner.os }}-pnpm-store-
- name: Set CI env - name: Set CI env
run: export CI=true run: export CI=true
- name: Set NC Edition
run: export EE=true
- name: setup pg - name: setup pg
working-directory: ./ working-directory: ./
run: docker-compose -f ./tests/playwright/scripts/docker-compose-playwright-pg.yml up -d & run: docker-compose -f ./tests/playwright/scripts/docker-compose-playwright-pg.yml up -d &

2
.github/workflows/playwright-test-workflow.yml

@ -56,7 +56,7 @@ jobs:
- name: Set CI env - name: Set CI env
run: export CI=true run: export CI=true
- name: Set NC Edition - name: Set NC Edition
run: export EE=true run: export EE=false
- name: install dependencies - name: install dependencies
run: pnpm bootstrap run: pnpm bootstrap
- name: Setup mysql - name: Setup mysql

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

@ -78,7 +78,7 @@ onMounted(() => {
<GeneralIcon icon="arrowUp" class="!min-w-5" /> <GeneralIcon icon="arrowUp" class="!min-w-5" />
</div> </div>
<template #overlay> <template #overlay>
<NcMenu> <NcMenu data-testid="nc-sidebar-userinfo">
<NcMenuItem data-testid="nc-sidebar-user-logout" @click="logout"> <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" /> <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" /> <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> <template>
<!-- Quick Import From --> <!-- Quick Import From -->
<NcSubMenu class="py-0"> <NcSubMenu class="py-0" data-testid="nc-sidebar-project-import">
<template #title> <template #title>
<GeneralIcon icon="download" /> <GeneralIcon icon="download" />

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

@ -89,6 +89,10 @@ const projectViewOpen = computed(() => {
return routeNameAfterProjectView.split('-').length === 2 || routeNameAfterProjectView.split('-').length === 1 return routeNameAfterProjectView.split('-').length === 2 || routeNameAfterProjectView.split('-').length === 1
}) })
const showBaseOption = computed(() => {
return ['airtableImport', 'csvImport', 'jsonImport', 'excelImport'].some((permission) => isUIAllowed(permission))
})
const enableEditMode = () => { const enableEditMode = () => {
editMode.value = true editMode.value = true
tempTitle.value = project.value.title! tempTitle.value = project.value.title!
@ -440,11 +444,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
</span> </span>
<div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(project)"></div> <div :class="{ 'flex flex-grow h-full': !editMode }" @click="onProjectClick(project)"></div>
<NcDropdown <NcDropdown v-model:visible="isOptionsOpen" :trigger="['click']">
v-if="isUIAllowed('tableCreate', { roles: projectRole })"
v-model:visible="isOptionsOpen"
:trigger="['click']"
>
<NcButton <NcButton
class="nc-sidebar-node-btn" class="nc-sidebar-node-btn"
:class="{ '!text-black !opacity-100': isOptionsOpen }" :class="{ '!text-black !opacity-100': isOptionsOpen }"
@ -462,29 +462,40 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
maxHeight: '70vh', maxHeight: '70vh',
overflow: 'overlay', overflow: 'overlay',
}" }"
:data-testid="`nc-sidebar-project-${project.title}-options`"
@click="isOptionsOpen = false" @click="isOptionsOpen = false"
> >
<template v-if="!isSharedBase"> <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" /> <GeneralIcon icon="edit" class="group-hover:text-black" />
{{ $t('general.rename') }} {{ $t('general.rename') }}
</NcMenuItem> </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 <NcMenuItem
v-if="isUIAllowed('projectDuplicate', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })" v-if="isUIAllowed('projectDuplicate', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })"
data-testid="nc-sidebar-project-duplicate"
@click="duplicateProject(project)" @click="duplicateProject(project)"
> >
<GeneralIcon icon="duplicate" class="text-gray-700" /> <GeneralIcon icon="duplicate" class="text-gray-700" />
{{ $t('general.duplicate') }} {{ $t('general.duplicate') }}
</NcMenuItem> </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 --> <!-- ERD View -->
<NcMenuItem key="erd" @click="openProjectErdView(project)"> <NcMenuItem key="erd" data-testid="nc-sidebar-project-relations" @click="openProjectErdView(project)">
<GeneralIcon icon="erd" /> <GeneralIcon icon="erd" />
Relations Relations
</NcMenuItem> </NcMenuItem>
@ -494,32 +505,36 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
v-if="isUIAllowed('apiDocs')" v-if="isUIAllowed('apiDocs')"
key="api" key="api"
v-e="['e:api-docs']" 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)" @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') }} {{ $t('activity.account.swagger') }}
</NcMenuItem> </NcMenuItem>
</template> </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 <NcMenuItem
v-if="isUIAllowed('settingsPage')" v-if="isUIAllowed('projectMiscSettings')"
key="teamAndSettings" key="teamAndSettings"
v-e="['c:navdraw:project-settings']" v-e="['c:navdraw:project-settings']"
data-testid="nc-sidebar-project-settings"
class="nc-sidebar-project-project-settings" class="nc-sidebar-project-project-settings"
@click="toggleDialog(true, 'teamAndAuth', undefined, project.id)" @click="toggleDialog(true, 'teamAndAuth', undefined, project.id)"
> >
<GeneralIcon icon="settings" class="group-hover:text-black" /> <GeneralIcon icon="settings" class="group-hover:text-black" />
{{ $t('activity.settings') }} {{ $t('activity.settings') }}
</NcMenuItem> </NcMenuItem>
<template v-if="project.bases && project.bases[0]">
<NcDivider />
<DashboardTreeViewBaseOptions v-model:project="project" :base="project.bases[0]" />
<NcDivider />
</template>
<NcMenuItem <NcMenuItem
v-if="isUIAllowed('projectDelete', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })" v-if="isUIAllowed('projectDelete', { roles: [stringifyRolesObj(orgRoles), projectRole].join() })"
data-testid="nc-sidebar-project-delete"
class="!text-red-500 !hover:bg-red-50" class="!text-red-500 !hover:bg-red-50"
@click="isProjectDeleteDialogVisible = true" @click="isProjectDeleteDialogVisible = true"
> >
@ -642,7 +657,7 @@ const DlgProjectDuplicateOnOk = async (jobData: { id: string; project_id: string
Relations Relations
</NcMenuItem> </NcMenuItem>
<DashboardTreeViewBaseOptions v-model:project="project" :base="base" /> <DashboardTreeViewBaseOptions v-if="showBaseOption" v-model:project="project" :base="base" />
</NcMenu> </NcMenu>
</template> </template>
</NcDropdown> </NcDropdown>

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

@ -222,6 +222,7 @@ const isTableOpened = computed(() => {
@click.stop @click.stop
> >
<MdiDotsHorizontal <MdiDotsHorizontal
data-testid="nc-sidebar-table-context-menu"
class="min-w-5.75 min-h-5.75 mt-0.2 mr-0.25 px-0.5 !text-gray-600 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)" class="min-w-5.75 min-h-5.75 mt-0.2 mr-0.25 px-0.5 !text-gray-600 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)"
/> />

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

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

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

@ -25,6 +25,8 @@ const rolePermissions = {
projectDelete: true, projectDelete: true,
projectDuplicate: true, projectDuplicate: true,
newUser: true, newUser: true,
tableRename: true,
tableDelete: true,
viewCreateOrEdit: true, viewCreateOrEdit: true,
}, },
}, },
@ -64,6 +66,10 @@ const rolePermissions = {
viewCreateOrEdit: true, viewCreateOrEdit: true,
viewShare: true, viewShare: true,
projectShare: true, projectShare: true,
projectMiscSettings: true,
csvImport: true,
projectRename: true,
projectDuplicate: true,
}, },
}, },
[ProjectRoles.EDITOR]: { [ProjectRoles.EDITOR]: {
@ -74,8 +80,7 @@ const rolePermissions = {
filterSync: true, filterSync: true,
filterChildrenRead: true, filterChildrenRead: true,
viewFieldEdit: true, viewFieldEdit: true,
csvImport: true, csvTableImport: true,
apiDocs: true,
}, },
}, },
[ProjectRoles.COMMENTER]: { [ProjectRoles.COMMENTER]: {
@ -89,6 +94,7 @@ const rolePermissions = {
include: { include: {
projectSettings: true, projectSettings: true,
expandedForm: true, expandedForm: true,
apiDocs: true,
}, },
}, },
[ProjectRoles.NO_ACCESS]: { [ProjectRoles.NO_ACCESS]: {

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

@ -385,9 +385,9 @@ export class ColumnPageObject extends BasePage {
} }
// select all menu access // select all menu access
expect( await expect(
await this.grid.get().locator('[data-testid="nc-check-all"]').locator('input[type="checkbox"]').count() await this.grid.get().locator('[data-testid="nc-check-all"]').locator('input[type="checkbox"]')
).toBe(role === 'creator' || role === 'owner' || role === 'editor' ? 1 : 0); ).toHaveCount(role === 'creator' || role === 'owner' || role === 'editor' ? 1 : 0);
if (role === 'creator' || role === 'owner' || role === 'editor') { if (role === 'creator' || role === 'owner' || role === 'editor') {
await this.grid.selectAll(); 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) // add delay for UI to render (can wait for count to stabilize by reading it multiple times)
await this.rootPage.waitForTimeout(100); 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 }); 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() { async save() {
await this.waitForResponse({ 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'], httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: '/visibility-rules', 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);
}
}

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

@ -0,0 +1,73 @@
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(`nc-tbl-side-node-${tableTitle}`);
}
async click({ tableTitle }: { tableTitle: string }) {
await this.get({
tableTitle,
}).click();
}
async clickOptions({ tableTitle }: { tableTitle: string }) {
await this.get({
tableTitle,
}).hover();
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() { async click() {
await this.get().click(); await this.rootPage.getByTestId('nc-sidebar-userinfo').click();
} }
async clickLogout() { async clickLogout() {

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

@ -4,21 +4,25 @@ import { DashboardPage } from '..';
import BasePage from '../../Base'; import BasePage from '../../Base';
import { DocsSidebarPage } from './DocsSidebar'; import { DocsSidebarPage } from './DocsSidebar';
import { SidebarUserMenuObject } from './UserMenu'; import { SidebarUserMenuObject } from './UserMenu';
import { SidebarProjectNodeObject } from './ProjectNode';
import { SidebarTableNodeObject } from './TableNode';
export class SidebarPage extends BasePage { export class SidebarPage extends BasePage {
readonly dashboard: DashboardPage; readonly dashboard: DashboardPage;
readonly docsSidebar: DocsSidebarPage; readonly docsSidebar: DocsSidebarPage;
readonly quickImportButton: Locator;
readonly createProjectBtn: Locator; readonly createProjectBtn: Locator;
readonly userMenu: SidebarUserMenuObject; readonly userMenu: SidebarUserMenuObject;
readonly projectNode: SidebarProjectNodeObject;
readonly tableNode: SidebarTableNodeObject;
constructor(dashboard: DashboardPage) { constructor(dashboard: DashboardPage) {
super(dashboard.rootPage); super(dashboard.rootPage);
this.dashboard = dashboard; this.dashboard = dashboard;
this.docsSidebar = new DocsSidebarPage(this); this.docsSidebar = new DocsSidebarPage(this);
this.userMenu = new SidebarUserMenuObject(this); this.userMenu = new SidebarUserMenuObject(this);
this.quickImportButton = dashboard.get().locator('.nc-import-menu'); this.createProjectBtn = dashboard.get().getByTestId('nc-sidebar-create-project-btn');
this.createProjectBtn = dashboard.get().locator('.nc-create-project-btn'); this.projectNode = new SidebarProjectNodeObject(this);
this.tableNode = new SidebarTableNodeObject(this);
} }
get() { 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 }) { async openProject({ title }: { title: string }) {
await this.get().locator(`.project-title-node`).getByText(title).click(); await this.get().locator(`.project-title-node`).getByText(title).click();
@ -57,7 +76,10 @@ export class SidebarPage extends BasePage {
httpMethodsToMatch: ['POST'], httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: `api/v1/db/meta/projects/`, requestUrlPathToMatch: `api/v1/db/meta/projects/`,
}); });
await this.dashboard.docs.pagesList.waitForOpen({ title });
if (type === ProjectTypes.DOCUMENTATION) {
await this.dashboard.docs.pagesList.waitForOpen({ title });
}
} }
async createView({ title, type }: { title: string; type: ViewTypes }) { async createView({ title, type }: { title: string; type: ViewTypes }) {

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

@ -291,7 +291,7 @@ export class TreeViewPage extends BasePage {
// add new table button & context menu is visible only for owner & creator // 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-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 // table context menu
const tblNode = await this.getTable({ index: 0, tableTitle: param.tableTitle }); const tblNode = await this.getTable({ index: 0, tableTitle: param.tableTitle });

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

@ -369,11 +369,8 @@ export class CellPageObject extends BasePage {
.waitFor({ state: 'visible', timeout: 3000 }); .waitFor({ state: 'visible', timeout: 3000 });
await this.waitForResponse({ await this.waitForResponse({
uiAction: async () => uiAction: () =>
await this.rootPage this.rootPage.locator(`[data-testid="nc-child-list-item"]`).last().click({ force: true, timeout: 3000 }),
.locator(`[data-testid="nc-child-list-item"]`)
.last()
.click({ force: true, timeout: 3000 }),
requestUrlPathToMatch: '/api/v1/db/data/noco/', requestUrlPathToMatch: '/api/v1/db/data/noco/',
httpMethodsToMatch: ['GET'], httpMethodsToMatch: ['GET'],
}); });

20
tests/playwright/setup/index.ts

@ -153,6 +153,8 @@ export interface NcContext {
workerId?: string; workerId?: string;
rootUser: UserType & { password: string }; rootUser: UserType & { password: string };
workspace: WorkspaceType; workspace: WorkspaceType;
defaultProjectTitle: string;
defaultTableTitle: string;
} }
selectors.setTestIdAttribute('data-testid'); selectors.setTestIdAttribute('data-testid');
@ -384,7 +386,12 @@ const setup = async ({
email: `user@nocodb.com`, email: `user@nocodb.com`,
password: getDefaultPwd(), 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) { } catch (e) {
// ignore error: some roles will not have permission for license reset // ignore error: some roles will not have permission for license reset
// console.error(`Error resetting project: ${process.env.TEST_PARALLEL_INDEX}`, e); // console.error(`Error resetting project: ${process.env.TEST_PARALLEL_INDEX}`, e);
@ -436,7 +443,16 @@ const setup = async ({
} }
await page.goto(projectUrl, { waitUntil: 'networkidle' }); 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> => {}; export const unsetup = async (context: NcContext): Promise<void> => {};

Loading…
Cancel
Save