termcast 1.3.52 → 1.3.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +209 -7
- package/dist/app.js.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/cli.js.map +1 -1
- package/dist/components/candle-chart.d.ts +110 -0
- package/dist/components/candle-chart.d.ts.map +1 -0
- package/dist/components/candle-chart.js +295 -0
- package/dist/components/candle-chart.js.map +1 -0
- package/dist/components/list.d.ts.map +1 -1
- package/dist/components/list.js +3 -0
- package/dist/components/list.js.map +1 -1
- package/dist/components/table.d.ts +2 -0
- package/dist/components/table.d.ts.map +1 -1
- package/dist/components/table.js +41 -4
- package/dist/components/table.js.map +1 -1
- package/dist/examples/simple-candle-chart-data.d.ts +9064 -0
- package/dist/examples/simple-candle-chart-data.d.ts.map +1 -0
- package/dist/examples/simple-candle-chart-data.js +12683 -0
- package/dist/examples/simple-candle-chart-data.js.map +1 -0
- package/dist/examples/simple-candle-chart.d.ts +2 -0
- package/dist/examples/simple-candle-chart.d.ts.map +1 -0
- package/dist/examples/simple-candle-chart.js +125 -0
- package/dist/examples/simple-candle-chart.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/dialog.d.ts +1 -0
- package/dist/internal/dialog.d.ts.map +1 -1
- package/dist/internal/dialog.js +4 -0
- package/dist/internal/dialog.js.map +1 -1
- package/dist/state.d.ts +1 -0
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js.map +1 -1
- package/package.json +1 -1
- package/src/app.tsx +269 -8
- package/src/cli.tsx +5 -5
- package/src/components/candle-chart.tsx +410 -0
- package/src/components/list.tsx +3 -0
- package/src/components/table.tsx +46 -4
- package/src/examples/simple-candle-chart-data.ts +12683 -0
- package/src/examples/simple-candle-chart.tsx +363 -0
- package/src/examples/simple-candle-chart.vitest.tsx +269 -0
- package/src/examples/simple-detail-table.vitest.tsx +2 -2
- package/src/examples/simple-table-wrap.vitest.tsx +19 -19
- package/src/examples/table-flex-grow.vitest.tsx +8 -8
- package/src/index.tsx +7 -0
- package/src/internal/dialog.tsx +5 -0
- package/src/state.tsx +1 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
// Example: CandleChart component showing realistic crypto OHLC data.
|
|
2
|
+
// List of markets with candlestick charts in the side detail panel.
|
|
3
|
+
// Green bars = bullish (close >= open), red bars = bearish (close < open).
|
|
4
|
+
// Wicks extend from body to high/low extremes.
|
|
5
|
+
//
|
|
6
|
+
// Demonstrates:
|
|
7
|
+
// - CandleChart in List.Item.Detail (side panel)
|
|
8
|
+
// - CandleChart in full-page Detail (pushed on Enter)
|
|
9
|
+
// - Mixed component types: CandleChart + Graph overlay, CandleChart + BarChart volume,
|
|
10
|
+
// Row for side-by-side comparisons, metadata with tags
|
|
11
|
+
// - Realistic crypto market action from frozen Coinbase daily OHLCV data
|
|
12
|
+
|
|
13
|
+
import React from 'react'
|
|
14
|
+
import { Action, ActionPanel, List, Detail, Color, CandleChart, Graph, BarChart, Row } from 'termcast'
|
|
15
|
+
import type { CandleData } from 'termcast'
|
|
16
|
+
import { useNavigation } from 'termcast/src/internal/navigation'
|
|
17
|
+
import { renderWithProviders } from '../utils'
|
|
18
|
+
import { cryptoMarketData } from './simple-candle-chart-data'
|
|
19
|
+
|
|
20
|
+
interface Market {
|
|
21
|
+
symbol: string
|
|
22
|
+
displaySymbol: string
|
|
23
|
+
name: string
|
|
24
|
+
sector: string
|
|
25
|
+
currentPrice: number
|
|
26
|
+
change: number
|
|
27
|
+
priceDecimals: number
|
|
28
|
+
candles: CandleData[]
|
|
29
|
+
closingPrices: number[]
|
|
30
|
+
volume: number[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface MixedItem {
|
|
34
|
+
title: string
|
|
35
|
+
subtitle: string
|
|
36
|
+
market: Market
|
|
37
|
+
mode: 'candle-only' | 'candle-with-line' | 'candle-with-volume' | 'side-by-side'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const tickers: Market[] = cryptoMarketData.map((market) => {
|
|
41
|
+
const candles: CandleData[] = market.candles.map((candle) => {
|
|
42
|
+
return {
|
|
43
|
+
open: candle.open,
|
|
44
|
+
close: candle.close,
|
|
45
|
+
high: candle.high,
|
|
46
|
+
low: candle.low,
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
symbol: market.symbol,
|
|
52
|
+
displaySymbol: market.symbol.replace('-USD', ''),
|
|
53
|
+
name: market.name,
|
|
54
|
+
sector: market.sector,
|
|
55
|
+
currentPrice: market.currentPrice,
|
|
56
|
+
change: market.change,
|
|
57
|
+
priceDecimals: market.priceDecimals,
|
|
58
|
+
candles,
|
|
59
|
+
closingPrices: candles.map((candle) => {
|
|
60
|
+
return candle.close
|
|
61
|
+
}),
|
|
62
|
+
volume: [...market.volume],
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const mixedItems: MixedItem[] = [
|
|
67
|
+
{ title: 'BTC - Candles', subtitle: 'Real BTC/USD hourly candles', market: tickers[0]!, mode: 'candle-only' },
|
|
68
|
+
{ title: 'ETH - Candle + Line', subtitle: 'Candles plus closing line', market: tickers[1]!, mode: 'candle-with-line' },
|
|
69
|
+
{ title: 'SOL - Candle + Volume', subtitle: 'Candles plus volume split', market: tickers[2]!, mode: 'candle-with-volume' },
|
|
70
|
+
{ title: 'BTC vs ETH', subtitle: 'Side-by-side crypto leaders', market: tickers[0]!, mode: 'side-by-side' },
|
|
71
|
+
{ title: 'DOGE - Candle + Line', subtitle: 'Low-priced asset formatting', market: tickers[4]!, mode: 'candle-with-line' },
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
// 300 hourly candles ≈ 12.5 days
|
|
75
|
+
const xLabels = ['12d', '8d', '4d', 'Now']
|
|
76
|
+
|
|
77
|
+
function formatNumber({ value, decimals }: { value: number; decimals: number }): string {
|
|
78
|
+
return new Intl.NumberFormat('en-US', {
|
|
79
|
+
minimumFractionDigits: decimals,
|
|
80
|
+
maximumFractionDigits: decimals,
|
|
81
|
+
}).format(value)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatPrice({ value, decimals }: { value: number; decimals: number }): string {
|
|
85
|
+
return `$${formatNumber({ value, decimals })}`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function formatPercent(change: number): string {
|
|
89
|
+
return change >= 0 ? `+${change.toFixed(1)}%` : `${change.toFixed(1)}%`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function changeColor(change: number): Color.ColorLike {
|
|
93
|
+
if (change > 0) return Color.Green
|
|
94
|
+
if (change < 0) return Color.Red
|
|
95
|
+
return Color.SecondaryText
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function yAxisFormatter({ market }: { market: Market }): (value: number) => string {
|
|
99
|
+
return (value) => {
|
|
100
|
+
return formatPrice({ value, decimals: market.priceDecimals })
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function ChartForMode({ item, height }: { item: MixedItem; height: number }): any {
|
|
105
|
+
const { market } = item
|
|
106
|
+
const yFormat = yAxisFormatter({ market })
|
|
107
|
+
|
|
108
|
+
switch (item.mode) {
|
|
109
|
+
case 'candle-only': {
|
|
110
|
+
return (
|
|
111
|
+
<CandleChart
|
|
112
|
+
data={market.candles}
|
|
113
|
+
height={height}
|
|
114
|
+
xLabels={xLabels}
|
|
115
|
+
yTicks={4}
|
|
116
|
+
yFormat={yFormat}
|
|
117
|
+
/>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
case 'candle-with-line': {
|
|
121
|
+
return (
|
|
122
|
+
<>
|
|
123
|
+
<CandleChart
|
|
124
|
+
data={market.candles}
|
|
125
|
+
height={height}
|
|
126
|
+
xLabels={xLabels}
|
|
127
|
+
yTicks={4}
|
|
128
|
+
yFormat={yFormat}
|
|
129
|
+
/>
|
|
130
|
+
<Graph
|
|
131
|
+
height={Math.max(5, height - 3)}
|
|
132
|
+
xLabels={xLabels}
|
|
133
|
+
yTicks={3}
|
|
134
|
+
yFormat={yFormat}
|
|
135
|
+
variant="area"
|
|
136
|
+
>
|
|
137
|
+
<Graph.Line data={market.closingPrices} color={Color.Blue} title="Close" />
|
|
138
|
+
</Graph>
|
|
139
|
+
</>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
case 'candle-with-volume': {
|
|
143
|
+
const firstHalf = market.volume.slice(0, 15).reduce((sum, value) => {
|
|
144
|
+
return sum + value
|
|
145
|
+
}, 0)
|
|
146
|
+
const secondHalf = market.volume.slice(15).reduce((sum, value) => {
|
|
147
|
+
return sum + value
|
|
148
|
+
}, 0)
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<>
|
|
152
|
+
<CandleChart
|
|
153
|
+
data={market.candles}
|
|
154
|
+
height={height}
|
|
155
|
+
xLabels={xLabels}
|
|
156
|
+
yTicks={4}
|
|
157
|
+
yFormat={yFormat}
|
|
158
|
+
/>
|
|
159
|
+
<BarChart height={1}>
|
|
160
|
+
<BarChart.Segment value={firstHalf} label="First half" color={Color.Blue} />
|
|
161
|
+
<BarChart.Segment value={secondHalf} label="Second half" color={Color.Orange} />
|
|
162
|
+
</BarChart>
|
|
163
|
+
</>
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
case 'side-by-side': {
|
|
167
|
+
const eth = tickers[1]!
|
|
168
|
+
return (
|
|
169
|
+
<Row>
|
|
170
|
+
<CandleChart
|
|
171
|
+
data={market.candles}
|
|
172
|
+
height={height}
|
|
173
|
+
xLabels={['30d', 'Now']}
|
|
174
|
+
yTicks={3}
|
|
175
|
+
yFormat={yAxisFormatter({ market })}
|
|
176
|
+
/>
|
|
177
|
+
<CandleChart
|
|
178
|
+
data={eth.candles}
|
|
179
|
+
height={height}
|
|
180
|
+
xLabels={['30d', 'Now']}
|
|
181
|
+
yTicks={3}
|
|
182
|
+
yFormat={yAxisFormatter({ market: eth })}
|
|
183
|
+
/>
|
|
184
|
+
</Row>
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function CandleDetailView({ item }: { item: MixedItem }): any {
|
|
191
|
+
const { pop } = useNavigation()
|
|
192
|
+
const { market } = item
|
|
193
|
+
const changeStr = formatPercent(market.change)
|
|
194
|
+
|
|
195
|
+
const markdown = [
|
|
196
|
+
`# ${market.displaySymbol} - ${market.name}`,
|
|
197
|
+
'',
|
|
198
|
+
`**Category:** ${market.sector}`,
|
|
199
|
+
'',
|
|
200
|
+
`**Price:** ${formatPrice({ value: market.currentPrice, decimals: market.priceDecimals })} `,
|
|
201
|
+
`**24h change:** ${changeStr}`,
|
|
202
|
+
'',
|
|
203
|
+
`**Mode:** \`${item.mode}\``,
|
|
204
|
+
'',
|
|
205
|
+
'300 hourly candles from Coinbase Exchange, frozen so the example stays deterministic.',
|
|
206
|
+
].join('\n')
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<Detail
|
|
210
|
+
navigationTitle={`${market.displaySymbol} Detail`}
|
|
211
|
+
markdown={markdown}
|
|
212
|
+
metadata={
|
|
213
|
+
<Detail.Metadata>
|
|
214
|
+
<ChartForMode item={item} height={15} />
|
|
215
|
+
<Detail.Metadata.Separator />
|
|
216
|
+
<Detail.Metadata.Label
|
|
217
|
+
title="Price"
|
|
218
|
+
text={{ value: formatPrice({ value: market.currentPrice, decimals: market.priceDecimals }), color: changeColor(market.change) }}
|
|
219
|
+
/>
|
|
220
|
+
<Detail.Metadata.Label
|
|
221
|
+
title="Change"
|
|
222
|
+
text={{ value: changeStr, color: changeColor(market.change) }}
|
|
223
|
+
/>
|
|
224
|
+
<Detail.Metadata.Label title="Category" text={market.sector} />
|
|
225
|
+
<Detail.Metadata.Separator />
|
|
226
|
+
<Detail.Metadata.TagList title="Components">
|
|
227
|
+
<Detail.Metadata.TagList.Item text="CandleChart" color={Color.Green} />
|
|
228
|
+
{item.mode === 'candle-with-line' && (
|
|
229
|
+
<Detail.Metadata.TagList.Item text="Graph" color={Color.Blue} />
|
|
230
|
+
)}
|
|
231
|
+
{item.mode === 'candle-with-volume' && (
|
|
232
|
+
<Detail.Metadata.TagList.Item text="BarChart" color={Color.Orange} />
|
|
233
|
+
)}
|
|
234
|
+
{item.mode === 'side-by-side' && (
|
|
235
|
+
<Detail.Metadata.TagList.Item text="Row" color={Color.Purple} />
|
|
236
|
+
)}
|
|
237
|
+
</Detail.Metadata.TagList>
|
|
238
|
+
</Detail.Metadata>
|
|
239
|
+
}
|
|
240
|
+
actions={
|
|
241
|
+
<ActionPanel>
|
|
242
|
+
<Action title="Go Back" onAction={() => { pop() }} />
|
|
243
|
+
</ActionPanel>
|
|
244
|
+
}
|
|
245
|
+
/>
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function SimpleCandleChart() {
|
|
250
|
+
const { push } = useNavigation()
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<List
|
|
254
|
+
navigationTitle="Crypto Markets"
|
|
255
|
+
searchBarPlaceholder="Search markets..."
|
|
256
|
+
isShowingDetail={true}
|
|
257
|
+
>
|
|
258
|
+
<List.Section title="Watchlist">
|
|
259
|
+
{tickers.map((market) => {
|
|
260
|
+
const changeStr = formatPercent(market.change)
|
|
261
|
+
return (
|
|
262
|
+
<List.Item
|
|
263
|
+
key={market.symbol}
|
|
264
|
+
id={market.symbol}
|
|
265
|
+
title={market.displaySymbol}
|
|
266
|
+
subtitle={market.name}
|
|
267
|
+
accessories={[
|
|
268
|
+
{ text: { value: formatPrice({ value: market.currentPrice, decimals: market.priceDecimals }), color: changeColor(market.change) } },
|
|
269
|
+
{ text: { value: changeStr, color: changeColor(market.change) } },
|
|
270
|
+
]}
|
|
271
|
+
detail={
|
|
272
|
+
<List.Item.Detail
|
|
273
|
+
metadata={
|
|
274
|
+
<List.Item.Detail.Metadata>
|
|
275
|
+
<CandleChart
|
|
276
|
+
data={market.candles}
|
|
277
|
+
height={10}
|
|
278
|
+
xLabels={xLabels}
|
|
279
|
+
yTicks={4}
|
|
280
|
+
yFormat={yAxisFormatter({ market })}
|
|
281
|
+
/>
|
|
282
|
+
<List.Item.Detail.Metadata.Label
|
|
283
|
+
title="Price"
|
|
284
|
+
text={{ value: formatPrice({ value: market.currentPrice, decimals: market.priceDecimals }), color: changeColor(market.change) }}
|
|
285
|
+
/>
|
|
286
|
+
<List.Item.Detail.Metadata.Label
|
|
287
|
+
title="Change"
|
|
288
|
+
text={{ value: changeStr, color: changeColor(market.change) }}
|
|
289
|
+
/>
|
|
290
|
+
<List.Item.Detail.Metadata.Label title="Category" text={market.sector} />
|
|
291
|
+
<List.Item.Detail.Metadata.Separator />
|
|
292
|
+
<List.Item.Detail.Metadata.Label title={`${market.symbol} Hourly OHLC`} />
|
|
293
|
+
</List.Item.Detail.Metadata>
|
|
294
|
+
}
|
|
295
|
+
/>
|
|
296
|
+
}
|
|
297
|
+
actions={
|
|
298
|
+
<ActionPanel>
|
|
299
|
+
<Action
|
|
300
|
+
title="Open Detail"
|
|
301
|
+
onAction={() => {
|
|
302
|
+
push(
|
|
303
|
+
<CandleDetailView
|
|
304
|
+
item={{ title: market.displaySymbol, subtitle: market.name, market, mode: 'candle-only' }}
|
|
305
|
+
/>,
|
|
306
|
+
)
|
|
307
|
+
}}
|
|
308
|
+
/>
|
|
309
|
+
</ActionPanel>
|
|
310
|
+
}
|
|
311
|
+
/>
|
|
312
|
+
)
|
|
313
|
+
})}
|
|
314
|
+
</List.Section>
|
|
315
|
+
|
|
316
|
+
<List.Section title="Mixed Components">
|
|
317
|
+
{mixedItems.map((item) => {
|
|
318
|
+
const { market } = item
|
|
319
|
+
const changeStr = formatPercent(market.change)
|
|
320
|
+
return (
|
|
321
|
+
<List.Item
|
|
322
|
+
key={item.title}
|
|
323
|
+
id={item.title}
|
|
324
|
+
title={item.title}
|
|
325
|
+
subtitle={item.subtitle}
|
|
326
|
+
accessories={[
|
|
327
|
+
{ text: { value: formatPrice({ value: market.currentPrice, decimals: market.priceDecimals }), color: changeColor(market.change) } },
|
|
328
|
+
]}
|
|
329
|
+
detail={
|
|
330
|
+
<List.Item.Detail
|
|
331
|
+
metadata={
|
|
332
|
+
<List.Item.Detail.Metadata>
|
|
333
|
+
<ChartForMode item={item} height={8} />
|
|
334
|
+
<List.Item.Detail.Metadata.Separator />
|
|
335
|
+
<List.Item.Detail.Metadata.Label
|
|
336
|
+
title="Price"
|
|
337
|
+
text={{ value: formatPrice({ value: market.currentPrice, decimals: market.priceDecimals }), color: changeColor(market.change) }}
|
|
338
|
+
/>
|
|
339
|
+
<List.Item.Detail.Metadata.Label
|
|
340
|
+
title="Change"
|
|
341
|
+
text={{ value: changeStr, color: changeColor(market.change) }}
|
|
342
|
+
/>
|
|
343
|
+
</List.Item.Detail.Metadata>
|
|
344
|
+
}
|
|
345
|
+
/>
|
|
346
|
+
}
|
|
347
|
+
actions={
|
|
348
|
+
<ActionPanel>
|
|
349
|
+
<Action
|
|
350
|
+
title="Open Detail"
|
|
351
|
+
onAction={() => { push(<CandleDetailView item={item} />) }}
|
|
352
|
+
/>
|
|
353
|
+
</ActionPanel>
|
|
354
|
+
}
|
|
355
|
+
/>
|
|
356
|
+
)
|
|
357
|
+
})}
|
|
358
|
+
</List.Section>
|
|
359
|
+
</List>
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
renderWithProviders(<SimpleCandleChart />)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// E2E tests for CandleChart component showing realistic crypto OHLC candles.
|
|
2
|
+
// Verifies candle body (▌/▘/▖) and wick (│) characters render,
|
|
3
|
+
// Y-axis labels, X-axis labels, and navigation between tickers.
|
|
4
|
+
// Also tests full-page Detail view and mixed component types
|
|
5
|
+
// (CandleChart + Graph, CandleChart + BarChart, Row side-by-side).
|
|
6
|
+
|
|
7
|
+
import { test, expect, afterEach, beforeEach } from 'vitest'
|
|
8
|
+
import { launchTerminal, Session } from 'tuistory/src'
|
|
9
|
+
|
|
10
|
+
let session: Session
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
session = await launchTerminal({
|
|
14
|
+
command: 'bun',
|
|
15
|
+
args: ['src/examples/simple-candle-chart.tsx'],
|
|
16
|
+
cols: 100,
|
|
17
|
+
rows: 30,
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
session?.close()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('candle chart renders in list detail with axes', async () => {
|
|
26
|
+
const text = await session.text({
|
|
27
|
+
waitFor: (text) => {
|
|
28
|
+
return text.includes('BTC') && text.includes('$') && text.includes('12d')
|
|
29
|
+
},
|
|
30
|
+
timeout: 10000,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
expect(text).toMatchInlineSnapshot(`
|
|
34
|
+
"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
Crypto Markets ───────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
> Search markets...
|
|
40
|
+
|
|
41
|
+
Watchlist │ $74,678│ │
|
|
42
|
+
›BTC Bitcoin │ │ ▌▌▖│
|
|
43
|
+
ETH Ethereum │ │ ▌│▘▌▖│
|
|
44
|
+
SOL Solana │ $70,438│ │ │ ▌ │▘▘▌
|
|
45
|
+
XRP XRP │ │ ▖▖▖▖ ▌▌ ▖▖▌ ▌││
|
|
46
|
+
DOGE Dogecoin │ │ ▌││▌▖▖ ▖▖│││▌▘▌▌│ ▘▘▘▌
|
|
47
|
+
BNB BNB │ $66,197│▖▖ │▌ ▘▘▘▌▖▖▌▘▌▖▌▌▌ ▘▘
|
|
48
|
+
│ ││▌▖ │▌▘ ▘▘▌▌ ▘▘▘▘
|
|
49
|
+
Mixed Components │ │ │▌▌▘▘ ▘▘
|
|
50
|
+
BTC - Candles Real BTC/USD hourly candles │ $61,957│ ││
|
|
51
|
+
ETH - Candle + Line Candles plus closing line │ 12d 8d 4d Now
|
|
52
|
+
SOL - Candle + Volume Candles plus volume spli │
|
|
53
|
+
BTC vs ETH Side-by-side crypto leaders │ Price: $67,641
|
|
54
|
+
DOGE - Candle + Line Low-priced asset formatti │
|
|
55
|
+
│ Change: -0.2%
|
|
56
|
+
│
|
|
57
|
+
│ Category: Store of Value
|
|
58
|
+
│
|
|
59
|
+
│ ────────────────────────────────────────────
|
|
60
|
+
│
|
|
61
|
+
↵ open detail ↑↓ navigate ^k actions │ BTC-USD Hourly OHLC
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
"
|
|
65
|
+
`)
|
|
66
|
+
|
|
67
|
+
expect(text).toContain('BTC')
|
|
68
|
+
expect(text).toContain('12d')
|
|
69
|
+
expect(text).toContain('Now')
|
|
70
|
+
expect(text).toContain('$')
|
|
71
|
+
}, 30000)
|
|
72
|
+
|
|
73
|
+
test('push to full-page detail view on Enter', async () => {
|
|
74
|
+
await session.text({ waitFor: (t) => t.includes('BTC'), timeout: 10000 })
|
|
75
|
+
// Press Enter to push to Detail view
|
|
76
|
+
session.sendKey('return')
|
|
77
|
+
|
|
78
|
+
const text = await session.text({
|
|
79
|
+
waitFor: (t) => t.includes('candle-only') && t.includes('Go Back'),
|
|
80
|
+
timeout: 10000,
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(text).toMatchInlineSnapshot(`
|
|
84
|
+
"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
█
|
|
88
|
+
|
|
89
|
+
BTC - Bitcoin
|
|
90
|
+
|
|
91
|
+
Category: Store of Value
|
|
92
|
+
|
|
93
|
+
Price: $67,641
|
|
94
|
+
24h change: -0.2%
|
|
95
|
+
|
|
96
|
+
Mode: candle-only
|
|
97
|
+
|
|
98
|
+
300 hourly candles from Coinbase Exchange, frozen so the example stays deterministic.
|
|
99
|
+
|
|
100
|
+
$74,678│ │
|
|
101
|
+
│ ▖▌▌│ │
|
|
102
|
+
│ ▌│▘▌▖▖▌▌
|
|
103
|
+
│ │▌ ││ ▌│││
|
|
104
|
+
│ ▌▘ ▘▌▖▌▌▖▖
|
|
105
|
+
$70,438│ │ │ │▌ │││▌
|
|
106
|
+
│ ▌▌ │ ▌▌▖▖ ││││▌▘ ▘▌
|
|
107
|
+
│ ▌▘▌▌▘▌▖│ ││ │ ▌││▘▌▖│▌▌▖▖▌ ▌▖▖▖▖▖▖▖
|
|
108
|
+
│ ▖▌ │││ ▌▌▘▌▌▌ │▖▖▖ ││ ││ ▌ │▌│▌││▘▘ ││ ▘▘│▘
|
|
109
|
+
$66,197│▖▖▖ │ ▌ ▘▘ │ ▘▌▖ │▌▘ ▘▌▌▌│▖▌▌▖▌ ▘▌▌
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
esc go back ^k actions ↵ Go Back powered by termcast.app
|
|
113
|
+
|
|
114
|
+
"
|
|
115
|
+
`)
|
|
116
|
+
|
|
117
|
+
// Full-page detail has larger chart and markdown content
|
|
118
|
+
expect(text).toContain('Bitcoin')
|
|
119
|
+
expect(text).toContain('candle-only')
|
|
120
|
+
}, 30000)
|
|
121
|
+
|
|
122
|
+
test('candle + line overlay (mixed components)', async () => {
|
|
123
|
+
await session.text({ waitFor: (t) => t.includes('Mixed Components'), timeout: 10000 })
|
|
124
|
+
// Navigate past Watchlist (6 items) to Mixed Components section,
|
|
125
|
+
// then one more item to ETH - Candle + Line.
|
|
126
|
+
for (let i = 0; i < 7; i++) {
|
|
127
|
+
session.sendKey('down')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const text = await session.text({
|
|
131
|
+
waitFor: (t) => t.includes('›ETH - Candle + Line') && t.includes('closing line'),
|
|
132
|
+
timeout: 10000,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
expect(text).toMatchInlineSnapshot(`
|
|
136
|
+
"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
Crypto Markets ───────────────────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
> Search markets...
|
|
142
|
+
|
|
143
|
+
Watchlist │ $2,220│ │
|
|
144
|
+
BTC Bitcoin │ │ │ ▌▌▖▖
|
|
145
|
+
ETH Ethereum │ $2,073│ ▖▖▖ │ ▖▖ ▖▌▘▘▌▖▖▖
|
|
146
|
+
SOL Solana │ │ │▌│▘▌▌▌ ▖▖ ▌▘▌││▌│ │││▌
|
|
147
|
+
XRP XRP │ │ │▌▘ │ ▌▖ ▖▌▘▌▖▌ ▌▌▘▘ ▌▌▘▘
|
|
148
|
+
DOGE Dogecoin │ $1,927│▖▖ ▖▌ │▘▌▖▌ │││ │
|
|
149
|
+
BNB BNB │ │▘▘▌▖▌▘│ ▘▘
|
|
150
|
+
│ $1,780│ ▘▘
|
|
151
|
+
Mixed Components │ 12d 8d 4d Now
|
|
152
|
+
BTC - Candles Real BTC/USD hourly candles │
|
|
153
|
+
›ETH - Candle + Line Candles plus closing line │ $2,197│ ⢠⣆⣠⡀
|
|
154
|
+
SOL - Candle + Volume Candles plus volume spli │ │ ⣴⣤⣤ ⣀ ⢠⣀ ⢠⣿⣿⣿⣷⣶⣦⡀
|
|
155
|
+
BTC vs ETH Side-by-side crypto leaders │ $1,997│ ⢰⣿⣿⣿⣿⣿⣇ ⣠⣷⣴⣄⣀⢸⣿⣶⣠⣤⣼⣿⣿⣿⣿⣿⣿⣧⣤⣤⣤
|
|
156
|
+
DOGE - Candle + Line Low-priced asset formatti │ │⣶⡄ ⣦⣾⣿⣿⣿⣿⣿⣿⣶⣶⢠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
|
157
|
+
│ $1,797│⣿⣿⣷⣦⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
|
|
158
|
+
│ 12d 8d 4d Now
|
|
159
|
+
│
|
|
160
|
+
│ ────────────────────────────────────────────
|
|
161
|
+
│
|
|
162
|
+
│ Price: $1,971
|
|
163
|
+
│
|
|
164
|
+
↵ open detail ↑↓ navigate ^k actions │ Change: -0.3%
|
|
165
|
+
|
|
166
|
+
"
|
|
167
|
+
`)
|
|
168
|
+
|
|
169
|
+
expect(text).toContain('Candle + Line')
|
|
170
|
+
}, 30000)
|
|
171
|
+
|
|
172
|
+
test('candle + volume bar chart (mixed components)', async () => {
|
|
173
|
+
await session.text({ waitFor: (t) => t.includes('Mixed Components'), timeout: 10000 })
|
|
174
|
+
// Navigate to "Candle + Volume" item (8 downs from top)
|
|
175
|
+
for (let i = 0; i < 8; i++) {
|
|
176
|
+
session.sendKey('down')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const text = await session.text({
|
|
180
|
+
waitFor: (t) => t.includes('›SOL - Candle + Volume') && t.includes('half'),
|
|
181
|
+
timeout: 10000,
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
expect(text).toMatchInlineSnapshot(`
|
|
185
|
+
"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
Crypto Markets ───────────────────────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
> Search markets...
|
|
191
|
+
|
|
192
|
+
Watchlist │ $95.03│ ││
|
|
193
|
+
BTC Bitcoin │ │ │ │ ▖▖▖▖
|
|
194
|
+
ETH Ethereum │ $88.26│ ▌▌▖ ││ │ ▖▖ ▌▘▘▘▌▌▌│
|
|
195
|
+
SOL Solana │ │ ▖▌│▘▌▌▌ │▌▌▖▖▌▘▌▖▌▘ ▘▌│││
|
|
196
|
+
XRP XRP │ │ │▌ │ ▌▖│ ▌▘▘▘▌▌ ▘▘││ ▘▘▘▌
|
|
197
|
+
DOGE Dogecoin │ $81.48│▖▖ ▌▘ │▘▌▌▘ │
|
|
198
|
+
BNB BNB │ │▘▘▌▖▌▘ ▘▘
|
|
199
|
+
│ $74.71│ ▘▘│
|
|
200
|
+
Mixed Components │ 12d 8d 4d Now
|
|
201
|
+
BTC - Candles Real BTC/USD hourly candles │
|
|
202
|
+
ETH - Candle + Line Candles plus closing line │ ┌Second half: 95.3%┐
|
|
203
|
+
›SOL - Candle + Volume Candles plus volume spli │
|
|
204
|
+
BTC vs ETH Side-by-side crypto leaders │
|
|
205
|
+
DOGE - Candle + Line Low-priced asset formatti │ ────────────────────────────────────────────
|
|
206
|
+
│
|
|
207
|
+
│ Price: $83.31
|
|
208
|
+
│
|
|
209
|
+
↵ open detail ↑↓ navigate ^k actions │ Change: -0.4%
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
"
|
|
216
|
+
`)
|
|
217
|
+
|
|
218
|
+
expect(text).toContain('Candle + Volume')
|
|
219
|
+
}, 30000)
|
|
220
|
+
|
|
221
|
+
test('side-by-side candle charts in Row', async () => {
|
|
222
|
+
await session.text({ waitFor: (t) => t.includes('Mixed Components'), timeout: 10000 })
|
|
223
|
+
// Navigate to "BTC vs ETH" item (9 downs from top)
|
|
224
|
+
for (let i = 0; i < 9; i++) {
|
|
225
|
+
session.sendKey('down')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const text = await session.text({
|
|
229
|
+
waitFor: (t) => t.includes('›BTC vs ETH'),
|
|
230
|
+
timeout: 10000,
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
expect(text).toMatchInlineSnapshot(`
|
|
234
|
+
"
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
Crypto Markets ───────────────────────────────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
> Search markets...
|
|
240
|
+
|
|
241
|
+
Watchlist │ $74,678│ ││ $2,220│ │
|
|
242
|
+
BTC Bitcoin │ │ ▌▌ │ │ ││
|
|
243
|
+
ETH Ethereum │ │ │ │ ▌▘▌ │ ▖▖│ │ ▌▌▖
|
|
244
|
+
SOL Solana │ │ ▖▖ ││▌ ▌│ │ ▌▘▌ ││││▌│▌
|
|
245
|
+
XRP XRP │ $68,318│ ▌▘▌ ││▌▘▘ ▘▘ $2,000│ ▌│▌ ▌▌▌▌▌ ▘▘
|
|
246
|
+
DOGE Dogecoin │ │▖ ▌│▌▖▌▘▘│ ││ ▌ ▘▌▌│││
|
|
247
|
+
BNB BNB │ │▌▌▘ ▘▘ │▌▖▌ ││
|
|
248
|
+
│ $61,957│ │ │ $1,780│││
|
|
249
|
+
Mixed Components │ 30d Now 30d Now
|
|
250
|
+
BTC - Candles Real BTC/USD hourly candles │
|
|
251
|
+
ETH - Candle + Line Candles plus closing line │ ────────────────────────────────────────────
|
|
252
|
+
SOL - Candle + Volume Candles plus volume spli │
|
|
253
|
+
›BTC vs ETH Side-by-side crypto leaders │ Price: $67,641
|
|
254
|
+
DOGE - Candle + Line Low-priced asset formatti │
|
|
255
|
+
│ Change: -0.2%
|
|
256
|
+
│
|
|
257
|
+
│
|
|
258
|
+
↵ open detail ↑↓ navigate ^k actions │
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
"
|
|
265
|
+
`)
|
|
266
|
+
|
|
267
|
+
expect(text).toContain('BTC vs ETH')
|
|
268
|
+
expect(text).toContain('Side-by-side')
|
|
269
|
+
}, 30000)
|
|
@@ -66,6 +66,7 @@ test('markdown tables render with borderless layout', async () => {
|
|
|
66
66
|
ap-south-1 89ms /api/health 500
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
esc go back ^k actions powered by termcast.app
|
|
69
70
|
|
|
70
71
|
|
|
71
72
|
|
|
@@ -82,7 +83,6 @@ test('markdown tables render with borderless layout', async () => {
|
|
|
82
83
|
|
|
83
84
|
|
|
84
85
|
|
|
85
|
-
esc go back ^k actions powered by termcast.app
|
|
86
86
|
|
|
87
87
|
"
|
|
88
88
|
`)
|
|
@@ -166,6 +166,7 @@ test('two tables render side by side in a Row', async () => {
|
|
|
166
166
|
ap-south-1 89ms /api/health 500
|
|
167
167
|
|
|
168
168
|
|
|
169
|
+
esc go back ^k actions powered by termcast.app
|
|
169
170
|
|
|
170
171
|
|
|
171
172
|
|
|
@@ -182,7 +183,6 @@ test('two tables render side by side in a Row', async () => {
|
|
|
182
183
|
|
|
183
184
|
|
|
184
185
|
|
|
185
|
-
esc go back ^k actions powered by termcast.app
|
|
186
186
|
|
|
187
187
|
"
|
|
188
188
|
`)
|