Run playwright UI tests

This commit is contained in:
2026-03-06 14:40:43 -05:00
parent 610fce8db3
commit 8e62c66677
37 changed files with 4430 additions and 643 deletions

View File

@@ -17,7 +17,7 @@ import {
XAxis,
YAxis
} from 'recharts';
import { ChartNoAxesCombined, ChevronDown, Download, RefreshCcw, Search } from 'lucide-react';
import { AlertTriangle, ChartNoAxesCombined, ChevronDown, Download, RefreshCcw, Search } from 'lucide-react';
import { AppShell } from '@/components/shell/app-shell';
import { MetricCard } from '@/components/dashboard/metric-card';
import {
@@ -41,11 +41,9 @@ import { companyFinancialStatementsQueryOptions } from '@/lib/query/options';
import type {
CompanyFinancialStatementsResponse,
DimensionBreakdownRow,
FilingFaithfulStatementRow,
FinancialHistoryWindow,
FinancialStatementKind,
FinancialStatementMode,
StandardizedStatementRow
TaxonomyStatementRow
} from '@/lib/types';
type LoadOptions = {
@@ -59,11 +57,6 @@ const FINANCIAL_VALUE_SCALE_OPTIONS: Array<{ value: NumberScaleUnit; label: stri
{ value: 'billions', label: 'Billions (B)' }
];
const MODE_OPTIONS: Array<{ value: FinancialStatementMode; label: string }> = [
{ value: 'standardized', label: 'Standardized' },
{ value: 'filing_faithful', label: 'Filing-faithful' }
];
const STATEMENT_OPTIONS: Array<{ value: FinancialStatementKind; label: string }> = [
{ value: 'income', label: 'Income' },
{ value: 'balance', label: 'Balance Sheet' },
@@ -85,6 +78,7 @@ const CHART_TOOLTIP_BORDER = 'rgba(123, 255, 217, 0.45)';
type OverviewPoint = {
periodId: string;
filingDate: string;
periodEnd: string | null;
label: string;
revenue: number | null;
netIncome: number | null;
@@ -136,22 +130,6 @@ function ratioPercent(numerator: number | null, denominator: number | null) {
return (numerator / denominator) * 100;
}
function toStandardizedRows(data: CompanyFinancialStatementsResponse | null) {
if (!data || data.mode !== 'standardized') {
return [];
}
return data.rows as StandardizedStatementRow[];
}
function toFilingFaithfulRows(data: CompanyFinancialStatementsResponse | null) {
if (!data || data.mode !== 'filing_faithful') {
return [];
}
return data.rows as FilingFaithfulStatementRow[];
}
function rowValue(row: { values: Record<string, number | null> }, periodId: string) {
return periodId in row.values ? row.values[periodId] : null;
}
@@ -168,30 +146,39 @@ function mergeFinancialPages(
.filter((period, index, list) => list.findIndex((item) => item.id === period.id) === index)
.sort((a, b) => Date.parse(a.filingDate) - Date.parse(b.filingDate));
const rowMap = new Map<string, StandardizedStatementRow | FilingFaithfulStatementRow>();
const rowMap = new Map<string, TaxonomyStatementRow>();
for (const row of [...base.rows, ...next.rows]) {
const existing = rowMap.get(row.key);
if (!existing) {
rowMap.set(row.key, {
...row,
values: { ...row.values }
values: { ...row.values },
units: { ...row.units },
sourceFactIds: [...row.sourceFactIds]
});
continue;
}
existing.hasDimensions = existing.hasDimensions || row.hasDimensions;
existing.order = Math.min(existing.order, row.order);
existing.depth = Math.min(existing.depth, row.depth);
for (const [periodId, value] of Object.entries(row.values)) {
if (!(periodId in existing.values)) {
existing.values[periodId] = value;
}
}
if ('sourceConcepts' in existing && 'sourceConcepts' in row) {
for (const concept of row.sourceConcepts) {
if (!existing.sourceConcepts.includes(concept)) {
existing.sourceConcepts.push(concept);
}
for (const [periodId, unit] of Object.entries(row.units)) {
if (!(periodId in existing.units)) {
existing.units[periodId] = unit;
}
}
for (const factId of row.sourceFactIds) {
if (!existing.sourceFactIds.includes(factId)) {
existing.sourceFactIds.push(factId);
}
}
}
@@ -220,21 +207,19 @@ function mergeFinancialPages(
return Object.fromEntries(map.entries());
})();
const mergedRows = [...rowMap.values()];
return {
...next,
periods,
rows: next.mode === 'standardized'
? mergedRows as StandardizedStatementRow[]
: mergedRows as FilingFaithfulStatementRow[],
rows: [...rowMap.values()],
nextCursor: next.nextCursor,
coverage: {
...next.coverage,
filings: periods.length,
rows: rowMap.size,
dimensions: dimensionBreakdown
? Object.values(dimensionBreakdown).reduce((total, rows) => total + rows.length, 0)
: 0
: 0,
facts: next.coverage.facts
},
dataSourceStatus: {
...next.dataSourceStatus,
@@ -244,20 +229,20 @@ function mergeFinancialPages(
};
}
function findStandardizedRowValue(
data: CompanyFinancialStatementsResponse | null,
preferredKey: string,
fallbackIncludes: string[]
) {
const rows = toStandardizedRows(data);
const exact = rows.find((row) => row.key === preferredKey);
function findRowByLocalNames(rows: TaxonomyStatementRow[], localNames: string[]) {
const exact = rows.find((row) => localNames.some((name) => row.localName.toLowerCase() === name.toLowerCase()));
if (exact) {
return exact;
}
const exactLabel = rows.find((row) => localNames.some((name) => row.label.toLowerCase() === name.toLowerCase()));
if (exactLabel) {
return exactLabel;
}
return rows.find((row) => {
const haystack = `${row.key} ${row.label} ${row.concept}`.toLowerCase();
return fallbackIncludes.some((needle) => haystack.includes(needle));
const haystack = `${row.key} ${row.label} ${row.localName} ${row.qname}`.toLowerCase();
return localNames.some((name) => haystack.includes(name.toLowerCase()));
}) ?? null;
}
@@ -265,28 +250,49 @@ function buildOverviewSeries(
incomeData: CompanyFinancialStatementsResponse | null,
balanceData: CompanyFinancialStatementsResponse | null
): OverviewPoint[] {
const periodMap = new Map<string, { filingDate: string }>();
const periodMap = new Map<string, { filingDate: string; periodEnd: string | null }>();
for (const source of [incomeData, balanceData]) {
for (const period of source?.periods ?? []) {
periodMap.set(period.id, { filingDate: period.filingDate });
periodMap.set(period.id, {
filingDate: period.filingDate,
periodEnd: period.periodEnd
});
}
}
const periods = [...periodMap.entries()]
.map(([periodId, data]) => ({ periodId, filingDate: data.filingDate }))
.sort((a, b) => Date.parse(a.filingDate) - Date.parse(b.filingDate));
.map(([periodId, data]) => ({
periodId,
filingDate: data.filingDate,
periodEnd: data.periodEnd
}))
.sort((a, b) => Date.parse(a.periodEnd ?? a.filingDate) - Date.parse(b.periodEnd ?? b.filingDate));
const revenueRow = findStandardizedRowValue(incomeData, 'revenue', ['revenue', 'sales']);
const netIncomeRow = findStandardizedRowValue(incomeData, 'net-income', ['net income', 'profit']);
const assetsRow = findStandardizedRowValue(balanceData, 'total-assets', ['total assets']);
const cashRow = findStandardizedRowValue(balanceData, 'cash-and-equivalents', ['cash']);
const debtRow = findStandardizedRowValue(balanceData, 'total-debt', ['debt', 'borrowings']);
const incomeRows = incomeData?.rows ?? [];
const balanceRows = balanceData?.rows ?? [];
const revenueRow = findRowByLocalNames(incomeRows, [
'RevenueFromContractWithCustomerExcludingAssessedTax',
'Revenues',
'SalesRevenueNet',
'Revenue'
]);
const netIncomeRow = findRowByLocalNames(incomeRows, ['NetIncomeLoss', 'ProfitLoss']);
const assetsRow = findRowByLocalNames(balanceRows, ['Assets']);
const cashRow = findRowByLocalNames(balanceRows, [
'CashAndCashEquivalentsAtCarryingValue',
'CashCashEquivalentsAndShortTermInvestments',
'CashAndShortTermInvestments',
'Cash'
]);
const debtRow = findRowByLocalNames(balanceRows, ['LongTermDebt', 'Debt', 'LongTermDebtNoncurrent']);
return periods.map((period) => ({
periodId: period.periodId,
filingDate: period.filingDate,
label: formatShortDate(period.filingDate),
periodEnd: period.periodEnd,
label: formatShortDate(period.periodEnd ?? period.filingDate),
revenue: revenueRow ? rowValue(revenueRow, period.periodId) : null,
netIncome: netIncomeRow ? rowValue(netIncomeRow, period.periodId) : null,
totalAssets: assetsRow ? rowValue(assetsRow, period.periodId) : null,
@@ -311,7 +317,6 @@ function FinancialsPageContent() {
const [tickerInput, setTickerInput] = useState('MSFT');
const [ticker, setTicker] = useState('MSFT');
const [mode, setMode] = useState<FinancialStatementMode>('standardized');
const [statement, setStatement] = useState<FinancialStatementKind>('income');
const [window, setWindow] = useState<FinancialHistoryWindow>('10y');
const [valueScale, setValueScale] = useState<NumberScaleUnit>('millions');
@@ -344,18 +349,18 @@ function FinancialsPageContent() {
const [incomeResponse, balanceResponse] = await Promise.all([
queryClient.ensureQueryData(companyFinancialStatementsQueryOptions({
ticker: symbol,
mode: 'standardized',
statement: 'income',
window: selectedWindow,
includeDimensions: false,
includeFacts: false,
limit: selectedWindow === 'all' ? 120 : 80
})),
queryClient.ensureQueryData(companyFinancialStatementsQueryOptions({
ticker: symbol,
mode: 'standardized',
statement: 'balance',
window: selectedWindow,
includeDimensions: false,
includeFacts: false,
limit: selectedWindow === 'all' ? 120 : 80
}))
]);
@@ -380,10 +385,10 @@ function FinancialsPageContent() {
try {
const response = await queryClient.ensureQueryData(companyFinancialStatementsQueryOptions({
ticker: normalizedTicker,
mode,
statement,
window,
includeDimensions,
includeFacts: false,
cursor: nextCursor,
limit: window === 'all' ? 60 : 80
}));
@@ -408,7 +413,6 @@ function FinancialsPageContent() {
}
}, [
queryClient,
mode,
statement,
window,
dimensionsEnabled,
@@ -429,7 +433,7 @@ function FinancialsPageContent() {
await queueFilingSync({ ticker: targetTicker, limit: 20 });
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
void queryClient.invalidateQueries({ queryKey: ['filings'] });
void queryClient.invalidateQueries({ queryKey: ['financials-v2'] });
void queryClient.invalidateQueries({ queryKey: ['financials-v3'] });
await loadFinancials(targetTicker);
} catch (err) {
setError(err instanceof Error ? err.message : `Failed to queue financial sync for ${targetTicker}`);
@@ -442,25 +446,30 @@ function FinancialsPageContent() {
if (!isPending && isAuthenticated) {
void loadFinancials(ticker);
}
}, [isPending, isAuthenticated, ticker, mode, statement, window, dimensionsEnabled, loadFinancials]);
}, [isPending, isAuthenticated, ticker, statement, window, dimensionsEnabled, loadFinancials]);
const periods = useMemo(() => {
return [...(financials?.periods ?? [])]
.sort((a, b) => Date.parse(a.filingDate) - Date.parse(b.filingDate));
}, [financials?.periods]);
const standardizedRows = useMemo(() => toStandardizedRows(financials), [financials]);
const filingFaithfulRows = useMemo(() => toFilingFaithfulRows(financials), [financials]);
const statementRows = mode === 'standardized'
? standardizedRows
: filingFaithfulRows;
const statementRows = useMemo(() => financials?.rows ?? [], [financials?.rows]);
const overviewSeries = useMemo(() => {
return buildOverviewSeries(overviewIncome, overviewBalance);
}, [overviewIncome, overviewBalance]);
const latestOverview = overviewSeries[overviewSeries.length - 1] ?? null;
const latestTaxonomyMetrics = financials?.metrics.taxonomy
?? overviewIncome?.metrics.taxonomy
?? overviewBalance?.metrics.taxonomy
?? null;
const latestRevenue = latestOverview?.revenue ?? latestTaxonomyMetrics?.revenue ?? null;
const latestNetIncome = latestOverview?.netIncome ?? latestTaxonomyMetrics?.netIncome ?? null;
const latestTotalAssets = latestOverview?.totalAssets ?? latestTaxonomyMetrics?.totalAssets ?? null;
const latestCash = latestOverview?.cash ?? latestTaxonomyMetrics?.cash ?? null;
const latestDebt = latestOverview?.debt ?? latestTaxonomyMetrics?.debt ?? null;
const latestReferenceDate = latestOverview?.filingDate ?? periods[periods.length - 1]?.filingDate ?? null;
const selectedRow = useMemo(() => {
if (!selectedRowKey) {
@@ -480,13 +489,11 @@ function FinancialsPageContent() {
return direct;
}
if ('concept' in selectedRow && selectedRow.concept) {
const conceptKey = selectedRow.concept.toLowerCase();
for (const rows of Object.values(financials.dimensionBreakdown)) {
const matched = rows.filter((row) => (row.concept ?? '').toLowerCase() === conceptKey);
if (matched.length > 0) {
return matched;
}
const conceptKey = selectedRow.qname.toLowerCase();
for (const rows of Object.values(financials.dimensionBreakdown)) {
const matched = rows.filter((row) => (row.concept ?? '').toLowerCase() === conceptKey);
if (matched.length > 0) {
return matched;
}
}
@@ -498,16 +505,6 @@ function FinancialsPageContent() {
}, [valueScale]);
const controlSections = useMemo<FinancialControlSection[]>(() => [
{
id: 'mode',
label: 'Mode',
value: mode,
options: MODE_OPTIONS,
onChange: (nextValue) => {
setMode(nextValue as FinancialStatementMode);
setSelectedRowKey(null);
}
},
{
id: 'statement',
label: 'Statement',
@@ -535,7 +532,7 @@ function FinancialsPageContent() {
options: FINANCIAL_VALUE_SCALE_OPTIONS,
onChange: (nextValue) => setValueScale(nextValue as NumberScaleUnit)
}
], [mode, statement, window, valueScale]);
], [statement, window, valueScale]);
const controlActions = useMemo<FinancialControlAction[]>(() => {
const actions: FinancialControlAction[] = [];
@@ -581,7 +578,7 @@ function FinancialsPageContent() {
return (
<AppShell
title="Financials"
subtitle="Dual-mode financial statements with standardized comparability and filing-faithful presentation."
subtitle="Taxonomy-native financial statements with PDF LLM metric validation."
activeTicker={financials?.company.ticker ?? ticker}
actions={(
<div className="flex w-full flex-wrap items-center justify-end gap-2 sm:w-auto">
@@ -651,42 +648,42 @@ function FinancialsPageContent() {
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<MetricCard
label="Latest Revenue"
value={latestOverview ? asDisplayCurrency(latestOverview.revenue, valueScale) : 'n/a'}
delta={latestOverview ? formatLongDate(latestOverview.filingDate) : 'No standardized history'}
value={asDisplayCurrency(latestRevenue, valueScale)}
delta={latestReferenceDate ? formatLongDate(latestReferenceDate) : 'No history'}
/>
<MetricCard
label="Latest Net Income"
value={latestOverview ? asDisplayCurrency(latestOverview.netIncome, valueScale) : 'n/a'}
delta={latestOverview ? `Net margin ${formatPercent(ratioPercent(latestOverview.netIncome, latestOverview.revenue) ?? 0)}` : 'No standardized history'}
positive={(latestOverview?.netIncome ?? 0) >= 0}
value={asDisplayCurrency(latestNetIncome, valueScale)}
delta={latestReferenceDate ? `Net margin ${formatPercent(ratioPercent(latestNetIncome, latestRevenue) ?? 0)}` : 'No history'}
positive={(latestNetIncome ?? 0) >= 0}
/>
<MetricCard
label="Latest Total Assets"
value={latestOverview ? asDisplayCurrency(latestOverview.totalAssets, valueScale) : 'n/a'}
delta={latestOverview ? `Debt ${asDisplayCurrency(latestOverview.debt, valueScale)}` : 'No standardized history'}
value={asDisplayCurrency(latestTotalAssets, valueScale)}
delta={latestReferenceDate ? `Debt ${asDisplayCurrency(latestDebt, valueScale)}` : 'No history'}
/>
<MetricCard
label="Cash / Debt"
value={latestOverview && latestOverview.cash !== null && latestOverview.debt !== null && latestOverview.debt !== 0
? `${(latestOverview.cash / latestOverview.debt).toFixed(2)}x`
value={latestCash !== null && latestDebt !== null && latestDebt !== 0
? `${(latestCash / latestDebt).toFixed(2)}x`
: 'n/a'}
delta={latestOverview ? `Cash ${asDisplayCurrency(latestOverview.cash, valueScale)}` : 'No standardized history'}
delta={latestReferenceDate ? `Cash ${asDisplayCurrency(latestCash, valueScale)}` : 'No history'}
/>
</div>
<FinancialControlBar
title="Financial Controls"
subtitle={`Compact multi-filter control bar. Current display scale: ${selectedScaleLabel}.`}
subtitle={`Taxonomy statement controls. Current display scale: ${selectedScaleLabel}.`}
sections={controlSections}
actions={controlActions}
/>
<div className="grid grid-cols-1 gap-6 xl:grid-cols-2">
<Panel title="Standardized Income Trend" subtitle="Overview chart remains anchored to standardized rows.">
<Panel title="Income Trend" subtitle="Overview chart from taxonomy-derived income and balance concepts.">
{loading ? (
<p className="text-sm text-[color:var(--terminal-muted)]">Loading overview chart...</p>
) : overviewSeries.length === 0 ? (
<p className="text-sm text-[color:var(--terminal-muted)]">No standardized income history available yet.</p>
<p className="text-sm text-[color:var(--terminal-muted)]">No income history available yet.</p>
) : (
<div className="h-[320px]">
<ResponsiveContainer width="100%" height="100%">
@@ -714,11 +711,11 @@ function FinancialsPageContent() {
)}
</Panel>
<Panel title="Standardized Balance Trend" subtitle="Assets, cash, and debt from standardized balance rows.">
<Panel title="Balance Trend" subtitle="Assets, cash, and debt from taxonomy-derived balance concepts.">
{loading ? (
<p className="text-sm text-[color:var(--terminal-muted)]">Loading balance chart...</p>
) : overviewSeries.length === 0 ? (
<p className="text-sm text-[color:var(--terminal-muted)]">No standardized balance history available yet.</p>
<p className="text-sm text-[color:var(--terminal-muted)]">No balance history available yet.</p>
) : (
<div className="h-[320px]">
<ResponsiveContainer width="100%" height="100%">
@@ -750,7 +747,7 @@ function FinancialsPageContent() {
<Panel
title="Statement Matrix"
subtitle={`${mode === 'standardized' ? 'Cross-company comparable' : 'Filing-native'} ${statement.replace('_', ' ')} rows by period. Click rows with available dimensions for drill-down.`}
subtitle={`Taxonomy-native ${statement.replace('_', ' ')} rows by period. Extension concepts are tagged; rows with dimensions can be drilled down.`}
>
{loading ? (
<p className="text-sm text-[color:var(--terminal-muted)]">Loading statement matrix...</p>
@@ -785,17 +782,11 @@ function FinancialsPageContent() {
}}
>
<td className="sticky left-0 z-10 bg-[color:var(--panel)]">
{'depth' in row ? (
<div className="flex items-center gap-2">
<span style={{ paddingLeft: `${Math.min(row.depth, 10) * 12}px` }}>{row.label}</span>
{row.hasDimensions ? <ChevronDown className="size-3 text-[color:var(--accent)]" /> : null}
</div>
) : (
<div className="flex items-center gap-2">
<span>{row.label}</span>
{row.hasDimensions ? <ChevronDown className="size-3 text-[color:var(--accent)]" /> : null}
</div>
)}
<div className="flex items-center gap-2">
<span style={{ paddingLeft: `${Math.min(row.depth, 10) * 12}px` }}>{row.label}</span>
{row.isExtension ? <span className="rounded border border-[color:var(--line-weak)] px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-[color:var(--terminal-muted)]">Ext</span> : null}
{row.hasDimensions ? <ChevronDown className="size-3 text-[color:var(--accent)]" /> : null}
</div>
</td>
{periods.map((period) => (
<td key={`${row.key}-${period.id}`}>
@@ -850,7 +841,44 @@ function FinancialsPageContent() {
</Panel>
{financials ? (
<Panel title="Data Source Status" subtitle="Hydration and parsing status for filing statement snapshots.">
<Panel title="Metric Validation" subtitle="Taxonomy-derived metrics are canonical; PDF LLM extraction is used for validation only.">
<div className="mb-3 flex items-center gap-2 text-sm text-[color:var(--terminal-muted)]">
<AlertTriangle className="size-4" />
<span>Overall status: {financials.metrics.validation?.status ?? 'not_run'}</span>
</div>
{(financials.metrics.validation?.checks.length ?? 0) === 0 ? (
<p className="text-sm text-[color:var(--terminal-muted)]">No validation checks available yet.</p>
) : (
<div className="overflow-x-auto">
<table className="data-table min-w-[760px]">
<thead>
<tr>
<th>Metric</th>
<th>Taxonomy</th>
<th>LLM (PDF)</th>
<th>Status</th>
<th>Pages</th>
</tr>
</thead>
<tbody>
{financials.metrics.validation?.checks.map((check) => (
<tr key={check.metricKey}>
<td>{check.metricKey}</td>
<td>{asDisplayCurrency(check.taxonomyValue, valueScale)}</td>
<td>{asDisplayCurrency(check.llmValue, valueScale)}</td>
<td>{check.status}</td>
<td>{check.evidencePages.join(', ') || 'n/a'}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</Panel>
) : null}
{financials ? (
<Panel title="Data Source Status" subtitle="Hydration and taxonomy parsing status for filing snapshots.">
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-5">
<div className="rounded-lg border border-[color:var(--line-weak)] bg-[color:var(--panel-soft)] px-3 py-2 text-sm">
<p className="text-[color:var(--terminal-muted)]">Hydrated</p>
@@ -879,7 +907,7 @@ function FinancialsPageContent() {
<Panel>
<div className="flex items-center gap-2 text-xs uppercase tracking-[0.24em] text-[color:var(--terminal-muted)]">
<ChartNoAxesCombined className="size-4" />
Financial Statements V2: standardized + filing-faithful history
Financial Statements V3: taxonomy + PDF LLM validation
</div>
</Panel>
</AppShell>