feat(financials): add compact surface UI and graphing states
This commit is contained in:
346
e2e/financials.spec.ts
Normal file
346
e2e/financials.spec.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
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
|
||||
}
|
||||
]
|
||||
},
|
||||
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: {
|
||||
regime: 'us-gaap',
|
||||
fiscalPack: isBank ? 'bank_lender' : 'core',
|
||||
parserVersion: '0.1.0',
|
||||
unmappedRowCount: 0,
|
||||
materialUnmappedRowCount: 0
|
||||
},
|
||||
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.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();
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
@@ -29,6 +29,12 @@ function createFinancialsPayload(input: {
|
||||
cadence: 'annual' | 'quarterly' | 'ltm';
|
||||
surface: string;
|
||||
}) {
|
||||
const fiscalPack = input.ticker === 'JPM'
|
||||
? 'bank_lender'
|
||||
: input.ticker === 'BLK'
|
||||
? 'broker_asset_manager'
|
||||
: 'core';
|
||||
|
||||
return {
|
||||
financials: {
|
||||
company: {
|
||||
@@ -83,7 +89,50 @@ function createFinancialsPayload(input: {
|
||||
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',
|
||||
@@ -103,7 +152,8 @@ function createFinancialsPayload(input: {
|
||||
resolvedSourceRowKeys: {
|
||||
[`${input.ticker}-p1`]: 'total_assets',
|
||||
[`${input.ticker}-p2`]: 'total_assets'
|
||||
}
|
||||
},
|
||||
resolutionMethod: 'direct'
|
||||
},
|
||||
{
|
||||
key: 'free_cash_flow',
|
||||
@@ -123,10 +173,12 @@ function createFinancialsPayload(input: {
|
||||
resolvedSourceRowKeys: {
|
||||
[`${input.ticker}-p1`]: 'free_cash_flow',
|
||||
[`${input.ticker}-p2`]: 'free_cash_flow'
|
||||
}
|
||||
},
|
||||
resolutionMethod: 'direct'
|
||||
}
|
||||
]
|
||||
},
|
||||
statementDetails: null,
|
||||
ratioRows: [
|
||||
{
|
||||
key: 'gross_margin',
|
||||
@@ -177,6 +229,13 @@ function createFinancialsPayload(input: {
|
||||
taxonomy: null,
|
||||
validation: null
|
||||
},
|
||||
normalization: {
|
||||
regime: 'unknown',
|
||||
fiscalPack,
|
||||
parserVersion: '0.0.0',
|
||||
unmappedRowCount: 0,
|
||||
materialUnmappedRowCount: 0
|
||||
},
|
||||
dimensionBreakdown: null
|
||||
}
|
||||
};
|
||||
@@ -204,6 +263,10 @@ async function mockGraphingFinancials(page: Page) {
|
||||
? 'NVIDIA Corporation'
|
||||
: ticker === 'AMD'
|
||||
? 'Advanced Micro Devices, Inc.'
|
||||
: ticker === 'BLK'
|
||||
? 'BlackRock, Inc.'
|
||||
: ticker === 'JPM'
|
||||
? 'JPMorgan Chase & Co.'
|
||||
: 'Microsoft Corporation';
|
||||
|
||||
await route.fulfill({
|
||||
@@ -227,7 +290,7 @@ test('supports graphing compare controls and partial failures', async ({ page },
|
||||
|
||||
await expect(page).toHaveURL(/tickers=MSFT%2CAAPL%2CNVDA/);
|
||||
await expect(page.getByRole('heading', { name: 'Graphing' })).toBeVisible();
|
||||
await expect(page.getByText('Microsoft Corporation')).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/);
|
||||
@@ -245,11 +308,25 @@ test('supports graphing compare controls and partial failures', async ({ page },
|
||||
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.')).toBeVisible();
|
||||
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('Ticker not found')).toBeVisible();
|
||||
await expect(page.getByText('Microsoft Corporation')).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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user