portapack 0.3.0 → 0.3.2

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.
Files changed (74) hide show
  1. package/.eslintrc.json +67 -8
  2. package/.github/workflows/ci.yml +5 -4
  3. package/.releaserc.js +25 -27
  4. package/CHANGELOG.md +12 -19
  5. package/LICENSE.md +21 -0
  6. package/README.md +34 -36
  7. package/commitlint.config.js +30 -34
  8. package/dist/cli/cli-entry.cjs +199 -135
  9. package/dist/cli/cli-entry.cjs.map +1 -1
  10. package/dist/index.d.ts +0 -3
  11. package/dist/index.js +194 -134
  12. package/dist/index.js.map +1 -1
  13. package/docs/.vitepress/config.ts +36 -34
  14. package/docs/.vitepress/sidebar-generator.ts +89 -38
  15. package/docs/cli.md +29 -82
  16. package/docs/code-of-conduct.md +7 -1
  17. package/docs/configuration.md +103 -117
  18. package/docs/contributing.md +6 -2
  19. package/docs/deployment.md +10 -5
  20. package/docs/development.md +8 -5
  21. package/docs/getting-started.md +76 -45
  22. package/docs/index.md +1 -1
  23. package/docs/public/android-chrome-192x192.png +0 -0
  24. package/docs/public/android-chrome-512x512.png +0 -0
  25. package/docs/public/apple-touch-icon.png +0 -0
  26. package/docs/public/favicon-16x16.png +0 -0
  27. package/docs/public/favicon-32x32.png +0 -0
  28. package/docs/public/favicon.ico +0 -0
  29. package/docs/site.webmanifest +1 -0
  30. package/docs/troubleshooting.md +12 -1
  31. package/examples/main.ts +7 -10
  32. package/examples/sample-project/script.js +1 -1
  33. package/jest.config.ts +8 -13
  34. package/nodemon.json +5 -10
  35. package/package.json +2 -5
  36. package/src/cli/cli-entry.ts +2 -2
  37. package/src/cli/cli.ts +21 -16
  38. package/src/cli/options.ts +127 -113
  39. package/src/core/bundler.ts +254 -221
  40. package/src/core/extractor.ts +639 -520
  41. package/src/core/minifier.ts +173 -162
  42. package/src/core/packer.ts +141 -137
  43. package/src/core/parser.ts +74 -73
  44. package/src/core/web-fetcher.ts +270 -258
  45. package/src/index.ts +18 -17
  46. package/src/types.ts +9 -11
  47. package/src/utils/font.ts +12 -6
  48. package/src/utils/logger.ts +110 -105
  49. package/src/utils/meta.ts +75 -76
  50. package/src/utils/mime.ts +50 -50
  51. package/src/utils/slugify.ts +33 -34
  52. package/tests/unit/cli/cli-entry.test.ts +72 -70
  53. package/tests/unit/cli/cli.test.ts +314 -278
  54. package/tests/unit/cli/options.test.ts +294 -301
  55. package/tests/unit/core/bundler.test.ts +426 -329
  56. package/tests/unit/core/extractor.test.ts +828 -380
  57. package/tests/unit/core/minifier.test.ts +374 -274
  58. package/tests/unit/core/packer.test.ts +298 -264
  59. package/tests/unit/core/parser.test.ts +538 -150
  60. package/tests/unit/core/web-fetcher.test.ts +389 -359
  61. package/tests/unit/index.test.ts +238 -197
  62. package/tests/unit/utils/font.test.ts +26 -21
  63. package/tests/unit/utils/logger.test.ts +267 -260
  64. package/tests/unit/utils/meta.test.ts +29 -28
  65. package/tests/unit/utils/mime.test.ts +73 -74
  66. package/tests/unit/utils/slugify.test.ts +14 -12
  67. package/tsconfig.build.json +9 -10
  68. package/tsconfig.jest.json +2 -1
  69. package/tsconfig.json +2 -2
  70. package/tsup.config.ts +8 -8
  71. package/typedoc.json +5 -9
  72. package/docs/demo.md +0 -46
  73. /package/docs/{portapack-transparent.png → public/portapack-transparent.png} +0 -0
  74. /package/docs/{portapack.jpg → public/portapack.jpg} +0 -0
