Browse Source

Merge pull request #4393 from nocodb/refactor/playwright-refactor

refactor(test): Playwright followup
pull/4471/head
navi 2 years ago committed by GitHub
parent
commit
1de5c8f630
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      packages/nc-gui/components/tabs/auth/user-management/UsersModal.vue
  2. 22
      packages/nc-gui/pages/index/index/[projectId].vue
  3. 19
      packages/nc-gui/pages/index/index/create.vue
  4. 5
      packages/nc-gui/pages/index/index/user.vue
  5. 51
      tests/playwright/pages/Account/ChangePassword.ts
  6. 3
      tests/playwright/pages/Account/Users.ts
  7. 10
      tests/playwright/pages/Account/index.ts
  8. 2
      tests/playwright/pages/Dashboard/Grid/index.ts
  9. 24
      tests/playwright/pages/Dashboard/Settings/Teams.ts
  10. 1
      tests/playwright/pages/Dashboard/TreeView.ts
  11. 1
      tests/playwright/pages/Dashboard/WebhookForm/index.ts
  12. 8
      tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts
  13. 25
      tests/playwright/pages/Dashboard/common/Toolbar/index.ts
  14. 32
      tests/playwright/pages/Dashboard/index.ts
  15. 50
      tests/playwright/pages/ProjectsPage/index.ts
  16. 2
      tests/playwright/tests/accountUserSettings.spec.ts
  17. 27
      tests/playwright/tests/authChangePassword.spec.ts
  18. 2
      tests/playwright/tests/metaSync.spec.ts
  19. 3
      tests/playwright/tests/toolbarOperations.spec.ts
  20. 7
      tests/playwright/tests/viewGridShare.spec.ts
  21. 9
      tests/playwright/tests/viewKanban.spec.ts

9
packages/nc-gui/components/tabs/auth/user-management/UsersModal.vue

