From 20eacefd4e6d1a8f403d49e1b913a7b185ac36e0 Mon Sep 17 00:00:00 2001 From: Jonas Templestein <242550+jonastemplestein@users.noreply.github.com> Date: Wed, 7 Oct 2026 11:24:59 +0100 Subject: [PATCH] spinner-waiter: inputValue waits like an action, and a target that is ready by the loading check proceeds Port of the branch fix/input-value-loading, which iterate/private has run on since before the repo split (pinned as pkg.pr.new/middlewright@e3f2374): - inputValue joins the patched methods, so a read of an input that has not appeared yet waits while loading UI is visible, then returns the current value. An explicit timeout still passes through. - Before the no-spinner fast-fail, the target is checked once more. A target that became ready while the loading check ran proceeds with its normal budget instead of a 1 ms timeout. Co-Authored-By: Claude Opus 5.5 --- README.md | 6 +++-- spec/spinner-waiter.spec.ts | 51 ++++++++++++++++++++++++++++++++++- src/plugin-system.ts | 1 + src/plugins/spinner-waiter.ts | 4 +++ 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f8bd6f3..f26d69d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A plugin/middleware system for Playwright locator actions — the one Playwright doesn't have. -Wrap `click`, `fill`, `waitFor` and friends with composable middleware, so your tests can be smart about *why* an action is slow or failing, without sprinkling `waitForSomething()` helpers through every test. +Wrap `click`, `fill`, `inputValue`, `waitFor` and friends with composable middleware, so your tests can be smart about *why* an action is slow or failing, without sprinkling `waitForSomething()` helpers through every test. ## Quick start @@ -523,7 +523,7 @@ configured pattern must match the nearby comment: ## How it works -`addPlugins` patches `Locator.prototype` (once per process), replacing `click`, `dblclick`, `fill`, `type`, `press`, `clear`, `blur`, `focus`, `hover` and `waitFor` with a dispatcher. The dispatcher looks up the plugin state stored on the action's page; if the page has plugins, it runs the middleware chain (each middleware calling `next()` until the original method runs); if not, it calls the original method directly. +`addPlugins` patches `Locator.prototype` (once per process), replacing `click`, `dblclick`, `fill`, `type`, `press`, `clear`, `blur`, `focus`, `hover`, `inputValue` and `waitFor` with a dispatcher. The dispatcher looks up the plugin state stored on the action's page; if the page has plugins, it runs the middleware chain (each middleware calling `next()` until the original method runs); if not, it calls the original method directly. To keep Playwright's HTML report pointing at *your test code* rather than plugin internals, it registers the plugin files with playwright-core's internal `setBoxedStackPrefixes` — the same mechanism Playwright uses to hide its own frames. This is the unofficial-API part of the hack; set `PLAYWRIGHT_PLUGIN_DEBUG=1` to disable it when debugging the plugins themselves. @@ -546,3 +546,5 @@ Extracted from the internal test infrastructure of the [iterate](https://github. ## License MIT + +`inputValue()` uses the same loading-aware wait when the input has not appeared yet, then returns its current value. It does not wait for a particular value; an explicit timeout still passes through unchanged. diff --git a/spec/spinner-waiter.spec.ts b/spec/spinner-waiter.spec.ts index e303413..6fcd229 100644 --- a/spec/spinner-waiter.spec.ts +++ b/spec/spinner-waiter.spec.ts @@ -1,6 +1,6 @@ import http from "node:http"; import type { AddressInfo } from "node:net"; -import { test as base, expect } from "@playwright/test"; +import { test as base, expect, selectors } from "@playwright/test"; import { addPlugins, defaultSelectors, spinnerWaiter, type Plugin } from "../src/index.ts"; const test = base.extend<{ slowMutationTimeout: number }>({ @@ -32,6 +32,28 @@ test("slow button succeeds when there's a spinner", async ({ page }) => { await page.getByText("work done").waitFor(); }); +test("inputValue waits for a loading composer and returns its value", async ({ page }) => { + await page.setContent(` +

Opening chat…

+ + `); + expect(await page.getByLabel("Message").inputValue()).toBe("About my edited note"); +}); + +test("inputValue preserves an explicit timeout and can read disabled inputs", async ({ page }) => { + await page.setContent(''); + expect(await page.getByLabel("Saved note").inputValue()).toBe("green apples"); + await page.setContent('

Opening chat…

'); + // Explicit timeout must bypass spinner-waiter even while progress is visible. + await expect(page.getByLabel("Missing").inputValue({ timeout: 100 })).rejects.toThrow( + /Timeout 100ms exceeded/, + ); +}); + test("visible disabled button succeeds when there's a spinner", async ({ page }) => { await page.setContent(` +

Loading…

+ `); + await page.getByRole("button", { name: "Submit", exact: true }).click(); + await page.getByRole("button", { name: "submitted", exact: true }).waitFor(); + }, +); + base("no-spinner fast fail still runs later middleware", async ({ page: basePage }, testInfo) => { const calls: string[] = []; const afterSpinner: Plugin = { diff --git a/src/plugin-system.ts b/src/plugin-system.ts index 7788774..228dd1b 100644 --- a/src/plugin-system.ts +++ b/src/plugin-system.ts @@ -27,6 +27,7 @@ const overrideableMethods = [ "blur", "focus", "hover", + "inputValue", ...oneArgMethods, ] satisfies (keyof Locator)[]; type OverrideableMethod = (typeof overrideableMethods)[number]; diff --git a/src/plugins/spinner-waiter.ts b/src/plugins/spinner-waiter.ts index d724660..feffb88 100644 --- a/src/plugins/spinner-waiter.ts +++ b/src/plugins/spinner-waiter.ts @@ -160,6 +160,10 @@ export const spinnerWaiter = Object.assign( } if (!loading) { + // The target may have appeared while we checked loading. Do not + // turn a now-ready action into a 1ms timeout just because its + // spinner disappeared between the two observations. + if (await locatorIsReady(locator, method)) return next(); // No spinner, no navigation - call action, suggest a spinner if it fails settings.log(`${locator} not ready, nothing loading, failing fast`); try {