(
opts: AuthAssertionOpts = {},
)
| 143 | } |
| 144 | |
| 145 | export function buildAuthAssertion( |
| 146 | opts: AuthAssertionOpts = {}, |
| 147 | ): (page: Page) => Promise<void> { |
| 148 | const timeout = opts.signOutTimeoutMs ?? POST_SIGN_OUT_TIMEOUT_MS; |
| 149 | const detectTimeout = opts.detectTimeoutMs ?? SIGN_IN_MOUNT_TIMEOUT_MS; |
| 150 | const click = opts.click ?? defaultClick; |
| 151 | return async (page: Page): Promise<void> => { |
| 152 | // Sign-out button must be visible — both shapes show it after auth. |
| 153 | try { |
| 154 | await page.waitForSelector(SIGN_OUT_BUTTON_SELECTOR, { |
| 155 | state: "visible", |
| 156 | timeout: detectTimeout, |
| 157 | }); |
| 158 | } catch { |
| 159 | throw new Error( |
| 160 | `auth: sign-out button ${SIGN_OUT_BUTTON_SELECTOR} not visible — demo did not reach authenticated state (idiomatic: sign-in click failed; legacy: demo did not load authenticated)`, |
| 161 | ); |
| 162 | } |
| 163 | |
| 164 | // Single deadline gates the entire post-sign-out assertion so the |
| 165 | // total wall-clock STAYS within `timeout` (the previous version |
| 166 | // had a hardcoded 3s legacy banner-flip wait + 500ms sleep + 2s |
| 167 | // fill + 2s press + remaining error-poll, all of which could |
| 168 | // stack to ~15s when the caller asked for 8s — observable |
| 169 | // contract violation that only surfaced under tight test |
| 170 | // timeouts). |
| 171 | const deadline = Date.now() + timeout; |
| 172 | const remainingMs = (): number => Math.max(0, deadline - Date.now()); |
| 173 | // Idiomatic detection gets the smaller of 3s or the caller's |
| 174 | // budget — fast-path for a fresh React render. Inner waitForSelector |
| 175 | // bound to remaining, so a 100ms deadline can't be overshot by a |
| 176 | // 200ms inner timeout the way the prior 3s-loop-with-200ms-inner |
| 177 | // could. |
| 178 | await click(page, SIGN_OUT_BUTTON_SELECTOR); |
| 179 | |
| 180 | const idiomaticBudgetMs = Math.min(3_000, timeout); |
| 181 | const idiomaticDeadline = Date.now() + idiomaticBudgetMs; |
| 182 | let signInCardMounted = false; |
| 183 | while (Date.now() < idiomaticDeadline) { |
| 184 | const innerTimeout = Math.min(200, idiomaticDeadline - Date.now()); |
| 185 | if (innerTimeout <= 0) break; |
| 186 | try { |
| 187 | await page.waitForSelector(SIGN_IN_CARD_SELECTOR, { |
| 188 | state: "visible", |
| 189 | timeout: innerTimeout, |
| 190 | }); |
| 191 | signInCardMounted = true; |
| 192 | break; |
| 193 | } catch { |
| 194 | // Not yet — continue polling. |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | if (signInCardMounted) { |
| 199 | // Idiomatic shape: SignInCard re-mounted. Pass. |
| 200 | return; |
| 201 | } |
| 202 |
no test coverage detected
searching dependent graphs…