portapack 0.2.1 → 0.3.1
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/.github/workflows/ci.yml +5 -4
- package/CHANGELOG.md +20 -0
- package/README.md +81 -219
- package/dist/cli/{cli-entry.js → cli-entry.cjs} +620 -513
- package/dist/cli/cli-entry.cjs.map +1 -0
- package/dist/index.d.ts +51 -56
- package/dist/index.js +517 -458
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.ts +0 -1
- package/docs/cli.md +108 -45
- package/docs/configuration.md +101 -116
- package/docs/getting-started.md +74 -44
- package/jest.config.ts +18 -8
- package/jest.setup.cjs +66 -146
- package/package.json +5 -5
- package/src/cli/cli-entry.ts +15 -15
- package/src/cli/cli.ts +130 -119
- package/src/core/bundler.ts +174 -63
- package/src/core/extractor.ts +364 -277
- package/src/core/web-fetcher.ts +205 -141
- package/src/index.ts +161 -224
- package/tests/unit/cli/cli-entry.test.ts +66 -77
- package/tests/unit/cli/cli.test.ts +243 -145
- package/tests/unit/core/bundler.test.ts +334 -258
- package/tests/unit/core/extractor.test.ts +608 -1064
- package/tests/unit/core/minifier.test.ts +130 -221
- package/tests/unit/core/packer.test.ts +255 -106
- package/tests/unit/core/parser.test.ts +89 -458
- package/tests/unit/core/web-fetcher.test.ts +310 -265
- package/tests/unit/index.test.ts +206 -300
- package/tests/unit/utils/logger.test.ts +32 -28
- package/tsconfig.jest.json +8 -7
- package/tsup.config.ts +34 -29
- package/dist/cli/cli-entry.js.map +0 -1
- package/docs/demo.md +0 -46
- package/output.html +0 -1
- package/site-packed.html +0 -1
- package/test-output.html +0 -0
@@ -1,79 +1,107 @@
|
|
1
1
|
// tests/unit/cli/cli.test.ts
|
2
2
|
|
3
|
-
import { jest, describe, it,
|
3
|
+
import { jest, describe, it, beforeEach, afterEach, expect } from '@jest/globals'; // Assuming beforeAll/afterAll aren't strictly needed for these tests unless used by setup
|
4
|
+
|
4
5
|
// Import types
|
5
|
-
import type { CLIOptions, BuildResult, CLIResult, BundleMetadata
|
6
|
+
import type { CLIOptions, BuildResult, CLIResult, BundleMetadata } from '../../../src/types';
|
6
7
|
import { LogLevel } from '../../../src/types';
|
7
8
|
|
8
9
|
// --- Import ACTUAL fs module to spy on ---
|
9
10
|
import fs from 'fs';
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
11
|
+
// Import package.json for version checks if needed (ensure path is correct)
|
12
|
+
import * as packageJson from '../../../package.json';
|
13
|
+
|
14
|
+
// --- MOCK DEPENDENCIES ---
|
15
|
+
// Mock the function that parses options (dependency of cli.ts)
|
16
|
+
const mockParseOptions = jest.fn<() => CLIOptions>();
|
17
|
+
// Mock the main pack function (dependency of cli.ts)
|
18
|
+
const mockPackFn = jest.fn<() => Promise<BuildResult>>();
|
19
|
+
|
20
|
+
// Tell Jest to replace the actual modules with our mocks *before* cli.ts is imported
|
21
|
+
jest.mock('../../../src/cli/options', () => ({
|
22
|
+
__esModule: true,
|
17
23
|
parseOptions: mockParseOptions
|
18
24
|
}));
|
19
|
-
jest.
|
20
|
-
|
21
|
-
|
25
|
+
jest.mock('../../../src/index', () => ({
|
26
|
+
__esModule: true,
|
27
|
+
pack: mockPackFn
|
22
28
|
}));
|
23
29
|
|
24
|
-
|
25
|
-
//
|
26
|
-
|
27
|
-
beforeAll(async () => {
|
28
|
-
// Import AFTER mocks for external deps are configured
|
29
|
-
const cliModule = await import('../../../src/cli/cli'); // Adjust path
|
30
|
-
runCli = cliModule.runCli;
|
31
|
-
});
|
32
|
-
|
30
|
+
// --- Import the module under test AFTER mocks are defined ---
|
31
|
+
// Now imports the *actual* runCli function, not a mocked version
|
32
|
+
import { runCli } from '../../../src/cli/cli';
|
33
33
|
|
34
34
|
// --- Test Suite ---
|
35
35
|
describe('CLI Runner Logic', () => {
|
36
36
|
|
37
|
-
// --- Declare Spies for FS module ---
|
37
|
+
// --- Declare Spies for FS module (used by cli.ts) ---
|
38
38
|
let writeFileSyncSpy: jest.SpiedFunction<typeof fs.writeFileSync>;
|
39
39
|
let existsSyncSpy: jest.SpiedFunction<typeof fs.existsSync>;
|
40
40
|
let readFileSyncSpy: jest.SpiedFunction<typeof fs.readFileSync>;
|
41
41
|
|
42
42
|
// Helper to create mock metadata
|
43
43
|
const createMockMetadata = (input: string, overrides: Partial<BundleMetadata> = {}): BundleMetadata => ({
|
44
|
-
input: input,
|
45
|
-
|
44
|
+
input: input,
|
45
|
+
assetCount: 5,
|
46
|
+
outputSize: 1024,
|
47
|
+
buildTimeMs: 100,
|
48
|
+
pagesBundled: undefined, // Default to undefined
|
49
|
+
errors: [], // Default to empty errors/warnings array
|
50
|
+
...overrides,
|
46
51
|
});
|
47
52
|
|
53
|
+
// Default options structure returned by the mock parseOptions
|
54
|
+
const defaultCliOptions: CLIOptions = {
|
55
|
+
input: 'default.html',
|
56
|
+
output: undefined,
|
57
|
+
recursive: false,
|
58
|
+
dryRun: false,
|
59
|
+
logLevel: LogLevel.INFO,
|
60
|
+
verbose: false,
|
61
|
+
embedAssets: true,
|
62
|
+
minifyHtml: true,
|
63
|
+
minifyCss: true,
|
64
|
+
minifyJs: true
|
65
|
+
};
|
66
|
+
|
48
67
|
beforeEach(() => {
|
49
68
|
// --- Reset ALL Mocks and Spies ---
|
69
|
+
// Clears call history etc. between tests
|
50
70
|
jest.clearAllMocks();
|
51
71
|
|
52
72
|
// --- Setup Spies on FS module ---
|
53
|
-
//
|
54
|
-
writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
73
|
+
// Mock implementations for fs functions called by cli.ts
|
74
|
+
writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => { }); // Don't actually write files
|
75
|
+
existsSyncSpy = jest.spyOn(fs, 'existsSync').mockReturnValue(true); // Assume files exist by default
|
76
|
+
|
77
|
+
// Mock readFileSync to handle the package.json read required by getPackageJson (internal to cli.ts)
|
78
|
+
readFileSyncSpy = jest.spyOn(fs, 'readFileSync')
|
79
|
+
.mockImplementation((pathInput, options) => {
|
80
|
+
const pathStr = String(pathInput); // Ensure path is treated as a string
|
81
|
+
// Check if it's trying to read package.json (adjust check if needed)
|
82
|
+
if (pathStr.includes('portapack') && pathStr.endsWith('package.json')) {
|
83
|
+
return JSON.stringify({ version: "1.0.0-test" }); // Return mock version
|
84
|
+
}
|
85
|
+
// If cli.ts needed to read other files, mock them here too
|
86
|
+
// if (pathStr.endsWith('some-other-expected-file.txt')) { return 'file content'; }
|
87
|
+
|
88
|
+
// Throw an error for any unexpected file reads during tests
|
89
|
+
throw new Error(`Unexpected readFileSync call in test: ${pathStr}`);
|
90
|
+
});
|
91
|
+
|
92
|
+
// --- Default mock implementations for dependencies ---
|
93
|
+
// Ensure parseOptions returns a valid structure by default
|
94
|
+
mockParseOptions.mockReturnValue(defaultCliOptions);
|
95
|
+
// Ensure pack resolves successfully by default
|
96
|
+
mockPackFn.mockResolvedValue({
|
97
|
+
html: '<html>Default Pack Result</html>',
|
98
|
+
metadata: createMockMetadata(defaultCliOptions.input ?? 'default.html') // Use input from default options
|
72
99
|
});
|
73
100
|
});
|
74
101
|
|
75
102
|
afterEach(() => {
|
76
103
|
// Restore original implementations spied on by jest.spyOn
|
104
|
+
// Important to avoid mocks leaking between test files
|
77
105
|
jest.restoreAllMocks();
|
78
106
|
});
|
79
107
|
|
@@ -81,150 +109,220 @@ describe('CLI Runner Logic', () => {
|
|
81
109
|
|
82
110
|
it('calls parseOptions with process arguments', async () => {
|
83
111
|
const testArgs = ['node', 'cli.js', 'test-input.html', '-o', 'test-output.html'];
|
84
|
-
|
85
|
-
|
112
|
+
const specificOptions = { ...defaultCliOptions, input: 'test-input.html', output: 'test-output.html' };
|
113
|
+
mockParseOptions.mockReturnValueOnce(specificOptions); // Override default for this test
|
114
|
+
// Configure pack mock for this specific input if its result matters
|
115
|
+
mockPackFn.mockResolvedValueOnce({ html: '<html>Specific Test Result</html>', metadata: createMockMetadata('test-input.html') });
|
86
116
|
|
87
117
|
await runCli(testArgs);
|
88
118
|
|
89
119
|
expect(mockParseOptions).toHaveBeenCalledTimes(1);
|
90
120
|
expect(mockParseOptions).toHaveBeenCalledWith(testArgs);
|
121
|
+
// Verify pack was called because parseOptions returned valid input and not dry-run
|
122
|
+
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
123
|
+
// Check if pack was called with the correct arguments derived from options
|
124
|
+
expect(mockPackFn).toHaveBeenCalledWith(specificOptions.input, specificOptions);
|
91
125
|
});
|
92
126
|
|
93
|
-
it('
|
127
|
+
it('calls pack for non-recursive input and writes file', async () => {
|
94
128
|
const args = ['node', 'cli.js', 'index.html', '-o', 'output.html'];
|
95
129
|
const mockCliOptions: CLIOptions = {
|
96
|
-
|
97
|
-
|
130
|
+
...defaultCliOptions,
|
131
|
+
input: 'index.html',
|
132
|
+
output: 'output.html', // Explicit output path
|
133
|
+
recursive: false
|
98
134
|
};
|
99
135
|
const specificMetadata = createMockMetadata('index.html', { assetCount: 3, outputSize: 2048, buildTimeMs: 55 });
|
100
|
-
mockParseOptions.mockReturnValueOnce(mockCliOptions);
|
101
|
-
|
102
|
-
|
103
|
-
|
136
|
+
mockParseOptions.mockReturnValueOnce(mockCliOptions); // Use specific options for this test
|
137
|
+
mockPackFn.mockResolvedValueOnce({ // Use specific pack result for this test
|
138
|
+
html: '<html>Packed Non-Recursive</html>',
|
139
|
+
metadata: specificMetadata
|
104
140
|
});
|
105
141
|
|
106
142
|
const result = await runCli(args);
|
107
143
|
|
108
|
-
|
109
|
-
expect(
|
110
|
-
//
|
144
|
+
// Check that parseOptions was called
|
145
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
146
|
+
// Check pack was called correctly
|
147
|
+
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
148
|
+
expect(mockPackFn).toHaveBeenCalledWith(mockCliOptions.input, mockCliOptions); // Called with input and options object
|
149
|
+
// Check file write happened
|
111
150
|
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
|
112
|
-
expect(writeFileSyncSpy).toHaveBeenCalledWith('output.html', '<html>
|
113
|
-
//
|
151
|
+
expect(writeFileSyncSpy).toHaveBeenCalledWith('output.html', '<html>Packed Non-Recursive</html>', 'utf-8');
|
152
|
+
// Check result object
|
114
153
|
expect(result.exitCode).toBe(0);
|
115
154
|
expect(result.stdout).toContain('✅ Packed: index.html → output.html');
|
116
|
-
//
|
155
|
+
expect(result.stdout).toContain('📦 Size: 2.00 KB'); // Check stats based on mock metadata
|
117
156
|
});
|
118
157
|
|
119
|
-
it('
|
158
|
+
it('calls pack for recursive input (boolean) and writes file', async () => {
|
120
159
|
const args = ['node', 'cli.js', 'https://site.com', '-r', '-o', 'site-packed.html'];
|
121
160
|
const mockCliOptions: CLIOptions = {
|
122
|
-
|
123
|
-
|
161
|
+
...defaultCliOptions,
|
162
|
+
input: 'https://site.com',
|
163
|
+
output: 'site-packed.html',
|
164
|
+
recursive: true, // Recursive flag is true
|
124
165
|
};
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
166
|
+
const specificMetadata = createMockMetadata('https://site.com', { pagesBundled: 3, assetCount: 10 });
|
167
|
+
mockParseOptions.mockReturnValueOnce(mockCliOptions);
|
168
|
+
mockPackFn.mockResolvedValueOnce({
|
169
|
+
html: '<html>Packed Recursive Boolean</html>',
|
170
|
+
metadata: specificMetadata
|
171
|
+
});
|
131
172
|
|
132
173
|
const result = await runCli(args);
|
133
174
|
|
134
|
-
expect(
|
135
|
-
// Check
|
136
|
-
expect(
|
137
|
-
expect(
|
138
|
-
//
|
175
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
176
|
+
// Check pack was called
|
177
|
+
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
178
|
+
expect(mockPackFn).toHaveBeenCalledWith(mockCliOptions.input, mockCliOptions);
|
179
|
+
// Check file write
|
139
180
|
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
|
140
|
-
expect(writeFileSyncSpy).toHaveBeenCalledWith('site-packed.html', '<html>
|
181
|
+
expect(writeFileSyncSpy).toHaveBeenCalledWith('site-packed.html', '<html>Packed Recursive Boolean</html>', 'utf-8');
|
182
|
+
// Check result object and specific recursive output
|
141
183
|
expect(result.exitCode).toBe(0);
|
142
184
|
expect(result.stdout).toContain('✅ Packed: https://site.com → site-packed.html');
|
143
|
-
//
|
185
|
+
expect(result.stdout).toContain('🧩 Pages: 3'); // Check pages bundled output
|
144
186
|
});
|
145
187
|
|
146
|
-
|
188
|
+
it('calls pack for recursive input (number) and writes file', async () => {
|
147
189
|
const args = ['node', 'cli.js', 'https://site.com', '-r', '2', '-o', 'site-packed.html'];
|
148
190
|
const mockCliOptions: CLIOptions = {
|
149
|
-
|
150
|
-
|
191
|
+
...defaultCliOptions,
|
192
|
+
input: 'https://site.com',
|
193
|
+
output: 'site-packed.html',
|
194
|
+
recursive: 2, // Recursive flag is number
|
151
195
|
};
|
152
|
-
const specificMetadata = createMockMetadata('https://site.com', { pagesBundled: 5
|
196
|
+
const specificMetadata = createMockMetadata('https://site.com', { pagesBundled: 5 });
|
153
197
|
mockParseOptions.mockReturnValueOnce(mockCliOptions);
|
154
|
-
|
155
|
-
|
156
|
-
|
198
|
+
mockPackFn.mockResolvedValueOnce({
|
199
|
+
html: '<html>Packed Recursive Number</html>',
|
200
|
+
metadata: specificMetadata
|
157
201
|
});
|
158
202
|
|
159
203
|
const result = await runCli(args);
|
160
204
|
|
161
|
-
expect(
|
162
|
-
|
163
|
-
expect(
|
164
|
-
|
205
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
206
|
+
// Check pack was called
|
207
|
+
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
208
|
+
expect(mockPackFn).toHaveBeenCalledWith(mockCliOptions.input, mockCliOptions);
|
209
|
+
// Check file write
|
165
210
|
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
|
166
|
-
expect(writeFileSyncSpy).toHaveBeenCalledWith('site-packed.html', '<html>
|
211
|
+
expect(writeFileSyncSpy).toHaveBeenCalledWith('site-packed.html', '<html>Packed Recursive Number</html>', 'utf-8');
|
212
|
+
// Check result object and specific recursive output
|
167
213
|
expect(result.exitCode).toBe(0);
|
168
214
|
expect(result.stdout).toContain('✅ Packed: https://site.com → site-packed.html');
|
169
|
-
|
215
|
+
expect(result.stdout).toContain('🧩 Pages: 5');
|
216
|
+
});
|
217
|
+
|
218
|
+
it('returns error if input is missing', async () => {
|
219
|
+
const args = ['node', 'cli.js']; // No input provided
|
220
|
+
// Simulate parseOptions returning options without an input
|
221
|
+
mockParseOptions.mockReturnValueOnce({ ...defaultCliOptions, input: undefined });
|
222
|
+
|
223
|
+
const result = await runCli(args);
|
224
|
+
|
225
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
226
|
+
// Check for early exit
|
227
|
+
expect(result.exitCode).toBe(1);
|
228
|
+
expect(result.stderr).toContain('❌ Missing input file or URL');
|
229
|
+
// Ensure pack and writeFile were not called
|
230
|
+
expect(mockPackFn).not.toHaveBeenCalled();
|
231
|
+
expect(writeFileSyncSpy).not.toHaveBeenCalled();
|
232
|
+
});
|
233
|
+
|
234
|
+
it('skips processing and writing in dry-run mode', async () => {
|
235
|
+
const args = ['node', 'cli.js', 'index.html', '--dry-run'];
|
236
|
+
// Simulate parseOptions returning dryRun: true
|
237
|
+
mockParseOptions.mockReturnValueOnce({ ...defaultCliOptions, input: 'index.html', dryRun: true });
|
238
|
+
|
239
|
+
const result = await runCli(args);
|
240
|
+
|
241
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
242
|
+
// Ensure pack and writeFile were not called due to dry run
|
243
|
+
expect(mockPackFn).not.toHaveBeenCalled();
|
244
|
+
expect(writeFileSyncSpy).not.toHaveBeenCalled();
|
245
|
+
// Check for successful exit code and dry run message
|
246
|
+
expect(result.exitCode).toBe(0);
|
247
|
+
expect(result.stdout).toContain('💡 Dry run mode');
|
248
|
+
});
|
249
|
+
|
250
|
+
it('handles unexpected errors from pack() gracefully', async () => {
|
251
|
+
const args = ['node', 'cli.js', './valid-input.html'];
|
252
|
+
const errorMessage = 'Something broke during processing';
|
253
|
+
// Use valid options, but make pack() reject
|
254
|
+
mockParseOptions.mockReturnValueOnce({ ...defaultCliOptions, input: './valid-input.html' });
|
255
|
+
mockPackFn.mockRejectedValueOnce(new Error(errorMessage)); // Simulate error from pack
|
256
|
+
|
257
|
+
const result = await runCli(args);
|
258
|
+
|
259
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
260
|
+
expect(mockPackFn).toHaveBeenCalledTimes(1); // Ensure pack was called
|
261
|
+
// Check for error exit code and message
|
262
|
+
expect(result.exitCode).toBe(1);
|
263
|
+
expect(result.stderr).toContain(`💥 Error: ${errorMessage}`);
|
264
|
+
// Ensure file was not written on error
|
265
|
+
expect(writeFileSyncSpy).not.toHaveBeenCalled();
|
266
|
+
});
|
267
|
+
|
268
|
+
it('displays warnings from metadata in stderr', async () => {
|
269
|
+
const args = ['node', 'cli.js', 'index.html', '-o', 'output.html']; // Ensure output path is set
|
270
|
+
const warningMessage = 'Asset not found: missing.css';
|
271
|
+
const metadataWithWarning = createMockMetadata('index.html', { errors: [warningMessage] }); // Add error/warning to metadata
|
272
|
+
mockParseOptions.mockReturnValueOnce({ ...defaultCliOptions, input: 'index.html', output: 'output.html' });
|
273
|
+
mockPackFn.mockResolvedValueOnce({ html: '<html>Warning</html>', metadata: metadataWithWarning }); // Return metadata with warning
|
274
|
+
|
275
|
+
const result = await runCli(args);
|
276
|
+
|
277
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
278
|
+
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
279
|
+
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); // Verify write still happens
|
280
|
+
expect(writeFileSyncSpy).toHaveBeenCalledWith('output.html', '<html>Warning</html>', 'utf-8');
|
281
|
+
// Should exit 0 even with warnings
|
282
|
+
expect(result.exitCode).toBe(0);
|
283
|
+
// Standard output should still show success
|
284
|
+
expect(result.stdout).toContain('✅ Packed: index.html → output.html');
|
285
|
+
expect(result.stderr).toMatch(/⚠️\s+1\s+warning\(s\):/);
|
286
|
+
// Warnings should be logged to stderr
|
287
|
+
expect(result.stderr).toContain(`- ${warningMessage}`);
|
170
288
|
});
|
171
289
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
expect(result.stderr).toContain(`💥 Error: ${errorMessage}`);
|
211
|
-
expect(writeFileSyncSpy).not.toHaveBeenCalled(); // Check SPY not called
|
212
|
-
});
|
213
|
-
|
214
|
-
it('displays warnings from metadata', async () => {
|
215
|
-
const args = ['node', 'cli.js', 'index.html'];
|
216
|
-
const warningMessage = 'Asset not found: missing.css';
|
217
|
-
const metadataWithWarning = createMockMetadata('index.html', { errors: [warningMessage] });
|
218
|
-
mockParseOptions.mockReturnValueOnce({ input: 'index.html', output: 'output.html', recursive: false } as CLIOptions);
|
219
|
-
mockGeneratePortableHTML.mockResolvedValueOnce({ html: '<html>Warning</html>', metadata: metadataWithWarning });
|
220
|
-
|
221
|
-
const result = await runCli(args);
|
222
|
-
|
223
|
-
expect(result.exitCode).toBe(0);
|
224
|
-
expect(result.stdout).toContain('✅ Packed: index.html → output.html');
|
225
|
-
expect(result.stderr).toContain('⚠️ 1 warning(s):');
|
226
|
-
expect(result.stderr).toContain(`- ${warningMessage}`);
|
227
|
-
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); // Check SPY called
|
228
|
-
expect(writeFileSyncSpy).toHaveBeenCalledWith('output.html', '<html>Warning</html>', 'utf-8');
|
229
|
-
});
|
290
|
+
it('displays verbose startup logs when --verbose is used', async () => {
|
291
|
+
const args = ['node', 'cli.js', 'index.html', '--verbose', '-o', 'out.html']; // Add verbose flag
|
292
|
+
const verboseOptions: CLIOptions = {
|
293
|
+
...defaultCliOptions,
|
294
|
+
input: 'index.html',
|
295
|
+
output: 'out.html', // Explicit output needed for logs
|
296
|
+
verbose: true,
|
297
|
+
logLevel: LogLevel.DEBUG // Verbose usually implies a lower log level from parseOptions
|
298
|
+
};
|
299
|
+
mockParseOptions.mockReturnValueOnce(verboseOptions);
|
300
|
+
// We still need pack to resolve successfully
|
301
|
+
mockPackFn.mockResolvedValue({
|
302
|
+
html: '<!DOCTYPE html><html><body>Mock HTML</body></html>',
|
303
|
+
metadata: createMockMetadata('index.html') // Use simple metadata
|
304
|
+
});
|
305
|
+
|
306
|
+
const result = await runCli(args);
|
307
|
+
|
308
|
+
const expectedVersionString = `📦 PortaPack v${packageJson.version}`; // Get version dynamically
|
309
|
+
|
310
|
+
expect(mockParseOptions).toHaveBeenCalledWith(args);
|
311
|
+
expect(mockPackFn).toHaveBeenCalledTimes(1);
|
312
|
+
expect(writeFileSyncSpy).toHaveBeenCalledTimes(1); // Write should still occur
|
313
|
+
|
314
|
+
// Check stdout for verbose logs
|
315
|
+
expect(result.stdout).toContain(expectedVersionString);
|
316
|
+
expect(result.stdout).toContain('📥 Input: index.html');
|
317
|
+
expect(result.stdout).toContain('📤 Output: out.html'); // Checks resolved output path
|
318
|
+
expect(result.stdout).toContain('Recursive: false'); // Check other options logged
|
319
|
+
expect(result.stdout).toContain(`Log Level: ${verboseOptions.logLevel}`); // Check logged log level
|
320
|
+
|
321
|
+
// Check for standard success logs as well
|
322
|
+
expect(result.stdout).toContain('✅ Packed: index.html → out.html');
|
323
|
+
expect(result.exitCode).toBe(0);
|
324
|
+
});
|
325
|
+
|
326
|
+
// Add more tests for other options, edge cases, different log levels etc.
|
327
|
+
|
230
328
|
});
|