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.
- package/.eslintrc.json +67 -8
- package/.github/workflows/ci.yml +5 -4
- package/.releaserc.js +25 -27
- package/CHANGELOG.md +12 -19
- package/LICENSE.md +21 -0
- package/README.md +34 -36
- package/commitlint.config.js +30 -34
- package/dist/cli/cli-entry.cjs +199 -135
- package/dist/cli/cli-entry.cjs.map +1 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.js +194 -134
- package/dist/index.js.map +1 -1
- package/docs/.vitepress/config.ts +36 -34
- package/docs/.vitepress/sidebar-generator.ts +89 -38
- package/docs/cli.md +29 -82
- package/docs/code-of-conduct.md +7 -1
- package/docs/configuration.md +103 -117
- package/docs/contributing.md +6 -2
- package/docs/deployment.md +10 -5
- package/docs/development.md +8 -5
- package/docs/getting-started.md +76 -45
- package/docs/index.md +1 -1
- package/docs/public/android-chrome-192x192.png +0 -0
- package/docs/public/android-chrome-512x512.png +0 -0
- package/docs/public/apple-touch-icon.png +0 -0
- package/docs/public/favicon-16x16.png +0 -0
- package/docs/public/favicon-32x32.png +0 -0
- package/docs/public/favicon.ico +0 -0
- package/docs/site.webmanifest +1 -0
- package/docs/troubleshooting.md +12 -1
- package/examples/main.ts +7 -10
- package/examples/sample-project/script.js +1 -1
- package/jest.config.ts +8 -13
- package/nodemon.json +5 -10
- package/package.json +2 -5
- package/src/cli/cli-entry.ts +2 -2
- package/src/cli/cli.ts +21 -16
- package/src/cli/options.ts +127 -113
- package/src/core/bundler.ts +254 -221
- package/src/core/extractor.ts +639 -520
- package/src/core/minifier.ts +173 -162
- package/src/core/packer.ts +141 -137
- package/src/core/parser.ts +74 -73
- package/src/core/web-fetcher.ts +270 -258
- package/src/index.ts +18 -17
- package/src/types.ts +9 -11
- package/src/utils/font.ts +12 -6
- package/src/utils/logger.ts +110 -105
- package/src/utils/meta.ts +75 -76
- package/src/utils/mime.ts +50 -50
- package/src/utils/slugify.ts +33 -34
- package/tests/unit/cli/cli-entry.test.ts +72 -70
- package/tests/unit/cli/cli.test.ts +314 -278
- package/tests/unit/cli/options.test.ts +294 -301
- package/tests/unit/core/bundler.test.ts +426 -329
- package/tests/unit/core/extractor.test.ts +828 -380
- package/tests/unit/core/minifier.test.ts +374 -274
- package/tests/unit/core/packer.test.ts +298 -264
- package/tests/unit/core/parser.test.ts +538 -150
- package/tests/unit/core/web-fetcher.test.ts +389 -359
- package/tests/unit/index.test.ts +238 -197
- package/tests/unit/utils/font.test.ts +26 -21
- package/tests/unit/utils/logger.test.ts +267 -260
- package/tests/unit/utils/meta.test.ts +29 -28
- package/tests/unit/utils/mime.test.ts +73 -74
- package/tests/unit/utils/slugify.test.ts +14 -12
- package/tsconfig.build.json +9 -10
- package/tsconfig.jest.json +2 -1
- package/tsconfig.json +2 -2
- package/tsup.config.ts +8 -8
- package/typedoc.json +5 -9
- package/docs/demo.md +0 -46
- /package/docs/{portapack-transparent.png → public/portapack-transparent.png} +0 -0
- /package/docs/{portapack.jpg → public/portapack.jpg} +0 -0
package/tests/unit/index.test.ts
CHANGED
@@ -1,17 +1,20 @@
|
|
1
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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', () => ({
|
46
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
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
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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
|
+
});
|