BL-024 - the last 21 native dialogs, onto the shared kit
S1 counted 79 native dialogs app-wide and its tasks removed 58; the audit found the rest on surfaces no S1 task named: admin.js (6), users.js (10), the launcher's inline script (5). All 21 now go through wp-dialog.js - the T7.9 kit extracted as a self-injecting shared component: markup and styles land on first use, styles are theme tokens only with its own wp-dlg-* class names (the consoles' existing .modal styles are untouched), 44px targets on coarse pointers, and the whole file is guarded so the creator's inline copy - which owns the same-id markup in its HTML - still wins on its own page. The kit's toast comes along (S10 role rules), since none of the three pages had one. Conversion follows the T7.9 precedent: confirms -> wpConfirmDialog with named ok-labels, the password prompt -> wpPromptDialog whose validate() finally enforces min-12 AT the input (it was label-text-only before, server-enforced), API failures with detail -> wpAlertDialog, small info/validation messages -> the announced toast. New probe console_dialogs_check (17): counts pinned at 0, kit guarded and loaded by all three pages, and the users console driven live with natives poisoned - reset a password end to end (short refused inline, good one accepted by the server and announced), cancel a delete and prove nothing died. Items: BL-024 (closed), S1 completed to zero app-wide, C1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -313,6 +313,7 @@ The August 20 integration adds:
|
||||
```bash
|
||||
python tests/assets_check.py # D11 - Micron picker: read-only, degrades 31 checks
|
||||
python tests/critical_reopen_check.py # BL-021 - on-hold mail reaches PM + CM 11 checks
|
||||
python tests/console_dialogs_check.py # BL-024 - consoles/launcher: 21 natives -> 0 17 checks
|
||||
```
|
||||
|
||||
**Three probes were re-pointed at `T7.1`.** `sections_check.py` 5b drove the live
|
||||
|
||||
@@ -525,7 +525,7 @@ deliberately deferred.
|
||||
a product conversation about where it displays and who reads it.
|
||||
- **Suggested wave or follow-up:** next revision; needs Nick for placement.
|
||||
|
||||
### BL-024 — 21 native dialogs remain on the operator consoles and the launcher
|
||||
### BL-024 — CLOSED 2026-08-20 (wp-dialog.js, the T7.9 kit shared; 21 -> 0; `console_dialogs_check` 17)
|
||||
|
||||
- **Found during:** T9.5 (the audit's dialog count)
|
||||
- **Where:** `admin.js` (6), `users.js` (10), `index.html` (5)
|
||||
|
||||
@@ -217,7 +217,8 @@
|
||||
|
||||
<script src="wp-usage.js"></script>
|
||||
<script src="console-util.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
<script src="wp-dialog.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
<!-- The app bar's project switcher reads ProjectData; without this the bar on this
|
||||
page could never show a project and always read "Select a project" (F1). Must
|
||||
parse before wp-chrome.js, which reads it as it mounts. -->
|
||||
|
||||
@@ -146,7 +146,9 @@ async function seedDemo(){
|
||||
snapshot();
|
||||
}
|
||||
async function cleanDemo(){
|
||||
if(!confirm('Delete ALL projects whose number starts with DEMO- or SMOKE- (and their SOPs/WPs via cascade)?')) return;
|
||||
if(!(await wpConfirmDialog({title:'Delete demo data',
|
||||
message:'Delete ALL projects whose number starts with DEMO- or SMOKE- (and their SOPs/WPs via cascade)?',
|
||||
okLabel:'Delete them'}))) return;
|
||||
const o=document.getElementById('demo-out'); o.innerHTML='';
|
||||
// archived=all, or an archived DEMO-/SMOKE- project becomes unreachable from
|
||||
// this button — the default list hides it and nothing else here can delete it.
|
||||
@@ -250,10 +252,12 @@ async function archiveProject(id, name, archived){
|
||||
'• Nothing is deleted. Unarchive here at any time to bring it back.'
|
||||
: 'Unarchive “'+name+'”?\n\n'+
|
||||
'It becomes visible in the pickers again and can be edited as normal.';
|
||||
if(!confirm(ask)) return;
|
||||
if(!(await wpConfirmDialog({title:(archived?'Archive':'Unarchive')+' project',
|
||||
message:ask, okLabel:archived?'Archive':'Unarchive'}))) return;
|
||||
const { status, json } = await api('POST','/api/projects/'+id+'/archive',{archived:!!archived});
|
||||
if(status===200) loadProjects();
|
||||
else alert('Could not '+(archived?'archive':'unarchive')+' '+name+': '+((json && json.detail)||('HTTP '+status)));
|
||||
else wpAlertDialog({title:(archived?'Archive':'Unarchive')+' failed',
|
||||
message:'Could not '+(archived?'archive':'unarchive')+' '+name+': '+((json && json.detail)||('HTTP '+status))});
|
||||
}
|
||||
|
||||
// Named deleteProjectAdmin, not deleteProject: every function in this file is a
|
||||
@@ -261,13 +265,16 @@ async function archiveProject(id, name, archived){
|
||||
// enough to collide with one of them later. The -Admin suffix also says which of the
|
||||
// two project deletions this is — the console's, not a project member's.
|
||||
async function deleteProjectAdmin(id, name){
|
||||
if(!confirm('DELETE “'+name+'” permanently?\n\n'+
|
||||
if(!(await wpConfirmDialog({title:'Delete project permanently',
|
||||
message:'DELETE “'+name+'” permanently?\n\n'+
|
||||
'Its SOP, EVERY work package on it and every access assignment are deleted with it '+
|
||||
'(database cascade). This cannot be undone.\n\n'+
|
||||
'If you only want it out of the way, cancel and use Archive instead.')) return;
|
||||
'If you only want it out of the way, cancel and use Archive instead.',
|
||||
okLabel:'Delete permanently'}))) return;
|
||||
const { status, json } = await api('DELETE','/api/projects/'+id);
|
||||
if(status===200) loadProjects();
|
||||
else alert('Could not delete '+name+': '+((json && json.detail)||('HTTP '+status)));
|
||||
else wpAlertDialog({title:'Delete failed',
|
||||
message:'Could not delete '+name+': '+((json && json.detail)||('HTTP '+status))});
|
||||
}
|
||||
|
||||
// ── default members on new projects ─────────────────────────────────────────────
|
||||
@@ -373,7 +380,8 @@ async function setAutoAdd(id, username){
|
||||
_defMemUsers = _defMemUsers.map(u => u.id===json.id ? json : u);
|
||||
renderDefaultMembers();
|
||||
} else {
|
||||
alert('Could not change the new-project default for '+username+': '+((json && json.detail)||('HTTP '+status)));
|
||||
wpAlertDialog({title:'Change failed',
|
||||
message:'Could not change the new-project default for '+username+': '+((json && json.detail)||('HTTP '+status))});
|
||||
loadDefaultMembers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,6 +593,7 @@
|
||||
<script src="feedback-config.js"></script>
|
||||
<script src="project-data.js"></script>
|
||||
<script src="help.js"></script>
|
||||
<script src="wp-dialog.js"></script>
|
||||
<script>
|
||||
// ── PROJECT SELECTION ─────────────────────────────────────────────────────
|
||||
const esc = ProjectData.esc;
|
||||
@@ -1046,7 +1047,7 @@
|
||||
const text = document.getElementById('comment-text').value.trim();
|
||||
|
||||
if (!text) {
|
||||
alert('Please enter feedback.');
|
||||
toast('Please enter feedback.', 'alert');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1067,7 +1068,7 @@
|
||||
function exportFeedback() {
|
||||
const saved = localStorage.getItem('wp_suite_index_comments');
|
||||
const data = saved ? JSON.parse(saved) : [];
|
||||
if (!data.length) { alert('No feedback to export yet.'); return; }
|
||||
if (!data.length) { toast('No feedback to export yet.', 'alert'); return; }
|
||||
const payload = { app: 'Work Package Suite', source: 'home', exportedAt: new Date().toISOString(), comments: data };
|
||||
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
|
||||
const a = document.createElement('a');
|
||||
@@ -1085,7 +1086,7 @@
|
||||
try {
|
||||
const inc = JSON.parse(r.result);
|
||||
const incoming = Array.isArray(inc) ? inc : (inc.comments || []);
|
||||
if (!incoming.length) { alert('No feedback found in that file.'); return; }
|
||||
if (!incoming.length) { toast('No feedback found in that file.', 'alert'); return; }
|
||||
const saved = localStorage.getItem('wp_suite_index_comments');
|
||||
allComments = saved ? JSON.parse(saved) : [];
|
||||
const seen = new Set(allComments.map(c => c.timestamp + '|' + c.text));
|
||||
@@ -1093,8 +1094,8 @@
|
||||
incoming.forEach(c => { const k = c.timestamp + '|' + c.text; if (c.text && !seen.has(k)) { allComments.push(c); seen.add(k); added++; } });
|
||||
localStorage.setItem('wp_suite_index_comments', JSON.stringify(allComments));
|
||||
loadComments();
|
||||
alert('Imported ' + added + ' feedback item' + (added === 1 ? '' : 's') + '.');
|
||||
} catch (e) { alert('Could not read that file.'); }
|
||||
toast('Imported ' + added + ' feedback item' + (added === 1 ? '' : 's') + '.');
|
||||
} catch (e) { toast('Could not read that file.', 'alert'); }
|
||||
ev.target.value = '';
|
||||
};
|
||||
r.readAsText(f);
|
||||
|
||||
@@ -102,7 +102,8 @@
|
||||
</div>
|
||||
|
||||
<script src="console-util.js"></script>
|
||||
<script src="users.js"></script>
|
||||
<script src="wp-dialog.js"></script>
|
||||
<script src="users.js"></script>
|
||||
<!-- The app bar's project switcher reads ProjectData; without this the bar on this
|
||||
page could never show a project and always read "Select a project" (F1). Must
|
||||
parse before wp-chrome.js, which reads it as it mounts. -->
|
||||
|
||||
@@ -255,37 +255,44 @@ function projAccessCell(u){
|
||||
// Each one reloads on failure so a control can never sit there showing a value the
|
||||
// server refused.
|
||||
async function resetPw(id, username){
|
||||
const pw = prompt('New password for "'+username+'" (min 12 characters):');
|
||||
// The min-12 rule was stated in the prompt label and enforced only by the
|
||||
// server round-trip; the kit's validate() answers AT the input instead.
|
||||
const pw = await wpPromptDialog({title:'Reset password',
|
||||
message:'Set a new password for "'+username+'". Their existing sessions are signed out.',
|
||||
label:'New password (min 12 characters)',
|
||||
validate:v => (v && v.length >= 12) ? '' : 'At least 12 characters.'});
|
||||
if(pw === null) return;
|
||||
const { status, json } = await api('POST','/api/auth/users/'+id+'/password',{new_password:pw});
|
||||
if(status === 200) alert('Password reset for '+username+'. Their existing sessions are signed out.');
|
||||
else alert('Could not reset the password: '+apiError(status, json));
|
||||
if(status === 200) toast('Password reset for '+username+'. Their existing sessions are signed out.');
|
||||
else wpAlertDialog({title:'Reset failed', message:'Could not reset the password: '+apiError(status, json)});
|
||||
}
|
||||
|
||||
async function toggleActive(id, makeActive){
|
||||
const { status, json } = await api('POST','/api/auth/users/'+id+'/active',{is_active:makeActive});
|
||||
if(status === 200) loadUsers();
|
||||
else { alert('Could not change that account: '+apiError(status, json)); loadUsers(); }
|
||||
else { wpAlertDialog({title:'Change failed', message:'Could not change that account: '+apiError(status, json)}); loadUsers(); }
|
||||
}
|
||||
|
||||
async function changeRole(id, role, username){
|
||||
const { status, json } = await api('POST','/api/auth/users/'+id+'/role',{role});
|
||||
if(status !== 200) alert('Could not change permissions for '+username+': '+apiError(status, json));
|
||||
if(status !== 200) wpAlertDialog({title:'Change failed', message:'Could not change permissions for '+username+': '+apiError(status, json)});
|
||||
loadUsers();
|
||||
}
|
||||
|
||||
async function changeProjectRole(id, project_role, username){
|
||||
const { status, json } = await api('POST','/api/auth/users/'+id+'/project-role',{project_role});
|
||||
if(status !== 200) alert('Could not set the project role for '+username+': '+apiError(status, json));
|
||||
if(status !== 200) wpAlertDialog({title:'Change failed', message:'Could not set the project role for '+username+': '+apiError(status, json)});
|
||||
loadUsers();
|
||||
}
|
||||
|
||||
async function deleteUser(id, username){
|
||||
if(!confirm('Delete user "'+username+'"?\n\nTheir account and every project assignment go with it. '+
|
||||
'This cannot be undone — disable the account instead if you only want to block sign-in.')) return;
|
||||
if(!(await wpConfirmDialog({title:'Delete user',
|
||||
message:'Delete user "'+username+'"?\n\nTheir account and every project assignment go with it. '+
|
||||
'This cannot be undone — disable the account instead if you only want to block sign-in.',
|
||||
okLabel:'Delete user'}))) return;
|
||||
const { status, json } = await api('DELETE','/api/auth/users/'+id);
|
||||
if(status === 200) loadUsers();
|
||||
else alert('Could not delete '+username+': '+apiError(status, json));
|
||||
else wpAlertDialog({title:'Delete failed', message:'Could not delete '+username+': '+apiError(status, json)});
|
||||
}
|
||||
|
||||
// ── create ────────────────────────────────────────────────────────────────────
|
||||
@@ -368,7 +375,7 @@ async function createUser(){
|
||||
// more the person is on, and a save leaves those others untouched.
|
||||
async function manageProjects(id, username){
|
||||
const { status, json } = await api('GET','/api/auth/users/'+id+'/projects');
|
||||
if(status !== 200 || !json){ alert('Could not load projects: '+apiError(status, json)); return; }
|
||||
if(status !== 200 || !json){ wpAlertDialog({title:'Could not load projects', message:'Could not load projects: '+apiError(status, json)}); return; }
|
||||
openProjectModal(id, username, json);
|
||||
}
|
||||
function closeProjectModal(){ const m = document.getElementById('proj-modal'); if(m) m.remove(); }
|
||||
@@ -453,7 +460,7 @@ function openProjectModal(userId, username, data){
|
||||
const { status, json } = await api('PUT','/api/auth/users/'+userId+'/projects',
|
||||
{ project_ids: ids, roles: roleMap });
|
||||
if(status === 200){ closeProjectModal(); loadUsers(); }
|
||||
else alert('Save failed: '+apiError(status, json));
|
||||
else wpAlertDialog({title:'Save failed', message:'Save failed: '+apiError(status, json)});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
172
html/wp-dialog.js
Normal file
172
html/wp-dialog.js
Normal file
@@ -0,0 +1,172 @@
|
||||
/* Dialog kit + toast, shared (BL-024, 2026-08-20).
|
||||
*
|
||||
* The T7.9 kit, extracted for the pages the S1 tasks never named: the launcher
|
||||
* (index.html), the admin console and the user console carried 21 native
|
||||
* dialogs between them. Same contract as the creator's copy:
|
||||
*
|
||||
* wpConfirmDialog({title, message, okLabel, cancelLabel}) -> Promise<bool>
|
||||
* wpPromptDialog({title, message, label, value, validate}) -> Promise<string|null>
|
||||
* wpAlertDialog({title, message, okLabel}) -> Promise (value not meaningful)
|
||||
* toast(msg, kind) kind 'alert' interrupts (role=alert); default role=status
|
||||
*
|
||||
* Self-contained on purpose: markup and styles are injected on first use, the
|
||||
* styles are theme tokens only (the token rule), and the class names are its
|
||||
* own (wp-dlg-*) so the consoles' existing .modal styles are never touched.
|
||||
* The creator keeps its inline copy - it owns the same-id markup in its HTML -
|
||||
* so everything here is guarded: if the page already has the kit, this file
|
||||
* defines nothing.
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
if (typeof global.wpConfirmDialog === 'function') return; // the creator's copy wins
|
||||
|
||||
var CSS =
|
||||
'#wp-dlg-overlay{position:fixed;inset:0;background:var(--wp-scrim-cool-strong);' +
|
||||
'display:none;align-items:center;justify-content:center;z-index:10500;padding:20px;}' +
|
||||
'#wp-dlg-overlay.open{display:flex;}' +
|
||||
'.wp-dlg{background:var(--cds-layer);color:var(--cds-text-primary);max-width:480px;width:100%;' +
|
||||
'border-radius:8px;box-shadow:0 12px 40px rgba(20,30,50,.3);overflow:hidden;' +
|
||||
'font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;font-size:14px;}' +
|
||||
'.wp-dlg-head{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;' +
|
||||
'border-bottom:1px solid var(--cds-border-subtle);font-weight:700;}' +
|
||||
'.wp-dlg-x{background:none;border:none;font-size:18px;line-height:1;cursor:pointer;' +
|
||||
'color:var(--cds-text-secondary);padding:4px 6px;}' +
|
||||
'.wp-dlg-x:focus-visible{outline:2px solid var(--cds-focus);outline-offset:1px;}' +
|
||||
'.wp-dlg-body{padding:16px 18px;}' +
|
||||
'#wp-dlg-msg{white-space:pre-wrap;line-height:1.5;}' +
|
||||
'#wp-dlg-input-wrap{margin-top:10px;}' +
|
||||
'#wp-dlg-input-wrap label{display:block;font-size:12px;margin-bottom:4px;color:var(--cds-text-secondary);}' +
|
||||
'#wp-dlg-input{width:100%;box-sizing:border-box;padding:8px 10px;font:inherit;' +
|
||||
'border:1px solid var(--cds-border-strong);border-radius:4px;background:var(--cds-field);}' +
|
||||
'#wp-dlg-input:focus{outline:2px solid var(--cds-focus);outline-offset:-1px;}' +
|
||||
'#wp-dlg-err{color:var(--cds-text-error);font-size:12px;font-weight:600;margin-top:4px;}' +
|
||||
'#wp-dlg-err:empty{display:none;}' +
|
||||
'.wp-dlg-foot{display:flex;justify-content:flex-end;gap:10px;padding:12px 18px;' +
|
||||
'border-top:1px solid var(--cds-border-subtle);}' +
|
||||
'.wp-dlg-btn{font:inherit;font-weight:600;padding:8px 16px;border-radius:6px;cursor:pointer;' +
|
||||
'border:1px solid var(--cds-border-strong);background:var(--cds-layer);color:var(--cds-text-primary);}' +
|
||||
'.wp-dlg-btn.primary{background:var(--cds-interactive-01);border-color:var(--cds-interactive-01);' +
|
||||
'color:var(--cds-text-on-color);}' +
|
||||
'.wp-dlg-btn:focus-visible{outline:2px solid var(--cds-focus);outline-offset:1px;}' +
|
||||
'@media(pointer:coarse){.wp-dlg-btn{min-height:44px;}.wp-dlg-x{min-width:44px;min-height:44px;}}' +
|
||||
'#toast{position:fixed;bottom:26px;left:50%;transform:translateX(-50%) translateY(20px);' +
|
||||
'background:var(--cds-background-inverse);color:var(--cds-text-inverse);padding:9px 16px;' +
|
||||
'border-radius:6px;font-size:13px;opacity:0;transition:opacity .18s,transform .18s;' +
|
||||
'pointer-events:none;z-index:10600;max-width:min(480px,calc(100vw - 32px));}' +
|
||||
'#toast.show{opacity:1;transform:translateX(-50%) translateY(0);}';
|
||||
|
||||
function ensure() {
|
||||
var ov = document.getElementById('wp-dlg-overlay');
|
||||
if (ov) return ov;
|
||||
var st = document.createElement('style');
|
||||
st.textContent = CSS;
|
||||
document.head.appendChild(st);
|
||||
ov = document.createElement('div');
|
||||
ov.id = 'wp-dlg-overlay';
|
||||
ov.setAttribute('role', 'dialog');
|
||||
ov.setAttribute('aria-modal', 'true');
|
||||
ov.setAttribute('aria-labelledby', 'wp-dlg-title');
|
||||
ov.innerHTML =
|
||||
'<div class="wp-dlg">' +
|
||||
'<div class="wp-dlg-head"><div id="wp-dlg-title"></div>' +
|
||||
'<button type="button" class="wp-dlg-x" id="wp-dlg-x" title="Cancel" aria-label="Cancel">✕</button></div>' +
|
||||
'<div class="wp-dlg-body">' +
|
||||
'<div id="wp-dlg-msg"></div>' +
|
||||
'<div id="wp-dlg-input-wrap">' +
|
||||
'<label id="wp-dlg-label" for="wp-dlg-input"></label>' +
|
||||
'<input type="text" id="wp-dlg-input">' +
|
||||
'<div id="wp-dlg-err" role="alert"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="wp-dlg-foot">' +
|
||||
'<button type="button" class="wp-dlg-btn" id="wp-dlg-cancel">Cancel</button>' +
|
||||
'<button type="button" class="wp-dlg-btn primary" id="wp-dlg-ok">OK</button>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(ov);
|
||||
document.getElementById('wp-dlg-x').addEventListener('click', cancel);
|
||||
document.getElementById('wp-dlg-cancel').addEventListener('click', cancel);
|
||||
document.getElementById('wp-dlg-ok').addEventListener('click', ok);
|
||||
document.getElementById('wp-dlg-input').addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') ok();
|
||||
});
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && ov.classList.contains('open')) cancel();
|
||||
});
|
||||
return ov;
|
||||
}
|
||||
|
||||
var resolveFn = null;
|
||||
|
||||
function open(opts) {
|
||||
return new Promise(function (res) {
|
||||
resolveFn = res;
|
||||
var ov = ensure();
|
||||
ov._opts = opts || {};
|
||||
document.getElementById('wp-dlg-title').textContent = opts.title || 'Confirm';
|
||||
document.getElementById('wp-dlg-msg').textContent = opts.message || '';
|
||||
document.getElementById('wp-dlg-input-wrap').style.display = opts.input ? '' : 'none';
|
||||
document.getElementById('wp-dlg-label').textContent = opts.label || '';
|
||||
var inp = document.getElementById('wp-dlg-input');
|
||||
inp.value = (opts.value != null ? String(opts.value) : '');
|
||||
document.getElementById('wp-dlg-err').textContent = '';
|
||||
document.getElementById('wp-dlg-ok').textContent = opts.okLabel || 'OK';
|
||||
var cb = document.getElementById('wp-dlg-cancel');
|
||||
cb.textContent = opts.cancelLabel || 'Cancel';
|
||||
cb.style.display = opts.okOnly ? 'none' : '';
|
||||
ov.classList.add('open');
|
||||
setTimeout(function () {
|
||||
(opts.input ? inp : document.getElementById('wp-dlg-ok')).focus();
|
||||
}, 0);
|
||||
});
|
||||
}
|
||||
|
||||
function close(val) {
|
||||
var ov = document.getElementById('wp-dlg-overlay');
|
||||
if (ov) ov.classList.remove('open');
|
||||
var r = resolveFn;
|
||||
resolveFn = null;
|
||||
if (r) r(val);
|
||||
}
|
||||
|
||||
function ok() {
|
||||
var ov = document.getElementById('wp-dlg-overlay');
|
||||
var opts = (ov && ov._opts) || {};
|
||||
if (opts.input) {
|
||||
var v = document.getElementById('wp-dlg-input').value;
|
||||
if (opts.validate) {
|
||||
var err = opts.validate(v);
|
||||
if (err) {
|
||||
document.getElementById('wp-dlg-err').textContent = err;
|
||||
document.getElementById('wp-dlg-input').focus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
close(v);
|
||||
} else close(true);
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
var ov = document.getElementById('wp-dlg-overlay');
|
||||
var opts = (ov && ov._opts) || {};
|
||||
close(opts.input ? null : false);
|
||||
}
|
||||
|
||||
global.wpConfirmDialog = function (opts) { return open(Object.assign({}, opts, { input: false })); };
|
||||
global.wpPromptDialog = function (opts) { return open(Object.assign({}, opts, { input: true })); };
|
||||
global.wpAlertDialog = function (opts) { return open(Object.assign({}, opts, { input: false, okOnly: true })); };
|
||||
|
||||
if (typeof global.toast !== 'function') {
|
||||
// S10's rule, same as the creator: role BEFORE text, 'alert' interrupts.
|
||||
global.toast = function (msg, kind) {
|
||||
ensure();
|
||||
var t = document.getElementById('toast');
|
||||
if (!t) { t = document.createElement('div'); t.id = 'toast'; document.body.appendChild(t); }
|
||||
t.setAttribute('role', kind === 'alert' ? 'alert' : 'status');
|
||||
t.textContent = msg;
|
||||
t.classList.add('show');
|
||||
clearTimeout(global.toast._t);
|
||||
global.toast._t = setTimeout(function () { t.classList.remove('show'); }, 2200);
|
||||
};
|
||||
}
|
||||
})(window);
|
||||
149
tests/console_dialogs_check.py
Normal file
149
tests/console_dialogs_check.py
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Are the consoles' and launcher's 21 native dialogs gone? — BL-024, 2026-08-20.
|
||||
|
||||
S1 counted 79 native dialogs app-wide; its two tasks (T5.8 wizard, T7.9
|
||||
creator) removed 58 and the audit found the remaining 21 on surfaces no S1
|
||||
task named: admin.js (6), users.js (10), the launcher's inline script (5).
|
||||
They now go through `wp-dialog.js` — the T7.9 kit extracted as a shared,
|
||||
self-injecting component (guarded so the creator's inline copy still wins on
|
||||
its own page).
|
||||
|
||||
Static half greps the counts; browser half drives the password-reset prompt on
|
||||
the users console with natives poisoned, and proves validate() answers AT the
|
||||
input while the server round-trip completes end to end.
|
||||
|
||||
Boots its own throwaway SQLite + uvicorn + headless browser; run it alone.
|
||||
Exit 0 all passed, 1 a failure, 2 could not run.
|
||||
"""
|
||||
import io
|
||||
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 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=240):
|
||||
return re.sub(r"\s+", " ", str(v)).encode("ascii", "replace").decode()[:n]
|
||||
|
||||
|
||||
def strip_js(src):
|
||||
src = re.sub(r"/\*.*?\*/", "", src, flags=re.S)
|
||||
return "\n".join(re.sub(r"(?<![:'\"])//.*$", "", ln) for ln in src.split("\n"))
|
||||
|
||||
|
||||
def natives(name):
|
||||
code = strip_js(io.open(os.path.join(HTML, name), encoding="utf-8").read())
|
||||
return len(re.findall(r"(?<![\w.$])(alert|confirm|prompt)\(", code))
|
||||
|
||||
|
||||
def main():
|
||||
exe = cdp.find_browser()
|
||||
if not exe:
|
||||
print("no headless-capable browser found; set WP_BROWSER.")
|
||||
return 2
|
||||
|
||||
print("\n1. the counts (baseline 6 + 10 + 5 = 21)")
|
||||
for name in ("admin.js", "users.js", "index.html"):
|
||||
chk("%s: 0 native dialogs" % name, natives(name) == 0, natives(name))
|
||||
chk("wp-dialog.js exists, has the guard, and no natives of its own",
|
||||
natives("wp-dialog.js") == 0
|
||||
and "typeof global.wpConfirmDialog === 'function'" in
|
||||
io.open(os.path.join(HTML, "wp-dialog.js"), encoding="utf-8").read())
|
||||
for page in ("index.html", "admin.html", "users.html"):
|
||||
chk("%s loads the kit" % page,
|
||||
'src="wp-dialog.js"' in io.open(os.path.join(HTML, page), encoding="utf-8").read())
|
||||
chk("the creator keeps its own copy (it owns the same-id markup in its HTML)",
|
||||
"function wpConfirmDialog" in
|
||||
io.open(os.path.join(HTML, "wp-creation-app.js"), encoding="utf-8").read())
|
||||
|
||||
tmpdir = tempfile.mkdtemp(prefix="wpsuite-condlg-")
|
||||
db_path = os.path.join(tmpdir, "check.db")
|
||||
server = None
|
||||
browser = None
|
||||
try:
|
||||
tok = seed(db_path)
|
||||
port = cdp.free_port()
|
||||
base = "http://127.0.0.1:%d" % port
|
||||
server = start_server(port, db_path)
|
||||
|
||||
print("\n2. the users console, natives poisoned")
|
||||
browser = cdp.Browser(exe)
|
||||
page = browser.page()
|
||||
page.clear_cookies()
|
||||
page.set_cookie("wp_session", tok["root"])
|
||||
page.viewport(1440, 900)
|
||||
page.goto(base + "/users.html")
|
||||
time.sleep(2.0)
|
||||
page.eval("window.alert=()=>{throw new Error('native alert reached')};"
|
||||
"window.confirm=()=>{throw new Error('native confirm reached')};"
|
||||
"window.prompt=()=>{throw new Error('native prompt reached')};")
|
||||
chk("the console booted with a user table",
|
||||
page.eval("!!document.querySelector('table')"))
|
||||
|
||||
page.eval("void resetPw('user_pat','pat')")
|
||||
time.sleep(0.4)
|
||||
chk("the reset prompt is the kit's modal, open, focused at the input",
|
||||
page.eval("(() => { const o=document.getElementById('wp-dlg-overlay');"
|
||||
" return !!o && o.classList.contains('open')"
|
||||
" && document.activeElement.id==='wp-dlg-input'; })()"))
|
||||
page.eval("document.getElementById('wp-dlg-input').value='short';"
|
||||
"document.getElementById('wp-dlg-ok').click()")
|
||||
chk("a short password is refused AT the input - dialog stays, error says why",
|
||||
page.eval("(() => { const o=document.getElementById('wp-dlg-overlay');"
|
||||
" return o.classList.contains('open')"
|
||||
" && /12 characters/.test(document.getElementById('wp-dlg-err').textContent); })()"))
|
||||
page.eval("document.getElementById('wp-dlg-input').value='CorrectHorseBattery10';"
|
||||
"document.getElementById('wp-dlg-ok').click()")
|
||||
time.sleep(1.2)
|
||||
chk("a good answer closes the dialog and the server accepts it",
|
||||
page.eval("!document.getElementById('wp-dlg-overlay').classList.contains('open')"))
|
||||
chk("...announced through the kit's toast (role=status)",
|
||||
page.eval("(() => { const t=document.getElementById('toast');"
|
||||
" return !!t && t.getAttribute('role')==='status'"
|
||||
" && /Password reset for pat/.test(t.textContent); })()"))
|
||||
st, _ = api(base, "/api/auth/login", "x", "POST",
|
||||
{"username": "pat", "password": "CorrectHorseBattery10"})
|
||||
chk("...and the new password actually works", st == 200, st)
|
||||
|
||||
print("\n3. destroy needs a real yes")
|
||||
page.eval("void deleteUser('user_bob','bob')")
|
||||
time.sleep(0.4)
|
||||
chk("the delete asks through the kit, spelling out what goes with it",
|
||||
page.eval("(() => { const o=document.getElementById('wp-dlg-overlay');"
|
||||
" return o.classList.contains('open')"
|
||||
" && /cannot be undone/.test(document.getElementById('wp-dlg-msg').textContent); })()"))
|
||||
page.eval("document.getElementById('wp-dlg-cancel').click()")
|
||||
time.sleep(0.6)
|
||||
st, users = api(base, "/api/auth/users", tok["root"])
|
||||
chk("cancel means no: bob is still an account",
|
||||
st == 200 and any(u.get("username") == "bob" for u in (users or [])),
|
||||
ascii_([u.get("username") for u in (users or [])]))
|
||||
errs = [e for e in page.js_errors() if "beforeunload" not in e]
|
||||
chk("no JavaScript errors, and no path reached a native dialog (they throw here)",
|
||||
not errs, ascii_(errs[:3]))
|
||||
finally:
|
||||
if browser:
|
||||
browser.close()
|
||||
if server:
|
||||
server.terminate()
|
||||
|
||||
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())
|
||||
Reference in New Issue
Block a user