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-financials-${testInfo.workerIndex}-${toSlug(testInfo.title)}-${Date.now()}@example.com`; await page.goto('/auth/signup'); await page.locator('input[autocomplete="name"]').fill('Playwright Financials 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 }); } function buildFinancialsPayload(ticker: 'MSFT' | 'JPM') { const isBank = ticker === 'JPM'; const prefix = ticker.toLowerCase(); return { financials: { company: { ticker, companyName: isBank ? 'JPMorgan Chase & Co.' : 'Microsoft Corporation', cik: null }, surfaceKind: 'income_statement', cadence: 'annual', displayModes: ['standardized', 'faithful'], defaultDisplayMode: 'standardized', periods: [ { id: `${prefix}-fy24`, filingId: 1, accessionNumber: `0000-${prefix}-1`, filingDate: '2025-02-01', periodStart: '2024-01-01', periodEnd: '2024-12-31', filingType: '10-K', periodLabel: 'FY 2024' }, { id: `${prefix}-fy25`, filingId: 2, accessionNumber: `0000-${prefix}-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: { [`${prefix}-fy24`]: 245_000, [`${prefix}-fy25`]: 262_000 }, sourceConcepts: ['revenue'], sourceRowKeys: ['revenue'], sourceFactIds: [1], formulaKey: null, hasDimensions: false, resolvedSourceRowKeys: { [`${prefix}-fy24`]: 'revenue', [`${prefix}-fy25`]: 'revenue' }, statement: 'income', resolutionMethod: 'direct', confidence: 'high', warningCodes: [] }, { key: 'gross_profit', label: 'Gross Profit', category: 'profit', order: 20, unit: 'currency', values: { [`${prefix}-fy24`]: isBank ? null : 171_000, [`${prefix}-fy25`]: isBank ? null : 185_000 }, sourceConcepts: ['gross_profit'], sourceRowKeys: ['gross_profit'], sourceFactIds: [2], formulaKey: isBank ? null : 'revenue_less_cost_of_revenue', hasDimensions: false, resolvedSourceRowKeys: { [`${prefix}-fy24`]: 'gross_profit', [`${prefix}-fy25`]: 'gross_profit' }, statement: 'income', resolutionMethod: isBank ? 'not_meaningful' : 'formula_derived', confidence: isBank ? 'high' : 'medium', warningCodes: isBank ? ['gross_profit_not_meaningful_bank_pack'] : ['formula_resolved'] }, { key: 'operating_expenses', label: 'Operating Expenses', category: 'opex', order: 30, unit: 'currency', values: { [`${prefix}-fy24`]: isBank ? 121_000 : 76_000, [`${prefix}-fy25`]: isBank ? 126_000 : 82_000 }, sourceConcepts: ['operating_expenses'], sourceRowKeys: ['operating_expenses'], sourceFactIds: [3], formulaKey: null, hasDimensions: false, resolvedSourceRowKeys: { [`${prefix}-fy24`]: 'operating_expenses', [`${prefix}-fy25`]: 'operating_expenses' }, statement: 'income', detailCount: 3, resolutionMethod: 'direct', confidence: 'high', warningCodes: [] }, { key: 'selling_general_and_administrative', label: 'SG&A', category: 'opex', order: 40, unit: 'currency', values: { [`${prefix}-fy24`]: isBank ? null : 44_000, [`${prefix}-fy25`]: isBank ? null : 47_500 }, sourceConcepts: ['selling_general_and_administrative'], sourceRowKeys: ['selling_general_and_administrative'], sourceFactIds: [4], formulaKey: null, hasDimensions: false, resolvedSourceRowKeys: { [`${prefix}-fy24`]: 'selling_general_and_administrative', [`${prefix}-fy25`]: 'selling_general_and_administrative' }, statement: 'income', resolutionMethod: isBank ? 'not_meaningful' : 'direct', confidence: 'high', warningCodes: isBank ? ['expense_breakdown_not_meaningful_bank_pack'] : [] }, { key: 'research_and_development', label: 'Research Expense', category: 'opex', order: 50, unit: 'currency', values: { [`${prefix}-fy24`]: isBank ? null : 26_000, [`${prefix}-fy25`]: isBank ? null : 28_000 }, sourceConcepts: ['research_and_development'], sourceRowKeys: ['research_and_development'], sourceFactIds: [5], formulaKey: null, hasDimensions: false, resolvedSourceRowKeys: { [`${prefix}-fy24`]: 'research_and_development', [`${prefix}-fy25`]: 'research_and_development' }, statement: 'income', resolutionMethod: isBank ? 'not_meaningful' : 'direct', confidence: 'high', warningCodes: isBank ? ['expense_breakdown_not_meaningful_bank_pack'] : [] }, { key: 'other_operating_expense', label: 'Other Expense', category: 'opex', order: 60, unit: 'currency', values: { [`${prefix}-fy24`]: isBank ? null : 6_000, [`${prefix}-fy25`]: isBank ? null : 6_500 }, sourceConcepts: ['other_operating_expense'], sourceRowKeys: ['other_operating_expense'], sourceFactIds: [6], formulaKey: isBank ? null : 'operating_expenses_residual', hasDimensions: false, resolvedSourceRowKeys: { [`${prefix}-fy24`]: 'other_operating_expense', [`${prefix}-fy25`]: 'other_operating_expense' }, statement: 'income', resolutionMethod: isBank ? 'not_meaningful' : 'formula_derived', confidence: isBank ? 'high' : 'medium', warningCodes: isBank ? ['expense_breakdown_not_meaningful_bank_pack'] : ['formula_resolved'] }, { key: 'operating_income', label: 'Operating Income', category: 'profit', order: 70, unit: 'currency', values: { [`${prefix}-fy24`]: isBank ? 124_000 : 95_000, [`${prefix}-fy25`]: isBank ? 131_000 : 103_000 }, sourceConcepts: ['operating_income'], sourceRowKeys: ['operating_income'], sourceFactIds: [7], formulaKey: null, hasDimensions: false, resolvedSourceRowKeys: { [`${prefix}-fy24`]: 'operating_income', [`${prefix}-fy25`]: 'operating_income' }, statement: 'income', resolutionMethod: 'direct', confidence: 'high', warningCodes: [] } ] }, statementDetails: isBank ? null : { selling_general_and_administrative: [ { key: 'corporate_sga', parentSurfaceKey: 'selling_general_and_administrative', label: 'Corporate SG&A', conceptKey: 'corporate_sga', qname: 'us-gaap:CorporateSga', namespaceUri: 'http://fasb.org/us-gaap/2024', localName: 'CorporateSga', unit: 'USD', values: { [`${prefix}-fy24`]: 44_000, [`${prefix}-fy25`]: 47_500 }, sourceFactIds: [104], isExtension: false, dimensionsSummary: [], residualFlag: false } ], research_and_development: [ { key: 'product_rnd', parentSurfaceKey: 'research_and_development', label: 'Product R&D', conceptKey: 'product_rnd', qname: 'us-gaap:ProductResearchAndDevelopment', namespaceUri: 'http://fasb.org/us-gaap/2024', localName: 'ProductResearchAndDevelopment', unit: 'USD', values: { [`${prefix}-fy24`]: 26_000, [`${prefix}-fy25`]: 28_000 }, sourceFactIds: [105], isExtension: false, dimensionsSummary: [], residualFlag: false } ], other_operating_expense: [ { key: 'other_opex_residual', parentSurfaceKey: 'other_operating_expense', label: 'Other Operating Expense Residual', conceptKey: 'other_opex_residual', qname: 'us-gaap:OtherOperatingExpense', namespaceUri: 'http://fasb.org/us-gaap/2024', localName: 'OtherOperatingExpense', unit: 'USD', values: { [`${prefix}-fy24`]: 6_000, [`${prefix}-fy25`]: 6_500 }, sourceFactIds: [106], isExtension: false, dimensionsSummary: [], residualFlag: false } ], unmapped: [ { key: 'other_income_unmapped', parentSurfaceKey: 'unmapped', label: 'Other Income Residual', conceptKey: 'other_income_unmapped', qname: 'us-gaap:OtherNonoperatingIncomeExpense', namespaceUri: 'http://fasb.org/us-gaap/2024', localName: 'OtherNonoperatingIncomeExpense', unit: 'USD', values: { [`${prefix}-fy24`]: 1_200, [`${prefix}-fy25`]: 1_450 }, sourceFactIds: [107], isExtension: false, dimensionsSummary: [], residualFlag: true } ] }, ratioRows: [], kpiRows: null, trendSeries: [ { key: 'revenue', label: 'Revenue', category: 'revenue', unit: 'currency', values: { [`${prefix}-fy24`]: 245_000, [`${prefix}-fy25`]: 262_000 } } ], categories: [ { key: 'revenue', label: 'Revenue', count: 1 }, { key: 'profit', label: 'Profit', count: 3 }, { key: 'opex', label: 'Operating Expenses', count: 4 } ], availability: { adjusted: false, customMetrics: false }, nextCursor: null, facts: null, coverage: { filings: 2, rows: 8, dimensions: 0, facts: 0 }, dataSourceStatus: { enabled: true, hydratedFilings: 2, partialFilings: 0, failedFilings: 0, pendingFilings: 0, queuedSync: false }, metrics: { taxonomy: null, validation: null }, normalization: { parserEngine: 'fiscal-xbrl', regime: 'us-gaap', fiscalPack: isBank ? 'bank_lender' : 'core', parserVersion: '0.1.0', surfaceRowCount: 8, detailRowCount: isBank ? 0 : 4, kpiRowCount: 0, unmappedRowCount: isBank ? 0 : 1, materialUnmappedRowCount: 0, warnings: isBank ? [] : ['income_sparse_mapping', 'unmapped_cash_flow_bridge'] }, dimensionBreakdown: null } }; } async function mockFinancials(page: Page) { await page.route('**/api/financials/company**', async (route) => { const url = new URL(route.request().url()); const ticker = (url.searchParams.get('ticker') ?? 'MSFT').toUpperCase() as 'MSFT' | 'JPM'; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(buildFinancialsPayload(ticker)) }); }); } test('renders the standardized operating expense tree and inspector details', async ({ page }, testInfo) => { await signUp(page, testInfo); await mockFinancials(page); await page.goto('/financials?ticker=MSFT'); await expect(page.getByText('Normalization Summary')).toBeVisible(); await expect(page.getByText('fiscal-xbrl 0.1.0')).toBeVisible(); await expect(page.getByText('Parser Warnings')).toBeVisible(); await expect(page.getByText('income_sparse_mapping')).toBeVisible(); await expect(page.getByText('unmapped_cash_flow_bridge')).toBeVisible(); await expect(page.getByText('Parser residual rows are available under the Unmapped / Residual section.')).toBeVisible(); await expect(page.getByRole('button', { name: 'Expand Operating Expenses details' })).toBeVisible(); await page.getByRole('button', { name: 'Expand Operating Expenses details' }).click(); await expect(page.getByRole('button', { name: /^SG&A/ })).toBeVisible(); await expect(page.getByRole('button', { name: /^Research Expense/ })).toBeVisible(); await expect(page.getByRole('button', { name: /^Other Expense/ })).toBeVisible(); await page.getByRole('button', { name: /^SG&A/ }).click(); await expect(page.getByText('Row Details')).toBeVisible(); await expect(page.getByText('selling_general_and_administrative', { exact: true }).first()).toBeVisible(); await expect(page.getByText('Corporate SG&A')).toBeVisible(); await expect(page.getByText('Unmapped / Residual')).toBeVisible(); await page.getByRole('button', { name: /^Other Income Residual/ }).click(); await expect(page.getByText('other_income_unmapped', { exact: true })).toBeVisible(); await expect(page.getByText('Unmapped / Residual', { exact: true }).last()).toBeVisible(); }); test('shows not meaningful expense breakdown rows for bank pack filings', async ({ page }, testInfo) => { await signUp(page, testInfo); await mockFinancials(page); await page.goto('/financials?ticker=JPM'); await page.getByRole('button', { name: 'Expand Operating Expenses details' }).click(); const sgaButton = page.getByRole('button', { name: /^SG&A/ }); await expect(sgaButton).toBeVisible(); await expect(sgaButton).toContainText('N/M'); await sgaButton.click(); await expect(page.getByText('not_meaningful', { exact: true }).first()).toBeVisible(); await expect(page.getByText('expense_breakdown_not_meaningful_bank_pack')).toBeVisible(); });