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
package/tests/unit/index.test.ts
CHANGED
@@ -1,339 +1,245 @@
|
|
1
1
|
// tests/unit/index.test.ts
|
2
2
|
|
3
|
-
import {
|
4
|
-
|
5
|
-
|
6
|
-
// Import types
|
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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
import {
|
18
|
-
|
19
|
-
|
20
|
-
// ---
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
72
|
-
|
73
|
-
|
74
|
-
const
|
75
|
-
const
|
76
|
-
|
77
|
-
const
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
//
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
198
|
-
|
199
|
-
mockFinish.mockReturnValueOnce(expectedMetadata);
|
114
|
+
(mockBundleMultiPageHTMLFn as any).mockReturnValue(mockBundledHtml);
|
115
|
+
});
|
200
116
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
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
|
-
|
208
|
-
const
|
209
|
-
|
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
|
-
|
212
|
-
|
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
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
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
|
-
|
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
|
-
|
228
|
-
|
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
|
-
|
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('
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
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
|
-
|
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('✅
|
261
|
-
|
262
|
-
|
263
|
-
|
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
|
-
|
293
|
-
|
294
|
-
it('✅
|
295
|
-
|
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
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
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
|
});
|