superkit-mcp-server 1.0.1 → 1.0.2
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/ARCHITECTURE.md +2 -3
- package/README.md +1 -0
- package/build/index.js +75 -0
- package/build/tools/autoPreview.js +99 -0
- package/build/tools/checklist.js +120 -0
- package/build/tools/sessionManager.js +107 -0
- package/build/tools/validators/__tests__/apiSchema.test.js +77 -0
- package/build/tools/validators/__tests__/convertRules.test.js +38 -0
- package/build/tools/validators/__tests__/frontendDesign.test.js +55 -0
- package/build/tools/validators/__tests__/geoChecker.test.js +45 -0
- package/build/tools/validators/__tests__/i18nChecker.test.js +32 -0
- package/build/tools/validators/__tests__/lintRunner.test.js +65 -0
- package/build/tools/validators/__tests__/mobileAudit.test.js +40 -0
- package/build/tools/validators/__tests__/playwrightRunner.test.js +55 -0
- package/build/tools/validators/__tests__/reactPerformanceChecker.test.js +49 -0
- package/build/tools/validators/__tests__/securityScan.test.js +42 -0
- package/build/tools/validators/__tests__/seoChecker.test.js +44 -0
- package/build/tools/validators/__tests__/testRunner.test.js +49 -0
- package/build/tools/validators/__tests__/typeCoverage.test.js +62 -0
- package/build/tools/validators/accessibilityChecker.js +124 -0
- package/build/tools/validators/apiValidator.js +140 -0
- package/build/tools/validators/convertRules.js +170 -0
- package/build/tools/validators/geoChecker.js +176 -0
- package/build/tools/validators/i18nChecker.js +205 -0
- package/build/tools/validators/lighthouseAudit.js +50 -0
- package/build/tools/validators/lintRunner.js +106 -0
- package/build/tools/validators/mobileAudit.js +190 -0
- package/build/tools/validators/playwrightRunner.js +101 -0
- package/build/tools/validators/reactPerformanceChecker.js +199 -0
- package/build/tools/validators/schemaValidator.js +105 -0
- package/build/tools/validators/securityScan.js +215 -0
- package/build/tools/validators/seoChecker.js +122 -0
- package/build/tools/validators/testRunner.js +111 -0
- package/build/tools/validators/typeCoverage.js +150 -0
- package/build/tools/validators/uxAudit.js +222 -0
- package/build/tools/verifyAll.js +159 -0
- package/package.json +5 -3
- package/skills/tech/api-patterns/SKILL.md +1 -1
- package/skills/tech/clean-code/SKILL.md +14 -14
- package/skills/tech/doc.md +3 -3
- package/skills/tech/frontend-design/SKILL.md +1 -1
- package/skills/tech/geo-fundamentals/SKILL.md +1 -1
- package/skills/tech/i18n-localization/SKILL.md +1 -1
- package/skills/tech/lint-and-validate/SKILL.md +2 -2
- package/skills/tech/mobile-design/SKILL.md +1 -1
- package/skills/tech/nextjs-react-expert/SKILL.md +1 -1
- package/skills/tech/parallel-agents/SKILL.md +3 -3
- package/skills/tech/performance-profiling/SKILL.md +1 -1
- package/skills/tech/vulnerability-scanner/SKILL.md +1 -1
- package/skills/tech/webapp-testing/SKILL.md +3 -3
- package/workflows/review-compound.md +1 -1
- package/skills/tech/api-patterns/scripts/api_validator.py +0 -211
- package/skills/tech/database-design/scripts/schema_validator.py +0 -172
- package/skills/tech/frontend-design/scripts/accessibility_checker.py +0 -183
- package/skills/tech/frontend-design/scripts/ux_audit.py +0 -722
- package/skills/tech/geo-fundamentals/scripts/geo_checker.py +0 -289
- package/skills/tech/i18n-localization/scripts/i18n_checker.py +0 -241
- package/skills/tech/lint-and-validate/scripts/lint_runner.py +0 -184
- package/skills/tech/lint-and-validate/scripts/type_coverage.py +0 -173
- package/skills/tech/mobile-design/scripts/mobile_audit.py +0 -670
- package/skills/tech/nextjs-react-expert/scripts/convert_rules.py +0 -222
- package/skills/tech/nextjs-react-expert/scripts/react_performance_checker.py +0 -252
- package/skills/tech/performance-profiling/scripts/lighthouse_audit.py +0 -76
- package/skills/tech/seo-fundamentals/scripts/seo_checker.py +0 -219
- package/skills/tech/testing-patterns/scripts/test_runner.py +0 -219
- package/skills/tech/vulnerability-scanner/scripts/security_scan.py +0 -458
- package/skills/tech/webapp-testing/scripts/playwright_runner.py +0 -173
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { checkGeoPage } from '../geoChecker.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
vi.mock('fs/promises');
|
|
5
|
+
describe('geoChecker', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.clearAllMocks();
|
|
8
|
+
});
|
|
9
|
+
describe('checkGeoPage', () => {
|
|
10
|
+
it('should detect structured schema and H1s', async () => {
|
|
11
|
+
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<head>
|
|
14
|
+
<script type="application/ld+json">
|
|
15
|
+
{
|
|
16
|
+
"@type": "Article",
|
|
17
|
+
"author": "Test Author"
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<h1>Main Title</h1>
|
|
23
|
+
<p>Some content with numbers like 50% and $100.</p>
|
|
24
|
+
</body>
|
|
25
|
+
</html>
|
|
26
|
+
`);
|
|
27
|
+
const result = await checkGeoPage('/mock/page.html');
|
|
28
|
+
expect(result.passed.some(m => m.includes('JSON-LD structured data'))).toBe(true);
|
|
29
|
+
expect(result.passed.some(m => m.includes('Single H1'))).toBe(true);
|
|
30
|
+
expect(result.passed.some(m => m.includes('Original statistics'))).toBe(true);
|
|
31
|
+
expect(result.score).toBeGreaterThan(0);
|
|
32
|
+
});
|
|
33
|
+
it('should flag missing structural tags', async () => {
|
|
34
|
+
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
35
|
+
<body>
|
|
36
|
+
<p>No headings, no structure, no stats.</p>
|
|
37
|
+
</body>
|
|
38
|
+
`);
|
|
39
|
+
const result = await checkGeoPage('/mock/bad.html');
|
|
40
|
+
expect(result.issues.some(m => m.includes('No JSON-LD'))).toBe(true);
|
|
41
|
+
expect(result.issues.some(m => m.includes('No H1 heading'))).toBe(true);
|
|
42
|
+
expect(result.score).toBeLessThan(50);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { checkLocaleCompleteness } from '../i18nChecker.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
vi.mock('fs/promises');
|
|
5
|
+
describe('i18nChecker', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.clearAllMocks();
|
|
8
|
+
});
|
|
9
|
+
describe('checkLocaleCompleteness', () => {
|
|
10
|
+
it('should detect missing translation keys in other locales', async () => {
|
|
11
|
+
// Mock translation files
|
|
12
|
+
vi.mocked(fs.readFile).mockImplementation(async (file) => {
|
|
13
|
+
if (file.includes('en'))
|
|
14
|
+
return JSON.stringify({ hello: 'World', nested: { a: 1, b: 2 } });
|
|
15
|
+
if (file.includes('fr'))
|
|
16
|
+
return JSON.stringify({ hello: 'Monde', nested: { a: 1 } });
|
|
17
|
+
return "{}";
|
|
18
|
+
});
|
|
19
|
+
const result = await checkLocaleCompleteness(['/locales/en/common.json', '/locales/fr/common.json']);
|
|
20
|
+
expect(result.issues.some(i => i.includes('Missing 1 keys'))).toBe(true);
|
|
21
|
+
expect(result.passed.some(p => p.includes('Found 2 language'))).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
it('should pass matching keys', async () => {
|
|
24
|
+
vi.mocked(fs.readFile).mockImplementation(async (file) => {
|
|
25
|
+
return JSON.stringify({ a: 1, b: 2 });
|
|
26
|
+
});
|
|
27
|
+
const result = await checkLocaleCompleteness(['/en/app.json', '/es/app.json']);
|
|
28
|
+
expect(result.issues.length).toBe(0);
|
|
29
|
+
expect(result.passed.some(p => p.includes('matching keys'))).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { detectProjectType, runLinter } from '../lintRunner.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
import * as cp from 'child_process';
|
|
6
|
+
// Mock fs and child_process modules
|
|
7
|
+
vi.mock('fs/promises');
|
|
8
|
+
vi.mock('fs', () => ({ existsSync: vi.fn() }));
|
|
9
|
+
vi.mock('child_process');
|
|
10
|
+
describe('lintRunner', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
describe('detectProjectType', () => {
|
|
15
|
+
it('should detect a node project with lint script', async () => {
|
|
16
|
+
existsSync.mockImplementation((p) => p.endsWith('package.json'));
|
|
17
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
|
|
18
|
+
scripts: { lint: 'eslint .' }
|
|
19
|
+
}));
|
|
20
|
+
const info = await detectProjectType('/mock/path');
|
|
21
|
+
expect(info.type).toBe('node');
|
|
22
|
+
expect(info.linters).toEqual([{ name: 'npm lint', cmd: ['npm', 'run', 'lint'] }]);
|
|
23
|
+
});
|
|
24
|
+
it('should detect a python project with ruff', async () => {
|
|
25
|
+
existsSync.mockImplementation((p) => p.endsWith('pyproject.toml') || p.endsWith('mypy.ini'));
|
|
26
|
+
const info = await detectProjectType('/mock/python');
|
|
27
|
+
expect(info.type).toBe('python');
|
|
28
|
+
expect(info.linters).toContainEqual({ name: 'ruff', cmd: ['ruff', 'check', '.'] });
|
|
29
|
+
expect(info.linters).toContainEqual({ name: 'mypy', cmd: ['mypy', '.'] });
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('runLinter', () => {
|
|
33
|
+
it('should return passed on success code', async () => {
|
|
34
|
+
// Mock spawn
|
|
35
|
+
const mockChild = {
|
|
36
|
+
stdout: { on: vi.fn((event, cb) => cb('success out')) },
|
|
37
|
+
stderr: { on: vi.fn() },
|
|
38
|
+
on: vi.fn((event, cb) => {
|
|
39
|
+
if (event === 'close')
|
|
40
|
+
cb(0);
|
|
41
|
+
}),
|
|
42
|
+
kill: vi.fn()
|
|
43
|
+
};
|
|
44
|
+
vi.mocked(cp.spawn).mockReturnValue(mockChild);
|
|
45
|
+
const result = await runLinter({ name: 'test', cmd: ['npm', 'test'] }, '/mock');
|
|
46
|
+
expect(result.passed).toBe(true);
|
|
47
|
+
expect(result.output).toContain('success out');
|
|
48
|
+
});
|
|
49
|
+
it('should return failed on error code', async () => {
|
|
50
|
+
const mockChild = {
|
|
51
|
+
stdout: { on: vi.fn() },
|
|
52
|
+
stderr: { on: vi.fn((event, cb) => cb('error out')) },
|
|
53
|
+
on: vi.fn((event, cb) => {
|
|
54
|
+
if (event === 'close')
|
|
55
|
+
cb(1);
|
|
56
|
+
}),
|
|
57
|
+
kill: vi.fn()
|
|
58
|
+
};
|
|
59
|
+
vi.mocked(cp.spawn).mockReturnValue(mockChild);
|
|
60
|
+
const result = await runLinter({ name: 'failtest', cmd: ['npm', 'fail'] }, '/mock');
|
|
61
|
+
expect(result.passed).toBe(false);
|
|
62
|
+
expect(result.error).toContain('error out');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { runMobileAudit } from '../mobileAudit.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
vi.mock('fs/promises');
|
|
5
|
+
describe('mobileAudit', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.clearAllMocks();
|
|
8
|
+
});
|
|
9
|
+
it('should report warnings for generic rn file issues', async () => {
|
|
10
|
+
vi.mocked(fs.readdir).mockResolvedValue([{
|
|
11
|
+
name: 'Screen.tsx',
|
|
12
|
+
isDirectory: () => false
|
|
13
|
+
}]);
|
|
14
|
+
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
15
|
+
import React from 'react';
|
|
16
|
+
import { View, ScrollView } from 'react-native';
|
|
17
|
+
export const Screen = () => {
|
|
18
|
+
return (
|
|
19
|
+
<ScrollView>
|
|
20
|
+
{data.map(d => <View key={index} />)}
|
|
21
|
+
</ScrollView>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
`);
|
|
25
|
+
const res = await runMobileAudit('.');
|
|
26
|
+
expect(res.passed).toBe(false);
|
|
27
|
+
expect(res.report).toContain('ScrollView with .map()'); // critical performance
|
|
28
|
+
});
|
|
29
|
+
it('should ignore non react native / flutter files', async () => {
|
|
30
|
+
vi.mocked(fs.readdir).mockResolvedValue([{
|
|
31
|
+
name: 'regular.tsx',
|
|
32
|
+
isDirectory: () => false
|
|
33
|
+
}]);
|
|
34
|
+
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
35
|
+
export const App = () => <div>Hello Web</div>
|
|
36
|
+
`);
|
|
37
|
+
const res = await runMobileAudit('.');
|
|
38
|
+
expect(res.passed).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { runPlaywrightTest, runPlaywrightA11y } from '../playwrightRunner.js';
|
|
3
|
+
vi.mock('playwright', () => {
|
|
4
|
+
return {
|
|
5
|
+
chromium: {
|
|
6
|
+
launch: vi.fn().mockResolvedValue({
|
|
7
|
+
newContext: vi.fn().mockResolvedValue({
|
|
8
|
+
newPage: vi.fn().mockResolvedValue({
|
|
9
|
+
goto: vi.fn().mockResolvedValue({
|
|
10
|
+
status: () => 200,
|
|
11
|
+
ok: () => true
|
|
12
|
+
}),
|
|
13
|
+
title: vi.fn().mockResolvedValue('Mock Title'),
|
|
14
|
+
url: () => 'https://example.com',
|
|
15
|
+
locator: vi.fn().mockReturnValue({ count: vi.fn().mockResolvedValue(5) }),
|
|
16
|
+
on: vi.fn(),
|
|
17
|
+
evaluate: vi.fn().mockResolvedValue('{"dom_content_loaded":100,"load_complete":200}'),
|
|
18
|
+
screenshot: vi.fn().mockResolvedValue(true)
|
|
19
|
+
})
|
|
20
|
+
}),
|
|
21
|
+
newPage: vi.fn().mockResolvedValue({
|
|
22
|
+
goto: vi.fn().mockResolvedValue({
|
|
23
|
+
status: () => 200,
|
|
24
|
+
ok: () => true
|
|
25
|
+
}),
|
|
26
|
+
locator: vi.fn().mockReturnValue({ count: vi.fn().mockResolvedValue(3) })
|
|
27
|
+
}),
|
|
28
|
+
close: vi.fn().mockResolvedValue(undefined)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
describe('playwrightRunner', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
});
|
|
37
|
+
it('should successfully run a basic browser test', async () => {
|
|
38
|
+
const res = await runPlaywrightTest('https://example.com');
|
|
39
|
+
console.log("PLAYWRIGHT TEST:", res);
|
|
40
|
+
expect(res.error).toBeUndefined();
|
|
41
|
+
expect(res.status).toBe('success');
|
|
42
|
+
expect(res.health.loaded).toBe(true);
|
|
43
|
+
expect(res.performance.dom_content_loaded).toBe(100);
|
|
44
|
+
expect(res.elements.links).toBe(5);
|
|
45
|
+
expect(res.summary).toContain('[OK] Page loaded');
|
|
46
|
+
});
|
|
47
|
+
it('should calculate accessibility counts properly', async () => {
|
|
48
|
+
const res = await runPlaywrightA11y('https://example.com');
|
|
49
|
+
console.log("PLAYWRIGHT A11y:", res);
|
|
50
|
+
expect(res.error).toBeUndefined();
|
|
51
|
+
expect(res.status).toBe('success');
|
|
52
|
+
expect(res.accessibility.images_with_alt).toBe(3);
|
|
53
|
+
expect(res.accessibility.headings.h1).toBe(3);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { runReactPerformanceChecker } from '../reactPerformanceChecker.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
vi.mock('fs/promises');
|
|
5
|
+
describe('reactPerformanceChecker', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.clearAllMocks();
|
|
8
|
+
});
|
|
9
|
+
it('should detect waterfalls and barrel imports', async () => {
|
|
10
|
+
vi.mocked(fs.readdir).mockResolvedValue([{
|
|
11
|
+
name: 'Home.tsx',
|
|
12
|
+
isDirectory: () => false
|
|
13
|
+
}]);
|
|
14
|
+
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
15
|
+
import { something } from '@/components/index';
|
|
16
|
+
|
|
17
|
+
async function loadData() {
|
|
18
|
+
const a = await fetchA();
|
|
19
|
+
const b = await fetchB();
|
|
20
|
+
return { a, b };
|
|
21
|
+
}
|
|
22
|
+
`);
|
|
23
|
+
const res = await runReactPerformanceChecker('.');
|
|
24
|
+
console.log("RPC BAD REPORT:", res.report);
|
|
25
|
+
expect(res.passed).toBe(false); // CRITICAL issue makes it fail
|
|
26
|
+
expect(res.report).toContain('Sequential awaits detected (waterfall)');
|
|
27
|
+
expect(res.report).toContain('Potential barrel imports detected');
|
|
28
|
+
});
|
|
29
|
+
it('should pass cleanly configured react app', async () => {
|
|
30
|
+
vi.mocked(fs.readdir).mockResolvedValue([{
|
|
31
|
+
name: 'App.tsx',
|
|
32
|
+
isDirectory: () => false
|
|
33
|
+
}]);
|
|
34
|
+
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
35
|
+
import { useQuery } from '@tanstack/react-query';
|
|
36
|
+
import Image from 'next/image';
|
|
37
|
+
|
|
38
|
+
const App = React.memo((props: Props) => {
|
|
39
|
+
const q = useQuery('data', fetchParallel);
|
|
40
|
+
return <Image src="foo" alt="bar" />
|
|
41
|
+
});
|
|
42
|
+
export default App;
|
|
43
|
+
`);
|
|
44
|
+
const res = await runReactPerformanceChecker('.');
|
|
45
|
+
console.log("RPC GOOD REPORT:", res.report);
|
|
46
|
+
expect(res.passed).toBe(true);
|
|
47
|
+
expect(res.report).toContain('[SUCCESS]');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { scanSecrets, scanCodePatterns } from '../securityScan.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
vi.mock('fs/promises');
|
|
5
|
+
describe('securityScan', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.clearAllMocks();
|
|
8
|
+
});
|
|
9
|
+
describe('scanSecrets', () => {
|
|
10
|
+
it('should detect AWS keys', async () => {
|
|
11
|
+
// Mock file system
|
|
12
|
+
vi.mocked(fs.readdir).mockResolvedValue([{
|
|
13
|
+
name: 'config.json',
|
|
14
|
+
isDirectory: () => false
|
|
15
|
+
}]);
|
|
16
|
+
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
17
|
+
{ "aws_key": "AKIA1234567890123456" }
|
|
18
|
+
`);
|
|
19
|
+
const result = await scanSecrets('/mock');
|
|
20
|
+
expect(result.findings).toBeDefined();
|
|
21
|
+
expect(result.findings.some((f) => f.type === 'AWS Access Key')).toBe(true);
|
|
22
|
+
expect(result.by_severity.critical).toBeGreaterThan(0);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe('scanCodePatterns', () => {
|
|
26
|
+
it('should detect eval() and child_process.exec()', async () => {
|
|
27
|
+
vi.mocked(fs.readdir).mockResolvedValue([{
|
|
28
|
+
name: 'bad_code.js',
|
|
29
|
+
isDirectory: () => false
|
|
30
|
+
}]);
|
|
31
|
+
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
32
|
+
eval('2 + 2');
|
|
33
|
+
import { exec } from 'child_process';
|
|
34
|
+
child_process.exec('rm -rf /');
|
|
35
|
+
`);
|
|
36
|
+
const result = await scanCodePatterns('/mock');
|
|
37
|
+
expect(result.findings.length).toBeGreaterThan(1);
|
|
38
|
+
expect(result.findings.some((f) => f.pattern === 'eval() usage')).toBe(true);
|
|
39
|
+
expect(result.findings.some((f) => f.pattern === 'child_process.exec')).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { runSeoChecker } from '../seoChecker.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
vi.mock('fs/promises');
|
|
5
|
+
describe('seoChecker', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.clearAllMocks();
|
|
8
|
+
});
|
|
9
|
+
it('should report SEO issues on a page', async () => {
|
|
10
|
+
vi.mocked(fs.readdir).mockResolvedValue([{
|
|
11
|
+
name: 'index.tsx', // valid page layout file
|
|
12
|
+
isDirectory: () => false
|
|
13
|
+
}]);
|
|
14
|
+
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
15
|
+
export default function Index() {
|
|
16
|
+
return (
|
|
17
|
+
<html>
|
|
18
|
+
<head>
|
|
19
|
+
<title>Oops missing description and og</title>
|
|
20
|
+
</head>
|
|
21
|
+
<body>
|
|
22
|
+
<h1>Title</h1>
|
|
23
|
+
<h1>Duplicate Title</h1>
|
|
24
|
+
<img src="foo" />
|
|
25
|
+
<img src="bar" alt="" />
|
|
26
|
+
</body>
|
|
27
|
+
</html>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
`);
|
|
31
|
+
const res = await runSeoChecker('.');
|
|
32
|
+
expect(res.passed).toBe(false);
|
|
33
|
+
expect(res.report).toContain('Image missing alt attribute');
|
|
34
|
+
});
|
|
35
|
+
it('should ignore non-pages files like utilities', async () => {
|
|
36
|
+
vi.mocked(fs.readdir).mockResolvedValue([{
|
|
37
|
+
name: 'api.util.ts',
|
|
38
|
+
isDirectory: () => false
|
|
39
|
+
}]);
|
|
40
|
+
const res = await runSeoChecker('.');
|
|
41
|
+
expect(res.passed).toBe(true);
|
|
42
|
+
expect(res.report).toContain('[!] No page files found.');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { runTestRunner, detectTestFramework } from '../testRunner.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
import * as child_process from 'child_process';
|
|
5
|
+
vi.mock('fs/promises');
|
|
6
|
+
vi.mock('child_process', () => ({
|
|
7
|
+
exec: vi.fn((cmd, options, callback) => {
|
|
8
|
+
callback(null, { stdout: '1 passed', stderr: '' });
|
|
9
|
+
})
|
|
10
|
+
}));
|
|
11
|
+
describe('testRunner', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
it('should detect node test frameworks via package.json', async () => {
|
|
16
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
|
|
17
|
+
scripts: { test: "something" },
|
|
18
|
+
devDependencies: { vitest: "^1.0.0" }
|
|
19
|
+
}));
|
|
20
|
+
vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT')); // no python
|
|
21
|
+
const info = await detectTestFramework('.');
|
|
22
|
+
expect(info.type).toBe('node');
|
|
23
|
+
expect(info.framework).toBe('vitest');
|
|
24
|
+
expect(info.cmd).toEqual(['npm', 'test']);
|
|
25
|
+
expect(info.coverageCmd).toEqual(['npx', 'vitest', 'run', '--coverage']);
|
|
26
|
+
});
|
|
27
|
+
it('should detect python tests via pyproject.toml', async () => {
|
|
28
|
+
vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT')); // no package.json
|
|
29
|
+
vi.mocked(fs.stat).mockResolvedValue({});
|
|
30
|
+
const info = await detectTestFramework('.');
|
|
31
|
+
expect(info.type).toBe('python');
|
|
32
|
+
expect(info.framework).toBe('pytest');
|
|
33
|
+
expect(info.cmd).toEqual(['python', '-m', 'pytest', '-v']);
|
|
34
|
+
});
|
|
35
|
+
it('should run tests and report success', async () => {
|
|
36
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
|
|
37
|
+
scripts: { test: "jest" }
|
|
38
|
+
}));
|
|
39
|
+
// Mock child_process.exec to simulate passing tests
|
|
40
|
+
vi.mocked(child_process.exec).mockImplementation((...args) => {
|
|
41
|
+
const cb = args[args.length - 1];
|
|
42
|
+
cb(null, { stdout: 'Tests: 1 passed, 1 total', stderr: '' });
|
|
43
|
+
return {};
|
|
44
|
+
});
|
|
45
|
+
const res = await runTestRunner('.');
|
|
46
|
+
expect(res.passed).toBe(true);
|
|
47
|
+
expect(res.report).toContain('Tests: 1 total, 1 passed, 0 failed');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { checkTypescriptCoverage, checkPythonCoverage } from '../typeCoverage.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
vi.mock('fs/promises');
|
|
5
|
+
describe('typeCoverage', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
vi.clearAllMocks();
|
|
8
|
+
});
|
|
9
|
+
describe('checkTypescriptCoverage', () => {
|
|
10
|
+
it('should analyze a typescript file and find any types', async () => {
|
|
11
|
+
// Mock readdir to return 1 TS file
|
|
12
|
+
vi.mocked(fs.readdir).mockResolvedValue([{
|
|
13
|
+
name: 'index.ts',
|
|
14
|
+
isDirectory: () => false
|
|
15
|
+
}]);
|
|
16
|
+
// Mock readFile to return a snippet with an untyped function and an "any"
|
|
17
|
+
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
18
|
+
function test(): any { return 1; }
|
|
19
|
+
const test2 = (x) => console.log(x);
|
|
20
|
+
`);
|
|
21
|
+
const result = await checkTypescriptCoverage('/mock/folder');
|
|
22
|
+
expect(result.type).toBe('typescript');
|
|
23
|
+
expect(result.files).toBe(1);
|
|
24
|
+
expect(result.stats.any_count).toBeGreaterThan(0);
|
|
25
|
+
expect(result.stats.untyped_functions).toBeGreaterThan(0);
|
|
26
|
+
});
|
|
27
|
+
it('should handle perfectly typed code without any', async () => {
|
|
28
|
+
vi.mocked(fs.readdir).mockResolvedValue([{
|
|
29
|
+
name: 'good.ts',
|
|
30
|
+
isDirectory: () => false
|
|
31
|
+
}]);
|
|
32
|
+
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
33
|
+
function typedFunc(val: string): number { return val.length; }
|
|
34
|
+
`);
|
|
35
|
+
const result = await checkTypescriptCoverage('/mock/good');
|
|
36
|
+
expect(result.stats.any_count).toBe(0);
|
|
37
|
+
expect(result.stats.untyped_functions).toBe(0);
|
|
38
|
+
expect(result.passed.some(p => p.includes('100%'))).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
describe('checkPythonCoverage', () => {
|
|
42
|
+
it('should detect Python type hint issues', async () => {
|
|
43
|
+
vi.mocked(fs.readdir).mockResolvedValue([{
|
|
44
|
+
name: 'script.py',
|
|
45
|
+
isDirectory: () => false
|
|
46
|
+
}]);
|
|
47
|
+
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
48
|
+
def bad_func(arg):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def good_func(arg: str) -> bool:
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
def any_func(arg: Any) -> Any:
|
|
55
|
+
pass
|
|
56
|
+
`);
|
|
57
|
+
const result = await checkPythonCoverage('/mock/py');
|
|
58
|
+
expect(result.stats.any_count).toBeGreaterThan(0);
|
|
59
|
+
expect(result.stats.untyped_functions).toBe(1);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
export async function findHtmlFiles(projectPath) {
|
|
4
|
+
let files = [];
|
|
5
|
+
const skipDirs = new Set(['node_modules', '.next', 'dist', 'build', '.git']);
|
|
6
|
+
async function search(dir) {
|
|
7
|
+
try {
|
|
8
|
+
const items = await fs.readdir(dir, { withFileTypes: true });
|
|
9
|
+
for (const item of items) {
|
|
10
|
+
if (skipDirs.has(item.name))
|
|
11
|
+
continue;
|
|
12
|
+
const fullPath = path.join(dir, item.name);
|
|
13
|
+
if (item.isDirectory())
|
|
14
|
+
await search(fullPath);
|
|
15
|
+
else {
|
|
16
|
+
const ext = path.extname(item.name).toLowerCase();
|
|
17
|
+
if (['.html', '.jsx', '.tsx'].includes(ext)) {
|
|
18
|
+
files.push(fullPath);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
24
|
+
}
|
|
25
|
+
await search(projectPath);
|
|
26
|
+
return files.slice(0, 50);
|
|
27
|
+
}
|
|
28
|
+
export async function checkAccessibility(filePath) {
|
|
29
|
+
const issues = [];
|
|
30
|
+
try {
|
|
31
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
32
|
+
const lowerContent = content.toLowerCase();
|
|
33
|
+
const inputs = content.match(/<input[^>]*>/gi) || [];
|
|
34
|
+
for (const inp of inputs) {
|
|
35
|
+
if (!inp.toLowerCase().includes('type="hidden"')) {
|
|
36
|
+
if (!inp.toLowerCase().includes('aria-label') && !inp.toLowerCase().includes('id=')) {
|
|
37
|
+
issues.push("Input without label or aria-label");
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const buttons = content.match(/<button[^>]*>[^<]*<\/button>/gi) || [];
|
|
43
|
+
for (const btn of buttons) {
|
|
44
|
+
if (!btn.toLowerCase().includes('aria-label')) {
|
|
45
|
+
const text = btn.replace(/<[^>]+>/g, '').trim();
|
|
46
|
+
if (!text) {
|
|
47
|
+
issues.push("Button without accessible text");
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (lowerContent.includes('<html') && !lowerContent.includes('lang=')) {
|
|
53
|
+
issues.push("Missing lang attribute on <html>");
|
|
54
|
+
}
|
|
55
|
+
if (lowerContent.includes('<main') || lowerContent.includes('<body')) {
|
|
56
|
+
if (!lowerContent.includes('skip') && !lowerContent.includes('#main')) {
|
|
57
|
+
issues.push("Consider adding skip-to-main-content link");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const onClickCount = (lowerContent.match(/onclick=/g) || []).length;
|
|
61
|
+
const keyCount = (lowerContent.match(/onkeydown=|onkeyup=/g) || []).length;
|
|
62
|
+
if (onClickCount > 0 && keyCount === 0) {
|
|
63
|
+
issues.push("onClick without keyboard handler (onKeyDown)");
|
|
64
|
+
}
|
|
65
|
+
if (lowerContent.includes('tabindex=')) {
|
|
66
|
+
if (!lowerContent.includes('tabindex="-1"') && !lowerContent.includes('tabindex="0"')) {
|
|
67
|
+
const positive = lowerContent.match(/tabindex="([1-9]\d*)"/);
|
|
68
|
+
if (positive)
|
|
69
|
+
issues.push("Avoid positive tabIndex values");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (lowerContent.includes('autoplay') && !lowerContent.includes('muted')) {
|
|
73
|
+
issues.push("Autoplay media should be muted");
|
|
74
|
+
}
|
|
75
|
+
if (lowerContent.includes('role="button"')) {
|
|
76
|
+
const divButtons = content.match(/<div[^>]*role="button"[^>]*>/gi) || [];
|
|
77
|
+
for (const div of divButtons) {
|
|
78
|
+
if (!div.toLowerCase().includes('tabindex')) {
|
|
79
|
+
issues.push("role='button' without tabindex");
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
issues.push(`Error reading file: ${e.message.substring(0, 50)}`);
|
|
87
|
+
}
|
|
88
|
+
return issues;
|
|
89
|
+
}
|
|
90
|
+
export async function runAccessibilityChecker(projectPath = ".") {
|
|
91
|
+
const root = path.resolve(projectPath);
|
|
92
|
+
let report = `============================================================\n`;
|
|
93
|
+
report += `[ACCESSIBILITY CHECKER] WCAG Compliance Audit\n`;
|
|
94
|
+
report += `============================================================\n`;
|
|
95
|
+
report += `Project: ${root}\n------------------------------------------------------------\n`;
|
|
96
|
+
const files = await findHtmlFiles(root);
|
|
97
|
+
report += `Found ${files.length} HTML/JSX/TSX files\n`;
|
|
98
|
+
if (files.length === 0) {
|
|
99
|
+
return { passed: true, report: report + "No HTML files found\n" };
|
|
100
|
+
}
|
|
101
|
+
const allIssues = [];
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
const issues = await checkAccessibility(file);
|
|
104
|
+
if (issues.length > 0) {
|
|
105
|
+
allIssues.push({ file: path.basename(file), issues });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
report += `\n============================================================\nACCESSIBILITY ISSUES\n============================================================\n`;
|
|
109
|
+
if (allIssues.length > 0) {
|
|
110
|
+
for (const item of allIssues.slice(0, 10)) {
|
|
111
|
+
report += `\n${item.file}:\n`;
|
|
112
|
+
for (const issue of item.issues)
|
|
113
|
+
report += ` - ${issue}\n`;
|
|
114
|
+
}
|
|
115
|
+
if (allIssues.length > 10)
|
|
116
|
+
report += `\n... and ${allIssues.length - 10} more files with issues\n`;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
report += "No accessibility issues found!\n";
|
|
120
|
+
}
|
|
121
|
+
const totalIssues = allIssues.reduce((sum, item) => sum + item.issues.length, 0);
|
|
122
|
+
const passed = totalIssues < 5;
|
|
123
|
+
return { passed, report };
|
|
124
|
+
}
|