Skip to content

Commit

Permalink
test(website): e2e tests
Browse files Browse the repository at this point in the history
  • Loading branch information
TobiasKampmann committed Jan 9, 2024
1 parent 484b192 commit 6e31579
Show file tree
Hide file tree
Showing 12 changed files with 194 additions and 63 deletions.
6 changes: 5 additions & 1 deletion website/src/components/User/GroupManager.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type FC, type FormEvent, useRef, useState } from 'react';

import { useGroupManagerHooks } from '../../hooks/useGroupOperations.ts';
import { routes } from '../../routes.ts';
import type { Group } from '../../types/backend.ts';
import { type ClientConfig } from '../../types/runtimeConfig.ts';
import { ConfirmationDialog } from '../ConfirmationDialog.tsx';
Expand Down Expand Up @@ -82,11 +83,14 @@ const InnerGroupManager: FC<GroupManagerProps> = ({ clientConfig, accessToken, u
{!groupsOfUser.isLoading &&
groupsOfUser.data?.map((group) => (
<li key={group.groupName} className='flex items-center gap-6 bg-gray-100 p-2 mb-2 rounded'>
<span className='text-lg'>{group.groupName}</span>
<a className='text-lg' href={routes.groupOverviewPage(group.groupName)}>
{group.groupName}
</a>
<button
onClick={() => handleOpenConfirmationDialog(group)}
className='px-2 py-1 bg-red-500 text-white rounded'
title='Leave group'
aria-label={`Leave group ${group.groupName}`}
>
<LeaveIcon className='w-4 h-4' />
</button>
Expand Down
1 change: 1 addition & 0 deletions website/src/components/User/GroupPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const InnerGroupPage: FC<GroupPageProps> = ({ groupName, clientConfig, accessTok
onClick={() => handleOpenConfirmationDialog(user)}
className='px-2 py-1 bg-red-500 text-white rounded'
title='Remove user from group'
aria-label={`Remove User ${user.name}`}
>
<DeleteIcon className='w-4 h-4' />
</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
import { BackButton } from '../../../../components/Navigation/BackButton';
import { GroupPage } from '../../../../components/User/GroupPage';
import { getRuntimeConfig } from '../../../../config';
import BaseLayout from '../../../../layouts/BaseLayout.astro';
import { getAccessToken } from '../../../../utils/getAccessToken';
import { BackButton } from '../../../components/Navigation/BackButton';
import { GroupPage } from '../../../components/User/GroupPage';
import { getRuntimeConfig } from '../../../config';
import BaseLayout from '../../../layouts/BaseLayout.astro';
import { getAccessToken } from '../../../utils/getAccessToken';
const accessToken = getAccessToken(Astro.locals.session)!;
const groupName = Astro.params.groupName!;
Expand Down
10 changes: 3 additions & 7 deletions website/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ export const routes = {
const userPagePath = `/user` as const;
return organism === undefined ? userPagePath : withOrganism(organism, userPagePath);
},
groupOverviewPage: (organism?: string | undefined) => {
const groupPagePath = `/group` as const;
return organism === undefined ? groupPagePath : withOrganism(organism, groupPagePath);
groupOverviewPage: (groupName: string) => {
const groupPagePath = `/group/${groupName}` as const;
return groupPagePath;
},
userSequencesPage: (organism: string) => withOrganism(organism, `/user/sequences`),
versionPage: (organism: string, accession: string) => withOrganism(organism, `/sequences/${accession}/versions`),
Expand Down Expand Up @@ -92,10 +92,6 @@ function topNavigationItems(organism: string | undefined) {
text: 'User',
path: routes.userOverviewPage(organism),
},
{
text: 'Group',
path: routes.groupOverviewPage(organism),
},
];
}

Expand Down
5 changes: 0 additions & 5 deletions website/src/services/serviceHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Zodios } from '@zodios/core';
import { ZodiosHooks, type ZodiosHooksInstance } from '@zodios/react';

import { backendApi } from './backendApi.ts';
import { groupManagementApi } from './groupManagementApi.ts';
import { lapisApi } from './lapisApi.ts';
import type { Schema } from '../types/config.ts';
import type { LapisBaseRequest } from '../types/lapis.ts';
Expand All @@ -14,10 +13,6 @@ export function backendClientHooks(clientConfig: ClientConfig) {
return new ZodiosHooks('loculus', new Zodios(clientConfig.backendUrl, backendApi));
}

export function groupManagementClientHooks(clientConfig: ClientConfig) {
return new ZodiosHooks('pathoplexus', new Zodios(clientConfig.backendUrl, groupManagementApi));
}

export function lapisClientHooks(lapisUrl: string) {
const zodiosHooks = new ZodiosHooks('lapis', new Zodios(lapisUrl, lapisApi, { transform: false }));
return {
Expand Down
12 changes: 9 additions & 3 deletions website/tests/e2e.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { RevisePage } from './pages/revise/revise.page';
import { SearchPage } from './pages/search/search.page';
import { SequencePage } from './pages/sequences/sequences.page';
import { SubmitPage } from './pages/submit/submit.page';
import { UserPage } from './pages/user/user.page';
import { GroupPage } from './pages/user/group/group.page.ts';
import { UserSequencePage } from './pages/user/userSequencePage/userSequencePage.ts';
import { ACCESS_TOKEN_COOKIE, clientMetadata, realmPath, REFRESH_TOKEN_COOKIE } from '../src/middleware/authMiddleware';
import { BackendClient } from '../src/services/backendClient';
import { GroupManagementClient } from '../src/services/groupManagementClient.ts';
Expand All @@ -20,7 +21,8 @@ type E2EFixture = {
searchPage: SearchPage;
sequencePage: SequencePage;
submitPage: SubmitPage;
userPage: UserPage;
userPage: UserSequencePage;
groupPage: GroupPage;
revisePage: RevisePage;
editPage: EditPage;
navigationFixture: NavigationFixture;
Expand Down Expand Up @@ -167,9 +169,13 @@ export const test = base.extend<E2EFixture>({
await use(submitPage);
},
userPage: async ({ page }, use) => {
const userPage = new UserPage(page);
const userPage = new UserSequencePage(page);
await use(userPage);
},
groupPage: async ({ page }, use) => {
const groupPage = new GroupPage(page);
await use(groupPage);
},
revisePage: async ({ page }, use) => {
const revisePage = new RevisePage(page);
await use(revisePage);
Expand Down
4 changes: 2 additions & 2 deletions website/tests/pages/edit/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { routes } from '../../../src/routes.ts';
import type { AccessionVersion } from '../../../src/types/backend.ts';
import { baseUrl, dummyOrganism, expect, test } from '../../e2e.fixture';
import { prepareDataToBe } from '../../util/prepareDataToBe.ts';
import type { UserPage } from '../user/user.page.ts';
import type { UserSequencePage } from '../user/userSequencePage/userSequencePage.ts';

test.describe('The edit page', () => {
test(
Expand All @@ -25,7 +25,7 @@ test.describe('The edit page', () => {
},
);

const testEditFlow = async (editPage: EditPage, userPage: UserPage, testSequence: AccessionVersion) => {
const testEditFlow = async (editPage: EditPage, userPage: UserSequencePage, testSequence: AccessionVersion) => {
await userPage.clickOnEditForSequenceEntry(testSequence);

expect(await editPage.page.isVisible(`text=Edit Id: ${testSequence.accession}`)).toBe(true);
Expand Down
80 changes: 80 additions & 0 deletions website/tests/pages/user/group/group.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { Page } from '@playwright/test';

import { routes } from '../../../../src/routes.ts';
import { baseUrl, dummyOrganism, expect } from '../../../e2e.fixture';

export class GroupPage {
constructor(public readonly page: Page) {}

public async goToUserPage() {
await this.page.goto(`${baseUrl}${routes.userOverviewPage()}`, { waitUntil: 'networkidle' });
await this.page.waitForURL(`${baseUrl}${routes.userOverviewPage()}`);
}

public async goToGroupPage(groupName: string) {
await this.page.goto(`${baseUrl}${routes.groupOverviewPage(groupName)}`, {
waitUntil: 'networkidle',
});
await this.page.waitForURL(`${baseUrl}${routes.groupOverviewPage(groupName)}`);
}

public async createGroup(uniqueGroupName: string) {
const newGroupField = this.page.getByRole('textbox', { name: 'new group name' });
await newGroupField.fill(uniqueGroupName);
const createGroupButton = this.page.getByRole('button', { name: 'Create group' });
await createGroupButton.click();
}

public getLocatorForButtonToLeaveGroup(groupName: string) {
return this.page.locator('li').filter({ hasText: groupName }).getByRole('button');
}

public async leaveGroup(uniqueGroupName: string) {
const buttonToLeaveGroup = this.getLocatorForButtonToLeaveGroup(uniqueGroupName);
await buttonToLeaveGroup.waitFor({ state: 'visible' });
await buttonToLeaveGroup.click();

const confirmButton = this.page.getByRole('button', { name: 'Confirm' });
await confirmButton.click();
}

public async verifyGroupIsPresent(groupName: string) {
const linkToNewGroup = this.page.getByRole('link', { name: groupName });
await expect(linkToNewGroup).toBeVisible();

expect(await linkToNewGroup.getAttribute('href')).toBe(`/group/${groupName}`);

return linkToNewGroup;
}

public getLocatorForButtonToRemoveUser(userName: string) {
return this.page.getByLabel(`Remove User ${userName}`, { exact: true });
}

public async verifyUserIsPresent(userName: string) {
const userLocator = this.page.locator('ul').getByText(userName, { exact: true });
await expect(userLocator).toBeVisible();
return userLocator;
}

public async addNewUserToGroup(uniqueUserName: string) {
const buttonToAddUserToGroup = this.page.getByRole('button', { name: 'Add user' });
const fieldToAddUserToGroup = this.page.getByRole('textbox', { name: 'new user name' });
await expect(buttonToAddUserToGroup).toBeVisible();
await expect(fieldToAddUserToGroup).toBeVisible();
await fieldToAddUserToGroup.fill(uniqueUserName);

await buttonToAddUserToGroup.click();

await this.verifyUserIsPresent(uniqueUserName);
}

public async removeUserFromGroup(uniqueUserName: string) {
const buttonToRemoveUserFromGroup = this.getLocatorForButtonToRemoveUser(uniqueUserName);
await buttonToRemoveUserFromGroup.waitFor({ state: 'visible' });
await buttonToRemoveUserFromGroup.click();

const confirmButton = this.page.getByRole('button', { name: 'Confirm' });
await confirmButton.click();
}
}
26 changes: 26 additions & 0 deletions website/tests/pages/user/group/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { v4 } from 'uuid';

import { expect, test } from '../../../e2e.fixture';
import { DEFAULT_GROUP_NAME } from '../../../playwrightSetup.ts';

test.describe('The group page', () => {
test('should see all users of the group, add a user and remove it afterwards', async ({
groupPage,
loginAsTestUser,
}) => {
const { username } = await loginAsTestUser();

await groupPage.goToGroupPage(DEFAULT_GROUP_NAME);

await groupPage.verifyUserIsPresent(username);

const uniqueUserName = v4();
await groupPage.addNewUserToGroup(uniqueUserName);

await groupPage.verifyUserIsPresent(uniqueUserName);

await groupPage.removeUserFromGroup(uniqueUserName);

await expect(groupPage.getLocatorForButtonToRemoveUser(uniqueUserName)).not.toBeVisible();
});
});
48 changes: 13 additions & 35 deletions website/tests/pages/user/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,23 @@
import { v4 } from 'uuid';

import { expect, test } from '../../e2e.fixture';
import { submitRevisedDataViaApi } from '../../util/backendCalls.ts';
import { prepareDataToBe } from '../../util/prepareDataToBe.ts';
import { DEFAULT_GROUP_NAME } from '../../playwrightSetup.ts';

test.describe('The user page', () => {
test('should show sequence entries, their status and a link to the editPage', async ({
userPage,
test('should see the groups the user is member of, create a group and leave it afterwards', async ({
groupPage,
loginAsTestUser,
}) => {
const { token } = await loginAsTestUser();

const [sequenceEntryAwaitingApproval] = await prepareDataToBe('awaitingApproval', token);
const [sequenceEntryWithErrors] = await prepareDataToBe('erroneous', token);
const [sequenceEntryReleasable] = await prepareDataToBe('approvedForRelease', token);
const [sequenceEntryToBeRevised] = await prepareDataToBe('approvedForRelease', token);
await submitRevisedDataViaApi([sequenceEntryToBeRevised.accession], token);
await loginAsTestUser();

await userPage.gotoUserSequencePage();
await groupPage.goToUserPage();
await groupPage.verifyGroupIsPresent(DEFAULT_GROUP_NAME);

const sequencesArePresent = await userPage.verifyTableEntries([
{
...sequenceEntryWithErrors,
status: 'HAS_ERRORS',
isRevocation: false,
},
{
...sequenceEntryAwaitingApproval,
status: 'AWAITING_APPROVAL',
isRevocation: false,
},
{
...sequenceEntryReleasable,
status: 'APPROVED_FOR_RELEASE',
isRevocation: false,
},
{
...sequenceEntryToBeRevised,
status: 'APPROVED_FOR_RELEASE',
isRevocation: false,
},
]);
const uniqueGroupName = v4();
await groupPage.createGroup(uniqueGroupName);
const linkToNewGroup = await groupPage.verifyGroupIsPresent(uniqueGroupName);

expect(sequencesArePresent).toBe(true);
await groupPage.leaveGroup(uniqueGroupName);
await expect(linkToNewGroup).not.toBeVisible();
});
});
45 changes: 45 additions & 0 deletions website/tests/pages/user/userSequencePage/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { expect, test } from '../../../e2e.fixture';
import { submitRevisedDataViaApi } from '../../../util/backendCalls.ts';
import { prepareDataToBe } from '../../../util/prepareDataToBe.ts';

test.describe('The user sequence page', () => {
test('should show sequence entries, their status and a link to the editPage', async ({
userPage,
loginAsTestUser,
}) => {
const { token } = await loginAsTestUser();

const [sequenceEntryAwaitingApproval] = await prepareDataToBe('awaitingApproval', token);
const [sequenceEntryWithErrors] = await prepareDataToBe('erroneous', token);
const [sequenceEntryReleasable] = await prepareDataToBe('approvedForRelease', token);
const [sequenceEntryToBeRevised] = await prepareDataToBe('approvedForRelease', token);
await submitRevisedDataViaApi([sequenceEntryToBeRevised.accession], token);

await userPage.gotoUserSequencePage();

const sequencesArePresent = await userPage.verifyTableEntries([
{
...sequenceEntryWithErrors,
status: 'HAS_ERRORS',
isRevocation: false,
},
{
...sequenceEntryAwaitingApproval,
status: 'AWAITING_APPROVAL',
isRevocation: false,
},
{
...sequenceEntryReleasable,
status: 'APPROVED_FOR_RELEASE',
isRevocation: false,
},
{
...sequenceEntryToBeRevised,
status: 'APPROVED_FOR_RELEASE',
isRevocation: false,
},
]);

expect(sequencesArePresent).toBe(true);
});
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Page } from '@playwright/test';

import { routes } from '../../../src/routes.ts';
import type { AccessionVersion, SequenceEntryStatus } from '../../../src/types/backend.ts';
import { getAccessionVersionString } from '../../../src/utils/extractAccessionVersion.ts';
import { baseUrl, dummyOrganism } from '../../e2e.fixture';
import { routes } from '../../../../src/routes.ts';
import type { AccessionVersion, SequenceEntryStatus } from '../../../../src/types/backend.ts';
import { getAccessionVersionString } from '../../../../src/utils/extractAccessionVersion.ts';
import { baseUrl, dummyOrganism } from '../../../e2e.fixture';

export class UserPage {
export class UserSequencePage {
private readonly sequenceBoxNames = [
`userSequences.receivedExpanded`,
`userSequences.processingExpanded`,
Expand Down

0 comments on commit 6e31579

Please sign in to comment.