@@ -1,17 +1,20 @@
1
- // tests/unit/index.test.ts
1
+ /**
2
+ * @file tests/unit/index.test.ts
3
+ * @description Unit tests for the API / functionality of PortaPack.
4
+ */
2
5
 
3
6
  import { describe, it, expect, jest, beforeEach } from '@jest/globals';
4
7
  import path from 'path';
5
8
 
6
9
  // --- Import necessary types ---
7
10
  import type {
8
- ParsedHTML,
9
- Asset,
10
- BundleOptions,
11
- PageEntry,
12
- BundleMetadata,
13
- BuildResult,
14
- // PackOptions, // Defined inline in index.ts, not exported here
11
+ ParsedHTML,
12
+ Asset,
13
+ BundleOptions,
14
+ PageEntry,
15
+ BundleMetadata,
16
+ BuildResult,
17
+ // PackOptions, // Defined inline in index.ts, not exported here
15
18
  } from '../../src/types';
16
19
  import { LogLevel } from '../../src/types';
17
20
  import { Logger } from '../../src/utils/logger';
@@ -34,24 +37,32 @@ const mockSetHtmlSizeFn = jest.fn();
34
37
 
35
38
  // --- Explicitly Mock Modules with Factories ---
36
39
  jest.mock('../../src/utils/meta', () => ({
37
- __esModule: true,
38
- BuildTimer: jest.fn().mockImplementation(() => ({
39
- finish: mockFinishFn,
40
- setPageCount: mockSetPageCountFn,
41
- setHtmlSize: mockSetHtmlSizeFn,
42
- })),
40
+ __esModule: true,
41
+ BuildTimer: jest.fn().mockImplementation(() => ({
42
+ finish: mockFinishFn,
43
+ setPageCount: mockSetPageCountFn,
44
+ setHtmlSize: mockSetHtmlSizeFn,
45
+ })),
43
46
  }));
44
47
  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 }));
48
+ jest.mock('../../src/core/extractor', () => ({
49
+ __esModule: true,
50
+ extractAssets: mockExtractAssetsFn,
51
+ }));
52
+ jest.mock('../../src/core/minifier', () => ({
53
+ __esModule: true,
54
+ minifyAssets: mockMinifyAssetsFn,
55
+ }));
47
56
  jest.mock('../../src/core/packer', () => ({ __esModule: true, packHTML: mockPackHTMLFn }));
48
57
  jest.mock('../../src/core/web-fetcher', () => ({
49
- __esModule: true,
50
- fetchAndPackWebPage: mockFetchAndPackWebPageFn,
51
- recursivelyBundleSite: mockRecursivelyBundleSiteFn,
58
+ __esModule: true,
59
+ fetchAndPackWebPage: mockFetchAndPackWebPageFn,
60
+ recursivelyBundleSite: mockRecursivelyBundleSiteFn,
61
+ }));
62
+ jest.mock('../../src/core/bundler', () => ({
63
+ __esModule: true,
64
+ bundleMultiPageHTML: mockBundleMultiPageHTMLFn,
52
65
  }));
53
- jest.mock('../../src/core/bundler', () => ({ __esModule: true, bundleMultiPageHTML: mockBundleMultiPageHTMLFn }));
54
-
55
66
 
56
67
  // --- IMPORT MODULES ---
57
68
  import { BuildTimer } from '../../src/utils/meta';
@@ -59,187 +70,217 @@ import { generatePortableHTML, generateRecursivePortableHTML, pack } from '../..
59
70
 
60
71
  // --- TEST SETUP ---
