Files
Neon-Desk/e2e/financials.spec.ts

380 lines
15 KiB
TypeScript

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 });
await expect(page).toHaveURL(/\/$/, { 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();
});