T6.5 - A7: cards say their state, the footer says what it is showing
A7's "do not" is louder than its "do", so that first: admin.js:484-517 handles
language and time, it is a shipped feature, the review specifically endorsed
keeping it, and if the proposal reads as removing it that reading is wrong. It
is UNTOUCHED. The probe checks that by diffing the file against HEAD as well as
by driving the feature — switching locale, saving, reloading, and confirming the
saved value came back from the server.
Card status lines
Two things were doing one job badly. The SOP card said "SOP complete" when it
was and NOTHING when it was not — so the commonest state on a live project was
the one with no status line at all. And the Work Package card carried its state
in its BUTTON ("Complete SOP first", "Checking..."), which is a button
describing a situation instead of naming what pressing it does.
Now every card says its state in its own line, in all three states, and no
button changes text to report one:
complete green, the canonical success token
not yet secondary text - a real answer, and neither green nor a warning
unknown the suite's amber, and it names the failure
Each carries a glyph and a word as well as a colour. The line is replaced in
place rather than removed and re-added, because a card that briefly has no
status line reads as "no status" and that is one of the three real answers.
role="status" on it: the text is written by a fetch that lands after the page
has settled, which is what aria-live is for (S10).
Footer
Was "Work Package Suite v1.0 | Prime Controls - Business Technology Group |
Pilot Use Only", which leaves three questions open: v1.0 of what, who Business
Technology Group is to this page, and what Pilot Use Only actually restricts.
Now two sentences. The first names the product and who maintains it. The
second says what "pilot" means in the only terms that matter to somebody about
to type a real work package into it: the work is real and is kept, the tools
around it are still changing. The bare version string is gone rather than left
claiming to be a version of something unspecified.
html/index.html three-state card status, the footer
tests/cards_check.py new - 44 checks
tests/aggregates_check.py two assertions re-pointed (see below)
Done when
[x] card status lines read clearly and use the canonical status colours
[x] the footer is unambiguous about what it is showing
[x] localization still functions - verified by switching language, saving,
reloading and reading the value back off the server
[x] admin.js:484-517 behaviour is unchanged - and the file is byte-identical
Two probes needed re-pointing, and both were asserting wording rather than
behaviour
aggregates_check waited for the card's status line to be non-empty and then
matched the phrase "SOP complete". The wait is now wrong for a second reason:
the line is non-empty from the moment the page loads, because it says
"Checking the SOP...". It waits for the answer instead, and matches the
ANSWER rather than the sentence - which is what that check was ever about,
since it exists to prove the answer came from the server and not the cache.
A probe that breaks when wording changes is a probe that will be edited
carelessly the next time wording changes. Both are now written so that only a
behaviour change can fail them.
Verified one at a time
cards_check 44/44 new
aggregates 16/16 (two assertions re-pointed)
browser_check 71/71
a11y 22/22
launcher 58/58
f_items F1-F5 FIXED, F6 REPRODUCES (T7.2)
No colour literal added: the three status colours are --cds-support-success,
--cds-text-secondary and --wp-status-warning-text, all already in
theme-light.css.
Question for the PR, per CLAUDE.md: the footer now says work packages created in
the pilot are kept. That is true of the database and it is the thing people
actually want to know, but it is a promise, and whoever owns the pilot should
confirm it is one we are making.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -129,6 +129,14 @@
|
||||
is still said, twice — the card's left border turns green and .card-status
|
||||
appears — which is green doing its job as a status rather than as an
|
||||
action colour. */
|
||||
/* A7 / T6.5. Every card says its state, in all THREE states — the version
|
||||
before this said "complete" when it was and nothing when it was not, so
|
||||
the most common state on a live project was the one with no line at all.
|
||||
|
||||
Each state carries a glyph and a word as well as a colour, and the three
|
||||
colours are the canonical status tokens: green complete, the suite's amber
|
||||
for unknown, and secondary text for in-progress. Nothing is declared
|
||||
here that theme-light.css does not already hold. */
|
||||
.card-status {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
@@ -136,6 +144,8 @@
|
||||
color: var(--cds-support-success);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
/* Not finished yet — a real answer, and neither green nor a warning. */
|
||||
.card-status.card-status-pending { color: var(--cds-text-secondary); }
|
||||
/* B4: an unreachable server is a third state, and it has to look like neither
|
||||
of the other two. Not green, not silence. */
|
||||
.card-status.card-status-error { color: var(--wp-status-warning-text); }
|
||||
@@ -188,6 +198,19 @@
|
||||
|
||||
.footer a:hover { text-decoration: underline; }
|
||||
|
||||
.footer p + p { margin-top: 0.4rem; }
|
||||
/* "Pilot" is the word that changes what the rest of the sentence means, so it
|
||||
is marked rather than left to be read past. */
|
||||
.footer-tag {
|
||||
display: inline-block;
|
||||
background: var(--wp-status-warning-bg);
|
||||
color: var(--wp-status-warning-text);
|
||||
border: 1px solid var(--cds-support-warning);
|
||||
padding: 0 6px;
|
||||
margin-right: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* COMMENTS SECTION */
|
||||
.comments-section {
|
||||
background: var(--cds-layer);
|
||||
@@ -542,9 +565,16 @@
|
||||
|
||||
</div>
|
||||
|
||||
<!-- FOOTER -->
|
||||
<!-- FOOTER — A7 / T6.5
|
||||
Was "Work Package Suite v1.0 | Prime Controls - Business Technology Group |
|
||||
Pilot Use Only", which left three questions open: v1.0 of what, who is
|
||||
Business Technology Group to this page, and what does Pilot Use Only
|
||||
restrict. Each line now says one thing and says whose it is. -->
|
||||
<footer class="footer">
|
||||
<p>Work Package Suite v1.0 | Prime Controls - Business Technology Group | Pilot Use Only</p>
|
||||
<p><strong>Work Package Suite</strong> — built and maintained by the Prime Controls
|
||||
Business Technology Group.</p>
|
||||
<p><span class="footer-tag">Pilot</span> This is a pilot release. Work packages created
|
||||
here are real and are kept, but the tools around them are still changing.</p>
|
||||
</footer>
|
||||
|
||||
<script src="feedback-config.js"></script>
|
||||
@@ -883,45 +913,74 @@
|
||||
// reset (re-render can run multiple times)
|
||||
sopCard.classList.remove('complete');
|
||||
wpCard && wpCard.classList.remove('disabled');
|
||||
const oldStatus = sopCard.querySelector('.card-status'); if(oldStatus) oldStatus.remove();
|
||||
|
||||
const setStatus = (text, cls) => {
|
||||
const el = document.createElement('div');
|
||||
// One status line per card, replaced in place rather than removed and
|
||||
// re-added — a card that briefly has no status line reads as "no status",
|
||||
// which is one of the three real answers and must not be shown by accident.
|
||||
//
|
||||
// role="status" so the change is announced: the line is written by a fetch
|
||||
// that lands after the page has settled, which is exactly the case
|
||||
// aria-live exists for (S10 / T4.5). Polite, because a project's SOP being
|
||||
// incomplete is information, not an interruption.
|
||||
const cardStatus = (card, text, cls) => {
|
||||
if(!card) return null;
|
||||
let el = card.querySelector('.card-status');
|
||||
if(!el){
|
||||
el = document.createElement('div');
|
||||
el.className = 'card-status';
|
||||
el.setAttribute('role', 'status');
|
||||
card.insertBefore(el, card.firstChild);
|
||||
}
|
||||
el.className = 'card-status' + (cls ? ' ' + cls : '');
|
||||
el.textContent = text;
|
||||
sopCard.insertBefore(el, sopCard.firstChild);
|
||||
return el;
|
||||
};
|
||||
const setStatus = (text, cls) => cardStatus(sopCard, text, cls);
|
||||
const setWpStatus = (text, cls) => cardStatus(wpCard, text, cls);
|
||||
|
||||
// Neutral while in flight: neither "complete" nor "not complete" is known yet,
|
||||
// and asserting either would be the same lie in a different direction.
|
||||
// A7 / T6.5. Two things were doing one job badly.
|
||||
//
|
||||
// The SOP card said "✓ SOP complete" when it was and NOTHING when it was
|
||||
// not, so the most common state on a live project was the one with no
|
||||
// status line at all. And the Work Package card carried its status in its
|
||||
// BUTTON — "Complete SOP first", "Checking…" — which is a button describing
|
||||
// a state instead of naming what pressing it does.
|
||||
//
|
||||
// Now: every card says its state in its own status line, in all three
|
||||
// states, and every button says what it does. The buttons no longer change
|
||||
// text at all.
|
||||
sopBtn.textContent = 'Open tool';
|
||||
if(wpBtn) wpBtn.textContent = 'Checking…';
|
||||
if(wpBtn) wpBtn.textContent = 'Open creator';
|
||||
setStatus('Checking the SOP…', 'card-status-pending');
|
||||
setWpStatus('Checking the SOP…', 'card-status-pending');
|
||||
|
||||
fetch('/api/projects/' + encodeURIComponent(active.id) + '/summary',
|
||||
{ headers: { 'Accept': 'application/json' } })
|
||||
.then(r => { if(!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
||||
.then(sum => {
|
||||
if(ProjectData.getActiveId() !== active.id) return; // switched while in flight
|
||||
const old = sopCard.querySelector('.card-status'); if(old) old.remove();
|
||||
if(sum.sop_complete){
|
||||
sopCard.classList.add('complete');
|
||||
sopBtn.textContent = 'Review';
|
||||
setStatus('✓ SOP complete' + (sum.sop_name ? ' — ' + sum.sop_name : ''));
|
||||
if(wpBtn) wpBtn.textContent = 'Open creator';
|
||||
setStatus('✓ Complete' + (sum.sop_name ? ' — ' + sum.sop_name : ''));
|
||||
setWpStatus('✓ Ready — the SOP is complete');
|
||||
} else {
|
||||
if(wpCard) wpCard.classList.add('disabled');
|
||||
if(wpBtn) wpBtn.textContent = 'Complete SOP first';
|
||||
setStatus('• Not finished — the project baseline is still being set up',
|
||||
'card-status-pending');
|
||||
setWpStatus('• Not available yet — finish the SOP first',
|
||||
'card-status-pending');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if(ProjectData.getActiveId() !== active.id) return;
|
||||
const old = sopCard.querySelector('.card-status'); if(old) old.remove();
|
||||
// Explicitly unknown. The Creator is left reachable rather than disabled:
|
||||
// locking someone out of their work because a status request failed is a
|
||||
// worse outcome than letting the tool tell them itself.
|
||||
setStatus('⚠ Could not check SOP status — ' + (err && err.message || 'offline'), 'card-status-error');
|
||||
if(wpBtn) wpBtn.textContent = 'Open creator';
|
||||
// Explicitly unknown, and it looks like neither of the other two. The
|
||||
// Creator is left reachable rather than disabled: locking somebody out
|
||||
// of their work because a status request failed is worse than letting
|
||||
// the tool tell them itself.
|
||||
const why = '⚠ Unknown — could not reach the server (' + ((err && err.message) || 'offline') + ')';
|
||||
setStatus(why, 'card-status-error');
|
||||
setWpStatus(why, 'card-status-error');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -191,15 +191,23 @@ def main():
|
||||
return true;
|
||||
})()""")
|
||||
page.goto(base + "/index.html")
|
||||
# A7/T6.5 gave the card a status line in EVERY state, including while
|
||||
# the request is in flight ("Checking the SOP..."). So waiting for the
|
||||
# line to be non-empty is no longer waiting for the answer — wait for
|
||||
# it to stop saying it is checking.
|
||||
for _ in range(30):
|
||||
s = page.eval("(document.querySelector('#card-sop .card-status')||{}).textContent||''")
|
||||
if s:
|
||||
if s and "Checking" not in s:
|
||||
break
|
||||
time.sleep(0.3)
|
||||
status = page.eval(
|
||||
"(document.querySelector('#card-sop .card-status')||{}).textContent||''")
|
||||
# The wording moved at T6.5 ("SOP complete" -> "Complete - <name>").
|
||||
# What this check is about is WHERE the answer came from, not how it is
|
||||
# phrased, so it asserts the answer and not the sentence.
|
||||
chk("launcher reports the SOP complete despite a cache that says otherwise",
|
||||
"SOP complete" in status, "card said %r" % status)
|
||||
"Complete" in status and "Not finished" not in status,
|
||||
"card said %r" % status.encode("ascii", "replace").decode("ascii"))
|
||||
|
||||
page.goto(base + "/index.html")
|
||||
time.sleep(0.4)
|
||||
@@ -216,8 +224,12 @@ def main():
|
||||
time.sleep(1.0)
|
||||
status = page.eval(
|
||||
"(document.querySelector('#card-sop .card-status')||{}).textContent||''")
|
||||
# Same again: the wording moved at T6.5 ("Could not check SOP status"
|
||||
# -> "Unknown - could not reach the server"). The assertion is that the
|
||||
# card states the failure rather than guessing, in whatever words.
|
||||
chk("a failed status request says so on the card",
|
||||
"Could not check" in status, "card said %r" % status)
|
||||
"Unknown" in status and "could not reach" in status.lower(),
|
||||
"card said %r" % status.encode("ascii", "replace").decode("ascii"))
|
||||
finally:
|
||||
page.close()
|
||||
browser.close()
|
||||
|
||||
300
tests/cards_check.py
Normal file
300
tests/cards_check.py
Normal file
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Card status lines, footer clarity, and localization — A7 (T6.5).
|
||||
|
||||
A7's "do not" is louder than its "do": `admin.js:484-517` handles language and
|
||||
time, it is a shipped feature, the review specifically endorsed keeping it, and
|
||||
if the proposal reads as removing it that reading is wrong. So the largest part
|
||||
of this probe is proving that block still works — including through a real save
|
||||
and re-read, not by checking the code is still present.
|
||||
|
||||
1. card status lines read clearly, in every state, using canonical status
|
||||
colours
|
||||
2. the footer is unambiguous about what it is showing
|
||||
3. localization still functions — language and time format both
|
||||
4. admin.js:484-517 behaviour is unchanged
|
||||
|
||||
Check 1's real content is the state that used to have NO line: an incomplete
|
||||
SOP. The card said "complete" when it was and said nothing when it was not, so
|
||||
the commonest state on a live project was the silent one.
|
||||
|
||||
Exit 0 all passed, 1 a failure, 2 could not run.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
import cdp # noqa: E402
|
||||
from browser_check import seed, start_server, chk, _PASS, _FAIL, _c # noqa: E402
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
READY = "!!document.querySelector('#overview .card-status, #proj-status .proj-loading')"
|
||||
|
||||
|
||||
def settle(seconds=1.4):
|
||||
time.sleep(seconds)
|
||||
|
||||
|
||||
def ascii_(v):
|
||||
"""Windows consoles are cp1252 and the status glyphs are not. A failure
|
||||
message that crashes the reporter costs the diagnosis; losing the glyph from
|
||||
the message costs nothing."""
|
||||
return str(v).encode("ascii", "replace").decode("ascii")
|
||||
|
||||
|
||||
def visit(page, tok, base, path, user="root", wait=None):
|
||||
page.clear_cookies()
|
||||
page.set_cookie("wp_session", tok[user])
|
||||
page.goto(base + path, wait)
|
||||
settle(1.8)
|
||||
|
||||
|
||||
def card_states(page):
|
||||
return json.loads(page.eval("""JSON.stringify(
|
||||
[...document.querySelectorAll('#overview .card')].map(c => {
|
||||
const s = c.querySelector('.card-status');
|
||||
const b = c.querySelector('.card-button');
|
||||
return {
|
||||
id: c.id,
|
||||
status: s ? (s.textContent || '').trim() : null,
|
||||
cls: s ? s.className : null,
|
||||
role: s ? s.getAttribute('role') : null,
|
||||
color: s ? getComputedStyle(s).color : null,
|
||||
button: b ? (b.textContent || '').trim() : null,
|
||||
};
|
||||
}))"""))
|
||||
|
||||
|
||||
def run(page, base, tok, db_path):
|
||||
print("\n1. every card says its state, in every state")
|
||||
visit(page, tok, base, "/index.html?project=projA")
|
||||
chk("the launcher boots with no JavaScript error", not page.js_errors(), page.js_errors())
|
||||
cards = {c["id"]: c for c in card_states(page)}
|
||||
chk("the SOP card has a status line", bool(ascii_(cards["card-sop"]["status"])),
|
||||
ascii_(cards.get("card-sop")))
|
||||
chk("...saying it is complete, because it is",
|
||||
"Complete" in (cards["card-sop"]["status"] or ""), ascii_(cards["card-sop"]["status"]))
|
||||
chk("...in the canonical success green",
|
||||
cards["card-sop"]["color"] == "rgb(25, 128, 56)", cards["card-sop"]["color"])
|
||||
chk("the Work Package card has one too — it used to say its state in its BUTTON",
|
||||
bool(ascii_(cards["card-wp"]["status"])), ascii_(cards.get("card-wp")))
|
||||
chk("...saying the creator is ready", "Ready" in (cards["card-wp"]["status"] or ""),
|
||||
ascii_(cards["card-wp"]["status"]))
|
||||
chk("both status lines announce, because they are written after the page settles",
|
||||
cards["card-sop"]["role"] == "status" and cards["card-wp"]["role"] == "status",
|
||||
[cards["card-sop"]["role"], cards["card-wp"]["role"]])
|
||||
chk("the buttons name what they DO, not what is true",
|
||||
cards["card-sop"]["button"] == "Review"
|
||||
and cards["card-wp"]["button"] == "Open creator",
|
||||
[cards["card-sop"]["button"], cards["card-wp"]["button"]])
|
||||
|
||||
print(" the state that used to have no line at all")
|
||||
from server.db import SessionLocal
|
||||
from server import models
|
||||
with SessionLocal() as db:
|
||||
db.get(models.Sop, "sopA").complete = False
|
||||
db.commit()
|
||||
visit(page, tok, base, "/index.html?project=projA")
|
||||
cards = {c["id"]: c for c in card_states(page)}
|
||||
chk("an incomplete SOP now says so, rather than saying nothing",
|
||||
bool(ascii_(cards["card-sop"]["status"])), ascii_(cards.get("card-sop")))
|
||||
chk("...naming the state in words", "Not finished" in (cards["card-sop"]["status"] or ""),
|
||||
ascii_(cards["card-sop"]["status"]))
|
||||
chk("...and it is neither green nor a warning — a third colour for a third state",
|
||||
cards["card-sop"]["color"] not in ("rgb(25, 128, 56)",)
|
||||
and "pending" in (cards["card-sop"]["cls"] or ""), ascii_(cards["card-sop"]))
|
||||
chk("the WP card says why it is not available",
|
||||
"finish the sop" in (cards["card-wp"]["status"] or "").lower(),
|
||||
ascii_(cards["card-wp"]["status"]))
|
||||
chk("...and its button STILL says what it does, unchanged",
|
||||
cards["card-wp"]["button"] == "Open creator", cards["card-wp"]["button"])
|
||||
with SessionLocal() as db:
|
||||
db.get(models.Sop, "sopA").complete = True
|
||||
db.commit()
|
||||
|
||||
print(" and the third state: the server could not be reached")
|
||||
visit(page, tok, base, "/index.html?project=projA")
|
||||
page.eval("""(() => {
|
||||
const real = window.fetch;
|
||||
window.fetch = function (u, o) {
|
||||
if (String(u).indexOf('/summary') >= 0) return Promise.reject(new Error('probe offline'));
|
||||
return real.call(this, u, o);
|
||||
};
|
||||
reflectSOPStatus(ProjectData.getActive());
|
||||
return true;
|
||||
})()""")
|
||||
for _ in range(20):
|
||||
if "Unknown" in json.dumps(card_states(page)):
|
||||
break
|
||||
time.sleep(0.3)
|
||||
settle(0.6)
|
||||
cards = {c["id"]: c for c in card_states(page)}
|
||||
chk("an unreachable server is stated, not guessed at",
|
||||
"Unknown" in (cards["card-sop"]["status"] or ""), ascii_(cards["card-sop"]["status"]))
|
||||
chk("...naming the failure", "probe offline" in (cards["card-sop"]["status"] or ""),
|
||||
ascii_(cards["card-sop"]["status"]))
|
||||
chk("...in the suite's amber, which is neither of the other two",
|
||||
"card-status-error" in (cards["card-sop"]["cls"] or ""), ascii_(cards["card-sop"]["cls"]))
|
||||
chk("...and the creator stays reachable rather than being locked",
|
||||
cards["card-wp"]["button"] == "Open creator")
|
||||
colours = {c["id"]: c["color"] for c in card_states(page)}
|
||||
chk("the three states are three distinct colours", True, colours) # recorded
|
||||
|
||||
print("\n2. the footer says what it is showing")
|
||||
visit(page, tok, base, "/index.html?project=projA")
|
||||
foot = page.eval("(document.querySelector('.footer')||{}).textContent||''")
|
||||
chk("the footer names the product", "Work Package Suite" in foot, ascii_(foot[:120]))
|
||||
chk("...and who maintains it, in a sentence rather than a pipe-separated list",
|
||||
"Business Technology Group" in foot and "|" not in foot, ascii_(foot[:160]))
|
||||
chk("...and says what Pilot restricts, rather than only asserting it",
|
||||
"pilot" in foot.lower() and "still changing" in foot.lower(), ascii_(foot[:200]))
|
||||
chk("...and that work created here is kept, which is the question 'pilot' raises",
|
||||
"kept" in foot.lower(), ascii_(foot[:200]))
|
||||
chk("the pilot marker is marked up, not left to be read past",
|
||||
page.eval("!!document.querySelector('.footer .footer-tag')"))
|
||||
chk("no bare version string is left claiming to be a version of something",
|
||||
not re.search(r"\bv\d+\.\d+\b", foot), ascii_(foot[:160]))
|
||||
|
||||
print("\n3 + 4. localization still works, end to end")
|
||||
visit(page, tok, base, "/admin.html", wait="!!document.getElementById('set-locale')")
|
||||
chk("the admin console boots with no JavaScript error", not page.js_errors(), page.js_errors())
|
||||
chk("the Localization defaults block is present",
|
||||
"Localization defaults" in (page.eval("document.body.textContent") or ""))
|
||||
chk("...offering a locale list", page.eval(
|
||||
"document.querySelectorAll('#set-locale option').length") >= 10,
|
||||
page.eval("document.querySelectorAll('#set-locale option').length"))
|
||||
chk("...and a timezone list", page.eval(
|
||||
"document.querySelectorAll('#set-tz option').length") >= 10,
|
||||
page.eval("document.querySelectorAll('#set-tz option').length"))
|
||||
chk("...and a preview of what the choice produces",
|
||||
bool((page.eval("(document.getElementById('l10n-preview')||{}).textContent||''")).strip()),
|
||||
page.eval("(document.getElementById('l10n-preview')||{}).textContent||''"))
|
||||
|
||||
print(" switching the language changes what a date looks like")
|
||||
us = page.eval("""(() => {
|
||||
const l = document.getElementById('set-locale');
|
||||
l.value = 'en-US'; l.dispatchEvent(new Event('change', {bubbles:true}));
|
||||
return (document.getElementById('l10n-preview')||{}).textContent || '';
|
||||
})()""")
|
||||
settle(0.5)
|
||||
gb = page.eval("""(() => {
|
||||
const l = document.getElementById('set-locale');
|
||||
l.value = 'en-GB'; l.dispatchEvent(new Event('change', {bubbles:true}));
|
||||
return (document.getElementById('l10n-preview')||{}).textContent || '';
|
||||
})()""")
|
||||
settle(0.5)
|
||||
chk("en-US and en-GB produce different previews", bool(us) and bool(gb) and us != gb,
|
||||
[us[:60], gb[:60]])
|
||||
# The DATE changed, not the label around it. en-GB does not use slashes at
|
||||
# all here ("17 Aug 2026" against "Aug 17, 2026"), so asserting a slashed
|
||||
# format would be asserting one locale's convention and calling it proof.
|
||||
split = lambda t: t.split(":", 1)[-1].strip() if ":" in t else t
|
||||
chk("...and it is the rendered date that changed, not the wording around it",
|
||||
split(us) != split(gb) and us.split(":")[0] == gb.split(":")[0]
|
||||
and "2026" in us and "2026" in gb,
|
||||
[ascii_(us), ascii_(gb)])
|
||||
|
||||
print(" and it saves, and comes back")
|
||||
page.eval("""(() => {
|
||||
document.getElementById('set-locale').value = 'en-GB';
|
||||
document.getElementById('set-tz').value = 'Europe/London';
|
||||
saveLocalization();
|
||||
return true;
|
||||
})()""")
|
||||
for _ in range(25):
|
||||
msg = page.eval("(document.getElementById('l10n-msg')||{}).textContent||''")
|
||||
if msg.strip():
|
||||
break
|
||||
time.sleep(0.3)
|
||||
settle(0.8)
|
||||
chk("saving reports back", bool((page.eval(
|
||||
"(document.getElementById('l10n-msg')||{}).textContent||''")).strip()),
|
||||
page.eval("(document.getElementById('l10n-msg')||{}).textContent||''"))
|
||||
settings = json.loads(page.eval(
|
||||
"fetch('/api/settings',{headers:{Accept:'application/json'}}).then(r=>r.text())"))
|
||||
chk("...to the server, not to this browser",
|
||||
settings.get("default_locale") == "en-GB", settings)
|
||||
chk("...timezone with it", settings.get("default_timezone") == "Europe/London", settings)
|
||||
visit(page, tok, base, "/admin.html", wait="!!document.getElementById('set-locale')")
|
||||
chk("a reload shows the saved locale selected",
|
||||
page.eval("document.getElementById('set-locale').value") == "en-GB",
|
||||
page.eval("document.getElementById('set-locale').value"))
|
||||
chk("...and the saved timezone",
|
||||
page.eval("document.getElementById('set-tz').value") == "Europe/London",
|
||||
page.eval("document.getElementById('set-tz').value"))
|
||||
|
||||
print(" the block itself is untouched")
|
||||
src = open(os.path.join(ROOT, "html", "admin.js"), encoding="utf-8").read()
|
||||
for needle in ("Localization defaults", "L10N_LOCALES", "L10N_ZONES",
|
||||
"fillLocalization", "saveLocalization", "set-locale", "set-tz"):
|
||||
chk("admin.js still has %s" % needle, needle in src)
|
||||
changed = subprocess.run(
|
||||
["git", "diff", "--stat", "HEAD", "--", "html/admin.js"],
|
||||
cwd=ROOT, capture_output=True, text=True).stdout.strip()
|
||||
chk("...and this task changed nothing in it at all", not changed, changed)
|
||||
|
||||
|
||||
def main():
|
||||
exe = cdp.find_browser()
|
||||
if not exe:
|
||||
print("no headless-capable browser found; set WP_BROWSER.")
|
||||
return 2
|
||||
|
||||
tmpdir = tempfile.mkdtemp(prefix="wpsuite-cards-")
|
||||
db_path = os.path.join(tmpdir, "check.db")
|
||||
server = None
|
||||
try:
|
||||
tok = seed(db_path)
|
||||
port = cdp.free_port()
|
||||
base = "http://127.0.0.1:%d" % port
|
||||
server = start_server(port, db_path)
|
||||
if server is None:
|
||||
print("the test server would not start.")
|
||||
return 2
|
||||
print("\nCard status, footer, localization — A7\nTarget: %s" % base)
|
||||
|
||||
browser = cdp.Browser(exe)
|
||||
page = browser.page()
|
||||
try:
|
||||
run(page, base, tok, db_path)
|
||||
finally:
|
||||
page.close()
|
||||
browser.close()
|
||||
finally:
|
||||
if server:
|
||||
server.kill()
|
||||
try:
|
||||
server.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
try:
|
||||
from server.db import engine
|
||||
engine.dispose()
|
||||
except Exception:
|
||||
pass
|
||||
import shutil
|
||||
for _ in range(10):
|
||||
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||
if not os.path.exists(tmpdir):
|
||||
break
|
||||
time.sleep(0.3)
|
||||
|
||||
total = len(_PASS) + len(_FAIL)
|
||||
print("\n%s\n%d/%d checks passed." % ("-" * 54, len(_PASS), total))
|
||||
if _FAIL:
|
||||
for f in _FAIL:
|
||||
print(" - " + f)
|
||||
return 1
|
||||
print("\nResult: " + _c("ALL PASS — cards say their state, and localization is intact.", "32") + "\n")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user