diff --git a/e2e/smoke/tests/client-page-chart.spec.ts b/e2e/smoke/tests/client-page-chart.spec.ts new file mode 100644 index 000000000..d54ed15a3 --- /dev/null +++ b/e2e/smoke/tests/client-page-chart.spec.ts @@ -0,0 +1,137 @@ +import { test, expect } from '@playwright/test'; +import path from 'path'; + +test('client page: age gate dismiss + StackedBarChart renders', async ({ page }) => { + const consoleErrors: string[] = []; + const allConsoleMessages: Array<{ type: string; text: string }> = []; + + page.on('console', (msg) => { + allConsoleMessages.push({ type: msg.type(), text: msg.text() }); + if (msg.type() === 'error') { + consoleErrors.push(msg.text()); + } + }); + + page.on('pageerror', (err) => { + consoleErrors.push(`PAGE ERROR: ${err.message}`); + }); + + // Step 1: Navigate + console.log('\nNavigating to /customers/client...'); + await page.goto('http://www.atlilith.local/customers/client', { + waitUntil: 'networkidle', + timeout: 30000, + }); + + // Step 2: Wait for age gate + await page.waitForTimeout(2000); + + // Step 3: Take screenshot before age gate dismissal + await page.screenshot({ + path: path.join('test-results', 'client-1-before-agegate.png'), + fullPage: false, + }); + console.log('Screenshot 1: before-agegate.png'); + + // Step 4: Dismiss age gate + const ageGateButton = page.getByRole('button', { name: /i am 18 or older/i }); + const ageGateVisible = await ageGateButton.isVisible().catch(() => false); + + if (ageGateVisible) { + console.log('Age gate detected — clicking "I am 18 or older"'); + await ageGateButton.click(); + await page.waitForTimeout(2000); + } else { + console.log('No age gate visible (may be cached from prior session)'); + } + + // Step 5: Full page screenshot after age gate + await page.screenshot({ + path: path.join('test-results', 'client-2-full-page.png'), + fullPage: true, + }); + console.log('Screenshot 2: full-page.png (full page after age gate)'); + + // Step 6: Find and scroll to "Monthly Allowances by Tier" section + const chartHeading = page.locator('text=Monthly Allowances by Tier').first(); + const chartHeadingCount = await chartHeading.count(); + console.log(`\nChart heading "Monthly Allowances by Tier" found: ${chartHeadingCount > 0}`); + + if (chartHeadingCount > 0) { + await chartHeading.scrollIntoViewIfNeeded(); + await page.waitForTimeout(800); + + await page.screenshot({ + path: path.join('test-results', 'client-3-chart-section.png'), + fullPage: false, + }); + console.log('Screenshot 3: chart-section.png (scrolled to chart)'); + } else { + // Try broader search for any chart-related content + const bodyText = await page.locator('body').innerText(); + const chartRelatedLines = bodyText + .split('\n') + .filter((line) => + line.toLowerCase().includes('allowance') || + line.toLowerCase().includes('tier') || + line.toLowerCase().includes('monthly'), + ) + .slice(0, 10); + console.log('Chart-related text found on page:', chartRelatedLines); + } + + // Step 7: Check for SVG/canvas chart rendering + const svgCount = await page.locator('svg').count(); + const canvasCount = await page.locator('canvas').count(); + console.log(`\nSVG elements: ${svgCount}`); + console.log(`Canvas elements: ${canvasCount}`); + + // Check specifically for recharts (StackedBarChart uses recharts via @lilith/ui-charts) + const rechartsWrapper = await page.locator('.recharts-wrapper').count(); + const rechartsBarChart = await page.locator('.recharts-bar-chart, [class*="recharts-bar"]').count(); + console.log(`recharts-wrapper elements: ${rechartsWrapper}`); + console.log(`recharts bar elements: ${rechartsBarChart}`); + + // Step 8: Report all console errors + console.log(`\n=== Console Errors (${consoleErrors.length} total) ===`); + consoleErrors.forEach((e, i) => console.log(` [${i + 1}] ${e}`)); + + // Filter for chart/ui-charts specific errors + const chartErrors = consoleErrors.filter( + (e) => + e.toLowerCase().includes('chart') || + e.toLowerCase().includes('stacked') || + e.toLowerCase().includes('recharts') || + e.toLowerCase().includes('ui-chart'), + ); + console.log(`\n=== Chart-specific Errors (${chartErrors.length}) ===`); + chartErrors.forEach((e) => console.log(' ', e)); + + // Check for import/export errors (module loading failures) + const moduleErrors = consoleErrors.filter( + (e) => + e.includes('does not provide an export') || + e.includes('Failed to fetch') || + e.includes('Cannot find module') || + e.includes('SyntaxError') || + e.includes('ReferenceError'), + ); + console.log(`\n=== Module Errors (${moduleErrors.length}) ===`); + moduleErrors.forEach((e) => console.log(' ', e)); + + // Step 9: Assertions + // The page should have rendered content + const bodyText = await page.locator('body').innerText(); + expect(bodyText.length, 'Page should have substantial content after age gate').toBeGreaterThan(500); + + // No critical module loading errors + expect( + moduleErrors, + 'No module loading errors should be present', + ).toHaveLength(0); + + console.log('\n=== Test Complete ==='); + console.log(`Total console errors: ${consoleErrors.length}`); + console.log(`Chart heading visible: ${chartHeadingCount > 0}`); + console.log(`SVG elements rendered: ${svgCount}`); +}); diff --git a/features/landing/frontend-public/src/pages/customer/components/ClientVisualizations.tsx b/features/landing/frontend-public/src/pages/customer/components/ClientVisualizations.tsx index 205f5dfb7..96cb898ff 100644 --- a/features/landing/frontend-public/src/pages/customer/components/ClientVisualizations.tsx +++ b/features/landing/frontend-public/src/pages/customer/components/ClientVisualizations.tsx @@ -2,14 +2,18 @@ * ClientVisualizations * * Data visualizations for the client landing page: - * - ClientAfterStats: Grouped SVG bar chart of monthly allowances per tier + * - ClientAfterStats: Stacked bar chart of monthly allowances per tier * - ClientAfterBenefits: Horizontal tier progression timeline * * Tier data is fetched from the merchant backend via useTiers(). - * Uses plain CSS (no styled-components) to match InfoPage pattern. + * Chart rendering delegates to @lilith/ui-charts StackedBarChart. */ +import { useMemo } from 'react' + import { useReducedMotion } from '@lilith/ui-accessibility' +import { StackedBarChart } from '@lilith/ui-charts' +import type { StackedSeriesConfig, StackedDataPoint } from '@lilith/ui-charts' import { m } from '@lilith/ui-motion' import { useTiers } from '@/features/pricing/hooks/useTiers' @@ -33,59 +37,35 @@ function tierColor(slug: string): string { return TIER_COLORS[slug] ?? 'rgba(255,255,255,0.5)' } -const CATEGORY_COLORS = { - messages: '#9370db', - discoveries: '#ba55d3', - views: '#ffd700', -} as const - // --------------------------------------------------------------------------- -// Chart constants +// Chart series config — matches the StackedBarChart API // --------------------------------------------------------------------------- -const CHART_WIDTH = 900 -const CHART_HEIGHT = 340 -const CHART_MARGIN = { top: 40, right: 20, bottom: 80, left: 60 } -const PLOT_W = CHART_WIDTH - CHART_MARGIN.left - CHART_MARGIN.right -const PLOT_H = CHART_HEIGHT - CHART_MARGIN.top - CHART_MARGIN.bottom - -const BARS_PER_GROUP = 3 -const GROUP_GAP_RATIO = 0.3 - -// Log scale to handle the wide range (50 → 8000) while keeping bars visually -// distinct. Bars are sized by log(value) / log(LOG_CEILING). -const LOG_CEILING = 10000 -const LOG_MAX = Math.log10(LOG_CEILING) - -const Y_TICKS = [10, 50, 100, 500, 1000, 5000] - -function barHeight(value: number): number { - if (value <= 0) return 0 - const v = value === -1 ? LOG_CEILING : value - return (Math.log10(v) / LOG_MAX) * PLOT_H -} - -function barY(value: number): number { - return PLOT_H - barHeight(value) -} - -function yTickPos(tick: number): number { - return PLOT_H - (Math.log10(tick) / LOG_MAX) * PLOT_H -} +const CHART_SERIES: StackedSeriesConfig[] = [ + { key: 'messages', name: 'Messages', color: '#9370db' }, + { key: 'discoveries', name: 'Discoveries', color: '#ba55d3' }, + { key: 'views', name: 'Views', color: '#ffd700' }, +] // --------------------------------------------------------------------------- -// ClientAfterStats — Allowance chart from API data +// ClientAfterStats — Allowance chart using @lilith/ui-charts StackedBarChart // --------------------------------------------------------------------------- export function ClientAfterStats() { const prefersReducedMotion = useReducedMotion() const { tiers, isLoading } = useTiers() - if (isLoading || tiers.length === 0) return null + const chartData: StackedDataPoint[] = useMemo(() => + tiers.map((tier) => ({ + label: tier.name, + messages: tier.features.messagesPerMonth === -1 ? 0 : tier.features.messagesPerMonth, + discoveries: tier.features.profileDiscoveriesPerMonth === -1 ? 0 : tier.features.profileDiscoveriesPerMonth, + views: tier.features.profileViewsPerMonth === -1 ? 0 : tier.features.profileViewsPerMonth, + })), + [tiers], + ) - const groupCount = tiers.length - const groupWidth = PLOT_W / groupCount - const bw = (groupWidth * (1 - GROUP_GAP_RATIO)) / BARS_PER_GROUP + if (isLoading || tiers.length === 0) return null const animProps = prefersReducedMotion ? {} @@ -103,149 +83,17 @@ export function ClientAfterStats() {
- {/* Legend */} -