ios-app-review-plugin 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/.claude/settings.local.json +42 -0
- package/.github/actions/ios-review/action.yml +106 -0
- package/.github/workflows/ci.yml +103 -0
- package/.github/workflows/publish.yml +57 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +175 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/bitrise/step.sh +128 -0
- package/bitrise/step.yml +101 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzers/asc-iap.d.ts.map +1 -0
- package/dist/analyzers/asc-metadata.d.ts.map +1 -0
- package/dist/analyzers/asc-screenshots.d.ts.map +1 -0
- package/dist/analyzers/asc-version.d.ts.map +1 -0
- package/dist/analyzers/code-scanner.d.ts.map +1 -0
- package/dist/analyzers/deprecated-api.d.ts.map +1 -0
- package/dist/analyzers/entitlements.d.ts.map +1 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/info-plist.d.ts.map +1 -0
- package/dist/analyzers/privacy.d.ts.map +1 -0
- package/dist/analyzers/private-api.d.ts.map +1 -0
- package/dist/analyzers/security.d.ts.map +1 -0
- package/dist/analyzers/ui-ux.d.ts.map +1 -0
- package/dist/asc/auth.d.ts.map +1 -0
- package/dist/asc/client.d.ts.map +1 -0
- package/dist/asc/endpoints/apps.d.ts.map +1 -0
- package/dist/asc/endpoints/iap.d.ts.map +1 -0
- package/dist/asc/endpoints/screenshots.d.ts.map +1 -0
- package/dist/asc/endpoints/versions.d.ts.map +1 -0
- package/dist/asc/errors.d.ts.map +1 -0
- package/dist/asc/index.d.ts.map +1 -0
- package/dist/asc/types.d.ts.map +1 -0
- package/dist/badge/generator.d.ts.map +1 -0
- package/dist/badge/index.d.ts.map +1 -0
- package/dist/badge/types.d.ts.map +1 -0
- package/dist/cache/file-cache.d.ts.map +1 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cli/commands/help.d.ts.map +1 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/version.d.ts.map +1 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/git/diff.d.ts.map +1 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/types.d.ts.map +1 -0
- package/dist/guidelines/database.d.ts.map +1 -0
- package/dist/guidelines/index.d.ts.map +1 -0
- package/dist/guidelines/matcher.d.ts.map +1 -0
- package/dist/guidelines/types.d.ts.map +1 -0
- package/dist/history/comparator.d.ts.map +1 -0
- package/dist/history/index.d.ts.map +1 -0
- package/dist/history/store.d.ts.map +1 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +994 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/plist.d.ts.map +1 -0
- package/dist/parsers/xcodeproj.d.ts.map +1 -0
- package/dist/progress/index.d.ts.map +1 -0
- package/dist/progress/reporter.d.ts.map +1 -0
- package/dist/progress/types.d.ts.map +1 -0
- package/dist/reports/html.d.ts.map +1 -0
- package/dist/reports/index.d.ts.map +1 -0
- package/dist/reports/json.d.ts.map +1 -0
- package/dist/reports/markdown.d.ts.map +1 -0
- package/dist/reports/types.d.ts.map +1 -0
- package/dist/rules/engine.d.ts.map +1 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/loader.d.ts.map +1 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/types/index.d.ts.map +1 -0
- package/docs/ANALYZERS.md +237 -0
- package/docs/API.md +308 -0
- package/docs/BADGES.md +130 -0
- package/docs/CI_CD.md +283 -0
- package/docs/CLI.md +140 -0
- package/docs/REPORTS.md +212 -0
- package/docs/ROADMAP.md +267 -0
- package/docs/RULES.md +182 -0
- package/docs/SECURITY.md +89 -0
- package/docs/TROUBLESHOOTING.md +227 -0
- package/docs/tutorials/ASC_SETUP.md +188 -0
- package/docs/tutorials/CI_INTEGRATION.md +292 -0
- package/docs/tutorials/CUSTOM_RULES.md +291 -0
- package/docs/tutorials/GETTING_STARTED.md +226 -0
- package/docs/video-scripts/01-introduction.md +106 -0
- package/docs/video-scripts/02-cli-usage.md +120 -0
- package/docs/video-scripts/03-ci-integration.md +198 -0
- package/eslint.config.js +33 -0
- package/examples/.ios-review-rules.json +82 -0
- package/examples/bitrise-workflow.yml +129 -0
- package/examples/fastlane-lane.rb +71 -0
- package/examples/github-action.yml +147 -0
- package/fastlane/Fastfile.example +114 -0
- package/fastlane/README.md +99 -0
- package/jest.config.js +36 -0
- package/package.json +65 -0
- package/scripts/benchmark.ts +112 -0
- package/scripts/debug-parser.ts +37 -0
- package/scripts/debug-pbxproj.ts +36 -0
- package/scripts/debug-specific.ts +47 -0
- package/scripts/test-analyze.ts +67 -0
- package/scripts/xcode-cloud-review.sh +167 -0
- package/src/analyzer.ts +227 -0
- package/src/analyzers/asc-iap.ts +300 -0
- package/src/analyzers/asc-metadata.ts +326 -0
- package/src/analyzers/asc-screenshots.ts +310 -0
- package/src/analyzers/asc-version.ts +368 -0
- package/src/analyzers/code-scanner.ts +408 -0
- package/src/analyzers/deprecated-api.ts +390 -0
- package/src/analyzers/entitlements.ts +345 -0
- package/src/analyzers/index.ts +12 -0
- package/src/analyzers/info-plist.ts +409 -0
- package/src/analyzers/privacy.ts +376 -0
- package/src/analyzers/private-api.ts +377 -0
- package/src/analyzers/security.ts +327 -0
- package/src/analyzers/ui-ux.ts +509 -0
- package/src/asc/auth.ts +204 -0
- package/src/asc/client.ts +258 -0
- package/src/asc/endpoints/apps.ts +115 -0
- package/src/asc/endpoints/iap.ts +171 -0
- package/src/asc/endpoints/screenshots.ts +164 -0
- package/src/asc/endpoints/versions.ts +174 -0
- package/src/asc/errors.ts +109 -0
- package/src/asc/index.ts +108 -0
- package/src/asc/types.ts +369 -0
- package/src/badge/generator.ts +48 -0
- package/src/badge/index.ts +2 -0
- package/src/badge/types.ts +5 -0
- package/src/cache/file-cache.ts +75 -0
- package/src/cache/index.ts +2 -0
- package/src/cache/types.ts +10 -0
- package/src/cli/commands/help.ts +41 -0
- package/src/cli/commands/scan.ts +44 -0
- package/src/cli/commands/version.ts +12 -0
- package/src/cli/index.ts +92 -0
- package/src/cli/types.ts +17 -0
- package/src/git/diff.ts +21 -0
- package/src/git/index.ts +2 -0
- package/src/git/types.ts +5 -0
- package/src/guidelines/database.ts +344 -0
- package/src/guidelines/index.ts +4 -0
- package/src/guidelines/matcher.ts +84 -0
- package/src/guidelines/types.ts +28 -0
- package/src/history/comparator.ts +114 -0
- package/src/history/index.ts +3 -0
- package/src/history/store.ts +135 -0
- package/src/history/types.ts +40 -0
- package/src/index.ts +1113 -0
- package/src/parsers/index.ts +3 -0
- package/src/parsers/plist.ts +253 -0
- package/src/parsers/xcodeproj.ts +265 -0
- package/src/progress/index.ts +2 -0
- package/src/progress/reporter.ts +65 -0
- package/src/progress/types.ts +9 -0
- package/src/reports/html.ts +322 -0
- package/src/reports/index.ts +20 -0
- package/src/reports/json.ts +92 -0
- package/src/reports/markdown.ts +187 -0
- package/src/reports/types.ts +26 -0
- package/src/rules/engine.ts +121 -0
- package/src/rules/index.ts +3 -0
- package/src/rules/loader.ts +83 -0
- package/src/rules/types.ts +25 -0
- package/src/types/index.ts +247 -0
- package/tests/analyzer.test.ts +142 -0
- package/tests/analyzers/asc-iap.test.ts +228 -0
- package/tests/analyzers/asc-metadata.test.ts +210 -0
- package/tests/analyzers/asc-screenshots.test.ts +135 -0
- package/tests/analyzers/asc-version.test.ts +259 -0
- package/tests/analyzers/code-scanner.test.ts +745 -0
- package/tests/analyzers/deprecated-api.test.ts +286 -0
- package/tests/analyzers/entitlements.test.ts +411 -0
- package/tests/analyzers/info-plist.test.ts +148 -0
- package/tests/analyzers/privacy.test.ts +623 -0
- package/tests/analyzers/private-api.test.ts +255 -0
- package/tests/analyzers/security.test.ts +300 -0
- package/tests/analyzers/ui-ux.test.ts +357 -0
- package/tests/asc/auth.test.ts +189 -0
- package/tests/asc/client.test.ts +207 -0
- package/tests/asc/endpoints.test.ts +1359 -0
- package/tests/badge/generator.test.ts +73 -0
- package/tests/cache/file-cache.test.ts +124 -0
- package/tests/cli/cli-index.test.ts +510 -0
- package/tests/cli/commands.test.ts +67 -0
- package/tests/cli/scan.test.ts +152 -0
- package/tests/git/diff.test.ts +69 -0
- package/tests/guidelines/matcher.test.ts +209 -0
- package/tests/history/comparator.test.ts +272 -0
- package/tests/history/store.test.ts +200 -0
- package/tests/integration/cli.test.ts +95 -0
- package/tests/integration/e2e.test.ts +130 -0
- package/tests/parsers/plist.test.ts +240 -0
- package/tests/parsers/xcodeproj.test.ts +289 -0
- package/tests/progress/reporter.test.ts +117 -0
- package/tests/reports/html.test.ts +176 -0
- package/tests/reports/json.test.ts +235 -0
- package/tests/reports/markdown.test.ts +196 -0
- package/tests/rules/engine.test.ts +229 -0
- package/tests/rules/loader.test.ts +187 -0
- package/tests/setup.ts +15 -0
- package/tsconfig.json +27 -0
- package/tsconfig.test.json +9 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { parsePlist, fileExists, readFile, parsePbxproj } from '../../src/parsers/plist.js';
|
|
5
|
+
|
|
6
|
+
describe('plist parser', () => {
|
|
7
|
+
let tempDir: string;
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'plist-test-'));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterAll(async () => {
|
|
14
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('parsePlist', () => {
|
|
18
|
+
it('should parse a valid XML plist file', async () => {
|
|
19
|
+
const plistPath = path.join(tempDir, 'valid.plist');
|
|
20
|
+
await fs.writeFile(
|
|
21
|
+
plistPath,
|
|
22
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
23
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
24
|
+
<plist version="1.0">
|
|
25
|
+
<dict>
|
|
26
|
+
<key>CFBundleIdentifier</key>
|
|
27
|
+
<string>com.test.app</string>
|
|
28
|
+
<key>CFBundleName</key>
|
|
29
|
+
<string>TestApp</string>
|
|
30
|
+
<key>CFBundleVersion</key>
|
|
31
|
+
<string>1</string>
|
|
32
|
+
</dict>
|
|
33
|
+
</plist>`
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const result = await parsePlist<Record<string, unknown>>(plistPath);
|
|
37
|
+
|
|
38
|
+
expect(result['CFBundleIdentifier']).toBe('com.test.app');
|
|
39
|
+
expect(result['CFBundleName']).toBe('TestApp');
|
|
40
|
+
expect(result['CFBundleVersion']).toBe('1');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should throw for an invalid plist file', async () => {
|
|
44
|
+
const plistPath = path.join(tempDir, 'invalid.plist');
|
|
45
|
+
await fs.writeFile(plistPath, 'this is not valid plist content at all <><>');
|
|
46
|
+
|
|
47
|
+
await expect(parsePlist(plistPath)).rejects.toThrow();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('fileExists', () => {
|
|
52
|
+
it('should return true for an existing file', async () => {
|
|
53
|
+
const filePath = path.join(tempDir, 'exists.txt');
|
|
54
|
+
await fs.writeFile(filePath, 'hello');
|
|
55
|
+
|
|
56
|
+
const result = await fileExists(filePath);
|
|
57
|
+
expect(result).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should return false for a non-existing file', async () => {
|
|
61
|
+
const filePath = path.join(tempDir, 'does-not-exist.txt');
|
|
62
|
+
|
|
63
|
+
const result = await fileExists(filePath);
|
|
64
|
+
expect(result).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('readFile', () => {
|
|
69
|
+
it('should read file contents as a string', async () => {
|
|
70
|
+
const filePath = path.join(tempDir, 'readable.txt');
|
|
71
|
+
await fs.writeFile(filePath, 'file content here');
|
|
72
|
+
|
|
73
|
+
const result = await readFile(filePath);
|
|
74
|
+
expect(result).toBe('file content here');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('parsePbxproj', () => {
|
|
79
|
+
it('should extract archiveVersion, objectVersion, and rootObject', () => {
|
|
80
|
+
const content = `// !$*UTF8*$!
|
|
81
|
+
{
|
|
82
|
+
archiveVersion = 1;
|
|
83
|
+
classes = {};
|
|
84
|
+
objectVersion = 56;
|
|
85
|
+
objects = {
|
|
86
|
+
AAAAAAAAAAAAAAAAAAAAAAAA = { isa = PBXProject; mainGroup = BBBBBBBBBBBBBBBBBBBBBBBB; targets = (); };
|
|
87
|
+
BBBBBBBBBBBBBBBBBBBBBBBB = { isa = PBXGroup; children = (); sourceTree = "<group>"; };
|
|
88
|
+
};
|
|
89
|
+
rootObject = AAAAAAAAAAAAAAAAAAAAAAAA;
|
|
90
|
+
}
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
const result = parsePbxproj(content);
|
|
94
|
+
|
|
95
|
+
expect(result.archiveVersion).toBe('1');
|
|
96
|
+
expect(result.objectVersion).toBe('56');
|
|
97
|
+
expect(result.rootObject).toBe('AAAAAAAAAAAAAAAAAAAAAAAA');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should parse objects with various isa types', () => {
|
|
101
|
+
const content = `// !$*UTF8*$!
|
|
102
|
+
{
|
|
103
|
+
archiveVersion = 1;
|
|
104
|
+
objectVersion = 56;
|
|
105
|
+
objects = {
|
|
106
|
+
AAAAAAAAAAAAAAAAAAAAAAAA = { isa = PBXProject; mainGroup = CCCCCCCCCCCCCCCCCCCCCCCC; targets = (BBBBBBBBBBBBBBBBBBBBBBBB); };
|
|
107
|
+
BBBBBBBBBBBBBBBBBBBBBBBB = { isa = PBXNativeTarget; name = MyApp; productType = "com.apple.product-type.application"; buildPhases = (); };
|
|
108
|
+
CCCCCCCCCCCCCCCCCCCCCCCC = { isa = PBXGroup; children = (); sourceTree = "<group>"; };
|
|
109
|
+
DDDDDDDDDDDDDDDDDDDDDDDD = { isa = PBXFileReference; path = "AppDelegate.swift"; sourceTree = "<group>"; };
|
|
110
|
+
EEEEEEEEEEEEEEEEEEEEEEEE = { isa = XCBuildConfiguration; name = Debug; buildSettings = { PRODUCT_BUNDLE_IDENTIFIER = "com.test.myapp"; }; };
|
|
111
|
+
};
|
|
112
|
+
rootObject = AAAAAAAAAAAAAAAAAAAAAAAA;
|
|
113
|
+
}
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
const result = parsePbxproj(content);
|
|
117
|
+
|
|
118
|
+
expect(result.objects['AAAAAAAAAAAAAAAAAAAAAAAA']?.isa).toBe('PBXProject');
|
|
119
|
+
expect(result.objects['BBBBBBBBBBBBBBBBBBBBBBBB']?.isa).toBe('PBXNativeTarget');
|
|
120
|
+
expect(result.objects['BBBBBBBBBBBBBBBBBBBBBBBB']?.name).toBe('MyApp');
|
|
121
|
+
expect(result.objects['BBBBBBBBBBBBBBBBBBBBBBBB']?.productType).toBe(
|
|
122
|
+
'com.apple.product-type.application'
|
|
123
|
+
);
|
|
124
|
+
expect(result.objects['CCCCCCCCCCCCCCCCCCCCCCCC']?.isa).toBe('PBXGroup');
|
|
125
|
+
expect(result.objects['DDDDDDDDDDDDDDDDDDDDDDDD']?.isa).toBe('PBXFileReference');
|
|
126
|
+
expect(result.objects['DDDDDDDDDDDDDDDDDDDDDDDD']?.path).toBe('AppDelegate.swift');
|
|
127
|
+
expect(result.objects['EEEEEEEEEEEEEEEEEEEEEEEE']?.isa).toBe('XCBuildConfiguration');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should parse buildSettings correctly', () => {
|
|
131
|
+
const content = `// !$*UTF8*$!
|
|
132
|
+
{
|
|
133
|
+
archiveVersion = 1;
|
|
134
|
+
objectVersion = 56;
|
|
135
|
+
objects = {
|
|
136
|
+
AAAAAAAAAAAAAAAAAAAAAAAA = { isa = XCBuildConfiguration; name = Debug; buildSettings = { PRODUCT_BUNDLE_IDENTIFIER = "com.test.app"; IPHONEOS_DEPLOYMENT_TARGET = "16.0"; INFOPLIST_FILE = "App/Info.plist"; CODE_SIGN_ENTITLEMENTS = "App/App.entitlements"; SWIFT_VERSION = "5.0"; }; };
|
|
137
|
+
BBBBBBBBBBBBBBBBBBBBBBBB = { isa = PBXProject; mainGroup = CCCCCCCCCCCCCCCCCCCCCCCC; targets = (); };
|
|
138
|
+
CCCCCCCCCCCCCCCCCCCCCCCC = { isa = PBXGroup; children = (); sourceTree = "<group>"; };
|
|
139
|
+
};
|
|
140
|
+
rootObject = BBBBBBBBBBBBBBBBBBBBBBBB;
|
|
141
|
+
}
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
const result = parsePbxproj(content);
|
|
145
|
+
const config = result.objects['AAAAAAAAAAAAAAAAAAAAAAAA'];
|
|
146
|
+
|
|
147
|
+
expect(config?.buildSettings).toBeDefined();
|
|
148
|
+
expect(config!.buildSettings!['PRODUCT_BUNDLE_IDENTIFIER']).toBe('com.test.app');
|
|
149
|
+
expect(config!.buildSettings!['IPHONEOS_DEPLOYMENT_TARGET']).toBe('16.0');
|
|
150
|
+
expect(config!.buildSettings!['INFOPLIST_FILE']).toBe('App/Info.plist');
|
|
151
|
+
expect(config!.buildSettings!['CODE_SIGN_ENTITLEMENTS']).toBe('App/App.entitlements');
|
|
152
|
+
expect(config!.buildSettings!['SWIFT_VERSION']).toBe('5.0');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should parse arrays (files, buildPhases, buildConfigurations)', () => {
|
|
156
|
+
const content = `// !$*UTF8*$!
|
|
157
|
+
{
|
|
158
|
+
archiveVersion = 1;
|
|
159
|
+
objectVersion = 56;
|
|
160
|
+
objects = {
|
|
161
|
+
AAAAAAAAAAAAAAAAAAAAAAAA = { isa = PBXNativeTarget; name = TestApp; buildPhases = (BBBBBBBBBBBBBBBBBBBBBBBB, CCCCCCCCCCCCCCCCCCCCCCCC); buildConfigurationList = DDDDDDDDDDDDDDDDDDDDDDDD; };
|
|
162
|
+
BBBBBBBBBBBBBBBBBBBBBBBB = { isa = PBXSourcesBuildPhase; files = (EEEEEEEEEEEEEEEEEEEEEEEE, FFFFFFFFFFFFFFFFFFFFFFFF); };
|
|
163
|
+
CCCCCCCCCCCCCCCCCCCCCCCC = { isa = PBXFrameworksBuildPhase; files = (); };
|
|
164
|
+
DDDDDDDDDDDDDDDDDDDDDDDD = { isa = XCConfigurationList; buildConfigurations = (111111111111111111111111, 222222222222222222222222); };
|
|
165
|
+
EEEEEEEEEEEEEEEEEEEEEEEE = { isa = PBXBuildFile; };
|
|
166
|
+
FFFFFFFFFFFFFFFFFFFFFFFF = { isa = PBXBuildFile; };
|
|
167
|
+
111111111111111111111111 = { isa = XCBuildConfiguration; name = Debug; buildSettings = { }; };
|
|
168
|
+
222222222222222222222222 = { isa = XCBuildConfiguration; name = Release; buildSettings = { }; };
|
|
169
|
+
333333333333333333333333 = { isa = PBXProject; mainGroup = 444444444444444444444444; targets = (AAAAAAAAAAAAAAAAAAAAAAAA); };
|
|
170
|
+
444444444444444444444444 = { isa = PBXGroup; children = (); sourceTree = "<group>"; };
|
|
171
|
+
};
|
|
172
|
+
rootObject = 333333333333333333333333;
|
|
173
|
+
}
|
|
174
|
+
`;
|
|
175
|
+
|
|
176
|
+
const result = parsePbxproj(content);
|
|
177
|
+
|
|
178
|
+
// Check buildPhases array
|
|
179
|
+
const target = result.objects['AAAAAAAAAAAAAAAAAAAAAAAA'];
|
|
180
|
+
expect(target?.buildPhases).toEqual([
|
|
181
|
+
'BBBBBBBBBBBBBBBBBBBBBBBB',
|
|
182
|
+
'CCCCCCCCCCCCCCCCCCCCCCCC',
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
// Check files array
|
|
186
|
+
const sourcesPhase = result.objects['BBBBBBBBBBBBBBBBBBBBBBBB'];
|
|
187
|
+
expect(sourcesPhase?.files).toEqual([
|
|
188
|
+
'EEEEEEEEEEEEEEEEEEEEEEEE',
|
|
189
|
+
'FFFFFFFFFFFFFFFFFFFFFFFF',
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
// Check buildConfigurations array
|
|
193
|
+
const configList = result.objects['DDDDDDDDDDDDDDDDDDDDDDDD'];
|
|
194
|
+
expect(configList?.['buildConfigurations']).toEqual([
|
|
195
|
+
'111111111111111111111111',
|
|
196
|
+
'222222222222222222222222',
|
|
197
|
+
]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should handle content with comments (/* ... */)', () => {
|
|
201
|
+
const content = `// !$*UTF8*$!
|
|
202
|
+
{
|
|
203
|
+
archiveVersion = 1;
|
|
204
|
+
objectVersion = 56;
|
|
205
|
+
objects = {
|
|
206
|
+
AAAAAAAAAAAAAAAAAAAAAAAA /* MyApp */ = { isa = PBXNativeTarget; name = MyApp; productType = "com.apple.product-type.application"; buildPhases = (); };
|
|
207
|
+
BBBBBBBBBBBBBBBBBBBBBBBB /* Project object */ = { isa = PBXProject; mainGroup = CCCCCCCCCCCCCCCCCCCCCCCC; targets = (AAAAAAAAAAAAAAAAAAAAAAAA /* MyApp */); };
|
|
208
|
+
CCCCCCCCCCCCCCCCCCCCCCCC = { isa = PBXGroup; children = (); sourceTree = "<group>"; };
|
|
209
|
+
};
|
|
210
|
+
rootObject = BBBBBBBBBBBBBBBBBBBBBBBB /* Project object */;
|
|
211
|
+
}
|
|
212
|
+
`;
|
|
213
|
+
|
|
214
|
+
const result = parsePbxproj(content);
|
|
215
|
+
|
|
216
|
+
expect(result.rootObject).toBe('BBBBBBBBBBBBBBBBBBBBBBBB');
|
|
217
|
+
expect(result.objects['AAAAAAAAAAAAAAAAAAAAAAAA']?.isa).toBe('PBXNativeTarget');
|
|
218
|
+
expect(result.objects['AAAAAAAAAAAAAAAAAAAAAAAA']?.name).toBe('MyApp');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should handle empty objects section', () => {
|
|
222
|
+
const content = `// !$*UTF8*$!
|
|
223
|
+
{
|
|
224
|
+
archiveVersion = 1;
|
|
225
|
+
objectVersion = 56;
|
|
226
|
+
objects = {
|
|
227
|
+
};
|
|
228
|
+
rootObject = AAAAAAAAAAAAAAAAAAAAAAAA;
|
|
229
|
+
}
|
|
230
|
+
`;
|
|
231
|
+
|
|
232
|
+
const result = parsePbxproj(content);
|
|
233
|
+
|
|
234
|
+
expect(result.archiveVersion).toBe('1');
|
|
235
|
+
expect(result.objectVersion).toBe('56');
|
|
236
|
+
expect(result.rootObject).toBe('AAAAAAAAAAAAAAAAAAAAAAAA');
|
|
237
|
+
expect(Object.keys(result.objects)).toHaveLength(0);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { parseXcodeProject } from '../../src/parsers/xcodeproj.js';
|
|
5
|
+
|
|
6
|
+
describe('parseXcodeProject', () => {
|
|
7
|
+
let tempDir: string;
|
|
8
|
+
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'xcodeproj-test-'));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterAll(async () => {
|
|
14
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const VALID_PBXPROJ = `// !$*UTF8*$!
|
|
18
|
+
{
|
|
19
|
+
archiveVersion = 1;
|
|
20
|
+
classes = {};
|
|
21
|
+
objectVersion = 56;
|
|
22
|
+
objects = {
|
|
23
|
+
AAAAAAAAAAAAAAAAAAAAAAAA = { isa = PBXProject; buildConfigurationList = BBBBBBBBBBBBBBBBBBBBBBBB; mainGroup = CCCCCCCCCCCCCCCCCCCCCCCC; targets = (DDDDDDDDDDDDDDDDDDDDDDDD); };
|
|
24
|
+
BBBBBBBBBBBBBBBBBBBBBBBB = { isa = XCConfigurationList; buildConfigurations = (EEEEEEEEEEEEEEEEEEEEEEEE, FFFFFFFFFFFFFFFFFFFFFFFF); };
|
|
25
|
+
EEEEEEEEEEEEEEEEEEEEEEEE = { isa = XCBuildConfiguration; name = Debug; buildSettings = { PRODUCT_BUNDLE_IDENTIFIER = "com.test.app"; IPHONEOS_DEPLOYMENT_TARGET = "16.0"; INFOPLIST_FILE = "TestApp/Info.plist"; CODE_SIGN_ENTITLEMENTS = "TestApp/TestApp.entitlements"; }; };
|
|
26
|
+
FFFFFFFFFFFFFFFFFFFFFFFF = { isa = XCBuildConfiguration; name = Release; buildSettings = { PRODUCT_BUNDLE_IDENTIFIER = "com.test.app"; IPHONEOS_DEPLOYMENT_TARGET = "16.0"; }; };
|
|
27
|
+
DDDDDDDDDDDDDDDDDDDDDDDD = { isa = PBXNativeTarget; name = TestApp; productType = "com.apple.product-type.application"; buildConfigurationList = BBBBBBBBBBBBBBBBBBBBBBBB; buildPhases = (111111111111111111111111); };
|
|
28
|
+
CCCCCCCCCCCCCCCCCCCCCCCC = { isa = PBXGroup; children = (); sourceTree = "<group>"; };
|
|
29
|
+
111111111111111111111111 = { isa = PBXSourcesBuildPhase; files = (222222222222222222222222); };
|
|
30
|
+
222222222222222222222222 = { isa = PBXBuildFile; fileRef = 333333333333333333333333; };
|
|
31
|
+
333333333333333333333333 = { isa = PBXFileReference; path = "ViewController.swift"; sourceTree = "<group>"; };
|
|
32
|
+
};
|
|
33
|
+
rootObject = AAAAAAAAAAAAAAAAAAAAAAAA;
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
describe('valid .xcodeproj', () => {
|
|
38
|
+
it('should parse a valid .xcodeproj project', async () => {
|
|
39
|
+
const xcodeproj = path.join(tempDir, 'TestApp.xcodeproj');
|
|
40
|
+
await fs.mkdir(xcodeproj, { recursive: true });
|
|
41
|
+
await fs.writeFile(path.join(xcodeproj, 'project.pbxproj'), VALID_PBXPROJ);
|
|
42
|
+
|
|
43
|
+
const project = await parseXcodeProject(xcodeproj);
|
|
44
|
+
|
|
45
|
+
expect(project.name).toBe('TestApp');
|
|
46
|
+
expect(project.path).toBe(xcodeproj);
|
|
47
|
+
expect(project.targets.length).toBeGreaterThan(0);
|
|
48
|
+
expect(project.configurations.length).toBeGreaterThan(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should extract targets with name, productType, bundleIdentifier, and deploymentTarget', async () => {
|
|
52
|
+
const xcodeproj = path.join(tempDir, 'TargetApp.xcodeproj');
|
|
53
|
+
await fs.mkdir(xcodeproj, { recursive: true });
|
|
54
|
+
await fs.writeFile(path.join(xcodeproj, 'project.pbxproj'), VALID_PBXPROJ);
|
|
55
|
+
|
|
56
|
+
const project = await parseXcodeProject(xcodeproj);
|
|
57
|
+
|
|
58
|
+
const target = project.targets.find((t) => t.name === 'TestApp');
|
|
59
|
+
expect(target).toBeDefined();
|
|
60
|
+
expect(target!.name).toBe('TestApp');
|
|
61
|
+
expect(target!.type).toBe('application');
|
|
62
|
+
expect(target!.bundleIdentifier).toBe('com.test.app');
|
|
63
|
+
expect(target!.deploymentTarget).toBe('16.0');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should extract build configurations', async () => {
|
|
67
|
+
const xcodeproj = path.join(tempDir, 'ConfigApp.xcodeproj');
|
|
68
|
+
await fs.mkdir(xcodeproj, { recursive: true });
|
|
69
|
+
await fs.writeFile(path.join(xcodeproj, 'project.pbxproj'), VALID_PBXPROJ);
|
|
70
|
+
|
|
71
|
+
const project = await parseXcodeProject(xcodeproj);
|
|
72
|
+
|
|
73
|
+
expect(project.configurations).toContain('Debug');
|
|
74
|
+
expect(project.configurations).toContain('Release');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('invalid project path', () => {
|
|
79
|
+
it('should throw for a path that is not .xcodeproj or .xcworkspace', async () => {
|
|
80
|
+
const invalidDir = path.join(tempDir, 'NotAProject');
|
|
81
|
+
await fs.mkdir(invalidDir, { recursive: true });
|
|
82
|
+
|
|
83
|
+
await expect(parseXcodeProject(invalidDir)).rejects.toThrow(
|
|
84
|
+
'Invalid project path'
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should throw if project.pbxproj is missing', async () => {
|
|
89
|
+
const xcodeproj = path.join(tempDir, 'EmptyProj.xcodeproj');
|
|
90
|
+
await fs.mkdir(xcodeproj, { recursive: true });
|
|
91
|
+
|
|
92
|
+
await expect(parseXcodeProject(xcodeproj)).rejects.toThrow(
|
|
93
|
+
'project.pbxproj not found'
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('.xcworkspace parsing', () => {
|
|
99
|
+
it('should parse a .xcworkspace that references a .xcodeproj', async () => {
|
|
100
|
+
// Create the workspace directory
|
|
101
|
+
const workspace = path.join(tempDir, 'WorkspaceApp.xcworkspace');
|
|
102
|
+
await fs.mkdir(workspace, { recursive: true });
|
|
103
|
+
|
|
104
|
+
// Create the referenced xcodeproj inside tempDir
|
|
105
|
+
const xcodeproj = path.join(tempDir, 'WorkspaceApp.xcodeproj');
|
|
106
|
+
await fs.mkdir(xcodeproj, { recursive: true });
|
|
107
|
+
await fs.writeFile(path.join(xcodeproj, 'project.pbxproj'), VALID_PBXPROJ);
|
|
108
|
+
|
|
109
|
+
// Write workspace data referencing the xcodeproj
|
|
110
|
+
await fs.writeFile(
|
|
111
|
+
path.join(workspace, 'contents.xcworkspacedata'),
|
|
112
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
113
|
+
<Workspace version="1.0">
|
|
114
|
+
<FileRef location="group:WorkspaceApp.xcodeproj"></FileRef>
|
|
115
|
+
</Workspace>`
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const project = await parseXcodeProject(workspace);
|
|
119
|
+
|
|
120
|
+
expect(project.name).toBe('WorkspaceApp');
|
|
121
|
+
expect(project.path).toBe(workspace);
|
|
122
|
+
expect(project.targets.length).toBeGreaterThan(0);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should throw for an empty .xcworkspace with no project refs', async () => {
|
|
126
|
+
const workspace = path.join(tempDir, 'EmptyWorkspace.xcworkspace');
|
|
127
|
+
await fs.mkdir(workspace, { recursive: true });
|
|
128
|
+
|
|
129
|
+
await fs.writeFile(
|
|
130
|
+
path.join(workspace, 'contents.xcworkspacedata'),
|
|
131
|
+
`<?xml version="1.0" encoding="UTF-8"?>
|
|
132
|
+
<Workspace version="1.0">
|
|
133
|
+
</Workspace>`
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await expect(parseXcodeProject(workspace)).rejects.toThrow(
|
|
137
|
+
'No projects found in workspace'
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should throw if contents.xcworkspacedata is missing', async () => {
|
|
142
|
+
const workspace = path.join(tempDir, 'NoContents.xcworkspace');
|
|
143
|
+
await fs.mkdir(workspace, { recursive: true });
|
|
144
|
+
|
|
145
|
+
await expect(parseXcodeProject(workspace)).rejects.toThrow(
|
|
146
|
+
'contents.xcworkspacedata not found'
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('source files in build phases', () => {
|
|
152
|
+
it('should extract source files from PBXSourcesBuildPhase referencing PBXFileReference', async () => {
|
|
153
|
+
// The pbxproj parser extracts fileRef by matching the pattern inside PBXBuildFile objects.
|
|
154
|
+
// We need to use a multi-line format where fileRef appears as a parseable key with a 24-char hex ID.
|
|
155
|
+
const pbxprojWithSourceFiles = `// !$*UTF8*$!
|
|
156
|
+
{
|
|
157
|
+
archiveVersion = 1;
|
|
158
|
+
classes = {};
|
|
159
|
+
objectVersion = 56;
|
|
160
|
+
objects = {
|
|
161
|
+
AAAAAAAAAAAAAAAAAAAAAAAA = {
|
|
162
|
+
isa = PBXProject;
|
|
163
|
+
buildConfigurationList = BBBBBBBBBBBBBBBBBBBBBBBB;
|
|
164
|
+
mainGroup = CCCCCCCCCCCCCCCCCCCCCCCC;
|
|
165
|
+
targets = (DDDDDDDDDDDDDDDDDDDDDDDD);
|
|
166
|
+
};
|
|
167
|
+
BBBBBBBBBBBBBBBBBBBBBBBB = {
|
|
168
|
+
isa = XCConfigurationList;
|
|
169
|
+
buildConfigurations = (EEEEEEEEEEEEEEEEEEEEEEEE);
|
|
170
|
+
};
|
|
171
|
+
EEEEEEEEEEEEEEEEEEEEEEEE = {
|
|
172
|
+
isa = XCBuildConfiguration;
|
|
173
|
+
name = Debug;
|
|
174
|
+
buildSettings = {
|
|
175
|
+
PRODUCT_BUNDLE_IDENTIFIER = "com.test.app";
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
DDDDDDDDDDDDDDDDDDDDDDDD = {
|
|
179
|
+
isa = PBXNativeTarget;
|
|
180
|
+
name = TestApp;
|
|
181
|
+
productType = "com.apple.product-type.application";
|
|
182
|
+
buildConfigurationList = BBBBBBBBBBBBBBBBBBBBBBBB;
|
|
183
|
+
buildPhases = (111111111111111111111111);
|
|
184
|
+
};
|
|
185
|
+
CCCCCCCCCCCCCCCCCCCCCCCC = {
|
|
186
|
+
isa = PBXGroup;
|
|
187
|
+
children = ();
|
|
188
|
+
sourceTree = "<group>";
|
|
189
|
+
};
|
|
190
|
+
111111111111111111111111 = {
|
|
191
|
+
isa = PBXSourcesBuildPhase;
|
|
192
|
+
files = (222222222222222222222222);
|
|
193
|
+
};
|
|
194
|
+
222222222222222222222222 = {
|
|
195
|
+
isa = PBXBuildFile;
|
|
196
|
+
fileRef = 333333333333333333333333;
|
|
197
|
+
};
|
|
198
|
+
333333333333333333333333 = {
|
|
199
|
+
isa = PBXFileReference;
|
|
200
|
+
path = "ViewController.swift";
|
|
201
|
+
sourceTree = "<group>";
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
rootObject = AAAAAAAAAAAAAAAAAAAAAAAA;
|
|
205
|
+
}
|
|
206
|
+
`;
|
|
207
|
+
const xcodeproj = path.join(tempDir, 'SourceFilesApp.xcodeproj');
|
|
208
|
+
await fs.mkdir(xcodeproj, { recursive: true });
|
|
209
|
+
await fs.writeFile(path.join(xcodeproj, 'project.pbxproj'), pbxprojWithSourceFiles);
|
|
210
|
+
|
|
211
|
+
const project = await parseXcodeProject(xcodeproj);
|
|
212
|
+
|
|
213
|
+
const target = project.targets.find((t) => t.name === 'TestApp');
|
|
214
|
+
expect(target).toBeDefined();
|
|
215
|
+
// sourceFiles is populated when the parser can resolve fileRef through PBXBuildFile objects
|
|
216
|
+
expect(target!.sourceFiles).toBeDefined();
|
|
217
|
+
expect(Array.isArray(target!.sourceFiles)).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
describe('entitlements path in build settings', () => {
|
|
222
|
+
it('should extract entitlementsPath from CODE_SIGN_ENTITLEMENTS', async () => {
|
|
223
|
+
const xcodeproj = path.join(tempDir, 'EntApp.xcodeproj');
|
|
224
|
+
await fs.mkdir(xcodeproj, { recursive: true });
|
|
225
|
+
await fs.writeFile(path.join(xcodeproj, 'project.pbxproj'), VALID_PBXPROJ);
|
|
226
|
+
|
|
227
|
+
const project = await parseXcodeProject(xcodeproj);
|
|
228
|
+
|
|
229
|
+
const target = project.targets.find((t) => t.name === 'TestApp');
|
|
230
|
+
expect(target).toBeDefined();
|
|
231
|
+
expect(target!.entitlementsPath).toBeDefined();
|
|
232
|
+
expect(target!.entitlementsPath).toContain('TestApp.entitlements');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('product type mapping', () => {
|
|
237
|
+
it('should map com.apple.product-type.application to application', async () => {
|
|
238
|
+
const xcodeproj = path.join(tempDir, 'AppTypeApp.xcodeproj');
|
|
239
|
+
await fs.mkdir(xcodeproj, { recursive: true });
|
|
240
|
+
await fs.writeFile(path.join(xcodeproj, 'project.pbxproj'), VALID_PBXPROJ);
|
|
241
|
+
|
|
242
|
+
const project = await parseXcodeProject(xcodeproj);
|
|
243
|
+
const target = project.targets.find((t) => t.name === 'TestApp');
|
|
244
|
+
expect(target!.type).toBe('application');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should map com.apple.product-type.framework to framework', async () => {
|
|
248
|
+
const pbxproj = VALID_PBXPROJ.replace(
|
|
249
|
+
'com.apple.product-type.application',
|
|
250
|
+
'com.apple.product-type.framework'
|
|
251
|
+
);
|
|
252
|
+
const xcodeproj = path.join(tempDir, 'FrameworkApp.xcodeproj');
|
|
253
|
+
await fs.mkdir(xcodeproj, { recursive: true });
|
|
254
|
+
await fs.writeFile(path.join(xcodeproj, 'project.pbxproj'), pbxproj);
|
|
255
|
+
|
|
256
|
+
const project = await parseXcodeProject(xcodeproj);
|
|
257
|
+
const target = project.targets.find((t) => t.name === 'TestApp');
|
|
258
|
+
expect(target!.type).toBe('framework');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should map com.apple.product-type.library.static to staticLibrary', async () => {
|
|
262
|
+
const pbxproj = VALID_PBXPROJ.replace(
|
|
263
|
+
'com.apple.product-type.application',
|
|
264
|
+
'com.apple.product-type.library.static'
|
|
265
|
+
);
|
|
266
|
+
const xcodeproj = path.join(tempDir, 'StaticLibApp.xcodeproj');
|
|
267
|
+
await fs.mkdir(xcodeproj, { recursive: true });
|
|
268
|
+
await fs.writeFile(path.join(xcodeproj, 'project.pbxproj'), pbxproj);
|
|
269
|
+
|
|
270
|
+
const project = await parseXcodeProject(xcodeproj);
|
|
271
|
+
const target = project.targets.find((t) => t.name === 'TestApp');
|
|
272
|
+
expect(target!.type).toBe('staticLibrary');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should map unknown product types to unknown', async () => {
|
|
276
|
+
const pbxproj = VALID_PBXPROJ.replace(
|
|
277
|
+
'com.apple.product-type.application',
|
|
278
|
+
'com.apple.product-type.something-new'
|
|
279
|
+
);
|
|
280
|
+
const xcodeproj = path.join(tempDir, 'UnknownTypeApp.xcodeproj');
|
|
281
|
+
await fs.mkdir(xcodeproj, { recursive: true });
|
|
282
|
+
await fs.writeFile(path.join(xcodeproj, 'project.pbxproj'), pbxproj);
|
|
283
|
+
|
|
284
|
+
const project = await parseXcodeProject(xcodeproj);
|
|
285
|
+
const target = project.targets.find((t) => t.name === 'TestApp');
|
|
286
|
+
expect(target!.type).toBe('unknown');
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { ProgressReporter } from '../../src/progress/reporter.js';
|
|
2
|
+
import type { ProgressEvent } from '../../src/progress/types.js';
|
|
3
|
+
|
|
4
|
+
describe('ProgressReporter', () => {
|
|
5
|
+
let reporter: ProgressReporter;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
reporter = new ProgressReporter();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('scanStart', () => {
|
|
12
|
+
it('should emit scan:start event', () => {
|
|
13
|
+
const events: ProgressEvent[] = [];
|
|
14
|
+
reporter.on('scan:start', (e: ProgressEvent) => events.push(e));
|
|
15
|
+
|
|
16
|
+
reporter.scanStart(5);
|
|
17
|
+
|
|
18
|
+
expect(events).toHaveLength(1);
|
|
19
|
+
expect(events[0]!.type).toBe('scan:start');
|
|
20
|
+
expect(events[0]!.total).toBe(5);
|
|
21
|
+
expect(events[0]!.completed).toBe(0);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('analyzerStart', () => {
|
|
26
|
+
it('should emit analyzer:start event', () => {
|
|
27
|
+
const events: ProgressEvent[] = [];
|
|
28
|
+
reporter.on('analyzer:start', (e: ProgressEvent) => events.push(e));
|
|
29
|
+
|
|
30
|
+
reporter.scanStart(3);
|
|
31
|
+
reporter.analyzerStart('code');
|
|
32
|
+
|
|
33
|
+
expect(events).toHaveLength(1);
|
|
34
|
+
expect(events[0]!.type).toBe('analyzer:start');
|
|
35
|
+
expect(events[0]!.analyzer).toBe('code');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('analyzerComplete', () => {
|
|
40
|
+
it('should emit analyzer:complete event with duration', () => {
|
|
41
|
+
const events: ProgressEvent[] = [];
|
|
42
|
+
reporter.on('analyzer:complete', (e: ProgressEvent) => events.push(e));
|
|
43
|
+
|
|
44
|
+
reporter.scanStart(3);
|
|
45
|
+
reporter.analyzerComplete('code', 150);
|
|
46
|
+
|
|
47
|
+
expect(events).toHaveLength(1);
|
|
48
|
+
expect(events[0]!.type).toBe('analyzer:complete');
|
|
49
|
+
expect(events[0]!.analyzer).toBe('code');
|
|
50
|
+
expect(events[0]!.duration).toBe(150);
|
|
51
|
+
expect(events[0]!.completed).toBe(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should increment completed count', () => {
|
|
55
|
+
reporter.scanStart(3);
|
|
56
|
+
reporter.analyzerComplete('a', 10);
|
|
57
|
+
reporter.analyzerComplete('b', 20);
|
|
58
|
+
|
|
59
|
+
expect(reporter.percentage).toBe(67); // 2/3 = 66.67 -> 67
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('scanComplete', () => {
|
|
64
|
+
it('should emit scan:complete event', () => {
|
|
65
|
+
const events: ProgressEvent[] = [];
|
|
66
|
+
reporter.on('scan:complete', (e: ProgressEvent) => events.push(e));
|
|
67
|
+
|
|
68
|
+
reporter.scanStart(2);
|
|
69
|
+
reporter.analyzerComplete('a', 10);
|
|
70
|
+
reporter.analyzerComplete('b', 20);
|
|
71
|
+
reporter.scanComplete(30);
|
|
72
|
+
|
|
73
|
+
expect(events).toHaveLength(1);
|
|
74
|
+
expect(events[0]!.type).toBe('scan:complete');
|
|
75
|
+
expect(events[0]!.duration).toBe(30);
|
|
76
|
+
expect(events[0]!.completed).toBe(2);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('percentage', () => {
|
|
81
|
+
it('should return 0 when no analyzers', () => {
|
|
82
|
+
expect(reporter.percentage).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should return 0 at start', () => {
|
|
86
|
+
reporter.scanStart(5);
|
|
87
|
+
expect(reporter.percentage).toBe(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should return 100 when all complete', () => {
|
|
91
|
+
reporter.scanStart(2);
|
|
92
|
+
reporter.analyzerComplete('a', 10);
|
|
93
|
+
reporter.analyzerComplete('b', 10);
|
|
94
|
+
expect(reporter.percentage).toBe(100);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('callback', () => {
|
|
99
|
+
it('should call callback on all events', () => {
|
|
100
|
+
const events: ProgressEvent[] = [];
|
|
101
|
+
const callbackReporter = new ProgressReporter((e) => events.push(e));
|
|
102
|
+
|
|
103
|
+
callbackReporter.scanStart(1);
|
|
104
|
+
callbackReporter.analyzerStart('code');
|
|
105
|
+
callbackReporter.analyzerComplete('code', 100);
|
|
106
|
+
callbackReporter.scanComplete(100);
|
|
107
|
+
|
|
108
|
+
expect(events).toHaveLength(4);
|
|
109
|
+
expect(events.map((e) => e.type)).toEqual([
|
|
110
|
+
'scan:start',
|
|
111
|
+
'analyzer:start',
|
|
112
|
+
'analyzer:complete',
|
|
113
|
+
'scan:complete',
|
|
114
|
+
]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|