Add research workspace and graphing flows
This commit is contained in:
100
e2e/auth.spec.ts
100
e2e/auth.spec.ts
@@ -1,17 +1,51 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
|
||||
const PASSWORD = 'Sup3rSecure!123';
|
||||
|
||||
test('redirects protected routes to sign in and preserves the return path', async ({ page }) => {
|
||||
await page.goto('/analysis?ticker=nvda');
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
function createDeferred() {
|
||||
let resolve: (() => void) | null = null;
|
||||
const promise = new Promise<void>((done) => {
|
||||
resolve = done;
|
||||
});
|
||||
|
||||
return {
|
||||
promise,
|
||||
resolve: () => resolve?.()
|
||||
};
|
||||
}
|
||||
|
||||
async function gotoAuthPage(page: Page, path: string) {
|
||||
await page.goto(path, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
test('preserves the return path while switching between auth screens and shows the expected controls', async ({ page }) => {
|
||||
await gotoAuthPage(page, '/auth/signin?next=%2Fanalysis%3Fticker%3DNVDA');
|
||||
|
||||
await expect(page).toHaveURL(/\/auth\/signin\?/);
|
||||
await expect(page.getByRole('heading', { name: 'Secure Sign In' })).toBeVisible();
|
||||
expect(new URL(page.url()).searchParams.get('next')).toBe('/analysis?ticker=nvda');
|
||||
await expect(page.getByText('Use email/password or request a magic link.')).toBeVisible();
|
||||
await expect(page.locator('input[autocomplete="email"]')).toBeVisible();
|
||||
await expect(page.locator('input[autocomplete="current-password"]')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Sign in with password' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Send magic link' })).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'Create one' })).toHaveAttribute('href', '/auth/signup?next=%2Fanalysis%3Fticker%3DNVDA');
|
||||
|
||||
await page.getByRole('link', { name: 'Create one' }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/auth\/signup\?next=%2Fanalysis%3Fticker%3DNVDA$/);
|
||||
await expect(page.getByRole('heading', { name: 'Create Account' })).toBeVisible();
|
||||
await expect(page.getByText('Set up your operator profile to access portfolio and filings intelligence.')).toBeVisible();
|
||||
await expect(page.locator('input[autocomplete="name"]')).toBeVisible();
|
||||
await expect(page.locator('input[autocomplete="email"]')).toBeVisible();
|
||||
await expect(page.locator('input[autocomplete="new-password"]').first()).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Create account' })).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'Sign in' })).toHaveAttribute('href', '/auth/signin?next=%2Fanalysis%3Fticker%3DNVDA');
|
||||
});
|
||||
|
||||
test('shows client-side validation when signup passwords do not match', async ({ page }) => {
|
||||
await page.goto('/auth/signup');
|
||||
await gotoAuthPage(page, '/auth/signup');
|
||||
|
||||
await page.locator('input[autocomplete="name"]').fill('Playwright User');
|
||||
await page.locator('input[autocomplete="email"]').fill('mismatch@example.com');
|
||||
@@ -22,17 +56,57 @@ test('shows client-side validation when signup passwords do not match', async ({
|
||||
await expect(page.getByText('Passwords do not match.')).toBeVisible();
|
||||
});
|
||||
|
||||
test('creates a new account and lands on the command center', async ({ page }) => {
|
||||
const email = `playwright-${Date.now()}@example.com`;
|
||||
test('shows loading affordances while sign-in is in flight', async ({ page }) => {
|
||||
const gate = createDeferred();
|
||||
|
||||
await page.goto('/auth/signup');
|
||||
await page.route('**/api/auth/sign-in/email', async (route) => {
|
||||
await gate.promise;
|
||||
await route.fulfill({
|
||||
status: 401,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ message: 'Invalid credentials' })
|
||||
});
|
||||
});
|
||||
|
||||
await gotoAuthPage(page, '/auth/signin');
|
||||
await page.locator('input[autocomplete="email"]').fill('playwright@example.com');
|
||||
await page.locator('input[autocomplete="current-password"]').fill(PASSWORD);
|
||||
|
||||
const submitButton = page.getByRole('button', { name: 'Sign in with password' });
|
||||
const magicLinkButton = page.getByRole('button', { name: 'Send magic link' });
|
||||
|
||||
await submitButton.click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Signing in...' })).toBeDisabled();
|
||||
await expect(magicLinkButton).toBeDisabled();
|
||||
|
||||
gate.resolve();
|
||||
|
||||
await expect(page.getByText('Invalid credentials')).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows loading affordances while sign-up is in flight', async ({ page }) => {
|
||||
const gate = createDeferred();
|
||||
|
||||
await page.route('**/api/auth/sign-up/email', async (route) => {
|
||||
await gate.promise;
|
||||
await route.fulfill({
|
||||
status: 409,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ message: 'Email already exists' })
|
||||
});
|
||||
});
|
||||
|
||||
await gotoAuthPage(page, '/auth/signup');
|
||||
await page.locator('input[autocomplete="name"]').fill('Playwright User');
|
||||
await page.locator('input[autocomplete="email"]').fill(email);
|
||||
await page.locator('input[autocomplete="email"]').fill(`playwright-${Date.now()}@example.com`);
|
||||
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).toHaveURL(/\/$/);
|
||||
await expect(page.getByRole('heading', { name: 'Command Center' })).toBeVisible();
|
||||
await expect(page.getByText('Quick Links')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Creating account...' })).toBeDisabled();
|
||||
|
||||
gate.resolve();
|
||||
|
||||
await expect(page.getByText('Email already exists')).toBeVisible();
|
||||
});
|
||||
|
||||
4
e2e/fixtures/sample-research.txt
Normal file
4
e2e/fixtures/sample-research.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
Supply-chain diligence notes:
|
||||
- Channel checks remain constructive.
|
||||
- Gross margin watchpoint is still mix-driven.
|
||||
- Need follow-up on inventory normalization pace.
|
||||
255
e2e/graphing.spec.ts
Normal file
255
e2e/graphing.spec.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
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;
|
||||
}) {
|
||||
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'
|
||||
}
|
||||
},
|
||||
{
|
||||
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'
|
||||
}
|
||||
},
|
||||
{
|
||||
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'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
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
|
||||
},
|
||||
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.'
|
||||
: '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')).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.')).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();
|
||||
});
|
||||
@@ -17,14 +17,59 @@ function toSlug(value: string) {
|
||||
|
||||
async function signUp(page: Page, testInfo: TestInfo) {
|
||||
const email = `playwright-research-${testInfo.workerIndex}-${toSlug(testInfo.title)}-${Date.now()}@example.com`;
|
||||
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://127.0.0.1:3400';
|
||||
const output = execFileSync('bun', [
|
||||
'-e',
|
||||
`
|
||||
const [email, password] = process.argv.slice(1);
|
||||
const { auth } = await import('./lib/auth');
|
||||
const response = await auth.api.signUpEmail({
|
||||
body: {
|
||||
name: 'Playwright Research User',
|
||||
email,
|
||||
password,
|
||||
callbackURL: '/'
|
||||
},
|
||||
asResponse: true
|
||||
});
|
||||
|
||||
await page.goto('/auth/signup');
|
||||
await page.locator('input[autocomplete="name"]').fill('Playwright Research 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();
|
||||
console.log(JSON.stringify({
|
||||
status: response.status,
|
||||
sessionCookie: response.headers.get('set-cookie')
|
||||
}));
|
||||
`,
|
||||
email,
|
||||
PASSWORD
|
||||
], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
DATABASE_URL: `file:${E2E_DATABASE_PATH}`,
|
||||
BETTER_AUTH_BASE_URL: baseURL,
|
||||
BETTER_AUTH_SECRET: 'playwright-e2e-secret-playwright-e2e-secret',
|
||||
BETTER_AUTH_TRUSTED_ORIGINS: baseURL
|
||||
},
|
||||
encoding: 'utf8'
|
||||
});
|
||||
|
||||
const { status, sessionCookie } = JSON.parse(output) as { status: number; sessionCookie: string | null };
|
||||
expect(status).toBe(200);
|
||||
expect(sessionCookie).toBeTruthy();
|
||||
|
||||
const [cookieNameValue] = sessionCookie!.split(';');
|
||||
const separatorIndex = cookieNameValue!.indexOf('=');
|
||||
const cookieName = cookieNameValue!.slice(0, separatorIndex);
|
||||
const cookieValue = cookieNameValue!.slice(separatorIndex + 1);
|
||||
|
||||
await page.context().addCookies([{
|
||||
name: cookieName,
|
||||
value: cookieValue,
|
||||
url: baseURL,
|
||||
httpOnly: true,
|
||||
sameSite: 'Lax'
|
||||
}]);
|
||||
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveURL(/\/$/);
|
||||
return email;
|
||||
}
|
||||
@@ -109,7 +154,9 @@ finally:
|
||||
}
|
||||
|
||||
test('supports the core coverage-to-research workflow', async ({ page }, testInfo) => {
|
||||
test.slow();
|
||||
const accessionNumber = `0001045810-26-${String(Date.now()).slice(-6)}`;
|
||||
const uploadFixture = join(process.cwd(), 'e2e', 'fixtures', 'sample-research.txt');
|
||||
await signUp(page, testInfo);
|
||||
|
||||
seedFiling({
|
||||
@@ -139,30 +186,61 @@ test('supports the core coverage-to-research workflow', async ({ page }, testInf
|
||||
await expect(page).toHaveURL(/\/analysis\?ticker=NVDA/);
|
||||
await expect(page.getByText('Coverage Workflow')).toBeVisible();
|
||||
|
||||
await page.getByLabel('Journal title').fill('Own-the-stack moat check');
|
||||
await page.getByLabel('Journal body').fill('Monitor hyperscaler concentration, gross margin durability, and Blackwell shipment cadence.');
|
||||
await page.getByRole('link', { name: 'Open research' }).click();
|
||||
await expect(page).toHaveURL(/\/research\?ticker=NVDA/);
|
||||
await page.getByLabel('Research note title').fill('Own-the-stack moat check');
|
||||
await page.getByLabel('Research note summary').fill('Initial moat checkpoint');
|
||||
await page.getByLabel('Research note body').fill('Monitor hyperscaler concentration, gross margin durability, and Blackwell shipment cadence.');
|
||||
await page.getByLabel('Research note tags').fill('moat, thesis');
|
||||
await page.getByRole('button', { name: 'Save note' }).click();
|
||||
await expect(page.getByText('Own-the-stack moat check')).toBeVisible();
|
||||
await expect(page.getByText('Saved note to the research library.')).toBeVisible();
|
||||
|
||||
await page.getByLabel('Upload title').fill('Supply-chain diligence');
|
||||
await page.getByLabel('Upload summary').fill('Vendor and channel-check notes');
|
||||
await page.getByLabel('Upload tags').fill('diligence, channel-check');
|
||||
await page.getByLabel('Upload file').setInputFiles(uploadFixture);
|
||||
await page.locator('button', { hasText: 'Upload file' }).click();
|
||||
await expect(page.getByText('Uploaded research file.')).toBeVisible();
|
||||
|
||||
await page.goto(`/analysis?ticker=NVDA`);
|
||||
await page.getByRole('link', { name: 'Open summary' }).first().click();
|
||||
await expect(page).toHaveURL(/\/analysis\/reports\/NVDA\//);
|
||||
await page.getByRole('button', { name: 'Add to journal' }).click();
|
||||
await expect(page.getByText('Saved to the company research journal.')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Save to library' }).click();
|
||||
await expect(page.getByText('Saved to the company research library.')).toBeVisible();
|
||||
|
||||
await page.getByRole('link', { name: 'Back to analysis' }).click();
|
||||
await page.goto('/research?ticker=NVDA');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await expect(page).toHaveURL(/\/research\?ticker=NVDA/);
|
||||
await expect(page.getByRole('heading', { name: '10-K AI memo' }).first()).toBeVisible();
|
||||
|
||||
await page.getByLabel('Memo rating').selectOption('buy');
|
||||
await page.getByLabel('Memo conviction').selectOption('high');
|
||||
await page.getByLabel('Memo time horizon').fill('24');
|
||||
await page.getByLabel('Packet title').fill('NVIDIA buy-side packet');
|
||||
await page.getByLabel('Packet subtitle').fill('AI infrastructure compounder');
|
||||
await page.getByLabel('Memo Thesis').fill('Maintain a constructive stance as datacenter demand and platform depth widen the moat.');
|
||||
await page.getByLabel('Memo Catalysts').fill('Blackwell ramp, enterprise inference demand, and sustained operating leverage.');
|
||||
await page.getByLabel('Memo Risks').fill('Customer concentration, competition, and execution on supply.');
|
||||
await page.getByRole('button', { name: 'Save memo' }).click();
|
||||
await expect(page.getByText('Saved investment memo.')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Attach' }).first().click();
|
||||
await expect(page.getByText('Attached evidence to Thesis.')).toBeVisible();
|
||||
|
||||
await page.goto('/analysis?ticker=NVDA');
|
||||
await expect(page).toHaveURL(/\/analysis\?ticker=NVDA/);
|
||||
await expect(page.getByText('10-K AI memo')).toBeVisible();
|
||||
|
||||
await page.getByRole('link', { name: 'Open financials' }).click();
|
||||
await page.goto('/financials?ticker=NVDA');
|
||||
await expect(page).toHaveURL(/\/financials\?ticker=NVDA/);
|
||||
|
||||
await page.getByRole('link', { name: 'Filings' }).first().click();
|
||||
await page.goto('/filings?ticker=NVDA');
|
||||
await expect(page).toHaveURL(/\/filings\?ticker=NVDA/);
|
||||
await expect(page.getByRole('cell', { name: 'NVIDIA Corporation' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /journal/i }).first()).toBeVisible();
|
||||
await expect(page.getByRole('link', { name: 'Summary' }).first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('supports add, edit, and delete holding flows with summary refresh', async ({ page }, testInfo) => {
|
||||
test.slow();
|
||||
await signUp(page, testInfo);
|
||||
|
||||
await page.goto('/portfolio');
|
||||
|
||||
Reference in New Issue
Block a user