Fix freshness and daily date labels
This commit is contained in:
parent
91677127a0
commit
4488c7d4b9
6 changed files with 77 additions and 52 deletions
|
|
@ -1,46 +1,19 @@
|
||||||
# Current recommendations
|
# Current recommendations
|
||||||
|
|
||||||
Last reviewed: 2026-08-11 03:39 CDT.
|
Last reviewed: 2026-08-11 04:13 CDT.
|
||||||
|
|
||||||
This file is the short list of work worth considering next. Verified history,
|
This file is the short list of work worth considering next. Verified history,
|
||||||
measurements and completed work remain in `docs/IMPLEMENTATION_PLAN.md`.
|
measurements and completed work remain in `docs/IMPLEMENTATION_PLAN.md`.
|
||||||
|
|
||||||
## Fix next
|
## Fix next
|
||||||
|
|
||||||
### Show unambiguous market freshness
|
### Deepen freshness telemetry
|
||||||
|
|
||||||
The best location is the status strip directly below the chart. Keep the header
|
The status bar now reports when the browser received its latest snapshot or bar.
|
||||||
for coarse transport state (`LIVE · SCHWAB`, `DELAYED · YAHOO 10m`) and replace
|
A later server-side implementation can distinguish market closure, source delay
|
||||||
the ambiguous `LAST BAR` age with browser-observed receipt time:
|
and transport failure by carrying exact trade time, the last settled minute,
|
||||||
|
source receipt time and an application heartbeat over the existing WebSocket.
|
||||||
```text
|
Any stale threshold must account for Yahoo's declared delay.
|
||||||
UPDATED 3s AGO
|
|
||||||
```
|
|
||||||
|
|
||||||
On mobile use a compact form:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Updated 3s ago · Live
|
|
||||||
```
|
|
||||||
|
|
||||||
No additional request is needed. Record `Date.now()` when an existing WebSocket
|
|
||||||
`bar` message arrives. A snapshot should initially say `LOADED JUST NOW`; level,
|
|
||||||
alert and preference messages are not market updates and must not refresh it.
|
|
||||||
|
|
||||||
Do not derive update age from `bar.t`: it is the bucket start, and selected
|
|
||||||
timeframe bar messages currently overwrite `status.last_bar_t`, making an hourly
|
|
||||||
bar appear stale even when just received.
|
|
||||||
|
|
||||||
A later, more complete implementation can add these fields to the existing
|
|
||||||
WebSocket rather than add polling:
|
|
||||||
|
|
||||||
- exact `last_trade_t` where the source supplies it;
|
|
||||||
- `last_settled_m1_t`, measured from the end of the minute;
|
|
||||||
- `last_source_received_at`;
|
|
||||||
- an application heartbeat for transport health.
|
|
||||||
|
|
||||||
Stale warnings must distinguish market closure from transport failure and must
|
|
||||||
subtract Yahoo's declared delay before calling delayed data overdue.
|
|
||||||
|
|
||||||
## Mobile authoring
|
## Mobile authoring
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,8 @@ createApp({
|
||||||
setup() {
|
setup() {
|
||||||
const status = ref({ stream: 'disconnected', bars_held: {} });
|
const status = ref({ stream: 'disconnected', bars_held: {} });
|
||||||
const deployment = ref(null);
|
const deployment = ref(null);
|
||||||
|
const dataReceivedAt = ref(null);
|
||||||
|
const dataReceiptVerb = ref('LOADED');
|
||||||
const price = ref(null);
|
const price = ref(null);
|
||||||
const prefs = ref(loadPrefs());
|
const prefs = ref(loadPrefs());
|
||||||
const timeframe = ref(prefs.value.base_tf || '1m');
|
const timeframe = ref(prefs.value.base_tf || '1m');
|
||||||
|
|
@ -111,17 +113,29 @@ createApp({
|
||||||
const seconds = Math.max(0, Math.floor(now.value / 1000 - status.value.last_bar_t));
|
const seconds = Math.max(0, Math.floor(now.value / 1000 - status.value.last_bar_t));
|
||||||
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m`;
|
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m`;
|
||||||
});
|
});
|
||||||
const updatedAt = computed(() => {
|
const deployedAt = computed(() => {
|
||||||
if (!deployment.value?.started_at) return '—';
|
if (!deployment.value?.started_at) return '—';
|
||||||
return new Intl.DateTimeFormat(undefined, {
|
return new Intl.DateTimeFormat(undefined, {
|
||||||
month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit',
|
month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit',
|
||||||
}).format(new Date(deployment.value.started_at));
|
}).format(new Date(deployment.value.started_at));
|
||||||
});
|
});
|
||||||
|
const deployMeta = computed(() => {
|
||||||
|
if (!deployment.value) return '—';
|
||||||
|
return `${deployment.value.commit.slice(0, 8)} · ${deployedAt.value}`;
|
||||||
|
});
|
||||||
const deploymentTitle = computed(() => {
|
const deploymentTitle = computed(() => {
|
||||||
if (!deployment.value?.started_at) return '';
|
if (!deployment.value?.started_at) return '';
|
||||||
const timestamp = new Date(deployment.value.started_at).toLocaleString();
|
const timestamp = new Date(deployment.value.started_at).toLocaleString();
|
||||||
return `Running since ${timestamp} · ${deployment.value.commit}`;
|
return `Running since ${timestamp} · ${deployment.value.commit}`;
|
||||||
});
|
});
|
||||||
|
const dataAge = computed(() => {
|
||||||
|
if (dataReceivedAt.value == null) return 'WAITING';
|
||||||
|
const seconds = Math.max(0, Math.floor((now.value - dataReceivedAt.value) / 1000));
|
||||||
|
if (seconds < 5) return 'JUST NOW';
|
||||||
|
if (seconds < 60) return `${seconds}s AGO`;
|
||||||
|
if (seconds < 3600) return `${Math.floor(seconds / 60)}m AGO`;
|
||||||
|
return `${Math.floor(seconds / 3600)}h AGO`;
|
||||||
|
});
|
||||||
const manualLines = computed(() => levels.value.filter(level => level.kind === 'manual'));
|
const manualLines = computed(() => levels.value.filter(level => level.kind === 'manual'));
|
||||||
const hasLineSelection = computed(() => selectedLines.value.length > 0 || selectedLine.value != null);
|
const hasLineSelection = computed(() => selectedLines.value.length > 0 || selectedLine.value != null);
|
||||||
const allManualSelected = computed(() => manualLines.value.length > 0 && selectedLines.value.length === manualLines.value.length);
|
const allManualSelected = computed(() => manualLines.value.length > 0 && selectedLines.value.length === manualLines.value.length);
|
||||||
|
|
@ -179,6 +193,8 @@ createApp({
|
||||||
socket.onmessage = ({ data }) => {
|
socket.onmessage = ({ data }) => {
|
||||||
const message = JSON.parse(data);
|
const message = JSON.parse(data);
|
||||||
if (message.type === 'snapshot') {
|
if (message.type === 'snapshot') {
|
||||||
|
dataReceivedAt.value = Date.now();
|
||||||
|
dataReceiptVerb.value = 'LOADED';
|
||||||
chartApi.setBars(message.bars);
|
chartApi.setBars(message.bars);
|
||||||
levels.value = message.levels || [];
|
levels.value = message.levels || [];
|
||||||
syncVisibleLevels();
|
syncVisibleLevels();
|
||||||
|
|
@ -190,6 +206,8 @@ createApp({
|
||||||
// series is older than everything in it, which the chart rejects
|
// series is older than everything in it, which the chart rejects
|
||||||
// outright. The event carries its timeframe, so honour it.
|
// outright. The event carries its timeframe, so honour it.
|
||||||
if (message.tf && message.tf !== timeframe.value) return;
|
if (message.tf && message.tf !== timeframe.value) return;
|
||||||
|
dataReceivedAt.value = Date.now();
|
||||||
|
dataReceiptVerb.value = 'UPDATED';
|
||||||
chartApi.updateBar(message.bar);
|
chartApi.updateBar(message.bar);
|
||||||
price.value = message.bar.c;
|
price.value = message.bar.c;
|
||||||
status.value.last_bar_t = message.bar.t;
|
status.value.last_bar_t = message.bar.t;
|
||||||
|
|
@ -569,6 +587,6 @@ createApp({
|
||||||
window.removeEventListener('keydown', handleKeydown);
|
window.removeEventListener('keydown', handleKeydown);
|
||||||
});
|
});
|
||||||
|
|
||||||
return { status, price, barAge, updatedAt, deploymentTitle, timeframe, timeframes, prefs, clusters, alerts, armedTool, drawName, drawColor, drawWidth, drawSide, snap, selectedLine, selectedLines, manualLines, hasLineSelection, allManualSelected, alertPrice, alertNote, levelColor, levelWidth, addPriceAlert, armTool, selectTimeframe, allEnabled, toggleGroup, deleteSelected, deleteLine, selectLine, toggleLineSelection, toggleSelectAll, deleteSelectedLines, renameLine, updateLineStyle, setArmed, commentText, commentFloat, comments, drawings, filteredDrawings, drawingFilter, drawingKind, deleteDrawing, deleteFilteredDrawings, toggleComment, togglePinned };
|
return { status, price, barAge, dataAge, dataReceiptVerb, deployMeta, deploymentTitle, timeframe, timeframes, prefs, clusters, alerts, armedTool, drawName, drawColor, drawWidth, drawSide, snap, selectedLine, selectedLines, manualLines, hasLineSelection, allManualSelected, alertPrice, alertNote, levelColor, levelWidth, addPriceAlert, armTool, selectTimeframe, allEnabled, toggleGroup, deleteSelected, deleteLine, selectLine, toggleLineSelection, toggleSelectAll, deleteSelectedLines, renameLine, updateLineStyle, setArmed, commentText, commentFloat, comments, drawings, filteredDrawings, drawingFilter, drawingKind, deleteDrawing, deleteFilteredDrawings, toggleComment, togglePinned };
|
||||||
},
|
},
|
||||||
}).mount('#app');
|
}).mount('#app');
|
||||||
|
|
|
||||||
|
|
@ -96,11 +96,16 @@ class ConfluenceChart {
|
||||||
// the label is localised, via the browser's own zone.
|
// the label is localised, via the browser's own zone.
|
||||||
tickMarkFormatter: (time, tickMarkType) => {
|
tickMarkFormatter: (time, tickMarkType) => {
|
||||||
const at = new Date(time * 1000);
|
const at = new Date(time * 1000);
|
||||||
if (tickMarkType === LightweightCharts.TickMarkType.Year) return String(at.getFullYear());
|
const daily = this.bars[0]?.tf === '1d';
|
||||||
if (tickMarkType === LightweightCharts.TickMarkType.Month) {
|
if (tickMarkType === LightweightCharts.TickMarkType.Year) {
|
||||||
return at.toLocaleString(undefined, { month: 'short' });
|
return String(daily ? at.getUTCFullYear() : at.getFullYear());
|
||||||
|
}
|
||||||
|
if (tickMarkType === LightweightCharts.TickMarkType.Month) {
|
||||||
|
return at.toLocaleString(undefined, { month: 'short', ...(daily ? { timeZone: 'UTC' } : {}) });
|
||||||
|
}
|
||||||
|
if (tickMarkType === LightweightCharts.TickMarkType.DayOfMonth) {
|
||||||
|
return String(daily ? at.getUTCDate() : at.getDate());
|
||||||
}
|
}
|
||||||
if (tickMarkType === LightweightCharts.TickMarkType.DayOfMonth) return String(at.getDate());
|
|
||||||
return at.toLocaleTimeString(undefined, {
|
return at.toLocaleTimeString(undefined, {
|
||||||
hour12: false, hour: '2-digit', minute: '2-digit',
|
hour12: false, hour: '2-digit', minute: '2-digit',
|
||||||
...(tickMarkType === LightweightCharts.TickMarkType.TimeWithSeconds
|
...(tickMarkType === LightweightCharts.TickMarkType.TimeWithSeconds
|
||||||
|
|
@ -110,9 +115,12 @@ class ConfluenceChart {
|
||||||
},
|
},
|
||||||
localization: {
|
localization: {
|
||||||
// The crosshair readout, which would otherwise disagree with the axis.
|
// The crosshair readout, which would otherwise disagree with the axis.
|
||||||
timeFormatter: time => new Date(time * 1000).toLocaleString(undefined, {
|
timeFormatter: time => {
|
||||||
hour12: false, month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
|
const daily = this.bars[0]?.tf === '1d';
|
||||||
}),
|
return new Date(time * 1000).toLocaleString(undefined, daily
|
||||||
|
? { timeZone: 'UTC', year: 'numeric', month: 'short', day: 'numeric' }
|
||||||
|
: { hour12: false, month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
rightPriceScale: { borderVisible: false },
|
rightPriceScale: { borderVisible: false },
|
||||||
// Every price-bearing series shares this scale. A second autoscaled price
|
// Every price-bearing series shares this scale. A second autoscaled price
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@
|
||||||
<span v-if="armedTool === 'trendline'" class="arm-hint">Drag on the chart from one point to the other</span>
|
<span v-if="armedTool === 'trendline'" class="arm-hint">Drag on the chart from one point to the other</span>
|
||||||
<span v-else-if="armedTool === 'level'" class="arm-hint">Click or drag on the chart to set the price</span>
|
<span v-else-if="armedTool === 'level'" class="arm-hint">Click or drag on the chart to set the price</span>
|
||||||
<span v-else-if="selectedLine" class="arm-hint">Line selected — Delete removes it</span>
|
<span v-else-if="selectedLine" class="arm-hint">Line selected — Delete removes it</span>
|
||||||
<span class="app-updated" :title="deploymentTitle">UPDATED <b>{{ updatedAt }}</b></span>
|
<span class="data-freshness">{{ dataReceiptVerb }} <b>{{ dataAge }}</b></span>
|
||||||
|
<span class="app-meta" :title="deploymentTitle">DEPLOY <b>{{ deployMeta }}</b></span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<aside>
|
<aside>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ button { border:1px solid var(--line); background:transparent; color:var(--muted
|
||||||
.drawing-tools { min-height:38px; padding:5px 12px; display:flex; align-items:center; gap:9px; border-bottom:1px solid var(--line); color:var(--muted); font-size:10px; }.drawing-tools button,.drawing-tools select,.drawing-tools .line-name { padding:4px 8px; font-size:10px; }.drawing-tools select,.drawing-tools .line-name { background:var(--panel); color:var(--fg); border:1px solid var(--line); }.drawing-tools .line-name { width:130px; font:inherit; }.drawing-tools label { display:flex; gap:4px; align-items:center; }.drawing-tools input { accent-color:var(--accent); }
|
.drawing-tools { min-height:38px; padding:5px 12px; display:flex; align-items:center; gap:9px; border-bottom:1px solid var(--line); color:var(--muted); font-size:10px; }.drawing-tools button,.drawing-tools select,.drawing-tools .line-name { padding:4px 8px; font-size:10px; }.drawing-tools select,.drawing-tools .line-name { background:var(--panel); color:var(--fg); border:1px solid var(--line); }.drawing-tools .line-name { width:130px; font:inherit; }.drawing-tools label { display:flex; gap:4px; align-items:center; }.drawing-tools input { accent-color:var(--accent); }
|
||||||
#chart { position:relative; height:calc(100vh - 190px); min-height:420px; }.chart-preview,.chart-handles { position:absolute; inset:0; width:100%; height:100%; overflow:hidden; pointer-events:none; }.chart-preview { z-index:4; }.chart-handles { z-index:6; }.chart-preview line[hidden],.chart-anchor[hidden] { display:none; }.chart-anchor { stroke:var(--panel); stroke-width:2px; cursor:grab; pointer-events:all; touch-action:none; }.chart-anchor:active { cursor:grabbing; }.chart-tooltip { position:absolute; z-index:5; padding:4px 7px; border:1px solid var(--line); background:var(--panel); color:var(--fg); font-size:10px; pointer-events:none; }.chart-tooltip[hidden] { display:none; }
|
#chart { position:relative; height:calc(100vh - 190px); min-height:420px; }.chart-preview,.chart-handles { position:absolute; inset:0; width:100%; height:100%; overflow:hidden; pointer-events:none; }.chart-preview { z-index:4; }.chart-handles { z-index:6; }.chart-preview line[hidden],.chart-anchor[hidden] { display:none; }.chart-anchor { stroke:var(--panel); stroke-width:2px; cursor:grab; pointer-events:all; touch-action:none; }.chart-anchor:active { cursor:grabbing; }.chart-tooltip { position:absolute; z-index:5; padding:4px 7px; border:1px solid var(--line); background:var(--panel); color:var(--fg); font-size:10px; pointer-events:none; }.chart-tooltip[hidden] { display:none; }
|
||||||
.chart-context-menu { position:absolute; z-index:8; width:165px; padding:4px; border:1px solid var(--line); background:var(--panel); box-shadow:0 5px 18px color-mix(in srgb,var(--fg) 15%,transparent); pointer-events:auto; }.chart-context-menu[hidden] { display:none; }.chart-context-menu button { width:100%; padding:6px 8px; text-align:left; color:var(--fg); font-size:10px; }
|
.chart-context-menu { position:absolute; z-index:8; width:165px; padding:4px; border:1px solid var(--line); background:var(--panel); box-shadow:0 5px 18px color-mix(in srgb,var(--fg) 15%,transparent); pointer-events:auto; }.chart-context-menu[hidden] { display:none; }.chart-context-menu button { width:100%; padding:6px 8px; text-align:left; color:var(--fg); font-size:10px; }
|
||||||
.statusbar { min-height:34px; display:flex; align-items:center; gap:24px; padding:6px 13px; border-top:1px solid var(--line); color:var(--muted); font-size:10px; }.statusbar b { color:var(--fg); text-transform:uppercase; }.app-updated { margin-left:auto; white-space:nowrap; }
|
.statusbar { min-height:34px; display:flex; align-items:center; gap:24px; padding:6px 13px; border-top:1px solid var(--line); color:var(--muted); font-size:10px; }.statusbar b { color:var(--fg); text-transform:uppercase; }.data-freshness { margin-left:auto; }.data-freshness,.app-meta { white-space:nowrap; }
|
||||||
aside { padding:16px; }h2 { margin:0 0 12px; color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:1.3px; }h2:not(:first-child) { margin-top:30px; }.empty { border-left:2px solid var(--line); padding:10px 12px; color:var(--muted); font-size:11px; }
|
aside { padding:16px; }h2 { margin:0 0 12px; color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:1.3px; }h2:not(:first-child) { margin-top:30px; }.empty { border-left:2px solid var(--line); padding:10px 12px; color:var(--muted); font-size:11px; }
|
||||||
.sidebar-section { margin-top:30px; }.sidebar-section:first-of-type { margin-top:0; }.sidebar-section summary { margin-bottom:12px; color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:1.3px; cursor:pointer; user-select:none; }.sidebar-section:not([open]) summary { margin-bottom:0; }
|
.sidebar-section { margin-top:30px; }.sidebar-section:first-of-type { margin-top:0; }.sidebar-section summary { margin-bottom:12px; color:var(--muted); font-size:11px; text-transform:uppercase; letter-spacing:1.3px; cursor:pointer; user-select:none; }.sidebar-section:not([open]) summary { margin-bottom:0; }
|
||||||
.trendline-actions { display:flex; gap:5px; margin-bottom:7px; }.trendline-actions button { flex:1; padding:4px; font-size:9px; }.trendline-row { display:grid; grid-template-columns:auto minmax(0,1fr) auto; gap:5px 8px; padding:7px; border:1px solid transparent; }.trendline-row.selected { border-color:var(--accent); }.trendline-row>.line-select { align-self:center; accent-color:var(--accent); }.trendline-row>input:not(.line-select) { min-width:0; border:0; border-bottom:1px solid var(--line); background:transparent; color:var(--fg); font:inherit; font-size:11px; }.trendline-row span:not(.drawing-icon) { grid-column:2; color:var(--muted); font-size:9px; text-transform:uppercase; }.trendline-row>.drawing-icon { grid-column:1; align-self:center; }.trendline-row button { grid-column:3; grid-row:1; padding:3px 6px; font-size:9px; }.line-style-controls { grid-column:3; display:flex; align-items:center; gap:4px; }.line-style-controls input { width:24px; height:20px; padding:0; border:0; background:transparent; }.line-style-controls select { border:1px solid var(--line); background:var(--panel); color:var(--fg); font-size:9px; }
|
.trendline-actions { display:flex; gap:5px; margin-bottom:7px; }.trendline-actions button { flex:1; padding:4px; font-size:9px; }.trendline-row { display:grid; grid-template-columns:auto minmax(0,1fr) auto; gap:5px 8px; padding:7px; border:1px solid transparent; }.trendline-row.selected { border-color:var(--accent); }.trendline-row>.line-select { align-self:center; accent-color:var(--accent); }.trendline-row>input:not(.line-select) { min-width:0; border:0; border-bottom:1px solid var(--line); background:transparent; color:var(--fg); font:inherit; font-size:11px; }.trendline-row span:not(.drawing-icon) { grid-column:2; color:var(--muted); font-size:9px; text-transform:uppercase; }.trendline-row>.drawing-icon { grid-column:1; align-self:center; }.trendline-row button { grid-column:3; grid-row:1; padding:3px 6px; font-size:9px; }.line-style-controls { grid-column:3; display:flex; align-items:center; gap:4px; }.line-style-controls input { width:24px; height:20px; padding:0; border:0; background:transparent; }.line-style-controls select { border:1px solid var(--line); background:var(--panel); color:var(--fg); font-size:9px; }
|
||||||
|
|
|
||||||
|
|
@ -25,28 +25,53 @@ test('the viewport opens on the live edge, not in the past', { timeout: 180000 }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the running deployment time is visible without making the status bar taller',
|
test('data freshness and deployment metadata share the existing status row',
|
||||||
{ timeout: 180000 }, async () => {
|
{ timeout: 180000 }, async () => {
|
||||||
await withChart(async page => {
|
await withChart(async page => {
|
||||||
await page.waitForFunction(() => {
|
await page.waitForFunction(() => {
|
||||||
const node = document.querySelector('.app-updated b');
|
const freshness = document.querySelector('.data-freshness b');
|
||||||
return node && node.textContent.trim() !== '—';
|
const deployment = document.querySelector('.app-meta b');
|
||||||
|
return freshness?.textContent.trim() !== 'WAITING'
|
||||||
|
&& deployment?.textContent.trim() !== '—';
|
||||||
});
|
});
|
||||||
const state = await page.evaluate(() => {
|
const state = await page.evaluate(() => {
|
||||||
const node = document.querySelector('.app-updated');
|
const freshness = document.querySelector('.data-freshness');
|
||||||
|
const deployment = document.querySelector('.app-meta');
|
||||||
return {
|
return {
|
||||||
text: node.textContent.trim(),
|
freshness: freshness.textContent.trim(),
|
||||||
title: node.title,
|
deployment: deployment.textContent.trim(),
|
||||||
|
title: deployment.title,
|
||||||
barHeight: document.querySelector('.statusbar').getBoundingClientRect().height,
|
barHeight: document.querySelector('.statusbar').getBoundingClientRect().height,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
assert.match(state.text, /^UPDATED\s+\S/);
|
assert.match(state.freshness, /^(LOADED|UPDATED)\s+(JUST NOW|\d+[smh] AGO)$/);
|
||||||
|
assert.match(state.deployment, /^DEPLOY\s+(dev|[0-9a-f]{8}) · \S/);
|
||||||
assert.match(state.title, /^Running since .+ · (dev|[0-9a-f]{7,40})$/);
|
assert.match(state.title, /^Running since .+ · (dev|[0-9a-f]{7,40})$/);
|
||||||
assert.ok(state.barHeight <= 40, `status bar grew to ${state.barHeight}px`);
|
assert.ok(state.barHeight <= 40, `status bar grew to ${state.barHeight}px`);
|
||||||
assertNoPageErrors(page, assert);
|
assertNoPageErrors(page, assert);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a daily crosshair shows the session date, not the previous evening',
|
||||||
|
{ timeout: 180000 }, async () => {
|
||||||
|
await withChart(async page => {
|
||||||
|
await selectTimeframe(page, '1d');
|
||||||
|
const state = await page.evaluate(() => {
|
||||||
|
const c = window.__chart;
|
||||||
|
const time = c.bars[c.bars.length - 1].t;
|
||||||
|
return {
|
||||||
|
actual: c.chart.options().localization.timeFormatter(time),
|
||||||
|
expected: new Date(time * 1000).toLocaleString(undefined, {
|
||||||
|
timeZone: 'UTC', year: 'numeric', month: 'short', day: 'numeric',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
assert.equal(state.actual, state.expected);
|
||||||
|
assert.doesNotMatch(state.actual, /\d:\d/);
|
||||||
|
assertNoPageErrors(page, assert);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test('every timeframe gives one slot per candle', { timeout: 300000 }, async () => {
|
test('every timeframe gives one slot per candle', { timeout: 300000 }, async () => {
|
||||||
await withChart(async page => {
|
await withChart(async page => {
|
||||||
for (const tf of TIMEFRAMES) {
|
for (const tf of TIMEFRAMES) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue