What this machine is set to
@@ -4876,7 +4892,31 @@
Run
if (shown) { drawOpenScreens(shown); }
}
- function showView(which) {
+ // What each view is called at the top of the pane. It read "Runs on this
+ // machine" over all of them, so switching view changed nothing a screen
+ // reader would say and nothing a glance would catch.
+ function headingFor(box) {
+ return {
+ office: "The office",
+ graph: "Who asked whom",
+ when: "When runs ran",
+ waiting: "Waiting to run",
+ terminal: "Terminal"
+ }[box] || "Runs on this machine";
+ }
+
+ // Which view the address names, or -1 for none of them.
+ function viewInAddress() {
+ var named = location.hash.replace(/^#/, "");
+
+ for (var i = 0; i < views.length; i++) {
+ if (views[i].box === named) { return i; }
+ }
+
+ return -1;
+ }
+
+ function showView(which, person) {
if (board.on) {
// Take the screens back off the board before showing one on its own.
board.on = false;
@@ -4903,7 +4943,36 @@ Run
// none of it belongs above a page of preferences.
onlySettings(views[which].box === "settings");
+ var heading = document.getElementById("runs");
+
+ if (heading) {
+ heading.textContent = headingFor(views[which].box);
+ heading.parentElement.setAttribute("data-view", views[which].box);
+ }
+
if (shown) { views[which].draw(shown); }
+
+ // Moved to when somebody chose it, and not when the page opens on it: a
+ // view that changes and leaves focus on the button it was chosen with is
+ // one a keyboard user has to go and find, and a screen reader says
+ // nothing about. Settings draws its heading above, so it exists by now.
+ if (person) {
+ var top = views[which].box === "settings"
+ ? document.getElementById("settings-heading")
+ : heading;
+
+ if (top) {
+ top.setAttribute("tabindex", "-1");
+ heading = top;
+ heading.focus();
+ }
+ }
+
+ // The address says which view this is, so Back goes to the last one and a
+ // bookmark opens this one. The token stays where it is, in the query.
+ if (person && location.hash !== "#" + views[which].box) {
+ history.pushState(null, "", "#" + views[which].box);
+ }
}
// The runs as they were last drawn, so switching view does not have to wait
@@ -4973,7 +5042,7 @@ Run
(function () {
views.forEach(function (one, i) {
document.getElementById(one.button).addEventListener("click", function () {
- showView(i);
+ showView(i, true);
});
});
@@ -4985,6 +5054,16 @@ Run
railUp();
plainly();
askTheMachine();
+
+ // Opened on the view the address names, and moved by Back and Forward.
+ // Neither takes the focus: nobody chose them on this page.
+ var named = viewInAddress();
+
+ if (named > 0) { showView(named, false); }
+
+ window.addEventListener("popstate", function () {
+ showView(Math.max(0, viewInAddress()), false);
+ });
}
if (solo) {
@@ -6569,6 +6648,20 @@ Run
if (prefs) { telling.appendChild(prefs); }
+ // ---- this machine ----
+ // Where notices go, what may start a run from outside, where this page
+ // listens and which office art it draws. It sat under the runs list, where
+ // nothing about it belonged, and is settings - of the machine rather than
+ // the screen, which is why it says so and types a command for each change.
+ // Moved rather than rebuilt, so its controls keep what they were wired to;
+ // fillSettings still decides whether it shows at all.
+ var machine = document.getElementById("machine");
+
+ if (machine) {
+ page.appendChild(machine);
+ machine.open = true;
+ }
+
// ---- what is not here ----
var elsewhere = group(page, "Everything else");
@@ -7776,7 +7869,12 @@ Run
*/
function fillSettings() {
return api("/api/settings").then(function (answer) {
- var fold = document.getElementById("settings");
+ // Its own id. It was "settings", the same as the Settings page, and
+ // getElementById returns the first: this unhid the page rather than the
+ // fold, and where notices go, the trigger token, the listen address and
+ // the office art were out of reach from the dashboard from the day the
+ // Settings page arrived.
+ var fold = document.getElementById("machine");
if (!fold) { return; }
diff --git a/tests/Loadout.Tests/Integration/DashboardServerTests.cs b/tests/Loadout.Tests/Integration/DashboardServerTests.cs
index 9ca07c5d..c3ff15b2 100644
--- a/tests/Loadout.Tests/Integration/DashboardServerTests.cs
+++ b/tests/Loadout.Tests/Integration/DashboardServerTests.cs
@@ -1213,6 +1213,57 @@ public async Task An_office_with_no_art_still_holds_its_desks()
text.Should().NotContain(".desk .card");
}
+ [Fact]
+ public async Task Every_id_on_the_page_is_used_once()
+ {
+ // The Settings page and the "What this machine is set to" fold were both
+ // id="settings". getElementById returns the first, so the code meant to
+ // show the fold unhid the page instead, and where notices go, the
+ // trigger token, the listen address and the office art could not be
+ // reached from the dashboard at all.
+ var text = await (await GetAsync("/")).Content.ReadAsStringAsync();
+
+ var ids = System.Text.RegularExpressions.Regex.Matches(text, "\\sid=\"([^\"]+)\"")
+ .Select(match => match.Groups[1].Value)
+ .GroupBy(id => id)
+ .Where(group => group.Count() > 1)
+ .Select(group => group.Key);
+
+ ids.Should().BeEmpty("an id names one element");
+
+ // And the machine's settings wait out of sight until the Settings page
+ // takes them, rather than appearing under the list of runs.
+ text.Should().Contain("section.pane > #machine { display: none; }");
+ }
+
+ [Fact]
+ public async Task A_view_says_what_it_is_and_takes_the_focus()
+ {
+ // Switching view left focus on the button and the heading reading
+ // "Runs on this machine" over the office, the graph and the timeline,
+ // so somebody on a screen reader was told nothing had changed.
+ var text = await (await GetAsync("/")).Content.ReadAsStringAsync();
+
+ text.Should().Contain("function headingFor(box)");
+ text.Should().Contain("heading.textContent = headingFor(views[which].box);");
+ text.Should().Contain("top.setAttribute(\"tabindex\", \"-1\");");
+
+ // And the address says which view it is, so Back and a bookmark work.
+ text.Should().Contain("history.pushState(null, \"\", \"#\" + views[which].box);");
+ text.Should().Contain("window.addEventListener(\"popstate\"");
+ }
+
+ [Fact]
+ public async Task A_short_view_does_not_push_the_page_down()
+ {
+ // The page is a grid at least a window tall. With nothing saying which
+ // row takes the spare height, a short view - Waiting, Settings - shared
+ // it out, and the header row grew to 192 pixels over 69 of header.
+ var text = await (await GetAsync("/")).Content.ReadAsStringAsync();
+
+ text.Should().Contain("grid-template-rows: auto minmax(0, 1fr);");
+ }
+
[Fact]
public async Task A_row_of_choices_wraps_rather_than_widening_the_pane()
{
@@ -1709,7 +1760,7 @@ public async Task The_page_offers_a_way_to_start_one()
// Folded away, because the page is for watching what is already going
// and a form that is always open is one somebody fills in by accident.
- text.Should().Contain("");
+ text.Should().Contain("");
// "Run a team", not "Start a team". The old heading sat above a form
// asking for a team name, what it was for and a project, and somebody