Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/dev-workflow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ The `robotdashboard` command only exists after `pip install`, and an installed c
python -m robotframework_dashboard.main -f tests -n robot_dashboard.html
```

This invokes `robotframework_dashboard/main.py:main()` and accepts every CLI flag. `tests/` contains 18 ready-made `output.xml` fixtures (two simulated projects, generated by `tests/robot/resources/generator/generate.py`), so the command above is the standard "give me a dashboard to look at". For the full-featured example with tags, versions, custom filters and log links use `python scripts/example.py` instead.
This invokes `robotframework_dashboard/main.py:main()` and accepts every CLI flag. `tests/` contains 18 ready-made `output.xml` fixtures (two simulated projects, generated by `tests/robot/resources/generator/generate.py`), so the command above is the standard "give me a dashboard to look at". For the full-featured example with tags, versions, custom filters and log links use `python scripts/example.py --test` instead (without `--test` it also overwrites the committed `example/` files, which is a release step).

Common variants:

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Use the project scripts — do **not** call `pytest`, `vitest`, `robot`, or `pab
| Python unit tests | `bash scripts/python-tests.sh` / `scripts\python-tests.bat` |
| JS unit tests | `bash scripts/javascript-tests.sh` / `scripts\javascript-tests.bat` |
| Generate a dashboard from source | `python -m robotframework_dashboard.main -f tests -n robot_dashboard.html` |
| Build the example dashboard (tags, versions, filters, logs) | `python scripts/example.py` / `scripts\example.bat` |
| Build the example dashboard (tags, versions, filters, logs) | `python scripts/example.py` / `scripts\example.bat`; add `--test` to build only in the repo root and leave `example/` untouched |
| Regenerate the `output.xml` fixtures | `python tests/robot/resources/generator/generate.py` (then refresh screenshots/CLI/DB references in Docker) |
| Docs site | `npm run docs:dev` / `npm run docs:build` |

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ python tests/robot/resources/generator/generate.py
then refresh the reference screenshots, `cli_output` and `database_output` files by running the robot tests in Docker.
The generator suites are not part of the test pipeline. See `tests/robot/resources/generator/README.md`.

The example dashboard in `example/` is built from the same fixtures with `python scripts/example.py` (or `scripts\example.bat` on Windows).
The example dashboard in `example/` is built from the same fixtures with `python scripts/example.py` (or `scripts\example.bat` on Windows). Add `--test` to build `robot_dashboard.html` in the repo root without updating `example/`, handy for eyeballing a change.

## Running Tests Locally - in a Docker Container
Run the tests locally on your PC before pushing and waiting for the results from the GitHub actions is always a good idea. But this requires to install the required components in your native PC. In some cases this will not work as expected bacause of the differemt versions used. E.g. screenshots taken during the tests may differ, so that the tests might fail.
Expand Down
13 changes: 13 additions & 0 deletions robotframework_dashboard/css/components/02-filters.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@
padding: 0.25rem;
}

/* the wrapper of select + panel is the anchor, so a panel flipped by set_filter_dropdown_visible
sits directly on top of its select and shrinks towards it while the list is being searched */
:has(> .filterCheckBoxes) {
position: relative;
}

.filterCheckBoxes.drop-up {
top: auto;
bottom: 100%;
margin-top: 0 !important;
margin-bottom: 0.5rem;
}

