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

Add recreate project E2E test #1102

Merged
merged 2 commits into from
Oct 10, 2024
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
4 changes: 2 additions & 2 deletions frontend/src/lib/layout/AppBar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@
<!-- https://daisyui.com/components/navbar -->
<header>
{#if environmentName !== 'production'}
<a href="https://public.languagedepot.org" class="flex gap-2 justify-center items-center bg-warning text-warning-content p-2 underline">
<a href="https://lexbox.org" class="flex gap-2 justify-center items-center bg-warning text-warning-content p-2 underline">
{$t('environment_warning', { environmentName })}
<span class="i-mdi-open-in-new text-xl shrink-0" />
</a>
{/if}
<div class="navbar justify-between bg-primary text-primary-content md:pl-3 md:pr-6">
<a href={loggedIn ? '/' : '/login'} class="flex btn btn-primary text-left font-normal px-2">
<a id="home" href={loggedIn ? '/' : '/login'} class="flex btn btn-primary text-left font-normal px-2">
<AppLogo class="h-[3em] w-[3em]" mono />
<div class="flex flex-col text-2xl md:text-3xl tracking-wider">
<span class="md:leading-none">{$t('appbar.app_name')}</span>
Expand Down
22 changes: 22 additions & 0 deletions frontend/tests/components/deleteProjectModal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { type Locator, type Page } from '@playwright/test';
import { BaseComponent } from './baseComponent';

export class DeleteProjectModal extends BaseComponent {

get confirmationInput(): Locator {
return this.componentLocator.getByLabel(`Enter 'DELETE PROJECT' to confirm`);
}

get submitButton(): Locator {
return this.componentLocator.getByRole('button', { name: 'Delete Project' });
}

constructor(page: Page) {
super(page, page.locator(`dialog.modal:has-text("Enter 'DELETE PROJECT' to confirm")`));
}

async confirmDeleteProject(): Promise<void> {
await this.confirmationInput.fill('DELETE PROJECT');
await this.submitButton.click();
}
}
2 changes: 1 addition & 1 deletion frontend/tests/errorHandling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ test('client-side gql 500 does not break the application', async ({ page }) => {
await responsePromise.catch(() => { });// Ignore the error
await expect(page.locator(':text-matches("Unexpected response:.*(500)", "g")').first()).toBeVisible();
await page.getByRole('button', { name: 'Dismiss' }).click();
await page.getByText('Lexbox').click();
await page.locator('#home').click();
await new UserDashboardPage(page).waitFor();
test.fail(); // Everything up to here passed, but we expect a soft 500 response assertion to ultimately fail the test
});
Expand Down
2 changes: 1 addition & 1 deletion frontend/tests/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface TempProject {
name: string
}

type CreateProjectResponse = {data: {createProject: {createProjectResponse: {id: UUID}}}}
export type CreateProjectResponse = {data: {createProject: {createProjectResponse: {id: UUID}}}}

type Fixtures = {
contextFactory: (options: BrowserContextOptions) => Promise<BrowserContext>,
Expand Down
7 changes: 7 additions & 0 deletions frontend/tests/pages/adminDashboardPage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Locator, Page } from '@playwright/test';

import { AuthenticatedBasePage } from './authenticatedBasePage';
import { CreateProjectPage } from './createProjectPage';
import { ProjectPage } from './projectPage';

export class AdminDashboardPage extends AuthenticatedBasePage {
Expand All @@ -19,4 +21,9 @@ export class AdminDashboardPage extends AuthenticatedBasePage {
const table = this.page.locator('table').nth(0);
return table.getByRole('link', {name: projectName, exact: true}).click();
}

async clickCreateProject(): Promise<CreateProjectPage> {
await this.page.getByRole('link', {name: 'Create Project', exact: true}).click();
return new CreateProjectPage(this.page).waitFor();
}
}
25 changes: 25 additions & 0 deletions frontend/tests/pages/createProjectPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { BasePage } from './basePage';
import type { Page } from '@playwright/test';

export class CreateProjectPage extends BasePage {
constructor(page: Page) {
super(page, page.getByRole('heading', { name: 'Create Project' }), `/project/create`);
}

async fillForm(values: { code: string, customCode?: boolean, name?: string, type?: string, purpose?: string, description?: string }): Promise<void> {
const { code, customCode = false, name = code, type, purpose = 'Software Developer', description = name } = values;
await this.page.getByLabel('Name').fill(name);
await this.page.getByLabel('Description').fill(description ?? name);
if (type) await this.page.getByLabel('Project type').selectOption({ label: type });
if (purpose) await this.page.getByLabel('Purpose').selectOption({ label: purpose });
await this.page.getByLabel('Language Code').fill(code);
if (customCode) {
await this.page.getByLabel('Custom Code').check();
await this.page.getByLabel('Code', { exact: true }).fill(code);
}
}

async submit(): Promise<void> {
await this.page.getByRole('button', {name: 'Create Project'}).click();
}
}
6 changes: 4 additions & 2 deletions frontend/tests/pages/projectPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Locator, Page } from '@playwright/test';

