@ytspar/sweetlink 1.18.0 → 1.20.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.
- package/README.md +40 -0
- package/dist/cli/outputSchemas.d.ts +57 -1
- package/dist/cli/outputSchemas.d.ts.map +1 -1
- package/dist/cli/outputSchemas.js +36 -1
- package/dist/cli/outputSchemas.js.map +1 -1
- package/dist/cli/sweetlink.js +537 -36
- package/dist/cli/sweetlink.js.map +1 -1
- package/dist/daemon/browser.d.ts.map +1 -1
- package/dist/daemon/browser.js.map +1 -1
- package/dist/daemon/client.d.ts +7 -0
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +16 -2
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/demo.d.ts.map +1 -1
- package/dist/daemon/demo.js +6 -2
- package/dist/daemon/demo.js.map +1 -1
- package/dist/daemon/diff.d.ts.map +1 -1
- package/dist/daemon/diff.js +5 -3
- package/dist/daemon/diff.js.map +1 -1
- package/dist/daemon/evidence.d.ts.map +1 -1
- package/dist/daemon/evidence.js +5 -5
- package/dist/daemon/evidence.js.map +1 -1
- package/dist/daemon/index.js +1 -1
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/listeners.d.ts.map +1 -1
- package/dist/daemon/listeners.js +7 -5
- package/dist/daemon/listeners.js.map +1 -1
- package/dist/daemon/recording.d.ts +5 -0
- package/dist/daemon/recording.d.ts.map +1 -1
- package/dist/daemon/recording.js +34 -11
- package/dist/daemon/recording.js.map +1 -1
- package/dist/daemon/refs.d.ts.map +1 -1
- package/dist/daemon/refs.js.map +1 -1
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +419 -80
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/summary.d.ts +1 -1
- package/dist/daemon/summary.d.ts.map +1 -1
- package/dist/daemon/types.d.ts +1 -1
- package/dist/daemon/types.d.ts.map +1 -1
- package/dist/daemon/types.js.map +1 -1
- package/dist/daemon/viewer.d.ts +1 -1
- package/dist/daemon/viewer.d.ts.map +1 -1
- package/dist/daemon/viewer.js +18 -10
- package/dist/daemon/viewer.js.map +1 -1
- package/dist/ruler.js +3 -1
- package/dist/ruler.js.map +1 -1
- package/dist/runs.d.ts +34 -0
- package/dist/runs.d.ts.map +1 -0
- package/dist/runs.js +61 -0
- package/dist/runs.js.map +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +20 -10
- package/dist/server/index.js.map +1 -1
- package/dist/simulator/android.d.ts +35 -0
- package/dist/simulator/android.d.ts.map +1 -0
- package/dist/simulator/android.js +127 -0
- package/dist/simulator/android.js.map +1 -0
- package/dist/simulator/ios.d.ts +39 -0
- package/dist/simulator/ios.d.ts.map +1 -0
- package/dist/simulator/ios.js +123 -0
- package/dist/simulator/ios.js.map +1 -0
- package/dist/term/ansi.d.ts +37 -0
- package/dist/term/ansi.d.ts.map +1 -0
- package/dist/term/ansi.js +205 -0
- package/dist/term/ansi.js.map +1 -0
- package/dist/term/player.d.ts +25 -0
- package/dist/term/player.d.ts.map +1 -0
- package/dist/term/player.js +243 -0
- package/dist/term/player.js.map +1 -0
- package/dist/term/recorder.d.ts +33 -0
- package/dist/term/recorder.d.ts.map +1 -0
- package/dist/term/recorder.js +77 -0
- package/dist/term/recorder.js.map +1 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +8 -4
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon/server.js
CHANGED
|
@@ -6,17 +6,17 @@
|
|
|
6
6
|
* Manages idle timer for auto-shutdown.
|
|
7
7
|
*/
|
|
8
8
|
import { createServer } from 'http';
|
|
9
|
-
import { closeBrowser, getBrowserInstance, getPage, initBrowser, takeResponsiveScreenshots, takeScreenshot } from './browser.js';
|
|
10
|
-
import { annotateScreenshot, diffSnapshots } from './diff.js';
|
|
9
|
+
import { closeBrowser, getBrowserInstance, getPage, initBrowser, takeResponsiveScreenshots, takeScreenshot, } from './browser.js';
|
|
11
10
|
import { takeDeviceScreenshots } from './devices.js';
|
|
12
|
-
import {
|
|
13
|
-
import { getRecordingPage, getRecordingStatus, isRecording, logAction, pauseRecording, resumeRecording, startRecording, stopRecording } from './recording.js';
|
|
11
|
+
import { annotateScreenshot, diffSnapshots } from './diff.js';
|
|
14
12
|
import { detectServerErrors } from './errorPatterns.js';
|
|
13
|
+
import { consoleBuffer, dialogBuffer, formatConsoleEntries, formatNetworkEntries, getErrorCount, getWarningCount, networkBuffer, } from './listeners.js';
|
|
14
|
+
import { getRecordingEventCursors, getRecordingPage, getRecordingStatus, isRecording, logAction, pauseRecording, resumeRecording, startRecording, stopRecording, } from './recording.js';
|
|
15
|
+
import { buildRefMap, checkRefStale, formatRefMap, getBaseline, getCurrentRefMap, resolveRef, setBaseline, } from './refs.js';
|
|
15
16
|
import { generateSummary } from './summary.js';
|
|
17
|
+
import { DAEMON_IDLE_TIMEOUT_MS, DEFAULT_RESPONSIVE_VIEWPORTS } from './types.js';
|
|
16
18
|
import { generateViewer } from './viewer.js';
|
|
17
19
|
import { visualDiff } from './visualDiff.js';
|
|
18
|
-
import { buildRefMap, checkRefStale, formatRefMap, getBaseline, getCurrentRefMap, resolveRef, setBaseline, } from './refs.js';
|
|
19
|
-
import { DAEMON_IDLE_TIMEOUT_MS, DEFAULT_RESPONSIVE_VIEWPORTS } from './types.js';
|
|
20
20
|
// ============================================================================
|
|
21
21
|
// State
|
|
22
22
|
// ============================================================================
|
|
@@ -197,7 +197,10 @@ async function captureFailure(page, reason) {
|
|
|
197
197
|
const dir = '.sweetlink/failures';
|
|
198
198
|
await fsp.mkdir(dir, { recursive: true });
|
|
199
199
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
200
|
-
const slug = reason
|
|
200
|
+
const slug = reason
|
|
201
|
+
.replace(/[^a-z0-9]/gi, '-')
|
|
202
|
+
.slice(0, 40)
|
|
203
|
+
.toLowerCase();
|
|
201
204
|
const filePath = `${dir}/${stamp}-${slug}.png`;
|
|
202
205
|
const buf = await page.screenshot({ fullPage: false });
|
|
203
206
|
await fsp.writeFile(filePath, buf);
|
|
@@ -207,6 +210,307 @@ async function captureFailure(page, reason) {
|
|
|
207
210
|
return undefined;
|
|
208
211
|
}
|
|
209
212
|
}
|
|
213
|
+
function slugifyArtifact(value) {
|
|
214
|
+
return (value
|
|
215
|
+
.toLowerCase()
|
|
216
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
217
|
+
.replace(/^-|-$/g, '')
|
|
218
|
+
.slice(0, 72) || 'inspect');
|
|
219
|
+
}
|
|
220
|
+
function normalizeActionTranscript(value) {
|
|
221
|
+
if (!Array.isArray(value))
|
|
222
|
+
return [];
|
|
223
|
+
return value.flatMap((entry) => {
|
|
224
|
+
if (!entry || typeof entry !== 'object')
|
|
225
|
+
return [];
|
|
226
|
+
const record = entry;
|
|
227
|
+
const action = typeof record.action === 'string' ? record.action : undefined;
|
|
228
|
+
if (!action)
|
|
229
|
+
return [];
|
|
230
|
+
return [
|
|
231
|
+
{
|
|
232
|
+
action,
|
|
233
|
+
target: typeof record.target === 'string' ? record.target : undefined,
|
|
234
|
+
result: typeof record.result === 'string' ? record.result : undefined,
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
async function getPageInfo(page) {
|
|
240
|
+
return page.evaluate(() => {
|
|
241
|
+
const nav = performance.getEntriesByType('navigation')[0];
|
|
242
|
+
const fcp = performance.getEntriesByName('first-contentful-paint')[0];
|
|
243
|
+
return {
|
|
244
|
+
url: location.href,
|
|
245
|
+
title: document.title,
|
|
246
|
+
viewport: {
|
|
247
|
+
width: window.innerWidth,
|
|
248
|
+
height: window.innerHeight,
|
|
249
|
+
deviceScaleFactor: window.devicePixelRatio,
|
|
250
|
+
},
|
|
251
|
+
vitals: {
|
|
252
|
+
fcp: fcp ? Math.round(fcp.startTime) : null,
|
|
253
|
+
pageSize: nav ? nav.transferSize || nav.encodedBodySize || null : null,
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async function getAxeSource() {
|
|
259
|
+
try {
|
|
260
|
+
const axeModule = await import('axe-core');
|
|
261
|
+
const candidate = axeModule;
|
|
262
|
+
return candidate.source ?? candidate.default?.source ?? null;
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async function runInspectA11y(page) {
|
|
269
|
+
const source = await getAxeSource();
|
|
270
|
+
if (!source) {
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
error: 'axe-core is not available in this environment',
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
await page.addScriptTag({ content: source });
|
|
278
|
+
return await page.evaluate(async () => {
|
|
279
|
+
const axe = window.axe;
|
|
280
|
+
if (!axe)
|
|
281
|
+
throw new Error('axe-core did not initialize on the page');
|
|
282
|
+
const compactIssue = (issue) => ({
|
|
283
|
+
id: issue.id,
|
|
284
|
+
impact: issue.impact ?? 'unknown',
|
|
285
|
+
help: issue.help ?? '',
|
|
286
|
+
description: issue.description ?? '',
|
|
287
|
+
helpUrl: issue.helpUrl ?? '',
|
|
288
|
+
nodes: (issue.nodes ?? []).slice(0, 3).map((node) => ({
|
|
289
|
+
target: (node.target ?? []).join(' '),
|
|
290
|
+
html: (node.html ?? '').slice(0, 180),
|
|
291
|
+
summary: node.failureSummary ??
|
|
292
|
+
node.any?.[0]?.message ??
|
|
293
|
+
node.all?.[0]?.message ??
|
|
294
|
+
node.none?.[0]?.message ??
|
|
295
|
+
'',
|
|
296
|
+
})),
|
|
297
|
+
});
|
|
298
|
+
const result = await axe.run(document, {
|
|
299
|
+
runOnly: {
|
|
300
|
+
type: 'tag',
|
|
301
|
+
values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'best-practice'],
|
|
302
|
+
},
|
|
303
|
+
rules: {
|
|
304
|
+
'color-contrast': { enabled: true },
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
const byImpact = {};
|
|
308
|
+
for (const violation of result.violations) {
|
|
309
|
+
const impact = violation.impact ?? 'unknown';
|
|
310
|
+
byImpact[impact] = (byImpact[impact] ?? 0) + 1;
|
|
311
|
+
}
|
|
312
|
+
return {
|
|
313
|
+
ok: true,
|
|
314
|
+
summary: {
|
|
315
|
+
violations: result.violations.length,
|
|
316
|
+
incomplete: result.incomplete.length,
|
|
317
|
+
passes: result.passes.length,
|
|
318
|
+
byImpact,
|
|
319
|
+
},
|
|
320
|
+
violations: result.violations.map(compactIssue),
|
|
321
|
+
incomplete: result.incomplete.map(compactIssue),
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
return {
|
|
327
|
+
ok: false,
|
|
328
|
+
error: error instanceof Error ? error.message : String(error),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function inspectNextActions(counts, artifacts) {
|
|
333
|
+
const actions = [
|
|
334
|
+
`Open ${artifacts.summaryMarkdown} and ${artifacts.screenshotPng} before making visual claims.`,
|
|
335
|
+
];
|
|
336
|
+
if (counts.consoleErrors > 0) {
|
|
337
|
+
actions.push('Investigate console errors before changing UI behavior.');
|
|
338
|
+
}
|
|
339
|
+
else if (counts.consoleWarnings > 0) {
|
|
340
|
+
actions.push('Review console warnings for stale props, hydration, or deprecated API signals.');
|
|
341
|
+
}
|
|
342
|
+
if (counts.networkFailures > 0) {
|
|
343
|
+
actions.push('Inspect failed network entries before assuming the UI state is a component bug.');
|
|
344
|
+
}
|
|
345
|
+
if ((counts.a11yViolations ?? 0) > 0 || (counts.a11yIncomplete ?? 0) > 0) {
|
|
346
|
+
actions.push(`Use ${artifacts.a11yJson ?? 'the a11y artifact'} to prioritize accessibility fixes.`);
|
|
347
|
+
}
|
|
348
|
+
if (counts.refs > 0) {
|
|
349
|
+
actions.push('Use @e refs for click/fill/press actions, then rerun inspect after DOM changes.');
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
actions.push('No interactive refs were found; inspect the DOM/outline before attempting actions.');
|
|
353
|
+
}
|
|
354
|
+
return actions;
|
|
355
|
+
}
|
|
356
|
+
function renderInspectSummary(data) {
|
|
357
|
+
const transcript = data.actionTranscript.length > 0
|
|
358
|
+
? data.actionTranscript
|
|
359
|
+
.map((entry, index) => {
|
|
360
|
+
const target = entry.target ? ` target=${entry.target}` : '';
|
|
361
|
+
const result = entry.result ? ` result=${entry.result}` : '';
|
|
362
|
+
return `${index + 1}. ${entry.action}${target}${result}`;
|
|
363
|
+
})
|
|
364
|
+
.join('\n')
|
|
365
|
+
: '(none supplied)';
|
|
366
|
+
const refs = data.refs.length > 0
|
|
367
|
+
? data.refs.map((ref) => `- ${ref.ref} [${ref.role}] "${ref.name}"`).join('\n')
|
|
368
|
+
: '(no interactive refs)';
|
|
369
|
+
const a11ySummary = data.a11y
|
|
370
|
+
? JSON.stringify(data.a11y.summary ?? { ok: data.a11y.ok, error: data.a11y.error }, null, 2)
|
|
371
|
+
: '(skipped)';
|
|
372
|
+
return [
|
|
373
|
+
'# Sweetlink Inspect',
|
|
374
|
+
'',
|
|
375
|
+
`- URL: ${data.url}`,
|
|
376
|
+
`- Title: ${data.title || '(untitled)'}`,
|
|
377
|
+
`- Generated: ${data.generatedAt}`,
|
|
378
|
+
`- Viewport: ${data.viewport.width}x${data.viewport.height} @${data.viewport.deviceScaleFactor}x`,
|
|
379
|
+
`- FCP: ${data.vitals.fcp ?? 'n/a'}ms`,
|
|
380
|
+
`- Page size: ${data.vitals.pageSize ?? 'n/a'} bytes`,
|
|
381
|
+
'',
|
|
382
|
+
'## Expected Outcome',
|
|
383
|
+
'',
|
|
384
|
+
data.expectedOutcome ?? '(not supplied)',
|
|
385
|
+
'',
|
|
386
|
+
'## Action Transcript',
|
|
387
|
+
'',
|
|
388
|
+
transcript,
|
|
389
|
+
'',
|
|
390
|
+
'## Counts',
|
|
391
|
+
'',
|
|
392
|
+
`- Interactive refs: ${data.counts.refs}`,
|
|
393
|
+
`- Console: ${data.counts.consoleEntries} entries, ${data.counts.consoleErrors} errors, ${data.counts.consoleWarnings} warnings`,
|
|
394
|
+
`- Network: ${data.counts.networkEntries} entries, ${data.counts.networkFailures} failures`,
|
|
395
|
+
`- Accessibility: ${data.counts.a11yViolations ?? 'n/a'} violations, ${data.counts.a11yIncomplete ?? 'n/a'} incomplete`,
|
|
396
|
+
'',
|
|
397
|
+
'## Next Actions',
|
|
398
|
+
'',
|
|
399
|
+
data.nextActions.map((action) => `- ${action}`).join('\n'),
|
|
400
|
+
'',
|
|
401
|
+
'## Artifacts',
|
|
402
|
+
'',
|
|
403
|
+
...Object.entries(data.artifacts).map(([key, value]) => `- ${key}: ${value ?? '(not generated)'}`),
|
|
404
|
+
'',
|
|
405
|
+
'## Interactive Refs',
|
|
406
|
+
'',
|
|
407
|
+
refs,
|
|
408
|
+
'',
|
|
409
|
+
'## Console',
|
|
410
|
+
'',
|
|
411
|
+
data.consoleText,
|
|
412
|
+
'',
|
|
413
|
+
'## Network',
|
|
414
|
+
'',
|
|
415
|
+
data.networkText,
|
|
416
|
+
'',
|
|
417
|
+
'## Accessibility',
|
|
418
|
+
'',
|
|
419
|
+
a11ySummary,
|
|
420
|
+
'',
|
|
421
|
+
].join('\n');
|
|
422
|
+
}
|
|
423
|
+
async function handleInspect(params, url) {
|
|
424
|
+
await initBrowser(url);
|
|
425
|
+
const page = getRecordingPage() ?? getPage();
|
|
426
|
+
const pageInfo = await getPageInfo(page);
|
|
427
|
+
const generatedAt = new Date().toISOString();
|
|
428
|
+
const label = typeof params.label === 'string' ? params.label : 'inspect';
|
|
429
|
+
const stamp = generatedAt.replace(/[:.]/g, '-');
|
|
430
|
+
const { promises: fsp } = await import('fs');
|
|
431
|
+
const path = await import('path');
|
|
432
|
+
const dir = path.resolve(`.sweetlink/inspect/${stamp}-${slugifyArtifact(label)}`);
|
|
433
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
434
|
+
const lastRaw = typeof params.last === 'number' ? params.last : Number(params.last);
|
|
435
|
+
const last = Number.isFinite(lastRaw) && lastRaw > 0 ? Math.min(Math.floor(lastRaw), 500) : 50;
|
|
436
|
+
const includeA11y = params.includeA11y !== false;
|
|
437
|
+
const expectedOutcome = typeof params.expectedOutcome === 'string' ? params.expectedOutcome : undefined;
|
|
438
|
+
const actionTranscript = normalizeActionTranscript(params.actionTranscript);
|
|
439
|
+
const refMap = await buildRefMap(page, { interactive: true });
|
|
440
|
+
const snapshotText = formatRefMap(refMap);
|
|
441
|
+
const screenshotBuffer = await page.screenshot({ fullPage: true });
|
|
442
|
+
const consoleEntries = consoleBuffer.toArray().slice(-last);
|
|
443
|
+
const networkEntries = networkBuffer.toArray().slice(-last);
|
|
444
|
+
const consoleText = formatConsoleEntries(consoleEntries);
|
|
445
|
+
const networkText = formatNetworkEntries(networkEntries);
|
|
446
|
+
const a11y = includeA11y ? await runInspectA11y(page) : undefined;
|
|
447
|
+
const artifacts = {
|
|
448
|
+
dir,
|
|
449
|
+
summaryMarkdown: path.join(dir, 'SUMMARY.md'),
|
|
450
|
+
contextJson: path.join(dir, 'context.json'),
|
|
451
|
+
screenshotPng: path.join(dir, 'screenshot.png'),
|
|
452
|
+
snapshotMarkdown: path.join(dir, 'snapshot.md'),
|
|
453
|
+
consoleText: path.join(dir, 'console.txt'),
|
|
454
|
+
networkText: path.join(dir, 'network.txt'),
|
|
455
|
+
a11yJson: includeA11y ? path.join(dir, 'a11y.json') : undefined,
|
|
456
|
+
};
|
|
457
|
+
const a11ySummary = a11y?.summary;
|
|
458
|
+
const counts = {
|
|
459
|
+
refs: refMap.entries.length,
|
|
460
|
+
consoleEntries: consoleEntries.length,
|
|
461
|
+
consoleErrors: consoleEntries.filter((entry) => entry.level === 'error').length,
|
|
462
|
+
consoleWarnings: consoleEntries.filter((entry) => entry.level === 'warning').length,
|
|
463
|
+
networkEntries: networkEntries.length,
|
|
464
|
+
networkFailures: networkEntries.filter((entry) => entry.status >= 400 || entry.status === 0)
|
|
465
|
+
.length,
|
|
466
|
+
a11yViolations: a11ySummary?.violations,
|
|
467
|
+
a11yIncomplete: a11ySummary?.incomplete,
|
|
468
|
+
};
|
|
469
|
+
const nextActions = inspectNextActions(counts, artifacts);
|
|
470
|
+
const context = {
|
|
471
|
+
url: pageInfo.url,
|
|
472
|
+
title: pageInfo.title,
|
|
473
|
+
generatedAt,
|
|
474
|
+
viewport: pageInfo.viewport,
|
|
475
|
+
vitals: pageInfo.vitals,
|
|
476
|
+
artifacts,
|
|
477
|
+
counts,
|
|
478
|
+
refs: refMap.entries,
|
|
479
|
+
console: { entries: consoleEntries, formatted: consoleText },
|
|
480
|
+
network: { entries: networkEntries, formatted: networkText },
|
|
481
|
+
a11y,
|
|
482
|
+
expectedOutcome,
|
|
483
|
+
actionTranscript,
|
|
484
|
+
failureArtifacts: [],
|
|
485
|
+
nextActions,
|
|
486
|
+
};
|
|
487
|
+
await fsp.writeFile(artifacts.screenshotPng, screenshotBuffer);
|
|
488
|
+
await fsp.writeFile(artifacts.snapshotMarkdown, snapshotText, 'utf-8');
|
|
489
|
+
await fsp.writeFile(artifacts.consoleText, consoleText, 'utf-8');
|
|
490
|
+
await fsp.writeFile(artifacts.networkText, networkText, 'utf-8');
|
|
491
|
+
if (artifacts.a11yJson) {
|
|
492
|
+
await fsp.writeFile(artifacts.a11yJson, JSON.stringify(a11y ?? null, null, 2), 'utf-8');
|
|
493
|
+
}
|
|
494
|
+
const summary = renderInspectSummary({
|
|
495
|
+
url: context.url,
|
|
496
|
+
title: context.title,
|
|
497
|
+
generatedAt: context.generatedAt,
|
|
498
|
+
viewport: context.viewport,
|
|
499
|
+
vitals: context.vitals,
|
|
500
|
+
artifacts: context.artifacts,
|
|
501
|
+
counts: context.counts,
|
|
502
|
+
refs: context.refs,
|
|
503
|
+
consoleText,
|
|
504
|
+
networkText,
|
|
505
|
+
a11y,
|
|
506
|
+
expectedOutcome,
|
|
507
|
+
actionTranscript,
|
|
508
|
+
nextActions,
|
|
509
|
+
});
|
|
510
|
+
await fsp.writeFile(artifacts.summaryMarkdown, summary, 'utf-8');
|
|
511
|
+
await fsp.writeFile(artifacts.contextJson, JSON.stringify(context, null, 2), 'utf-8');
|
|
512
|
+
return { ok: true, data: context };
|
|
513
|
+
}
|
|
210
514
|
async function handleClickRef(params, url) {
|
|
211
515
|
await initBrowser(url);
|
|
212
516
|
// Use recording page if recording, otherwise main page
|
|
@@ -238,7 +542,17 @@ async function handleClickRef(params, url) {
|
|
|
238
542
|
}
|
|
239
543
|
const box = await locator.boundingBox();
|
|
240
544
|
const t0 = Date.now();
|
|
241
|
-
|
|
545
|
+
try {
|
|
546
|
+
await locator.click();
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
const failureScreenshot = await captureFailure(page, `click-failed-${ref}`);
|
|
550
|
+
return {
|
|
551
|
+
ok: false,
|
|
552
|
+
error: `Click failed for ${ref}: ${error instanceof Error ? error.message : String(error)}`,
|
|
553
|
+
data: failureScreenshot ? { failureScreenshot } : undefined,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
242
556
|
const durationMs = Date.now() - t0;
|
|
243
557
|
// Log action if recording
|
|
244
558
|
if (isRecording()) {
|
|
@@ -277,7 +591,17 @@ async function handleFillRef(params, url) {
|
|
|
277
591
|
}
|
|
278
592
|
const box = await locator.boundingBox();
|
|
279
593
|
const t0 = Date.now();
|
|
280
|
-
|
|
594
|
+
try {
|
|
595
|
+
await locator.fill(value);
|
|
596
|
+
}
|
|
597
|
+
catch (error) {
|
|
598
|
+
const failureScreenshot = await captureFailure(page, `fill-failed-${ref}`);
|
|
599
|
+
return {
|
|
600
|
+
ok: false,
|
|
601
|
+
error: `Fill failed for ${ref}: ${error instanceof Error ? error.message : String(error)}`,
|
|
602
|
+
data: failureScreenshot ? { failureScreenshot } : undefined,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
281
605
|
const durationMs = Date.now() - t0;
|
|
282
606
|
if (isRecording()) {
|
|
283
607
|
await logAction('fill', [ref, value], page, box ?? undefined, durationMs);
|
|
@@ -334,11 +658,13 @@ async function handleClickCss(params, url) {
|
|
|
334
658
|
return { ours: true };
|
|
335
659
|
if (top === intended || intended.contains(top))
|
|
336
660
|
return { ours: true };
|
|
337
|
-
const desc =
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
661
|
+
const desc = [
|
|
662
|
+
top.tagName.toLowerCase(),
|
|
663
|
+
top.id ? `#${top.id}` : '',
|
|
664
|
+
top.className && typeof top.className === 'string'
|
|
665
|
+
? `.${top.className.split(/\s+/).filter(Boolean).slice(0, 2).join('.')}`
|
|
666
|
+
: '',
|
|
667
|
+
].join('');
|
|
342
668
|
return { ours: false, coveredBy: desc };
|
|
343
669
|
}, { cx, cy, sel: selector ?? null });
|
|
344
670
|
if (!info.ours) {
|
|
@@ -499,11 +825,15 @@ async function handleRecordStart(params, url) {
|
|
|
499
825
|
const storageState = params.storageState || undefined;
|
|
500
826
|
const trace = params.trace || undefined;
|
|
501
827
|
const result = await startRecording(browser, url, '.sweetlink', {
|
|
502
|
-
viewport,
|
|
828
|
+
viewport,
|
|
829
|
+
label,
|
|
830
|
+
storageState,
|
|
831
|
+
trace,
|
|
503
832
|
});
|
|
504
833
|
return { ok: true, data: { sessionId: result.sessionId, label, trace: !!trace } };
|
|
505
834
|
}
|
|
506
835
|
async function handleRecordStop() {
|
|
836
|
+
const eventCursors = getRecordingEventCursors();
|
|
507
837
|
const manifest = await stopRecording();
|
|
508
838
|
if (!manifest) {
|
|
509
839
|
return { ok: false, error: 'No recording in progress' };
|
|
@@ -513,8 +843,14 @@ async function handleRecordStop() {
|
|
|
513
843
|
let viewerPath;
|
|
514
844
|
let summaryPath;
|
|
515
845
|
try {
|
|
516
|
-
const
|
|
517
|
-
const
|
|
846
|
+
const allConsoleLogs = consoleBuffer.toArray();
|
|
847
|
+
const allNetworkLogs = networkBuffer.toArray();
|
|
848
|
+
const consoleLogs = eventCursors
|
|
849
|
+
? allConsoleLogs.slice(eventCursors.consoleStartCursor)
|
|
850
|
+
: allConsoleLogs;
|
|
851
|
+
const networkLogs = eventCursors
|
|
852
|
+
? allNetworkLogs.slice(eventCursors.networkStartCursor)
|
|
853
|
+
: allNetworkLogs;
|
|
518
854
|
viewerPath = await generateViewer(manifest, {
|
|
519
855
|
sessionDir,
|
|
520
856
|
consoleEntries: consoleLogs,
|
|
@@ -523,16 +859,17 @@ async function handleRecordStop() {
|
|
|
523
859
|
// Generate SUMMARY.md
|
|
524
860
|
const { promises: fsp } = await import('fs');
|
|
525
861
|
// Detect server errors from console log messages
|
|
526
|
-
const consoleText = consoleLogs.map(e => e.message).join('\n');
|
|
862
|
+
const consoleText = consoleLogs.map((e) => e.message).join('\n');
|
|
527
863
|
const serverErrors = detectServerErrors(consoleText);
|
|
528
864
|
if (serverErrors.length > 0) {
|
|
529
865
|
manifest.errors.server = serverErrors.length;
|
|
530
866
|
}
|
|
867
|
+
await fsp.writeFile(`${sessionDir}/sweetlink-session.json`, JSON.stringify(manifest, null, 2), 'utf-8');
|
|
531
868
|
const summaryMd = generateSummary({
|
|
532
869
|
manifest,
|
|
533
870
|
consoleEntries: consoleLogs,
|
|
534
871
|
networkEntries: networkLogs,
|
|
535
|
-
serverErrors: serverErrors.map(e => ({
|
|
872
|
+
serverErrors: serverErrors.map((e) => ({
|
|
536
873
|
source: 'server',
|
|
537
874
|
message: e.line,
|
|
538
875
|
timestamp: Date.now(),
|
|
@@ -549,7 +886,9 @@ async function handleRecordStop() {
|
|
|
549
886
|
console.error('[Daemon] Report generation error:', e);
|
|
550
887
|
}
|
|
551
888
|
// Include a browser-accessible URL for the viewer
|
|
552
|
-
const viewerUrl = manifest.sessionId && daemonPort
|
|
889
|
+
const viewerUrl = manifest.sessionId && daemonPort
|
|
890
|
+
? `http://127.0.0.1:${daemonPort}/viewer/${manifest.sessionId}`
|
|
891
|
+
: undefined;
|
|
553
892
|
return { ok: true, data: { manifest, viewerPath, viewerUrl, summaryPath } };
|
|
554
893
|
}
|
|
555
894
|
async function handleRecordStatus() {
|
|
@@ -588,8 +927,14 @@ async function handleSessionsList() {
|
|
|
588
927
|
try {
|
|
589
928
|
const raw = await fsp.readFile(manifestPath, 'utf-8');
|
|
590
929
|
const m = JSON.parse(raw);
|
|
591
|
-
const hasVideo = await fsp
|
|
592
|
-
|
|
930
|
+
const hasVideo = await fsp
|
|
931
|
+
.access(path.join(dir, e.name, 'session.webm'))
|
|
932
|
+
.then(() => true)
|
|
933
|
+
.catch(() => false);
|
|
934
|
+
const hasViewer = await fsp
|
|
935
|
+
.access(path.join(dir, e.name, 'viewer.html'))
|
|
936
|
+
.then(() => true)
|
|
937
|
+
.catch(() => false);
|
|
593
938
|
sessions.push({
|
|
594
939
|
sessionId: m.sessionId,
|
|
595
940
|
label: m.label,
|
|
@@ -611,16 +956,20 @@ async function handleSessionsList() {
|
|
|
611
956
|
sessions.sort((a, b) => (b.startedAt ?? '').localeCompare(a.startedAt ?? ''));
|
|
612
957
|
// Also write an index.html that links to every viewer for quick browsing.
|
|
613
958
|
try {
|
|
614
|
-
const items = sessions
|
|
959
|
+
const items = sessions
|
|
960
|
+
.map((s) => {
|
|
615
961
|
const viewerLink = s.hasViewer ? `${s.sessionId}/viewer.html` : '#';
|
|
616
962
|
const errorsTotal = s.errors ? s.errors.console + s.errors.network + s.errors.server : 0;
|
|
617
963
|
const errBadge = errorsTotal > 0
|
|
618
964
|
? `<span style="color:#c00;font-weight:600">${errorsTotal} err</span>`
|
|
619
965
|
: '<span style="color:#0a0">clean</span>';
|
|
620
|
-
const labelHtml = s.label
|
|
966
|
+
const labelHtml = s.label
|
|
967
|
+
? `<span style="color:#06c">${escapeHtml(s.label)}</span> · `
|
|
968
|
+
: '';
|
|
621
969
|
const dur = s.duration ? `${s.duration.toFixed(1)}s` : '—';
|
|
622
|
-
return `<li><a href="${viewerLink}">${labelHtml}<code>${
|
|
623
|
-
})
|
|
970
|
+
return `<li><a href="${viewerLink}">${labelHtml}<code>${escapeHtml(s.sessionId)}</code></a> · ${escapeHtml(s.url ?? '')} · ${dur} · ${s.actionCount} actions · ${errBadge}</li>`;
|
|
971
|
+
})
|
|
972
|
+
.join('\n');
|
|
624
973
|
const indexHtml = `<!DOCTYPE html>
|
|
625
974
|
<html><head><title>Sweetlink Sessions</title>
|
|
626
975
|
<style>
|
|
@@ -635,15 +984,21 @@ a{text-decoration:none;color:#222}
|
|
|
635
984
|
</body></html>`;
|
|
636
985
|
await fsp.writeFile(path.join(dir, 'index.html'), indexHtml);
|
|
637
986
|
}
|
|
638
|
-
catch {
|
|
987
|
+
catch {
|
|
988
|
+
/* index is best-effort */
|
|
989
|
+
}
|
|
639
990
|
return { ok: true, data: { sessions, indexPath: path.join(dir, 'index.html') } };
|
|
640
991
|
}
|
|
641
992
|
catch (err) {
|
|
642
993
|
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
643
994
|
}
|
|
644
995
|
}
|
|
645
|
-
function
|
|
646
|
-
return String(s)
|
|
996
|
+
function escapeHtml(s) {
|
|
997
|
+
return String(s)
|
|
998
|
+
.replace(/&/g, '&')
|
|
999
|
+
.replace(/</g, '<')
|
|
1000
|
+
.replace(/>/g, '>')
|
|
1001
|
+
.replace(/"/g, '"');
|
|
647
1002
|
}
|
|
648
1003
|
async function handleGenerateViewer(params) {
|
|
649
1004
|
const sessionDir = params.sessionDir;
|
|
@@ -663,7 +1018,10 @@ async function handleGenerateViewer(params) {
|
|
|
663
1018
|
return { ok: true, data: { viewerPath } };
|
|
664
1019
|
}
|
|
665
1020
|
catch (error) {
|
|
666
|
-
return {
|
|
1021
|
+
return {
|
|
1022
|
+
ok: false,
|
|
1023
|
+
error: `Failed to generate viewer: ${error instanceof Error ? error.message : error}`,
|
|
1024
|
+
};
|
|
667
1025
|
}
|
|
668
1026
|
}
|
|
669
1027
|
async function handleVisualDiff(params) {
|
|
@@ -692,55 +1050,36 @@ async function handleVisualDiff(params) {
|
|
|
692
1050
|
// ============================================================================
|
|
693
1051
|
// Request Handling
|
|
694
1052
|
// ============================================================================
|
|
1053
|
+
const DAEMON_HANDLERS = {
|
|
1054
|
+
ping: () => handlePing(),
|
|
1055
|
+
shutdown: () => handleShutdown(),
|
|
1056
|
+
screenshot: (params, url) => handleScreenshot(params, url),
|
|
1057
|
+
'screenshot-responsive': (params, url) => handleResponsiveScreenshot(params, url),
|
|
1058
|
+
snapshot: (params, url) => handleSnapshot(params, url),
|
|
1059
|
+
inspect: (params, url) => handleInspect(params, url),
|
|
1060
|
+
'click-ref': (params, url) => handleClickRef(params, url),
|
|
1061
|
+
'click-css': (params, url) => handleClickCss(params, url),
|
|
1062
|
+
'fill-ref': (params, url) => handleFillRef(params, url),
|
|
1063
|
+
'hover-ref': (params, url) => handleHoverRef(params, url),
|
|
1064
|
+
'press-key': (params, url) => handlePressKey(params, url),
|
|
1065
|
+
'console-read': (params, url) => handleConsoleRead(params, url),
|
|
1066
|
+
'network-read': (params, url) => handleNetworkRead(params, url),
|
|
1067
|
+
'dialog-read': (_params, url) => handleDialogRead(url),
|
|
1068
|
+
'screenshot-devices': (params, url) => handleScreenshotDevices(params, url),
|
|
1069
|
+
'visual-diff': (params) => handleVisualDiff(params),
|
|
1070
|
+
'record-start': (params, url) => handleRecordStart(params, url),
|
|
1071
|
+
'record-pause': () => handleRecordPause(),
|
|
1072
|
+
'record-resume': () => handleRecordResume(),
|
|
1073
|
+
'sessions-list': () => handleSessionsList(),
|
|
1074
|
+
'record-stop': () => handleRecordStop(),
|
|
1075
|
+
'record-status': () => handleRecordStatus(),
|
|
1076
|
+
'generate-viewer': (params) => handleGenerateViewer(params),
|
|
1077
|
+
};
|
|
695
1078
|
async function handleRequest(action, params, url) {
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
return handleShutdown();
|
|
701
|
-
case 'screenshot':
|
|
702
|
-
return handleScreenshot(params, url);
|
|
703
|
-
case 'screenshot-responsive':
|
|
704
|
-
return handleResponsiveScreenshot(params, url);
|
|
705
|
-
case 'snapshot':
|
|
706
|
-
return handleSnapshot(params, url);
|
|
707
|
-
case 'click-ref':
|
|
708
|
-
return handleClickRef(params, url);
|
|
709
|
-
case 'click-css':
|
|
710
|
-
return handleClickCss(params, url);
|
|
711
|
-
case 'fill-ref':
|
|
712
|
-
return handleFillRef(params, url);
|
|
713
|
-
case 'hover-ref':
|
|
714
|
-
return handleHoverRef(params, url);
|
|
715
|
-
case 'press-key':
|
|
716
|
-
return handlePressKey(params, url);
|
|
717
|
-
case 'console-read':
|
|
718
|
-
return handleConsoleRead(params, url);
|
|
719
|
-
case 'network-read':
|
|
720
|
-
return handleNetworkRead(params, url);
|
|
721
|
-
case 'dialog-read':
|
|
722
|
-
return handleDialogRead(url);
|
|
723
|
-
case 'screenshot-devices':
|
|
724
|
-
return handleScreenshotDevices(params, url);
|
|
725
|
-
case 'visual-diff':
|
|
726
|
-
return handleVisualDiff(params);
|
|
727
|
-
case 'record-start':
|
|
728
|
-
return handleRecordStart(params, url);
|
|
729
|
-
case 'record-pause':
|
|
730
|
-
return handleRecordPause();
|
|
731
|
-
case 'record-resume':
|
|
732
|
-
return handleRecordResume();
|
|
733
|
-
case 'sessions-list':
|
|
734
|
-
return handleSessionsList();
|
|
735
|
-
case 'record-stop':
|
|
736
|
-
return handleRecordStop();
|
|
737
|
-
case 'record-status':
|
|
738
|
-
return handleRecordStatus();
|
|
739
|
-
case 'generate-viewer':
|
|
740
|
-
return handleGenerateViewer(params);
|
|
741
|
-
default:
|
|
742
|
-
return { ok: false, error: `Unknown action: ${action}` };
|
|
743
|
-
}
|
|
1079
|
+
const handler = DAEMON_HANDLERS[action];
|
|
1080
|
+
if (!handler)
|
|
1081
|
+
return { ok: false, error: `Unknown action: ${action}` };
|
|
1082
|
+
return handler(params, url);
|
|
744
1083
|
}
|
|
745
1084
|
function readBody(req) {
|
|
746
1085
|
return new Promise((resolve, reject) => {
|
|
@@ -807,8 +1146,8 @@ export function startServer(options) {
|
|
|
807
1146
|
const { promises: fsp } = await import('fs');
|
|
808
1147
|
const entries = await fsp.readdir('.sweetlink', { withFileTypes: true });
|
|
809
1148
|
const sessions = entries
|
|
810
|
-
.filter(e => e.isDirectory() && e.name.startsWith('session-'))
|
|
811
|
-
.map(e => e.name)
|
|
1149
|
+
.filter((e) => e.isDirectory() && e.name.startsWith('session-'))
|
|
1150
|
+
.map((e) => e.name)
|
|
812
1151
|
.sort()
|
|
813
1152
|
.reverse();
|
|
814
1153
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|