main
mjs 49 lines 3.4 KB
Raw
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 });