@ -150,11 +150,16 @@ const emailField = (inputEl: typeof Input) => {
wrap-class-name="nc-modal-invite-user-and-share-base" wrap-class-name="nc-modal-invite-user-and-share-base"
@cancel="emit('closed')" @cancel="emit('closed')"
> >
<div class="flex flex-col"> <div class="flex flex-col" data-testid="invite-user-and-share-base-modal">
<div class="flex flex-row justify-between items-center pb-1.5 mb-2 border-b-1 w-full"> <div class="flex flex-row justify-between items-center pb-1.5 mb-2 border-b-1 w-full">
<a-typography-title class="select-none" :level="4"> {{ $t('activity.share') }}: {{ project.title }} </a-typography-title> <a-typography-title class="select-none" :level="4"> {{ $t('activity.share') }}: {{ project.title }} </a-typography-title>
<a-button type="text" class="!rounded-md mr-1 -mt-1.5" @click="emit('closed')"> <a-button
type="text"
class="!rounded-md mr-1 -mt-1.5"
data-testid="invite-user-and-share-base-modal-close-btn"
@click="emit('closed')"
>
<template #icon> <template #icon>
<MaterialSymbolsCloseRounded class="flex mx-auto" /> <MaterialSymbolsCloseRounded class="flex mx-auto" />
</template> </template>

22
packages/nc-gui/pages/index/index/[projectId].vue

@ -1,6 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Form } from 'ant-design-vue' import type { Form } from 'ant-design-vue'
import type { ProjectType } from 'nocodb-sdk' import type { ProjectType } from 'nocodb-sdk'
import type { VNodeRef } from '@vue/runtime-core'
import { import {
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
message, message,
@ -8,14 +9,13 @@ import {
projectTitleValidator, projectTitleValidator,
reactive, reactive,
ref, ref,
tryOnMounted,
useProject, useProject,
useRoute, useRoute,
} from '#imports' } from '#imports'
const route = useRoute() const route = useRoute()
const { project, loadProject, updateProject, isLoading, projectLoadedHook } = useProject() const { loadProject, updateProject, isLoading } = useProject()
loadProject(false) loadProject(false)
@ -43,21 +43,7 @@ const renameProject = async () => {
} }
} }
// select and focus title field on load const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
projectLoadedHook(async () => {
formState.title = project.value.title as string
tryOnMounted(() => {
// todo: replace setTimeout and follow better approach
setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]')
input.focus()
input.setSelectionRange(0, formState.title?.length)
}, 150)
})
})
</script> </script>
<template> <template>
@ -89,7 +75,7 @@ projectLoadedHook(async () => {
@finish="renameProject" @finish="renameProject"
> >
<a-form-item :label="$t('labels.projName')" name="title" :rules="nameValidationRules"> <a-form-item :label="$t('labels.projName')" name="title" :rules="nameValidationRules">
<a-input v-model:value="formState.title" name="title" class="nc-metadb-project-name" /> <a-input :ref="focus" v-model:value="formState.title" name="title" class="nc-metadb-project-name" />
</a-form-item> </a-form-item>
<div class="text-center"> <div class="text-center">

19
packages/nc-gui/pages/index/index/create.vue

@ -1,11 +1,10 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { Form } from 'ant-design-vue' import type { Form } from 'ant-design-vue'
import type { VNodeRef } from '@vue/runtime-core'
import { import {
extractSdkResponseErrorMsg, extractSdkResponseErrorMsg,
message, message,
navigateTo, navigateTo,
nextTick,
onMounted,
projectTitleValidator, projectTitleValidator,
reactive, reactive,
ref, ref,
@ -47,19 +46,7 @@ const createProject = async () => {
} }
} }
// select and focus title field on load const focus: VNodeRef = (el) => (el as HTMLInputElement)?.focus()
onMounted(async () => {
await nextTick(() => {
// todo: replace setTimeout and follow better approach
setTimeout(() => {
const input = form.value?.$el?.querySelector('input[type=text]')
input.setSelectionRange(0, formState.title.length)
input.focus()
}, 500)
})
})
</script> </script>
<template> <template>
@ -88,7 +75,7 @@ onMounted(async () => {
@finish="createProject" @finish="createProject"
> >
<a-form-item :label="$t('labels.projName')" name="title" :rules="nameValidationRules" class="m-10"> <a-form-item :label="$t('labels.projName')" name="title" :rules="nameValidationRules" class="m-10">
<a-input v-model:value="formState.title" name="title" class="nc-metadb-project-name" /> <a-input :ref="focus" v-model:value="formState.title" name="title" class="nc-metadb-project-name" />
</a-form-item> </a-form-item>
<div class="text-center"> <div class="text-center">

5
packages/nc-gui/pages/index/index/user.vue

@ -68,7 +68,10 @@ const resetError = () => {
</script> </script>
<template> <template>
<div class="relative flex flex-col justify-center gap-2 w-full p-8 md:(bg-white rounded-lg border-1 border-gray-200 shadow)"> <div
class="relative flex flex-col justify-center gap-2 w-full p-8 md:(bg-white rounded-lg border-1 border-gray-200 shadow)"
data-testid="user-change-password"
>
<LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent)" :animate="isLoading" /> <LazyGeneralNocoIcon class="color-transition hover:(ring ring-accent)" :animate="isLoading" />
<div <div

51
tests/playwright/pages/Account/ChangePassword.ts

@ -0,0 +1,51 @@
import { expect, Page } from '@playwright/test';
import BasePage from '../Base';
export class ChangePasswordPage extends BasePage {
constructor(rootPage: Page) {
super(rootPage);
}
get() {
return this.rootPage.getByTestId('nc-user-settings-form');
}
async changePassword({
oldPass,
newPass,
repeatPass,
networkValidation,
}: {
oldPass: string;
newPass: string;
repeatPass: string;
networkValidation?: boolean;
}) {
const currentPassword = this.get().locator('input[data-testid="nc-user-settings-form__current-password"]');
const newPassword = this.get().locator('input[data-testid="nc-user-settings-form__new-password"]');
const confirmPassword = this.get().locator('input[data-testid="nc-user-settings-form__new-password-repeat"]');
await currentPassword.fill(oldPass);
await newPassword.fill(newPass);
await confirmPassword.fill(repeatPass);
const submitChangePassword = this.get().locator('button[data-testid="nc-user-settings-form__submit"]').click();
if (networkValidation) {
await this.waitForResponse({
uiAction: submitChangePassword,
httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: 'api/v1/auth/password/change',
});
} else {
await submitChangePassword;
}
}
async verifyFormError({ error }: { error: string }) {
await expect(this.get().getByTestId('nc-user-settings-form__error')).toHaveText(error);
}
async verifyPasswordDontMatchError() {
await expect(this.rootPage.locator('.ant-form-item-explain-error')).toHaveText('Passwords do not match');
}
}

3
tests/playwright/pages/Account/Users.ts

@ -1,10 +1,12 @@
import { Locator } from '@playwright/test'; import { Locator } from '@playwright/test';
import BasePage from '../Base'; import BasePage from '../Base';
import { ChangePasswordPage } from './ChangePassword';
import { AccountPage } from './index'; import { AccountPage } from './index';
export class AccountUsersPage extends BasePage { export class AccountUsersPage extends BasePage {
readonly inviteUserBtn: Locator; readonly inviteUserBtn: Locator;
readonly inviteUserModal: Locator; readonly inviteUserModal: Locator;
readonly changePasswordPage: ChangePasswordPage;
private accountPage: AccountPage; private accountPage: AccountPage;
constructor(accountPage: AccountPage) { constructor(accountPage: AccountPage) {
@ -12,6 +14,7 @@ export class AccountUsersPage extends BasePage {
this.accountPage = accountPage; this.accountPage = accountPage;
this.inviteUserBtn = this.get().locator(`[data-testid="nc-super-user-invite"]`); this.inviteUserBtn = this.get().locator(`[data-testid="nc-super-user-invite"]`);
this.inviteUserModal = accountPage.rootPage.locator(`.nc-modal-invite-user`); this.inviteUserModal = accountPage.rootPage.locator(`.nc-modal-invite-user`);
this.changePasswordPage = new ChangePasswordPage(this.rootPage);
} }
async goto() { async goto() {

10
tests/playwright/pages/Account/index.ts

@ -1,9 +1,19 @@
import { Page } from '@playwright/test'; import { Page } from '@playwright/test';
import BasePage from '../Base'; import BasePage from '../Base';
import { AccountSettingsPage } from './Settings';
import { AccountTokenPage } from './Token';
import { AccountUsersPage } from './Users';
export class AccountPage extends BasePage { export class AccountPage extends BasePage {
readonly settings: AccountSettingsPage;
readonly token: AccountTokenPage;
readonly users: AccountUsersPage;
constructor(page: Page) { constructor(page: Page) {
super(page); super(page);
this.settings = new AccountSettingsPage(this);
this.token = new AccountTokenPage(this);
this.users = new AccountUsersPage(this);
} }
get() { get() {

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

@ -132,7 +132,7 @@ export class GridPage extends BasePage {
} }
async deleteRow(index: number) { async deleteRow(index: number) {
await this.get().locator(`td[data-testid="cell-Title-${index}"]`).click({ await this.get().getByTestId(`cell-Title-${index}`).click({
button: 'right', button: 'right',
}); });

24
tests/playwright/pages/Dashboard/Settings/Teams.ts

@ -1,32 +1,31 @@
import { expect, Locator } from '@playwright/test'; import { Locator } from '@playwright/test';
import { SettingsPage } from '.'; import { SettingsPage } from '.';
import BasePage from '../../Base'; import BasePage from '../../Base';
import { writeFileAsync } from 'xlsx';
import { ToolbarPage } from '../common/Toolbar';
export class TeamsPage extends BasePage { export class TeamsPage extends BasePage {
private readonly settings: SettingsPage; private readonly settings: SettingsPage;
readonly inviteTeamBtn: Locator; private readonly inviteTeamBtn: Locator;
readonly inviteTeamModal: Locator; private readonly inviteTeamModal: Locator;
constructor(settings: SettingsPage) { constructor(settings: SettingsPage) {
super(settings.rootPage); super(settings.rootPage);
this.settings = settings; this.settings = settings;
this.inviteTeamBtn = this.get().locator(`button:has-text("Invite Team")`); this.inviteTeamBtn = this.get().locator(`button:has-text("Invite Team")`);
this.inviteTeamModal = this.rootPage.locator(`.nc-modal-invite-user-and-share-base`); this.inviteTeamModal = this.rootPage.getByTestId('invite-user-and-share-base-modal');
} }
get() { get() {
return this.settings.get().locator(`[data-testid="nc-settings-subtab-Users Management"]`); return this.settings.get().getByTestId('nc-settings-subtab-Users Management');
} }
// Prefixing to differentiate between emails created by the tests which are deleted after the test run
prefixEmail(email: string) { prefixEmail(email: string) {
const parallelId = process.env.TEST_PARALLEL_INDEX ?? '0'; const parallelId = process.env.TEST_PARALLEL_INDEX ?? '0';
return `nc_test_${parallelId}_${email}`; return `nc_test_${parallelId}_${email}`;
} }
getSharedBaseSubModal() { getSharedBaseSubModal() {
return this.rootPage.locator(`[data-testid="nc-share-base-sub-modal"]`); return this.rootPage.getByTestId('nc-share-base-sub-modal');
} }
async invite({ email, role }: { email: string; role: string }) { async invite({ email, role }: { email: string; role: string }) {
@ -44,8 +43,8 @@ export class TeamsPage extends BasePage {
} }
async closeInvite() { async closeInvite() {
// two btn-icon-only in invite modal: close & copy url // todo: Fix the case where there is ghost dom for previous modal
await this.inviteTeamModal.locator(`button.ant-btn-icon-only:visible`).first().click(); await this.inviteTeamModal.getByTestId('invite-user-and-share-base-modal-close-btn').last().click();
} }
async inviteMore() { async inviteMore() {
@ -53,7 +52,7 @@ export class TeamsPage extends BasePage {
} }
async toggleSharedBase({ toggle }: { toggle: boolean }) { async toggleSharedBase({ toggle }: { toggle: boolean }) {
const toggleBtn = await this.getSharedBaseSubModal().locator(`.nc-disable-shared-base`); const toggleBtn = this.getSharedBaseSubModal().locator(`.nc-disable-shared-base`);
const toggleBtnText = await toggleBtn.first().innerText(); const toggleBtnText = await toggleBtn.first().innerText();
const disabledBase = toggleBtnText.includes('Disable'); const disabledBase = toggleBtnText.includes('Disable');
@ -76,8 +75,7 @@ export class TeamsPage extends BasePage {
} }
async getSharedBaseUrl() { async getSharedBaseUrl() {
const url = await this.getSharedBaseSubModal().locator(`.nc-url:visible`).innerText(); return await this.getSharedBaseSubModal().locator(`.nc-url:visible`).textContent();
return url;
} }
async sharedBaseActions({ action }: { action: string }) { async sharedBaseActions({ action }: { action: string }) {

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

@ -57,6 +57,7 @@ export class TreeViewPage extends BasePage {
responseJsonMatcher: json => json.title === title && json.type === 'table', responseJsonMatcher: json => json.title === title && json.type === 'table',
}); });
// Tab render is slow for playwright
await this.dashboard.waitForTabRender({ title }); await this.dashboard.waitForTabRender({ title });
} }

1
tests/playwright/pages/Dashboard/WebhookForm/index.ts

@ -23,7 +23,6 @@ export class WebhookFormPage extends BasePage {
return this.dashboard.get().locator(`.nc-drawer-webhook-body`); return this.dashboard.get().locator(`.nc-drawer-webhook-body`);
} }
// todo: Removing opening webhook drawer logic as it belongs to `Toolbar` page
async create({ title, event, url = 'http://localhost:9090/hook' }: { title: string; event: string; url?: string }) { async create({ title, event, url = 'http://localhost:9090/hook' }: { title: string; event: string; url?: string }) {
await this.toolbar.clickActions(); await this.toolbar.clickActions();
await this.toolbar.actions.click('Webhooks'); await this.toolbar.actions.click('Webhooks');

8
tests/playwright/pages/Dashboard/common/Toolbar/Filter.ts

@ -40,11 +40,6 @@ export class ToolbarFilterPage extends BasePage {
value: string; value: string;
isLocallySaved: boolean; isLocallySaved: boolean;
}) { }) {
await this.toolbar.clickFilter();
// todo: If the filter menu is open for the first time for the table, there can will be a api call which will re render the filter menu
await this.rootPage.waitForTimeout(1000);
await this.get().locator(`button:has-text("Add Filter")`).first().click(); await this.get().locator(`button:has-text("Add Filter")`).first().click();
await this.rootPage.locator('.nc-filter-field-select').last().click(); await this.rootPage.locator('.nc-filter-field-select').last().click();
@ -82,9 +77,6 @@ export class ToolbarFilterPage extends BasePage {
requestUrlPathToMatch: isLocallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`, requestUrlPathToMatch: isLocallySaved ? `/api/v1/db/public/` : `/api/v1/db/data/noco/`,
}); });
await this.toolbar.parent.dashboard.waitForLoaderToDisappear(); await this.toolbar.parent.dashboard.waitForLoaderToDisappear();
await this.toolbar.clickFilter();
await this.toolbar.parent.waitLoading(); await this.toolbar.parent.waitLoading();
} }

25
tests/playwright/pages/Dashboard/common/Toolbar/index.ts

@ -69,13 +69,30 @@ export class ToolbarPage extends BasePage {
if (menuOpen) await this.sort.get().waitFor({ state: 'hidden' }); if (menuOpen) await this.sort.get().waitFor({ state: 'hidden' });
} }
async clickFilter() { async clickFilter({
// `networkValidation` is used to verify that api calls are made when the button is clicked
// which happens when the filter is opened for the first time
networkValidation,
}: { networkValidation?: boolean } = {}) {
const menuOpen = await this.filter.get().isVisible(); const menuOpen = await this.filter.get().isVisible();
await this.get().locator(`button.nc-filter-menu-btn`).click(); const clickFilterAction = this.get().locator(`button.nc-filter-menu-btn`).click();
// Wait for the menu to close // Wait for the menu to close
if (menuOpen) await this.filter.get().waitFor({ state: 'hidden' }); if (menuOpen) {
await clickFilterAction;
await this.filter.get().waitFor({ state: 'hidden' });
} else {
if (networkValidation) {
// Since on opening filter menu, api is called to fetch filter options, and will rerender the menu
await this.waitForResponse({
uiAction: clickFilterAction,
requestUrlPathToMatch: '/api/v1/db',
httpMethodsToMatch: ['GET'],
});
} else {
await clickFilterAction;
}
}
} }
async clickShareView() { async clickShareView() {

32
tests/playwright/pages/Dashboard/index.ts

@ -61,7 +61,7 @@ export class DashboardPage extends BasePage {
} }
async gotoSettings() { async gotoSettings() {
await this.rootPage.locator('[data-testid="nc-project-menu"]').click(); await this.rootPage.getByTestId('nc-project-menu').click();
await this.rootPage.locator('div.nc-project-menu-item:has-text(" Team & Settings")').click(); await this.rootPage.locator('div.nc-project-menu-item:has-text(" Team & Settings")').click();
} }
@ -79,9 +79,6 @@ export class DashboardPage extends BasePage {
} }
async clickHome() { async clickHome() {
// todo: Fast page transition breaks the vue router
await this.rootPage.waitForTimeout(2000);
await this.rootPage.getByTestId('nc-noco-brand-icon').click(); await this.rootPage.getByTestId('nc-noco-brand-icon').click();
const projectsPage = new ProjectsPage(this.rootPage); const projectsPage = new ProjectsPage(this.rootPage);
await projectsPage.waitToBeRendered(); await projectsPage.waitToBeRendered();
@ -124,32 +121,9 @@ export class DashboardPage extends BasePage {
} }
} }
async openPasswordChangeModal() {
// open change password portal
await this.rootPage.locator('.nc-menu-accounts').click();
await this.rootPage
.locator('.nc-dropdown-user-accounts-menu')
.getByTestId('nc-menu-accounts__user-settings')
.click();
}
// todo: Move this to a seperate page
async changePassword({ oldPass, newPass, repeatPass }: { oldPass: string; newPass: string; repeatPass: string }) {
// change password
const currentPassword = this.rootPage.locator('input[data-testid="nc-user-settings-form__current-password"]');
const newPassword = this.rootPage.locator('input[data-testid="nc-user-settings-form__new-password"]');
const confirmPassword = this.rootPage.locator('input[data-testid="nc-user-settings-form__new-password-repeat"]');
await currentPassword.fill(oldPass);
await newPassword.fill(newPass);
await confirmPassword.fill(repeatPass);
await this.rootPage.locator('button[data-testid="nc-user-settings-form__submit"]').click();
}
async signOut() { async signOut() {
await this.rootPage.locator('[data-testid="nc-project-menu"]').click(); await this.rootPage.getByTestId('nc-project-menu').click();
const projMenu = await this.rootPage.locator('.nc-dropdown-project-menu'); const projMenu = this.rootPage.locator('.nc-dropdown-project-menu');
await projMenu.locator('[data-menu-id="account"]:visible').click(); await projMenu.locator('[data-menu-id="account"]:visible').click();
await this.rootPage.locator('div.nc-project-menu-item:has-text("Sign Out"):visible').click(); await this.rootPage.locator('div.nc-project-menu-item:has-text("Sign Out"):visible').click();
await this.rootPage.locator('[data-testid="nc-form-signin"]:visible').waitFor(); await this.rootPage.locator('[data-testid="nc-form-signin"]:visible').waitFor();

50
tests/playwright/pages/ProjectsPage/index.ts

@ -28,7 +28,7 @@ export class ProjectsPage extends BasePage {
}) { }) {
if (!withoutPrefix) name = this.prefixTitle(name); if (!withoutPrefix) name = this.prefixTitle(name);
await this.rootPage.locator('.nc-new-project-menu').click(); await this.get().locator('.nc-new-project-menu').click();
const createProjectMenu = await this.rootPage.locator('.nc-dropdown-create-project'); const createProjectMenu = await this.rootPage.locator('.nc-dropdown-create-project');
@ -38,20 +38,18 @@ export class ProjectsPage extends BasePage {
await createProjectMenu.locator(`.ant-dropdown-menu-title-content`).nth(1).click(); await createProjectMenu.locator(`.ant-dropdown-menu-title-content`).nth(1).click();
} }
// todo: Fast page transition breaks the vue router
await this.rootPage.waitForTimeout(2000);
await this.rootPage.locator(`.nc-metadb-project-name`).waitFor(); await this.rootPage.locator(`.nc-metadb-project-name`).waitFor();
await this.rootPage.locator(`input.nc-metadb-project-name`).fill(name); await this.rootPage.locator(`input.nc-metadb-project-name`).fill(name);
await this.rootPage.waitForTimeout(2000); const createProjectSubmitAction = this.rootPage.locator(`button:has-text("Create")`).click();
await this.waitForResponse({
await this.rootPage.locator(`button:has-text("Create")`).click({ uiAction: createProjectSubmitAction,
delay: 2000, httpMethodsToMatch: ['POST'],
requestUrlPathToMatch: '/api/v1/db/meta/projects/',
}); });
// fix me! wait for page to be rendered completely // wait for dashboard to render
await this.rootPage.waitForTimeout(2000); await this.rootPage.locator('.nc-container').waitFor({ state: 'visible' });
} }
async checkProjectCreateButton({ exists = true }) { async checkProjectCreateButton({ exists = true }) {
@ -91,9 +89,6 @@ export class ProjectsPage extends BasePage {
withoutPrefix?: boolean; withoutPrefix?: boolean;
waitForAuthTab?: boolean; waitForAuthTab?: boolean;
}) { }) {
// todo: Fast page transition breaks the vue router
await this.rootPage.waitForTimeout(2000);
if (!withoutPrefix) title = this.prefixTitle(title); if (!withoutPrefix) title = this.prefixTitle(title);
let project: any; let project: any;
@ -138,7 +133,13 @@ export class ProjectsPage extends BasePage {
if (!withoutPrefix) title = this.prefixTitle(title); if (!withoutPrefix) title = this.prefixTitle(title);
await this.get().locator(`[data-testid="delete-project-${title}"]`).click(); await this.get().locator(`[data-testid="delete-project-${title}"]`).click();
await this.rootPage.locator(`button:has-text("Yes")`).click();
const deleteProjectAction = this.rootPage.locator(`button:has-text("Yes")`).click();
await this.waitForResponse({
uiAction: deleteProjectAction,
httpMethodsToMatch: ['DELETE'],
requestUrlPathToMatch: '/api/v1/db/meta/projects/',
});
await this.get().locator('.ant-table-row', { hasText: title }).waitFor({ state: 'hidden' }); await this.get().locator('.ant-table-row', { hasText: title }).waitFor({ state: 'hidden' });
} }
@ -161,9 +162,6 @@ export class ProjectsPage extends BasePage {
}); });
await projRow.locator('.nc-action-btn').nth(0).click(); await projRow.locator('.nc-action-btn').nth(0).click();
// todo: Fast page transition breaks the vue router
await this.rootPage.waitForTimeout(2000);
await project.locator('input.nc-metadb-project-name').fill(newTitle); await project.locator('input.nc-metadb-project-name').fill(newTitle);
// press enter to save // press enter to save
const submitAction = project.locator('input.nc-metadb-project-name').press('Enter'); const submitAction = project.locator('input.nc-metadb-project-name').press('Enter');
@ -172,9 +170,6 @@ export class ProjectsPage extends BasePage {
requestUrlPathToMatch: 'api/v1/db/meta/projects/', requestUrlPathToMatch: 'api/v1/db/meta/projects/',
httpMethodsToMatch: ['PATCH'], httpMethodsToMatch: ['PATCH'],
}); });
// todo: vue navigation breaks if page changes very quickly
await this.rootPage.waitForTimeout(1000);
} }
async openLanguageMenu() { async openLanguageMenu() {
@ -187,10 +182,23 @@ export class ProjectsPage extends BasePage {
} }
async verifyLanguage(param: { json: any }) { async verifyLanguage(param: { json: any }) {
const title = await this.rootPage.locator(`.nc-project-page-title`); const title = this.rootPage.locator(`.nc-project-page-title`);
const menu = this.rootPage.locator(`.nc-new-project-menu`); const menu = this.rootPage.locator(`.nc-new-project-menu`);
await expect(title).toHaveText(param.json.title.myProject); await expect(title).toHaveText(param.json.title.myProject);
await expect(menu).toHaveText(param.json.title.newProj); await expect(menu).toHaveText(param.json.title.newProj);
await this.rootPage.locator(`[placeholder="${param.json.activity.searchProject}"]`).waitFor(); await this.rootPage.locator(`[placeholder="${param.json.activity.searchProject}"]`).waitFor();
} }
async openPasswordChangeModal() {
// open change password portal
await this.rootPage.locator('.nc-menu-accounts').click();
await this.rootPage
.locator('.nc-dropdown-user-accounts-menu')
.getByTestId('nc-menu-accounts__user-settings')
.click();
}
async waitForRender() {
await this.rootPage.locator('.nc-project-page-title:has-text("My Projects")').waitFor();
}
} }

2
tests/playwright/tests/accountUserSettings.spec.ts

@ -13,7 +13,7 @@ test.describe('App settings', () => {
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
context = await setup({ page }); context = await setup({ page });
accountPage = new AccountPage(page); accountPage = new AccountPage(page);
accountSettingsPage = new AccountSettingsPage(accountPage); accountSettingsPage = accountPage.settings;
}); });
test('Toggle invite only signup', async () => { test('Toggle invite only signup', async () => {

27
tests/playwright/tests/authChangePassword.spec.ts

@ -4,17 +4,24 @@ import setup from '../setup';
import { LoginPage } from '../pages/LoginPage'; import { LoginPage } from '../pages/LoginPage';
import { SettingsPage, SettingTab } from '../pages/Dashboard/Settings'; import { SettingsPage, SettingTab } from '../pages/Dashboard/Settings';
import { SignupPage } from '../pages/SignupPage'; import { SignupPage } from '../pages/SignupPage';
import { ProjectsPage } from '../pages/ProjectsPage';
import { AccountPage } from '../pages/Account';
test.describe('Auth', () => { test.describe('Auth', () => {
let context: any;
let dashboard: DashboardPage; let dashboard: DashboardPage;
let settings: SettingsPage; let settings: SettingsPage;
let context: any;
let signupPage: SignupPage; let signupPage: SignupPage;
let projectsPage: ProjectsPage;
let accountPage: AccountPage;
test.beforeEach(async ({ page }) => { test.beforeEach(async ({ page }) => {
context = await setup({ page }); context = await setup({ page });
dashboard = new DashboardPage(page, context.project); dashboard = new DashboardPage(page, context.project);
signupPage = new SignupPage(page); signupPage = new SignupPage(page);
projectsPage = new ProjectsPage(page);
accountPage = new AccountPage(page);
settings = dashboard.settings; settings = dashboard.settings;
}); });
@ -37,31 +44,31 @@ test.describe('Auth', () => {
password: 'Password123.', password: 'Password123.',
}); });
await dashboard.openPasswordChangeModal(); await projectsPage.openPasswordChangeModal();
// Existing active pass incorrect // Existing active pass incorrect
await dashboard.changePassword({ await accountPage.users.changePasswordPage.changePassword({
oldPass: '123456789', oldPass: '123456789',
newPass: '123456789', newPass: '123456789',
repeatPass: '123456789', repeatPass: '123456789',
}); });
await dashboard.rootPage await accountPage.users.changePasswordPage.verifyFormError({ error: 'Current password is wrong' });
.locator('[data-testid="nc-user-settings-form__error"]:has-text("Current password is wrong")')
.waitFor();
// New pass and repeat pass mismatch // New pass and repeat pass mismatch
await dashboard.changePassword({ await accountPage.users.changePasswordPage.changePassword({
oldPass: 'Password123.', oldPass: 'Password123.',
newPass: '123456789', newPass: '123456789',
repeatPass: '987654321', repeatPass: '987654321',
networkValidation: false,
}); });
await dashboard.rootPage.locator('.ant-form-item-explain-error:has-text("Passwords do not match")').waitFor(); await accountPage.users.changePasswordPage.verifyPasswordDontMatchError();
// All good // All good
await dashboard.changePassword({ await accountPage.users.changePasswordPage.changePassword({
oldPass: 'Password123.', oldPass: 'Password123.',
newPass: 'NewPasswordConfigured', newPass: 'NewPasswordConfigured',
repeatPass: 'NewPasswordConfigured', repeatPass: 'NewPasswordConfigured',
networkValidation: true,
}); });
const loginPage = new LoginPage(page); const loginPage = new LoginPage(page);
@ -69,6 +76,6 @@ test.describe('Auth', () => {
await loginPage.fillPassword('NewPasswordConfigured'); await loginPage.fillPassword('NewPasswordConfigured');
await loginPage.submit(); await loginPage.submit();
await page.locator('.nc-project-page-title:has-text("My Projects")').waitFor(); await projectsPage.waitForRender();
}); });
}); });

2
tests/playwright/tests/metaSync.spec.ts

@ -260,12 +260,14 @@ test.describe('Meta sync', () => {
isLocallySaved: false, isLocallySaved: false,
}); });
await dashboard.grid.toolbar.clickFilter();
await dashboard.grid.toolbar.filter.addNew({ await dashboard.grid.toolbar.filter.addNew({
columnTitle: 'Col1', columnTitle: 'Col1',
opType: '>=', opType: '>=',
value: '5', value: '5',
isLocallySaved: false, isLocallySaved: false,
}); });
await dashboard.grid.toolbar.clickFilter();
await dashboard.grid.verifyRowCount({ count: 5 }); await dashboard.grid.verifyRowCount({ count: 5 });
}); });

3
tests/playwright/tests/toolbarOperations.spec.ts

@ -56,12 +56,15 @@ test.describe('Toolbar operations (GRID)', () => {
await validateFirstRow('Afghanistan'); await validateFirstRow('Afghanistan');
// Filter column // Filter column
await toolbar.clickFilter();
await toolbar.filter.addNew({ await toolbar.filter.addNew({
columnTitle: 'Country', columnTitle: 'Country',
value: 'India', value: 'India',
opType: 'is equal', opType: 'is equal',
isLocallySaved: false, isLocallySaved: false,
}); });
await toolbar.clickFilter();
await validateFirstRow('India'); await validateFirstRow('India');
// Reset filter // Reset filter

7
tests/playwright/tests/viewGridShare.spec.ts

@ -39,12 +39,14 @@ test.describe('Shared view', () => {
isLocallySaved: false, isLocallySaved: false,
}); });
// filter // filter
await dashboard.grid.toolbar.clickFilter();
await dashboard.grid.toolbar.filter.addNew({ await dashboard.grid.toolbar.filter.addNew({
columnTitle: 'Address', columnTitle: 'Address',
value: 'Ab', value: 'Ab',
opType: 'is like', opType: 'is like',
isLocallySaved: false, isLocallySaved: false,
}); });
await dashboard.grid.toolbar.clickFilter();
// share with password disabled, download enabled // share with password disabled, download enabled
await dashboard.grid.toolbar.clickShareView(); await dashboard.grid.toolbar.clickShareView();
@ -106,12 +108,14 @@ test.describe('Shared view', () => {
}); });
if (isMysql(context)) { if (isMysql(context)) {
await sharedPage.grid.toolbar.clickFilter();
await sharedPage.grid.toolbar.filter.addNew({ await sharedPage.grid.toolbar.filter.addNew({
columnTitle: 'District', columnTitle: 'District',
value: 'Ta', value: 'Ta',
opType: 'is like', opType: 'is like',
isLocallySaved: true, isLocallySaved: true,
}); });
await sharedPage.grid.toolbar.clickFilter();
} }
await sharedPage.grid.toolbar.fields.toggle({ title: 'LastUpdate', isLocallySaved: true }); await sharedPage.grid.toolbar.fields.toggle({ title: 'LastUpdate', isLocallySaved: true });
expectedColumns[6].isVisible = false; expectedColumns[6].isVisible = false;
@ -191,12 +195,15 @@ test.describe('Shared view', () => {
title: 'New Column', title: 'New Column',
isVisible: true, isVisible: true,
}); });
await sharedPage2.grid.toolbar.clickFilter();
await sharedPage2.grid.toolbar.filter.addNew({ await sharedPage2.grid.toolbar.filter.addNew({
columnTitle: 'Country', columnTitle: 'Country',
value: 'New Country', value: 'New Country',
opType: 'is like', opType: 'is like',
isLocallySaved: true, isLocallySaved: true,
}); });
await sharedPage2.grid.toolbar.clickFilter();
await sharedPage2.grid.cell.verify({ await sharedPage2.grid.cell.verify({
index: 0, index: 0,
columnHeader: 'Country', columnHeader: 'Country',

9
tests/playwright/tests/viewKanban.spec.ts

@ -142,12 +142,17 @@ test.describe('View', () => {
}); });
// verify filter // verify filter
await toolbar.clickFilter({
networkValidation: true,
});
await toolbar.filter.addNew({ await toolbar.filter.addNew({
columnTitle: 'Title', columnTitle: 'Title',
opType: 'is like', opType: 'is like',
value: 'BA', value: 'BA',
isLocallySaved: false, isLocallySaved: false,
}); });
await toolbar.clickFilter();
// verify card order // verify card order
const order4 = [ const order4 = [
['BAKED CLEOPATRA', 'BALLROOM MOCKINGBIRD'], ['BAKED CLEOPATRA', 'BALLROOM MOCKINGBIRD'],
@ -188,12 +193,16 @@ test.describe('View', () => {
isAscending: false, isAscending: false,
isLocallySaved: false, isLocallySaved: false,
}); });
await toolbar.clickFilter();
await toolbar.filter.addNew({ await toolbar.filter.addNew({
columnTitle: 'Title', columnTitle: 'Title',
opType: 'is like', opType: 'is like',
value: 'BA', value: 'BA',
isLocallySaved: false, isLocallySaved: false,
}); });
await toolbar.clickFilter();
await toolbar.fields.hideAll(); await toolbar.fields.hideAll();
await toolbar.fields.toggle({ title: 'Title' }); await toolbar.fields.toggle({ title: 'Title' });

Loading…
Cancel
Save