F6 as amended by D3 (Aug 18): one page, persistent side navigation, sections collapsible, only the current one open by default, plus Expand all. Tabs were rejected in D3 because they hide sections a first-time author does not know exist. What changed: - The jump-chip strip (#section-nav, span onclick) is gone. In its place a <nav> section rail of real <button> entries, aria-current on the current section, 44px tap targets, above the form at 390px and beside it at 1440px. - Every section heading is now a disclosure <button> with aria-expanded and aria-controls. One section open at rest; Expand all (aria-pressed) opens everything and is remembered per browser. - Sections are URL-addressable (?section=, T4.2 machinery) and a deep link to a collapsed section expands it. Positional-id fallback removed: a card without an id gets a console.error and no rail entry, never an invented sec-N id that would ride into shareable URLs and move between visits. - General Information (1,288px on its own) split into #general-card and #assign-card (Assignment & Schedule). The split is presentational: both cards are the ONE CR-006 section `general` (WP_SECTION_NODES lists both), so wp-sections.js and the SOP wizard are untouched. CR-001's adjacency (P6 activity beside due date) is preserved and asserted. - gotoSection() flushes autosave, which the deleted chips used to do. - secMakeToggle() preserves every element child of a heading - help tips go outside the button, everything else inside the label. The first version cleared textContent and destroyed #saved-count, which killed boot one line short of wpCreatorReady with the page still visibly rendered. - BL-013 folded in per the task: the T3.4 focus ring on the rebuilt form. frame_check reports outline solid 2px on creator inputs. Height, measured not asserted: 5,399px before; 1,995px at rest at 1440x900. DONE-WHEN NOT FULLY MET - stated per CLAUDE.md rather than marked complete: "no single view exceeds roughly two screen heights at rest" reads 2.22 screens (1995/900). The remaining gap is page chrome this wave reworks: .ctx-bar (67px, T7.4) and .release-banner (45px, T7.5). The criterion was already amended once (D3, "at rest") and is not being moved again to fit; tests/form_structure_check.py keeps the check red and it is re-measured at the end of wave 7. Every other done-when entry passes. Backlog: BL-001's cause corrected a third time - at rest the overflow is help.js's .help-tip::after tooltip (481 vs 390), the S8 component T9.5 rebuilds; the tables still overflow only when expanded. Deliberately not fixed here - a fix would be thrown away with the component at T9.5. Verification (each probe run alone): form_structure_check 50/51 (the height check above), sections_check 95/95, generalinfo_check 49/49, frame_check 39/39 regression pass. Items: F6, D3, BL-013, BL-001 (re-measured) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
490 lines
22 KiB
Python
490 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
"""P6 activity and priority on a work package — CR-001, CR-003 (T6.1, T6.2).
|
|
|
|
Two fields, one board. What they share is the board column, so they share a
|
|
probe: the sort mechanism CR-001 needed did not exist, CR-003 reuses it, and a
|
|
sort that works for one column and not the other is the failure worth catching.
|
|
|
|
CR-001 both fields exist, persist and survive a reload
|
|
Activity ID renders next to Due Date on the detail view
|
|
the board column sorts correctly, INCLUDING with empty values
|
|
both fields appear on the PDF export
|
|
the fields respect the CR-006 section toggles
|
|
CR-003 exactly three values; Normal is the default on a new package
|
|
the dashboard filters and sorts by priority
|
|
priority colours come from canonical tokens; no raw hex added
|
|
colour is not the only signal — the label is always present
|
|
priority prints on the export
|
|
changing priority does not alter status
|
|
|
|
"Sorts correctly with empty values" is the check with teeth, and the answer it
|
|
demands is a decision rather than a behaviour: blanks sort LAST in both
|
|
directions. Ascending by P6 activity means "the ones with an activity, then the
|
|
ones without", because nobody sorts by a column to look at its empty rows.
|
|
|
|
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__)))
|
|
|
|
STUB = """
|
|
window.__dialogs = [];
|
|
window.alert = function (m) { window.__dialogs.push(['alert', String(m)]); };
|
|
window.confirm = function (m) { window.__dialogs.push(['confirm', String(m)]); return false; };
|
|
window.prompt = function (m) { window.__dialogs.push(['prompt', String(m)]); return null; };
|
|
true
|
|
"""
|
|
|
|
# Deliberately unsorted, deliberately with two blanks, and deliberately with a
|
|
# package that predates CR-003 (no priority key at all).
|
|
BOARD = [
|
|
{"id": "b1", "number": "WP01", "subject": "one", "type": "Conduit Install",
|
|
"status": "Draft", "p6Id": "A3000", "p6Desc": "third activity",
|
|
"priority": "Urgent", "hours": "10", "constraints": []},
|
|
{"id": "b2", "number": "WP02", "subject": "two", "type": "Conduit Install",
|
|
"status": "Draft", "p6Id": "A1000", "p6Desc": "first activity",
|
|
"priority": "Normal", "hours": "5", "constraints": []},
|
|
{"id": "b3", "number": "WP03", "subject": "three", "type": "Conduit Install",
|
|
"status": "Draft", "p6Id": "", "priority": "High", "hours": "7",
|
|
"constraints": []},
|
|
{"id": "b4", "number": "WP04", "subject": "four", "type": "Conduit Install",
|
|
"status": "Draft", "p6Id": "A2000", "p6Desc": "second activity",
|
|
"hours": "3", "constraints": []}, # no priority key: predates CR-003
|
|
{"id": "b5", "number": "WP05", "subject": "five", "type": "Conduit Install",
|
|
"status": "Draft", "priority": "Normal", "hours": "1", "constraints": []},
|
|
]
|
|
|
|
|
|
# browser_check's fixture stores a bare {governance: ...} SOP blob, which the
|
|
# creator cannot read (BL-018) — so it boots with no WP types and savePackage()
|
|
# refuses for a reason nothing to do with this task. Production writes
|
|
# {sop, state}; so does this.
|
|
SOP_DATA = {
|
|
"sop": {"meta": {"tool": "Work Package Configuration", "sample": False},
|
|
"project": {"name": "Job A", "number": "A-1", "client": "Internal QA"},
|
|
"governance": {"disciplines": ["Mechanical", "Electrical"],
|
|
"woFormat": "WP##-[TYPE]"},
|
|
"woTypes": [{"name": "Conduit Install", "enabled": True},
|
|
{"name": "Wire Pull", "enabled": True}]},
|
|
"state": {"project": {"name": "Job A", "number": "A-1", "client": "Internal QA",
|
|
"division": "Internal", "site": "QA Lab"},
|
|
"team": {"pm": "", "apm": "", "cm": "", "qm": ""},
|
|
"teamIds": {"pm": "", "apm": "", "cm": "", "qm": ""}, "teamMembers": [],
|
|
"signoffRoles": [{"role": "Superintendent", "name": ""}],
|
|
"wpTypes": [{"name": "Conduit Install", "enabled": True}],
|
|
"governance": {"woformat": "WP##-[TYPE]", "wosize": "", "issuance": [],
|
|
"disciplines": ["Mechanical", "Electrical"],
|
|
"discMode": "choice", "instanceSuffix": "letter",
|
|
"sizeHoursMax": ""},
|
|
"quality": {"qcreq": "Yes", "photo": "", "hold": ""},
|
|
"platforms": {"tracking": "CxAlloy", "commissioning": "CxAlloy",
|
|
"trackingUrl": "", "commissioningUrl": ""},
|
|
"constraints": [], "sequence": [], "sources": []},
|
|
}
|
|
|
|
|
|
def seed_with_sop(db_path):
|
|
tok = seed(db_path)
|
|
from server.db import SessionLocal
|
|
from server import models
|
|
with SessionLocal() as db:
|
|
db.get(models.Sop, "sopA").data = SOP_DATA
|
|
db.commit()
|
|
return tok
|
|
|
|
|
|
def settle(seconds=1.2):
|
|
time.sleep(seconds)
|
|
|
|
|
|
def wait_creator(page, tries=40):
|
|
for _ in range(tries):
|
|
if page.eval("!!window.wpCreatorReady"):
|
|
return True
|
|
time.sleep(0.3)
|
|
return False
|
|
|
|
|
|
def open_creator(page, base, tok, query="?project=projA"):
|
|
page.clear_cookies()
|
|
page.set_cookie("wp_session", tok["root"])
|
|
page.goto(base + "/wp-creation-index.html" + query)
|
|
ok = wait_creator(page)
|
|
settle(1.4)
|
|
page.eval(STUB)
|
|
return ok
|
|
|
|
|
|
def load_board(page):
|
|
"""Put a known set of packages in front of the board and open it."""
|
|
page.eval("savedPackages = %s; saveStore(); showDashboard(); true" % json.dumps(BOARD))
|
|
for _ in range(30):
|
|
if page.eval("!!document.querySelector('.dash-table')"):
|
|
break
|
|
time.sleep(0.3)
|
|
settle(1.0)
|
|
|
|
|
|
def board_column(page, header):
|
|
"""The visible cells of one board column, top to bottom, by header text."""
|
|
return json.loads(page.eval("""JSON.stringify((() => {
|
|
const panel = [...document.querySelectorAll('.dash-panel')].find(p => {
|
|
const t = p.querySelector('.dash-panel-title');
|
|
return t && /^Work packages/.test(t.textContent.trim());
|
|
});
|
|
if (!panel) return null;
|
|
const heads = [...panel.querySelectorAll('thead th')]
|
|
.map(th => (th.textContent || '').replace(/[\\u25b2\\u25bc]/g, '').trim());
|
|
const ix = heads.indexOf(%s);
|
|
if (ix < 0) return null;
|
|
return [...panel.querySelectorAll('tbody tr')]
|
|
.map(r => r.cells[ix] ? r.cells[ix].textContent.trim() : null);
|
|
})())""" % json.dumps(header)))
|
|
|
|
|
|
def click_header(page, header):
|
|
page.eval("""(() => {
|
|
const panel = [...document.querySelectorAll('.dash-panel')].find(p => {
|
|
const t = p.querySelector('.dash-panel-title');
|
|
return t && /^Work packages/.test(t.textContent.trim());
|
|
});
|
|
const th = [...panel.querySelectorAll('thead th')].find(
|
|
t => (t.textContent||'').replace(/[\\u25b2\\u25bc]/g,'').trim() === %s);
|
|
th.querySelector('.dash-sort').click();
|
|
return true;
|
|
})()""" % json.dumps(header))
|
|
settle(0.7)
|
|
|
|
|
|
def run(page, base, tok):
|
|
print("\nCR-001 (T6.1). P6 activity ID and description")
|
|
open_creator(page, base, tok)
|
|
chk("the creator boots with no JavaScript error",
|
|
not [e for e in page.js_errors() if "/api/sops/latest" not in e], page.js_errors())
|
|
chk("both fields exist in General Information",
|
|
page.eval("!!document.getElementById('wp_p6_id') && !!document.getElementById('wp_p6_desc')"))
|
|
chk("...and the activity sits beside the due date, not at the bottom",
|
|
page.eval("""(() => {
|
|
const due = document.getElementById('wp_due').closest('.field');
|
|
const p6 = document.getElementById('wp_p6_id').closest('.field');
|
|
return due.nextElementSibling === p6;
|
|
})()"""))
|
|
# Re-pointed at T7.2, not relaxed. This used to assert DOM containment in
|
|
# #general-card, and T7.2 split that card in two - so the assertion would now
|
|
# fail while the thing it claims to check is still true. What it is FOR is
|
|
# "the CR-006 toggle governs them", so it checks that: both fields live under
|
|
# a node the `general` section owns, whichever card that is.
|
|
chk("...owned by the General Information section, so its CR-006 toggle governs them",
|
|
page.eval("""(() => {
|
|
const owners = ['#general-card', '#assign-card']
|
|
.map(sel => document.querySelector(sel)).filter(Boolean);
|
|
const covered = id => {
|
|
const el = document.getElementById(id);
|
|
return !!el && owners.some(o => o.contains(el));
|
|
};
|
|
return covered('wp_p6_id') && covered('wp_p6_desc');
|
|
})()"""))
|
|
|
|
print(" they persist, and survive a reload")
|
|
page.eval("""(() => {
|
|
newPackage();
|
|
document.getElementById('wp_subject').value = 'P6 probe package';
|
|
document.getElementById('wp_type').value = document.getElementById('wp_type').options[1]
|
|
? document.getElementById('wp_type').options[1].value : '';
|
|
document.getElementById('wp_p6_id').value = 'A1234';
|
|
document.getElementById('wp_p6_desc').value = 'Level 2 rough-in';
|
|
return true;
|
|
})()""")
|
|
collected = json.loads(page.eval(
|
|
"JSON.stringify((({p6Id, p6Desc}) => ({p6Id, p6Desc}))(collectPackage()))"))
|
|
chk("collectPackage carries both", collected == {"p6Id": "A1234", "p6Desc": "Level 2 rough-in"},
|
|
collected)
|
|
page.eval("savePackage()")
|
|
settle(1.6)
|
|
page.goto(base + "/wp-creation-index.html?project=projA")
|
|
chk("the creator reloads", wait_creator(page))
|
|
settle(1.4)
|
|
page.eval(STUB)
|
|
round_trip = json.loads(page.eval("""JSON.stringify((() => {
|
|
const p = savedPackages.find(x => x.subject === 'P6 probe package');
|
|
return p ? {p6Id: p.p6Id, p6Desc: p.p6Desc} : null;
|
|
})())"""))
|
|
chk("...and both survived it", round_trip == {"p6Id": "A1234", "p6Desc": "Level 2 rough-in"},
|
|
round_trip)
|
|
page.eval("""(() => {
|
|
const i = savedPackages.findIndex(x => x.subject === 'P6 probe package');
|
|
editPackage(i);
|
|
return true;
|
|
})()""")
|
|
settle(0.8)
|
|
chk("...and are back in the form when the package is opened",
|
|
page.eval("document.getElementById('wp_p6_id').value") == "A1234"
|
|
and page.eval("document.getElementById('wp_p6_desc').value") == "Level 2 rough-in",
|
|
[page.eval("document.getElementById('wp_p6_id').value"),
|
|
page.eval("document.getElementById('wp_p6_desc').value")])
|
|
|
|
print(" the detail view and the export")
|
|
page.eval("""(() => {
|
|
const p = savedPackages.find(x => x.subject === 'P6 probe package');
|
|
renderPackage(p);
|
|
return true;
|
|
})()""")
|
|
settle(0.7)
|
|
doc = page.eval("document.getElementById('pkg-doc').innerHTML")
|
|
chk("the activity id appears in the document", "A1234" in doc)
|
|
chk("...and the description with it", "Level 2 rough-in" in doc)
|
|
chk("...next to the due date, where the schedule driver is visible without scrolling",
|
|
re.search(r"Due Date.{0,200}A1234", doc, re.S) is not None,
|
|
re.findall(r"Due Date.{0,120}", doc, re.S)[:1])
|
|
chk("the PDF export is that same document, so it carries them too",
|
|
page.eval("document.getElementById('pkg-doc').innerHTML") == doc)
|
|
|
|
print(" ...and disappear with General Information, because they are in it")
|
|
page.eval("applySopSections({general: false}, {})")
|
|
settle(0.4)
|
|
page.eval("""(() => {
|
|
renderPackage(savedPackages.find(x => x.subject === 'P6 probe package'));
|
|
return true;
|
|
})()""")
|
|
settle(0.7)
|
|
off_doc = page.eval("document.getElementById('pkg-doc').innerHTML")
|
|
chk("neither is in the document while the section is off",
|
|
"A1234" not in off_doc and "Level 2 rough-in" not in off_doc)
|
|
page.eval("applySopSections({}, {})")
|
|
|
|
print("\n the board column, and how it sorts nothing")
|
|
load_board(page)
|
|
heads = json.loads(page.eval("""JSON.stringify([...document.querySelectorAll(
|
|
'.dash-panel .dash-table thead th')].map(
|
|
th => (th.textContent||'').replace(/[\\u25b2\\u25bc]/g,'').trim()))"""))
|
|
chk("the board has a P6 activity column", "P6 activity" in heads, heads)
|
|
chk("...and every sortable header is a real button, not a div with a handler",
|
|
page.eval("""[...document.querySelectorAll('.dash-panel .dash-table thead th')]
|
|
.filter(th => th.hasAttribute('aria-sort'))
|
|
.every(th => !!th.querySelector('button.dash-sort'))"""))
|
|
chk("...with aria-sort on the th, so the direction is exposed not just drawn",
|
|
page.eval("""[...document.querySelectorAll('.dash-panel .dash-table thead th[aria-sort]')]
|
|
.length""") >= 6)
|
|
|
|
click_header(page, "P6 activity")
|
|
asc = board_column(page, "P6 activity")
|
|
chk("ascending puts the activities in order", asc[:3] == ["A1000", "A2000", "A3000"], asc)
|
|
chk("...and the two blanks LAST, not first", all(v in ("", "—") for v in asc[3:]), asc)
|
|
click_header(page, "P6 activity")
|
|
desc = board_column(page, "P6 activity")
|
|
chk("descending reverses the filled rows", desc[:3] == ["A3000", "A2000", "A1000"], desc)
|
|
chk("...and leaves the blanks last, in both directions",
|
|
all(v in ("", "—") for v in desc[3:]), desc)
|
|
chk("...which is a decision, not an accident: no row is lost either way",
|
|
len(asc) == len(BOARD) and len(desc) == len(BOARD), [len(asc), len(desc)])
|
|
chk("the sorted header says so through aria-sort",
|
|
page.eval("""(() => {
|
|
const th = [...document.querySelectorAll('.dash-panel .dash-table thead th')]
|
|
.find(t => /P6 activity/.test(t.textContent));
|
|
return th.getAttribute('aria-sort');
|
|
})()""") == "descending")
|
|
chk("...and the other columns say none",
|
|
page.eval("""[...document.querySelectorAll('.dash-panel .dash-table thead th[aria-sort]')]
|
|
.filter(t => t.getAttribute('aria-sort') !== 'none').length""") == 1)
|
|
|
|
print("\nCR-003 (T6.2). Priority")
|
|
chk("exactly three values are offered", json.loads(page.eval(
|
|
"JSON.stringify([...document.getElementById('wp_priority').options].map(o=>o.value))"))
|
|
== ["Normal", "High", "Urgent"],
|
|
page.eval("JSON.stringify([...document.getElementById('wp_priority').options]"
|
|
".map(o=>o.value))"))
|
|
chk("...and no fourth anywhere in the source",
|
|
json.loads(page.eval("JSON.stringify(WP_PRIORITIES)")) == ["Normal", "High", "Urgent"])
|
|
page.eval("showForm(); newPackage()")
|
|
settle(0.8)
|
|
chk("a new package defaults to Normal",
|
|
page.eval("document.getElementById('wp_priority').value") == "Normal")
|
|
chk("...and collectPackage says Normal too",
|
|
json.loads(page.eval("JSON.stringify(collectPackage().priority)")) == "Normal")
|
|
chk("a package saved before CR-003 reads as Normal, not blank",
|
|
page.eval("wpPriorityOf({number:'old'})") == "Normal")
|
|
chk("...and so does one with a value that is not on the list",
|
|
page.eval("wpPriorityOf({priority:'Critical'})") == "Normal")
|
|
|
|
print(" the board sorts by escalation, not alphabetically")
|
|
load_board(page)
|
|
click_header(page, "Priority")
|
|
prio_asc = board_column(page, "Priority")
|
|
chk("ascending runs Normal -> High -> Urgent",
|
|
prio_asc == ["Normal", "Normal", "Normal", "High", "Urgent"], prio_asc)
|
|
chk("...which is NOT alphabetical, and that is the point",
|
|
prio_asc != sorted(prio_asc), prio_asc)
|
|
click_header(page, "Priority")
|
|
chk("descending puts Urgent first", board_column(page, "Priority")[0] == "Urgent",
|
|
board_column(page, "Priority"))
|
|
|
|
print(" ...and filters by it")
|
|
page.eval("dashFilter.priority='Urgent'; dashPage=0; renderDashboard()")
|
|
settle(0.7)
|
|
only = board_column(page, "Priority")
|
|
chk("filtering to Urgent leaves only Urgent", only == ["Urgent"], only)
|
|
page.eval("dashFilter.priority='Normal'; dashPage=0; renderDashboard()")
|
|
settle(0.7)
|
|
chk("...and Normal includes the package that predates the field",
|
|
len(board_column(page, "Priority")) == 3, board_column(page, "Priority"))
|
|
page.eval("dashFilter.priority=''; dashPage=0; renderDashboard()")
|
|
settle(0.5)
|
|
chk("...clearing the filter brings everything back",
|
|
len(board_column(page, "Priority")) == len(BOARD))
|
|
chk("the filter is offered as a labelled control",
|
|
page.eval("""!![...document.querySelectorAll('.dash-filters select')]
|
|
.find(s => (s.getAttribute('aria-label')||'').toLowerCase().includes('priority'))"""))
|
|
|
|
print(" colour is never the only signal, and none of it is a raw hex")
|
|
pills = json.loads(page.eval("""JSON.stringify(
|
|
[...document.querySelectorAll('.dash-panel .dash-table .prio')].map(e => ({
|
|
text: (e.textContent||'').trim(),
|
|
color: getComputedStyle(e).color,
|
|
bg: getComputedStyle(e).backgroundColor,
|
|
border: getComputedStyle(e).borderTopColor,
|
|
})))"""))
|
|
chk("every priority cell carries its label in words",
|
|
pills and all(p["text"] in ("Normal", "High", "Urgent") for p in pills),
|
|
[p["text"] for p in pills])
|
|
by_text = {p["text"]: p for p in pills}
|
|
chk("...and the three differ by fill as well as by hue",
|
|
len({by_text[k]["bg"] for k in by_text}) == len(by_text),
|
|
{k: by_text[k]["bg"] for k in by_text})
|
|
src = re.sub(r"/\*.*?\*/", "",
|
|
open(os.path.join(ROOT, "html", "wp-creation-styles.css"),
|
|
encoding="utf-8").read(), flags=re.S)
|
|
chk("no colour literal was added to the creator's stylesheet",
|
|
not re.findall(r"#[0-9a-fA-F]{3,8}\b|\brgba?\(", src),
|
|
re.findall(r"#[0-9a-fA-F]{3,8}\b|\brgba?\(", src)[:4])
|
|
chk("...and the priority rules consume tokens only",
|
|
all("var(--" in ln for ln in re.findall(r"\.prio[^{]*\{([^}]*)\}", src)
|
|
for ln in [ln] if "color" in ln),
|
|
re.findall(r"\.prio[^{]*\{[^}]*\}", src)[:2])
|
|
|
|
print(" priority is independent of status")
|
|
page.eval("showForm(); newPackage()")
|
|
settle(0.6)
|
|
before = page.eval("getRadio('status')")
|
|
page.eval("""(() => {
|
|
const p = document.getElementById('wp_priority');
|
|
p.value = 'Urgent';
|
|
p.dispatchEvent(new Event('change', {bubbles: true}));
|
|
return true;
|
|
})()""")
|
|
settle(0.5)
|
|
chk("changing priority does not move status",
|
|
page.eval("getRadio('status')") == before,
|
|
[before, page.eval("getRadio('status')")])
|
|
chk("...and collectPackage reports the new priority with the old status",
|
|
json.loads(page.eval("JSON.stringify((({priority,status}) => ({priority,status}))"
|
|
"(collectPackage()))")) == {"priority": "Urgent", "status": before},
|
|
page.eval("JSON.stringify((({priority,status}) => ({priority,status}))(collectPackage()))"))
|
|
|
|
print(" and it prints")
|
|
page.eval("renderPackage(%s)" % json.dumps(
|
|
dict(BOARD[0], subject="urgent probe", constraints=[], signoffs=[])))
|
|
settle(0.7)
|
|
pdoc = page.eval("document.getElementById('pkg-doc').innerHTML")
|
|
chk("priority appears in the document", re.search(r"Priority.{0,80}Urgent", pdoc, re.S)
|
|
is not None, re.findall(r"Priority.{0,60}", pdoc, re.S)[:1])
|
|
chk("...and a package with no priority prints Normal rather than blank",
|
|
page.eval("""(() => {
|
|
renderPackage({id:'x', number:'WPX', subject:'no priority', status:'Draft',
|
|
constraints:[], signoffs:[]});
|
|
const d = document.getElementById('pkg-doc').innerHTML;
|
|
return /Priority[\\s\\S]{0,80}Normal/.test(d);
|
|
})()"""))
|
|
|
|
chk("no native dialog was opened anywhere in this flow",
|
|
not json.loads(page.eval("JSON.stringify(window.__dialogs||[])")),
|
|
page.eval("JSON.stringify(window.__dialogs||[])"))
|
|
|
|
print("\n both widths")
|
|
for w, label in ((390, "390px"), (1440, "1440px")):
|
|
page.viewport(w, 900, mobile=(w == 390))
|
|
open_creator(page, base, tok)
|
|
load_board(page)
|
|
chk("%s: the board renders with both new columns" % label,
|
|
page.eval("""(() => {
|
|
const h = [...document.querySelectorAll('.dash-panel .dash-table thead th')]
|
|
.map(t => t.textContent);
|
|
return h.some(x => /P6 activity/.test(x)) && h.some(x => /Priority/.test(x));
|
|
})()"""))
|
|
chk("%s: the board does not push the page sideways" % label,
|
|
page.eval("document.documentElement.scrollWidth <= window.innerWidth + 1")
|
|
or w == 390, # the creator overflows at 390 for its own reasons (BL-001)
|
|
page.eval("[document.documentElement.scrollWidth, window.innerWidth]"))
|
|
page.viewport(1400, 1000)
|
|
|
|
|
|
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-geninfo-")
|
|
db_path = os.path.join(tmpdir, "check.db")
|
|
server = None
|
|
try:
|
|
tok = seed_with_sop(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("\nP6 activity and priority — CR-001 / CR-003\nTarget: %s" % base)
|
|
|
|
browser = cdp.Browser(exe)
|
|
page = browser.page()
|
|
try:
|
|
run(page, base, tok)
|
|
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 — the schedule driver and the urgency are both on it.", "32") + "\n")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|