@midscene/core 1.17.2 → 1.17.3
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/dist/es/agent/utils.mjs +1 -1
- package/dist/es/ai-model/model-adapter/resolve.mjs +2 -2
- package/dist/es/ai-model/model-adapter/resolve.mjs.map +1 -1
- package/dist/es/ai-model/models/ant-ling/adapter.mjs +51 -0
- package/dist/es/ai-model/models/ant-ling/adapter.mjs.map +1 -0
- package/dist/es/ai-model/models/jev/adapter.mjs +1 -1
- package/dist/es/ai-model/models/jev/adapter.mjs.map +1 -1
- package/dist/es/ai-model/models/model-fallback.mjs +2 -2
- package/dist/es/ai-model/models/model-fallback.mjs.map +1 -1
- package/dist/es/ai-model/models/registry.mjs +2 -0
- package/dist/es/ai-model/models/registry.mjs.map +1 -1
- package/dist/es/dump/execution-recorder.mjs +70 -0
- package/dist/es/dump/execution-recorder.mjs.map +1 -0
- package/dist/es/dump/html-utils.mjs +51 -1
- package/dist/es/dump/html-utils.mjs.map +1 -1
- package/dist/es/dump/report-artifact-reader.mjs +85 -0
- package/dist/es/dump/report-artifact-reader.mjs.map +1 -0
- package/dist/es/dump/report-artifact-store.mjs +164 -0
- package/dist/es/dump/report-artifact-store.mjs.map +1 -0
- package/dist/es/dump/screenshot-store.mjs +40 -15
- package/dist/es/dump/screenshot-store.mjs.map +1 -1
- package/dist/es/dump/test-report-images.mjs +120 -0
- package/dist/es/dump/test-report-images.mjs.map +1 -0
- package/dist/es/report-cli.mjs +8 -7
- package/dist/es/report-cli.mjs.map +1 -1
- package/dist/es/report-generator.mjs +56 -47
- package/dist/es/report-generator.mjs.map +1 -1
- package/dist/es/report-html-template.mjs +1 -1
- package/dist/es/report.mjs +29 -55
- package/dist/es/report.mjs.map +1 -1
- package/dist/es/test-run-report.mjs.map +1 -1
- package/dist/es/utils.mjs +1 -1
- package/dist/lib/agent/utils.js +1 -1
- package/dist/lib/ai-model/model-adapter/resolve.js +2 -2
- package/dist/lib/ai-model/model-adapter/resolve.js.map +1 -1
- package/dist/lib/ai-model/models/ant-ling/adapter.js +85 -0
- package/dist/lib/ai-model/models/ant-ling/adapter.js.map +1 -0
- package/dist/lib/ai-model/models/jev/adapter.js +1 -1
- package/dist/lib/ai-model/models/jev/adapter.js.map +1 -1
- package/dist/lib/ai-model/models/model-fallback.js +2 -2
- package/dist/lib/ai-model/models/model-fallback.js.map +1 -1
- package/dist/lib/ai-model/models/registry.js +4 -2
- package/dist/lib/ai-model/models/registry.js.map +1 -1
- package/dist/lib/dump/execution-recorder.js +104 -0
- package/dist/lib/dump/execution-recorder.js.map +1 -0
- package/dist/lib/dump/html-utils.js +67 -5
- package/dist/lib/dump/html-utils.js.map +1 -1
- package/dist/lib/dump/report-artifact-reader.js +134 -0
- package/dist/lib/dump/report-artifact-reader.js.map +1 -0
- package/dist/lib/dump/report-artifact-store.js +204 -0
- package/dist/lib/dump/report-artifact-store.js.map +1 -0
- package/dist/lib/dump/screenshot-store.js +42 -14
- package/dist/lib/dump/screenshot-store.js.map +1 -1
- package/dist/lib/dump/test-report-images.js +154 -0
- package/dist/lib/dump/test-report-images.js.map +1 -0
- package/dist/lib/report-cli.js +8 -7
- package/dist/lib/report-cli.js.map +1 -1
- package/dist/lib/report-generator.js +54 -45
- package/dist/lib/report-generator.js.map +1 -1
- package/dist/lib/report-html-template.js +1 -1
- package/dist/lib/report.js +54 -56
- package/dist/lib/report.js.map +1 -1
- package/dist/lib/test-run-report.js.map +1 -1
- package/dist/lib/utils.js +1 -1
- package/dist/types/ai-model/model-adapter/resolve.d.ts +1 -1
- package/dist/types/ai-model/model-adapter/resolve.d.ts.map +1 -1
- package/dist/types/ai-model/model-adapter/types.d.ts +4 -2
- package/dist/types/ai-model/model-adapter/types.d.ts.map +1 -1
- package/dist/types/ai-model/models/ant-ling/adapter.d.ts +23 -0
- package/dist/types/ai-model/models/ant-ling/adapter.d.ts.map +1 -0
- package/dist/types/ai-model/models/jev/adapter.d.ts +1 -1
- package/dist/types/ai-model/models/registry.d.ts +20 -1
- package/dist/types/ai-model/models/registry.d.ts.map +1 -1
- package/dist/types/dump/execution-recorder.d.ts +23 -0
- package/dist/types/dump/execution-recorder.d.ts.map +1 -0
- package/dist/types/dump/html-utils.d.ts +9 -0
- package/dist/types/dump/html-utils.d.ts.map +1 -1
- package/dist/types/dump/report-artifact-reader.d.ts +19 -0
- package/dist/types/dump/report-artifact-reader.d.ts.map +1 -0
- package/dist/types/dump/report-artifact-store.d.ts +22 -0
- package/dist/types/dump/report-artifact-store.d.ts.map +1 -0
- package/dist/types/dump/screenshot-store.d.ts +6 -0
- package/dist/types/dump/screenshot-store.d.ts.map +1 -1
- package/dist/types/dump/test-report-images.d.ts +14 -0
- package/dist/types/dump/test-report-images.d.ts.map +1 -0
- package/dist/types/report-cli.d.ts.map +1 -1
- package/dist/types/report-generator.d.ts +4 -3
- package/dist/types/report-generator.d.ts.map +1 -1
- package/dist/types/report.d.ts +3 -0
- package/dist/types/report.d.ts.map +1 -1
- package/dist/types/test-run-report.d.ts +2 -0
- package/dist/types/test-run-report.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { parseReportArtifactManifest } from "@midscene/shared/report-artifact";
|
|
5
|
+
import { antiEscapeScriptTag } from "@midscene/shared/utils";
|
|
6
|
+
import { extractReportArtifactPathAsync, extractReportArtifactPathSync, extractTestRunReportDumpSync, generateDumpScriptTag, streamDumpScriptsAsync, streamDumpScriptsSync } from "./html-utils.mjs";
|
|
7
|
+
async function readReportArtifactManifestAsync(reportPath) {
|
|
8
|
+
const manifestPath = join(dirname(reportPath), 'report.json');
|
|
9
|
+
const exists = await stat(manifestPath).then(()=>true).catch((error)=>{
|
|
10
|
+
if ('ENOENT' === error.code) return false;
|
|
11
|
+
throw error;
|
|
12
|
+
});
|
|
13
|
+
if (!exists) return;
|
|
14
|
+
const entry = await extractReportArtifactPathAsync(reportPath);
|
|
15
|
+
if (!entry) return;
|
|
16
|
+
const manifest = parseReportArtifactManifest(JSON.parse(await readFile(join(dirname(reportPath), entry), 'utf8')));
|
|
17
|
+
if (manifest.entry !== basename(reportPath)) throw new Error('Report entry does not match its artifact manifest');
|
|
18
|
+
return manifest;
|
|
19
|
+
}
|
|
20
|
+
async function readReportActionDump(reportPath) {
|
|
21
|
+
let base;
|
|
22
|
+
const executions = new Map();
|
|
23
|
+
let anonymous = 0;
|
|
24
|
+
const collect = (content)=>{
|
|
25
|
+
const parsed = JSON.parse(content);
|
|
26
|
+
if (!parsed || !Array.isArray(parsed.executions)) throw new Error(`Invalid report execution data: ${reportPath}`);
|
|
27
|
+
base = parsed;
|
|
28
|
+
for (const execution of parsed.executions)executions.set(execution.id ? `id:${execution.id}` : `anonymous:${anonymous++}`, execution);
|
|
29
|
+
};
|
|
30
|
+
const manifest = await readReportArtifactManifestAsync(reportPath);
|
|
31
|
+
if (manifest) for (const group of manifest.groups)for (const path of group.dumps)collect(await readFile(join(dirname(reportPath), path), 'utf8'));
|
|
32
|
+
else await streamDumpScriptsAsync(reportPath, ({ content })=>{
|
|
33
|
+
collect(antiEscapeScriptTag(content));
|
|
34
|
+
return false;
|
|
35
|
+
});
|
|
36
|
+
if (!base) throw new Error(`No replay dump found in report: ${reportPath}`);
|
|
37
|
+
return {
|
|
38
|
+
...base,
|
|
39
|
+
executions: [
|
|
40
|
+
...executions.values()
|
|
41
|
+
]
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function readReportArtifactManifest(reportPath) {
|
|
45
|
+
const manifestPath = join(dirname(reportPath), 'report.json');
|
|
46
|
+
if (!existsSync(manifestPath) || !existsSync(reportPath)) return;
|
|
47
|
+
const entry = extractReportArtifactPathSync(reportPath);
|
|
48
|
+
if (!entry) return;
|
|
49
|
+
const manifest = parseReportArtifactManifest(JSON.parse(readFileSync(join(dirname(reportPath), entry), 'utf8')));
|
|
50
|
+
if (manifest.entry !== basename(reportPath)) throw new Error('Report entry does not match its artifact manifest');
|
|
51
|
+
return manifest;
|
|
52
|
+
}
|
|
53
|
+
function readReportDumpScripts(reportPath) {
|
|
54
|
+
const dumps = [];
|
|
55
|
+
streamReportDumpScripts(reportPath, (dump)=>{
|
|
56
|
+
dumps.push(dump);
|
|
57
|
+
return false;
|
|
58
|
+
});
|
|
59
|
+
return dumps;
|
|
60
|
+
}
|
|
61
|
+
function streamReportDumpScripts(reportPath, visitor) {
|
|
62
|
+
const manifest = readReportArtifactManifest(reportPath);
|
|
63
|
+
if (!manifest) return void streamDumpScriptsSync(reportPath, (dump)=>dump.openTag.includes('data-group-id') ? visitor(dump) : false);
|
|
64
|
+
for (const group of manifest.groups)for (const path of group.dumps){
|
|
65
|
+
const content = readFileSync(join(dirname(reportPath), path), 'utf8');
|
|
66
|
+
JSON.parse(content);
|
|
67
|
+
const tag = generateDumpScriptTag(content, {
|
|
68
|
+
'data-group-id': group.id,
|
|
69
|
+
...group.attributes
|
|
70
|
+
});
|
|
71
|
+
if (visitor({
|
|
72
|
+
openTag: tag.slice(0, tag.indexOf('>') + 1),
|
|
73
|
+
content
|
|
74
|
+
})) return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function readReportRunnerDump(reportPath) {
|
|
78
|
+
const manifest = readReportArtifactManifest(reportPath);
|
|
79
|
+
if (!manifest) return extractTestRunReportDumpSync(reportPath) ?? void 0;
|
|
80
|
+
if (!manifest.runner) return;
|
|
81
|
+
return JSON.parse(readFileSync(join(dirname(reportPath), manifest.runner), 'utf8'));
|
|
82
|
+
}
|
|
83
|
+
export { readReportActionDump, readReportArtifactManifest, readReportArtifactManifestAsync, readReportDumpScripts, readReportRunnerDump, streamReportDumpScripts };
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=report-artifact-reader.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dump/report-artifact-reader.mjs","sources":["../../../src/dump/report-artifact-reader.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'node:fs';\nimport { readFile, stat } from 'node:fs/promises';\nimport { basename, dirname, join } from 'node:path';\nimport { parseReportArtifactManifest } from '@midscene/shared/report-artifact';\nimport { antiEscapeScriptTag } from '@midscene/shared/utils';\nimport type { TestRunReportDump } from '../test-run-report';\nimport type { IExecutionDump, IReportActionDump } from '../types';\nimport {\n extractReportArtifactPathAsync,\n extractReportArtifactPathSync,\n extractTestRunReportDumpSync,\n generateDumpScriptTag,\n streamDumpScriptsAsync,\n streamDumpScriptsSync,\n} from './html-utils';\n\n/** Async counterpart for servers; missing manifests identify ordinary inline HTML. */\nexport async function readReportArtifactManifestAsync(reportPath: string) {\n const manifestPath = join(dirname(reportPath), 'report.json');\n const exists = await stat(manifestPath)\n .then(() => true)\n .catch((error) => {\n if (error.code === 'ENOENT') return false;\n throw error;\n });\n if (!exists) return undefined;\n const entry = await extractReportArtifactPathAsync(reportPath);\n if (!entry) return undefined;\n const manifest = parseReportArtifactManifest(\n JSON.parse(await readFile(join(dirname(reportPath), entry), 'utf8')),\n );\n if (manifest.entry !== basename(reportPath))\n throw new Error('Report entry does not match its artifact manifest');\n return manifest;\n}\n\n/** Read the complete replay, retaining the latest progress for every execution ID. */\nexport async function readReportActionDump(\n reportPath: string,\n): Promise<IReportActionDump> {\n let base: IReportActionDump | undefined;\n const executions = new Map<string, IExecutionDump>();\n let anonymous = 0;\n const collect = (content: string) => {\n const parsed = JSON.parse(content) as IReportActionDump;\n if (!parsed || !Array.isArray(parsed.executions))\n throw new Error(`Invalid report execution data: ${reportPath}`);\n base = parsed;\n for (const execution of parsed.executions)\n executions.set(\n execution.id ? `id:${execution.id}` : `anonymous:${anonymous++}`,\n execution,\n );\n };\n const manifest = await readReportArtifactManifestAsync(reportPath);\n if (manifest) {\n for (const group of manifest.groups) {\n for (const path of group.dumps)\n collect(await readFile(join(dirname(reportPath), path), 'utf8'));\n }\n } else {\n await streamDumpScriptsAsync(reportPath, ({ content }) => {\n collect(antiEscapeScriptTag(content));\n return false;\n });\n }\n if (!base) throw new Error(`No replay dump found in report: ${reportPath}`);\n return { ...base, executions: [...executions.values()] };\n}\n\n/** Read the versioned directory format; ordinary inline HTML stays self-contained. */\nexport function readReportArtifactManifest(reportPath: string) {\n const manifestPath = join(dirname(reportPath), 'report.json');\n if (!existsSync(manifestPath) || !existsSync(reportPath)) return undefined;\n const entry = extractReportArtifactPathSync(reportPath);\n if (!entry) return undefined;\n const manifest = parseReportArtifactManifest(\n JSON.parse(readFileSync(join(dirname(reportPath), entry), 'utf8')),\n );\n if (manifest.entry !== basename(reportPath))\n throw new Error('Report entry does not match its artifact manifest');\n return manifest;\n}\n\nexport function readReportDumpScripts(\n reportPath: string,\n): { openTag: string; content: string }[] {\n const dumps: { openTag: string; content: string }[] = [];\n streamReportDumpScripts(reportPath, (dump) => {\n dumps.push(dump);\n return false;\n });\n return dumps;\n}\n\n/** Stream inline HTML or external data one execution at a time. */\nexport function streamReportDumpScripts(\n reportPath: string,\n visitor: (dump: { openTag: string; content: string }) => boolean,\n): void {\n const manifest = readReportArtifactManifest(reportPath);\n if (!manifest) {\n // The bundled Viewer can contain literal dump-tag strings. Generated\n // execution tags carry a group ID; template strings must not be parsed.\n streamDumpScriptsSync(reportPath, (dump) =>\n dump.openTag.includes('data-group-id') ? visitor(dump) : false,\n );\n return;\n }\n for (const group of manifest.groups) {\n for (const path of group.dumps) {\n const content = readFileSync(join(dirname(reportPath), path), 'utf8');\n // Validate before passing the data to synchronous report consumers.\n JSON.parse(content);\n const tag = generateDumpScriptTag(content, {\n 'data-group-id': group.id,\n ...group.attributes,\n });\n if (visitor({ openTag: tag.slice(0, tag.indexOf('>') + 1), content }))\n return;\n }\n }\n}\n\nexport function readReportRunnerDump(\n reportPath: string,\n): TestRunReportDump | undefined {\n const manifest = readReportArtifactManifest(reportPath);\n if (!manifest) return extractTestRunReportDumpSync(reportPath) ?? undefined;\n if (!manifest.runner) return undefined;\n return JSON.parse(\n readFileSync(join(dirname(reportPath), manifest.runner), 'utf8'),\n ) as TestRunReportDump;\n}\n"],"names":["readReportArtifactManifestAsync","reportPath","manifestPath","join","dirname","exists","stat","error","entry","extractReportArtifactPathAsync","manifest","parseReportArtifactManifest","JSON","readFile","basename","Error","readReportActionDump","base","executions","Map","anonymous","collect","content","parsed","Array","execution","group","path","streamDumpScriptsAsync","antiEscapeScriptTag","readReportArtifactManifest","existsSync","extractReportArtifactPathSync","readFileSync","readReportDumpScripts","dumps","streamReportDumpScripts","dump","visitor","streamDumpScriptsSync","tag","generateDumpScriptTag","readReportRunnerDump","extractTestRunReportDumpSync","undefined"],"mappings":"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"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { mkdir, open as promises_open, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join, resolve as external_node_path_resolve } from "node:path";
|
|
3
|
+
import { parseReportArtifactManifest } from "@midscene/shared/report-artifact";
|
|
4
|
+
import { uuid } from "@midscene/shared/utils";
|
|
5
|
+
function _define_property(obj, key, value) {
|
|
6
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
7
|
+
value: value,
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
writable: true
|
|
11
|
+
});
|
|
12
|
+
else obj[key] = value;
|
|
13
|
+
return obj;
|
|
14
|
+
}
|
|
15
|
+
async function writeReportArtifactFile(path, content) {
|
|
16
|
+
await mkdir(dirname(path), {
|
|
17
|
+
recursive: true
|
|
18
|
+
});
|
|
19
|
+
const temporary = `${path}.${uuid()}.tmp`;
|
|
20
|
+
try {
|
|
21
|
+
await writeFile(temporary, content, 'utf8');
|
|
22
|
+
await rename(temporary, path);
|
|
23
|
+
} finally{
|
|
24
|
+
await unlink(temporary).catch((error)=>{
|
|
25
|
+
if ('ENOENT' !== error.code) throw error;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function withReportArtifactLock(root, publish, timeoutMs = 10000) {
|
|
30
|
+
const lockPath = external_node_path_resolve(root, '.report.json.lock');
|
|
31
|
+
await mkdir(dirname(lockPath), {
|
|
32
|
+
recursive: true
|
|
33
|
+
});
|
|
34
|
+
const startedAt = Date.now();
|
|
35
|
+
const acquire = async ()=>{
|
|
36
|
+
while(true)try {
|
|
37
|
+
return await promises_open(lockPath, 'wx');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if ('EEXIST' !== error.code) throw error;
|
|
40
|
+
if (Date.now() - startedAt >= timeoutMs) throw new Error(`Timed out waiting for report artifact lock ${lockPath}. Another publisher may still be running. If its owner crashed, remove the lock only after confirming that process has exited.`, {
|
|
41
|
+
cause: error
|
|
42
|
+
});
|
|
43
|
+
await new Promise((resolve)=>setTimeout(resolve, 20 + 10 * Math.random()));
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const lock = await acquire();
|
|
47
|
+
try {
|
|
48
|
+
await lock.writeFile(JSON.stringify({
|
|
49
|
+
pid: process.pid
|
|
50
|
+
}));
|
|
51
|
+
return await publish();
|
|
52
|
+
} finally{
|
|
53
|
+
await lock.close().finally(()=>unlink(lockPath));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
class ReportArtifactStore {
|
|
57
|
+
async writeExecution(key, serialized, attributes) {
|
|
58
|
+
this.assertOpen();
|
|
59
|
+
this.queue = this.queue.then(async ()=>{
|
|
60
|
+
let path = this.executionPaths.get(key);
|
|
61
|
+
if (!path) {
|
|
62
|
+
path = `data/${this.manifest.reportId}/${++this.nextExecution}.json`;
|
|
63
|
+
this.executionPaths.set(key, path);
|
|
64
|
+
}
|
|
65
|
+
await writeReportArtifactFile(join(this.root, path), serialized);
|
|
66
|
+
const group = this.manifest.groups[0];
|
|
67
|
+
group.attributes = attributes;
|
|
68
|
+
if (!group.dumps.includes(path)) group.dumps.push(path);
|
|
69
|
+
await this.publish();
|
|
70
|
+
});
|
|
71
|
+
await this.queue;
|
|
72
|
+
}
|
|
73
|
+
async writeRunner(serialized) {
|
|
74
|
+
this.assertOpen();
|
|
75
|
+
this.queue = this.queue.then(async ()=>{
|
|
76
|
+
const path = `data/${this.manifest.reportId}/runner.json`;
|
|
77
|
+
await writeReportArtifactFile(join(this.root, path), serialized);
|
|
78
|
+
this.manifest.runner = path;
|
|
79
|
+
await this.publish();
|
|
80
|
+
});
|
|
81
|
+
await this.queue;
|
|
82
|
+
}
|
|
83
|
+
async finalize() {
|
|
84
|
+
if (!this.finalization) this.finalization = this.queue = this.queue.then(async ()=>{
|
|
85
|
+
this.manifest.groups[0].status = 'complete';
|
|
86
|
+
await this.publish();
|
|
87
|
+
});
|
|
88
|
+
await this.finalization;
|
|
89
|
+
}
|
|
90
|
+
assertOpen() {
|
|
91
|
+
if (this.finalization) throw new Error('The report artifact stream has been finalized.');
|
|
92
|
+
}
|
|
93
|
+
async publish() {
|
|
94
|
+
const path = external_node_path_resolve(this.root, 'report.json');
|
|
95
|
+
await withReportArtifactLock(this.root, async ()=>{
|
|
96
|
+
let manifest = this.manifest;
|
|
97
|
+
if (this.reuseExistingReport || this.hasPublished) {
|
|
98
|
+
const existing = await readFile(path, 'utf8').catch((error)=>{
|
|
99
|
+
if ('ENOENT' === error.code) return;
|
|
100
|
+
throw error;
|
|
101
|
+
});
|
|
102
|
+
if (void 0 !== existing) {
|
|
103
|
+
const previous = parseReportArtifactManifest(JSON.parse(existing));
|
|
104
|
+
if (previous.kind !== manifest.kind || previous.entry !== manifest.entry) throw new Error('Cannot combine reports with different kinds or entries');
|
|
105
|
+
manifest = {
|
|
106
|
+
...previous,
|
|
107
|
+
groups: [
|
|
108
|
+
...new Map([
|
|
109
|
+
...previous.groups,
|
|
110
|
+
...manifest.groups
|
|
111
|
+
].map((group)=>[
|
|
112
|
+
group.id,
|
|
113
|
+
group
|
|
114
|
+
])).values()
|
|
115
|
+
],
|
|
116
|
+
runner: manifest.runner ?? previous.runner
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
manifest = {
|
|
121
|
+
...manifest,
|
|
122
|
+
status: manifest.groups.every((group)=>'complete' === group.status) ? 'complete' : 'running'
|
|
123
|
+
};
|
|
124
|
+
parseReportArtifactManifest(manifest);
|
|
125
|
+
await writeReportArtifactFile(path, JSON.stringify(manifest));
|
|
126
|
+
this.hasPublished = true;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
constructor(root, reportId, kind = 'agent', reuseExistingReport = false, entry = 'index.html'){
|
|
130
|
+
_define_property(this, "root", void 0);
|
|
131
|
+
_define_property(this, "reuseExistingReport", void 0);
|
|
132
|
+
_define_property(this, "executionPaths", void 0);
|
|
133
|
+
_define_property(this, "nextExecution", void 0);
|
|
134
|
+
_define_property(this, "manifest", void 0);
|
|
135
|
+
_define_property(this, "queue", void 0);
|
|
136
|
+
_define_property(this, "hasPublished", void 0);
|
|
137
|
+
_define_property(this, "finalization", void 0);
|
|
138
|
+
this.root = root;
|
|
139
|
+
this.reuseExistingReport = reuseExistingReport;
|
|
140
|
+
this.executionPaths = new Map();
|
|
141
|
+
this.nextExecution = 0;
|
|
142
|
+
this.queue = Promise.resolve();
|
|
143
|
+
this.hasPublished = false;
|
|
144
|
+
if (!/^[a-zA-Z0-9_-]{1,128}$/.test(reportId)) throw new Error('Invalid report artifact ID');
|
|
145
|
+
this.manifest = {
|
|
146
|
+
schemaVersion: 1,
|
|
147
|
+
kind,
|
|
148
|
+
status: 'running',
|
|
149
|
+
reportId,
|
|
150
|
+
entry,
|
|
151
|
+
groups: [
|
|
152
|
+
{
|
|
153
|
+
id: reportId,
|
|
154
|
+
status: 'running',
|
|
155
|
+
attributes: {},
|
|
156
|
+
dumps: []
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export { ReportArtifactStore, withReportArtifactLock, writeReportArtifactFile };
|
|
163
|
+
|
|
164
|
+
//# sourceMappingURL=report-artifact-store.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dump/report-artifact-store.mjs","sources":["../../../src/dump/report-artifact-store.ts"],"sourcesContent":["import {\n mkdir,\n open,\n readFile,\n rename,\n unlink,\n writeFile,\n} from 'node:fs/promises';\nimport { dirname, join, resolve } from 'node:path';\nimport {\n type ReportArtifactManifest,\n parseReportArtifactManifest,\n} from '@midscene/shared/report-artifact';\nimport { uuid } from '@midscene/shared/utils';\n\n/** Publish individual files atomically; readers never observe truncated JSON. */\nexport async function writeReportArtifactFile(\n path: string,\n content: string | Uint8Array,\n): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n const temporary = `${path}.${uuid()}.tmp`;\n try {\n await writeFile(temporary, content, 'utf8');\n await rename(temporary, path);\n } finally {\n await unlink(temporary).catch((error) => {\n if (error.code !== 'ENOENT') throw error;\n });\n }\n}\n\n/** Serialize the entire read/merge/write transaction across Node processes. */\nexport async function withReportArtifactLock<T>(\n root: string,\n publish: () => Promise<T>,\n timeoutMs = 10_000,\n): Promise<T> {\n const lockPath = resolve(root, '.report.json.lock');\n await mkdir(dirname(lockPath), { recursive: true });\n const startedAt = Date.now();\n const acquire = async () => {\n while (true) {\n try {\n return await open(lockPath, 'wx');\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n if (Date.now() - startedAt >= timeoutMs) {\n throw new Error(\n `Timed out waiting for report artifact lock ${lockPath}. Another publisher may still be running. If its owner crashed, remove the lock only after confirming that process has exited.`,\n { cause: error },\n );\n }\n await new Promise((resolve) =>\n setTimeout(resolve, 20 + Math.random() * 10),\n );\n }\n }\n };\n const lock = await acquire();\n try {\n await lock.writeFile(JSON.stringify({ pid: process.pid }));\n return await publish();\n } finally {\n // Never expire a live publisher's lock based on elapsed time: a slow\n // filesystem or paused process must not result in two simultaneous owners.\n await lock.close().finally(() => unlink(lockPath));\n }\n}\n\n/** Stores report data without rendering HTML or owning execution resources. */\nexport class ReportArtifactStore {\n private readonly executionPaths = new Map<string, string>();\n private nextExecution = 0;\n private readonly manifest: ReportArtifactManifest;\n private queue: Promise<void> = Promise.resolve();\n private hasPublished = false;\n private finalization?: Promise<void>;\n\n constructor(\n private readonly root: string,\n reportId: string,\n kind: 'agent' | 'test' = 'agent',\n private readonly reuseExistingReport = false,\n entry = 'index.html',\n ) {\n if (!/^[a-zA-Z0-9_-]{1,128}$/.test(reportId))\n throw new Error('Invalid report artifact ID');\n this.manifest = {\n schemaVersion: 1,\n kind,\n status: 'running',\n reportId,\n entry,\n groups: [{ id: reportId, status: 'running', attributes: {}, dumps: [] }],\n };\n }\n\n async writeExecution(\n key: string,\n serialized: string,\n attributes: Record<string, string>,\n ): Promise<void> {\n this.assertOpen();\n this.queue = this.queue.then(async () => {\n let path = this.executionPaths.get(key);\n if (!path) {\n path = `data/${this.manifest.reportId}/${++this.nextExecution}.json`;\n this.executionPaths.set(key, path);\n }\n await writeReportArtifactFile(join(this.root, path), serialized);\n const group = this.manifest.groups[0];\n group.attributes = attributes;\n if (!group.dumps.includes(path)) group.dumps.push(path);\n await this.publish();\n });\n await this.queue;\n }\n\n async writeRunner(serialized: string): Promise<void> {\n this.assertOpen();\n this.queue = this.queue.then(async () => {\n const path = `data/${this.manifest.reportId}/runner.json`;\n await writeReportArtifactFile(join(this.root, path), serialized);\n this.manifest.runner = path;\n await this.publish();\n });\n await this.queue;\n }\n\n async finalize(): Promise<void> {\n if (!this.finalization) {\n this.finalization = this.queue = this.queue.then(async () => {\n this.manifest.groups[0].status = 'complete';\n await this.publish();\n });\n }\n await this.finalization;\n }\n\n private assertOpen(): void {\n if (this.finalization)\n throw new Error('The report artifact stream has been finalized.');\n }\n\n private async publish(): Promise<void> {\n const path = resolve(this.root, 'report.json');\n await withReportArtifactLock(this.root, async () => {\n let manifest = this.manifest;\n if (this.reuseExistingReport || this.hasPublished) {\n const existing = await readFile(path, 'utf8').catch((error) => {\n if (error.code === 'ENOENT') return undefined;\n throw error;\n });\n if (existing !== undefined) {\n const previous = parseReportArtifactManifest(JSON.parse(existing));\n if (\n previous.kind !== manifest.kind ||\n previous.entry !== manifest.entry\n )\n throw new Error(\n 'Cannot combine reports with different kinds or entries',\n );\n manifest = {\n ...previous,\n groups: [\n ...new Map(\n [...previous.groups, ...manifest.groups].map((group) => [\n group.id,\n group,\n ]),\n ).values(),\n ],\n runner: manifest.runner ?? previous.runner,\n };\n }\n }\n manifest = {\n ...manifest,\n status: manifest.groups.every((group) => group.status === 'complete')\n ? 'complete'\n : 'running',\n };\n parseReportArtifactManifest(manifest);\n await writeReportArtifactFile(path, JSON.stringify(manifest));\n this.hasPublished = true;\n });\n }\n}\n"],"names":["writeReportArtifactFile","path","content","mkdir","dirname","temporary","uuid","writeFile","rename","unlink","error","withReportArtifactLock","root","publish","timeoutMs","lockPath","resolve","startedAt","Date","acquire","open","Error","Promise","setTimeout","Math","lock","JSON","process","ReportArtifactStore","key","serialized","attributes","join","group","manifest","existing","readFile","undefined","previous","parseReportArtifactManifest","Map","reportId","kind","reuseExistingReport","entry"],"mappings":"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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
-
import { writeFile } from "node:fs/promises";
|
|
4
3
|
import { dirname, isAbsolute, join } from "node:path";
|
|
5
4
|
import { collectImageScriptIds, extractImageByIdSync } from "./html-utils.mjs";
|
|
6
5
|
import { imageFileExtensionForMimeType, imageRefFileExtension, normalizeImageUrlRef, normalizeScreenshotRef, normalizeStoredImageRef } from "./image-reference.mjs";
|
|
6
|
+
import { writeReportArtifactFile } from "./report-artifact-store.mjs";
|
|
7
7
|
function _define_property(obj, key, value) {
|
|
8
8
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
9
9
|
value: value,
|
|
@@ -28,6 +28,10 @@ function parseBase64ImageDataUrl(imageUrl) {
|
|
|
28
28
|
function isBase64ImageDataUrl(value) {
|
|
29
29
|
return base64ImageDataUrlPattern.test(value);
|
|
30
30
|
}
|
|
31
|
+
function reportImageFileName(extension, bytes) {
|
|
32
|
+
const hash = createHash('sha256').update(extension).update('\0').update(bytes).digest('hex');
|
|
33
|
+
return `${hash}.${extension}`;
|
|
34
|
+
}
|
|
31
35
|
function resolveImageSource(refInput, options) {
|
|
32
36
|
const ref = normalizeStoredImageRef(refInput);
|
|
33
37
|
const id = ref?.id ?? options.fallbackId;
|
|
@@ -78,6 +82,11 @@ class ReportImageStore {
|
|
|
78
82
|
enableFileCopies() {
|
|
79
83
|
this.alsoWriteFileCopy = true;
|
|
80
84
|
}
|
|
85
|
+
getFilePath(id) {
|
|
86
|
+
const file = this.fileLocations.get(id);
|
|
87
|
+
if (!file) throw new Error(`ReportImageStore: image ${id} has no persisted file`);
|
|
88
|
+
return file.relativePath;
|
|
89
|
+
}
|
|
81
90
|
async persist(screenshot) {
|
|
82
91
|
await this.ensureExistingInlineImageIdsLoaded();
|
|
83
92
|
const shouldWriteFileCopy = 'directory' === this.mode || this.alsoWriteFileCopy;
|
|
@@ -146,11 +155,7 @@ class ReportImageStore {
|
|
|
146
155
|
await this.existingInlineImageIdsPromise;
|
|
147
156
|
}
|
|
148
157
|
async persistToSharedFileIfNeeded(screenshot, options) {
|
|
149
|
-
const
|
|
150
|
-
const { relativePath, absolutePath } = this.writtenFileIds.has(screenshot.id) ? {
|
|
151
|
-
relativePath: `./screenshots/${fileName}`,
|
|
152
|
-
absolutePath: join(this.screenshotsDir, fileName)
|
|
153
|
-
} : await this.writeImageFileIfNeeded({
|
|
158
|
+
const { relativePath, absolutePath } = this.fileLocations.get(screenshot.id) ?? await this.writeImageFileIfNeeded({
|
|
154
159
|
id: screenshot.id,
|
|
155
160
|
extension: screenshot.extension,
|
|
156
161
|
rawBase64: screenshot.rawBase64
|
|
@@ -164,17 +169,33 @@ class ReportImageStore {
|
|
|
164
169
|
if (!existsSync(screenshotsDir)) mkdirSync(screenshotsDir, {
|
|
165
170
|
recursive: true
|
|
166
171
|
});
|
|
167
|
-
const
|
|
172
|
+
const known = this.fileLocations.get(image.id);
|
|
173
|
+
if (known) return known;
|
|
174
|
+
const bytes = Buffer.from(image.rawBase64, 'base64');
|
|
175
|
+
const fileName = reportImageFileName(image.extension, bytes);
|
|
168
176
|
const relativePath = `./screenshots/${fileName}`;
|
|
169
177
|
const absolutePath = join(screenshotsDir, fileName);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
178
|
+
let publication = this.pendingFiles.get(fileName);
|
|
179
|
+
if (!publication) {
|
|
180
|
+
publication = (async ()=>{
|
|
181
|
+
if (!this.writtenFileIds.has(fileName)) {
|
|
182
|
+
await writeReportArtifactFile(absolutePath, bytes);
|
|
183
|
+
this.writtenFileIds.add(fileName);
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
relativePath,
|
|
187
|
+
absolutePath
|
|
188
|
+
};
|
|
189
|
+
})();
|
|
190
|
+
this.pendingFiles.set(fileName, publication);
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const location = await publication;
|
|
194
|
+
this.fileLocations.set(image.id, location);
|
|
195
|
+
return location;
|
|
196
|
+
} finally{
|
|
197
|
+
this.pendingFiles.delete(fileName);
|
|
173
198
|
}
|
|
174
|
-
return {
|
|
175
|
-
relativePath,
|
|
176
|
-
absolutePath
|
|
177
|
-
};
|
|
178
199
|
}
|
|
179
200
|
loadDataUri(refInput) {
|
|
180
201
|
const ref = normalizeStoredImageRef(refInput);
|
|
@@ -199,6 +220,8 @@ class ReportImageStore {
|
|
|
199
220
|
});
|
|
200
221
|
this.writtenInlineIds.clear();
|
|
201
222
|
this.writtenFileIds.clear();
|
|
223
|
+
this.fileLocations.clear();
|
|
224
|
+
this.pendingFiles.clear();
|
|
202
225
|
this.existingInlineImageIdsPromise = void 0;
|
|
203
226
|
}
|
|
204
227
|
constructor(options){
|
|
@@ -209,6 +232,8 @@ class ReportImageStore {
|
|
|
209
232
|
_define_property(this, "alsoWriteFileCopy", void 0);
|
|
210
233
|
_define_property(this, "writtenInlineIds", new Set());
|
|
211
234
|
_define_property(this, "writtenFileIds", new Set());
|
|
235
|
+
_define_property(this, "fileLocations", new Map());
|
|
236
|
+
_define_property(this, "pendingFiles", new Map());
|
|
212
237
|
_define_property(this, "shouldReuseExistingInlineReport", void 0);
|
|
213
238
|
_define_property(this, "existingInlineImageIdsPromise", void 0);
|
|
214
239
|
this.mode = options.mode;
|
|
@@ -219,6 +244,6 @@ class ReportImageStore {
|
|
|
219
244
|
this.shouldReuseExistingInlineReport = Boolean(options.reuseExistingReport && 'inline' === this.mode);
|
|
220
245
|
}
|
|
221
246
|
}
|
|
222
|
-
export { ReportImageStore, ReportImageStore as ScreenshotStore, imageRefFileExtension, isBase64ImageDataUrl, normalizeImageUrlRef, normalizeScreenshotRef, normalizeStoredImageRef, parseBase64ImageDataUrl, resolveImageSource, resolveScreenshotSource };
|
|
247
|
+
export { ReportImageStore, ReportImageStore as ScreenshotStore, imageRefFileExtension, isBase64ImageDataUrl, normalizeImageUrlRef, normalizeScreenshotRef, normalizeStoredImageRef, parseBase64ImageDataUrl, reportImageFileName, resolveImageSource, resolveScreenshotSource };
|
|
223
248
|
|
|
224
249
|
//# sourceMappingURL=screenshot-store.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dump/screenshot-store.mjs","sources":["../../../src/dump/screenshot-store.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';\nimport { writeFile as writeFileAsync } from 'node:fs/promises';\nimport { dirname, isAbsolute, join } from 'node:path';\nimport type { ScreenshotItem } from '../screenshot-item';\nimport { collectImageScriptIds, extractImageByIdSync } from './html-utils';\nimport {\n type ImageUrlRef,\n type ScreenshotRef,\n type StoredImageRef,\n imageFileExtensionForMimeType,\n imageRefFileExtension,\n normalizeImageUrlRef,\n normalizeScreenshotRef,\n normalizeStoredImageRef,\n} from './image-reference';\n\nexport type {\n ImageUrlRef,\n ScreenshotRef,\n StoredImageRef,\n} from './image-reference';\nexport {\n imageRefFileExtension,\n normalizeImageUrlRef,\n normalizeScreenshotRef,\n normalizeStoredImageRef,\n} from './image-reference';\n\nconst base64ImageDataUrlPattern =\n /^data:(image\\/[a-z0-9.+-]+)(?:;[^,]*)?;base64,([a-z0-9+/=\\s]+)$/i;\ntype ResolvedImageSource =\n | {\n type: 'data-uri';\n id: string;\n mimeType: StoredImageRef['mimeType'];\n dataUri: string;\n }\n | {\n type: 'file';\n id: string;\n mimeType: StoredImageRef['mimeType'];\n filePath: string;\n };\n\n/** Parse and normalize a base64 image data URL for report persistence. */\nexport function parseBase64ImageDataUrl(imageUrl: string): {\n mimeType: `image/${string}`;\n extension: string;\n rawBase64: string;\n} {\n const match = imageUrl.match(base64ImageDataUrlPattern);\n if (!match) {\n throw new Error(\n 'ReportImageStore: reference image must be a valid base64 image data URL',\n );\n }\n const mimeType = match[1].toLowerCase() as `image/${string}`;\n return {\n mimeType,\n extension: imageFileExtensionForMimeType(mimeType),\n rawBase64: match[2].replace(/\\s/g, ''),\n };\n}\n\nexport function isBase64ImageDataUrl(value: string): boolean {\n return base64ImageDataUrlPattern.test(value);\n}\n\nexport function resolveImageSource(\n refInput: unknown,\n options: {\n reportPath: string;\n imageDirectory?: string;\n fallbackId?: string;\n fallbackMimeType?: ScreenshotRef['mimeType'];\n },\n): ResolvedImageSource {\n const ref = normalizeStoredImageRef(refInput);\n const id = ref?.id ?? options.fallbackId;\n const mimeType = ref?.mimeType ?? options.fallbackMimeType;\n\n if (!id || !mimeType) {\n throw new Error(\n 'ReportImageStore: image id and mimeType are required to resolve an image',\n );\n }\n\n const resolveReportRelativePath = (filePath: string): string =>\n isAbsolute(filePath)\n ? filePath\n : join(dirname(options.reportPath), filePath);\n\n if (ref?.storage === 'file') {\n if (!ref.path) {\n throw new Error(\n `ReportImageStore: image ref \"${ref.id}\" missing file path`,\n );\n }\n\n const explicitFilePath = resolveReportRelativePath(ref.path);\n if (existsSync(explicitFilePath)) {\n return {\n type: 'file',\n id,\n mimeType,\n filePath: explicitFilePath,\n };\n }\n }\n\n const inlineDataUri = extractImageByIdSync(options.reportPath, id);\n if (inlineDataUri) {\n return {\n type: 'data-uri',\n id,\n mimeType,\n dataUri: inlineDataUri,\n };\n }\n\n const extension = ref\n ? imageRefFileExtension(ref)\n : imageFileExtensionForMimeType(options.fallbackMimeType!);\n if (options.imageDirectory) {\n const companionImagePath = join(\n options.imageDirectory,\n `${id}.${extension}`,\n );\n if (existsSync(companionImagePath)) {\n return {\n type: 'file',\n id,\n mimeType,\n filePath: companionImagePath,\n };\n }\n }\n const siblingImagePath = join(\n dirname(options.reportPath),\n 'screenshots',\n `${id}.${extension}`,\n );\n if (existsSync(siblingImagePath)) {\n return {\n type: 'file',\n id,\n mimeType,\n filePath: siblingImagePath,\n };\n }\n\n throw new Error(\n `ReportImageStore: cannot resolve image \"${id}\" from ${options.reportPath}`,\n );\n}\n\nexport function resolveScreenshotSource(\n refInput: unknown,\n options: {\n reportPath: string;\n imageDirectory?: string;\n fallbackId?: string;\n fallbackMimeType?: ScreenshotRef['mimeType'];\n },\n): ResolvedImageSource {\n const ref = normalizeScreenshotRef(refInput);\n return resolveImageSource(ref, options);\n}\n\n/** Persists and restores every image asset referenced by a report. */\nexport class ReportImageStore {\n private readonly mode: 'inline' | 'directory';\n private readonly reportPath: string;\n private readonly screenshotsDir?: string;\n private readonly writeInlineImage?: (\n id: string,\n base64: string,\n ) => void | Promise<void>;\n private alsoWriteFileCopy: boolean;\n private readonly writtenInlineIds = new Set<string>();\n private readonly writtenFileIds = new Set<string>();\n private readonly shouldReuseExistingInlineReport: boolean;\n private existingInlineImageIdsPromise?: Promise<void>;\n\n constructor(options: {\n mode: 'inline' | 'directory';\n reportPath: string;\n screenshotsDir?: string;\n writeInlineImage?: (id: string, base64: string) => void | Promise<void>;\n alsoWriteFileCopy?: boolean;\n /** @deprecated Use alsoWriteFileCopy instead. */\n ensureFileCopy?: boolean;\n /** Reuse inline image assets already present in reportPath. */\n reuseExistingReport?: boolean;\n }) {\n this.mode = options.mode;\n this.reportPath = options.reportPath;\n this.screenshotsDir = options.screenshotsDir;\n this.writeInlineImage = options.writeInlineImage;\n this.alsoWriteFileCopy =\n options.alsoWriteFileCopy ?? options.ensureFileCopy ?? false;\n this.shouldReuseExistingInlineReport = Boolean(\n options.reuseExistingReport && this.mode === 'inline',\n );\n }\n\n enableFileCopies(): void {\n this.alsoWriteFileCopy = true;\n }\n\n async persist(screenshot: ScreenshotItem): Promise<ScreenshotRef> {\n await this.ensureExistingInlineImageIdsLoaded();\n const shouldWriteFileCopy =\n this.mode === 'directory' || this.alsoWriteFileCopy;\n const fileRef = shouldWriteFileCopy\n ? await this.persistToSharedFileIfNeeded(screenshot, {\n markAsPersisted: this.mode === 'directory',\n })\n : null;\n\n if (this.mode === 'inline') {\n if (!this.writeInlineImage) {\n throw new Error(\n 'ReportImageStore: writeInlineImage is required in inline mode',\n );\n }\n if (!this.writtenInlineIds.has(screenshot.id)) {\n await this.writeInlineImage(screenshot.id, screenshot.base64);\n this.writtenInlineIds.add(screenshot.id);\n }\n return screenshot.markPersistedInline(this.reportPath);\n }\n\n if (!fileRef) {\n throw new Error(\n 'ReportImageStore: file persistence is required in directory mode',\n );\n }\n return fileRef;\n }\n\n /** Persist a base64 multimodal prompt image and return its content-addressed ref. */\n async persistReferenceImage(imageUrl: string): Promise<ImageUrlRef> {\n await this.ensureExistingInlineImageIdsLoaded();\n const { mimeType, extension, rawBase64 } =\n parseBase64ImageDataUrl(imageUrl);\n const id = `reference-${createHash('sha256')\n .update(mimeType)\n .update('\\0')\n .update(Buffer.from(rawBase64, 'base64'))\n .digest('hex')}`;\n const shouldWriteFileCopy =\n this.mode === 'directory' || this.alsoWriteFileCopy;\n const fileLocation = shouldWriteFileCopy\n ? await this.writeImageFileIfNeeded({ id, extension, rawBase64 })\n : null;\n\n let ref: ImageUrlRef;\n if (this.mode === 'inline') {\n if (!this.writeInlineImage) {\n throw new Error(\n 'ReportImageStore: writeInlineImage is required in inline mode',\n );\n }\n if (!this.writtenInlineIds.has(id)) {\n await this.writeInlineImage(id, imageUrl);\n this.writtenInlineIds.add(id);\n }\n ref = {\n type: 'midscene_image_url_ref',\n id,\n mimeType,\n storage: 'inline',\n };\n } else {\n if (!fileLocation) {\n throw new Error(\n 'ReportImageStore: file persistence is required in directory mode',\n );\n }\n ref = {\n type: 'midscene_image_url_ref',\n id,\n mimeType,\n storage: 'file',\n path: fileLocation.relativePath,\n };\n }\n return ref;\n }\n\n private async ensureExistingInlineImageIdsLoaded(): Promise<void> {\n if (!this.shouldReuseExistingInlineReport) return;\n if (!this.existingInlineImageIdsPromise) {\n this.existingInlineImageIdsPromise = (async () => {\n try {\n const imageIds = await collectImageScriptIds(this.reportPath);\n for (const imageId of imageIds) this.writtenInlineIds.add(imageId);\n } catch (error) {\n if (\n typeof error === 'object' &&\n error !== null &&\n 'code' in error &&\n error.code === 'ENOENT'\n ) {\n return;\n }\n throw new Error(\n `ReportImageStore: failed to index existing inline images from ${this.reportPath}`,\n { cause: error },\n );\n }\n })();\n }\n await this.existingInlineImageIdsPromise;\n }\n\n private async persistToSharedFileIfNeeded(\n screenshot: ScreenshotItem,\n options: {\n markAsPersisted: boolean;\n },\n ): Promise<ScreenshotRef> {\n const fileName = `${screenshot.id}.${screenshot.extension}`;\n const { relativePath, absolutePath } = this.writtenFileIds.has(\n screenshot.id,\n )\n ? {\n relativePath: `./screenshots/${fileName}`,\n absolutePath: join(this.screenshotsDir!, fileName),\n }\n : await this.writeImageFileIfNeeded({\n id: screenshot.id,\n extension: screenshot.extension,\n rawBase64: screenshot.rawBase64,\n });\n\n if (options.markAsPersisted) {\n return screenshot.markPersistedToPath(relativePath, absolutePath);\n }\n\n return screenshot.registerPersistedFileCopy(relativePath, absolutePath);\n }\n\n private async writeImageFileIfNeeded(image: {\n id: string;\n extension: string;\n rawBase64: string;\n }): Promise<{ relativePath: string; absolutePath: string }> {\n const screenshotsDir = this.screenshotsDir;\n if (!screenshotsDir) {\n throw new Error(\n 'ReportImageStore: screenshotsDir is required when file persistence is enabled',\n );\n }\n if (!existsSync(screenshotsDir)) {\n mkdirSync(screenshotsDir, { recursive: true });\n }\n\n const fileName = `${image.id}.${image.extension}`;\n const relativePath = `./screenshots/${fileName}`;\n const absolutePath = join(screenshotsDir, fileName);\n if (!this.writtenFileIds.has(image.id)) {\n await writeFileAsync(\n absolutePath,\n Buffer.from(image.rawBase64, 'base64'),\n );\n this.writtenFileIds.add(image.id);\n }\n return { relativePath, absolutePath };\n }\n\n /** Resolve any stored report image reference to a data URI. */\n loadDataUri(refInput: unknown): string {\n const ref = normalizeStoredImageRef(refInput);\n if (!ref) {\n throw new Error('ReportImageStore: invalid image reference');\n }\n\n const resolved = resolveImageSource(ref, {\n reportPath: this.reportPath,\n imageDirectory: this.screenshotsDir,\n });\n\n if (resolved.type === 'data-uri') {\n return resolved.dataUri;\n }\n\n const data = readFileSync(resolved.filePath);\n return `data:${resolved.mimeType};base64,${data.toString('base64')}`;\n }\n\n /** @deprecated Use loadDataUri. */\n loadBase64(refInput: unknown): string {\n const screenshotRef = normalizeScreenshotRef(refInput);\n if (!screenshotRef) {\n throw new Error('ReportImageStore: invalid screenshot reference');\n }\n return this.loadDataUri(screenshotRef);\n }\n\n cleanup(): void {\n if (\n this.mode === 'directory' &&\n this.screenshotsDir &&\n existsSync(this.screenshotsDir)\n ) {\n rmSync(this.screenshotsDir, { recursive: true, force: true });\n }\n this.writtenInlineIds.clear();\n this.writtenFileIds.clear();\n this.existingInlineImageIdsPromise = undefined;\n }\n}\n\n/** @deprecated Use ReportImageStore. */\nexport { ReportImageStore as ScreenshotStore };\n"],"names":["base64ImageDataUrlPattern","parseBase64ImageDataUrl","imageUrl","match","Error","mimeType","imageFileExtensionForMimeType","isBase64ImageDataUrl","value","resolveImageSource","refInput","options","ref","normalizeStoredImageRef","id","resolveReportRelativePath","filePath","isAbsolute","join","dirname","explicitFilePath","existsSync","inlineDataUri","extractImageByIdSync","extension","imageRefFileExtension","companionImagePath","siblingImagePath","resolveScreenshotSource","normalizeScreenshotRef","ReportImageStore","screenshot","shouldWriteFileCopy","fileRef","rawBase64","createHash","Buffer","fileLocation","imageIds","collectImageScriptIds","imageId","error","fileName","relativePath","absolutePath","image","screenshotsDir","mkdirSync","writeFileAsync","resolved","data","readFileSync","screenshotRef","rmSync","undefined","Set","Boolean"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dump/screenshot-store.mjs","sources":["../../../src/dump/screenshot-store.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';\nimport { dirname, isAbsolute, join } from 'node:path';\nimport type { ScreenshotItem } from '../screenshot-item';\nimport { collectImageScriptIds, extractImageByIdSync } from './html-utils';\nimport {\n type ImageUrlRef,\n type ScreenshotRef,\n type StoredImageRef,\n imageFileExtensionForMimeType,\n imageRefFileExtension,\n normalizeImageUrlRef,\n normalizeScreenshotRef,\n normalizeStoredImageRef,\n} from './image-reference';\nimport { writeReportArtifactFile } from './report-artifact-store';\n\nexport type {\n ImageUrlRef,\n ScreenshotRef,\n StoredImageRef,\n} from './image-reference';\nexport {\n imageRefFileExtension,\n normalizeImageUrlRef,\n normalizeScreenshotRef,\n normalizeStoredImageRef,\n} from './image-reference';\n\nconst base64ImageDataUrlPattern =\n /^data:(image\\/[a-z0-9.+-]+)(?:;[^,]*)?;base64,([a-z0-9+/=\\s]+)$/i;\ntype ResolvedImageSource =\n | {\n type: 'data-uri';\n id: string;\n mimeType: StoredImageRef['mimeType'];\n dataUri: string;\n }\n | {\n type: 'file';\n id: string;\n mimeType: StoredImageRef['mimeType'];\n filePath: string;\n };\n\n/** Parse and normalize a base64 image data URL for report persistence. */\nexport function parseBase64ImageDataUrl(imageUrl: string): {\n mimeType: `image/${string}`;\n extension: string;\n rawBase64: string;\n} {\n const match = imageUrl.match(base64ImageDataUrlPattern);\n if (!match) {\n throw new Error(\n 'ReportImageStore: reference image must be a valid base64 image data URL',\n );\n }\n const mimeType = match[1].toLowerCase() as `image/${string}`;\n return {\n mimeType,\n extension: imageFileExtensionForMimeType(mimeType),\n rawBase64: match[2].replace(/\\s/g, ''),\n };\n}\n\nexport function isBase64ImageDataUrl(value: string): boolean {\n return base64ImageDataUrlPattern.test(value);\n}\n\n/** Stable asset names allow different observations to share the same bytes. */\nexport function reportImageFileName(\n extension: string,\n bytes: Uint8Array,\n): string {\n const hash = createHash('sha256')\n .update(extension)\n .update('\\0')\n .update(bytes)\n .digest('hex');\n return `${hash}.${extension}`;\n}\n\nexport function resolveImageSource(\n refInput: unknown,\n options: {\n reportPath: string;\n imageDirectory?: string;\n fallbackId?: string;\n fallbackMimeType?: ScreenshotRef['mimeType'];\n },\n): ResolvedImageSource {\n const ref = normalizeStoredImageRef(refInput);\n const id = ref?.id ?? options.fallbackId;\n const mimeType = ref?.mimeType ?? options.fallbackMimeType;\n\n if (!id || !mimeType) {\n throw new Error(\n 'ReportImageStore: image id and mimeType are required to resolve an image',\n );\n }\n\n const resolveReportRelativePath = (filePath: string): string =>\n isAbsolute(filePath)\n ? filePath\n : join(dirname(options.reportPath), filePath);\n\n if (ref?.storage === 'file') {\n if (!ref.path) {\n throw new Error(\n `ReportImageStore: image ref \"${ref.id}\" missing file path`,\n );\n }\n\n const explicitFilePath = resolveReportRelativePath(ref.path);\n if (existsSync(explicitFilePath)) {\n return {\n type: 'file',\n id,\n mimeType,\n filePath: explicitFilePath,\n };\n }\n }\n\n const inlineDataUri = extractImageByIdSync(options.reportPath, id);\n if (inlineDataUri) {\n return {\n type: 'data-uri',\n id,\n mimeType,\n dataUri: inlineDataUri,\n };\n }\n\n const extension = ref\n ? imageRefFileExtension(ref)\n : imageFileExtensionForMimeType(options.fallbackMimeType!);\n if (options.imageDirectory) {\n const companionImagePath = join(\n options.imageDirectory,\n `${id}.${extension}`,\n );\n if (existsSync(companionImagePath)) {\n return {\n type: 'file',\n id,\n mimeType,\n filePath: companionImagePath,\n };\n }\n }\n const siblingImagePath = join(\n dirname(options.reportPath),\n 'screenshots',\n `${id}.${extension}`,\n );\n if (existsSync(siblingImagePath)) {\n return {\n type: 'file',\n id,\n mimeType,\n filePath: siblingImagePath,\n };\n }\n\n throw new Error(\n `ReportImageStore: cannot resolve image \"${id}\" from ${options.reportPath}`,\n );\n}\n\nexport function resolveScreenshotSource(\n refInput: unknown,\n options: {\n reportPath: string;\n imageDirectory?: string;\n fallbackId?: string;\n fallbackMimeType?: ScreenshotRef['mimeType'];\n },\n): ResolvedImageSource {\n const ref = normalizeScreenshotRef(refInput);\n return resolveImageSource(ref, options);\n}\n\n/** Persists and restores every image asset referenced by a report. */\nexport class ReportImageStore {\n private readonly mode: 'inline' | 'directory';\n private readonly reportPath: string;\n private readonly screenshotsDir?: string;\n private readonly writeInlineImage?: (\n id: string,\n base64: string,\n ) => void | Promise<void>;\n private alsoWriteFileCopy: boolean;\n private readonly writtenInlineIds = new Set<string>();\n private readonly writtenFileIds = new Set<string>();\n private readonly fileLocations = new Map<\n string,\n { relativePath: string; absolutePath: string }\n >();\n private readonly pendingFiles = new Map<\n string,\n Promise<{ relativePath: string; absolutePath: string }>\n >();\n private readonly shouldReuseExistingInlineReport: boolean;\n private existingInlineImageIdsPromise?: Promise<void>;\n\n constructor(options: {\n mode: 'inline' | 'directory';\n reportPath: string;\n screenshotsDir?: string;\n writeInlineImage?: (id: string, base64: string) => void | Promise<void>;\n alsoWriteFileCopy?: boolean;\n /** @deprecated Use alsoWriteFileCopy instead. */\n ensureFileCopy?: boolean;\n /** Reuse inline image assets already present in reportPath. */\n reuseExistingReport?: boolean;\n }) {\n this.mode = options.mode;\n this.reportPath = options.reportPath;\n this.screenshotsDir = options.screenshotsDir;\n this.writeInlineImage = options.writeInlineImage;\n this.alsoWriteFileCopy =\n options.alsoWriteFileCopy ?? options.ensureFileCopy ?? false;\n this.shouldReuseExistingInlineReport = Boolean(\n options.reuseExistingReport && this.mode === 'inline',\n );\n }\n\n enableFileCopies(): void {\n this.alsoWriteFileCopy = true;\n }\n\n /** Resolve an observation/reference ID to its content-addressed asset. */\n getFilePath(id: string): string {\n const file = this.fileLocations.get(id);\n if (!file)\n throw new Error(`ReportImageStore: image ${id} has no persisted file`);\n return file.relativePath;\n }\n\n async persist(screenshot: ScreenshotItem): Promise<ScreenshotRef> {\n await this.ensureExistingInlineImageIdsLoaded();\n const shouldWriteFileCopy =\n this.mode === 'directory' || this.alsoWriteFileCopy;\n const fileRef = shouldWriteFileCopy\n ? await this.persistToSharedFileIfNeeded(screenshot, {\n markAsPersisted: this.mode === 'directory',\n })\n : null;\n\n if (this.mode === 'inline') {\n if (!this.writeInlineImage) {\n throw new Error(\n 'ReportImageStore: writeInlineImage is required in inline mode',\n );\n }\n if (!this.writtenInlineIds.has(screenshot.id)) {\n await this.writeInlineImage(screenshot.id, screenshot.base64);\n this.writtenInlineIds.add(screenshot.id);\n }\n return screenshot.markPersistedInline(this.reportPath);\n }\n\n if (!fileRef) {\n throw new Error(\n 'ReportImageStore: file persistence is required in directory mode',\n );\n }\n return fileRef;\n }\n\n /** Persist a base64 multimodal prompt image and return its content-addressed ref. */\n async persistReferenceImage(imageUrl: string): Promise<ImageUrlRef> {\n await this.ensureExistingInlineImageIdsLoaded();\n const { mimeType, extension, rawBase64 } =\n parseBase64ImageDataUrl(imageUrl);\n const id = `reference-${createHash('sha256')\n .update(mimeType)\n .update('\\0')\n .update(Buffer.from(rawBase64, 'base64'))\n .digest('hex')}`;\n const shouldWriteFileCopy =\n this.mode === 'directory' || this.alsoWriteFileCopy;\n const fileLocation = shouldWriteFileCopy\n ? await this.writeImageFileIfNeeded({ id, extension, rawBase64 })\n : null;\n\n let ref: ImageUrlRef;\n if (this.mode === 'inline') {\n if (!this.writeInlineImage) {\n throw new Error(\n 'ReportImageStore: writeInlineImage is required in inline mode',\n );\n }\n if (!this.writtenInlineIds.has(id)) {\n await this.writeInlineImage(id, imageUrl);\n this.writtenInlineIds.add(id);\n }\n ref = {\n type: 'midscene_image_url_ref',\n id,\n mimeType,\n storage: 'inline',\n };\n } else {\n if (!fileLocation) {\n throw new Error(\n 'ReportImageStore: file persistence is required in directory mode',\n );\n }\n ref = {\n type: 'midscene_image_url_ref',\n id,\n mimeType,\n storage: 'file',\n path: fileLocation.relativePath,\n };\n }\n return ref;\n }\n\n private async ensureExistingInlineImageIdsLoaded(): Promise<void> {\n if (!this.shouldReuseExistingInlineReport) return;\n if (!this.existingInlineImageIdsPromise) {\n this.existingInlineImageIdsPromise = (async () => {\n try {\n const imageIds = await collectImageScriptIds(this.reportPath);\n for (const imageId of imageIds) this.writtenInlineIds.add(imageId);\n } catch (error) {\n if (\n typeof error === 'object' &&\n error !== null &&\n 'code' in error &&\n error.code === 'ENOENT'\n ) {\n return;\n }\n throw new Error(\n `ReportImageStore: failed to index existing inline images from ${this.reportPath}`,\n { cause: error },\n );\n }\n })();\n }\n await this.existingInlineImageIdsPromise;\n }\n\n private async persistToSharedFileIfNeeded(\n screenshot: ScreenshotItem,\n options: {\n markAsPersisted: boolean;\n },\n ): Promise<ScreenshotRef> {\n const { relativePath, absolutePath } =\n this.fileLocations.get(screenshot.id) ??\n (await this.writeImageFileIfNeeded({\n id: screenshot.id,\n extension: screenshot.extension,\n rawBase64: screenshot.rawBase64,\n }));\n\n if (options.markAsPersisted) {\n return screenshot.markPersistedToPath(relativePath, absolutePath);\n }\n\n return screenshot.registerPersistedFileCopy(relativePath, absolutePath);\n }\n\n private async writeImageFileIfNeeded(image: {\n id: string;\n extension: string;\n rawBase64: string;\n }): Promise<{ relativePath: string; absolutePath: string }> {\n const screenshotsDir = this.screenshotsDir;\n if (!screenshotsDir) {\n throw new Error(\n 'ReportImageStore: screenshotsDir is required when file persistence is enabled',\n );\n }\n if (!existsSync(screenshotsDir)) {\n mkdirSync(screenshotsDir, { recursive: true });\n }\n\n const known = this.fileLocations.get(image.id);\n if (known) return known;\n const bytes = Buffer.from(image.rawBase64, 'base64');\n const fileName = reportImageFileName(image.extension, bytes);\n const relativePath = `./screenshots/${fileName}`;\n const absolutePath = join(screenshotsDir, fileName);\n let publication = this.pendingFiles.get(fileName);\n if (!publication) {\n publication = (async () => {\n if (!this.writtenFileIds.has(fileName)) {\n await writeReportArtifactFile(absolutePath, bytes);\n this.writtenFileIds.add(fileName);\n }\n return { relativePath, absolutePath };\n })();\n this.pendingFiles.set(fileName, publication);\n }\n try {\n const location = await publication;\n this.fileLocations.set(image.id, location);\n return location;\n } finally {\n this.pendingFiles.delete(fileName);\n }\n }\n\n /** Resolve any stored report image reference to a data URI. */\n loadDataUri(refInput: unknown): string {\n const ref = normalizeStoredImageRef(refInput);\n if (!ref) {\n throw new Error('ReportImageStore: invalid image reference');\n }\n\n const resolved = resolveImageSource(ref, {\n reportPath: this.reportPath,\n imageDirectory: this.screenshotsDir,\n });\n\n if (resolved.type === 'data-uri') {\n return resolved.dataUri;\n }\n\n const data = readFileSync(resolved.filePath);\n return `data:${resolved.mimeType};base64,${data.toString('base64')}`;\n }\n\n /** @deprecated Use loadDataUri. */\n loadBase64(refInput: unknown): string {\n const screenshotRef = normalizeScreenshotRef(refInput);\n if (!screenshotRef) {\n throw new Error('ReportImageStore: invalid screenshot reference');\n }\n return this.loadDataUri(screenshotRef);\n }\n\n cleanup(): void {\n if (\n this.mode === 'directory' &&\n this.screenshotsDir &&\n existsSync(this.screenshotsDir)\n ) {\n rmSync(this.screenshotsDir, { recursive: true, force: true });\n }\n this.writtenInlineIds.clear();\n this.writtenFileIds.clear();\n this.fileLocations.clear();\n this.pendingFiles.clear();\n this.existingInlineImageIdsPromise = undefined;\n }\n}\n\n/** @deprecated Use ReportImageStore. */\nexport { ReportImageStore as ScreenshotStore };\n"],"names":["base64ImageDataUrlPattern","parseBase64ImageDataUrl","imageUrl","match","Error","mimeType","imageFileExtensionForMimeType","isBase64ImageDataUrl","value","reportImageFileName","extension","bytes","hash","createHash","resolveImageSource","refInput","options","ref","normalizeStoredImageRef","id","resolveReportRelativePath","filePath","isAbsolute","join","dirname","explicitFilePath","existsSync","inlineDataUri","extractImageByIdSync","imageRefFileExtension","companionImagePath","siblingImagePath","resolveScreenshotSource","normalizeScreenshotRef","ReportImageStore","file","screenshot","shouldWriteFileCopy","fileRef","rawBase64","Buffer","fileLocation","imageIds","collectImageScriptIds","imageId","error","relativePath","absolutePath","image","screenshotsDir","mkdirSync","known","fileName","publication","writeReportArtifactFile","location","resolved","data","readFileSync","screenshotRef","rmSync","undefined","Set","Map","Boolean"],"mappings":"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"}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { generateImageScriptTag, parseImageScripts } from "./html-utils.mjs";
|
|
5
|
+
import { imageRefFileExtension, normalizeStoredImageRef } from "./image-reference.mjs";
|
|
6
|
+
import { parseBase64ImageDataUrl, reportImageFileName } from "./screenshot-store.mjs";
|
|
7
|
+
function _define_property(obj, key, value) {
|
|
8
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
9
|
+
value: value,
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true
|
|
13
|
+
});
|
|
14
|
+
else obj[key] = value;
|
|
15
|
+
return obj;
|
|
16
|
+
}
|
|
17
|
+
class TestReportImages {
|
|
18
|
+
reference(ref, dataUri) {
|
|
19
|
+
const known = this.locations.get(ref.id);
|
|
20
|
+
if (known) return {
|
|
21
|
+
...ref,
|
|
22
|
+
...known
|
|
23
|
+
};
|
|
24
|
+
const location = this.external ? {
|
|
25
|
+
storage: 'file',
|
|
26
|
+
path: `./screenshots/${reportImageFileName(imageRefFileExtension(ref), Buffer.from(parseBase64ImageDataUrl(dataUri).rawBase64, 'base64'))}`
|
|
27
|
+
} : {
|
|
28
|
+
storage: 'inline',
|
|
29
|
+
path: void 0
|
|
30
|
+
};
|
|
31
|
+
this.locations.set(ref.id, location);
|
|
32
|
+
return {
|
|
33
|
+
...ref,
|
|
34
|
+
...location
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
rewrite(serialized) {
|
|
38
|
+
return JSON.stringify(JSON.parse(serialized), (_key, value)=>{
|
|
39
|
+
const ref = normalizeStoredImageRef(value);
|
|
40
|
+
return ref ? {
|
|
41
|
+
...ref,
|
|
42
|
+
...this.locations.get(ref.id)
|
|
43
|
+
} : value;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
collect(serialized) {
|
|
47
|
+
const refs = new Map();
|
|
48
|
+
JSON.parse(serialized, (_key, value)=>{
|
|
49
|
+
const ref = normalizeStoredImageRef(value);
|
|
50
|
+
if (ref && !this.locations.has(ref.id)) refs.set(ref.id, ref);
|
|
51
|
+
return value;
|
|
52
|
+
});
|
|
53
|
+
return [
|
|
54
|
+
...refs.values()
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
rewriteSync(serialized, sourcePath) {
|
|
58
|
+
let inlineImages;
|
|
59
|
+
for (const ref of this.collect(serialized)){
|
|
60
|
+
let data;
|
|
61
|
+
if ('file' === ref.storage) data = `data:${ref.mimeType};base64,${readFileSync(resolve(dirname(sourcePath), ref.path)).toString('base64')}`;
|
|
62
|
+
else {
|
|
63
|
+
inlineImages ??= parseImageScripts(readFileSync(sourcePath, 'utf8'));
|
|
64
|
+
data = inlineImages[ref.id];
|
|
65
|
+
if (!data) throw new Error(`Missing report image ${ref.id} in ${sourcePath}`);
|
|
66
|
+
}
|
|
67
|
+
const target = this.reference(ref, data);
|
|
68
|
+
if ('file' === target.storage) {
|
|
69
|
+
if (!this.files.has(target.path)) {
|
|
70
|
+
const file = join(dirname(this.reportPath), target.path);
|
|
71
|
+
mkdirSync(dirname(file), {
|
|
72
|
+
recursive: true
|
|
73
|
+
});
|
|
74
|
+
writeFileSync(file, Buffer.from(parseBase64ImageDataUrl(data).rawBase64, 'base64'));
|
|
75
|
+
this.files.add(target.path);
|
|
76
|
+
}
|
|
77
|
+
} else appendFileSync(this.reportPath, `${generateImageScriptTag(ref.id, data)}\n`);
|
|
78
|
+
}
|
|
79
|
+
return this.rewrite(serialized);
|
|
80
|
+
}
|
|
81
|
+
async rewriteAsync(serialized, sourcePath) {
|
|
82
|
+
let inlineImages;
|
|
83
|
+
for (const ref of this.collect(serialized)){
|
|
84
|
+
let data;
|
|
85
|
+
if ('file' === ref.storage) {
|
|
86
|
+
const bytes = await readFile(resolve(dirname(sourcePath), ref.path));
|
|
87
|
+
data = `data:${ref.mimeType};base64,${bytes.toString('base64')}`;
|
|
88
|
+
} else {
|
|
89
|
+
inlineImages ??= parseImageScripts(await readFile(sourcePath, 'utf8'));
|
|
90
|
+
data = inlineImages[ref.id];
|
|
91
|
+
if (!data) throw new Error(`Missing report image ${ref.id} in ${sourcePath}`);
|
|
92
|
+
}
|
|
93
|
+
const target = this.reference(ref, data);
|
|
94
|
+
if ('file' === target.storage) {
|
|
95
|
+
if (!this.files.has(target.path)) {
|
|
96
|
+
const file = join(dirname(this.reportPath), target.path);
|
|
97
|
+
await mkdir(dirname(file), {
|
|
98
|
+
recursive: true
|
|
99
|
+
});
|
|
100
|
+
await writeFile(file, Buffer.from(parseBase64ImageDataUrl(data).rawBase64, 'base64'));
|
|
101
|
+
this.files.add(target.path);
|
|
102
|
+
}
|
|
103
|
+
} else await appendFile(this.reportPath, `${generateImageScriptTag(ref.id, data)}\n`);
|
|
104
|
+
}
|
|
105
|
+
return this.rewrite(serialized);
|
|
106
|
+
}
|
|
107
|
+
constructor(reportPath, external){
|
|
108
|
+
_define_property(this, "reportPath", void 0);
|
|
109
|
+
_define_property(this, "external", void 0);
|
|
110
|
+
_define_property(this, "locations", void 0);
|
|
111
|
+
_define_property(this, "files", void 0);
|
|
112
|
+
this.reportPath = reportPath;
|
|
113
|
+
this.external = external;
|
|
114
|
+
this.locations = new Map();
|
|
115
|
+
this.files = new Set();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export { TestReportImages };
|
|
119
|
+
|
|
120
|
+
//# sourceMappingURL=test-report-images.mjs.map
|