import { expect, test, type Page, type TestInfo } from '@playwright/test'; const PASSWORD = 'Sup3rSecure!123'; function toSlug(value: string) { return value .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 48); } async function signUp(page: Page, testInfo: TestInfo) { const email = `playwright-graphing-${testInfo.workerIndex}-${toSlug(testInfo.title)}-${Date.now()}@example.com`; await page.goto('/auth/signup'); await page.locator('input[autocomplete="name"]').fill('Playwright Graphing User'); await page.locator('input[autocomplete="email"]').fill(email); await page.locator('input[autocomplete="new-password"]').first().fill(PASSWORD); await page.locator('input[autocomplete="new-password"]').nth(1).fill(PASSWORD); await page.getByRole('button', { name: 'Create account' }).click(); await expect(page.getByRole('heading', { name: 'Command Center' })).toBeVisible({ timeout: 30_000 }); await expect(page).toHaveURL(/\/$/, { timeout: 30_000 }); } function createFinancialsPayload(input: { ticker: string; companyName: string; cadence: 'annual' | 'quarterly' | 'ltm'; surface: string; }) { const fiscalPack = input.ticker === 'JPM' ? 'bank_lender' : input.ticker === 'BLK' ? 'broker_asset_manager' : 'core'; return { financials: { company: { ticker: input.ticker, companyName: input.companyName, cik: null }, surfaceKind: input.surface, cadence: input.cadence, displayModes: ['standardized'], defaultDisplayMode: 'standardized', periods: [ { id: `${input.ticker}-p1`, filingId: 1, accessionNumber: `0000-${input.ticker}-1`, filingDate: '2025-02-01', periodStart: '2024-01-01', periodEnd: '2024-12-31', filingType: '10-K', periodLabel: 'FY 2024' }, { id: `${input.ticker}-p2`, filingId: 2, accessionNumber: `0000-${input.ticker}-2`, filingDate: '2026-02-01', periodStart: '2025-01-01', periodEnd: '2025-12-31', filingType: '10-K', periodLabel: 'FY 2025' } ], statementRows: { faithful: [], standardized: [ { key: 'revenue', label: 'Revenue', category: 'revenue', order: 10, unit: 'currency', values: { [`${input.ticker}-p1`]: input.ticker === 'AAPL' ? 320 : 280, [`${input.ticker}-p2`]: input.ticker === 'AAPL' ? 360 : 330 }, sourceConcepts: ['revenue'], sourceRowKeys: ['revenue'], sourceFactIds: [1], formulaKey: null, hasDimensions: false, resolvedSourceRowKeys: { [`${input.ticker}-p1`]: 'revenue', [`${input.ticker}-p2`]: 'revenue' }, resolutionMethod: 'direct' }, { key: 'gross_profit', label: 'Gross Profit', category: 'profit', order: 15, unit: 'currency', values: { [`${input.ticker}-p1`]: input.ticker === 'JPM' ? null : input.ticker === 'AAPL' ? 138 : 112, [`${input.ticker}-p2`]: input.ticker === 'JPM' ? null : input.ticker === 'AAPL' ? 156 : 128 }, sourceConcepts: ['gross_profit'], sourceRowKeys: ['gross_profit'], sourceFactIds: [11], formulaKey: input.ticker === 'JPM' ? null : 'revenue_less_cost_of_revenue', hasDimensions: false, resolvedSourceRowKeys: { [`${input.ticker}-p1`]: 'gross_profit', [`${input.ticker}-p2`]: 'gross_profit' }, resolutionMethod: input.ticker === 'JPM' ? 'not_meaningful' : 'formula_derived' }, { key: 'other_operating_expense', label: 'Other Expense', category: 'opex', order: 16, unit: 'currency', values: { [`${input.ticker}-p1`]: input.ticker === 'BLK' ? null : input.ticker === 'AAPL' ? 12 : 8, [`${input.ticker}-p2`]: input.ticker === 'BLK' ? null : input.ticker === 'AAPL' ? 14 : 10 }, sourceConcepts: ['other_operating_expense'], sourceRowKeys: ['other_operating_expense'], sourceFactIds: [12], formulaKey: input.ticker === 'BLK' ? null : 'operating_expenses_residual', hasDimensions: false, resolvedSourceRowKeys: { [`${input.ticker}-p1`]: 'other_operating_expense', [`${input.ticker}-p2`]: 'other_operating_expense' }, resolutionMethod: input.ticker === 'BLK' ? 'not_meaningful' : 'formula_derived' }, { key: 'total_assets', label: 'Total Assets', category: 'asset', order: 20, unit: 'currency', values: { [`${input.ticker}-p1`]: input.ticker === 'AAPL' ? 410 : 380, [`${input.ticker}-p2`]: input.ticker === 'AAPL' ? 450 : 420 }, sourceConcepts: ['total_assets'], sourceRowKeys: ['total_assets'], sourceFactIds: [2], formulaKey: null, hasDimensions: false, resolvedSourceRowKeys: { [`${input.ticker}-p1`]: 'total_assets', [`${input.ticker}-p2`]: 'total_assets' }, resolutionMethod: 'direct' }, { key: 'free_cash_flow', label: 'Free Cash Flow', category: 'cash_flow', order: 30, unit: 'currency', values: { [`${input.ticker}-p1`]: input.ticker === 'AAPL' ? 95 : 80, [`${input.ticker}-p2`]: input.ticker === 'AAPL' ? 105 : 92 }, sourceConcepts: ['free_cash_flow'], sourceRowKeys: ['free_cash_flow'], sourceFactIds: [3], formulaKey: null, hasDimensions: false, resolvedSourceRowKeys: { [`${input.ticker}-p1`]: 'free_cash_flow', [`${input.ticker}-p2`]: 'free_cash_flow' }, resolutionMethod: 'direct' } ] }, statementDetails: null, ratioRows: [ { key: 'gross_margin', label: 'Gross Margin', category: 'margins', order: 10, unit: 'percent', values: { [`${input.ticker}-p1`]: input.ticker === 'AAPL' ? 0.43 : 0.39, [`${input.ticker}-p2`]: input.ticker === 'AAPL' ? 0.45 : 0.41 }, sourceConcepts: ['gross_margin'], sourceRowKeys: ['gross_margin'], sourceFactIds: [4], formulaKey: 'gross_margin', hasDimensions: false, resolvedSourceRowKeys: { [`${input.ticker}-p1`]: null, [`${input.ticker}-p2`]: null }, denominatorKey: 'revenue' } ], kpiRows: null, trendSeries: [], categories: [], availability: { adjusted: false, customMetrics: false }, nextCursor: null, facts: null, coverage: { filings: 2, rows: 3, dimensions: 0, facts: 0 }, dataSourceStatus: { enabled: true, hydratedFilings: 2, partialFilings: 0, failedFilings: 0, pendingFilings: 0, queuedSync: false }, metrics: { taxonomy: null, validation: null }, normalization: { regime: 'unknown', fiscalPack, parserVersion: '0.0.0', unmappedRowCount: 0, materialUnmappedRowCount: 0 }, dimensionBreakdown: null } }; } async function mockGraphingFinancials(page: Page) { await page.route('**/api/financials/company**', async (route) => { const url = new URL(route.request().url()); const ticker = url.searchParams.get('ticker') ?? 'MSFT'; const cadence = (url.searchParams.get('cadence') ?? 'annual') as 'annual' | 'quarterly' | 'ltm'; const surface = url.searchParams.get('surface') ?? 'income_statement'; if (ticker === 'BAD') { await route.fulfill({ status: 404, contentType: 'application/json', body: JSON.stringify({ error: 'Ticker not found' }) }); return; } const companyName = ticker === 'AAPL' ? 'Apple Inc.' : ticker === 'NVDA' ? 'NVIDIA Corporation' : ticker === 'AMD' ? 'Advanced Micro Devices, Inc.' : ticker === 'BLK' ? 'BlackRock, Inc.' : ticker === 'JPM' ? 'JPMorgan Chase & Co.' : 'Microsoft Corporation'; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(createFinancialsPayload({ ticker, companyName, cadence, surface })) }); }); } test('supports graphing compare controls and partial failures', async ({ page }, testInfo) => { await signUp(page, testInfo); await mockGraphingFinancials(page); await page.goto('/graphing'); await expect(page).toHaveURL(/tickers=MSFT%2CAAPL%2CNVDA/); await expect(page.getByRole('heading', { name: 'Graphing' })).toBeVisible(); await expect(page.getByText('Microsoft Corporation').first()).toBeVisible(); await page.getByRole('button', { name: 'Graph surface Balance Sheet' }).click(); await expect(page).toHaveURL(/surface=balance_sheet/); await expect(page).toHaveURL(/metric=total_assets/); await page.getByRole('button', { name: 'Graph cadence Quarterly' }).click(); await expect(page).toHaveURL(/cadence=quarterly/); await page.getByRole('button', { name: 'Chart type Bar' }).click(); await expect(page).toHaveURL(/chart=bar/); await page.getByRole('button', { name: 'Remove AAPL' }).click(); await expect(page).not.toHaveURL(/AAPL/); await page.getByLabel('Compare tickers').fill('MSFT, NVDA, AMD'); await page.getByRole('button', { name: 'Update Compare Set' }).click(); await expect(page).toHaveURL(/tickers=MSFT%2CNVDA%2CAMD/); await expect(page.getByText('Advanced Micro Devices, Inc.').first()).toBeVisible(); await page.goto('/graphing?tickers=MSFT,BAD&surface=income_statement&metric=revenue&cadence=annual&chart=line&scale=millions'); await expect(page.getByText('Partial coverage detected.')).toBeVisible(); await expect(page.getByRole('cell', { name: /BAD/ })).toBeVisible(); await expect(page.getByText('Microsoft Corporation').first()).toBeVisible(); }); test('distinguishes not meaningful metrics from missing data in the latest values table', async ({ page }, testInfo) => { await signUp(page, testInfo); await mockGraphingFinancials(page); await page.goto('/graphing?tickers=MSFT,BLK&surface=income_statement&metric=other_operating_expense&cadence=annual&chart=line&scale=millions'); await expect(page.getByRole('combobox', { name: 'Metric selector' })).toHaveValue('other_operating_expense'); await expect(page.getByRole('cell', { name: 'broker_asset_manager' })).toBeVisible(); await expect(page.getByText('Not meaningful for this pack')).toBeVisible(); await page.goto('/graphing?tickers=JPM,MSFT&surface=income_statement&metric=gross_profit&cadence=annual&chart=line&scale=millions'); await expect(page.getByText('not meaningful for the selected pack', { exact: false })).toBeVisible(); await expect(page.getByRole('cell', { name: 'bank_lender' })).toBeVisible(); await expect(page.getByText('Ready')).toBeVisible(); });