playwright-toolbox 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.changeset/README.md +9 -0
  2. package/.changeset/config.json +11 -0
  3. package/README.md +90 -0
  4. package/package.json +26 -0
  5. package/packages/playwright-config/CHANGELOG.md +21 -0
  6. package/packages/playwright-config/README.md +22 -0
  7. package/packages/playwright-config/package.json +47 -0
  8. package/packages/playwright-config/src/index.ts +21 -0
  9. package/packages/playwright-config/tsconfig.json +19 -0
  10. package/packages/playwright-history-dashboard/CHANGELOG.md +21 -0
  11. package/packages/playwright-history-dashboard/README.md +216 -0
  12. package/packages/playwright-history-dashboard/RELEASING.md +249 -0
  13. package/packages/playwright-history-dashboard/dashboard/index.html +2825 -0
  14. package/packages/playwright-history-dashboard/package-lock.json +105 -0
  15. package/packages/playwright-history-dashboard/package.json +56 -0
  16. package/packages/playwright-history-dashboard/pw-dashboard.config.js +22 -0
  17. package/packages/playwright-history-dashboard/scripts/init.ts +95 -0
  18. package/packages/playwright-history-dashboard/src/reporter.ts +376 -0
  19. package/packages/playwright-history-dashboard/tsconfig.json +19 -0
  20. package/packages/pw-standard/.eslintrc.js +23 -0
  21. package/packages/pw-standard/CHANGELOG.md +31 -0
  22. package/packages/pw-standard/README.md +50 -0
  23. package/packages/pw-standard/jest.config.js +28 -0
  24. package/packages/pw-standard/package.json +86 -0
  25. package/packages/pw-standard/src/base/index.ts +19 -0
  26. package/packages/pw-standard/src/eslint/index.ts +91 -0
  27. package/packages/pw-standard/src/eslint/rules/no-brittle-selectors.ts +53 -0
  28. package/packages/pw-standard/src/eslint/rules/no-focused-tests.ts +61 -0
  29. package/packages/pw-standard/src/eslint/rules/no-page-pause.ts +37 -0
  30. package/packages/pw-standard/src/eslint/rules/no-wait-for-timeout.ts +34 -0
  31. package/packages/pw-standard/src/eslint/rules/prefer-web-first-assertions.ts +90 -0
  32. package/packages/pw-standard/src/eslint/rules/require-test-description.ts +159 -0
  33. package/packages/pw-standard/src/eslint/types.ts +20 -0
  34. package/packages/pw-standard/src/eslint/utils/ast.ts +59 -0
  35. package/packages/pw-standard/src/index.ts +13 -0
  36. package/packages/pw-standard/src/playwright/index.ts +6 -0
  37. package/packages/pw-standard/src/tsconfig/base.json +21 -0
  38. package/packages/pw-standard/src/tsconfig/strict.json +11 -0
  39. package/packages/pw-standard/tests/eslint/no-brittle-selectors.test.ts +34 -0
  40. package/packages/pw-standard/tests/eslint/no-page-pause-and-focused.test.ts +41 -0
  41. package/packages/pw-standard/tests/eslint/no-wait-for-timeout.test.ts +30 -0
  42. package/packages/pw-standard/tests/eslint/prefer-web-first-assertions.test.ts +25 -0
  43. package/packages/pw-standard/tests/eslint/require-test-description.test.ts +49 -0
  44. package/packages/pw-standard/tsconfig.json +24 -0
