portapack 0.2.1 → 0.3.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.
@@ -1,339 +1,245 @@
1
1
  // tests/unit/index.test.ts
2
2
 
3
- import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';
4
- // Import the actual class ONLY for type info using Parameters/ReturnType etc.
5
- import { BuildTimer } from '../../src/utils/meta'; // Adjust path if needed
6
- // Import types and other necessary modules
3
+ import { describe, it, expect, jest, beforeEach } from '@jest/globals';
4
+ import path from 'path';
5
+
6
+ // --- Import necessary types ---
7
7
  import type {
8
8
  ParsedHTML,
9
9
  Asset,
10
10
  BundleOptions,
11
11
  PageEntry,
12
- BuildResult,
13
12
  BundleMetadata,
14
- CLIOptions // Import if needed for options object structure
15
- } from '../../src/types'; // Adjust path, use .js if needed
16
- import { LogLevel } from '../../src/types'; // Adjust path, use .js if needed
17
- import { Logger } from '../../src/utils/logger'; // Adjust path, use .js if needed
18
-
19
-
20
- // --- Import Modules to Spy On ---
21
- import * as parser from '../../src/core/parser'; // Adjust path
22
- import * as extractor from '../../src/core/extractor'; // Adjust path
23
- import * as minifier from '../../src/core/minifier'; // Adjust path
24
- import * as packer from '../../src/core/packer'; // Adjust path
25
- import * as webFetcher from '../../src/core/web-fetcher'; // Adjust path
26
- import * as bundler from '../../src/core/bundler'; // Adjust path
27
-
28
-
29
- // --- Mock ONLY BuildTimer using auto-mock ---
30
- // This replaces the class with a Jest mock constructor.
31
- jest.mock('../../src/utils/meta'); // Adjust path
32
-
33
-
34
- // --- Define types for instance methods for clarity later ---
35
- type FinishSig = BuildTimer['finish'];
36
- type SetPageCountSig = BuildTimer['setPageCount'];
37
-
38
- // --- Declare variables for method mocks ---
39
- // These will hold our specific Jest mock functions for the instance methods
40
- let mockFinish: jest.Mock<FinishSig>;
41
- let mockSetPageCount: jest.Mock<SetPageCountSig>;
42
-
43
-
44
- // --- Define types for core functions (signatures of actual functions) ---
45
- type ParseHTMLFn = typeof parser.parseHTML;
46
- type ExtractAssetsFn = typeof extractor.extractAssets;
47
- type MinifyAssetsFn = typeof minifier.minifyAssets;
48
- type PackHTMLFn = typeof packer.packHTML;
49
- type CoreFetchFn = typeof webFetcher.fetchAndPackWebPage;
50
- type CoreRecursiveFn = typeof webFetcher.recursivelyBundleSite;
51
- type CoreBundleMultiPageFn = typeof bundler.bundleMultiPageHTML;
52
-
53
- // --- Declare spies for core functions ---
54
- let parseHTMLSpy: jest.SpiedFunction<ParseHTMLFn>;
55
- let extractAssetsSpy: jest.SpiedFunction<ExtractAssetsFn>;
56
- let minifyAssetsSpy: jest.SpiedFunction<MinifyAssetsFn>;
57
- let packHTMLSpy: jest.SpiedFunction<PackHTMLFn>;
58
- let coreFetchAndPackSpy: jest.SpiedFunction<CoreFetchFn>;
59
- let coreRecursivelyBundleSiteSpy: jest.SpiedFunction<CoreRecursiveFn>;
60
- let coreBundleMultiPageHTMLSpy: jest.SpiedFunction<CoreBundleMultiPageFn>;
61
-
62
-
63
- // --- Import Module Under Test ---
64
- // This MUST come AFTER jest.mock calls for BuildTimer
65
- import * as PortaPack from '../../src/index'; // Adjust path
66
-
67
-
68
- // --- Test Suite ---
13
+ BuildResult,
14
+ // PackOptions, // Defined inline in index.ts, not exported here
15
+ } from '../../src/types';
16
+ import { LogLevel } from '../../src/types';
17
+ import { Logger } from '../../src/utils/logger';
18
+
19
+ // --- MOCK DEPENDENCIES FIRST ---
20
+
21
+ // --- Define PLAIN TOP-LEVEL mock functions ---
22
+ const mockParseHTMLFn = jest.fn();
23
+ const mockExtractAssetsFn = jest.fn();
24
+ const mockMinifyAssetsFn = jest.fn();
25
+ const mockPackHTMLFn = jest.fn();
26
+ const mockFetchAndPackWebPageFn = jest.fn();
27
+ const mockRecursivelyBundleSiteFn = jest.fn();
28
+ const mockBundleMultiPageHTMLFn = jest.fn();
29
+
30
+ // --- Define BuildTimer mock state variables ---
31
+ let mockFinishFn = jest.fn();
32
+ let mockSetPageCountFn = jest.fn();
33
+ const mockSetHtmlSizeFn = jest.fn();
34
+
35
+ // --- Explicitly Mock Modules with Factories ---
36
+ jest.mock('../../src/utils/meta', () => ({
37
+ __esModule: true,
38
+ BuildTimer: jest.fn().mockImplementation(() => ({
39
+ finish: mockFinishFn,
40
+ setPageCount: mockSetPageCountFn,
41
+ setHtmlSize: mockSetHtmlSizeFn,
42
+ })),
43
+ }));
44
+ jest.mock('../../src/core/parser', () => ({ __esModule: true, parseHTML: mockParseHTMLFn }));
45
+ jest.mock('../../src/core/extractor', () => ({ __esModule: true, extractAssets: mockExtractAssetsFn }));
46
+ jest.mock('../../src/core/minifier', () => ({ __esModule: true, minifyAssets: mockMinifyAssetsFn }));
47
+ jest.mock('../../src/core/packer', () => ({ __esModule: true, packHTML: mockPackHTMLFn }));
48
+ jest.mock('../../src/core/web-fetcher', () => ({
49
+ __esModule: true,
50
+ fetchAndPackWebPage: mockFetchAndPackWebPageFn,
51
+ recursivelyBundleSite: mockRecursivelyBundleSiteFn,
52
+ }));
53
+ jest.mock('../../src/core/bundler', () => ({ __esModule: true, bundleMultiPageHTML: mockBundleMultiPageHTMLFn }));
54
+
55
+
56
+ // --- IMPORT MODULES ---
57
+ import { BuildTimer } from '../../src/utils/meta';
58
+ import { generatePortableHTML, generateRecursivePortableHTML, pack } from '../../src/index';
59
+
60
+ // --- TEST SETUP ---
69
61
  describe('📦 PortaPack Index (Public API)', () => {
70
-
71
- // --- Test Data ---
72
- const mockLocalInput = 'local/index.html';
73
- const mockRemoteInput = 'https://example.com/page';
74
- const mockInvalidUrl = 'ftp://invalid.com';
75
- const mockHtmlContent = '<html><body>Mock Content</body></html>';
76
- const mockBundledHtml = '<html><template>...</template></html>';
77
- const mockPages: PageEntry[] = [{ url: 'page1.html', html: '<p>Page 1</p>' }, { url: 'page2.html', html: '<p>Page 2</p>' }];
78
-
79
- // Logger instance and spies
80
- let logger: Logger;
81
- let loggerInfoSpy: jest.SpiedFunction<Logger['info']>;
82
- let loggerErrorSpy: jest.SpiedFunction<Logger['error']>;
83
- let loggerWarnSpy: jest.SpiedFunction<Logger['warn']>;
84
- let loggerDebugSpy: jest.SpiedFunction<Logger['debug']>;
85
-
86
- // --- Mock Results for Core Functions ---
87
- const parsedResult: ParsedHTML = { htmlContent: '<raw>', assets: [{ type: 'css', url: 'style.css' }] };
88
- const extractedResult: ParsedHTML = { htmlContent: '<raw>', assets: [{ type: 'css', url: 'style.css', content: 'body{}' }] };
89
- const minifiedResult: ParsedHTML = { htmlContent: '<raw>', assets: [{ type: 'css', url: 'style.css', content: 'body{}' }] };
90
- const packedResult = mockHtmlContent;
91
- // FIX 1: Ensure mock metadata conforms to BundleMetadata where needed by mocks/spies
92
- const coreFetchMetadata: BundleMetadata = { input: mockRemoteInput, assetCount: 5, outputSize: 500, buildTimeMs: 50, errors: ['Fetch warning'] };
93
- const coreFetchResult = { html: mockHtmlContent, metadata: coreFetchMetadata }; // Use full metadata
94
-
95
- // Note: recursive result metadata 'pages' isn't part of BundleMetadata, use pagesBundled
96
- const coreRecursiveMetadata: BundleMetadata = { input: mockRemoteInput, assetCount: 0, outputSize: 600, buildTimeMs: 60, errors: ['Crawl warning'], pagesBundled: 3 };
97
- const coreRecursiveResult = { html: mockBundledHtml, pages: 3, metadata: coreRecursiveMetadata }; // pages is separate from metadata
98
-
99
- const coreBundledResult = mockBundledHtml;
100
-
101
- // Base metadata structure for finish calls
102
- const baseMockFinalMetadata: Omit<BundleMetadata, 'input'> = { assetCount: 0, outputSize: 0, buildTimeMs: 100, errors: [], pagesBundled: undefined };
103
- type FinishMetaArg = Parameters<BuildTimer['finish']>[1];
62
+ const logger = new Logger(LogLevel.INFO);
63
+ const mockHtmlPath = (global as any).__TEST_DIRECTORIES__?.sampleProject
64
+ ? path.join((global as any).__TEST_DIRECTORIES__.sampleProject, 'index.html')
65
+ : 'local/index.html';
66
+ const mockRemoteUrl = 'https://example.com';
67
+ const mockOutputPath = 'test-output.html'; // Define an output path
68
+
69
+ const mockParsed: ParsedHTML = { htmlContent: '<html><body>Mock Parsed</body></html>', assets: [] };
70
+ const mockPacked = '<html><body>packed!</body></html>';
71
+ const mockBundledHtml = mockPacked;
72
+
73
+ // Base metadata without input/outputFile
74
+ const baseMetadata: Omit<BundleMetadata, 'input'> = { // FIX: Removed outputFile
75
+ assetCount: 0, outputSize: mockPacked.length, buildTimeMs: 100, errors: [], pagesBundled: undefined
76
+ };
77
+ // Define expected metadata per test case
78
+ let expectedLocalMetadata: BundleMetadata;
79
+ let expectedRemoteMetadata: BundleMetadata;
80
+ let expectedRecursiveMetadata: BundleMetadata;
104
81
 
105
82
 
106
83
  beforeEach(() => {
107
- // Clear all mocks before each test - this includes the BuildTimer constructor mock
108
84
  jest.clearAllMocks();
109
- // --- FIX: Removed explicit .mockClear() on BuildTimer ---
110
- // (BuildTimer as jest.Mock).mockClear(); // REMOVED
111
-
112
- // --- Mock BuildTimer Prototype Methods ---
113
- mockFinish = jest.fn<FinishSig>();
114
- mockSetPageCount = jest.fn<SetPageCountSig>();
115
- // Assign mocks to the prototype of the *mocked* BuildTimer
116
- BuildTimer.prototype.finish = mockFinish;
117
- BuildTimer.prototype.setPageCount = mockSetPageCount;
118
- // ---------------------------------------
119
-
120
- // Configure the default return value of the mock 'finish' method
121
- mockFinish.mockImplementation(
122
- (html?: string, meta?: FinishMetaArg): BundleMetadata => ({
123
- input: 'placeholder-input', // Tests needing specific input should override via mockReturnValueOnce
124
- outputSize: html?.length ?? 0,
125
- buildTimeMs: 100,
126
- assetCount: meta?.assetCount ?? 0,
127
- pagesBundled: meta?.pagesBundled,
128
- errors: meta?.errors ?? []
129
- })
130
- );
131
85
 
132
- // --- Setup Spies on Imported Core Functions ---
133
- parseHTMLSpy = jest.spyOn(parser, 'parseHTML').mockResolvedValue(parsedResult);
134
- extractAssetsSpy = jest.spyOn(extractor, 'extractAssets').mockResolvedValue(extractedResult);
135
- minifyAssetsSpy = jest.spyOn(minifier, 'minifyAssets').mockResolvedValue(minifiedResult);
136
- packHTMLSpy = jest.spyOn(packer, 'packHTML').mockReturnValue(packedResult);
137
- // Ensure spies resolve with correctly typed values
138
- coreFetchAndPackSpy = jest.spyOn(webFetcher, 'fetchAndPackWebPage').mockResolvedValue(coreFetchResult);
139
- coreRecursivelyBundleSiteSpy = jest.spyOn(webFetcher, 'recursivelyBundleSite').mockResolvedValue(coreRecursiveResult);
140
- coreBundleMultiPageHTMLSpy = jest.spyOn(bundler, 'bundleMultiPageHTML').mockReturnValue(coreBundledResult);
141
-
142
- // Setup logger spies
143
- logger = new Logger(LogLevel.DEBUG);
144
- loggerInfoSpy = jest.spyOn(logger, 'info');
145
- loggerErrorSpy = jest.spyOn(logger, 'error');
146
- loggerWarnSpy = jest.spyOn(logger, 'warn');
147
- loggerDebugSpy = jest.spyOn(logger, 'debug');
148
- });
149
-
150
- afterEach(() => {
151
- // Restore original implementations spied on by jest.spyOn
152
- jest.restoreAllMocks();
153
- });
154
-
155
- // ========================================
156
- // Tests (These should now pass with correct mocks/spies)
157
- // ========================================
158
- describe('generatePortableHTML()', () => {
159
-
160
- it('✅ handles local files: calls parser, extractor, minifier, packer', async () => {
161
- const options: BundleOptions = { embedAssets: true, minifyHtml: true, minifyCss: true, minifyJs: true };
162
- const expectedMetadata: BundleMetadata = { ...baseMockFinalMetadata, input: mockLocalInput, assetCount: minifiedResult.assets.length, outputSize: packedResult.length };
163
- mockFinish.mockReturnValueOnce(expectedMetadata); // Configure specific finish return
164
-
165
- const result = await PortaPack.generatePortableHTML(mockLocalInput, options, logger);
166
-
167
- // Verify spies were called
168
- expect(parseHTMLSpy).toHaveBeenCalledWith(mockLocalInput, logger);
169
- expect(extractAssetsSpy).toHaveBeenCalledWith(parsedResult, true, mockLocalInput, logger);
170
- expect(minifyAssetsSpy).toHaveBeenCalledWith(extractedResult, options, logger);
171
- expect(packHTMLSpy).toHaveBeenCalledWith(minifiedResult, logger);
172
-
173
- // Verify BuildTimer interactions
174
- expect(BuildTimer).toHaveBeenCalledTimes(1); // Constructor called
175
- expect(BuildTimer).toHaveBeenCalledWith(mockLocalInput); // Constructor args
176
- expect(mockFinish).toHaveBeenCalledTimes(1); // Instance method called
177
- expect(mockFinish).toHaveBeenCalledWith(packedResult, { assetCount: minifiedResult.assets.length }); // Instance method args
178
-
179
- // Verify result
180
- expect(result.html).toBe(packedResult);
181
- expect(result.metadata).toEqual(expectedMetadata);
182
- // ... logging checks ...
86
+ // Re-initialize mutable mock functions
87
+ expectedLocalMetadata = { ...baseMetadata, input: mockHtmlPath };
88
+ mockFinishFn = jest.fn().mockReturnValue(expectedLocalMetadata); // Default return
89
+ mockSetPageCountFn = jest.fn();
90
+ mockSetHtmlSizeFn.mockClear();
91
+
92
+ // --- Configure mocks using CASTS ('as any') before configuration methods ---
93
+ (mockParseHTMLFn as any).mockImplementation(() => Promise.resolve(mockParsed));
94
+ (mockExtractAssetsFn as any).mockImplementation(() => Promise.resolve(mockParsed));
95
+ (mockMinifyAssetsFn as any).mockImplementation(() => Promise.resolve(mockParsed));
96
+ (mockPackHTMLFn as any).mockReturnValue(mockPacked);
97
+
98
+ // FIX: Add string type hint and cast before mockImplementation
99
+ (mockFetchAndPackWebPageFn as any).mockImplementation(async (url: string) => {
100
+ // Return structure must match BuildResult
101
+ return Promise.resolve({ html: mockPacked, metadata: { ...baseMetadata, input: url } as BundleMetadata });
183
102
  });
184
103
 
185
- it('✅ handles local files: uses baseUrl from options for extraction', async () => {
186
- const options: BundleOptions = { baseUrl: 'http://example.com/base/' };
187
- // Configure mock finish return for this test if metadata is checked
188
- const expectedMetadata: BundleMetadata = { ...baseMockFinalMetadata, input: mockLocalInput, assetCount: minifiedResult.assets.length, outputSize: packedResult.length };
189
- mockFinish.mockReturnValueOnce(expectedMetadata);
190
-
191
- await PortaPack.generatePortableHTML(mockLocalInput, options, logger);
192
- expect(BuildTimer).toHaveBeenCalledWith(mockLocalInput);
193
- expect(extractAssetsSpy).toHaveBeenCalledWith(parsedResult, true, options.baseUrl, logger);
194
- // ... logging checks ...
195
- });
104
+ // FIX: Cast before mockImplementation
105
+ (mockRecursivelyBundleSiteFn as any).mockImplementation(
106
+ async (/* startUrl, outputFile, maxDepth, logger */) => {
107
+ mockSetPageCountFn(3);
108
+ // Return structure expected by generateRecursivePortableHTML
109
+ // FIX: Cast return value to satisfy Promise<never> if needed, although mockImplementation often avoids this
110
+ return Promise.resolve({ html: mockPacked, pages: 3 });
111
+ }
112
+ );
196
113
 
197
- it('✅ handles local files: uses default options correctly', async () => {
198
- const expectedMetadata: BundleMetadata = { ...baseMockFinalMetadata, input: mockLocalInput, assetCount: minifiedResult.assets.length, outputSize: packedResult.length };
199
- mockFinish.mockReturnValueOnce(expectedMetadata);
114
+ (mockBundleMultiPageHTMLFn as any).mockReturnValue(mockBundledHtml);
115
+ });
200
116
 
201
- await PortaPack.generatePortableHTML(mockLocalInput, {}, logger);
202
- expect(BuildTimer).toHaveBeenCalledWith(mockLocalInput);
203
- expect(extractAssetsSpy).toHaveBeenCalledWith(parsedResult, true, mockLocalInput, logger);
204
- expect(minifyAssetsSpy).toHaveBeenCalledWith(extractedResult, {}, logger);
117
+ // --- Tests for pack() ---
118
+ describe('pack()', () => {
119
+ it('✅ delegates to generatePortableHTML for local files', async () => {
120
+ expectedLocalMetadata = { ...baseMetadata, input: mockHtmlPath };
121
+ mockFinishFn.mockReturnValueOnce(expectedLocalMetadata);
122
+ // FIX: Use 'output' option
123
+ const result = await pack(mockHtmlPath, { output: mockOutputPath, loggerInstance: logger });
124
+ expect(mockParseHTMLFn).toHaveBeenCalledWith(mockHtmlPath, expect.any(Logger));
125
+ expect(result.html).toBe(mockPacked);
126
+ expect(result.metadata).toEqual(expectedLocalMetadata);
205
127
  });
206
128
 
207
- it('✅ handles local files: handles processing errors', async () => {
208
- const processingError = new Error('Minification failed');
209
- minifyAssetsSpy.mockRejectedValue(processingError); // Make the spy reject
129
+ it('✅ uses fetchAndPackWebPage for remote non-recursive input', async () => {
130
+ const remoteUrl = `${mockRemoteUrl}/page`;
131
+ expectedRemoteMetadata = { ...baseMetadata, input: remoteUrl };
132
+ mockFinishFn.mockReturnValueOnce(expectedRemoteMetadata);
133
+ // FIX: Use mockImplementationOnce with cast for specific return value
134
+ (mockFetchAndPackWebPageFn as any).mockImplementationOnce(async () => Promise.resolve({ html: mockPacked, metadata: { ...baseMetadata, input: remoteUrl } as BundleMetadata }));
210
135
 
211
- await expect(PortaPack.generatePortableHTML(mockLocalInput, {}, logger))
212
- .rejects.toThrow(processingError);
213
- expect(BuildTimer).toHaveBeenCalledWith(mockLocalInput);
214
- expect(loggerErrorSpy).toHaveBeenCalledWith(expect.stringContaining(`Error during local processing for ${mockLocalInput}: ${processingError.message}`));
215
- expect(mockFinish).not.toHaveBeenCalled();
216
- });
136
+ // FIX: Use 'output' option
137
+ const result = await pack(remoteUrl, { recursive: false, output: mockOutputPath, loggerInstance: logger });
217
138
 
218
- it('✅ handles remote URLs: delegates to fetchAndPackWebPage', async () => {
219
- const options: BundleOptions = { logLevel: LogLevel.ERROR };
220
- // Use the defined coreFetchResult which now has full metadata
221
- const publicFetchResult: BuildResult = { html: coreFetchResult.html, metadata: coreFetchResult.metadata };
222
- const mockPublicFetch = jest.spyOn(PortaPack, 'fetchAndPackWebPage')
223
- .mockResolvedValue(publicFetchResult);
139
+ expect(mockFetchAndPackWebPageFn).toHaveBeenCalledWith(remoteUrl, expect.any(Logger));
140
+ expect(result.html).toBe(mockPacked);
141
+ expect(result.metadata).toEqual(expectedRemoteMetadata);
142
+ });
224
143
 
225
- const result = await PortaPack.generatePortableHTML(mockRemoteInput, options, logger);
144
+ it('✅ uses recursivelyBundleSite for recursive input', async () => {
145
+ const remoteUrl = `${mockRemoteUrl}/site`;
146
+ expectedRecursiveMetadata = { ...baseMetadata, input: remoteUrl, pagesBundled: 3 };
147
+ mockFinishFn.mockReturnValueOnce(expectedRecursiveMetadata);
226
148
 
227
- expect(BuildTimer).toHaveBeenCalledWith(mockRemoteInput);
228
- expect(mockPublicFetch).toHaveBeenCalledWith(mockRemoteInput, options, logger);
229
- expect(result).toEqual(publicFetchResult);
230
- expect(loggerInfoSpy).toHaveBeenCalledWith(expect.stringContaining('Input is a remote URL'));
231
- expect(parseHTMLSpy).not.toHaveBeenCalled(); // Check spies not called
149
+ // FIX: Use 'output' option
150
+ const result = await pack(remoteUrl, { recursive: true, output: mockOutputPath, loggerInstance: logger });
232
151
 
233
- mockPublicFetch.mockRestore();
152
+ expect(mockRecursivelyBundleSiteFn).toHaveBeenCalledWith(remoteUrl, 'output.html', 1, expect.any(Logger));
153
+ expect(result.html).toBe(mockPacked);
154
+ expect(result.metadata).toEqual(expectedRecursiveMetadata);
234
155
  });
235
156
 
236
- it(' handles remote URLs: handles fetch errors', async () => {
237
- const fetchError = new Error('Network Error');
238
- const mockPublicFetch = jest.spyOn(PortaPack, 'fetchAndPackWebPage')
239
- .mockRejectedValue(fetchError);
240
- await expect(PortaPack.generatePortableHTML(mockRemoteInput, {}, logger))
241
- .rejects.toThrow(fetchError);
242
- expect(BuildTimer).toHaveBeenCalledWith(mockRemoteInput);
243
- expect(loggerErrorSpy).toHaveBeenCalledWith(expect.stringContaining(`Failed to fetch remote URL ${mockRemoteInput}: ${fetchError.message}`));
244
- mockPublicFetch.mockRestore();
245
- });
157
+ it(' uses custom recursion depth if provided', async () => {
158
+ const remoteUrl = `${mockRemoteUrl}/site`;
159
+ expectedRecursiveMetadata = { ...baseMetadata, input: remoteUrl, pagesBundled: 3 };
160
+ mockFinishFn.mockReturnValueOnce(expectedRecursiveMetadata);
161
+ // FIX: Use 'output' option
162
+ await pack(remoteUrl, { recursive: 5, output: mockOutputPath, loggerInstance: logger });
246
163
 
247
- it('✅ uses passed logger instance', async () => {
248
- const customLogger = new Logger(LogLevel.WARN);
249
- const customLoggerInfoSpy = jest.spyOn(customLogger, 'info');
250
- const publicFetchResult: BuildResult = { html: '', metadata: { ...baseMockFinalMetadata, input: mockRemoteInput }}; // Adjust metadata as needed
251
- const mockPublicFetch = jest.spyOn(PortaPack, 'fetchAndPackWebPage').mockResolvedValue(publicFetchResult);
252
-
253
- await PortaPack.generatePortableHTML(mockRemoteInput, {}, customLogger);
254
- expect(BuildTimer).toHaveBeenCalledWith(mockRemoteInput);
255
- expect(customLoggerInfoSpy).toHaveBeenCalledWith(expect.stringContaining('Generating portable HTML'));
256
- expect(mockPublicFetch).toHaveBeenCalledWith(mockRemoteInput, {}, customLogger);
257
- mockPublicFetch.mockRestore();
164
+ expect(mockRecursivelyBundleSiteFn).toHaveBeenCalledWith(remoteUrl, 'output.html', 5, expect.any(Logger));
258
165
  });
259
166
 
260
- it('✅ creates logger instance if none provided', async () => {
261
- const publicFetchResult: BuildResult = { html: '', metadata: { ...baseMockFinalMetadata, input: mockRemoteInput }};
262
- const mockPublicFetch = jest.spyOn(PortaPack, 'fetchAndPackWebPage').mockResolvedValue(publicFetchResult);
263
- await PortaPack.generatePortableHTML(mockRemoteInput);
264
- expect(BuildTimer).toHaveBeenCalledWith(mockRemoteInput);
265
- expect(mockPublicFetch).toHaveBeenCalledWith(mockRemoteInput, {}, expect.any(Logger));
266
- mockPublicFetch.mockRestore();
167
+ it('✅ throws on unsupported protocols (e.g., ftp)', async () => {
168
+ // FIX: Use 'output' option
169
+ await expect(pack('ftp://weird.site', { output: mockOutputPath })).rejects.toThrow(/unsupported protocol or input type/i);
170
+ // ... other assertions ...
267
171
  });
268
172
  });
269
173
 
270
- describe('generateRecursivePortableHTML()', () => {
271
- it('✅ calls coreRecursivelyBundleSite with correct parameters', async () => {
272
- const depth = 2;
273
- const options: BundleOptions = { logLevel: LogLevel.INFO };
274
- const expectedPlaceholder = `${new URL(mockRemoteInput).hostname}_recursive.html`;
275
- // Use the defined coreRecursiveResult which has full metadata
276
- const expectedMetadata: BundleMetadata = coreRecursiveResult.metadata;
277
- mockFinish.mockReturnValueOnce(expectedMetadata); // Configure finish mock return
278
-
279
- const result = await PortaPack.generateRecursivePortableHTML(mockRemoteInput, depth, options, logger);
280
-
281
- // Check the SPY on the CORE function
282
- expect(coreRecursivelyBundleSiteSpy).toHaveBeenCalledWith(mockRemoteInput, expectedPlaceholder, depth, logger);
283
- expect(BuildTimer).toHaveBeenCalledTimes(1);
284
- expect(BuildTimer).toHaveBeenCalledWith(mockRemoteInput);
285
- expect(mockSetPageCount).toHaveBeenCalledWith(coreRecursiveResult.pages);
286
- expect(mockFinish).toHaveBeenCalledWith(coreRecursiveResult.html, { assetCount: 0, pagesBundled: coreRecursiveResult.pages });
287
- expect(result.html).toBe(coreRecursiveResult.html);
288
- expect(result.metadata).toEqual(expectedMetadata);
289
- // ... logging checks ...
290
- });
291
174
 
292
- it('❌ throws error for invalid URL (non-http/s)', async () => { /* test as before */ });
293
- it('❌ handles errors from coreRecursivelyBundleSite', async () => { /* test as before, check coreRecursivelyBundleSiteSpy */ });
294
- it('✅ uses default depth of 1 if not specified', async () => { /* test as before, check coreRecursivelyBundleSiteSpy */ });
295
- it('✅ includes warnings from core metadata in final metadata', async () => { /* test as before, adjust mockRecursiveResult value */ });
296
- });
175
+ // --- Tests for generatePortableHTML() ---
176
+ describe('generatePortableHTML()', () => {
177
+ it('✅ should bundle local HTML with all core steps', async () => {
178
+ expectedLocalMetadata = { ...baseMetadata, input: mockHtmlPath };
179
+ mockFinishFn.mockReturnValueOnce(expectedLocalMetadata);
180
+ // FIX: Use 'output' option
181
+ const result = await generatePortableHTML(mockHtmlPath, { output: mockOutputPath }, logger);
182
+
183
+ expect(mockParseHTMLFn).toHaveBeenCalledWith(mockHtmlPath, logger);
184
+ expect(mockExtractAssetsFn).toHaveBeenCalledWith(mockParsed, true, mockHtmlPath, logger);
185
+ expect(mockMinifyAssetsFn).toHaveBeenCalledWith(mockParsed, { output: mockOutputPath }, logger);
186
+ expect(mockPackHTMLFn).toHaveBeenCalledWith(mockParsed, logger);
187
+ expect(mockFinishFn).toHaveBeenCalledWith(mockPacked, { assetCount: mockParsed.assets.length });
188
+ expect(result.html).toBe(mockPacked);
189
+ expect(result.metadata).toEqual(expectedLocalMetadata);
190
+ });
297
191
 
298
- describe('fetchAndPackWebPage()', () => {
299
- it('✅ calls coreFetchAndPack with correct URL and logger', async () => {
300
- const options: BundleOptions = { logLevel: LogLevel.WARN };
301
- // Use the defined coreFetchResult which has full metadata
302
- const expectedMetadata: BundleMetadata = coreFetchResult.metadata;
303
- mockFinish.mockReturnValueOnce(expectedMetadata);
304
-
305
- const result = await PortaPack.fetchAndPackWebPage(mockRemoteInput, options, logger);
306
-
307
- expect(coreFetchAndPackSpy).toHaveBeenCalledWith(mockRemoteInput, logger); // Check spy
308
- expect(BuildTimer).toHaveBeenCalledTimes(1);
309
- expect(BuildTimer).toHaveBeenCalledWith(mockRemoteInput);
310
- expect(mockFinish).toHaveBeenCalledWith(coreFetchResult.html, {
311
- assetCount: coreFetchResult.metadata?.assetCount ?? 0,
312
- errors: coreFetchResult.metadata?.errors ?? []
313
- });
314
- expect(result.html).toBe(coreFetchResult.html);
315
- expect(result.metadata).toEqual(expectedMetadata);
316
- // ... logging checks ...
192
+ it('✅ should call fetchAndPackWebPage for remote input', async () => {
193
+ const remoteUrl = `${mockRemoteUrl}/page2`;
194
+ expectedRemoteMetadata = { ...baseMetadata, input: remoteUrl };
195
+ mockFinishFn.mockReturnValueOnce(expectedRemoteMetadata);
196
+ // Configure fetch mock to return specific metadata
197
+ const fetcherReturnMetadata = { ...baseMetadata, input: remoteUrl };
198
+ // FIX: Cast needed before configuration method
199
+ (mockFetchAndPackWebPageFn as any).mockImplementationOnce(async () => Promise.resolve({ html: mockPacked, metadata: fetcherReturnMetadata }));
200
+
201
+ // FIX: Use 'output' option
202
+ const result = await generatePortableHTML(remoteUrl, { output: mockOutputPath }, logger);
203
+
204
+ expect(mockFetchAndPackWebPageFn).toHaveBeenCalledWith(remoteUrl, logger);
205
+ expect(mockFinishFn).toHaveBeenCalledWith(mockPacked, fetcherReturnMetadata);
206
+ expect(result.html).toBe(mockPacked);
207
+ expect(result.metadata).toEqual(expectedRemoteMetadata);
208
+ });
209
+
210
+ it('✅ should throw on bad input file', async () => {
211
+ const badPath = '/non/existent/file.html';
212
+ const mockError = new Error('File not found');
213
+ // FIX: Cast before mockImplementationOnce
214
+ (mockParseHTMLFn as any).mockImplementationOnce(() => Promise.reject(mockError));
215
+
216
+ // FIX: Use 'output' option
217
+ await expect(generatePortableHTML(badPath, { output: mockOutputPath }, logger)).rejects.toThrow(mockError);
218
+
219
+ expect(mockParseHTMLFn).toHaveBeenCalledWith(badPath, logger);
220
+ expect(mockFinishFn).not.toHaveBeenCalled();
221
+ });
222
+ });
223
+
224
+ // --- Tests for generateRecursivePortableHTML() ---
225
+ describe('generateRecursivePortableHTML()', () => {
226
+ it('✅ should handle recursive remote bundling', async () => {
227
+ const remoteUrl = `${mockRemoteUrl}/site2`;
228
+ expectedRecursiveMetadata = { ...baseMetadata, input: remoteUrl, pagesBundled: 3 };
229
+ mockFinishFn.mockReturnValueOnce(expectedRecursiveMetadata);
230
+ // Configure the core function mock return value for this test
231
+ // FIX: Cast before configuration method if needed for specific return
232
+ (mockRecursivelyBundleSiteFn as any).mockResolvedValueOnce({ html: mockPacked, pages: 3 });
233
+
234
+ // FIX: Use 'output' option
235
+ const result = await generateRecursivePortableHTML(remoteUrl, 2, { output: mockOutputPath }, logger);
236
+
237
+ expect(mockRecursivelyBundleSiteFn).toHaveBeenCalledWith(remoteUrl, 'output.html', 2, logger);
238
+ expect(mockSetPageCountFn).toHaveBeenCalledWith(3);
239
+ expect(mockFinishFn).toHaveBeenCalledWith(mockPacked, { assetCount: 0, pagesBundled: 3 });
240
+ expect(result.html).toBe(mockPacked);
241
+ expect(result.metadata).toEqual(expectedRecursiveMetadata);
317
242
  });
318
- it('❌ throws error for invalid URL (non-http/s)', async () => { /* test as before, check coreFetchAndPackSpy */ });
319
- it('❌ handles errors from coreFetchAndPack', async () => { /* test as before, check coreFetchAndPackSpy */ });
320
- it('✅ handles coreFetch result with missing metadata gracefully', async () => {
321
- // Make spy return undefined metadata
322
- coreFetchAndPackSpy.mockResolvedValueOnce({ html: coreFetchResult.html, metadata: { input: mockRemoteInput, assetCount: 0, outputSize: coreFetchResult.html.length, buildTimeMs: 0, errors: [] } });
323
- const expectedMetadata: BundleMetadata = { ...baseMockFinalMetadata, input: mockRemoteInput, assetCount: 0, outputSize: coreFetchResult.html.length, errors: [] };
324
- mockFinish.mockReturnValueOnce(expectedMetadata);
325
-
326
- const result = await PortaPack.fetchAndPackWebPage(mockRemoteInput, {}, logger);
327
- expect(BuildTimer).toHaveBeenCalledWith(mockRemoteInput);
328
- expect(mockFinish).toHaveBeenCalledWith(coreFetchResult.html, { assetCount: 0, errors: [] });
329
- expect(result.metadata).toEqual(expectedMetadata);
330
- });
331
243
  });
332
244
 
333
- describe('bundleMultiPageHTML()', () => {
334
- // Tests remain the same as they don't involve BuildTimer
335
- it('✅ calls coreBundleMultiPageHTML with pages and logger', () => { /* test as before, check coreBundleMultiPageHTMLSpy */ });
336
- it('❌ handles errors from coreBundleMultiPageHTML', () => { /* test as before, check coreBundleMultiPageHTMLSpy */ });
337
- it('✅ creates logger instance if none provided', () => { /* test as before, check coreBundleMultiPageHTMLSpy */ });
338
- });
339
245
  });