diff --git a/src/common/schema.ts b/src/common/schema.ts index 088b8e55a..5abe196c3 100644 --- a/src/common/schema.ts +++ b/src/common/schema.ts @@ -1115,7 +1115,8 @@ export interface components { | components["schemas"]["ModuleMobileInternet"] | components["schemas"]["ModuleHomeInternet"] | components["schemas"]["ModuleGasSupply"] - | components["schemas"]["ModuleAnnualIncomeRange"]; + | components["schemas"]["ModuleAnnualIncomeRange"] + | components["schemas"]["ModuleACNSaver"]; /** ModuleAdditionalTarget */ ModuleAdditionalTarget: { output: string; @@ -1966,6 +1967,22 @@ export interface components { data?: { [key: string]: unknown }; }[]; }; + /** + * ModuleACNSaver + * @description This module is created ad-hoc for Accenture to target their saver/investor personas. + */ + ModuleACNSaver: { + output: ( + | "ACN.PRAGMATICO DIGITALE" + | "ACN.EMERGENTE ASPIRAZIONALE" + | "ACN.INVESTITORE SOFISTICATO" + | "ACN.SOCIALE COLLABORATIVO" + | "ACN.CONSERVATORE PRUDENTE" + )[]; + /** @enum {undefined} */ + type: "acn_saver_personas"; + variant: string; + }; }; responses: { /** Shared error response */ @@ -3927,6 +3944,7 @@ export interface operations { count: number; /** @description The plan ID to associate with the generation */ planId: string; + context?: string; }; }; }; @@ -5258,7 +5276,6 @@ export interface operations { 400: components["responses"]["Error"]; 403: components["responses"]["Error"]; 404: components["responses"]["Error"]; - 406: components["responses"]["Error"]; 500: components["responses"]["Error"]; 502: components["responses"]["Error"]; }; diff --git a/src/features/api/index.ts b/src/features/api/index.ts index 2d1ebbbc2..c3fd54f6e 100644 --- a/src/features/api/index.ts +++ b/src/features/api/index.ts @@ -1902,6 +1902,7 @@ export type PostServicesApiKUsecasesApiArg = { count: number; /** The plan ID to associate with the generation */ planId: string; + context?: string; }; }; export type GetServicesApiKJobsByJobIdApiResponse = @@ -3179,6 +3180,17 @@ export type ModuleAnnualIncomeRange = { type: 'annual_income_range'; variant: string; }; +export type ModuleAcnSaver = { + output: ( + | 'ACN.PRAGMATICO DIGITALE' + | 'ACN.EMERGENTE ASPIRAZIONALE' + | 'ACN.INVESTITORE SOFISTICATO' + | 'ACN.SOCIALE COLLABORATIVO' + | 'ACN.CONSERVATORE PRUDENTE' + )[]; + type: 'acn_saver_personas'; + variant: string; +}; export type Module = | ModuleTitle | ModuleDate @@ -3203,7 +3215,8 @@ export type Module = | ModuleMobileInternet | ModuleHomeInternet | ModuleGasSupply - | ModuleAnnualIncomeRange; + | ModuleAnnualIncomeRange + | ModuleAcnSaver; export type PlanStatus = 'pending_review' | 'draft' | 'approved' | 'paying'; export type PurchasablePlanRules = | 'number_of_modules' diff --git a/src/features/planModules/index.tsx b/src/features/planModules/index.tsx index c8f416cd7..9a8227acf 100644 --- a/src/features/planModules/index.tsx +++ b/src/features/planModules/index.tsx @@ -69,6 +69,8 @@ const planModuleSlice = createSlice({ state.records[action.payload.type] = { ...oldVal, output: action.payload.output, + // Preserve variant or use 'default' if not set + variant: oldVal?.variant || 'default', }; if (!state.currentModules.includes(action.payload.type)) { state.currentModules.push(action.payload.type); diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 1826b97e7..224ced97d 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -921,10 +921,12 @@ "__PLAN_PAGE_MODULE_GENDER_TOTAL_PERCENTAGE_LABEL": "Total:", "__PLAN_PAGE_MODULE_GENDER_UNASSIGNED_PERCENTAGE_ERROR": "Please assign a percentage to each selected gender.", "__PLAN_PAGE_MODULE_GOAL_INFO": "Enter goal (max 256 characters)", - "__PLAN_PAGE_MODULE_GOAL_LABEL": "Specify the main goal of this activity", - "__PLAN_PAGE_MODULE_GOAL_PLACEHOLDER": "Describe what you want to achieve", + "__PLAN_PAGE_MODULE_GOAL_LABEL": "What do you want to understand from this activity?", + "__PLAN_PAGE_MODULE_GOAL_PLACEHOLDER": "e.g., Understand whether new visitors can find a product, add it to the cart smoothly, and notice the gift card option.", "__PLAN_PAGE_MODULE_GOAL_REMOVE_TOOLTIP_BUTTON": "Delete", "__PLAN_PAGE_MODULE_GOAL_TITLE": "Goal of the activity", + "__PLAN_PAGE_MODULE_GOAL_TOOLTIP_TITLE": "How to write a good goal", + "__PLAN_PAGE_MODULE_GOAL_TOOLTIP_DESCRIPTION": "Specify the goal of this activity to help us set it up effectively.

Tip: concentrate on the outcome you want to explore in the user journey, not on each step.

Testers won't see this field.", "__PLAN_PAGE_MODULE_INCOME_25K_TO_50K_LABEL": "€25,000 - €50,000", "__PLAN_PAGE_MODULE_INCOME_ALL_LABEL": "All income levels", "__PLAN_PAGE_MODULE_INCOME_ALL_LABEL_HINT": "Participants equally distributed across income brackets", diff --git a/src/pages/JoinPage/sendToHubspot.ts b/src/pages/JoinPage/sendToHubspot.ts index a2e61f2a5..733a45f65 100644 --- a/src/pages/JoinPage/sendToHubspot.ts +++ b/src/pages/JoinPage/sendToHubspot.ts @@ -8,6 +8,8 @@ export async function sendToHubspot(data: { firstName: string; lastName: string; }) { + if (process.env.NODE_ENV === 'test') return false; // Skip actual API call during tests + const PORTAL_ID = isDev() ? '50612068' : '6087279'; const FORM_ID = isDev() ? STAGING_FORM_ID : PRODUCTION_FORM_ID; const url = `https://api.hsforms.com/submissions/v3/integration/submit/${PORTAL_ID}/${FORM_ID}`; diff --git a/src/pages/Plan/ModulesList.tsx b/src/pages/Plan/ModulesList.tsx index 8f616621a..3352d0549 100644 --- a/src/pages/Plan/ModulesList.tsx +++ b/src/pages/Plan/ModulesList.tsx @@ -82,8 +82,8 @@ export const ModulesList = () => { const breakpointSm = parseInt(appTheme.breakpoints.sm, 10); const isMobile = width < breakpointSm; - const currentModules = useAppSelector( - (state) => state.planModules.currentModules + const { currentModules, records } = useAppSelector( + (state) => state.planModules ); const { activeTab, newModule, setNewModule } = usePlanContext(); const { t } = useTranslation(); @@ -139,9 +139,14 @@ export const ModulesList = () => { )} {groupConfig.map((group) => { - const groupModules = group.modules.filter((module) => - currentModules.includes(module) - ); + // Filter modules that are in currentModules and don't have variant "hidden" + const groupModules = group.modules.filter((module) => { + if (!currentModules.includes(module)) return false; + if (!Object.hasOwn(records, module)) return true; + const moduleRecord = records[module]; + if (moduleRecord?.variant === 'hidden') return false; + return true; + }); if (groupModules.length === 0) return null; return (
diff --git a/src/pages/Plan/modules/Factory/index.tsx b/src/pages/Plan/modules/Factory/index.tsx index 53a8df25f..a365763b9 100644 --- a/src/pages/Plan/modules/Factory/index.tsx +++ b/src/pages/Plan/modules/Factory/index.tsx @@ -18,6 +18,7 @@ import { LanguageModule } from './modules/Language'; import { LiteracyModule } from './modules/Literacy'; import { LocalityModule } from './modules/Locality'; import { OutOfScopeModule } from './modules/OutOfScope'; +import { ACNSaverPersonasModule } from './modules/CustomModules/Accenture/ACNSaverPersonas'; import { SetupNoteModule } from './modules/SetupNote'; import { TargetNoteModule } from './modules/TargetNote'; import { TargetSizeModule } from './modules/TargetSize'; @@ -53,6 +54,7 @@ const modules: Record< [InternetHomeModule.slug]: InternetHomeModule, [TitleModule.slug]: TitleModule, [DatesModule.slug]: DatesModule, + [ACNSaverPersonasModule.slug]: ACNSaverPersonasModule, }; export function getModuleBySlug( diff --git a/src/pages/Plan/modules/Factory/modules/CustomModules/Accenture/ACNSaverPersonas/Component.tsx b/src/pages/Plan/modules/Factory/modules/CustomModules/Accenture/ACNSaverPersonas/Component.tsx new file mode 100644 index 000000000..920afdce1 --- /dev/null +++ b/src/pages/Plan/modules/Factory/modules/CustomModules/Accenture/ACNSaverPersonas/Component.tsx @@ -0,0 +1,13 @@ +import { useModule } from 'src/features/modules/useModule'; + +const ACNSaverPersonas = () => { + const { value } = useModule('acn_saver_personas'); + + if (value?.variant === 'hidden') { + return null; + } + + return
Personas Module - Default Variant
; +}; + +export default ACNSaverPersonas; diff --git a/src/pages/Plan/modules/Factory/modules/CustomModules/Accenture/ACNSaverPersonas/index.tsx b/src/pages/Plan/modules/Factory/modules/CustomModules/Accenture/ACNSaverPersonas/index.tsx new file mode 100644 index 000000000..82fc75e3d --- /dev/null +++ b/src/pages/Plan/modules/Factory/modules/CustomModules/Accenture/ACNSaverPersonas/index.tsx @@ -0,0 +1,16 @@ +import { createModuleDefinition } from '../../../../ModuleDefinition'; +import Personas from './Component'; +import useIcon from './useIcon'; +import useSubtitle from './useSubtitle'; +import useTitle from './useTitle'; + +export const ACNSaverPersonasModule = createModuleDefinition({ + slug: 'acn_saver_personas', + Component: Personas, + useTitle, + useIcon, + useSubtitle, + defaultData: [], + defaultVariant: 'default', + tab: 'setup', +}); diff --git a/src/pages/Plan/modules/Factory/modules/CustomModules/Accenture/ACNSaverPersonas/useIcon.tsx b/src/pages/Plan/modules/Factory/modules/CustomModules/Accenture/ACNSaverPersonas/useIcon.tsx new file mode 100644 index 000000000..e93f9bccb --- /dev/null +++ b/src/pages/Plan/modules/Factory/modules/CustomModules/Accenture/ACNSaverPersonas/useIcon.tsx @@ -0,0 +1,3 @@ +const useIcon = () => null; + +export default useIcon; diff --git a/src/pages/Plan/modules/Factory/modules/CustomModules/Accenture/ACNSaverPersonas/useSubtitle.tsx b/src/pages/Plan/modules/Factory/modules/CustomModules/Accenture/ACNSaverPersonas/useSubtitle.tsx new file mode 100644 index 000000000..306fba309 --- /dev/null +++ b/src/pages/Plan/modules/Factory/modules/CustomModules/Accenture/ACNSaverPersonas/useSubtitle.tsx @@ -0,0 +1,3 @@ +const useSubtitle = () => 'User personas'; + +export default useSubtitle; diff --git a/src/pages/Plan/modules/Factory/modules/CustomModules/Accenture/ACNSaverPersonas/useTitle.tsx b/src/pages/Plan/modules/Factory/modules/CustomModules/Accenture/ACNSaverPersonas/useTitle.tsx new file mode 100644 index 000000000..4ec110927 --- /dev/null +++ b/src/pages/Plan/modules/Factory/modules/CustomModules/Accenture/ACNSaverPersonas/useTitle.tsx @@ -0,0 +1,3 @@ +const useTitle = () => 'Personas'; + +export default useTitle; diff --git a/src/pages/Plan/modules/Factory/modules/Goal/Component.tsx b/src/pages/Plan/modules/Factory/modules/Goal/Component.tsx index 71a2321fa..daedb828f 100644 --- a/src/pages/Plan/modules/Factory/modules/Goal/Component.tsx +++ b/src/pages/Plan/modules/Factory/modules/Goal/Component.tsx @@ -6,6 +6,7 @@ import { FormField, IconButton, Label, + MD, Notification, SM, Span, @@ -234,12 +235,42 @@ const GoalContent = () => {
- +
+ + + + {t('__PLAN_PAGE_MODULE_GOAL_TOOLTIP_TITLE')} + + + }} + /> + + + } + > + + + + +
{ ref={editorRef} placeholderOptions={{ placeholder: t('__PLAN_PAGE_MODULE_GOAL_PLACEHOLDER'), + showOnlyCurrent: false, }} headerTitle={' '} disableSaveShortcut diff --git a/src/pages/Plan/modules/Factory/modules/Tasks/Component/parts/modal/CreateTaskListsWithAI.tsx b/src/pages/Plan/modules/Factory/modules/Tasks/Component/parts/modal/CreateTaskListsWithAI.tsx index 3d1b81d28..f2b853847 100644 --- a/src/pages/Plan/modules/Factory/modules/Tasks/Component/parts/modal/CreateTaskListsWithAI.tsx +++ b/src/pages/Plan/modules/Factory/modules/Tasks/Component/parts/modal/CreateTaskListsWithAI.tsx @@ -27,7 +27,6 @@ import { } from 'src/features/api'; import { useModuleTasksContext } from '../../context'; import { useModuleTasks } from '../../hooks'; -import { processItemOutput } from '../processItemOutput'; import { LoadingSpinner } from './LoadingSpinner'; // constants @@ -39,7 +38,6 @@ const MODULES_TO_PROMPT = [ 'language', 'touchpoints', ]; -const MAX_PROMPT_LENGTH = 102300; const CreateTaskListsWithAI = () => { const { setOutput, value: currentTasks } = useModuleTasks(); @@ -93,20 +91,18 @@ const CreateTaskListsWithAI = () => { const handleClick = async () => { // gather modules info to prepend to the user prompt - const modulesInfo = Object.entries(records) - .filter(([key]) => MODULES_TO_PROMPT.includes(key)) - .map( - ([key, item]) => - `Module: ${key}, Config: ${JSON.stringify(processItemOutput(item))}` + const context = JSON.stringify( + Object.entries(records.records).filter(([key]) => + MODULES_TO_PROMPT.includes(key) ) - .join('\n'); - const fullPrompt = `User prompt:\n${userPrompt}\n[Modules info]:\n${modulesInfo}`; + ); await postServicesApiKUsecases({ body: { planId: planId || '', count: usecaseNumber as number, // usecaseNumber always have a value here because the button is disabled when it's undefined - requirements: fullPrompt.slice(0, MAX_PROMPT_LENGTH), + requirements: userPrompt, + context, }, }); }; diff --git a/src/pages/Plan/modules/Factory/modules/Tasks/Component/parts/processItemOutput.ts b/src/pages/Plan/modules/Factory/modules/Tasks/Component/parts/processItemOutput.ts deleted file mode 100644 index 1a3596fd0..000000000 --- a/src/pages/Plan/modules/Factory/modules/Tasks/Component/parts/processItemOutput.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Helper function to process module item output for better readability in the AI prompt -export const processItemOutput = (item: any): any => { - if (Array.isArray(item)) { - // If it's an array, process each item in the array - return item.map((subItem) => processItemOutput(subItem)); - } - if (typeof item === 'object' && item !== null) { - // recursively process nested values - return Object.fromEntries( - Object.entries(item) - // here we can filter out any keys we don't want to include in the prompt, for example 'id' - .filter(([key]) => key !== 'id') - .map(([key, value]) => [key, processItemOutput(value)]) - ); - } - return item; -}; diff --git a/src/pages/Plan/modules/Factory/modules/Touchpoints/Component/index.tsx b/src/pages/Plan/modules/Factory/modules/Touchpoints/Component/index.tsx index 84b871bb8..e93438cd5 100644 --- a/src/pages/Plan/modules/Factory/modules/Touchpoints/Component/index.tsx +++ b/src/pages/Plan/modules/Factory/modules/Touchpoints/Component/index.tsx @@ -1,10 +1,19 @@ +import { useModule } from 'src/features/modules/useModule'; import { ModuleTouchpointsContextProvider } from './context'; import { TouchpointsList } from './parts'; -const TouchPoints = () => ( - - - -); +const TouchPoints = () => { + const { value } = useModule('touchpoints'); + + if (value?.variant === 'hidden') { + return null; + } + + return ( + + + + ); +}; export default TouchPoints; diff --git a/src/pages/Plan/modules/Factory/modules/Touchpoints/useIcon.tsx b/src/pages/Plan/modules/Factory/modules/Touchpoints/useIcon.tsx index 003a49daf..d7a3dcdff 100644 --- a/src/pages/Plan/modules/Factory/modules/Touchpoints/useIcon.tsx +++ b/src/pages/Plan/modules/Factory/modules/Touchpoints/useIcon.tsx @@ -1,12 +1,18 @@ import { ReactComponent as TouchpointsIcon } from 'src/assets/icons/touchpoints-icon.svg'; +import { useModule } from 'src/features/modules/useModule'; import useIconColor from '../../useIconColor'; const useIcon = (withValidation: boolean = true) => { + const { value } = useModule('touchpoints'); const color = useIconColor({ module_type: 'touchpoints', withValidation, }); + if (value?.variant === 'hidden') { + return null; + } + return ; }; diff --git a/src/pages/Plan/navigation/aside/AddBlockButton.tsx b/src/pages/Plan/navigation/aside/AddBlockButton.tsx index 4961002e6..67168472a 100644 --- a/src/pages/Plan/navigation/aside/AddBlockButton.tsx +++ b/src/pages/Plan/navigation/aside/AddBlockButton.tsx @@ -39,10 +39,17 @@ const AddBlockButton = () => { const { planComposedStatus } = usePlan(planId); const { activeTab } = usePlanContext(); const availableModules = getModulesByTab(activeTab.name); - const { currentModules } = useAppSelector((state) => state.planModules); + const { currentModules, records } = useAppSelector( + (state) => state.planModules + ); + // Filter out modules that are already added or have variant "hidden" const items = availableModules.filter((module_type) => { + // Don't show modules that are already added if (currentModules.find((module) => module === module_type)) return false; + // Don't show modules with variant "hidden" (preconfigurated) + const module = records[module_type]; + if (module?.variant === 'hidden') return false; return true; }); diff --git a/src/pages/Plan/navigation/aside/NavBody.tsx b/src/pages/Plan/navigation/aside/NavBody.tsx index 04f528da1..aed615dde 100644 --- a/src/pages/Plan/navigation/aside/NavBody.tsx +++ b/src/pages/Plan/navigation/aside/NavBody.tsx @@ -32,7 +32,9 @@ const NavBody = () => { // Sort availableModules according to group/order structure const groupConfig = MODULE_GROUPS[activeTab.name] || []; const { getPlanStatus } = useModuleConfiguration(); - const { currentModules } = useAppSelector((state) => state.planModules); + const { currentModules, records } = useAppSelector( + (state) => state.planModules + ); const { t } = useTranslation(); const { width } = useWindowSize(); const breakpointSm = parseInt(appTheme.breakpoints.sm, 10); @@ -49,9 +51,14 @@ const NavBody = () => { {groupConfig.map((group) => { // Get modules in this group that are present in currentModules - const groupModules = group.modules.filter((module) => - currentModules.includes(module) - ); + // and don't have variant "hidden" + const groupModules = group.modules.filter((module) => { + if (!currentModules.includes(module)) return false; + if (!Object.hasOwn(records, module)) return true; + const moduleRecord = records[module]; + if (moduleRecord?.variant === 'hidden') return false; + return true; + }); if (groupModules.length === 0) return null; return (
diff --git a/src/pages/Plan/navigation/aside/modal/AddBlockModal.tsx b/src/pages/Plan/navigation/aside/modal/AddBlockModal.tsx index 8c7fa1d83..9f2f232d8 100644 --- a/src/pages/Plan/navigation/aside/modal/AddBlockModal.tsx +++ b/src/pages/Plan/navigation/aside/modal/AddBlockModal.tsx @@ -26,15 +26,24 @@ const AddBlockModal = () => { const { t } = useTranslation(); const { modalRef, setModalRef } = usePlanNavContext(); const { activeTab } = usePlanContext(); - const { currentModules } = useAppSelector((state) => state.planModules); + const { currentModules, records } = useAppSelector( + (state) => state.planModules + ); const groupConfig = MODULE_GROUPS[activeTab.name] || []; // Build grouped items with enabled/disabled state + // Filter out modules with variant "hidden" const groupedItems = groupConfig.map((group) => { - const items = group.modules.map((module_type) => ({ - type: module_type as components['schemas']['Module']['type'], - enabled: !currentModules.includes(module_type), - })); + const items = group.modules + .filter((module_type) => { + // Hide modules with variant "hidden" from the add modal + const module = records[module_type]; + return module?.variant !== 'hidden'; + }) + .map((module_type) => ({ + type: module_type as components['schemas']['Module']['type'], + enabled: !currentModules.includes(module_type), + })); return { id: group.id, title: group.title, diff --git a/tests/api/plans/pid/_get/200_draft_mandatory_and_acn_hidden_personas_module.json b/tests/api/plans/pid/_get/200_draft_mandatory_and_acn_hidden_personas_module.json new file mode 100644 index 000000000..82cbe0242 --- /dev/null +++ b/tests/api/plans/pid/_get/200_draft_mandatory_and_acn_hidden_personas_module.json @@ -0,0 +1,54 @@ +{ + "config": { + "modules": [ + { + "output": "My Plan", + "type": "title", + "variant": "default" + }, + { + "output": { + "start": "2041-12-17T08:00:00.000Z" + }, + "type": "dates", + "variant": "default" + }, + { + "type": "acn_saver_personas", + "variant": "hidden", + "output": [ + "ACN.PRAGMATICO DIGITALE", + "ACN.EMERGENTE ASPIRAZIONALE", + "ACN.SOCIALE COLLABORATIVO", + "ACN.INVESTITORE SOFISTICATO", + "ACN.CONSERVATORE PRUDENTE" + ] + }, + { + "output": [ + { + "description": "description kind bug", + "kind": "bug", + "title": "Search for bugs", + "id": "bug-1" + }, + { + "description": "description kind video", + "kind": "video", + "title": "Think aloud", + "id": "video-1" + } + ], + "type": "tasks", + "variant": "default" + } + ] + }, + "id": 13, + "project": { + "id": 90, + "name": "MyProject" + }, + "status": "draft", + "workspace_id": 1 +} diff --git a/tests/e2e/plan/modules/goal.spec.ts b/tests/e2e/plan/modules/goal.spec.ts index 7ccd4b217..5aebdc5a2 100644 --- a/tests/e2e/plan/modules/goal.spec.ts +++ b/tests/e2e/plan/modules/goal.spec.ts @@ -54,6 +54,31 @@ test.describe('The title module defines the Plan title.', () => { ); }); + test('It should show the correct label', async () => { + await expect(goalModule.elements().moduleLabel()).toContainText( + planPage.i18n.t('__PLAN_PAGE_MODULE_GOAL_LABEL') + ); + }); + + test('It should show the placeholder when the input is empty', async () => { + await goalModule.elements().moduleInput().click(); + await goalModule.elements().moduleInput().press('Control+a'); + await goalModule.elements().moduleInput().press('Backspace'); + await expect(goalModule.elements().editorParagraph()).toHaveAttribute( + 'data-placeholder', + planPage.i18n.t('__PLAN_PAGE_MODULE_GOAL_PLACEHOLDER') + ); + }); + + test('It should show the info button next to the label', async () => { + await expect(goalModule.elements().infoButton()).toBeVisible(); + }); + + test('It should show a tooltip when hovering the info button', async () => { + await goalModule.elements().infoButton().hover(); + await expect(goalModule.elements().tooltipTitle()).toBeVisible(); + }); + test('It should allow improving the goal with AI only when word count >= 4', async ({ page, }) => { diff --git a/tests/fixtures/pages/Plan/Module_goal.ts b/tests/fixtures/pages/Plan/Module_goal.ts index 85b41c714..ed80d9d4b 100644 --- a/tests/fixtures/pages/Plan/Module_goal.ts +++ b/tests/fixtures/pages/Plan/Module_goal.ts @@ -19,6 +19,13 @@ export class GoalModule { tab: () => this.page.getByTestId('setup-tab'), moduleError: () => module.getByTestId('goal-error'), moduleInput: () => module.locator('[role="textbox"]').first(), + moduleLabel: () => module.locator('label'), + infoButton: () => module.getByTestId('goal-info-button'), + tooltipTitle: () => + this.page.getByText( + this.i18n.t('__PLAN_PAGE_MODULE_GOAL_TOOLTIP_TITLE') + ), + editorParagraph: () => module.locator('.ProseMirror p').first(), aiButton: () => module.getByRole('button', { name: this.i18n.t('GENERATE_WITH_AI_CTA_LABEL'),