Skip to content

Commit ebfa101

Browse files
committed
feat(report): support WebP screenshot consumers
1 parent eb26577 commit ebfa101

22 files changed

Lines changed: 390 additions & 64 deletions

apps/report/src/App.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,16 @@ import {
5454
getEmptyDumpDescription,
5555
parseDumpAttributes,
5656
} from './utils/report-dump';
57+
import {
58+
type ReportScreenshotSourceRef,
59+
resolveScreenshotFallbackPath,
60+
} from './utils/screenshot-source';
5761

5862
// Shared image cache across all test cases — resolved images are cached by id
5963
const imageCache = new Map<string, string>();
6064

6165
function resolveImageFromDom(
62-
refOrId: string | { id: string; storage?: 'inline' | 'file'; path?: string },
66+
refOrId: string | ReportScreenshotSourceRef,
6367
): string {
6468
const id = typeof refOrId === 'string' ? refOrId : refOrId.id;
6569
const cached = imageCache.get(id);
@@ -74,12 +78,7 @@ function resolveImageFromDom(
7478
return data;
7579
}
7680

77-
if (typeof refOrId === 'object' && refOrId?.storage === 'file') {
78-
return refOrId.path || `./screenshots/${id}.png`;
79-
}
80-
81-
// Fallback to directory path
82-
return `./screenshots/${id}.png`;
81+
return resolveScreenshotFallbackPath(refOrId);
8382
}
8483

8584
let globalRenderCount = 1;

