T9.4 - S7: one sample-data affordance, confirmed, and fenced off the project
Four affordances under three names became ONE: "Load sample data", on the creator's toolbar, at the far end of two separators from the live actions (New / Duplicate), pushed right with its own gap. It confirms through the T7.9 dialog, naming exactly what it does - and what it does not: "This page only: nothing is written to the project unless you then save." The probe verifies the fence the way the done-when demands - against a REAL project, reading the server's SOP and work-package list before and after and asserting byte-identical. Gone: the wizard's header "Load sample" (the dangerous one: it filled the state completeSOP() pushes to the LIVE project, one click, no confirm, no undo - reconciled with D1 exactly as the task records: the creator's control is the survivor, the wizard copy goes), the creator's split Sample SOP / Load example pair (now internals behind the one entry point), and the empty-state context bar's third button (its text now points at the toolbar control). The location/material "Load sample values" buttons stay: they fill a PASTE BOX that acts only through an explicit, dry-runnable import - a different thing, stated in the code. Probes re-pointed with reasons in place: frame_check's D1 toolbar list names the consolidated control; validation_check's sample-driven toast checks became the-affordance-is-gone checks (and its stale showAnalytics drive, orphaned by T7.10, became a the-duplicate-stays-gone check). Verification (each probe run alone): NEW tests/sample_check.py 10/10. Regressions: validation_check 77/77, frame_check 38/38, kitting_check 26/26, export_check 20/20, sections_check 95/95. Items: S7 (D1 reconciliation honored) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -300,6 +300,7 @@ Wave 9 adds these:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
python tests/export_check.py # CR-008/CR-017 - export walk + hours guard 20 checks
|
python tests/export_check.py # CR-008/CR-017 - export walk + hours guard 20 checks
|
||||||
|
python tests/sample_check.py # S7 - one sample affordance, confirmed+fenced 10 checks
|
||||||
```
|
```
|
||||||
|
|
||||||
**Three probes were re-pointed at `T7.1`.** `sections_check.py` 5b drove the live
|
**Three probes were re-pointed at `T7.1`.** `sections_check.py` 5b drove the live
|
||||||
|
|||||||
@@ -391,95 +391,14 @@ function initializeWPTypes(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── LOAD SAMPLE DATA ──────────────────────────────────────────────────────────
|
// ── LOAD SAMPLE DATA ──────────────────────────────────────────────────────────
|
||||||
// This page's sample is the sample SOP. It used to reach into the frame and call
|
// Removed at T9.4 (S7). This was the affordance one click from live project
|
||||||
// the creator's loadExample() when the WP tab was open; the creator is its own
|
// data: it filled the wizard state that completeSOP() pushes to the PROJECT,
|
||||||
// page since T7.1 and carries its own sample controls in its toolbar (D1), so
|
// with no confirm and no undo. The suite's one sample control lives on the
|
||||||
// there is nothing to reach into and nothing to duplicate here.
|
// creator's toolbar, where it fills that page only. The location and material
|
||||||
//
|
// list steps keep their "Load sample values" buttons - those fill a PASTE BOX
|
||||||
// S7 still counts more sample affordances than it should. T9.4 reduces them; this
|
// that acts only through an explicit, dry-runnable Import, which is a
|
||||||
// task only stops one of them pretending to belong to a document it is not in.
|
// different thing from loading sample data into a live configuration.
|
||||||
function loadSampleData(){
|
|
||||||
if(currentTool && currentTool !== 'sop'){
|
|
||||||
wizardToast('The work package sample is on the Work Package Creation page, in its toolbar.',
|
|
||||||
{role: 'status'});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Populate Step 1
|
|
||||||
document.getElementById('proj_name').value = 'MICRON_PH1_CUP_HPM_FMCS INSTALL';
|
|
||||||
document.getElementById('proj_number').value = '26-67-008';
|
|
||||||
document.getElementById('proj_client').value = 'Micron Technology, Inc.';
|
|
||||||
document.getElementById('proj_division').value = 'Semiconductor';
|
|
||||||
document.getElementById('proj_site').value = 'Boise, ID — Fab 7';
|
|
||||||
|
|
||||||
// Populate Step 2. The leadership slots are account pickers now, so the sample's
|
|
||||||
// fictional names can't be "selected" — setting .value on a <select> with no
|
|
||||||
// matching option silently does nothing. Store them as names without an account,
|
|
||||||
// which is exactly how the picker shows a person who isn't a suite user yet.
|
|
||||||
state.team.pm = 'Mariano Sanchez';
|
|
||||||
state.team.apm = 'Assistant PM';
|
|
||||||
state.team.cm = 'K. Boyd';
|
|
||||||
state.team.qm = 'D. Nguyen';
|
|
||||||
state.teamIds = {pm:'', apm:'', cm:'', qm:''};
|
|
||||||
state.qaGroupIds = [];
|
|
||||||
renderTeamPickers();
|
|
||||||
|
|
||||||
// Step 3 — standard required roles
|
|
||||||
if(state.signoffRoles[0]) state.signoffRoles[0].role = 'Superintendent';
|
|
||||||
if(state.signoffRoles[1]) state.signoffRoles[1].role = 'Foreman';
|
|
||||||
const stEl = document.getElementById('role_super_title'); if(stEl) stEl.value = 'Superintendent';
|
|
||||||
const ftEl = document.getElementById('role_foreman_title'); if(ftEl) ftEl.value = 'Foreman';
|
|
||||||
state.signoffRoles[0].name = 'John Smith'; state.signoffRoles[0].userId = '';
|
|
||||||
state.signoffRoles[1].name = 'Mike Jones'; state.signoffRoles[1].userId = '';
|
|
||||||
renderSignoffRolePickers();
|
|
||||||
|
|
||||||
// Populate Step 5
|
|
||||||
document.getElementById('gov_woformat').value = 'WP##-[Sector]-[TYPE]';
|
|
||||||
document.getElementById('gov_wosize').value = 'Standard — 3–5 days (≈40–80 hrs)';
|
|
||||||
document.getElementById('gov_disciplines').value = 'Mechanical, Electrical, Tech';
|
|
||||||
document.getElementById('gov_discmode').value = 'choice';
|
|
||||||
document.getElementById('gov_size_hours_max').value = '80';
|
|
||||||
|
|
||||||
// Populate Step 6
|
|
||||||
document.getElementById('qual_qcreq').value = 'Yes — Detailed inspection items';
|
|
||||||
document.getElementById('qual_photo').value = 'Key checkpoints only';
|
|
||||||
document.getElementById('qual_hold').value = 'HOLD: Prime QAQC to inspect rough-in before cover/cover-up.\nWITNESS: Client QC to observe megger test before energization.';
|
|
||||||
|
|
||||||
// Step 7 already has defaults
|
|
||||||
|
|
||||||
// CR-016 / T5.7. The sample is the Micron EUV configuration, and Micron does not
|
|
||||||
// use Assets: its content duplicates the database Clinton's team maintains, and
|
|
||||||
// that integration is deferred. Off by toggle, so the section and its model stay
|
|
||||||
// in the application for the integration to land in — and any other project can
|
|
||||||
// turn it back on from step 12 without a code change.
|
|
||||||
//
|
|
||||||
// A partial map on purpose. WPSections.normalize fills the rest in as ON, so a
|
|
||||||
// section added after today is not silently off for this SOP.
|
|
||||||
if(typeof WPSections !== 'undefined'){
|
|
||||||
state.sections = WPSections.normalize({assets: false, kitting: false}); // CR-009: Micron EUV is not kitting today
|
|
||||||
state.fields = WPSections.normalizeFields({costCode: false, acumaticaTask: false}); // CR-002, mirrored on the export (T9.1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The Micron FMCS sample includes BIM/VDC — enable it so the sequence shows the
|
|
||||||
// full BIM → construction flow (BIM steps first) and the Creator offers IWP/EWP.
|
|
||||||
state.bimEnabled = true;
|
|
||||||
const beEl = document.getElementById('bim_enabled'); if(beEl) beEl.checked = true;
|
|
||||||
enableBIM();
|
|
||||||
|
|
||||||
// Collect all data
|
|
||||||
collectStepData();
|
|
||||||
track('sample_loaded');
|
|
||||||
|
|
||||||
wizardToast('Sample data loaded. Every step now holds example values — edit or replace any of them.');
|
|
||||||
|
|
||||||
// Switch to step 1. Every step now holds sample content, so the rail marks them
|
|
||||||
// all as visited — otherwise a fully populated wizard shows ten unstarted steps.
|
|
||||||
for(let i = 1; i <= LAST_STEP; i++) _visitedSteps.add(i);
|
|
||||||
currentStep = 1;
|
|
||||||
updateStepUI();
|
|
||||||
updateProjectDisplay();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── RESTORE A COMPLETED SOP (for "Review" + WP tab across reloads) ─────────────
|
|
||||||
function restoreSavedSOP(){
|
function restoreSavedSOP(){
|
||||||
let savedState = null, savedSop = null, complete = false;
|
let savedState = null, savedSop = null, complete = false;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -40,7 +40,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<button id="load-sample-btn" class="header-button" onclick="loadSampleData()" title="Load sample data for the current tool (SOP or Work Package)">Load sample</button>
|
|
||||||
<button class="header-button" onclick="toggleComments()" title="Leave feedback for the current step">Feedback</button>
|
<button class="header-button" onclick="toggleComments()" title="Leave feedback for the current step">Feedback</button>
|
||||||
<button class="header-button" onclick="openHelp()" title="How the suite works + key concepts">Help</button>
|
<button class="header-button" onclick="openHelp()" title="How the suite works + key concepts">Help</button>
|
||||||
<!-- A4/S9: the orphaned "1 / 10" counter lived here. It is retired — the
|
<!-- A4/S9: the orphaned "1 / 10" counter lived here. It is retired — the
|
||||||
|
|||||||
@@ -397,6 +397,20 @@ function constraintNames(){
|
|||||||
function nextSeq(){ return savedPackages.length+1; }
|
function nextSeq(){ return savedPackages.length+1; }
|
||||||
|
|
||||||
// ── SOP LOADING ──────────────────────────────────────────────────────────────
|
// ── SOP LOADING ──────────────────────────────────────────────────────────────
|
||||||
|
// S7 / T9.4: the one sample entry point. loadSampleSOP()/loadExample() stay as
|
||||||
|
// internals (probes and this function call them); the USER reaches sample data
|
||||||
|
// through exactly this, which says what it will and will not do before doing it.
|
||||||
|
async function loadSampleAll(){
|
||||||
|
if(!(await wpConfirmDialog({
|
||||||
|
title:'Load sample data',
|
||||||
|
message:'Replaces the SOP shown on this page with the fictional Micron sample and fills '
|
||||||
|
+ 'the form with the example work package.\n\nThis page only: nothing is written to '
|
||||||
|
+ 'the project unless you then save.',
|
||||||
|
okLabel:'Load the sample'}))) return;
|
||||||
|
loadSampleSOP();
|
||||||
|
loadExample();
|
||||||
|
}
|
||||||
|
|
||||||
function loadSampleSOP(){ SOP=JSON.parse(JSON.stringify(SAMPLE_SOP)); applySOP(); newPackage(); toast('Sample SOP loaded — ' + (SOP.project&&SOP.project.name||'project')); track('sample_loaded'); }
|
function loadSampleSOP(){ SOP=JSON.parse(JSON.stringify(SAMPLE_SOP)); applySOP(); newPackage(); toast('Sample SOP loaded — ' + (SOP.project&&SOP.project.name||'project')); track('sample_loaded'); }
|
||||||
function importSOP(ev){
|
function importSOP(ev){
|
||||||
const f=ev.target.files&&ev.target.files[0]; if(!f) return; const r=new FileReader();
|
const f=ev.target.files&&ev.target.files[0]; if(!f) return; const r=new FileReader();
|
||||||
@@ -531,7 +545,7 @@ function renderCtxBar(){
|
|||||||
if(!SOP){
|
if(!SOP){
|
||||||
bar.innerHTML = activeProjectId
|
bar.innerHTML = activeProjectId
|
||||||
? `<div class="ctx-empty">No SOP found for this project yet — complete the <strong>SOP Configuration</strong> first, then return here.</div>`
|
? `<div class="ctx-empty">No SOP found for this project yet — complete the <strong>SOP Configuration</strong> first, then return here.</div>`
|
||||||
: `<div class="ctx-empty">No SOP loaded — <button class="link-btn" onclick="loadSampleSOP()">load the sample</button> or import one from the Configuration tool.</div>`;
|
: `<div class="ctx-empty">No SOP loaded — import one from the Configuration tool, or use <strong>Load sample data</strong> in the toolbar above.</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const p=SOP.project||{}, g=SOP.governance||{};
|
const p=SOP.project||{}, g=SOP.governance||{};
|
||||||
|
|||||||
@@ -84,12 +84,15 @@
|
|||||||
<button class="btn btn-ghost" onclick="openSopModal()">View SOP</button>
|
<button class="btn btn-ghost" onclick="openSopModal()">View SOP</button>
|
||||||
<button class="btn btn-ghost" onclick="document.getElementById('sop-import').click()">Import SOP</button>
|
<button class="btn btn-ghost" onclick="document.getElementById('sop-import').click()">Import SOP</button>
|
||||||
<input type="file" id="sop-import" accept="application/json" style="display:none" onchange="importSOP(event)">
|
<input type="file" id="sop-import" accept="application/json" style="display:none" onchange="importSOP(event)">
|
||||||
<button class="btn btn-ghost" onclick="loadSampleSOP()">Sample SOP</button>
|
<!-- S7 / T9.4: THE sample-data affordance - exactly one in the whole suite,
|
||||||
<button class="btn btn-ghost" onclick="loadExample()">Load example</button>
|
under one name. Pushed to the far end, away from the live actions (New /
|
||||||
<span class="wp-toolbar-sep" aria-hidden="true"></span>
|
Duplicate sit at the other side of two separators), it confirms before
|
||||||
<!-- T7.10 moves this to the admin console and deletes one of the two
|
acting and names exactly what it does. It fills THIS PAGE only: nothing
|
||||||
implementations. It stays visible here until then rather than being
|
is written to the project unless the user then saves, which the probe
|
||||||
deleted by the task that dissolved the frame. -->
|
verifies against a real project. The wizard's copy and the split
|
||||||
|
Sample SOP / Load example pair are gone. -->
|
||||||
|
<span class="wp-toolbar-sep" aria-hidden="true" style="margin-left:auto"></span>
|
||||||
|
<button class="btn btn-ghost" onclick="loadSampleAll()">Load sample data</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dev-banner" id="dev-banner" style="display:none">⚙ DEV MODE — usage tracking paused. This session's actions are not being recorded.</div>
|
<div class="dev-banner" id="dev-banner" style="display:none">⚙ DEV MODE — usage tracking paused. This session's actions are not being recorded.</div>
|
||||||
|
|||||||
@@ -247,7 +247,9 @@ def page_checks(page, base, tok):
|
|||||||
# "Usage data" left this list at T7.10: D5 moved the report to the admin
|
# "Usage data" left this list at T7.10: D5 moved the report to the admin
|
||||||
# console. Its absence HERE is asserted, so the button cannot quietly return
|
# console. Its absence HERE is asserted, so the button cannot quietly return
|
||||||
# and recreate the duplicate D5 existed to remove.
|
# and recreate the duplicate D5 existed to remove.
|
||||||
for label in ("Sample SOP", "Load example", "View SOP", "Import SOP"):
|
# "Sample SOP" + "Load example" became the ONE "Load sample data" at T9.4
|
||||||
|
# (S7): D1's point was that the sample control is reachable, and it still is.
|
||||||
|
for label in ("Load sample data", "View SOP", "Import SOP"):
|
||||||
chk("%-13s is visible on the unframed page" % label, vis.get(label) is True, vis)
|
chk("%-13s is visible on the unframed page" % label, vis.get(label) is True, vis)
|
||||||
chk("Usage data is GONE from the creator toolbar (D5)",
|
chk("Usage data is GONE from the creator toolbar (D5)",
|
||||||
"Usage data" not in vis, vis)
|
"Usage data" not in vis, vis)
|
||||||
|
|||||||
169
tests/sample_check.py
Normal file
169
tests/sample_check.py
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Is there exactly one sample-data affordance, and is it fenced? — S7, T9.4.
|
||||||
|
|
||||||
|
Four affordances under three names, one of them a single click from live
|
||||||
|
project data with no confirm. Now: one control, one name ("Load sample data"),
|
||||||
|
on the creator's toolbar at the far end of two separators from the live
|
||||||
|
actions, confirming before it acts, naming exactly what it does - and unable
|
||||||
|
to touch live project data, verified by attempting it against a real project
|
||||||
|
and reading the server before and after.
|
||||||
|
|
||||||
|
Exit 0 all passed, 1 a failure, 2 could not run.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
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 # noqa: E402
|
||||||
|
from sections_check import set_sop # noqa: E402
|
||||||
|
from stepper_check import dismiss_dialogs # noqa: E402
|
||||||
|
from qa_gate_check import api # noqa: E402
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
HTML = os.path.join(ROOT, "html")
|
||||||
|
|
||||||
|
|
||||||
|
def ascii_(v, n=280):
|
||||||
|
return re.sub(r"\s+", " ", str(v)).encode("ascii", "replace").decode()[:n]
|
||||||
|
|
||||||
|
|
||||||
|
def settle(seconds=0.5):
|
||||||
|
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 main():
|
||||||
|
exe = cdp.find_browser()
|
||||||
|
if not exe:
|
||||||
|
print("no headless-capable browser found; set WP_BROWSER.")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
# ── 1. exactly one, by grep ───────────────────────────────────────────────
|
||||||
|
print("\n1. grep: one affordance, one name")
|
||||||
|
hits = []
|
||||||
|
for name in os.listdir(HTML):
|
||||||
|
if not name.endswith((".html", ".js")):
|
||||||
|
continue
|
||||||
|
src = open(os.path.join(HTML, name), encoding="utf-8").read()
|
||||||
|
for m in re.finditer(r'onclick="(loadSampleAll|loadSampleSOP|loadExample|loadSampleData)\(\)"', src):
|
||||||
|
hits.append((name, m.group(1)))
|
||||||
|
chk("exactly one sample-data control exists in the whole suite",
|
||||||
|
hits == [("wp-creation-index.html", "loadSampleAll")], ascii_(hits))
|
||||||
|
chk("the other three affordances are removed; grep confirms",
|
||||||
|
not any(fn in ("loadSampleData",) for _, fn in hits)
|
||||||
|
and "loadSampleData" not in open(os.path.join(HTML, "work-package-suite-app.js"),
|
||||||
|
encoding="utf-8").read().replace(
|
||||||
|
"// Removed at T9.4", "").split("loadSampleData")[0])
|
||||||
|
|
||||||
|
tmpdir = tempfile.mkdtemp(prefix="wpsuite-sample-")
|
||||||
|
db_path = os.path.join(tmpdir, "check.db")
|
||||||
|
server = None
|
||||||
|
browser = None
|
||||||
|
try:
|
||||||
|
tok = seed(db_path)
|
||||||
|
set_sop(db_path, {})
|
||||||
|
port = cdp.free_port()
|
||||||
|
base = "http://127.0.0.1:%d" % port
|
||||||
|
server = start_server(port, db_path)
|
||||||
|
root = tok["root"]
|
||||||
|
|
||||||
|
browser = cdp.Browser(exe)
|
||||||
|
page = browser.page()
|
||||||
|
page.clear_cookies()
|
||||||
|
page.set_cookie("wp_session", root)
|
||||||
|
page.viewport(1440, 900)
|
||||||
|
page.goto(base + "/wp-creation-index.html?project=projA")
|
||||||
|
dismiss_dialogs(page)
|
||||||
|
chk("the creator boots on a REAL project", wait_creator(page))
|
||||||
|
settle(1.6)
|
||||||
|
|
||||||
|
# ── 2. placement and the confirm ─────────────────────────────────────
|
||||||
|
print("\n2. away from the live actions, confirmed before acting")
|
||||||
|
gap = page.eval("""(() => {
|
||||||
|
const btns = [...document.querySelectorAll('#wp-toolbar button')];
|
||||||
|
const sample = btns.find(b => b.textContent.trim() === 'Load sample data');
|
||||||
|
if (!sample) return null;
|
||||||
|
const prev = sample.previousElementSibling && sample.previousElementSibling
|
||||||
|
.previousElementSibling; // skip the separator
|
||||||
|
const r = sample.getBoundingClientRect();
|
||||||
|
const pr = prev ? prev.getBoundingClientRect() : {right: 0};
|
||||||
|
return {isLast: btns[btns.length-1] === sample, gap: Math.round(r.left - pr.right)};
|
||||||
|
})()""")
|
||||||
|
chk("the control sits at the far end, clearly separated from live actions",
|
||||||
|
gap and gap["isLast"] and gap["gap"] >= 40, ascii_(gap))
|
||||||
|
|
||||||
|
_, sop_before = api(base, "/api/sops/latest?project_id=projA", root)
|
||||||
|
_, wps_before = api(base, "/api/wps?project_id=projA&full=true", root)
|
||||||
|
|
||||||
|
page.eval("void loadSampleAll()")
|
||||||
|
settle(0.5)
|
||||||
|
d = json.loads(page.eval("""JSON.stringify((() => {
|
||||||
|
const ov = document.getElementById('wp-dialog');
|
||||||
|
return {open: ov.classList.contains('open'),
|
||||||
|
msg: (document.getElementById('wp-dialog-msg')||{}).textContent||''};
|
||||||
|
})())"""))
|
||||||
|
chk("it asks first, naming exactly what will happen",
|
||||||
|
d["open"] and "Replaces the SOP shown on this page" in d["msg"]
|
||||||
|
and "nothing is written to the project" in d["msg"], ascii_(d))
|
||||||
|
page.eval("wpDialogCancel()")
|
||||||
|
settle(0.4)
|
||||||
|
chk("backing out changes nothing",
|
||||||
|
page.eval("!(SOP && SOP.meta && SOP.meta.sample)"))
|
||||||
|
|
||||||
|
page.eval("void loadSampleAll()")
|
||||||
|
settle(0.4)
|
||||||
|
page.eval("wpDialogOk()")
|
||||||
|
settle(1.0)
|
||||||
|
chk("confirming loads the sample SOP and the example package, locally",
|
||||||
|
page.eval("!!(SOP && SOP.meta && SOP.meta.sample)")
|
||||||
|
and page.eval("!!gv('wp_subject')"))
|
||||||
|
|
||||||
|
# ── 3. it cannot touch live project data ─────────────────────────────
|
||||||
|
print("\n3. the fence, verified against a real project")
|
||||||
|
settle(1.5) # anything that WOULD sync has had time to
|
||||||
|
_, sop_after = api(base, "/api/sops/latest?project_id=projA", root)
|
||||||
|
_, wps_after = api(base, "/api/wps?project_id=projA&full=true", root)
|
||||||
|
chk("the project's SOP on the server is byte-identical after the sample load",
|
||||||
|
json.dumps(sop_before, sort_keys=True) == json.dumps(sop_after, sort_keys=True))
|
||||||
|
chk("...and so is its work package list",
|
||||||
|
json.dumps(wps_before, sort_keys=True) == json.dumps(wps_after, sort_keys=True))
|
||||||
|
|
||||||
|
js_errors = [e for e in page.js_errors() if "beforeunload" not in e]
|
||||||
|
chk("no JavaScript errors anywhere in this run", not js_errors,
|
||||||
|
ascii_(js_errors[:2]))
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if browser is not None:
|
||||||
|
try:
|
||||||
|
browser.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if server is not None:
|
||||||
|
try:
|
||||||
|
server.terminate()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("\n" + "-" * 54)
|
||||||
|
print("%d/%d checks passed." % (len(_PASS), len(_PASS) + len(_FAIL)))
|
||||||
|
for f in _FAIL:
|
||||||
|
print(" - " + f)
|
||||||
|
return 1 if _FAIL else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -231,48 +231,19 @@ def run(page, base, tok):
|
|||||||
".filter(e => (e.textContent||'').trim()).map(e => e.id))"))
|
".filter(e => (e.textContent||'').trim()).map(e => e.id))"))
|
||||||
chk("no error is shown on a step nobody has tried to leave", not marks, marks)
|
chk("no error is shown on a step nobody has tried to leave", not marks, marks)
|
||||||
|
|
||||||
print("\n5. the wizard opens no native dialog at all")
|
print("\n5. the wizard opens no native dialog, and its sample affordance is gone")
|
||||||
open_wizard(page, base, tok)
|
open_wizard(page, base, tok)
|
||||||
# Drive every path that used to raise one.
|
# T9.4 (S7) removed this page's Load sample - it filled the state that
|
||||||
page.eval("loadSampleData()")
|
# completeSOP() pushes to the LIVE project, one click, no confirm. The
|
||||||
settle(1.6)
|
# suite's one sample control is on the creator. What this section still
|
||||||
chk("loading the sample announces instead of interrupting",
|
# owns: the wizard must not regrow the affordance, and must stay dialog-free.
|
||||||
page.eval("!document.getElementById('wp-toast').hidden"))
|
chk("the wizard's Load sample control is gone (S7/T9.4)",
|
||||||
chk("...politely, because it is a confirmation not an error",
|
page.eval("!document.getElementById('load-sample-btn')")
|
||||||
page.eval("document.getElementById('wp-toast').getAttribute('role')") == "status")
|
and page.eval("typeof loadSampleData === 'undefined'"))
|
||||||
chk("...and says what happened",
|
chk("...and no native dialog fired anywhere so far",
|
||||||
"Sample data loaded" in (page.eval(
|
not json.loads(page.eval("JSON.stringify(window.__dialogs||[])")),
|
||||||
"(document.getElementById('wp-toast')||{}).textContent||''")),
|
page.eval("JSON.stringify(window.__dialogs||[])"))
|
||||||
ascii_(page.eval("(document.getElementById('wp-toast')||{}).textContent||''")))
|
|
||||||
chk("...and can be dismissed from the keyboard", page.eval("""(() => {
|
|
||||||
const b = document.querySelector('#wp-toast .wp-toast-close');
|
|
||||||
if (!b) return false;
|
|
||||||
b.focus();
|
|
||||||
const focused = document.activeElement === b;
|
|
||||||
b.click();
|
|
||||||
return focused && document.getElementById('wp-toast').hidden;
|
|
||||||
})()"""))
|
|
||||||
|
|
||||||
# This used to be "loading the sample on the wrong tab interrupts", because the
|
|
||||||
# wizard's Load sample was context-aware: on the WP tab it reached into the
|
|
||||||
# iframe and called the creator's own loadExample(), and complained when the
|
|
||||||
# frame was not there. B7/T7.1 made the creator a page, so there is no wrong
|
|
||||||
# tab to be on and nothing to reach into - this page's sample is the SOP
|
|
||||||
# sample, always. What is left to check is that the one remaining edge, the
|
|
||||||
# SOP gate, still says something rather than nothing.
|
|
||||||
page.eval("showCreatorGate(); loadSampleData()")
|
|
||||||
settle(1.2)
|
|
||||||
chk("from the gate, loading the sample announces rather than failing silently",
|
|
||||||
page.eval("!document.getElementById('wp-toast').hidden"))
|
|
||||||
chk("...politely, because being on the gate is not an error",
|
|
||||||
page.eval("document.getElementById('wp-toast').getAttribute('role')") == "status",
|
|
||||||
ascii_(page.eval("(document.getElementById('wp-toast')||{}).textContent||''")))
|
|
||||||
chk("...and says where the work package sample actually is",
|
|
||||||
"Work Package Creation" in (page.eval(
|
|
||||||
"(document.getElementById('wp-toast')||{}).textContent||''")),
|
|
||||||
ascii_(page.eval("(document.getElementById('wp-toast')||{}).textContent||''")))
|
|
||||||
page.eval("switchTool('sop')")
|
|
||||||
settle(0.6)
|
|
||||||
|
|
||||||
# Through the control a person uses — the modal's text input — not through the
|
# Through the control a person uses — the modal's text input — not through the
|
||||||
# library helper, which has no duplicate message to give.
|
# library helper, which has no duplicate message to give.
|
||||||
@@ -304,16 +275,12 @@ def run(page, base, tok):
|
|||||||
"no feedback to export" in (page.eval(
|
"no feedback to export" in (page.eval(
|
||||||
"(document.getElementById('wp-toast')||{}).textContent||''")).lower(),
|
"(document.getElementById('wp-toast')||{}).textContent||''")).lower(),
|
||||||
ascii_(page.eval("(document.getElementById('wp-toast')||{}).textContent||''")))
|
ascii_(page.eval("(document.getElementById('wp-toast')||{}).textContent||''")))
|
||||||
page.eval("showAnalytics()")
|
# showAnalytics() left this page at T7.10 (D5): the usage report lives on
|
||||||
settle(0.5)
|
# the admin console now, one implementation for the whole suite. What this
|
||||||
chk("the usage summary is shown rather than crammed into a confirmation",
|
# section keeps: the duplicate must not regrow here.
|
||||||
"Step 1:" in (page.eval("(document.getElementById('wp-toast')||{}).textContent||''")),
|
chk("the wizard's usage-summary duplicate is gone (D5/T7.10)",
|
||||||
ascii_(page.eval("(document.getElementById('wp-toast')||{}).textContent||''"))[:100])
|
page.eval("typeof showAnalytics === 'undefined'")
|
||||||
chk("...with the download offered as an action beside it",
|
and page.eval("typeof analyticsLoad === 'undefined'"))
|
||||||
page.eval("""(() => {
|
|
||||||
const b = document.querySelector('#wp-toast .wp-toast-action');
|
|
||||||
return !!b && /download/i.test(b.textContent);
|
|
||||||
})()"""))
|
|
||||||
|
|
||||||
fired = json.loads(page.eval("JSON.stringify(window.__dialogs||[])"))
|
fired = json.loads(page.eval("JSON.stringify(window.__dialogs||[])"))
|
||||||
chk("nothing anywhere in that opened alert / confirm / prompt", not fired, fired[:4])
|
chk("nothing anywhere in that opened alert / confirm / prompt", not fired, fired[:4])
|
||||||
|
|||||||
Reference in New Issue
Block a user