61
72
  describe('📦 PortaPack Index (Public API)', () => {
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;
81
-
82
-
83
- beforeEach(() => {
84
- jest.clearAllMocks();
85
-
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 });
102
- });
103
-
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
- );
113
-
114
- (mockBundleMultiPageHTMLFn as any).mockReturnValue(mockBundledHtml);
73
+ const logger = new Logger(LogLevel.INFO);
74
+ const mockHtmlPath = (global as any).__TEST_DIRECTORIES__?.sampleProject
75
+ ? path.join((global as any).__TEST_DIRECTORIES__.sampleProject, 'index.html')
76
+ : 'local/index.html';
77
+ const mockRemoteUrl = 'https://example.com';
78
+ const mockOutputPath = 'test-output.html'; // Define an output path
79
+
80
+ const mockParsed: ParsedHTML = {
81
+ htmlContent: '<html><body>Mock Parsed</body></html>',
82
+ assets: [],
83
+ };
84
+ const mockPacked = '<html><body>packed!</body></html>';
85
+ const mockBundledHtml = mockPacked;
86
+
87
+ // Base metadata without input/outputFile
88
+ const baseMetadata: Omit<BundleMetadata, 'input'> = {
89
+ assetCount: 0,
90
+ outputSize: mockPacked.length,
91
+ buildTimeMs: 100,
92
+ errors: [],
93
+ pagesBundled: undefined,
94
+ };
95
+ // Define expected metadata per test case
96
+ let expectedLocalMetadata: BundleMetadata;
97
+ let expectedRemoteMetadata: BundleMetadata;
98
+ let expectedRecursiveMetadata: BundleMetadata;
99
+
100
+ beforeEach(() => {
101
+ jest.clearAllMocks();
102
+
103
+ // Re-initialize mutable mock functions
104
+ expectedLocalMetadata = { ...baseMetadata, input: mockHtmlPath };
105
+ mockFinishFn = jest.fn().mockReturnValue(expectedLocalMetadata); // Default return
106
+ mockSetPageCountFn = jest.fn();
107
+ mockSetHtmlSizeFn.mockClear();
108
+
109
+ (mockParseHTMLFn as any).mockImplementation(() => Promise.resolve(mockParsed));
110
+ (mockExtractAssetsFn as any).mockImplementation(() => Promise.resolve(mockParsed));
111
+ (mockMinifyAssetsFn as any).mockImplementation(() => Promise.resolve(mockParsed));
112
+ (mockPackHTMLFn as any).mockReturnValue(mockPacked);
113
+
114
+ (mockFetchAndPackWebPageFn as any).mockImplementation(async (url: string) => {
115
+ // Return structure must match BuildResult
116
+ return Promise.resolve({
117
+ html: mockPacked,
118
+ metadata: { ...baseMetadata, input: url } as BundleMetadata,
119
+ });
120
+ });
121
+
122
+ (mockRecursivelyBundleSiteFn as any).mockImplementation(
123
+ async (/* startUrl, outputFile, maxDepth, logger */) => {
124
+ mockSetPageCountFn(3);
125
+ // Return structure expected by generateRecursivePortableHTML
126
+ return Promise.resolve({ html: mockPacked, pages: 3 });
127
+ }
128
+ );
129
+
130
+ (mockBundleMultiPageHTMLFn as any).mockReturnValue(mockBundledHtml);
131
+ });
132
+
133
+ // --- Tests for pack() ---
134
+ describe('pack()', () => {
135
+ it('✅ delegates to generatePortableHTML for local files', async () => {
136
+ expectedLocalMetadata = { ...baseMetadata, input: mockHtmlPath };
137
+ mockFinishFn.mockReturnValueOnce(expectedLocalMetadata);
138
+ const result = await pack(mockHtmlPath, { output: mockOutputPath, loggerInstance: logger });
139
+ expect(mockParseHTMLFn).toHaveBeenCalledWith(mockHtmlPath, expect.any(Logger));
140
+ expect(result.html).toBe(mockPacked);
141
+ expect(result.metadata).toEqual(expectedLocalMetadata);
142
+ });
143
+
144
+ it('✅ uses fetchAndPackWebPage for remote non-recursive input', async () => {
145
+ const remoteUrl = `${mockRemoteUrl}/page`;
146
+ expectedRemoteMetadata = { ...baseMetadata, input: remoteUrl };
147
+ mockFinishFn.mockReturnValueOnce(expectedRemoteMetadata);
148
+ (mockFetchAndPackWebPageFn as any).mockImplementationOnce(async () =>
149
+ Promise.resolve({
150
+ html: mockPacked,
151
+ metadata: { ...baseMetadata, input: remoteUrl } as BundleMetadata,
152
+ })
153
+ );
154
+
155
+ const result = await pack(remoteUrl, {
156
+ recursive: false,
157
+ output: mockOutputPath,
158
+ loggerInstance: logger,
159
+ });
160
+
161
+ expect(mockFetchAndPackWebPageFn).toHaveBeenCalledWith(remoteUrl, expect.any(Logger));
162
+ expect(result.html).toBe(mockPacked);
163
+ expect(result.metadata).toEqual(expectedRemoteMetadata);
115
164
  });
116
165
 
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);
127
- });
128
-
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 }));
135
-
136
- // FIX: Use 'output' option
137
- const result = await pack(remoteUrl, { recursive: false, output: mockOutputPath, loggerInstance: logger });
138
-
139
- expect(mockFetchAndPackWebPageFn).toHaveBeenCalledWith(remoteUrl, expect.any(Logger));
140
- expect(result.html).toBe(mockPacked);
141
- expect(result.metadata).toEqual(expectedRemoteMetadata);
142
- });
143
-
144
- it('✅ uses recursivelyBundleSite for recursive input', async () => {
145
- const remoteUrl = `${mockRemoteUrl}/site`;
146
- expectedRecursiveMetadata = { ...baseMetadata, input: remoteUrl, pagesBundled: 3 };
147
- mockFinishFn.mockReturnValueOnce(expectedRecursiveMetadata);
148
-
149
- // FIX: Use 'output' option
150
- const result = await pack(remoteUrl, { recursive: true, output: mockOutputPath, loggerInstance: logger });
151
-
152
- expect(mockRecursivelyBundleSiteFn).toHaveBeenCalledWith(remoteUrl, 'output.html', 1, expect.any(Logger));
153
- expect(result.html).toBe(mockPacked);
154
- expect(result.metadata).toEqual(expectedRecursiveMetadata);
155
- });
156
-
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 });
163
-
164
- expect(mockRecursivelyBundleSiteFn).toHaveBeenCalledWith(remoteUrl, 'output.html', 5, expect.any(Logger));
165
- });
166
-
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 ...
171
- });
166
+ it('✅ uses recursivelyBundleSite for recursive input', async () => {
167
+ const remoteUrl = `${mockRemoteUrl}/site`;
168
+ expectedRecursiveMetadata = { ...baseMetadata, input: remoteUrl, pagesBundled: 3 };
169
+ mockFinishFn.mockReturnValueOnce(expectedRecursiveMetadata);
170
+
171
+ const result = await pack(remoteUrl, {
172
+ recursive: true,
173
+ output: mockOutputPath,
174
+ loggerInstance: logger,
175
+ });
176
+
177
+ expect(mockRecursivelyBundleSiteFn).toHaveBeenCalledWith(
178
+ remoteUrl,
179
+ 'output.html',
180
+ 1,
181
+ expect.any(Logger)
182
+ );
183
+ expect(result.html).toBe(mockPacked);
184
+ expect(result.metadata).toEqual(expectedRecursiveMetadata);
172
185
  });
