fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

21
node_modules/eslint-plugin-playwright/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Max Schmitt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

85
node_modules/eslint-plugin-playwright/README.md generated vendored Normal file
View File

@@ -0,0 +1,85 @@
# ESLint Plugin Playwright
[![Test](https://github.com/playwright-community/eslint-plugin-playwright/actions/workflows/test.yml/badge.svg)](https://github.com/playwright-community/eslint-plugin-playwright/actions/workflows/test.yml)
[![npm](https://img.shields.io/npm/v/eslint-plugin-playwright)](https://www.npmjs.com/package/eslint-plugin-playwright)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
ESLint plugin for [Playwright](https://github.com/microsoft/playwright).
## Installation
npm
```bash
npm install -D eslint-plugin-playwright
```
Yarn
```bash
yarn add -D eslint-plugin-playwright
```
pnpm
```bash
pnpm add -D eslint-plugin-playwright
```
## Usage
This plugin bundles two configurations to work with both `@playwright/test` or
`jest-playwright`.
### With [Playwright test runner](https://playwright.dev/docs/writing-tests)
```json
{
"extends": ["plugin:playwright/recommended"]
}
```
### With [Jest Playwright](https://github.com/playwright-community/jest-playwright)
```json
{
"extends": ["plugin:playwright/jest-playwright"]
}
```
## List of Supported Rules
✔: Enabled in the recommended configuration.\
🔧: Some problems reported by this rule are automatically fixable by the [`--fix`](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix)
command line option.\
💡: Some problems reported by this rule are manually fixable by editor
[suggestions](https://eslint.org/docs/latest/developer-guide/working-with-rules#providing-suggestions).
| ✔ | 🔧 | 💡 | Rule | Description |
| :-: | :-: | :-: | --------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| ✔ | | | [expect-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/expect-expect.md) | Enforce assertion to be made in a test body |
| ✔ | | | [max-nested-describe](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-nested-describe.md) | Enforces a maximum depth to nested describe calls |
| ✔ | 🔧 | | [missing-playwright-await](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/missing-playwright-await.md) | Enforce Playwright APIs to be awaited |
| ✔ | | | [no-conditional-in-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-in-test.md) | Disallow conditional logic in tests |
| ✔ | | 💡 | [no-element-handle](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-element-handle.md) | Disallow usage of element handles |
| ✔ | | | [no-eval](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-eval.md) | Disallow usage of `page.$eval` and `page.$$eval` |
| ✔ | | 💡 | [no-focused-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-focused-test.md) | Disallow usage of `.only` annotation |
| ✔ | | | [no-force-option](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-force-option.md) | Disallow usage of the `{ force: true }` option |
| ✔ | | | [no-nested-step](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nested-step.md) | Disallow nested `test.step()` methods |
| ✔ | | | [no-networkidle](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-networkidle.md) | Disallow usage of the `networkidle` option |
| | | | [no-nth-methods](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nth-methods.md) | Disallow usage of `first()`, `last()`, and `nth()` methods |
| ✔ | | | [no-page-pause](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-page-pause.md) | Disallow using `page.pause` |
| ✔ | 🔧 | | [no-useless-await](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-await.md) | Disallow unnecessary `await`s for Playwright methods |
| | | | [no-restricted-matchers](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers |
| ✔ | | 💡 | [no-skipped-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-skipped-test.md) | Disallow usage of the `.skip` annotation |
| ✔ | 🔧 | | [no-useless-not](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-not.md) | Disallow usage of `not` matchers when a specific matcher exists |
| ✔ | | 💡 | [no-wait-for-timeout](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-timeout.md) | Disallow usage of `page.waitForTimeout` |
| | | 💡 | [prefer-strict-equal](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` |
| | 🔧 | | [prefer-lowercase-title](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-lowercase-title.md) | Enforce lowercase test names |
| | 🔧 | | [prefer-to-be](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-be.md) | Suggest using `toBe()` |
| | 🔧 | | [prefer-to-contain](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-contain.md) | Suggest using `toContain()` |
| | 🔧 | | [prefer-to-have-length](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-length.md) | Suggest using `toHaveLength()` |
| ✔ | 🔧 | | [prefer-web-first-assertions](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-web-first-assertions.md) | Suggest using web first assertions |
| | | | [require-top-level-describe](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-top-level-describe.md) | Require test cases and hooks to be inside a `test.describe` block |
| | 🔧 | | [require-soft-assertions](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-soft-assertions.md) | Require assertions to use `expect.soft()` |
| ✔ | | | [valid-expect](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-expect.md) | Enforce valid `expect()` usage |

119
node_modules/eslint-plugin-playwright/lib/index.js generated vendored Normal file
View File

@@ -0,0 +1,119 @@
"use strict";
const expect_expect_1 = require("./rules/expect-expect");
const max_nested_describe_1 = require("./rules/max-nested-describe");
const missing_playwright_await_1 = require("./rules/missing-playwright-await");
const no_conditional_in_test_1 = require("./rules/no-conditional-in-test");
const no_element_handle_1 = require("./rules/no-element-handle");
const no_eval_1 = require("./rules/no-eval");
const no_focused_test_1 = require("./rules/no-focused-test");
const no_force_option_1 = require("./rules/no-force-option");
const no_nested_step_1 = require("./rules/no-nested-step");
const no_networkidle_1 = require("./rules/no-networkidle");
const no_nth_methods_1 = require("./rules/no-nth-methods");
const no_page_pause_1 = require("./rules/no-page-pause");
const no_restricted_matchers_1 = require("./rules/no-restricted-matchers");
const no_skipped_test_1 = require("./rules/no-skipped-test");
const no_useless_await_1 = require("./rules/no-useless-await");
const no_useless_not_1 = require("./rules/no-useless-not");
const no_wait_for_timeout_1 = require("./rules/no-wait-for-timeout");
const prefer_lowercase_title_1 = require("./rules/prefer-lowercase-title");
const prefer_strict_equal_1 = require("./rules/prefer-strict-equal");
const prefer_to_be_1 = require("./rules/prefer-to-be");
const prefer_to_contain_1 = require("./rules/prefer-to-contain");
const prefer_to_have_length_1 = require("./rules/prefer-to-have-length");
const prefer_web_first_assertions_1 = require("./rules/prefer-web-first-assertions");
const require_soft_assertions_1 = require("./rules/require-soft-assertions");
const require_top_level_describe_1 = require("./rules/require-top-level-describe");
const valid_expect_1 = require("./rules/valid-expect");
const recommended = {
env: {
'shared-node-browser': true,
},
plugins: ['playwright'],
rules: {
'no-empty-pattern': 'off',
'playwright/expect-expect': 'warn',
'playwright/max-nested-describe': 'warn',
'playwright/missing-playwright-await': 'error',
'playwright/no-conditional-in-test': 'warn',
'playwright/no-element-handle': 'warn',
'playwright/no-eval': 'warn',
'playwright/no-focused-test': 'error',
'playwright/no-force-option': 'warn',
'playwright/no-nested-step': 'warn',
'playwright/no-networkidle': 'error',
'playwright/no-page-pause': 'warn',
'playwright/no-skipped-test': 'warn',
'playwright/no-useless-await': 'warn',
'playwright/no-useless-not': 'warn',
'playwright/no-wait-for-timeout': 'warn',
'playwright/prefer-web-first-assertions': 'error',
'playwright/valid-expect': 'error',
},
};
module.exports = {
configs: {
'jest-playwright': {
env: {
jest: true,
'shared-node-browser': true,
},
globals: {
browser: true,
browserName: true,
context: true,
deviceName: true,
jestPlaywright: true,
page: true,
},
plugins: ['jest', 'playwright'],
rules: {
'jest/no-standalone-expect': [
'error',
{
additionalTestBlockFunctions: [
'test.jestPlaywrightDebug',
'it.jestPlaywrightDebug',
'test.jestPlaywrightSkip',
'it.jestPlaywrightSkip',
'test.jestPlaywrightConfig',
'it.jestPlaywrightConfig',
],
},
],
'playwright/missing-playwright-await': 'error',
'playwright/no-page-pause': 'warn',
},
},
'playwright-test': recommended,
recommended,
},
rules: {
'expect-expect': expect_expect_1.default,
'max-nested-describe': max_nested_describe_1.default,
'missing-playwright-await': missing_playwright_await_1.default,
'no-conditional-in-test': no_conditional_in_test_1.default,
'no-element-handle': no_element_handle_1.default,
'no-eval': no_eval_1.default,
'no-focused-test': no_focused_test_1.default,
'no-force-option': no_force_option_1.default,
'no-nested-step': no_nested_step_1.default,
'no-networkidle': no_networkidle_1.default,
'no-nth-methods': no_nth_methods_1.default,
'no-page-pause': no_page_pause_1.default,
'no-restricted-matchers': no_restricted_matchers_1.default,
'no-skipped-test': no_skipped_test_1.default,
'no-useless-await': no_useless_await_1.default,
'no-useless-not': no_useless_not_1.default,
'no-wait-for-timeout': no_wait_for_timeout_1.default,
'prefer-lowercase-title': prefer_lowercase_title_1.default,
'prefer-strict-equal': prefer_strict_equal_1.default,
'prefer-to-be': prefer_to_be_1.default,
'prefer-to-contain': prefer_to_contain_1.default,
'prefer-to-have-length': prefer_to_have_length_1.default,
'prefer-web-first-assertions': prefer_web_first_assertions_1.default,
'require-soft-assertions': require_soft_assertions_1.default,
'require-top-level-describe': require_top_level_describe_1.default,
'valid-expect': valid_expect_1.default,
},
};

View File

@@ -0,0 +1,44 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
exports.default = {
create(context) {
const unchecked = [];
function checkExpressions(nodes) {
for (const node of nodes) {
const index = node.type === 'CallExpression' ? unchecked.indexOf(node) : -1;
if (index !== -1) {
unchecked.splice(index, 1);
break;
}
}
}
return {
CallExpression(node) {
if ((0, ast_1.isTest)(node, ['fixme', 'only', 'skip'])) {
unchecked.push(node);
}
else if ((0, ast_1.isExpectCall)(node)) {
checkExpressions(context.getAncestors());
}
},
'Program:exit'() {
unchecked.forEach((node) => {
context.report({ messageId: 'noAssertions', node });
});
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Enforce assertion to be made in a test body',
recommended: true,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/expect-expect.md',
},
messages: {
noAssertions: 'Test has no assertions',
},
type: 'problem',
},
};

View File

@@ -0,0 +1,63 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
exports.default = {
create(context) {
const { options } = context;
const max = options[0]?.max ?? 5;
const describeCallbackStack = [];
function pushDescribeCallback(node) {
if (node.parent.type !== 'CallExpression' ||
!(0, ast_1.isDescribeCall)(node.parent)) {
return;
}
describeCallbackStack.push(0);
if (describeCallbackStack.length > max) {
context.report({
data: {
depth: describeCallbackStack.length.toString(),
max: max.toString(),
},
messageId: 'exceededMaxDepth',
node: node.parent.callee,
});
}
}
function popDescribeCallback(node) {
const { parent } = node;
if (parent.type === 'CallExpression' && (0, ast_1.isDescribeCall)(parent)) {
describeCallbackStack.pop();
}
}
return {
ArrowFunctionExpression: pushDescribeCallback,
'ArrowFunctionExpression:exit': popDescribeCallback,
FunctionExpression: pushDescribeCallback,
'FunctionExpression:exit': popDescribeCallback,
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Enforces a maximum depth to nested describe calls',
recommended: true,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/max-nested-describe.md',
},
messages: {
exceededMaxDepth: 'Maximum describe call depth exceeded ({{ depth }}). Maximum allowed is {{ max }}.',
},
schema: [
{
additionalProperties: false,
properties: {
max: {
minimum: 0,
type: 'integer',
},
},
type: 'object',
},
],
type: 'suggestion',
},
};

View File

@@ -0,0 +1,141 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
const validTypes = new Set([
'AwaitExpression',
'ReturnStatement',
'ArrowFunctionExpression',
]);
const expectPlaywrightMatchers = [
'toBeChecked',
'toBeDisabled',
'toBeEnabled',
'toEqualText',
'toEqualUrl',
'toEqualValue',
'toHaveFocus',
'toHaveSelector',
'toHaveSelectorCount',
'toHaveText',
'toMatchAttribute',
'toMatchComputedStyle',
'toMatchText',
'toMatchTitle',
'toMatchURL',
'toMatchValue',
];
const playwrightTestMatchers = [
'toBeChecked',
'toBeDisabled',
'toBeEditable',
'toBeEmpty',
'toBeEnabled',
'toBeFocused',
'toBeHidden',
'toBeVisible',
'toContainText',
'toHaveAttribute',
'toHaveClass',
'toHaveCount',
'toHaveCSS',
'toHaveId',
'toHaveJSProperty',
'toBeOK',
'toHaveScreenshot',
'toHaveText',
'toHaveTitle',
'toHaveURL',
'toHaveValue',
];
function getCallType(node, awaitableMatchers) {
// test.step
if (node.callee.type === 'MemberExpression' &&
(0, ast_1.isIdentifier)(node.callee.object, 'test') &&
(0, ast_1.isPropertyAccessor)(node.callee, 'step')) {
return { messageId: 'testStep' };
}
const expectType = (0, ast_1.getExpectType)(node);
if (!expectType)
return;
// expect.poll
if (expectType === 'poll') {
return { messageId: 'expectPoll' };
}
// expect with awaitable matcher
const [lastMatcher] = (0, ast_1.getMatchers)(node).slice(-1);
const matcherName = (0, ast_1.getStringValue)(lastMatcher);
if (awaitableMatchers.has(matcherName)) {
return { data: { matcherName }, messageId: 'expect' };
}
}
function isPromiseAll(node) {
return node.type === 'ArrayExpression' &&
node.parent.type === 'CallExpression' &&
node.parent.callee.type === 'MemberExpression' &&
(0, ast_1.isIdentifier)(node.parent.callee.object, 'Promise') &&
(0, ast_1.isIdentifier)(node.parent.callee.property, 'all')
? node.parent
: null;
}
function checkValidity(node) {
if (validTypes.has(node.parent.type))
return;
const promiseAll = isPromiseAll(node.parent);
return promiseAll
? checkValidity(promiseAll)
: node.parent.type === 'MemberExpression' ||
(node.parent.type === 'CallExpression' && node.parent.callee === node)
? checkValidity(node.parent)
: node;
}
exports.default = {
create(context) {
const options = context.options[0] || {};
const awaitableMatchers = new Set([
...expectPlaywrightMatchers,
...playwrightTestMatchers,
// Add any custom matchers to the set
...(options.customMatchers || []),
]);
return {
CallExpression(node) {
const result = getCallType(node, awaitableMatchers);
const reportNode = result ? checkValidity(node) : undefined;
if (result && reportNode) {
context.report({
data: result.data,
fix: (fixer) => fixer.insertTextBefore(node, 'await '),
messageId: result.messageId,
node: node.callee,
});
}
},
};
},
meta: {
docs: {
category: 'Possible Errors',
description: `Identify false positives when async Playwright APIs are not properly awaited.`,
recommended: true,
},
fixable: 'code',
messages: {
expect: "'{{matcherName}}' must be awaited or returned.",
expectPoll: "'expect.poll' matchers must be awaited or returned.",
testStep: "'test.step' must be awaited or returned.",
},
schema: [
{
additionalProperties: false,
properties: {
customMatchers: {
items: { type: 'string' },
type: 'array',
},
},
type: 'object',
},
],
type: 'problem',
},
};

View File

@@ -0,0 +1,32 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
exports.default = {
create(context) {
function checkConditional(node) {
const call = (0, ast_1.findParent)(node, 'CallExpression');
if (call && (0, ast_1.isTest)(call)) {
context.report({ messageId: 'conditionalInTest', node });
}
}
return {
ConditionalExpression: checkConditional,
IfStatement: checkConditional,
LogicalExpression: checkConditional,
SwitchStatement: checkConditional,
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow conditional logic in tests',
recommended: true,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-conditional-in-test.md',
},
messages: {
conditionalInTest: 'Avoid having conditionals in tests',
},
schema: [],
type: 'problem',
},
};

View File

@@ -0,0 +1,57 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
function getPropertyRange(node) {
return node.type === 'Identifier'
? node.range
: [node.range[0] + 1, node.range[1] - 1];
}
exports.default = {
create(context) {
return {
CallExpression(node) {
if ((0, ast_1.isPageMethod)(node, '$') || (0, ast_1.isPageMethod)(node, '$$')) {
context.report({
messageId: 'noElementHandle',
node: node.callee,
suggest: [
{
fix: (fixer) => {
const { property } = node.callee;
// Replace $/$$ with locator
const fixes = [
fixer.replaceTextRange(getPropertyRange(property), 'locator'),
];
// Remove the await expression if it exists as locators do
// not need to be awaited.
if (node.parent.type === 'AwaitExpression') {
fixes.push(fixer.removeRange([node.parent.range[0], node.range[0]]));
}
return fixes;
},
messageId: (0, ast_1.isPageMethod)(node, '$')
? 'replaceElementHandleWithLocator'
: 'replaceElementHandlesWithLocator',
},
],
});
}
},
};
},
meta: {
docs: {
category: 'Possible Errors',
description: 'The use of ElementHandle is discouraged, use Locator instead',
recommended: true,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-element-handle.md',
},
hasSuggestions: true,
messages: {
noElementHandle: 'Unexpected use of element handles.',
replaceElementHandlesWithLocator: 'Replace `page.$$` with `page.locator`',
replaceElementHandleWithLocator: 'Replace `page.$` with `page.locator`',
},
type: 'suggestion',
},
};

View File

@@ -0,0 +1,31 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
exports.default = {
create(context) {
return {
CallExpression(node) {
const isEval = (0, ast_1.isPageMethod)(node, '$eval');
if (isEval || (0, ast_1.isPageMethod)(node, '$$eval')) {
context.report({
messageId: isEval ? 'noEval' : 'noEvalAll',
node: node.callee,
});
}
},
};
},
meta: {
docs: {
category: 'Possible Errors',
description: 'The use of `page.$eval` and `page.$$eval` are discouraged, use `locator.evaluate` or `locator.evaluateAll` instead',
recommended: true,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-eval.md',
},
messages: {
noEval: 'Unexpected use of page.$eval().',
noEvalAll: 'Unexpected use of page.$$eval().',
},
type: 'problem',
},
};

View File

@@ -0,0 +1,44 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
exports.default = {
create(context) {
return {
CallExpression(node) {
if (((0, ast_1.isTest)(node) || (0, ast_1.isDescribeCall)(node)) &&
node.callee.type === 'MemberExpression' &&
(0, ast_1.isPropertyAccessor)(node.callee, 'only')) {
const { callee } = node;
context.report({
messageId: 'noFocusedTest',
node: node.callee.property,
suggest: [
{
// - 1 to remove the `.only` annotation with dot notation
fix: (fixer) => fixer.removeRange([
callee.property.range[0] - 1,
callee.range[1],
]),
messageId: 'suggestRemoveOnly',
},
],
});
}
},
};
},
meta: {
docs: {
category: 'Possible Errors',
description: 'Prevent usage of `.only()` focus test annotation',
recommended: true,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-focused-test.md',
},
hasSuggestions: true,
messages: {
noFocusedTest: 'Unexpected focused test.',
suggestRemoveOnly: 'Remove .only() annotation.',
},
type: 'problem',
},
};

View File

@@ -0,0 +1,51 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
function isForceOptionEnabled(node) {
const arg = node.arguments[node.arguments.length - 1];
return (arg?.type === 'ObjectExpression' &&
arg.properties.find((property) => property.type === 'Property' &&
(0, ast_1.getStringValue)(property.key) === 'force' &&
(0, ast_1.isBooleanLiteral)(property.value, true)));
}
// https://playwright.dev/docs/api/class-locator
const methodsWithForceOption = new Set([
'check',
'uncheck',
'click',
'dblclick',
'dragTo',
'fill',
'hover',
'selectOption',
'selectText',
'setChecked',
'tap',
]);
exports.default = {
create(context) {
return {
MemberExpression(node) {
if (methodsWithForceOption.has((0, ast_1.getStringValue)(node.property)) &&
node.parent.type === 'CallExpression') {
const reportNode = isForceOptionEnabled(node.parent);
if (reportNode) {
context.report({ messageId: 'noForceOption', node: reportNode });
}
}
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Prevent usage of `{ force: true }` option.',
recommended: true,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-force-option.md',
},
messages: {
noForceOption: 'Unexpected use of { force: true } option.',
},
type: 'suggestion',
},
};

View File

@@ -0,0 +1,52 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
function isStepCall(node) {
const inner = node.type === 'CallExpression' ? node.callee : node;
if (inner.type !== 'MemberExpression') {
return false;
}
return (0, ast_1.isPropertyAccessor)(inner, 'step');
}
exports.default = {
create(context) {
const stack = [];
function pushStepCallback(node) {
if (node.parent.type !== 'CallExpression' || !isStepCall(node.parent)) {
return;
}
stack.push(0);
if (stack.length > 1) {
context.report({
messageId: 'noNestedStep',
node: node.parent.callee,
});
}
}
function popStepCallback(node) {
const { parent } = node;
if (parent.type === 'CallExpression' && isStepCall(parent)) {
stack.pop();
}
}
return {
ArrowFunctionExpression: pushStepCallback,
'ArrowFunctionExpression:exit': popStepCallback,
FunctionExpression: pushStepCallback,
'FunctionExpression:exit': popStepCallback,
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow nested `test.step()` methods',
recommended: true,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nested-step.md',
},
messages: {
noNestedStep: 'Do not nest `test.step()` methods.',
},
schema: [],
type: 'problem',
},
};

View File

@@ -0,0 +1,57 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
const messageId = 'noNetworkidle';
const methods = new Set([
'goBack',
'goForward',
'goto',
'reload',
'setContent',
'waitForLoadState',
'waitForURL',
]);
exports.default = {
create(context) {
return {
CallExpression(node) {
if (node.callee.type !== 'MemberExpression')
return;
const methodName = (0, ast_1.getStringValue)(node.callee.property);
if (!methods.has(methodName))
return;
// waitForLoadState has a single string argument
if (methodName === 'waitForLoadState') {
const arg = node.arguments[0];
if (arg && (0, ast_1.isStringLiteral)(arg, 'networkidle')) {
context.report({ messageId, node: arg });
}
return;
}
// All other methods have an options object
if (node.arguments.length >= 2) {
const [_, arg] = node.arguments;
if (arg.type !== 'ObjectExpression')
return;
const property = arg.properties
.filter((p) => p.type === 'Property')
.find((p) => (0, ast_1.isStringLiteral)(p.value, 'networkidle'));
if (property) {
context.report({ messageId, node: property.value });
}
}
},
};
},
meta: {
docs: {
category: 'Possible Errors',
description: 'Prevent usage of the networkidle option',
recommended: true,
},
messages: {
noNetworkidle: 'Unexpected use of networkidle.',
},
type: 'problem',
},
};

View File

@@ -0,0 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
const methods = new Set(['first', 'last', 'nth']);
exports.default = {
create(context) {
return {
CallExpression(node) {
if (node.callee.type !== 'MemberExpression')
return;
const method = (0, ast_1.getStringValue)(node.callee.property);
if (!methods.has(method))
return;
context.report({
data: { method },
loc: {
end: node.loc.end,
start: node.callee.property.loc.start,
},
messageId: 'noNthMethod',
});
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow usage of nth methods',
recommended: true,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-nth-methods.md',
},
messages: {
noNthMethod: 'Unexpected use of {{method}}()',
},
type: 'problem',
},
};

View File

@@ -0,0 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
exports.default = {
create(context) {
return {
CallExpression(node) {
if ((0, ast_1.isPageMethod)(node, 'pause')) {
context.report({ messageId: 'noPagePause', node });
}
},
};
},
meta: {
docs: {
category: 'Possible Errors',
description: 'Prevent usage of page.pause()',
recommended: true,
},
messages: {
noPagePause: 'Unexpected use of page.pause().',
},
type: 'problem',
},
};

View File

@@ -0,0 +1,72 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
const parseExpectCall_1 = require("../utils/parseExpectCall");
exports.default = {
create(context) {
const restrictedChains = (context.options?.[0] ?? {});
return {
CallExpression(node) {
const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
if (!expectCall)
return;
Object.entries(restrictedChains)
.map(([restriction, message]) => {
const chain = expectCall.members;
const restrictionLinks = restriction.split('.').length;
// Find in the full chain, where the restriction chain starts
const startIndex = chain.findIndex((_, i) => {
// Construct the partial chain to compare against the restriction
// chain string.
const partial = chain
.slice(i, i + restrictionLinks)
.map(ast_1.getStringValue)
.join('.');
return partial === restriction;
});
return {
// If the restriction chain was found, return the portion of the
// chain that matches the restriction chain.
chain: startIndex !== -1
? chain.slice(startIndex, startIndex + restrictionLinks)
: [],
message,
restriction,
};
})
.filter(({ chain }) => chain.length)
.forEach(({ chain, message, restriction }) => {
context.report({
data: { message: message ?? '', restriction },
loc: {
end: chain[chain.length - 1].loc.end,
start: chain[0].loc.start,
},
messageId: message ? 'restrictedWithMessage' : 'restricted',
});
});
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow specific matchers & modifiers',
recommended: false,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md',
},
messages: {
restricted: 'Use of `{{restriction}}` is disallowed',
restrictedWithMessage: '{{message}}',
},
schema: [
{
additionalProperties: {
type: ['string', 'null'],
},
type: 'object',
},
],
type: 'suggestion',
},
};

View File

@@ -0,0 +1,48 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
exports.default = {
create(context) {
return {
CallExpression(node) {
const { callee } = node;
if (((0, ast_1.isTestIdentifier)(callee) || (0, ast_1.isDescribeCall)(node)) &&
callee.type === 'MemberExpression' &&
(0, ast_1.isPropertyAccessor)(callee, 'skip')) {
const isHook = (0, ast_1.isTest)(node) || (0, ast_1.isDescribeCall)(node);
context.report({
messageId: 'noSkippedTest',
node: isHook ? callee.property : node,
suggest: [
{
fix: (fixer) => {
return isHook
? fixer.removeRange([
callee.property.range[0] - 1,
callee.range[1],
])
: fixer.remove(node.parent);
},
messageId: 'removeSkippedTestAnnotation',
},
],
});
}
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Prevent usage of the `.skip()` skip test annotation.',
recommended: true,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-skipped-test.md',
},
hasSuggestions: true,
messages: {
noSkippedTest: 'Unexpected use of the `.skip()` annotation.',
removeSkippedTestAnnotation: 'Remove the `.skip()` annotation.',
},
type: 'suggestion',
},
};

View File

@@ -0,0 +1,87 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
const locatorMethods = new Set([
'and',
'first',
'getByAltText',
'getByLabel',
'getByPlaceholder',
'getByRole',
'getByTestId',
'getByText',
'getByTitle',
'last',
'locator',
'nth',
'or',
]);
const pageMethods = new Set([
'childFrames',
'frame',
'frameLocator',
'frames',
'isClosed',
'isDetached',
'mainFrame',
'name',
'on',
'page',
'parentFrame',
'setDefaultNavigationTimeout',
'setDefaultTimeout',
'url',
'video',
'viewportSize',
'workers',
]);
function isSupportedMethod(node) {
if (node.callee.type !== 'MemberExpression')
return false;
const name = (0, ast_1.getStringValue)(node.callee.property);
return (locatorMethods.has(name) ||
(pageMethods.has(name) && (0, ast_1.isPageMethod)(node, name)));
}
exports.default = {
create(context) {
return {
AwaitExpression(node) {
// Must be a call expression
if (node.argument.type !== 'CallExpression')
return;
// Must be a foo.bar() call, bare calls are ignored
const { callee } = node.argument;
if (callee.type !== 'MemberExpression')
return;
// Must be a method we care about
if (!isSupportedMethod(node.argument))
return;
const start = node.loc.start;
const range = node.range;
context.report({
fix: (fixer) => fixer.removeRange([range[0], range[0] + 6]),
loc: {
end: {
column: start.column + 5,
line: start.line,
},
start,
},
messageId: 'noUselessAwait',
});
},
};
},
meta: {
docs: {
category: 'Possible Errors',
description: 'Disallow unnecessary awaits for Playwright methods',
recommended: true,
},
fixable: 'code',
messages: {
noUselessAwait: 'Unnecessary await expression. This method does not return a Promise.',
},
type: 'problem',
},
};

View File

@@ -0,0 +1,59 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
const fixer_1 = require("../utils/fixer");
const parseExpectCall_1 = require("../utils/parseExpectCall");
const matcherMap = {
toBeDisabled: 'toBeEnabled',
toBeEnabled: 'toBeDisabled',
toBeHidden: 'toBeVisible',
toBeVisible: 'toBeHidden',
};
exports.default = {
create(context) {
return {
CallExpression(node) {
const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
if (!expectCall)
return;
// As the name implies, this rule only implies if the not modifier is
// part of the matcher chain
const notModifier = expectCall.modifiers.find((mod) => (0, ast_1.getStringValue)(mod) === 'not');
if (!notModifier)
return;
// This rule only applies to specific matchers that have opposites
if (expectCall.matcherName in matcherMap) {
const newMatcher = matcherMap[expectCall.matcherName];
context.report({
data: { new: newMatcher, old: expectCall.matcherName },
fix: (fixer) => [
fixer.removeRange([
notModifier.range[0] - (0, fixer_1.getRangeOffset)(notModifier),
notModifier.range[1] + 1,
]),
(0, fixer_1.replaceAccessorFixer)(fixer, expectCall.matcher, newMatcher),
],
loc: {
end: expectCall.matcher.loc.end,
start: notModifier.loc.start,
},
messageId: 'noUselessNot',
});
}
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: `Disallow usage of 'not' matchers when a more specific matcher exists`,
recommended: true,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-not.md',
},
fixable: 'code',
messages: {
noUselessNot: 'Unexpected usage of not.{{old}}(). Use {{new}}() instead.',
},
type: 'problem',
},
};

View File

@@ -0,0 +1,39 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
exports.default = {
create(context) {
return {
CallExpression(node) {
if ((0, ast_1.isPageMethod)(node, 'waitForTimeout')) {
context.report({
messageId: 'noWaitForTimeout',
node,
suggest: [
{
fix: (fixer) => fixer.remove(node.parent && node.parent.type !== 'AwaitExpression'
? node.parent
: node.parent.parent),
messageId: 'removeWaitForTimeout',
},
],
});
}
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Prevent usage of page.waitForTimeout()',
recommended: true,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-timeout.md',
},
hasSuggestions: true,
messages: {
noWaitForTimeout: 'Unexpected use of page.waitForTimeout().',
removeWaitForTimeout: 'Remove the page.waitForTimeout() method.',
},
type: 'suggestion',
},
};

View File

@@ -0,0 +1,106 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
function isString(node) {
return node && ((0, ast_1.isStringLiteral)(node) || node.type === 'TemplateLiteral');
}
exports.default = {
create(context) {
const { allowedPrefixes, ignore, ignoreTopLevelDescribe } = {
allowedPrefixes: [],
ignore: [],
ignoreTopLevelDescribe: false,
...(context.options?.[0] ?? {}),
};
let describeCount = 0;
return {
CallExpression(node) {
const method = (0, ast_1.isDescribeCall)(node)
? 'test.describe'
: (0, ast_1.isTest)(node)
? 'test'
: null;
if (method === 'test.describe') {
describeCount++;
if (ignoreTopLevelDescribe && describeCount === 1) {
return;
}
}
else if (!method) {
return;
}
const [title] = node.arguments;
if (!isString(title)) {
return;
}
const description = (0, ast_1.getStringValue)(title);
if (!description ||
allowedPrefixes.some((name) => description.startsWith(name))) {
return;
}
const firstCharacter = description.charAt(0);
if (!firstCharacter ||
firstCharacter === firstCharacter.toLowerCase() ||
ignore.includes(method)) {
return;
}
context.report({
data: { method },
fix(fixer) {
const rangeIgnoringQuotes = [
title.range[0] + 1,
title.range[1] - 1,
];
const newDescription = description.substring(0, 1).toLowerCase() +
description.substring(1);
return fixer.replaceTextRange(rangeIgnoringQuotes, newDescription);
},
messageId: 'unexpectedLowercase',
node: node.arguments[0],
});
},
'CallExpression:exit'(node) {
if ((0, ast_1.isDescribeCall)(node)) {
describeCount--;
}
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Enforce lowercase test names',
recommended: false,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-lowercase-title.md',
},
fixable: 'code',
messages: {
unexpectedLowercase: '`{{method}}`s should begin with lowercase',
},
schema: [
{
additionalProperties: false,
properties: {
allowedPrefixes: {
additionalItems: false,
items: { type: 'string' },
type: 'array',
},
ignore: {
additionalItems: false,
items: {
enum: ['test.describe', 'test'],
},
type: 'array',
},
ignoreTopLevelDescribe: {
default: false,
type: 'boolean',
},
},
type: 'object',
},
],
type: 'suggestion',
},
};

View File

@@ -0,0 +1,43 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const fixer_1 = require("../utils/fixer");
const parseExpectCall_1 = require("../utils/parseExpectCall");
exports.default = {
create(context) {
return {
CallExpression(node) {
const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
if (expectCall?.matcherName === 'toEqual') {
context.report({
messageId: 'useToStrictEqual',
node: expectCall.matcher,
suggest: [
{
fix: (fixer) => {
return (0, fixer_1.replaceAccessorFixer)(fixer, expectCall.matcher, 'toStrictEqual');
},
messageId: 'suggestReplaceWithStrictEqual',
},
],
});
}
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using `toStrictEqual()`',
recommended: false,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-strict-equal.md',
},
fixable: 'code',
hasSuggestions: true,
messages: {
suggestReplaceWithStrictEqual: 'Replace with `toStrictEqual()`',
useToStrictEqual: 'Use toStrictEqual() instead',
},
schema: [],
type: 'suggestion',
},
};

View File

@@ -0,0 +1,88 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
const fixer_1 = require("../utils/fixer");
const parseExpectCall_1 = require("../utils/parseExpectCall");
function shouldUseToBe(expectCall) {
let arg = expectCall.args[0];
if (arg.type === 'UnaryExpression' && arg.operator === '-') {
arg = arg.argument;
}
if (arg.type === 'Literal') {
// regex literals are classed as literals, but they're actually objects
// which means "toBe" will give different results than other matchers
return !('regex' in arg);
}
return arg.type === 'TemplateLiteral';
}
function reportPreferToBe(context, expectCall, whatToBe, notModifier) {
context.report({
fix(fixer) {
const fixes = [
(0, fixer_1.replaceAccessorFixer)(fixer, expectCall.matcher, `toBe${whatToBe}`),
];
if (expectCall.args?.length && whatToBe !== '') {
fixes.push(fixer.remove(expectCall.args[0]));
}
if (notModifier) {
const [start, end] = notModifier.range;
fixes.push(fixer.removeRange([start - 1, end]));
}
return fixes;
},
messageId: `useToBe${whatToBe}`,
node: expectCall.matcher,
});
}
exports.default = {
create(context) {
return {
CallExpression(node) {
const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
if (!expectCall)
return;
const notMatchers = ['toBeUndefined', 'toBeDefined'];
const notModifier = expectCall.modifiers.find((node) => (0, ast_1.getStringValue)(node) === 'not');
if (notModifier && notMatchers.includes(expectCall.matcherName)) {
return reportPreferToBe(context, expectCall, expectCall.matcherName === 'toBeDefined' ? 'Undefined' : 'Defined', notModifier);
}
const argumentMatchers = ['toBe', 'toEqual', 'toStrictEqual'];
const firstArg = expectCall.args[0];
if (!argumentMatchers.includes(expectCall.matcherName) || !firstArg) {
return;
}
if (firstArg.type === 'Literal' && firstArg.value === null) {
return reportPreferToBe(context, expectCall, 'Null');
}
if ((0, ast_1.isIdentifier)(firstArg, 'undefined')) {
const name = notModifier ? 'Defined' : 'Undefined';
return reportPreferToBe(context, expectCall, name, notModifier);
}
if ((0, ast_1.isIdentifier)(firstArg, 'NaN')) {
return reportPreferToBe(context, expectCall, 'NaN');
}
if (shouldUseToBe(expectCall) && expectCall.matcherName !== 'toBe') {
reportPreferToBe(context, expectCall, '');
}
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using `toBe()` for primitive literals',
recommended: false,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-be.md',
},
fixable: 'code',
messages: {
useToBe: 'Use `toBe` when expecting primitive literals',
useToBeDefined: 'Use `toBeDefined` instead',
useToBeNaN: 'Use `toBeNaN` instead',
useToBeNull: 'Use `toBeNull` instead',
useToBeUndefined: 'Use `toBeUndefined` instead',
},
schema: [],
type: 'suggestion',
},
};

View File

@@ -0,0 +1,75 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
const parseExpectCall_1 = require("../utils/parseExpectCall");
const matchers = new Set(['toBe', 'toEqual', 'toStrictEqual']);
const isFixableIncludesCallExpression = (node) => node.type === 'CallExpression' &&
node.callee.type === 'MemberExpression' &&
(0, ast_1.isPropertyAccessor)(node.callee, 'includes') &&
node.arguments.length === 1 &&
node.arguments[0].type !== 'SpreadElement';
exports.default = {
create(context) {
return {
CallExpression(node) {
const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
if (!expectCall || expectCall.args.length === 0)
return;
const { args, matcher, matcherName } = expectCall;
const [includesCall] = node.arguments;
const [matcherArg] = args;
if (!includesCall ||
matcherArg.type === 'SpreadElement' ||
!matchers.has(matcherName) ||
!(0, ast_1.isBooleanLiteral)(matcherArg) ||
!isFixableIncludesCallExpression(includesCall)) {
return;
}
const notModifier = expectCall.modifiers.find((node) => (0, ast_1.getStringValue)(node) === 'not');
context.report({
fix(fixer) {
const sourceCode = context.getSourceCode();
// We need to negate the expectation if the current expected
// value is itself negated by the "not" modifier
const addNotModifier = matcherArg.type === 'Literal' &&
matcherArg.value === !!notModifier;
const fixes = [
// remove the "includes" call entirely
fixer.removeRange([
includesCall.callee.property.range[0] - 1,
includesCall.range[1],
]),
// replace the current matcher with "toContain", adding "not" if needed
fixer.replaceText(matcher, addNotModifier ? 'not.toContain' : 'toContain'),
// replace the matcher argument with the value from the "includes"
fixer.replaceText(expectCall.args[0], sourceCode.getText(includesCall.arguments[0])),
];
// Remove the "not" modifier if needed
if (notModifier) {
fixes.push(fixer.removeRange([
notModifier.range[0],
notModifier.range[1] + 1,
]));
}
return fixes;
},
messageId: 'useToContain',
node: matcher,
});
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using toContain()',
recommended: false,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-contain.md',
},
fixable: 'code',
messages: {
useToContain: 'Use toContain() instead',
},
type: 'suggestion',
},
};

View File

@@ -0,0 +1,52 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
const fixer_1 = require("../utils/fixer");
const parseExpectCall_1 = require("../utils/parseExpectCall");
const lengthMatchers = new Set(['toBe', 'toEqual', 'toStrictEqual']);
exports.default = {
create(context) {
return {
CallExpression(node) {
const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
if (!expectCall || !lengthMatchers.has(expectCall.matcherName)) {
return;
}
const [argument] = node.arguments;
if (argument?.type !== 'MemberExpression' ||
!(0, ast_1.isPropertyAccessor)(argument, 'length')) {
return;
}
context.report({
fix(fixer) {
return [
// remove the "length" property accessor
fixer.removeRange([
argument.property.range[0] - 1,
argument.range[1],
]),
// replace the current matcher with "toHaveLength"
(0, fixer_1.replaceAccessorFixer)(fixer, expectCall.matcher, 'toHaveLength'),
];
},
messageId: 'useToHaveLength',
node: expectCall.matcher,
});
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Suggest using `toHaveLength()`',
recommended: false,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-to-have-length.md',
},
fixable: 'code',
messages: {
useToHaveLength: 'Use toHaveLength() instead',
},
schema: [],
type: 'suggestion',
},
};

View File

@@ -0,0 +1,150 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getMatcherCall = void 0;
const ast_1 = require("../utils/ast");
const parseExpectCall_1 = require("../utils/parseExpectCall");
const methods = {
getAttribute: {
matcher: 'toHaveAttribute',
type: 'string',
},
innerText: { matcher: 'toHaveText', type: 'string' },
inputValue: { matcher: 'toHaveValue', type: 'string' },
isChecked: { matcher: 'toBeChecked', type: 'boolean' },
isDisabled: {
inverse: 'toBeEnabled',
matcher: 'toBeDisabled',
type: 'boolean',
},
isEditable: { matcher: 'toBeEditable', type: 'boolean' },
isEnabled: {
inverse: 'toBeDisabled',
matcher: 'toBeEnabled',
type: 'boolean',
},
isHidden: {
inverse: 'toBeVisible',
matcher: 'toBeHidden',
type: 'boolean',
},
isVisible: {
inverse: 'toBeHidden',
matcher: 'toBeVisible',
type: 'boolean',
},
textContent: { matcher: 'toHaveText', type: 'string' },
};
const supportedMatchers = new Set([
'toBe',
'toEqual',
'toBeTruthy',
'toBeFalsy',
]);
function getMatcherCall(node) {
const grandparent = node.parent?.parent;
return grandparent.type === 'CallExpression' ? grandparent : undefined;
}
exports.getMatcherCall = getMatcherCall;
exports.default = {
create(context) {
return {
CallExpression(node) {
const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
if (!expectCall)
return;
const [arg] = node.arguments;
if (arg.type !== 'AwaitExpression' ||
arg.argument.type !== 'CallExpression' ||
arg.argument.callee.type !== 'MemberExpression') {
return;
}
// Matcher must be supported
if (!supportedMatchers.has(expectCall.matcherName))
return;
// Playwright method must be supported
const method = (0, ast_1.getStringValue)(arg.argument.callee.property);
const methodConfig = methods[method];
if (!methodConfig)
return;
// Change the matcher
const { args, matcher } = expectCall;
const notModifier = expectCall.modifiers.find((mod) => (0, ast_1.getStringValue)(mod) === 'not');
const isFalsy = methodConfig.type === 'boolean' &&
((!!args.length && (0, ast_1.isBooleanLiteral)(args[0], false)) ||
expectCall.matcherName === 'toBeFalsy');
const isInverse = methodConfig.inverse
? notModifier || isFalsy
: notModifier && isFalsy;
// Replace the old matcher with the new matcher. The inverse
// matcher should only be used if the old statement was not a
// double negation.
const newMatcher = (+!!notModifier ^ +isFalsy && methodConfig.inverse) ||
methodConfig.matcher;
const { callee } = arg.argument;
context.report({
data: {
matcher: newMatcher,
method,
},
fix: (fixer) => {
const methodArgs = arg.argument.type === 'CallExpression'
? arg.argument.arguments
: [];
const methodEnd = methodArgs.length
? methodArgs[methodArgs.length - 1].range[1] + 1
: callee.property.range[1] + 2;
const fixes = [
// Add await to the expect call
fixer.insertTextBefore(node, 'await '),
// Remove the await keyword
fixer.replaceTextRange([arg.range[0], arg.argument.range[0]], ''),
// Remove the old Playwright method and any arguments
fixer.replaceTextRange([callee.property.range[0] - 1, methodEnd], ''),
];
// Remove not from matcher chain if no longer needed
if (isInverse && notModifier) {
const notRange = notModifier.range;
fixes.push(fixer.removeRange([notRange[0], notRange[1] + 1]));
}
// Add not to the matcher chain if no inverse matcher exists
if (!methodConfig.inverse && !notModifier && isFalsy) {
fixes.push(fixer.insertTextBefore(matcher, 'not.'));
}
fixes.push(fixer.replaceText(matcher, newMatcher));
// Remove boolean argument if it exists
const [matcherArg] = args ?? [];
if (matcherArg && (0, ast_1.isBooleanLiteral)(matcherArg)) {
fixes.push(fixer.remove(matcherArg));
}
// Add the new matcher arguments if needed
const hasOtherArgs = !!methodArgs.filter((arg) => !(0, ast_1.isBooleanLiteral)(arg)).length;
if (methodArgs) {
const range = matcher.range;
const stringArgs = methodArgs
.map((arg) => (0, ast_1.getRawValue)(arg))
.concat(hasOtherArgs ? '' : [])
.join(', ');
fixes.push(fixer.insertTextAfterRange([range[0], range[1] + 1], stringArgs));
}
return fixes;
},
messageId: 'useWebFirstAssertion',
node,
});
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Prefer web first assertions',
recommended: true,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-web-first-assertions.md',
},
fixable: 'code',
messages: {
useWebFirstAssertion: 'Replace {{method}}() with {{matcher}}().',
},
type: 'suggestion',
},
};

View File

@@ -0,0 +1,31 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
exports.default = {
create(context) {
return {
CallExpression(node) {
if ((0, ast_1.getExpectType)(node) === 'standalone') {
context.report({
fix: (fixer) => fixer.insertTextAfter(node.callee, '.soft'),
messageId: 'requireSoft',
node: node.callee,
});
}
},
};
},
meta: {
docs: {
description: 'Require all assertions to use `expect.soft`',
recommended: false,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-soft-assertions.md',
},
fixable: 'code',
messages: {
requireSoft: 'Unexpected non-soft assertion',
},
schema: [],
type: 'suggestion',
},
};

View File

@@ -0,0 +1,70 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
const misc_1 = require("../utils/misc");
exports.default = {
create(context) {
const { maxTopLevelDescribes } = {
maxTopLevelDescribes: Infinity,
...(context.options?.[0] ?? {}),
};
let topLevelDescribeCount = 0;
let describeCount = 0;
return {
CallExpression(node) {
if ((0, ast_1.isDescribeCall)(node)) {
describeCount++;
if (describeCount === 1) {
topLevelDescribeCount++;
if (topLevelDescribeCount > maxTopLevelDescribes) {
context.report({
data: (0, misc_1.getAmountData)(maxTopLevelDescribes),
messageId: 'tooManyDescribes',
node: node.callee,
});
}
}
}
else if (!describeCount) {
if ((0, ast_1.isTest)(node)) {
context.report({ messageId: 'unexpectedTest', node: node.callee });
}
else if ((0, ast_1.isTestHook)(node)) {
context.report({ messageId: 'unexpectedHook', node: node.callee });
}
}
},
'CallExpression:exit'(node) {
if ((0, ast_1.isDescribeCall)(node)) {
describeCount--;
}
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Require test cases and hooks to be inside a `test.describe` block',
recommended: false,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-top-level-describe.md',
},
messages: {
tooManyDescribes: 'There should not be more than {{max}} describe{{s}} at the top level',
unexpectedHook: 'All hooks must be wrapped in a describe block.',
unexpectedTest: 'All test cases must be wrapped in a describe block.',
},
schema: [
{
additionalProperties: false,
properties: {
maxTopLevelDescribes: {
minimum: 1,
type: 'number',
},
},
type: 'object',
},
],
type: 'suggestion',
},
};

View File

@@ -0,0 +1,97 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const ast_1 = require("../utils/ast");
const misc_1 = require("../utils/misc");
const parseExpectCall_1 = require("../utils/parseExpectCall");
function isMatcherCalled(node) {
if (node.parent.type !== 'MemberExpression') {
// Just asserting that the parent is a call expression is not enough as
// the node could be an argument of a call expression which doesn't
// determine if it is called. To determine if it is called, we verify
// that the parent call expression callee is the same as the node.
return {
called: node.parent.type === 'CallExpression' && node.parent.callee === node,
node,
};
}
// If the parent is a member expression, we continue traversing upward to
// handle matcher chains of unknown length. e.g. expect().not.something.
return isMatcherCalled(node.parent);
}
exports.default = {
create(context) {
const options = {
maxArgs: 2,
minArgs: 1,
...(context.options?.[0] ?? {}),
};
const minArgs = Math.min(options.minArgs, options.maxArgs);
const maxArgs = Math.max(options.minArgs, options.maxArgs);
return {
CallExpression(node) {
if (!(0, ast_1.isExpectCall)(node))
return;
const expectCall = (0, parseExpectCall_1.parseExpectCall)(node);
if (!expectCall) {
context.report({ messageId: 'matcherNotFound', node });
}
else {
const result = isMatcherCalled(node);
if (!result.called) {
context.report({
messageId: 'matcherNotCalled',
node: result.node.type === 'MemberExpression'
? result.node.property
: result.node,
});
}
}
if (node.arguments.length < minArgs) {
context.report({
data: (0, misc_1.getAmountData)(minArgs),
messageId: 'notEnoughArgs',
node,
});
}
if (node.arguments.length > maxArgs) {
context.report({
data: (0, misc_1.getAmountData)(maxArgs),
messageId: 'tooManyArgs',
node,
});
}
},
};
},
meta: {
docs: {
category: 'Possible Errors',
description: 'Enforce valid `expect()` usage',
recommended: true,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-expect.md',
},
messages: {
matcherNotCalled: 'Matchers must be called to assert.',
matcherNotFound: 'Expect must have a corresponding matcher call.',
notEnoughArgs: 'Expect requires at least {{amount}} argument{{s}}.',
tooManyArgs: 'Expect takes at most {{amount}} argument{{s}}.',
},
schema: [
{
additionalProperties: false,
properties: {
maxArgs: {
minimum: 1,
type: 'number',
},
minArgs: {
minimum: 1,
type: 'number',
},
},
type: 'object',
},
],
type: 'problem',
},
};

140
node_modules/eslint-plugin-playwright/lib/utils/ast.js generated vendored Normal file
View File

@@ -0,0 +1,140 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isPageMethod = exports.getMatchers = exports.isExpectCall = exports.getExpectType = exports.isTestHook = exports.isTest = exports.findParent = exports.isDescribeCall = exports.isTestIdentifier = exports.isPropertyAccessor = exports.isBooleanLiteral = exports.isStringLiteral = exports.isIdentifier = exports.getRawValue = exports.getStringValue = void 0;
function getStringValue(node) {
if (!node)
return '';
return node.type === 'Identifier'
? node.name
: node.type === 'TemplateLiteral'
? node.quasis[0].value.raw
: node.type === 'Literal' && typeof node.value === 'string'
? node.value
: '';
}
exports.getStringValue = getStringValue;
function getRawValue(node) {
return node.type === 'Literal' ? node.raw : undefined;
}
exports.getRawValue = getRawValue;
function isIdentifier(node, name) {
return (node.type === 'Identifier' &&
(typeof name === 'string' ? node.name === name : name.test(node.name)));
}
exports.isIdentifier = isIdentifier;
function isLiteral(node, type, value) {
return (node.type === 'Literal' &&
(value === undefined
? typeof node.value === type
: node.value === value));
}
function isStringLiteral(node, value) {
return isLiteral(node, 'string', value);
}
exports.isStringLiteral = isStringLiteral;
function isBooleanLiteral(node, value) {
return isLiteral(node, 'boolean', value);
}
exports.isBooleanLiteral = isBooleanLiteral;
function isPropertyAccessor(node, name) {
return getStringValue(node.property) === name;
}
exports.isPropertyAccessor = isPropertyAccessor;
function isTestIdentifier(node) {
return (isIdentifier(node, 'test') ||
(node.type === 'MemberExpression' && isIdentifier(node.object, 'test')));
}
exports.isTestIdentifier = isTestIdentifier;
const describeProperties = new Set([
'parallel',
'serial',
'only',
'skip',
'fixme',
]);
function isDescribeCall(node) {
const inner = node.type === 'CallExpression' ? node.callee : node;
// Allow describe without test prefix
if (isIdentifier(inner, 'describe')) {
return true;
}
if (inner.type !== 'MemberExpression') {
return false;
}
return isPropertyAccessor(inner, 'describe')
? true
: describeProperties.has(getStringValue(inner.property))
? isDescribeCall(inner.object)
: false;
}
exports.isDescribeCall = isDescribeCall;
function findParent(node, type) {
if (!node.parent)
return;
return node.parent.type === type
? node.parent
: findParent(node.parent, type);
}
exports.findParent = findParent;
function isTest(node, modifiers) {
return (isTestIdentifier(node.callee) &&
!isDescribeCall(node) &&
(node.callee.type !== 'MemberExpression' ||
!modifiers ||
modifiers?.includes(getStringValue(node.callee.property))) &&
node.arguments.length === 2 &&
['ArrowFunctionExpression', 'FunctionExpression'].includes(node.arguments[1].type));
}
exports.isTest = isTest;
const testHooks = new Set(['afterAll', 'afterEach', 'beforeAll', 'beforeEach']);
function isTestHook(node) {
return (node.callee.type === 'MemberExpression' &&
isIdentifier(node.callee.object, 'test') &&
testHooks.has(getStringValue(node.callee.property)));
}
exports.isTestHook = isTestHook;
const expectSubCommands = new Set(['soft', 'poll']);
function getExpectType(node) {
if (isIdentifier(node.callee, /(^expect|Expect)$/)) {
return 'standalone';
}
if (node.callee.type === 'MemberExpression' &&
isIdentifier(node.callee.object, 'expect')) {
const type = getStringValue(node.callee.property);
return expectSubCommands.has(type) ? type : undefined;
}
}
exports.getExpectType = getExpectType;
function isExpectCall(node) {
return !!getExpectType(node);
}
exports.isExpectCall = isExpectCall;
function getMatchers(node, chain = []) {
if (node.parent.type === 'MemberExpression' && node.parent.object === node) {
return getMatchers(node.parent, [
...chain,
node.parent.property,
]);
}
return chain;
}
exports.getMatchers = getMatchers;
/**
* Digs through a series of MemberExpressions and CallExpressions to find an
* Identifier with the given name.
*/
function dig(node, identifier) {
return node.type === 'MemberExpression'
? dig(node.property, identifier)
: node.type === 'CallExpression'
? dig(node.callee, identifier)
: node.type === 'Identifier'
? isIdentifier(node, identifier)
: false;
}
function isPageMethod(node, name) {
return (node.callee.type === 'MemberExpression' &&
dig(node.callee.object, /(^(page|frame)|(Page|Frame)$)/) &&
isPropertyAccessor(node.callee, name));
}
exports.isPageMethod = isPageMethod;

View File

@@ -0,0 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.replaceAccessorFixer = exports.getRangeOffset = void 0;
const getRangeOffset = (node) => node.type === 'Identifier' ? 0 : 1;
exports.getRangeOffset = getRangeOffset;
/**
* Replaces an accessor node with the given `text`.
*
* This ensures that fixes produce valid code when replacing both dot-based and
* bracket-based property accessors.
*/
function replaceAccessorFixer(fixer, node, text) {
const [start, end] = node.range;
return fixer.replaceTextRange([start + (0, exports.getRangeOffset)(node), end - (0, exports.getRangeOffset)(node)], text);
}
exports.replaceAccessorFixer = replaceAccessorFixer;

View File

@@ -0,0 +1,8 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAmountData = void 0;
const getAmountData = (amount) => ({
amount: amount.toString(),
s: amount === 1 ? '' : 's',
});
exports.getAmountData = getAmountData;

View File

@@ -0,0 +1,38 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseExpectCall = void 0;
const ast_1 = require("./ast");
const MODIFIER_NAMES = new Set(['not', 'resolves', 'rejects']);
function getExpectArguments(node) {
const grandparent = node.parent.parent;
return grandparent.type === 'CallExpression' ? grandparent.arguments : [];
}
function parseExpectCall(node) {
if (!(0, ast_1.isExpectCall)(node)) {
return;
}
const members = (0, ast_1.getMatchers)(node);
const modifiers = [];
let matcher;
// Separate the matchers (e.g. toBe) from modifiers (e.g. not)
members.forEach((item) => {
if (MODIFIER_NAMES.has((0, ast_1.getStringValue)(item))) {
modifiers.push(item);
}
else {
matcher = item;
}
});
// Rules only run against full expect calls with matchers
if (!matcher) {
return;
}
return {
args: getExpectArguments(matcher),
matcher,
matcherName: (0, ast_1.getStringValue)(matcher),
members,
modifiers,
};
}
exports.parseExpectCall = parseExpectCall;

View File

@@ -0,0 +1,2 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });

50
node_modules/eslint-plugin-playwright/package.json generated vendored Normal file
View File

@@ -0,0 +1,50 @@
{
"name": "eslint-plugin-playwright",
"description": "ESLint plugin for Playwright testing.",
"version": "0.15.3",
"packageManager": "pnpm@8.4.0",
"main": "lib/index.js",
"repository": "https://github.com/playwright-community/eslint-plugin-playwright",
"author": "Mark Skelton <mark@mskelton.dev>",
"contributors": [
"Max Schmitt <max@schmitt.mx>"
],
"license": "MIT",
"files": [
"lib",
"!lib/**/*.spec.js"
],
"scripts": {
"build": "tsc --project tsconfig.build.json",
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "jest",
"ts": "tsc"
},
"devDependencies": {
"@mskelton/eslint-config": "^8.0.0",
"@types/dedent": "^0.7.0",
"@types/eslint": "^8.40.2",
"@types/estree": "^1.0.1",
"@typescript-eslint/eslint-plugin": "^5.59.11",
"@typescript-eslint/parser": "^5.59.11",
"dedent": "^0.7.0",
"eslint": "^8.43.0",
"eslint-plugin-sort": "^2.10.0",
"jest": "^29.5.0",
"prettier": "^2.8.8",
"semantic-release": "^21.0.5",
"ts-jest": "^29.1.0",
"typescript": "^5.1.3"
},
"peerDependencies": {
"eslint": ">=7",
"eslint-plugin-jest": ">=25"
},
"peerDependenciesMeta": {
"eslint-plugin-jest": {
"optional": true
}
}
}