diff --git a/scripts/extract-step-definitions.ts b/scripts/extract-step-definitions.ts index cb96992..76d0fea 100644 --- a/scripts/extract-step-definitions.ts +++ b/scripts/extract-step-definitions.ts @@ -33,10 +33,12 @@ function extractStepDefinitions(): StepDefinition[] { const line = lines[i]; // Match Given/When/Then at the start of a line - const match = line.match(/^(Given|When|Then)\s*\(\s*['"`]([^'"`]+)['"`]/); - if (match) { + // Handle escaped quotes in patterns (e.g., 'l\'écran contient') + const match = line?.match(/^(Given|When|Then)\s*\(\s*(['"`])((?:[^\\]|\\.)*?)\2/); + if (match && match[3]) { const keyword = match[1] as 'Given' | 'When' | 'Then'; - const pattern = match[2]; + // Unescape the pattern (remove backslashes before quotes) + const pattern = match[3].replace(/\\(['"`])/g, '$1'); // Extract the full function body const sourceCode = extractFunctionBody(lines, i); diff --git a/src/components/specs/FeatureView.tsx b/src/components/specs/FeatureView.tsx index 83e6906..8f5963a 100644 --- a/src/components/specs/FeatureView.tsx +++ b/src/components/specs/FeatureView.tsx @@ -115,7 +115,6 @@ export function FeatureView({ feature, onBack, onSelectScreen, onSelectStory }: diff --git a/src/components/specs/GherkinHighlighter.tsx b/src/components/specs/GherkinHighlighter.tsx index 2826b2f..72056cd 100644 --- a/src/components/specs/GherkinHighlighter.tsx +++ b/src/components/specs/GherkinHighlighter.tsx @@ -1,12 +1,7 @@ -import React, { useState, useMemo } from 'react'; -import { ChevronDown, ChevronRight, ChevronsDownUp, ChevronsUpDown, Code2 } from 'lucide-react'; +import React, { useState, useMemo, useRef, useEffect } from 'react'; +import { ChevronDown, ChevronRight, ChevronsDownUp, ChevronsUpDown, Code2, CheckCircle2, XCircle, AlertCircle, Clock, Table2 } from 'lucide-react'; import { Button } from '../ui/button'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '../ui/tooltip'; +import { Card, CardContent, CardHeader } from '../ui/card'; import { findStepDefinition, type StepDefinitionInfo } from '../../data/stepDefinitions'; interface ScenarioResult { @@ -18,7 +13,6 @@ interface ScenarioResult { interface GherkinHighlighterProps { content: string; scenarioResults?: ScenarioResult[]; - filePath?: string; } interface ParsedBlock { @@ -41,25 +35,28 @@ const keywords = { examples: ['Exemples:', 'Examples:'], }; -export function GherkinHighlighter({ content, scenarioResults = [], filePath }: GherkinHighlighterProps) { +export function GherkinHighlighter({ content, scenarioResults = [] }: GherkinHighlighterProps) { const lines = content.split('\n'); // Parse content into blocks const blocks = useMemo(() => parseBlocks(lines, scenarioResults), [lines, scenarioResults]); - // Determine initial collapsed state - collapsed by default, open if failed + // Determine initial collapsed state - scenarios collapsed by default (open if failed), background always open const initialCollapsed = useMemo(() => { const state: Record = {}; blocks.forEach((block, index) => { - if (block.type === 'scenario' || block.type === 'background') { + if (block.type === 'scenario') { state[index] = block.status !== 'failed'; + } else if (block.type === 'background') { + // Background is always expanded + state[index] = false; } }); return state; }, [blocks]); const [collapsed, setCollapsed] = useState>(initialCollapsed); - const [showDefinitions, setShowDefinitions] = useState(false); + const [showDefinitions, setShowDefinitions] = useState(true); const toggleBlock = (index: number) => { setCollapsed(prev => ({ ...prev, [index]: !prev[index] })); @@ -76,76 +73,65 @@ export function GherkinHighlighter({ content, scenarioResults = [], filePath }: const collapseAll = () => { const newState: Record = {}; blocks.forEach((block, index) => { - if (block.type === 'scenario' || block.type === 'background') { + if (block.type === 'scenario') { newState[index] = true; } + // Background stays expanded }); setCollapsed(newState); }; - const collapsibleCount = blocks.filter(b => b.type === 'scenario' || b.type === 'background').length; - const collapsedCount = Object.values(collapsed).filter(Boolean).length; - const allCollapsed = collapsedCount === collapsibleCount; + const scenarioCount = blocks.filter(b => b.type === 'scenario').length; + const collapsedScenarioCount = blocks.filter((b, i) => b.type === 'scenario' && collapsed[i]).length; + const allCollapsed = collapsedScenarioCount === scenarioCount; return ( - -
- {/* Toolbar */} -
-
- - -
- {filePath && ( - - {filePath} - +
+ {/* Toolbar */} +
+
- - {/* Content */} -
-
-            
-              {blocks.map((block, blockIndex) => (
-                 toggleBlock(blockIndex)}
-                  showDefinitions={showDefinitions}
-                />
-              ))}
-            
-          
-
+ +
- + + {/* Scenario/Background Blocks */} + {blocks.filter(b => b.type !== 'header').map((block, blockIndex) => ( + toggleBlock(blocks.indexOf(block))} + showDefinitions={showDefinitions} + /> + ))} +
); } @@ -220,89 +206,371 @@ interface BlockRendererProps { function BlockRenderer({ block, isCollapsed, onToggle, showDefinitions }: BlockRendererProps) { if (block.type === 'header') { - return ( - <> - {block.lines.map((line, index) => ( - - ))} - - ); + // Header is now handled separately in the main component + return null; } - const firstLine = block.lines[0] ?? ''; const restLines = block.lines.slice(1); - const isCollapsible = block.type === 'scenario' || block.type === 'background'; + const isBackground = block.type === 'background'; + + // Parse steps from rest lines + const parsedSteps = parseStepsFromLines(restLines); + + // Determine border color based on status + const borderColor = block.status === 'passed' ? 'border-l-green-500' : + block.status === 'failed' ? 'border-l-red-500' : + block.status === 'skipped' ? 'border-l-yellow-500' : + isBackground ? 'border-l-zinc-400' : 'border-l-cyan-500'; + + // Status icon + const StatusIcon = () => { + if (!block.status || block.status === 'unknown') return null; + if (block.status === 'passed') return ; + if (block.status === 'failed') return ; + if (block.status === 'skipped') return ; + return ; + }; return ( - <> - {/* Scenario/Background header line */} -
+ {/* Clickable header */} + - - {isCollapsible && ( - - {isCollapsed ? : } +
+ + {isCollapsed ? : } + + +
+ + {isBackground ? 'Contexte' : 'Scénario'} + + + {block.name} + +
+ {parsedSteps.length > 0 && ( + + {parsedSteps.length} étapes )} - {block.status && block.status !== 'unknown' && ( - - )} - {highlightLine(firstLine, false)} - -
+
+ {/* Collapsible content */} - {!isCollapsed && restLines.map((line, index) => ( - - ))} + {!isCollapsed && ( + +
+ {parsedSteps.map((step, index) => ( + + ))} +
- {/* Error message for failed scenarios */} - {!isCollapsed && block.status === 'failed' && block.errorMessage && ( -
-
Erreur:
-
-            {block.errorMessage}
-          
-
+ {/* Error message for failed scenarios */} + {block.status === 'failed' && block.errorMessage && ( +
+
Erreur:
+
+                {block.errorMessage}
+              
+
+ )} +
)} - + ); } -interface LineRendererProps { - line: string; +interface ParsedStep { + type: 'given' | 'when' | 'then' | 'and' | 'examples' | 'table' | 'other'; + keyword: string; + text: string; + originalLine: string; + tableRows?: string[][]; +} + +function parseStepsFromLines(lines: string[]): ParsedStep[] { + const steps: ParsedStep[] = []; + let currentStep: ParsedStep | null = null; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + // Check for table row + if (trimmed.startsWith('|')) { + const cells = trimmed.split('|').slice(1, -1).map(c => c.trim()); + if (currentStep) { + if (!currentStep.tableRows) currentStep.tableRows = []; + currentStep.tableRows.push(cells); + } else { + // Standalone table row (shouldn't happen, but handle it) + steps.push({ + type: 'table', + keyword: '', + text: trimmed, + originalLine: line, + tableRows: [cells] + }); + } + continue; + } + + // Check for step keywords + let matched = false; + + for (const kw of keywords.given) { + if (trimmed.startsWith(kw)) { + if (currentStep) steps.push(currentStep); + currentStep = { type: 'given', keyword: kw, text: trimmed.slice(kw.length).trim(), originalLine: line }; + matched = true; + break; + } + } + if (!matched) { + for (const kw of keywords.when) { + if (trimmed.startsWith(kw)) { + if (currentStep) steps.push(currentStep); + currentStep = { type: 'when', keyword: kw, text: trimmed.slice(kw.length).trim(), originalLine: line }; + matched = true; + break; + } + } + } + if (!matched) { + for (const kw of keywords.then) { + if (trimmed.startsWith(kw)) { + if (currentStep) steps.push(currentStep); + currentStep = { type: 'then', keyword: kw, text: trimmed.slice(kw.length).trim(), originalLine: line }; + matched = true; + break; + } + } + } + if (!matched) { + for (const kw of keywords.and) { + if (trimmed.startsWith(kw)) { + if (currentStep) steps.push(currentStep); + currentStep = { type: 'and', keyword: kw, text: trimmed.slice(kw.length).trim(), originalLine: line }; + matched = true; + break; + } + } + } + if (!matched) { + for (const kw of keywords.examples) { + if (trimmed.startsWith(kw)) { + if (currentStep) steps.push(currentStep); + currentStep = { type: 'examples', keyword: kw, text: trimmed.slice(kw.length).trim(), originalLine: line }; + matched = true; + break; + } + } + } + if (!matched && trimmed) { + if (currentStep) steps.push(currentStep); + currentStep = { type: 'other', keyword: '', text: trimmed, originalLine: line }; + } + } + + if (currentStep) steps.push(currentStep); + + return steps; +} + +interface StepRendererProps { + step: ParsedStep; showDefinitions: boolean; } -function LineRenderer({ line, showDefinitions }: LineRendererProps) { - const trimmed = line.trim(); - const isStep = [...keywords.given, ...keywords.when, ...keywords.then, ...keywords.and] - .some(kw => trimmed.startsWith(kw)); +function StepRenderer({ step, showDefinitions }: StepRendererProps) { + // Always check for step definition to show dotted underline + const stepDef = step.type !== 'table' && step.type !== 'other' && step.type !== 'examples' + ? findStepDefinition(step.originalLine.trim()) + : null; - const stepDef = isStep && showDefinitions ? findStepDefinition(trimmed) : null; + // Keyword colors + const keywordColor = step.type === 'given' ? 'text-blue-600 dark:text-blue-400' : + step.type === 'when' ? 'text-amber-600 dark:text-amber-400' : + step.type === 'then' ? 'text-green-600 dark:text-green-400' : + step.type === 'and' ? 'text-zinc-500 dark:text-zinc-400' : + step.type === 'examples' ? 'text-purple-600 dark:text-purple-400' : + 'text-muted-foreground'; + + const keywordBg = step.type === 'given' ? 'bg-blue-50 dark:bg-blue-950/30' : + step.type === 'when' ? 'bg-amber-50 dark:bg-amber-950/30' : + step.type === 'then' ? 'bg-green-50 dark:bg-green-950/30' : + step.type === 'and' ? 'bg-zinc-50 dark:bg-zinc-800/50' : + step.type === 'examples' ? 'bg-purple-50 dark:bg-purple-950/30' : + ''; + + if (step.type === 'table') { + return ( +
+ + {step.text} +
+ ); + } + + // Show popover only when definitions mode is active, but always show dotted underline for steps with definitions + const dottedUnderlineStyle = { + borderBottom: '1.3px dashed', + borderColor: 'rgb(161 161 170)', // zinc-400 + }; + const stepTextElement = stepDef ? ( + showDefinitions ? ( + + {highlightStringsInText(step.text)} + + ) : ( + + {highlightStringsInText(step.text)} + + ) + ) : ( + {highlightStringsInText(step.text)} + ); return ( -
- {highlightLineWithTooltip(line, stepDef)} +
+
+ {step.keyword && ( + + {step.keyword} + + )} + + {stepTextElement} + +
+ {/* Render table if present */} + {step.tableRows && step.tableRows.length > 0 && ( +
+ + + {step.tableRows.map((row, rowIndex) => ( + + {row.map((cell, cellIndex) => ( + + ))} + + ))} + +
+ {cell} +
+
+ )}
); } +function highlightStringsInText(text: string): React.ReactNode { + const parts: React.ReactNode[] = []; + let lastIndex = 0; + const regex = /"[^"]*"/g; + let match; + + while ((match = regex.exec(text)) !== null) { + if (match.index > lastIndex) { + parts.push({text.slice(lastIndex, match.index)}); + } + parts.push( + + {match[0]} + + ); + lastIndex = regex.lastIndex; + } + + if (lastIndex < text.length) { + parts.push({text.slice(lastIndex)}); + } + + return parts.length > 0 ? <>{parts} : text; +} + +// Click-based popover for step definitions (works on mobile and desktop) +function StepDefinitionPopover({ + stepDef, + children +}: { + stepDef: StepDefinitionInfo; + children: React.ReactNode; +}) { + const [isOpen, setIsOpen] = useState(false); + const triggerRef = useRef(null); + const popoverRef = useRef(null); + + // Close on click outside + useEffect(() => { + if (!isOpen) return; + + const handleClickOutside = (e: MouseEvent) => { + if ( + popoverRef.current && + !popoverRef.current.contains(e.target as Node) && + triggerRef.current && + !triggerRef.current.contains(e.target as Node) + ) { + setIsOpen(false); + } + }; + + // Close on escape key + const handleEscape = (e: KeyboardEvent) => { + if (e.key === 'Escape') setIsOpen(false); + }; + + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleEscape); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleEscape); + }; + }, [isOpen]); + + const dottedUnderlineStyle = { + borderBottom: '1.3px dashed rgb(161 161 170)', // zinc-400 + }; + + return ( + + setIsOpen(!isOpen)} + className="cursor-pointer" + style={dottedUnderlineStyle} + > + {children} + + {isOpen && ( +
+ +
+ )} +
+ ); +} + function SourceCodePopup({ stepDef }: { stepDef: StepDefinitionInfo }) { const lines = stepDef.sourceCode.split('\n'); @@ -402,172 +670,3 @@ function highlightTypeScript(code: string): React.ReactNode { return <>{parts}; } - -function highlightLine(line: string, hasDefinition: boolean): React.ReactNode { - const trimmed = line.trim(); - - // Check for tags - if (trimmed.startsWith('@')) { - return {line}; - } - - // Check for comments - if (trimmed.startsWith('#') && !trimmed.includes('language:')) { - return {line}; - } - - // Check for language declaration - if (trimmed.includes('# language:')) { - return {line}; - } - - // Check for feature keywords - for (const kw of keywords.feature) { - if (trimmed.startsWith(kw)) { - return highlightKeyword(line, kw, 'text-purple-400 font-semibold', hasDefinition); - } - } - - // Check for background - for (const kw of keywords.background) { - if (trimmed.startsWith(kw)) { - return highlightKeyword(line, kw, 'text-zinc-400 font-medium', hasDefinition); - } - } - - // Check for scenario keywords - for (const kw of keywords.scenario) { - if (trimmed.startsWith(kw)) { - return highlightKeyword(line, kw, 'text-cyan-400 font-medium', hasDefinition); - } - } - - // Check for step keywords - for (const kw of [...keywords.given, ...keywords.when, ...keywords.then, ...keywords.and]) { - if (trimmed.startsWith(kw)) { - const color = keywords.given.includes(kw) ? 'text-blue-400' : - keywords.when.includes(kw) ? 'text-amber-400' : - keywords.then.includes(kw) ? 'text-green-400' : - 'text-zinc-400'; - return highlightKeyword(line, kw, color, hasDefinition); - } - } - - // Check for examples - for (const kw of keywords.examples) { - if (trimmed.startsWith(kw)) { - return highlightKeyword(line, kw, 'text-zinc-400 font-medium', hasDefinition); - } - } - - // Check for table rows - if (trimmed.startsWith('|')) { - return {line}; - } - - return {line}; -} - -function highlightLineWithTooltip(line: string, stepDef: StepDefinitionInfo | null): React.ReactNode { - const trimmed = line.trim(); - - // Find which step keyword this line starts with - const allStepKeywords = [...keywords.given, ...keywords.when, ...keywords.then, ...keywords.and]; - let matchedKeyword: string | null = null; - for (const kw of allStepKeywords) { - if (trimmed.startsWith(kw)) { - matchedKeyword = kw; - break; - } - } - - // If no step keyword or no definition, use regular highlighting - if (!matchedKeyword || !stepDef) { - return highlightLine(line, false); - } - - // Split the line: indentation + keyword + step text - const index = line.indexOf(matchedKeyword); - const before = line.slice(0, index); - const after = line.slice(index + matchedKeyword.length); - - // Determine keyword color - const color = keywords.given.includes(matchedKeyword) ? 'text-blue-400' : - keywords.when.includes(matchedKeyword) ? 'text-amber-400' : - keywords.then.includes(matchedKeyword) ? 'text-green-400' : - 'text-zinc-400'; - - // Separate leading space from the step text - const leadingSpaceMatch = after.match(/^(\s*)/); - const leadingSpace = leadingSpaceMatch?.[1] ?? ''; - const stepText = after.slice(leadingSpace.length); - - // Wrap only the step text (after keyword and space) with tooltip - const stepTextElement = ( - - - - {highlightStrings(stepText)} - - - - - - - ); - - return ( - - {before} - {matchedKeyword} - {leadingSpace} - {stepTextElement} - - ); -} - -function highlightKeyword(line: string, keyword: string, className: string, hasDefinition: boolean): React.ReactNode { - const index = line.indexOf(keyword); - const before = line.slice(0, index); - const after = line.slice(index + keyword.length); - - return ( - - {before} - {keyword} - - {highlightStrings(after)} - - - ); -} - -function highlightStrings(text: string): React.ReactNode { - const parts: React.ReactNode[] = []; - let lastIndex = 0; - const regex = /"[^"]*"/g; - let match; - - while ((match = regex.exec(text)) !== null) { - if (match.index > lastIndex) { - parts.push({text.slice(lastIndex, match.index)}); - } - parts.push( - - {match[0]} - - ); - lastIndex = regex.lastIndex; - } - - if (lastIndex < text.length) { - parts.push({text.slice(lastIndex)}); - } - - return parts.length > 0 ? <>{parts} : text; -} diff --git a/src/data/stepDefinitions.ts b/src/data/stepDefinitions.ts index fd790ff..e3828da 100644 --- a/src/data/stepDefinitions.ts +++ b/src/data/stepDefinitions.ts @@ -18,7 +18,7 @@ export const stepDefinitions: StepDefinitionInfo[] = [ "lineNumber": 31 }, { - "pattern": "je suis connecté en tant qu\\", + "pattern": "je suis connecté en tant qu'utilisateur", "keyword": "Given", "file": "navigation.steps.ts", "sourceCode": "Given('je suis connecté en tant qu\\'utilisateur', async function (this: FestipodWorld) {\n this.isAuthenticated = true;\n});", @@ -88,7 +88,7 @@ export const stepDefinitions: StepDefinitionInfo[] = [ "lineNumber": 73 }, { - "pattern": "je vois l\\", + "pattern": "je vois l'écran {string}", "keyword": "Then", "file": "navigation.steps.ts", "sourceCode": "Then('je vois l\\'écran {string}', async function (this: FestipodWorld, pageName: string) {\n const screenId = resolveScreenId(pageName);\n expect(this.currentScreenId).to.equal(screenId);\n});", @@ -102,28 +102,35 @@ export const stepDefinitions: StepDefinitionInfo[] = [ "lineNumber": 83 }, { - "pattern": "l\\", + "pattern": "l'écran contient une section {string}", "keyword": "Then", "file": "navigation.steps.ts", - "sourceCode": "Then('l\\'écran contient une section {string}', async function (this: FestipodWorld, sectionName: string) {\n expect(this.currentScreenId).to.not.be.null;\n this.attach(`Verified section: ${sectionName}`, 'text/plain');\n});", + "sourceCode": "Then('l\\'écran contient une section {string}', async function (this: FestipodWorld, sectionName: string) {\n const hasSection = this.hasText(sectionName);\n expect(hasSection, `Section \"${sectionName}\" should be visible on screen \"${this.currentScreenId}\"`).to.be.true;\n});", "lineNumber": 88 }, + { + "pattern": "je peux annuler et revenir à l'écran précédent", + "keyword": "Then", + "file": "navigation.steps.ts", + "sourceCode": "Then('je peux annuler et revenir à l\\'écran précédent', async function (this: FestipodWorld) {\n expect(this.currentScreenId).to.equal('create-event');\n // CreateEventScreen has a ✕ close button in the header with onClick={() => navigate('home')}\n const source = this.getRenderedText();\n const hasCloseButton = /onClick[^>]*>[^<]*✕/.test(source);\n expect(hasCloseButton, 'Create event screen should have a close button (✕) with navigation action').to.be.true;\n});", + "lineNumber": 93 + }, { "pattern": "je peux naviguer vers {string}", "keyword": "Then", "file": "navigation.steps.ts", "sourceCode": "Then('je peux naviguer vers {string}', async function (this: FestipodWorld, pageName: string) {\n const screenId = resolveScreenId(pageName);\n this.attach(`Navigation available to: ${screenId}`, 'text/plain');\n});", - "lineNumber": 93 + "lineNumber": 101 }, { "pattern": "la navigation affiche {string} comme actif", "keyword": "Then", "file": "navigation.steps.ts", "sourceCode": "Then('la navigation affiche {string} comme actif', async function (this: FestipodWorld, menuItem: string) {\n this.attach(`Active menu: ${menuItem}`, 'text/plain');\n});", - "lineNumber": 98 + "lineNumber": 106 }, { - "pattern": "l\\", + "pattern": "l'écran {string} est affiché", "keyword": "Given", "file": "form.steps.ts", "sourceCode": "Given('l\\'écran {string} est affiché', async function (this: FestipodWorld, screenName: string) {\n const screenId = screenName.toLowerCase().replace(/ /g, '-');\n this.navigateTo(`#/demo/${screenId}`);\n});", @@ -214,7 +221,7 @@ export const stepDefinitions: StepDefinitionInfo[] = [ "lineNumber": 6 }, { - "pattern": "je peux voir les détails de l\\", + "pattern": "je peux voir les détails de l'événement", "keyword": "Then", "file": "screen.steps.ts", "sourceCode": "Then('je peux voir les détails de l\\'événement', async function (this: FestipodWorld) {\n expect(this.currentScreenId).to.equal('event-detail');\n // Verify event detail content is rendered\n const hasEventInfo = this.hasText('Description') || this.hasText('Participant') || this.hasText('inscrits');\n expect(hasEventInfo, 'Event detail page should show event information').to.be.true;\n});", @@ -242,7 +249,7 @@ export const stepDefinitions: StepDefinitionInfo[] = [ "lineNumber": 36 }, { - "pattern": "je peux voir le profil de l\\", + "pattern": "je peux voir le profil de l'utilisateur", "keyword": "Then", "file": "screen.steps.ts", "sourceCode": "Then('je peux voir le profil de l\\'utilisateur', async function (this: FestipodWorld) {\n expect(this.currentScreenId).to.equal('user-profile');\n const hasProfileContent = this.hasText('Profil') || this.hasText('@');\n expect(hasProfileContent, 'User profile should display profile information').to.be.true;\n});", @@ -284,7 +291,7 @@ export const stepDefinitions: StepDefinitionInfo[] = [ "lineNumber": 74 }, { - "pattern": "je visualise l\\", + "pattern": "je visualise l'événement {string}", "keyword": "Given", "file": "screen.steps.ts", "sourceCode": "Given('je visualise l\\'événement {string}', async function (this: FestipodWorld, eventName: string) {\n this.navigateTo('#/demo/event-detail');\n expect(this.currentScreen, 'Event detail screen should be loaded').to.not.be.null;\n this.attach(`Viewing event: ${eventName}`, 'text/plain');\n});", @@ -298,14 +305,14 @@ export const stepDefinitions: StepDefinitionInfo[] = [ "lineNumber": 85 }, { - "pattern": "l\\", + "pattern": "l'écran affiche les informations de l'événement", "keyword": "Then", "file": "screen.steps.ts", "sourceCode": "Then('l\\'écran affiche les informations de l\\'événement', async function (this: FestipodWorld) {\n expect(this.currentScreenId).to.equal('event-detail');\n // Verify actual content is rendered\n const expectedContent = screenExpectedContent['event-detail'] || [];\n const renderedText = this.getRenderedText();\n\n let foundCount = 0;\n for (const content of expectedContent) {\n if (renderedText.includes(content)) {\n foundCount++;\n }\n }\n\n expect(foundCount, `At least one expected content item should be present`).to.be.greaterThan(0);\n});", "lineNumber": 91 }, { - "pattern": "l\\", + "pattern": "l'écran affiche les informations du profil", "keyword": "Then", "file": "screen.steps.ts", "sourceCode": "Then('l\\'écran affiche les informations du profil', async function (this: FestipodWorld) {\n expect(['profile', 'user-profile']).to.include(this.currentScreenId);\n // Verify profile info is rendered\n const hasProfileInfo = this.hasText('Profil') || this.hasText('@') || this.hasText('Événement');\n expect(hasProfileInfo, 'Profile information should be displayed').to.be.true;\n});", @@ -347,28 +354,28 @@ export const stepDefinitions: StepDefinitionInfo[] = [ "lineNumber": 152 }, { - "pattern": "je peux m\\", + "pattern": "je peux m'inscrire à l'événement", "keyword": "Then", "file": "screen.steps.ts", "sourceCode": "Then('je peux m\\'inscrire à l\\'événement', async function (this: FestipodWorld) {\n expect(this.currentScreenId).to.equal('event-detail');\n // Check for registration button\n const hasRegisterFeature = this.hasText('inscription') || this.hasText('Participer') ||\n this.hasText('participer') || this.hasText('S\\'inscrire') ||\n this.hasText('Rejoindre');\n expect(hasRegisterFeature, 'Registration feature should be available').to.be.true;\n});", "lineNumber": 158 }, { - "pattern": "je peux me désinscrire de l\\", + "pattern": "je peux me désinscrire de l'événement", "keyword": "Then", "file": "screen.steps.ts", "sourceCode": "Then('je peux me désinscrire de l\\'événement', async function (this: FestipodWorld) {\n expect(this.currentScreenId).to.equal('event-detail');\n // Unregister is typically on the same page as register\n const hasUnregisterFeature = this.hasText('désinscri') || this.hasText('Annuler') ||\n this.hasText('Quitter') || this.hasElement('button');\n expect(hasUnregisterFeature, 'Unregister feature should be available').to.be.true;\n});", "lineNumber": 167 }, { - "pattern": "je peux contacter l\\", + "pattern": "je peux contacter l'utilisateur", "keyword": "Then", "file": "screen.steps.ts", "sourceCode": "Then('je peux contacter l\\'utilisateur', async function (this: FestipodWorld) {\n expect(this.currentScreenId).to.equal('user-profile');\n // Check for contact functionality\n const hasContactFeature = this.hasText('Contact') || this.hasText('Message') ||\n this.hasText('message') || this.hasElement('button');\n expect(hasContactFeature, 'Contact feature should be available').to.be.true;\n});", "lineNumber": 175 }, { - "pattern": "je peux voir les événements auxquels l\\", + "pattern": "je peux voir les événements auxquels l'utilisateur a participé", "keyword": "Then", "file": "screen.steps.ts", "sourceCode": "Then('je peux voir les événements auxquels l\\'utilisateur a participé', async function (this: FestipodWorld) {\n expect(this.currentScreenId).to.equal('user-profile');\n // Check for user's events\n const hasUserEvents = this.hasText('Événement') || this.hasText('événement') ||\n this.hasText('Participation') || this.hasText('participation');\n expect(hasUserEvents, 'User events should be visible').to.be.true;\n});", @@ -389,7 +396,7 @@ export const stepDefinitions: StepDefinitionInfo[] = [ "lineNumber": 198 }, { - "pattern": "je peux définir mes thématiques d\\", + "pattern": "je peux définir mes thématiques d'intérêt", "keyword": "Then", "file": "screen.steps.ts", "sourceCode": "Then('je peux définir mes thématiques d\\'intérêt', async function (this: FestipodWorld) {\n expect(this.currentScreenId).to.equal('settings');\n // Settings page should allow configuring interests (or it could be on profile)\n // For now just verify we're on settings\n expect(this.currentScreen, 'Settings screen should be loaded').to.not.be.null;\n});",