From 247455654842949ad0a0532be3919c2884d192fc Mon Sep 17 00:00:00 2001 From: Georgi Parlakov Date: Wed, 16 Oct 2024 00:14:51 +0300 Subject: [PATCH] e2e: campaign application admin and giver specs (#1951) * e2e: campaign application admin and giver specs - add logic for admin or giver user login (see fixtures.ts) - add logic to get a localized text for the e2e tests - test edit a campaign application from admin UI - test create a campaign application from general user UI * fix: failing e2e test due to data differences - locally and remotely the data of the current user (/me) and the types of campaigns (/campaign-types) causes the e2e tests to fail - hence the need to mock the data for these 2 requests (like the the other e2e tests) to have a stable user experience tests --- .../campaign-application-admin.spec.ts | 170 +++++++++++++ .../campaign-application-create.spec.ts | 10 - .../campaign-application-giver.spec.ts | 234 ++++++++++++++++++ e2e/tsconfig.json | 11 + e2e/utils/fixtures.ts | 45 ++-- e2e/utils/texts-localized.ts | 9 + public/locales/bg/campaign-application.json | 3 + public/locales/en/campaign-application.json | 3 + .../CampaignApplications.tsx | 4 +- 9 files changed, 462 insertions(+), 27 deletions(-) create mode 100644 e2e/tests/regression/campaign-application/campaign-application-admin.spec.ts delete mode 100644 e2e/tests/regression/campaign-application/campaign-application-create.spec.ts create mode 100644 e2e/tests/regression/campaign-application/campaign-application-giver.spec.ts create mode 100644 e2e/tsconfig.json create mode 100644 e2e/utils/texts-localized.ts diff --git a/e2e/tests/regression/campaign-application/campaign-application-admin.spec.ts b/e2e/tests/regression/campaign-application/campaign-application-admin.spec.ts new file mode 100644 index 000000000..8a16e66b3 --- /dev/null +++ b/e2e/tests/regression/campaign-application/campaign-application-admin.spec.ts @@ -0,0 +1,170 @@ +import { + CampaignApplicationResponse, + CampaignApplicationExisting, + CampaignApplicationAdminResponse, +} from '../../../../src/gql/campaign-applications' +import { Page } from 'playwright/test' +import { expect, adminTest as test } from '../../../utils/fixtures' +import { textLocalized } from '../../../utils/texts-localized' + +test.describe('Campaign application admin', () => { + test('should see list of applications', async ({ page, baseURL }) => { + // arrange + const { paginationFooter } = await setup(page) + .withCampaignApplications([ + { id: '1', state: 'review' }, + { id: '2', state: 'approved' }, + { id: '3', state: 'denied' }, + { id: '4', state: 'forCommitteeReview' }, + { id: '5' }, + ]) + .build() + + // act + await page.goto(`${baseURL}/admin/campaign-applications`) + + // assert + const t = await textLocalized().campaign.bg() + await expect(page.getByRole('heading')).toHaveText(t.admin.title) + await expect(page.getByRole('row')).toHaveCount(6) // title + 5 campaigns + await expect(page.getByRole('row').nth(1)).toContainText(t.status.review) + await expect(page.getByRole('row').nth(2)).toContainText(t.status.approved) + await expect(page.getByRole('row').nth(3)).toContainText(t.status.denied) + await expect(page.getByRole('row').nth(4)).toContainText(t.status.forCommitteeReview) + await expect(page.getByRole('row').nth(5)).toContainText(t.status.requestInfo) + await expect(paginationFooter(page)).toHaveText('Rows per page:1001–5 of 5') + }) + + test('should open a campaign application for edit', async ({ page, baseURL }) => { + // arrange + await setup(page).withEditCampaignApplication({}).build() + + // act + await page.goto(`${baseURL}/admin/campaign-applications/edit/1234`) + + // assert + const t = await textLocalized().campaign.bg() + await expect(page.getByRole('heading').first()).toHaveText(t.admin.title) + await expect(page.getByRole('heading').nth(1)).toHaveText(t.steps.admin.title) + }) + + test('should update status of campaign application to approved, archive it, and set the external link', async ({ + page, + baseURL, + }) => { + // arrange + await setup(page).withEditCampaignApplication({ id: '1234', state: 'review' }).build() + await page.goto(`${baseURL}/admin/campaign-applications/edit/1234`) + const t = await textLocalized().campaign.bg() + + // act + await page.getByLabel(t.steps.admin.status).click() + await page.getByText(t.status.approved).click() + + const [req] = await Promise.all([ + page.waitForRequest(/campaign-application\/1234/), + page.getByRole('button', { name: t.result.editButton }).click(), + ]) + + // assert + const postData = req.postDataJSON() + expect(postData.state).toEqual('approved') + expect(page.getByText(t.result.edited)).toBeInViewport() + }) +}) + +function setup(page: Page) { + const promises: Promise[] = [] + + const builder = { + withCampaignApplications(cams: Array>) { + promises.push( + page.route('*/**/api/v1/campaign-application/list', (route, req) => { + return route.fulfill({ + json: cams.map((c) => ({ ...defaultCampaignApplication(), ...c })), + }) + }), + ) + return builder + }, + + withEditCampaignApplication(c: Partial) { + promises.push( + page.route('*/**/api/v1/campaign-application/byId/*', (route, req) => { + return route.fulfill({ + json: { ...camAppForEdit(), ...c }, + }) + }), + page.route(`*/**/api/v1/campaign-application/${c.id}`, (r) => { + return r.fulfill({ json: { ...camAppForEdit(), ...c } }) + }), + ) + return builder + }, + + async build() { + await promises + + const selectors = { + paginationFooter: (p: Page) => p.locator('.MuiDataGrid-footerContainer'), + } + + return selectors + }, + } + + return builder +} + +function defaultCampaignApplication(): CampaignApplicationAdminResponse { + return { + id: 'eb4347a2-c8b4-47f1-83e5-67457b20909c', + createdAt: '2024-09-13T09:26:50.909Z', + updatedAt: '2024-09-28T20:56:13.728Z', + organizerName: 'Giver Dev', + organizerEmail: 'giver@podkrepi.bg', + organizerPhone: '+35928700500', + beneficiary: 'Bene', + organizerBeneficiaryRel: 'бене', + campaignName: 'Camp name', + goal: 'Целта на кампанията', + history: '', + amount: '1455', + description: '', + state: 'requestInfo', + campaignTypeId: 'c6ef0a79-11cf-4175-9f66-3cec940c9259', + ticketURL: 'https://trello.com/linkforthiscamapp', + archived: false, + campaignEnd: 'date', + campaignEndDate: '2025-09-30T00:00:00.000Z', + acceptTermsAndConditions: true, + transparencyTermsAccepted: true, + personalInformationProcessingAccepted: true, + } +} + +function camAppForEdit(): CampaignApplicationExisting { + return { + id: 'eb4347a2-c8b4-47f1-83e5-67457b20909c', + organizerName: 'Giver Dev', + organizerEmail: 'giver@podkrepi.bg', + organizerPhone: '+35928700500', + beneficiary: 'Bene', + organizerBeneficiaryRel: 'бене', + campaignName: 'Camp name', + goal: 'Целта на кампанията', + history: '', + amount: '1455', + description: '', + state: 'requestInfo', + campaignTypeId: 'c6ef0a79-11cf-4175-9f66-3cec940c9259', + ticketURL: 'https://trello.com/linkforthiscamapp', + archived: false, + campaignEnd: 'date', + campaignEndDate: '2025-09-30T00:00:00.000Z', + acceptTermsAndConditions: true, + transparencyTermsAccepted: true, + personalInformationProcessingAccepted: true, + documents: [], + } +} diff --git a/e2e/tests/regression/campaign-application/campaign-application-create.spec.ts b/e2e/tests/regression/campaign-application/campaign-application-create.spec.ts deleted file mode 100644 index df61cb4b4..000000000 --- a/e2e/tests/regression/campaign-application/campaign-application-create.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { test, expect } from '../../../utils/fixtures' - -test.describe('Create campaign application', () => { - test('should see list of applications', async ({ page, baseURL }) => { - await page.goto(`${baseURL}/admin/campaign-applications`) - - await expect(page.getByRole('heading')).toHaveText('Кандидат Кампании') - // await expect(page.getByRole('row')).toHaveCount(1); // just title b/c no campaign applications yet - }) -}) diff --git a/e2e/tests/regression/campaign-application/campaign-application-giver.spec.ts b/e2e/tests/regression/campaign-application/campaign-application-giver.spec.ts new file mode 100644 index 000000000..1a916af60 --- /dev/null +++ b/e2e/tests/regression/campaign-application/campaign-application-giver.spec.ts @@ -0,0 +1,234 @@ +import { + CampaignApplicationResponse, + CampaignApplicationExisting, + CampaignApplicationAdminResponse, +} from '../../../../src/gql/campaign-applications' +import { Page } from 'playwright/test' +import { expect, giverTest as test } from '../../../utils/fixtures' +import { textLocalized } from '../../../utils/texts-localized' + +test.describe('Campaign application giver', () => { + test('should see the first step - organizer - of create campaign application wizard and after accepting the terms to be able to go to step 2', async ({ + page, + baseURL, + }) => { + // arrange + // act + await page.goto(`${baseURL}/campaigns/application`) + + // assert + const t = await textLocalized().campaign.bg() + await expect(page.getByRole('heading')).toHaveText(t.steps.organizer.title) + + await page.getByRole('checkbox').first().click() + await page.getByRole('checkbox').nth(1).click() + await page.getByRole('checkbox').nth(2).click() + + await page.getByRole('button', { name: t.cta.next }).click() + + // assert + await expect(page.getByRole('heading')).toHaveText(t.steps.application.title) + }) + + test('should see the second step -application - of create campaign application wizard and after filling in the beneficiary, relations, title, type and funds go to step 3', async ({ + page, + baseURL, + }) => { + // arrange + await page.goto(`${baseURL}/campaigns/application`) + const t = await textLocalized().campaign.bg() + + // step 1 + await page.getByRole('checkbox').first().click() + await page.getByRole('checkbox').nth(1).click() + await page.getByRole('checkbox').nth(2).click() + await page.getByRole('button', { name: t.cta.next }).click() + + // act + await page.getByLabel(t.steps.application.beneficiary).fill('beneficiary') + await page.getByLabel(t.steps.application.beneficiaryRelationship).fill('rel') + await page.getByLabel(t.steps.application.campaignTitle).fill('title') + + // select type of campaign app by opening the dropdown and arrow down and enter to select + await page.locator('[name="applicationBasic.campaignType"]').click({ force: true }) // this is the underlying input and it's hidden - hence the force + await page.keyboard.down('ArrowDown') + await page.keyboard.down('Enter') + + await page.getByLabel(t.steps.application.funds).fill('12345') + + // go next + await page.getByRole('button', { name: t.cta.next }).click() + + // assert + await expect(page.getByRole('heading')).toHaveText(t.steps.details.title) + }) + + test('should see the third step - details - of create campaign application wizard and after filling the title, description, history and 2 files be able to create a new campaign application', async ({ + page, + baseURL, + }) => { + // arrange + await setupMeAndCampaignTypes(page) + await page.goto(`${baseURL}/campaigns/application`) + const t = await textLocalized().campaign.bg() + + // step 1 + await page.getByRole('checkbox').first().click() + await page.getByRole('checkbox').nth(1).click() + await page.getByRole('checkbox').nth(2).click() + await page.getByRole('button', { name: t.cta.next }).click() + // step 2 + await page.getByLabel(t.steps.application.beneficiary).fill('beneficiary') + await page.getByLabel(t.steps.application.beneficiaryRelationship).fill('rel') + await page.getByLabel(t.steps.application.campaignTitle).fill('title') + + // select type of campaign app by opening the dropdown and arrow down and enter to select + await page.locator('[name="applicationBasic.campaignType"]').click({ force: true }) // this is the underlying input and it's hidden - hence the force + await page.keyboard.down('ArrowDown') + await page.keyboard.down('Enter') + + await page.getByLabel(t.steps.application.funds).fill('12345') + + await page.getByRole('button', { name: t.cta.next }).click() + + // act + await page.getByLabel(t.steps.details.cause).fill('goal') + await page.getByLabel(t.steps.details.description).fill('description') + await page.getByLabel(t.steps.details['current-status'].label).fill('history') + + await page.getByLabel(t.steps.details.documents).setInputFiles([ + { + name: 'file.txt', + mimeType: 'text/plain', + buffer: Buffer.from('this is test'), + }, + { + name: 'file1.txt', + mimeType: 'text/plain', + buffer: Buffer.from('this is test'), + }, + ]) + + // ensure we intercept the create and not let it go to the server... + page.route('*/**/api/v1/campaign-application/create', (route, req) => { + return route.fulfill({ + json: defaultCampaignApplication(), + }) + }) + // and the upload file as well + page.route('*/**/api/v1/campaign-application/uploadFile/*', (route, req) => { + return route.fulfill({ + json: { id: '1' }, + }) + }) + + const [createApplication, uploadFile1, uploadFile2] = await Promise.all([ + page.waitForRequest(/\/api\/v1\/campaign-application\/create/), + page.waitForRequest(/\/api\/v1\/campaign-application\/uploadFile.*/), + page.waitForRequest(/\/api\/v1\/campaign-application\/uploadFile.*/), + page.getByRole('button', { name: t.cta.submit }).click(), + ]) + + // assert + await expect(createApplication.postDataJSON()).toEqual({ + acceptTermsAndConditions: true, + amount: '12345', + archived: false, + beneficiary: 'beneficiary', + campaignEnd: 'funds', + campaignName: 'title', + campaignTypeId: '34b501f0-b3c3-43d9-9be0-7f7258eeb247', + description: 'description', + goal: 'goal', + history: 'history', + organizerBeneficiaryRel: 'rel', + organizerEmail: 'giver@podkrepi.bg', + organizerName: 'Giver Dev', + organizerPhone: '+35928700500', + personalInformationProcessingAccepted: true, + state: 'review', + ticketURL: '', + transparencyTermsAccepted: true, + }) + + expect(uploadFile1.method()).toEqual('POST') + expect(uploadFile1.url()).toMatch('api/v1/campaign-application/uploadFile/created') + expect(uploadFile2.method()).toEqual('POST') + expect(uploadFile2.url()).toMatch('api/v1/campaign-application/uploadFile/created') + + await expect(page.getByRole('heading')).toHaveText(t.result.created) + await expect(page.getByText('file.txt')).toBeVisible() + await expect(page.getByText('file1.txt')).toBeVisible() + await expect(page.getByText('giver@podkrepi.bg')).toBeVisible() + await expect(page.getByText('Giver Dev')).toBeVisible() + await expect(page.getByText('+35928700500')).toBeVisible() + await expect(page.getByText('beneficiary')).toBeVisible() + await expect(page.getByText('rel')).toBeVisible() + await expect(page.getByText('title')).toBeVisible() + await expect(page.getByText('12345')).toBeVisible() + await expect(page.getByText(t.steps.application['campaign-end'].options.funds)).toBeVisible() + await expect(page.getByText('goal')).toBeVisible() + }) +}) + +function defaultCampaignApplication() { + return { + id: 'created', + acceptTermsAndConditions: true, + personalInformationProcessingAccepted: true, + transparencyTermsAccepted: true, + organizerName: 'Giver Dev', + organizerEmail: 'giver@podkrepi.bg', + organizerPhone: '+35928700500', + beneficiary: 'beneficiary', + campaignName: 'title', + amount: '12345', + goal: 'goal', + description: '', + organizerBeneficiaryRel: 'rel', + history: '', + campaignEnd: 'funds', + campaignTypeId: 'b9043466-a3c1-4ced-b951-6282ca3e6a7b', + archived: false, + state: 'review', + ticketURL: '', + } +} + +async function setupMeAndCampaignTypes(page: Page) { + await page.route('*/**/api/v1/account/me', (req) => + req.fulfill({ + json: { + user: { + id: '99c18c81-54bc-4f32-ab50-3ac5c383f44b', + firstName: 'Giver', + lastName: 'Dev', + email: 'giver@podkrepi.bg', + phone: '+35928700500', + }, + }, + }), + ) + await page.route('*/**/api/v1/campaign-types/', (req) => + req.fulfill({ + json: [ + { + id: '0c80a28c-f09e-4e82-b2ec-6682ae559cab', + name: 'Transplantation', + slug: 'transplantation', + description: 'Ullam exercitationem optio tempora ullam.', + parentId: 'b9043466-a3c1-4ced-b951-6282ca3e6a7b', + category: 'medical', + }, + { + id: '34b501f0-b3c3-43d9-9be0-7f7258eeb247', + name: 'Membership', + slug: 'membership', + description: 'Membership Campaigns', + parentId: null, + category: 'others', + }, + ], + }), + ) +} diff --git a/e2e/tsconfig.json b/e2e/tsconfig.json new file mode 100644 index 000000000..0bc1c6ac7 --- /dev/null +++ b/e2e/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "..", + "paths": { + "@src/*": ["./src/*"] + } + }, + "include": ["./e2e", "./src"], + "exclude": ["./node_modules"] +} diff --git a/e2e/utils/fixtures.ts b/e2e/utils/fixtures.ts index 4b08001f4..ea13fef30 100644 --- a/e2e/utils/fixtures.ts +++ b/e2e/utils/fixtures.ts @@ -1,30 +1,45 @@ -import { test as base } from '@playwright/test' +import { test, test as base } from '@playwright/test' import dotenv from 'dotenv' +import fs from 'fs' +import path from 'path' dotenv.config({ path: '../.env.local' }) dotenv.config({ path: '../.env' }) -const email = process.env.PODKREPI_EMAIL! +const adminEmail = process.env.PODKREPI_EMAIL! const password = process.env.PODKREPI_PASSWORD! -export const test = base.extend({ - storageState: async ({ browser, baseURL }, use) => { - const page = await browser.newPage() - await page.goto(`${baseURL}/login`) +const testExtendFn = (useThisEmail: string = adminEmail) => + base.extend({ + storageState: async ({ browser, baseURL }, use) => { + const id = useThisEmail.replace(/\W/, '') + const fileName = path.resolve(test.info().project.outputDir, `.auth/${id}.json`) - await page.locator('[name=email]').fill(email) - await page.locator('[name=password]').fill(password) + if (fs.existsSync(fileName)) { + // Reuse existing authentication state if any. + await use(fileName) + return + } + const page = await browser.newPage() + await page.goto(`${baseURL}/login`) - await page.locator('[type=submit]').click() - await page.waitForURL((url) => !url.pathname.includes('login')) + await page.locator('[name=email]').fill(useThisEmail) + await page.locator('[name=password]').fill(password) - const state = await page.context().storageState() + await page.locator('[type=submit]').click() + await page.waitForURL((url) => !url.pathname.includes('login')) - await page.close() + await page.context().storageState({ path: fileName }) - use(state) - }, -}) + await page.close() + + await use(fileName) + }, + }) + +export const adminTest = testExtendFn(adminEmail) + +export const giverTest = testExtendFn('giver@podkrepi.bg') /** export the expect for consistency i.e. to be able to do `import { test, expect } from '../utils/fixtures'` */ export { expect } from 'playwright/test' diff --git a/e2e/utils/texts-localized.ts b/e2e/utils/texts-localized.ts new file mode 100644 index 000000000..1a6ce2911 --- /dev/null +++ b/e2e/utils/texts-localized.ts @@ -0,0 +1,9 @@ +export function textLocalized() { + const campaign = { + bg: async () => await import(`../../public/locales/bg/campaign-application.json`), + en: async () => await import(`../../public/locales/bg/campaign-application.json`), + } + return { + campaign, + } +} diff --git a/public/locales/bg/campaign-application.json b/public/locales/bg/campaign-application.json index 1a075e329..9d8178c6e 100644 --- a/public/locales/bg/campaign-application.json +++ b/public/locales/bg/campaign-application.json @@ -86,5 +86,8 @@ "approved": "Одобрена", "denied": "Отказана", "abandoned": "Изоставена" + }, + "admin": { + "title": "Кандидат Кампании" } } diff --git a/public/locales/en/campaign-application.json b/public/locales/en/campaign-application.json index 847b7f696..175d24561 100644 --- a/public/locales/en/campaign-application.json +++ b/public/locales/en/campaign-application.json @@ -86,5 +86,8 @@ "approved": "Approved", "denied": "Denied", "abandoned": "Abandoned" + }, + "admin": { + "title": "Campaign Applications" } } diff --git a/src/components/admin/campaign-applications/CampaignApplications.tsx b/src/components/admin/campaign-applications/CampaignApplications.tsx index b808da677..e946a97c4 100644 --- a/src/components/admin/campaign-applications/CampaignApplications.tsx +++ b/src/components/admin/campaign-applications/CampaignApplications.tsx @@ -5,11 +5,11 @@ import AdminLayout from 'components/common/navigation/AdminLayout' import CampaignApplicationsGrid from './CampaignApplicationsGrid' export default function CampaignApplicationsPage() { - const { t } = useTranslation('campaigns') + const { t } = useTranslation('campaign-application') return ( - +