Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Projects tests #1390

Merged
merged 14 commits into from
May 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions test/e2e/components/notice.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Locator, Page } from "@playwright/test";

export class NoticeElement {
readonly page: Page;
readonly notice: Locator;
readonly noticeMessage: Locator;
readonly noticeDetails: Locator;

constructor(page: Page) {
this.page = page;
this.notice = page.locator('[ng-repeat="notice in $ctrl.notices()"]');
this.noticeMessage = page.locator('[data-ng-hide="notice.details"]');
this.noticeDetails = page.locator('[ng-show="notice.details"]');
}
}
87 changes: 76 additions & 11 deletions test/e2e/pages/projects.page.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { expect, Locator, Page } from '@playwright/test';

export type UserRoles =
'can manage' |
'can edit' |
'can comment' |
'can view'
;
'can manage' |
'can edit' |
'can comment' |
'can view'
;

export class ProjectsPage {
readonly page: Page;
readonly pageName: Locator;
readonly projectsList: Locator;
readonly projectNames: Locator;

readonly projectNameLinked: string;
readonly projectNameUnlinked: string;

readonly createButton: Locator;
readonly createNonSRProjectButton: Locator; // SR - send/receive
readonly projectNameInput: Locator;
Expand All @@ -28,6 +32,9 @@ export class ProjectsPage {
readonly shareProjectUserRoleDropdown: Locator;
readonly shareProjectSendInvitationButton: Locator;

readonly projectsPerPageDropdown: Locator;
readonly addAsTechSupportBtnText: string;

static readonly url: string = '/app/projects';

constructor(page: Page) {
Expand All @@ -36,6 +43,9 @@ export class ProjectsPage {
this.projectsList = page.locator('[data-ng-repeat="project in visibleProjects"]');
this.projectNames = this.projectsList.locator('a[href^="/app/lexicon"]');

this.projectNameLinked = 'projectNameLinked';
this.projectNameUnlinked = '';

this.createButton = page.locator('button:has-text("Start or Join a New Project")');
this.createNonSRProjectButton = page.locator('text=Create a non-send/receive project (not recommended)');
this.projectNameInput = page.locator('[placeholder="eg\\:\\ My\\ Dictionary"]');
Expand All @@ -50,7 +60,10 @@ export class ProjectsPage {
this.shareProjectButton = page.locator('span:has-text("Share")');
this.shareProjectEmailInput = page.locator('[placeholder="Email"]');
this.shareProjectUserRoleDropdown = page.locator('role-dropdown[target="\'email_invite\'"]');
this.shareProjectSendInvitationButton = page.locator('button[ng-click="$ctrl.sendEmailInvite()"]')
this.shareProjectSendInvitationButton = page.locator('button[ng-click="$ctrl.sendEmailInvite()"]');

this.projectsPerPageDropdown = page.locator('select[data-ng-model="$ctrl.itemsPerPage"]');
this.addAsTechSupportBtnText = 'text=Tech Support';
}

async goto() {
Expand All @@ -60,6 +73,9 @@ export class ProjectsPage {
//await this.page.waitForLoadState('domcontentloaded');
}
await expect(this.createButton).toBeVisible();
if (await this.projectsPerPageDropdown.isVisible()) {
await this.projectsPerPageDropdown.selectOption('100');
}
}

async createEmptyProject(projectName: string) {
Expand Down Expand Up @@ -93,14 +109,63 @@ export class ProjectsPage {
return await this.projectsList.count();
}

async findProject(projectName: string): Promise<string> {
// in order to be able to run the tests in parallel, this function only counts the projects created in that test file
async countSpecificProjects(projects: string): Promise<number> {
await this.goto();
if (await this.page.locator('text=' + projectName + ' >> nth=1').isVisible()) {
return 'text=' + projectName + ' >> nth=1';
const nAllProjects = await this.projectNames.count();
let nSpecificProjects = 0;
for (let i = 0; i < nAllProjects; i++) {
const projectName = await this.projectNames.nth(i).locator('span').innerText();
if (projectName.includes(projects)) {
nSpecificProjects++;
}
}
else {
return '-1';
return nSpecificProjects;
}

async findProject(projectName: string): Promise<string> {
await this.goto();
const foundElements = this.page.locator('span:has-text("' + projectName + '")');
const nFoundElements = await foundElements.count();
for (let i = 0; i < nFoundElements; i++) {
if (await foundElements.nth(i).isVisible()) {
return 'span:has-text("' + projectName + '") >> nth=' + i;
}
}
return '-1';
}

async findProjectRow(projectName: string): Promise<Locator> {
await this.goto();
const rowLocator = this.page.locator(`css=[data-ng-class="{active: $ctrl.isSelected(project)}"]:has(span:has-text("${projectName}"))`);
if (await rowLocator.count() == 1) {
return rowLocator;
}
return undefined;
}

async projectIsLinked(projectName: string): Promise<boolean> {
const rowLocator: Locator = await this.findProjectRow(projectName);
expect(rowLocator).not.toBeUndefined();
return rowLocator.locator('a').isVisible();
}

async projectLinkLocator(projectName: string): Promise<Locator> {
const rowLocator: Locator = await this.findProjectRow(projectName);
expect(rowLocator).not.toBeUndefined();
return rowLocator.locator('a');
}

async projectHasAddTechSupportButton(projectName: string): Promise<boolean> {
const rowLocator: Locator = await this.findProjectRow(projectName);
expect(rowLocator).not.toBeUndefined();
return rowLocator.locator('text=Tech Support').isVisible();
}

async projectAddTechSupportButtonLocator(projectName: string): Promise<Locator> {
const rowLocator: Locator = await this.findProjectRow(projectName);
expect(rowLocator).not.toBeUndefined();
return rowLocator.locator('text=Tech Support');
}

async clickOnProject(projectName: string) {
Expand Down
98 changes: 98 additions & 0 deletions test/e2e/projects-settings.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { expect } from '@playwright/test';
import { test } from './utils/fixtures';
import { ProjectSettingsPage } from './pages/project-settings.page';
import { initTestProject, addUserToProject } from './utils/testSetup';
import { ProjectsPage } from './pages/projects.page';
import { gotoProjectDirectly } from './utils/navigation';


export type Project = {
name: string,
code: string,
id: string
}

test.describe('E2E Project Settings app', () => {
let projectSettingsPageManager: ProjectSettingsPage;
const projects: Project[] = [
{
name: 'projects_settings_spec_ts Project 01',
code: 'p01_projects_settings_spec_ts__project_01',
id: ''
},
{
name: 'projects_settings_spec_ts Project 02',
code: 'p02_projects_settings_spec_ts__project_02',
id: ''
},
{
name: 'projects_settings_spec_ts Project 03',
code: 'p03_projects_settings_spec_ts__project_03',
id: ''
},
];
const project4: Project = {
name: 'projects_settings_spec_ts Project 04',
code: 'p04_projects_settings_spec_ts__project_04',
id: ''
};

test.beforeAll(async ({ request, admin, member, manager, managerTab }) => {
projectSettingsPageManager = new ProjectSettingsPage(managerTab);

for (const project of projects) {
const projectId = await initTestProject(request, project.code, project.name, admin.username, [member.username]);
project.id = projectId;
}
await addUserToProject(request, projects[0].code, manager.username, 'manager');
project4.id = await initTestProject(request, project4.code, project4.name, manager.username, []);
});


// test if can change project name

test('Normal user cannot access projectSettings to a project of which the user is a member', async ({ memberTab }) => {
const projectSettingsPage = new ProjectSettingsPage(memberTab);
await gotoProjectDirectly(projectSettingsPage.page, projects[0].id, projects[0].name);
await expect(projectSettingsPage.settingsMenuLink).not.toBeVisible();
});

test('Project owner can manage project they own', async ({ adminTab }) => {
const projectSettingsPage = new ProjectSettingsPage(adminTab);
await projectSettingsPage.gotoProjectSettingsDirectly(projects[0].id, projects[0].name);
expect(await projectSettingsPage.noticeList.count()).toBe(0);
await projectSettingsPage.deleteTab.tabTitle.click();
await expect(projectSettingsPage.deleteTab.deleteProjectButton).toBeVisible();
await expect(projectSettingsPage.deleteTab.deleteProjectButton).toBeDisabled();
});


test('Manager cannot view delete tab if not owner', async ({ manager }) => {
await projectSettingsPageManager.gotoProjectSettingsDirectly(projects[0].id, projects[0].name);
expect(await projectSettingsPageManager.projectTab.projectOwner.innerText()).not.toContain(manager.username);
await expect(projectSettingsPageManager.deleteTab.tabTitle).not.toBeVisible();
});


test('Manager can delete if owner', async () => {
await projectSettingsPageManager.projectsPage.goto();
const nProjects = await projectSettingsPageManager.projectsPage.countSpecificProjects('projects_settings_spec_ts');
await projectSettingsPageManager.gotoProjectSettingsDirectly(project4.id, project4.name);
expect(await projectSettingsPageManager.countNotices()).toBe(0);

await projectSettingsPageManager.deleteTab.tabTitle.click();
await expect(projectSettingsPageManager.deleteTab.deleteProjectButton).toBeVisible();
await expect(projectSettingsPageManager.deleteTab.deleteProjectButton).toBeDisabled();
await projectSettingsPageManager.deleteTab.confirmDeleteInput.fill('delete');
await expect(projectSettingsPageManager.deleteTab.deleteProjectButton).toBeEnabled();
await projectSettingsPageManager.deleteTab.deleteProjectButton.click();
await projectSettingsPageManager.deleteModal.confirm.click();

await projectSettingsPageManager.page.waitForNavigation({ url: ProjectsPage.url });
//Or...
// await projectSettingsPageManager.page.waitForNavigation({ waitUntil: 'networkidle' });

expect(await projectSettingsPageManager.projectsPage.countSpecificProjects('projects_settings_spec_ts')).toBe(nProjects - 1);
});

});
137 changes: 137 additions & 0 deletions test/e2e/projects.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { expect } from '@playwright/test';
import { test } from './utils/fixtures';

import { ProjectsPage } from './pages/projects.page';
import { NoticeElement } from './components/notice.component';

import { Project } from './projects-settings.spec';

import { initTestProject, addUserToProject } from './utils/testSetup';
import { gotoProjectDirectly } from './utils/navigation';


test.describe('E2E Projects List app', () => {
let projectsPageMember: ProjectsPage;
let projectsPageAdmin: ProjectsPage;

const projects: Project[] = [
{
name: 'projects_spec_ts Project 01',
code: 'p01_projects_spec_ts__project_01',
id: ''
},
{
name: 'projects_spec_ts Project 02',
code: 'p02_projects_spec_ts__project_02',
id: ''
},
{
name: 'projects_spec_ts Project 03',
code: 'p03_projects_spec_ts__project_03',
id: ''
},
];
const project4: Project = {
name: 'projects_spec_ts Project 04',
code: 'p04_projects_spec_ts__project_04',
id: ''
};
const project5: Project = {
name: 'projects_spec_ts Project 05',
code: 'p05_projects_spec_ts__project_05',
id: ''
};

test.beforeAll(async ({ request, member, manager, memberTab, admin, adminTab }) => {
projectsPageMember = new ProjectsPage(memberTab);
projectsPageAdmin = new ProjectsPage(adminTab);

for (const project of projects) {
const projectId = await initTestProject(request, project.code, project.name, manager.username, [member.username]);
project.id = projectId;
}
project4.id = await initTestProject(request, project4.code, project4.name, manager.username, [admin.username]);
project5.id = await initTestProject(request, project5.code, project5.name, manager.username, []);

});

test.describe('for Normal User', () => {

test.beforeEach(async () => {
await projectsPageMember.goto();
});

test('Should list projects of which the user is a member', async () => {
for (const project of projects) {
expect(await projectsPageMember.findProject(project.name)).not.toMatch('-1');
}
});

test('Should not list projects the user is not a member of', async () => {
expect(await projectsPageMember.findProject(project4.name)).toMatch('-1');
});

test('Project to which user is added shows up when page reloaded', async ({ request, member }) => {
const nProjects = await projectsPageMember.countProjects();

await addUserToProject(request, project4.code, member.username);
await projectsPageMember.page.reload();
await projectsPageMember.goto();
expect(await projectsPageMember.countProjects()).toBe(nProjects + 1);
});
});


test.describe('for System Admin User', () => {
test.beforeEach(async () => {
await projectsPageAdmin.goto();
});

test('Should list all projects', async () => {
for (const project of [...projects, project4, project5]) {
expect(await projectsPageAdmin.findProject(project.name)).not.toMatch('-1');
}
// only project4 where admin is a member should be linked
for (const project of [...projects, project5]) {
await expect(await projectsPageAdmin.projectLinkLocator(project.name)).not.toBeVisible();
}
await expect(await projectsPageAdmin.projectLinkLocator(project4.name)).toBeVisible();
});

test('Should allow admin to add him- or herself to the project as tech support if not already a manager', async () => {
expect(await projectsPageAdmin.projectIsLinked(project5.name)).toBe(false);
expect(await projectsPageAdmin.projectHasAddTechSupportButton(project5.name)).toBe(true);

await (await projectsPageAdmin.projectAddTechSupportButtonLocator(project5.name)).click();

const noticeElement = new NoticeElement(projectsPageAdmin.page);
await expect(noticeElement.notice).toBeVisible();
await expect(noticeElement.notice).toContainText(`You are now Tech Support for the '${project5.name}' project.`);
await expect(await projectsPageAdmin.projectAddTechSupportButtonLocator(project5.name)).not.toBeVisible();
await expect(await projectsPageAdmin.projectLinkLocator(project5.name)).toBeVisible();

// admin is a contributor
expect(await projectsPageAdmin.projectIsLinked(project4.name)).toBe(true);
expect(await projectsPageAdmin.projectHasAddTechSupportButton(project4.name)).toBe(true);
});
});

test.describe('Lexicon E2E Project Access', () => {

test('Admin added to project when accessing without membership', async () => {
// this is already tested in a test above but makes the test more understandable
await expect(await projectsPageAdmin.projectLinkLocator(projects[2].name)).not.toBeVisible();
await gotoProjectDirectly(projectsPageAdmin.page, projects[2].id, projects[2].name);
await projectsPageAdmin.goto();
await expect(await projectsPageAdmin.projectLinkLocator(projects[2].name)).toBeVisible();
});

test('User redirected to projects app when accessing without membership', async ({ baseURL }) => {
await projectsPageMember.page.goto('/app/lexicon/' + project5.id + '/#!/editor/list');
// redirect
await expect(projectsPageMember.createButton).toBeVisible();
expect(projectsPageMember.page.url().startsWith(baseURL + ProjectsPage.url)).toBe(true);
});

});
});
6 changes: 6 additions & 0 deletions test/e2e/utils/navigation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { expect, Page } from '@playwright/test';

export async function gotoProjectDirectly(page: Page, projectId: string, projectName: string) {
await page.goto('app/lexicon/' + projectId);
await expect(page.locator('.page-name >> text=' + projectName)).toBeVisible();
}