Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Sections dated before 2026-09-19 predate the cycle and stay as they are. Named m

## Unreleased

- fix(monkey): **the monkey no longer signs itself out of a non-English app** (#469). `monkey.js` recognised only English sign-out captions, so in a Dutch app it clicked "Afmelden", landed on `login.html`, scored a false CRASH and fuzzed nothing more (seed 204262061: 3 observations, 1 crash-class; 24 and 0 with the label excluded). The default now also covers Dutch, German, French, Spanish, Portuguese and Italian sign-out captions, matched as the whole caption so ordinary verbs stay fuzzable ("Ziek afmelden", "Vom Kurs abmelden", and sign-in captions such as "Aanmelden"); a project adds its own in `project.config.js` `signOutPattern` or `SIGN_OUT_PATTERN`, added to the default, never replacing it, with its own regex flags kept. `rapidTab` no longer presses Enter while focus is on a sign-out control (top bar, nav item, aria-label or title, each tested on its own), the route by which a layout control outside `.mx-page` was reached. `test-monkey-left-app.sh` gains the issue's captions, sign-in near-misses, the over-match captions, the project pattern and its flags, and a rapidTab guard scoped to that branch (it fails on a blind-Enter mutation): 9 FAIL on master, 22/22 with the fix. — jopterhorst
- fix(progress-report): **the build plan is shown once: the phase cards are the plan.** Expanding a card now shows its full rows (#, Kind, Step, Produces/Proves, Depends on, State, Scripts) and Claims, which only the duplicate "Full plan tables" block carried; that block is gone and views A (script folders) and B (module proof) are their own section. The `now` phase renders open with a selected style, opening another phase closes the previous one (accordion; `#phase-N` links still open their card), and a viewer's saved open/closed state survives the 30 s reload and wins over the default (#474) — field feedback from a requirements-driven project at Stage 5 — MendixMau (field feedback 2026-10-20)

## v2026.10.19
Expand Down
3 changes: 3 additions & 0 deletions project-tests/e2e/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ const cfg = {
// this app has no nav tree, so '.mx-navigationtree, .mx-page' alone is not
// enough to prove it routed).
readySelectors: PROJ.readySelectors || ['.mx-navigationtree', '.mx-page'],
// Extra sign-out captions for monkey.js, ADDED to its multilingual default (#469): a RegExp
// or regex string from project.config.js `signOutPattern`, or SIGN_OUT_PATTERN in the env.
signOutPattern: process.env.SIGN_OUT_PATTERN || PROJ.signOutPattern || null,
// This project runs natively under Studio Pro (no Docker), so `mxcli oql -p`
// alone would try to route through `docker compose exec` and fail. --direct
// connects straight to the M2EE admin API on the host instead.
Expand Down
54 changes: 51 additions & 3 deletions project-tests/e2e/monkey.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,52 @@ async function typeSafely(page, handle, text) {
// toggle), so Back left the app — to login.html, then to about:blank. The oracle scored the
// non-app page "blank page", every later round on that target scored it again, and the next
// target was never reached: 10 crash-class findings, one target unfuzzed, no app defect.
const SIGN_OUT = /\b(sign|log)\s*(out|off)\b/i;
//
// SIGN_OUT IS MULTILINGUAL, AND A PROJECT CAN ADD TO IT (#469). The English-only pattern missed
// a Dutch app's "Afmelden": the monkey signed itself out in round 1, scored login.html as a
// CRASH, and fuzzed nothing more (seed 204262061: 3 observations, 1 crash-class; 24 and 0 once
// the label was excluded). The default covers the common captions of the languages Mendix apps
// ship in most; anything else goes in project.config.js `signOutPattern` (a RegExp or a regex
// string) or the SIGN_OUT_PATTERN env var, and is ADDED to the default, never a replacement.
//
// The non-English words match the WHOLE caption only. They are also ordinary verbs, so a word
// match left "Ziek afmelden" (report sick), "Afmelden nieuwsbrief", "Vom Kurs abmelden" and
// "Disconnetti dispositivo" unfuzzed. The English phrase keeps its word match, as before.
const SIGN_OUT_DEFAULT = new RegExp(
'\\b(?:sign|log)\\s*(?:out|off)\\b' + // en: Sign out, Log off, Logout
'|^\\s*(?:' + [
'afmelden', 'uitloggen', // nl
'abmelden', 'ausloggen', // de
'se\\s+d[ée]connecter', 'd[ée]connexion', // fr
'cerrar\\s+sesi[óo]n', // es
'(?:terminar|encerrar)\\s+sess[ãa]o', // pt
'disconnetti', // it
].join('|') + ')\\s*$', 'i');
// A project RegExp keeps its own flags (m, s, u, …), merged with the default's `i`; `g` and `y`
// are dropped because they make .test() stateful across calls.
function signOutPattern(extra) {
if (!extra) return SIGN_OUT_DEFAULT;
const src = extra instanceof RegExp ? extra.source : String(extra);
const flags = [...new Set(('i' + (extra instanceof RegExp ? extra.flags : '')).replace(/[gy]/g, ''))].join('');
try { return new RegExp(`(?:${SIGN_OUT_DEFAULT.source})|(?:${src})`, flags); }
catch (e) {
console.log(` ⚠ signOutPattern ${JSON.stringify(src)} is not a valid regex (${e.message}) — using the built-in sign-out captions only`);
return SIGN_OUT_DEFAULT;
}
}
const SIGN_OUT = signOutPattern(cfg.signOutPattern);
// The keyboard can reach controls the button locator never offers: the layout's top bar, a
// navigation item, an icon-only button whose caption is its aria-label or title.
async function focusIsSignOut(page) {
const labels = await page.evaluate(() => {
const e = document.activeElement;
if (!e || e === document.body) return [];
return [e.innerText || e.textContent || '', e.getAttribute('aria-label') || '', e.getAttribute('title') || ''];
}).catch(() => []);
return isSignOutLabel(labels);
}
// Each label on its own: the non-English captions are anchored to the whole caption.
function isSignOutLabel(labels) { return labels.some(t => t && SIGN_OUT.test(t)); }
function leftApp(url, baseUrl) {
const base = String(baseUrl).replace(/\/+$/, '');
if (!url || (url !== base && !url.startsWith(base + '/'))) return true;
Expand Down Expand Up @@ -208,7 +253,10 @@ async function round(page, target, bucket) {
}
} else if (abuse === 'rapidTab') {
for (let i = 0; i < 15; i++) await page.keyboard.press('Tab').catch(() => {});
await page.keyboard.press('Enter').catch(() => {});
// Never press Enter on a sign-out control (#469): step past it, a few Tabs at most.
for (let k = 0; k < 5 && await focusIsSignOut(page); k++) await page.keyboard.press('Tab').catch(() => {});
if (await focusIsSignOut(page)) what += ' (focus stayed on a sign-out control: Enter skipped)';
else await page.keyboard.press('Enter').catch(() => {});
} else if (abuse === 'reloadMidFlow') {
if (btn) btn.click({ force: true, timeout: 4000 }).catch(() => {});
await page.waitForTimeout(300);
Expand All @@ -224,7 +272,7 @@ async function round(page, target, bucket) {
}

// leftApp is exported for tests/wave2/test-monkey-left-app.sh; requiring this file runs nothing.
module.exports = { leftApp, SIGN_OUT };
module.exports = { leftApp, SIGN_OUT, SIGN_OUT_DEFAULT, signOutPattern, isSignOutLabel };

// ── main ────────────────────────────────────────────────────────────────────
if (require.main === module) (async () => {
Expand Down
5 changes: 5 additions & 0 deletions project-tests/e2e/project.config.template.js
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,11 @@ module.exports = {
mxcli: process.env.MXCLI || './mxcli',
credentials,
readySelectors: READY_SELECTORS,
// Sign-out captions monkey.js must never click, ADDED to its built-in list (English, Dutch,
// German, French, Spanish, Portuguese, Italian). Set it when the app's sign-out control
// reads otherwise, e.g. /\bkirjaudu ulos\b/i — a monkey that signs itself out scores the
// login page as a crash and fuzzes nothing more (#469).
signOutPattern: null,
dsCss: DS_CSS,
NAV,
TARGETS,
Expand Down
46 changes: 45 additions & 1 deletion tests/wave2/test-monkey-left-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ L('OTHER', 'http://localhost:80801/p/cases');
L('SLASHBASE', 'http://localhost:8080/p/cases', base + '/');
const S = t => console.log(`S:${t} ${M.SIGN_OUT.test(t)}`);
['Sign out', 'Log off', 'Logout', 'Switch theme', 'Signature', 'Log in'].forEach(S);
// #469: the captions the issue names, verbatim, and near-misses that must stay fuzzable.
['Afmelden', 'Uitloggen', 'Abmelden', 'Se déconnecter', 'Aanmelden', 'Inloggen', 'Anmelden', 'Se connecter', 'Melden'].forEach(S);
const X = (k, extra, t) => console.log(`X:${k} ${typeof M.signOutPattern === 'function' ? M.signOutPattern(extra).test(t) : 'NOFN'}`);
X('cfgre', /\bkirjaudu ulos\b/i, 'Kirjaudu ulos');
X('cfgstr', '\\bkirjaudu ulos\\b', 'Kirjaudu ulos');
X('cfgkeeps', /\bkirjaudu ulos\b/i, 'Afmelden');
X('cfgbad', '(', 'Afmelden');
X('cfgnone', null, 'Kirjaudu ulos');
// Review of #481: ordinary verbs inside a longer caption are not sign-out controls.
['Ziek afmelden', 'Afmelden nieuwsbrief', 'Vom Kurs abmelden', 'Disconnetti dispositivo', ' Afmelden '].forEach(S);
X('cfgflags', /^kirjaudu ulos$/m, 'Valikko\nKirjaudu ulos');
const I = (k, labels) => console.log(`I:${k} ${typeof M.isSignOutLabel === 'function' ? M.isSignOutLabel(labels) : 'NOFN'}`);
I('aria', ['', 'Afmelden', '']);
I('title', ['\ue000', '', 'Uitloggen']);
I('none', ['Ziek afmelden', 'Menu', '']);
process.exit(0);
EOF
OUT="$(cd "$WORK/tests/e2e" && printf '%s' "$JS" | APP_PORT=1 timeout 30 node - 2>&1)"
Expand All @@ -85,12 +100,41 @@ fi
[ "$(line OTHER)" = true ] && ok "a lookalike origin (port prefix match) is outside the app" || bad "prefix match leaks another origin in" "$(line OTHER)"
[ "$(line SLASHBASE)" = false ] && ok "a baseUrl with a trailing slash still matches its routes" || bad "trailing-slash baseUrl breaks the match" "$(line SLASHBASE)"

s() { printf '%s\n' "$OUT" | sed -n "s/^S:$1 //p"; }
s() { printf '%s\n' "$OUT" | K="S:$1" awk '{b=$NF; sub(/ [^ ]*$/, ""); if ($0 == ENVIRON["K"]) print b}'; } # exact label: "Afmelden" must not read "Afmelden nieuwsbrief"
[ "$(s 'Sign out')" = true ] && [ "$(s 'Log off')" = true ] && [ "$(s 'Logout')" = true ] \
&& ok "SIGN_OUT matches Sign out (captured), Log off, Logout" || bad "a sign-out label is not excluded" "$(s 'Sign out')/$(s 'Log off')/$(s 'Logout')"
[ "$(s 'Switch theme')" = false ] && [ "$(s 'Signature')" = false ] && [ "$(s 'Log in')" = false ] \
&& ok "SIGN_OUT leaves Switch theme (captured), Signature, Log in fuzzable" || bad "SIGN_OUT over-matches" "$(s 'Switch theme')/$(s 'Signature')/$(s 'Log in')"

# #469 — non-English sign-out captions (verbatim from the issue's field run and its list).
[ "$(s 'Afmelden')" = true ] && [ "$(s 'Uitloggen')" = true ] && [ "$(s 'Abmelden')" = true ] && [ "$(s 'Se déconnecter')" = true ] \
&& ok "SIGN_OUT matches Afmelden (captured), Uitloggen, Abmelden, Se déconnecter" \
|| bad "a non-English sign-out label is clicked — the #469 false CRASH" "$(s 'Afmelden')/$(s 'Uitloggen')/$(s 'Abmelden')/$(s 'Se déconnecter')"
[ "$(s 'Aanmelden')" = false ] && [ "$(s 'Inloggen')" = false ] && [ "$(s 'Anmelden')" = false ] && [ "$(s 'Se connecter')" = false ] && [ "$(s 'Melden')" = false ] \
&& ok "SIGN_OUT leaves sign-IN and look-alike captions fuzzable (Aanmelden, Inloggen, Anmelden, Se connecter, Melden)" \
|| bad "SIGN_OUT over-matches a sign-in caption" "$(s 'Aanmelden')/$(s 'Inloggen')/$(s 'Anmelden')/$(s 'Se connecter')/$(s 'Melden')"
x() { printf '%s\n' "$OUT" | sed -n "s/^X:$1 //p"; }
[ "$(x cfgre)" = true ] && [ "$(x cfgstr)" = true ] && ok "a project signOutPattern (RegExp or string) is honoured" \
|| bad "project signOutPattern ignored" "$(x cfgre)/$(x cfgstr)"
[ "$(x cfgkeeps)" = true ] && ok "a project pattern adds to the default, it does not replace it" || bad "project pattern dropped the default" "$(x cfgkeeps)"
[ "$(x cfgbad)" = true ] && [ "$(x cfgnone)" = false ] && ok "an invalid project pattern falls back to the default; none means default only" \
|| bad "invalid/absent project pattern mishandled" "$(x cfgbad)/$(x cfgnone)"
[ "$(s 'Ziek afmelden')" = false ] && [ "$(s 'Afmelden nieuwsbrief')" = false ] && [ "$(s 'Vom Kurs abmelden')" = false ] && [ "$(s 'Disconnetti dispositivo')" = false ] \
&& ok "a non-English verb inside a longer caption stays fuzzable (Ziek afmelden, Afmelden nieuwsbrief, Vom Kurs abmelden, Disconnetti dispositivo)" \
|| bad "SIGN_OUT over-matches an ordinary caption" "$(s 'Ziek afmelden')/$(s 'Afmelden nieuwsbrief')/$(s 'Vom Kurs abmelden')/$(s 'Disconnetti dispositivo')"
[ "$(s ' Afmelden ')" = true ] && ok "a whole caption with surrounding whitespace still matches" || bad "padded caption missed" "$(s ' Afmelden ')"
[ "$(x cfgflags)" = true ] && ok "a project RegExp keeps its own flags (m)" || bad "project RegExp flags dropped" "$(x cfgflags)"
i() { printf '%s\n' "$OUT" | sed -n "s/^I:$1 //p"; }
[ "$(i aria)" = true ] && [ "$(i title)" = true ] && [ "$(i none)" = false ] \
&& ok "focus labels (text, aria-label, title) are tested one by one" || bad "focus label test wrong" "$(i aria)/$(i title)/$(i none)"
grep -q 'signOutPattern: *process.env.SIGN_OUT_PATTERN' "$E2E/config.js" && grep -q '^ *signOutPattern:' "$CFG" \
&& ok "config.js reads signOutPattern (env, then project.config) and the template declares it" \
|| bad "signOutPattern is not plumbed through config.js / the template (a consumer with no producer)"
# Scoped to the rapidTab branch itself: the ABUSES list and focusIsSignOut's definition also
# mention these names, and an unscoped match passed a blind Enter (review of #481).
awk '/abuse === .rapidTab./{r=1;next} r&&/else if \(abuse ===/{r=0} r&&/focusIsSignOut\(page\)/{f=1} r&&/press\(.Enter.\)/{if(!f)bad=1; e=1} END{exit !(e&&!bad)}' "$SUT" \
&& ok "rapidTab checks focus for a sign-out control before Enter" || bad "rapidTab presses Enter without checking what has focus (#469)"

# The tested functions must be the ones the rounds use.
awk '/hasNotText: *SIGN_OUT/ {f=1} END{exit !f}' "$SUT" && ok "round()'s button locator filters SIGN_OUT" \
|| bad "the button locator does not filter SIGN_OUT — the tested regex is not the one that runs"
Expand Down
Loading