.filterCheckBoxes .list-group-item {
background-color: transparent;
border-color: transparent;
Expand Down
9 changes: 4 additions & 5 deletions robotframework_dashboard/js/eventlisteners/filter_modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
setup_custom_filters_in_select_filter_buttons,
} from "../filter/modal_options.js";
import { setup_suite_path_navigator } from "../filter/suite_path.js";
import { clear_all_filters } from "../filter/controls.js";
import { clear_all_filters, set_filter_dropdown_visible } from "../filter/controls.js";
import {
capture_default_filters,
populate_filter_profile_select,
Expand Down Expand Up @@ -89,9 +89,8 @@ function setup_filter_modal() {
});
// eventlistener for the runTags
function show_checkboxes() {
const checkboxes = document.getElementById("runTagCheckBoxes");
showingRunTags = !showingRunTags;
checkboxes.style.display = showingRunTags ? "block" : "none";
set_filter_dropdown_visible(runTagsSelectElement, checkboxesElement, showingRunTags);
}
const checkboxesElement = document.getElementById("runTagCheckBoxes");
const runTagsSelectElement = document.getElementById("selectRunTags");
Expand All @@ -107,7 +106,7 @@ function setup_filter_modal() {
const projectVersionSelectElement = document.getElementById("selectProjectVersion");
function toggle_project_version_filter_dialogue() {
showingProjectVersionDialogue = !showingProjectVersionDialogue;
projectVersionCheckboxes.style.display = showingProjectVersionDialogue ? "block" : "none";
set_filter_dropdown_visible(projectVersionSelectElement, projectVersionCheckboxes, showingProjectVersionDialogue);
}
projectVersionSelectElement.addEventListener("pointerdown", toggle_project_version_filter_dialogue);
document.body.addEventListener("pointerdown", function (event) {
Expand Down Expand Up @@ -166,7 +165,7 @@ function setup_filter_modal() {
let showingFilterProfiles = false;
function toggle_filter_profiles() {
showingFilterProfiles = !showingFilterProfiles;
document.getElementById("filterProfileCheckBoxes").style.display = showingFilterProfiles ? "block" : "none";
set_filter_dropdown_visible(selectFilterProfileElement, filterProfileCheckBoxes, showingFilterProfiles);
}
document.getElementById("selectFilterProfile").addEventListener("click", toggle_filter_profiles);
const filterProfileCheckBoxes = document.getElementById("filterProfileCheckBoxes");
Expand Down
3 changes: 2 additions & 1 deletion robotframework_dashboard/js/eventlisteners/settings_modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { setup_data_and_graphs } from "../menu.js";
import { toggle_theme, apply_theme_colors, apply_custom_branding } from "../theme.js";
import { collect_custom_filter_dimensions, dashboardPages } from "../filter/pipeline.js";
import { setup_lowest_highest_dates } from "../filter/modal_options.js";
import { set_filter_dropdown_visible } from "../filter/controls.js";
import { update_duration_comparison_for_all_projects } from "../graph_creation/overview.js";
import { confirm_action } from "./confirm_modal.js";

Expand Down Expand Up @@ -89,7 +90,7 @@ function setup_settings_modal() {
let showing = false;
function toggle() {
showing = !showing;
checkBoxesEl.style.display = showing ? "block" : "none";
set_filter_dropdown_visible(selectEl, checkBoxesEl, showing);
}
selectEl.addEventListener("pointerdown", toggle);
document.body.addEventListener("pointerdown", function (event) {
Expand Down
36 changes: 36 additions & 0 deletions robotframework_dashboard/js/filter/controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,45 @@ function set_filter_show_current_version(version) {
update_filter_active_indicator("projectVersionInputItemAll", "filterVersionSelectedIndicator");
}

// gap between the select and its dropdown panel (the mt-2 margin) plus breathing room to the viewport edge
const FILTER_DROPDOWN_GAP = 8;
const FILTER_DROPDOWN_EDGE_MARGIN = 16;
const FILTER_DROPDOWN_MIN_HEIGHT = 120;

// decide whether a dropdown panel opens below or above its select and how tall it may get:
// below while the content fits there, otherwise on whichever side has more room
function get_filter_dropdown_placement(spaceBelow, spaceAbove, contentHeight, defaultMaxHeight) {
const wantedHeight = Math.min(contentHeight, defaultMaxHeight);
const dropUp = wantedHeight > spaceBelow && spaceAbove > spaceBelow;
const available = dropUp ? spaceAbove : spaceBelow;
const maxHeight = Math.max(Math.min(defaultMaxHeight, available), FILTER_DROPDOWN_MIN_HEIGHT);
return { dropUp, maxHeight };
}

// show or hide a .filterCheckBoxes panel; when shown it flips above the select if there is not
// enough room below, so opening it does not stretch the modal and make the page jump
function set_filter_dropdown_visible(selectElement, panelElement, visible) {
if (!visible) {
panelElement.style.display = "none";
return;
}
panelElement.classList.remove("drop-up");
panelElement.style.display = "block";
const selectRect = selectElement.getBoundingClientRect();
const spaceBelow = window.innerHeight - selectRect.bottom - FILTER_DROPDOWN_GAP - FILTER_DROPDOWN_EDGE_MARGIN;
const spaceAbove = selectRect.top - FILTER_DROPDOWN_GAP - FILTER_DROPDOWN_EDGE_MARGIN;
const { dropUp, maxHeight } = get_filter_dropdown_placement(
spaceBelow, spaceAbove, panelElement.scrollHeight, window.innerHeight * 0.5
);
panelElement.classList.toggle("drop-up", dropUp);
panelElement.style.maxHeight = `${maxHeight}px`;
}

export {
clear_all_filters,
clear_overview_project_navigation_filter,
get_filter_dropdown_placement,
set_filter_dropdown_visible,
set_filter_show_current_version,
setup_filter_active_indicator,
setup_filter_checkbox_handler_listeners,
Expand Down
4 changes: 2 additions & 2 deletions robotframework_dashboard/js/filter/modal_options.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { settings } from '../variables/settings.js';
import { runs } from '../variables/data.js';
import { collect_custom_filter_dimensions } from './pipeline.js';
import { setup_filter_active_indicator, setup_filter_checkbox_handler_listeners, setup_filter_checkbox_subfilter } from './controls.js';
import { set_filter_dropdown_visible, setup_filter_active_indicator, setup_filter_checkbox_handler_listeners, setup_filter_checkbox_subfilter } from './controls.js';

// function to setup run amount filter maximum
function setup_run_amount_filter() {
Expand Down Expand Up @@ -262,7 +262,7 @@ function setup_custom_filters_in_select_filter_buttons() {
const selectEl = document.getElementById(selectId);
const checkBoxesEl = document.getElementById(checkBoxesId);
let showing = false;
function toggle() { showing = !showing; checkBoxesEl.style.display = showing ? "block" : "none"; }
function toggle() { showing = !showing; set_filter_dropdown_visible(selectEl, checkBoxesEl, showing); }
selectEl.addEventListener("pointerdown", toggle);
document.body.addEventListener("pointerdown", function (event) {
if (showing && !checkBoxesEl.contains(event.target) && !selectEl.contains(event.target)) {
Expand Down
14 changes: 13 additions & 1 deletion scripts/example.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"""Build the example dashboard (example/robot_dashboard.html + example/robot_results.db).

python scripts/example.py
python scripts/example.py --test # leave example/ untouched, only build in the repo root

Imports every fixture from tests/robot/resources/outputs/ with run tags, project
versions, timezones and custom filters so the example shows all dashboard features,
then copies the result into example/. Runs the package from source (python -m), so
then copies the result into example/ (skipped with --test). Runs the package from source (python -m), so
no install is needed. The fixtures come from tests/robot/resources/generator/.
"""

import argparse
import shutil
import subprocess
import sys
Expand Down Expand Up @@ -70,12 +72,22 @@ def build_commands():


def main():
parser = argparse.ArgumentParser(description="Build the example dashboard and database.")
parser.add_argument(
"--test",
action="store_true",
help=f"only build {DASHBOARD.name} and {DATABASE.name} in the repo root, do not update example/",
)
args = parser.parse_args()
for stale in (DATABASE, DASHBOARD):
if stale.exists():
stale.unlink()
for command in build_commands():
print(" ".join(command[3:5]), flush=True)
subprocess.run(command, cwd=ROOT, check=True)
if args.test:
print(f"Built {DASHBOARD} and {DATABASE}, example/ left untouched")
return
shutil.copy(DATABASE, EXAMPLE / DATABASE.name)
shutil.copy(DASHBOARD, EXAMPLE / DASHBOARD.name)
print(f"Updated {EXAMPLE / DATABASE.name} and {EXAMPLE / DASHBOARD.name}")
Expand Down
34 changes: 34 additions & 0 deletions tests/javascript/filter_dropdown.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, it, expect, vi } from 'vitest';

vi.mock('@js/variables/globals.js', () => import('./mocks/globals.js'));
vi.mock('@js/variables/data.js', () => import('./mocks/data.js'));
vi.mock('@js/variables/graphs.js', () => import('./mocks/graphs.js'));

import { get_filter_dropdown_placement } from '@js/filter/controls.js';

describe('get_filter_dropdown_placement', () => {
it('opens below when the content fits below', () => {
expect(get_filter_dropdown_placement(400, 600, 300, 500)).toEqual({ dropUp: false, maxHeight: 400 });
});

it('opens above when the content does not fit below and there is more room above', () => {
expect(get_filter_dropdown_placement(150, 600, 300, 500)).toEqual({ dropUp: true, maxHeight: 500 });
});

it('stays below when the content does not fit either way but below has more room', () => {
expect(get_filter_dropdown_placement(250, 200, 300, 500)).toEqual({ dropUp: false, maxHeight: 250 });
});

it('caps the height at the room above when flipped', () => {
expect(get_filter_dropdown_placement(100, 220, 400, 500)).toEqual({ dropUp: true, maxHeight: 220 });
});

it('only compares against the default max height for very long lists', () => {
// 2000px of options, but the panel never grows beyond 500px, which fits below
expect(get_filter_dropdown_placement(550, 800, 2000, 500)).toEqual({ dropUp: false, maxHeight: 500 });
});

it('never shrinks below the minimum height', () => {
expect(get_filter_dropdown_placement(20, 10, 300, 500)).toEqual({ dropUp: false, maxHeight: 120 });
});
});