Wskaźnik offline: ramka zamiast belki + naprawa cyklicznego migania
Dwa zgłoszenia z kiosku (2026-07-31): 1. BELKA ZASŁANIAŁA DANE ALS. Pełnoekranowy pasek u góry zajmował pas ekranu i obcinał dane informacyjne w iframe. Zastąpiony cienką pulsującą RAMKĄ wokół całego ekranu (pointer-events: none, niczego nie zakrywa) plus małą plakietką w rogu. Z daleka widoczna równie dobrze. 2. WSKAŹNIK MIGAŁ zamiast świecić ciągle. disconnectedSince było resetowane przy KAŻDEJ nieudanej próbie reconnectu, nie tylko przy przejściu połączone->rozłączone — przy odstępie ponowień > 10 s (backoff dochodzi do 30 s) licznik ciszy zerował się co próbę i wskaźnik cyklicznie znikał i wracał. Teraz znacznik początku przerwy ustawiany wyłącznie przy przejściu ze stanu połączonego. Kontekst zgłoszenia: strona działa jeszcze przeciw STAREMU LuPortowi, który nie ma kanału zbiorczego ani uwierzytelniania WS — połączenie pada ciągle (oczekiwane do czasu wdrożenia; potem wstaje bez zmian), a błąd zamieniał tę ciągłą przerwę w miganie. Testy: scenariusz 7 próbkuje wskaźnik co 500 ms przez 90 s ciągłej awarii i wymaga 100% aktywnych próbek — z błędem aktywnych było 51/182 (72% czasu zgaszony; dokładnie zgłoszone "pojawia się i znika"). Fałszywy WebSocket odrzuca teraz połączenia jak przeglądarka (onclose po nieudanej próbie), więc długa przerwa z ponowieniami jest testowalna. 11/11 scenariuszy PASS, mutacja poprawki wywraca scenariusz 7. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,13 +18,15 @@ także po każdym wznowieniu połączenia.
|
|||||||
|----------|-------|
|
|----------|-------|
|
||||||
| Brak maszyny w alarmie | Iframe z infoterminalem ALS |
|
| Brak maszyny w alarmie | Iframe z infoterminalem ALS |
|
||||||
| Co najmniej jedna maszyna w alarmie | Ekran alarmowy z kartami maszyn |
|
| Co najmniej jedna maszyna w alarmie | Ekran alarmowy z kartami maszyn |
|
||||||
| Brak łączności z LuPort > 10 s | Pomarańczowy pasek u góry (na obu widokach) |
|
| Brak łączności z LuPort > 10 s | Pomarańczowa pulsująca ramka wokół ekranu + plakietka w rogu (na obu widokach) |
|
||||||
|
|
||||||
Alarm jest wykrywany, gdy pole `status` maszyny zawiera słowo **`alarm`**
|
Alarm jest wykrywany, gdy pole `status` maszyny zawiera słowo **`alarm`**
|
||||||
(bez rozróżnienia wielkości liter), np. `"Alarm - awaria"`, `"ALARM"`.
|
(bez rozróżnienia wielkości liter), np. `"Alarm - awaria"`, `"ALARM"`.
|
||||||
|
|
||||||
Pomarańczowy pasek to celowo INNY kolor niż czerwień alarmów: mówi „ekran może
|
Wskaźnik offline to cienka pulsująca RAMKA wokół całego ekranu plus mała
|
||||||
pokazywać nieświeży obraz", a nie „maszyna stoi".
|
plakietka w rogu — niczego nie zasłania (pełnoekranowa belka zakrywała dane
|
||||||
|
ALS w iframe). Pomarańcz to celowo INNY kolor niż czerwień alarmów: mówi
|
||||||
|
„ekran może pokazywać nieświeży obraz", a nie „maszyna stoi".
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -39,7 +41,7 @@ Wszystkie parametry na początku sekcji `<script>` w `als_alert_screen.html`:
|
|||||||
| `MACHINES` | filtr maszyn pokazywanych na ekranie; `[]` = wszystkie |
|
| `MACHINES` | filtr maszyn pokazywanych na ekranie; `[]` = wszystkie |
|
||||||
| `WATCHDOG_SILENCE_MS` | po jakiej ciszy uznać połączenie za martwe (90 s) |
|
| `WATCHDOG_SILENCE_MS` | po jakiej ciszy uznać połączenie za martwe (90 s) |
|
||||||
| `STALE_MS` | po jakim czasie dane maszyny znikają z ekranu (5 min) |
|
| `STALE_MS` | po jakim czasie dane maszyny znikają z ekranu (5 min) |
|
||||||
| `OFFLINE_BANNER_AFTER_MS` | po jakim czasie bez łączności pokazać pasek (10 s) |
|
| `OFFLINE_BANNER_AFTER_MS` | po jakim czasie bez łączności pokazać ramkę offline (10 s) |
|
||||||
|
|
||||||
Oba kluczowe parametry można też nadpisać w adresie strony — wygodne przy
|
Oba kluczowe parametry można też nadpisać w adresie strony — wygodne przy
|
||||||
konfiguracji kiosku bez edycji pliku:
|
konfiguracji kiosku bez edycji pliku:
|
||||||
@@ -85,8 +87,16 @@ Strona obsługuje dwa typy ramek — obydwa tak samo (liczy się najnowszy stan)
|
|||||||
połączenie i wymusza reconnect.
|
połączenie i wymusza reconnect.
|
||||||
3. **Wygasanie danych.** Dane maszyny starsze niż 5 min znikają z ekranu —
|
3. **Wygasanie danych.** Dane maszyny starsze niż 5 min znikają z ekranu —
|
||||||
alarm maszyny usuniętej z widgetu ALS nie wisi w nieskończoność.
|
alarm maszyny usuniętej z widgetu ALS nie wisi w nieskończoność.
|
||||||
4. **Widoczna utrata łączności.** Pasek offline + pasek statusu w rogu.
|
4. **Widoczna utrata łączności.** Pulsująca ramka offline świeci CIĄGLE
|
||||||
Ekran nigdy nie pokazuje po cichu nieświeżego obrazu.
|
przez całą przerwę (znacznik początku przerwy ustawiany tylko przy
|
||||||
|
przejściu połączone→rozłączone — reset przy każdej nieudanej próbie
|
||||||
|
powodował cykliczne miganie). Ekran nigdy nie pokazuje po cichu
|
||||||
|
nieświeżego obrazu.
|
||||||
|
|
||||||
|
Uwaga: do czasu wdrożenia NOWEGO LuPorta ramka będzie świecić stale —
|
||||||
|
stary system nie ma kanału zbiorczego ani uwierzytelniania WS, więc nowa
|
||||||
|
strona nie ma się z czym połączyć. To oczekiwane; po przełączeniu
|
||||||
|
serwera na nowy system strona wstaje bez żadnych zmian.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -134,9 +144,10 @@ CHROME_BIN=/usr/bin/chromium python3 test/testy_ekranu.py
|
|||||||
|
|
||||||
Deterministyczne, bez sieci: skrypt aplikacji jest wycinany z żywego
|
Deterministyczne, bez sieci: skrypt aplikacji jest wycinany z żywego
|
||||||
`als_alert_screen.html`, `window.WebSocket` podmieniany na fałszywkę,
|
`als_alert_screen.html`, `window.WebSocket` podmieniany na fałszywkę,
|
||||||
a scenariusz (10 asercji: migawka, aktualizacje, reconnect, baner offline,
|
a scenariusz (11 asercji: migawka, aktualizacje, reconnect, ciągłość
|
||||||
watchdog, wygasanie danych) jedzie na wirtualnym czasie Chromium — całość
|
wskaźnika offline podczas długiej przerwy, watchdog, wygasanie danych)
|
||||||
trwa kilka sekund mimo symulowania ~6 minut.
|
jedzie na wirtualnym czasie Chromium — całość trwa kilkanaście sekund mimo
|
||||||
|
symulowania ~8 minut.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+54
-32
@@ -145,43 +145,56 @@
|
|||||||
text-shadow: 0 0 24px rgba(255, 0, 0, 0.6);
|
text-shadow: 0 0 24px rgba(255, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── OFFLINE BANNER ──────────────────────────── */
|
/* ── OFFLINE INDICATOR ───────────────────────── */
|
||||||
/* Widoczna z daleka informacja o utracie łączności z LuPort.
|
/* Widoczna z daleka informacja o utracie łączności z LuPort — CIENKA
|
||||||
POMARAŃCZOWA, nie czerwona — czerwień jest zarezerwowana dla alarmów
|
pulsująca RAMKA wokół całego ekranu plus mała plakietka w rogu.
|
||||||
maszyn; ten pasek mówi „ekran może pokazywać nieświeży obraz",
|
Wcześniejsza pełnoekranowa belka u góry zasłaniała dane ALS
|
||||||
a nie „maszyna stoi". Poprzednia wersja tej strony traciła połączenia
|
w iframe (zgłoszone 2026-07-31) — ramka niczego nie zakrywa,
|
||||||
PO CICHU (patrz README, sekcja Historia) — ten pasek jest wprost
|
a z daleka widać ją równie dobrze. POMARAŃCZOWA, nie czerwona:
|
||||||
wnioskiem z tamtej awarii: utrata łączności ma być widoczna z hali,
|
czerwień jest zarezerwowana dla alarmów maszyn; ramka mówi „ekran
|
||||||
a nie odkrywana po fakcie. */
|
może pokazywać nieświeży obraz", a nie „maszyna stoi". Utrata
|
||||||
#offline-banner {
|
łączności MA być widoczna z hali — poprzednia wersja strony traciła
|
||||||
|
połączenia po cichu (README, Historia). */
|
||||||
|
#offline-indicator {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
z-index: 9998;
|
|
||||||
background: #7a4a00;
|
|
||||||
color: #ffd9a0;
|
|
||||||
border-bottom: 3px solid #ffaa33;
|
|
||||||
padding: 10px 24px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: clamp(16px, 2vw, 28px);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
}
|
||||||
#offline-banner.active {
|
#offline-indicator.active {
|
||||||
display: block;
|
display: block;
|
||||||
animation: offlineBlink 2s step-start infinite;
|
|
||||||
}
|
}
|
||||||
@keyframes offlineBlink {
|
#offline-frame {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
border: 6px solid #ffaa33;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9998;
|
||||||
|
animation: offlinePulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes offlinePulse {
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
background: #7a4a00;
|
border-color: #ffaa33;
|
||||||
|
box-shadow: inset 0 0 20px rgba(255, 170, 51, 0.25);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
background: #9a5e00;
|
border-color: #7a4a00;
|
||||||
|
box-shadow: inset 0 0 6px rgba(255, 170, 51, 0.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#offline-chip {
|
||||||
|
position: fixed;
|
||||||
|
top: 10px;
|
||||||
|
right: 12px;
|
||||||
|
background: #7a4a00;
|
||||||
|
color: #ffd9a0;
|
||||||
|
border: 1px solid #ffaa33;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── STATUS BAR ──────────────────────────────── */
|
/* ── STATUS BAR ──────────────────────────────── */
|
||||||
#status-bar {
|
#status-bar {
|
||||||
@@ -222,7 +235,10 @@
|
|||||||
<div id="machines-grid"></div>
|
<div id="machines-grid"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="offline-banner">⚠ BRAK POŁĄCZENIA Z LUPORT — DANE MOGĄ BYĆ NIEAKTUALNE ⚠</div>
|
<div id="offline-indicator">
|
||||||
|
<div id="offline-frame"></div>
|
||||||
|
<div id="offline-chip">⚠ OFFLINE — dane mogą być nieaktualne</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Ten element MUSI istnieć w DOM. Poprzednia wersja miała go
|
<!-- Ten element MUSI istnieć w DOM. Poprzednia wersja miała go
|
||||||
zakomentowanego, a refreshStatus() dalej po niego sięgał — TypeError
|
zakomentowanego, a refreshStatus() dalej po niego sięgał — TypeError
|
||||||
@@ -369,8 +385,14 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
|
// Znacznik początku przerwy ustawiany WYŁĄCZNIE przy przejściu
|
||||||
|
// połączone->rozłączone. Reset przy każdej NIEUDANEJ próbie
|
||||||
|
// reconnectu (jak wcześniej) zerował licznik ciszy co próbę i przy
|
||||||
|
// odstępie ponowień > 10 s wskaźnik offline cyklicznie znikał
|
||||||
|
// i wracał (zgłoszone 2026-07-31: „pasek pojawia się i znika") —
|
||||||
|
// zamiast świecić ciągle przez całą przerwę.
|
||||||
|
if (wsConnected) disconnectedSince = Date.now();
|
||||||
wsConnected = false;
|
wsConnected = false;
|
||||||
disconnectedSince = Date.now();
|
|
||||||
clearTimeout(stableTimer);
|
clearTimeout(stableTimer);
|
||||||
refreshStatus();
|
refreshStatus();
|
||||||
reconnectTimer = setTimeout(connect, nextReconnectDelay());
|
reconnectTimer = setTimeout(connect, nextReconnectDelay());
|
||||||
@@ -482,11 +504,11 @@
|
|||||||
: "WS: rozłączono — ponawiam…";
|
: "WS: rozłączono — ponawiam…";
|
||||||
}
|
}
|
||||||
|
|
||||||
const banner = document.getElementById("offline-banner");
|
const indicator = document.getElementById("offline-indicator");
|
||||||
if (banner) {
|
if (indicator) {
|
||||||
const offlineLongEnough =
|
const offlineLongEnough =
|
||||||
!wsConnected && Date.now() - disconnectedSince > OFFLINE_BANNER_AFTER_MS;
|
!wsConnected && Date.now() - disconnectedSince > OFFLINE_BANNER_AFTER_MS;
|
||||||
banner.classList.toggle("active", offlineLongEnough && !DEBUG_ALARM);
|
indicator.classList.toggle("active", offlineLongEnough && !DEBUG_ALARM);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
+44
-17
@@ -40,8 +40,9 @@ function assert(nazwa, warunek) {
|
|||||||
}
|
}
|
||||||
const alarmActive = () => document.getElementById("alarm-view").classList.contains("active");
|
const alarmActive = () => document.getElementById("alarm-view").classList.contains("active");
|
||||||
const cards = () => [...document.querySelectorAll(".card-machine-id")].map(e => e.textContent);
|
const cards = () => [...document.querySelectorAll(".card-machine-id")].map(e => e.textContent);
|
||||||
const bannerActive = () => document.getElementById("offline-banner").classList.contains("active");
|
const offline = () => document.getElementById("offline-indicator").classList.contains("active");
|
||||||
const inst = () => FakeWebSocket.instances;
|
const inst = () => FakeWebSocket.instances;
|
||||||
|
const otwarty = () => inst().find(w => w.readyState === 1);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
assert("1. polaczenie nawiazane", inst().length === 1 && inst()[0].readyState === 1);
|
assert("1. polaczenie nawiazane", inst().length === 1 && inst()[0].readyState === 1);
|
||||||
@@ -67,46 +68,72 @@ setTimeout(() => {
|
|||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
assert("5. maszyna spoza filtra ignorowana", !alarmActive());
|
assert("5. maszyna spoza filtra ignorowana", !alarmActive());
|
||||||
|
// serwer pada NA DLUGO: kazda kolejna proba polaczenia jest odrzucana
|
||||||
FakeWebSocket.autoOpen = false;
|
FakeWebSocket.autoOpen = false;
|
||||||
inst()[0]._serverClose();
|
inst()[0]._serverClose();
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
||||||
// KLUCZOWY test regresji: po zamknięciu MUSI powstać nowe połączenie.
|
// KLUCZOWY test regresji starego bledu: po zamknieciu MUSI powstac nowa proba.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
assert("6. reconnect zaplanowany i wykonany", inst().length >= 2);
|
assert("6. reconnect zaplanowany i wykonany", inst().length >= 2);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
|
// ANTY-MIGANIE (regresja 2026-07-31): przy DLUGIEJ przerwie wskaznik ma
|
||||||
|
// swiecic CIAGLE. Blad resetowal disconnectedSince przy kazdej nieudanej
|
||||||
|
// probie, wiec przy odstepie ponowien > 10 s wskaznik cyklicznie znikal.
|
||||||
|
// Probkujemy co 500 ms przez 90 s ciaglej awarii — z bledem ~30% probek
|
||||||
|
// bylo "zgaszonych", po poprawce wszystkie musza byc aktywne.
|
||||||
|
window.__probki = { aktywne: 0, wszystkie: 0 };
|
||||||
|
const probkowanie = setInterval(() => {
|
||||||
|
window.__probki.wszystkie++;
|
||||||
|
if (offline()) window.__probki.aktywne++;
|
||||||
|
}, 500);
|
||||||
|
setTimeout(() => { window.__probki.aktywne = 0; window.__probki.wszystkie = 0; }, 20000);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
assert("7. baner offline widoczny po 10 s", bannerActive());
|
clearInterval(probkowanie);
|
||||||
|
const p = window.__probki;
|
||||||
|
assert("7. wskaznik offline swieci CIAGLE przez cala przerwe (" +
|
||||||
|
p.aktywne + "/" + p.wszystkie + " probek)", p.wszystkie > 100 && p.aktywne === p.wszystkie);
|
||||||
|
// serwer wraca — najblizsza zaplanowana proba wstanie
|
||||||
FakeWebSocket.autoOpen = true;
|
FakeWebSocket.autoOpen = true;
|
||||||
inst()[inst().length-1]._open();
|
}, 111000);
|
||||||
inst()[inst().length-1]._message({type:"machine_data", machine:"4.09", data:{status:"Alarm - test"}});
|
|
||||||
}, 16000);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
assert("8. po powrocie: baner znika, alarm 4.09", !bannerActive() && cards().join()==="4.09");
|
assert("8. po powrocie serwera: polaczono, wskaznik zgaszony", !!otwarty() && !offline());
|
||||||
}, 17000);
|
otwarty() && otwarty()._message({type:"machine_data", machine:"4.09", data:{status:"Alarm - test"}});
|
||||||
|
}, 155000);
|
||||||
setTimeout(() => { window.__przedWatchdog = inst().length; }, 18000);
|
|
||||||
setTimeout(() => {
|
|
||||||
assert("9. watchdog: cisza 100s wymusza nowe polaczenie", inst().length > window.__przedWatchdog);
|
|
||||||
}, 125000);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
assert("10. przeterminowany alarm znika z ekranu", !alarmActive());
|
assert("9. alarm po powrocie widoczny", alarmActive() && cards().join()==="4.09");
|
||||||
|
window.__przedWatchdog = inst().length;
|
||||||
|
}, 156000);
|
||||||
|
|
||||||
|
// cisza > 90 s -> watchdog ubija polaczenie, reconnect wstaje od reki
|
||||||
|
setTimeout(() => {
|
||||||
|
assert("10. watchdog: cisza wymusza nowe polaczenie", inst().length > window.__przedWatchdog);
|
||||||
|
}, 270000);
|
||||||
|
|
||||||
|
// dane 4.09 z t=155 s bez odswiezenia -> po 5 min znikaja z ekranu
|
||||||
|
setTimeout(() => {
|
||||||
|
assert("11. przeterminowany alarm znika z ekranu", !alarmActive());
|
||||||
document.getElementById("wynik").textContent += "\\nKONIEC";
|
document.getElementById("wynik").textContent += "\\nKONIEC";
|
||||||
}, 340000);
|
}, 470000);
|
||||||
"""
|
"""
|
||||||
|
|
||||||
FAKE_WS = """
|
FAKE_WS = """
|
||||||
class FakeWebSocket {
|
class FakeWebSocket {
|
||||||
static instances = [];
|
static instances = [];
|
||||||
|
// true = polaczenie wstaje (serwer dziala)
|
||||||
|
// false = polaczenie ODRZUCANE po chwili (serwer lezy / zly adres) —
|
||||||
|
// przegladarka w tej sytuacji wola onclose, wiec fake tez musi,
|
||||||
|
// inaczej nie da sie przetestowac dlugiej przerwy z ponowieniami.
|
||||||
static autoOpen = true;
|
static autoOpen = true;
|
||||||
constructor(url) {
|
constructor(url) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.readyState = 0;
|
this.readyState = 0;
|
||||||
FakeWebSocket.instances.push(this);
|
FakeWebSocket.instances.push(this);
|
||||||
if (FakeWebSocket.autoOpen) setTimeout(() => this._open(), 5);
|
if (FakeWebSocket.autoOpen) setTimeout(() => this._open(), 5);
|
||||||
|
else setTimeout(() => this._serverClose(), 5);
|
||||||
}
|
}
|
||||||
_open() {
|
_open() {
|
||||||
if (this.readyState !== 0) return;
|
if (this.readyState !== 0) return;
|
||||||
@@ -141,7 +168,7 @@ def zbuduj_harness() -> str:
|
|||||||
markup = re.sub(r'src="http://[^"]*"', 'src="about:blank"', markup)
|
markup = re.sub(r'src="http://[^"]*"', 'src="about:blank"', markup)
|
||||||
return f"""<!doctype html><html><head><meta charset="utf-8"><style>
|
return f"""<!doctype html><html><head><meta charset="utf-8"><style>
|
||||||
#alarm-view{{display:none}} #alarm-view.active{{display:flex}}
|
#alarm-view{{display:none}} #alarm-view.active{{display:flex}}
|
||||||
#offline-banner{{display:none}} #offline-banner.active{{display:block}}
|
#offline-indicator{{display:none}} #offline-indicator.active{{display:block}}
|
||||||
</style></head><body>
|
</style></head><body>
|
||||||
{markup}
|
{markup}
|
||||||
<pre id="wynik"></pre>
|
<pre id="wynik"></pre>
|
||||||
@@ -175,7 +202,7 @@ def main() -> int:
|
|||||||
"--headless",
|
"--headless",
|
||||||
"--disable-gpu",
|
"--disable-gpu",
|
||||||
"--no-sandbox",
|
"--no-sandbox",
|
||||||
"--virtual-time-budget=350000",
|
"--virtual-time-budget=480000",
|
||||||
"--dump-dom",
|
"--dump-dom",
|
||||||
f"file://{harness}",
|
f"file://{harness}",
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user