diff --git a/docs/evidence/retained-ui-bugs-before-after.mp4 b/docs/evidence/retained-ui-bugs-before-after.mp4 new file mode 100644 index 000000000..5b874abdd Binary files /dev/null and b/docs/evidence/retained-ui-bugs-before-after.mp4 differ diff --git a/e2e/quality-backlog.spec.ts b/e2e/quality-backlog.spec.ts new file mode 100644 index 000000000..ab3700a54 --- /dev/null +++ b/e2e/quality-backlog.spec.ts @@ -0,0 +1,107 @@ +import { expect, test } from '@playwright/test'; +import { setLocale, stubSupabaseSession } from './helpers/auth'; + +const locales = ['en', 'es', 'hu'] as const; +const titledRoutes = ['generate', 'attack', 'brainstorm', 'factoids', 'ask'] as const; + +test('Sources does not show the stale consultation date', async ({ page }) => { + await page.goto('/expocit/sources'); + + await expect(page.getByRole('heading', { name: 'Sources' })).toBeVisible(); + await expect(page.getByText(/Consulta: 8 de marzo de 2026/i)).toHaveCount(0); +}); + +for (const locale of locales) { + test(`navigation and page titles agree in ${locale}`, async ({ page }, testInfo) => { + await setLocale(page, locale); + await stubSupabaseSession(page); + await page.addInitScript(() => { + window.localStorage.setItem('debate-trainer-onboarding', JSON.stringify({ + state: { hasCompletedOnboarding: true, isTourRunning: false }, + version: 0, + })); + }); + await page.goto('/app'); + + for (const route of titledRoutes) { + const desktopLink = page.locator(`[data-tour="nav-${route}"]`).first(); + const label = await desktopLink.getAttribute('aria-label'); + expect(label).toBeTruthy(); + if (testInfo.project.name === 'mobile-chromium') { + await page.locator('header button.lg\\:hidden[aria-expanded]').click(); + await page.getByRole('navigation', { name: 'Mobile navigation' }) + .getByRole('link') + .filter({ hasText: label! }) + .first() + .click(); + } else { + await desktopLink.click(); + } + await expect(page).toHaveURL(new RegExp(`/app/${route}$`)); + await expect(page.getByRole('heading', { level: 1, name: label!, exact: true }).last()).toBeVisible(); + } + }); +} + +test('Factoids names the save action with its claim', async ({ page }) => { + await setLocale(page, 'en'); + await stubSupabaseSession(page); + await page.route('**/functions/v1/ai', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + output: { + factoids: [{ + id: 'fact-1', + shortClaim: 'Solar costs fell by 42 percent', + keyFigure: '42%', + context: 'The official report compares the relevant period.', + limitations: 'The result depends on the measured market segment.', + citation: { + title: 'Official energy statistics', + publisher: 'Department of Energy', + date: '2024', + url: 'https://energy.gov/reports/solar-costs-2024', + verified: 'verified', + }, + howToUse: 'Use this as a bounded cost trend.', + relevanceScore: 92, + }], + searchSuggestions: [], + warnings: [], + verificationStatus: 'complete', + }, + }), + }); + }); + + await page.goto('/app/factoids'); + await page.getByRole('textbox', { name: /statement/i }).fill('Solar energy costs are falling'); + await page.getByRole('button', { name: /find factoids/i }).click(); + + await expect(page.getByRole('button', { + name: 'Save Solar costs fell by 42 percent to library', + })).toBeVisible(); +}); + +test('Flowchart confirms selected-node deletion and reports success', async ({ page }, testInfo) => { + await setLocale(page, 'en'); + await stubSupabaseSession(page); + await page.goto('/app/flowchart'); + + await page.getByText('Argument 1', { exact: true }).click(); + await page.getByRole('button', { name: 'Delete' }).last().click(); + await expect(page.getByRole('alertdialog', { name: 'Delete node?' })).toBeVisible(); + const confirm = page.getByRole('button', { name: 'Delete node' }); + if (testInfo.project.name === 'mobile-chromium') { + const box = await confirm.boundingBox(); + expect(box).not.toBeNull(); + await page.touchscreen.tap(box!.x + box!.width / 2, box!.y + box!.height / 2); + } else { + await confirm.click(); + } + + await expect(page.getByText('Node deleted', { exact: true })).toBeVisible(); + await expect(page.getByText('Argument 1', { exact: true })).toHaveCount(0); +}); diff --git a/src/components/features/factoids/FactoidsResults.tsx b/src/components/features/factoids/FactoidsResults.tsx index 8e2bce65e..0d601109a 100644 --- a/src/components/features/factoids/FactoidsResults.tsx +++ b/src/components/features/factoids/FactoidsResults.tsx @@ -81,6 +81,7 @@ function FactoidCard({ factoid, onSave }: { factoid: Factoid, onSave?: FactoidsR size="icon" className="h-8 w-8 text-muted-foreground hover:text-primary" onClick={() => onSave(factoid.shortClaim, `${factoid.context}\n\nKey Figure: ${factoid.keyFigure}\nLimitations: ${factoid.limitations}`, factoid.citation?.url)} + aria-label={t('factoidsResults.saveClaimToLibrary', { claim: factoid.shortClaim })} title={t('factoidsResults.saveToLibrary')} > diff --git a/src/locales/en.json b/src/locales/en.json index 763d01fee..062f101df 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -747,6 +747,7 @@ "urlNeedsVerification": "URL requires verification", "howToUse": "How to use:", "saveToLibrary": "Save to Library", + "saveClaimToLibrary": "Save {{claim}} to library", "discuss": "Discuss Evidence", "verifyAll": "Verify All Sources", "verifying": "Verifying Sources...", @@ -1000,7 +1001,12 @@ "saveFailedToast": "Failed to save flowchart", "loadedToast": "Flowchart loaded", "deletedToast": "Flowchart deleted", - "deleteFailedToast": "Failed to delete flowchart" + "deleteFailedToast": "Failed to delete flowchart", + "deleteNodeTitle": "Delete node?", + "deleteNodeDescription": "{{label}} and all of its connections will be removed. This cannot be undone.", + "deleteNodeConfirm": "Delete node", + "deleteNodeCancel": "Cancel", + "nodeDeletedToast": "Node deleted" }, "historyPage": { "title": "History", diff --git a/src/locales/es.json b/src/locales/es.json index 9a32000f4..00aa92c6d 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -747,6 +747,7 @@ "urlNeedsVerification": "La URL requiere verificación", "howToUse": "Cómo usar:", "saveToLibrary": "Guardar en Biblioteca", + "saveClaimToLibrary": "Guardar {{claim}} en la biblioteca", "discuss": "Discutir Evidencia", "verifyAll": "Verificar Todas las Fuentes", "verifying": "Verificando Fuentes...", @@ -1000,7 +1001,12 @@ "saveFailedToast": "Error al guardar diagrama", "loadedToast": "Diagrama cargado", "deletedToast": "Diagrama eliminado", - "deleteFailedToast": "Error al eliminar diagrama" + "deleteFailedToast": "Error al eliminar diagrama", + "deleteNodeTitle": "¿Eliminar nodo?", + "deleteNodeDescription": "Se eliminarán «{{label}}» y todas sus conexiones. Esta acción no se puede deshacer.", + "deleteNodeConfirm": "Eliminar nodo", + "deleteNodeCancel": "Cancelar", + "nodeDeletedToast": "Nodo eliminado" }, "historyPage": { "title": "Historial", diff --git a/src/locales/hu.json b/src/locales/hu.json index ea5aab57a..9ddcce179 100644 --- a/src/locales/hu.json +++ b/src/locales/hu.json @@ -747,6 +747,7 @@ "urlNeedsVerification": "Az URL ellenőrzést igényel", "howToUse": "Használat:", "saveToLibrary": "Mentés a Könyvtárba", + "saveClaimToLibrary": "„{{claim}}” mentése a könyvtárba", "discuss": "Bizonyítékok Megvitatása", "verifyAll": "Összes Forrás Ellenőrzése", "verifying": "Források Ellenőrzése...", @@ -1000,7 +1001,12 @@ "saveFailedToast": "Nem sikerült menteni a folyamatábrát", "loadedToast": "Folyamatábra betöltve", "deletedToast": "Folyamatábra törölve", - "deleteFailedToast": "Nem sikerült törölni a folyamatábrát" + "deleteFailedToast": "Nem sikerült törölni a folyamatábrát", + "deleteNodeTitle": "Csomópont törlése?", + "deleteNodeDescription": "A(z) „{{label}}” csomópont és minden kapcsolata törlődik. Ez a művelet nem vonható vissza.", + "deleteNodeConfirm": "Csomópont törlése", + "deleteNodeCancel": "Mégse", + "nodeDeletedToast": "Csomópont törölve" }, "historyPage": { "title": "Előzmények", diff --git a/src/pages/AskPage.tsx b/src/pages/AskPage.tsx index 766f77c82..a93e09935 100644 --- a/src/pages/AskPage.tsx +++ b/src/pages/AskPage.tsx @@ -311,7 +311,7 @@ export default function AskPage() {
-

{t('ask.title')}

+

{t('nav.ask')}

-

{t('attack.title')}

+

{t('nav.attack')}

{t('attack.subtitle')}

diff --git a/src/pages/BrainstormPage.tsx b/src/pages/BrainstormPage.tsx index 3b1c1fd10..724fb8406 100644 --- a/src/pages/BrainstormPage.tsx +++ b/src/pages/BrainstormPage.tsx @@ -96,7 +96,7 @@ export default function BrainstormPage() {
-

{t('brainstorm.title')}

+

{t('nav.brainstorm')}

{t('brainstorm.subtitle')}

diff --git a/src/pages/FactoidsPage.tsx b/src/pages/FactoidsPage.tsx index dfe200d1c..bcbcc9ac3 100644 --- a/src/pages/FactoidsPage.tsx +++ b/src/pages/FactoidsPage.tsx @@ -111,7 +111,7 @@ export default function FactoidsPage() {
-

{t('factoids.title')}

+

{t('nav.factoids')}

{t('factoids.subtitle')}

diff --git a/src/pages/FlowchartPage.tsx b/src/pages/FlowchartPage.tsx index 9ab25dc9f..f04c322b9 100644 --- a/src/pages/FlowchartPage.tsx +++ b/src/pages/FlowchartPage.tsx @@ -40,6 +40,16 @@ import { DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog'; import { Plus, Download, @@ -219,6 +229,7 @@ export default function FlowchartPage() { const [newNodeType, setNewNodeType] = useState('contention'); const [newNodeStrength, setNewNodeStrength] = useState<'strong' | 'moderate' | 'weak'>('moderate'); const [selectedNode, setSelectedNode] = useState(null); + const [isDeleteNodeDialogOpen, setIsDeleteNodeDialogOpen] = useState(false); const [editingNodeId, setEditingNodeId] = useState(null); const [selectedEdgeType, setSelectedEdgeType] = useState('support'); @@ -300,6 +311,7 @@ export default function FlowchartPage() { const handleLoadFlowchart = (data: FlowchartData) => { setNodes(data.nodes); setEdges(data.edges); + setSelectedNode(null); setIsLoadDialogOpen(false); toast.success(t('flowchart.loadedToast')); }; @@ -339,7 +351,9 @@ export default function FlowchartPage() { setNodes((nds) => nds.filter((n) => n.id !== selectedNode)); setEdges((eds) => eds.filter((e) => e.source !== selectedNode && e.target !== selectedNode)); setSelectedNode(null); - }, [selectedNode, setNodes, setEdges]); + setIsDeleteNodeDialogOpen(false); + toast.success(t('flowchart.nodeDeletedToast')); + }, [selectedNode, setNodes, setEdges, t]); const autoLayout = useCallback(() => { const lanes: Record = { @@ -583,7 +597,7 @@ export default function FlowchartPage() { {t('flowchart.edit')} - @@ -655,7 +669,7 @@ export default function FlowchartPage() { {t('flowchart.edit')} {selectedNode !== 'thesis' && ( - @@ -702,6 +716,28 @@ export default function FlowchartPage() { + + + + {t('flowchart.deleteNodeTitle')} + + {t('flowchart.deleteNodeDescription', { + label: nodes.find((node) => node.id === selectedNode)?.data.label, + })} + + + + {t('flowchart.deleteNodeCancel')} + + {t('flowchart.deleteNodeConfirm')} + + + + + {/* Legend */} diff --git a/src/pages/GeneratePage.tsx b/src/pages/GeneratePage.tsx index bbe58dd2a..f2e8ebe27 100644 --- a/src/pages/GeneratePage.tsx +++ b/src/pages/GeneratePage.tsx @@ -329,7 +329,7 @@ export default function GeneratePage() {
-

{t('generate.title')}

+

{t('nav.generate')}

{t('generate.subtitle')}

diff --git a/src/pages/SourcesPage.tsx b/src/pages/SourcesPage.tsx index 203c6d972..1d60ea86b 100644 --- a/src/pages/SourcesPage.tsx +++ b/src/pages/SourcesPage.tsx @@ -151,8 +151,7 @@ export default function SourcesPage() { className="text-primary underline underline-offset-2 break-words [overflow-wrap:anywhere] hover:text-primary/80 transition-colors" > {source.url} - {" "} - [Consulta: 8 de marzo de 2026]. + ))} diff --git a/src/test/components/FactoidsResults.test.tsx b/src/test/components/FactoidsResults.test.tsx index 70fb97a22..a63864b50 100644 --- a/src/test/components/FactoidsResults.test.tsx +++ b/src/test/components/FactoidsResults.test.tsx @@ -51,3 +51,18 @@ test('renders consulted sources with a fallback label for weak sources', () => { expect(screen.getByRole('link', { name: /broad fallback article/i })).toHaveAttribute('href', discoveredSources[0].url); expect(screen.getByText('fallback context only')).toBeInTheDocument(); }); + +test('names the save action with the factoid claim', () => { + render(); + + expect(screen.getByRole('button', { + name: 'Save Solar costs fell by 42 percent to library', + })).toBeInTheDocument(); +}); diff --git a/src/test/pages/AttackPage.test.tsx b/src/test/pages/AttackPage.test.tsx index 93d681baf..af3937bae 100644 --- a/src/test/pages/AttackPage.test.tsx +++ b/src/test/pages/AttackPage.test.tsx @@ -114,7 +114,7 @@ describe('AttackPage Integration', () => { ); - expect(screen.getByText('Attack My Case')).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Attack' })).toBeInTheDocument(); expect(screen.getByTestId('attack-form')).toBeInTheDocument(); expect(screen.queryByTestId('attack-results')).not.toBeInTheDocument(); }); diff --git a/src/test/pages/FlowchartPage.test.tsx b/src/test/pages/FlowchartPage.test.tsx new file mode 100644 index 000000000..544091879 --- /dev/null +++ b/src/test/pages/FlowchartPage.test.tsx @@ -0,0 +1,110 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { beforeEach, expect, test, vi } from 'vitest'; +import FlowchartPage from '@/pages/FlowchartPage'; + +const { mockSavedFlowcharts, mockToastSuccess } = vi.hoisted(() => ({ + mockSavedFlowcharts: [] as Array<{ + id: string; + title: string; + createdAt: number; + data: { nodes: unknown[]; edges: unknown[] }; + }>, + mockToastSuccess: vi.fn(), +})); + +vi.mock('sonner', () => ({ + toast: { success: mockToastSuccess, error: vi.fn() }, +})); + +vi.mock('html-to-image', () => ({ toPng: vi.fn() })); + +vi.mock('@/lib/stores/session-store', () => ({ + useSessionStore: () => ({ + pendingFlowchartData: null as null, + setPendingFlowchartData: vi.fn(), + saveFlowchart: vi.fn(), + savedFlowcharts: mockSavedFlowcharts, + deleteSavedFlowchart: vi.fn(), + }), +})); + +vi.mock('reactflow', async () => { + const React = await import('react'); + return { + default: ({ nodes, onNodeClick }: { + nodes: Array<{ id: string; data: { label: string } }>; + onNodeClick: (event: React.MouseEvent, node: { id: string }) => void; + }) =>
{nodes.map((node) => ( + + ))}
, + useNodesState: (initial: unknown[]) => { + const [nodes, setNodes] = React.useState(initial); + return [nodes, setNodes, vi.fn()]; + }, + useEdgesState: (initial: unknown[]) => { + const [edges, setEdges] = React.useState(initial); + return [edges, setEdges, vi.fn()]; + }, + addEdge: vi.fn(), + Controls: (): null => null, + Background: (): null => null, + MiniMap: (): null => null, + Handle: (): null => null, + Position: { Top: 'top', Bottom: 'bottom' }, + MarkerType: { ArrowClosed: 'arrowclosed' }, + BackgroundVariant: { Dots: 'dots' }, + }; +}); + +beforeEach(() => { + mockSavedFlowcharts.length = 0; + mockToastSuccess.mockClear(); +}); + +test('keeps a selected node when deletion is cancelled', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: 'Argument 1' })); + fireEvent.click(screen.getAllByRole('button', { name: 'Delete' })[0]); + + expect(screen.getByRole('alertdialog', { name: 'Delete node?' })).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(screen.getByRole('button', { name: 'Argument 1' })).toBeInTheDocument(); + expect(mockToastSuccess).not.toHaveBeenCalled(); +}); + +test('deletes a selected node only after confirmation and reports success', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: 'Argument 1' })); + fireEvent.click(screen.getAllByRole('button', { name: 'Delete' })[0]); + fireEvent.click(screen.getByRole('button', { name: 'Delete node' })); + + expect(screen.queryByRole('button', { name: 'Argument 1' })).not.toBeInTheDocument(); + expect(mockToastSuccess).toHaveBeenCalledWith('Node deleted'); +}); + +test('clears a stale node selection when a saved flowchart is loaded', () => { + mockSavedFlowcharts.push({ + id: 'saved-1', + title: 'Loaded chart', + createdAt: Date.now(), + data: { + nodes: [{ + id: 'loaded-node', + type: 'custom', + position: { x: 0, y: 0 }, + data: { label: 'Loaded argument', type: 'contention' }, + }], + edges: [], + }, + }); + render(); + + fireEvent.click(screen.getByRole('button', { name: 'Argument 1' })); + fireEvent.click(screen.getByRole('button', { name: 'Load' })); + fireEvent.click(screen.getByRole('button', { name: /Loaded chart/ })); + + expect(screen.getByRole('button', { name: 'Loaded argument' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument(); +}); diff --git a/src/test/pages/SourcesPage.test.tsx b/src/test/pages/SourcesPage.test.tsx new file mode 100644 index 000000000..4285df873 --- /dev/null +++ b/src/test/pages/SourcesPage.test.tsx @@ -0,0 +1,9 @@ +import { render, screen } from '@testing-library/react'; +import SourcesPage from '@/pages/SourcesPage'; + +test('does not present a stale consultation date as current source metadata', () => { + render(); + + expect(screen.queryByText(/Consulta: 8 de marzo de 2026/i)).not.toBeInTheDocument(); + expect(screen.getByRole('link', { name: /cambridge\.org/i })).toBeInTheDocument(); +});