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/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/assets/banner_suggestions/cyber.svg b/src/assets/banner_suggestions/cyber.svg new file mode 100644 index 000000000..675c63470 --- /dev/null +++ b/src/assets/banner_suggestions/cyber.svg @@ -0,0 +1,234 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/banner_suggestions/experience.svg b/src/assets/banner_suggestions/experience.svg index fbe6599bb..01a34681c 100644 --- a/src/assets/banner_suggestions/experience.svg +++ b/src/assets/banner_suggestions/experience.svg @@ -1,428 +1,339 @@ - - - - + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - + + + + + + + + + + + + + + + + - - - + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - + + + + + + + + + + + + + - + - - + + - - + + - + - + - - + + - + - + - + - + - - + + - + - + - + - + - - + + - + - + - + - + - - + + - + - + - - - - - - - - - - + - + - - + + - + - + - + - - - - - - + + + + + - - - + + + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - + - - - - - - - - - - - - - - - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/src/assets/banner_suggestions/testing_automation.svg b/src/assets/banner_suggestions/testing_automation.svg deleted file mode 100644 index eb628809d..000000000 --- a/src/assets/banner_suggestions/testing_automation.svg +++ /dev/null @@ -1,331 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 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/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) => ( ))} - - - + + + )} 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..254c0a77f 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 ? ( @@ -109,7 +107,7 @@ export const UserItem = ({ }} > {user.invitationPending && ( - + {t('__WORKSPACE_SETTINGS_MEMBER_RESEND_INVITE_ACTION')} )} 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/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..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/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..44db1ea32 --- /dev/null +++ b/src/features/navigation/Navigation/index.tsx @@ -0,0 +1,95 @@ +import { Content } from '@appquality/unguess-design-system'; +import { ComponentProps, useEffect, useMemo } 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(); + + const parameter = useMemo(() => { + if (!params) return ''; + return Object.keys(params) + .filter((key) => key !== 'language') + .map((key) => params[`${key}`] ?? '') + .join(''); + }, [params]); + + 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/locales/en/translation.json b/src/locales/en/translation.json index e9c5b7428..8c57b5aab 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -40,19 +40,12 @@ "__ASIDE_NAVIGATION_MODULE_TARGET_SUBTITLE": "Participant number", "__ASIDE_NAVIGATION_MODULE_TASKS_SUBTITLE": " ", "__ASIDE_NAVIGATION_MODULE_TOUCHPOINTS_SUBTITLE": "Interaction points", - "__BANNER_CROSS_FUNCTIONAL_CTA_AUTOMATION": "Get in touch", - "__BANNER_CROSS_FUNCTIONAL_CTA_EXPERIENCE": "Get in touch", - "__BANNER_CROSS_FUNCTIONAL_CTA_LOADING": "sending...", - "__BANNER_CROSS_FUNCTIONAL_MESSAGE_AUTOMATION": "Try out our testing automation services", - "__BANNER_CROSS_FUNCTIONAL_MESSAGE_AUTOMATION_ANCHOR": "Discover more", - "__BANNER_CROSS_FUNCTIONAL_MESSAGE_EXPERIENCE": "Try out User Testing", + "__BANNER_CROSS_CTA": "Try out", + "__BANNER_CROSS_FUNCTIONAL_MESSAGE_CYBER": "Identify security flaws before hackers do", + "__BANNER_CROSS_FUNCTIONAL_MESSAGE_EXPERIENCE": "Experience our starter usability package", "__BANNER_CROSS_FUNCTIONAL_MESSAGE_EXPERIENCE_ANCHOR": "Discover more", - "__BANNER_CROSS_FUNCTIONAL_TITLE_AUTOMATION": "Optimize your testing efforts:", - "__BANNER_CROSS_FUNCTIONAL_TITLE_EXPERIENCE": "Fix usability issues ahead of time:", - "__BANNER_CROSS_FUNCTIONAL_TOAST_ERROR": "Something went wrong, please try again later.", - "__BANNER_CROSS_FUNCTIONAL_TOAST_SUCCESS": "Email sent, our team will contact you soon", - "__BANNER_CROSS_TOAST_ERROR": "Something went wrong, please try again later.", - "__BANNER_CROSS_TOAST_SUCCESS": "Email sent, our team will contact you soon", + "__BANNER_CROSS_FUNCTIONAL_TITLE_CYBER": "Make your product bulletproof:", + "__BANNER_CROSS_FUNCTIONAL_TITLE_EXPERIENCE": "Validate your UX:", "__BREADCRUMB_ITEM_SERVICES": "Services", "__BROWSER_ERROR_REQUIRED": "No browser selected: Choose at least one browser to continue", "__BUG_COMMENTS_CHAT_BOLD": "Bold", @@ -345,6 +338,8 @@ "__CAMPAIGN_EXP_WIDGET_SENTIMENT_LIST_SENTIMENT_LABEL": "Sentiment", "__CAMPAIGN_EXP_WIDGET_SENTIMENT_LIST_USECASE_LABEL_one": "Use Case (tot. {{count}})", "__CAMPAIGN_EXP_WIDGET_SENTIMENT_LIST_USECASE_LABEL_other": "Use Cases (tot. {{count}})", + "__CAMPAIGN_PAGE_ADDITIONAL_SECTION_SUBTITLE": "Advanced metrics for more precise and impactful analysis", + "__CAMPAIGN_PAGE_ADDITIONAL_SECTION_TITLE": "Focus on Details", "__CAMPAIGN_PAGE_ARCHIVE_CAMPAIGN_MODAL_BUTTON_CANCEL": "Cancel", "__CAMPAIGN_PAGE_ARCHIVE_CAMPAIGN_MODAL_BUTTON_CONFIRM": "Confirm", "__CAMPAIGN_PAGE_ARCHIVE_CAMPAIGN_MODAL_DESCRIPTION_1": "If you archive the activity, it will no longer appear in the active project but will remain accessible in the archive.", @@ -419,6 +414,7 @@ "__CAMPAIGN_PAGE_NAVIGATION_BUG_EXTERNAL_LINK_LABEL": "Go to bug list", "__CAMPAIGN_PAGE_NAVIGATION_BUG_GROUP_DETAILS_LABEL": "INSIGHTS", "__CAMPAIGN_PAGE_NAVIGATION_BUG_GROUP_OTHER_LABEL": "DOWNLOAD", + "__CAMPAIGN_PAGE_NAVIGATION_BUG_ITEM_ADDITIONALS_LABEL": "Other details", "__CAMPAIGN_PAGE_NAVIGATION_BUG_ITEM_DETAILS_DEVICES_LABEL": "Devices and types", "__CAMPAIGN_PAGE_NAVIGATION_BUG_ITEM_DETAILS_UNIQUE_BUGS_LABEL": "Unique bugs distribution", "__CAMPAIGN_PAGE_NAVIGATION_BUG_ITEM_OTHER_REPORTS_LABEL": "Reports & attachments", @@ -446,11 +442,11 @@ "__CAMPAIGN_PAGE_REPORTS_GENERATE_REPORT_CARD_BUTTON_LABEL": "Download", "__CAMPAIGN_PAGE_REPORTS_GENERATE_REPORT_CARD_META": "Bugs Report", "__CAMPAIGN_PAGE_REPORTS_TITLE": "Reports & attachments", - "__CAMPAIGN_PAGE_SUGGESTIONS_AUTOMATION_CONTENT": "Optimize testing efforts", - "__CAMPAIGN_PAGE_SUGGESTIONS_AUTOMATION_HEADER": "Explore automation services", - "__CAMPAIGN_PAGE_SUGGESTIONS_AUTOMATION_TAG": "New In!", - "__CAMPAIGN_PAGE_SUGGESTIONS_CTA": "Get in touch", + "__CAMPAIGN_PAGE_SUGGESTIONS_CTA": "Try Out", "__CAMPAIGN_PAGE_SUGGESTIONS_CTA_LOADING": "sending...", + "__CAMPAIGN_PAGE_SUGGESTIONS_CYBER_CONTENT": "Strengthen your digital defense", + "__CAMPAIGN_PAGE_SUGGESTIONS_CYBER_HEADER": "Explore security services", + "__CAMPAIGN_PAGE_SUGGESTIONS_CYBER_TAG": "New in!", "__CAMPAIGN_PAGE_SUGGESTIONS_EXPERIENCE_CONTENT": "Address Usability, early on", "__CAMPAIGN_PAGE_SUGGESTIONS_EXPERIENCE_HEADER": "Try out Usability Test", "__CAMPAIGN_PAGE_SUGGESTIONS_EXPERIENCE_TAG": "Suggested", @@ -459,6 +455,8 @@ "__CAMPAIGN_PAGE_UNIQUE_BUGS_TITLE": "Unique bugs", "__CAMPAIGN_PAGE_UPDATE_CAMPAIGN_NAME_ERROR": "Error", "__CAMPAIGN_PAGE_UX_QUESTION_ACCORDION_TITLE": "The questions that shaped our research", + "__CAMPAIGN_PAGE_WIDGET_BUGS_BY_ADDITIONAL_CHART_HEADER": "Tot. bugs", + "__CAMPAIGN_PAGE_WIDGET_BUGS_BY_ADDITIONAL_TOOLTIP_UNIQUE_BUGS_LABEL": "Unique bugs:", "__CAMPAIGN_PAGE_WIDGET_BUGS_BY_DEVICE_CHART_HEADER": "Tot. bugs", "__CAMPAIGN_PAGE_WIDGET_BUGS_BY_DEVICE_CHART_TOOLTIP_DRILLDOWN": "👉 Drill down to:", "__CAMPAIGN_PAGE_WIDGET_BUGS_BY_DEVICE_CHART_TOOLTIP_VALUE": "Bugs: {{value}}", @@ -491,6 +489,10 @@ "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_COUNT_LABEL_one": "{{count}} unique bug", "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_COUNT_LABEL_many": "{{count}} unique bugs", "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_COUNT_LABEL_other": "{{count}} unique bugs", + "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_ONLY_COUNT_LABEL_one": "total bug", + "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_ONLY_COUNT_LABEL_other": "total bugs", + "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_ONLY_HEADER": "Total bugs", + "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_ONLY_TOOLTIP": "Monitor the total number of unique bugs identified during the testers' activity, that is, individual issues that were reported as separate problems during the test sessions.", "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_REPORTED_BY": "Reported by testers", "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_TOOLTIP": "Monitor unique bugs reported only once by a single tester or a series of bugs reported by different testers, which we organize into duplicate bug groups", "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_TOTAL_LABEL": "out of {{ total }} total", @@ -1116,7 +1118,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", @@ -1285,6 +1292,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.", @@ -1445,7 +1457,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", diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 8c1c448bd..5da5fc760 100644 --- a/src/locales/it/translation.json +++ b/src/locales/it/translation.json @@ -40,19 +40,12 @@ "__ASIDE_NAVIGATION_MODULE_TARGET_SUBTITLE": "", "__ASIDE_NAVIGATION_MODULE_TASKS_SUBTITLE": "", "__ASIDE_NAVIGATION_MODULE_TOUCHPOINTS_SUBTITLE": "", - "__BANNER_CROSS_FUNCTIONAL_CTA_AUTOMATION": "Contattaci", - "__BANNER_CROSS_FUNCTIONAL_CTA_EXPERIENCE": "Contattaci", - "__BANNER_CROSS_FUNCTIONAL_CTA_LOADING": "invio in corso...", - "__BANNER_CROSS_FUNCTIONAL_MESSAGE_AUTOMATION": "Prova i nostri servizi di test automation", - "__BANNER_CROSS_FUNCTIONAL_MESSAGE_AUTOMATION_ANCHOR": "Scopri di più", + "__BANNER_CROSS_CTA": "", + "__BANNER_CROSS_FUNCTIONAL_MESSAGE_CYBER": "", "__BANNER_CROSS_FUNCTIONAL_MESSAGE_EXPERIENCE": "Prova il Test di Usabilità", "__BANNER_CROSS_FUNCTIONAL_MESSAGE_EXPERIENCE_ANCHOR": "Scopri di più", - "__BANNER_CROSS_FUNCTIONAL_TITLE_AUTOMATION": "Ottimizza i tuoi sforzi di test:", + "__BANNER_CROSS_FUNCTIONAL_TITLE_CYBER": "", "__BANNER_CROSS_FUNCTIONAL_TITLE_EXPERIENCE": "Risolvi i problemi di usabilità prima del tempo:", - "__BANNER_CROSS_FUNCTIONAL_TOAST_ERROR": "Qualcosa è andato storto, riprova più tardi.", - "__BANNER_CROSS_FUNCTIONAL_TOAST_SUCCESS": "Email inviata, il nostro team ti contatterà presto", - "__BANNER_CROSS_TOAST_ERROR": "Qualcosa è andato storto, riprova più tardi.", - "__BANNER_CROSS_TOAST_SUCCESS": "Email inviata, il nostro team ti contatterà presto", "__BREADCRUMB_ITEM_SERVICES": "Servizi", "__BROWSER_ERROR_REQUIRED": "", "__BUG_COMMENTS_CHAT_BOLD": "Grassetto", @@ -364,6 +357,8 @@ "__CAMPAIGN_EXP_WIDGET_SENTIMENT_LIST_USECASE_LABEL_one": "Use Case (tot. {{count}})", "__CAMPAIGN_EXP_WIDGET_SENTIMENT_LIST_USECASE_LABEL_many": "", "__CAMPAIGN_EXP_WIDGET_SENTIMENT_LIST_USECASE_LABEL_other": "Use Case (tot. {{count}})", + "__CAMPAIGN_PAGE_ADDITIONAL_SECTION_SUBTITLE": "", + "__CAMPAIGN_PAGE_ADDITIONAL_SECTION_TITLE": "", "__CAMPAIGN_PAGE_ARCHIVE_CAMPAIGN_MODAL_BUTTON_CANCEL": "", "__CAMPAIGN_PAGE_ARCHIVE_CAMPAIGN_MODAL_BUTTON_CONFIRM": "", "__CAMPAIGN_PAGE_ARCHIVE_CAMPAIGN_MODAL_DESCRIPTION_1": "", @@ -441,6 +436,7 @@ "__CAMPAIGN_PAGE_NAVIGATION_BUG_EXTERNAL_LINK_LABEL": "Vai alla lista bug", "__CAMPAIGN_PAGE_NAVIGATION_BUG_GROUP_DETAILS_LABEL": "APPROFONDISCI", "__CAMPAIGN_PAGE_NAVIGATION_BUG_GROUP_OTHER_LABEL": "NEL DETTAGLIO", + "__CAMPAIGN_PAGE_NAVIGATION_BUG_ITEM_ADDITIONALS_LABEL": "", "__CAMPAIGN_PAGE_NAVIGATION_BUG_ITEM_DETAILS_DEVICES_LABEL": "Dispositivi e tipologie bug", "__CAMPAIGN_PAGE_NAVIGATION_BUG_ITEM_DETAILS_UNIQUE_BUGS_LABEL": "Distribuzione bug unici", "__CAMPAIGN_PAGE_NAVIGATION_BUG_ITEM_OTHER_REPORTS_LABEL": "Report & documenti", @@ -468,11 +464,11 @@ "__CAMPAIGN_PAGE_REPORTS_GENERATE_REPORT_CARD_BUTTON_LABEL": "Scarica", "__CAMPAIGN_PAGE_REPORTS_GENERATE_REPORT_CARD_META": "Report Bug", "__CAMPAIGN_PAGE_REPORTS_TITLE": "Report & documenti", - "__CAMPAIGN_PAGE_SUGGESTIONS_AUTOMATION_CONTENT": "Ottimizza gli sforzi di test", - "__CAMPAIGN_PAGE_SUGGESTIONS_AUTOMATION_HEADER": "Esplora i servizi di automazione", - "__CAMPAIGN_PAGE_SUGGESTIONS_AUTOMATION_TAG": "Novità!", "__CAMPAIGN_PAGE_SUGGESTIONS_CTA": "Contattaci", "__CAMPAIGN_PAGE_SUGGESTIONS_CTA_LOADING": "invio in corso...", + "__CAMPAIGN_PAGE_SUGGESTIONS_CYBER_CONTENT": "", + "__CAMPAIGN_PAGE_SUGGESTIONS_CYBER_HEADER": "", + "__CAMPAIGN_PAGE_SUGGESTIONS_CYBER_TAG": "", "__CAMPAIGN_PAGE_SUGGESTIONS_EXPERIENCE_CONTENT": "Affronta l'usabilità fin dall'inizio", "__CAMPAIGN_PAGE_SUGGESTIONS_EXPERIENCE_HEADER": "Prova il test di usabilità", "__CAMPAIGN_PAGE_SUGGESTIONS_EXPERIENCE_TAG": "Consigliato", @@ -481,6 +477,8 @@ "__CAMPAIGN_PAGE_UNIQUE_BUGS_TITLE": "Bug unici", "__CAMPAIGN_PAGE_UPDATE_CAMPAIGN_NAME_ERROR": "Errore", "__CAMPAIGN_PAGE_UX_QUESTION_ACCORDION_TITLE": "Le domande che hanno guidato la nostra ricerca", + "__CAMPAIGN_PAGE_WIDGET_BUGS_BY_ADDITIONAL_CHART_HEADER": "Tot. bugs", + "__CAMPAIGN_PAGE_WIDGET_BUGS_BY_ADDITIONAL_TOOLTIP_UNIQUE_BUGS_LABEL": "", "__CAMPAIGN_PAGE_WIDGET_BUGS_BY_DEVICE_CHART_HEADER": "Tot. bug", "__CAMPAIGN_PAGE_WIDGET_BUGS_BY_DEVICE_CHART_TOOLTIP_DRILLDOWN": "👉 Visualizza dettaglio di:", "__CAMPAIGN_PAGE_WIDGET_BUGS_BY_DEVICE_CHART_TOOLTIP_VALUE": "Bug: {{value}}", @@ -513,6 +511,11 @@ "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_COUNT_LABEL_one": "{{count}} bug unico", "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_COUNT_LABEL_many": "{{count}} bug unici", "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_COUNT_LABEL_other": "{{count}} bug unici", + "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_ONLY_COUNT_LABEL_one": "total bugs", + "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_ONLY_COUNT_LABEL_many": "total bugs", + "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_ONLY_COUNT_LABEL_other": "total bugs", + "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_ONLY_HEADER": "", + "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_ONLY_TOOLTIP": "", "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_REPORTED_BY": "I tester hanno segnalato", "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_TOOLTIP": "Monitora i bug univoci segnalati una sola volta da un solo tester. Oppure monitora una serie di bug segnalati da tester diversi, che noi organizziamo in gruppi di bug duplicati", "__CAMPAIGN_PAGE_WIDGET_UNIQUE_BUGS_TOTAL_LABEL": "su {{ total }} totali", @@ -1145,7 +1148,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": "", @@ -1314,6 +1322,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/Bug/Content.tsx b/src/pages/Bug/Content.tsx index fd2b021c6..096f2fe39 100644 --- a/src/pages/Bug/Content.tsx +++ b/src/pages/Bug/Content.tsx @@ -26,8 +26,8 @@ export const Content = ({ bug, campaignId }: Props) => ( - {bug.media && bug.media.length ? : null} + {bug.media && bug.media.length ? : null} diff --git a/src/pages/Bugs/Content/BugPreview.tsx b/src/pages/Bugs/Content/BugPreview.tsx index 359c41834..f992feef2 100644 --- a/src/pages/Bugs/Content/BugPreview.tsx +++ b/src/pages/Bugs/Content/BugPreview.tsx @@ -1,5 +1,7 @@ import { Skeleton } from '@appquality/unguess-design-system'; import { useEffect, useMemo, useRef } from 'react'; +import { createSearchParams } from 'react-router-dom'; +import { useAppSelector } from 'src/app/hooks'; import { AnchorButtons } from 'src/common/components/BugDetail/AnchorButtons'; import BugAttachments from 'src/common/components/BugDetail/Attachments'; import { BugDuplicates } from 'src/common/components/BugDetail/BugDuplicates'; @@ -18,8 +20,6 @@ import { getSelectedBugId, } from 'src/features/bugsPage/bugsPageSlice'; import styled from 'styled-components'; -import { useAppSelector } from 'src/app/hooks'; -import { createSearchParams } from 'react-router-dom'; import { BugCommentsDetail } from './components/BugCommentsDetails'; import BugHeader from './components/BugHeader'; import { BugPreviewContextProvider } from './context/BugPreviewContext'; @@ -206,6 +206,7 @@ export const BugPreview = ({ + {media && media.length ? : null} - {currentBugId && ( )} 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..0c9e491fc 100644 --- a/src/pages/Bugs/Content/BugsTable/components/Reccomendation.tsx +++ b/src/pages/Bugs/Content/BugsTable/components/Reccomendation.tsx @@ -1,27 +1,46 @@ -import { - GlobalAlert, - Anchor, - useToast, - Notification, -} from '@appquality/unguess-design-system'; -import { t } from 'i18next'; +import { GlobalAlert } from '@appquality/unguess-design-system'; 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 +54,17 @@ 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/Bugs/PageHeader/Tools/index.tsx b/src/pages/Bugs/PageHeader/Tools/index.tsx index 52e1f3940..33a5b345b 100644 --- a/src/pages/Bugs/PageHeader/Tools/index.tsx +++ b/src/pages/Bugs/PageHeader/Tools/index.tsx @@ -12,6 +12,7 @@ import { CampaignStatus } from 'src/types'; import { PageMeta } from 'src/common/components/PageMeta'; import { CampaignSettings } from 'src/common/components/inviteUsers/campaignSettings'; import { useGetCampaignsByCidQuery } from 'src/features/api'; +import { useCanAccessToActiveWorkspace } from 'src/hooks/useCanAccessToActiveWorkspace'; import { UniqueBugsCounter } from './UniqueBugsCounter'; import { useCampaignBugs } from './useCampaignBugs'; @@ -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} ); }; diff --git a/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugs/Trend.tsx b/src/pages/Campaign/useWidgets/Functional/widgets/Trend.tsx similarity index 75% rename from src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugs/Trend.tsx rename to src/pages/Campaign/useWidgets/Functional/widgets/Trend.tsx index a85c49502..66d23fd58 100644 --- a/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugs/Trend.tsx +++ b/src/pages/Campaign/useWidgets/Functional/widgets/Trend.tsx @@ -1,7 +1,8 @@ import { Span, Tag } from '@appquality/unguess-design-system'; -import { appTheme } from 'src/app/theme'; import { useTranslation } from 'react-i18next'; +import { appTheme } from 'src/app/theme'; import { ReactComponent as TrendIcon } from 'src/assets/icons/trend-icon.svg'; +import { useGetCampaignsByCidWidgetsQuery } from 'src/features/api'; const BasicTrendPill = ({ color, text }: { color: string; text: string }) => ( @@ -46,7 +47,20 @@ const NegativeTrendPill = ({ trend }: { trend: number }) => { ); }; -const TrendPill = ({ trend }: { trend: number }) => { +const TrendPill = ({ campaignId }: { campaignId: number }) => { + const { data, isLoading, isFetching, isError } = + useGetCampaignsByCidWidgetsQuery({ + cid: campaignId.toString(), + s: 'unique-bugs', + updateTrend: true, + }); + if (isLoading || isFetching || isError || !data) { + return null; + } + if (data.kind !== 'campaignUniqueBugs') { + return null; + } + const { trend } = data.data; if (trend > 0) { return ; } diff --git a/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugs/index.tsx b/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugs/index.tsx index 70c006d68..2c86f78e6 100644 --- a/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugs/index.tsx +++ b/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugs/index.tsx @@ -1,11 +1,11 @@ -import { WaffleChart, XL, Span } from '@appquality/unguess-design-system'; -import { appTheme } from 'src/app/theme'; +import { Span, WaffleChart, XL } from '@appquality/unguess-design-system'; import { Trans, useTranslation } from 'react-i18next'; +import { appTheme } from 'src/app/theme'; import { BasicWidget } from 'src/pages/Campaign/widgetCards/BasicWidget'; +import { TrendPill } from '../Trend'; +import { WidgetLoader } from '../widgetLoader'; import { useUniqueBugs } from './useUniqueBugs'; import WaffleTooltip from './WaffleTooltip'; -import { TrendPill } from './Trend'; -import { WidgetLoader } from '../widgetLoader'; const primaryTextColor = { color: appTheme.components.text.primaryColor, @@ -16,7 +16,6 @@ export const UniqueBugs = ({ campaignId }: { campaignId: number }) => { const { totalBugs, uniqueBugs, - trendBugs, uniquePercentage, isLoading, isFetching, @@ -74,7 +73,7 @@ export const UniqueBugs = ({ campaignId }: { campaignId: number }) => { )} - + ); diff --git a/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/Chart/index.tsx b/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/Chart/index.tsx new file mode 100644 index 000000000..ceb81476e --- /dev/null +++ b/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/Chart/index.tsx @@ -0,0 +1,81 @@ +import { PieChart, SM, Span } from '@appquality/unguess-design-system'; +import { useTranslation } from 'react-i18next'; +import { appTheme } from 'src/app/theme'; +import styled from 'styled-components'; +import { WidgetLoader } from '../../widgetLoader'; +import { useBugsByAdditional } from '../useBugsByAdditional'; +import { useMaxItems } from '../useMaxItems'; + +const TooltipSM = styled(SM)` + color: ${({ theme }) => theme.components.text.primaryColor}; +`; + +const Tooltip = styled.div` + padding: ${({ theme }) => theme.space.sm}; + background: ${({ theme }) => theme.palette.white}; + box-shadow: ${({ theme }) => theme.shadows.boxShadow(theme)}; + max-width: 25ch; +`; + +export const Chart = ({ + slug, + name, + campaignId, +}: { + name: string; + slug: string; + campaignId: string; +}) => { + const { t } = useTranslation(); + const { items, total, isLoading, isError } = useBugsByAdditional({ + campaignId, + slug, + }); + const newItems = useMaxItems(items, 6); + + if (isLoading || isError) { + return ; + } + return ( +
+ { + if (labelPosition === 'arclink' && label) return label.toString(); + if (labelPosition === 'legend' && data?.fullName) + return data.fullName.toString(); + return id.toString(); + }} + data={newItems} + theme={{ labels: { text: { fontSize: 10 } } }} + tooltip={({ label, value }) => ( + + + {name}: {label} + + + {t( + '__CAMPAIGN_PAGE_WIDGET_BUGS_BY_ADDITIONAL_TOOLTIP_UNIQUE_BUGS_LABEL' + )} + + {value} + + + + )} + /> +
+ ); +}; diff --git a/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/List/index.tsx b/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/List/index.tsx new file mode 100644 index 000000000..33811dfc2 --- /dev/null +++ b/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/List/index.tsx @@ -0,0 +1,75 @@ +import { XL } from '@appquality/unguess-design-system'; +import { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { List as CampaignList } from 'src/pages/Campaign/List'; +import { ListItem } from 'src/pages/Campaign/List/ListItem'; +import { WidgetLoader } from '../../widgetLoader'; +import { useBugsByAdditional } from '../useBugsByAdditional'; + +export const List = ({ + slug, + name, + campaignId, +}: { + slug: string; + name: string; + campaignId: string; +}) => { + const { t } = useTranslation(); + const { items, total, isLoading, isError } = useBugsByAdditional({ + slug, + campaignId, + }); + const [currentPage, setCurrentPage] = useState(1); + const [paginatedItems, setPaginatedItems] = useState(items); + const pageSize = 6; + const maxPages = useMemo( + () => Math.ceil(items.length / pageSize), + [items, pageSize] + ); + + useEffect(() => { + setPaginatedItems( + items.slice((currentPage - 1) * pageSize, currentPage * pageSize) + ); + }, [currentPage, items]); + + if (isLoading || isError) { + return ; + } + + return ( + + {total}{' '} + + {t('__CAMPAIGN_PAGE_WIDGET_BUGS_BY_USECASE_LIST_CONTENT')} + + + } + > + + {name} + + {t('__CAMPAIGN_PAGE_WIDGET_BUGS_BY_USECASE_COLUMN_RIGHT')} + + + {paginatedItems.map((item) => ( + + {item.children} + + ))} + + + ); +}; diff --git a/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/index.tsx b/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/index.tsx new file mode 100644 index 000000000..be9f1d366 --- /dev/null +++ b/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/index.tsx @@ -0,0 +1,30 @@ +import { useParams } from 'react-router-dom'; +import FlipCard from 'src/pages/Campaign/widgetCards/FlipCard'; +import { Chart } from './Chart'; +import { List } from './List'; + +const UniqueBugsByAdditional = ({ + name, + slug, + height, +}: { + name: string; + slug: string; + height: string; +}) => { + const { campaignId } = useParams(); + if (!campaignId) { + return null; + } + return ( + + {name} + } + back={} + /> + + ); +}; + +export default UniqueBugsByAdditional; diff --git a/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/types.ts b/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/types.ts new file mode 100644 index 000000000..4ad6d6938 --- /dev/null +++ b/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/types.ts @@ -0,0 +1,13 @@ +interface PieDatum { + [key: string]: string | number; +} + +export interface WidgetItem extends PieDatum { + id: string; + label: string; + value: number; + key: number; + children: string; + numerator: number; + denominator: number; +} diff --git a/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/useBugsByAdditional.ts b/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/useBugsByAdditional.ts new file mode 100644 index 000000000..81de7767f --- /dev/null +++ b/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/useBugsByAdditional.ts @@ -0,0 +1,58 @@ +import { useMemo } from 'react'; +import { useGetCampaignsByCidBugsQuery } from 'src/features/api'; +import { WidgetItem } from './types'; + +export const useBugsByAdditional = ({ + campaignId, + slug, +}: { + campaignId: string; + slug: string; +}): { + total: number; + items: WidgetItem[]; + isLoading: boolean; + isError: boolean; +} => { + const { data, isLoading, isError } = useGetCampaignsByCidBugsQuery({ + cid: campaignId, + filterBy: { is_duplicated: 0 }, + }); + + const result = useMemo( + () => + Object.values( + (data?.items || []).reduce((acc, bug) => { + const value = bug.additional_fields?.find( + (field) => field.slug === slug + )?.value; + if (typeof value === 'undefined') return acc; + + if (!(value in acc)) { + acc[`${value}`] = { + id: value, + label: value, + value: 0, + key: 0, + children: value, + numerator: 0, + denominator: data?.total || 0, + }; + } + + acc[`${value}`].value += 1; + acc[`${value}`].numerator += 1; + + return acc; + }, {} as Record) + ), + [data?.items, slug] + ); + + return { + items: result, + total: data?.total || 0, + isLoading, + isError, + }; +}; diff --git a/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/useMaxItems.ts b/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/useMaxItems.ts new file mode 100644 index 000000000..6ab26aee7 --- /dev/null +++ b/src/pages/Campaign/useWidgets/Functional/widgets/UniqueBugsByAdditional/useMaxItems.ts @@ -0,0 +1,35 @@ +import { useTranslation } from 'react-i18next'; +import { WidgetItem } from './types'; + +interface Datum { + id: string; + value: number; + [key: string]: string | number | undefined; +} + +export const useMaxItems = (items: WidgetItem[], maxItems = 6): Datum[] => { + const { t } = useTranslation(); + const sortedItems = [...items].sort((x, y) => { + if (x.value < y.value) { + return 1; + } + if (x.value > y.value) { + return -1; + } + return 0; + }); + const exceding = sortedItems.slice(maxItems - 1, sortedItems.length); + if (exceding.length === 0) { + return sortedItems; + } + const excedingValue = exceding.reduce((acc, curr) => acc + curr.value, 0); + const excedingLabel = t('__CAMPAIGN_PAGE_WIDGET_BUGS_BY_USECASE', 'others'); + const excedingData = { + id: excedingLabel, + label: excedingLabel, + value: excedingValue, + }; + const newItems: Datum[] = sortedItems.slice(0, maxItems - 1); + newItems.push(excedingData); + return newItems; +}; 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/src/pages/Dashboard/Modals/DeleteProjectModal.tsx b/src/pages/Dashboard/Modals/DeleteProjectModal.tsx new file mode 100644 index 000000000..7a6bb33f0 --- /dev/null +++ b/src/pages/Dashboard/Modals/DeleteProjectModal.tsx @@ -0,0 +1,87 @@ +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'; +import { useDeleteProjectsByPidMutation } from 'src/features/api'; + +const DeleteProjectModal = ({ + projectId, + onQuit, +}: { + projectId: number; + onQuit: () => void; +}) => { + const { t } = useTranslation(); + const { addToast } = useToast(); + const navigate = useNavigate(); + const [deleteProject, { isLoading }] = useDeleteProjectsByPidMutation(); + + const showDeleteErrorToast = (error: Error) => { + addToast( + ({ close }) => ( + + ), + { placement: 'top' } + ); + }; + + const handleConfirm = async () => { + try { + await deleteProject({ pid: projectId.toString() }).unwrap(); + navigate(`/`); + } catch (e) { + showDeleteErrorToast(e as unknown as Error); + } + onQuit(); + }; + + return ( + + + {t('__PROJECT_PAGE_DELETE_PROJECT_MODAL_TITLE')} + + + + + + + + + + + + + + + ); +}; + +export { DeleteProjectModal }; diff --git a/src/pages/Dashboard/headerContent.tsx b/src/pages/Dashboard/headerContent.tsx index cf5a23df3..d8fe37fc7 100644 --- a/src/pages/Dashboard/headerContent.tsx +++ b/src/pages/Dashboard/headerContent.tsx @@ -2,6 +2,7 @@ import { Button, PageHeader } from '@appquality/unguess-design-system'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { useAppSelector } from 'src/app/hooks'; +import { appTheme } from 'src/app/theme'; import { LayoutWrapper } from 'src/common/components/LayoutWrapper'; import { PageTitle } from 'src/common/components/PageTitle'; import { useCanAccessToActiveWorkspace } from 'src/hooks/useCanAccessToActiveWorkspace'; @@ -31,7 +32,7 @@ export const DashboardHeaderContent = ({ {hasWorksPacePermission && ( -
+
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/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})`} )} 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 = ({ diff --git a/src/pages/Dashboard/projectPageHeader.tsx b/src/pages/Dashboard/projectPageHeader.tsx index 7c2ca2cfb..2c9ae041e 100644 --- a/src/pages/Dashboard/projectPageHeader.tsx +++ b/src/pages/Dashboard/projectPageHeader.tsx @@ -1,10 +1,13 @@ import { Button, + IconButton, LG, PageHeader, Skeleton, XXXL, } from '@appquality/unguess-design-system'; +import { ReactComponent as DeleteIcon } from '@zendeskgarden/svg-icons/src/16/trash-stroke.svg'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import { useAppSelector } from 'src/app/hooks'; @@ -18,6 +21,8 @@ import styled from 'styled-components'; import { Counters } from './Counters'; import { EditableDescription } from './EditableDescription'; import { EditableTitle } from './EditableTitle'; +import { useProjectPlans } from './hooks/useProjectPlans'; +import { DeleteProjectModal } from './Modals/DeleteProjectModal'; const StyledPageHeaderMeta = styled(PageHeader.Meta)` justify-content: space-between; @@ -38,6 +43,7 @@ const StyledPageHeaderMeta = styled(PageHeader.Meta)` const StyledDiv = styled.div` display: flex; + gap: ${({ theme }) => theme.space.xs}; `; export const ProjectPageHeader = ({ projectId }: { projectId: number }) => { @@ -47,6 +53,11 @@ 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 { items: plans, isLoading: isLoadingPlans } = useProjectPlans({ + projectId: projectId || 0, + }); const { isLoading, @@ -83,14 +94,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 @@ -100,7 +117,7 @@ export const ProjectPageHeader = ({ projectId }: { projectId: number }) => { - + {hasWorksPacePermission && } {hasWorksPacePermission && ( )} + {hasWorksPacePermission && + project?.campaigns_count === 0 && + plans.length === 0 && ( + setDeleteModalOpen(true)}> + + + )} )} + {deleteModalOpen && ( + setDeleteModalOpen(false)} + /> + )} diff --git a/src/pages/Insights/PageHeader.tsx b/src/pages/Insights/PageHeader.tsx index c79ca9188..0c0b1d344 100644 --- a/src/pages/Insights/PageHeader.tsx +++ b/src/pages/Insights/PageHeader.tsx @@ -17,6 +17,7 @@ import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute'; import { styled } from 'styled-components'; import { ReactComponent as VideoListIcon } from '@zendeskgarden/svg-icons/src/16/play-circle-stroke.svg'; import { ReactComponent as DashboardIcon } from 'src/assets/icons/dashboard-icon.svg'; +import { useCanAccessToActiveWorkspace } from 'src/hooks/useCanAccessToActiveWorkspace'; import { useCampaign } from '../Campaign/pageHeader/useCampaign'; const Wrapper = styled.div` @@ -50,7 +51,7 @@ const InsightsPageHeader = () => { const videosRoute = useLocalizeRoute(`campaigns/${campaignId}/videos`); const campaignRoute = useLocalizeRoute(`campaigns/${campaignId}`); const { hasFeatureFlag } = useFeatureFlag(); - + const hasWorkspaceAccess = useCanAccessToActiveWorkspace(); const hasTaggingToolFeature = hasFeatureFlag(FEATURE_FLAG_TAGGING_TOOL); const { isUserLoading, isLoading, isError, campaign, project } = useCampaign( @@ -83,7 +84,9 @@ const InsightsPageHeader = () => { {t('__INSIGHTS_PAGE_TITLE')} - {!campaign.isArchived && } + {!campaign.isArchived && hasWorkspaceAccess && ( + + )} {' '} {t('__INSIGHTS_PAGE_NAVIGATION_LABEL')} 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/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/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/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/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/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..c5bed6670 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 ( <> @@ -100,8 +103,8 @@ const TaskItem = ({ )} -
- {kind !== 'explorative-bug' && ( +
+ {!isSimpleInterface && (
- + {value.map((task) => ( ))} diff --git a/src/pages/Plan/modules/Tasks/parts/modal/AccessibilityTasks.tsx b/src/pages/Plan/modules/Tasks/parts/modal/AccessibilityTasks.tsx new file mode 100644 index 000000000..504d645c4 --- /dev/null +++ b/src/pages/Plan/modules/Tasks/parts/modal/AccessibilityTasks.tsx @@ -0,0 +1,37 @@ +import { Button, SM } from '@appquality/unguess-design-system'; +import { useTranslation } from 'react-i18next'; +import { appTheme } from 'src/app/theme'; +import { ReactComponent as AccessibilityTaskIcon } from 'src/assets/icons/accessibility-task-icon.svg'; +import { useHandleModalItemClick } from '../../utils'; +import { ButtonsContainer } from './ButtonsContainer'; + +const AccessibilityTasks = () => { + const { t } = useTranslation(); + const handleModalItemClick = useHandleModalItemClick(); + + return ( + <> + + {t( + '__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_ACCESSIBILITY_TASKS_LABEL' + ).toUpperCase()} + + + + + + ); +}; + +export { AccessibilityTasks }; 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..b229e280a 100644 --- a/src/pages/Plan/modules/Tasks/parts/modal/TasksModal.tsx +++ b/src/pages/Plan/modules/Tasks/parts/modal/TasksModal.tsx @@ -7,14 +7,13 @@ import { useFeatureFlag } from 'src/hooks/useFeatureFlag'; import styled from 'styled-components'; import { useModuleTasksContext } from '../../context'; import { useModuleTasks } from '../../hooks'; +import { AccessibilityTasks } from './AccessibilityTasks'; import { ExperientialTasks } from './ExperientialTasks'; import { FunctionalTasks } from './FunctionalTasks'; import { SurveyTasks } from './SurveyTasks'; const StyledTabs = styled(Tabs)` - > button { - width: 33.33%; - } + display: flex; `; const TasksModal = () => { @@ -22,33 +21,23 @@ const TasksModal = () => { const { variant, setVariant } = useModuleTasks(); const { modalRef, setModalRef } = useModuleTasksContext(); const { hasFeatureFlag } = useFeatureFlag(); + const variants = [ + 'default', + 'functional', + 'experiential', + 'accessibility', + ] as const; - const selectActiveVariant = (index: number) => { - switch (index) { - case 0: - return 'default'; - case 1: - return 'functional'; - case 2: - return 'experiential'; - default: - return 'default'; - } - }; + const selectActiveVariant = (index: number) => variants[`${index}`]; const getActiveVariantIndex = ( v: components['schemas']['Module']['variant'] ) => { - switch (v) { - case 'default': - return 0; - case 'functional': - return 1; - case 'experiential': - return 2; - default: - return 0; + const index = variants.findIndex((item) => item === v); + if (index === -1) { + return 0; } + return index; }; return ( @@ -57,6 +46,7 @@ const TasksModal = () => { onClose={() => setModalRef(null)} placement="auto" hasArrow={false} + role="dialog" > {t('__PLAN_PAGE_MODULE_TASKS_ADD_TASK_MODAL_TITLE')} @@ -77,6 +67,8 @@ const TasksModal = () => { + + { + + + diff --git a/src/pages/Plan/modules/Tasks/utils/getIconFromTask.tsx b/src/pages/Plan/modules/Tasks/utils/getIconFromTask.tsx index 793e6d02d..93e234f51 100644 --- a/src/pages/Plan/modules/Tasks/utils/getIconFromTask.tsx +++ b/src/pages/Plan/modules/Tasks/utils/getIconFromTask.tsx @@ -1,5 +1,6 @@ import { getColor } from '@appquality/unguess-design-system'; import { appTheme } from 'src/app/theme'; +import { ReactComponent as AccessibilityTaskIcon } from 'src/assets/icons/accessibility-task-icon.svg'; import { ReactComponent as ExploratoryTaskIcon } from 'src/assets/icons/exploratory-task-icon.svg'; import { ReactComponent as FunctionalTaskIcon } from 'src/assets/icons/functional-task-icon.svg'; import { ReactComponent as SurveyTaskIcon } from 'src/assets/icons/survey-task-icon.svg'; @@ -50,6 +51,8 @@ const getIconFromTaskOutput = ( return ; case 'survey': return ; + case 'accessibility': + return ; default: return null; } diff --git a/src/pages/Video/components/Observation.tsx b/src/pages/Video/components/Observation.tsx index 02f793305..824b12ad0 100644 --- a/src/pages/Video/components/Observation.tsx +++ b/src/pages/Video/components/Observation.tsx @@ -1,5 +1,5 @@ import { - AccordionNew as Accordion, + AccordionNew, IconButton, Notification, Tooltip, @@ -17,7 +17,6 @@ import { GetVideosByVidApiResponse, GetVideosByVidObservationsApiResponse, } from 'src/features/api'; - import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute'; import { formatDuration } from 'src/pages/Videos/utils/formatDuration'; import { styled } from 'styled-components'; @@ -142,16 +141,15 @@ const Observation = ({ return ( <> - - - + } > - - + - - - + + + - - - + + + ); }; 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); diff --git a/src/pages/Videos/Metas.tsx b/src/pages/Videos/Metas.tsx index b86bc5f05..53ee8f87c 100644 --- a/src/pages/Videos/Metas.tsx +++ b/src/pages/Videos/Metas.tsx @@ -31,6 +31,7 @@ import { appTheme } from 'src/app/theme'; import { useLocalizeRoute } from 'src/hooks/useLocalizedRoute'; import { FEATURE_FLAG_TAGGING_TOOL } from 'src/constants'; import { useFeatureFlag } from 'src/hooks/useFeatureFlag'; +import { useCanAccessToActiveWorkspace } from 'src/hooks/useCanAccessToActiveWorkspace'; import { getAllSeverityTags } from './utils/getSeverityTagsWithCount'; const ButtonWrapper = styled.div` @@ -99,6 +100,7 @@ export const Metas = ({ const { t } = useTranslation(); const { addToast } = useToast(); const { hasFeatureFlag } = useFeatureFlag(); + const hasWorkspaceAccess = useCanAccessToActiveWorkspace(); const hasTaggingToolFeature = hasFeatureFlag(FEATURE_FLAG_TAGGING_TOOL); @@ -245,7 +247,7 @@ export const Metas = ({ - {!campaign.isArchived && } + {!campaign.isArchived && hasWorkspaceAccess && } <> {' '} 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_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_unquoted.json b/tests/api/plans/pid/_get/200_pending_review_unquoted.json new file mode 100644 index 000000000..5ba6a92e9 --- /dev/null +++ b/tests/api/plans/pid/_get/200_pending_review_unquoted.json @@ -0,0 +1,98 @@ +{ + "id": 13, + "workspace_id": 1, + "status": "pending_review", + "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": "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/_patch/200_draft_mandatory_only.json similarity index 96% rename from tests/api/plans/pid/_get/200_pending_review.json rename to tests/api/plans/pid/_patch/200_draft_mandatory_only.json index a5c182490..86284a8e3 100644 --- a/tests/api/plans/pid/_get/200_pending_review.json +++ b/tests/api/plans/pid/_patch/200_draft_mandatory_only.json @@ -1,7 +1,7 @@ { "id": 13, "workspace_id": 1, - "status": "pending_review", + "status": "draft", "project": { "id": 90, "name": "MyProject" diff --git a/tests/e2e/campaign.spec.ts b/tests/e2e/campaign.spec.ts new file mode 100644 index 000000000..21ff38d8b --- /dev/null +++ b/tests/e2e/campaign.spec.ts @@ -0,0 +1,40 @@ +import { test, expect } from '../fixtures/app'; +import { Campaign } from '../fixtures/pages/Campaign'; + +test.describe('campaign page', () => { + let campaign: Campaign; + + test.beforeEach(async ({ page }) => { + campaign = new Campaign(page); + + await campaign.loggedIn(); + await campaign.mockPreferences(); + await campaign.mockWorkspace(); + await campaign.mockExperientialCampaign(); + await campaign.mockWorkspacesList(); + await campaign.open(); + }); + + test('the invite users button should be visible', async () => { + await expect(campaign.elements().inviteUsersButton()).toBeVisible(); + }); +}); + +test.describe('Campaign page on a shared workspace', () => { + let campaign: Campaign; + + test.beforeEach(async ({ page }) => { + campaign = new Campaign(page); + + await campaign.loggedIn(); + await campaign.mockPreferences(); + await campaign.mockWorkspace(); + await campaign.mockExperientialCampaign(); + await campaign.mocksharedWorkspacesList(); + await campaign.open(); + }); + + test('should hide the invite users button', async () => { + await expect(campaign.elements().inviteUsersButton()).not.toBeVisible(); + }); +}); diff --git a/tests/e2e/dashboard.spec.ts b/tests/e2e/dashboard_home.spec.ts similarity index 98% rename from tests/e2e/dashboard.spec.ts rename to tests/e2e/dashboard_home.spec.ts index ceddbeb01..bafda0f77 100644 --- a/tests/e2e/dashboard.spec.ts +++ b/tests/e2e/dashboard_home.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from '../fixtures/app'; -import { Dashboard } from '../fixtures/pages/Dashboard'; +import { Dashboard } from '../fixtures/pages/DashboardHome'; import { PlanCreationInterface } from '../fixtures/components/PlanCreationInterface'; import { PromoList } from '../fixtures/components/PromoList'; diff --git a/tests/e2e/insights.spec.ts b/tests/e2e/insights.spec.ts index 31f5564b6..2fd618fb0 100644 --- a/tests/e2e/insights.spec.ts +++ b/tests/e2e/insights.spec.ts @@ -24,4 +24,26 @@ test.describe('Insights page', () => { insightsData.length ); }); + + test('should display a invite users btn', async () => { + await expect(insightsPage.elements().inviteUsersButton()).toBeVisible(); + }); +}); + +test.describe('Insights page on a shared workspace', () => { + let insightsPage: Insights; + + test.beforeEach(async ({ page }) => { + insightsPage = new Insights(page); + await insightsPage.loggedIn(); + await insightsPage.mockPreferences(); + await insightsPage.mockWorkspace(); + await insightsPage.mockExperientialCampaign(); + await insightsPage.mocksharedWorkspacesList(); + await insightsPage.open(); + }); + + test('should hide the invite users button', async () => { + await expect(insightsPage.elements().inviteUsersButton()).not.toBeVisible(); + }); }); diff --git a/tests/e2e/plan/approved.spec.ts b/tests/e2e/plan/approved.spec.ts new file mode 100644 index 000000000..6470ad067 --- /dev/null +++ b/tests/e2e/plan/approved.spec.ts @@ -0,0 +1,48 @@ +import { test, expect } from '../../fixtures/app'; +import { PlanPage } from '../../fixtures/pages/Plan'; + +test.describe('A Plan page in approved state', () => { + 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.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( + moduleBuilderPage.elements().extraActionsMenu() + ).not.toBeVisible(); + 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.expectAllModulesToBeReadonly(); + }); +}); diff --git a/tests/e2e/plan/draft.spec.ts b/tests/e2e/plan/draft.spec.ts index 96790580e..7aae72d0b 100644 --- a/tests/e2e/plan/draft.spec.ts +++ b/tests/e2e/plan/draft.spec.ts @@ -1,63 +1,39 @@ 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(); }); 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(); - - // 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(); 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(); }); - 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(); - - // 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 ({ @@ -65,7 +41,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' ); @@ -77,76 +53,38 @@ 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) => - /\/api\/plans\/1(?!\/status)/.test(response.url()) && + /\/api\/plans\/1/.test(response.url()) && response.status() === 200 && 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' - ); + test('if PATCH plan is ok then calls the PATCH Status', async () => { 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' }); - }); - 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(); + const response = await requestQuotationModal.submitRequest(); + const data = response.request().postDataJSON(); + expect(data).toEqual({ status: 'pending_review' }); }); - 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; - test.beforeEach(async ({ page }, testinfo) => { - testinfo.setTimeout(60000); + test.beforeEach(async ({ page }) => { planPage = new PlanPage(page); await planPage.loggedIn(); await planPage.mockPreferences(); await planPage.mockWorkspace(); await planPage.mockWorkspacesList(); await planPage.mockGetDraftWithOnlyMandatoryModulesPlan(); - + await planPage.mockDeletePlan(); await planPage.open(); }); @@ -167,13 +105,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/e2e/plan/modules/date.spec.ts b/tests/e2e/plan/modules/date.spec.ts new file mode 100644 index 000000000..ab7838dc8 --- /dev/null +++ b/tests/e2e/plan/modules/date.spec.ts @@ -0,0 +1,39 @@ +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 25a59db3c..b1c91780c 100644 --- a/tests/e2e/plan/modules/tasks.spec.ts +++ b/tests/e2e/plan/modules/tasks.spec.ts @@ -1,29 +1,86 @@ -import { test } from '../../../fixtures/app'; +import { test, expect } from '../../../fixtures/app'; import { PlanPage } from '../../../fixtures/pages/Plan'; +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'; 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().tabInstructions().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 ({ + i18n, + }) => { + await expect(tasksModule.elements().module()).toBeVisible(); + const tasks = TasksModule.getTasksFromPlan(apiGetDraftMandatoryPlan); + await expect(tasksModule.elements().taskListItem()).toHaveCount( + tasks.length + ); + + // delete each item + 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(); + 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 = tasksModule.elements().taskTitleInput(element); + 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/e2e/plan/pending_review_quoted.spec.ts b/tests/e2e/plan/pending_review_quoted.spec.ts new file mode 100644 index 000000000..0d5bc0e66 --- /dev/null +++ b/tests/e2e/plan/pending_review_quoted.spec.ts @@ -0,0 +1,51 @@ +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( + moduleBuilderPage.elements().goToDashboardCTA() + ).not.toBeVisible(); + await expect( + moduleBuilderPage.elements().extraActionsMenu() + ).not.toBeVisible(); + 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.spec.ts b/tests/e2e/plan/pending_review_unquoted.spec.ts similarity index 60% rename from tests/e2e/plan/pending_review.spec.ts rename to tests/e2e/plan/pending_review_unquoted.spec.ts index 2696f6eb6..79ad9b416 100644 --- a/tests/e2e/plan/pending_review.spec.ts +++ b/tests/e2e/plan/pending_review_unquoted.spec.ts @@ -6,36 +6,44 @@ test.describe('A Plan page in pending request', () => { test.beforeEach(async ({ page }) => { moduleBuilderPage = new PlanPage(page); + await moduleBuilderPage.loggedIn(); await moduleBuilderPage.mockPreferences(); await moduleBuilderPage.mockWorkspace(); await moduleBuilderPage.mockWorkspacesList(); - await moduleBuilderPage.mockGetPendingReviewPlan(); + await moduleBuilderPage.mockGetPendingReviewPlan_WithoutQuote(); 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().confirmActivityCTA() - ).toBeVisible(); + moduleBuilderPage.elements().requestQuotationCTA() + ).not.toBeVisible(); await expect( moduleBuilderPage.elements().confirmActivityCTA() ).toBeDisabled(); - }); - test('all inputs should be readonly', async () => { await expect( - moduleBuilderPage.elements().saveConfigurationCTA() + moduleBuilderPage.elements().goToDashboardCTA() ).not.toBeVisible(); await expect( - moduleBuilderPage.elements().requestQuotationCTA() + moduleBuilderPage.elements().extraActionsMenu() ).not.toBeVisible(); + 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(); }); - - // 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.expectAllModulesToBeReadonly(); }); }); diff --git a/tests/e2e/project.spec.ts b/tests/e2e/project.spec.ts index 051eb0cc4..49ffeef35 100644 --- a/tests/e2e/project.spec.ts +++ b/tests/e2e/project.spec.ts @@ -39,6 +39,10 @@ test.describe('project page', () => { ); }); + test('the invite users button should be visible', async () => { + await expect(project.elements().inviteUsersButton()).toBeVisible(); + }); + test('should open the create plan interface when clicking on a promo item, preselected project in dropdown and a more info should go to the single template', async ({ page, }) => { @@ -97,6 +101,28 @@ test.describe('project page', () => { }); }); +test.describe('project page on a shared workspace', () => { + let project: Project; + + test.beforeEach(async ({ page }) => { + project = new Project(page); + + await project.loggedIn(); + await project.mockPreferences(); + await project.mockWorkspace(); + await project.mockworkspacesPlans(); + await project.mockworkspacesCampaigns(); + await project.mockProject(); + await project.mockProjectCampaigns(); + await project.mocksharedWorkspacesList(); + await project.open(); + }); + + test('should hide the invite users button', async () => { + await expect(project.elements().inviteUsersButton()).not.toBeVisible(); + }); +}); + test.describe('project page empty state', () => { let project: Project; let planCreationInterface: PlanCreationInterface; diff --git a/tests/fixtures/DashboardsBase.ts b/tests/fixtures/DashboardsBase.ts new file mode 100644 index 000000000..634c715a7 --- /dev/null +++ b/tests/fixtures/DashboardsBase.ts @@ -0,0 +1,29 @@ +import { type Page } from '@playwright/test'; +import { i18n } from 'i18next'; +import { getI18nInstance } from 'playwright-i18next-fixture'; +import { UnguessPage } from './UnguessPage'; + +// This is a base class for the various dashboard pages. +// It contains common methods and properties that can be used by the +// specific dashboard pages that extend this class, like Insights, Bugs and Video. +export class DashboardBase extends UnguessPage { + readonly page: Page; + + readonly i18n: i18n; + + constructor(page: Page) { + super(page); + this.page = page; + this.i18n = getI18nInstance() as unknown as i18n; + } + + elements() { + return { + ...super.elements(), + inviteUsersButton: () => + this.page + .getByRole('button') + .filter({ hasText: this.i18n.t('__CAMPAIGN_SETTINGS_CTA_TEXT') }), + }; + } +} diff --git a/tests/fixtures/pages/Campaign.ts b/tests/fixtures/pages/Campaign.ts new file mode 100644 index 000000000..a26137111 --- /dev/null +++ b/tests/fixtures/pages/Campaign.ts @@ -0,0 +1,23 @@ +import { type Page } from '@playwright/test'; +import { UnguessPage } from '../UnguessPage'; + +export class Campaign extends UnguessPage { + readonly page: Page; + + readonly url = 'campaigns/1'; + + constructor(page: Page) { + super(page); + this.page = page; + } + + elements() { + return { + ...super.elements(), + inviteUsersButton: () => + this.page + .getByRole('button') + .filter({ hasText: this.i18n.t('__CAMPAIGN_SETTINGS_CTA_TEXT') }), + }; + } +} diff --git a/tests/fixtures/pages/Dashboard.ts b/tests/fixtures/pages/DashboardHome.ts similarity index 100% rename from tests/fixtures/pages/Dashboard.ts rename to tests/fixtures/pages/DashboardHome.ts diff --git a/tests/fixtures/pages/Insights.ts b/tests/fixtures/pages/Insights.ts index a4659e904..a024cce8a 100644 --- a/tests/fixtures/pages/Insights.ts +++ b/tests/fixtures/pages/Insights.ts @@ -1,7 +1,7 @@ import { type Page } from '@playwright/test'; -import { UnguessPage } from '../UnguessPage'; +import { DashboardBase } from '../DashboardsBase'; -export class Insights extends UnguessPage { +export class Insights extends DashboardBase { readonly page: Page; constructor(page: Page) { diff --git a/tests/fixtures/pages/Plan.ts b/tests/fixtures/pages/Plan.ts deleted file mode 100644 index 51c26ece5..000000000 --- a/tests/fixtures/pages/Plan.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { type Page } from '@playwright/test'; -import { UnguessPage } from '../UnguessPage'; - -export class PlanPage extends UnguessPage { - readonly page: Page; - - constructor(page: Page) { - super(page); - this.page = page; - this.url = `plans/1`; - } - - 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'), - 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: () => - this.elements() - .deletePlanModal() - .getByText(this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_TITLE')), - deletePlanModalConfirmCTA: () => - this.elements() - .deletePlanModal() - .getByRole('button', { - name: this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_BUTTON_CONFIRM'), - }), - deletePlanModalCancelCTA: () => - this.elements() - .deletePlanModal() - .getByText( - this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_BUTTON_CANCEL') - ), - }; - } - - 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' - ); - if (!titleModule) { - throw new Error('No title module found in plan'); - } - if (typeof titleModule.output !== 'string') { - throw new Error('Invalid title module output'); - } - return titleModule.output; - } - - async fillInputTItle(value: string) { - await this.elements().titleModule().click(); - await this.elements().titleModuleInput().fill(value); - 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 mockGetDraftPlan() { - await this.page.route('*/**/api/plans/1', async (route) => { - await route.fulfill({ - path: 'tests/api/plans/pid/_get/200_draft_complete.json', - }); - }); - } - - 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', - }); - }); - } - - // 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', - }); - }); - } - - // 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', - }); - }); - } - - async mockGetPendingReviewPlan() { - await this.page.route('*/**/api/plans/1', async (route) => { - await route.fulfill({ - path: 'tests/api/plans/pid/_get/200_pending_review.json', - }); - }); - } - - async mockPatchPlan() { - await this.page.route('*/**/api/plans/1', async (route) => { - await route.fulfill({ - path: 'tests/api/plans/pid/_patch/200_Example_1.json', - }); - }); - } - - 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', - }); - }); - } -} diff --git a/tests/fixtures/pages/Plan/Module_age.ts b/tests/fixtures/pages/Plan/Module_age.ts new file mode 100644 index 000000000..4dd8a7632 --- /dev/null +++ b/tests/fixtures/pages/Plan/Module_age.ts @@ -0,0 +1,31 @@ +import { expect, type Page } from '@playwright/test'; + +export class AgeModule { + 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(); + 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 new file mode 100644 index 000000000..418e5f051 --- /dev/null +++ b/tests/fixtures/pages/Plan/Module_digital_literacy.ts @@ -0,0 +1,54 @@ +import { expect, 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 { + tab: () => this.page.getByTestId('target-tab'), + 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; + } + + async goToTab() { + await this.elements().tab().click(); + } + + async expectToBeReadonly() { + const digitalLiteracyCheckbox = this.elements().moduleInput(); + 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 new file mode 100644 index 000000000..c8ed790d1 --- /dev/null +++ b/tests/fixtures/pages/Plan/Module_gender.ts @@ -0,0 +1,38 @@ +import { expect, type Page } from '@playwright/test'; +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(); + 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_goal.ts b/tests/fixtures/pages/Plan/Module_goal.ts new file mode 100644 index 000000000..f79c63d0a --- /dev/null +++ b/tests/fixtures/pages/Plan/Module_goal.ts @@ -0,0 +1,45 @@ +import { expect, 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'), + tab: () => this.page.getByTestId('setup-tab'), + 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; + } + + 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 new file mode 100644 index 000000000..cd3c75889 --- /dev/null +++ b/tests/fixtures/pages/Plan/Module_language.ts @@ -0,0 +1,52 @@ +import { expect, 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 { + tab: () => this.page.getByTestId('target-tab'), + 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; + } + + async goToTab() { + await this.elements().tab().click(); + } + + async expectToBeReadonly() { + const languageRadioInputs = this.elements().languageRadioInput(); + 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_out_of_scope.ts b/tests/fixtures/pages/Plan/Module_out_of_scope.ts new file mode 100644 index 000000000..38cc3dfe9 --- /dev/null +++ b/tests/fixtures/pages/Plan/Module_out_of_scope.ts @@ -0,0 +1,43 @@ +import { expect, 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 { + tab: () => this.page.getByTestId('instructions-tab'), + 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; + } + + 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 new file mode 100644 index 000000000..7279cf366 --- /dev/null +++ b/tests/fixtures/pages/Plan/Module_target.ts @@ -0,0 +1,50 @@ +import { expect, 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 { + 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'), + }; + } + + 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; + } + + 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 new file mode 100644 index 000000000..7c92056e2 --- /dev/null +++ b/tests/fixtures/pages/Plan/Module_tasks.ts @@ -0,0 +1,98 @@ +import { expect, Locator, 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 { + tab: () => this.page.getByTestId('instructions-tab'), + 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() + .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' + ), + }), + }; + } + + 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; + } + + async goToTab() { + await this.elements().tab().click(); + } + + async expectToBeReadonly() { + 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/RequestQuotationModal.ts b/tests/fixtures/pages/Plan/RequestQuotationModal.ts new file mode 100644 index 000000000..30a6c8f9b --- /dev/null +++ b/tests/fixtures/pages/Plan/RequestQuotationModal.ts @@ -0,0 +1,91 @@ +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'), + }), + 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'), + 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 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) => + /\/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/index.ts b/tests/fixtures/pages/Plan/index.ts new file mode 100644 index 000000000..bc4247572 --- /dev/null +++ b/tests/fixtures/pages/Plan/index.ts @@ -0,0 +1,268 @@ +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 { LanguageModule } from './Module_language'; +import { OutOfScopeModule } from './Module_out_of_scope'; +import { TargetModule } from './Module_target'; +import { TasksModule } from './Module_tasks'; + +interface TabModule { + expectToBeReadonly(): Promise; + goToTab(): Promise; +} +export class PlanPage extends UnguessPage { + readonly page: Page; + + readonly modules: { [index: string]: TabModule }; + + constructor(page: Page) { + super(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`; + } + + elements() { + return { + ...super.elements(), + confirmActivityCTA: () => + 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', { + name: this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_TITLE'), + }), + deletePlanModalCancelCTA: () => + this.elements() + .deletePlanModal() + .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'), + }), + deletePlanModalTitle: () => + this.elements() + .deletePlanModal() + .getByText(this.i18n.t('__PLAN_PAGE_DELETE_PLAN_MODAL_TITLE')), + descriptionModule: () => this.page.getByTestId('description-module'), + extraActionsMenu: () => this.page.getByTestId('extra-actions-menu'), + pageHeader: () => this.page.getByTestId('plan-page-header'), + 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: () => + this.elements().titleModule().getByTestId('title-error'), + titleModuleInput: () => + this.elements().titleModule().getByTestId('title-input'), + titleModuleOutput: () => + this.elements().titleModule().getByTestId('title-output'), + }; + } + + static getTitleFromPlan(plan: any) { + const titleModule = plan.config.modules.find( + (module) => module.type === 'title' + ); + if (!titleModule) { + throw new Error('No title module found in plan'); + } + if (typeof titleModule.output !== 'string') { + throw new Error('Invalid title module output'); + } + return titleModule.output; + } + + async fillInputTItle(value: string) { + await this.elements().titleModule().click(); + await this.elements().titleModuleInput().fill(value); + await this.elements().titleModuleInput().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 expectAllModulesToBeReadonly() { + // 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() { + await this.page.route('*/**/api/plans/1', async (route) => { + 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) => { + 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) => { + 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) => { + 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_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_quoted.json', + }); + } else { + await route.fallback(); + } + }); + } + + 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') { + await route.fulfill({ + path: 'tests/api/plans/pid/_patch/200_draft_mandatory_only.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(); + } + }); + } +} diff --git a/tests/fixtures/pages/Project.ts b/tests/fixtures/pages/Project.ts index d2c084deb..ff83ef143 100644 --- a/tests/fixtures/pages/Project.ts +++ b/tests/fixtures/pages/Project.ts @@ -27,6 +27,10 @@ export class Project extends UnguessPage { this.page.getByRole('list', { name: 'project-campaigns-card-list' }), projectCardListItems: () => this.elements().projectsCardList().getByRole('listitem'), + inviteUsersButton: () => + this.page + .getByRole('button') + .filter({ hasText: this.i18n.t('__PROJECT_SETTINGS_CTA_TEXT') }), }; } 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"