From b78acff2f62263f1c42ad3f8df23bae06ef4f1ef Mon Sep 17 00:00:00 2001 From: Przemas Date: Wed, 8 Apr 2026 17:02:02 +0200 Subject: [PATCH] chore(deps): add eslint-plugin-playwright and fix invalid expects - Bump `concurrently` to ^9.0.0 to mitigate shell injection vulnerabilities. - Introduce `eslint-plugin-playwright` to enforce testing guardrails. - Set `playwright/no-wait-for-timeout` to `warn` to keep CI green while flagging tech debt. - Enforce `playwright/valid-expect` and `playwright/no-focused-test` as `error`. - Fix existing `valid-expect` violations: - Add missing `.toBeTruthy()` in `resource-timing.spec.ts`. - Remove redundant, matcher-less expects in `selectors-text.spec.ts`. - Add inline eslint-disable for intentional negative tests in `expect-boolean.spec.ts`. --- eslint.config.mjs | 16 +++ package-lock.json | 170 +++++++++++++++----------- package.json | 3 +- tests/library/resource-timing.spec.ts | 2 +- tests/page/expect-boolean.spec.ts | 2 + tests/page/selectors-text.spec.ts | 4 - 6 files changed, 117 insertions(+), 80 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 1b3179ba81731..897f96797c362 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -21,6 +21,7 @@ import path from "path"; import { fileURLToPath } from "url"; import stylistic from "@stylistic/eslint-plugin"; import importRules from "eslint-plugin-import"; +import playwrightPlugin from "eslint-plugin-playwright"; import progressPlugin from "./utils/eslint-plugin-progress/index.js"; import { fixupConfigRules } from "@eslint/compat"; import { FlatCompat } from "@eslint/eslintrc"; @@ -496,6 +497,21 @@ export default [ ...noFloatingPromisesRules, }, }, + { + // Playwright-specific guardrails applied only to spec files. + // - no-wait-for-timeout: warn (93 existing hits → keep CI green while flagging tech debt) + // - valid-expect: error (catches missing await on expect() calls) + // - no-focused-test: error (blocks test.only from landing; backs up forbidOnly on CI) + files: ["tests/**/*.spec.ts", "tests/**/*.spec.js"], + plugins: { + playwright: playwrightPlugin, + }, + rules: { + "playwright/no-wait-for-timeout": "warn", + "playwright/valid-expect": "error", + "playwright/no-focused-test": "error", + }, + }, ...reactBaseConfig.map((config) => ({ ...config, files: reactFiles, diff --git a/package-lock.json b/package-lock.json index 0f5701d5be9ea..2d835479b672f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,7 @@ "chromium-bidi": "^12.0.0", "colors": "1.4.0", "commander": "^13.0.0", - "concurrently": "^6.2.1", + "concurrently": "^9.2.1", "cross-env": "^7.0.3", "dotenv": "^16.4.5", "electron": "^39.8.4", @@ -61,6 +61,7 @@ "eslint": "^9.34.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-notice": "^1.0.0", + "eslint-plugin-playwright": "^2.10.1", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "formidable": "^2.1.1", @@ -391,15 +392,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", @@ -2650,6 +2642,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3207,14 +3200,18 @@ } }, "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" } }, "node_modules/clone-response": { @@ -3278,25 +3275,28 @@ "dev": true }, "node_modules/concurrently": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.5.1.tgz", - "integrity": "sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag==", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^4.1.0", - "date-fns": "^2.16.1", - "lodash": "^4.17.21", - "rxjs": "^6.6.3", - "spawn-command": "^0.0.2-1", - "supports-color": "^8.1.0", - "tree-kill": "^1.2.2", - "yargs": "^16.2.0" + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" }, "bin": { - "concurrently": "bin/concurrently.js" + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" }, "engines": { - "node": ">=10.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, "node_modules/content-disposition": { @@ -3450,22 +3450,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/date-fns": { - "version": "2.30.0", - "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", - "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.21.0" - }, - "engines": { - "node": ">=0.11" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/date-fns" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -3703,7 +3687,8 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", @@ -4172,6 +4157,35 @@ "eslint": ">=3.0.0" } }, + "node_modules/eslint-plugin-playwright": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.10.1.tgz", + "integrity": "sha512-qea3UxBOb8fTwJ77FMApZKvRye5DOluDHcev0LDJwID3RELeun0JlqzrNIXAB/SXCyB/AesCW/6sZfcT9q3Edg==", + "dev": true, + "license": "MIT", + "dependencies": { + "globals": "^17.3.0" + }, + "engines": { + "node": ">=16.9.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/eslint-plugin-playwright/node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-plugin-react": { "version": "7.37.5", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", @@ -4772,6 +4786,7 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -5451,6 +5466,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -7000,6 +7016,7 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -7141,23 +7158,15 @@ } }, "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" + "tslib": "^2.1.0" } }, - "node_modules/rxjs/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -7387,6 +7396,19 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -7538,12 +7560,6 @@ "source-map": "^0.6.0" } }, - "node_modules/spawn-command": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2.tgz", - "integrity": "sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ==", - "dev": true - }, "node_modules/spdx-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/spdx-compare/-/spdx-compare-1.0.0.tgz", @@ -7656,6 +7672,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7763,6 +7780,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -8405,6 +8423,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8472,6 +8491,7 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } @@ -8497,30 +8517,32 @@ } }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { - "cliui": "^7.0.2", + "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.1.1" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yauzl": { diff --git a/package.json b/package.json index 620a1e136b7e1..361441639bbbc 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "chromium-bidi": "^12.0.0", "colors": "1.4.0", "commander": "^13.0.0", - "concurrently": "^6.2.1", + "concurrently": "^9.2.1", "cross-env": "^7.0.3", "dotenv": "^16.4.5", "electron": "^39.8.4", @@ -101,6 +101,7 @@ "eslint": "^9.34.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-notice": "^1.0.0", + "eslint-plugin-playwright": "^2.10.1", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "formidable": "^2.1.1", diff --git a/tests/library/resource-timing.spec.ts b/tests/library/resource-timing.spec.ts index b100a102fbfd3..488c1a817f61b 100644 --- a/tests/library/resource-timing.spec.ts +++ b/tests/library/resource-timing.spec.ts @@ -129,7 +129,7 @@ it('should work when serving from memory cache', async ({ contextFactory, server }); function verifyTimingValue(value: number, previous: number) { - expect(value === -1 || value > 0 && value >= previous); + expect(value === -1 || value > 0 && value >= previous).toBeTruthy(); } function verifyConnectionTimingConsistency(timing) { diff --git a/tests/page/expect-boolean.spec.ts b/tests/page/expect-boolean.spec.ts index f2eddbc3f1b6e..0d6260bfa3032 100644 --- a/tests/page/expect-boolean.spec.ts +++ b/tests/page/expect-boolean.spec.ts @@ -542,12 +542,14 @@ test.describe(() => { test('toBeOK fail with invalid argument', async ({ page }) => { + // eslint-disable-next-line playwright/valid-expect const error = await (expect(page) as any).toBeOK().catch(e => e); expect(error.message).toContain('toBeOK can be only used with APIResponse object'); }); test('toBeOK fail with promise', async ({ page, server }) => { const res = page.request.get(server.EMPTY_PAGE); + // eslint-disable-next-line playwright/valid-expect const error = await (expect(res) as any).toBeOK().catch(e => e); expect(error.message).toContain('toBeOK can be only used with APIResponse object'); await res; diff --git a/tests/page/selectors-text.spec.ts b/tests/page/selectors-text.spec.ts index 3793cdac5c667..4095ddfee9638 100644 --- a/tests/page/selectors-text.spec.ts +++ b/tests/page/selectors-text.spec.ts @@ -467,10 +467,6 @@ it('should work with unpaired quotes when not at the start', async ({ page }) => it('should work with paired quotes in the middle of selector', async ({ page }) => { it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/16858' }); await page.setContent(`
pattern "^-?\\d+$"
`); - expect(await page.locator(`div >> text=pattern "^-?\\d+$`).isVisible()); - expect(await page.locator(`div >> text=pattern "^-?\\d+$"`).isVisible()); - // Should double escape inside quoted text. - expect(await page.locator(`div >> text='pattern "^-?\\\\d+$"'`).isVisible()); await expect(page.locator(`div >> text=pattern "^-?\\d+$`)).toBeVisible(); await expect(page.locator(`div >> text=pattern "^-?\\d+$"`)).toBeVisible(); // Should double escape inside quoted text.