scenescout 3.20.1 → 3.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/dist/check-run.js +21 -0
- package/dist/cli.js +5 -1
- package/dist/engine/browser.js +6 -24
- package/dist/engine/film.js +41 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# scenescout
|
|
2
2
|
|
|
3
|
+
## 3.20.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f8f7ad2: A recorded `scenescout check` (`--record` or `--video`) no longer overwrites a `replay.html` in the output folder that it did not write: it stops before it starts, with exit code 2 and a message naming the file. With `--video`, a video that fails to start no longer stops every later flow from being filmed.
|
|
8
|
+
|
|
3
9
|
## 3.20.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/check-run.js
CHANGED
|
@@ -512,6 +512,27 @@ export function clearReplayOutput(outDir) {
|
|
|
512
512
|
if (fs.readdirSync(root).length === 0)
|
|
513
513
|
fs.rmdirSync(root);
|
|
514
514
|
}
|
|
515
|
+
/**
|
|
516
|
+
* Why a recorded check (--record or --video) must not start, or null: a
|
|
517
|
+
* replay.html in the output folder that a check did not write is the
|
|
518
|
+
* project's own, and is neither removed nor overwritten. Asked before the
|
|
519
|
+
* browser starts, so the run is not spent first.
|
|
520
|
+
*/
|
|
521
|
+
export function replayPageConflict(outDir) {
|
|
522
|
+
const page = path.join(outDir, REPLAY_FILE);
|
|
523
|
+
let text;
|
|
524
|
+
try {
|
|
525
|
+
text = fs.readFileSync(page, "utf8");
|
|
526
|
+
}
|
|
527
|
+
catch (err) {
|
|
528
|
+
if (err.code === "ENOENT")
|
|
529
|
+
return null;
|
|
530
|
+
throw err;
|
|
531
|
+
}
|
|
532
|
+
if (isGeneratedReplay(text))
|
|
533
|
+
return null;
|
|
534
|
+
return `${page} is not a page SceneScout wrote, so a recorded check will not overwrite it. Move or rename it, or pass --out to write the check somewhere else`;
|
|
535
|
+
}
|
|
515
536
|
/**
|
|
516
537
|
* The replay page's model for a recorded check, with its frames copied from
|
|
517
538
|
* the browsers' scratch folders to replay-frames/ beside the report: routes
|
package/dist/cli.js
CHANGED
|
@@ -21,7 +21,7 @@ import { APPROX_DISK_MB, defaultAttachNote, defaultEngine, launchTarget, parseBr
|
|
|
21
21
|
import { CLIENT_LABELS, firstMessageHint, manualFor, parseClients, registerWithClient, vscodeBinary } from "./clients.js";
|
|
22
22
|
import { CLAUDE_CODE_NOT_NEEDED, CLI_NAME, desktopExtensionRoots, diagnose, doctorAllGood, findDesktopExtension, installClosing, ensureCommand, findOnUserPath, installSkill, isEphemeralRoot, launchCommand, manualRegisterCommand, planCommand, registerMcp, resolveClaudeDir, spawnRunner, } from "./installer.js";
|
|
23
23
|
import { downloadBrowsers, presentBrowsers } from "./installer.js";
|
|
24
|
-
import { baselinesDirOf, clearReplayOutput, defaultCheckDir, readCheckInputs, runCheck } from "./check-run.js";
|
|
24
|
+
import { baselinesDirOf, clearReplayOutput, replayPageConflict, defaultCheckDir, readCheckInputs, runCheck } from "./check-run.js";
|
|
25
25
|
import { buildCheckReplayHtml, commitOf, REPLAY_FILE, replayFrames, replayVideos } from "./engine/check-replay.js";
|
|
26
26
|
import { recordChoice } from "./engine/capture.js";
|
|
27
27
|
import { httpClient, httpJudgeAsk, runCi } from "./ci-run.js";
|
|
@@ -583,6 +583,10 @@ async function check(args) {
|
|
|
583
583
|
try {
|
|
584
584
|
// --record, else SCENESCOUT_RECORD, else off.
|
|
585
585
|
options = { ...options, record: recordChoice(options.record, process.env) };
|
|
586
|
+
// A replay.html the project keeps there is its own: a recorded check refuses to start rather than overwrite it.
|
|
587
|
+
const conflict = options.record || options.video ? replayPageConflict(outDir) : null;
|
|
588
|
+
if (conflict)
|
|
589
|
+
throw new Error(conflict);
|
|
586
590
|
// A replay page an earlier run left must never be read, or uploaded, as this run's.
|
|
587
591
|
clearReplayOutput(outDir);
|
|
588
592
|
inputs = readCheckInputs(options);
|
package/dist/engine/browser.js
CHANGED
|
@@ -16,6 +16,7 @@ import { extractCreatedIds, isOwnedResource, normalizeId } from "./ownership.js"
|
|
|
16
16
|
import { formatJourney, journeyTime, measureJourney } from "./journey.js";
|
|
17
17
|
import { describeStep, elementStateMatches, FLOW_AFTER_LAST_STEP_MS, isAction, matchRequest, notActionable, parseTarget, repeatFailure, splitRefusals, TARGET_HELP, urlMatches, } from "./flow.js";
|
|
18
18
|
import { framePath, RECORD_MAX_FRAMES } from "./replay.js";
|
|
19
|
+
import { film } from "./film.js";
|
|
19
20
|
import { InFlightRequests, keepWatchingUrl, normalizePace, SETTLE_TICK_MS, shouldKeepWaiting } from "./settle.js";
|
|
20
21
|
import { crawledRoute, crawlLine, isFileMediaType, isNonPageResource, mainStateFlag, mediaTypeOf } from "./crawl.js";
|
|
21
22
|
import { authToRemember, BODY_FETCH_MAX, buildRequestScript, formatPageRequests, formatReplay, PageRequests, replaySignature, requestHeaders, resolveMethod, resolveRequestUrl, resolveTarget, staleCredentialNote, toReplayResult, wantsView, } from "./request.js";
|
|
@@ -1689,30 +1690,11 @@ export class BrowserEngine {
|
|
|
1689
1690
|
*/
|
|
1690
1691
|
async filming(videoTo, work) {
|
|
1691
1692
|
const page = this.requirePage();
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
await page.screencast.start({ path: videoTo, ...(page.viewportSize() ? { size: page.viewportSize() } : {}) });
|
|
1698
|
-
filming = true;
|
|
1699
|
-
}
|
|
1700
|
-
catch (err) {
|
|
1701
|
-
videoError = `the video could not be started: ${why(err)}`;
|
|
1702
|
-
}
|
|
1703
|
-
let value;
|
|
1704
|
-
try {
|
|
1705
|
-
value = await work();
|
|
1706
|
-
}
|
|
1707
|
-
finally {
|
|
1708
|
-
// The page the filming began on, even if the work moved the session to a popup.
|
|
1709
|
-
if (filming)
|
|
1710
|
-
await page.screencast.stop().catch((err) => (videoError = `the video could not be saved: ${why(err)}`));
|
|
1711
|
-
}
|
|
1712
|
-
if (videoError)
|
|
1713
|
-
return { value, video: null, videoError };
|
|
1714
|
-
const saved = await fs.promises.stat(videoTo).then((st) => st.size > 0, () => false);
|
|
1715
|
-
return saved ? { value, video: videoTo } : { value, video: null, videoError: "no video was written" };
|
|
1693
|
+
// The page the filming began on is the one stopped, even if the work moved the session to a popup.
|
|
1694
|
+
return film(page.screencast, videoTo, page.viewportSize(), work, {
|
|
1695
|
+
prepare: (file) => fs.promises.mkdir(path.dirname(file), { recursive: true }).then(() => undefined),
|
|
1696
|
+
written: (file) => fs.promises.stat(file).then((st) => st.size > 0, () => false),
|
|
1697
|
+
});
|
|
1716
1698
|
}
|
|
1717
1699
|
requirePage() {
|
|
1718
1700
|
if (!this.page || !this.memory) {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filming one piece of work into a video file (`scenescout check --video`):
|
|
3
|
+
* start, run the work, stop, and say whether a video was saved. The browser
|
|
4
|
+
* hands in its page's screencast; nothing here needs Playwright, so the
|
|
5
|
+
* failure paths are table-tested.
|
|
6
|
+
*/
|
|
7
|
+
const why = (err) => (err instanceof Error ? err.message.split("\n")[0] : String(err));
|
|
8
|
+
/**
|
|
9
|
+
* Run `work` while `screencast` films it into `videoTo`. The work's own result
|
|
10
|
+
* and errors are never touched by the filming: a video that cannot be started
|
|
11
|
+
* or saved is reported in `videoError`, and the work runs regardless.
|
|
12
|
+
*
|
|
13
|
+
* A start that fails is followed by a stop, its error ignored: a screencast
|
|
14
|
+
* left half-started would refuse every later start ("already started"), so
|
|
15
|
+
* one failure would cost the video of every flow after it.
|
|
16
|
+
*/
|
|
17
|
+
export async function film(screencast, videoTo, size, work, io) {
|
|
18
|
+
let videoError;
|
|
19
|
+
let filming = false;
|
|
20
|
+
try {
|
|
21
|
+
await io.prepare(videoTo);
|
|
22
|
+
await screencast.start({ path: videoTo, ...(size ? { size } : {}) });
|
|
23
|
+
filming = true;
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
videoError = `the video could not be started: ${why(err)}`;
|
|
27
|
+
// Whatever the failed start left running is stopped, so the next flow can start afresh; there is nothing to save.
|
|
28
|
+
await screencast.stop().catch(() => undefined);
|
|
29
|
+
}
|
|
30
|
+
let value;
|
|
31
|
+
try {
|
|
32
|
+
value = await work();
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
if (filming)
|
|
36
|
+
await screencast.stop().catch((err) => (videoError = `the video could not be saved: ${why(err)}`));
|
|
37
|
+
}
|
|
38
|
+
if (videoError)
|
|
39
|
+
return { value, video: null, videoError };
|
|
40
|
+
return (await io.written(videoTo)) ? { value, video: videoTo } : { value, video: null, videoError: "no video was written" };
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "scenescout",
|
|
3
|
-
"version": "3.20.
|
|
3
|
+
"version": "3.20.2",
|
|
4
4
|
"description": "SceneScout — exploratory UI testing for AI coding agents. An MCP server that gives any agent (Claude Code, Cursor, VS Code Copilot, Codex, Gemini CLI and others) a structured view of a running web app, always-on oracles, a network-level write policy, memory across runs and a gap-checked report.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "brunoboto96",
|