173
186
 
187
+ it('✅ uses custom recursion depth if provided', async () => {
188
+ const remoteUrl = `${mockRemoteUrl}/site`;
189
+ expectedRecursiveMetadata = { ...baseMetadata, input: remoteUrl, pagesBundled: 3 };
190
+ mockFinishFn.mockReturnValueOnce(expectedRecursiveMetadata);
191
+ await pack(remoteUrl, { recursive: 5, output: mockOutputPath, loggerInstance: logger });
192
+
193
+ expect(mockRecursivelyBundleSiteFn).toHaveBeenCalledWith(
194
+ remoteUrl,
195
+ 'output.html',
196
+ 5,
197
+ expect.any(Logger)
198
+ );
199
+ });
174
200
 
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
- });
191
-
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();
201
+ it('✅ throws on unsupported protocols (e.g., ftp)', async () => {
202
+ await expect(pack('ftp://weird.site', { output: mockOutputPath })).rejects.toThrow(
203
+ /unsupported protocol or input type/i
204
+ );
205
+ });
206
+ });
207
+
208
+ // --- Tests for generatePortableHTML() ---
209
+ describe('generatePortableHTML()', () => {
210
+ it('✅ should bundle local HTML with all core steps', async () => {
211
+ expectedLocalMetadata = { ...baseMetadata, input: mockHtmlPath };
212
+ mockFinishFn.mockReturnValueOnce(expectedLocalMetadata);
213
+ const result = await generatePortableHTML(mockHtmlPath, { output: mockOutputPath }, logger);
214
+
215
+ expect(mockParseHTMLFn).toHaveBeenCalledWith(mockHtmlPath, logger);
216
+ expect(mockExtractAssetsFn).toHaveBeenCalledWith(mockParsed, true, mockHtmlPath, logger);
217
+ expect(mockMinifyAssetsFn).toHaveBeenCalledWith(
218
+ mockParsed,
219
+ { output: mockOutputPath },
220
+ logger
221
+ );
222
+ expect(mockPackHTMLFn).toHaveBeenCalledWith(mockParsed, logger);
223
+ expect(mockFinishFn).toHaveBeenCalledWith(mockPacked, {
224
+ assetCount: mockParsed.assets.length,
221
225
  });
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);
242
- });
226
+ expect(result.html).toBe(mockPacked);
227
+ expect(result.metadata).toEqual(expectedLocalMetadata);
243
228
  });
