v3.30.4: the in-app dialogs (native alert/confirm/prompt replaced)

The native browser dialogs block the page and look foreign inside the
app.  One shared modal (#pysim-dialog-modal) with the promise-based
helpers pysimAlert/pysimConfirm/pysimPrompt replaces all 71 call sites
(60 alert, 10 confirm, 1 prompt in profilerNew); deferredPrompt.prompt()
is the PWA install prompt and is untouched.

The helpers serialize through ONE promise queue (overlapping dialogs
appear in order, no message is overwritten).  Escape cancels; Enter
accepts except with the Cancel button focused (native behaviour); key
repeats/IME are ignored; a POINTER answer within 500 ms of a dialog
opening is dropped (the buttons keep the same position, so a
double-click's second click would otherwise answer the next queued
dialog - e.g. an alert queued before a destructive confirm).  Backdrop
clicks are ignored.  The dialog is role="dialog" aria-modal="true" and
traps Tab (the handler cycles the controls the dialog shows: alert OK;
confirm Cancel/OK; prompt Cancel/input/OK), so the focus can neither
leave the modal nor reach a background control (Space would activate
it).  When the modal is absent the helpers fall back to the native
dialogs - the Node tests extract single functions without the markup.

Alerts are fire-and-forget (a notification, not a decision); the 10
confirms and the prompt are awaited, which made five functions async
(scriptsDelete, pysimCustomRemove, profilerNew, profilerDelete,
snapshotDelete).  Because the modal no longer freezes the page, the
delete flows that mutated by index after the confirmation mutate by
identity now (profilerDelete/snapshotDelete/pysimCustomRemove/
testSuiteDeleteScript/testSuiteDelete): a list reorder during the open
dialog would otherwise delete the wrong entry.

The nine dialog messages that were still raw English literals are
t()-wrapped - three of them (No secured packet to verify/send, Card
reader server is not connected) activated RU entries that had existed
unused because they were never wrapped; Import error and Snapshot save
failed are new keys.

Drive-by: the batch modal's #ram-op-batch-info used whitespace-pre-line,
which has no rule in the prebuilt style.css (the v3.30.0 stale-style
trap), so its notice never line-broke - now whitespace-pre-wrap.

Tests: a new frontend/tests/dialogs.test.js (15 tests: the modal path,
the queue, the double-answer guard, the keyboard, the grace, the trap,
the fallback, the sweep pin, the stylesheet pin) plus the async/helper
updates in 8 existing test files.  945 frontend + 952 Python;
sw cache simple-v454.
This commit is contained in:
2026-10-18 00:30:06 +03:00
parent ea1d943432
commit 9a688207ba
13 changed files with 747 additions and 98 deletions
+5
View File
@@ -60,6 +60,11 @@ code = extractConst(html, 'CARDS_TAR_DEFAULTS') + extractConst(html, 'CARDS_TAR_
+ 'globalThis.navReplace = function() {};\n' + code;
eval(code);
globalThis.t = s => s;
// The dialog sweep replaced the native alert/confirm with the helpers: route
// them to the native stubs each test installs on globalThis.
globalThis.pysimAlert = m => globalThis.alert(m);
globalThis.pysimConfirm = async m => globalThis.confirm(m);
globalThis.pysimPrompt = async (m, v) => globalThis.prompt(m, v);
const CARD_IDS = ['cards-name','cards-iccid','cards-release','cards-pin1','cards-puk1','cards-pin2','cards-puk2','cards-adm',
'cards-tars','cards-list-view','cards-editor-view','cards-editor-title','cards-new-btn',
+5
View File
@@ -39,6 +39,11 @@ for (const fn of ['swapNibbles', 'encIccid', 'decIccid',
code += extractConst(html, 'CARDS_TAR_DEFAULTS') + extractConst(html, 'CARDS_TAR_ROLES')
+ extractConst(html, 'CARDS_MSL_DEFAULT');
eval(code);
// The dialog sweep replaced the native alert/confirm with the helpers: route
// them to the native stubs each test installs on globalThis.
globalThis.pysimAlert = m => globalThis.alert(m);
globalThis.pysimConfirm = async m => globalThis.confirm(m);
globalThis.pysimPrompt = async (m, v) => globalThis.prompt(m, v);
// live-card style raw EF.ICCID content (nibble-swapped digits + 'F' pad)
const RAW_HEX = '980711090000640090F8';
+3
View File
@@ -50,6 +50,9 @@ const app = new Function('els', `
var t = k => k;
var alert = m => { sandbox.alerts.push(String(m)); };
var confirm = m => { sandbox.confirmCalls = (sandbox.confirmCalls || []); sandbox.confirmCalls.push(String(m)); return sandbox.confirmResult; };
var pysimAlert = m => alert(m);
var pysimConfirm = async m => confirm(m);
var pysimPrompt = async () => ''; // not used by the outfile flow
var ioStatus = (id, msg) => { sandbox.io.push([id, msg]); };
var pysimFetchOk = async (path, body) => {
sandbox.calls.push({ path: path, body: body });
+12 -6
View File
@@ -5,7 +5,7 @@ const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
function extractFunc(src, name, asyncFn) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
@@ -18,7 +18,7 @@ function extractFunc(src, name) {
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1);
}
let code = 'var pysimCustomFiles = [];\nvar pysimCustomEditIndex = null;\nvar _pysimCustomDropped = 0;\nvar pysimFsTreeRoot = null;\n';
@@ -29,11 +29,17 @@ for (const fn of ['pysimCustomNormPath', 'pysimCustomKindForName', 'pysimCustomF
'pysimCustomRootChanged', 'pysimCustomParentInput', 'pysimCustomSubmit',
'pysimCustomEdit', 'pysimCustomEditCancel', 'pysimCustomRemove', 'pysimCustomRender',
'pysimFsNodePath', 'pysimFsFindNodeByPath', 'pysimCustomInject', 'pysimCustomRefreshTree']) {
code += extractFunc(html, fn) + '\n';
// pysimCustomRemove is async (it awaits the shared confirm dialog)
code += extractFunc(html, fn, fn === 'pysimCustomRemove') + '\n';
}
code += html.match(/const CUSTOM_ROOTS = \[[^\]]*\];/)[0].replace('const ', 'var ') + '\n';
code += 'globalThis.esc = s => s;\nglobalThis.t = s => s;\nglobalThis.pysimFsRenderTree = () => {};\n';
eval(code);
// The dialog sweep replaced the native alert/confirm with the helpers: route
// them to the native stubs setup() installs on globalThis.
globalThis.pysimAlert = m => globalThis.alert(m);
globalThis.pysimConfirm = async m => globalThis.confirm(m);
globalThis.pysimPrompt = async (m, v) => globalThis.prompt(m, v);
function fakeEl(id) {
const classes = new Set();
@@ -262,17 +268,17 @@ test('editing an entry to another defined path is rejected', () => {
assert.strictEqual(pysimCustomEditIndex, 0);
});
test('deleting a DF cascades to its children only after confirmation', () => {
test('deleting a DF cascades to its children only after confirmation', async () => {
setup([
{ path: 'MF/A153', name: 'DF.A1', kind: 'df' },
{ path: 'MF/A153/4954', name: 'EF.SPNS', kind: 'ef' },
{ path: 'MF/6F46', name: 'EF.SPN', kind: 'ef' },
]);
globalThis.confirmResult = false;
pysimCustomRemove(0);
await pysimCustomRemove(0);
assert.strictEqual(pysimCustomFiles.length, 3, 'cancelled delete must keep everything');
globalThis.confirmResult = true;
pysimCustomRemove(0);
await pysimCustomRemove(0);
assert.deepStrictEqual(pysimCustomFiles.map(c => c.path), ['MF/6F46']);
});
+449
View File
@@ -0,0 +1,449 @@
// In-app dialog tests: the shared alert/confirm/prompt modal, its promise
// queue and the native fallback. Mirrors the ram_batch harness (extractFunc
// from index.html, a sandbox built with new Function, an els map and a
// document stub).
const { test } = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function extractFunc(src, name) {
const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{');
const m = re.exec(src);
if (!m) throw new Error('function ' + name + ' not found');
let i = m.index + m[0].length - 1;
let depth = 0;
for (; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) break;
}
}
return src.slice(m.index, i + 1);
}
let code = '';
for (const fn of ['pysimAlert', 'pysimConfirm', 'pysimPrompt', '_pysimDialogShow',
'_pysimDialogNow', '_pysimDialogRun', 'pysimDialogAnswer', '_pysimDialogKeydown']) {
code += extractFunc(html, fn) + '\n';
}
const els = {};
function setEl(id, value) {
els[id] = {
id: id, value: value === undefined ? '' : value, textContent: '', innerHTML: '',
disabled: false, style: {}, focused: false, selected: false,
classes: {},
classList: {
add(c) { els[id].classes[c] = true; },
remove(c) { delete els[id].classes[c]; },
toggle(c, on) { if (on) els[id].classes[c] = true; else delete els[id].classes[c]; },
contains(c) { return !!els[id].classes[c]; },
},
focus() { els[id].focused = true; },
select() { els[id].selected = true; },
};
return els[id];
}
function clearEls() {
for (const k of Object.keys(els)) delete els[k];
}
// The five elements the dialog touches.
function setDialogEls() {
for (const id of ['pysim-dialog-modal', 'pysim-dialog-message', 'pysim-dialog-input',
'pysim-dialog-cancel', 'pysim-dialog-ok']) setEl(id, '');
}
// The document stub records the keydown listener, so the tests exercise the
// open/close wiring instead of calling the handler directly.
const listeners = {};
const documentStub = {
getElementById: id => els[id] || null,
activeElement: null,
addEventListener: (type, fn) => { (listeners[type] = listeners[type] || []).push(fn); },
removeEventListener: (type, fn) => {
const a = listeners[type] || [];
const i = a.indexOf(fn);
if (i >= 0) a.splice(i, 1);
},
};
function pressKey(key, extra) {
const ev = Object.assign({ key: key, preventDefault() {} }, extra || {});
for (const fn of (listeners['keydown'] || []).slice()) fn(ev);
}
const tick = () => new Promise(r => setImmediate(r));
const app = new Function('els', 'document', `
var sandbox = { alerts: [], confirms: [], prompts: [], confirmAnswer: true, promptAnswer: '' };
var alert = m => sandbox.alerts.push(String(m));
var confirm = m => { sandbox.confirms.push(String(m)); return sandbox.confirmAnswer; };
var prompt = (m, v) => { sandbox.prompts.push([String(m), v]); return sandbox.promptAnswer; };
var _pysimDialogQueue = Promise.resolve();
var _pysimDialogResolve = null;
var _pysimDialogKind = null;
var _pysimDialogOpenedAt = 0;
${code}
sandbox.pysimAlert = pysimAlert;
sandbox.pysimConfirm = pysimConfirm;
sandbox.pysimPrompt = pysimPrompt;
sandbox.pysimDialogAnswer = pysimDialogAnswer;
sandbox.getOpenedAt = () => _pysimDialogOpenedAt;
return sandbox;
`)(els, documentStub);
test('pysimAlert sets the message, hides Cancel and resolves on OK', async () => {
clearEls();
setDialogEls();
listeners.keydown = [];
const p = app.pysimAlert('Hello');
await tick();
assert.strictEqual(els['pysim-dialog-message'].textContent, 'Hello');
assert.ok(els['pysim-dialog-modal'].classes['hidden'] === undefined);
assert.ok(els['pysim-dialog-cancel'].classes['hidden']); // an alert has no Cancel
assert.ok(els['pysim-dialog-input'].classes['hidden']); // and no input
assert.strictEqual((listeners.keydown || []).length, 1); // the keydown listener is armed
app.pysimDialogAnswer(true);
assert.ok(els['pysim-dialog-modal'].classes['hidden']);
assert.strictEqual(await p, undefined);
});
test('pysimConfirm resolves true on OK and false on Cancel/Escape', async () => {
clearEls();
setDialogEls();
listeners.keydown = [];
let p = app.pysimConfirm('Sure?');
await tick();
assert.ok(els['pysim-dialog-cancel'].classes['hidden'] === undefined);
app.pysimDialogAnswer(true);
assert.strictEqual(await p, true);
// the Cancel button
p = app.pysimConfirm('Sure?');
await tick();
app.pysimDialogAnswer(false);
assert.strictEqual(await p, false);
// Escape cancels
p = app.pysimConfirm('Sure?');
await tick();
pressKey('Escape');
assert.strictEqual(await p, false);
// Enter accepts
p = app.pysimConfirm('Sure?');
await tick();
pressKey('Enter');
assert.strictEqual(await p, true);
});
test('pysimPrompt prefills the default, returns the typed value or null', async () => {
clearEls();
setDialogEls();
listeners.keydown = [];
let p = app.pysimPrompt('Name?', 'default');
await tick();
assert.ok(els['pysim-dialog-input'].classes['hidden'] === undefined);
assert.strictEqual(els['pysim-dialog-input'].value, 'default');
assert.ok(els['pysim-dialog-input'].focused); // the input is focused
assert.ok(els['pysim-dialog-input'].selected); // with its text selected
els['pysim-dialog-input'].value = 'typed';
app.pysimDialogAnswer(true);
assert.strictEqual(await p, 'typed');
// Cancel discards the typed value
p = app.pysimPrompt('Name?', 'default');
await tick();
els['pysim-dialog-input'].value = 'typed';
app.pysimDialogAnswer(false);
assert.strictEqual(await p, null);
// Escape is a cancel too
p = app.pysimPrompt('Name?', 'x');
await tick();
pressKey('Escape');
assert.strictEqual(await p, null);
});
test('overlapping calls queue in order (one dialog at a time)', async () => {
clearEls();
setDialogEls();
listeners.keydown = [];
let firstDone = false;
let secondDone = false;
const p1 = app.pysimAlert('first');
p1.then(() => { firstDone = true; });
const p2 = app.pysimConfirm('second');
p2.then(() => { secondDone = true; });
await tick();
// only the first is open; the second waits
assert.strictEqual(els['pysim-dialog-message'].textContent, 'first');
assert.ok(els['pysim-dialog-cancel'].classes['hidden']);
assert.ok(!firstDone && !secondDone);
app.pysimDialogAnswer(true);
await tick();
// the second opens now, with its own message and Cancel
assert.strictEqual(els['pysim-dialog-message'].textContent, 'second');
assert.ok(els['pysim-dialog-cancel'].classes['hidden'] === undefined);
assert.ok(firstDone && !secondDone);
app.pysimDialogAnswer(false);
assert.strictEqual(await p2, false);
});
test('a second answer after the dialog resolved is a no-op', async () => {
clearEls();
setDialogEls();
listeners.keydown = [];
const p = app.pysimConfirm('once');
await tick();
app.pysimDialogAnswer(true);
app.pysimDialogAnswer(false); // must not change the settled result
assert.strictEqual(await p, true);
// the keydown listener is removed when the dialog closes
assert.strictEqual((listeners.keydown || []).length, 0);
});
test('without the modal the native dialogs are used', async () => {
clearEls(); // no #pysim-dialog-modal in the els map
listeners.keydown = [];
app.alerts = [];
app.confirms = [];
app.prompts = [];
app.confirmAnswer = true;
app.promptAnswer = 'native';
assert.strictEqual(await app.pysimAlert('A'), undefined);
assert.deepStrictEqual(app.alerts, ['A']);
assert.strictEqual(await app.pysimConfirm('C'), true);
assert.deepStrictEqual(app.confirms, ['C']);
app.confirmAnswer = false;
assert.strictEqual(await app.pysimConfirm('C2'), false);
assert.strictEqual(await app.pysimPrompt('P', 'def'), 'native');
assert.deepStrictEqual(app.prompts, [['P', 'def']]);
assert.strictEqual((listeners.keydown || []).length, 0);
});
test('Enter with the Cancel button focused cancels', async () => {
clearEls();
setDialogEls();
listeners.keydown = [];
documentStub.activeElement = els['pysim-dialog-cancel'];
const p = app.pysimConfirm('Cancel focus?');
await tick();
pressKey('Enter');
assert.strictEqual(await p, false);
documentStub.activeElement = null;
});
test('a repeated Enter does not answer a queued dialog', async () => {
clearEls();
setDialogEls();
listeners.keydown = [];
documentStub.activeElement = null;
const p1 = app.pysimConfirm('first');
const p2 = app.pysimConfirm('second');
await tick();
pressKey('Enter', { repeat: true }); // a held key: ignored
pressKey('Enter', { repeat: true });
assert.strictEqual(els['pysim-dialog-message'].textContent, 'first');
assert.ok(els['pysim-dialog-modal'].classes['hidden'] === undefined);
pressKey('Enter'); // a real press answers it
await tick();
assert.strictEqual(await p1, true);
assert.strictEqual(els['pysim-dialog-message'].textContent, 'second');
app.pysimDialogAnswer(true);
assert.strictEqual(await p2, true);
});
test('an event answer inside the grace window is ignored, a later one is accepted', async () => {
clearEls();
setDialogEls();
listeners.keydown = [];
documentStub.activeElement = null;
const p = app.pysimConfirm('grace');
await tick();
// a pointer answer within the grace window: ignored (a double-click's 2nd
// click on the freshly opened dialog)
app.pysimDialogAnswer(true, { type: 'click', timeStamp: app.getOpenedAt() + 50 });
assert.ok(els['pysim-dialog-modal'].classes['hidden'] === undefined);
// a programmatic answer without an event is never ignored
app.pysimDialogAnswer(true);
assert.strictEqual(await p, true);
// a keyboard answer is never grace-checked (a deliberate Enter must work)
const p2 = app.pysimConfirm('keyboard');
await tick();
app.pysimDialogAnswer(false, { type: 'keydown', timeStamp: app.getOpenedAt() + 5 });
assert.strictEqual(await p2, false);
// a pointer answer outside the window: accepted
const p3 = app.pysimConfirm('later');
await tick();
app.pysimDialogAnswer(false, { type: 'click', timeStamp: app.getOpenedAt() + 600 });
assert.strictEqual(await p3, false);
});
// The dialog's own controls, in tab order; the trap must cycle them.
const FOCUS_IDS = ['pysim-dialog-cancel', 'pysim-dialog-input', 'pysim-dialog-ok'];
function resetFocus() { for (const id of FOCUS_IDS) if (els[id]) els[id].focused = false; }
function focusedIds() { return FOCUS_IDS.filter(id => els[id] && els[id].focused); }
function tab(extra) {
resetFocus();
let prevented = false;
pressKey('Tab', Object.assign({ preventDefault: () => { prevented = true; } }, extra || {}));
return prevented;
}
test('Tab cycles the dialog controls and cannot leave the modal', async () => {
clearEls();
setDialogEls();
listeners.keydown = [];
documentStub.activeElement = null;
const p = app.pysimConfirm('trap');
await tick();
assert.deepStrictEqual(focusedIds(), ['pysim-dialog-ok']); // the default focus
// the browser's own focus move is suppressed
assert.ok(tab(), 'Tab must be handled, not left to the browser');
assert.deepStrictEqual(focusedIds(), ['pysim-dialog-cancel']); // wraps to the first
documentStub.activeElement = els['pysim-dialog-cancel'];
assert.ok(tab());
assert.deepStrictEqual(focusedIds(), ['pysim-dialog-ok']);
// Shift+Tab walks back
documentStub.activeElement = els['pysim-dialog-ok'];
assert.ok(tab({ shiftKey: true }));
assert.deepStrictEqual(focusedIds(), ['pysim-dialog-cancel']);
// a focus that escaped the dialog is pulled back in
documentStub.activeElement = null;
assert.ok(tab());
assert.deepStrictEqual(focusedIds(), ['pysim-dialog-cancel']);
documentStub.activeElement = null;
assert.ok(tab({ shiftKey: true }));
assert.deepStrictEqual(focusedIds(), ['pysim-dialog-ok']);
app.pysimDialogAnswer(true);
await p;
});
test('the trap follows the dialog kind (alert: OK only, prompt: Cancel/input/OK)', async () => {
clearEls();
setDialogEls();
listeners.keydown = [];
documentStub.activeElement = null;
// an alert has a single control: Tab stays on it
let p = app.pysimAlert('only ok');
await tick();
documentStub.activeElement = els['pysim-dialog-ok'];
tab();
assert.deepStrictEqual(focusedIds(), ['pysim-dialog-ok']);
app.pysimDialogAnswer(true);
await p;
// a prompt: Cancel -> input -> OK -> Cancel
clearEls();
setDialogEls();
listeners.keydown = [];
documentStub.activeElement = null;
p = app.pysimPrompt('q', 'v');
await tick();
assert.deepStrictEqual(focusedIds(), ['pysim-dialog-input']);
documentStub.activeElement = els['pysim-dialog-input'];
tab();
assert.deepStrictEqual(focusedIds(), ['pysim-dialog-ok']);
documentStub.activeElement = els['pysim-dialog-ok'];
tab();
assert.deepStrictEqual(focusedIds(), ['pysim-dialog-cancel']);
documentStub.activeElement = els['pysim-dialog-cancel'];
tab();
assert.deepStrictEqual(focusedIds(), ['pysim-dialog-input']);
app.pysimDialogAnswer(true);
await p;
});
test('a missing inner element falls back to the native dialog', async () => {
clearEls();
// the modal and every element but the OK button exist
for (const id of ['pysim-dialog-modal', 'pysim-dialog-message', 'pysim-dialog-input',
'pysim-dialog-cancel']) setEl(id, '');
listeners.keydown = [];
app.alerts = [];
app.confirms = [];
app.confirmAnswer = false;
assert.strictEqual(await app.pysimConfirm('C'), false);
assert.deepStrictEqual(app.confirms, ['C']);
assert.strictEqual(await app.pysimAlert('A'), undefined);
assert.deepStrictEqual(app.alerts, ['A']);
assert.strictEqual((listeners.keydown || []).length, 0);
});
test('the dialog markup and the helpers exist in index.html', () => {
assert.match(html, /<div id="pysim-dialog-modal"[^>]*role="dialog"[^>]*aria-modal="true"[^>]*class="fixed inset-0 bg-black\/50 z-50 flex items-center justify-center hidden">/);
assert.match(html, /id="pysim-dialog-message"/);
assert.match(html, /id="pysim-dialog-input"[^>]*type="text"/);
assert.match(html, /id="pysim-dialog-cancel"[^>]*onclick="pysimDialogAnswer\(false, event\)"[^>]*data-l10n="Cancel"/);
assert.match(html, /id="pysim-dialog-ok"[^>]*onclick="pysimDialogAnswer\(true, event\)"[^>]*data-l10n="OK"/);
for (const fn of ['pysimAlert', 'pysimConfirm', 'pysimPrompt', '_pysimDialogShow',
'pysimDialogAnswer', '_pysimDialogKeydown']) {
assert.ok(html.includes('function ' + fn), 'missing ' + fn);
}
});
test('every dialog class exists in the prebuilt stylesheet', () => {
// The stale-style trap: style.css is a prebuilt Tailwind output, so a
// class used only in index.html has no rule until `npm run build` runs.
// Tailwind escapes ':', '/' and the bracket characters in the selectors;
// stripping the backslashes mirrors the class names as written.
const css = fs.readFileSync(path.join(__dirname, '..', 'style.css'), 'utf8').replace(/\\/g, '');
const start = html.indexOf('<div id="pysim-dialog-modal"');
assert.ok(start > 0, 'the dialog markup is missing');
const block = html.slice(start, html.indexOf('<script>', start));
const classes = new Set();
for (const m of block.matchAll(/class="([^"]+)"/g)) {
for (const c of m[1].split(/\s+/)) if (c) classes.add(c);
}
assert.ok(classes.size >= 10, 'expected the dialog classes, got ' + classes.size);
// Escape regex metacharacters and anchor the class end, so `.mx-4` cannot
// be satisfied by a `.mx-40` rule (a substring check would accept it).
const selRe = c => new RegExp('\\.' + c.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&') + '(?![\\w-])');
for (const c of classes) {
assert.match(css, selRe(c), 'missing stylesheet rule for .' + c);
}
});
// A pure comment line is prose, not a call: "... with its prompt (v3.6.38)"
// must not trip the space-tolerant scan below.
function isCommentLine(line) {
const t = line.trim();
return t.startsWith('//') || t.startsWith('*') || t.startsWith('/*');
}
test('no native dialog call site is left outside the fallback branch', () => {
// The sweep moved every alert/confirm/prompt call site onto the helpers.
// Only the intentional native fallback inside _pysimDialogRun and the PWA
// install prompt (deferredPrompt.prompt()) may remain; a future bare call
// names its line in the failure message.
const lines = html.split('\n');
const start = lines.findIndex(l => l.includes('function _pysimDialogRun('));
assert.ok(start > 0, '_pysimDialogRun is missing');
const end = lines.findIndex((l, i) => i > start && /^function /.test(l));
assert.ok(end > start, 'the end of _pysimDialogRun is missing');
const fallback = lines.slice(start, end).join('\n');
assert.match(fallback, /\balert\(/, 'the native alert fallback is missing');
assert.match(fallback, /\bconfirm\(/, 'the native confirm fallback is missing');
assert.match(fallback, /\bprompt\(/, 'the native prompt fallback is missing');
// catch a qualified native (window.alert / globalThis.confirm) and a
// space before '(' too
const re = /(?<![\w.])(?:(?:window|globalThis)\.)?(alert|confirm|prompt)\s*\(/;
const violations = [];
const missingAwait = [];
lines.forEach((line, i) => {
if (re.test(line) && !(i >= start && i < end) &&
!line.includes('deferredPrompt.prompt()') && !isCommentLine(line)) {
violations.push((i + 1) + ': ' + line.trim());
}
// A missing await is the dangerous case: a Promise is always truthy,
// so `if (pysimConfirm(x))` would silently never fire the guard.
for (const fn of ['pysimConfirm', 'pysimPrompt']) {
if (!new RegExp('\\b' + fn + '\\s*\\(').test(line)) continue;
if (new RegExp('function\\s+' + fn + '\\s*\\(').test(line)) continue;
if (new RegExp('await\\s+' + fn + '\\s*\\(').test(line)) continue;
missingAwait.push((i + 1) + ': ' + line.trim());
}
});
assert.deepStrictEqual(violations, [],
'bare native dialog calls left outside _pysimDialogRun:\n' + violations.join('\n'));
assert.deepStrictEqual(missingAwait, [],
'pysimConfirm/pysimPrompt calls without await:\n' + missingAwait.join('\n'));
});
+5
View File
@@ -60,6 +60,11 @@ globalThis.pysimApplyAvailability = () => {};
eval(code);
code += 'var _ramCardIdx = null;\nvar _ramOpLast = null;\nvar _ramExplorerData = null;\n';
eval(code);
// The dialog sweep replaced the native alert/confirm with the helpers: route
// them to the native stubs each test installs on globalThis.
globalThis.pysimAlert = m => globalThis.alert(m);
globalThis.pysimConfirm = async m => globalThis.confirm(m);
globalThis.pysimPrompt = async (m, v) => globalThis.prompt(m, v);
const els = {};
const doc = { getElementById: (id) => { if (!els[id]) els[id] = {value:''}; return els[id]; } };
+3
View File
@@ -79,6 +79,9 @@ const app = new Function('els', `
var t = k => k;
var isViewVisible = () => sandbox.visibility;
var alert = m => sandbox.alerts.push(String(m));
var pysimAlert = m => alert(m);
var pysimConfirm = async () => true; // not used by the batch flow
var pysimPrompt = async () => ''; // not used by the batch flow
var pysimFetch = async (p) => {
sandbox.fetched.push(p);
if (sandbox.hangs) return new Promise(() => {}); // a stalled connection
+5
View File
@@ -50,6 +50,11 @@ for (const c of ['CARDS_TAR_DEFAULTS', 'CARDS_TAR_ROLES', 'CARDS_MSL_DEFAULT'])
code = 'var ' + c + ' = ' + m[1] + ';\n' + code;
}
eval(code);
// The dialog sweep replaced the native alert/confirm with the helpers: route
// them to the native stubs fakeEnv() installs on globalThis.
globalThis.pysimAlert = m => globalThis.alert(m);
globalThis.pysimConfirm = async m => globalThis.confirm(m);
globalThis.pysimPrompt = async (m, v) => globalThis.prompt(m, v);
function fakeEnv(por, success) {
const els = {};
+5
View File
@@ -90,6 +90,11 @@ eval('var _testEditStep = null; var _testEditChecks = []; var _testEditStepIndex
+ ' var _testScriptDraft = null; var _testRunState = null; var _testSaveTimer = null;'
+ ' var _testWriteChains = new WeakMap(); var _testLastPresetIdx = -1;'
+ ' var _testRunScriptRef = null; var _testSuiteIdx = -1;');
// The dialog sweep replaced the native alert/confirm with the helpers: route
// them to the native stubs each test installs on globalThis.
globalThis.pysimAlert = m => globalThis.alert(m);
globalThis.pysimConfirm = async m => globalThis.confirm(m);
globalThis.pysimPrompt = async (m, v) => globalThis.prompt(m, v);
globalThis.localStorage = { getItem: () => null, setItem: () => {}, removeItem: () => {} };
function fakeForm(values) {