fix(e2e): let the persona probe actually wait for a slow SPA

Playwright's isVisible() is an immediate, non-retrying check; the timeout option
it accepts is ignored. Two call sites in the shared admin persona passed a
substantial timeout and behaved as if it applied:

  :75  gates the whole native-login path, called with probeTimeout=10_000 (:101)
  :141 the fallback that decides whether an authenticated surface was reached,
       called with resolveTimeout(15_000)

The trace of EspoCRM variant 1 shows the option never reaching the call. Every
navigation carries its own budget -- {"url": ..., "timeout": 60000} -- while the
probes record only {"selector": "input[type='password']:visible >> nth=0",
"strict": true}, with no timeout field at all. The first probe answers false
220 ms after it starts, against the 10 s it was given; the three fallback probes
that follow take 36, 32 and 29 ms. The whole native-login attempt, four
navigations and four probes, is over 1.15 s after it began.

That is not enough for an SPA. The DOM snapshot at the moment of the first probe
is ["BODY", {"data-id": "espocrm"}, ["DIV", {"class": "container content"}]] --
an empty shell, with espo.js and espo-main.js still in flight. The login form the
probe waits for renders later, so the persona falls through to /login, /admin/
and /admin, collects three Apache 404s, and asserts that the administrator never
reached an authenticated surface.

Both sites move to waitFor({state: "visible", timeout}), which is the waiting
form and is already the idiom elsewhere in this directory.

The four remaining isVisible({timeout}) calls in the file stay as they are: they
ask whether something is visible right now -- is the password field STILL there
after submitting, is a Login control STILL showing -- where an immediate answer
is the correct semantics and the timeout argument is inert but harmless.

Fixes the EspoCRM 1 and 2 failures and applies to every SPA the shared persona
drives; it does not touch EspoCRM 0, whose OIDC back-channel fails for an
unrelated reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-08-14 02:02:28 +02:00
1 parent 8c7593f43f
commit 7538b8a0bb
1 file changed
+7 -2
@@ -72,7 +72,11 @@ async function runAdminFlow(page, opts = {}) {
const base = appBaseUrl.replace(/\/$/, "");
const tryNativeLogin = async (probeTimeout = 5_000) => {
const passwordField = page.locator("input[type='password']:visible").first();
if (!(await passwordField.isVisible({ timeout: probeTimeout }).catch(() => false))) {
const passwordReady = await passwordField
.waitFor({ state: "visible", timeout: probeTimeout })
.then(() => true)
.catch(() => false);
if (!passwordReady) {
return false;
}
const usernameField = page
@@ -138,7 +142,8 @@ async function runAdminFlow(page, opts = {}) {
if (!adminReachedAuthenticated) {
adminReachedAuthenticated = await adminAuthMarker(page)
.first()
.isVisible({ timeout: resolveTimeout(15_000) })
.waitFor({ state: "visible", timeout: resolveTimeout(15_000) })
.then(() => true)
.catch(() => false);
}
if (!adminReachedAuthenticated) {