From e9ef2d4e45016e6e58aeabc1b7a12c3f76bddd22 Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Thu, 13 Aug 2026 12:23:47 +0100 Subject: [PATCH 1/3] more guidance for clankers in lint errors --- src/lint/plugin.ts | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/lint/plugin.ts b/src/lint/plugin.ts index 6b2a13f..eec1438 100644 --- a/src/lint/plugin.ts +++ b/src/lint/plugin.ts @@ -21,8 +21,8 @@ const preferLocatorWaits = { fixable: "code", schema: [], messages: { - visible: "Use locator.waitFor() instead of expect(locator).toBeVisible().", - text: "Use locator.filter({ hasText }).waitFor() instead of expect(locator).toContainText().", + visible: "Use locator.waitFor() instead of expect(locator).toBeVisible(). That way you'll benefit from spinnerWaiter's automatic loading UI detection.", + text: "Use locator.filter({ hasText }).waitFor() instead of expect(locator).toContainText(). That way you'll benefit from spinnerWaiter's automatic loading UI detection.", }, }, create(context: any) { @@ -72,8 +72,17 @@ const requireTimeoutComment = { }, schema: requiredPatternsSchema, messages: { - unexplained: - "Usually remove the timeout and add loading UI for spinnerWaiter. If a product or Middlewright limit prevents that, add a nearby // comment matching every required pattern: {{patterns}}. See https://github.com/iterate/middlewright#dont-fix-slow-tests-with-longer-timeouts", + unexplained: dedent` + Avoid locator timeouts by using spinnerWaiter. Best ways to resolve: + - If there's already loading UI, just remove the timeout and rely on spinnerWaiter to wait for the loading UI. + - If there's no loading UI, add loading UI and remove the timeout. + - If there's a loading UI but it takes even longer than the spinnerWaiter spinner timeout, use \`await spinnerWaiter.settings.run({ spinnerTimeout: 123_456 }, async () => ...)\` or similar to wait even longer for the spinner to complete. + - If there's loading UI from a part of the code that we don't control (e.g. a library) and it isn't matched by the default spinner selectors, use \`await spinnerWaiter.settings.run({ spinnerSelectors: ["myCustomSpinnerClass"] }, async () => ...)\` + - If it is truly impossible for there to be loading UI, add a nearby // comment matching every required pattern: {{patterns}}. + - If you're in a block which has done \`await spinnerWaiter.settings.run({ disabled: true }, async () => ...)\`, you should probably *un-disable* for that block and apply the above suggestions to the inner code. + + See https://github.com/iterate/middlewright#dont-fix-slow-tests-with-longer-timeouts for more details. + ` }, }, create(context: any) { @@ -120,8 +129,13 @@ const preferPositiveWaits = { }, schema: requiredPatternsSchema, messages: { - detached: - "Wait for positive UI instead of element detachment, or explain an exceptional detached wait in a nearby // comment matching every required pattern: {{patterns}}. See https://github.com/iterate/middlewright#prefer-positive-waits-over-absence", + detached: dedent` + Wait for positive UI instead of element detachment. + If there's no postitive UI to wait for your first port of call should be to *add* the positive UI to the product. + If it is truly impossible for there to be a positive UI, then that is quite surprising and you should question that. + If you have questioned it and it is still impossible, then you should make sure you've first validated that the UI that's becoming detached previously was visible. + Then explain why it's impossible, and why it's safe to wait for detachment in a nearby // comment matching every required pattern: {{patterns}}. + ` }, }, create(context: any) { @@ -254,3 +268,9 @@ export default { "require-timeout-comment": requireTimeoutComment, }, }; + +function dedent(strings: TemplateStringsArray, ...values: any[]) { + const indented = strings.map((string, i) => string + (values[i] || "")).join(""); + const startWhitespace = indented.match(/^(\s*)/)?.[0] || ""; + return indented.replaceAll(startWhitespace, '\n').replace(/\n$/, ''); +} \ No newline at end of file From 942f715e12e47d7184753fd3294d903a0eaa61bb Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Fri, 14 Aug 2026 14:27:23 +0100 Subject: [PATCH 2/3] Fix multiline lint message dedentation --- spec/lint-plugin.spec.ts | 13 +++++++++++++ src/lint/plugin.ts | 31 ++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/spec/lint-plugin.spec.ts b/spec/lint-plugin.spec.ts index 6e2b450..b5ee757 100644 --- a/spec/lint-plugin.spec.ts +++ b/spec/lint-plugin.spec.ts @@ -4,12 +4,25 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { promisify } from "node:util"; import { test, expect } from "@playwright/test"; +import lintPlugin from "../src/lint/plugin.js"; const execFileAsync = promisify(execFile); const preferLocatorWaitsRules = { "middlewright/prefer-locator-waits": "error" }; const preferPositiveWaitsRules = { "middlewright/prefer-positive-waits": "error" }; const requireTimeoutCommentRules = { "middlewright/require-timeout-comment": "error" }; +test("formats multiline lint guidance without outer indentation", () => { + const messages = [ + lintPlugin.rules["prefer-positive-waits"].meta.messages.detached, + lintPlugin.rules["require-timeout-comment"].meta.messages.unexplained, + ]; + + for (const message of messages) { + expect(message).toBe(message.trim()); + expect(message).not.toMatch(/(^|\n)[ \t]+\S/); + } +}); + test("fixes visible locator assertions to locator waits", async () => { await using fixture = await lintFixture( `await expect(page.getByText("Ready")).toBeVisible();\n`, diff --git a/src/lint/plugin.ts b/src/lint/plugin.ts index eec1438..b212826 100644 --- a/src/lint/plugin.ts +++ b/src/lint/plugin.ts @@ -75,14 +75,14 @@ const requireTimeoutComment = { unexplained: dedent` Avoid locator timeouts by using spinnerWaiter. Best ways to resolve: - If there's already loading UI, just remove the timeout and rely on spinnerWaiter to wait for the loading UI. - - If there's no loading UI, add loading UI and remove the timeout. + - If there's no loading UI, remove the timeout and add loading UI for spinnerWaiter. - If there's a loading UI but it takes even longer than the spinnerWaiter spinner timeout, use \`await spinnerWaiter.settings.run({ spinnerTimeout: 123_456 }, async () => ...)\` or similar to wait even longer for the spinner to complete. - - If there's loading UI from a part of the code that we don't control (e.g. a library) and it isn't matched by the default spinner selectors, use \`await spinnerWaiter.settings.run({ spinnerSelectors: ["myCustomSpinnerClass"] }, async () => ...)\` + - If there's loading UI from a part of the code that we don't control (e.g. a library) and it isn't matched by the default spinner selectors, use \`await spinnerWaiter.settings.run({ spinnerSelectors: ["myCustomSpinnerClass"] }, async () => ...)\`. - If it is truly impossible for there to be loading UI, add a nearby // comment matching every required pattern: {{patterns}}. - If you're in a block which has done \`await spinnerWaiter.settings.run({ disabled: true }, async () => ...)\`, you should probably *un-disable* for that block and apply the above suggestions to the inner code. See https://github.com/iterate/middlewright#dont-fix-slow-tests-with-longer-timeouts for more details. - ` + `, }, }, create(context: any) { @@ -131,11 +131,13 @@ const preferPositiveWaits = { messages: { detached: dedent` Wait for positive UI instead of element detachment. - If there's no postitive UI to wait for your first port of call should be to *add* the positive UI to the product. + If there's no positive UI to wait for your first port of call should be to *add* the positive UI to the product. If it is truly impossible for there to be a positive UI, then that is quite surprising and you should question that. If you have questioned it and it is still impossible, then you should make sure you've first validated that the UI that's becoming detached previously was visible. Then explain why it's impossible, and why it's safe to wait for detachment in a nearby // comment matching every required pattern: {{patterns}}. - ` + + See https://github.com/iterate/middlewright#prefer-positive-waits-over-absence for more details. + `, }, }, create(context: any) { @@ -270,7 +272,18 @@ export default { }; function dedent(strings: TemplateStringsArray, ...values: any[]) { - const indented = strings.map((string, i) => string + (values[i] || "")).join(""); - const startWhitespace = indented.match(/^(\s*)/)?.[0] || ""; - return indented.replaceAll(startWhitespace, '\n').replace(/\n$/, ''); -} \ No newline at end of file + const indented = strings + .map((string, index) => string + (index < values.length ? String(values[index]) : "")) + .join(""); + const lines = indented.split(/\r?\n/); + + while (lines[0]?.trim() === "") lines.shift(); + while (lines.at(-1)?.trim() === "") lines.pop(); + + const indentation = Math.min( + ...lines + .filter((line) => line.trim()) + .map((line) => line.match(/^[ \t]*/)?.[0].length || 0), + ); + return lines.map((line) => line.slice(indentation)).join("\n"); +} From e9d224b18228f20e884d17d747b62bd9ea66babf Mon Sep 17 00:00:00 2001 From: Misha Kaletsky <15040698+mmkal@users.noreply.github.com> Date: Fri, 14 Aug 2026 14:32:14 +0100 Subject: [PATCH 3/3] Use shared dedent dependency --- src/lint/plugin.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/lint/plugin.ts b/src/lint/plugin.ts index b212826..7f8f862 100644 --- a/src/lint/plugin.ts +++ b/src/lint/plugin.ts @@ -1,3 +1,5 @@ +import dedent from "dedent"; + const requiredPatternsSchema = [ { type: "object", @@ -270,20 +272,3 @@ export default { "require-timeout-comment": requireTimeoutComment, }, }; - -function dedent(strings: TemplateStringsArray, ...values: any[]) { - const indented = strings - .map((string, index) => string + (index < values.length ? String(values[index]) : "")) - .join(""); - const lines = indented.split(/\r?\n/); - - while (lines[0]?.trim() === "") lines.shift(); - while (lines.at(-1)?.trim() === "") lines.pop(); - - const indentation = Math.min( - ...lines - .filter((line) => line.trim()) - .map((line) => line.match(/^[ \t]*/)?.[0].length || 0), - ); - return lines.map((line) => line.slice(indentation)).join("\n"); -}