apps/report/src/components/playground/index.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ import {
1414
} from '@midscene/core/dump';
1515
import { type PlaygroundSDK, noReplayAPIs } from '@midscene/playground';
1616
import type { ServerResponse } from '@midscene/playground';
17+
import {
18+
screenshotImageExtension,
19+
screenshotImageFormatFromMimeType,
20+
} from '@midscene/shared/img/image-format';
1721
import {
1822
ContextPreview,
1923
Logo,
@@ -55,7 +59,11 @@ async function loadReferencedReplay(result: PlaygroundResult) {
5559
}
5660
const dump = (await response.json()) as IReportActionDump;
5761
result.dump = restoreImageReferences(dump, (ref) => {
58-
const extension = ref.mimeType === 'image/jpeg' ? 'jpeg' : 'png';
62+
const format = screenshotImageFormatFromMimeType(ref.mimeType);
63+
if (!format) {
64+
throw new Error(`Unsupported screenshot mime type: ${ref.mimeType}`);
65+
}
66+
const extension = screenshotImageExtension(format);
5967
return new URL(
6068
`screenshots/${encodeURIComponent(ref.id)}.${extension}`,
6169
result.report!.url,

apps/report/src/components/timeline/build-timeline-screenshots.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { buildTimelineScreenshots } from './build-timeline-screenshots';
44

55
const onePixelPngBase64 =
66
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAFgwJ/lz8yrwAAAABJRU5ErkJggg==';
7+
const webpBase64 =
8+
'UklGRjQAAABXRUJQVlA4ICgAAACQAQCdASoCAAMAAMASJQBOl0AAjNAA/v4icv1difCfoP7mxzi2QwAA';
79

810
interface TaskFixtureOptions {
911
id: string;
@@ -103,6 +105,19 @@ describe('buildTimelineScreenshots', () => {
103105
);
104106
});
105107

108+
it('labels raw WebP recorder screenshots with the WebP MIME type', () => {
109+
const task = makeTask({
110+
id: 'raw-webp-recorder',
111+
startTs: 1000,
112+
recorder: [{ ts: 1200, screenshot: webpBase64 }],
113+
});
114+
115+
const { allScreenshots } = buildTimelineScreenshots([task]);
116+
117+
expect(allScreenshots).toHaveLength(1);
118+
expect(allScreenshots[0].img).toBe(`data:image/webp;base64,${webpBase64}`);
119+
});
120+
106121
it('keeps already-prefixed data URLs unchanged', () => {
107122
const dataUrl = `data:image/png;base64,${onePixelPngBase64}`;
108123
const task = makeTask({

apps/report/src/components/timeline/build-timeline-screenshots.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import type { ExecutionTask } from '@midscene/core';
2+
import {
3+
inferScreenshotImageFormatFromBase64,
4+
screenshotImageMimeType,
5+
} from '@midscene/shared/img/image-format';
26

37
export interface TimelineScreenshot {
48
id: string;
@@ -30,11 +34,8 @@ const imageSrcFromString = (value: string): string => {
3034
}
3135

3236
const body = trimmed.replace(/\s/g, '');
33-
const mimeType = body.startsWith('/9j/')
34-
? 'image/jpeg'
35-
: body.startsWith('UklGR')
36-
? 'image/webp'
37-
: 'image/png';
37+
const format = inferScreenshotImageFormatFromBase64(body) ?? 'png';
38+
const mimeType = screenshotImageMimeType(format);
3839
return `data:${mimeType};base64,${body}`;
3940
};
4041

apps/report/src/utils/markdown-export.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,27 @@ describe('markdown-export helpers', () => {
105105
);
106106
});
107107

108+
it('packages WebP data URI attachments without changing their bytes', () => {
109+
const files = buildMarkdownArchiveFiles('# webp report', [
110+
{
111+
id: 'webp-inline',
112+
suggestedFileName: 'webp-inline.webp',
113+
mimeType: 'image/webp',
114+
executionIndex: 0,
115+
taskIndex: 0,
116+
base64Data: `data:image/webp;base64,${btoa('webp-bytes')}`,
117+
},
118+
]);
119+
120+
expect(Object.keys(files).sort()).toEqual([
121+
'report.md',
122+
'screenshots/webp-inline.webp',
123+
]);
124+
expect(
125+
new TextDecoder().decode(files['screenshots/webp-inline.webp']),
126+
).toBe('webp-bytes');
127+
});
128+
108129
it('builds display items from markdown attachment names and paths', () => {
109130
const items = getMarkdownAttachmentDisplayItems([
110131
{

apps/report/src/utils/markdown-export.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { MarkdownAttachment } from '@midscene/core';
22

33
const defaultScreenshotBaseDir = './screenshots';
4-
const dataUrlBase64Pattern = /^data:image\/(?:png|jpeg|jpg);base64,/i;
4+
const dataUrlBase64Pattern = /^data:image\/(?:png|jpeg|jpg|webp);base64,/i;
55
const rawBase64Pattern = /^[a-zA-Z0-9+/=\s]+$/;
66

77
type MarkdownExport = {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { resolveScreenshotFallbackPath } from './screenshot-source';
3+
4+
describe('resolveScreenshotFallbackPath', () => {
5+
it('keeps the legacy PNG fallback when only an id is available', () => {
6+
expect(resolveScreenshotFallbackPath('legacy-shot')).toBe(
7+
'./screenshots/legacy-shot.png',
8+
);
9+
});
10+
11+
it('uses the MIME-specific extension for screenshot references', () => {
12+
expect(
13+
resolveScreenshotFallbackPath({
14+
id: 'webp-shot',
15+
mimeType: 'image/webp',
16+
storage: 'inline',
17+
}),
18+
).toBe('./screenshots/webp-shot.webp');
19+
expect(
20+
resolveScreenshotFallbackPath({
21+
id: 'jpeg-shot',
22+
mimeType: 'image/jpeg',
23+
storage: 'inline',
24+
}),
25+
).toBe('./screenshots/jpeg-shot.jpeg');
26+
});
27+
28+
it('prefers an explicit file-backed path', () => {
29+
expect(
30+
resolveScreenshotFallbackPath({
31+
id: 'webp-shot',
32+
mimeType: 'image/webp',
33+
storage: 'file',
34+
path: './assets/custom.webp',
35+
}),
36+
).toBe('./assets/custom.webp');
37+
});
38+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {
2+
screenshotImageExtension,
3+
screenshotImageFormatFromMimeType,
4+
} from '@midscene/shared/img/image-format';
5+
6+
export interface ReportScreenshotSourceRef {
7+
id: string;
8+
mimeType?: unknown;
9+
storage?: 'inline' | 'file';
10+
path?: string;
11+
}
12+
13+
export function resolveScreenshotFallbackPath(
14+
refOrId: string | ReportScreenshotSourceRef,
15+
): string {
16+
if (
17+
typeof refOrId === 'object' &&
18+
refOrId.storage === 'file' &&
19+
refOrId.path
20+
) {
21+
return refOrId.path;
22+
}
23+
24+
const id = typeof refOrId === 'string' ? refOrId : refOrId.id;
25+
const format =
26+
typeof refOrId === 'object'
27+
? screenshotImageFormatFromMimeType(refOrId.mimeType)
28+
: undefined;
29+
const extension = format ? screenshotImageExtension(format) : 'png';
30+
return `./screenshots/${id}.${extension}`;
31+
}

apps/site/docs/en/api.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,7 +1240,7 @@ interface RecordToReportOptions {
12401240
screenshotBase64?: string;
12411241
screenshots?: {
12421242
/**
1243-
* PNG/JPEG data URI, or raw PNG base64 body.
1243+
* PNG/JPEG/WebP data URI, or raw PNG/WebP base64 body.
12441244
*/
12451245
base64: string;
12461246
description?: string;
@@ -1258,7 +1258,7 @@ function recordToReport(
12581258
- `title?: string` - Optional, the title of the screenshot, if not provided, the title will be 'untitled'.
12591259
- `options?: RecordToReportOptions` - Optional, a configuration object containing:
12601260
- `content?: string` - The description of the screenshot.
1261-
- `screenshots?: Array<{ base64: string; description?: string }>` - Record one or more provided screenshots under one report entry. When this option is set, Midscene will not capture another screenshot automatically. `base64` should be a PNG/JPEG data URI such as `data:image/png;base64,...`; a raw base64 body is also accepted and treated as PNG.
1261+
- `screenshots?: Array<{ base64: string; description?: string }>` - Record one or more provided screenshots under one report entry. When this option is set, Midscene will not capture another screenshot automatically. `base64` should be a PNG/JPEG/WebP data URI such as `data:image/webp;base64,...`; raw WebP base64 is detected by its signature, while other raw base64 bodies retain the existing PNG default.
12621262

12631263
- Compatibility:
12641264

apps/site/docs/zh/api.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,7 +1230,7 @@ interface RecordToReportOptions {
12301230
screenshotBase64?: string;
12311231
screenshots?: {
12321232
/**
1233-
* PNG/JPEG data URI,或裸 PNG base64 body。
1233+
* PNG/JPEG/WebP data URI,或裸 PNG/WebP base64 body。
12341234
*/
12351235
base64: string;
12361236
description?: string;
@@ -1248,7 +1248,7 @@ function recordToReport(
12481248
- `title?: string` - 可选,截图的标题,如果未提供,则标题为 'untitled'。
12491249
- `options?: RecordToReportOptions` - 可选,一个配置对象,包含:
12501250
- `content?: string` - 截图的描述。
1251-
- `screenshots?: Array<{ base64: string; description?: string }>` - 在同一个报告条目下记录一张或多张传入的截图。设置该选项后,Midscene 不会再自动截图。`base64` 推荐使用 PNG/JPEG data URI,例如 `data:image/png;base64,...`也可以传裸 base64 body,此时会按 PNG 处理
1251+
- `screenshots?: Array<{ base64: string; description?: string }>` - 在同一个报告条目下记录一张或多张传入的截图。设置该选项后,Midscene 不会再自动截图。`base64` 推荐使用 PNG/JPEG/WebP data URI,例如 `data:image/webp;base64,...`裸 WebP base64 会通过文件签名识别,其他裸 base64 body 保持现有的 PNG 默认行为
12521252

12531253
- 兼容性:
12541254

0 commit comments

Comments
 (0)