244
229
 
245
- });
230
+ it('✅ should call fetchAndPackWebPage for remote input', async () => {
231
+ const remoteUrl = `${mockRemoteUrl}/page2`;
232
+ expectedRemoteMetadata = { ...baseMetadata, input: remoteUrl };
233
+ mockFinishFn.mockReturnValueOnce(expectedRemoteMetadata);
234
+ // Configure fetch mock to return specific metadata
235
+ const fetcherReturnMetadata = { ...baseMetadata, input: remoteUrl };
236
+ (mockFetchAndPackWebPageFn as any).mockImplementationOnce(async () =>
237
+ Promise.resolve({ html: mockPacked, metadata: fetcherReturnMetadata })
238
+ );
239
+
240
+ const result = await generatePortableHTML(remoteUrl, { output: mockOutputPath }, logger);
241
+
242
+ expect(mockFetchAndPackWebPageFn).toHaveBeenCalledWith(remoteUrl, logger);
243
+ expect(mockFinishFn).toHaveBeenCalledWith(mockPacked, fetcherReturnMetadata);
244
+ expect(result.html).toBe(mockPacked);
245
+ expect(result.metadata).toEqual(expectedRemoteMetadata);
246
+ });
247
+
248
+ it('✅ should throw on bad input file', async () => {
249
+ const badPath = '/non/existent/file.html';
250
+ const mockError = new Error('File not found');
251
+ (mockParseHTMLFn as any).mockImplementationOnce(() => Promise.reject(mockError));
252
+
253
+ // Use 'output' option
254
+ await expect(
255
+ generatePortableHTML(badPath, { output: mockOutputPath }, logger)
256
+ ).rejects.toThrow(mockError);
257
+
258
+ expect(mockParseHTMLFn).toHaveBeenCalledWith(badPath, logger);
259
+ expect(mockFinishFn).not.toHaveBeenCalled();
260
+ });
261
+ });
262
+
263
+ // --- Tests for generateRecursivePortableHTML() ---
264
+ describe('generateRecursivePortableHTML()', () => {
265
+ it('✅ should handle recursive remote bundling', async () => {
266
+ const remoteUrl = `${mockRemoteUrl}/site2`;
267
+ expectedRecursiveMetadata = { ...baseMetadata, input: remoteUrl, pagesBundled: 3 };
268
+ mockFinishFn.mockReturnValueOnce(expectedRecursiveMetadata);
269
+ // Configure the core function mock return value for this test
270
+ (mockRecursivelyBundleSiteFn as any).mockResolvedValueOnce({ html: mockPacked, pages: 3 });
271
+
272
+ const result = await generateRecursivePortableHTML(
273
+ remoteUrl,
274
+ 2,
275
+ { output: mockOutputPath },
276
+ logger
277
+ );
278
+
279
+ expect(mockRecursivelyBundleSiteFn).toHaveBeenCalledWith(remoteUrl, 'output.html', 2, logger);
280
+ expect(mockSetPageCountFn).toHaveBeenCalledWith(3);
281
+ expect(mockFinishFn).toHaveBeenCalledWith(mockPacked, { assetCount: 0, pagesBundled: 3 });
282
+ expect(result.html).toBe(mockPacked);
283
+ expect(result.metadata).toEqual(expectedRecursiveMetadata);
284
+ });
285
+ });
286
+ });
@@ -1,30 +1,36 @@
1
- // tests/unit/utils/font.test.ts
1
+ /**
2
+ * @file tests/unit/utils/font.test.ts
3
+ * @description Unit tests for the font utils.
4
+ */
2
5
 
