minipug 1.0.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/LICENSE +21 -0
- package/README.md +54 -0
- package/index.mjs +332 -0
- package/package.json +23 -0
- package/test.js +86 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 ChromeAutopilot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# minipug
|
|
2
|
+
|
|
3
|
+
Extract LLM-friendly representation of the DOM in [Pug](https://pugjs.org/) format.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm i -s minipug
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import MiniPug from 'minipug'
|
|
15
|
+
const minipug = new MiniPug(document)
|
|
16
|
+
const pug = minipug.convert()
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Example
|
|
20
|
+
|
|
21
|
+
Input:
|
|
22
|
+
```html
|
|
23
|
+
<html>
|
|
24
|
+
<head>
|
|
25
|
+
<title>Example Domain</title>
|
|
26
|
+
<meta charset="utf-8">
|
|
27
|
+
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
|
28
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
29
|
+
<style type="text/css">
|
|
30
|
+
body {
|
|
31
|
+
background-color: #f0f0f2;
|
|
32
|
+
margin: 0;
|
|
33
|
+
padding: 0;
|
|
34
|
+
}
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<h1>Hello World</h1>
|
|
39
|
+
<section class="example-section">
|
|
40
|
+
<p>This is an example.</p>
|
|
41
|
+
<button aria-label="Next page" class="z2iX5wAef9nHv">Next page</button>
|
|
42
|
+
</section>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Output:
|
|
49
|
+
```pug
|
|
50
|
+
h1 Hello World
|
|
51
|
+
section
|
|
52
|
+
p This is an example.
|
|
53
|
+
button(aria-label="Next page") Next page
|
|
54
|
+
```
|
package/index.mjs
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
export default class MiniPug {
|
|
2
|
+
constructor(document) {
|
|
3
|
+
this.document = document;
|
|
4
|
+
this.ignoreTags = new Set(['html', 'body']);
|
|
5
|
+
this.skipDescendantsTags = new Set(['noscript']);
|
|
6
|
+
this.whitelistedTags = new Set([
|
|
7
|
+
'nav', 'main', 'header', 'footer', 'aside',
|
|
8
|
+
'article', 'section',
|
|
9
|
+
'a', 'form', 'input', 'textarea', 'button', 'select', 'option',
|
|
10
|
+
'h1', 'h2', 'h3', 'h4', 'h5',
|
|
11
|
+
'ul', 'ol', 'li', 'dl', 'dt', 'dd',
|
|
12
|
+
'caption',
|
|
13
|
+
'pre', 'code',
|
|
14
|
+
'fieldset', 'legend',
|
|
15
|
+
'dialog', 'details', 'summary',
|
|
16
|
+
'iframe', 'br', 'hr'
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
this.tableTags = new Set(['table', 'tr', 'th', 'td']);
|
|
20
|
+
|
|
21
|
+
this.whitelistedClassSubstrings = new Set([
|
|
22
|
+
'up', 'down', 'left', 'right', 'arrow', 'caret', 'chevron', 'star',
|
|
23
|
+
'increase', 'decrease', 'plus', 'minus', 'expand', 'collapse', 'open', 'close',
|
|
24
|
+
'success', 'error', 'warning', 'valid', 'invalid',
|
|
25
|
+
'active', 'inactive', 'enabled', 'disabled', 'next', 'prev', 'previous', 'first', 'last'
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
this.whitelistedAttributes = new Set([
|
|
29
|
+
'href', 'target', 'download',
|
|
30
|
+
'action', 'method', 'type', 'name', 'value', 'placeholder', 'required', 'checked', 'selected',
|
|
31
|
+
'aria-label', 'aria-expanded', 'aria-hidden', 'aria-controls', 'aria-current',
|
|
32
|
+
'aria-describedby', 'aria-disabled', 'aria-haspopup', 'aria-invalid',
|
|
33
|
+
'aria-labelledby', 'aria-live', 'aria-pressed', 'aria-required', 'aria-selected',
|
|
34
|
+
'aria-checked', 'aria-valuenow', 'aria-valuemin', 'aria-valuemax', 'role', 'title', 'alt',
|
|
35
|
+
'disabled', 'readonly'
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
this.booleanAttributes = new Set([
|
|
39
|
+
'checked', 'selected', 'disabled', 'readonly', 'required'
|
|
40
|
+
]);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
isVisible(element) {
|
|
44
|
+
if (!(element instanceof Element)) return true;
|
|
45
|
+
if (element.tagName.toLowerCase() === 'body') return true; // Always consider body visible
|
|
46
|
+
|
|
47
|
+
const style = window.getComputedStyle(element);
|
|
48
|
+
const rect = element.getBoundingClientRect();
|
|
49
|
+
|
|
50
|
+
// If element has zero dimensions, check if it has visible absolutely positioned children
|
|
51
|
+
if (rect.width === 0 && rect.height === 0) {
|
|
52
|
+
for (const child of element.children) {
|
|
53
|
+
const childStyle = window.getComputedStyle(child);
|
|
54
|
+
if ((childStyle.position === 'absolute' || childStyle.position === 'fixed') &&
|
|
55
|
+
this.isVisible(child)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return !(
|
|
62
|
+
style.display === 'none' ||
|
|
63
|
+
style.visibility === 'hidden' ||
|
|
64
|
+
element.getAttribute('aria-hidden') === 'true' ||
|
|
65
|
+
(rect.width === 0 && rect.height === 0) ||
|
|
66
|
+
style.opacity === '0' ||
|
|
67
|
+
(
|
|
68
|
+
(style.position === 'absolute' || style.position === 'fixed') &&
|
|
69
|
+
(
|
|
70
|
+
rect.left + rect.width < 0 ||
|
|
71
|
+
rect.top + rect.height < 0 ||
|
|
72
|
+
rect.left > window.innerWidth ||
|
|
73
|
+
rect.top > window.innerHeight
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// hasWhitelistedClass(element) {
|
|
80
|
+
// const classAttr = element.getAttribute('class');
|
|
81
|
+
// if (!classAttr) return false;
|
|
82
|
+
|
|
83
|
+
// const normalizedClasses = classAttr.toLowerCase();
|
|
84
|
+
// for (const substring of this.whitelistedClassSubstrings) {
|
|
85
|
+
// if (normalizedClasses.includes(substring)) {
|
|
86
|
+
// return true;
|
|
87
|
+
// }
|
|
88
|
+
// }
|
|
89
|
+
// return false;
|
|
90
|
+
// }
|
|
91
|
+
|
|
92
|
+
findWhitelistedClasses(element, firstMatchOnly = false) {
|
|
93
|
+
const classAttr = element.getAttribute('class');
|
|
94
|
+
if (!classAttr) return [];
|
|
95
|
+
|
|
96
|
+
const matches = [];
|
|
97
|
+
const classes = classAttr.split(/\s+/);
|
|
98
|
+
const normalizedClasses = classes.map(c => c.toLowerCase());
|
|
99
|
+
|
|
100
|
+
for (const className of classes) {
|
|
101
|
+
const normalizedClass = className.toLowerCase();
|
|
102
|
+
for (const substring of this.whitelistedClassSubstrings) {
|
|
103
|
+
if (normalizedClass.includes(substring)) {
|
|
104
|
+
if (firstMatchOnly) return [className];
|
|
105
|
+
matches.push(className);
|
|
106
|
+
break; // Move to next className after finding a match
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return matches;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
hasWhitelistedClass(element) {
|
|
115
|
+
return this.findWhitelistedClasses(element, true).length > 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
hasWhitelistedAttribute(element) {
|
|
119
|
+
for (const attr of element.attributes) {
|
|
120
|
+
if (this.whitelistedAttributes.has(attr.name)) {
|
|
121
|
+
if (this.booleanAttributes.has(attr.name)) {
|
|
122
|
+
return true; // Boolean attributes are valid just by existing
|
|
123
|
+
}
|
|
124
|
+
if (attr.value.trim() !== '') {
|
|
125
|
+
return true; // Non-boolean attributes need non-empty values
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
hasDirectTextNode(element) {
|
|
133
|
+
for (const child of element.childNodes) {
|
|
134
|
+
if (child.nodeType === Node.TEXT_NODE && child.textContent.trim()) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
hasIncludedDescendant(element) {
|
|
142
|
+
for (const child of element.children) {
|
|
143
|
+
if (this.shouldIncludeElement(child)) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
if (this.hasIncludedDescendant(child)) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
shouldIncludeElement(element) {
|
|
154
|
+
if (this.ignoreTags.has(element.tagName.toLowerCase())) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// TODO: revisit tables
|
|
159
|
+
// const tagName = element.tagName.toLowerCase();
|
|
160
|
+
// if (this.tableTags.has(tagName)) {
|
|
161
|
+
// // For table elements, check if they have any included descendants
|
|
162
|
+
// return this.hasIncludedDescendant(element);
|
|
163
|
+
// }
|
|
164
|
+
const isWhitelistedTag = this.whitelistedTags.has(element.tagName.toLowerCase());
|
|
165
|
+
if (isWhitelistedTag) return true;
|
|
166
|
+
|
|
167
|
+
// For non-whitelisted tags, check if element has any meaningful content
|
|
168
|
+
const hasText = this.hasDirectTextNode(element);
|
|
169
|
+
if (hasText) return true;
|
|
170
|
+
|
|
171
|
+
const hasWhitelistedClass = this.hasWhitelistedClass(element);
|
|
172
|
+
if (hasWhitelistedClass) return true;
|
|
173
|
+
|
|
174
|
+
const hasWhitelistedAttribute = this.hasWhitelistedAttribute(element);
|
|
175
|
+
if (hasWhitelistedAttribute) return true;
|
|
176
|
+
|
|
177
|
+
// const hasIncludedChildren = this.hasIncludedDescendant(element);
|
|
178
|
+
// if (hasIncludedChildren) return true;
|
|
179
|
+
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
getCapturedValue(element) {
|
|
184
|
+
const tagName = element.tagName.toLowerCase();
|
|
185
|
+
const type = element.type?.toLowerCase();
|
|
186
|
+
|
|
187
|
+
if (tagName === 'input') {
|
|
188
|
+
if (type === 'checkbox' || type === 'radio') {
|
|
189
|
+
return element.checked;
|
|
190
|
+
}
|
|
191
|
+
return element.value;
|
|
192
|
+
}
|
|
193
|
+
if (tagName === 'textarea' || tagName === 'select') {
|
|
194
|
+
return element.value;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getAttributes(element) {
|
|
200
|
+
const attributes = {};
|
|
201
|
+
const tagName = element.tagName.toLowerCase();
|
|
202
|
+
|
|
203
|
+
for (const attr of element.attributes) {
|
|
204
|
+
if (!this.whitelistedAttributes.has(attr.name)) continue;
|
|
205
|
+
|
|
206
|
+
// Skip href attribute for img tags
|
|
207
|
+
if (['img', 'image'].includes(tagName) && attr.name === 'href') continue;
|
|
208
|
+
|
|
209
|
+
if (this.booleanAttributes.has(attr.name)) {
|
|
210
|
+
// For boolean attributes, include them only if they exist
|
|
211
|
+
attributes[attr.name] = true;
|
|
212
|
+
} else if (attr.value.trim() !== '') {
|
|
213
|
+
// For non-boolean attributes, include only if they have non-empty values
|
|
214
|
+
attributes[attr.name] = attr.value;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Capture live DOM values
|
|
219
|
+
const capturedValue = this.getCapturedValue(element);
|
|
220
|
+
if (capturedValue !== null) {
|
|
221
|
+
if (typeof capturedValue === 'boolean') {
|
|
222
|
+
if (capturedValue) {
|
|
223
|
+
attributes['checked'] = true;
|
|
224
|
+
}
|
|
225
|
+
} else if (capturedValue.trim() !== '') {
|
|
226
|
+
attributes['value'] = capturedValue;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return attributes;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
onlyHasTextNodes(node) {
|
|
234
|
+
for (const child of node.childNodes) {
|
|
235
|
+
if (child.nodeType !== Node.TEXT_NODE) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
convertNode(node, depth = 0) {
|
|
243
|
+
if (node.nodeType !== Node.ELEMENT_NODE) {
|
|
244
|
+
return '';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!this.isVisible(node)) {
|
|
248
|
+
return '';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const tagName = node.tagName.toLowerCase();
|
|
252
|
+
|
|
253
|
+
if (this.skipDescendantsTags.has(tagName)) {
|
|
254
|
+
return '';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let output = '';
|
|
258
|
+
const shouldInclude = this.shouldIncludeElement(node);
|
|
259
|
+
|
|
260
|
+
if (shouldInclude) {
|
|
261
|
+
output += `${' '.repeat(depth * 2)}${tagName}`;
|
|
262
|
+
|
|
263
|
+
const attributes = this.getAttributes(node);
|
|
264
|
+
if (Object.keys(attributes).length > 0) {
|
|
265
|
+
const attrStrings = [];
|
|
266
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
267
|
+
if (value === true) {
|
|
268
|
+
attrStrings.push(key);
|
|
269
|
+
} else {
|
|
270
|
+
attrStrings.push(`${key}="${value}"`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
output += `(${attrStrings.join(' ')})`;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Only output whitelisted classes
|
|
277
|
+
const relevantClasses = this.findWhitelistedClasses(node);
|
|
278
|
+
|
|
279
|
+
if (relevantClasses.length > 0) {
|
|
280
|
+
output += `.${relevantClasses.join('.')}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Handle text-only nodes differently
|
|
284
|
+
if (this.onlyHasTextNodes(node)) {
|
|
285
|
+
const text = Array.from(node.childNodes)
|
|
286
|
+
.map(child => child.textContent.trim())
|
|
287
|
+
.filter(Boolean)
|
|
288
|
+
.join(' ');
|
|
289
|
+
if (text) {
|
|
290
|
+
output += ` ${text}`;
|
|
291
|
+
}
|
|
292
|
+
output += '\n';
|
|
293
|
+
} else {
|
|
294
|
+
output += '\n';
|
|
295
|
+
// Process mixed content nodes in order
|
|
296
|
+
let pendingText = '';
|
|
297
|
+
for (const child of node.childNodes) {
|
|
298
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
299
|
+
const text = child.textContent.trim();
|
|
300
|
+
if (text) {
|
|
301
|
+
pendingText += (pendingText ? ' ' : '') + text;
|
|
302
|
+
}
|
|
303
|
+
} else if (child.nodeType === Node.ELEMENT_NODE) {
|
|
304
|
+
// Output any pending text before the element
|
|
305
|
+
if (pendingText) {
|
|
306
|
+
output += `${' '.repeat((depth + 1) * 2)}| ${pendingText}\n`;
|
|
307
|
+
pendingText = '';
|
|
308
|
+
}
|
|
309
|
+
output += this.convertNode(child, depth + 1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Output any remaining text
|
|
313
|
+
if (pendingText) {
|
|
314
|
+
output += `${' '.repeat((depth + 1) * 2)}| ${pendingText}\n`;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
// Even if we don't include this element, we still need to process its children
|
|
319
|
+
for (const child of node.childNodes) {
|
|
320
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
321
|
+
output += this.convertNode(child, depth);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return output;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
convert() {
|
|
330
|
+
return this.convertNode(this.document.body).trim();
|
|
331
|
+
}
|
|
332
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "minipug",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Extract LLM-friendly representation of a webpage",
|
|
5
|
+
"main": "index.mjs",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node test.js"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/will123195/minipug.git"
|
|
13
|
+
},
|
|
14
|
+
"author": "will123195",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/will123195/minipug/issues"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/will123195/minipug#readme",
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"puppeteer": "^21.5.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/test.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import puppeteer from 'puppeteer';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
const createTestHtml = async () => {
|
|
9
|
+
const htmlContent = `
|
|
10
|
+
<html>
|
|
11
|
+
<head>
|
|
12
|
+
<title>Example Domain</title>
|
|
13
|
+
<meta charset="utf-8">
|
|
14
|
+
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
|
|
15
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
16
|
+
<style type="text/css">
|
|
17
|
+
body {
|
|
18
|
+
background-color: #f0f0f2;
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 0;
|
|
21
|
+
}
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
<body>
|
|
25
|
+
<h1>Hello World</h1>
|
|
26
|
+
<section class="example-section">
|
|
27
|
+
<p>This is an example.</p>
|
|
28
|
+
<button aria-label="Next page" class="z2iX5wAef9nHv">Next page</button>
|
|
29
|
+
</section>
|
|
30
|
+
</body>
|
|
31
|
+
</html>
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
const testFilePath = path.join(__dirname, 'test.html');
|
|
35
|
+
await fs.writeFile(testFilePath, htmlContent);
|
|
36
|
+
return testFilePath;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const runTest = async () => {
|
|
40
|
+
console.log('Running MiniPug test with Puppeteer...');
|
|
41
|
+
const testFilePath = await createTestHtml();
|
|
42
|
+
const testFileUrl = `file://${testFilePath}`;
|
|
43
|
+
const browser = await puppeteer.launch({ headless: 'new' });
|
|
44
|
+
const page = await browser.newPage();
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await page.goto(testFileUrl);
|
|
48
|
+
const miniPugCode = await fs.readFile('./index.mjs', 'utf8');
|
|
49
|
+
const result = await page.evaluate(async (code) => {
|
|
50
|
+
const blob = new Blob([code], { type: 'application/javascript' });
|
|
51
|
+
const url = URL.createObjectURL(blob);
|
|
52
|
+
|
|
53
|
+
const MiniPugModule = await import(url);
|
|
54
|
+
const MiniPug = MiniPugModule.default;
|
|
55
|
+
|
|
56
|
+
const minipug = new MiniPug(document);
|
|
57
|
+
return minipug.convert();
|
|
58
|
+
}, miniPugCode);
|
|
59
|
+
|
|
60
|
+
const expectedOutput = `h1 Hello World\nsection\n p This is an example.\n button(aria-label="Next page") Next page`;
|
|
61
|
+
|
|
62
|
+
console.log('\nValidation:');
|
|
63
|
+
if (result.trim() === expectedOutput.trim()) {
|
|
64
|
+
console.log('✅ Test passed! Output matches expected result.');
|
|
65
|
+
} else {
|
|
66
|
+
console.log('❌ Test failed! Output does not match expected result.');
|
|
67
|
+
console.log('\nExpected:');
|
|
68
|
+
console.log(expectedOutput);
|
|
69
|
+
console.log(expectedOutput.length);
|
|
70
|
+
console.log('\nActual:');
|
|
71
|
+
console.log(result);
|
|
72
|
+
console.log(result.length);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
}
|
|
75
|
+
} finally {
|
|
76
|
+
// Always close the browser and clean up, regardless of success or failure
|
|
77
|
+
await browser.close();
|
|
78
|
+
await fs.unlink(testFilePath).catch(() => {});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// This will catch any errors that occur during the test execution
|
|
83
|
+
runTest().catch(error => {
|
|
84
|
+
console.error('Test error:', error);
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
});
|