diff --git a/.all-contributorsrc b/.all-contributorsrc index eab6c0a6a..8f241dcad 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -904,6 +904,24 @@ "contributions": [ "code" ] + }, + { + "login": "nikolay-yankov", + "name": "Nikolay Yankov", + "avatar_url": "https://avatars.githubusercontent.com/u/36303598?v=4", + "profile": "https://github.com/nikolay-yankov", + "contributions": [ + "code" + ] + }, + { + "login": "katina-anachkova", + "name": "Katina Anachkova", + "avatar_url": "https://avatars.githubusercontent.com/u/82702355?v=4", + "profile": "https://github.com/katina-anachkova", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 10, diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index b80f5a889..c4b830882 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -120,6 +120,7 @@ jobs: name: playwright-report path: ./frontend/e2e/test-results/ retention-days: 14 + overwrite: true - uses: actions/upload-artifact@v4 if: always() diff --git a/README.md b/README.md index 67bd26945..b49bfc908 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,9 @@ Watch releases of this repository to be notified about future updates: ## Contributors ✨ -[![All Contributors](https://img.shields.io/badge/all_contributors-84-orange.svg?style=flat-square)](#contributors-) + +[![All Contributors](https://img.shields.io/badge/all_contributors-86-orange.svg?style=flat-square)](#contributors-) + Please check [contributors guide](https://github.com/podkrepi-bg/frontend/blob/master/CONTRIBUTING.md) for: @@ -226,6 +228,8 @@ Thanks goes to these wonderful people: Martin Kovachki
Martin Kovachki

💻 ⚠️ Viktor Stefanov
Viktor Stefanov

💻 velnachev
velnachev

💻 + Nikolay Yankov
Nikolay Yankov

💻 + Katina Anachkova
Katina Anachkova

💻 diff --git a/e2e/README.md b/e2e/README.md index 0f3b4bde9..ba43be251 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -52,3 +52,24 @@ Options: ```shell yarn test:e2e --headed --debug -x -g support ``` + +### Tests with Authenticated user + +### Writing + +To auth a user we rely on the Storage where the session is stored. And the storage is filled in and accessed via a `test.extend` [fixture](https://playwright.dev/docs/auth#authenticate-with-api-request). It takes care to login and store the session and then use it in the tests. See the `e2e/utils/fixtures.ts` file + +This is the process for writing tests for auth user. + +- import the desired user `test` from the fixture + + `import { expect, giverTest as test } from '../../../utils/fixtures'` for the `giver` user + +- write your e2e tests ... + +> [Examples] `e2e/tests/regression/campaign-application/campaign-application-giver.spec.ts` and `e2e/tests/regression/campaign-application/campaign-application-admin.spec.ts` + +### Running + +- [Locally] run the 'docker compose -d keycloak pg-db', the api (`yarn dev` in the api repo folder), the app (`yarn dev` in the frontend repo folder), +- in the `frontend/e2e` folder run `yarn e2e:tests --ui` to start the playwright visual testing tool diff --git a/e2e/pages/web-pages/donation/donation.page.ts b/e2e/pages/web-pages/donation/donation.page.ts index a5d58287d..df4a4e392 100644 --- a/e2e/pages/web-pages/donation/donation.page.ts +++ b/e2e/pages/web-pages/donation/donation.page.ts @@ -56,7 +56,7 @@ export class DonationPage extends CampaignsPage { bgLocalizationValidation['informed-agree-with'] + ' ' + bgLocalizationValidation.gdpr private readonly enPrivacyCheckboxText = enLocalizationValidation['informed-agree-with'] + ' ' + enLocalizationValidation.gdpr - private readonly bgStripeErrorNoBalanceText = 'Картата Ви не разполага с достатъчно средства.' + private readonly bgStripeErrorNoBalanceText = 'В картата ви няма достатъчно средства. Опитайте с друга.' async checkPageUrlByRegExp(urlRegExpAsString?: string, timeoutParam = 10000): Promise { await this.page.waitForTimeout(1000) diff --git a/e2e/tests/regression/campaign-application/campaign-application-giver.spec.ts b/e2e/tests/regression/campaign-application/campaign-application-giver.spec.ts index 1a916af60..942e7c5c8 100644 --- a/e2e/tests/regression/campaign-application/campaign-application-giver.spec.ts +++ b/e2e/tests/regression/campaign-application/campaign-application-giver.spec.ts @@ -1,8 +1,3 @@ -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' @@ -169,6 +164,41 @@ test.describe('Campaign application giver', () => { await expect(page.getByText(t.steps.application['campaign-end'].options.funds)).toBeVisible() await expect(page.getByText('goal')).toBeVisible() }) + + test('should see the edit campaign application and be able to delete a selected file ', async ({ + page, + baseURL, + }) => { + // arrange + await setupMeAndCampaignTypes(page) + await setupCampaignApplicationForEdit(page) + await page.goto(`${baseURL}/campaigns/application/1234`) + const t = await textLocalized().campaign.bg() + await page.getByRole('button', { name: t.cta.next }).click() + await page.getByRole('button', { name: t.cta.next }).click() + + // expect to see 2 files + await expect(page.getByText('1234.txt')).toBeVisible() + await expect(page.getByText('document.pdf')).toBeVisible() + + // act + // hit the delete button ... + await page.locator('li').filter({ hasText: '1234.txt' }).getByLabel('delete').click() + const [editCamAppReq, fileDeleteReq] = await Promise.all([ + // the edit request to edit the CamApp entity + page.waitForRequest((r) => r.method() === 'PATCH'), + // the delete request to remove one of the files + page.waitForRequest((r) => r.method() === 'DELETE'), + // ... and when submit + page.getByRole('button', { name: t.cta.submit }).click(), + ]) + + await expect(editCamAppReq.postDataJSON()).toBeDefined() + + const fileDelRes = await fileDeleteReq.response() + + await expect(fileDelRes?.json()).resolves.toEqual({ id: 'ok' }) + }) }) function defaultCampaignApplication() { @@ -222,9 +252,9 @@ async function setupMeAndCampaignTypes(page: Page) { }, { id: '34b501f0-b3c3-43d9-9be0-7f7258eeb247', - name: 'Membership', - slug: 'membership', - description: 'Membership Campaigns', + name: 'Elderly', + slug: 'elderly', + description: 'Help elderly people', parentId: null, category: 'others', }, @@ -232,3 +262,42 @@ async function setupMeAndCampaignTypes(page: Page) { }), ) } + +async function setupCampaignApplicationForEdit( + page: Page, + application: Partial> = {}, +) { + await page.route('*/**/api/v1/campaign-application/byId/*', (req) => + req.fulfill({ + json: { + ...defaultCampaignApplication(), + id: 'forEdit', + documents: [ + { filename: '1234.txt', id: '1234' }, + { filename: 'document.pdf', id: 'doc-id-123123' }, + ], + ...application, + }, + }), + ) + + // on submit at the end of edit this patch request needs to be sent + await page.route('*/**/api/v1/campaign-application/forEdit', (req) => + req.fulfill({ + json: { + ...defaultCampaignApplication(), + id: 'forEdit', + ...application, + }, + }), + ) + + // delete file successful + await page.route('*/**/api/v1/campaign-application/fileById/*', (req) => + req.fulfill({ + json: { + id: 'ok', + }, + }), + ) +} diff --git a/e2e/utils/fixtures.ts b/e2e/utils/fixtures.ts index ea13fef30..a51ad18a1 100644 --- a/e2e/utils/fixtures.ts +++ b/e2e/utils/fixtures.ts @@ -1,3 +1,7 @@ +/** + * This is logic for authenticating and storing the session to be used in the tests. See the e2e/Readme.md - the section about Authenticated user + */ + import { test, test as base } from '@playwright/test' import dotenv from 'dotenv' import fs from 'fs' diff --git a/e2e/yarn.lock b/e2e/yarn.lock index d77e05353..626bd5bd9 100644 --- a/e2e/yarn.lock +++ b/e2e/yarn.lock @@ -235,13 +235,13 @@ __metadata: linkType: hard "cross-spawn@npm:^7.0.0": - version: 7.0.3 - resolution: "cross-spawn@npm:7.0.3" + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" dependencies: path-key: ^3.1.0 shebang-command: ^2.0.0 which: ^2.0.1 - checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52 + checksum: 8d306efacaf6f3f60e0224c287664093fa9185680b2d195852ba9a863f85d02dcc737094c6e512175f8ee0161f9b87c73c6826034c2422e39de7d6569cf4503b languageName: node linkType: hard diff --git a/public/locales/bg/donation-flow.json b/public/locales/bg/donation-flow.json index 8514038a1..e039106a0 100644 --- a/public/locales/bg/donation-flow.json +++ b/public/locales/bg/donation-flow.json @@ -102,7 +102,7 @@ }, "noregister": { "label": "Продължете без регистрация ", - "description": "Продължавайки без регистрация, нямате възможност да запазите дарението в историята на профила си както и да правите месечни дарения по избрана кампания" + "description": "Продължавайки без регистрация, нямате възможност да запазите дарението в историята на профила си както и да правите месечни дарения по избрана кампания." }, "field": { "password": "Парола", diff --git a/public/locales/en/donation-flow.json b/public/locales/en/donation-flow.json index b22973a45..2f6c0ca80 100644 --- a/public/locales/en/donation-flow.json +++ b/public/locales/en/donation-flow.json @@ -56,7 +56,7 @@ }, "card-data": { "name-label": "Cardholder name", - "error": { + "errors": { "email": "Please enter your email", "name": "Please enter your name" } @@ -106,7 +106,7 @@ }, "noregister": { "label": "Continue without registration", - "description": "You will not be able to get a donation certificate or a list of your donations. If you still want to receive a receipt, please share your email - it will not be visible in the platform" + "description": "You will not be able to get a donation certificate or a list of your donations. If you still want to receive a receipt, please share your email - it will not be visible in the platform." }, "field": { "password": "Password", @@ -128,7 +128,7 @@ "donation": "Donation", "transaction": { "title": "Transaction", - "description": "The transaction is only to compensate the transfer and is calculated based on your method of payment. \"Podkrepi.bg\" works with 0% commission" + "description": "The transaction is only to compensate the transfer and is calculated based on your method of payment. \"Podkrepi.bg\" works with 0% commission." }, "total": "Total", "field": { diff --git a/src/components/admin/campaign-applications/CampaignApplicationsGrid.tsx b/src/components/admin/campaign-applications/CampaignApplicationsGrid.tsx index 4d230a824..d6d3b3f78 100644 --- a/src/components/admin/campaign-applications/CampaignApplicationsGrid.tsx +++ b/src/components/admin/campaign-applications/CampaignApplicationsGrid.tsx @@ -115,7 +115,8 @@ export const useCampaignsList = () => { const { data, isLoading } = fetchMutation() return { - list: data?.sort((a, b) => b?.updatedAt?.localeCompare(a?.updatedAt ?? '') ?? 0), + // the data array is strict mode (sometimes) it throws a Readonly array error on the sort so create a shallow copy + list: [...(data ?? [])].sort((a, b) => b?.updatedAt?.localeCompare(a?.updatedAt ?? '') ?? 0), isLoading, } } diff --git a/src/components/client/campaign-application/steps/CampaignApplicationBasic.tsx b/src/components/client/campaign-application/steps/CampaignApplicationBasic.tsx index 864dcd8f9..807e746b2 100644 --- a/src/components/client/campaign-application/steps/CampaignApplicationBasic.tsx +++ b/src/components/client/campaign-application/steps/CampaignApplicationBasic.tsx @@ -58,7 +58,7 @@ export default function CampaignApplicationBasic() { /> - + {t('steps.details.title')} - - - + + + {t('campaigns:campaign.type')} - {data?.map((campaignType, index) => ( - - {campaignType.name} - - ))} + {data + ?.filter((campaignType) => !hideSystemTypes || !systemTypes.includes(campaignType?.name)) + ?.map((campaignType, index) => ( + + {campaignType.name} + + ))} {helperText && {helperText}} diff --git a/src/components/client/donation-flow/DonationFlowForm.tsx b/src/components/client/donation-flow/DonationFlowForm.tsx index de6b63a4a..7ee93d06a 100644 --- a/src/components/client/donation-flow/DonationFlowForm.tsx +++ b/src/components/client/donation-flow/DonationFlowForm.tsx @@ -101,6 +101,18 @@ export const validationSchema: yup.SchemaOf = yup export function DonationFlowForm() { const formikRef = useRef | null>(null) const { t } = useTranslation('donation-flow') + const { campaign, setupIntent, paymentError, setPaymentError, idempotencyKey } = useDonationFlow() + const stripe = useStripe() + const elements = useElements() + const router = useRouter() + const updateSetupIntentMutation = useUpdateSetupIntent() + const cancelSetupIntentMutation = useCancelSetupIntent() + const paymentMethodSectionRef = React.useRef(null) + const authenticationSectionRef = React.useRef(null) + const stripeChargeRef = React.useRef(idempotencyKey) + const [showCancelDialog, setShowCancelDialog] = React.useState(false) + const [submitPaymentLoading, setSubmitPaymentLoading] = React.useState(false) + const { data: { user: person } = { user: null } } = useCurrentPerson() const { data: session } = useSession({ required: false, onUnauthenticated: () => { @@ -117,17 +129,6 @@ export function DonationFlowForm() { formikRef.current?.setFieldValue('email', '') formikRef.current?.setFieldValue('isAnonymous', true, false) }, [session]) - const { campaign, setupIntent, paymentError, setPaymentError, idempotencyKey } = useDonationFlow() - const stripe = useStripe() - const elements = useElements() - const router = useRouter() - const updateSetupIntentMutation = useUpdateSetupIntent() - const cancelSetupIntentMutation = useCancelSetupIntent() - const paymentMethodSectionRef = React.useRef(null) - const authenticationSectionRef = React.useRef(null) - const [showCancelDialog, setShowCancelDialog] = React.useState(false) - const [submitPaymentLoading, setSubmitPaymentLoading] = React.useState(false) - const { data: { user: person } = { user: null } } = useCurrentPerson() return ( { - cancelSetupIntentMutation.mutate({ id: setupIntent.id }) - router.push(routes.campaigns.viewCampaignBySlug(campaign.slug)) + setShowCancelDialog(false) }} title={t('cancel-dialog.title')} content={t('cancel-dialog.content')} confirmButtonLabel={t('cancel-dialog.btn-continue')} cancelButtonLabel={t('cancel-dialog.btn-cancel')} handleConfirm={() => { - setShowCancelDialog(false) + cancelSetupIntentMutation.mutate({ id: setupIntent.id }) + router.push(routes.campaigns.viewCampaignBySlug(campaign.slug)) }} /> ) diff --git a/src/components/common/SocialShareListButton.tsx b/src/components/common/SocialShareListButton.tsx index ffe3bc4ab..b8fc77091 100644 --- a/src/components/common/SocialShareListButton.tsx +++ b/src/components/common/SocialShareListButton.tsx @@ -8,7 +8,7 @@ import { PopoverProps, ButtonProps, } from '@mui/material' -import { ContentCopy, Facebook, LinkedIn, Share, Twitter } from '@mui/icons-material' +import { ContentCopy, Facebook, LinkedIn, Share, Twitter, X } from '@mui/icons-material' import { AlertStore } from 'stores/AlertStore' import theme from 'common/theme' @@ -25,7 +25,7 @@ export default function SocialShareListButton({ }) { const { t } = useTranslation('common') const [anchorEl, setAnchorEl] = React.useState(null) - const serializedUrl = new URLSearchParams(url).toString() + const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget) } @@ -62,20 +62,20 @@ export default function SocialShareListButton({ {t('components.social-share.copy')} - + {t('components.social-share.share')} Facebook {t('components.social-share.share')} LinkedIn - - {t('components.social-share.share')} Twitter - + + {t('components.social-share.share')} X + diff --git a/src/components/common/form/AcceptPrivacyPolicyField.tsx b/src/components/common/form/AcceptPrivacyPolicyField.tsx index 37ecce0b7..99a4629b3 100644 --- a/src/components/common/form/AcceptPrivacyPolicyField.tsx +++ b/src/components/common/form/AcceptPrivacyPolicyField.tsx @@ -1,5 +1,6 @@ import { useTranslation } from 'next-i18next' import { Typography } from '@mui/material' +import { useRouter } from 'next/router' import { routes } from 'common/routes' import ExternalLink from 'components/common/ExternalLink' @@ -18,6 +19,8 @@ export default function AcceptPrivacyPolicyField({ ...rest }: AcceptGDPRFieldProps) { const { t } = useTranslation() + const { locale } = useRouter() + return ( {t('validation:informed-agree-with')}{' '} - {t('validation:gdpr')} + + {t('validation:gdpr')} + } {...rest} diff --git a/src/service/stripeClient.ts b/src/service/stripeClient.ts index 8ba7c964c..adda96283 100644 --- a/src/service/stripeClient.ts +++ b/src/service/stripeClient.ts @@ -4,4 +4,4 @@ const { publicRuntimeConfig: { STRIPE_PUBLISHABLE_KEY }, } = getConfig() -export const stripe = await loadStripe(STRIPE_PUBLISHABLE_KEY) +export const stripe = loadStripe(STRIPE_PUBLISHABLE_KEY) diff --git a/yarn.lock b/yarn.lock index 240a11f4e..820395f12 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6625,13 +6625,13 @@ __metadata: linkType: hard "cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": - version: 7.0.3 - resolution: "cross-spawn@npm:7.0.3" + version: 7.0.6 + resolution: "cross-spawn@npm:7.0.6" dependencies: path-key: ^3.1.0 shebang-command: ^2.0.0 which: ^2.0.1 - checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52 + checksum: 8d306efacaf6f3f60e0224c287664093fa9185680b2d195852ba9a863f85d02dcc737094c6e512175f8ee0161f9b87c73c6826034c2422e39de7d6569cf4503b languageName: node linkType: hard