@@ -0,0 +1,31 @@
1
+ # @acahet/pw-standard
2
+
3
+ ## 3.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 51cd994: feat: publish history dashboard to npm, extract playwright-config as standalone package, add missing .eslintrc.js to pw-standard
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [51cd994]
12
+ - @acahet/playwright-config@1.2.0
13
+
14
+ ## 2.0.0
15
+
16
+ ### Minor Changes
17
+
18
+ - 4329be3: Convert repository to npm workspaces with three independent packages and introduce `@acahet/playwright-config`.
19
+
20
+ Summary:
21
+
22
+ - migrate to `packages/*` workspace structure
23
+ - split Playwright config into dedicated package
24
+ - keep a compatibility bridge export at `@acahet/pw-standard/playwright`
25
+ - update dashboard package metadata/docs for new workspace location
26
+ - add root orchestration scripts and Changesets configuration
27
+
28
+ ### Patch Changes
29
+
30
+ - Updated dependencies [4329be3]
31
+ - @acahet/playwright-config@1.1.0
@@ -0,0 +1,50 @@
1
+ # @acahet/pw-standard
2
+
3
+ Shared Playwright standards package:
4
+
5
+ - ESLint plugin and custom rules
6
+ - base classes/fixtures entrypoint
7
+ - shared tsconfig presets
8
+ - compatibility bridge export for Playwright config (`@acahet/pw-standard/playwright`)
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm i -D @acahet/pw-standard
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ### ESLint plugin
19
+
20
+ ```ts
21
+ import plugin from '@acahet/pw-standard/eslint';
22
+
23
+ export default [
24
+ {
25
+ plugins: {
26
+ 'playwright-standards': plugin,
27
+ },
28
+ rules: {
29
+ 'playwright-standards/no-wait-for-timeout': 'error',
30
+ 'playwright-standards/no-brittle-selectors': 'error',
31
+ },
32
+ },
33
+ ];
34
+ ```
35
+
36
+ ### TSConfig preset
37
+
38
+ ```json
39
+ {
40
+ "extends": "@acahet/pw-standard/tsconfig/base"
41
+ }
42
+ ```
43
+
44
+ ### Playwright config compatibility export
45
+
46
+ ```ts
47
+ import * as configPreset from '@acahet/pw-standard/playwright';
48
+ ```
49
+
50
+ New projects should import Playwright config presets from `@acahet/playwright-config` directly.
@@ -0,0 +1,28 @@
1
+ /** @type {import('jest').Config} */
2
+ const config = {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'node',
5
+ roots: ['<rootDir>/tests'],
6
+ testMatch: ['**/*.test.ts'],
7
+ moduleNameMapper: {
8
+ '^@acahet/pw-standard/eslint$': '<rootDir>/src/eslint/index.ts',
9
+ '^@acahet/pw-standard/playwright$': '<rootDir>/src/playwright/index.ts',
10
+ '^@acahet/pw-standard/base$': '<rootDir>/src/base/index.ts',
11
+ },
12
+ collectCoverageFrom: [
13
+ 'src/**/*.ts',
14
+ '!src/**/*.d.ts',
15
+ '!src/**/index.ts', // barrel files — covered transitively
16
+ '!src/tsconfig/**',
17
+ ],
18
+ coverageThreshold: {
19
+ global: {
20
+ branches: 80,
21
+ functions: 80,
22
+ lines: 80,
23
+ statements: 80,
24
+ },
25
+ },
26
+ };
27
+
28
+ module.exports = config;
@@ -0,0 +1,86 @@
1
+ {
2
+ "name": "@acahet/pw-standard",
3
+ "version": "3.0.0",
4
+ "description": "Shared Playwright + TypeScript standards: ESLint rules, configs, base classes and fixtures",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./dist/index.js",
10
+ "require": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./eslint": {
14
+ "import": "./dist/eslint/index.js",
15
+ "require": "./dist/eslint/index.js",
16
+ "types": "./dist/eslint/index.d.ts"
17
+ },
18
+ "./playwright": {
19
+ "import": "./dist/playwright/index.js",
20
+ "require": "./dist/playwright/index.js",
21
+ "types": "./dist/playwright/index.d.ts"
22
+ },
23
+ "./tsconfig/base": "./src/tsconfig/base.json",
24
+ "./tsconfig/strict": "./src/tsconfig/strict.json",
25
+ "./base": {
26
+ "import": "./dist/base/index.js",
27
+ "require": "./dist/base/index.js",
28
+ "types": "./dist/base/index.d.ts"
29
+ }
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "src/tsconfig",
34
+ "README.md"
35
+ ],
36
+ "scripts": {
37
+ "build": "tsc --project tsconfig.json",
38
+ "build:watch": "tsc --project tsconfig.json --watch",
39
+ "test": "jest --config jest.config.js",
40
+ "test:watch": "jest --config jest.config.js --watch",
41
+ "test:coverage": "jest --config jest.config.js --coverage",
42
+ "lint": "eslint src --ext .ts",
43
+ "prepublishOnly": "npm run build && npm test"
44
+ },
45
+ "keywords": [
46
+ "eslint",
47
+ "eslint-plugin",
48
+ "playwright",
49
+ "typescript",
50
+ "testing",
51
+ "e2e",
52
+ "standards"
53
+ ],
54
+ "author": "",
55
+ "license": "MIT",
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "git+https://github.com/acahet-automation-org/playwright-toolbox.git",
59
+ "directory": "packages/pw-standard"
60
+ },
61
+ "peerDependencies": {
62
+ "eslint": ">=8.0.0",
63
+ "@playwright/test": ">=1.40.0",
64
+ "@acahet/playwright-config": ">=1.2.0"
65
+ },
66
+ "peerDependenciesMeta": {
67
+ "@playwright/test": {
68
+ "optional": true
69
+ }
70
+ },
71
+ "devDependencies": {
72
+ "@playwright/test": "^1.42.0",
73
+ "@types/eslint": "^8.56.0",
74
+ "@types/jest": "^29.5.12",
75
+ "@types/node": "^20.11.0",
76
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
77
+ "@typescript-eslint/parser": "^7.0.0",
78
+ "eslint": "^8.56.0",
79
+ "jest": "^29.7.0",
80
+ "ts-jest": "^29.1.2",
81
+ "typescript": "^5.3.3"
82
+ },
83
+ "engines": {
84
+ "node": ">=18.0.0"
85
+ }
86
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @acahet/pw-standard/base
3
+ *
4
+ * Shared base classes and fixtures for Playwright test suites.
5
+ *
6
+ * Usage:
7
+ *
8
+ * import { BasePage } from '@acahet/pw-standard/base';
9
+ *
10
+ * export class LoginPage extends BasePage {
11
+ * async goto() { await this.page.goto('/login'); }
12
+ * }
13
+ */
14
+
15
+ // Base classes will be added here — e.g.:
16
+ // export { BasePage } from './BasePage';
17
+ // export { BaseFixture } from './BaseFixture';
18
+
19
+ export {};
@@ -0,0 +1,91 @@
1
+ import { RuleMap, ConfigRules } from './types';
2
+ import noWaitForTimeout from './rules/no-wait-for-timeout';
3
+ import noBrittleSelectors from './rules/no-brittle-selectors';
4
+ import requireTestDescription from './rules/require-test-description';
5
+ import noPagePause from './rules/no-page-pause';
6
+ import noFocusedTests from './rules/no-focused-tests';
7
+ import preferWebFirstAssertions from './rules/prefer-web-first-assertions';
8
+
9
+ // ─── Rule registry ────────────────────────────────────────────────────────────
10
+ // Add new rules here — they are automatically picked up by all configs below.
11
+
12
+ const rules: RuleMap = {
13
+ 'no-wait-for-timeout': noWaitForTimeout,
14
+ 'no-brittle-selectors': noBrittleSelectors,
15
+ 'require-test-description': requireTestDescription,
16
+ 'no-page-pause': noPagePause,
17
+ 'no-focused-tests': noFocusedTests,
18
+ 'prefer-web-first-assertions': preferWebFirstAssertions,
19
+ };
20
+
21
+ // ─── Shared parser options ────────────────────────────────────────────────────
22
+
23
+ const PARSER_OPTIONS = {
24
+ ecmaVersion: 2020,
25
+ sourceType: 'module' as const,
26
+ };
27
+
28
+ // ─── Helper: prefix all rule keys with the plugin name ───────────────────────
29
+
30
+ function prefixedRules(severity: 'error' | 'warn'): ConfigRules {
31
+ return Object.keys(rules).reduce<ConfigRules>((acc, key) => {
32
+ const meta = rules[key].meta;
33
+ if (meta.docs?.recommended) {
34
+ acc[`playwright-standards/${key}`] = severity;
35
+ }
36
+ return acc;
37
+ }, {});
38
+ }
39
+
40
+ // ─── Configs ──────────────────────────────────────────────────────────────────
41
+
42
+ const configs = {
43
+ /**
44
+ * `recommended` — all recommended rules as errors.
45
+ * Best for CI enforcement.
46
+ */
47
+ recommended: {
48
+ plugins: ['playwright-standards'],
49
+ parserOptions: PARSER_OPTIONS,
50
+ rules: prefixedRules('error'),
51
+ },
52
+
53
+ /**
54
+ * `strict` — all rules (including non-recommended) as errors.
55
+ */
56
+ strict: {
57
+ plugins: ['playwright-standards'],
58
+ parserOptions: PARSER_OPTIONS,
59
+ rules: Object.keys(rules).reduce<ConfigRules>((acc, key) => {
60
+ acc[`playwright-standards/${key}`] = 'error';
61
+ return acc;
62
+ }, {}),
63
+ },
64
+
65
+ /**
66
+ * `warn` — all recommended rules as warnings.
67
+ * Useful when adopting the plugin incrementally.
68
+ */
69
+ warn: {
70
+ plugins: ['playwright-standards'],
71
+ parserOptions: PARSER_OPTIONS,
72
+ rules: prefixedRules('warn'),
73
+ },
74
+ };
75
+
76
+ // ─── Plugin export ────────────────────────────────────────────────────────────
77
+
78
+ const plugin = {
79
+ meta: {
80
+ name: '@acahet/pw-standard',
81
+ version: '1.0.0',
82
+ },
83
+ rules,
84
+ configs,
85
+ };
86
+
87
+ export default plugin;
88
+
89
+ // Named exports for consumers who import rules individually
90
+ export { rules, configs };
91
+ export type { RuleMap, ConfigRules };
@@ -0,0 +1,53 @@
1
+ import { Rule } from 'eslint';
2
+ import { CallExpression, Literal } from 'estree';
3
+ import { getMethodName, isBrittleSelector } from '../utils/ast';
4
+ import { RuleModule } from '../types';
5
+
6
+ const LOCATOR_METHODS = new Set([
7
+ 'locator',
8
+ '$',
9
+ '$$',
10
+ 'waitForSelector',
11
+ 'querySelector',
12
+ ]);
13
+
14
+ const rule: RuleModule = {
15
+ meta: {
16
+ type: 'suggestion',
17
+ docs: {
18
+ description:
19
+ 'Discourage brittle CSS / XPath selectors — prefer getByTestId, getByRole, getByLabel, etc.',
20
+ category: 'Best Practices',
21
+ recommended: true,
22
+ url: 'https://github.com/acahet-automation-org/playwright-standards/blob/main/docs/rules/no-brittle-selectors.md',
23
+ },
24
+ messages: {
25
+ brittleSelector:
26
+ 'Brittle selector "{{selector}}". Prefer getByTestId(), getByRole(), getByLabel(), or getByText() instead.',
27
+ },
28
+ schema: [],
29
+ },
30
+
31
+ create(context: Rule.RuleContext): Rule.RuleListener {
32
+ return {
33
+ CallExpression(node: CallExpression) {
34
+ const method = getMethodName(node);
35
+ if (!method || !LOCATOR_METHODS.has(method)) return;
36
+
37
+ const firstArg = node.arguments[0];
38
+ if (!firstArg || firstArg.type !== 'Literal') return;
39
+
40
+ const selector = String((firstArg as Literal).value);
41
+ if (isBrittleSelector(selector)) {
42
+ context.report({
43
+ node: firstArg,
44
+ messageId: 'brittleSelector',
45
+ data: { selector },
46
+ });
47
+ }
48
+ },
49
+ };
50
+ },
51
+ };
52
+
53
+ export default rule;
@@ -0,0 +1,61 @@
1
+ import { Rule } from 'eslint';
2
+ import { CallExpression, MemberExpression, Identifier } from 'estree';
3
+ import { RuleModule } from '../types';
4
+
5
+ const FOCUSED_METHODS = new Set(['only', 'skip']);
6
+ const TEST_DESCRIBE_OBJECTS = new Set(['test', 'it', 'describe']);
7
+
8
+ const rule: RuleModule = {
9
+ meta: {
10
+ type: 'problem',
11
+ docs: {
12
+ description: 'Disallow test.only() / describe.only() — remove before committing',
13
+ category: 'Best Practices',
14
+ recommended: true,
15
+ url: 'https://github.com/acahet-automation-org/playwright-standards/blob/main/docs/rules/no-focused-tests.md',
16
+ },
17
+ messages: {
18
+ noOnly: '"{{parent}}.only()" found. Remove it before committing to avoid blocking the full test suite.',
19
+ noSkip: '"{{parent}}.skip()" found. Remove or resolve the skip before committing.',
20
+ },
21
+ schema: [
22
+ {
23
+ type: 'object',
24
+ properties: {
25
+ allowSkip: { type: 'boolean' },
26
+ },
27
+ additionalProperties: false,
28
+ },
29
+ ],
30
+ },
31
+
32
+ create(context: Rule.RuleContext): Rule.RuleListener {
33
+ const options = context.options[0] ?? {};
34
+ const allowSkip: boolean = options.allowSkip ?? false;
35
+
36
+ return {
37
+ CallExpression(node: CallExpression) {
38
+ if (node.callee.type !== 'MemberExpression') return;
39
+ const callee = node.callee as MemberExpression;
40
+
41
+ if (callee.object.type !== 'Identifier') return;
42
+ const parentName = (callee.object as Identifier).name;
43
+ if (!TEST_DESCRIBE_OBJECTS.has(parentName)) return;
44
+
45
+ if (callee.property.type !== 'Identifier') return;
46
+ const methodName = (callee.property as Identifier).name;
47
+ if (!FOCUSED_METHODS.has(methodName)) return;
48
+
49
+ if (methodName === 'skip' && allowSkip) return;
50
+
51
+ context.report({
52
+ node,
53
+ messageId: methodName === 'only' ? 'noOnly' : 'noSkip',
54
+ data: { parent: parentName },
55
+ });
56
+ },
57
+ };
58
+ },
59
+ };
60
+
61
+ export default rule;
@@ -0,0 +1,37 @@
1
+ import { Rule } from 'eslint';
2
+ import { CallExpression, MemberExpression, Identifier } from 'estree';
3
+ import { RuleModule } from '../types';
4
+
5
+ const rule: RuleModule = {
6
+ meta: {
7
+ type: 'problem',
8
+ docs: {
9
+ description: 'Disallow page.pause() — remove before committing',
10
+ category: 'Best Practices',
11
+ recommended: true,
12
+ url: 'https://github.com/acahet-automation-org/playwright-standards/blob/main/docs/rules/no-page-pause.md',
13
+ },
14
+ messages: {
15
+ noPagePause:
16
+ 'page.pause() is for local debugging only. Remove it before committing.',
17
+ },
18
+ schema: [],
19
+ },
20
+
21
+ create(context: Rule.RuleContext): Rule.RuleListener {
22
+ return {
23
+ CallExpression(node: CallExpression) {
24
+ if (node.callee.type !== 'MemberExpression') return;
25
+ const callee = node.callee as MemberExpression;
26
+ if (
27
+ callee.property.type === 'Identifier' &&
28
+ (callee.property as Identifier).name === 'pause'
29
+ ) {
30
+ context.report({ node, messageId: 'noPagePause' });
31
+ }
32
+ },
33
+ };
34
+ },
35
+ };
36
+
37
+ export default rule;
@@ -0,0 +1,34 @@
1
+ import { Rule } from 'eslint';
2
+ import { CallExpression } from 'estree';
3
+ import { isWaitForTimeout } from '../utils/ast';
4
+ import { RuleModule } from '../types';
5
+
6
+ const rule: RuleModule = {
7
+ meta: {
8
+ type: 'problem',
9
+ docs: {
10
+ description:
11
+ 'Disallow waitForTimeout() — use explicit wait conditions instead',
12
+ category: 'Best Practices',
13
+ recommended: true,
14
+ url: 'https://github.com/acahet-automation-org/playwright-standards/blob/main/docs/rules/no-wait-for-timeout.md',
15
+ },
16
+ messages: {
17
+ noWaitForTimeout:
18
+ 'Avoid waitForTimeout(). Use waitFor(), waitForResponse(), or expect(...).toBeVisible() instead.',
19
+ },
20
+ schema: [],
21
+ },
22
+
23
+ create(context: Rule.RuleContext): Rule.RuleListener {
24
+ return {
25
+ CallExpression(node: CallExpression) {
26
+ if (isWaitForTimeout(node)) {
27
+ context.report({ node, messageId: 'noWaitForTimeout' });
28
+ }
29
+ },
30
+ };
31
+ },
32
+ };
33
+
34
+ export default rule;
@@ -0,0 +1,90 @@
1
+ import { Rule } from 'eslint';
2
+ import { CallExpression, Identifier } from 'estree';
3
+ import { RuleModule } from '../types';
4
+
5
+ const EAGER_METHODS = new Set([
6
+ 'innerText',
7
+ 'textContent',
8
+ 'getAttribute',
9
+ 'isVisible',
10
+ 'isHidden',
11
+ 'isEnabled',
12
+ 'isDisabled',
13
+ 'isChecked',
14
+ 'inputValue',
15
+ 'innerHTML',
16
+ ]);
17
+
18
+ const PREFERRED: Record<string, string> = {
19
+ innerText: 'toHaveText()',
20
+ textContent: 'toHaveText()',
21
+ getAttribute: 'toHaveAttribute()',
22
+ isVisible: 'toBeVisible()',
23
+ isHidden: 'toBeHidden()',
24
+ isEnabled: 'toBeEnabled()',
25
+ isDisabled: 'toBeDisabled()',
26
+ isChecked: 'toBeChecked()',
27
+ inputValue: 'toHaveValue()',
28
+ innerHTML: 'toContainText() or toHaveText()',
29
+ };
30
+
31
+ const rule: RuleModule = {
32
+ meta: {
33
+ type: 'suggestion',
34
+ docs: {
35
+ description:
36
+ 'Prefer Playwright web-first assertions over awaited property calls',
37
+ category: 'Best Practices',
38
+ recommended: true,
39
+ url: 'https://github.com/acahet-automation-org/playwright-standards/blob/main/docs/rules/prefer-web-first-assertions.md',
40
+ },
41
+ messages: {
42
+ preferWebFirst:
43
+ 'Avoid awaiting "{{method}}()" inside expect(). Use the web-first assertion "{{preferred}}" instead — it auto-retries.',
44
+ },
45
+ schema: [],
46
+ },
47
+
48
+ create(context: Rule.RuleContext): Rule.RuleListener {
49
+ const sourceCode = context.sourceCode;
50
+
51
+ return {
52
+ 'CallExpression > AwaitExpression > CallExpression'(
53
+ node: CallExpression,
54
+ ) {
55
+ if (node.callee.type !== 'MemberExpression') return;
56
+ const prop = node.callee.property;
57
+ if (prop.type !== 'Identifier') return;
58
+
59
+ const method = (prop as Identifier).name;
60
+ if (!EAGER_METHODS.has(method)) return;
61
+
62
+ const ancestors = sourceCode.getAncestors(node);
63
+ const awaitExpr = ancestors[ancestors.length - 1];
64
+ if (!awaitExpr || awaitExpr.type !== 'AwaitExpression') return;
65
+
66
+ const outerCall = ancestors[ancestors.length - 2];
67
+ if (
68
+ !outerCall ||
69
+ outerCall.type !== 'CallExpression' ||
70
+ (outerCall as CallExpression).callee.type !==
71
+ 'Identifier' ||
72
+ ((outerCall as CallExpression).callee as Identifier)
73
+ .name !== 'expect'
74
+ )
75
+ return;
76
+
77
+ context.report({
78
+ node,
79
+ messageId: 'preferWebFirst',
80
+ data: {
81
+ method,
82
+ preferred: PREFERRED[method] ?? 'a web-first assertion',
83
+ },
84
+ });
85
+ },
86
+ };
87
+ },
88
+ };
89
+
90
+ export default rule;