From bb7b755c4805ae45ccf206e8697cd350e1de4c24 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 22 Apr 2025 17:54:35 +0200 Subject: [PATCH 01/49] feat: Refactor user state access and add Navigation component with header --- .../Navigation/NavigationHeader.tsx | 14 ++ .../NavigationProfileModal.tsx} | 138 +++--------------- src/features/navigation/Navigation/index.tsx | 97 ++++++++++++ src/hooks/useActiveWorkspace.ts | 64 +++----- src/hooks/useActiveWorkspaceProjects.ts | 10 +- src/hooks/useFeatureFlag.ts | 15 +- src/hooks/useGTMevent.ts | 12 +- src/pages/Dashboard/index.tsx | 2 +- src/pages/Video/index.tsx | 16 +- 9 files changed, 186 insertions(+), 182 deletions(-) create mode 100644 src/features/navigation/Navigation/NavigationHeader.tsx rename src/features/navigation/{Navigation.tsx => Navigation/NavigationProfileModal.tsx} (62%) create mode 100644 src/features/navigation/Navigation/index.tsx diff --git a/src/features/navigation/Navigation/NavigationHeader.tsx b/src/features/navigation/Navigation/NavigationHeader.tsx new file mode 100644 index 000000000..0a72e446b --- /dev/null +++ b/src/features/navigation/Navigation/NavigationHeader.tsx @@ -0,0 +1,14 @@ +import { Header } from 'src/common/components/navigation/header/header'; +import { useGetUsersMePreferencesQuery } from 'src/features/api'; + +export const NavigationHeader = ({ isMinimal }: { isMinimal?: boolean }) => { + const { isFetching: isFetchingPrefs } = useGetUsersMePreferencesQuery(); + + return ( +
+ ); +}; diff --git a/src/features/navigation/Navigation.tsx b/src/features/navigation/Navigation/NavigationProfileModal.tsx similarity index 62% rename from src/features/navigation/Navigation.tsx rename to src/features/navigation/Navigation/NavigationProfileModal.tsx index f695b3f08..8f75a58f9 100644 --- a/src/features/navigation/Navigation.tsx +++ b/src/features/navigation/Navigation/NavigationProfileModal.tsx @@ -1,76 +1,45 @@ import { - Content, Notification, ProfileModal, - Skeleton, useToast, } from '@appquality/unguess-design-system'; -import { ComponentProps, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom'; import { useAppDispatch, useAppSelector } from 'src/app/hooks'; -import { AppSidebar } from 'src/common/components/navigation/sidebar'; import { isDev } from 'src/common/isDevEnvironment'; import { prepareGravatar } from 'src/common/utils'; import WPAPI from 'src/common/wpapi'; -import { - setProfileModalOpen, - setSidebarOpen, - toggleSidebar, -} from 'src/features/navigation/navigationSlice'; -import { NoActiveWorkSpaceState } from 'src/features/templates/NoActiveWorkspaceState'; -import { useActiveWorkspace } from 'src/hooks/useActiveWorkspace'; -import i18n from 'src/i18n'; -import { styled } from 'styled-components'; -import { Header } from '../../common/components/navigation/header/header'; -import { usePathWithoutLocale } from './usePathWithoutLocale'; import { useGetUsersMePreferencesQuery, usePutUsersMePreferencesBySlugMutation, -} from '../api'; - -const StyledContent = styled(Content)< - ComponentProps & { - isMinimal?: boolean; - children?: React.ReactNode; - } ->` - height: ${({ isMinimal, theme }) => - isMinimal - ? '100%' - : `calc(100% - ${theme.components.chrome.header.height})`}; -`; +} from 'src/features/api'; +import { useActiveWorkspace } from 'src/hooks/useActiveWorkspace'; +import { setProfileModalOpen } from '../navigationSlice'; +import { usePathWithoutLocale } from '../usePathWithoutLocale'; -export const Navigation = ({ - children, - route, - isMinimal = false, -}: { - children: React.ReactNode; - route: string; - isMinimal?: boolean; -}) => { - const { t } = useTranslation(); - const dispatch = useAppDispatch(); - const pathWithoutLocale = usePathWithoutLocale(); +export const NavigationProfileModal = () => { + const isProfileModalOpen = useAppSelector( + (state) => state.navigation.isProfileModalOpen + ); const { userData: user } = useAppSelector((state) => state.user); - const { isProfileModalOpen } = useAppSelector((state) => state.navigation); const { activeWorkspace } = useActiveWorkspace(); + const { addToast } = useToast(); + const { t, i18n } = useTranslation(); + const dispatch = useAppDispatch(); - const { - data: preferences, - isLoading: isLoadingPrefs, - isFetching: isFetchingPrefs, - isError, - } = useGetUsersMePreferencesQuery(); + const { data: preferences } = useGetUsersMePreferencesQuery(); const notificationsPreference = preferences?.items?.find( (preference) => preference?.name === 'notifications_enabled' ); + const pathWithoutLocale = usePathWithoutLocale(); const [updatePreference] = usePutUsersMePreferencesBySlugMutation(); + const onProfileModalClose = () => { + dispatch(setProfileModalOpen(false)); + }; + const onSetSettings = async (value: string) => { await updatePreference({ slug: `${notificationsPreference?.name}`, @@ -93,39 +62,6 @@ export const Navigation = ({ }); }; - useEffect(() => { - switch (route) { - case 'service': - case 'campaigns': - case 'bugs': - case 'bug': - case 'video': - case 'videos': - case 'insights': - dispatch(setSidebarOpen(false)); - break; - case 'template': - dispatch(setSidebarOpen(false)); - break; - default: - dispatch(setSidebarOpen(true)); - break; - } - }, [route]); - - // Set current params - const params = useParams(); - - let parameter = ''; - - if (params) { - Object.keys(params).forEach((key) => { - if (key !== 'language') { - parameter = params[`${key}`] ?? ''; - } - }); - } - const profileModal = { user: { name: user.name, @@ -219,43 +155,7 @@ export const Navigation = ({ disableMenuLanguageSettings: true, }; - const toggleSidebarState = () => { - dispatch(toggleSidebar()); - }; - - const onProfileModalClose = () => { - dispatch(setProfileModalOpen(false)); - }; + if (!isProfileModalOpen) return null; - if (isLoadingPrefs) { - return ; - } - if (isError || !preferences) { - return null; - } - if (!activeWorkspace) return ; - return ( - <> -
- {isProfileModalOpen && ( - - )} - - - {children} - - - ); + return ; }; diff --git a/src/features/navigation/Navigation/index.tsx b/src/features/navigation/Navigation/index.tsx new file mode 100644 index 000000000..b3db216d3 --- /dev/null +++ b/src/features/navigation/Navigation/index.tsx @@ -0,0 +1,97 @@ +import { Content } from '@appquality/unguess-design-system'; +import { ComponentProps, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { useAppDispatch } from 'src/app/hooks'; +import { AppSidebar } from 'src/common/components/navigation/sidebar'; +import { + setSidebarOpen, + toggleSidebar, +} from 'src/features/navigation/navigationSlice'; +import { NoActiveWorkSpaceState } from 'src/features/templates/NoActiveWorkspaceState'; +import { useActiveWorkspace } from 'src/hooks/useActiveWorkspace'; +import { styled } from 'styled-components'; +import { NavigationHeader } from './NavigationHeader'; +import { NavigationProfileModal } from './NavigationProfileModal'; + +const StyledContent = styled(Content)< + ComponentProps & { + isMinimal?: boolean; + children?: React.ReactNode; + } +>` + height: ${({ isMinimal, theme }) => + isMinimal + ? '100%' + : `calc(100% - ${theme.components.chrome.header.height})`}; +`; + +export const Navigation = ({ + children, + route, + isMinimal = false, +}: { + children: React.ReactNode; + route: string; + isMinimal?: boolean; +}) => { + const dispatch = useAppDispatch(); + const { activeWorkspace, isLoading } = useActiveWorkspace(); + + useEffect(() => { + switch (route) { + case 'service': + case 'campaigns': + case 'bugs': + case 'bug': + case 'video': + case 'videos': + case 'insights': + dispatch(setSidebarOpen(false)); + break; + case 'template': + dispatch(setSidebarOpen(false)); + break; + default: + dispatch(setSidebarOpen(true)); + break; + } + }, [route]); + + // Set current params + const params = useParams(); + + let parameter = ''; + + if (params) { + Object.keys(params).forEach((key) => { + if (key !== 'language') { + parameter = params[`${key}`] ?? ''; + } + }); + } + + const toggleSidebarState = () => { + dispatch(toggleSidebar()); + }; + + if (!activeWorkspace && !isLoading) return ; + + return ( + <> + + + + + {children} + + + ); +}; diff --git a/src/hooks/useActiveWorkspace.ts b/src/hooks/useActiveWorkspace.ts index 27e0b07f2..e9db17977 100644 --- a/src/hooks/useActiveWorkspace.ts +++ b/src/hooks/useActiveWorkspace.ts @@ -1,65 +1,41 @@ -import { useEffect, useState } from 'react'; +import { useMemo } from 'react'; import { useAppSelector } from 'src/app/hooks'; -import { Workspace, useGetWorkspacesQuery } from 'src/features/api'; +import { useGetWorkspacesQuery } from 'src/features/api'; import { getWorkspaceFromLS, saveWorkspaceToLs, } from 'src/features/navigation/cachedStorage'; export const useActiveWorkspace = () => { - const cachedWorkspace = getWorkspaceFromLS(); const { data: workspaces, isLoading } = useGetWorkspacesQuery({ orderBy: 'company', }); - const [result, setResult] = useState(() => { - if ( - cachedWorkspace && - workspaces && - workspaces.items && - workspaces.items.map((w) => w.id).includes(cachedWorkspace.id) - ) { - return cachedWorkspace; - } - return undefined; - }); - const activeWorkspace = useAppSelector( + + const activeWorkspaceFromRedux = useAppSelector( (state) => state.navigation.activeWorkspace ); - useEffect(() => { - if (activeWorkspace) { - setResult(activeWorkspace); - return; + const workspace = useMemo(() => { + if (activeWorkspaceFromRedux) { + return activeWorkspaceFromRedux; } - if (result) return; + if (!isLoading && workspaces?.items?.length) { + const cached = getWorkspaceFromLS(); - if ( - isLoading || - !workspaces || - !workspaces?.items || - workspaces.items.length === 0 - ) - return; + const found = cached && workspaces.items.find((w) => w.id === cached.id); + if (found) { + return found; + } - if ( - cachedWorkspace && - workspaces.items.map((w) => w.id).includes(cachedWorkspace.id) - ) { - setResult(cachedWorkspace); - return; + // default fallback + const first = workspaces.items[0]; + saveWorkspaceToLs(first); + return first; } - if ( - !isLoading && - workspaces && - workspaces.items && - workspaces.items.length > 0 - ) { - saveWorkspaceToLs(workspaces.items[0]); - setResult(workspaces.items[0]); - } - }, [activeWorkspace, workspaces, isLoading]); + return undefined; + }, [activeWorkspaceFromRedux, workspaces, isLoading]); - return { activeWorkspace: result }; + return { activeWorkspace: workspace, isLoading }; }; diff --git a/src/hooks/useActiveWorkspaceProjects.ts b/src/hooks/useActiveWorkspaceProjects.ts index 2b9d5a870..661fb83a4 100644 --- a/src/hooks/useActiveWorkspaceProjects.ts +++ b/src/hooks/useActiveWorkspaceProjects.ts @@ -4,6 +4,11 @@ import { useActiveWorkspace } from './useActiveWorkspace'; const useActiveWorkspaceProjects = () => { const { activeWorkspace } = useActiveWorkspace(); + const { data, isLoading, isFetching, isError } = + useGetWorkspacesByWidProjectsQuery({ + wid: activeWorkspace?.id.toString() || '', + }); + // If there is no active workspace, we return an empty object if (!activeWorkspace) return { data: undefined, @@ -12,11 +17,6 @@ const useActiveWorkspaceProjects = () => { isError: false, }; - const { data, isLoading, isFetching, isError } = - useGetWorkspacesByWidProjectsQuery({ - wid: activeWorkspace?.id.toString() || '', - }); - return { data, isLoading, diff --git a/src/hooks/useFeatureFlag.ts b/src/hooks/useFeatureFlag.ts index b26bd73b2..0cd2cc39f 100644 --- a/src/hooks/useFeatureFlag.ts +++ b/src/hooks/useFeatureFlag.ts @@ -1,14 +1,21 @@ +import { shallowEqual } from 'react-redux'; import { useAppSelector } from 'src/app/hooks'; export const useFeatureFlag = () => { - const { userData: user } = useAppSelector((state) => state.user); + const { role, features } = useAppSelector( + (state) => ({ + role: state.user.userData.role, + features: state.user.userData.features, + }), + shallowEqual + ); const hasFeatureFlag = (slug?: string) => { - if (user && user.role === 'administrator') { + if (role === 'administrator') { return true; } - if (user && user.features) { - return user.features.some((feature) => feature.slug === slug); + if (features) { + return features.some((feature) => feature.slug === slug); } return false; }; diff --git a/src/hooks/useGTMevent.ts b/src/hooks/useGTMevent.ts index 3428ea434..1115515bf 100644 --- a/src/hooks/useGTMevent.ts +++ b/src/hooks/useGTMevent.ts @@ -1,5 +1,6 @@ import { useCallback } from 'react'; import TagManager from 'react-gtm-module'; +import { shallowEqual } from 'react-redux'; import { useAppSelector } from 'src/app/hooks'; import { useActiveWorkspace } from './useActiveWorkspace'; @@ -12,7 +13,16 @@ export interface GTMEventData { } export const useSendGTMevent = () => { - const { userData: user } = useAppSelector((state) => state.user); + const user = useAppSelector( + (state) => ({ + role: state.user.userData.role, + tryber_wp_user_id: state.user.userData.tryber_wp_user_id, + id: state.user.userData.id, + name: state.user.userData.name, + email: state.user.userData.email, + }), + shallowEqual + ); const { activeWorkspace } = useActiveWorkspace(); const callback = useCallback( diff --git a/src/pages/Dashboard/index.tsx b/src/pages/Dashboard/index.tsx index 00cd0a26f..89f2c655f 100644 --- a/src/pages/Dashboard/index.tsx +++ b/src/pages/Dashboard/index.tsx @@ -16,7 +16,7 @@ import { SuggestedCampaigns } from './SuggestedCampaigns'; const Dashboard = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const { status } = useAppSelector((state) => state.user); + const status = useAppSelector((state) => state.user.status); const sendGTMEvent = useSendGTMevent(); if (status === 'logged') dispatch(resetFilters()); // Reset filters diff --git a/src/pages/Video/index.tsx b/src/pages/Video/index.tsx index 1621e0fb7..106721d7c 100644 --- a/src/pages/Video/index.tsx +++ b/src/pages/Video/index.tsx @@ -1,20 +1,20 @@ +import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; -import { useGetCampaignWithWorkspaceQuery } from 'src/features/api/customEndpoints/getCampaignWithWorkspace'; -import { Page } from 'src/features/templates/Page'; -import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute'; import { useAppDispatch, useAppSelector } from 'src/app/hooks'; -import { useCampaignAnalytics } from 'src/hooks/useCampaignAnalytics'; -import { useEffect } from 'react'; +import { FEATURE_FLAG_TAGGING_TOOL } from 'src/constants'; +import { useGetCampaignWithWorkspaceQuery } from 'src/features/api/customEndpoints/getCampaignWithWorkspace'; import { setCampaignId, setPermissionSettingsTitle, setWorkspace, } from 'src/features/navigation/navigationSlice'; -import { FEATURE_FLAG_TAGGING_TOOL } from 'src/constants'; +import { Page } from 'src/features/templates/Page'; +import { useCampaignAnalytics } from 'src/hooks/useCampaignAnalytics'; import { useFeatureFlag } from 'src/hooks/useFeatureFlag'; -import VideoPageContent from './Content'; +import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute'; import VideoPageHeader from './components/PageHeader'; +import VideoPageContent from './Content'; const VideoPage = () => { const { t } = useTranslation(); @@ -23,7 +23,7 @@ const VideoPage = () => { const { campaignId } = useParams(); const dispatch = useAppDispatch(); const location = useLocation(); - const { status } = useAppSelector((state) => state.user); + const status = useAppSelector((state) => state.user.status); const { hasFeatureFlag } = useFeatureFlag(); const hasTaggingToolFeature = hasFeatureFlag(FEATURE_FLAG_TAGGING_TOOL); From 7ab70dafc0037f64ba849a1d5e729e8bc8c2c9e1 Mon Sep 17 00:00:00 2001 From: iacopolea Date: Wed, 23 Apr 2025 18:31:51 +0200 Subject: [PATCH 02/49] test(mock): add delete plan mock and update related tests --- tests/e2e/plan/draft.spec.ts | 12 +++++- tests/fixtures/pages/Plan.ts | 83 +++++++++++++++++++++++++++--------- 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/tests/e2e/plan/draft.spec.ts b/tests/e2e/plan/draft.spec.ts index 96790580e..b85d2f166 100644 --- a/tests/e2e/plan/draft.spec.ts +++ b/tests/e2e/plan/draft.spec.ts @@ -146,7 +146,7 @@ test.describe('When the user clicks on the dots menu', () => { await planPage.mockWorkspace(); await planPage.mockWorkspacesList(); await planPage.mockGetDraftWithOnlyMandatoryModulesPlan(); - + await planPage.mockDeletePlan(); await planPage.open(); }); @@ -167,13 +167,21 @@ test.describe('When the user clicks on the dots menu', () => { await expect(planPage.elements().deletePlanModalCancelCTA()).toBeEnabled(); }); - test("When the user clicks on the 'Delete permanently' CTA, he is redirected to the home page", async ({ + test("When the user clicks on the 'Delete permanently' CTA, a call to the DELETE endpoint is made and if ok he is redirected to the home page", async ({ page, }) => { + const deletePromise = page.waitForResponse( + (response) => + /\/api\/plans\/1/.test(response.url()) && + response.status() === 200 && + response.request().method() === 'DELETE' + ); await planPage.elements().extraActionsMenu().click(); await planPage.elements().deletePlanActionItem().click(); await planPage.elements().deletePlanModalConfirmCTA().click(); + await deletePromise; // wait for the DELETE request to be made + await expect(planPage.elements().deletePlanModal()).not.toBeVisible(); await expect(page).toHaveURL('/'); }); diff --git a/tests/fixtures/pages/Plan.ts b/tests/fixtures/pages/Plan.ts index 51c26ece5..e95997ffc 100644 --- a/tests/fixtures/pages/Plan.ts +++ b/tests/fixtures/pages/Plan.ts @@ -189,59 +189,100 @@ export class PlanPage extends UnguessPage { async mockGetDraftPlan() { await this.page.route('*/**/api/plans/1', async (route) => { - await route.fulfill({ - path: 'tests/api/plans/pid/_get/200_draft_complete.json', - }); + if (route.request().method() === 'GET') { + await route.fulfill({ + path: 'tests/api/plans/pid/_get/200_draft_complete.json', + }); + } else { + await route.fallback(); + } }); } async mockGetDraftPlanWithDateError() { await this.page.route('*/**/api/plans/1', async (route) => { - await route.fulfill({ - path: 'tests/api/plans/pid/_get/200_draft_complete_date_error.json', - }); + if (route.request().method() === 'GET') { + await route.fulfill({ + path: 'tests/api/plans/pid/_get/200_draft_complete_date_error.json', + }); + } else { + await route.fallback(); + } }); } // some modules are mandatory, in this api call we mock a plan with only mandatory modules async mockGetDraftWithOnlyMandatoryModulesPlan() { await this.page.route('*/**/api/plans/1', async (route) => { - await route.fulfill({ - path: 'tests/api/plans/pid/_get/200_draft_mandatory_only.json', - }); + if (route.request().method() === 'GET') { + await route.fulfill({ + path: 'tests/api/plans/pid/_get/200_draft_mandatory_only.json', + }); + } else { + await route.fallback(); + } }); } // some modules are mandatory, in this api call we mock a plan missing some mandatory modules async mockGetDraftWithMissingMandatoryModulesPlan() { await this.page.route('*/**/api/plans/1', async (route) => { - await route.fulfill({ - path: 'tests/api/plans/pid/_get/200_draft_missing_mandatory.json', - }); + if (route.request().method() === 'GET') { + await route.fulfill({ + path: 'tests/api/plans/pid/_get/200_draft_missing_mandatory.json', + }); + } else { + await route.fallback(); + } }); } async mockGetPendingReviewPlan() { await this.page.route('*/**/api/plans/1', async (route) => { - await route.fulfill({ - path: 'tests/api/plans/pid/_get/200_pending_review.json', - }); + if (route.request().method() === 'GET') { + await route.fulfill({ + path: 'tests/api/plans/pid/_get/200_pending_review.json', + }); + } else { + await route.fallback(); + } }); } async mockPatchPlan() { await this.page.route('*/**/api/plans/1', async (route) => { - await route.fulfill({ - path: 'tests/api/plans/pid/_patch/200_Example_1.json', - }); + if (route.request().method() === 'PATCH') { + await route.fulfill({ + path: 'tests/api/plans/pid/_patch/200_Example_1.json', + }); + } else { + await route.fallback(); + } }); } async mockPatchStatus() { await this.page.route('*/**/api/plans/1/status', async (route) => { - await route.fulfill({ - path: 'tests/api/plans/pid/status/_patch/request_Example_1.json', - }); + if (route.request().method() === 'PATCH') { + await route.fulfill({ + path: 'tests/api/plans/pid/status/_patch/request_Example_1.json', + }); + } else { + await route.fallback(); + } + }); + } + + async mockDeletePlan(statusCode: number = 200) { + await this.page.route('*/**/api/plans/1', async (route) => { + if (route.request().method() === 'DELETE') { + await route.fulfill({ + status: statusCode, + json: {}, + }); + } else { + await route.fallback(); + } }); } } From 305d6c69dee458a7243dccc02f3a8a5e4e2bf16f Mon Sep 17 00:00:00 2001 From: iacopolea Date: Thu, 24 Apr 2025 17:40:17 +0200 Subject: [PATCH 03/49] test(mock): add mockPatchPlan method and update related tests for PATCH Plan --- .../pid/_patch/200_draft_mandatory_only.json | 41 +++++++++++++++++++ tests/e2e/plan/draft.spec.ts | 5 ++- tests/fixtures/pages/Plan.ts | 2 +- 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 tests/api/plans/pid/_patch/200_draft_mandatory_only.json diff --git a/tests/api/plans/pid/_patch/200_draft_mandatory_only.json b/tests/api/plans/pid/_patch/200_draft_mandatory_only.json new file mode 100644 index 000000000..86284a8e3 --- /dev/null +++ b/tests/api/plans/pid/_patch/200_draft_mandatory_only.json @@ -0,0 +1,41 @@ +{ + "id": 13, + "workspace_id": 1, + "status": "draft", + "project": { + "id": 90, + "name": "MyProject" + }, + "config": { + "modules": [ + { + "type": "title", + "variant": "default", + "output": "My Plan" + }, + { + "type": "dates", + "variant": "default", + "output": { + "start": "2041-12-17T08:00:00.000Z" + } + }, + { + "type": "tasks", + "variant": "default", + "output": [ + { + "kind": "bug", + "title": "Search for bugs", + "description": "description kind bug" + }, + { + "kind": "video", + "title": "Think aloud", + "description": "description kind video" + } + ] + } + ] + } +} diff --git a/tests/e2e/plan/draft.spec.ts b/tests/e2e/plan/draft.spec.ts index b85d2f166..978b05975 100644 --- a/tests/e2e/plan/draft.spec.ts +++ b/tests/e2e/plan/draft.spec.ts @@ -14,6 +14,7 @@ test.describe('The module builder', () => { await planPage.mockWorkspacesList(); await planPage.mockGetDraftWithOnlyMandatoryModulesPlan(); await planPage.mockPatchStatus(); + await planPage.mockPatchPlan(); await planPage.open(); }); @@ -65,7 +66,7 @@ test.describe('The module builder', () => { }) => { const patchPromise = page.waitForResponse( (response) => - /\/api\/plans\/1(?!\/status)/.test(response.url()) && + /\/api\/plans\/1/.test(response.url()) && response.status() === 200 && response.request().method() === 'PATCH' ); @@ -85,7 +86,7 @@ test.describe('The module builder', () => { test('if confirmation calls the PATCH Plan', async ({ page }) => { const patchPromise = page.waitForResponse( (response) => - /\/api\/plans\/1(?!\/status)/.test(response.url()) && + /\/api\/plans\/1/.test(response.url()) && response.status() === 200 && response.request().method() === 'PATCH' ); diff --git a/tests/fixtures/pages/Plan.ts b/tests/fixtures/pages/Plan.ts index e95997ffc..ed51886ff 100644 --- a/tests/fixtures/pages/Plan.ts +++ b/tests/fixtures/pages/Plan.ts @@ -253,7 +253,7 @@ export class PlanPage extends UnguessPage { await this.page.route('*/**/api/plans/1', async (route) => { if (route.request().method() === 'PATCH') { await route.fulfill({ - path: 'tests/api/plans/pid/_patch/200_Example_1.json', + path: 'tests/api/plans/pid/_patch/200_draft_mandatory_only.json', }); } else { await route.fallback(); From 4a458fcc0c35336dd8d22736802d2bac7b1df913 Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 6 May 2025 10:31:41 +0200 Subject: [PATCH 04/49] fix(translation): Update "View all" text to "View more" for clarity --- src/locales/en/translation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index e9c5b7428..a8453fca6 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1445,7 +1445,7 @@ "_CAMPAIGN_WIDGET_UX_USER_ANALYSIS_HEADER": "Analyzed User Contributions", "_PLAN_PAGE_MODULE_LANGUAGE_DESCRIPTION": "You’ll receive feedback in the language you’re selecting", "_PLAN_PAGE_MODULE_LANGUAGE_SUBTITLE": "Select participants' preferred language", - "_PROJECT_PAGE_PLANS_GROUP_SEE_ALL": "View all", + "_PROJECT_PAGE_PLANS_GROUP_SEE_ALL": "View more", "_PROJECT_PAGE_PLANS_GROUP_SEE_LESS": "View less", "_PROJECT_PAGE_PLANS_GROUP_SUBTITLE": "Create and configure new activities or review those awaiting approval", "_PROJECT_PAGE_PLANS_GROUP_TITLE": "Setup activities", From 87a5fc5fd9d3a79577500e2d01f0e8769738d20a Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 6 May 2025 10:31:52 +0200 Subject: [PATCH 05/49] feat(plans): update button text to show remaining items count --- src/pages/Dashboard/project-items/Plans.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/Dashboard/project-items/Plans.tsx b/src/pages/Dashboard/project-items/Plans.tsx index f4dc601cd..9a8ed3860 100644 --- a/src/pages/Dashboard/project-items/Plans.tsx +++ b/src/pages/Dashboard/project-items/Plans.tsx @@ -89,9 +89,10 @@ export const Plans = ({ projectId }: { projectId: number }) => { {isPreview ? : } {isPreview - ? t('_PROJECT_PAGE_PLANS_GROUP_SEE_ALL') + ? `${t('_PROJECT_PAGE_PLANS_GROUP_SEE_ALL')} (${ + items.length - PREVIEW_ITEMS_MAX_SIZE + })` : t('_PROJECT_PAGE_PLANS_GROUP_SEE_LESS')} - {` (${items.length})`} )} From 7f70a1fab22429490235e3c1b3f174e926002927 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 6 May 2025 10:53:34 +0200 Subject: [PATCH 06/49] rework: Use accordion new in observations --- src/pages/Video/components/Observation.tsx | 142 ++++++++++----------- 1 file changed, 64 insertions(+), 78 deletions(-) diff --git a/src/pages/Video/components/Observation.tsx b/src/pages/Video/components/Observation.tsx index c39572103..824b12ad0 100644 --- a/src/pages/Video/components/Observation.tsx +++ b/src/pages/Video/components/Observation.tsx @@ -1,30 +1,27 @@ import { - Accordion, + AccordionNew, IconButton, - LG, Notification, - SM, - Title, Tooltip, useToast, } from '@appquality/unguess-design-system'; +import { useCallback, useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useParams } from 'react-router-dom'; +import { appTheme } from 'src/app/theme'; +import { ReactComponent as LinkIcon } from 'src/assets/icons/link-stroke.svg'; +import { ReactComponent as TagIcon } from 'src/assets/icons/tag-icon.svg'; +import { Divider } from 'src/common/components/divider'; +import { getColorWithAlpha } from 'src/common/utils'; import { GetVideosByVidApiResponse, GetVideosByVidObservationsApiResponse, } from 'src/features/api'; -import { ReactComponent as TagIcon } from 'src/assets/icons/tag-icon.svg'; -import { ReactComponent as LinkIcon } from 'src/assets/icons/link-stroke.svg'; -import { useTranslation } from 'react-i18next'; -import { useParams } from 'react-router-dom'; -import { useCallback, useEffect, useState } from 'react'; -import { appTheme } from 'src/app/theme'; import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute'; -import { styled } from 'styled-components'; -import { getColorWithAlpha } from 'src/common/utils'; import { formatDuration } from 'src/pages/Videos/utils/formatDuration'; -import { Divider } from 'src/common/components/divider'; -import { ObservationForm } from './ObservationForm'; +import { styled } from 'styled-components'; import { useVideoContext } from '../context/VideoContext'; +import { ObservationForm } from './ObservationForm'; const Circle = styled.div<{ color: string; @@ -38,12 +35,6 @@ const Circle = styled.div<{ border-radius: 50%; `; -const Container = styled.div` - display: flex; - align-items: center; - width: 100%; -`; - const Observation = ({ observation, refScroll, @@ -150,81 +141,76 @@ const Observation = ({ return ( <> - - - - - - + tag.group.name.toLowerCase() === 'severity' + )?.tag.style || appTheme.palette.grey[600] + } + style={{ + backgroundColor: getColorWithAlpha( observation.tags.find( (tag) => tag.group.name.toLowerCase() === 'severity' - )?.tag.style || appTheme.palette.grey[600] - } + )?.tag.style || appTheme.palette.grey[600], + 0.1 + ), + }} + > + tag.group.name.toLowerCase() === 'severity' )?.tag.style || appTheme.palette.grey[600], - 0.1 - ), }} + /> + + } + > + + + + + copyLink(`observation-${observation.id}`, event) + } > - tag.group.name.toLowerCase() === 'severity' - )?.tag.style || appTheme.palette.grey[600], - }} - /> - - - <LG isBold>{title}</LG> - <SM - style={{ - color: appTheme.palette.grey[600], - marginTop: appTheme.space.xs, - }} - > - {formatDuration(start)} - {formatDuration(end)} - </SM> - - - - copyLink(`observation-${observation.id}`, event) - } - > - - - - - - - + + + + + + - - - + + + ); }; From 5c203468c19d9f7862498a956b1541c4e4267408 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 6 May 2025 10:54:06 +0200 Subject: [PATCH 07/49] rework: Use accordionnew in prj invite --- .../inviteUsers/projectSettings.tsx | 57 ++++++++----------- src/common/components/inviteUsers/styled.tsx | 23 +------- .../components/inviteUsers/userItem.tsx | 36 ++++++------ 3 files changed, 43 insertions(+), 73 deletions(-) diff --git a/src/common/components/inviteUsers/projectSettings.tsx b/src/common/components/inviteUsers/projectSettings.tsx index 14e8b710e..ba689c224 100644 --- a/src/common/components/inviteUsers/projectSettings.tsx +++ b/src/common/components/inviteUsers/projectSettings.tsx @@ -1,12 +1,13 @@ import { + AccordionNew, Button, + getColor, Label, MD, Modal, ModalClose, Notification, Span, - getColor, useToast, } from '@appquality/unguess-design-system'; import { FormikHelpers } from 'formik'; @@ -32,8 +33,6 @@ import { FixedBody, FlexContainer, SettingsDivider, - StyledAccordion, - StyledAccordionPanel, UsersContainer, UsersLabel, } from './styled'; @@ -335,42 +334,36 @@ export const ProjectSettings = () => { )} {workspaceCount > 0 && ( - - - - - - - - {t('__PERMISSION_SETTINGS_WORKSPACE_USERS')}{' '} - - ({workspaceCount}) - - - - - - + + + } + > + + + {workspaceUsers?.items.map((user) => ( ))} - - - + + + )} diff --git a/src/common/components/inviteUsers/styled.tsx b/src/common/components/inviteUsers/styled.tsx index 7cb958430..7646f9c94 100644 --- a/src/common/components/inviteUsers/styled.tsx +++ b/src/common/components/inviteUsers/styled.tsx @@ -1,9 +1,4 @@ -import { - Accordion, - MD, - Modal, - getColor, -} from '@appquality/unguess-design-system'; +import { getColor, MD, Modal } from '@appquality/unguess-design-system'; import styled from 'styled-components'; export const FlexContainer = styled.div<{ isLoading?: boolean }>` @@ -25,22 +20,6 @@ export const SettingsDivider = styled.div` padding-top: ${({ theme }) => theme.space.base * 6}px; `; -export const StyledAccordion = styled(Accordion)<{ - isDisabled?: boolean; -}>` - ${({ isDisabled }) => - isDisabled && - ` - opacity: 0.5; - pointer-events: none; - `} -`; - -export const StyledAccordionPanel = styled(Accordion.Panel)` - padding: 0; - padding-left: ${({ theme }) => theme.space.xs}; -`; - export const UsersLabel = styled(MD)` display: flex; align-items: center; diff --git a/src/common/components/inviteUsers/userItem.tsx b/src/common/components/inviteUsers/userItem.tsx index f5501e75d..4a0b91f3d 100644 --- a/src/common/components/inviteUsers/userItem.tsx +++ b/src/common/components/inviteUsers/userItem.tsx @@ -2,10 +2,10 @@ import { Avatar, ButtonMenu, Ellipsis, + getColor, MD, SM, Span, - getColor, } from '@appquality/unguess-design-system'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -56,27 +56,25 @@ export const UserItem = ({ {getInitials(user.name.length ? user.name : user.email)}
- - + + {user.name.length ? user.name : user.email}{' '} {isMe && t('__WORKSPACE_SETTINGS_CURRENT_MEMBER_YOU_LABEL')} - - - {user.name.length > 0 && ( - - - {user.email} - + + {user.name.length > 0 && ( + + {user.email} + )}
{onResendInvite && onRemoveUser ? ( From a6c737667ee09ee9d4866c39b924637f12fe2698 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Tue, 6 May 2025 11:22:57 +0200 Subject: [PATCH 08/49] rework: Replace StyledAccordion with AccordionNew in campaign settings --- .../inviteUsers/campaignSettings.tsx | 109 ++++++++---------- 1 file changed, 48 insertions(+), 61 deletions(-) diff --git a/src/common/components/inviteUsers/campaignSettings.tsx b/src/common/components/inviteUsers/campaignSettings.tsx index bcce76764..b8be7573d 100644 --- a/src/common/components/inviteUsers/campaignSettings.tsx +++ b/src/common/components/inviteUsers/campaignSettings.tsx @@ -1,12 +1,13 @@ import { + AccordionNew, Button, + getColor, Label, MD, Modal, ModalClose, Notification, Span, - getColor, useToast, } from '@appquality/unguess-design-system'; import { FormikHelpers } from 'formik'; @@ -35,8 +36,6 @@ import { FixedBody, FlexContainer, SettingsDivider, - StyledAccordion, - StyledAccordionPanel, UsersContainer, UsersLabel, } from './styled'; @@ -361,42 +360,36 @@ export const CampaignSettings = () => { )} {projectCount > 0 && ( - - - - - - - - {t('__PERMISSION_SETTINGS_PROJECT_USERS')}{' '} - - ({projectCount}) - - - - - - + + + } + > + + + {projectUsers?.items.map((user) => ( ))} - - - + + + )} {workspaceUsersError && ( @@ -412,42 +405,36 @@ export const CampaignSettings = () => { )} {workspaceCount > 0 && ( - - - - - - - - {t('__PERMISSION_SETTINGS_WORKSPACE_USERS')}{' '} - - ({workspaceCount}) - - - - - - + + + } + > + + + {workspaceUsers?.items.map((user) => ( ))} - - - + + + )} From 0238af08c31d864b41050812c5f3077e98bfe24b Mon Sep 17 00:00:00 2001 From: Kariamos Date: Tue, 6 May 2025 14:22:21 +0200 Subject: [PATCH 09/49] style(project-items): update marginTop to use appTheme spacing --- src/pages/Dashboard/project-items/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Dashboard/project-items/index.tsx b/src/pages/Dashboard/project-items/index.tsx index b74b0f71e..763336d91 100644 --- a/src/pages/Dashboard/project-items/index.tsx +++ b/src/pages/Dashboard/project-items/index.tsx @@ -75,7 +75,7 @@ export const ProjectItems = ({ From 43562283b7e1dfaf8e975834676721a1df4712d7 Mon Sep 17 00:00:00 2001 From: iacopolea Date: Tue, 6 May 2025 18:39:45 +0200 Subject: [PATCH 10/49] feat: Enhance accessibility and error handling in modals and task management - Added role attributes to various components for improved accessibility. - Updated SendRequestModal to include error message title and role. - Enhanced TaskItem and TasksList with appropriate roles for better screen reader support. - Implemented role attributes in DeleteTaskConfirmationModal and TasksModal for consistency. - Refactored tests to utilize new RequestQuotationModal and TasksModule classes for better structure and maintainability. - Removed deprecated PlanPage class and integrated its functionality into the new structure. - Improved error handling in tests related to task management and request quotations. --- src/pages/Plan/modals/SendRequestModal.tsx | 4 +- .../Plan/modules/Tasks/parts/TaskItem.tsx | 3 +- .../Plan/modules/Tasks/parts/TasksList.tsx | 4 +- .../modal/DeleteTaskConfirmationModal.tsx | 2 +- .../modules/Tasks/parts/modal/TasksModal.tsx | 1 + tests/e2e/plan/draft.spec.ts | 61 ++------ tests/e2e/plan/modules/tasks.spec.ts | 78 ++++++++-- tests/e2e/plan/modules/title.spec.ts | 48 ++++--- .../pages/Plan/RequestQuotationModal.ts | 65 +++++++++ tests/fixtures/pages/Plan/TasksModule.ts | 77 ++++++++++ .../fixtures/pages/{Plan.ts => Plan/index.ts} | 134 ++++++++---------- 11 files changed, 319 insertions(+), 158 deletions(-) create mode 100644 tests/fixtures/pages/Plan/RequestQuotationModal.ts create mode 100644 tests/fixtures/pages/Plan/TasksModule.ts rename tests/fixtures/pages/{Plan.ts => Plan/index.ts} (92%) diff --git a/src/pages/Plan/modals/SendRequestModal.tsx b/src/pages/Plan/modals/SendRequestModal.tsx index c751e1b4c..dbea41698 100644 --- a/src/pages/Plan/modals/SendRequestModal.tsx +++ b/src/pages/Plan/modals/SendRequestModal.tsx @@ -37,6 +37,8 @@ const SendRequestModal = ({ onQuit }: { onQuit: () => void }) => { void }) => { } return ( - + {t('__PLAN_PAGE_MODAL_SEND_REQUEST_TITLE')} {t('__PLAN_PAGE_MODAL_SEND_REQUEST_BODY_TITLE')} diff --git a/src/pages/Plan/modules/Tasks/parts/TaskItem.tsx b/src/pages/Plan/modules/Tasks/parts/TaskItem.tsx index 32fb7757a..0b6cbe9d4 100644 --- a/src/pages/Plan/modules/Tasks/parts/TaskItem.tsx +++ b/src/pages/Plan/modules/Tasks/parts/TaskItem.tsx @@ -67,6 +67,7 @@ const TaskItem = ({ key={`task-${index}`} hasBorder type={hasError ? 'danger' : 'default'} + role="listitem" > @@ -100,7 +101,7 @@ const TaskItem = ({ )} -
+
{kind !== 'explorative-bug' && (
- + {value.map((task) => ( ))} diff --git a/src/pages/Plan/modules/Tasks/parts/modal/DeleteTaskConfirmationModal.tsx b/src/pages/Plan/modules/Tasks/parts/modal/DeleteTaskConfirmationModal.tsx index 9cdc09fc3..9edd27a90 100644 --- a/src/pages/Plan/modules/Tasks/parts/modal/DeleteTaskConfirmationModal.tsx +++ b/src/pages/Plan/modules/Tasks/parts/modal/DeleteTaskConfirmationModal.tsx @@ -45,7 +45,7 @@ const DeleteTaskConfirmationModal = ({ if (!state[0].isOpen) return null; return ( - + {t('__PLAN_PAGE_MODULE_TASKS_MODAL_CONFIRMATION_REMOVE_TASK_TITLE')} diff --git a/src/pages/Plan/modules/Tasks/parts/modal/TasksModal.tsx b/src/pages/Plan/modules/Tasks/parts/modal/TasksModal.tsx index c3c8ea728..f22140e9c 100644 --- a/src/pages/Plan/modules/Tasks/parts/modal/TasksModal.tsx +++ b/src/pages/Plan/modules/Tasks/parts/modal/TasksModal.tsx @@ -57,6 +57,7 @@ const TasksModal = () => { onClose={() => setModalRef(null)} placement="auto" hasArrow={false} + role="dialog" > {t('__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_TITLE')} diff --git a/tests/e2e/plan/draft.spec.ts b/tests/e2e/plan/draft.spec.ts index 978b05975..2b1c1da62 100644 --- a/tests/e2e/plan/draft.spec.ts +++ b/tests/e2e/plan/draft.spec.ts @@ -1,19 +1,22 @@ import examplePatch from '../../api/plans/pid/_patch/request_Example_1.json'; import { expect, test } from '../../fixtures/app'; import { PlanPage } from '../../fixtures/pages/Plan'; +import { RequestQuotationModal } from '../../fixtures/pages/Plan/RequestQuotationModal'; test.describe('The module builder', () => { let planPage: PlanPage; + let requestQuotationModal: RequestQuotationModal; test.beforeEach(async ({ page }, testinfo) => { testinfo.setTimeout(60000); planPage = new PlanPage(page); + requestQuotationModal = new RequestQuotationModal(page); await planPage.loggedIn(); await planPage.mockPreferences(); await planPage.mockWorkspace(); await planPage.mockWorkspacesList(); await planPage.mockGetDraftWithOnlyMandatoryModulesPlan(); - await planPage.mockPatchStatus(); + await requestQuotationModal.mockPatchStatus(); await planPage.mockPatchPlan(); await planPage.open(); @@ -25,10 +28,6 @@ test.describe('The module builder', () => { // Check if specific elements are visible on the "Setup" tab await expect(planPage.elements().titleModule()).toBeVisible(); - // await expect(planPage.elements().datesModule()).toBeVisible(); - - // Check if other modules are not visible - await expect(planPage.elements().tasksModule()).not.toBeVisible(); // Check if the save button, request quote CTA and dots menu are visible and enabled await expect(planPage.elements().saveConfigurationCTA()).toBeVisible(); @@ -39,21 +38,6 @@ test.describe('The module builder', () => { await expect(planPage.elements().extraActionsMenu()).not.toBeDisabled(); }); - test('The task module is visible if instructionTab is clicked', async () => { - await planPage.elements().instructionsTab().click(); - - await expect(planPage.elements().tasksModule()).toBeVisible(); - - // todo: check if the other modules are not visible - await expect(planPage.elements().datesModule()).not.toBeVisible(); - - await expect(planPage.elements().saveConfigurationCTA()).toBeVisible(); - await expect(planPage.elements().descriptionModule()).not.toBeVisible(); - await expect(planPage.elements().saveConfigurationCTA()).not.toBeDisabled(); - await expect(planPage.elements().requestQuotationCTA()).toBeVisible(); - await expect(planPage.elements().requestQuotationCTA()).not.toBeDisabled(); - }); - test("The summary Tab isn't clickable", async () => { await expect(planPage.elements().summaryTab()).toBeVisible(); await expect(planPage.elements().summaryTab()).toBeDisabled(); @@ -91,51 +75,22 @@ test.describe('The module builder', () => { response.request().method() === 'PATCH' ); await planPage.elements().requestQuotationCTA().click(); - await planPage.elements().requestQuotationModalCTA().click(); + await requestQuotationModal.elements().submitCTA().click(); const response = await patchPromise; const data = response.request().postDataJSON(); expect(data).toEqual(examplePatch); }); test('if PATCH plan is ok then calls the PATCH Status', async ({ page }) => { - const patchStatusPromise = page.waitForResponse( - (response) => - /\/api\/plans\/1\/status/.test(response.url()) && - response.status() === 200 && - response.request().method() === 'PATCH' - ); await planPage.elements().requestQuotationCTA().click(); - await planPage.elements().requestQuotationModalCTA().click(); - const responseStatus = await patchStatusPromise; - const dataStatus = responseStatus.request().postDataJSON(); - expect(dataStatus).toEqual({ status: 'pending_review' }); + const response = await requestQuotationModal.submitRequest(); + const data = response.request().postDataJSON(); + expect(data).toEqual({ status: 'pending_review' }); }); test('after requesting quotation CTA save and Request Quote should become disabled and all inputs should be readonly', async () => { // todo }); }); -test.describe('When there is an error in the module configuration (e.g. a date in the past)', () => { - let planPage: PlanPage; - - test.beforeEach(async ({ page }) => { - planPage = new PlanPage(page); - await planPage.loggedIn(); - await planPage.mockPreferences(); - await planPage.mockWorkspace(); - await planPage.mockWorkspacesList(); - await planPage.mockGetDraftPlanWithDateError(); - - await planPage.open(); - }); - - test('when a user click Save we trigger all fields validation, display error messages and trigger PATCH plan', async () => { - await expect(planPage.elements().datesModuleError()).not.toBeVisible(); - await planPage.elements().saveConfigurationCTA().click(); - // await expect(planPage.elements().datesModuleError()).toBeVisible(); - }); - test('when a user click Request Quotation we trigger all fields validation, display error messages and trigger PATCH plan but not the PATCH status', async () => {}); -}); - test.describe('When the user clicks on the dots menu', () => { let planPage: PlanPage; diff --git a/tests/e2e/plan/modules/tasks.spec.ts b/tests/e2e/plan/modules/tasks.spec.ts index 25a59db3c..d1918305f 100644 --- a/tests/e2e/plan/modules/tasks.spec.ts +++ b/tests/e2e/plan/modules/tasks.spec.ts @@ -1,29 +1,87 @@ -import { test } from '../../../fixtures/app'; +import { test, expect } from '../../../fixtures/app'; import { PlanPage } from '../../../fixtures/pages/Plan'; +import { TasksModule } from '../../../fixtures/pages/Plan/TasksModule'; +import apiGetDraftMandatoryPlan from '../../../api/plans/pid/_get/200_draft_mandatory_only.json'; +import { RequestQuotationModal } from '../../../fixtures/pages/Plan/RequestQuotationModal'; test.describe('The tasks module defines a list of activities.', () => { let moduleBuilderPage: PlanPage; + let tasksModule: TasksModule; + let requestQuotationModal: RequestQuotationModal; test.beforeEach(async ({ page }) => { moduleBuilderPage = new PlanPage(page); + tasksModule = new TasksModule(page); + requestQuotationModal = new RequestQuotationModal(page); await moduleBuilderPage.loggedIn(); await moduleBuilderPage.mockPreferences(); await moduleBuilderPage.mockWorkspace(); await moduleBuilderPage.mockWorkspacesList(); await moduleBuilderPage.mockGetDraftWithOnlyMandatoryModulesPlan(); await moduleBuilderPage.open(); + await moduleBuilderPage.elements().instructionsTab().click(); }); - test('It should have an output of an array of task objects, and it is required to have at least 1 item to Request a Quote', async () => { - // Todo + test('Tasks can be deleted, but it is required to have at least 1 item to Request a Quote', async ({ + page, + i18n, + }) => { + await expect(tasksModule.elements().module()).toBeVisible(); + const tasks = TasksModule.getTasksFromPlan(apiGetDraftMandatoryPlan); + await expect(tasksModule.elements().taskListItem()).toHaveCount( + tasks.length + ); + + // delete each item + for (const task of tasks) { + await tasksModule + .elements() + .taskListItem() + .getByRole('heading', { name: task.title }) + .getByRole('button', { + name: i18n.t('__PLAN_PAGE_MODULE_TASKS_REMOVE_TASK_BUTTON'), + }) + .click(); + await tasksModule + .elements() + .removeTaskConfirmationModalConfirmCTA() + .click(); + } + await expect(tasksModule.elements().taskListItem()).toHaveCount(0); + await expect(tasksModule.elements().taskListErrorRequired()).toBeVisible(); + await moduleBuilderPage.elements().requestQuotationCTA().click(); + requestQuotationModal.elements().submitCTA().click(); + await expect(requestQuotationModal.elements().errorMessage()).toContainText( + 'tasks' + ); }); - test(`The single task object have mandatory kind radio/select (bug, video or survey), - a mandatory title input text, - and an optional description text area`, async () => { - // Todo + test(`a mandatory title input text, a mandatory description text, and an optional url input`, async ({ + i18n, + }) => { + const tasks = TasksModule.getTasksFromPlan(apiGetDraftMandatoryPlan); + + const element = tasksModule.elements().taskListItem().nth(0); + const elementTitle = element.getByRole('textbox', { + name: i18n.t('__PLAN_PAGE_MODULE_TASKS_TASK_TITLE_LABEL'), + }); + await expect(elementTitle).toHaveValue(tasks[0].title); + await elementTitle.fill(''); + await elementTitle.blur(); + await expect( + element.getByText( + i18n.t('__PLAN_PAGE_MODULE_TASKS_TASK_TITLE_ERROR_REQUIRED') + ) + ).toBeVisible(); + }); + test('There should be a cta to add a task that append an empty task to the list. No limits', async () => { + const tasks = TasksModule.getTasksFromPlan(apiGetDraftMandatoryPlan); + await expect(tasksModule.elements().addTaskCTA()).toBeVisible(); + await tasksModule.elements().addTaskCTA().click(); + await expect(tasksModule.elements().addTaskModal()).toBeVisible(); + await tasksModule.elements().addTaskModalFunctionalBugHunting().click(); + await expect(tasksModule.elements().taskListItem()).toHaveCount( + tasks.length + 1 + ); }); - test('It should have a list of tasks that show the value of the module', async () => {}); - test('There should be a cta to add a task that append an empty task to the list. No limits', async () => {}); - test('There should be a cta to remove a task that remove the item from the list', async () => {}); }); diff --git a/tests/e2e/plan/modules/title.spec.ts b/tests/e2e/plan/modules/title.spec.ts index 1d3474142..31931a14e 100644 --- a/tests/e2e/plan/modules/title.spec.ts +++ b/tests/e2e/plan/modules/title.spec.ts @@ -1,22 +1,25 @@ import draftMandatory from '../../../api/plans/pid/_get/200_draft_mandatory_only.json'; import { expect, test } from '../../../fixtures/app'; import { PlanPage } from '../../../fixtures/pages/Plan'; +import { RequestQuotationModal } from '../../../fixtures/pages/Plan/RequestQuotationModal'; test.describe('The title module defines the Plan title.', () => { let planPage: PlanPage; + let requestQuotationModal: RequestQuotationModal; test.beforeEach(async ({ page }) => { planPage = new PlanPage(page); + requestQuotationModal = new RequestQuotationModal(page); await planPage.loggedIn(); await planPage.mockPreferences(); await planPage.mockWorkspace(); await planPage.mockWorkspacesList(); + await planPage.mockPatchPlan(); await planPage.mockGetDraftWithOnlyMandatoryModulesPlan(); await planPage.open(); }); - test('It should have a title input that show the current value of the module and a way to change that value', async () => { + test('It should have a title input that show the current value of the module and a way to change that value. Output a string', async () => { const title = PlanPage.getTitleFromPlan(draftMandatory); - await expect(planPage.elements().titleModule()).toBeVisible(); await expect( planPage.elements().titleModule().getByText(title) ).toBeVisible(); @@ -24,8 +27,18 @@ test.describe('The title module defines the Plan title.', () => { await expect( planPage.elements().titleModule().getByText('New Title') ).toBeVisible(); + const response = await planPage.saveConfiguration(); + const data = response.request().postDataJSON(); + expect(data.config.modules).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + type: 'title', + output: 'New Title', + }), + ]) + ); }); - test('It should have an output of a string, and should not be empty to Request a quote', async () => { + test('It should not be empty to Request a quote', async ({ i18n }) => { await planPage.fillInputTItle(''); const moduleTitle = planPage.elements().titleModule(); @@ -35,30 +48,25 @@ test.describe('The title module defines the Plan title.', () => { const titleError = planPage.elements().titleModuleError(); const titleErrorCount = await titleError.count(); expect(titleErrorCount).toBe(1); - await expect(titleError).toBeVisible(); - await expect(titleError).toHaveText( - planPage.i18n.t('__PLAN_TITLE_ERROR_EMPTY') - ); + await expect(titleError).toHaveText(i18n.t('__PLAN_TITLE_ERROR_EMPTY')); await planPage.elements().requestQuotationCTA().click(); // Modal contains the title module and it should be in the same state - const requestQuotationModal = planPage.elements().requestQuotationModal(); - await expect(requestQuotationModal).toBeVisible(); + await expect(requestQuotationModal.elements().modal()).toBeVisible(); + await expect(requestQuotationModal.elements().titleModule()).toBeVisible(); await expect( - requestQuotationModal.getByTestId('title-module') - ).toBeVisible(); - await expect( - requestQuotationModal.getByTestId('title-error') - ).toBeVisible(); + requestQuotationModal.elements().titleModuleError() + ).toHaveText(i18n.t('__PLAN_TITLE_ERROR_EMPTY')); }); - test('The title should have a maximum length of 256 characters', async () => { - await planPage.elements().titleModule().click(); - await planPage.elements().titleModuleInput().fill('a'.repeat(257)); - await planPage.elements().titleModuleInput().blur(); - await expect(planPage.elements().titleModuleError()).toBeVisible(); + test('The title should have a maximum length of 256 characters', async ({ + i18n, + }) => { + await planPage.fillInputTItle('a'.repeat(257)); await expect(planPage.elements().titleModuleError()).toHaveText( - planPage.i18n.t('__PLAN_TITLE_ERROR_MAX_LENGTH') + i18n.t('__PLAN_TITLE_ERROR_MAX_LENGTH') ); + await planPage.fillInputTItle('a'.repeat(256)); + await expect(planPage.elements().titleModuleError()).not.toBeVisible(); }); }); diff --git a/tests/fixtures/pages/Plan/RequestQuotationModal.ts b/tests/fixtures/pages/Plan/RequestQuotationModal.ts new file mode 100644 index 000000000..023e4240c --- /dev/null +++ b/tests/fixtures/pages/Plan/RequestQuotationModal.ts @@ -0,0 +1,65 @@ +import { type Page } from '@playwright/test'; +import { i18n } from 'i18next'; +import { getI18nInstance } from 'playwright-i18next-fixture'; + +export class RequestQuotationModal { + readonly page: Page; + + readonly testId = 'plan-creation-interface'; + + readonly i18n: i18n; + + constructor(page: Page) { + this.page = page; + this.i18n = getI18nInstance() as unknown as i18n; + } + + elements() { + return { + modal: () => + this.page.getByRole('dialog', { + name: this.i18n.t('__PLAN_PAGE_MODAL_SEND_REQUEST_TITLE'), + }), + titleModule: () => this.elements().modal().getByTestId('title-module'), + titleModuleInput: () => + this.elements().modal().getByTestId('title-input'), + titleModuleError: () => + this.elements().modal().getByTestId('title-error'), + submitCTA: () => + this.elements().modal().getByTestId('request-quotation-modal-cta'), + errorMessage: () => + this.page.getByRole('alert', { + name: this.i18n.t('__PLAN_PAGE_MODAL_SEND_REQUEST_TOAST_ERROR'), + }), + }; + } + + async fillInputTItle(value: string) { + await this.elements().titleModule().click(); + await this.elements().titleModuleInput().fill(value); + await this.elements().titleModuleInput().blur(); + } + + async submitRequest() { + const patchStatusPromise = this.page.waitForResponse( + (response) => + /\/api\/plans\/1\/status/.test(response.url()) && + response.status() === 200 && + response.request().method() === 'PATCH' + ); + await this.elements().submitCTA().click(); + return patchStatusPromise; + } + + async mockPatchStatus() { + await this.page.route('*/**/api/plans/1/status', async (route) => { + if (route.request().method() === 'PATCH') { + await route.fulfill({ + path: 'tests/api/plans/pid/status/_patch/request_Example_1.json', + }); + } else { + await route.fallback(); + } + }); + } +} diff --git a/tests/fixtures/pages/Plan/TasksModule.ts b/tests/fixtures/pages/Plan/TasksModule.ts new file mode 100644 index 000000000..60eaf2ec8 --- /dev/null +++ b/tests/fixtures/pages/Plan/TasksModule.ts @@ -0,0 +1,77 @@ +import { type Page } from '@playwright/test'; +import { i18n } from 'i18next'; +import { getI18nInstance } from 'playwright-i18next-fixture'; + +export class TasksModule { + readonly page: Page; + + readonly i18n: i18n; + + constructor(page: Page) { + this.page = page; + this.i18n = getI18nInstance() as unknown as i18n; + } + + elements() { + return { + module: () => this.page.getByTestId('tasks-module'), + taskList: () => this.elements().module().getByRole('list'), + taskListItem: () => this.elements().taskList().getByRole('listitem'), + taskListErrorRequired: () => + this.elements() + .module() + .getByText( + this.i18n.t('__PLAN_PAGE_MODULE_TASKS_TASK_ERROR_REQUIRED') + ), + removeTaskConfirmationModal: () => + this.page.getByRole('dialog', { + name: this.i18n.t( + '__PLAN_PAGE_MODULE_TASKS_MODAL_CONFIRMATION_REMOVE_TASK_TITLE' + ), + }), + removeTaskConfirmationModalConfirmCTA: () => + this.elements() + .removeTaskConfirmationModal() + .getByRole('button', { + name: this.i18n.t( + '__PLAN_PAGE_MODULE_TASKS_MODAL_CONFIRMATION_REMOVE_TASK_CONFIRM' + ), + }), + addTaskCTA: () => + this.elements() + .module() + .getByRole('button', { + name: this.i18n.t('__PLAN_PAGE_MODULE_TASKS_ADD_TASK_BUTTON'), + }), + addTaskModal: () => + this.elements() + .module() + .getByRole('dialog', { + name: this.i18n.t('__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_TITLE'), + }), + addTaskModalFunctionalBugHunting: () => + this.elements() + .addTaskModal() + .getByRole('button', { + name: this.i18n.t( + '__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_FUNCTIONAL_TASK_FUNCTIONAL_BUTTON' + ), + }), + }; + } + + async fillInputTItle(value: string) {} + + static getTasksFromPlan(plan: any) { + const tasksModule = plan.config.modules.find( + (module) => module.type === 'tasks' + ); + if (!tasksModule) { + throw new Error('No tasks module found in plan'); + } + if (!Array.isArray(tasksModule.output)) { + throw new Error('Invalid tasks module output'); + } + return tasksModule.output; + } +} diff --git a/tests/fixtures/pages/Plan.ts b/tests/fixtures/pages/Plan/index.ts similarity index 92% rename from tests/fixtures/pages/Plan.ts rename to tests/fixtures/pages/Plan/index.ts index ed51886ff..3b34e0b1d 100644 --- a/tests/fixtures/pages/Plan.ts +++ b/tests/fixtures/pages/Plan/index.ts @@ -1,5 +1,5 @@ import { type Page } from '@playwright/test'; -import { UnguessPage } from '../UnguessPage'; +import { UnguessPage } from '../../UnguessPage'; export class PlanPage extends UnguessPage { readonly page: Page; @@ -13,84 +13,77 @@ export class PlanPage extends UnguessPage { elements() { return { ...super.elements(), - pageHeader: () => this.page.getByTestId('plan-page-header'), - requestQuotationModal: () => - this.page.getByTestId('request-quotation-modal'), - goalModule: () => this.page.getByTestId('goal-module'), - goalModuleInput: () => this.page.getByRole('textbox'), - goalModuleError: () => this.page.getByTestId('goal-error'), - titleModule: () => - this.elements().pageHeader().getByTestId('title-module'), - titleModuleInput: () => - this.elements().titleModule().getByTestId('title-input'), - titleModuleOutput: () => - this.elements().titleModule().getByTestId('title-output'), - titleModuleError: () => - this.elements().titleModule().getByTestId('title-error'), - tasksModule: () => this.page.getByTestId('tasks-module'), + confirmActivityCTA: () => + this.elements().pageHeader().getByTestId('confirm-activity-cta'), datesModule: () => this.page.getByTestId('dates-module'), datesModuleDatepicker: () => this.elements().datesModule().getByTestId('dates-datepicker'), datesModuleError: () => this.elements().datesModule().getByTestId('dates-error'), - languageModule: () => this.page.getByTestId('language-module'), - languageRadioInput: () => - this.elements().languageModule().getByRole('radio'), - outOfScopeModule: () => this.page.getByTestId('out-of-scope-module'), - outOfScopeModuleInput: () => - this.elements().outOfScopeModule().getByRole('textbox'), - outOfScopeModuleError: () => this.page.getByTestId('out-of-scope-error'), - targetModule: () => this.page.getByTestId('target-module'), - targetModuleInput: () => this.page.getByTestId('target-input'), - targetModuleError: () => - this.elements().targetModule().getByTestId('target-error'), - descriptionModule: () => this.page.getByTestId('description-module'), - saveConfigurationCTA: () => - this.elements() - .pageHeader() - .getByRole('button', { - name: this.i18n.t('__PLAN_SAVE_CONFIGURATION_CTA'), - }), - requestQuotationCTA: () => - this.page.getByRole('button', { - name: this.i18n.t('__PLAN_REQUEST_QUOTATION_CTA'), - }), - confirmActivityCTA: () => - this.elements().pageHeader().getByTestId('confirm-activity-cta'), - requestQuotationModalCTA: () => - this.page.getByTestId('request-quotation-modal-cta'), - requestQuotationErrorMessage: () => - this.page.getByTestId('request-quotation-error-message'), - setupTab: () => this.page.getByTestId('setup-tab'), - targetTab: () => this.page.getByTestId('target-tab'), - instructionsTab: () => this.page.getByTestId('instructions-tab'), - summaryTab: () => this.page.getByTestId('summary-tab'), - digitalLiteracyModule: () => - this.page.getByTestId('digital-literacy-module'), - digitalLiteracyModuleErrorMessage: () => - this.page.getByTestId('literacy-error'), - extraActionsMenu: () => this.page.getByTestId('extra-actions-menu'), deletePlanActionItem: () => this.page.getByTestId('delete-action-item'), deletePlanModal: () => this.page.getByRole('dialog', { name: this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_TITLE'), }), - deletePlanModalTitle: () => + deletePlanModalCancelCTA: () => this.elements() .deletePlanModal() - .getByText(this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_TITLE')), + .getByText( + this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_BUTTON_CANCEL') + ), deletePlanModalConfirmCTA: () => this.elements() .deletePlanModal() .getByRole('button', { name: this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_BUTTON_CONFIRM'), }), - deletePlanModalCancelCTA: () => + deletePlanModalTitle: () => this.elements() .deletePlanModal() - .getByText( - this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_BUTTON_CANCEL') - ), + .getByText(this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_TITLE')), + descriptionModule: () => this.page.getByTestId('description-module'), + digitalLiteracyModule: () => + this.page.getByTestId('digital-literacy-module'), + digitalLiteracyModuleErrorMessage: () => + this.page.getByTestId('literacy-error'), + extraActionsMenu: () => this.page.getByTestId('extra-actions-menu'), + goalModule: () => this.page.getByTestId('goal-module'), + goalModuleError: () => this.page.getByTestId('goal-error'), + goalModuleInput: () => this.page.getByRole('textbox'), + instructionsTab: () => this.page.getByTestId('instructions-tab'), + languageModule: () => this.page.getByTestId('language-module'), + languageRadioInput: () => + this.elements().languageModule().getByRole('radio'), + outOfScopeModule: () => this.page.getByTestId('out-of-scope-module'), + outOfScopeModuleError: () => this.page.getByTestId('out-of-scope-error'), + outOfScopeModuleInput: () => + this.elements().outOfScopeModule().getByRole('textbox'), + pageHeader: () => this.page.getByTestId('plan-page-header'), + requestQuotationCTA: () => + this.page.getByRole('button', { + name: this.i18n.t('__PLAN_REQUEST_QUOTATION_CTA'), + }), + saveConfigurationCTA: () => + this.elements() + .pageHeader() + .getByRole('button', { + name: this.i18n.t('__PLAN_SAVE_CONFIGURATION_CTA'), + }), + setupTab: () => this.page.getByTestId('setup-tab'), + summaryTab: () => this.page.getByTestId('summary-tab'), + targetModule: () => this.page.getByTestId('target-module'), + targetModuleError: () => + this.elements().targetModule().getByTestId('target-error'), + targetModuleInput: () => this.page.getByTestId('target-input'), + targetTab: () => this.page.getByTestId('target-tab'), + titleModule: () => + this.elements().pageHeader().getByTestId('title-module'), + titleModuleError: () => + this.elements().titleModule().getByTestId('title-error'), + titleModuleInput: () => + this.elements().titleModule().getByTestId('title-input'), + titleModuleOutput: () => + this.elements().titleModule().getByTestId('title-output'), }; } @@ -187,6 +180,17 @@ export class PlanPage extends UnguessPage { await this.elements().targetModuleInput().blur(); } + async saveConfiguration() { + const patchPromise = this.page.waitForResponse( + (response) => + /\/api\/plans\/1/.test(response.url()) && + response.status() === 200 && + response.request().method() === 'PATCH' + ); + await this.elements().saveConfigurationCTA().click(); + return patchPromise; + } + async mockGetDraftPlan() { await this.page.route('*/**/api/plans/1', async (route) => { if (route.request().method() === 'GET') { @@ -261,18 +265,6 @@ export class PlanPage extends UnguessPage { }); } - async mockPatchStatus() { - await this.page.route('*/**/api/plans/1/status', async (route) => { - if (route.request().method() === 'PATCH') { - await route.fulfill({ - path: 'tests/api/plans/pid/status/_patch/request_Example_1.json', - }); - } else { - await route.fallback(); - } - }); - } - async mockDeletePlan(statusCode: number = 200) { await this.page.route('*/**/api/plans/1', async (route) => { if (route.request().method() === 'DELETE') { From b2ef6c6e1483c911dc6cc55f1fa0ad618de47e57 Mon Sep 17 00:00:00 2001 From: iacopolea Date: Wed, 7 May 2025 18:42:42 +0200 Subject: [PATCH 11/49] Refactor plan module tests to use dedicated module classes for better structure and maintainability - Updated language.spec.ts to utilize LanguageModule for language-related tests. - Refactored out_of_scope.spec.ts to use OutOfScopeModule for out of scope tests. - Modified target_size.spec.ts to implement TargetModule for target size tests. - Adjusted tasks.spec.ts to utilize Module_tasks for task-related tests. - Enhanced pending_review.spec.ts to incorporate various module classes for better organization. - Added RequestQuotationModal functionality for handling date inputs and validation. - Created new module classes for digital literacy, goal, language, out of scope, target, and tasks. - Removed deprecated methods from PlanPage and moved logic to respective module classes. - Introduced a new approved.spec.ts to test the approved state of a plan. - Added date.spec.ts to validate date input functionality in the request quotation modal. - Created a mock approved plan JSON for testing purposes. --- src/pages/Plan/Controls.tsx | 1 - src/pages/Plan/modules/Dates.tsx | 2 +- tests/api/plans/pid/_get/200_approved.json | 107 ++++++++++++ .../plans/pid/_get/200_draft_complete.json | 38 ++++- .../plans/pid/_get/200_pending_review.json | 59 ++++++- tests/e2e/plan/approved.spec.ts | 95 +++++++++++ tests/e2e/plan/draft.spec.ts | 8 +- tests/e2e/plan/modules/date.spec.ts | 40 +++++ .../e2e/plan/modules/digitalLiteracy.spec.ts | 34 +--- tests/e2e/plan/modules/goal.spec.ts | 42 ++--- tests/e2e/plan/modules/language.spec.ts | 33 ++-- tests/e2e/plan/modules/out_of_scope.spec.ts | 48 +++--- tests/e2e/plan/modules/target_size.spec.ts | 35 ++-- tests/e2e/plan/modules/tasks.spec.ts | 8 +- tests/e2e/plan/pending_review.spec.ts | 78 +++++++-- .../pages/Plan/Module_digital_literacy.ts | 37 +++++ tests/fixtures/pages/Plan/Module_goal.ts | 36 +++++ tests/fixtures/pages/Plan/Module_language.ts | 35 ++++ .../pages/Plan/Module_out_of_scope.ts | 34 ++++ tests/fixtures/pages/Plan/Module_target.ts | 41 +++++ .../Plan/{TasksModule.ts => Module_tasks.ts} | 8 +- .../pages/Plan/RequestQuotationModal.ts | 26 +++ tests/fixtures/pages/Plan/index.ts | 153 +++++------------- 23 files changed, 754 insertions(+), 244 deletions(-) create mode 100644 tests/api/plans/pid/_get/200_approved.json create mode 100644 tests/e2e/plan/approved.spec.ts create mode 100644 tests/e2e/plan/modules/date.spec.ts create mode 100644 tests/fixtures/pages/Plan/Module_digital_literacy.ts create mode 100644 tests/fixtures/pages/Plan/Module_goal.ts create mode 100644 tests/fixtures/pages/Plan/Module_language.ts create mode 100644 tests/fixtures/pages/Plan/Module_out_of_scope.ts create mode 100644 tests/fixtures/pages/Plan/Module_target.ts rename tests/fixtures/pages/Plan/{TasksModule.ts => Module_tasks.ts} (90%) diff --git a/src/pages/Plan/Controls.tsx b/src/pages/Plan/Controls.tsx index 9e4f815f8..75291e80f 100644 --- a/src/pages/Plan/Controls.tsx +++ b/src/pages/Plan/Controls.tsx @@ -104,7 +104,6 @@ export const Controls = () => { setIsSubmitted(false); }); }} - data-qa="confirm-activity-cta" > {t('__PLAN_PAGE_SUMMARY_TAB_CONFIRMATION_CARD_CONFIRM_CTA')} diff --git a/src/pages/Plan/modules/Dates.tsx b/src/pages/Plan/modules/Dates.tsx index 78a331cd0..5ef697a93 100644 --- a/src/pages/Plan/modules/Dates.tsx +++ b/src/pages/Plan/modules/Dates.tsx @@ -79,7 +79,7 @@ export const Dates = () => { {datesError && ( {datesError} diff --git a/tests/api/plans/pid/_get/200_approved.json b/tests/api/plans/pid/_get/200_approved.json new file mode 100644 index 000000000..2721f5460 --- /dev/null +++ b/tests/api/plans/pid/_get/200_approved.json @@ -0,0 +1,107 @@ +{ + "status": "approved", + "id": 1, + "workspace_id": 1, + "project": { + "id": 90, + "name": "MyProject" + }, + "quote": { + "id": 98, + "status": "approved", + "value": "3600 €" + }, + "campaign": { + "id": 6856, + "title": "Meet Accessibility Compliance 123", + "startDate": "Thu Apr 03 2025 06:00:00 GMT+0000 (Coordinated Universal Time)" + }, + "config": { + "modules": [ + { + "type": "title", + "variant": "default", + "output": "My Plan" + }, + { + "type": "dates", + "variant": "default", + "output": { + "start": "2041-12-17T08:00:00.000Z" + } + }, + { + "type": "out_of_scope", + "variant": "default", + "output": "Out of scope" + }, + { + "type": "target", + "variant": "default", + "output": 5 + }, + { + "type": "language", + "variant": "default", + "output": "en" + }, + { + "type": "goal", + "variant": "default", + "output": "This is a Goal" + }, + { + "type": "literacy", + "output": [ + { + "level": "intermediate", + "percentage": 100 + } + ], + "variant": "default" + }, + { + "type": "age", + "output": [ + { + "max": 24, + "min": 16, + "percentage": 50 + }, + { + "max": 70, + "min": 55, + "percentage": 50 + } + ], + "variant": "default" + }, + { + "type": "gender", + "output": [ + { + "gender": "female", + "percentage": 100 + } + ], + "variant": "default" + }, + { + "type": "tasks", + "variant": "default", + "output": [ + { + "kind": "bug", + "title": "Search for bugs", + "description": "description kind bug" + }, + { + "kind": "video", + "title": "Think aloud", + "description": "description kind bug" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/tests/api/plans/pid/_get/200_draft_complete.json b/tests/api/plans/pid/_get/200_draft_complete.json index 9cae11c59..029b29193 100644 --- a/tests/api/plans/pid/_get/200_draft_complete.json +++ b/tests/api/plans/pid/_get/200_draft_complete.json @@ -40,6 +40,42 @@ "variant": "default", "output": "This is a Goal" }, + { + "type": "literacy", + "output": [ + { + "level": "intermediate", + "percentage": 100 + } + ], + "variant": "default" + }, + { + "type": "age", + "output": [ + { + "max": 24, + "min": 16, + "percentage": 50 + }, + { + "max": 70, + "min": 55, + "percentage": 50 + } + ], + "variant": "default" + }, + { + "type": "gender", + "output": [ + { + "gender": "female", + "percentage": 100 + } + ], + "variant": "default" + }, { "type": "tasks", "variant": "default", @@ -58,4 +94,4 @@ } ] } -} +} \ No newline at end of file diff --git a/tests/api/plans/pid/_get/200_pending_review.json b/tests/api/plans/pid/_get/200_pending_review.json index a5c182490..5ba6a92e9 100644 --- a/tests/api/plans/pid/_get/200_pending_review.json +++ b/tests/api/plans/pid/_get/200_pending_review.json @@ -20,6 +20,63 @@ "start": "2041-12-17T08:00:00.000Z" } }, + { + "type": "out_of_scope", + "variant": "default", + "output": "Out of scope" + }, + { + "type": "target", + "variant": "default", + "output": 5 + }, + { + "type": "language", + "variant": "default", + "output": "en" + }, + { + "type": "goal", + "variant": "default", + "output": "This is a Goal" + }, + { + "type": "literacy", + "output": [ + { + "level": "intermediate", + "percentage": 100 + } + ], + "variant": "default" + }, + + { + "type": "age", + "output": [ + { + "max": 24, + "min": 16, + "percentage": 50 + }, + { + "max": 70, + "min": 55, + "percentage": 50 + } + ], + "variant": "default" + }, + { + "type": "gender", + "output": [ + { + "gender": "female", + "percentage": 100 + } + ], + "variant": "default" + }, { "type": "tasks", "variant": "default", @@ -32,7 +89,7 @@ { "kind": "video", "title": "Think aloud", - "description": "description kind video" + "description": "description kind bug" } ] } diff --git a/tests/e2e/plan/approved.spec.ts b/tests/e2e/plan/approved.spec.ts new file mode 100644 index 000000000..18377632c --- /dev/null +++ b/tests/e2e/plan/approved.spec.ts @@ -0,0 +1,95 @@ +import { test, expect } from '../../fixtures/app'; +import { PlanPage } from '../../fixtures/pages/Plan'; +import { GoalModule } from '../../fixtures/pages/Plan/Module_goal'; +import { TargetModule } from '../../fixtures/pages/Plan/Module_target'; +import { LanguageModule } from '../../fixtures/pages/Plan/Module_language'; +import { DigitalLiteracyModule } from '../../fixtures/pages/Plan/Module_digital_literacy'; +import { TasksModule } from '../../fixtures/pages/Plan/Module_tasks'; +import { OutOfScopeModule } from '../../fixtures/pages/Plan/Module_out_of_scope'; + +test.describe('A Plan page in accepted state', () => { + let moduleBuilderPage: PlanPage; + let goalModule: GoalModule; + let targetModule: TargetModule; + let languageModule: LanguageModule; + let digitalLiteracyModule: DigitalLiteracyModule; + let outOfScopeModule: OutOfScopeModule; + let tasksModule: TasksModule; + + test.beforeEach(async ({ page }) => { + moduleBuilderPage = new PlanPage(page); + goalModule = new GoalModule(page); + targetModule = new TargetModule(page); + languageModule = new LanguageModule(page); + digitalLiteracyModule = new DigitalLiteracyModule(page); + outOfScopeModule = new OutOfScopeModule(page); + tasksModule = new TasksModule(page); + + await moduleBuilderPage.loggedIn(); + await moduleBuilderPage.mockPreferences(); + await moduleBuilderPage.mockWorkspace(); + await moduleBuilderPage.mockWorkspacesList(); + await moduleBuilderPage.mockGetApprovedPlan(); + await moduleBuilderPage.open(); + }); + + test('has the summary tab visible and the confirm button is disabled', async ({ + page, + i18n, + }) => { + await expect( + moduleBuilderPage.elements().saveConfigurationCTA() + ).not.toBeVisible(); + await expect( + moduleBuilderPage.elements().requestQuotationCTA() + ).not.toBeVisible(); + await expect( + moduleBuilderPage.elements().confirmActivityCTA() + ).not.toBeVisible(); + await expect(moduleBuilderPage.elements().goToDashboardCTA()).toBeEnabled(); + await expect( + page + .getByRole('status') + .filter({ hasText: i18n.t('PLAN_GLOBAL_ALERT_APPROVED_STATE_TITLE') }) + ).toBeVisible(); + await expect( + page.getByText(i18n.t('__PLAN_PAGE_INTRODUCTION_CARD_APPROVED_TITLE')) + ).toBeVisible(); + }); + test('all inputs should be readonly', async () => { + await moduleBuilderPage.elements().tabSetup().click(); + await expect(goalModule.elements().moduleInput()).toHaveAttribute( + 'readonly', + '' + ); + await moduleBuilderPage.elements().tabTarget().click(); + await expect(targetModule.elements().moduleInput()).toHaveAttribute( + 'readonly', + '' + ); + const languageRadioInputs = languageModule.elements().languageRadioInput(); + for (let i = 0; i < (await languageRadioInputs.count()); i++) { + await expect(languageRadioInputs.nth(i)).toHaveAttribute('disabled', ''); + } + const digitalLiteracyCheckbox = digitalLiteracyModule + .elements() + .moduleInput(); + for (let i = 0; i < (await digitalLiteracyCheckbox.count()); i++) { + await expect(digitalLiteracyCheckbox.nth(i)).toHaveAttribute( + 'disabled', + '' + ); + } + await moduleBuilderPage.elements().tabInstructions().click(); + await expect(outOfScopeModule.elements().moduleInput()).toHaveAttribute( + 'readonly', + '' + ); + const taskselements = tasksModule.elements().taskListItem(); + for (let i = 0; i < (await taskselements.count()); i++) { + await expect( + tasksModule.elements().taskTitleInput(taskselements.nth(i)) + ).toHaveAttribute('readonly', ''); + } + }); +}); diff --git a/tests/e2e/plan/draft.spec.ts b/tests/e2e/plan/draft.spec.ts index 2b1c1da62..74955f222 100644 --- a/tests/e2e/plan/draft.spec.ts +++ b/tests/e2e/plan/draft.spec.ts @@ -24,7 +24,7 @@ test.describe('The module builder', () => { test('has a list of saved modules and not the others, a save button, a request quote cta and a dots menu cta', async () => { // Click the "Setup" tab - await planPage.elements().setupTab().click(); + await planPage.elements().tabSetup().click(); // Check if specific elements are visible on the "Setup" tab await expect(planPage.elements().titleModule()).toBeVisible(); @@ -39,10 +39,8 @@ test.describe('The module builder', () => { }); test("The summary Tab isn't clickable", async () => { - await expect(planPage.elements().summaryTab()).toBeVisible(); - await expect(planPage.elements().summaryTab()).toBeDisabled(); - - // TODO: add a test to check when the tab must be enabled + await expect(planPage.elements().tabSummary()).toBeVisible(); + await expect(planPage.elements().tabSummary()).toBeDisabled(); }); test('Clicking save button validate the current modules configurations and calls the PATCH Plan', async ({ diff --git a/tests/e2e/plan/modules/date.spec.ts b/tests/e2e/plan/modules/date.spec.ts new file mode 100644 index 000000000..6a0e12884 --- /dev/null +++ b/tests/e2e/plan/modules/date.spec.ts @@ -0,0 +1,40 @@ +import draftMandatory from '../../../api/plans/pid/_get/200_draft_mandatory_only.json'; +import { expect, test } from '../../../fixtures/app'; +import { PlanPage } from '../../../fixtures/pages/Plan'; +import { RequestQuotationModal } from '../../../fixtures/pages/Plan/RequestQuotationModal'; + +test.describe('The title module defines the Plan title.', () => { + let planPage: PlanPage; + let requestQuotationModal: RequestQuotationModal; + + test.beforeEach(async ({ page }) => { + planPage = new PlanPage(page); + requestQuotationModal = new RequestQuotationModal(page); + await planPage.loggedIn(); + await planPage.mockPreferences(); + await planPage.mockWorkspace(); + await planPage.mockWorkspacesList(); + await planPage.mockPatchPlan(); + await planPage.mockGetDraftWithOnlyMandatoryModulesPlan(); + await planPage.open(); + }); + test('It should be visible after opening a request quotation', async () => { + await expect( + requestQuotationModal.elements().datesModule() + ).not.toBeVisible(); + await planPage.elements().requestQuotationCTA().click(); + await expect(requestQuotationModal.elements().datesModule()).toBeVisible(); + }); + test('It should be at list one day in the future to request a quote', async ({ + i18n, + }) => { + await planPage.elements().requestQuotationCTA().click(); + await requestQuotationModal.fillInputDate('December 17, 2001'); + await expect( + requestQuotationModal.elements().datesModuleError() + ).toBeVisible(); + await expect( + requestQuotationModal.elements().datesModuleError() + ).toHaveText(i18n.t('__PLAN_DATE_IN_FUTURE_ERROR')); + }); +}); diff --git a/tests/e2e/plan/modules/digitalLiteracy.spec.ts b/tests/e2e/plan/modules/digitalLiteracy.spec.ts index 40f7ca23b..c7eaf001c 100644 --- a/tests/e2e/plan/modules/digitalLiteracy.spec.ts +++ b/tests/e2e/plan/modules/digitalLiteracy.spec.ts @@ -1,44 +1,24 @@ -import { test } from '../../../fixtures/app'; +import { test, expect } from '../../../fixtures/app'; import { PlanPage } from '../../../fixtures/pages/Plan'; +import { DigitalLiteracyModule } from '../../../fixtures/pages/Plan/Module_digital_literacy'; test.describe('The digital literacy module defines the users digital skills.', () => { let moduleBuilderPage: PlanPage; + let digitalLiteracyModule: DigitalLiteracyModule; test.beforeEach(async ({ page }) => { moduleBuilderPage = new PlanPage(page); + digitalLiteracyModule = new DigitalLiteracyModule(page); await moduleBuilderPage.loggedIn(); await moduleBuilderPage.mockPreferences(); await moduleBuilderPage.mockWorkspace(); await moduleBuilderPage.mockWorkspacesList(); - await moduleBuilderPage.mockGetDraftWithOnlyMandatoryModulesPlan(); + await moduleBuilderPage.mockGetDraftPlan(); await moduleBuilderPage.open(); + await moduleBuilderPage.elements().tabTarget().click(); }); test('It should have an output of an array of objects with level and percentage, and it is required to have at least 1 item to Request a Quote', async () => { - // expect(moduleBuilderPage.elements().digitalLiteracyModule()).toBeVisible(); - // moduleBuilderPage.elements().digitalLiteracyModule().click(); + await expect(digitalLiteracyModule.elements().module()).toBeVisible(); }); - - /* -- sia visibile il bottone delete modulo -- siano visibili le checkbox all level e le tre singole -- se non c'è una checkbox selezionata, c'è un errore -- se c'è almeno una checkbox selezionata, non c'è errore -- se clicco su una checkbox, la checkbox all levels deve essere deselezionata (opionale) -- se clicco su all levels, tutte le checkbox devono essere selezionate (opzionale) -- config literacy con tutti i livelli selezionati, mi aspetto le 3 checkbox selezionate e all levels selezionata -- config literacy con nessun livello selezionato, mi aspetto nessuna checkbox selezionata e all levels deselezionata -- config literacy con solo un livello selezionato, mi aspetto una checkbox selezionata e all levels deselezionata - - */ - - test('All levels checkbox should be checked when the page is opened', async () => { - // Todo - }); - - test(`It should have at least one checkbox checked`, async () => { - // Todo - }); - - test('There should be a cta to remove a task that remove the item from the list', async () => {}); }); diff --git a/tests/e2e/plan/modules/goal.spec.ts b/tests/e2e/plan/modules/goal.spec.ts index 88ce47fce..84a75f2dd 100644 --- a/tests/e2e/plan/modules/goal.spec.ts +++ b/tests/e2e/plan/modules/goal.spec.ts @@ -1,12 +1,16 @@ import draft from '../../../api/plans/pid/_get/200_draft_complete.json'; import { expect, test } from '../../../fixtures/app'; import { PlanPage } from '../../../fixtures/pages/Plan'; +import { GoalModule } from '../../../fixtures/pages/Plan/Module_goal'; test.describe('The title module defines the Plan title.', () => { let planPage: PlanPage; + let goalModule: GoalModule; test.beforeEach(async ({ page }) => { planPage = new PlanPage(page); + goalModule = new GoalModule(page); + await planPage.loggedIn(); await planPage.mockPreferences(); await planPage.mockWorkspace(); @@ -16,36 +20,36 @@ test.describe('The title module defines the Plan title.', () => { }); test('It should have a text area that show the value of the module', async () => { - const goal = PlanPage.getGoalFromPlan(draft); - await expect(planPage.elements().goalModule()).toBeVisible(); - await expect(planPage.elements().goalModuleInput()).toHaveValue(goal); + const goal = GoalModule.getGoalFromPlan(draft); + await expect(goalModule.elements().module()).toBeVisible(); + await expect(goalModule.elements().moduleInput()).toHaveValue(goal); }); test('It should have a text area that show the value of the module and a way to change it', async () => { - const goal = PlanPage.getGoalFromPlan(draft); - await expect(planPage.elements().goalModule()).toBeVisible(); - await expect(planPage.elements().goalModuleInput()).toHaveValue(goal); - await planPage.elements().goalModuleInput().click(); - await planPage.elements().goalModuleInput().fill('New Goal'); - await expect(planPage.elements().goalModuleInput()).toHaveValue('New Goal'); + const goal = GoalModule.getGoalFromPlan(draft); + await expect(goalModule.elements().module()).toBeVisible(); + await expect(goalModule.elements().moduleInput()).toHaveValue(goal); + await goalModule.elements().moduleInput().click(); + await goalModule.elements().moduleInput().fill('New Goal'); + await expect(goalModule.elements().moduleInput()).toHaveValue('New Goal'); }); test('It should show an error if the textArea is empty', async () => { - await planPage.elements().goalModuleInput().click(); - await planPage.elements().goalModuleInput().fill(''); - await planPage.elements().goalModuleInput().blur(); - await expect(planPage.elements().goalModuleError()).toBeVisible(); - await expect(planPage.elements().goalModuleError()).toHaveText( + await goalModule.elements().moduleInput().click(); + await goalModule.elements().moduleInput().fill(''); + await goalModule.elements().moduleInput().blur(); + await expect(goalModule.elements().moduleError()).toBeVisible(); + await expect(goalModule.elements().moduleError()).toHaveText( planPage.i18n.t('__PLAN_GOAL_SIZE_ERROR_REQUIRED') ); }); test('It should show an error if the textArea is too long', async () => { - await planPage.elements().goalModuleInput().click(); - await planPage.elements().goalModuleInput().fill('a'.repeat(257)); - await planPage.elements().goalModuleInput().blur(); - await expect(planPage.elements().goalModuleError()).toBeVisible(); - await expect(planPage.elements().goalModuleError()).toHaveText( + await goalModule.elements().moduleInput().click(); + await goalModule.elements().moduleInput().fill('a'.repeat(257)); + await goalModule.elements().moduleInput().blur(); + await expect(goalModule.elements().moduleError()).toBeVisible(); + await expect(goalModule.elements().moduleError()).toHaveText( planPage.i18n.t('__PLAN_GOAL_SIZE_ERROR_TOO_LONG') ); }); diff --git a/tests/e2e/plan/modules/language.spec.ts b/tests/e2e/plan/modules/language.spec.ts index a14bbe210..12d51547f 100644 --- a/tests/e2e/plan/modules/language.spec.ts +++ b/tests/e2e/plan/modules/language.spec.ts @@ -1,12 +1,15 @@ import draft from '../../../api/plans/pid/_get/200_draft_complete.json'; import { expect, test } from '../../../fixtures/app'; import { PlanPage } from '../../../fixtures/pages/Plan'; +import { LanguageModule } from '../../../fixtures/pages/Plan/Module_language'; test.describe('The tasks module defines the testers language', () => { let planPage: PlanPage; + let languageModule: LanguageModule; test.beforeEach(async ({ page }) => { planPage = new PlanPage(page); + languageModule = new LanguageModule(page); await planPage.loggedIn(); await planPage.mockPreferences(); await planPage.mockWorkspace(); @@ -16,42 +19,42 @@ test.describe('The tasks module defines the testers language', () => { }); test('It should alway have one element checked ', async () => { - const language = PlanPage.getLanguageFromPlan(draft); + const language = LanguageModule.getLanguageFromPlan(draft); - await planPage.elements().targetTab().click(); - await expect(planPage.elements().languageModule()).toBeVisible(); - const radioButtons = planPage.elements().languageRadioInput(); + await planPage.elements().tabTarget().click(); + await expect(languageModule.elements().module()).toBeVisible(); + const radioButtons = languageModule.elements().languageRadioInput(); const checkedCount = await radioButtons.evaluateAll( (elements) => elements.filter((el) => el instanceof HTMLInputElement && el.checked) .length ); expect(checkedCount).toBe(1); - const checkedRadio = planPage + const checkedRadio = languageModule .elements() - .languageModule() + .module() .getByRole('radio', { checked: true }); await expect(checkedRadio).toHaveAttribute('value', language); }); test('It should have an output of a string and be able to change ', async () => { - const language = PlanPage.getLanguageFromPlan(draft); + const language = LanguageModule.getLanguageFromPlan(draft); const newLanguage = 'it'; - await planPage.elements().targetTab().click(); - await expect(planPage.elements().languageModule()).toBeVisible(); - const initialCheckedRadio = planPage + await planPage.elements().tabTarget().click(); + await expect(languageModule.elements().module()).toBeVisible(); + const initialCheckedRadio = languageModule .elements() - .languageModule() + .module() .getByRole('radio', { checked: true }); await expect(initialCheckedRadio).toHaveAttribute('value', language); - const newRadioLabel = planPage + const newRadioLabel = languageModule .elements() - .languageModule() + .module() .getByText('Italian'); await newRadioLabel.click(); - const newRadio = planPage + const newRadio = languageModule .elements() - .languageModule() + .module() .getByRole('radio', { checked: true }); await expect(newRadio).toHaveAttribute('value', newLanguage); }); diff --git a/tests/e2e/plan/modules/out_of_scope.spec.ts b/tests/e2e/plan/modules/out_of_scope.spec.ts index 6bce21a6f..af95d5f76 100644 --- a/tests/e2e/plan/modules/out_of_scope.spec.ts +++ b/tests/e2e/plan/modules/out_of_scope.spec.ts @@ -1,58 +1,54 @@ import draft from '../../../api/plans/pid/_get/200_draft_complete.json'; import { expect, test } from '../../../fixtures/app'; import { PlanPage } from '../../../fixtures/pages/Plan'; +import { OutOfScopeModule } from '../../../fixtures/pages/Plan/Module_out_of_scope'; test.describe('The title module defines the Plan title.', () => { let planPage: PlanPage; + let module: OutOfScopeModule; test.beforeEach(async ({ page }) => { planPage = new PlanPage(page); + module = new OutOfScopeModule(page); await planPage.loggedIn(); await planPage.mockPreferences(); await planPage.mockWorkspace(); await planPage.mockWorkspacesList(); await planPage.mockGetDraftPlan(); await planPage.open(); + await planPage.elements().tabInstructions().click(); }); test('It should have a text area that show the value of the module', async () => { - const outOfScope = PlanPage.getOutOfScopeFromPlan(draft); - await planPage.elements().instructionsTab().click(); - await expect(planPage.elements().outOfScopeModule()).toBeVisible(); - await expect(planPage.elements().outOfScopeModuleInput()).toHaveValue( - outOfScope - ); + const outOfScope = OutOfScopeModule.getOutOfScopeFromPlan(draft); + await expect(module.elements().module()).toBeVisible(); + await expect(module.elements().moduleInput()).toHaveValue(outOfScope); }); test('It should have a text area that show the value of the module and a way to change it', async () => { - const outOfScope = PlanPage.getOutOfScopeFromPlan(draft); - await planPage.elements().instructionsTab().click(); - await expect(planPage.elements().outOfScopeModule()).toBeVisible(); - await expect(planPage.elements().outOfScopeModuleInput()).toHaveValue( - outOfScope - ); - await planPage.elements().outOfScopeModuleInput().click(); - await planPage.elements().outOfScopeModuleInput().fill('New out of scope'); - await expect(planPage.elements().outOfScopeModuleInput()).toHaveValue( + const outOfScope = OutOfScopeModule.getOutOfScopeFromPlan(draft); + await expect(module.elements().module()).toBeVisible(); + await expect(module.elements().moduleInput()).toHaveValue(outOfScope); + await module.elements().moduleInput().click(); + await module.elements().moduleInput().fill('New out of scope'); + await expect(module.elements().moduleInput()).toHaveValue( 'New out of scope' ); }); test('It should not show an error if the textArea is empty', async () => { - await planPage.elements().instructionsTab().click(); - await planPage.elements().outOfScopeModuleInput().click(); - await planPage.elements().outOfScopeModuleInput().fill(''); - await planPage.elements().outOfScopeModuleInput().blur(); - await expect(planPage.elements().outOfScopeModuleError()).not.toBeVisible(); + await module.elements().moduleInput().click(); + await module.elements().moduleInput().fill(''); + await module.elements().moduleInput().blur(); + await expect(module.elements().moduleError()).not.toBeVisible(); }); test('It should show an error if the textArea is too long', async () => { - await planPage.elements().instructionsTab().click(); - await planPage.elements().outOfScopeModuleInput().click(); - await planPage.elements().outOfScopeModuleInput().fill('a'.repeat(517)); - await planPage.elements().outOfScopeModuleInput().blur(); - await expect(planPage.elements().outOfScopeModuleError()).toBeVisible(); - await expect(planPage.elements().outOfScopeModuleError()).toHaveText( + await module.elements().moduleInput().click(); + await module.elements().moduleInput().fill('a'.repeat(517)); + await module.elements().moduleInput().blur(); + await expect(module.elements().moduleError()).toBeVisible(); + await expect(module.elements().moduleError()).toHaveText( planPage.i18n.t('__PLAN_OUT_OF_SCOPE_SIZE_ERROR_TOO_LONG') ); }); diff --git a/tests/e2e/plan/modules/target_size.spec.ts b/tests/e2e/plan/modules/target_size.spec.ts index 8edb3fce0..6e5108bce 100644 --- a/tests/e2e/plan/modules/target_size.spec.ts +++ b/tests/e2e/plan/modules/target_size.spec.ts @@ -1,49 +1,50 @@ import draft from '../../../api/plans/pid/_get/200_draft_complete.json'; import { expect, test } from '../../../fixtures/app'; import { PlanPage } from '../../../fixtures/pages/Plan'; +import { TargetModule } from '../../../fixtures/pages/Plan/Module_target'; test.describe('The title module defines the Plan title.', () => { let planPage: PlanPage; + let targetModule: TargetModule; test.beforeEach(async ({ page }) => { planPage = new PlanPage(page); + targetModule = new TargetModule(page); + await planPage.loggedIn(); await planPage.mockPreferences(); await planPage.mockWorkspace(); await planPage.mockWorkspacesList(); await planPage.mockGetDraftPlan(); await planPage.open(); + await planPage.elements().tabTarget().click(); }); test('It should exist on the tab screen target', async () => { - await planPage.elements().targetTab().click(); - await expect(planPage.elements().targetModule()).toBeVisible(); + await expect(targetModule.elements().module()).toBeVisible(); }); test('It should have a target input that show the current value of the module and a way to change that value', async () => { - const target = PlanPage.getTargetFromPlan(draft); - await planPage.elements().targetTab().click(); - await expect(planPage.elements().targetModule()).toBeVisible(); - await expect(planPage.elements().targetModuleInput()).toBeVisible(); - await expect(planPage.elements().targetModuleInput()).toHaveValue( + const target = TargetModule.getTargetFromPlan(draft); + await expect(targetModule.elements().module()).toBeVisible(); + await expect(targetModule.elements().moduleInput()).toBeVisible(); + await expect(targetModule.elements().moduleInput()).toHaveValue( target.toString() ); - await planPage.fillInputTarget('8'); - await expect(planPage.elements().targetModuleInput()).toHaveValue('8'); + await targetModule.fillInputTarget('8'); + await expect(targetModule.elements().moduleInput()).toHaveValue('8'); }); test('It should have an output of a number', async () => { - await planPage.elements().targetTab().click(); - await planPage.fillInputTarget(''); - await expect(planPage.elements().targetModuleError()).toBeVisible(); - await expect(planPage.elements().targetModuleError()).toHaveText( + await targetModule.fillInputTarget(''); + await expect(targetModule.elements().moduleError()).toBeVisible(); + await expect(targetModule.elements().moduleError()).toHaveText( planPage.i18n.t('__PLAN_TARGET_SIZE_ERROR_REQUIRED') ); }); test('It should have a number > 0 as an output', async () => { - await planPage.elements().targetTab().click(); - await planPage.fillInputTarget('0'); - await expect(planPage.elements().targetModuleError()).toBeVisible(); - await expect(planPage.elements().targetModuleError()).toHaveText( + await targetModule.fillInputTarget('0'); + await expect(targetModule.elements().moduleError()).toBeVisible(); + await expect(targetModule.elements().moduleError()).toHaveText( planPage.i18n.t('__PLAN_TARGET_SIZE_ERROR_REQUIRED') ); }); diff --git a/tests/e2e/plan/modules/tasks.spec.ts b/tests/e2e/plan/modules/tasks.spec.ts index d1918305f..821268749 100644 --- a/tests/e2e/plan/modules/tasks.spec.ts +++ b/tests/e2e/plan/modules/tasks.spec.ts @@ -1,6 +1,6 @@ import { test, expect } from '../../../fixtures/app'; import { PlanPage } from '../../../fixtures/pages/Plan'; -import { TasksModule } from '../../../fixtures/pages/Plan/TasksModule'; +import { TasksModule } from '../../../fixtures/pages/Plan/Module_tasks'; import apiGetDraftMandatoryPlan from '../../../api/plans/pid/_get/200_draft_mandatory_only.json'; import { RequestQuotationModal } from '../../../fixtures/pages/Plan/RequestQuotationModal'; @@ -19,7 +19,7 @@ test.describe('The tasks module defines a list of activities.', () => { await moduleBuilderPage.mockWorkspacesList(); await moduleBuilderPage.mockGetDraftWithOnlyMandatoryModulesPlan(); await moduleBuilderPage.open(); - await moduleBuilderPage.elements().instructionsTab().click(); + await moduleBuilderPage.elements().tabInstructions().click(); }); test('Tasks can be deleted, but it is required to have at least 1 item to Request a Quote', async ({ @@ -62,9 +62,7 @@ test.describe('The tasks module defines a list of activities.', () => { const tasks = TasksModule.getTasksFromPlan(apiGetDraftMandatoryPlan); const element = tasksModule.elements().taskListItem().nth(0); - const elementTitle = element.getByRole('textbox', { - name: i18n.t('__PLAN_PAGE_MODULE_TASKS_TASK_TITLE_LABEL'), - }); + const elementTitle = tasksModule.elements().taskTitleInput(element); await expect(elementTitle).toHaveValue(tasks[0].title); await elementTitle.fill(''); await elementTitle.blur(); diff --git a/tests/e2e/plan/pending_review.spec.ts b/tests/e2e/plan/pending_review.spec.ts index 2696f6eb6..d3130efea 100644 --- a/tests/e2e/plan/pending_review.spec.ts +++ b/tests/e2e/plan/pending_review.spec.ts @@ -1,11 +1,30 @@ import { test, expect } from '../../fixtures/app'; import { PlanPage } from '../../fixtures/pages/Plan'; +import { GoalModule } from '../../fixtures/pages/Plan/Module_goal'; +import { TargetModule } from '../../fixtures/pages/Plan/Module_target'; +import { LanguageModule } from '../../fixtures/pages/Plan/Module_language'; +import { DigitalLiteracyModule } from '../../fixtures/pages/Plan/Module_digital_literacy'; +import { TasksModule } from '../../fixtures/pages/Plan/Module_tasks'; +import { OutOfScopeModule } from '../../fixtures/pages/Plan/Module_out_of_scope'; test.describe('A Plan page in pending request', () => { let moduleBuilderPage: PlanPage; + let goalModule: GoalModule; + let targetModule: TargetModule; + let languageModule: LanguageModule; + let digitalLiteracyModule: DigitalLiteracyModule; + let outOfScopeModule: OutOfScopeModule; + let tasksModule: TasksModule; test.beforeEach(async ({ page }) => { moduleBuilderPage = new PlanPage(page); + goalModule = new GoalModule(page); + targetModule = new TargetModule(page); + languageModule = new LanguageModule(page); + digitalLiteracyModule = new DigitalLiteracyModule(page); + outOfScopeModule = new OutOfScopeModule(page); + tasksModule = new TasksModule(page); + await moduleBuilderPage.loggedIn(); await moduleBuilderPage.mockPreferences(); await moduleBuilderPage.mockWorkspace(); @@ -14,28 +33,65 @@ test.describe('A Plan page in pending request', () => { await moduleBuilderPage.open(); }); - test('has only the confirm button visible', async () => { + test('has the summary tab visible and the confirm button is disabled', async ({ + page, + i18n, + }) => { await expect( moduleBuilderPage.elements().saveConfigurationCTA() ).not.toBeVisible(); + await expect( + moduleBuilderPage.elements().requestQuotationCTA() + ).not.toBeVisible(); await expect( moduleBuilderPage.elements().confirmActivityCTA() ).toBeVisible(); await expect( moduleBuilderPage.elements().confirmActivityCTA() ).toBeDisabled(); - }); - test('all inputs should be readonly', async () => { await expect( - moduleBuilderPage.elements().saveConfigurationCTA() - ).not.toBeVisible(); + page + .getByRole('status') + .filter({ hasText: i18n.t('PLAN_GLOBAL_ALERT_SUBMITTED_STATE_TITLE') }) + ).toBeVisible(); await expect( - moduleBuilderPage.elements().requestQuotationCTA() - ).not.toBeVisible(); + page.getByText(i18n.t('__PLAN_PAGE_INTRODUCTION_CARD_SUBMITTED_TITLE')) + ).toBeVisible(); }); - - // posso tornare indietro dal preventivo? - test('there should be a cancel quotation button active', async () => { - // Todo + test('all inputs should be readonly', async () => { + await moduleBuilderPage.elements().tabSetup().click(); + await expect(goalModule.elements().moduleInput()).toHaveAttribute( + 'readonly', + '' + ); + await moduleBuilderPage.elements().tabTarget().click(); + await expect(targetModule.elements().moduleInput()).toHaveAttribute( + 'readonly', + '' + ); + const languageRadioInputs = languageModule.elements().languageRadioInput(); + for (let i = 0; i < (await languageRadioInputs.count()); i++) { + await expect(languageRadioInputs.nth(i)).toHaveAttribute('disabled', ''); + } + const digitalLiteracyCheckbox = digitalLiteracyModule + .elements() + .moduleInput(); + for (let i = 0; i < (await digitalLiteracyCheckbox.count()); i++) { + await expect(digitalLiteracyCheckbox.nth(i)).toHaveAttribute( + 'disabled', + '' + ); + } + await moduleBuilderPage.elements().tabInstructions().click(); + await expect(outOfScopeModule.elements().moduleInput()).toHaveAttribute( + 'readonly', + '' + ); + const taskselements = tasksModule.elements().taskListItem(); + for (let i = 0; i < (await taskselements.count()); i++) { + await expect( + tasksModule.elements().taskTitleInput(taskselements.nth(i)) + ).toHaveAttribute('readonly', ''); + } }); }); diff --git a/tests/fixtures/pages/Plan/Module_digital_literacy.ts b/tests/fixtures/pages/Plan/Module_digital_literacy.ts new file mode 100644 index 000000000..e9d80726e --- /dev/null +++ b/tests/fixtures/pages/Plan/Module_digital_literacy.ts @@ -0,0 +1,37 @@ +import { type Page } from '@playwright/test'; +import { i18n } from 'i18next'; +import { getI18nInstance } from 'playwright-i18next-fixture'; + +export class DigitalLiteracyModule { + readonly page: Page; + + readonly i18n: i18n; + + constructor(page: Page) { + this.page = page; + this.i18n = getI18nInstance() as unknown as i18n; + } + + elements() { + return { + module: () => this.page.getByTestId('digital-literacy-module'), + moduleInput: () => this.elements().module().getByRole('checkbox'), + digitalLiteracyModuleErrorMessage: () => + this.page.getByTestId('literacy-error'), + }; + } + + static getLanguageFromPlan(plan: any) { + const languageModule = plan.config.modules.find( + (module) => module.type === 'language' + ); + if (!languageModule) { + throw new Error('No language module found in plan'); + } + if (!(typeof languageModule.output === 'string')) { + throw new Error('Invalid language module output'); + } + const language = languageModule.output; + return language; + } +} diff --git a/tests/fixtures/pages/Plan/Module_goal.ts b/tests/fixtures/pages/Plan/Module_goal.ts new file mode 100644 index 000000000..3d734052e --- /dev/null +++ b/tests/fixtures/pages/Plan/Module_goal.ts @@ -0,0 +1,36 @@ +import { type Page } from '@playwright/test'; +import { i18n } from 'i18next'; +import { getI18nInstance } from 'playwright-i18next-fixture'; + +export class GoalModule { + readonly page: Page; + + readonly i18n: i18n; + + constructor(page: Page) { + this.page = page; + this.i18n = getI18nInstance() as unknown as i18n; + } + + elements() { + return { + module: () => this.page.getByTestId('goal-module'), + moduleError: () => this.elements().module().getByTestId('goal-error'), + moduleInput: () => this.elements().module().getByRole('textbox'), + }; + } + + static getGoalFromPlan(plan: any) { + const goalModule = plan.config.modules.find( + (module) => module.type === 'goal' + ); + if (!goalModule) { + throw new Error('No goal found in plan'); + } + if (!(typeof goalModule.output === 'string')) { + throw new Error('Invalid goal module output'); + } + const goalValue = goalModule.output; + return goalValue; + } +} diff --git a/tests/fixtures/pages/Plan/Module_language.ts b/tests/fixtures/pages/Plan/Module_language.ts new file mode 100644 index 000000000..bdb7a22e5 --- /dev/null +++ b/tests/fixtures/pages/Plan/Module_language.ts @@ -0,0 +1,35 @@ +import { type Page } from '@playwright/test'; +import { i18n } from 'i18next'; +import { getI18nInstance } from 'playwright-i18next-fixture'; + +export class LanguageModule { + readonly page: Page; + + readonly i18n: i18n; + + constructor(page: Page) { + this.page = page; + this.i18n = getI18nInstance() as unknown as i18n; + } + + elements() { + return { + module: () => this.page.getByTestId('language-module'), + languageRadioInput: () => this.elements().module().getByRole('radio'), + }; + } + + static getLanguageFromPlan(plan: any) { + const languageModule = plan.config.modules.find( + (module) => module.type === 'language' + ); + if (!languageModule) { + throw new Error('No language module found in plan'); + } + if (!(typeof languageModule.output === 'string')) { + throw new Error('Invalid language module output'); + } + const language = languageModule.output; + return language; + } +} diff --git a/tests/fixtures/pages/Plan/Module_out_of_scope.ts b/tests/fixtures/pages/Plan/Module_out_of_scope.ts new file mode 100644 index 000000000..b35c22d63 --- /dev/null +++ b/tests/fixtures/pages/Plan/Module_out_of_scope.ts @@ -0,0 +1,34 @@ +import { type Page } from '@playwright/test'; +import { i18n } from 'i18next'; +import { getI18nInstance } from 'playwright-i18next-fixture'; + +export class OutOfScopeModule { + readonly page: Page; + + readonly i18n: i18n; + + constructor(page: Page) { + this.page = page; + this.i18n = getI18nInstance() as unknown as i18n; + } + + elements() { + return { + module: () => this.page.getByTestId('out-of-scope-module'), + moduleError: () => + this.elements().module().getByTestId('out-of-scope-error'), + moduleInput: () => this.elements().module().getByRole('textbox'), + }; + } + + static getOutOfScopeFromPlan(plan: any) { + const outOfScopeModule = plan.config.modules.find( + (module) => module.type === 'out_of_scope' + ); + if (!outOfScopeModule) { + throw new Error('No outOfScope found in plan'); + } + const outOfScopeValue = outOfScopeModule.output; + return outOfScopeValue; + } +} diff --git a/tests/fixtures/pages/Plan/Module_target.ts b/tests/fixtures/pages/Plan/Module_target.ts new file mode 100644 index 000000000..ec1c962be --- /dev/null +++ b/tests/fixtures/pages/Plan/Module_target.ts @@ -0,0 +1,41 @@ +import { type Page } from '@playwright/test'; +import { i18n } from 'i18next'; +import { getI18nInstance } from 'playwright-i18next-fixture'; + +export class TargetModule { + readonly page: Page; + + readonly i18n: i18n; + + constructor(page: Page) { + this.page = page; + this.i18n = getI18nInstance() as unknown as i18n; + } + + elements() { + return { + module: () => this.page.getByTestId('target-module'), + moduleError: () => this.elements().module().getByTestId('target-error'), + moduleInput: () => this.page.getByTestId('target-input'), + }; + } + + async fillInputTarget(value: string) { + await this.elements().moduleInput().click(); + await this.elements().moduleInput().fill(value); + await this.elements().moduleInput().blur(); + } + + static getTargetFromPlan(plan: any): number { + const targetModule = plan.config.modules.find( + (module) => module.type === 'target' + ); + if (!targetModule) { + throw new Error('No target module found in plan'); + } + if (typeof targetModule.output !== 'number') { + throw new Error('Invalid target module output'); + } + return targetModule.output; + } +} diff --git a/tests/fixtures/pages/Plan/TasksModule.ts b/tests/fixtures/pages/Plan/Module_tasks.ts similarity index 90% rename from tests/fixtures/pages/Plan/TasksModule.ts rename to tests/fixtures/pages/Plan/Module_tasks.ts index 60eaf2ec8..8ea7aed5f 100644 --- a/tests/fixtures/pages/Plan/TasksModule.ts +++ b/tests/fixtures/pages/Plan/Module_tasks.ts @@ -1,4 +1,4 @@ -import { type Page } from '@playwright/test'; +import { Locator, type Page } from '@playwright/test'; import { i18n } from 'i18next'; import { getI18nInstance } from 'playwright-i18next-fixture'; @@ -17,6 +17,10 @@ export class TasksModule { module: () => this.page.getByTestId('tasks-module'), taskList: () => this.elements().module().getByRole('list'), taskListItem: () => this.elements().taskList().getByRole('listitem'), + taskTitleInput: (element: Locator) => + element.getByRole('textbox', { + name: this.i18n.t('__PLAN_PAGE_MODULE_TASKS_TASK_TITLE_LABEL'), + }), taskListErrorRequired: () => this.elements() .module() @@ -60,8 +64,6 @@ export class TasksModule { }; } - async fillInputTItle(value: string) {} - static getTasksFromPlan(plan: any) { const tasksModule = plan.config.modules.find( (module) => module.type === 'tasks' diff --git a/tests/fixtures/pages/Plan/RequestQuotationModal.ts b/tests/fixtures/pages/Plan/RequestQuotationModal.ts index 023e4240c..30a6c8f9b 100644 --- a/tests/fixtures/pages/Plan/RequestQuotationModal.ts +++ b/tests/fixtures/pages/Plan/RequestQuotationModal.ts @@ -20,6 +20,10 @@ export class RequestQuotationModal { this.page.getByRole('dialog', { name: this.i18n.t('__PLAN_PAGE_MODAL_SEND_REQUEST_TITLE'), }), + datesModule: () => this.elements().modal().getByTestId('dates-module'), + datesModuleInput: () => + this.elements().datesModule().getByRole('textbox'), + datesModuleError: () => this.elements().datesModule().getByRole('status'), titleModule: () => this.elements().modal().getByTestId('title-module'), titleModuleInput: () => this.elements().modal().getByTestId('title-input'), @@ -40,6 +44,28 @@ export class RequestQuotationModal { await this.elements().titleModuleInput().blur(); } + async fillInputDate(value: string) { + await this.elements().datesModule().click(); + await this.elements().datesModuleInput().fill(value); + await this.elements().datesModuleInput().blur(); + } + + static getDateFromPlan(plan: any) { + const dateModule = plan.config.modules.find( + (module) => module.type === 'dates' + ); + if (!dateModule) { + throw new Error('No date module found in plan'); + } + if ( + !(typeof dateModule.output === 'object' && 'start' in dateModule.output) + ) { + throw new Error('Invalid date module output'); + } + const dateValue = new Date(dateModule.output.start); + return dateValue; + } + async submitRequest() { const patchStatusPromise = this.page.waitForResponse( (response) => diff --git a/tests/fixtures/pages/Plan/index.ts b/tests/fixtures/pages/Plan/index.ts index 3b34e0b1d..a05dfcb43 100644 --- a/tests/fixtures/pages/Plan/index.ts +++ b/tests/fixtures/pages/Plan/index.ts @@ -14,12 +14,31 @@ export class PlanPage extends UnguessPage { return { ...super.elements(), confirmActivityCTA: () => - this.elements().pageHeader().getByTestId('confirm-activity-cta'), - datesModule: () => this.page.getByTestId('dates-module'), - datesModuleDatepicker: () => - this.elements().datesModule().getByTestId('dates-datepicker'), - datesModuleError: () => - this.elements().datesModule().getByTestId('dates-error'), + this.elements() + .pageHeader() + .getByRole('button', { + name: this.i18n.t( + '__PLAN_PAGE_SUMMARY_TAB_CONFIRMATION_CARD_CONFIRM_CTA' + ), + }), + requestQuotationCTA: () => + this.page.getByRole('button', { + name: this.i18n.t('__PLAN_REQUEST_QUOTATION_CTA'), + }), + saveConfigurationCTA: () => + this.elements() + .pageHeader() + .getByRole('button', { + name: this.i18n.t('__PLAN_SAVE_CONFIGURATION_CTA'), + }), + goToDashboardCTA: () => + this.elements() + .pageHeader() + .getByRole('button', { + name: this.i18n.t( + '__PLAN_PAGE_SUMMARY_TAB_CONFIRMATION_CARD_GO_TO_CAMPAIGN_CTA' + ), + }), deletePlanActionItem: () => this.page.getByTestId('delete-action-item'), deletePlanModal: () => this.page.getByRole('dialog', { @@ -42,40 +61,12 @@ export class PlanPage extends UnguessPage { .deletePlanModal() .getByText(this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_TITLE')), descriptionModule: () => this.page.getByTestId('description-module'), - digitalLiteracyModule: () => - this.page.getByTestId('digital-literacy-module'), - digitalLiteracyModuleErrorMessage: () => - this.page.getByTestId('literacy-error'), extraActionsMenu: () => this.page.getByTestId('extra-actions-menu'), - goalModule: () => this.page.getByTestId('goal-module'), - goalModuleError: () => this.page.getByTestId('goal-error'), - goalModuleInput: () => this.page.getByRole('textbox'), - instructionsTab: () => this.page.getByTestId('instructions-tab'), - languageModule: () => this.page.getByTestId('language-module'), - languageRadioInput: () => - this.elements().languageModule().getByRole('radio'), - outOfScopeModule: () => this.page.getByTestId('out-of-scope-module'), - outOfScopeModuleError: () => this.page.getByTestId('out-of-scope-error'), - outOfScopeModuleInput: () => - this.elements().outOfScopeModule().getByRole('textbox'), pageHeader: () => this.page.getByTestId('plan-page-header'), - requestQuotationCTA: () => - this.page.getByRole('button', { - name: this.i18n.t('__PLAN_REQUEST_QUOTATION_CTA'), - }), - saveConfigurationCTA: () => - this.elements() - .pageHeader() - .getByRole('button', { - name: this.i18n.t('__PLAN_SAVE_CONFIGURATION_CTA'), - }), - setupTab: () => this.page.getByTestId('setup-tab'), - summaryTab: () => this.page.getByTestId('summary-tab'), - targetModule: () => this.page.getByTestId('target-module'), - targetModuleError: () => - this.elements().targetModule().getByTestId('target-error'), - targetModuleInput: () => this.page.getByTestId('target-input'), - targetTab: () => this.page.getByTestId('target-tab'), + tabInstructions: () => this.page.getByTestId('instructions-tab'), + tabSetup: () => this.page.getByTestId('setup-tab'), + tabSummary: () => this.page.getByTestId('summary-tab'), + tabTarget: () => this.page.getByTestId('target-tab'), titleModule: () => this.elements().pageHeader().getByTestId('title-module'), titleModuleError: () => @@ -87,61 +78,6 @@ export class PlanPage extends UnguessPage { }; } - static getDateFromPlan(plan: any) { - const dateModule = plan.config.modules.find( - (module) => module.type === 'dates' - ); - if (!dateModule) { - throw new Error('No date module found in plan'); - } - if ( - !(typeof dateModule.output === 'object' && 'start' in dateModule.output) - ) { - throw new Error('Invalid date module output'); - } - const dateValue = new Date(dateModule.output.start); - return dateValue; - } - - static getOutOfScopeFromPlan(plan: any) { - const outOfScopeModule = plan.config.modules.find( - (module) => module.type === 'out_of_scope' - ); - if (!outOfScopeModule) { - throw new Error('No outOfScope found in plan'); - } - const outOfScopeValue = outOfScopeModule.output; - return outOfScopeValue; - } - - static getGoalFromPlan(plan: any) { - const goalModule = plan.config.modules.find( - (module) => module.type === 'goal' - ); - if (!goalModule) { - throw new Error('No goal found in plan'); - } - if (!(typeof goalModule.output === 'string')) { - throw new Error('Invalid goal module output'); - } - const goalValue = goalModule.output; - return goalValue; - } - - static getLanguageFromPlan(plan: any) { - const languageModule = plan.config.modules.find( - (module) => module.type === 'language' - ); - if (!languageModule) { - throw new Error('No language module found in plan'); - } - if (!(typeof languageModule.output === 'string')) { - throw new Error('Invalid language module output'); - } - const language = languageModule.output; - return language; - } - static getTitleFromPlan(plan: any) { const titleModule = plan.config.modules.find( (module) => module.type === 'title' @@ -161,25 +97,6 @@ export class PlanPage extends UnguessPage { await this.elements().titleModuleInput().blur(); } - static getTargetFromPlan(plan: any): number { - const targetModule = plan.config.modules.find( - (module) => module.type === 'target' - ); - if (!targetModule) { - throw new Error('No target module found in plan'); - } - if (typeof targetModule.output !== 'number') { - throw new Error('Invalid target module output'); - } - return targetModule.output; - } - - async fillInputTarget(value: string) { - await this.elements().targetModuleInput().click(); - await this.elements().targetModuleInput().fill(value); - await this.elements().targetModuleInput().blur(); - } - async saveConfiguration() { const patchPromise = this.page.waitForResponse( (response) => @@ -253,6 +170,18 @@ export class PlanPage extends UnguessPage { }); } + async mockGetApprovedPlan() { + await this.page.route('*/**/api/plans/1', async (route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ + path: 'tests/api/plans/pid/_get/200_approved.json', + }); + } else { + await route.fallback(); + } + }); + } + async mockPatchPlan() { await this.page.route('*/**/api/plans/1', async (route) => { if (route.request().method() === 'PATCH') { From ca17c9657257f68250f22ccd86cdaab18d516035 Mon Sep 17 00:00:00 2001 From: ZecD Date: Thu, 8 May 2025 08:46:33 +0200 Subject: [PATCH 12/49] feat(project): Add delete project modal and integrate with project header --- .../Dashboard/Modals/DeleteProjectModal.tsx | 88 +++++++++++++++++++ src/pages/Dashboard/projectPageHeader.tsx | 16 ++++ 2 files changed, 104 insertions(+) create mode 100644 src/pages/Dashboard/Modals/DeleteProjectModal.tsx diff --git a/src/pages/Dashboard/Modals/DeleteProjectModal.tsx b/src/pages/Dashboard/Modals/DeleteProjectModal.tsx new file mode 100644 index 000000000..1ae873f6d --- /dev/null +++ b/src/pages/Dashboard/Modals/DeleteProjectModal.tsx @@ -0,0 +1,88 @@ +import { useNavigate } from 'react-router-dom'; +import { useTranslation, Trans } from 'react-i18next'; +import { + Modal, + ModalClose, + Button, + FooterItem, + Notification, + MD, + Span, + useToast, + Dots, +} from '@appquality/unguess-design-system'; +import { appTheme } from 'src/app/theme'; + +const DeleteProjectModal = ({ + projectId, + onQuit, +}: { + projectId: string; + onQuit: () => void; +}) => { + const { t } = useTranslation(); + const { addToast } = useToast(); + const navigate = useNavigate(); + + const showDeleteErrorToast = (error: Error) => { + addToast( + ({ close }) => ( + + ), + { placement: 'top' } + ); + }; + + const handleConfirm = async () => { + try { + await deleteProject({ pid: projectId }); + navigate(`/`); + } catch (e) { + showDeleteErrorToast(e as unknown as Error); + } + onQuit(); + }; + + return ( + + + {t('__PROJECT_PAGE_DELETE_PROJECT_MODAL_TITLE')} + + + , boldSpan: }} + /> + + + + + + + + + + + + ); +}; + +export { DeleteProjectModal }; diff --git a/src/pages/Dashboard/projectPageHeader.tsx b/src/pages/Dashboard/projectPageHeader.tsx index 7c2ca2cfb..f204bb412 100644 --- a/src/pages/Dashboard/projectPageHeader.tsx +++ b/src/pages/Dashboard/projectPageHeader.tsx @@ -1,5 +1,6 @@ import { Button, + IconButton, LG, PageHeader, Skeleton, @@ -8,6 +9,7 @@ import { import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import { useAppSelector } from 'src/app/hooks'; +import { useState } from 'react'; import { appTheme } from 'src/app/theme'; import { ProjectSettings } from 'src/common/components/inviteUsers/projectSettings'; import { LayoutWrapper } from 'src/common/components/LayoutWrapper'; @@ -15,9 +17,11 @@ import { useGetProjectsByPidQuery } from 'src/features/api'; import { useCanAccessToActiveWorkspace } from 'src/hooks/useCanAccessToActiveWorkspace'; import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute'; import styled from 'styled-components'; +import { ReactComponent as DeleteIcon } from '@zendeskgarden/svg-icons/src/16/trash-stroke.svg'; import { Counters } from './Counters'; import { EditableDescription } from './EditableDescription'; import { EditableTitle } from './EditableTitle'; +import { DeleteProjectModal } from './Modals/DeleteProjectModal'; const StyledPageHeaderMeta = styled(PageHeader.Meta)` justify-content: space-between; @@ -47,6 +51,7 @@ export const ProjectPageHeader = ({ projectId }: { projectId: number }) => { const location = useLocation(); const { status } = useAppSelector((state) => state.user); const templatesRoute = useLocalizeRoute('templates'); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); const { isLoading, @@ -112,9 +117,20 @@ export const ProjectPageHeader = ({ projectId }: { projectId: number }) => { {t('__DASHBOARD_CTA_NEW_ACTIVITY')} )} + {project?.campaigns_count === 0 && ( + setDeleteModalOpen(true)}> + + + )} )} + {deleteModalOpen && ( + setDeleteModalOpen(false)} + /> + )} From a2efc8fbf2f12d33b5ba4a548f57a0280b23f57a Mon Sep 17 00:00:00 2001 From: iacopolea Date: Thu, 8 May 2025 12:56:51 +0200 Subject: [PATCH 13/49] feat: Update plan module components to enable/disable inputs based on draft status and refactor tests for improved readability --- src/pages/Plan/modules/Age.tsx | 2 +- src/pages/Plan/modules/Gender.tsx | 2 +- .../pid/_get/200_pending_review_quoted.json | 108 ++++++++++++++++++ ....json => 200_pending_review_unquoted.json} | 0 tests/e2e/plan/approved.spec.ts | 36 +----- tests/e2e/plan/pending_review.spec.ts | 97 ---------------- tests/e2e/plan/pending_review_quoted.spec.ts | 45 ++++++++ .../e2e/plan/pending_review_unquoted.spec.ts | 43 +++++++ tests/fixtures/pages/Plan/Module_age.ts | 29 +++++ .../pages/Plan/Module_digital_literacy.ts | 17 ++- tests/fixtures/pages/Plan/Module_gender.ts | 34 ++++++ tests/fixtures/pages/Plan/Module_goal.ts | 11 +- tests/fixtures/pages/Plan/Module_language.ts | 15 ++- .../pages/Plan/Module_out_of_scope.ts | 11 +- tests/fixtures/pages/Plan/Module_target.ts | 11 +- tests/fixtures/pages/Plan/Module_tasks.ts | 16 ++- tests/fixtures/pages/Plan/index.ts | 47 +++++++- 17 files changed, 383 insertions(+), 141 deletions(-) create mode 100644 tests/api/plans/pid/_get/200_pending_review_quoted.json rename tests/api/plans/pid/_get/{200_pending_review.json => 200_pending_review_unquoted.json} (100%) delete mode 100644 tests/e2e/plan/pending_review.spec.ts create mode 100644 tests/e2e/plan/pending_review_quoted.spec.ts create mode 100644 tests/e2e/plan/pending_review_unquoted.spec.ts create mode 100644 tests/fixtures/pages/Plan/Module_age.ts create mode 100644 tests/fixtures/pages/Plan/Module_gender.ts diff --git a/src/pages/Plan/modules/Age.tsx b/src/pages/Plan/modules/Age.tsx index 39e852252..1d48ec1f4 100644 --- a/src/pages/Plan/modules/Age.tsx +++ b/src/pages/Plan/modules/Age.tsx @@ -211,7 +211,7 @@ const Age = () => { key={`range-${ar.min}`} value={`${ar.min}-${ar.max}`} name={`range-${ar.min}-${ar.max}`} - disabled={getPlanStatus() === 'pending_review'} + disabled={getPlanStatus() !== 'draft'} checked={value?.output.some( (item) => item.min === ar.min && item.max === ar.max )} diff --git a/src/pages/Plan/modules/Gender.tsx b/src/pages/Plan/modules/Gender.tsx index 49985182c..3069c3489 100644 --- a/src/pages/Plan/modules/Gender.tsx +++ b/src/pages/Plan/modules/Gender.tsx @@ -180,7 +180,7 @@ const Gender = () => { key={gender} value={gender.toLowerCase()} name={`gender-${gender}`} - disabled={getPlanStatus() === 'pending_review'} + disabled={getPlanStatus() !== 'draft'} checked={value?.output.some( (item) => item.gender === gender.toLowerCase() )} diff --git a/tests/api/plans/pid/_get/200_pending_review_quoted.json b/tests/api/plans/pid/_get/200_pending_review_quoted.json new file mode 100644 index 000000000..d6ac0f12b --- /dev/null +++ b/tests/api/plans/pid/_get/200_pending_review_quoted.json @@ -0,0 +1,108 @@ +{ + "id": 13, + "workspace_id": 1, + "status": "pending_review", + "project": { + "id": 90, + "name": "MyProject" + }, + "quote": { + "id": 98, + "status": "proposed", + "value": "3600 €" + }, + "campaign": { + "id": 6856, + "title": "Meet Accessibility Compliance 123", + "startDate": "Thu Apr 03 2025 06:00:00 GMT+0000 (Coordinated Universal Time)" + }, + "config": { + "modules": [ + { + "type": "title", + "variant": "default", + "output": "My Plan" + }, + { + "type": "dates", + "variant": "default", + "output": { + "start": "2041-12-17T08:00:00.000Z" + } + }, + { + "type": "out_of_scope", + "variant": "default", + "output": "Out of scope" + }, + { + "type": "target", + "variant": "default", + "output": 5 + }, + { + "type": "language", + "variant": "default", + "output": "en" + }, + { + "type": "goal", + "variant": "default", + "output": "This is a Goal" + }, + { + "type": "literacy", + "output": [ + { + "level": "intermediate", + "percentage": 100 + } + ], + "variant": "default" + }, + + { + "type": "age", + "output": [ + { + "max": 24, + "min": 16, + "percentage": 50 + }, + { + "max": 70, + "min": 55, + "percentage": 50 + } + ], + "variant": "default" + }, + { + "type": "gender", + "output": [ + { + "gender": "female", + "percentage": 100 + } + ], + "variant": "default" + }, + { + "type": "tasks", + "variant": "default", + "output": [ + { + "kind": "bug", + "title": "Search for bugs", + "description": "description kind bug" + }, + { + "kind": "video", + "title": "Think aloud", + "description": "description kind bug" + } + ] + } + ] + } +} diff --git a/tests/api/plans/pid/_get/200_pending_review.json b/tests/api/plans/pid/_get/200_pending_review_unquoted.json similarity index 100% rename from tests/api/plans/pid/_get/200_pending_review.json rename to tests/api/plans/pid/_get/200_pending_review_unquoted.json diff --git a/tests/e2e/plan/approved.spec.ts b/tests/e2e/plan/approved.spec.ts index 18377632c..1f4efe013 100644 --- a/tests/e2e/plan/approved.spec.ts +++ b/tests/e2e/plan/approved.spec.ts @@ -56,40 +56,8 @@ test.describe('A Plan page in accepted state', () => { page.getByText(i18n.t('__PLAN_PAGE_INTRODUCTION_CARD_APPROVED_TITLE')) ).toBeVisible(); }); + test('all inputs should be readonly', async () => { - await moduleBuilderPage.elements().tabSetup().click(); - await expect(goalModule.elements().moduleInput()).toHaveAttribute( - 'readonly', - '' - ); - await moduleBuilderPage.elements().tabTarget().click(); - await expect(targetModule.elements().moduleInput()).toHaveAttribute( - 'readonly', - '' - ); - const languageRadioInputs = languageModule.elements().languageRadioInput(); - for (let i = 0; i < (await languageRadioInputs.count()); i++) { - await expect(languageRadioInputs.nth(i)).toHaveAttribute('disabled', ''); - } - const digitalLiteracyCheckbox = digitalLiteracyModule - .elements() - .moduleInput(); - for (let i = 0; i < (await digitalLiteracyCheckbox.count()); i++) { - await expect(digitalLiteracyCheckbox.nth(i)).toHaveAttribute( - 'disabled', - '' - ); - } - await moduleBuilderPage.elements().tabInstructions().click(); - await expect(outOfScopeModule.elements().moduleInput()).toHaveAttribute( - 'readonly', - '' - ); - const taskselements = tasksModule.elements().taskListItem(); - for (let i = 0; i < (await taskselements.count()); i++) { - await expect( - tasksModule.elements().taskTitleInput(taskselements.nth(i)) - ).toHaveAttribute('readonly', ''); - } + await moduleBuilderPage.expectAllModulesToBeReadonly(); }); }); diff --git a/tests/e2e/plan/pending_review.spec.ts b/tests/e2e/plan/pending_review.spec.ts deleted file mode 100644 index d3130efea..000000000 --- a/tests/e2e/plan/pending_review.spec.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { test, expect } from '../../fixtures/app'; -import { PlanPage } from '../../fixtures/pages/Plan'; -import { GoalModule } from '../../fixtures/pages/Plan/Module_goal'; -import { TargetModule } from '../../fixtures/pages/Plan/Module_target'; -import { LanguageModule } from '../../fixtures/pages/Plan/Module_language'; -import { DigitalLiteracyModule } from '../../fixtures/pages/Plan/Module_digital_literacy'; -import { TasksModule } from '../../fixtures/pages/Plan/Module_tasks'; -import { OutOfScopeModule } from '../../fixtures/pages/Plan/Module_out_of_scope'; - -test.describe('A Plan page in pending request', () => { - let moduleBuilderPage: PlanPage; - let goalModule: GoalModule; - let targetModule: TargetModule; - let languageModule: LanguageModule; - let digitalLiteracyModule: DigitalLiteracyModule; - let outOfScopeModule: OutOfScopeModule; - let tasksModule: TasksModule; - - test.beforeEach(async ({ page }) => { - moduleBuilderPage = new PlanPage(page); - goalModule = new GoalModule(page); - targetModule = new TargetModule(page); - languageModule = new LanguageModule(page); - digitalLiteracyModule = new DigitalLiteracyModule(page); - outOfScopeModule = new OutOfScopeModule(page); - tasksModule = new TasksModule(page); - - await moduleBuilderPage.loggedIn(); - await moduleBuilderPage.mockPreferences(); - await moduleBuilderPage.mockWorkspace(); - await moduleBuilderPage.mockWorkspacesList(); - await moduleBuilderPage.mockGetPendingReviewPlan(); - await moduleBuilderPage.open(); - }); - - test('has the summary tab visible and the confirm button is disabled', async ({ - page, - i18n, - }) => { - await expect( - moduleBuilderPage.elements().saveConfigurationCTA() - ).not.toBeVisible(); - await expect( - moduleBuilderPage.elements().requestQuotationCTA() - ).not.toBeVisible(); - await expect( - moduleBuilderPage.elements().confirmActivityCTA() - ).toBeVisible(); - await expect( - moduleBuilderPage.elements().confirmActivityCTA() - ).toBeDisabled(); - await expect( - page - .getByRole('status') - .filter({ hasText: i18n.t('PLAN_GLOBAL_ALERT_SUBMITTED_STATE_TITLE') }) - ).toBeVisible(); - await expect( - page.getByText(i18n.t('__PLAN_PAGE_INTRODUCTION_CARD_SUBMITTED_TITLE')) - ).toBeVisible(); - }); - test('all inputs should be readonly', async () => { - await moduleBuilderPage.elements().tabSetup().click(); - await expect(goalModule.elements().moduleInput()).toHaveAttribute( - 'readonly', - '' - ); - await moduleBuilderPage.elements().tabTarget().click(); - await expect(targetModule.elements().moduleInput()).toHaveAttribute( - 'readonly', - '' - ); - const languageRadioInputs = languageModule.elements().languageRadioInput(); - for (let i = 0; i < (await languageRadioInputs.count()); i++) { - await expect(languageRadioInputs.nth(i)).toHaveAttribute('disabled', ''); - } - const digitalLiteracyCheckbox = digitalLiteracyModule - .elements() - .moduleInput(); - for (let i = 0; i < (await digitalLiteracyCheckbox.count()); i++) { - await expect(digitalLiteracyCheckbox.nth(i)).toHaveAttribute( - 'disabled', - '' - ); - } - await moduleBuilderPage.elements().tabInstructions().click(); - await expect(outOfScopeModule.elements().moduleInput()).toHaveAttribute( - 'readonly', - '' - ); - const taskselements = tasksModule.elements().taskListItem(); - for (let i = 0; i < (await taskselements.count()); i++) { - await expect( - tasksModule.elements().taskTitleInput(taskselements.nth(i)) - ).toHaveAttribute('readonly', ''); - } - }); -}); diff --git a/tests/e2e/plan/pending_review_quoted.spec.ts b/tests/e2e/plan/pending_review_quoted.spec.ts new file mode 100644 index 000000000..4c65d887f --- /dev/null +++ b/tests/e2e/plan/pending_review_quoted.spec.ts @@ -0,0 +1,45 @@ +import { test, expect } from '../../fixtures/app'; +import { PlanPage } from '../../fixtures/pages/Plan'; + +test.describe('A Plan page in pending request with a quote from us', () => { + let moduleBuilderPage: PlanPage; + + test.beforeEach(async ({ page }) => { + moduleBuilderPage = new PlanPage(page); + await moduleBuilderPage.loggedIn(); + await moduleBuilderPage.mockPreferences(); + await moduleBuilderPage.mockWorkspace(); + await moduleBuilderPage.mockWorkspacesList(); + await moduleBuilderPage.mockGetPendingReviewPlan_WithQuote(); + await moduleBuilderPage.open(); + }); + + test('has the summary tab visible and the confirm button is now enabled', async ({ + page, + i18n, + }) => { + await expect( + moduleBuilderPage.elements().saveConfigurationCTA() + ).not.toBeVisible(); + await expect( + moduleBuilderPage.elements().requestQuotationCTA() + ).not.toBeVisible(); + await expect( + moduleBuilderPage.elements().confirmActivityCTA() + ).toBeEnabled(); + await expect( + page + .getByRole('status') + .filter({ hasText: i18n.t('PLAN_GLOBAL_ALERT_AWATING_STATE_TITLE') }) + ).toBeVisible(); + await expect( + page.getByText( + i18n.t('__PLAN_PAGE_INTRODUCTION_CARD_AWAITING_REVIEW_TITLE') + ) + ).toBeVisible(); + }); + + test('all inputs should be readonly', async () => { + await moduleBuilderPage.expectAllModulesToBeReadonly(); + }); +}); diff --git a/tests/e2e/plan/pending_review_unquoted.spec.ts b/tests/e2e/plan/pending_review_unquoted.spec.ts new file mode 100644 index 000000000..7cdf385ae --- /dev/null +++ b/tests/e2e/plan/pending_review_unquoted.spec.ts @@ -0,0 +1,43 @@ +import { test, expect } from '../../fixtures/app'; +import { PlanPage } from '../../fixtures/pages/Plan'; + +test.describe('A Plan page in pending request', () => { + let moduleBuilderPage: PlanPage; + + test.beforeEach(async ({ page }) => { + moduleBuilderPage = new PlanPage(page); + + await moduleBuilderPage.loggedIn(); + await moduleBuilderPage.mockPreferences(); + await moduleBuilderPage.mockWorkspace(); + await moduleBuilderPage.mockWorkspacesList(); + await moduleBuilderPage.mockGetPendingReviewPlan_WithoutQuote(); + await moduleBuilderPage.open(); + }); + + test('has the summary tab visible and the confirm button is disabled', async ({ + page, + i18n, + }) => { + await expect( + moduleBuilderPage.elements().saveConfigurationCTA() + ).not.toBeVisible(); + await expect( + moduleBuilderPage.elements().requestQuotationCTA() + ).not.toBeVisible(); + await expect( + moduleBuilderPage.elements().confirmActivityCTA() + ).toBeDisabled(); + await expect( + page + .getByRole('status') + .filter({ hasText: i18n.t('PLAN_GLOBAL_ALERT_SUBMITTED_STATE_TITLE') }) + ).toBeVisible(); + await expect( + page.getByText(i18n.t('__PLAN_PAGE_INTRODUCTION_CARD_SUBMITTED_TITLE')) + ).toBeVisible(); + }); + test('all inputs should be readonly', async () => { + await moduleBuilderPage.expectAllModulesToBeReadonly(); + }); +}); diff --git a/tests/fixtures/pages/Plan/Module_age.ts b/tests/fixtures/pages/Plan/Module_age.ts new file mode 100644 index 000000000..aacc51d40 --- /dev/null +++ b/tests/fixtures/pages/Plan/Module_age.ts @@ -0,0 +1,29 @@ +import { expect, type Page } from '@playwright/test'; +import { PlanModule } from '.'; + +export class AgeModule implements PlanModule { + readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + elements() { + return { + module: () => this.page.getByTestId('age-module'), + tab: () => this.page.getByTestId('target-tab'), + moduleInput: () => this.elements().module().getByRole('checkbox'), + }; + } + + async goToTab() { + await this.elements().tab().click(); + } + + async expectToBeReadonly() { + const ageCheckbox = this.elements().moduleInput(); + for (let i = 0; i < (await ageCheckbox.count()); i++) { + await expect(ageCheckbox.nth(i)).toHaveAttribute('disabled', ''); + } + } +} diff --git a/tests/fixtures/pages/Plan/Module_digital_literacy.ts b/tests/fixtures/pages/Plan/Module_digital_literacy.ts index e9d80726e..42bbd0b10 100644 --- a/tests/fixtures/pages/Plan/Module_digital_literacy.ts +++ b/tests/fixtures/pages/Plan/Module_digital_literacy.ts @@ -1,4 +1,4 @@ -import { type Page } from '@playwright/test'; +import { expect, type Page } from '@playwright/test'; import { i18n } from 'i18next'; import { getI18nInstance } from 'playwright-i18next-fixture'; @@ -14,6 +14,7 @@ export class DigitalLiteracyModule { elements() { return { + tab: () => this.page.getByTestId('target-tab'), module: () => this.page.getByTestId('digital-literacy-module'), moduleInput: () => this.elements().module().getByRole('checkbox'), digitalLiteracyModuleErrorMessage: () => @@ -34,4 +35,18 @@ export class DigitalLiteracyModule { const language = languageModule.output; return language; } + + async goToTab() { + await this.elements().tab().click(); + } + + async expectToBeReadonly() { + const digitalLiteracyCheckbox = this.elements().moduleInput(); + for (let i = 0; i < (await digitalLiteracyCheckbox.count()); i++) { + await expect(digitalLiteracyCheckbox.nth(i)).toHaveAttribute( + 'disabled', + '' + ); + } + } } diff --git a/tests/fixtures/pages/Plan/Module_gender.ts b/tests/fixtures/pages/Plan/Module_gender.ts new file mode 100644 index 000000000..51e83a2bc --- /dev/null +++ b/tests/fixtures/pages/Plan/Module_gender.ts @@ -0,0 +1,34 @@ +import { expect, type Page } from '@playwright/test'; +import { table } from 'console'; +import { i18n } from 'i18next'; +import { getI18nInstance } from 'playwright-i18next-fixture'; + +export class GenderModule { + readonly page: Page; + + readonly i18n: i18n; + + constructor(page: Page) { + this.page = page; + this.i18n = getI18nInstance() as unknown as i18n; + } + + elements() { + return { + tab: () => this.page.getByTestId('target-tab'), + module: () => this.page.getByTestId('gender-module'), + moduleInput: () => this.elements().module().getByRole('checkbox'), + }; + } + + async goToTab() { + await this.elements().tab().click(); + } + + async expectToBeReadonly() { + const genderCheckbox = this.elements().moduleInput(); + for (let i = 0; i < (await genderCheckbox.count()); i++) { + await expect(genderCheckbox.nth(i)).toHaveAttribute('disabled', ''); + } + } +} diff --git a/tests/fixtures/pages/Plan/Module_goal.ts b/tests/fixtures/pages/Plan/Module_goal.ts index 3d734052e..f79c63d0a 100644 --- a/tests/fixtures/pages/Plan/Module_goal.ts +++ b/tests/fixtures/pages/Plan/Module_goal.ts @@ -1,4 +1,4 @@ -import { type Page } from '@playwright/test'; +import { expect, type Page } from '@playwright/test'; import { i18n } from 'i18next'; import { getI18nInstance } from 'playwright-i18next-fixture'; @@ -15,6 +15,7 @@ export class GoalModule { elements() { return { module: () => this.page.getByTestId('goal-module'), + tab: () => this.page.getByTestId('setup-tab'), moduleError: () => this.elements().module().getByTestId('goal-error'), moduleInput: () => this.elements().module().getByRole('textbox'), }; @@ -33,4 +34,12 @@ export class GoalModule { const goalValue = goalModule.output; return goalValue; } + + async goToTab() { + await this.elements().tab().click(); + } + + async expectToBeReadonly() { + await expect(this.elements().moduleInput()).toHaveAttribute('readonly', ''); + } } diff --git a/tests/fixtures/pages/Plan/Module_language.ts b/tests/fixtures/pages/Plan/Module_language.ts index bdb7a22e5..1da75976a 100644 --- a/tests/fixtures/pages/Plan/Module_language.ts +++ b/tests/fixtures/pages/Plan/Module_language.ts @@ -1,4 +1,5 @@ -import { type Page } from '@playwright/test'; +import { expect, type Page } from '@playwright/test'; +import { table } from 'console'; import { i18n } from 'i18next'; import { getI18nInstance } from 'playwright-i18next-fixture'; @@ -14,6 +15,7 @@ export class LanguageModule { elements() { return { + tab: () => this.page.getByTestId('target-tab'), module: () => this.page.getByTestId('language-module'), languageRadioInput: () => this.elements().module().getByRole('radio'), }; @@ -32,4 +34,15 @@ export class LanguageModule { const language = languageModule.output; return language; } + + async goToTab() { + await this.elements().tab().click(); + } + + async expectToBeReadonly() { + const languageRadioInputs = this.elements().languageRadioInput(); + for (let i = 0; i < (await languageRadioInputs.count()); i++) { + await expect(languageRadioInputs.nth(i)).toHaveAttribute('disabled', ''); + } + } } diff --git a/tests/fixtures/pages/Plan/Module_out_of_scope.ts b/tests/fixtures/pages/Plan/Module_out_of_scope.ts index b35c22d63..38cc3dfe9 100644 --- a/tests/fixtures/pages/Plan/Module_out_of_scope.ts +++ b/tests/fixtures/pages/Plan/Module_out_of_scope.ts @@ -1,4 +1,4 @@ -import { type Page } from '@playwright/test'; +import { expect, type Page } from '@playwright/test'; import { i18n } from 'i18next'; import { getI18nInstance } from 'playwright-i18next-fixture'; @@ -14,6 +14,7 @@ export class OutOfScopeModule { elements() { return { + tab: () => this.page.getByTestId('instructions-tab'), module: () => this.page.getByTestId('out-of-scope-module'), moduleError: () => this.elements().module().getByTestId('out-of-scope-error'), @@ -31,4 +32,12 @@ export class OutOfScopeModule { const outOfScopeValue = outOfScopeModule.output; return outOfScopeValue; } + + async goToTab() { + await this.elements().tab().click(); + } + + async expectToBeReadonly() { + await expect(this.elements().moduleInput()).toHaveAttribute('readonly', ''); + } } diff --git a/tests/fixtures/pages/Plan/Module_target.ts b/tests/fixtures/pages/Plan/Module_target.ts index ec1c962be..7279cf366 100644 --- a/tests/fixtures/pages/Plan/Module_target.ts +++ b/tests/fixtures/pages/Plan/Module_target.ts @@ -1,4 +1,4 @@ -import { type Page } from '@playwright/test'; +import { expect, type Page } from '@playwright/test'; import { i18n } from 'i18next'; import { getI18nInstance } from 'playwright-i18next-fixture'; @@ -14,6 +14,7 @@ export class TargetModule { elements() { return { + tab: () => this.page.getByTestId('target-tab'), module: () => this.page.getByTestId('target-module'), moduleError: () => this.elements().module().getByTestId('target-error'), moduleInput: () => this.page.getByTestId('target-input'), @@ -38,4 +39,12 @@ export class TargetModule { } return targetModule.output; } + + async goToTab() { + await this.elements().tab().click(); + } + + async expectToBeReadonly() { + await expect(this.elements().moduleInput()).toHaveAttribute('readonly', ''); + } } diff --git a/tests/fixtures/pages/Plan/Module_tasks.ts b/tests/fixtures/pages/Plan/Module_tasks.ts index 8ea7aed5f..d838417b8 100644 --- a/tests/fixtures/pages/Plan/Module_tasks.ts +++ b/tests/fixtures/pages/Plan/Module_tasks.ts @@ -1,4 +1,4 @@ -import { Locator, type Page } from '@playwright/test'; +import { expect, Locator, type Page } from '@playwright/test'; import { i18n } from 'i18next'; import { getI18nInstance } from 'playwright-i18next-fixture'; @@ -14,6 +14,7 @@ export class TasksModule { elements() { return { + tab: () => this.page.getByTestId('instructions-tab'), module: () => this.page.getByTestId('tasks-module'), taskList: () => this.elements().module().getByRole('list'), taskListItem: () => this.elements().taskList().getByRole('listitem'), @@ -76,4 +77,17 @@ export class TasksModule { } return tasksModule.output; } + + async goToTab() { + await this.elements().tab().click(); + } + + async expectToBeReadonly() { + const taskselements = this.elements().taskListItem(); + for (let i = 0; i < (await taskselements.count()); i++) { + await expect( + this.elements().taskTitleInput(taskselements.nth(i)) + ).toHaveAttribute('readonly', ''); + } + } } diff --git a/tests/fixtures/pages/Plan/index.ts b/tests/fixtures/pages/Plan/index.ts index a05dfcb43..476a1f6a9 100644 --- a/tests/fixtures/pages/Plan/index.ts +++ b/tests/fixtures/pages/Plan/index.ts @@ -1,11 +1,35 @@ import { type Page } from '@playwright/test'; import { UnguessPage } from '../../UnguessPage'; +import { AgeModule } from './Module_age'; +import { GenderModule } from './Module_gender'; +import { GoalModule } from './Module_goal'; +import { OutOfScopeModule } from './Module_out_of_scope'; +import { DigitalLiteracyModule } from './Module_digital_literacy'; +import { LanguageModule } from './Module_language'; +import { TargetModule } from './Module_target'; +import { TasksModule } from './Module_tasks'; + +export interface PlanModule { + expectToBeReadonly(): Promise; + goToTab(): Promise; +} export class PlanPage extends UnguessPage { readonly page: Page; + readonly modules: PlanModule[]; constructor(page: Page) { super(page); + this.modules = [ + new AgeModule(page), + new GenderModule(page), + new OutOfScopeModule(page), + new GoalModule(page), + new DigitalLiteracyModule(page), + new LanguageModule(page), + new TargetModule(page), + new TasksModule(page), + ]; this.page = page; this.url = `plans/1`; } @@ -108,6 +132,13 @@ export class PlanPage extends UnguessPage { return patchPromise; } + async expectAllModulesToBeReadonly() { + for (const module of this.modules) { + await module.goToTab(); + await module.expectToBeReadonly(); + } + } + async mockGetDraftPlan() { await this.page.route('*/**/api/plans/1', async (route) => { if (route.request().method() === 'GET') { @@ -158,11 +189,23 @@ export class PlanPage extends UnguessPage { }); } - async mockGetPendingReviewPlan() { + async mockGetPendingReviewPlan_WithoutQuote() { + await this.page.route('*/**/api/plans/1', async (route) => { + if (route.request().method() === 'GET') { + await route.fulfill({ + path: 'tests/api/plans/pid/_get/200_pending_review_unquoted.json', + }); + } else { + await route.fallback(); + } + }); + } + + async mockGetPendingReviewPlan_WithQuote() { await this.page.route('*/**/api/plans/1', async (route) => { if (route.request().method() === 'GET') { await route.fulfill({ - path: 'tests/api/plans/pid/_get/200_pending_review.json', + path: 'tests/api/plans/pid/_get/200_pending_review_quoted.json', }); } else { await route.fallback(); From 823f56cf7c4ab4f796c73ebe4ef89fc463d14368 Mon Sep 17 00:00:00 2001 From: iacopolea Date: Thu, 8 May 2025 13:23:44 +0200 Subject: [PATCH 14/49] chore: Remove unused import from GenderModule and LanguageModule --- tests/fixtures/pages/Plan/Module_gender.ts | 1 - tests/fixtures/pages/Plan/Module_language.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/fixtures/pages/Plan/Module_gender.ts b/tests/fixtures/pages/Plan/Module_gender.ts index 51e83a2bc..195dcc5e1 100644 --- a/tests/fixtures/pages/Plan/Module_gender.ts +++ b/tests/fixtures/pages/Plan/Module_gender.ts @@ -1,5 +1,4 @@ import { expect, type Page } from '@playwright/test'; -import { table } from 'console'; import { i18n } from 'i18next'; import { getI18nInstance } from 'playwright-i18next-fixture'; diff --git a/tests/fixtures/pages/Plan/Module_language.ts b/tests/fixtures/pages/Plan/Module_language.ts index 1da75976a..03d4b1802 100644 --- a/tests/fixtures/pages/Plan/Module_language.ts +++ b/tests/fixtures/pages/Plan/Module_language.ts @@ -1,5 +1,4 @@ import { expect, type Page } from '@playwright/test'; -import { table } from 'console'; import { i18n } from 'i18next'; import { getI18nInstance } from 'playwright-i18next-fixture'; From 9fc54e7ec61b7f55eb1ebecdebffab2923b9f4d6 Mon Sep 17 00:00:00 2001 From: iacopolea Date: Thu, 8 May 2025 14:25:37 +0200 Subject: [PATCH 15/49] feat: Update E2E tests and modules for improved structure and readability; enhance parallel job execution in workflow --- .github/workflows/e2e.yml | 4 +- tests/e2e/plan/approved.spec.ts | 23 ++------ tests/e2e/plan/draft.spec.ts | 24 ++------- tests/e2e/plan/modules/date.spec.ts | 1 - tests/e2e/plan/modules/tasks.spec.ts | 31 +++++------ tests/e2e/plan/pending_review_quoted.spec.ts | 6 +++ .../e2e/plan/pending_review_unquoted.spec.ts | 6 +++ tests/fixtures/pages/Plan/Module_age.ts | 10 ++-- .../pages/Plan/Module_digital_literacy.ts | 10 ++-- tests/fixtures/pages/Plan/Module_gender.ts | 9 +++- tests/fixtures/pages/Plan/Module_language.ts | 9 +++- tests/fixtures/pages/Plan/Module_tasks.ts | 15 ++++-- tests/fixtures/pages/Plan/index.ts | 54 ++++++++++++------- 13 files changed, 109 insertions(+), 93 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c810d8ee3..15149cc65 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -16,8 +16,8 @@ jobs: strategy: fail-fast: false matrix: - # run 3 copies of the job in parallel - shard: [1, 2, 3] + # run 4 copies of the job in parallel + shard: [1, 2, 3, 4] name: "Playwright Tests - pwc" timeout-minutes: 20 diff --git a/tests/e2e/plan/approved.spec.ts b/tests/e2e/plan/approved.spec.ts index 1f4efe013..6470ad067 100644 --- a/tests/e2e/plan/approved.spec.ts +++ b/tests/e2e/plan/approved.spec.ts @@ -1,29 +1,11 @@ import { test, expect } from '../../fixtures/app'; import { PlanPage } from '../../fixtures/pages/Plan'; -import { GoalModule } from '../../fixtures/pages/Plan/Module_goal'; -import { TargetModule } from '../../fixtures/pages/Plan/Module_target'; -import { LanguageModule } from '../../fixtures/pages/Plan/Module_language'; -import { DigitalLiteracyModule } from '../../fixtures/pages/Plan/Module_digital_literacy'; -import { TasksModule } from '../../fixtures/pages/Plan/Module_tasks'; -import { OutOfScopeModule } from '../../fixtures/pages/Plan/Module_out_of_scope'; -test.describe('A Plan page in accepted state', () => { +test.describe('A Plan page in approved state', () => { let moduleBuilderPage: PlanPage; - let goalModule: GoalModule; - let targetModule: TargetModule; - let languageModule: LanguageModule; - let digitalLiteracyModule: DigitalLiteracyModule; - let outOfScopeModule: OutOfScopeModule; - let tasksModule: TasksModule; test.beforeEach(async ({ page }) => { moduleBuilderPage = new PlanPage(page); - goalModule = new GoalModule(page); - targetModule = new TargetModule(page); - languageModule = new LanguageModule(page); - digitalLiteracyModule = new DigitalLiteracyModule(page); - outOfScopeModule = new OutOfScopeModule(page); - tasksModule = new TasksModule(page); await moduleBuilderPage.loggedIn(); await moduleBuilderPage.mockPreferences(); @@ -47,6 +29,9 @@ test.describe('A Plan page in accepted state', () => { moduleBuilderPage.elements().confirmActivityCTA() ).not.toBeVisible(); await expect(moduleBuilderPage.elements().goToDashboardCTA()).toBeEnabled(); + await expect( + moduleBuilderPage.elements().extraActionsMenu() + ).not.toBeVisible(); await expect( page .getByRole('status') diff --git a/tests/e2e/plan/draft.spec.ts b/tests/e2e/plan/draft.spec.ts index 74955f222..7aae72d0b 100644 --- a/tests/e2e/plan/draft.spec.ts +++ b/tests/e2e/plan/draft.spec.ts @@ -23,18 +23,11 @@ test.describe('The module builder', () => { }); test('has a list of saved modules and not the others, a save button, a request quote cta and a dots menu cta', async () => { - // Click the "Setup" tab - await planPage.elements().tabSetup().click(); - - // Check if specific elements are visible on the "Setup" tab await expect(planPage.elements().titleModule()).toBeVisible(); - - // Check if the save button, request quote CTA and dots menu are visible and enabled - await expect(planPage.elements().saveConfigurationCTA()).toBeVisible(); await expect(planPage.elements().saveConfigurationCTA()).not.toBeDisabled(); - await expect(planPage.elements().requestQuotationCTA()).toBeVisible(); await expect(planPage.elements().requestQuotationCTA()).not.toBeDisabled(); - await expect(planPage.elements().extraActionsMenu()).toBeVisible(); + await expect(planPage.elements().confirmActivityCTA()).not.toBeVisible(); + await expect(planPage.elements().goToDashboardCTA()).not.toBeVisible(); await expect(planPage.elements().extraActionsMenu()).not.toBeDisabled(); }); @@ -60,11 +53,6 @@ test.describe('The module builder', () => { expect(data).toEqual(examplePatch); }); - // flusso di richiesta preventivo - test('Clicking request quotation ask for confirmation first', async () => { - // todo: come up with some common usecases in which the user perform some changes to the form, then click the submit button - // todo: ask confirmation - }); test('if confirmation calls the PATCH Plan', async ({ page }) => { const patchPromise = page.waitForResponse( (response) => @@ -78,22 +66,18 @@ test.describe('The module builder', () => { const data = response.request().postDataJSON(); expect(data).toEqual(examplePatch); }); - test('if PATCH plan is ok then calls the PATCH Status', async ({ page }) => { + test('if PATCH plan is ok then calls the PATCH Status', async () => { await planPage.elements().requestQuotationCTA().click(); const response = await requestQuotationModal.submitRequest(); const data = response.request().postDataJSON(); expect(data).toEqual({ status: 'pending_review' }); }); - test('after requesting quotation CTA save and Request Quote should become disabled and all inputs should be readonly', async () => { - // todo - }); }); test.describe('When the user clicks on the dots menu', () => { let planPage: PlanPage; - test.beforeEach(async ({ page }, testinfo) => { - testinfo.setTimeout(60000); + test.beforeEach(async ({ page }) => { planPage = new PlanPage(page); await planPage.loggedIn(); await planPage.mockPreferences(); diff --git a/tests/e2e/plan/modules/date.spec.ts b/tests/e2e/plan/modules/date.spec.ts index 6a0e12884..ab7838dc8 100644 --- a/tests/e2e/plan/modules/date.spec.ts +++ b/tests/e2e/plan/modules/date.spec.ts @@ -1,4 +1,3 @@ -import draftMandatory from '../../../api/plans/pid/_get/200_draft_mandatory_only.json'; import { expect, test } from '../../../fixtures/app'; import { PlanPage } from '../../../fixtures/pages/Plan'; import { RequestQuotationModal } from '../../../fixtures/pages/Plan/RequestQuotationModal'; diff --git a/tests/e2e/plan/modules/tasks.spec.ts b/tests/e2e/plan/modules/tasks.spec.ts index 821268749..b1c91780c 100644 --- a/tests/e2e/plan/modules/tasks.spec.ts +++ b/tests/e2e/plan/modules/tasks.spec.ts @@ -23,7 +23,6 @@ test.describe('The tasks module defines a list of activities.', () => { }); test('Tasks can be deleted, but it is required to have at least 1 item to Request a Quote', async ({ - page, i18n, }) => { await expect(tasksModule.elements().module()).toBeVisible(); @@ -33,20 +32,22 @@ test.describe('The tasks module defines a list of activities.', () => { ); // delete each item - for (const task of tasks) { - await tasksModule - .elements() - .taskListItem() - .getByRole('heading', { name: task.title }) - .getByRole('button', { - name: i18n.t('__PLAN_PAGE_MODULE_TASKS_REMOVE_TASK_BUTTON'), - }) - .click(); - await tasksModule - .elements() - .removeTaskConfirmationModalConfirmCTA() - .click(); - } + await Promise.all( + tasks.map(async (task) => { + await tasksModule + .elements() + .taskListItem() + .getByRole('heading', { name: task.title }) + .getByRole('button', { + name: i18n.t('__PLAN_PAGE_MODULE_TASKS_REMOVE_TASK_BUTTON'), + }) + .click(); + await tasksModule + .elements() + .removeTaskConfirmationModalConfirmCTA() + .click(); + }) + ); await expect(tasksModule.elements().taskListItem()).toHaveCount(0); await expect(tasksModule.elements().taskListErrorRequired()).toBeVisible(); await moduleBuilderPage.elements().requestQuotationCTA().click(); diff --git a/tests/e2e/plan/pending_review_quoted.spec.ts b/tests/e2e/plan/pending_review_quoted.spec.ts index 4c65d887f..0d5bc0e66 100644 --- a/tests/e2e/plan/pending_review_quoted.spec.ts +++ b/tests/e2e/plan/pending_review_quoted.spec.ts @@ -27,6 +27,12 @@ test.describe('A Plan page in pending request with a quote from us', () => { await expect( moduleBuilderPage.elements().confirmActivityCTA() ).toBeEnabled(); + await expect( + moduleBuilderPage.elements().goToDashboardCTA() + ).not.toBeVisible(); + await expect( + moduleBuilderPage.elements().extraActionsMenu() + ).not.toBeVisible(); await expect( page .getByRole('status') diff --git a/tests/e2e/plan/pending_review_unquoted.spec.ts b/tests/e2e/plan/pending_review_unquoted.spec.ts index 7cdf385ae..79ad9b416 100644 --- a/tests/e2e/plan/pending_review_unquoted.spec.ts +++ b/tests/e2e/plan/pending_review_unquoted.spec.ts @@ -28,6 +28,12 @@ test.describe('A Plan page in pending request', () => { await expect( moduleBuilderPage.elements().confirmActivityCTA() ).toBeDisabled(); + await expect( + moduleBuilderPage.elements().goToDashboardCTA() + ).not.toBeVisible(); + await expect( + moduleBuilderPage.elements().extraActionsMenu() + ).not.toBeVisible(); await expect( page .getByRole('status') diff --git a/tests/fixtures/pages/Plan/Module_age.ts b/tests/fixtures/pages/Plan/Module_age.ts index aacc51d40..4dd8a7632 100644 --- a/tests/fixtures/pages/Plan/Module_age.ts +++ b/tests/fixtures/pages/Plan/Module_age.ts @@ -1,7 +1,6 @@ import { expect, type Page } from '@playwright/test'; -import { PlanModule } from '.'; -export class AgeModule implements PlanModule { +export class AgeModule { readonly page: Page; constructor(page: Page) { @@ -22,8 +21,11 @@ export class AgeModule implements PlanModule { async expectToBeReadonly() { const ageCheckbox = this.elements().moduleInput(); - for (let i = 0; i < (await ageCheckbox.count()); i++) { - await expect(ageCheckbox.nth(i)).toHaveAttribute('disabled', ''); + const count = await ageCheckbox.count(); + const checks: Promise[] = []; + for (let i = 0; i < count; i += 1) { + checks.push(expect(ageCheckbox.nth(i)).toHaveAttribute('disabled', '')); } + await Promise.all(checks); } } diff --git a/tests/fixtures/pages/Plan/Module_digital_literacy.ts b/tests/fixtures/pages/Plan/Module_digital_literacy.ts index 42bbd0b10..418e5f051 100644 --- a/tests/fixtures/pages/Plan/Module_digital_literacy.ts +++ b/tests/fixtures/pages/Plan/Module_digital_literacy.ts @@ -42,11 +42,13 @@ export class DigitalLiteracyModule { async expectToBeReadonly() { const digitalLiteracyCheckbox = this.elements().moduleInput(); - for (let i = 0; i < (await digitalLiteracyCheckbox.count()); i++) { - await expect(digitalLiteracyCheckbox.nth(i)).toHaveAttribute( - 'disabled', - '' + const count = await digitalLiteracyCheckbox.count(); + const checks: Promise[] = []; + for (let i = 0; i < count; i += 1) { + checks.push( + expect(digitalLiteracyCheckbox.nth(i)).toHaveAttribute('disabled', '') ); } + await Promise.all(checks); } } diff --git a/tests/fixtures/pages/Plan/Module_gender.ts b/tests/fixtures/pages/Plan/Module_gender.ts index 195dcc5e1..c8ed790d1 100644 --- a/tests/fixtures/pages/Plan/Module_gender.ts +++ b/tests/fixtures/pages/Plan/Module_gender.ts @@ -26,8 +26,13 @@ export class GenderModule { async expectToBeReadonly() { const genderCheckbox = this.elements().moduleInput(); - for (let i = 0; i < (await genderCheckbox.count()); i++) { - await expect(genderCheckbox.nth(i)).toHaveAttribute('disabled', ''); + const count = await genderCheckbox.count(); + const checks: Promise[] = []; + for (let i = 0; i < count; i += 1) { + checks.push( + expect(genderCheckbox.nth(i)).toHaveAttribute('disabled', '') + ); } + await Promise.all(checks); } } diff --git a/tests/fixtures/pages/Plan/Module_language.ts b/tests/fixtures/pages/Plan/Module_language.ts index 03d4b1802..cd3c75889 100644 --- a/tests/fixtures/pages/Plan/Module_language.ts +++ b/tests/fixtures/pages/Plan/Module_language.ts @@ -40,8 +40,13 @@ export class LanguageModule { async expectToBeReadonly() { const languageRadioInputs = this.elements().languageRadioInput(); - for (let i = 0; i < (await languageRadioInputs.count()); i++) { - await expect(languageRadioInputs.nth(i)).toHaveAttribute('disabled', ''); + const count = await languageRadioInputs.count(); + const checks: Promise[] = []; + for (let i = 0; i < count; i += 1) { + checks.push( + expect(languageRadioInputs.nth(i)).toHaveAttribute('disabled', '') + ); } + await Promise.all(checks); } } diff --git a/tests/fixtures/pages/Plan/Module_tasks.ts b/tests/fixtures/pages/Plan/Module_tasks.ts index d838417b8..7c92056e2 100644 --- a/tests/fixtures/pages/Plan/Module_tasks.ts +++ b/tests/fixtures/pages/Plan/Module_tasks.ts @@ -83,11 +83,16 @@ export class TasksModule { } async expectToBeReadonly() { - const taskselements = this.elements().taskListItem(); - for (let i = 0; i < (await taskselements.count()); i++) { - await expect( - this.elements().taskTitleInput(taskselements.nth(i)) - ).toHaveAttribute('readonly', ''); + const taskElements = this.elements().taskListItem(); + const count = await taskElements.count(); + const checks: Promise[] = []; + for (let i = 0; i < count; i += 1) { + checks.push( + expect( + this.elements().taskTitleInput(taskElements.nth(i)) + ).toHaveAttribute('readonly', '') + ); } + await Promise.all(checks); } } diff --git a/tests/fixtures/pages/Plan/index.ts b/tests/fixtures/pages/Plan/index.ts index 476a1f6a9..bc4247572 100644 --- a/tests/fixtures/pages/Plan/index.ts +++ b/tests/fixtures/pages/Plan/index.ts @@ -1,35 +1,35 @@ import { type Page } from '@playwright/test'; import { UnguessPage } from '../../UnguessPage'; import { AgeModule } from './Module_age'; +import { DigitalLiteracyModule } from './Module_digital_literacy'; import { GenderModule } from './Module_gender'; import { GoalModule } from './Module_goal'; -import { OutOfScopeModule } from './Module_out_of_scope'; -import { DigitalLiteracyModule } from './Module_digital_literacy'; import { LanguageModule } from './Module_language'; +import { OutOfScopeModule } from './Module_out_of_scope'; import { TargetModule } from './Module_target'; import { TasksModule } from './Module_tasks'; -export interface PlanModule { +interface TabModule { expectToBeReadonly(): Promise; goToTab(): Promise; } - export class PlanPage extends UnguessPage { readonly page: Page; - readonly modules: PlanModule[]; + + readonly modules: { [index: string]: TabModule }; constructor(page: Page) { super(page); - this.modules = [ - new AgeModule(page), - new GenderModule(page), - new OutOfScopeModule(page), - new GoalModule(page), - new DigitalLiteracyModule(page), - new LanguageModule(page), - new TargetModule(page), - new TasksModule(page), - ]; + this.modules = { + age: new AgeModule(page), + gender: new GenderModule(page), + outOfScope: new OutOfScopeModule(page), + goal: new GoalModule(page), + digitalLiteracy: new DigitalLiteracyModule(page), + language: new LanguageModule(page), + target: new TargetModule(page), + tasks: new TasksModule(page), + }; this.page = page; this.url = `plans/1`; } @@ -133,10 +133,26 @@ export class PlanPage extends UnguessPage { } async expectAllModulesToBeReadonly() { - for (const module of this.modules) { - await module.goToTab(); - await module.expectToBeReadonly(); - } + // tab setup + this.elements().tabSetup().click(); + await Promise.all([this.modules.goal.expectToBeReadonly()]); + + // tab target + this.elements().tabTarget().click(); + await Promise.all([ + this.modules.target.expectToBeReadonly(), + this.modules.age.expectToBeReadonly(), + this.modules.gender.expectToBeReadonly(), + this.modules.digitalLiteracy.expectToBeReadonly(), + this.modules.language.expectToBeReadonly(), + ]); + + // tab instructions + this.elements().tabInstructions().click(); + await Promise.all([ + this.modules.outOfScope.expectToBeReadonly(), + this.modules.tasks.expectToBeReadonly(), + ]); } async mockGetDraftPlan() { From 67383ec5708dfbfd1381db41871fc4f6a1dbf794 Mon Sep 17 00:00:00 2001 From: Marco Bonomo Date: Thu, 8 May 2025 16:32:11 +0200 Subject: [PATCH 16/49] rework: Add NotUniqueBugs component and update CampaignOverview logic for bug uniqueness --- .../Functional/CampaignOverview.tsx | 29 +++++++++++++-- .../widgets/NotUniqueBugs/index.tsx | 36 +++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 src/pages/Campaign/useWidgets/Functional/widgets/NotUniqueBugs/index.tsx diff --git a/src/pages/Campaign/useWidgets/Functional/CampaignOverview.tsx b/src/pages/Campaign/useWidgets/Functional/CampaignOverview.tsx index 192405dce..0ea44ddc5 100644 --- a/src/pages/Campaign/useWidgets/Functional/CampaignOverview.tsx +++ b/src/pages/Campaign/useWidgets/Functional/CampaignOverview.tsx @@ -1,10 +1,11 @@ import { useTranslation } from 'react-i18next'; -import { Campaign } from 'src/features/api'; +import { Campaign, useGetCampaignsByCidBugsQuery } from 'src/features/api'; import BugDistributionCard from './widgets/BugDistributionCard'; import { Progress } from './widgets/Progress'; import { UniqueBugs } from './widgets/UniqueBugs'; import { Suggestions } from './widgets/Reccomendations'; import { WidgetSectionNew } from '../../WidgetSection'; +import { NotUniqueBugs } from './widgets/NotUniqueBugs'; export const CampaignOverview = ({ id, @@ -14,13 +15,37 @@ export const CampaignOverview = ({ campaign: Campaign; }) => { const { t } = useTranslation(); + let hasOnlyUniqueBugs = false; + + // Check if all the bugs in the campaign are unique + const { data: dataFilter } = useGetCampaignsByCidBugsQuery({ + cid: campaign.id?.toString() ?? '0', + filterBy: { is_duplicated: 0 }, + }); + + const { data: dataNoFilter } = useGetCampaignsByCidBugsQuery({ + cid: campaign.id?.toString() ?? '0', + }); + + if (dataFilter && dataNoFilter) { + const filterCount = dataFilter.items?.length; + const noFilterCount = dataNoFilter.items?.length; + if (filterCount === noFilterCount) { + hasOnlyUniqueBugs = true; + } + } + return ( - + {hasOnlyUniqueBugs ? ( + + ) : ( + + )} diff --git a/src/pages/Campaign/useWidgets/Functional/widgets/NotUniqueBugs/index.tsx b/src/pages/Campaign/useWidgets/Functional/widgets/NotUniqueBugs/index.tsx new file mode 100644 index 000000000..0698edd9d --- /dev/null +++ b/src/pages/Campaign/useWidgets/Functional/widgets/NotUniqueBugs/index.tsx @@ -0,0 +1,36 @@ +import { Span } from '@appquality/unguess-design-system'; +import { useTranslation } from 'react-i18next'; +import { appTheme } from 'src/app/theme'; +import { BasicWidget } from 'src/pages/Campaign/widgetCards/BasicWidget'; +import { WidgetLoader } from '../widgetLoader'; + +const primaryTextColor = { + color: appTheme.components.text.primaryColor, +}; + +export const NotUniqueBugs = ({ campaignId }: { campaignId: number }) => { + const { t } = useTranslation(); + const isLoading = true; // Simulating loading state + + return ( + + + Title + + {isLoading ? ( + + ) : ( + <> + {campaignId} + description} + footer={<>footer} + /> + + )} + + Footer + + ); +}; From 56ff0241579167718efea17c2b05da5c1839a476 Mon Sep 17 00:00:00 2001 From: Marco Bonomo Date: Thu, 8 May 2025 16:52:59 +0200 Subject: [PATCH 17/49] rework: Rename NotUniqueBugs to OnlyUniqueBugs and update CampaignOverview logic --- .../Campaign/useWidgets/Functional/CampaignOverview.tsx | 6 +++--- .../widgets/{NotUniqueBugs => OnlyUniqueBugs}/index.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) rename src/pages/Campaign/useWidgets/Functional/widgets/{NotUniqueBugs => OnlyUniqueBugs}/index.tsx (93%) diff --git a/src/pages/Campaign/useWidgets/Functional/CampaignOverview.tsx b/src/pages/Campaign/useWidgets/Functional/CampaignOverview.tsx index 0ea44ddc5..39e27eefa 100644 --- a/src/pages/Campaign/useWidgets/Functional/CampaignOverview.tsx +++ b/src/pages/Campaign/useWidgets/Functional/CampaignOverview.tsx @@ -5,7 +5,7 @@ import { Progress } from './widgets/Progress'; import { UniqueBugs } from './widgets/UniqueBugs'; import { Suggestions } from './widgets/Reccomendations'; import { WidgetSectionNew } from '../../WidgetSection'; -import { NotUniqueBugs } from './widgets/NotUniqueBugs'; +import { OnlyUniqueBugs } from './widgets/OnlyUniqueBugs'; export const CampaignOverview = ({ id, @@ -42,9 +42,9 @@ export const CampaignOverview = ({ > {hasOnlyUniqueBugs ? ( - + ) : ( - + )} diff --git a/src/pages/Campaign/useWidgets/Functional/widgets/NotUniqueBugs/index.tsx b/src/pages/Campaign/useWidgets/Functional/widgets/OnlyUniqueBugs/index.tsx similarity index 93% rename from src/pages/Campaign/useWidgets/Functional/widgets/NotUniqueBugs/index.tsx rename to src/pages/Campaign/useWidgets/Functional/widgets/OnlyUniqueBugs/index.tsx index 0698edd9d..3d3214d85 100644 --- a/src/pages/Campaign/useWidgets/Functional/widgets/NotUniqueBugs/index.tsx +++ b/src/pages/Campaign/useWidgets/Functional/widgets/OnlyUniqueBugs/index.tsx @@ -8,7 +8,7 @@ const primaryTextColor = { color: appTheme.components.text.primaryColor, }; -export const NotUniqueBugs = ({ campaignId }: { campaignId: number }) => { +export const OnlyUniqueBugs = ({ campaignId }: { campaignId: number }) => { const { t } = useTranslation(); const isLoading = true; // Simulating loading state From f68ed0d2bb24920ea0d0e9adc1a1e5c1270f5155 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Thu, 8 May 2025 19:05:23 +0200 Subject: [PATCH 18/49] feat: Update promo banners --- package.json | 2 +- src/pages/Bugs/Content/BugsTable/AllBugs.tsx | 20 +- .../Bugs/Content/BugsTable/BugsByState.tsx | 17 +- .../Bugs/Content/BugsTable/BugsByUsecase.tsx | 17 +- .../BugsTable/components/Reccomendation.tsx | 168 +++++----------- .../Functional/widgets/Reccomendations.tsx | 180 +++++++----------- .../widgetCards/common/WidgetCardFooter.tsx | 8 +- yarn.lock | 8 +- 8 files changed, 145 insertions(+), 275 deletions(-) diff --git a/package.json b/package.json index 35f998665..3fc4d9910 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@appquality/languages": "1.4.3", - "@appquality/unguess-design-system": "4.0.36", + "@appquality/unguess-design-system": "4.0.37", "@headwayapp/react-widget": "^0.0.4", "@reduxjs/toolkit": "^1.8.0", "@sentry/react": "^8.32.0", diff --git a/src/pages/Bugs/Content/BugsTable/AllBugs.tsx b/src/pages/Bugs/Content/BugsTable/AllBugs.tsx index 665336f27..3e7a4b21d 100644 --- a/src/pages/Bugs/Content/BugsTable/AllBugs.tsx +++ b/src/pages/Bugs/Content/BugsTable/AllBugs.tsx @@ -1,17 +1,16 @@ +import { AccordionNew } from '@appquality/unguess-design-system'; +import { t } from 'i18next'; import { appTheme } from 'src/app/theme'; +import { getSelectedFiltersIds } from 'src/features/bugsPage/bugsPageSlice'; import useWindowSize from 'src/hooks/useWindowSize'; import { styled } from 'styled-components'; -import { useGetCampaignsByCidSuggestionsQuery } from 'src/features/api'; -import { AccordionNew } from '@appquality/unguess-design-system'; -import { getSelectedFiltersIds } from 'src/features/bugsPage/bugsPageSlice'; -import { t } from 'i18next'; -import { InfoRowMeta } from './components/InfoRowMeta'; -import AllBugsTable from './components/SingleGroupTable'; import BugCards from './components/BugCards'; -import { useBugs } from './hooks/useBugs'; -import { LoadingState } from './components/LoadingState'; import { EmptyState } from './components/EmptyState'; +import { InfoRowMeta } from './components/InfoRowMeta'; +import { LoadingState } from './components/LoadingState'; import { Reccomendation } from './components/Reccomendation'; +import AllBugsTable from './components/SingleGroupTable'; +import { useBugs } from './hooks/useBugs'; const Wrapper = styled.div<{ isFetching?: boolean; @@ -31,9 +30,6 @@ export const AllBugs = ({ campaignId }: { campaignId: number }) => { const breakpointMd = parseInt(appTheme.breakpoints.md, 10); const isMdBreakpoint = width < breakpointMd; const { data, isLoading, isFetching, isError } = useBugs(campaignId); - const { data: suggestions } = useGetCampaignsByCidSuggestionsQuery({ - cid: campaignId.toString(), - }); const { allBugs: bugs } = data; const filterBy = getSelectedFiltersIds(); @@ -49,7 +45,7 @@ export const AllBugs = ({ campaignId }: { campaignId: number }) => { return ( - + diff --git a/src/pages/Bugs/Content/BugsTable/BugsByState.tsx b/src/pages/Bugs/Content/BugsTable/BugsByState.tsx index 9a57aae40..56060c072 100644 --- a/src/pages/Bugs/Content/BugsTable/BugsByState.tsx +++ b/src/pages/Bugs/Content/BugsTable/BugsByState.tsx @@ -3,13 +3,12 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getCustomStatusInfo } from 'src/common/components/utils/getCustomStatusInfo'; import { styled } from 'styled-components'; -import { useGetCampaignsByCidSuggestionsQuery } from 'src/features/api'; -import { EmptyState } from './components/EmptyState'; import { EmptyGroup } from './components/EmptyGroup'; -import { useBugsByState } from './hooks/useBugsByState'; +import { EmptyState } from './components/EmptyState'; import { LoadingState } from './components/LoadingState'; -import BugStateAccordion from './components/SingleGroupAccordion'; import { Reccomendation } from './components/Reccomendation'; +import BugStateAccordion from './components/SingleGroupAccordion'; +import { useBugsByState } from './hooks/useBugsByState'; const Wrapper = styled.div<{ isFetching?: boolean; @@ -34,9 +33,6 @@ export const BugsByState = ({ const { t } = useTranslation(); const { data, isError, isFetching, isLoading } = useBugsByState(campaignId); const { bugsByStates } = data; - const { data: suggestions } = useGetCampaignsByCidSuggestionsQuery({ - cid: campaignId.toString(), - }); const emptyBugStates = useMemo( () => bugsByStates.filter((item) => item.bugs.length === 0), @@ -73,11 +69,8 @@ export const BugsByState = ({ } ${`(${item.bugs.length})`}`} item={item} /> - {i === 0 && suggestions && ( - + {i === 0 && ( + )} ))} diff --git a/src/pages/Bugs/Content/BugsTable/BugsByUsecase.tsx b/src/pages/Bugs/Content/BugsTable/BugsByUsecase.tsx index b94b62213..07db66aa9 100644 --- a/src/pages/Bugs/Content/BugsTable/BugsByUsecase.tsx +++ b/src/pages/Bugs/Content/BugsTable/BugsByUsecase.tsx @@ -2,14 +2,13 @@ import { AccordionNew, MD } from '@appquality/unguess-design-system'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { styled } from 'styled-components'; -import { useGetCampaignsByCidSuggestionsQuery } from 'src/features/api'; -import { EmptyState } from './components/EmptyState'; import { CompletionTooltip } from './components/CompletionTooltip'; import { EmptyGroup } from './components/EmptyGroup'; +import { EmptyState } from './components/EmptyState'; import { LoadingState } from './components/LoadingState'; -import { useBugsByUseCase } from './hooks/useBugsByUseCase'; -import BugsByUseCaseAccordion from './components/SingleGroupAccordion'; import { Reccomendation } from './components/Reccomendation'; +import BugsByUseCaseAccordion from './components/SingleGroupAccordion'; +import { useBugsByUseCase } from './hooks/useBugsByUseCase'; const Wrapper = styled.div<{ isFetching?: boolean; @@ -33,9 +32,6 @@ export const BugsByUsecase = ({ }) => { const { t } = useTranslation(); const { data, isError, isFetching, isLoading } = useBugsByUseCase(campaignId); - const { data: suggestions } = useGetCampaignsByCidSuggestionsQuery({ - cid: campaignId.toString(), - }); const { bugsByUseCases } = data; const emptyUseCases = useMemo( @@ -80,11 +76,8 @@ export const BugsByUsecase = ({ ) } /> - {i === 0 && suggestions && ( - + {i === 0 && ( + )} ))} diff --git a/src/pages/Bugs/Content/BugsTable/components/Reccomendation.tsx b/src/pages/Bugs/Content/BugsTable/components/Reccomendation.tsx index bf9b26447..993958c73 100644 --- a/src/pages/Bugs/Content/BugsTable/components/Reccomendation.tsx +++ b/src/pages/Bugs/Content/BugsTable/components/Reccomendation.tsx @@ -1,27 +1,47 @@ -import { - GlobalAlert, - Anchor, - useToast, - Notification, -} from '@appquality/unguess-design-system'; -import { t } from 'i18next'; +import { GlobalAlert } from '@appquality/unguess-design-system'; +import { ReactComponent as IconMail } from '@zendeskgarden/svg-icons/src/16/email-stroke.svg'; import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; import { appTheme } from 'src/app/theme'; -import { ReactComponent as IconMail } from '@zendeskgarden/svg-icons/src/16/email-stroke.svg'; -import { - GetCampaignsByCidSuggestionsApiResponse, - usePostCampaignsByCidSuggestionsMutation, -} from 'src/features/api'; +import { useGetCampaignsByCidSuggestionsQuery } from 'src/features/api'; import { useSendGTMevent } from 'src/hooks/useGTMevent'; -import { useParams } from 'react-router-dom'; -export const Reccomendation = ({ - suggestion, -}: GetCampaignsByCidSuggestionsApiResponse) => { - const [sendMail, { isLoading }] = usePostCampaignsByCidSuggestionsMutation(); - const { addToast } = useToast(); - const { campaignId } = useParams(); +const useBannerData = (banner_type: string) => { + const { t } = useTranslation(); + + switch (banner_type) { + case 'banner_cyber_security': + return { + type: 'primary' as const, + title: t('__BANNER_CROSS_FUNCTIONAL_TITLE_CYBER'), + message: t('__BANNER_CROSS_FUNCTIONAL_MESSAGE_CYBER'), + }; + case 'banner_user_experience': + return { + type: 'accent' as const, + title: t('__BANNER_CROSS_FUNCTIONAL_TITLE_EXPERIENCE'), + message: t('__BANNER_CROSS_FUNCTIONAL_MESSAGE_EXPERIENCE'), + }; + default: + return { + type: 'accent' as const, + title: '', + message: '', + }; + } +}; +export const Reccomendation = ({ campaignId }: { campaignId: number }) => { + const { t } = useTranslation(); + const { data: suggestions } = useGetCampaignsByCidSuggestionsQuery({ + cid: campaignId.toString(), + }); + const { type, title, message } = useBannerData( + suggestions?.suggestion?.slug || '' + ); + const navigate = useNavigate(); + const suggestion = suggestions?.suggestion; const sendGTMEvent = useSendGTMevent(); useEffect(() => { if (!suggestion?.slug) { @@ -35,112 +55,22 @@ export const Reccomendation = ({ }); }, [suggestion?.slug]); - const handleCtaClick = async () => { - if (!suggestion || isLoading) { - return; - } - - sendGTMEvent({ - event: 'reccomendation', - category: 'bugs', - action: 'click', - content: suggestion.slug, - target: 'get_in_touch', - }); - - sendMail({ cid: campaignId || '0', body: { slug: suggestion.slug } }) - .unwrap() - .then(() => - addToast( - ({ close }) => ( - - ), - { placement: 'top' } - ) - ) - .catch((error) => - addToast( - ({ close }) => ( - - ), - { placement: 'top' } - ) - ); - }; - - const getCtaLabel = (slug: string) => { - if (isLoading) return t('__BANNER_CROSS_FUNCTIONAL_CTA_LOADING'); - if (slug === 'banner_testing_automation') { - return ( - <> - {t('__BANNER_CROSS_FUNCTIONAL_CTA_AUTOMATION')}{' '} - - - ); - } - return ( - <> - {t('__BANNER_CROSS_FUNCTIONAL_CTA_EXPERIENCE')}{' '} - - - ); - }; - if (!suggestion) { return null; } return ( - {suggestion.slug === 'banner_testing_automation' - ? t('__BANNER_CROSS_FUNCTIONAL_MESSAGE_AUTOMATION') - : t('__BANNER_CROSS_FUNCTIONAL_MESSAGE_EXPERIENCE')}{' '} - {suggestion.serviceId && ( - { - sendGTMEvent({ - event: 'reccomendation', - category: 'bugs', - action: 'click', - content: suggestion.slug, - target: 'view_more', - }); - }} - isExternal - > - {suggestion.slug === 'banner_testing_automation' - ? t('__BANNER_CROSS_FUNCTIONAL_MESSAGE_AUTOMATION_ANCHOR') - : t('__BANNER_CROSS_FUNCTIONAL_MESSAGE_EXPERIENCE_ANCHOR')} - - )} - - } + type={type} + title={title} + message={message} cta={{ - label: getCtaLabel(suggestion.slug), - onClick: handleCtaClick, + label: ( + <> + {t('__BANNER_CROSS_CTA')}{' '} + + + ), + onClick: () => navigate(`/templates/${suggestion.serviceId}`), }} style={{ marginBottom: appTheme.space.lg }} /> diff --git a/src/pages/Campaign/useWidgets/Functional/widgets/Reccomendations.tsx b/src/pages/Campaign/useWidgets/Functional/widgets/Reccomendations.tsx index 709ed7b74..9e721d016 100644 --- a/src/pages/Campaign/useWidgets/Functional/widgets/Reccomendations.tsx +++ b/src/pages/Campaign/useWidgets/Functional/widgets/Reccomendations.tsx @@ -1,27 +1,20 @@ import { Button, - IconButton, SpecialCard, Tag, - useToast, - Notification, XL, } from '@appquality/unguess-design-system'; -import styled from 'styled-components'; -import { useTranslation } from 'react-i18next'; -import { BasicWidget } from 'src/pages/Campaign/widgetCards/BasicWidget'; -import { ReactComponent as ImgExperience } from 'src/assets/banner_suggestions/experience.svg'; -import { ReactComponent as ImgAutomation } from 'src/assets/banner_suggestions/testing_automation.svg'; -import { ReactComponent as IconService } from '@zendeskgarden/svg-icons/src/16/book-open-stroke.svg'; import { ReactComponent as IconMail } from '@zendeskgarden/svg-icons/src/16/email-stroke.svg'; -import { - useGetCampaignsByCidSuggestionsQuery, - usePostCampaignsByCidSuggestionsMutation, -} from 'src/features/api'; -import { Link } from 'react-router-dom'; -import { appTheme } from 'src/app/theme'; import { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { appTheme } from 'src/app/theme'; +import { ReactComponent as ImgExperience } from 'src/assets/banner_suggestions/experience.svg'; +import { ReactComponent as ImgCyber } from 'src/assets/banner_suggestions/testing_automation.svg'; +import { useGetCampaignsByCidSuggestionsQuery } from 'src/features/api'; import { useSendGTMevent } from 'src/hooks/useGTMevent'; +import { BasicWidget } from 'src/pages/Campaign/widgetCards/BasicWidget'; +import styled from 'styled-components'; const StyledTagNew = styled(Tag)` height: ${({ theme }) => theme.space.base * 6}px; @@ -29,13 +22,43 @@ const StyledTagNew = styled(Tag)` ${({ theme }) => theme.space.base * 2}px; `; +const BasicWidgetFooter = styled(BasicWidget.Footer)` + justify-content: center; +`; + +const useBannerData = (banner_type: string) => { + const { t } = useTranslation(); + switch (banner_type) { + case 'banner_cyber_security': + return { + tag: t('__CAMPAIGN_PAGE_SUGGESTIONS_CYBER_TAG'), + header: t('__CAMPAIGN_PAGE_SUGGESTIONS_CYBER_HEADER'), + content: t('__CAMPAIGN_PAGE_SUGGESTIONS_CYBER_CONTENT'), + Image: ImgCyber, + }; + case 'banner_user_experience': + return { + tag: t('__CAMPAIGN_PAGE_SUGGESTIONS_EXPERIENCE_TAG'), + header: t('__CAMPAIGN_PAGE_SUGGESTIONS_EXPERIENCE_HEADER'), + content: t('__CAMPAIGN_PAGE_SUGGESTIONS_EXPERIENCE_CONTENT'), + Image: ImgExperience, + }; + default: + return { + tag: '', + header: '', + content: '', + Image: null, + }; + } +}; + export const Suggestions = ({ campaignId }: { campaignId: string }) => { const { t } = useTranslation(); + const navigate = useNavigate(); const { data: suggestions } = useGetCampaignsByCidSuggestionsQuery({ cid: campaignId, }); - const [sendMail, { isLoading }] = usePostCampaignsByCidSuggestionsMutation(); - const { addToast } = useToast(); const sendGTMEvent = useSendGTMevent(); @@ -51,43 +74,9 @@ export const Suggestions = ({ campaignId }: { campaignId: string }) => { }); }, [suggestions]); - const handleCtaClick = async () => { - if (!suggestions?.suggestion || isLoading) { - return; - } - - sendMail({ cid: campaignId, body: { slug: suggestions.suggestion.slug } }) - .unwrap() - .then(() => - addToast( - ({ close }) => ( - - ), - { placement: 'top' } - ) - ) - .catch((error) => - addToast( - ({ close }) => ( - - ), - { placement: 'top' } - ) - ); - }; + const { header, content, tag, Image } = useBannerData( + suggestions?.suggestion?.slug || '' + ); if (!suggestions?.suggestion) { return null; @@ -96,70 +85,33 @@ export const Suggestions = ({ campaignId }: { campaignId: string }) => { return ( - - {suggestions.suggestion.slug === 'banner_testing_automation' - ? t('__CAMPAIGN_PAGE_SUGGESTIONS_AUTOMATION_TAG') - : t('__CAMPAIGN_PAGE_SUGGESTIONS_EXPERIENCE_TAG')} + + {tag} - <> - {suggestions.suggestion.slug === 'banner_testing_automation' ? ( - - ) : ( - - )} - - {suggestions.suggestion.slug === 'banner_testing_automation' - ? t('__CAMPAIGN_PAGE_SUGGESTIONS_AUTOMATION_HEADER') - : t('__CAMPAIGN_PAGE_SUGGESTIONS_EXPERIENCE_HEADER')} -
- } - content={ - - {suggestions.suggestion.slug === 'banner_testing_automation' - ? t('__CAMPAIGN_PAGE_SUGGESTIONS_AUTOMATION_CONTENT') - : t('__CAMPAIGN_PAGE_SUGGESTIONS_EXPERIENCE_CONTENT')} - - } - footer="" - /> - - {suggestions.suggestion.serviceId && ( - { - sendGTMEvent({ - event: 'reccomendation', - category: 'campaign', - action: 'click', - target: 'service', - content: suggestions.suggestion?.slug, - }); - }} - > - - - - - )} - - - + + ) : null} ); }; diff --git a/src/pages/Campaign/widgetCards/common/WidgetCardFooter.tsx b/src/pages/Campaign/widgetCards/common/WidgetCardFooter.tsx index c26f82b22..2398a3462 100644 --- a/src/pages/Campaign/widgetCards/common/WidgetCardFooter.tsx +++ b/src/pages/Campaign/widgetCards/common/WidgetCardFooter.tsx @@ -1,9 +1,15 @@ import { SpecialCard } from '@appquality/unguess-design-system'; export const WidgetCardFooter = ({ + className, children, noDivider, }: { + className?: string; children: React.ReactNode; noDivider?: boolean; -}) => {children}; +}) => ( + + {children} + +); diff --git a/yarn.lock b/yarn.lock index 6bd9a21eb..87f0a2d69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -59,10 +59,10 @@ dependencies: hls.js "^1.4.8" -"@appquality/unguess-design-system@4.0.36": - version "4.0.36" - resolved "https://registry.yarnpkg.com/@appquality/unguess-design-system/-/unguess-design-system-4.0.36.tgz#c773cf1777e04850b2ba5209106e7c10ae7cc686" - integrity sha512-BEBK4eO2s2KzEmq6ZaiiF0CWLhNuyA6X0q3Zz6ZzuSc+Q0kTw0DWDHQPlngFE66vBnG26U3unUH9UiagVW1l9Q== +"@appquality/unguess-design-system@4.0.37": + version "4.0.37" + resolved "https://registry.npmjs.org/@appquality/unguess-design-system/-/unguess-design-system-4.0.37.tgz#01b2f6a458ca8b47cae6cfcfb91982704b73c639" + integrity sha512-dg/8Yj66tZxHLrH6p/cE/QIT4Ycmyaem+ozahniejhR4l1PrmIoMwqiTrl7K0Lux/BP9gKKL9q+1KVtAZhJXcA== dependencies: "@appquality/stream-player" "1.0.6" "@nivo/bar" "^0.87.0" From 0dfc4ea44bb52a2ce644cf2ce2d85d1fc47cb75f Mon Sep 17 00:00:00 2001 From: ZecD Date: Fri, 9 May 2025 08:56:28 +0200 Subject: [PATCH 19/49] feat: add delete project functionality with confirmation modal - Implemented `deleteProjectsByPid` mutation in the API to handle project deletion. - Updated `apiTags` to invalidate relevant tags upon project deletion. - Added translations for delete project modal in English and Italian. - Created `DeleteProjectModal` component to confirm project deletion. - Integrated delete functionality into the project page header, allowing users to delete projects when no campaigns or plans are associated. --- src/common/schema.ts | 89 +++++++------------ src/features/api/apiTags.ts | 3 + src/features/api/index.ts | 33 +++---- src/locales/en/translation.json | 5 ++ src/locales/it/translation.json | 6 ++ .../Dashboard/Modals/DeleteProjectModal.tsx | 6 +- src/pages/Dashboard/projectPageHeader.tsx | 19 +++- 7 files changed, 83 insertions(+), 78 deletions(-) diff --git a/src/common/schema.ts b/src/common/schema.ts index 2cc0ea7d9..648d23243 100644 --- a/src/common/schema.ts +++ b/src/common/schema.ts @@ -343,6 +343,7 @@ export interface paths { '/projects/{pid}': { /** Retrieve projects details from an ID. */ get: operations['get-projects-projectId']; + delete: operations['delete-projects-projectId']; /** Update fields of a specific project. Currently only the project name is editable. */ patch: operations['patch-projects-pid']; parameters: { @@ -446,26 +447,6 @@ export interface paths { }; }; }; - '/videos/{vid}/sentiment': { - /** - * This endpoint generates a new sentiment for the provided video if it does not already exist. - * - * **Security**: Requires Bearer Authentication. Provide your bearer token in the Authorization header when making requests to protected resources. Example: Authorization: Bearer 123. - * - * **Path Parameters**: - * - * vid (string, required): The ID of the video for which the translation is to be generated. - * Request Body (application/json): - * - * language (string, required): The language code for the desired translation. - */ - post: operations['post-videos-vid-sentiment']; - parameters: { - path: { - vid: string; - }; - }; - }; '/workspaces': { get: operations['get-workspaces']; /** This endpoint is useful to add a new workspace. Only admin can use this. */ @@ -895,6 +876,16 @@ export interface components { usecaseTitle: string; })[]; }; + MediaSentiment: { + value: number; + reason: string; + paragraphs: { + start: number; + end: number; + value: number; + reason: string; + }[]; + }; Module: | components['schemas']['ModuleTitle'] | components['schemas']['ModuleDate'] @@ -1236,6 +1227,7 @@ export interface components { }; }; transcript?: components['schemas']['Transcript']; + sentiment?: components['schemas']['MediaSentiment']; }; /** VideoTag */ VideoTag: { @@ -3132,6 +3124,26 @@ export interface operations { 500: components['responses']['Error']; }; }; + 'delete-projects-projectId': { + parameters: { + path: { + /** Project id */ + pid: components['parameters']['pid']; + }; + }; + responses: { + /** OK */ + 200: unknown; + 400: components['responses']['Error']; + 401: components['responses']['Error']; + 403: components['responses']['Error']; + 405: components['responses']['Error']; + 500: components['responses']['Error']; + }; + requestBody: { + unknown; + }; + }; /** Update fields of a specific project. Currently only the project name is editable. */ 'patch-projects-pid': { parameters: { @@ -3547,41 +3559,6 @@ export interface operations { }; }; }; - /** - * This endpoint generates a new sentiment for the provided video if it does not already exist. - * - * **Security**: Requires Bearer Authentication. Provide your bearer token in the Authorization header when making requests to protected resources. Example: Authorization: Bearer 123. - * - * **Path Parameters**: - * - * vid (string, required): The ID of the video for which the translation is to be generated. - * Request Body (application/json): - * - * language (string, required): The language code for the desired translation. - */ - 'post-videos-vid-sentiment': { - parameters: { - path: { - vid: string; - }; - }; - responses: { - /** OK */ - 200: { - content: { - 'application/json': { [key: string]: unknown }; - }; - }; - 400: components['responses']['Error']; - 403: components['responses']['Error']; - 500: components['responses']['Error']; - }; - requestBody: { - content: { - 'application/json': { [key: string]: unknown }; - }; - }; - }; 'get-workspaces': { parameters: { query: { @@ -3973,7 +3950,7 @@ export interface operations { /** Start pagination parameter */ start?: components['parameters']['start']; /** Orders results */ - orderBy?: 'updated_at' | 'id'; + orderBy?: 'updated_at' | 'id' | 'order'; /** Order value (ASC, DESC) */ order?: components['parameters']['order']; /** filterBy[]= */ diff --git a/src/features/api/apiTags.ts b/src/features/api/apiTags.ts index 8e697a2e3..578b553c5 100644 --- a/src/features/api/apiTags.ts +++ b/src/features/api/apiTags.ts @@ -280,6 +280,9 @@ unguessApi.enhanceEndpoints({ patchPlansByPidStatus: { invalidatesTags: ['Plans', 'Projects'], }, + deleteProjectsByPid: { + invalidatesTags: ['Projects'], + }, }, }); diff --git a/src/features/api/index.ts b/src/features/api/index.ts index 20ac6e677..48ae7add2 100644 --- a/src/features/api/index.ts +++ b/src/features/api/index.ts @@ -435,6 +435,16 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.body, }), }), + deleteProjectsByPid: build.mutation< + DeleteProjectsByPidApiResponse, + DeleteProjectsByPidApiArg + >({ + query: (queryArg) => ({ + url: `/projects/${queryArg.pid}`, + method: 'DELETE', + body: queryArg.body, + }), + }), getProjectsByPidCampaigns: build.query< GetProjectsByPidCampaignsApiResponse, GetProjectsByPidCampaignsApiArg @@ -572,16 +582,6 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.body, }), }), - postVideosByVidSentiment: build.mutation< - PostVideosByVidSentimentApiResponse, - PostVideosByVidSentimentApiArg - >({ - query: (queryArg) => ({ - url: `/videos/${queryArg.vid}/sentiment`, - method: 'POST', - body: queryArg.body, - }), - }), getWorkspaces: build.query({ query: (queryArg) => ({ url: `/workspaces`, @@ -1482,6 +1482,12 @@ export type PatchProjectsByPidApiArg = { description: string; }; }; +export type DeleteProjectsByPidApiResponse = /** status 200 OK */ void; +export type DeleteProjectsByPidApiArg = { + /** Project id */ + pid: string; + body: string; +}; export type GetProjectsByPidCampaignsApiResponse = /** status 200 OK */ { items?: CampaignWithOutput[]; start?: number; @@ -1640,11 +1646,6 @@ export type PostVideosByVidTranslationApiArg = { language: string; }; }; -export type PostVideosByVidSentimentApiResponse = /** status 200 OK */ object; -export type PostVideosByVidSentimentApiArg = { - vid: string; - body: object; -}; export type GetWorkspacesApiResponse = /** status 200 OK */ { items?: Workspace[]; start?: number; @@ -2773,6 +2774,7 @@ export const { usePostProjectsMutation, useGetProjectsByPidQuery, usePatchProjectsByPidMutation, + useDeleteProjectsByPidMutation, useGetProjectsByPidCampaignsQuery, useGetProjectsByPidUsersQuery, usePostProjectsByPidUsersMutation, @@ -2788,7 +2790,6 @@ export const { useDeleteVideosByVidObservationsAndOidMutation, useGetVideosByVidTranslationQuery, usePostVideosByVidTranslationMutation, - usePostVideosByVidSentimentMutation, useGetWorkspacesQuery, usePostWorkspacesMutation, useGetWorkspacesByWidQuery, diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index e9c5b7428..d71adb121 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1285,6 +1285,11 @@ "__PROJECT_FORM_NAME_MAX": "This name is a bit long. We advise you to stay within 64 characters including spaces.", "__PROJECT_FORM_NAME_REQUIRED": "Choose a name before creating the project", "__PROJECT_PAGE_ARCHIVE_DESCRIPTION": "The Archive helps you keep your projects organized. You can restore a activity at any time or leave it here for future reference", + "__PROJECT_PAGE_DELETE_PROJECT_MODAL_BODY": "Once the project has been deleted, it can no longer be recovered All current shares will be removed and will not be visible anymore.", + "__PROJECT_PAGE_DELETE_PROJECT_MODAL_BUTTON_CANCEL": "Cancel", + "__PROJECT_PAGE_DELETE_PROJECT_MODAL_BUTTON_CONFIRM": "Delete", + "__PROJECT_PAGE_DELETE_PROJECT_MODAL_ERROR": "The project could not be deleted. Please try again", + "__PROJECT_PAGE_DELETE_PROJECT_MODAL_TITLE": "Are you sure you want to delete this project?", "__PROJECT_PAGE_EMPTY_STATE_NO_FEATURE": "Manage your own projects and test campaigns!", "__PROJECT_PAGE_UPDATE_PROJECT_DESCRIPTION_PLACEHOLDER": "Write a description", "__PROJECT_PAGE_UPDATE_PROJECT_NAME_ERROR": "The project name is already taken. Choose another name.", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 8c1c448bd..65caa7613 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1314,6 +1314,12 @@ "__PROJECT_FORM_NAME_MAX": "", "__PROJECT_FORM_NAME_REQUIRED": "", "__PROJECT_PAGE_ARCHIVE_DESCRIPTION": "", + "__PROJECT_PAGE_DELETE_PROJECT_MODAL_BODY_1": "", + "__PROJECT_PAGE_DELETE_PROJECT_MODAL_BODY_2": "", + "__PROJECT_PAGE_DELETE_PROJECT_MODAL_BUTTON_CANCEL": "", + "__PROJECT_PAGE_DELETE_PROJECT_MODAL_BUTTON_CONFIRM": "", + "__PROJECT_PAGE_DELETE_PROJECT_MODAL_ERROR": "", + "__PROJECT_PAGE_DELETE_PROJECT_MODAL_TITLE": "", "__PROJECT_PAGE_EMPTY_STATE_NO_FEATURE": "", "__PROJECT_PAGE_UPDATE_PROJECT_DESCRIPTION_PLACEHOLDER": "Aggiungi una descrizione", "__PROJECT_PAGE_UPDATE_PROJECT_NAME_ERROR": "Il nome del progetto è già in uso. Scegline un altro.", diff --git a/src/pages/Dashboard/Modals/DeleteProjectModal.tsx b/src/pages/Dashboard/Modals/DeleteProjectModal.tsx index 1ae873f6d..2c9f17fce 100644 --- a/src/pages/Dashboard/Modals/DeleteProjectModal.tsx +++ b/src/pages/Dashboard/Modals/DeleteProjectModal.tsx @@ -12,17 +12,19 @@ import { Dots, } from '@appquality/unguess-design-system'; import { appTheme } from 'src/app/theme'; +import { useDeleteProjectsByPidMutation } from 'src/features/api'; const DeleteProjectModal = ({ projectId, onQuit, }: { - projectId: string; + projectId: number; onQuit: () => void; }) => { const { t } = useTranslation(); const { addToast } = useToast(); const navigate = useNavigate(); + const [deleteProject, { isLoading }] = useDeleteProjectsByPidMutation(); const showDeleteErrorToast = (error: Error) => { addToast( @@ -45,7 +47,7 @@ const DeleteProjectModal = ({ const handleConfirm = async () => { try { - await deleteProject({ pid: projectId }); + await deleteProject({ pid: projectId.toString(), body: '' }).unwrap(); navigate(`/`); } catch (e) { showDeleteErrorToast(e as unknown as Error); diff --git a/src/pages/Dashboard/projectPageHeader.tsx b/src/pages/Dashboard/projectPageHeader.tsx index f204bb412..755f9c7a1 100644 --- a/src/pages/Dashboard/projectPageHeader.tsx +++ b/src/pages/Dashboard/projectPageHeader.tsx @@ -22,6 +22,7 @@ import { Counters } from './Counters'; import { EditableDescription } from './EditableDescription'; import { EditableTitle } from './EditableTitle'; import { DeleteProjectModal } from './Modals/DeleteProjectModal'; +import { useProjectPlans } from './hooks/useProjectPlans'; const StyledPageHeaderMeta = styled(PageHeader.Meta)` justify-content: space-between; @@ -53,6 +54,10 @@ export const ProjectPageHeader = ({ projectId }: { projectId: number }) => { const templatesRoute = useLocalizeRoute('templates'); const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const { items: plans, isLoading: isLoadingPlans } = useProjectPlans({ + projectId: projectId || 0, + }); + const { isLoading, isFetching, @@ -88,14 +93,20 @@ export const ProjectPageHeader = ({ projectId }: { projectId: number }) => { - {isLoading || isFetching || status === 'loading' ? ( + {isLoading || + isLoadingPlans || + isFetching || + status === 'loading' ? ( ) : ( titleContent )} - {isLoading || isFetching || status === 'loading' ? ( + {isLoading || + isLoadingPlans || + isFetching || + status === 'loading' ? ( ) : ( descriptionContent @@ -117,7 +128,7 @@ export const ProjectPageHeader = ({ projectId }: { projectId: number }) => { {t('__DASHBOARD_CTA_NEW_ACTIVITY')} )} - {project?.campaigns_count === 0 && ( + {project?.campaigns_count === 0 && plans.length === 0 && ( setDeleteModalOpen(true)}> @@ -127,7 +138,7 @@ export const ProjectPageHeader = ({ projectId }: { projectId: number }) => { )} {deleteModalOpen && ( setDeleteModalOpen(false)} /> )} From 4375771717589c1f74228f71e5564e9ef4d67867 Mon Sep 17 00:00:00 2001 From: ZecD Date: Fri, 9 May 2025 09:00:45 +0200 Subject: [PATCH 20/49] fix(schema): remove unknown requestBody type from operations interface --- src/common/schema.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/common/schema.ts b/src/common/schema.ts index 648d23243..fc856551e 100644 --- a/src/common/schema.ts +++ b/src/common/schema.ts @@ -3140,9 +3140,7 @@ export interface operations { 405: components['responses']['Error']; 500: components['responses']['Error']; }; - requestBody: { - unknown; - }; + requestBody: {}; }; /** Update fields of a specific project. Currently only the project name is editable. */ 'patch-projects-pid': { From 17e21a849307147e5159859ee4adf96a8bbfd832 Mon Sep 17 00:00:00 2001 From: iacopolea Date: Fri, 9 May 2025 17:42:27 +0200 Subject: [PATCH 21/49] feat: Add data-qa attribute to invite users button and update visibility tests --- src/pages/Bugs/PageHeader/Tools/index.tsx | 8 ++-- src/pages/Campaign/pageHeader/Meta/index.tsx | 2 +- src/pages/Dashboard/projectPageHeader.tsx | 2 +- src/pages/Insights/PageHeader.tsx | 7 +++- src/pages/Videos/Metas.tsx | 4 +- tests/e2e/campaign.spec.ts | 40 +++++++++++++++++++ ...shboard.spec.ts => dashboard_home.spec.ts} | 2 +- tests/e2e/insights.spec.ts | 22 ++++++++++ tests/e2e/project.spec.ts | 26 ++++++++++++ tests/fixtures/DashboardsBase.ts | 29 ++++++++++++++ tests/fixtures/pages/Campaign.ts | 23 +++++++++++ .../pages/{Dashboard.ts => DashboardHome.ts} | 0 tests/fixtures/pages/Insights.ts | 3 +- tests/fixtures/pages/Project.ts | 4 ++ 14 files changed, 162 insertions(+), 10 deletions(-) create mode 100644 tests/e2e/campaign.spec.ts rename tests/e2e/{dashboard.spec.ts => dashboard_home.spec.ts} (98%) create mode 100644 tests/fixtures/DashboardsBase.ts create mode 100644 tests/fixtures/pages/Campaign.ts rename tests/fixtures/pages/{Dashboard.ts => DashboardHome.ts} (100%) diff --git a/src/pages/Bugs/PageHeader/Tools/index.tsx b/src/pages/Bugs/PageHeader/Tools/index.tsx index 52e1f3940..1c820ad3c 100644 --- a/src/pages/Bugs/PageHeader/Tools/index.tsx +++ b/src/pages/Bugs/PageHeader/Tools/index.tsx @@ -14,6 +14,7 @@ import { CampaignSettings } from 'src/common/components/inviteUsers/campaignSett import { useGetCampaignsByCidQuery } from 'src/features/api'; import { UniqueBugsCounter } from './UniqueBugsCounter'; import { useCampaignBugs } from './useCampaignBugs'; +import { useCanAccessToActiveWorkspace } from 'src/hooks/useCanAccessToActiveWorkspace'; const ButtonsWrapper = styled.div` display: flex; @@ -51,6 +52,7 @@ export const Tools = ({ customerTitle: string; }) => { const { t } = useTranslation(); + const hasWorksPacePermission = useCanAccessToActiveWorkspace(); const integrationCenterUrl = getLocalizeIntegrationCenterRoute(campaignId); const { isCampaignLoading, @@ -90,9 +92,9 @@ export const Tools = ({ {status && } - {campaignData && campaignData.isArchived !== true && ( - - )} + {campaignData && + campaignData.isArchived !== true && + hasWorksPacePermission && } ) : null} From 95321820f80ab77918b197e076e595921be92e30 Mon Sep 17 00:00:00 2001 From: Davide Bizzi Date: Mon, 12 May 2025 17:39:42 +0200 Subject: [PATCH 28/49] feat: Add accessibility task --- src/assets/icons/accessibility-task-icon.svg | 3 + src/common/schema.ts | 108 ++++++++---------- src/features/api/index.ts | 50 +++++--- src/locales/en/translation.json | 5 + src/locales/it/translation.json | 5 + src/pages/Plan/modules/Tasks/hooks/index.ts | 10 ++ .../Plan/modules/Tasks/parts/TaskItem.tsx | 16 +-- .../Tasks/parts/modal/AccessibilityTasks.tsx | 37 ++++++ .../modules/Tasks/parts/modal/TasksModal.tsx | 47 ++++---- .../modules/Tasks/utils/getIconFromTask.tsx | 3 + 10 files changed, 177 insertions(+), 107 deletions(-) create mode 100644 src/assets/icons/accessibility-task-icon.svg create mode 100644 src/pages/Plan/modules/Tasks/parts/modal/AccessibilityTasks.tsx diff --git a/src/assets/icons/accessibility-task-icon.svg b/src/assets/icons/accessibility-task-icon.svg new file mode 100644 index 000000000..a1a3193ff --- /dev/null +++ b/src/assets/icons/accessibility-task-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/common/schema.ts b/src/common/schema.ts index 2cc0ea7d9..75eee33f2 100644 --- a/src/common/schema.ts +++ b/src/common/schema.ts @@ -343,6 +343,7 @@ export interface paths { '/projects/{pid}': { /** Retrieve projects details from an ID. */ get: operations['get-projects-projectId']; + delete: operations['delete-projects-projectId']; /** Update fields of a specific project. Currently only the project name is editable. */ patch: operations['patch-projects-pid']; parameters: { @@ -446,26 +447,6 @@ export interface paths { }; }; }; - '/videos/{vid}/sentiment': { - /** - * This endpoint generates a new sentiment for the provided video if it does not already exist. - * - * **Security**: Requires Bearer Authentication. Provide your bearer token in the Authorization header when making requests to protected resources. Example: Authorization: Bearer 123. - * - * **Path Parameters**: - * - * vid (string, required): The ID of the video for which the translation is to be generated. - * Request Body (application/json): - * - * language (string, required): The language code for the desired translation. - */ - post: operations['post-videos-vid-sentiment']; - parameters: { - path: { - vid: string; - }; - }; - }; '/workspaces': { get: operations['get-workspaces']; /** This endpoint is useful to add a new workspace. Only admin can use this. */ @@ -895,6 +876,16 @@ export interface components { usecaseTitle: string; })[]; }; + MediaSentiment: { + value: number; + reason: string; + paragraphs: { + start: number; + end: number; + value: number; + reason: string; + }[]; + }; Module: | components['schemas']['ModuleTitle'] | components['schemas']['ModuleDate'] @@ -1236,6 +1227,7 @@ export interface components { }; }; transcript?: components['schemas']['Transcript']; + sentiment?: components['schemas']['MediaSentiment']; }; /** VideoTag */ VideoTag: { @@ -1466,7 +1458,10 @@ export interface components { * BannerType * @enum {string} */ - BannerType: 'banner_testing_automation' | 'banner_user_experience'; + BannerType: + | 'banner_testing_automation' + | 'banner_user_experience' + | 'banner_cyber_security'; /** CpReqTemplate */ CpReqTemplate: { id: number; @@ -1528,13 +1523,23 @@ export interface components { /** Format: uri */ url?: string; }; + /** OutputModuleTaskAccessibility */ + OutputModuleTaskAccessibility: { + /** @enum {string} */ + kind: 'accessibility'; + title: string; + description?: string; + /** Format: uri */ + url?: string; + }; /** SubcomponentTask */ OutputModuleTask: | components['schemas']['OutputModuleTaskVideo'] | components['schemas']['OutputModuleTaskBug'] | components['schemas']['OutputModuleTaskSurvey'] | components['schemas']['OutputModuleTaskModerateVideo'] - | components['schemas']['OutputModuleTaskExplorativeBug']; + | components['schemas']['OutputModuleTaskExplorativeBug'] + | components['schemas']['OutputModuleTaskAccessibility']; /** SubcomponentTouchpoints */ OutputModuleTouchpoints: | components['schemas']['OutputModuleTouchpointsAppDesktop'] @@ -1956,6 +1961,11 @@ export interface operations { }[]; siblings: number; comments: number; + additional_fields?: { + slug: string; + value: string; + name: string; + }[]; })[]; start?: number; limit?: number; @@ -3132,6 +3142,23 @@ export interface operations { 500: components['responses']['Error']; }; }; + 'delete-projects-projectId': { + parameters: { + path: { + /** Project id */ + pid: components['parameters']['pid']; + }; + }; + responses: { + /** OK */ + 200: unknown; + 400: components['responses']['Error']; + 401: components['responses']['Error']; + 403: components['responses']['Error']; + 405: components['responses']['Error']; + 500: components['responses']['Error']; + }; + }; /** Update fields of a specific project. Currently only the project name is editable. */ 'patch-projects-pid': { parameters: { @@ -3547,41 +3574,6 @@ export interface operations { }; }; }; - /** - * This endpoint generates a new sentiment for the provided video if it does not already exist. - * - * **Security**: Requires Bearer Authentication. Provide your bearer token in the Authorization header when making requests to protected resources. Example: Authorization: Bearer 123. - * - * **Path Parameters**: - * - * vid (string, required): The ID of the video for which the translation is to be generated. - * Request Body (application/json): - * - * language (string, required): The language code for the desired translation. - */ - 'post-videos-vid-sentiment': { - parameters: { - path: { - vid: string; - }; - }; - responses: { - /** OK */ - 200: { - content: { - 'application/json': { [key: string]: unknown }; - }; - }; - 400: components['responses']['Error']; - 403: components['responses']['Error']; - 500: components['responses']['Error']; - }; - requestBody: { - content: { - 'application/json': { [key: string]: unknown }; - }; - }; - }; 'get-workspaces': { parameters: { query: { @@ -3973,7 +3965,7 @@ export interface operations { /** Start pagination parameter */ start?: components['parameters']['start']; /** Orders results */ - orderBy?: 'updated_at' | 'id'; + orderBy?: 'updated_at' | 'id' | 'order'; /** Order value (ASC, DESC) */ order?: components['parameters']['order']; /** filterBy[]= */ diff --git a/src/features/api/index.ts b/src/features/api/index.ts index 20ac6e677..a95c87b53 100644 --- a/src/features/api/index.ts +++ b/src/features/api/index.ts @@ -435,6 +435,15 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.body, }), }), + deleteProjectsByPid: build.mutation< + DeleteProjectsByPidApiResponse, + DeleteProjectsByPidApiArg + >({ + query: (queryArg) => ({ + url: `/projects/${queryArg.pid}`, + method: 'DELETE', + }), + }), getProjectsByPidCampaigns: build.query< GetProjectsByPidCampaignsApiResponse, GetProjectsByPidCampaignsApiArg @@ -572,16 +581,6 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.body, }), }), - postVideosByVidSentiment: build.mutation< - PostVideosByVidSentimentApiResponse, - PostVideosByVidSentimentApiArg - >({ - query: (queryArg) => ({ - url: `/videos/${queryArg.vid}/sentiment`, - method: 'POST', - body: queryArg.body, - }), - }), getWorkspaces: build.query({ query: (queryArg) => ({ url: `/workspaces`, @@ -882,6 +881,11 @@ export type GetCampaignsByCidBugsApiResponse = /** status 200 OK */ { }[]; siblings: number; comments: number; + additional_fields?: { + slug: string; + value: string; + name: string; + }[]; })[]; start?: number; limit?: number; @@ -1482,6 +1486,11 @@ export type PatchProjectsByPidApiArg = { description: string; }; }; +export type DeleteProjectsByPidApiResponse = /** status 200 OK */ void; +export type DeleteProjectsByPidApiArg = { + /** Project id */ + pid: string; +}; export type GetProjectsByPidCampaignsApiResponse = /** status 200 OK */ { items?: CampaignWithOutput[]; start?: number; @@ -1640,11 +1649,6 @@ export type PostVideosByVidTranslationApiArg = { language: string; }; }; -export type PostVideosByVidSentimentApiResponse = /** status 200 OK */ object; -export type PostVideosByVidSentimentApiArg = { - vid: string; - body: object; -}; export type GetWorkspacesApiResponse = /** status 200 OK */ { items?: Workspace[]; start?: number; @@ -2247,7 +2251,10 @@ export type Report = { creation_date?: string; update_date?: string; }; -export type BannerType = 'banner_testing_automation' | 'banner_user_experience'; +export type BannerType = + | 'banner_testing_automation' + | 'banner_user_experience' + | 'banner_cyber_security'; export type Tenant = { /** tryber wp_user_id */ id: number; @@ -2505,12 +2512,19 @@ export type OutputModuleTaskExplorativeBug = { description?: string; url?: string; }; +export type OutputModuleTaskAccessibility = { + kind: 'accessibility'; + title: string; + description?: string; + url?: string; +}; export type SubcomponentTask = | SubcomponentTaskVideo | SubcomponentTaskBug | SubcomponentTaskSurvey | OutputModuleTaskModerateVideo - | OutputModuleTaskExplorativeBug; + | OutputModuleTaskExplorativeBug + | OutputModuleTaskAccessibility; export type ModuleTask = { type: 'tasks'; variant: string; @@ -2773,6 +2787,7 @@ export const { usePostProjectsMutation, useGetProjectsByPidQuery, usePatchProjectsByPidMutation, + useDeleteProjectsByPidMutation, useGetProjectsByPidCampaignsQuery, useGetProjectsByPidUsersQuery, usePostProjectsByPidUsersMutation, @@ -2788,7 +2803,6 @@ export const { useDeleteVideosByVidObservationsAndOidMutation, useGetVideosByVidTranslationQuery, usePostVideosByVidTranslationMutation, - usePostVideosByVidSentimentMutation, useGetWorkspacesQuery, usePostWorkspacesMutation, useGetWorkspacesByWidQuery, diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index e9c5b7428..80d91140a 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1116,7 +1116,12 @@ "__PLAN_PAGE_MODULE_TARGET_PLACEHOLDER": "Example: 8", "__PLAN_PAGE_MODULE_TARGET_REMOVE_BUTTON": "Delete", "__PLAN_PAGE_MODULE_TARGET_TITLE": "Target Size", + "__PLAN_PAGE_MODULE_TASKS_ACCESSIBILITY_TASK_ACCESSIBILITY_DESCRIPTION_DEFAULT": "Partecipants will test your product without defined tasks.", + "__PLAN_PAGE_MODULE_TASKS_ACCESSIBILITY_TASK_ACCESSIBILITY_TITLE_DEFAULT": "Accessibility", "__PLAN_PAGE_MODULE_TASKS_ADD_TASK_BUTTON": "Add task", + "__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_ACCESSIBILITY_TAB": "Accessibility", + "__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_ACCESSIBILITY_TASK_ACCESSIBILITY_BUTTON": "Accessibility Task", + "__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_ACCESSIBILITY_TASKS_LABEL": "Accessibility", "__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_DEFAULT_TAB": "All", "__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_EXPERIENTIAL_TAB": "Experience", "__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_EXPERIENTIAL_TASK_THINKING_ALOUD_BUTTON": "Thinking aloud task", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 8c1c448bd..403b22afd 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -1145,7 +1145,12 @@ "__PLAN_PAGE_MODULE_TARGET_PLACEHOLDER": "", "__PLAN_PAGE_MODULE_TARGET_REMOVE_BUTTON": "", "__PLAN_PAGE_MODULE_TARGET_TITLE": "", + "__PLAN_PAGE_MODULE_TASKS_ACCESSIBILITY_TASK_ACCESSIBILITY_DESCRIPTION_DEFAULT": "", + "__PLAN_PAGE_MODULE_TASKS_ACCESSIBILITY_TASK_ACCESSIBILITY_TITLE_DEFAULT": "", "__PLAN_PAGE_MODULE_TASKS_ADD_TASK_BUTTON": "", + "__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_ACCESSIBILITY_TAB": "", + "__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_ACCESSIBILITY_TASK_ACCESSIBILITY_BUTTON": "", + "__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_ACCESSIBILITY_TASKS_LABEL": "", "__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_DEFAULT_TAB": "", "__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_EXPERIENTIAL_TAB": "", "__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_EXPERIENTIAL_TASK_THINKING_ALOUD_BUTTON": "", diff --git a/src/pages/Plan/modules/Tasks/hooks/index.ts b/src/pages/Plan/modules/Tasks/hooks/index.ts index 964f14a44..e72013bc5 100644 --- a/src/pages/Plan/modules/Tasks/hooks/index.ts +++ b/src/pages/Plan/modules/Tasks/hooks/index.ts @@ -108,6 +108,11 @@ const useModuleTasks = () => { return t( '__PLAN_PAGE_MODULE_TASKS_FUNCTIONAL_TASK_EXPLORATORY_TITLE_DEFAULT' ); + + if (kind === 'accessibility') + return t( + '__PLAN_PAGE_MODULE_TASKS_ACCESSIBILITY_TASK_ACCESSIBILITY_TITLE_DEFAULT' + ); if (kind === 'moderate-video' && fill) return t( '__PLAN_PAGE_MODULE_TASKS_EXPERIENTIAL_TASK_MODERATE_TITLE_DEFAULT' @@ -118,6 +123,7 @@ const useModuleTasks = () => { ); if (kind === 'survey' && fill) return t('__PLAN_PAGE_MODULE_TASKS_SURVEY_TASK_SURVEY_TITLE_DEFAULT'); + return ''; } @@ -143,6 +149,10 @@ const useModuleTasks = () => { return t( '__PLAN_PAGE_MODULE_TASKS_SURVEY_TASK_SURVEY_DESCRIPTION_DEFAULT' ); + if (kind === 'accessibility' && fill) + return t( + '__PLAN_PAGE_MODULE_TASKS_ACCESSIBILITY_TASK_ACCESSIBILITY_DESCRIPTION_DEFAULT' + ); return ''; } diff --git a/src/pages/Plan/modules/Tasks/parts/TaskItem.tsx b/src/pages/Plan/modules/Tasks/parts/TaskItem.tsx index 32fb7757a..462f38375 100644 --- a/src/pages/Plan/modules/Tasks/parts/TaskItem.tsx +++ b/src/pages/Plan/modules/Tasks/parts/TaskItem.tsx @@ -6,16 +6,16 @@ import { Input, Label, MD, - Message, - Span, MediaInput, + Message, Paragraph, + Span, } from '@appquality/unguess-design-system'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { appTheme } from 'src/app/theme'; -import { ReactComponent as TrashIcon } from 'src/assets/icons/trash-stroke.svg'; import { ReactComponent as LinkIcon } from 'src/assets/icons/link-stroke.svg'; +import { ReactComponent as TrashIcon } from 'src/assets/icons/trash-stroke.svg'; import { components } from 'src/common/schema'; import { useModuleConfiguration } from 'src/features/modules/useModuleConfiguration'; import { useModuleTasks } from '../hooks'; @@ -59,6 +59,8 @@ const TaskItem = ({ validate(); }; + const isSimpleInterface = ['explorative-bug', 'accessibility'].includes(kind); + return ( <>
- {kind !== 'explorative-bug' && ( + {!isSimpleInterface && (