diff --git a/CHANGELOG.md b/CHANGELOG.md index b5a253d7..577914c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/project-tests/e2e/config.js b/project-tests/e2e/config.js index d0c72b1b..1ff9a4a5 100644 --- a/project-tests/e2e/config.js +++ b/project-tests/e2e/config.js @@ -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. diff --git a/project-tests/e2e/monkey.js b/project-tests/e2e/monkey.js index a3dd128c..8bb2263f 100644 --- a/project-tests/e2e/monkey.js +++ b/project-tests/e2e/monkey.js @@ -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; @@ -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); @@ -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 () => { diff --git a/project-tests/e2e/project.config.template.js b/project-tests/e2e/project.config.template.js index 790e23c2..0bb35334 100644 --- a/project-tests/e2e/project.config.template.js +++ b/project-tests/e2e/project.config.template.js @@ -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, diff --git a/tests/wave2/test-monkey-left-app.sh b/tests/wave2/test-monkey-left-app.sh index 654260e8..2c31aa4c 100755 --- a/tests/wave2/test-monkey-left-app.sh +++ b/tests/wave2/test-monkey-left-app.sh @@ -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)" @@ -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"