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.
- package/README.md +62 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/locator_examples/playwright_duplicate_locators_five.d.ts +7 -0
- package/dist/locator_examples/playwright_duplicate_locators_five.d.ts.map +1 -0
- package/dist/locator_examples/playwright_duplicate_locators_five.js +27 -0
- package/dist/locator_examples/playwright_duplicate_locators_five.js.map +1 -0
- package/dist/locator_examples/playwright_duplicate_locators_four.d.ts +7 -0
- package/dist/locator_examples/playwright_duplicate_locators_four.d.ts.map +1 -0
- package/dist/locator_examples/playwright_duplicate_locators_four.js +18 -0
- package/dist/locator_examples/playwright_duplicate_locators_four.js.map +1 -0
- package/dist/locator_examples/playwright_duplicate_locators_one.d.ts +8 -0
- package/dist/locator_examples/playwright_duplicate_locators_one.d.ts.map +1 -0
- package/dist/locator_examples/playwright_duplicate_locators_one.js +18 -0
- package/dist/locator_examples/playwright_duplicate_locators_one.js.map +1 -0
- package/dist/locator_examples/playwright_duplicate_locators_seven.d.ts +7 -0
- package/dist/locator_examples/playwright_duplicate_locators_seven.d.ts.map +1 -0
- package/dist/locator_examples/playwright_duplicate_locators_seven.js +12 -0
- package/dist/locator_examples/playwright_duplicate_locators_seven.js.map +1 -0
- package/dist/locator_examples/playwright_duplicate_locators_six.d.ts +7 -0
- package/dist/locator_examples/playwright_duplicate_locators_six.d.ts.map +1 -0
- package/dist/locator_examples/playwright_duplicate_locators_six.js +13 -0
- package/dist/locator_examples/playwright_duplicate_locators_six.js.map +1 -0
- package/dist/locator_examples/playwright_duplicate_locators_three.d.ts +7 -0
- package/dist/locator_examples/playwright_duplicate_locators_three.d.ts.map +1 -0
- package/dist/locator_examples/playwright_duplicate_locators_three.js +23 -0
- package/dist/locator_examples/playwright_duplicate_locators_three.js.map +1 -0
- package/dist/locator_examples/playwright_duplicate_locators_two.d.ts +6 -0
- package/dist/locator_examples/playwright_duplicate_locators_two.d.ts.map +1 -0
- package/dist/locator_examples/playwright_duplicate_locators_two.js +9 -0
- package/dist/locator_examples/playwright_duplicate_locators_two.js.map +1 -0
- package/dist/table/config.d.ts +3 -0
- package/dist/table/config.d.ts.map +1 -0
- package/dist/table/config.js +18 -0
- package/dist/table/config.js.map +1 -0
- package/dist/table/regexs.d.ts +5 -0
- package/dist/table/regexs.d.ts.map +1 -0
- package/dist/table/regexs.js +5 -0
- package/dist/table/regexs.js.map +1 -0
- package/dist/table/tableBuilder.d.ts +4 -0
- package/dist/table/tableBuilder.d.ts.map +1 -0
- package/dist/table/tableBuilder.js +119 -0
- package/dist/table/tableBuilder.js.map +1 -0
- package/dist/table/types.d.ts +8 -0
- package/dist/table/types.d.ts.map +1 -0
- package/dist/table/types.js +2 -0
- package/dist/table/types.js.map +1 -0
- package/dist/table/utils.d.ts +2 -0
- package/dist/table/utils.d.ts.map +1 -0
- package/dist/table/utils.js +18 -0
- package/dist/table/utils.js.map +1 -0
- package/images/max.webp +0 -0
- package/images/max_example_terminal.png +0 -0
- package/package.json +30 -0
- package/src/index.ts +35 -0
- package/src/locator_examples/playwright_duplicate_locators_one.ts +26 -0
- package/src/locator_examples/playwright_duplicate_locators_three.ts +25 -0
- package/src/locator_examples/playwright_duplicate_locators_two.ts +23 -0
- package/src/table/config.ts +18 -0
- package/src/table/regexs.ts +4 -0
- package/src/table/tableBuilder.ts +159 -0
- package/src/table/types.ts +9 -0
- package/src/table/utils.ts +18 -0
- 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
|
+
---
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"file":"regexs.d.ts","sourceRoot":"","sources":["../../src/table/regexs.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,MAAM;;;CAGlB,CAAA"}
|
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/table/types.ts"],"names":[],"mappings":""}
|
|
@@ -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"}
|
package/images/max.webp
ADDED
|
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,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
|
+
}
|