268 lines
9.7 KiB
TypeScript
268 lines
9.7 KiB
TypeScript
import { expect, test, type Page, type TestInfo } from '@playwright/test';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { join } from 'node:path';
|
|
|
|
const PASSWORD = 'Sup3rSecure!123';
|
|
const E2E_DATABASE_PATH = join(process.cwd(), 'data', 'e2e.sqlite');
|
|
|
|
test.describe.configure({ mode: 'serial' });
|
|
|
|
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-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
|
|
});
|
|
|
|
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;
|
|
}
|
|
|
|
function seedFiling(input: {
|
|
ticker: string;
|
|
companyName: string;
|
|
accessionNumber: string;
|
|
filingType: '10-K' | '10-Q';
|
|
filingDate: string;
|
|
summary: string;
|
|
}) {
|
|
const now = new Date().toISOString();
|
|
|
|
execFileSync('python3', [
|
|
'-c',
|
|
`
|
|
import json
|
|
import sqlite3
|
|
import sys
|
|
|
|
db_path, ticker, filing_type, filing_date, accession, company_name, now, summary = sys.argv[1:]
|
|
connection = sqlite3.connect(db_path)
|
|
try:
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO filing (
|
|
ticker,
|
|
filing_type,
|
|
filing_date,
|
|
accession_number,
|
|
cik,
|
|
company_name,
|
|
filing_url,
|
|
submission_url,
|
|
primary_document,
|
|
metrics,
|
|
analysis,
|
|
created_at,
|
|
updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
ticker,
|
|
filing_type,
|
|
filing_date,
|
|
accession,
|
|
"0001045810",
|
|
company_name,
|
|
f"https://www.sec.gov/Archives/{accession}.htm",
|
|
f"https://www.sec.gov/submissions/{accession}.json",
|
|
f"{accession}.htm",
|
|
json.dumps({
|
|
"revenue": 61000000000,
|
|
"netIncome": 29000000000,
|
|
"totalAssets": 98000000000,
|
|
"cash": 27000000000,
|
|
"debt": 11000000000,
|
|
}),
|
|
json.dumps({
|
|
"provider": "playwright",
|
|
"model": "fixture",
|
|
"text": summary,
|
|
}),
|
|
now,
|
|
now,
|
|
),
|
|
)
|
|
connection.commit()
|
|
finally:
|
|
connection.close()
|
|
`,
|
|
E2E_DATABASE_PATH,
|
|
input.ticker,
|
|
input.filingType,
|
|
input.filingDate,
|
|
input.accessionNumber,
|
|
input.companyName,
|
|
now,
|
|
input.summary
|
|
]);
|
|
}
|
|
|
|
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({
|
|
ticker: 'NVDA',
|
|
companyName: 'NVIDIA Corporation',
|
|
accessionNumber,
|
|
filingType: '10-K',
|
|
filingDate: '2026-02-18',
|
|
summary: 'AI datacenter demand remained the central upside driver with expanding operating leverage.'
|
|
});
|
|
|
|
await page.goto('/watchlist');
|
|
await page.getByLabel('Coverage ticker').fill('NVDA');
|
|
await page.getByLabel('Coverage company name').fill('NVIDIA Corporation');
|
|
await page.getByLabel('Coverage sector').fill('Technology');
|
|
await page.getByLabel('Coverage category').fill('Core');
|
|
await page.getByLabel('Coverage tags').fill('AI, semis');
|
|
await page.getByRole('button', { name: 'Save coverage' }).click();
|
|
|
|
await expect(page.getByRole('cell', { name: 'NVIDIA Corporation' })).toBeVisible();
|
|
await page.getByLabel('NVDA status').selectOption('active');
|
|
await expect(page.getByLabel('NVDA status')).toHaveValue('active');
|
|
await page.getByLabel('NVDA priority').selectOption('high');
|
|
await expect(page.getByLabel('NVDA priority')).toHaveValue('high');
|
|
|
|
await page.getByRole('link', { name: /^Analyze/ }).first().click();
|
|
await expect(page).toHaveURL(/\/analysis\?ticker=NVDA/);
|
|
await expect(page.getByText('Coverage Workflow')).toBeVisible();
|
|
|
|
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('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: 'Save to library' }).click();
|
|
await expect(page.getByText('Saved to the company research library.')).toBeVisible();
|
|
|
|
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 page.goto('/financials?ticker=NVDA');
|
|
await expect(page).toHaveURL(/\/financials\?ticker=NVDA/);
|
|
|
|
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('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');
|
|
await page.getByLabel('Holding ticker').fill('MSFT');
|
|
await page.getByLabel('Holding company name').fill('Microsoft Corporation');
|
|
await page.getByLabel('Holding shares').fill('10');
|
|
await page.getByLabel('Holding average cost').fill('100');
|
|
await page.getByLabel('Holding current price').fill('110');
|
|
await page.getByRole('button', { name: 'Save holding' }).click();
|
|
|
|
await expect(page.getByText('Microsoft Corporation')).toBeVisible();
|
|
await expect(page.getByRole('cell', { name: '$1,100.00' })).toBeVisible();
|
|
|
|
await page.getByRole('button', { name: /^Edit$/ }).first().click();
|
|
await page.getByLabel('Holding company name').fill('Microsoft Corp.');
|
|
await page.getByLabel('Holding current price').fill('120');
|
|
await page.getByRole('button', { name: 'Update holding' }).click();
|
|
|
|
await expect(page.getByText('Microsoft Corp.')).toBeVisible();
|
|
await expect(page.getByRole('cell', { name: '$1,200.00' })).toBeVisible();
|
|
|
|
await page.getByRole('button', { name: /^Delete$/ }).first().click();
|
|
await expect(page.getByText('No holdings added yet.')).toBeVisible();
|
|
});
|