@postalsys/email-text-tools 2.1.16 → 2.2.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.
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ const { describe, it, after } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const mimeHtml = require('../lib/mime-html');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+
9
+ describe('Edge Cases', () => {
10
+ describe('large HTML', () => {
11
+ it('should handle large HTML', async () => {
12
+ const largeContent = '<p>' + 'x'.repeat(10000) + '</p>';
13
+ const result = await mimeHtml.async({ html: largeContent });
14
+ assert.ok(result.length > 0);
15
+ });
16
+ });
17
+
18
+ describe('special characters', () => {
19
+ it('should handle UTF-8', () => {
20
+ const result = mimeHtml.sync({ html: '<p>Hello 世界</p>' });
21
+ assert.ok(result.includes('Hello'));
22
+ });
23
+
24
+ it('should handle entities', () => {
25
+ const result = mimeHtml.sync({ html: '<p>&lt;&gt;&amp;</p>' });
26
+ assert.ok(result.includes('&lt;') || result.includes('<'));
27
+ });
28
+ });
29
+
30
+ describe('problematic CSS', () => {
31
+ it('should handle modern selectors', () => {
32
+ const html = `
33
+ <style>
34
+ .test:is(.a) { color: red; }
35
+ </style>
36
+ <p class="test a">Test</p>
37
+ `;
38
+ const result = mimeHtml.sync({ html });
39
+ assert.ok(result.includes('Test'));
40
+ });
41
+ });
42
+
43
+ describe('real-world HTML', () => {
44
+ it('should process failing.html quickly', async () => {
45
+ const failingPath = path.join(__dirname, '../examples/failing.html');
46
+ if (fs.existsSync(failingPath)) {
47
+ const html = fs.readFileSync(failingPath, 'utf8');
48
+
49
+ const start = Date.now();
50
+ const result = await mimeHtml.async({ html });
51
+ const elapsed = Date.now() - start;
52
+
53
+ assert.ok(result.length > 0);
54
+ assert.ok(elapsed < 2000, `Took ${elapsed}ms`);
55
+ }
56
+ });
57
+ });
58
+
59
+ after(async () => {
60
+ await mimeHtml.closeWorkerPool();
61
+ });
62
+ });
@@ -0,0 +1,327 @@
1
+ 'use strict';
2
+
3
+ const { describe, it, before: _before, after, beforeEach: _beforeEach, afterEach: _afterEach } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const mimeHtml = require('../lib/mime-html');
6
+
7
+ describe('mime-html', () => {
8
+ describe('sync version', () => {
9
+ it('should process simple HTML', () => {
10
+ const input = {
11
+ html: '<html><body><p>Hello World</p></body></html>'
12
+ };
13
+ const result = mimeHtml.sync(input);
14
+
15
+ assert.ok(result.includes('Hello World'));
16
+ assert.ok(result.includes('<div'));
17
+ assert.ok(result.includes('</div>'));
18
+ });
19
+
20
+ it('should convert text to HTML when no HTML provided', () => {
21
+ const input = {
22
+ text: 'Hello World\nThis is a test'
23
+ };
24
+ const result = mimeHtml.sync(input);
25
+
26
+ assert.ok(result.includes('Hello World'));
27
+ assert.ok(result.includes('This is a test'));
28
+ assert.ok(result.includes('<br'));
29
+ });
30
+
31
+ it('should handle HTML with CSS styles', () => {
32
+ const input = {
33
+ html: `
34
+ <html>
35
+ <head>
36
+ <style>
37
+ p { color: red; }
38
+ .test { font-weight: bold; }
39
+ </style>
40
+ </head>
41
+ <body>
42
+ <p class="test">Styled text</p>
43
+ </body>
44
+ </html>
45
+ `
46
+ };
47
+ const result = mimeHtml.sync(input);
48
+
49
+ assert.ok(result.includes('Styled text'));
50
+ assert.ok(result.includes('style='));
51
+ });
52
+
53
+ it('should sanitize dangerous HTML', () => {
54
+ const input = {
55
+ html: `
56
+ <html>
57
+ <body>
58
+ <p>Safe content</p>
59
+ <script>alert('XSS')</script>
60
+ <img src=x onerror="alert('XSS')">
61
+ </body>
62
+ </html>
63
+ `
64
+ };
65
+ const result = mimeHtml.sync(input);
66
+
67
+ assert.ok(result.includes('Safe content'));
68
+ assert.ok(!result.includes('<script'));
69
+ assert.ok(!result.includes('onerror'));
70
+ });
71
+
72
+ it('should add target="_blank" to links', () => {
73
+ const input = {
74
+ html: '<a href="https://example.com">Link</a>'
75
+ };
76
+ const result = mimeHtml.sync(input);
77
+
78
+ assert.ok(result.includes('target="_blank"'));
79
+ assert.ok(result.includes('Link'));
80
+ });
81
+
82
+ it('should remove problematic CSS selectors', () => {
83
+ const input = {
84
+ html: `
85
+ <html>
86
+ <head>
87
+ <style>
88
+ .item:is(.active, .selected) { background: yellow; }
89
+ .element:where(.primary) { color: blue; }
90
+ .parent:has(> .child) { border: 1px solid red; }
91
+ p { color: green; }
92
+ </style>
93
+ </head>
94
+ <body>
95
+ <p class="item active">Test</p>
96
+ </body>
97
+ </html>
98
+ `
99
+ };
100
+
101
+ // Should complete without hanging
102
+ const result = mimeHtml.sync(input);
103
+ assert.ok(result.includes('Test'));
104
+ assert.ok(result.includes('color'));
105
+ });
106
+
107
+ it('should remove forbidden style properties', () => {
108
+ const input = {
109
+ html: `
110
+ <html>
111
+ <body style="position: fixed; width: 100%; height: 100%;">
112
+ <div style="position: absolute; top: 0;">Content</div>
113
+ </body>
114
+ </html>
115
+ `
116
+ };
117
+ const result = mimeHtml.sync(input);
118
+
119
+ assert.ok(result.includes('Content'));
120
+ assert.ok(!result.includes('position'));
121
+ assert.ok(!result.includes('width'));
122
+ assert.ok(!result.includes('height'));
123
+ });
124
+
125
+ it('should handle empty input gracefully', () => {
126
+ const input = { html: '' };
127
+ const result = mimeHtml.sync(input);
128
+
129
+ assert.ok(typeof result === 'string');
130
+ assert.ok(result.includes('<div'));
131
+ });
132
+
133
+ it('should handle malformed HTML', () => {
134
+ const input = {
135
+ html: '<p>Unclosed paragraph <div>Nested content</p></div>'
136
+ };
137
+ const result = mimeHtml.sync(input);
138
+
139
+ assert.ok(result.includes('Unclosed paragraph'));
140
+ assert.ok(result.includes('Nested content'));
141
+ });
142
+
143
+ it('should preserve HTML entities', () => {
144
+ const input = {
145
+ html: '<p>&lt;script&gt; &amp; &quot;quotes&quot;</p>'
146
+ };
147
+ const result = mimeHtml.sync(input);
148
+
149
+ assert.ok(result.includes('&lt;'));
150
+ assert.ok(result.includes('&amp;'));
151
+ assert.ok(result.includes('&quot;') || result.includes('"'));
152
+ });
153
+ });
154
+
155
+ describe('async version', () => {
156
+ it('should process simple HTML', async () => {
157
+ const input = {
158
+ html: '<html><body><p>Hello Async</p></body></html>'
159
+ };
160
+ const result = await mimeHtml.async(input);
161
+
162
+ assert.ok(result.includes('Hello Async'));
163
+ assert.ok(result.includes('<div'));
164
+ });
165
+
166
+ it('should handle timeout option', async () => {
167
+ const input = {
168
+ html: '<p>Quick test</p>'
169
+ };
170
+ const result = await mimeHtml.async(input, { timeout: 1000 });
171
+
172
+ assert.ok(result.includes('Quick test'));
173
+ });
174
+
175
+ it('should use worker pool by default', async () => {
176
+ const input = {
177
+ html: '<p>Pool test</p>'
178
+ };
179
+
180
+ const promises = [];
181
+ for (let i = 0; i < 5; i++) {
182
+ promises.push(mimeHtml.async(input));
183
+ }
184
+
185
+ const results = await Promise.all(promises);
186
+ assert.equal(results.length, 5);
187
+ results.forEach(result => {
188
+ assert.ok(result.includes('Pool test'));
189
+ });
190
+
191
+ const stats = mimeHtml.getWorkerPoolStats();
192
+ assert.ok(stats);
193
+ assert.ok(stats.totalWorkers >= 2);
194
+ });
195
+
196
+ it('should work without worker pool when disabled', async () => {
197
+ const input = {
198
+ html: '<p>No pool test</p>'
199
+ };
200
+ const result = await mimeHtml.async(input, { useWorkerPool: false });
201
+
202
+ assert.ok(result.includes('No pool test'));
203
+ });
204
+
205
+ it('should handle problematic CSS with pre-filtering', async () => {
206
+ const input = {
207
+ html: `
208
+ <html>
209
+ <head>
210
+ <style>
211
+ .test:is(.a, .b) { color: red; }
212
+ .item:where(.x) { color: blue; }
213
+ .parent:has(.child) { border: 1px solid; }
214
+ </style>
215
+ </head>
216
+ <body>
217
+ <p class="test a">Async test</p>
218
+ </body>
219
+ </html>
220
+ `
221
+ };
222
+
223
+ const start = Date.now();
224
+ const result = await mimeHtml.async(input, { timeout: 2000 });
225
+ const elapsed = Date.now() - start;
226
+
227
+ assert.ok(result.includes('Async test'));
228
+ assert.ok(elapsed < 1000, 'Should complete quickly with pre-filtering');
229
+ });
230
+
231
+ it('should handle errors with fallback', async () => {
232
+ const input = {
233
+ html: '<p>Error test</p>'
234
+ };
235
+
236
+ // Even with errors, should return result with fallback
237
+ const result = await mimeHtml.async(input, {
238
+ fallbackOnError: true,
239
+ logErrors: false
240
+ });
241
+
242
+ assert.ok(typeof result === 'string');
243
+ });
244
+
245
+ it('should throw errors when fallback disabled', async () => {
246
+ // Create intentionally problematic scenario
247
+ const input = {
248
+ html: null // This will cause issues
249
+ };
250
+
251
+ try {
252
+ await mimeHtml.async(input, {
253
+ fallbackOnError: false,
254
+ logErrors: false
255
+ });
256
+ assert.fail('Should have thrown an error');
257
+ } catch (err) {
258
+ assert.ok(err);
259
+ }
260
+ });
261
+ });
262
+
263
+ describe('backward compatibility', () => {
264
+ it('should maintain default export compatibility', () => {
265
+ const input = {
266
+ html: '<p>Compatibility test</p>'
267
+ };
268
+
269
+ // Default export should work
270
+ const result = mimeHtml(input);
271
+ assert.ok(result.includes('Compatibility test'));
272
+ });
273
+
274
+ it('should have sync and async methods available', () => {
275
+ assert.equal(typeof mimeHtml.sync, 'function');
276
+ assert.equal(typeof mimeHtml.async, 'function');
277
+ });
278
+
279
+ it('should have worker pool management methods', () => {
280
+ assert.equal(typeof mimeHtml.closeWorkerPool, 'function');
281
+ assert.equal(typeof mimeHtml.getWorkerPoolStats, 'function');
282
+ });
283
+ });
284
+
285
+ describe('concurrent processing', () => {
286
+ it('should handle multiple concurrent requests', async () => {
287
+ const htmls = [];
288
+ for (let i = 0; i < 10; i++) {
289
+ htmls.push({
290
+ html: `<p>Request ${i}</p>`
291
+ });
292
+ }
293
+
294
+ const promises = htmls.map((input, i) =>
295
+ mimeHtml.async(input).then(result => {
296
+ assert.ok(result.includes(`Request ${i}`));
297
+ return i;
298
+ })
299
+ );
300
+
301
+ const results = await Promise.all(promises);
302
+ assert.equal(results.length, 10);
303
+ assert.deepEqual(results, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
304
+ });
305
+
306
+ it('should handle mixed content types concurrently', async () => {
307
+ const inputs = [
308
+ { html: '<p>HTML content</p>' },
309
+ { text: 'Plain text content' },
310
+ { html: '<div>Another HTML</div>' },
311
+ { text: 'More text\nWith newlines' }
312
+ ];
313
+
314
+ const results = await Promise.all(inputs.map(input => mimeHtml.async(input)));
315
+
316
+ assert.ok(results[0].includes('HTML content'));
317
+ assert.ok(results[1].includes('Plain text content'));
318
+ assert.ok(results[2].includes('Another HTML'));
319
+ assert.ok(results[3].includes('More text'));
320
+ });
321
+ });
322
+
323
+ // Clean up worker pool after all tests
324
+ after(async () => {
325
+ await mimeHtml.closeWorkerPool();
326
+ });
327
+ });
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ const { describe, it, after: _after } = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const WorkerPool = require('../lib/worker-pool');
6
+ const path = require('path');
7
+
8
+ // Test for the worker pool free list regression fix
9
+ describe('Worker Pool Regression Tests', () => {
10
+ describe('free workers list management', () => {
11
+ it('should properly return workers to free list after completion', async () => {
12
+ // Create a simple test worker
13
+ const fs = require('fs');
14
+ const workerPath = path.join(__dirname, 'test-regression-worker.js');
15
+ const workerCode = `
16
+ const { parentPort } = require('worker_threads');
17
+ parentPort.on('message', (data) => {
18
+ setTimeout(() => {
19
+ parentPort.postMessage({ success: true, result: data });
20
+ }, 10);
21
+ });
22
+ `;
23
+ fs.writeFileSync(workerPath, workerCode);
24
+
25
+ try {
26
+ const pool = new WorkerPool({
27
+ workerPath,
28
+ minWorkers: 2,
29
+ maxWorkers: 4
30
+ });
31
+
32
+ // Initial state check
33
+ let stats = pool.getStats();
34
+ assert.equal(stats.totalWorkers, 2);
35
+ assert.equal(stats.freeWorkers, 2);
36
+ assert.equal(stats.busyWorkers, 0);
37
+
38
+ // Process multiple tasks concurrently
39
+ const promises = [];
40
+ for (let i = 0; i < 10; i++) {
41
+ promises.push(pool.process(`task ${i}`));
42
+ }
43
+
44
+ // Wait for all to complete
45
+ const results = await Promise.all(promises);
46
+
47
+ // All tasks should complete successfully
48
+ assert.equal(results.length, 10);
49
+ results.forEach((result, i) => {
50
+ assert.equal(result, `task ${i}`);
51
+ });
52
+
53
+ // After completion, all workers should be free
54
+ stats = pool.getStats();
55
+ assert.equal(stats.busyWorkers, 0);
56
+ assert.equal(stats.freeWorkers, stats.totalWorkers);
57
+
58
+ // Process another batch to ensure workers are reusable
59
+ const secondBatch = [];
60
+ for (let i = 0; i < 5; i++) {
61
+ secondBatch.push(pool.process(`second ${i}`));
62
+ }
63
+
64
+ const secondResults = await Promise.all(secondBatch);
65
+ assert.equal(secondResults.length, 5);
66
+
67
+ // Workers should still be properly managed
68
+ stats = pool.getStats();
69
+ assert.equal(stats.busyWorkers, 0);
70
+ assert.equal(stats.freeWorkers, stats.totalWorkers);
71
+
72
+ await pool.close();
73
+ } finally {
74
+ // Clean up test worker
75
+ const fs = require('fs');
76
+ try {
77
+ fs.unlinkSync(workerPath);
78
+ } catch (_err) {
79
+ // Ignore cleanup errors
80
+ }
81
+ }
82
+ });
83
+
84
+ it('should not have duplicate workers in free list', async () => {
85
+ const fs = require('fs');
86
+ const workerPath = path.join(__dirname, 'test-dup-worker.js');
87
+ const workerCode = `
88
+ const { parentPort } = require('worker_threads');
89
+ parentPort.on('message', (data) => {
90
+ setTimeout(() => {
91
+ parentPort.postMessage({ success: true, result: data });
92
+ }, 5);
93
+ });
94
+ `;
95
+ fs.writeFileSync(workerPath, workerCode);
96
+
97
+ try {
98
+ const pool = new WorkerPool({
99
+ workerPath,
100
+ minWorkers: 1,
101
+ maxWorkers: 2
102
+ });
103
+
104
+ // Process tasks to trigger worker management
105
+ const results = await Promise.all([pool.process('A'), pool.process('B'), pool.process('C')]);
106
+
107
+ assert.equal(results.length, 3);
108
+
109
+ // Check that free workers count matches actual workers
110
+ const stats = pool.getStats();
111
+ assert.ok(stats.freeWorkers <= stats.totalWorkers);
112
+ assert.equal(stats.busyWorkers, 0);
113
+
114
+ // The sum should always equal total
115
+ assert.equal(stats.busyWorkers + stats.freeWorkers, stats.totalWorkers);
116
+
117
+ await pool.close();
118
+ } finally {
119
+ const fs = require('fs');
120
+ try {
121
+ fs.unlinkSync(workerPath);
122
+ } catch (_err) {
123
+ // Ignore
124
+ }
125
+ }
126
+ });
127
+ });
128
+ });