import { AddMemberModal } from '../components/addMemberModal';
import { BasePage } from './basePage';
import { DeleteProjectModal } from '../components/deleteProjectModal';
import { ResetProjectModal } from '../components/resetProjectModal';

export class ProjectPage extends BasePage {
Expand All @@ -12,7 +13,7 @@ export class ProjectPage extends BasePage {
get addMemberButton(): Locator { return this.page.getByRole('button', {name: 'Add/Invite Member'}); }

constructor(page: Page, name: string, code: string) {
super(page, page.locator(`.breadcrumbs :text('${name}')`), `/project/${code}`);
super(page, page.getByRole('heading', {name: `Project: ${name}`}), `/project/${code}`);
}

openMoreSettings(): Promise<void> {
Expand All @@ -24,9 +25,10 @@ export class ProjectPage extends BasePage {
return new AddMemberModal(this.page).waitFor()
}

async clickDeleteProject(): Promise<void> {
async clickDeleteProject(): Promise<DeleteProjectModal> {
await this.openMoreSettings();
await this.deleteProjectButton.click();
return await new DeleteProjectModal(this.page).waitFor();
}

async clickResetProject(): Promise<ResetProjectModal> {
Expand Down
40 changes: 40 additions & 0 deletions frontend/tests/recreateProject.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import * as testEnv from './envVars';

import { AdminDashboardPage } from './pages/adminDashboardPage';
import { ProjectPage } from './pages/projectPage';
import { loginAs } from './utils/authHelpers';
import { test, type CreateProjectResponse } from './fixtures';
import { waitForGqlResponse } from './utils/gqlHelpers';
import { expect } from '@playwright/test';

test('delete and recreate project', async ({ page, uniqueTestId }) => {
// Step 1: Login
await loginAs(page.request, 'admin', testEnv.defaultPassword);
const adminDashboard = await new AdminDashboardPage(page).goto();

// Step 2: Create a new project
const createProjectPage = await adminDashboard.clickCreateProject();
const projectCode = `recreate-project-test-${uniqueTestId}`;
await createProjectPage.fillForm({ code: projectCode, customCode: true });
await createProjectPage.submit();
const projectPage = await new ProjectPage(page, projectCode, projectCode).waitFor();

// Step 3: Delete the project
const deleteProjectModal = await projectPage.clickDeleteProject();
await deleteProjectModal.confirmDeleteProject();
await adminDashboard.waitFor();

// Step 4: Recreate the project
await adminDashboard.clickCreateProject();
await createProjectPage.fillForm({ code: projectCode, customCode: true });

const createProjectResponse = await waitForGqlResponse<CreateProjectResponse>(page, async () => {
await createProjectPage.submit();
});
await projectPage.waitFor();

// Step 5: Clean up the last created project (the first one is only soft deleted, but that's probably fine)
const projectId = createProjectResponse.data.createProject.createProjectResponse.id;
const deleteResponse = await page.request.delete(`${testEnv.serverBaseUrl}/api/project/${projectId}`);
expect(deleteResponse.ok()).toBeTruthy();
});
13 changes: 12 additions & 1 deletion frontend/tests/utils/gqlHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, type APIRequestContext } from '@playwright/test';
import { expect, type APIRequestContext, type Page } from '@playwright/test';
import { serverBaseUrl } from '../envVars';

export function validateGqlErrors(json: {errors: unknown, data: unknown}, expectError = false): void {
Expand All @@ -17,3 +17,14 @@ export async function executeGql<T>(api: APIRequestContext, gql: string, expectE
validateGqlErrors(json as {errors: unknown, data: unknown}, expectError);
return json as T;
}

export async function waitForGqlResponse<T>(page: Page, action: () => Promise<void>): Promise<T> {
const gqlPromise = page.waitForResponse('/api/graphql');
await action();
const response = await gqlPromise;
expect(response.ok(), `code was ${response.status()} (${response.statusText()})`).toBeTruthy();
const json: unknown = await response.json();
expect(json, `for query ${response.request().postData()}`).not.toBeNull();
validateGqlErrors(json as { errors: unknown, data: unknown });
return json as T;
}