Fix filings ticker scope consistency
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { Bot, Download, ExternalLink, NotebookPen, Search, TimerReset } from 'lucide-react';
|
import { Bot, Download, ExternalLink, NotebookPen, Search, TimerReset } from 'lucide-react';
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { AppShell } from '@/components/shell/app-shell';
|
import { AppShell } from '@/components/shell/app-shell';
|
||||||
import { Panel } from '@/components/ui/panel';
|
import { Panel } from '@/components/ui/panel';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
@@ -28,6 +28,7 @@ const FINANCIAL_VALUE_SCALE_OPTIONS: Array<{ value: NumberScaleUnit; label: stri
|
|||||||
{ value: 'millions', label: 'Millions (M)' },
|
{ value: 'millions', label: 'Millions (M)' },
|
||||||
{ value: 'billions', label: 'Billions (B)' }
|
{ value: 'billions', label: 'Billions (B)' }
|
||||||
];
|
];
|
||||||
|
const FILINGS_QUERY_LIMIT = 120;
|
||||||
|
|
||||||
export default function FilingsPage() {
|
export default function FilingsPage() {
|
||||||
return (
|
return (
|
||||||
@@ -66,6 +67,15 @@ function parseTagsInput(input: string) {
|
|||||||
return [...unique];
|
return [...unique];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeTickerParam(value: string | null) {
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = value.trim().toUpperCase();
|
||||||
|
return normalized.length > 0 ? normalized : null;
|
||||||
|
}
|
||||||
|
|
||||||
function asScaledFinancialSnapshot(
|
function asScaledFinancialSnapshot(
|
||||||
value: number | null | undefined,
|
value: number | null | undefined,
|
||||||
scale: NumberScaleUnit
|
scale: NumberScaleUnit
|
||||||
@@ -123,54 +133,39 @@ function FilingExternalLink({ href, label }: FilingExternalLinkProps) {
|
|||||||
function FilingsPageContent() {
|
function FilingsPageContent() {
|
||||||
const { isPending, isAuthenticated } = useAuthGuard();
|
const { isPending, isAuthenticated } = useAuthGuard();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
const router = useRouter();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { prefetchReport } = useLinkPrefetch();
|
const { prefetchReport } = useLinkPrefetch();
|
||||||
|
const activeTickerFilter = useMemo(() => normalizeTickerParam(searchParams.get('ticker')), [searchParams]);
|
||||||
|
const activeFilingsQueryKey = useMemo(
|
||||||
|
() => queryKeys.filings(activeTickerFilter, FILINGS_QUERY_LIMIT),
|
||||||
|
[activeTickerFilter]
|
||||||
|
);
|
||||||
|
const filingsQuery = useQuery({
|
||||||
|
...filingsQueryOptions({ ticker: activeTickerFilter ?? undefined, limit: FILINGS_QUERY_LIMIT }),
|
||||||
|
enabled: !isPending && isAuthenticated
|
||||||
|
});
|
||||||
|
|
||||||
const [filings, setFilings] = useState<Filing[]>([]);
|
const [syncTickerInput, setSyncTickerInput] = useState(() => activeTickerFilter ?? '');
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [syncTickerInput, setSyncTickerInput] = useState('');
|
|
||||||
const [syncCategoryInput, setSyncCategoryInput] = useState('');
|
const [syncCategoryInput, setSyncCategoryInput] = useState('');
|
||||||
const [syncTagsInput, setSyncTagsInput] = useState('');
|
const [syncTagsInput, setSyncTagsInput] = useState('');
|
||||||
const [filterTickerInput, setFilterTickerInput] = useState('');
|
const [filterTickerInput, setFilterTickerInput] = useState(() => activeTickerFilter ?? '');
|
||||||
const [searchTicker, setSearchTicker] = useState('');
|
|
||||||
const [financialValueScale, setFinancialValueScale] = useState<NumberScaleUnit>('millions');
|
const [financialValueScale, setFinancialValueScale] = useState<NumberScaleUnit>('millions');
|
||||||
const [actionNotice, setActionNotice] = useState<string | null>(null);
|
const [actionNotice, setActionNotice] = useState<string | null>(null);
|
||||||
|
const [actionError, setActionError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ticker = searchParams.get('ticker');
|
setSyncTickerInput(activeTickerFilter ?? '');
|
||||||
if (ticker) {
|
setFilterTickerInput(activeTickerFilter ?? '');
|
||||||
const normalized = ticker.toUpperCase();
|
}, [activeTickerFilter]);
|
||||||
setSyncTickerInput(normalized);
|
|
||||||
setFilterTickerInput(normalized);
|
|
||||||
setSearchTicker(normalized);
|
|
||||||
}
|
|
||||||
}, [searchParams]);
|
|
||||||
|
|
||||||
const loadFilings = useCallback(async (ticker?: string) => {
|
const filings = filingsQuery.data?.filings ?? [];
|
||||||
const options = filingsQueryOptions({ ticker, limit: 120 });
|
const loading = filingsQuery.isPending;
|
||||||
|
const filingsError = filingsQuery.error instanceof Error
|
||||||
if (!queryClient.getQueryData(options.queryKey)) {
|
? filingsQuery.error.message
|
||||||
setLoading(true);
|
: filingsQuery.error
|
||||||
}
|
? 'Unable to fetch filings'
|
||||||
|
: null;
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await queryClient.fetchQuery(options);
|
|
||||||
setFilings(response.filings);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Unable to fetch filings');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}, [queryClient]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isPending && isAuthenticated) {
|
|
||||||
void loadFilings(searchTicker || undefined);
|
|
||||||
}
|
|
||||||
}, [isPending, isAuthenticated, searchTicker, loadFilings]);
|
|
||||||
|
|
||||||
const triggerSync = async () => {
|
const triggerSync = async () => {
|
||||||
if (!syncTickerInput.trim()) {
|
if (!syncTickerInput.trim()) {
|
||||||
@@ -178,6 +173,7 @@ function FilingsPageContent() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setActionError(null);
|
||||||
await queueFilingSync({
|
await queueFilingSync({
|
||||||
ticker: syncTickerInput.trim().toUpperCase(),
|
ticker: syncTickerInput.trim().toUpperCase(),
|
||||||
limit: 20,
|
limit: 20,
|
||||||
@@ -185,25 +181,26 @@ function FilingsPageContent() {
|
|||||||
tags: parseTagsInput(syncTagsInput)
|
tags: parseTagsInput(syncTagsInput)
|
||||||
});
|
});
|
||||||
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
|
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
|
||||||
void queryClient.invalidateQueries({ queryKey: ['filings'] });
|
void queryClient.invalidateQueries({ queryKey: activeFilingsQueryKey });
|
||||||
await loadFilings(searchTicker || undefined);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to queue filing sync');
|
setActionError(err instanceof Error ? err.message : 'Failed to queue filing sync');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const triggerAnalysis = async (accessionNumber: string) => {
|
const triggerAnalysis = async (accessionNumber: string) => {
|
||||||
try {
|
try {
|
||||||
|
setActionError(null);
|
||||||
await queueFilingAnalysis(accessionNumber);
|
await queueFilingAnalysis(accessionNumber);
|
||||||
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
|
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
|
||||||
void queryClient.invalidateQueries({ queryKey: ['report'] });
|
void queryClient.invalidateQueries({ queryKey: ['report'] });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to queue filing analysis');
|
setActionError(err instanceof Error ? err.message : 'Failed to queue filing analysis');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveToLibrary = async (filing: Filing) => {
|
const saveToLibrary = async (filing: Filing) => {
|
||||||
try {
|
try {
|
||||||
|
setActionError(null);
|
||||||
await createResearchArtifact({
|
await createResearchArtifact({
|
||||||
ticker: filing.ticker,
|
ticker: filing.ticker,
|
||||||
kind: 'filing',
|
kind: 'filing',
|
||||||
@@ -234,10 +231,23 @@ function FilingsPageContent() {
|
|||||||
void queryClient.invalidateQueries({ queryKey: queryKeys.watchlist() });
|
void queryClient.invalidateQueries({ queryKey: queryKeys.watchlist() });
|
||||||
setActionNotice(`Saved ${filing.accession_number} to the ${filing.ticker} research library.`);
|
setActionNotice(`Saved ${filing.accession_number} to the ${filing.ticker} research library.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to save filing to library');
|
setActionError(err instanceof Error ? err.message : 'Failed to save filing to library');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const replaceTickerFilter = (ticker: string | null) => {
|
||||||
|
const nextParams = new URLSearchParams(searchParams.toString());
|
||||||
|
|
||||||
|
if (ticker) {
|
||||||
|
nextParams.set('ticker', ticker);
|
||||||
|
} else {
|
||||||
|
nextParams.delete('ticker');
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextQuery = nextParams.toString();
|
||||||
|
router.replace(nextQuery ? `/filings?${nextQuery}` : '/filings', { scroll: false });
|
||||||
|
};
|
||||||
|
|
||||||
const groupedByTicker = useMemo(() => {
|
const groupedByTicker = useMemo(() => {
|
||||||
const counts = new Map<string, number>();
|
const counts = new Map<string, number>();
|
||||||
|
|
||||||
@@ -260,11 +270,11 @@ function FilingsPageContent() {
|
|||||||
<AppShell
|
<AppShell
|
||||||
title="Filings"
|
title="Filings"
|
||||||
subtitle="Sync SEC submissions, keep 10-K/10-Q financial snapshots, and analyze qualitative signals from other forms."
|
subtitle="Sync SEC submissions, keep 10-K/10-Q financial snapshots, and analyze qualitative signals from other forms."
|
||||||
activeTicker={searchTicker || null}
|
activeTicker={activeTickerFilter}
|
||||||
actions={(
|
actions={(
|
||||||
<>
|
<>
|
||||||
<Link
|
<Link
|
||||||
href={`/search${searchTicker ? `?ticker=${encodeURIComponent(searchTicker)}` : ''}`}
|
href={`/search${activeTickerFilter ? `?ticker=${encodeURIComponent(activeTickerFilter)}` : ''}`}
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-lg border border-[color:var(--line-weak)] px-3 py-2 text-sm text-[color:var(--accent)] transition hover:border-[color:var(--line-strong)] hover:text-[color:var(--accent-strong)]"
|
className="inline-flex items-center justify-center gap-2 rounded-lg border border-[color:var(--line-weak)] px-3 py-2 text-sm text-[color:var(--accent)] transition hover:border-[color:var(--line-strong)] hover:text-[color:var(--accent-strong)]"
|
||||||
>
|
>
|
||||||
<Search className="size-4" />
|
<Search className="size-4" />
|
||||||
@@ -274,8 +284,7 @@ function FilingsPageContent() {
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="w-full sm:w-auto"
|
className="w-full sm:w-auto"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void queryClient.invalidateQueries({ queryKey: queryKeys.filings(searchTicker || null, 120) });
|
void queryClient.invalidateQueries({ queryKey: activeFilingsQueryKey });
|
||||||
void loadFilings(searchTicker || undefined);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TimerReset className="size-4" />
|
<TimerReset className="size-4" />
|
||||||
@@ -323,7 +332,9 @@ function FilingsPageContent() {
|
|||||||
className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center"
|
className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-center"
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setSearchTicker(filterTickerInput.trim().toUpperCase());
|
const nextTicker = normalizeTickerParam(filterTickerInput);
|
||||||
|
setFilterTickerInput(nextTicker ?? '');
|
||||||
|
replaceTickerFilter(nextTicker);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
@@ -342,7 +353,7 @@ function FilingsPageContent() {
|
|||||||
className="w-full sm:w-auto"
|
className="w-full sm:w-auto"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFilterTickerInput('');
|
setFilterTickerInput('');
|
||||||
setSearchTicker('');
|
replaceTickerFilter(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
@@ -353,7 +364,7 @@ function FilingsPageContent() {
|
|||||||
|
|
||||||
<Panel
|
<Panel
|
||||||
title="Filing Ledger"
|
title="Filing Ledger"
|
||||||
subtitle={`${filings.length} records loaded${searchTicker ? ` for ${searchTicker}` : ''}. Values shown in ${selectedFinancialScaleLabel}.`}
|
subtitle={`${filings.length} records loaded${activeTickerFilter ? ` for ${activeTickerFilter}` : ''}. Values shown in ${selectedFinancialScaleLabel}.`}
|
||||||
variant="surface"
|
variant="surface"
|
||||||
actions={(
|
actions={(
|
||||||
<div className="flex w-full flex-wrap justify-start gap-2 sm:justify-end">
|
<div className="flex w-full flex-wrap justify-start gap-2 sm:justify-end">
|
||||||
@@ -371,7 +382,8 @@ function FilingsPageContent() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{error ? <p className="text-sm text-[#ffb5b5]">{error}</p> : null}
|
{actionError ? <p className="text-sm text-[#ffb5b5]">{actionError}</p> : null}
|
||||||
|
{filingsError ? <p className="text-sm text-[#ffb5b5]">{filingsError}</p> : null}
|
||||||
{actionNotice ? <p className="mt-2 text-sm text-[color:var(--accent)]">{actionNotice}</p> : null}
|
{actionNotice ? <p className="mt-2 text-sm text-[color:var(--accent)]">{actionNotice}</p> : null}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="text-sm text-[color:var(--terminal-muted)]">Fetching filings...</p>
|
<p className="text-sm text-[color:var(--terminal-muted)]">Fetching filings...</p>
|
||||||
|
|||||||
202
e2e/filings.spec.ts
Normal file
202
e2e/filings.spec.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import { expect, test, type Page } from '@playwright/test';
|
||||||
|
|
||||||
|
const PASSWORD = 'Sup3rSecure!123';
|
||||||
|
|
||||||
|
test.describe.configure({ mode: 'serial' });
|
||||||
|
|
||||||
|
type FilingFixture = {
|
||||||
|
id: number;
|
||||||
|
ticker: string;
|
||||||
|
filing_type: '10-K' | '10-Q' | '8-K';
|
||||||
|
filing_date: string;
|
||||||
|
accession_number: string;
|
||||||
|
cik: string;
|
||||||
|
company_name: string;
|
||||||
|
filing_url: string | null;
|
||||||
|
submission_url: string | null;
|
||||||
|
primary_document: string | null;
|
||||||
|
metrics: {
|
||||||
|
revenue: number | null;
|
||||||
|
netIncome: number | null;
|
||||||
|
totalAssets: number | null;
|
||||||
|
cash: number | null;
|
||||||
|
debt: number | null;
|
||||||
|
} | null;
|
||||||
|
analysis: null;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function uniqueEmail(prefix: string) {
|
||||||
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@example.com`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFiling(input: {
|
||||||
|
id: number;
|
||||||
|
ticker: string;
|
||||||
|
accessionNumber: string;
|
||||||
|
filingType: '10-K' | '10-Q' | '8-K';
|
||||||
|
filingDate: string;
|
||||||
|
companyName: string;
|
||||||
|
revenue?: number | null;
|
||||||
|
}): FilingFixture {
|
||||||
|
return {
|
||||||
|
id: input.id,
|
||||||
|
ticker: input.ticker,
|
||||||
|
filing_type: input.filingType,
|
||||||
|
filing_date: input.filingDate,
|
||||||
|
accession_number: input.accessionNumber,
|
||||||
|
cik: '0001045810',
|
||||||
|
company_name: input.companyName,
|
||||||
|
filing_url: `https://www.sec.gov/Archives/${input.accessionNumber}.htm`,
|
||||||
|
submission_url: `https://www.sec.gov/submissions/${input.accessionNumber}.json`,
|
||||||
|
primary_document: `${input.accessionNumber}.htm`,
|
||||||
|
metrics: input.revenue === undefined
|
||||||
|
? null
|
||||||
|
: {
|
||||||
|
revenue: input.revenue,
|
||||||
|
netIncome: null,
|
||||||
|
totalAssets: null,
|
||||||
|
cash: null,
|
||||||
|
debt: null
|
||||||
|
},
|
||||||
|
analysis: null,
|
||||||
|
created_at: '2026-03-14T12:00:00.000Z',
|
||||||
|
updated_at: '2026-03-14T12:00:00.000Z'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signUp(page: Page, email: string) {
|
||||||
|
await page.goto('/auth/signup');
|
||||||
|
await page.locator('input[autocomplete="name"]').fill('Playwright Filings 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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installFilingsRouteStub(
|
||||||
|
page: Page,
|
||||||
|
options?: {
|
||||||
|
unscopedDelayMs?: number;
|
||||||
|
scopedDelayMs?: number;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const nvdaFilings = [
|
||||||
|
createFiling({
|
||||||
|
id: 1,
|
||||||
|
ticker: 'NVDA',
|
||||||
|
accessionNumber: '0001045810-26-000001',
|
||||||
|
filingType: '10-K',
|
||||||
|
filingDate: '2026-03-13',
|
||||||
|
companyName: 'NVIDIA Corporation',
|
||||||
|
revenue: 130_500_000_000
|
||||||
|
})
|
||||||
|
];
|
||||||
|
const msftFilings = [
|
||||||
|
createFiling({
|
||||||
|
id: 2,
|
||||||
|
ticker: 'MSFT',
|
||||||
|
accessionNumber: '0000789019-26-000002',
|
||||||
|
filingType: '10-Q',
|
||||||
|
filingDate: '2026-03-12',
|
||||||
|
companyName: 'Microsoft Corporation',
|
||||||
|
revenue: 71_000_000_000
|
||||||
|
})
|
||||||
|
];
|
||||||
|
const mixedFilings = [...nvdaFilings, ...msftFilings];
|
||||||
|
|
||||||
|
await page.route(/\/api\/filings(\?.*)?$/, async (route) => {
|
||||||
|
const url = new URL(route.request().url());
|
||||||
|
const ticker = url.searchParams.get('ticker')?.trim().toUpperCase() ?? null;
|
||||||
|
const delay = ticker === 'NVDA'
|
||||||
|
? options?.scopedDelayMs ?? 0
|
||||||
|
: options?.unscopedDelayMs ?? 0;
|
||||||
|
|
||||||
|
if (delay > 0) {
|
||||||
|
await page.waitForTimeout(delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
const filings = ticker === 'NVDA' ? nvdaFilings : mixedFilings;
|
||||||
|
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({ filings })
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function filingsLedger(page: Page) {
|
||||||
|
return page.locator('section').filter({
|
||||||
|
has: page.getByRole('heading', { name: 'Filing Ledger' })
|
||||||
|
}).first();
|
||||||
|
}
|
||||||
|
|
||||||
|
test('direct URL entry keeps the filings ledger scoped to the URL ticker', async ({ page }) => {
|
||||||
|
await signUp(page, uniqueEmail('playwright-filings-direct'));
|
||||||
|
await installFilingsRouteStub(page);
|
||||||
|
|
||||||
|
await page.goto('/filings?ticker=NVDA', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
const ledger = await filingsLedger(page);
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/filings\?ticker=NVDA$/);
|
||||||
|
await expect(ledger.getByText('1 records loaded for NVDA. Values shown in Millions (M).')).toBeVisible();
|
||||||
|
await expect(ledger.getByRole('cell', { name: 'NVIDIA Corporation' })).toBeVisible();
|
||||||
|
await expect(ledger.getByRole('cell', { name: 'Microsoft Corporation' })).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('apply and clear keep the URL and visible filings rows aligned', async ({ page }) => {
|
||||||
|
await signUp(page, uniqueEmail('playwright-filings-apply-clear'));
|
||||||
|
await installFilingsRouteStub(page);
|
||||||
|
|
||||||
|
await page.goto('/filings', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
const ledger = await filingsLedger(page);
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/filings$/);
|
||||||
|
await expect(ledger.getByText('2 records loaded. Values shown in Millions (M).')).toBeVisible();
|
||||||
|
await expect(ledger.getByRole('cell', { name: 'NVIDIA Corporation' })).toBeVisible();
|
||||||
|
await expect(ledger.getByRole('cell', { name: 'Microsoft Corporation' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Ticker filter').fill('nvda');
|
||||||
|
await page.getByRole('button', { name: 'Apply' }).click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/filings\?ticker=NVDA$/);
|
||||||
|
await expect(ledger.getByText('1 records loaded for NVDA. Values shown in Millions (M).')).toBeVisible();
|
||||||
|
await expect(ledger.getByRole('cell', { name: 'NVIDIA Corporation' })).toBeVisible();
|
||||||
|
await expect(ledger.getByRole('cell', { name: 'Microsoft Corporation' })).toHaveCount(0);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Clear' }).click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/filings$/);
|
||||||
|
await expect(ledger.getByText('2 records loaded. Values shown in Millions (M).')).toBeVisible();
|
||||||
|
await expect(ledger.getByRole('cell', { name: 'NVIDIA Corporation' })).toBeVisible();
|
||||||
|
await expect(ledger.getByRole('cell', { name: 'Microsoft Corporation' })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a stale global filings response cannot overwrite a newer scoped ledger', async ({ page }) => {
|
||||||
|
await signUp(page, uniqueEmail('playwright-filings-stale'));
|
||||||
|
await installFilingsRouteStub(page, {
|
||||||
|
unscopedDelayMs: 900,
|
||||||
|
scopedDelayMs: 50
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto('/filings', { waitUntil: 'domcontentloaded' });
|
||||||
|
|
||||||
|
const ledger = await filingsLedger(page);
|
||||||
|
|
||||||
|
await page.getByPlaceholder('Ticker filter').fill('NVDA');
|
||||||
|
await page.getByRole('button', { name: 'Apply' }).click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/filings\?ticker=NVDA$/);
|
||||||
|
await expect(ledger.getByText('1 records loaded for NVDA. Values shown in Millions (M).')).toBeVisible();
|
||||||
|
|
||||||
|
await page.waitForTimeout(1_100);
|
||||||
|
|
||||||
|
await expect(ledger.getByRole('cell', { name: 'NVIDIA Corporation' })).toBeVisible();
|
||||||
|
await expect(ledger.getByRole('cell', { name: 'Microsoft Corporation' })).toHaveCount(0);
|
||||||
|
});
|
||||||
@@ -467,6 +467,47 @@ if (process.env.RUN_TASK_WORKFLOW_E2E === '1') {
|
|||||||
expect(task.payload.tags).toEqual(['semis', 'ai']);
|
expect(task.payload.tags).toEqual(['semis', 'ai']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('scopes the filings endpoint by ticker while leaving the global endpoint mixed', async () => {
|
||||||
|
if (!sqliteClient) {
|
||||||
|
throw new Error('sqlite client not initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
seedFilingRecord(sqliteClient, {
|
||||||
|
ticker: 'NVDA',
|
||||||
|
accessionNumber: '0000000000-26-000110',
|
||||||
|
filingType: '10-Q',
|
||||||
|
filingDate: '2026-03-12',
|
||||||
|
companyName: 'NVIDIA Corporation'
|
||||||
|
});
|
||||||
|
seedFilingRecord(sqliteClient, {
|
||||||
|
ticker: 'MSFT',
|
||||||
|
accessionNumber: '0000000000-26-000111',
|
||||||
|
filingType: '10-K',
|
||||||
|
filingDate: '2026-03-11',
|
||||||
|
companyName: 'Microsoft Corporation'
|
||||||
|
});
|
||||||
|
|
||||||
|
const scoped = await jsonRequest('GET', '/api/filings?ticker=NVDA&limit=120');
|
||||||
|
expect(scoped.response.status).toBe(200);
|
||||||
|
|
||||||
|
const scopedFilings = (scoped.json as {
|
||||||
|
filings: Array<{ ticker: string }>;
|
||||||
|
}).filings;
|
||||||
|
|
||||||
|
expect(scopedFilings.length).toBeGreaterThan(0);
|
||||||
|
expect(scopedFilings.every((filing) => filing.ticker === 'NVDA')).toBe(true);
|
||||||
|
|
||||||
|
const global = await jsonRequest('GET', '/api/filings?limit=120');
|
||||||
|
expect(global.response.status).toBe(200);
|
||||||
|
|
||||||
|
const globalTickers = new Set((global.json as {
|
||||||
|
filings: Array<{ ticker: string }>;
|
||||||
|
}).filings.map((filing) => filing.ticker));
|
||||||
|
|
||||||
|
expect(globalTickers.has('NVDA')).toBe(true);
|
||||||
|
expect(globalTickers.has('MSFT')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('updates coverage status and archives while appending status-change journal history', async () => {
|
it('updates coverage status and archives while appending status-change journal history', async () => {
|
||||||
const created = await jsonRequest('POST', '/api/watchlist', {
|
const created = await jsonRequest('POST', '/api/watchlist', {
|
||||||
ticker: 'amd',
|
ticker: 'amd',
|
||||||
|
|||||||
Reference in New Issue
Block a user