338 lines
12 KiB
TypeScript
338 lines
12 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-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: {
|
|
parserEngine: 'fiscal-xbrl',
|
|
regime: 'unknown',
|
|
fiscalPack,
|
|
parserVersion: '0.0.0',
|
|
surfaceRowCount: 0,
|
|
detailRowCount: 0,
|
|
kpiRowCount: 0,
|
|
unmappedRowCount: 0,
|
|
materialUnmappedRowCount: 0,
|
|
warnings: []
|
|
},
|
|
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();
|
|
});
|