3
6
  import { describe, it, expect, jest, beforeEach, beforeAll } from '@jest/globals';
4
7
 
5
8
  // Import only synchronous functions or types needed outside the async describe block
6
9
  import { getFontMimeType /*, encodeFontToDataURI */ } from '../../../src/utils/font'; // Commented out async import
7
10
 
8
-
9
11
  describe('🖋️ Font Utils', () => {
10
-
11
- // Tests for the synchronous function can remain outside
12
- describe('getFontMimeType()', () => {
13
- it('returns correct MIME for common formats', () => {
14
- expect(getFontMimeType('font.woff')).toBe('font/woff');
15
- expect(getFontMimeType('font.woff2')).toBe('font/woff2');
16
- expect(getFontMimeType('font.ttf')).toBe('font/ttf');
17
- expect(getFontMimeType('font.otf')).toBe('font/otf');
18
- expect(getFontMimeType('font.eot')).toBe('application/vnd.ms-fontobject');
19
- expect(getFontMimeType('font.svg')).toBe('application/octet-stream'); // Default
20
- });
21
- it('handles uppercase extensions', () => { expect(getFontMimeType('font.WOFF2')).toBe('font/woff2'); /* etc */ });
22
- it('handles file paths correctly', () => { expect(getFontMimeType('/path/to/font.woff2')).toBe('font/woff2'); /* etc */ });
23
- it('returns octet-stream for unknown or missing extensions', () => { expect(getFontMimeType('font.xyz')).toBe('application/octet-stream'); /* etc */ });
12
+ // Tests for the synchronous function can remain outside
13
+ describe('getFontMimeType()', () => {
14
+ it('returns correct MIME for common formats', () => {
15
+ expect(getFontMimeType('font.woff')).toBe('font/woff');
16
+ expect(getFontMimeType('font.woff2')).toBe('font/woff2');
17
+ expect(getFontMimeType('font.ttf')).toBe('font/ttf');
18
+ expect(getFontMimeType('font.otf')).toBe('font/otf');
19
+ expect(getFontMimeType('font.eot')).toBe('application/vnd.ms-fontobject');
20
+ expect(getFontMimeType('font.svg')).toBe('application/octet-stream'); // Default
21
+ });
22
+ it('handles uppercase extensions', () => {
23
+ expect(getFontMimeType('font.WOFF2')).toBe('font/woff2'); /* etc */
24
24
  });
25
+ it('handles file paths correctly', () => {
26
+ expect(getFontMimeType('/path/to/font.woff2')).toBe('font/woff2'); /* etc */
27
+ });
28
+ it('returns octet-stream for unknown or missing extensions', () => {
29
+ expect(getFontMimeType('font.xyz')).toBe('application/octet-stream'); /* etc */
30
+ });
31
+ });
25
32
 
26
- // --- FIX: Comment out the entire describe block for the failing async function ---
27
- /*
33
+ /*
28
34
  describe('encodeFontToDataURI()', () => {
29
35
  // --- Mock Setup Variables ---
30
36
  const mockReadFileImplementation = jest.fn();
@@ -76,6 +82,5 @@ describe('🖋️ Font Utils', () => {
76
82
  });
77
83
  });
78
84
  */
79
- // ---------------------------------------------------------------------------------
80
-
81
- });
85
+ // ---------------------------------------------------------------------------------
86
+ });