Files
l5y 86f49506ff web: remediate frontend design & UX audit (#855)
* web: remediate frontend design & UX audit

* web: remediate design UX review follow-up

* web: remediate post-deploy design review (detail view, footer, marker recency)

* web: drop single-pass tag-strip in isReportedField

* web: legend shape key + pressed state + chat overflow
2026-07-25 22:06:59 +02:00

276 lines
10 KiB
Ruby
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# frozen_string_literal: true
# Copyright © 2025-26 l5yth & contributors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
require "spec_helper"
# Server-rendered guards for the frontend design & UX audit remediation
# (SPEC UX1UX15, ACCEPTANCE UX-A1…UX-A11). Every example was written before
# the fix and demonstrated failing against the unfixed tree (Phase 2 of the
# bugfix protocol).
RSpec.describe "UX audit remediation markup" do
let(:app) { Sinatra::Application }
# Fetch a page body via rack-test.
#
# @param path [String] request path.
# @return [String] response body.
def body_of(path)
get path
expect(last_response).to be_ok
last_response.body
end
describe "degenerate-state voice (UX4)" do
it "ships a noscript notice naming the raw API" do
html = body_of("/")
expect(html).to include("<noscript>")
expect(html).to include("/api/nodes")
end
it "server-renders the nodes-table waiting row" do
html = body_of("/")
expect(html).to include("nodes-empty-row")
expect(html).to include("No nodes heard yet")
end
end
describe "legend defaults (UX8)" do
it "expands the legend on the dedicated map view" do
expect(body_of("/map")).to include('data-legend-collapsed="false"')
end
it "keeps the legend collapsed on the dashboard composite" do
expect(body_of("/")).to include('data-legend-collapsed="true"')
end
end
describe "nodes table IA (UX9)" do
it "carries a caption, column scopes, and the grouped header row" do
html = body_of("/nodes")
expect(html).to include("<caption")
expect(html).to include('scope="col"')
expect(html).to include("nodes-group-header")
expect(html).to include(">Identity<")
expect(html).to include(">Health<")
expect(html).to include(">Position<")
end
it "moves units into the battery and voltage headers" do
html = body_of("/nodes")
expect(html).to include("Battery %")
expect(html).to include("Voltage V")
end
it "adds the mobile disclosure column header" do
expect(body_of("/nodes")).to include("nodes-col--more")
end
it "exposes visually hidden section headings on the dashboard (table IA landmarks)" do
html = body_of("/")
expect(html).to match(%r{<h2[^>]*class="[^"]*visually-hidden[^"]*"[^>]*>Chat</h2>})
expect(html).to match(%r{<h2[^>]*class="[^"]*visually-hidden[^"]*"[^>]*>Map</h2>})
expect(html).to match(%r{<h2[^>]*class="[^"]*visually-hidden[^"]*"[^>]*>Nodes</h2>})
end
end
describe "federation table IA (UX9, UX12)" do
before do
allow(PotatoMesh::Config).to receive(:federation_enabled?).and_return(true)
allow_any_instance_of(Sinatra::Application).to receive(:federation_enabled?).and_return(true)
end
it "leads with the traveler columns and says Preset (table IA)" do
html = body_of("/federation")
expect(html).to include("<caption")
expect(html).to include('scope="col"')
name = html.index("instances-col--name")
domain = html.index("instances-col--domain")
preset = html.index(">Preset ")
frequency = html.index("instances-col--frequency")
nodes = html.index("instances-col--nodes")
latitude = html.index("instances-col--latitude")
expect([name, domain, preset, frequency, nodes, latitude]).to all(be_a(Integer))
expect(name).to be < domain
expect(domain).to be < preset
expect(preset).to be < frequency
expect(frequency).to be < nodes
expect(nodes).to be < latitude
end
end
describe "shell economics (UX11)" do
it "renders static pages in the footer, not the navs" do
html = body_of("/")
nav = html[%r{<nav class="site-nav".*?</nav>}m]
mobile_nav = html[%r{<nav class="mobile-nav".*?</nav>}m]
footer = html[%r{<footer.*?</footer>}m]
expect(nav).not_to include("/pages/")
expect(mobile_nav).not_to include("/pages/")
expect(footer).to include("/pages/about")
end
it "drops the protocol icon from the Charts nav links" do
html = body_of("/")
expect(html).not_to match(%r{meshtastic\.svg[^>]*>\s*Charts})
end
it "collapses the region selector behind a compact toggle with an honest option" do
allow(PotatoMesh::Config).to receive(:federation_enabled?).and_return(true)
allow_any_instance_of(Sinatra::Application).to receive(:federation_enabled?).and_return(true)
html = body_of("/")
expect(html).to include("instance-selector-toggle")
expect(html).to include("Other regions…")
expect(html).not_to include("Select region ...")
end
end
describe "join strip & preset config (UX12)" do
it "renders the join-line strip from the resolved Meshtastic preset config" do
html = body_of("/")
expect(html).to include("join-line")
expect(html).to include("Meshtastic")
expect(html).to include("#LongFast")
expect(html).to include("915MHz")
expect(html).not_to include("MeshCore ·")
end
it "adds the MeshCore join line only when both preset config values are set" do
allow(PotatoMesh::Config).to receive(:meshcore_preset).and_return("EU/UK Narrow")
allow(PotatoMesh::Config).to receive(:meshcore_freq).and_return("869MHz")
html = body_of("/")
expect(html).to include("MeshCore")
expect(html).to include("EU/UK Narrow")
expect(html).to include("869MHz")
end
end
describe "join strip moved to the footer (audit follow-up 04)" do
it "renders the join strip in the footer and drops the redundant details link" do
html = body_of("/")
# The strip now carries its footer-placement class and still names the
# resolved preset — it lives in the footer, next to the About link.
expect(html).to include("footer-join")
expect(html).to include("join-line")
expect(html).to include("#LongFast")
# The `details` link (its About-page shortcut) is gone: the footer's own
# About link sits right beside the strip.
expect(html).not_to include("join-line__more")
end
it "labels the two protocol toggles with per-protocol count elements" do
html = body_of("/")
expect(html).to include('id="protocolToggleMeshcoreCount"')
expect(html).to include('id="protocolToggleMeshtasticCount"')
expect(html).to include("protocol-toggle-count")
end
end
describe "footer dot separators (Post-Deploy 02·03)" do
it "renders footer-separator elements as dots, not em dashes" do
html = body_of("/")
# The links row separators become dots; the dangling em-dash that the
# wrapping .footer-links box stranded on line one is gone.
expect(html).to match(%r{<span class="footer-separator"[^>]*>·</span>})
expect(html).not_to match(%r{<span class="footer-separator"[^>]*>—</span>})
end
end
describe "preset config resolution (UX12)" do
it "prefers MESHTASTIC_PRESET/MESHTASTIC_FREQ over the deprecated pair (preset config)" do
within_env(
"MESHTASTIC_PRESET" => "MediumFast",
"MESHTASTIC_FREQ" => "869MHz",
"CHANNEL" => "#Legacy",
"FREQUENCY" => "433MHz",
) do
expect(PotatoMesh::Config.meshtastic_preset).to eq("MediumFast")
expect(PotatoMesh::Config.meshtastic_freq).to eq("869MHz")
end
end
it "falls back to the deprecated CHANNEL/FREQUENCY pair (preset config)" do
within_env(
"MESHTASTIC_PRESET" => nil,
"MESHTASTIC_FREQ" => nil,
"CHANNEL" => "#Legacy",
"FREQUENCY" => "433MHz",
) do
expect(PotatoMesh::Config.meshtastic_preset).to eq("#Legacy")
expect(PotatoMesh::Config.meshtastic_freq).to eq("433MHz")
end
end
it "defaults to #LongFast/915MHz when nothing is configured (preset config)" do
within_env(
"MESHTASTIC_PRESET" => nil,
"MESHTASTIC_FREQ" => nil,
"CHANNEL" => nil,
"FREQUENCY" => nil,
) do
expect(PotatoMesh::Config.meshtastic_preset).to eq("#LongFast")
expect(PotatoMesh::Config.meshtastic_freq).to eq("915MHz")
end
end
it "hides MeshCore until both values are configured (preset config)" do
within_env("MESHCORE_PRESET" => "EU/UK Narrow", "MESHCORE_FREQ" => nil) do
expect(PotatoMesh::Config.meshcore_preset).to eq("EU/UK Narrow")
expect(PotatoMesh::Config.meshcore_freq).to be_nil
expect(PotatoMesh::Config.meshcore_join_configured?).to be(false)
end
within_env("MESHCORE_PRESET" => "EU/UK Narrow", "MESHCORE_FREQ" => "869MHz") do
expect(PotatoMesh::Config.meshcore_join_configured?).to be(true)
end
end
end
describe "keyboard map equivalence (UX14)" do
it "describes every map region with the accessible-equivalent note" do
%w[/ /map].each do |path|
html = body_of(path)
expect(html).to include('aria-describedby="mapAccessNote"')
expect(html).to include("nodes table")
end
end
end
# Temporarily override environment variables for one example.
#
# @param values [Hash{String => String, nil}] variables to set (nil deletes).
# @yield the block executed under the modified environment.
# @return [void]
def within_env(values)
original = {}
values.each do |key, value|
original[key] = ENV.key?(key) ? ENV[key] : :__unset__
if value.nil?
ENV.delete(key)
else
ENV[key] = value
end
end
yield
ensure
original.each do |key, value|
if value == :__unset__
ENV.delete(key)
else
ENV[key] = value
end
end
end
end