max-locator-sniffer 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 (66) hide show
  1. package/README.md +62 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +25 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/locator_examples/playwright_duplicate_locators_five.d.ts +7 -0
  7. package/dist/locator_examples/playwright_duplicate_locators_five.d.ts.map +1 -0
  8. package/dist/locator_examples/playwright_duplicate_locators_five.js +27 -0
  9. package/dist/locator_examples/playwright_duplicate_locators_five.js.map +1 -0
  10. package/dist/locator_examples/playwright_duplicate_locators_four.d.ts +7 -0
  11. package/dist/locator_examples/playwright_duplicate_locators_four.d.ts.map +1 -0
  12. package/dist/locator_examples/playwright_duplicate_locators_four.js +18 -0
  13. package/dist/locator_examples/playwright_duplicate_locators_four.js.map +1 -0
  14. package/dist/locator_examples/playwright_duplicate_locators_one.d.ts +8 -0
  15. package/dist/locator_examples/playwright_duplicate_locators_one.d.ts.map +1 -0
  16. package/dist/locator_examples/playwright_duplicate_locators_one.js +18 -0
  17. package/dist/locator_examples/playwright_duplicate_locators_one.js.map +1 -0
  18. package/dist/locator_examples/playwright_duplicate_locators_seven.d.ts +7 -0
  19. package/dist/locator_examples/playwright_duplicate_locators_seven.d.ts.map +1 -0
  20. package/dist/locator_examples/playwright_duplicate_locators_seven.js +12 -0
  21. package/dist/locator_examples/playwright_duplicate_locators_seven.js.map +1 -0
  22. package/dist/locator_examples/playwright_duplicate_locators_six.d.ts +7 -0
  23. package/dist/locator_examples/playwright_duplicate_locators_six.d.ts.map +1 -0
  24. package/dist/locator_examples/playwright_duplicate_locators_six.js +13 -0
  25. package/dist/locator_examples/playwright_duplicate_locators_six.js.map +1 -0
  26. package/dist/locator_examples/playwright_duplicate_locators_three.d.ts +7 -0
  27. package/dist/locator_examples/playwright_duplicate_locators_three.d.ts.map +1 -0
  28. package/dist/locator_examples/playwright_duplicate_locators_three.js +23 -0
  29. package/dist/locator_examples/playwright_duplicate_locators_three.js.map +1 -0
  30. package/dist/locator_examples/playwright_duplicate_locators_two.d.ts +6 -0
  31. package/dist/locator_examples/playwright_duplicate_locators_two.d.ts.map +1 -0
  32. package/dist/locator_examples/playwright_duplicate_locators_two.js +9 -0
  33. package/dist/locator_examples/playwright_duplicate_locators_two.js.map +1 -0
  34. package/dist/table/config.d.ts +3 -0
  35. package/dist/table/config.d.ts.map +1 -0
  36. package/dist/table/config.js +18 -0
  37. package/dist/table/config.js.map +1 -0
  38. package/dist/table/regexs.d.ts +5 -0
  39. package/dist/table/regexs.d.ts.map +1 -0
  40. package/dist/table/regexs.js +5 -0
  41. package/dist/table/regexs.js.map +1 -0
  42. package/dist/table/tableBuilder.d.ts +4 -0
  43. package/dist/table/tableBuilder.d.ts.map +1 -0
  44. package/dist/table/tableBuilder.js +119 -0
  45. package/dist/table/tableBuilder.js.map +1 -0
  46. package/dist/table/types.d.ts +8 -0
  47. package/dist/table/types.d.ts.map +1 -0
  48. package/dist/table/types.js +2 -0
  49. package/dist/table/types.js.map +1 -0
  50. package/dist/table/utils.d.ts +2 -0
  51. package/dist/table/utils.d.ts.map +1 -0
  52. package/dist/table/utils.js +18 -0
  53. package/dist/table/utils.js.map +1 -0
  54. package/images/max.webp +0 -0
  55. package/images/max_example_terminal.png +0 -0
  56. package/package.json +30 -0
  57. package/src/index.ts +35 -0
  58. package/src/locator_examples/playwright_duplicate_locators_one.ts +26 -0
  59. package/src/locator_examples/playwright_duplicate_locators_three.ts +25 -0
  60. package/src/locator_examples/playwright_duplicate_locators_two.ts +23 -0
  61. package/src/table/config.ts +18 -0
  62. package/src/table/regexs.ts +4 -0
  63. package/src/table/tableBuilder.ts +159 -0
  64. package/src/table/types.ts +9 -0
  65. package/src/table/utils.ts +18 -0
  66. package/tsconfig.json +44 -0
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # Max Locator Sniffer
2
+
3
+ <img src="./images/max.webp" alt="Max the Locator Sniffer" width="160" />
4
+
5
+ **Max Locator Sniffer** is a playful but powerful CLI tool that scans your Playwright test code and **sniffs out duplicate locators**.
6
+
7
+ Duplicate locators can lead to **brittle, flaky tests**. Max helps you catch them early by showing:
8
+
9
+ - Which locators are duplicated
10
+ - Where they appear
11
+ - How often they are reused
12
+ - A clear severity signal so you know what to fix first
13
+
14
+ ---
15
+
16
+ ## ✨ Features
17
+
18
+ - 🔍 Finds **duplicate Playwright locators**
19
+ - 📁 Shows **every file location**
20
+ - 🔢 Displays **usage counts**
21
+ - ⚠️ Highlights **HIGH / MEDIUM / LOW severity**
22
+ - 🖥 Beautiful CLI table output
23
+ - ⚡ Fast scans using glob
24
+
25
+ ---
26
+
27
+ ## 📦 Installation
28
+
29
+ ```bash
30
+ npm install -g max-locator-sniffer
31
+ ```
32
+
33
+ ## 🚀 Usage
34
+
35
+ Scan a folder and all of its contents:
36
+
37
+ ```bash
38
+ max sniff src
39
+ ```
40
+
41
+ ## Example Output
42
+
43
+ <img src="./images/max_example_terminal.png" alt="Terminal window running the app"/>
44
+
45
+
46
+ ## Roadmap
47
+ - Flaky locator scoring system
48
+ - CSS complexity analysis
49
+ - JSON output mode
50
+ - CI mode (--fail-on high)
51
+ - HTML report output
52
+ - Flaky locator scoring system
53
+ - CSS complexity analysis
54
+ - JSON output mode
55
+ - CI mode (--fail-on high)
56
+
57
+ ## Why "Max"?
58
+ Inspired by Max the dog from The Grinch, this CLI tool loyally sniffs through your repo, hunting down flaky, duplicated locators so your test suite stays healthy and happy.
59
+
60
+ MIT © Ian Bridges
61
+
62
+ ---
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ // load using import
3
+ import { Command } from 'commander';
4
+ import { glob } from 'glob';
5
+ import color from 'colors';
6
+ import { tableConfig } from './table/config.js';
7
+ import { buildTable, displayTable } from './table/tableBuilder.js';
8
+ const program = new Command();
9
+ program
10
+ .name('max')
11
+ .description('🐶 Sniffs out duplicate Playwright locators')
12
+ .version('1.0.0');
13
+ program
14
+ .command('sniff')
15
+ .description('Scan files for duplicate Playwright locators')
16
+ .argument('<pattern>', 'Glob pattern to scan')
17
+ .action(async (pattern) => {
18
+ const filePaths = await glob(`${pattern}/**/*.{js,ts}`, {
19
+ ignore: 'node_modules/**',
20
+ });
21
+ const duplicateLocatorTable = buildTable(filePaths);
22
+ await displayTable(duplicateLocatorTable, tableConfig);
23
+ });
24
+ program.parse(process.argv);
25
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,oBAAoB;AACpB,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,KAAK,MAAM,QAAQ,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAEnE,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,KAAK,CAAC;KACX,WAAW,CAAC,6CAA6C,CAAC;KAC1D,OAAO,CAAC,OAAO,CAAC,CAAC;AAGpB,OAAO;KACJ,OAAO,CAAC,OAAO,CAAC;KAChB,WAAW,CAAC,8CAA8C,CAAC;KAC3D,QAAQ,CACP,WAAW,EACX,sBAAsB,CACvB;KACA,MAAM,CAAC,KAAK,EAAE,OAAe,EAAE,EAAE;IAChC,MAAM,SAAS,GAAa,MAAM,IAAI,CAAC,GAAG,OAAO,eAAe,EAAE;QAChE,MAAM,EAAE,iBAAiB;KAC1B,CAAC,CAAC;IAEH,MAAM,qBAAqB,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IAEpD,MAAM,YAAY,CAAC,qBAAqB,EAAE,WAAW,CAAC,CAAC;AACzD,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { type Locator, type Page } from '@playwright/test';
2
+ export declare class PlaywrightDevPage {
3
+ readonly page: Page;
4
+ readonly getStartedLink: Locator;
5
+ constructor(page: Page);
6
+ }
7
+ //# sourceMappingURL=playwright_duplicate_locators_five.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright_duplicate_locators_five.d.ts","sourceRoot":"","sources":["../../src/locator_examples/playwright_duplicate_locators_five.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAEnE,qBAAa,iBAAiB;IAC5B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;gBAErB,IAAI,EAAE,IAAI;CAqBvB"}
@@ -0,0 +1,27 @@
1
+ import { expect } from '@playwright/test';
2
+ export class PlaywrightDevPage {
3
+ page;
4
+ getStartedLink;
5
+ constructor(page) {
6
+ this.page = page;
7
+ this.getStartedLink = page.locator('#bob .bob > bob bob ~bob +bob');
8
+ this.getStartedLink = page.locator('a', { hasText: 'Get started..' });
9
+ this.getStartedLink = page.locator('a', { hasText: 'Get started...' });
10
+ this.getStartedLink = page.getByLabel('a');
11
+ this.getStartedLink = page.getByLabel('a');
12
+ this.getStartedLink = page.getByLabel('a');
13
+ this.getStartedLink = page.getByLabel('a');
14
+ this.getStartedLink = page.getByLabel('a');
15
+ this.getStartedLink = page.getByLabel('a');
16
+ this.getStartedLink = page.getByLabel('a');
17
+ this.getStartedLink = page.getByLabel('a');
18
+ this.getStartedLink = page.getByLabel('a');
19
+ this.getStartedLink = page.getByLabel('a');
20
+ this.getStartedLink = page.getByLabel('a');
21
+ this.getStartedLink = page.getByLabel('a');
22
+ this.getStartedLink = page.getByLabel('a');
23
+ this.getStartedLink = page.getByLabel('a');
24
+ this.getStartedLink = page.getByLabel('a');
25
+ }
26
+ }
27
+ //# sourceMappingURL=playwright_duplicate_locators_five.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright_duplicate_locators_five.js","sourceRoot":"","sources":["../../src/locator_examples/playwright_duplicate_locators_five.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAA2B,MAAM,kBAAkB,CAAC;AAEnE,MAAM,OAAO,iBAAiB;IACnB,IAAI,CAAO;IACX,cAAc,CAAU;IAEjC,YAAY,IAAU;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,+BAA+B,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;QACtE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,gBAAgB,EAAE,CAAC,CAAC;QACvE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;IAC7C,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ import { type Locator, type Page } from '@playwright/test';
2
+ export declare class PlaywrightDevPage {
3
+ readonly page: Page;
4
+ getStartedLink: Locator;
5
+ constructor(page: Page);
6
+ }
7
+ //# sourceMappingURL=playwright_duplicate_locators_four.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright_duplicate_locators_four.d.ts","sourceRoot":"","sources":["../../src/locator_examples/playwright_duplicate_locators_four.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAEnE,qBAAa,iBAAiB;IAC5B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;gBAGZ,IAAI,EAAE,IAAI;CAYvB"}
@@ -0,0 +1,18 @@
1
+ import { expect } from '@playwright/test';
2
+ export class PlaywrightDevPage {
3
+ page;
4
+ getStartedLink;
5
+ constructor(page) {
6
+ this.page = page;
7
+ this.getStartedLink = page.locator('a', { hasText: 'bob' });
8
+ this.getStartedLink = page.locator('a', { hasText: 'bob' });
9
+ this.getStartedLink = page.locator('a', { hasText: 'bob' });
10
+ this.getStartedLink = page.locator('a', { hasText: 'bob' });
11
+ this.getStartedLink = page.locator('a', { hasText: 'bob' });
12
+ this.getStartedLink = page.locator('a', { hasText: 'bob' });
13
+ this.getStartedLink = page.locator('a', { hasText: 'bob' });
14
+ this.getStartedLink = page.locator('a', { hasText: 'bob' });
15
+ this.getStartedLink = page.locator('a', { hasText: 'bob' });
16
+ }
17
+ }
18
+ //# sourceMappingURL=playwright_duplicate_locators_four.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright_duplicate_locators_four.js","sourceRoot":"","sources":["../../src/locator_examples/playwright_duplicate_locators_four.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAA2B,MAAM,kBAAkB,CAAC;AAEnE,MAAM,OAAO,iBAAiB;IACnB,IAAI,CAAO;IACpB,cAAc,CAAU;IAGxB,YAAY,IAAU;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QAC5D,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IAC9D,CAAC;CACF"}
@@ -0,0 +1,8 @@
1
+ import { type Locator, type Page } from '@playwright/test';
2
+ export declare class PlaywrightDevPage {
3
+ readonly page: Page;
4
+ getStartedLink: Locator;
5
+ bob: Locator;
6
+ constructor(page: Page);
7
+ }
8
+ //# sourceMappingURL=playwright_duplicate_locators_one.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright_duplicate_locators_one.d.ts","sourceRoot":"","sources":["../../src/locator_examples/playwright_duplicate_locators_one.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAEnE,qBAAa,iBAAiB;IAC5B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;IACxB,GAAG,EAAE,OAAO,CAAC;gBAGD,IAAI,EAAE,IAAI;CAWvB"}
@@ -0,0 +1,18 @@
1
+ import { expect } from '@playwright/test';
2
+ export class PlaywrightDevPage {
3
+ page;
4
+ getStartedLink;
5
+ bob;
6
+ constructor(page) {
7
+ this.page = page;
8
+ this.bob = page.locator('a', { hasText: 'Get started' });
9
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
10
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
11
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
12
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
13
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
14
+ this.getStartedLink = page.locator('a', { hasText: 'Now Get started' });
15
+ this.getStartedLink = page.locator('a', { hasText: 'Now Get started' });
16
+ }
17
+ }
18
+ //# sourceMappingURL=playwright_duplicate_locators_one.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright_duplicate_locators_one.js","sourceRoot":"","sources":["../../src/locator_examples/playwright_duplicate_locators_one.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAA2B,MAAM,kBAAkB,CAAC;AAEnE,MAAM,OAAO,iBAAiB;IACnB,IAAI,CAAO;IACpB,cAAc,CAAU;IACxB,GAAG,CAAU;IAGb,YAAY,IAAU;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACzD,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;IAC1E,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ import { type Locator, type Page } from '@playwright/test';
2
+ export declare class PlaywrightDevPage {
3
+ readonly page: Page;
4
+ readonly getStartedLink: Locator;
5
+ constructor(page: Page);
6
+ }
7
+ //# sourceMappingURL=playwright_duplicate_locators_seven.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright_duplicate_locators_seven.d.ts","sourceRoot":"","sources":["../../src/locator_examples/playwright_duplicate_locators_seven.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAEnE,qBAAa,iBAAiB;IAC5B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;gBAErB,IAAI,EAAE,IAAI;CAMvB"}
@@ -0,0 +1,12 @@
1
+ import { expect } from '@playwright/test';
2
+ export class PlaywrightDevPage {
3
+ page;
4
+ getStartedLink;
5
+ constructor(page) {
6
+ this.page = page;
7
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
8
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
9
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
10
+ }
11
+ }
12
+ //# sourceMappingURL=playwright_duplicate_locators_seven.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright_duplicate_locators_seven.js","sourceRoot":"","sources":["../../src/locator_examples/playwright_duplicate_locators_seven.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAA2B,MAAM,kBAAkB,CAAC;AAEnE,MAAM,OAAO,iBAAiB;IACnB,IAAI,CAAO;IACX,cAAc,CAAU;IAEjC,YAAY,IAAU;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;IACtE,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ import { type Locator, type Page } from '@playwright/test';
2
+ export declare class PlaywrightDevPage {
3
+ readonly page: Page;
4
+ readonly getStartedLink: Locator;
5
+ constructor(page: Page);
6
+ }
7
+ //# sourceMappingURL=playwright_duplicate_locators_six.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright_duplicate_locators_six.d.ts","sourceRoot":"","sources":["../../src/locator_examples/playwright_duplicate_locators_six.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAEnE,qBAAa,iBAAiB;IAC5B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;gBAErB,IAAI,EAAE,IAAI;CAOvB"}
@@ -0,0 +1,13 @@
1
+ import { expect } from '@playwright/test';
2
+ export class PlaywrightDevPage {
3
+ page;
4
+ getStartedLink;
5
+ constructor(page) {
6
+ this.page = page;
7
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
8
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
9
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
10
+ this.getStartedLink = page.locator('b', { hasText: 'Get started' });
11
+ }
12
+ }
13
+ //# sourceMappingURL=playwright_duplicate_locators_six.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright_duplicate_locators_six.js","sourceRoot":"","sources":["../../src/locator_examples/playwright_duplicate_locators_six.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAA2B,MAAM,kBAAkB,CAAC;AAEnE,MAAM,OAAO,iBAAiB;IACnB,IAAI,CAAO;IACX,cAAc,CAAU;IAEjC,YAAY,IAAU;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;IACtE,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ import { type Locator, type Page } from '@playwright/test';
2
+ export declare class PlaywrightDevPage {
3
+ readonly page: Page;
4
+ getStartedLink: Locator;
5
+ constructor(page: Page);
6
+ }
7
+ //# sourceMappingURL=playwright_duplicate_locators_three.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright_duplicate_locators_three.d.ts","sourceRoot":"","sources":["../../src/locator_examples/playwright_duplicate_locators_three.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,OAAO,EAAE,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAEnE,qBAAa,iBAAiB;IAC5B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,cAAc,EAAE,OAAO,CAAC;gBAGZ,IAAI,EAAE,IAAI;CAiBvB"}
@@ -0,0 +1,23 @@
1
+ import { expect } from '@playwright/test';
2
+ export class PlaywrightDevPage {
3
+ page;
4
+ getStartedLink;
5
+ constructor(page) {
6
+ this.page = page;
7
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
8
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
9
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
10
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
11
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
12
+ this.getStartedLink = page.locator('a', { hasText: 'Get started' });
13
+ this.getStartedLink = page.locator('a', { hasText: 'Now Get started' });
14
+ this.getStartedLink = page.locator('a', { hasText: 'Now Get started' });
15
+ this.getStartedLink = page.locator('a', { hasText: 'Now Get started' });
16
+ this.getStartedLink = page.locator('a', { hasText: 'Now Get started' });
17
+ this.getStartedLink = page.locator('a', { hasText: 'Now Get started' });
18
+ this.getStartedLink = page.locator('a', { hasText: 'Now Get started' });
19
+ this.getStartedLink = page.locator('a', { hasText: 'Now Get started' });
20
+ this.getStartedLink = page.locator('a', { hasText: 'Now Get started' });
21
+ }
22
+ }
23
+ //# sourceMappingURL=playwright_duplicate_locators_three.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright_duplicate_locators_three.js","sourceRoot":"","sources":["../../src/locator_examples/playwright_duplicate_locators_three.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAA2B,MAAM,kBAAkB,CAAC;AAEnE,MAAM,OAAO,iBAAiB;IACnB,IAAI,CAAO;IACpB,cAAc,CAAU;IAGxB,YAAY,IAAU;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;QACpE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;QACxE,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC,CAAC;IAC1E,CAAC;CACF"}
@@ -0,0 +1,6 @@
1
+ import { type Page } from '@playwright/test';
2
+ export declare class PlaywrightDevPage {
3
+ readonly page: Page;
4
+ constructor(page: Page);
5
+ }
6
+ //# sourceMappingURL=playwright_duplicate_locators_two.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright_duplicate_locators_two.d.ts","sourceRoot":"","sources":["../../src/locator_examples/playwright_duplicate_locators_two.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAEnE,qBAAa,iBAAiB;IAC5B,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;gBAGR,IAAI,EAAE,IAAI;CAGvB"}
@@ -0,0 +1,9 @@
1
+ import { expect } from '@playwright/test';
2
+ export class PlaywrightDevPage {
3
+ page;
4
+ // readonly getStartedLink: Locator;
5
+ constructor(page) {
6
+ this.page = page;
7
+ }
8
+ }
9
+ //# sourceMappingURL=playwright_duplicate_locators_two.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"playwright_duplicate_locators_two.js","sourceRoot":"","sources":["../../src/locator_examples/playwright_duplicate_locators_two.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAA2B,MAAM,kBAAkB,CAAC;AAEnE,MAAM,OAAO,iBAAiB;IACnB,IAAI,CAAO;IACpB,oCAAoC;IAEpC,YAAY,IAAU;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ import { type TableUserConfig } from 'table';
2
+ export declare const tableConfig: TableUserConfig;
3
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/table/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,KAAK,eAAe,EAAE,MAAM,OAAO,CAAC;AAGzE,eAAO,MAAM,WAAW,EAAE,eAczB,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { table, getBorderCharacters } from 'table';
2
+ // table config
3
+ export const tableConfig = {
4
+ border: getBorderCharacters('honeywell'),
5
+ columns: [
6
+ { alignment: 'center' }, // #
7
+ { alignment: 'left' }, // Locator
8
+ { alignment: 'right' }, // Total
9
+ { alignment: 'center' }, // Status
10
+ { alignment: 'left' }, // FilePath
11
+ { alignment: 'right' } // Count
12
+ ],
13
+ header: {
14
+ content: '🐶🦴 Max Sniffed Out Duplicate Locators'.cyan,
15
+ alignment: 'center'
16
+ }
17
+ };
18
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/table/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,mBAAmB,EAAwB,MAAM,OAAO,CAAC;AAEzE,eAAe;AACf,MAAM,CAAC,MAAM,WAAW,GAAoB;IACxC,MAAM,EAAE,mBAAmB,CAAC,WAAW,CAAC;IACxC,OAAO,EAAE;QACL,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAI,IAAI;QAC/B,EAAE,SAAS,EAAE,MAAM,EAAE,EAAG,UAAU;QAClC,EAAE,SAAS,EAAE,OAAO,EAAE,EAAI,QAAQ;QAClC,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAG,SAAS;QACnC,EAAE,SAAS,EAAE,MAAM,EAAE,EAAG,WAAW;QACnC,EAAE,SAAS,EAAE,OAAO,EAAE,CAAI,QAAQ;KACrC;IACD,MAAM,EAAE;QACJ,OAAO,EAAE,yCAAyC,CAAC,IAAI;QACvD,SAAS,EAAE,QAAQ;KACtB;CACJ,CAAC"}
@@ -0,0 +1,5 @@
1
+ export declare const regexs: {
2
+ getLocator: RegExp;
3
+ getLocatorPrefix: RegExp;
4
+ };
5
+ //# sourceMappingURL=regexs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regexs.d.ts","sourceRoot":"","sources":["../../src/table/regexs.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,MAAM;;;CAGlB,CAAA"}
@@ -0,0 +1,5 @@
1
+ export const regexs = {
2
+ getLocator: /page\.(locator|getBy|frameLocator)[\w\s*]*.*?\)/gs,
3
+ getLocatorPrefix: /page.(locator|getBy|frameLocator)/
4
+ };
5
+ //# sourceMappingURL=regexs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regexs.js","sourceRoot":"","sources":["../../src/table/regexs.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,MAAM,GAAG;IAClB,UAAU,EAAE,mDAAmD;IAC/D,gBAAgB,EAAE,mCAAmC;CACxD,CAAA"}
@@ -0,0 +1,4 @@
1
+ import { type TableUserConfig } from 'table';
2
+ export declare const displayTable: (duplicateLocatorTable: (string | number)[][], config: TableUserConfig) => Promise<void>;
3
+ export declare const buildTable: (filePaths: string[]) => (string | number)[][];
4
+ //# sourceMappingURL=tableBuilder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tableBuilder.d.ts","sourceRoot":"","sources":["../../src/table/tableBuilder.ts"],"names":[],"mappings":"AAGA,OAAO,EAAS,KAAK,eAAe,EAAE,MAAM,OAAO,CAAC;AAoCpD,eAAO,MAAM,YAAY,GAAU,uBAAuB,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,EAAE,EAAE,QAAQ,eAAe,kBAOvG,CAAA;AA2FD,eAAO,MAAM,UAAU,GAAI,WAAW,MAAM,EAAE,0BAqB7C,CAAA"}
@@ -0,0 +1,119 @@
1
+ import figlet from 'figlet';
2
+ import color from 'colors';
3
+ import * as fs from 'fs';
4
+ import { table } from 'table';
5
+ import { regexs } from './regexs.js';
6
+ import { locatorStatus } from './utils.js';
7
+ // turn on colors for console
8
+ color.enable();
9
+ const consoleHeadings = async () => {
10
+ // display heading for the search - max of tool and info
11
+ const heading = await figlet.text('MAX', { font: 'Larry 3D' });
12
+ console.log(heading.green.bold);
13
+ console.log('PLAYWRIGHT LOCATOR FINDER'
14
+ .green.bold);
15
+ console.log('────────────────────────────────────────────────────────────────────────────────'
16
+ .grey);
17
+ console.log('🐶 Max scans your codebase for duplicate Playwright locators\n' +
18
+ ' and flags their locations.'
19
+ .white);
20
+ console.log('────────────────────────────────────────────────────────────────────────────────\n'
21
+ .grey);
22
+ };
23
+ // display the table showing the contents of the script, along with the duplicate locators in the table
24
+ export const displayTable = async (duplicateLocatorTable, config) => {
25
+ // headings displayed beforing table
26
+ await consoleHeadings();
27
+ // display the final table detailing the duplicate locators
28
+ console.log(table(duplicateLocatorTable, config));
29
+ };
30
+ const listOfDuplicateLocators = (filePaths) => {
31
+ const locators = new Set();
32
+ const storeDuplicateLocators = new Map();
33
+ // read the files find the duplicates, filepath and number of times they appear
34
+ for (const filePath of filePaths) {
35
+ try {
36
+ const readFile = fs.readFileSync(filePath, { encoding: 'utf-8' });
37
+ const matches = readFile.match(regexs.getLocator) ?? [];
38
+ // search the matches store the unqie locators and the duplicates
39
+ for (const locator of matches) {
40
+ // always add into the all locators set
41
+ locators.add(locator);
42
+ const getDuplicateLocator = storeDuplicateLocators.get(locator);
43
+ // create new object here as the duplicate locator isn't here yet
44
+ if (!storeDuplicateLocators.get(locator)) {
45
+ storeDuplicateLocators.set(locator, {
46
+ files: new Map([[filePath, 1]])
47
+ });
48
+ // update the object as it exists
49
+ }
50
+ else {
51
+ // set the total number of locators in a file path
52
+ getDuplicateLocator?.files.set(filePath, (storeDuplicateLocators.get(locator)?.files.get(filePath) ?? 0) + 1);
53
+ }
54
+ }
55
+ }
56
+ catch (err) {
57
+ console.log(`Error reading file - ${err}`);
58
+ }
59
+ }
60
+ return storeDuplicateLocators;
61
+ };
62
+ const countLocatorsSeenAcrossFiles = (duplicateLocators) => {
63
+ let index = 0;
64
+ const tableRows = [];
65
+ // gets the total times a locator appears across all files
66
+ for (const [locator, files] of duplicateLocators) {
67
+ index++;
68
+ // get the total count for each locator across all files
69
+ const runningCount = [...files.files.values()].reduce((acc, val) => acc + val, 0);
70
+ // want to calculate the status based on the running count
71
+ const status = locatorStatus(runningCount);
72
+ // get the file paths for each of the locators
73
+ const filePaths = [...files.files.entries()].map((file) => `${file[0]}`).join('\n');
74
+ const fileCount = [...files.files.entries()].map((count) => `${count[1]}`).join('\n');
75
+ // only push into the table if the total number of locators is more than one
76
+ if (runningCount > 1) {
77
+ tableRows.push([index, locator.replace(regexs.getLocatorPrefix, ''), runningCount, status, filePaths, fileCount]);
78
+ }
79
+ }
80
+ return tableRows;
81
+ };
82
+ const orderTableByHighestStatus = (tableRows) => {
83
+ // order to show the highest status at the top of the table - will help to show the highest duplicates
84
+ const orderByTotalCount = tableRows.sort((first, second) => second[2] - first[2]);
85
+ // reorder the table numbers - sorting will change this
86
+ // now reorder the # numbering
87
+ const reorderedTable = orderByTotalCount.map((row, index) => {
88
+ return [
89
+ index + 1,
90
+ row[1],
91
+ row[2],
92
+ row[3],
93
+ row[4],
94
+ row[5]
95
+ ];
96
+ });
97
+ return reorderedTable;
98
+ };
99
+ export const buildTable = (filePaths) => {
100
+ const tableHeaders = ['#', 'Locator', 'Total', 'Status', 'FilePath', 'Count'];
101
+ // get the duplicate locators
102
+ const storeDuplicateLocators = listOfDuplicateLocators(filePaths);
103
+ // get the duplicate locators seen across a file
104
+ const locatorCountAcrossFiles = countLocatorsSeenAcrossFiles(storeDuplicateLocators);
105
+ // order the table by the highest status
106
+ const orderTable = orderTableByHighestStatus(locatorCountAcrossFiles);
107
+ if (orderTable.length < 1) {
108
+ console.log();
109
+ console.log('🐶 Max sniffed everywhere... no duplicate locators found!'.green.bold);
110
+ console.log('✨ Your selectors are clean, tidy, and robust. Good human.\n'.dim);
111
+ process.exit(0);
112
+ }
113
+ else {
114
+ // return the final table to be displayed in the console
115
+ const finalTable = [tableHeaders, ...orderTable];
116
+ return finalTable;
117
+ }
118
+ };
119
+ //# sourceMappingURL=tableBuilder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tableBuilder.js","sourceRoot":"","sources":["../../src/table/tableBuilder.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export type TableRows = [index: number, locator: string, total: number, status: string, filePath: string, count: string];
2
+ export type locator = string;
3
+ export type filePath = string;
4
+ export type count = number;
5
+ export type LocatorFiles = {
6
+ files: Map<filePath, count>;
7
+ };
8
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/table/types.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,SAAS,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;AACzH,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAC7B,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC;AAC9B,MAAM,MAAM,KAAK,GAAG,MAAM,CAAC;AAG3B,MAAM,MAAM,YAAY,GAAG;IAAE,KAAK,EAAE,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;CAAE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/table/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export declare const locatorStatus: (count: number) => string;
2
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/table/utils.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,WAM1C,CAAA"}
@@ -0,0 +1,18 @@
1
+ import color from 'colors';
2
+ // turn on colors for console
3
+ color.enable();
4
+ // determind the status of the locator found
5
+ // rules:
6
+ // > 10 = HIGH
7
+ // > 5 = MEDIUM
8
+ // else = LOW
9
+ export const locatorStatus = (count) => {
10
+ if (count > 10)
11
+ return 'HIGH'.red;
12
+ if (count > 4)
13
+ return 'MEDIUM'.yellow;
14
+ else {
15
+ return 'LOW'.grey;
16
+ }
17
+ };
18
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/table/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,QAAQ,CAAC;AAG3B,6BAA6B;AAC7B,KAAK,CAAC,MAAM,EAAE,CAAC;AAEf,4CAA4C;AAC5C,SAAS;AACT,cAAc;AACd,eAAe;AACf,aAAa;AACb,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAa,EAAE,EAAE;IAC3C,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,MAAM,CAAC,GAAG,CAAC;IAClC,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,QAAQ,CAAC,MAAM,CAAC;SACjC,CAAC;QACF,OAAO,KAAK,CAAC,IAAI,CAAC;IACtB,CAAC;AACL,CAAC,CAAA"}
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "max-locator-sniffer",
3
+ "version": "1.0.0",
4
+ "description": "a tool to audit playwright locators in a repo, Max finds duplicate locators.",
5
+ "license": "MIT",
6
+ "author": "Ian Bridges",
7
+ "type": "module",
8
+ "main": "index.js",
9
+ "bin": {
10
+ "max": "./dist/index.js"
11
+ },
12
+ "scripts": {
13
+ "dev": "tsx src/index.ts",
14
+ "build": "tsc",
15
+ "test": "tsx src/index.ts"
16
+ },
17
+ "devDependencies": {
18
+ "@playwright/test": "^1.57.0",
19
+ "@types/node": "^25.0.6",
20
+ "colors": "^1.4.0",
21
+ "commander": "^14.0.2",
22
+ "figlet": "^1.9.4",
23
+ "glob": "^13.0.0",
24
+ "i": "^0.3.7",
25
+ "npm": "^11.7.0",
26
+ "table": "^6.9.0",
27
+ "tsx": "^4.21.0",
28
+ "typescript": "^5.9.3"
29
+ }
30
+ }
package/src/index.ts ADDED
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+
3
+ // load using import
4
+ import { Command } from 'commander';
5
+ import { glob } from 'glob';
6
+ import color from 'colors';
7
+ import { tableConfig } from './table/config.js';
8
+ import { buildTable, displayTable } from './table/tableBuilder.js';
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('max')
14
+ .description('🐶 Sniffs out duplicate Playwright locators')
15
+ .version('1.0.0');
16
+
17
+
18
+ program
19
+ .command('sniff')
20
+ .description('Scan files for duplicate Playwright locators')
21
+ .argument(
22
+ '<pattern>',
23
+ 'Glob pattern to scan'
24
+ )
25
+ .action(async (pattern: string) => {
26
+ const filePaths: string[] = await glob(`${pattern}/**/*.{js,ts}`, {
27
+ ignore: 'node_modules/**',
28
+ });
29
+
30
+ const duplicateLocatorTable = buildTable(filePaths);
31
+
32
+ await displayTable(duplicateLocatorTable, tableConfig);
33
+ });
34
+
35
+ program.parse(process.argv);
@@ -0,0 +1,26 @@
1
+ import { expect, type Locator, type Page } from '@playwright/test';
2
+
3
+ export class HomePage {
4
+ readonly page: Page;
5
+ getStarted: Locator;
6
+ loginBtn: Locator;
7
+
8
+ constructor(page: Page) {
9
+ this.page = page;
10
+
11
+ this.getStarted = page.locator('a', { hasText: 'Get started' });
12
+ this.getStarted = page.locator('a', { hasText: 'Get started' });
13
+ this.getStarted = page.locator('a', { hasText: 'Get started' });
14
+
15
+ this.loginBtn = page.locator('button.login');
16
+ this.loginBtn = page.locator('button.login');
17
+ }
18
+
19
+ async goto() {
20
+ await this.page.goto('/');
21
+ }
22
+
23
+ async start() {
24
+ await this.getStarted.click();
25
+ }
26
+ }
@@ -0,0 +1,25 @@
1
+ import { type Locator, type Page } from '@playwright/test';
2
+
3
+ export class SettingsPage {
4
+ readonly page: Page;
5
+ getStarted: Locator;
6
+ nowGetStarted: Locator;
7
+ saveBtn: Locator;
8
+
9
+ constructor(page: Page) {
10
+ this.page = page;
11
+
12
+ this.getStarted = page.locator('a', { hasText: 'Get started' });
13
+ this.getStarted = page.locator('a', { hasText: 'Get started' });
14
+ this.getStarted = page.locator('a', { hasText: 'Get started' });
15
+ this.getStarted = page.locator('a', { hasText: 'Get started' });
16
+
17
+ this.nowGetStarted = page.locator('a', { hasText: 'Now Get started' });
18
+ this.nowGetStarted = page.locator('a', { hasText: 'Now Get started' });
19
+ this.nowGetStarted = page.locator('a', { hasText: 'Now Get started' });
20
+ this.nowGetStarted = page.locator('a', { hasText: 'Now Get started' });
21
+ this.nowGetStarted = page.locator('a', { hasText: 'Now Get started' });
22
+
23
+ this.saveBtn = page.locator('button[type="submit"]');
24
+ }
25
+ }
@@ -0,0 +1,23 @@
1
+ import { type Locator, type Page } from '@playwright/test';
2
+
3
+ export class DashboardPage {
4
+ readonly page: Page;
5
+ getStarted: Locator;
6
+ profileLink: Locator;
7
+ card: Locator;
8
+
9
+ constructor(page: Page) {
10
+ this.page = page;
11
+
12
+ this.getStarted = page.locator('a', { hasText: 'Get started' });
13
+ this.getStarted = page.locator('a', { hasText: 'Get started' });
14
+ this.getStarted = page.locator('a', { hasText: 'Get started' });
15
+ this.getStarted = page.locator('a', { hasText: 'Get started' });
16
+
17
+ this.profileLink = page.locator('a', { hasText: 'Profile' });
18
+ this.profileLink = page.locator('a', { hasText: 'Profile' });
19
+
20
+ this.card = page.locator('div.card.active > span.label');
21
+ this.card = page.locator('div.card.active > span.label');
22
+ }
23
+ }
@@ -0,0 +1,18 @@
1
+ import { table, getBorderCharacters, type TableUserConfig } from 'table';
2
+
3
+ // table config
4
+ export const tableConfig: TableUserConfig = {
5
+ border: getBorderCharacters('honeywell'),
6
+ columns: [
7
+ { alignment: 'center' }, // #
8
+ { alignment: 'left' }, // Locator
9
+ { alignment: 'right' }, // Total
10
+ { alignment: 'center' }, // Status
11
+ { alignment: 'left' }, // FilePath
12
+ { alignment: 'right' } // Count
13
+ ],
14
+ header: {
15
+ content: '🐶🦴 Max Sniffed Out Duplicate Locators'.cyan,
16
+ alignment: 'center'
17
+ }
18
+ };
@@ -0,0 +1,4 @@
1
+ export const regexs = {
2
+ getLocator: /page\.(locator|getBy|frameLocator)[\w\s*]*.*?\)/gs,
3
+ getLocatorPrefix: /page.(locator|getBy|frameLocator)/
4
+ }
@@ -0,0 +1,159 @@
1
+ import figlet from 'figlet';
2
+ import color from 'colors';
3
+ import * as fs from 'fs';
4
+ import { table, type TableUserConfig } from 'table';
5
+ import { regexs } from './regexs.js';
6
+ import type { locator, LocatorFiles, TableRows } from './types.js';
7
+ import { locatorStatus } from './utils.js';
8
+
9
+ // turn on colors for console
10
+ color.enable();
11
+
12
+ const consoleHeadings = async () => {
13
+ // display heading for the search - max of tool and info
14
+ const heading = await figlet.text('MAX', { font: 'Larry 3D' });
15
+ console.log(heading.green.bold);
16
+
17
+ console.log(
18
+ 'PLAYWRIGHT LOCATOR FINDER'
19
+ .green.bold
20
+ );
21
+
22
+ console.log(
23
+ '────────────────────────────────────────────────────────────────────────────────'
24
+ .grey
25
+ );
26
+
27
+ console.log(
28
+ '🐶 Max scans your codebase for duplicate Playwright locators\n' +
29
+ ' and flags their locations.'
30
+ .white
31
+ );
32
+
33
+ console.log(
34
+ '────────────────────────────────────────────────────────────────────────────────\n'
35
+ .grey
36
+ );
37
+
38
+ }
39
+ // display the table showing the contents of the script, along with the duplicate locators in the table
40
+ export const displayTable = async (duplicateLocatorTable: (string | number)[][], config: TableUserConfig) => {
41
+
42
+ // headings displayed beforing table
43
+ await consoleHeadings();
44
+
45
+ // display the final table detailing the duplicate locators
46
+ console.log(table(duplicateLocatorTable, config));
47
+ }
48
+
49
+ const listOfDuplicateLocators = (filePaths: string[]): Map<locator, LocatorFiles> => {
50
+ const locators = new Set<locator>();
51
+ const storeDuplicateLocators = new Map<locator, LocatorFiles>();
52
+
53
+ // read the files find the duplicates, filepath and number of times they appear
54
+ for (const filePath of filePaths) {
55
+ try {
56
+ const readFile = fs.readFileSync(filePath, { encoding: 'utf-8' });
57
+ const matches = readFile.match(regexs.getLocator) ?? [];
58
+
59
+ // search the matches store the unqie locators and the duplicates
60
+ for (const locator of matches) {
61
+
62
+ // always add into the all locators set
63
+ locators.add(locator);
64
+
65
+ const getDuplicateLocator = storeDuplicateLocators.get(locator);
66
+
67
+ // create new object here as the duplicate locator isn't here yet
68
+ if (!storeDuplicateLocators.get(locator)) {
69
+ storeDuplicateLocators.set(locator, {
70
+ files: new Map([[filePath, 1]])
71
+ });
72
+
73
+ // update the object as it exists
74
+ } else {
75
+ // set the total number of locators in a file path
76
+ getDuplicateLocator?.files.set(filePath, (storeDuplicateLocators.get(locator)?.files.get(filePath) ?? 0) + 1);
77
+
78
+ }
79
+ }
80
+ }
81
+ catch (err) {
82
+ console.log(`Error reading file - ${err}`);
83
+ }
84
+
85
+ }
86
+ return storeDuplicateLocators;
87
+ }
88
+
89
+ const countLocatorsSeenAcrossFiles = (duplicateLocators: Map<locator, LocatorFiles>): TableRows[] => {
90
+ let index = 0;
91
+ const tableRows: TableRows[] = [];
92
+
93
+ // gets the total times a locator appears across all files
94
+ for (const [locator, files] of duplicateLocators) {
95
+
96
+ index++;
97
+ // get the total count for each locator across all files
98
+ const runningCount = [...files.files.values()].reduce((acc, val) =>
99
+ acc + val, 0
100
+ );
101
+
102
+ // want to calculate the status based on the running count
103
+ const status = locatorStatus(runningCount);
104
+
105
+ // get the file paths for each of the locators
106
+ const filePaths = [...files.files.entries()].map((file) => `${file[0]}`).join('\n');
107
+
108
+ const fileCount = [...files.files.entries()].map((count) => `${count[1]}`).join('\n');
109
+
110
+ // only push into the table if the total number of locators is more than one
111
+ if (runningCount > 1) {
112
+ tableRows.push([index, locator.replace(regexs.getLocatorPrefix, ''), runningCount, status, filePaths, fileCount]);
113
+ }
114
+
115
+ }
116
+ return tableRows;
117
+ }
118
+
119
+ const orderTableByHighestStatus = (tableRows: TableRows[]): TableRows[] => {
120
+ // order to show the highest status at the top of the table - will help to show the highest duplicates
121
+ const orderByTotalCount = tableRows.sort((first: any, second: any) => second[2] - first[2]);
122
+
123
+ // reorder the table numbers - sorting will change this
124
+ // now reorder the # numbering
125
+ const reorderedTable: TableRows[] = orderByTotalCount.map((row, index) => {
126
+ return [
127
+ index + 1,
128
+ row[1],
129
+ row[2],
130
+ row[3],
131
+ row[4],
132
+ row[5]
133
+ ]
134
+ })
135
+ return reorderedTable;
136
+ }
137
+
138
+ export const buildTable = (filePaths: string[]) => {
139
+ const tableHeaders: string[] = ['#', 'Locator', 'Total', 'Status', 'FilePath', 'Count'];
140
+
141
+ // get the duplicate locators
142
+ const storeDuplicateLocators = listOfDuplicateLocators(filePaths);
143
+ // get the duplicate locators seen across a file
144
+ const locatorCountAcrossFiles = countLocatorsSeenAcrossFiles(storeDuplicateLocators);
145
+
146
+ // order the table by the highest status
147
+ const orderTable = orderTableByHighestStatus(locatorCountAcrossFiles);
148
+
149
+ if (orderTable.length < 1) {
150
+ console.log();
151
+ console.log('🐶 Max sniffed everywhere... no duplicate locators found!'.green.bold);
152
+ console.log('✨ Your selectors are clean, tidy, and robust. Good human.\n'.dim);
153
+ process.exit(0);
154
+ } else {
155
+ // return the final table to be displayed in the console
156
+ const finalTable: (string | number)[][] = [tableHeaders, ...orderTable];
157
+ return finalTable;
158
+ }
159
+ }
@@ -0,0 +1,9 @@
1
+ // TYPES
2
+ // header - '#', 'Locator', 'Total', 'Status', 'FilePath', 'Count'
3
+ export type TableRows = [index: number, locator: string, total: number, status: string, filePath: string, count: string];
4
+ export type locator = string;
5
+ export type filePath = string;
6
+ export type count = number;
7
+
8
+ // the file path and the number of times the locator is found in the file
9
+ export type LocatorFiles = { files: Map<filePath, count> };
@@ -0,0 +1,18 @@
1
+ import color from 'colors';
2
+
3
+
4
+ // turn on colors for console
5
+ color.enable();
6
+
7
+ // determind the status of the locator found
8
+ // rules:
9
+ // > 10 = HIGH
10
+ // > 5 = MEDIUM
11
+ // else = LOW
12
+ export const locatorStatus = (count: number) => {
13
+ if (count > 10) return 'HIGH'.red;
14
+ if (count > 4) return 'MEDIUM'.yellow;
15
+ else {
16
+ return 'LOW'.grey;
17
+ }
18
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "compilerOptions": {
4
+ // File Layout
5
+ "rootDir": "./src",
6
+ "outDir": "./dist",
7
+
8
+ // Environment Settings
9
+ // See also https://aka.ms/tsconfig/module
10
+ "module": "nodenext",
11
+ "target": "es2024",
12
+ "types": ["node"],
13
+ // For nodejs:
14
+ // "lib": ["esnext"],
15
+ // "types": ["node"],
16
+ // and npm install -D @types/nodev
17
+
18
+ // Other Outputs
19
+ "sourceMap": true,
20
+ "declaration": true,
21
+ "declarationMap": true,
22
+
23
+ // Stricter Typechecking Options
24
+ "noUncheckedIndexedAccess": true,
25
+ "exactOptionalPropertyTypes": true,
26
+
27
+ // Style Options
28
+ // "noImplicitReturns": true,
29
+ // "noImplicitOverride": true,
30
+ // "noUnusedLocals": true,
31
+ // "noUnusedParameters": true,
32
+ // "noFallthroughCasesInSwitch": true,
33
+ // "noPropertyAccessFromIndexSignature": true,
34
+
35
+ // Recommended Options
36
+ "strict": true,
37
+ "jsx": "react-jsx",
38
+ "verbatimModuleSyntax": true,
39
+ "isolatedModules": true,
40
+ "noUncheckedSideEffectImports": true,
41
+ "moduleDetection": "force",
42
+ "skipLibCheck": true,
43
+ }
44
+ }