eyeling 1.10.13 → 1.10.15
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/HANDBOOK.md +18 -0
- package/package.json +4 -2
- package/test/playground.test.js +555 -0
package/HANDBOOK.md
CHANGED
|
@@ -1831,3 +1831,21 @@ If you depend on Eyeling as a library, the package exposes:
|
|
|
1831
1831
|
|
|
1832
1832
|
See:
|
|
1833
1833
|
- [Chapter 14 — Entry points: CLI, bundle exports, and npm API](#ch14)
|
|
1834
|
+
|
|
1835
|
+
### A.9 Further reading
|
|
1836
|
+
If you want to go deeper into N3 itself and the logic/programming ideas behind Eyeling, these are good starting points:
|
|
1837
|
+
|
|
1838
|
+
N3 / Semantic Web specs and reports:
|
|
1839
|
+
- https://w3c.github.io/N3/spec/
|
|
1840
|
+
- https://w3c.github.io/N3/reports/20230703/semantics.html
|
|
1841
|
+
- https://w3c.github.io/N3/reports/20230703/builtins.html
|
|
1842
|
+
|
|
1843
|
+
Logic & reasoning background (Wikipedia):
|
|
1844
|
+
- https://en.wikipedia.org/wiki/Mathematical_logic
|
|
1845
|
+
- https://en.wikipedia.org/wiki/Automated_reasoning
|
|
1846
|
+
- https://en.wikipedia.org/wiki/Forward_chaining
|
|
1847
|
+
- https://en.wikipedia.org/wiki/Backward_chaining
|
|
1848
|
+
- https://en.wikipedia.org/wiki/Unification_%28computer_science%29
|
|
1849
|
+
- https://en.wikipedia.org/wiki/Prolog
|
|
1850
|
+
- https://en.wikipedia.org/wiki/Datalog
|
|
1851
|
+
- https://en.wikipedia.org/wiki/Skolem_normal_form
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eyeling",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.15",
|
|
4
4
|
"description": "A minimal Notation3 (N3) reasoner in JavaScript.",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -41,8 +41,10 @@
|
|
|
41
41
|
"test:n3gen": "node test/n3gen.test.js",
|
|
42
42
|
"test:examples": "node test/examples.test.js",
|
|
43
43
|
"test:manifest": "node test/manifest.test.js",
|
|
44
|
+
"test:playground": "node test/playground.test.js",
|
|
44
45
|
"test:package": "node test/package.test.js",
|
|
45
|
-
"test": "npm run
|
|
46
|
+
"test:all": "npm run test:packlist && npm run test:api && npm run test:n3gen && npm run test:examples && npm run test:manifest && npm run test:playground && npm run test:package",
|
|
47
|
+
"test": "npm run build && npm run test:all",
|
|
46
48
|
"preversion": "npm test",
|
|
47
49
|
"postversion": "git push --follow-tags"
|
|
48
50
|
}
|
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Smoke-test the browser playground (demo.html).
|
|
4
|
+
//
|
|
5
|
+
// Goal: ensure demo.html loads without runtime exceptions and that the default
|
|
6
|
+
// Socrates program can be executed to completion ("Done") with non-empty output.
|
|
7
|
+
//
|
|
8
|
+
// This test is dependency-free: it drives Chromium directly via the Chrome
|
|
9
|
+
// DevTools Protocol (CDP) over WebSocket.
|
|
10
|
+
|
|
11
|
+
const assert = require('node:assert/strict');
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const http = require('node:http');
|
|
14
|
+
const os = require('node:os');
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const { spawn } = require('node:child_process');
|
|
17
|
+
const { setTimeout: sleep } = require('node:timers/promises');
|
|
18
|
+
|
|
19
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
20
|
+
|
|
21
|
+
const TTY = process.stdout.isTTY;
|
|
22
|
+
const C = TTY
|
|
23
|
+
? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' }
|
|
24
|
+
: { g: '', r: '', y: '', dim: '', n: '' };
|
|
25
|
+
function ok(msg) {
|
|
26
|
+
console.log(`${C.g}OK${C.n} ${msg}`);
|
|
27
|
+
}
|
|
28
|
+
function info(msg) {
|
|
29
|
+
console.log(`${C.y}==${C.n} ${msg}`);
|
|
30
|
+
}
|
|
31
|
+
function fail(msg) {
|
|
32
|
+
console.error(`${C.r}FAIL${C.n} ${msg}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function guessContentType(p) {
|
|
36
|
+
const ext = path.extname(p).toLowerCase();
|
|
37
|
+
if (ext === '.html') return 'text/html; charset=utf-8';
|
|
38
|
+
if (ext === '.js') return 'application/javascript; charset=utf-8';
|
|
39
|
+
if (ext === '.css') return 'text/css; charset=utf-8';
|
|
40
|
+
if (ext === '.json') return 'application/json; charset=utf-8';
|
|
41
|
+
if (ext === '.ttl' || ext === '.n3') return 'text/plain; charset=utf-8';
|
|
42
|
+
if (ext === '.txt' || ext === '.md') return 'text/plain; charset=utf-8';
|
|
43
|
+
return 'application/octet-stream';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function startStaticServer(rootDir) {
|
|
47
|
+
const server = http.createServer((req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const url = new URL(req.url || '/', 'http://localhost');
|
|
50
|
+
let pathname = decodeURIComponent(url.pathname);
|
|
51
|
+
|
|
52
|
+
// Avoid noisy browser console errors.
|
|
53
|
+
if (pathname === '/favicon.ico') {
|
|
54
|
+
res.writeHead(204);
|
|
55
|
+
res.end();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (pathname === '/' || pathname === '') pathname = '/demo.html';
|
|
60
|
+
// Prevent directory traversal.
|
|
61
|
+
const fsPath = path.resolve(rootDir, '.' + pathname);
|
|
62
|
+
if (!fsPath.startsWith(rootDir)) {
|
|
63
|
+
res.writeHead(403);
|
|
64
|
+
res.end('Forbidden');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const st = fs.statSync(fsPath);
|
|
69
|
+
if (st.isDirectory()) {
|
|
70
|
+
res.writeHead(301, { Location: pathname.replace(/\/$/, '') + '/demo.html' });
|
|
71
|
+
res.end();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
res.writeHead(200, { 'Content-Type': guessContentType(fsPath), 'Cache-Control': 'no-store' });
|
|
76
|
+
fs.createReadStream(fsPath).pipe(res);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
79
|
+
res.end('Not found');
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
server.on('error', reject);
|
|
85
|
+
server.listen(0, '127.0.0.1', () => {
|
|
86
|
+
const addr = server.address();
|
|
87
|
+
resolve({
|
|
88
|
+
server,
|
|
89
|
+
port: addr.port,
|
|
90
|
+
baseUrl: `http://127.0.0.1:${addr.port}`,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function which(cmd) {
|
|
97
|
+
try {
|
|
98
|
+
// Avoid spawnSync (keeps this file in the same style as other tests: lightweight).
|
|
99
|
+
const paths = String(process.env.PATH || '').split(path.delimiter);
|
|
100
|
+
for (const p of paths) {
|
|
101
|
+
const fp = path.join(p, cmd);
|
|
102
|
+
if (fs.existsSync(fp)) return fp;
|
|
103
|
+
}
|
|
104
|
+
} catch (_) {}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function findChromium() {
|
|
109
|
+
// Allow overrides.
|
|
110
|
+
const env = process.env.EYELING_BROWSER || process.env.CHROME_BIN || process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
111
|
+
if (env && fs.existsSync(env)) return env;
|
|
112
|
+
|
|
113
|
+
// Common binaries.
|
|
114
|
+
const candidates = ['chromium', 'chromium-browser', 'google-chrome', 'google-chrome-stable', 'chrome'];
|
|
115
|
+
for (const c of candidates) {
|
|
116
|
+
const p = which(c);
|
|
117
|
+
if (p) return p;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Minimal CodeMirror stub for the playground.
|
|
123
|
+
// The real demo loads CodeMirror from a CDN. In CI/offline tests we intercept
|
|
124
|
+
// those script requests and provide this stub to prevent runtime failures.
|
|
125
|
+
const CODEMIRROR_STUB = String.raw`(function(){
|
|
126
|
+
if (window.CodeMirror) return;
|
|
127
|
+
|
|
128
|
+
function posToIndex(text, line, ch){
|
|
129
|
+
line = Math.max(0, line|0);
|
|
130
|
+
ch = Math.max(0, ch|0);
|
|
131
|
+
const norm = String(text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
132
|
+
const lines = norm.split('\n');
|
|
133
|
+
if (lines.length === 0) return 0;
|
|
134
|
+
if (line >= lines.length) line = lines.length - 1;
|
|
135
|
+
if (ch > lines[line].length) ch = lines[line].length;
|
|
136
|
+
let idx = 0;
|
|
137
|
+
for (let i = 0; i < line; i++) idx += lines[i].length + 1;
|
|
138
|
+
return idx + ch;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function mkWrapper(textarea){
|
|
142
|
+
var wrapper = document.createElement('div');
|
|
143
|
+
wrapper.className = 'CodeMirror';
|
|
144
|
+
|
|
145
|
+
var scroll = document.createElement('div');
|
|
146
|
+
scroll.className = 'CodeMirror-scroll';
|
|
147
|
+
scroll.style.overflow = 'auto';
|
|
148
|
+
|
|
149
|
+
var sizer = document.createElement('div');
|
|
150
|
+
sizer.className = 'CodeMirror-sizer';
|
|
151
|
+
|
|
152
|
+
var code = document.createElement('div');
|
|
153
|
+
code.className = 'CodeMirror-code';
|
|
154
|
+
|
|
155
|
+
var pre = document.createElement('pre');
|
|
156
|
+
pre.textContent = textarea.value || '';
|
|
157
|
+
|
|
158
|
+
code.appendChild(pre);
|
|
159
|
+
sizer.appendChild(code);
|
|
160
|
+
scroll.appendChild(sizer);
|
|
161
|
+
wrapper.appendChild(scroll);
|
|
162
|
+
|
|
163
|
+
return { wrapper: wrapper, scroll: scroll, pre: pre };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
window.CodeMirror = {
|
|
167
|
+
fromTextArea: function(textarea/*, opts*/){
|
|
168
|
+
var obj = mkWrapper(textarea);
|
|
169
|
+
try {
|
|
170
|
+
textarea.style.display = 'none';
|
|
171
|
+
textarea.parentNode.insertBefore(obj.wrapper, textarea.nextSibling);
|
|
172
|
+
} catch(_) {}
|
|
173
|
+
|
|
174
|
+
function sync(){ obj.pre.textContent = textarea.value || ''; }
|
|
175
|
+
function getLines(){
|
|
176
|
+
return String(textarea.value || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const doc = {
|
|
180
|
+
posFromIndex: function(i){
|
|
181
|
+
i = Math.max(0, i|0);
|
|
182
|
+
const lines = getLines();
|
|
183
|
+
let acc = 0;
|
|
184
|
+
for (let ln = 0; ln < lines.length; ln++){
|
|
185
|
+
const len = lines[ln].length;
|
|
186
|
+
if (i <= acc + len) return { line: ln, ch: i - acc };
|
|
187
|
+
acc += len + 1;
|
|
188
|
+
}
|
|
189
|
+
return { line: Math.max(0, lines.length - 1), ch: (lines[lines.length-1] || '').length };
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
getValue: function(){ return textarea.value || ''; },
|
|
195
|
+
setValue: function(v){ textarea.value = String(v == null ? '' : v); sync(); },
|
|
196
|
+
|
|
197
|
+
// Methods used by demo.html's streaming appender
|
|
198
|
+
getScrollerElement: function(){ return obj.scroll; },
|
|
199
|
+
lastLine: function(){ const ls = getLines(); return Math.max(0, ls.length - 1); },
|
|
200
|
+
getLine: function(n){ const ls = getLines(); return ls[n] == null ? '' : ls[n]; },
|
|
201
|
+
replaceRange: function(text, pos){
|
|
202
|
+
const cur = String(textarea.value || '');
|
|
203
|
+
const idx = posToIndex(cur, pos && pos.line, pos && pos.ch);
|
|
204
|
+
textarea.value = cur.slice(0, idx) + String(text == null ? '' : text) + cur.slice(idx);
|
|
205
|
+
sync();
|
|
206
|
+
},
|
|
207
|
+
|
|
208
|
+
// Misc methods used by layout / resizing code
|
|
209
|
+
refresh: function(){},
|
|
210
|
+
setSize: function(){},
|
|
211
|
+
setOption: function(){},
|
|
212
|
+
on: function(){},
|
|
213
|
+
operation: function(fn){ try{ fn(); } catch(_){} },
|
|
214
|
+
getWrapperElement: function(){ return obj.wrapper; },
|
|
215
|
+
getScrollInfo: function(){ return { height: 0, clientHeight: 0, top: 0 }; },
|
|
216
|
+
defaultTextHeight: function(){ return 17; },
|
|
217
|
+
|
|
218
|
+
// Error highlighting hooks (no-op in stub)
|
|
219
|
+
addLineClass: function(){},
|
|
220
|
+
removeLineClass: function(){},
|
|
221
|
+
clearGutter: function(){},
|
|
222
|
+
setGutterMarker: function(){},
|
|
223
|
+
|
|
224
|
+
// Minimal doc access for error helpers (if ever invoked)
|
|
225
|
+
getDoc: function(){ return doc; },
|
|
226
|
+
doc: doc
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
})();`;
|
|
231
|
+
|
|
232
|
+
function b64(s) {
|
|
233
|
+
return Buffer.from(String(s), 'utf8').toString('base64');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
class CDP {
|
|
237
|
+
constructor(ws) {
|
|
238
|
+
this.ws = ws;
|
|
239
|
+
this.nextId = 0;
|
|
240
|
+
this.pending = new Map();
|
|
241
|
+
this.handlers = new Map();
|
|
242
|
+
ws.onmessage = (ev) => {
|
|
243
|
+
const msg = JSON.parse(ev.data);
|
|
244
|
+
if (msg.id) {
|
|
245
|
+
const p = this.pending.get(msg.id);
|
|
246
|
+
if (!p) return;
|
|
247
|
+
this.pending.delete(msg.id);
|
|
248
|
+
if (msg.error) {
|
|
249
|
+
const e = new Error(msg.error.message || 'CDP error');
|
|
250
|
+
e.data = msg.error;
|
|
251
|
+
p.reject(e);
|
|
252
|
+
} else {
|
|
253
|
+
p.resolve(msg.result);
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const key = `${msg.sessionId || ''}:${msg.method}`;
|
|
258
|
+
const hs = this.handlers.get(key);
|
|
259
|
+
if (hs) {
|
|
260
|
+
for (const h of hs) {
|
|
261
|
+
try {
|
|
262
|
+
h(msg.params);
|
|
263
|
+
} catch (_) {}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
send(method, params = {}, sessionId, timeoutMs = 15000) {
|
|
270
|
+
const id = ++this.nextId;
|
|
271
|
+
const payload = { id, method, params };
|
|
272
|
+
if (sessionId) payload.sessionId = sessionId;
|
|
273
|
+
this.ws.send(JSON.stringify(payload));
|
|
274
|
+
return new Promise((resolve, reject) => {
|
|
275
|
+
const t = setTimeout(() => {
|
|
276
|
+
this.pending.delete(id);
|
|
277
|
+
reject(new Error(`CDP timeout (${timeoutMs}ms): ${method}`));
|
|
278
|
+
}, timeoutMs);
|
|
279
|
+
this.pending.set(id, {
|
|
280
|
+
resolve: (v) => {
|
|
281
|
+
clearTimeout(t);
|
|
282
|
+
resolve(v);
|
|
283
|
+
},
|
|
284
|
+
reject: (e) => {
|
|
285
|
+
clearTimeout(t);
|
|
286
|
+
reject(e);
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
on(method, sessionId, fn) {
|
|
293
|
+
const key = `${sessionId || ''}:${method}`;
|
|
294
|
+
let hs = this.handlers.get(key);
|
|
295
|
+
if (!hs) this.handlers.set(key, (hs = []));
|
|
296
|
+
hs.push(fn);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
once(method, sessionId, timeoutMs = 15000, predicate = null) {
|
|
300
|
+
return new Promise((resolve, reject) => {
|
|
301
|
+
const t = setTimeout(() => {
|
|
302
|
+
cleanup();
|
|
303
|
+
reject(new Error(`Timeout waiting for ${method}`));
|
|
304
|
+
}, timeoutMs);
|
|
305
|
+
const handler = (params) => {
|
|
306
|
+
if (predicate && !predicate(params)) return;
|
|
307
|
+
cleanup();
|
|
308
|
+
resolve(params);
|
|
309
|
+
};
|
|
310
|
+
const cleanup = () => {
|
|
311
|
+
clearTimeout(t);
|
|
312
|
+
const key = `${sessionId || ''}:${method}`;
|
|
313
|
+
const hs = this.handlers.get(key) || [];
|
|
314
|
+
const idx = hs.indexOf(handler);
|
|
315
|
+
if (idx >= 0) hs.splice(idx, 1);
|
|
316
|
+
};
|
|
317
|
+
this.on(method, sessionId, handler);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function main() {
|
|
323
|
+
const browserPath = findChromium();
|
|
324
|
+
assert.ok(browserPath, 'No Chromium/Chrome binary found. Set EYELING_BROWSER to override.');
|
|
325
|
+
|
|
326
|
+
let server = null;
|
|
327
|
+
let chrome = null;
|
|
328
|
+
let ws = null;
|
|
329
|
+
|
|
330
|
+
const profileDir = fs.mkdtempSync(path.join(os.tmpdir(), 'eyeling-playground-'));
|
|
331
|
+
|
|
332
|
+
async function cleanup() {
|
|
333
|
+
try {
|
|
334
|
+
if (ws) ws.close();
|
|
335
|
+
} catch (_) {}
|
|
336
|
+
try {
|
|
337
|
+
if (chrome) chrome.kill('SIGKILL');
|
|
338
|
+
} catch (_) {}
|
|
339
|
+
try {
|
|
340
|
+
if (server) server.close();
|
|
341
|
+
} catch (_) {}
|
|
342
|
+
try {
|
|
343
|
+
fs.rmSync(profileDir, { recursive: true, force: true });
|
|
344
|
+
} catch (_) {}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const started = await startStaticServer(ROOT);
|
|
349
|
+
server = started.server;
|
|
350
|
+
const demoUrl = `${started.baseUrl}/demo.html`;
|
|
351
|
+
info(`Static server: ${demoUrl}`);
|
|
352
|
+
|
|
353
|
+
const chromeArgs = [
|
|
354
|
+
'--headless=new',
|
|
355
|
+
'--disable-gpu',
|
|
356
|
+
'--no-sandbox',
|
|
357
|
+
'--disable-dev-shm-usage',
|
|
358
|
+
'--remote-debugging-port=0',
|
|
359
|
+
`--user-data-dir=${profileDir}`,
|
|
360
|
+
'about:blank',
|
|
361
|
+
];
|
|
362
|
+
|
|
363
|
+
chrome = spawn(browserPath, chromeArgs, { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
364
|
+
|
|
365
|
+
let wsUrl = null;
|
|
366
|
+
const wsRe = /DevTools listening on (ws:\/\/[^\s]+)/;
|
|
367
|
+
const stderrChunks = [];
|
|
368
|
+
|
|
369
|
+
chrome.stderr.on('data', (buf) => {
|
|
370
|
+
const s = String(buf);
|
|
371
|
+
stderrChunks.push(s);
|
|
372
|
+
const m = wsRe.exec(s);
|
|
373
|
+
if (m && m[1]) wsUrl = m[1];
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Wait for DevTools endpoint.
|
|
377
|
+
const start = Date.now();
|
|
378
|
+
while (!wsUrl) {
|
|
379
|
+
if (chrome.exitCode != null) {
|
|
380
|
+
throw new Error(`Chromium exited early: ${chrome.exitCode}\n${stderrChunks.join('')}`);
|
|
381
|
+
}
|
|
382
|
+
if (Date.now() - start > 15000) {
|
|
383
|
+
throw new Error(`Timed out waiting for DevTools URL.\n${stderrChunks.join('')}`);
|
|
384
|
+
}
|
|
385
|
+
await sleep(50);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
info(`Chromium: ${browserPath}`);
|
|
389
|
+
info(`CDP: ${wsUrl}`);
|
|
390
|
+
|
|
391
|
+
ws = new WebSocket(wsUrl);
|
|
392
|
+
await new Promise((resolve, reject) => {
|
|
393
|
+
ws.onopen = resolve;
|
|
394
|
+
ws.onerror = reject;
|
|
395
|
+
});
|
|
396
|
+
const cdp = new CDP(ws);
|
|
397
|
+
|
|
398
|
+
// Create and attach to a new page target.
|
|
399
|
+
const { targetId } = await cdp.send('Target.createTarget', { url: 'about:blank' });
|
|
400
|
+
const { sessionId } = await cdp.send('Target.attachToTarget', { targetId, flatten: true });
|
|
401
|
+
|
|
402
|
+
// Capture exceptions and console errors.
|
|
403
|
+
const exceptions = [];
|
|
404
|
+
const consoleErrors = [];
|
|
405
|
+
cdp.on('Runtime.exceptionThrown', sessionId, (p) => exceptions.push(p));
|
|
406
|
+
cdp.on('Log.entryAdded', sessionId, (p) => {
|
|
407
|
+
if (p && p.entry && p.entry.level === 'error') consoleErrors.push(p.entry);
|
|
408
|
+
});
|
|
409
|
+
cdp.on('Runtime.consoleAPICalled', sessionId, (p) => {
|
|
410
|
+
if (p && p.type === 'error') consoleErrors.push({ source: 'console', text: JSON.stringify(p.args || []) });
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
await cdp.send('Page.enable', {}, sessionId);
|
|
414
|
+
await cdp.send('Runtime.enable', {}, sessionId);
|
|
415
|
+
await cdp.send('Log.enable', {}, sessionId);
|
|
416
|
+
await cdp.send('Network.enable', {}, sessionId);
|
|
417
|
+
|
|
418
|
+
// Intercept CodeMirror + remote GitHub raw URLs (keep test deterministic).
|
|
419
|
+
const localPkg = fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8');
|
|
420
|
+
const localEyeling = fs.readFileSync(path.join(ROOT, 'eyeling.js'), 'utf8');
|
|
421
|
+
|
|
422
|
+
const intercept = new Map([
|
|
423
|
+
// CodeMirror assets (CDN)
|
|
424
|
+
['https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.js', { ct: 'application/javascript', body: CODEMIRROR_STUB }],
|
|
425
|
+
['https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/turtle/turtle.min.js', { ct: 'application/javascript', body: '' }],
|
|
426
|
+
['https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/sparql/sparql.min.js', { ct: 'application/javascript', body: '' }],
|
|
427
|
+
['https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.css', { ct: 'text/css', body: '/* stub */\n' }],
|
|
428
|
+
|
|
429
|
+
// GitHub raw references used for "latest" version display
|
|
430
|
+
['https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/package.json', { ct: 'application/json', body: localPkg }],
|
|
431
|
+
['https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/eyeling.js', { ct: 'application/javascript', body: localEyeling }],
|
|
432
|
+
]);
|
|
433
|
+
|
|
434
|
+
await cdp.send(
|
|
435
|
+
'Fetch.enable',
|
|
436
|
+
{
|
|
437
|
+
patterns: [
|
|
438
|
+
{ urlPattern: 'https://cdn.jsdelivr.net/npm/codemirror@5.65.16/*', requestStage: 'Request' },
|
|
439
|
+
{ urlPattern: 'https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/*', requestStage: 'Request' },
|
|
440
|
+
],
|
|
441
|
+
},
|
|
442
|
+
sessionId
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
cdp.on('Fetch.requestPaused', sessionId, async (p) => {
|
|
446
|
+
const url = p && p.request && p.request.url ? p.request.url : '';
|
|
447
|
+
const hit = intercept.get(url);
|
|
448
|
+
try {
|
|
449
|
+
if (hit) {
|
|
450
|
+
await cdp.send(
|
|
451
|
+
'Fetch.fulfillRequest',
|
|
452
|
+
{
|
|
453
|
+
requestId: p.requestId,
|
|
454
|
+
responseCode: 200,
|
|
455
|
+
responseHeaders: [
|
|
456
|
+
{ name: 'Content-Type', value: `${hit.ct}; charset=utf-8` },
|
|
457
|
+
{ name: 'Cache-Control', value: 'no-store' },
|
|
458
|
+
// Avoid CORS surprises for fetch() from the page.
|
|
459
|
+
{ name: 'Access-Control-Allow-Origin', value: '*' },
|
|
460
|
+
],
|
|
461
|
+
body: b64(hit.body),
|
|
462
|
+
},
|
|
463
|
+
sessionId
|
|
464
|
+
);
|
|
465
|
+
} else {
|
|
466
|
+
await cdp.send('Fetch.continueRequest', { requestId: p.requestId }, sessionId);
|
|
467
|
+
}
|
|
468
|
+
} catch (_) {
|
|
469
|
+
// Best-effort: if interception fails, just continue.
|
|
470
|
+
try {
|
|
471
|
+
await cdp.send('Fetch.continueRequest', { requestId: p.requestId }, sessionId);
|
|
472
|
+
} catch (_) {}
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const loadFired = cdp.once('Page.loadEventFired', sessionId, 30000);
|
|
477
|
+
const nav = await cdp.send('Page.navigate', { url: demoUrl }, sessionId);
|
|
478
|
+
assert.ok(!nav.errorText, `demo.html navigation failed: ${nav.errorText}`);
|
|
479
|
+
await loadFired;
|
|
480
|
+
|
|
481
|
+
// Click the Run button.
|
|
482
|
+
await cdp.send(
|
|
483
|
+
'Runtime.evaluate',
|
|
484
|
+
{ expression: `document.getElementById('run-btn') && document.getElementById('run-btn').click();`, returnByValue: true },
|
|
485
|
+
sessionId
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
// Wait for completion and capture output.
|
|
489
|
+
// The demo reports completion with status strings like:
|
|
490
|
+
// "Done. Derived: …", "Done (paused). …", or "Done. (Run N)".
|
|
491
|
+
let last = { status: '', output: '' };
|
|
492
|
+
const deadline = Date.now() + 60000;
|
|
493
|
+
|
|
494
|
+
while (Date.now() < deadline) {
|
|
495
|
+
// Fail fast on runtime exceptions (often indicates a broken CodeMirror stub or worker init).
|
|
496
|
+
if (exceptions.length) {
|
|
497
|
+
throw new Error(`Uncaught exception in demo.html: ${JSON.stringify(exceptions[0] || {})}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const r = await cdp.send(
|
|
501
|
+
'Runtime.evaluate',
|
|
502
|
+
{
|
|
503
|
+
expression: `(() => {
|
|
504
|
+
const s = document.getElementById('status');
|
|
505
|
+
const o = document.getElementById('output-editor');
|
|
506
|
+
return { status: s ? String(s.textContent || '') : '', output: o ? String(o.value || '') : '' };
|
|
507
|
+
})()`,
|
|
508
|
+
returnByValue: true,
|
|
509
|
+
},
|
|
510
|
+
sessionId
|
|
511
|
+
);
|
|
512
|
+
last = r && r.result ? r.result.value : last;
|
|
513
|
+
|
|
514
|
+
const st = (last && typeof last.status === 'string') ? last.status : '';
|
|
515
|
+
|
|
516
|
+
// Treat any "Reasoning error" as failure.
|
|
517
|
+
if (/Reasoning error/i.test(st)) {
|
|
518
|
+
throw new Error(`Playground reported error: ${st}
|
|
519
|
+
Output:
|
|
520
|
+
${last.output || ''}`);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Success conditions: status starts with "Done" (covers "Done." and "Done (paused).")
|
|
524
|
+
if (String(st || '').trim().startsWith('Done')) break;
|
|
525
|
+
|
|
526
|
+
await sleep(100);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
assert.ok(last && typeof last.status === 'string' && String(last.status || '').trim().startsWith('Done'), `Expected Done. Got: ${last.status}`);
|
|
530
|
+
assert.ok(last && typeof last.output === 'string' && last.output.length > 0, 'Expected non-empty output');
|
|
531
|
+
assert.match(last.output, /Socrates/i, 'Expected output to mention Socrates');
|
|
532
|
+
assert.match(last.output, /Mortal/i, 'Expected output to mention Mortal');
|
|
533
|
+
|
|
534
|
+
// Ensure no uncaught runtime exceptions.
|
|
535
|
+
assert.equal(exceptions.length, 0, `Uncaught exceptions in demo.html: ${JSON.stringify(exceptions[0] || {})}`);
|
|
536
|
+
|
|
537
|
+
// Console errors are noisy and often indicate a broken UI.
|
|
538
|
+
// (We suppress known noise like /favicon.ico on the server.)
|
|
539
|
+
assert.equal(consoleErrors.length, 0, `Console errors in demo.html: ${JSON.stringify(consoleErrors[0] || {})}`);
|
|
540
|
+
|
|
541
|
+
// Cleanup.
|
|
542
|
+
try {
|
|
543
|
+
await cdp.send('Browser.close');
|
|
544
|
+
} catch (_) {}
|
|
545
|
+
|
|
546
|
+
ok('demo.html loads and runs the default program');
|
|
547
|
+
} finally {
|
|
548
|
+
await cleanup();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
main().catch((e) => {
|
|
553
|
+
fail(e && e.stack ? e.stack : String(e));
|
|
554
|
+
process.exit(1);
|
|
555
|
+
});
|