#!/bin/bash # Drive the wizard's Storage step in a real browser. # # scripts/dev/lp-storage-custom-test # needs a running WebUI # # Two things this guards. # # A location whose drive is absent used to render as a warn card — "needs care", # "free of" with no numbers, an empty meter. A new user has no way to read that # as "this is a drive you registered and it is unplugged", so it looked like the # scan had found two broken disks. It must now say it is not connected, name the # path, and draw no meter. # # And a typed custom path must never reach the payload half-typed. The apply # side refuses a relative or system path and falls back to the system disk — # a fallback that looks exactly like having chosen the system disk on purpose, # which is the failure shape this project keeps having to fix. validateStep # has to block it at the step instead. # # One page load: the whole interaction runs in a single `lp-shot --eval`. REPO="$(cd "$(dirname "$0")/../.." && pwd)" SHOT="$REPO/scripts/dev/lp-shot" fail=0 chk(){ if [[ "$2" == "$3" ]]; then echo " ok $1"; else echo " FAIL $1: got '$2' want '$3'"; fail=1; fi; } command -v jq >/dev/null 2>&1 || { echo " SKIP jq not installed"; exit 0; } # A skip must not be able to stand in for a pass. If the WebUI answers HTTP # then the browser is the only thing that can have failed, and that is a # failure — skipping there is how this test reported success under sudo, where # chromium refuses to start at all. lp_reachable() { local u; u=$("$SHOT" --url 2>/dev/null) || return 1 [[ -n "$u" ]] || return 1 curl -fsS -o /dev/null --max-time 5 "$u" 2>/dev/null } read -r -d '' DRIVE <<'JS' const out = {}; const w = window.setupWizard; if (!w) return JSON.stringify({ error: 'wizard handle missing' }); const fire = (el, ev) => el.dispatchEvent(new Event(ev, { bubbles: true })); const sel = document.querySelector('#sw-storage-apps'); const inp = document.querySelector('#sw-storage-apps-custom'); const row = document.querySelector('#sw-storage-apps-custom-row'); const err = document.querySelector('#sw-storage-apps-custom-err'); if (!sel || !inp || !row) return JSON.stringify({ error: 'storage step not rendered' }); // An unmounted location must not be offered as somewhere to put app data: // the wizard cannot check its free space or write to it. // How many SHOULD be offline comes from the feed, not from the rendering. // Counting the cards that say "not connected" would make every assertion // below vacuous the moment the feature stopped working. const cards = Array.from(document.querySelectorAll('.setup-app-card, .setup-storage-card')); const expected = (w.storageCandidates || []).filter(c => c.state === 'unmounted'); out.expectedOffline = expected.length; const offline = cards.filter(c => expected.some(e => e.path && c.textContent.includes(e.path))); out.offlineCards = offline.length; out.offlineSayNotConnected = offline.length > 0 && offline.every(c => /not connected/i.test(c.textContent)); out.offlineSaysNotMounted = offline.length > 0 && offline.every(c => /not mounted right now/i.test(c.textContent)); out.offlineNamesPath = offline.length > 0 && offline.every(c => /·\s*\//.test(c.textContent)); out.offlineHasNoMeter = offline.length > 0 && offline.every(c => !c.querySelector('.setup-storage-meter')); out.offlineSaysNeedsCare = offline.some(c => /needs care/i.test(c.textContent)); out.offlineOffered = Array.from(sel.options) .some(o => offline.some(c => o.value && c.textContent.includes(o.value))); out.hasCustomOption = Array.from(sel.options).some(o => o.value === '__custom__'); out.rowHiddenAtRest = row.style.display === 'none'; sel.value = '__custom__'; fire(sel, 'change'); out.rowShownOnPick = row.style.display !== 'none'; out.emptyBlocks = !!w.validateStep(3); const probe = (v) => { inp.value = v; fire(inp, 'input'); return { blocks: !!w.validateStep(3), marked: inp.classList.contains('is-invalid'), said: (err && err.textContent) || '' }; }; out.relative = probe('mnt/nas'); out.systemDir = probe('/etc/libreportal'); out.root = probe('/'); out.spaces = probe('/mnt/my disk'); out.good = probe('/mnt/nas/apps'); out.goodStored = w.storageDefault; // Back to a scanned option: never blocked, and the error clears. sel.value = 'primary'; fire(sel, 'change'); out.primaryBlocks = !!w.validateStep(3); out.primaryStored = w.storageDefault; return JSON.stringify(out); JS J=$("$SHOT" --eval "/?step=3" "$DRIVE" 2>/dev/null | tr "'" '"' | sed 's/\bTrue\b/true/g; s/\bFalse\b/false/g') if [[ -z "$J" ]]; then if lp_reachable; then echo " FAIL the WebUI is up but the page returned nothing (browser failed?)"; exit 1 fi echo " SKIP no WebUI reachable"; exit 0 fi g(){ echo "$J" | jq -r "$1" 2>/dev/null; } if [[ "$(g '.error // empty')" != "" ]]; then echo " FAIL $(g .error)"; exit 1; fi echo "unmounted locations" if [[ "$(g .expectedOffline)" == "0" ]]; then echo " SKIP no unmounted location registered — nothing to check" else chk "every one has a card" "$(g .offlineCards)" "$(g .expectedOffline)" chk "badged 'not connected'" "$(g .offlineSayNotConnected)" true chk "say they are not mounted" "$(g .offlineSaysNotMounted)" true chk "name the path" "$(g .offlineNamesPath)" true chk "draw no free-space meter" "$(g .offlineHasNoMeter)" true chk "do not say 'needs care'" "$(g .offlineSaysNeedsCare)" false chk "are not offered as a target" "$(g .offlineOffered)" false fi echo "custom path" chk "the option exists" "$(g .hasCustomOption)" true chk "input hidden until picked" "$(g .rowHiddenAtRest)" true chk "input shown once picked" "$(g .rowShownOnPick)" true chk "empty blocks the step" "$(g .emptyBlocks)" true chk "relative path blocks" "$(g .relative.blocks)" true chk "relative path is marked" "$(g .relative.marked)" true chk "relative path says why" "$(g '.relative.said != ""')" true chk "system dir blocks" "$(g .systemDir.blocks)" true chk "/ blocks" "$(g .root.blocks)" true chk "path with spaces blocks" "$(g .spaces.blocks)" true chk "an absolute path passes" "$(g .good.blocks)" false chk "and is not marked" "$(g .good.marked)" false chk "and its error is cleared" "$(g '.good.said == ""')" true chk "and it is what gets stored" "$(g .goodStored)" /mnt/nas/apps chk "a scanned option never blocks" "$(g .primaryBlocks)" false chk "and replaces the custom value" "$(g .primaryStored)" primary [[ $fail -eq 0 ]] && echo "storage custom-path test: OK" exit $fail