From 4daf8cb47c5a84ebbd9aa6c7ba2e3e1b72b8eea7 Mon Sep 17 00:00:00 2001 From: Matias Perichon Date: Thu, 25 Jun 2026 16:58:59 -0400 Subject: [PATCH] Fix #311: resolve log path against filteredRuns to support local time mode When the convert-timezone toggle is on, chart labels show local-time run_start strings but the lookup in open_log_file compared against the raw `runs` global which still holds UTC run_start. The substring match failed, path resolved to empty, and the user saw a 'log file cannot be found' alert even though the log exists on disk. Match against filteredRuns first (whose run_start has been converted/stripped to mirror chart labels), then fall back to the raw runs array for runs that may have been filtered out. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- robotframework_dashboard/js/log.js | 9 ++- tests/javascript/log.test.js | 110 +++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 tests/javascript/log.test.js diff --git a/robotframework_dashboard/js/log.js b/robotframework_dashboard/js/log.js index 9a3a1baa..7ce01c42 100644 --- a/robotframework_dashboard/js/log.js +++ b/robotframework_dashboard/js/log.js @@ -25,7 +25,14 @@ function open_log_file(event, chartElement, callbackData = undefined, directRunS const index = chartElement[0].index runStart = event.chart.data.labels[index] } - var output = runs.find(run => run.run_start.slice(0, 19) === runStart.slice(0, 19)) + // Match against filteredRuns: its run_start has been timezone-converted/stripped to + // match what the chart actually displays. Falling back to the original `runs` global + // (UTC strings) breaks when the user has the "convert to local timezone" toggle on, + // since the chart label no longer equals the UTC run_start. See issue #311. + var output = filteredRuns.find(run => run.run_start.slice(0, 19) === runStart.slice(0, 19)) + if (!output) { output = filteredRuns.find(run => get_run_label(run) === runStart) } + // Fallback to the raw runs array for safety (e.g. if the run was filtered out) + if (!output) { output = runs.find(run => run.run_start.slice(0, 19) === runStart.slice(0, 19)) } if (!output) { output = runs.find(run => get_run_label(run) === runStart) } var path = output ? output.path : "" if (path == "") { diff --git a/tests/javascript/log.test.js b/tests/javascript/log.test.js new file mode 100644 index 00000000..4d87512c --- /dev/null +++ b/tests/javascript/log.test.js @@ -0,0 +1,110 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +// Mutable mock state for runs/filteredRuns so individual tests can set them up. +const dataMock = { + runs: [], + suites: [], + tests: [], + server: false, + use_logs: true, +}; +const globalsMock = { + filteredRuns: [], + filteredSuites: [], + filteredTests: [], + filteredKeywords: [], +}; + +vi.mock('@js/variables/data.js', () => dataMock); +vi.mock('@js/variables/globals.js', () => globalsMock); +vi.mock('@js/variables/settings.js', () => ({ + settings: { show: { aliases: 'run_start' } }, + get_run_label: (item) => item.run_start, +})); + +// log.js calls window.open and alert; stub them. +beforeEach(() => { + dataMock.runs.length = 0; + dataMock.suites.length = 0; + dataMock.tests.length = 0; + globalsMock.filteredRuns.length = 0; + globalsMock.filteredSuites.length = 0; + globalsMock.filteredTests.length = 0; + globalsMock.filteredKeywords.length = 0; + globalThis.window = { open: vi.fn(() => ({})), location: { href: 'http://localhost/' } }; + globalThis.alert = vi.fn(); +}); + +const { open_log_file } = await import('@js/log.js'); + +// Build a fake chart click event for a non-line, non-doughnut graph that +// reads chart.data.labels[index]. Matches the path log.js takes for e.g. +// the bar statistics graph (the one in the issue's repro). +function makeBarClickEvent(label, graphId = 'runStatisticsGraph') { + return { + chart: { + config: { _config: { type: 'bar' } }, + canvas: { id: graphId }, + data: { labels: [label] }, + }, + }; +} + +describe('open_log_file (issue #311)', () => { + it('resolves log path against filteredRuns when timezone is converted to local time', () => { + // The DB / unfiltered runs hold the original UTC run_start. + const utcStart = '2025-01-15 10:00:00'; + // After the "convert to local timezone" toggle, filteredRuns carries + // the local-time wall-clock string — this is what the chart label shows. + const localStart = '2025-01-15 12:00:00'; + const path = '/results/output.xml'; + + dataMock.runs.push({ run_start: utcStart, path, run_alias: null }); + globalsMock.filteredRuns.push({ run_start: localStart, path, run_alias: null }); + + const event = makeBarClickEvent(localStart); + const chartElement = [{ index: 0 }]; + + open_log_file(event, chartElement); + + // Before the fix: lookup happened against `runs` (UTC), no match, alert fired. + expect(globalThis.alert).not.toHaveBeenCalled(); + expect(globalThis.window.open).toHaveBeenCalledTimes(1); + const openedUrl = globalThis.window.open.mock.calls[0][0]; + // output.xml is rewritten to log.html by transform_file_path + expect(openedUrl).toContain('log.html'); + }); + + it('still resolves correctly in UTC mode (filteredRuns == runs run_start)', () => { + const utcStart = '2025-01-15 10:00:00'; + const path = '/results/output.xml'; + dataMock.runs.push({ run_start: utcStart, path, run_alias: null }); + globalsMock.filteredRuns.push({ run_start: utcStart, path, run_alias: null }); + + const event = makeBarClickEvent(utcStart); + open_log_file(event, [{ index: 0 }]); + + expect(globalThis.alert).not.toHaveBeenCalled(); + expect(globalThis.window.open).toHaveBeenCalledTimes(1); + }); + + it('falls back to raw runs when the clicked run was filtered out', () => { + const utcStart = '2025-01-15 10:00:00'; + const path = '/results/output.xml'; + dataMock.runs.push({ run_start: utcStart, path, run_alias: null }); + // filteredRuns is empty — run filtered away + + const event = makeBarClickEvent(utcStart); + open_log_file(event, [{ index: 0 }]); + + expect(globalThis.alert).not.toHaveBeenCalled(); + expect(globalThis.window.open).toHaveBeenCalledTimes(1); + }); + + it('alerts only when no matching run exists anywhere', () => { + const event = makeBarClickEvent('2099-12-31 00:00:00'); + open_log_file(event, [{ index: 0 }]); + expect(globalThis.alert).toHaveBeenCalledTimes(1); + expect(globalThis.window.open).not.toHaveBeenCalled(); + }); +});