| 1 | import assert from "node:assert/strict"; |
| 2 | import { readFileSync } from "node:fs"; |
| 3 | import test from "node:test"; |
| 4 | import ts from "typescript"; |
| 5 | |
| 6 | const workspace = readFileSync(new URL("../app/components/investment-workspace.tsx", import.meta.url), "utf8"); |
| 7 | const apiSource = readFileSync(new URL("../app/lib/portfolio-api.ts", import.meta.url), "utf8"); |
| 8 | const helperSource = readFileSync(new URL("../app/lib/market-intelligence.ts", import.meta.url), "utf8"); |
| 9 | const styles = readFileSync(new URL("../app/styles.css", import.meta.url), "utf8"); |
| 10 | const helperJavaScript = ts.transpileModule(helperSource, { |
| 11 | compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 } |
| 12 | }).outputText; |
| 13 | const helpers = await import(`data:text/javascript;base64,${Buffer.from(helperJavaScript).toString("base64")}`); |
| 14 | |
| 15 | test("sector options remain backend-owned exact values and single encoded", () => { |
| 16 | const dashboardStart = workspace.indexOf("function MultiPortfolioDashboard"); |
| 17 | const dashboardEnd = workspace.indexOf("function PortfolioCreatePanel", dashboardStart); |
| 18 | const dashboard = workspace.slice(dashboardStart, dashboardEnd); |
| 19 | assert.match(apiSource, /getMarketUniverseSectors:[\s\S]*?\/api\/v1\/research\/market-universe\/sectors/); |
| 20 | assert.match(dashboard, /sectorOptions\.map\(\(value\) => <option key=\{value\.name\} value=\{value\.name\}>\{value\.name\} \(\{value\.instrumentCount\}\)<\/option>/); |
| 21 | assert.doesNotMatch(dashboard, /Basic Materials|Consumer Defensive/); |
| 22 | const method = apiSource.slice(apiSource.indexOf("getSectorPerformance:"), apiSource.indexOf("createPortfolio:")); |
| 23 | assert.match(method, /new URLSearchParams\(\{ region, sector, period, limit: "5" \}\)/); |
| 24 | assert.doesNotMatch(method, /encodeURIComponent/); |
| 25 | }); |
| 26 | |
| 27 | test("region reload clears stale sectors and invalid sectors never dispatch performance", () => { |
| 28 | const discoveryCall = workspace.indexOf("portfolioApi.getMarketUniverseSectors(sectorPerformanceRegion)"); |
| 29 | const discovery = workspace.slice(workspace.lastIndexOf(" useEffect(() => {", discoveryCall), workspace.indexOf("\n }, [", discoveryCall)); |
| 30 | assert.match(discovery, /setSectorOptions\(\[\]\)/); |
| 31 | assert.match(discovery, /setSectorPerformanceSector\(""\)/); |
| 32 | assert.match(discovery, /setSectorPerformanceSector\(value\.sectors\[0\]\?\.name \?\? ""\)/); |
| 33 | const performanceCall = workspace.indexOf("portfolioApi.getSectorPerformance("); |
| 34 | const performance = workspace.slice(workspace.lastIndexOf(" useEffect(() => {", performanceCall), workspace.indexOf("\n }, [", performanceCall)); |
| 35 | assert.match(performance, /sectorOptionsRegion === sectorPerformanceRegion/); |
| 36 | assert.match(performance, /sectorOptions\.some\(\(option\) => option\.name === sectorPerformanceSector\)/); |
| 37 | }); |
| 38 | |
| 39 | test("regional defaults map canonically without provider routing", () => { |
| 40 | assert.equal(helpers.regionalWatchlistName("INDIA"), "WATCHLIST-IND"); |
| 41 | assert.equal(helpers.regionalWatchlistName("EUROPE"), "WATCHLIST-EU"); |
| 42 | assert.equal(helpers.regionalWatchlistName("USA"), "WATCHLIST-USA"); |
| 43 | const helper = helperSource.slice(helperSource.indexOf("export function regionalWatchlistName")); |
| 44 | assert.doesNotMatch(helper, /NSE|SEC|EODHD|Yahoo/); |
| 45 | }); |
| 46 | |
| 47 | test("card click opens public readiness without changing a regional watchlist or position", () => { |
| 48 | const start = workspace.indexOf("function openMarketIntelligenceResearch"); |
| 49 | const end = workspace.indexOf("\n async function openRegionalWatchlist", start); |
| 50 | const click = workspace.slice(start, end); |
| 51 | assert.match(click, /setSelectedResearchInstrumentId\(stock\.globalInstrumentId\)/); |
| 52 | assert.match(click, /openResearchReadiness\(stock\.globalInstrumentId, stock\.companyName\)/); |
| 53 | assert.doesNotMatch(click, /ensureDefaultWatchlist|addWatchlistInstrument|prefetchVisible|createPortfolio|createPosition|addPosition|updateHolding|portfolioApi\./); |
| 54 | }); |
| 55 | |
| 56 | test("Research context selector visibly distinguishes portfolios from watchlists", () => { |
| 57 | const start = workspace.indexOf("function ResearchContextSelector"); |
| 58 | const end = workspace.indexOf("function LoadingView", start); |
| 59 | const selector = workspace.slice(start, end); |
| 60 | assert.match(selector, /<span>Research context<\/span>/); |
| 61 | assert.match(selector, /<optgroup label="Portfolios">/); |
| 62 | assert.match(selector, /<optgroup label="Watchlists">/); |
| 63 | assert.match(selector, /portfolio:\$\{portfolio\.portfolioId\}/); |
| 64 | assert.match(selector, /watchlist:\$\{watchlist\.watchlistId\}/); |
| 65 | }); |
| 66 | |
| 67 | test("portfolio and watchlist research rows are mutually exclusive", () => { |
| 68 | const start = workspace.indexOf("function ResearchView"); |
| 69 | const end = workspace.indexOf("function ResearchOverview", start); |
| 70 | const research = workspace.slice(start, end); |
| 71 | assert.match(research, /researchContext\.kind === "WATCHLIST" \? \(watchlistResearch\?\.instruments \?\? \[\]\)\.map/); |
| 72 | assert.match(research, /researchContext\.kind === "PORTFOLIO" \? \(portfolioResearchSummary\?\.companies \?\? \[\]\)\.map/); |
| 73 | assert.doesNotMatch(research, /Portfolio holdings plus the active regional Market Intelligence context/); |
| 74 | assert.doesNotMatch(research, /transientMarketIntelligence/); |
| 75 | assert.match(research, /This research context contains actual portfolio holdings only/); |
| 76 | }); |
| 77 | |
| 78 | test("watchlist row is canonical non-held presentation with separated market return", () => { |
| 79 | const start = workspace.indexOf("key={`watchlist-${item.globalInstrumentId}`}"); |
| 80 | const end = workspace.indexOf("researchContext.kind === \"PORTFOLIO\"", start); |
| 81 | const row = workspace.slice(start, end); |
| 82 | assert.match(row, /data-held="false"/); |
| 83 | assert.match(row, /data-global-instrument-id=\{item\.globalInstrumentId\}/); |
| 84 | assert.match(row, /Public company research · Not held/); |
| 85 | assert.match(row, /Market return \(\{item\.sourcePeriod\}\)/); |
| 86 | assert.doesNotMatch(row, /Qty 0|Portfolio P&L|Average buy price|Invested amount/); |
| 87 | assert.match(row, /watchlistInstrumentId: item\.globalInstrumentId/); |
| 88 | assert.match(row, /researchInstrumentId: company\.instrumentId \?\? item\.globalInstrumentId/); |
| 89 | }); |
| 90 | |
| 91 | test("same drawer represents non-held state without fabricating a portfolio position", () => { |
| 92 | const start = workspace.indexOf("function StockResearchDrawer"); |
| 93 | const end = workspace.indexOf("function AllocationCharts", start); |
| 94 | const drawer = workspace.slice(start, end); |
| 95 | assert.match(drawer, /position\?: PortfolioPosition/); |
| 96 | assert.match(drawer, /watchlistItem\?: WatchlistResearchInstrument/); |
| 97 | assert.match(drawer, /Watchlist status<\/h3><p>Public company research<\/p>/); |
| 98 | const nonHeld = drawer.slice(drawer.indexOf("<h3>Watchlist status"), drawer.indexOf('<section><h3>Market Data')); |
| 99 | assert.doesNotMatch(nonHeld, /Quantity|Average Cost|Market Value|Unrealized P/); |
| 100 | assert.match(drawer, /Market return \(\{watchlistItem\.sourcePeriod\}\)/); |
| 101 | }); |
| 102 | |
| 103 | test("direct navigation clears watchlist presentation but not durable membership", () => { |
| 104 | const clear = workspace.slice(workspace.indexOf("function clearMarketIntelligenceContext"), workspace.indexOf("async function openResearchReadiness")); |
| 105 | assert.match(clear, /setResearchContext\(\{ kind: "PORTFOLIO", portfolioId: selectedPortfolioId \}\)/); |
| 106 | assert.match(clear, /setWatchlistResearch\(null\)/); |
| 107 | assert.doesNotMatch(clear, /removeWatchlistInstrument|DELETE|localStorage/); |
| 108 | const nav = workspace.slice(workspace.indexOf("<nav aria-label=\"Primary\">"), workspace.indexOf("</nav>")); |
| 109 | assert.match(nav, /clearMarketIntelligenceContext\(\);[\s\S]*?setView\(item\.id\)/); |
| 110 | }); |
| 111 | |
| 112 | test("positive negative and neutral watchlist rows retain explicit signs and interaction states", () => { |
| 113 | for (const tone of ["positive", "negative", "neutral"]) { |
| 114 | assert.match(styles, new RegExp(`\\.market-intelligence-research-row-${tone}\\b`)); |
| 115 | } |
| 116 | assert.match(styles, /\.market-intelligence-research-row:hover/); |
| 117 | assert.match(styles, /\.market-intelligence-research-row\[aria-current="true"\]/); |
| 118 | assert.equal(helpers.formatSignedPerformancePct(13.43), "+13.43%"); |
| 119 | assert.equal(helpers.formatSignedPerformancePct(-6.8), "-6.80%"); |
| 120 | assert.equal(helpers.formatSignedPerformancePct(0), "0.00%"); |
| 121 | }); |
| 122 | |
| 123 | test("watchlist API is provider neutral and separate from portfolio holdings", () => { |
| 124 | assert.match(apiSource, /listWatchlists:[\s\S]*?\/api\/v1\/research\/watchlists/); |
| 125 | assert.match(apiSource, /ensureDefaultWatchlist:[\s\S]*?\/api\/v1\/research\/watchlists\/default\/ensure/); |
| 126 | assert.match(apiSource, /addWatchlistInstrument:[\s\S]*?\/api\/v1\/research\/watchlists\/\$\{watchlistId\}\/instruments/); |
| 127 | const watchlistApi = apiSource.slice(apiSource.indexOf("listWatchlists:"), apiSource.indexOf("getPortfolioSummary:")); |
| 128 | assert.doesNotMatch(watchlistApi, /NSE|SEC|EODHD|createPortfolio|positions/); |
| 129 | }); |
| 130 | |
| 131 | for (const [region, name, performancePct] of [["INDIA", "WATCHLIST-IND", 8], ["INDIA", "WATCHLIST-IND", -8], ["USA", "WATCHLIST-USA", 3], ["EUROPE", "WATCHLIST-EU", -3]]) { |
| 132 | test(`ranked ${region} ${performancePct} persists canonical membership and refreshes Saved state`, async () => { |
| 133 | const start = workspace.indexOf(" async function addRankedStockToWatchlist"); |
| 134 | const end = workspace.indexOf(" async function removeWatchlistStock", start); |
| 135 | const js = ts.transpileModule(workspace.slice(start, end), { compilerOptions: { target: ts.ScriptTarget.ES2022 } }).outputText; |
| 136 | const calls = []; |
| 137 | let saved = {}; |
| 138 | const list = { watchlistId: "list-id", region, name }; |
| 139 | const api = { |
| 140 | ensureDefaultWatchlist: async (value) => { assert.equal(value, region); return list; }, |
| 141 | addWatchlistInstrument: async (...args) => { calls.push(args); }, |
| 142 | getWatchlistResearch: async () => ({ watchlist: list, instruments: [{ globalInstrumentId: "canonical-id" }] }), |
| 143 | }; |
| 144 | const handler = new Function("researchApi", "watchlistMutationRef", "setWatchlistMutation", "setWatchlistActionError", "setWatchlists", "setSavedWatchlistIds", "setWatchlistRevision", "getApiFailure", `${js}; return addRankedStockToWatchlist;`)( |
| 145 | api, { current: false }, () => {}, (error) => assert.equal(error, null), () => {}, (update) => { saved = update(saved); }, () => {}, (error) => { throw error; } |
| 146 | ); |
| 147 | await handler({ region, period: "WEEK", stock: { globalInstrumentId: "canonical-id", companyName: "Display only", ticker: "LOOSE", performancePct } }); |
| 148 | assert.deepEqual(calls, [["list-id", { globalInstrumentId: "canonical-id", sourcePeriod: "WEEK", sourcePerformancePct: performancePct }]]); |
| 149 | assert.deepEqual(saved, { "list-id": ["canonical-id"] }); |
| 150 | await handler({ region, period: "WEEK", stock: { globalInstrumentId: "", performancePct } }); |
| 151 | assert.equal(calls.length, 1); |
| 152 | }); |
| 153 | } |
| 154 | |
| 155 | test("both ranked groups expose Saved/Add and Research exposes all regional lists and removal", () => { |
| 156 | assert.equal((workspace.match(/onAddWatchlist=\{\(\) => onAddWatchlist\(marketIntelligenceSelection\(performance, stock\)\)\}/g) ?? []).length, 2); |
| 157 | assert.match(workspace, /disabled=\{saved \|\| busy \|\| !stock.globalInstrumentId/); |
| 158 | assert.match(workspace, /Saved in \$\{watchlistName\}/); |
| 159 | assert.match(workspace, /aria-label="Regional watchlists"/); |
| 160 | assert.match(workspace, /\["INDIA", "USA", "EUROPE"\] as const/); |
| 161 | assert.match(workspace, /researchApi.removeWatchlistInstrument\(watchlistId, globalInstrumentId\)/); |
| 162 | assert.match(workspace, /marketEnsureAuthReady, researchContext, watchlistRevision/); |
| 163 | }); |