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.
@@ -1,79 +1,107 @@
1
1
  // tests/unit/cli/cli.test.ts
2
2
 
3
- import { jest, describe, it, beforeAll, afterAll, beforeEach, expect } from '@jest/globals';
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, BundleOptions } from '../../../src/types'; // Ensure all needed types are imported
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
- // --- Mock ONLY external dependencies (options.ts, index.ts) ---
12
- const mockParseOptions = jest.fn<(argv: string[]) => CLIOptions>();
13
- const mockGeneratePortableHTML = jest.fn<(input: string, options: BundleOptions) => Promise<BuildResult>>();
14
- const mockGenerateRecursivePortableHTML = jest.fn<(url: string, depth: number | boolean, options: BundleOptions) => Promise<BuildResult>>();
15
-
16
- jest.unstable_mockModule('../../../src/cli/options', () => ({ // Adjust path
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.unstable_mockModule('../../../src/index', () => ({ // Adjust path
20
- generatePortableHTML: mockGeneratePortableHTML,
21
- generateRecursivePortableHTML: mockGenerateRecursivePortableHTML,
25
+ jest.mock('../../../src/index', () => ({
26
+ __esModule: true,
27
+ pack: mockPackFn
22
28
  }));
23
29
 
24
-
25
- // --- Dynamic Import of the module under test ---
26
- let runCli: (argv?: string[]) => Promise<CLIResult>;
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, assetCount: 5, outputSize: 1024, buildTimeMs: 100,
45
- pagesBundled: undefined, errors: [], ...overrides,
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
- // Spy on writeFileSync and provide a mock implementation (e.g., do nothing)
54
- writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
55
- // Spy on others used by getPackageJson and provide mock return values
56
- existsSyncSpy = jest.spyOn(fs, 'existsSync').mockReturnValue(true);
57
- readFileSyncSpy = jest.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify({ version: "1.0.0-test" }));
58
- // --------------------------------
59
-
60
- // Default mock implementations for external dependencies
61
- mockParseOptions.mockReturnValue({
62
- input: 'default.html', output: undefined, recursive: false, dryRun: false,
63
- logLevel: LogLevel.INFO, verbose: false, embedAssets: true, minifyHtml: true, minifyCss: true, minifyJs: true
64
- });
65
- mockGeneratePortableHTML.mockResolvedValue({
66
- html: '<html>Default Portable</html>',
67
- metadata: createMockMetadata('default.html')
68
- });
69
- mockGenerateRecursivePortableHTML.mockResolvedValue({
70
- html: '<html>Default Recursive</html>',
71
- metadata: createMockMetadata('https://default.recursive', { pagesBundled: 1, assetCount: 1 })
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
- mockParseOptions.mockReturnValueOnce({ input: 'test-input.html', output: 'test-output.html', recursive: false } as CLIOptions);
85
- mockGeneratePortableHTML.mockResolvedValueOnce({ html: '', metadata: createMockMetadata('test-input.html') });
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('runs generatePortableHTML when not recursive', async () => {
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
- input: 'index.html', output: 'output.html', recursive: false, dryRun: false,
97
- logLevel: LogLevel.INFO, verbose: false, minifyHtml: true, minifyCss: true, minifyJs: true, embedAssets: true
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
- mockGeneratePortableHTML.mockResolvedValueOnce({
102
- html: '<html>Generated Non-Recursive</html>',
103
- metadata: specificMetadata
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
- expect(mockGeneratePortableHTML).toHaveBeenCalledWith('index.html', mockCliOptions);
109
- expect(mockGenerateRecursivePortableHTML).not.toHaveBeenCalled();
110
- // --- Check the SPY ---
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>Generated Non-Recursive</html>', 'utf-8');
113
- // --- Check result ---
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
- // ... other stdout checks ...
155
+ expect(result.stdout).toContain('📦 Size: 2.00 KB'); // Check stats based on mock metadata
117
156
  });
118
157
 
119
- it('runs generateRecursivePortableHTML when recursive is enabled (boolean)', async () => {
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
- input: 'https://site.com', output: 'site-packed.html', recursive: true,
123
- dryRun: false, logLevel: LogLevel.INFO, verbose: false, /* other flags */
161
+ ...defaultCliOptions,
162
+ input: 'https://site.com',
163
+ output: 'site-packed.html',
164
+ recursive: true, // Recursive flag is true
124
165
  };
125
- const specificMetadata = createMockMetadata('https://site.com', { pagesBundled: 3, assetCount: 10, outputSize: 3000 });
126
- mockParseOptions.mockReturnValueOnce(mockCliOptions);
127
- mockGenerateRecursivePortableHTML.mockResolvedValueOnce({
128
- html: '<html>Generated Recursive Boolean</html>',
129
- metadata: specificMetadata
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(mockGenerateRecursivePortableHTML).toHaveBeenCalledTimes(1);
135
- // Check args passed to generateRecursivePortableHTML (cli.ts passes options now)
136
- expect(mockGenerateRecursivePortableHTML).toHaveBeenCalledWith('https://site.com', 1, mockCliOptions);
137
- expect(mockGeneratePortableHTML).not.toHaveBeenCalled();
138
- // --- Check the SPY ---
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>Generated Recursive Boolean</html>', 'utf-8');
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
- // ... other stdout checks ...
185
+ expect(result.stdout).toContain('🧩 Pages: 3'); // Check pages bundled output
144
186
  });
145
187
 
146
- it('runs generateRecursivePortableHTML when recursive is enabled (number)', async () => {
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
- input: 'https://site.com', output: 'site-packed.html', recursive: 2,
150
- dryRun: false, logLevel: LogLevel.INFO, verbose: false, /* other flags */
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, assetCount: 20, outputSize: 5000 });
196
+ const specificMetadata = createMockMetadata('https://site.com', { pagesBundled: 5 });
153
197
  mockParseOptions.mockReturnValueOnce(mockCliOptions);
154
- mockGenerateRecursivePortableHTML.mockResolvedValueOnce({
155
- html: '<html>Generated Recursive Number</html>',
156
- metadata: specificMetadata
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(mockGenerateRecursivePortableHTML).toHaveBeenCalledTimes(1);
162
- expect(mockGenerateRecursivePortableHTML).toHaveBeenCalledWith('https://site.com', 2, mockCliOptions);
163
- expect(mockGeneratePortableHTML).not.toHaveBeenCalled();
164
- // --- Check the SPY ---
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>Generated Recursive Number</html>', 'utf-8');
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
- // ... other stdout checks ...
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
- it('returns error if input is missing', async () => {
173
- const args = ['node', 'cli.js'];
174
- mockParseOptions.mockReturnValueOnce({ } as CLIOptions); // Simulate no input parsed
175
-
176
- const result = await runCli(args);
177
-
178
- expect(result.exitCode).toBe(1);
179
- expect(result.stderr).toContain('❌ Missing input file or URL');
180
- expect(mockGeneratePortableHTML).not.toHaveBeenCalled();
181
- expect(mockGenerateRecursivePortableHTML).not.toHaveBeenCalled();
182
- expect(writeFileSyncSpy).not.toHaveBeenCalled(); // Check SPY not called
183
- });
184
-
185
- it('skips processing and writing in dry-run mode', async () => {
186
- const args = ['node', 'cli.js', 'index.html', '--dry-run', '-o','should-not-write.html'];
187
- mockParseOptions.mockReturnValueOnce({
188
- input: 'index.html', output: 'should-not-write.html', dryRun: true, recursive: false
189
- } as CLIOptions);
190
-
191
- const result = await runCli(args);
192
-
193
- expect(mockGeneratePortableHTML).not.toHaveBeenCalled();
194
- expect(mockGenerateRecursivePortableHTML).not.toHaveBeenCalled();
195
- expect(writeFileSyncSpy).not.toHaveBeenCalled(); // Check SPY not called
196
- expect(result.exitCode).toBe(0);
197
- expect(result.stdout).toContain('💡 Dry run mode');
198
- expect(result.stdout).not.toContain(' Packed:');
199
- });
200
-
201
- it('handles unexpected errors gracefully', async () => {
202
- const args = ['node', 'cli.js', 'bad-input.html'];
203
- const errorMessage = 'Something broke during processing';
204
- mockParseOptions.mockReturnValueOnce({ input: 'bad-input.html', output: 'out.html', recursive: false } as CLIOptions);
205
- mockGeneratePortableHTML.mockRejectedValueOnce(new Error(errorMessage)); // Simulate error
206
-
207
- const result = await runCli(args);
208
-
209
- expect(result.exitCode).toBe(1);
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
  });