| 1 | import test from 'node:test'; |
| 2 | import assert from 'node:assert/strict'; |
| 3 | import fs from 'node:fs'; |
| 4 | import { createRequire } from 'node:module'; |
| 5 | import ts from 'typescript'; |
| 6 | import React from 'react'; |
| 7 | import { renderToStaticMarkup } from 'react-dom/server'; |
| 8 | |
| 9 | const require = createRequire(import.meta.url); |
| 10 | function component(name) { |
| 11 | const source = fs.readFileSync(new URL(`../app/components/${name}.tsx`, import.meta.url), 'utf8'); |
| 12 | const output = ts.transpileModule(source, { compilerOptions: { jsx: ts.JsxEmit.ReactJSX, module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 } }).outputText; |
| 13 | const module = { exports: {} }; |
| 14 | new Function('require', 'module', 'exports', output)(path => path.includes('portfolio-api') ? {} : require(path), module, module.exports); |
| 15 | return module.exports; |
| 16 | } |
| 17 | const { RadarContent, price } = component('opportunity-radar'); |
| 18 | const { Backtesting, BacktestResults } = component('backtesting'); |
| 19 | const item = i => ({ global_instrument_id: `${i}`, company_name: `Company ${i}`, symbol: `C${i}`, current_price: 100, |
| 20 | opportunity_score: 80, opportunity_confidence: 85, score_coverage: 90, new_investor_action: 'BUY_CANDIDATE', |
| 21 | existing_holder_action: 'HOLD', current_short_action: 'BUY', current_long_action: 'ACCUMULATE', |
| 22 | short_entry_low: null, short_entry_high: null, top_positive_reasons: ['QUALITY'], top_negative_reasons: ['MISSING_ATR'], |
| 23 | data_state: 'PARTIAL', missing_areas: ['ATR'], stale_areas: [], short_horizon: '1–3 months', long_horizon: '6–12 months' }); |
| 24 | const empty = { generated_at: null, best_buy_today: null, top_short_term: [], top_long_term: [], previous_recommendations: [] }; |
| 25 | |
| 26 | test('radar renders persisted 2–4 cards per horizon and missing ranges', () => { |
| 27 | for (const count of [2, 3, 4]) { |
| 28 | const picks = Array.from({ length: count }, (_, i) => item(i)); |
| 29 | const html = renderToStaticMarkup(React.createElement(RadarContent, { data: { ...empty, top_short_term: picks, top_long_term: picks }, heldIds: ['0'], watchlistedIds: ['1'] })); |
| 30 | assert.equal((html.match(/class="opportunity-card"/g) ?? []).length, count * 2); |
| 31 | assert.match(html, /Insufficient evidence/); |
| 32 | assert.match(html, /Held/); assert.match(html, /Watchlisted/); |
| 33 | assert.doesNotMatch(html, /₹0/); |
| 34 | } |
| 35 | assert.equal(price(null), 'Insufficient evidence'); |
| 36 | }); |
| 37 | test('empty and lifecycle render', () => { |
| 38 | assert.match(renderToStaticMarkup(React.createElement(RadarContent, { data: empty })), /No persisted opportunity cycle/); |
| 39 | const html = renderToStaticMarkup(React.createElement(RadarContent, { data: { ...empty, previous_recommendations: [{ ...item(1), current_short_action: 'PARTIAL_PROFIT', short_term_state: 'PARTIAL_PROFIT', current_long_action: 'HOLD', long_term_state: 'HOLDING' }] } })); |
| 40 | assert.match(html, /PARTIAL PROFIT/); assert.match(html, /Long: HOLD/); |
| 41 | }); |
| 42 | test('backtesting page and all horizon results render', () => { |
| 43 | assert.match(renderToStaticMarkup(React.createElement(Backtesting)), /Run backtest/); |
| 44 | const metric = { evaluated_count: 1, missing_count: 0, hit_rate: 100, average_return: 10, median_return: 10 }; |
| 45 | const run = { recommendation_count: 1, methodology: 'Recorded evidence', winners: [], losers: [], metrics: Object.fromEntries(['1W', '1M', '3M', '6M', '1Y'].map(h => [h, metric])) }; |
| 46 | const html = renderToStaticMarkup(React.createElement(BacktestResults, { run })); |
| 47 | for (const h of ['1W', '1M', '3M', '6M', '1Y']) assert.ok(html.includes(h)); |
| 48 | assert.match(html, /100.00%/); |
| 49 | }); |