main
mjs 105 lines 6.08 KB
Raw
1 import assert from "node:assert/strict";
2 import { readFileSync } from "node:fs";
3 import test from "node:test";
4
5 const workspace = readFileSync(new URL("../app/components/investment-workspace.tsx", import.meta.url), "utf8");
6 const api = readFileSync(new URL("../app/lib/portfolio-api.ts", import.meta.url), "utf8");
7 const styles = readFileSync(new URL("../app/styles.css", import.meta.url), "utf8");
8
9 const openStart = workspace.indexOf("async function openResearchReadiness");
10 const openEnd = workspace.indexOf("async function findResearchData", openStart);
11 const readPath = workspace.slice(openStart, openEnd);
12 const ensureStart = openEnd;
13 const ensureEnd = workspace.indexOf("function openMarketIntelligenceResearch", ensureStart);
14 const ensurePath = workspace.slice(ensureStart, ensureEnd);
15 const dialogStart = workspace.indexOf("function ResearchReadinessDialog");
16 const dialogEnd = workspace.indexOf("function AuthPasswordInput", dialogStart);
17 const dialog = workspace.slice(dialogStart, dialogEnd);
18
19 test("Research opens the provider-free readiness popup by global instrument identity", () => {
20 assert.match(workspace, /Research readiness\s*<\/Button>/);
21 assert.match(readPath, /setResearchReadinessDialog\(\{ globalInstrumentId, companyName \}\)/);
22 assert.match(readPath, /researchApi\.getReadiness\(globalInstrumentId\)/);
23 assert.doesNotMatch(readPath, /ensureReadiness|\.refresh\(|prefetchVisible|ensureDefaultWatchlist|addWatchlistInstrument/);
24 });
25
26 test("readiness API separates read-only GET from targeted POST", () => {
27 assert.match(api, /getReadiness: \(globalInstrumentId: string\) =>\s*request<ResearchReadiness>\(`\/api\/v1\/research\/readiness\/\$\{globalInstrumentId\}`\)/);
28 assert.match(api, /ensureReadiness: \(globalInstrumentId: string, requirements\?: string\[\]\) =>[\s\S]*?method: "POST"[\s\S]*?JSON\.stringify\(requirements\?\.length \? \{ requirements \} : \{\}\)/);
29 });
30
31 test("Find Data sends only selected requirement IDs", () => {
32 assert.match(ensurePath, /researchApi\.ensureReadiness\(dialog\.globalInstrumentId, requirements\)/);
33 assert.match(dialog, /onFindData\(\[requirement\.requirementId\]\)/);
34 assert.match(dialog, /findable\.map\(\(item\) => item\.requirementId\)/);
35 assert.doesNotMatch(ensurePath, /NSE|SEC|EODHD|Yahoo|provider/);
36 });
37
38 test("readiness statuses map to the required visual tones", () => {
39 assert.match(dialog, /status === "READY_FRESH"\) return "ready"/);
40 assert.match(dialog, /\["READY_STALE", "PARTIAL", "REFRESHING"\]\.includes\(status\).*return "warning"/s);
41 assert.match(dialog, /status === "UNSUPPORTED" \|\| status === "NOT_APPLICABLE"\) return "neutral"/);
42 assert.match(dialog, /return "danger"/);
43 for (const tone of ["ready", "warning", "danger", "neutral"]) {
44 assert.match(styles, new RegExp(`\\.readiness-status-${tone}\\b`));
45 }
46 });
47
48 test("popup shows provenance, freshness, reasons, upload placeholder, and analysis actions", () => {
49 assert.match(dialog, /requirement\.area\.replaceAll/);
50 assert.match(dialog, /requirement\.status\.replaceAll/);
51 assert.match(dialog, /As of/);
52 assert.match(dialog, /Source: \{requirement\.sourceProvider/);
53 assert.match(dialog, /href=\{requirement\.sourceUrl\} target="_blank" rel="noreferrer"/);
54 assert.match(dialog, /requirement\.missingReason/);
55 assert.match(dialog, /requirement\.conflictReason/);
56 assert.match(dialog, /"Finding…" : "Find Data"/);
57 assert.match(dialog, />Upload Evidence<\/Button>/);
58 assert.match(dialog, /analysisEligibility\?\.fullAnalysisAllowed/);
59 assert.match(dialog, /analysisEligibility\?\.partialAnalysisAllowed/);
60 assert.match(dialog, /"Run Partial Analysis"/);
61 });
62
63 test("quarterly projection opens a generic authoritative filing link", () => {
64 assert.match(workspace, /Source: \{filingSourceLabel\(result\.sourceName\)\}/);
65 assert.match(workspace, /href=\{result\.sourceUrl\} target="_blank" rel="noreferrer">View \{filingSourceLabel\(result\.sourceName\)\} Filing ↗<\/a>/);
66 assert.match(workspace, /function filingSourceLabel\(sourceName: string\)/);
67 assert.doesNotMatch(workspace.slice(workspace.indexOf("function filingSourceLabel"), workspace.indexOf("function AuthPasswordInput")), /NSE|SEC|EODHD/);
68 });
69
70 test("readiness popup reports completeness and data confidence without a stock score", () => {
71 assert.match(dialog, /readiness\.overallCompletenessPct/);
72 assert.match(dialog, /readiness\.criticalCompletenessPct/);
73 assert.match(dialog, /readiness\.confidence/);
74 assert.doesNotMatch(dialog, /stock score|investment score|final score/i);
75 });
76
77 test("readiness dialog is portaled, focus-contained, and locks background scrolling", () => {
78 assert.match(workspace, /import \{ createPortal \} from "react-dom"/);
79 assert.match(dialog, /return createPortal\([\s\S]*?document\.body/);
80 assert.match(dialog, /document\.body\.style\.overflow = "hidden"/);
81 assert.match(dialog, /appShell\.inert = true/);
82 assert.match(dialog, /appShell\.setAttribute\("aria-hidden", "true"\)/);
83 assert.match(dialog, /event\.key === "Escape"[\s\S]*?onClose\(\)/);
84 assert.match(dialog, /event\.key !== "Tab"[\s\S]*?first[\s\S]*?last/);
85 assert.match(dialog, /role="dialog"[\s\S]*?aria-modal="true"/);
86 assert.match(dialog, /className="readiness-popup-header"/);
87 assert.match(dialog, /className="readiness-popup-body"/);
88 });
89
90 test("readiness dialog has an opaque surface and one viewport-constrained scrolling body", () => {
91 const backdrop = styles.match(/\.readiness-popup-backdrop \{[\s\S]*?\n\}/)?.[0] ?? "";
92 const popup = styles.match(/\.readiness-popup \{[\s\S]*?\n\}/)?.[0] ?? "";
93 const body = styles.match(/\.readiness-popup-body \{[\s\S]*?\n\}/)?.[0] ?? "";
94
95 assert.match(backdrop, /position: fixed/);
96 assert.match(backdrop, /z-index: 1000/);
97 assert.match(backdrop, /background: rgb\(8 15 25 \/ 68%\)/);
98 assert.match(popup, /max-height: min\(920px, calc\(100dvh/);
99 assert.match(popup, /grid-template-rows: auto minmax\(0, 1fr\)/);
100 assert.match(popup, /overflow: hidden/);
101 assert.match(popup, /background: var\(--surface-raised\)/);
102 assert.doesNotMatch(popup, /overflow(?:-y)?: auto/);
103 assert.match(body, /overflow-y: auto/);
104 assert.match(body, /overscroll-behavior: contain/);
105 });