sootsim 0.1.83 → 0.1.84
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 +0 -1
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/expectations.ts +477 -0
- package/detox/gestures.ts +442 -0
- package/detox/index.ts +1436 -0
- package/detox/jest-environment.ts +86 -0
- package/detox/jest-preset.cjs +50 -0
- package/detox/matchers.ts +29 -0
- package/detox/navigation.ts +43 -0
- package/detox/run-test.ts +113 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-cli/bin.js +3 -3
- package/dist-cli/chunks/{agent-MQ7GLVIB.js → agent-2CWD6W6P.js} +2 -2
- package/dist-cli/chunks/{agent-wrapper-7KAFDQCN.js → agent-wrapper-5W3LOX6S.js} +2 -2
- package/dist-cli/chunks/{assert-TV46GUNU.js → assert-ZOMAMKRT.js} +2 -2
- package/dist-cli/chunks/auto-bootstrap-NYYSMTIM.js +2 -0
- package/dist-cli/chunks/beta-4K2SQACK.js +2 -0
- package/dist-cli/chunks/chunk-3HXQ7MJK.js +79 -0
- package/dist-cli/chunks/{chunk-4LS5MZAI.js → chunk-4K7BH2D4.js} +3 -3
- package/dist-cli/chunks/{chunk-FJYT7XL2.js → chunk-4OPRODFA.js} +2 -2
- package/dist-cli/chunks/{chunk-DP7O5MHK.js → chunk-4OWVPRZV.js} +2 -2
- package/dist-cli/chunks/{chunk-PM5NVKLP.js → chunk-5XCXOLG2.js} +2 -2
- package/dist-cli/chunks/chunk-67ZZ2CM5.js +1 -0
- package/dist-cli/chunks/{chunk-WN7M3QON.js → chunk-73UZXB4B.js} +2 -2
- package/dist-cli/chunks/{chunk-5DJXZIFZ.js → chunk-7NWNTUJF.js} +1 -1
- package/dist-cli/chunks/{chunk-Y2VJBRSP.js → chunk-7YHDJLO2.js} +1 -1
- package/dist-cli/chunks/{chunk-6NN2D4EJ.js → chunk-AJVTY6KY.js} +1 -1
- package/dist-cli/chunks/chunk-AWSQUOAS.js +67 -0
- package/dist-cli/chunks/{chunk-CJY3AVI7.js → chunk-BCBNVJVG.js} +1 -1
- package/dist-cli/chunks/{chunk-OYMFNU3M.js → chunk-BKBL6K2G.js} +1 -1
- package/dist-cli/chunks/{chunk-IBNRRAES.js → chunk-C3DPQZ4J.js} +2 -2
- package/dist-cli/chunks/chunk-D3ZSBIIY.js +2 -0
- package/dist-cli/chunks/{chunk-2AWQ7OB2.js → chunk-D4HUVLZR.js} +1 -1
- package/dist-cli/chunks/{chunk-F3HP444U.js → chunk-DUUSJDES.js} +1 -1
- package/dist-cli/chunks/{chunk-277XAALA.js → chunk-ELJLF4SG.js} +3 -3
- package/dist-cli/chunks/{chunk-RH4F2TF7.js → chunk-EQ7TFQ2F.js} +1 -1
- package/dist-cli/chunks/{chunk-HNWEELAE.js → chunk-EQCKGC4B.js} +1 -1
- package/dist-cli/chunks/chunk-FUCGLWNN.js +1 -0
- package/dist-cli/chunks/{chunk-FRM355UL.js → chunk-HYPJW65U.js} +2 -2
- package/dist-cli/chunks/chunk-IILJQCZA.js +2 -0
- package/dist-cli/chunks/{chunk-Y4BUVURT.js → chunk-KU6MSPAH.js} +2 -2
- package/dist-cli/chunks/{chunk-DM6WT7QM.js → chunk-OOOR7NT2.js} +1 -1
- package/dist-cli/chunks/{chunk-HAWOAQAG.js → chunk-P7WDNKOS.js} +3 -3
- package/dist-cli/chunks/{chunk-6TNANCQC.js → chunk-PPKKA5VW.js} +2 -2
- package/dist-cli/chunks/{chunk-JQ7ZXOXJ.js → chunk-PS2G44GT.js} +2 -2
- package/dist-cli/chunks/{chunk-ECJBV65H.js → chunk-QMSJR5R2.js} +2 -2
- package/dist-cli/chunks/{chunk-J2GYISVJ.js → chunk-RF4R2U46.js} +2 -2
- package/dist-cli/chunks/{chunk-VMXWC2JO.js → chunk-RIXUH3NK.js} +2 -2
- package/dist-cli/chunks/{chunk-2PY3UZVO.js → chunk-SFGUPL2X.js} +2 -2
- package/dist-cli/chunks/{chunk-572VSFNP.js → chunk-SQX5CAYG.js} +1 -1
- package/dist-cli/chunks/{chunk-NXATOWWF.js → chunk-SQZAC7C4.js} +1 -1
- package/dist-cli/chunks/{chunk-WTKTOL3C.js → chunk-SV7FOGJ3.js} +2 -2
- package/dist-cli/chunks/{chunk-JHJNODXN.js → chunk-TK3OJSEO.js} +2 -2
- package/dist-cli/chunks/{chunk-KASUZ5XV.js → chunk-TL7SIZ7S.js} +1 -1
- package/dist-cli/chunks/{chunk-6XZOEBTZ.js → chunk-V2GQ4WXJ.js} +2 -2
- package/dist-cli/chunks/{chunk-IP3QJLRH.js → chunk-VH7F45CN.js} +1 -1
- package/dist-cli/chunks/chunk-WNVNU2OW.js +4 -0
- package/dist-cli/chunks/{chunk-YUELRHGB.js → chunk-XQ2OBHBE.js} +2 -2
- package/dist-cli/chunks/{chunk-CYV6Y6YV.js → chunk-YCIA4BHJ.js} +2 -2
- package/dist-cli/chunks/chunk-ZSMMJMPA.js +1 -0
- package/dist-cli/chunks/cli-version-QB4VH24H.js +2 -0
- package/dist-cli/chunks/{compat-QLLWBTS3.js → compat-FWSEEGEH.js} +3 -3
- package/dist-cli/chunks/{config-2DSLDCXV.js → config-CYI2WAGP.js} +2 -2
- package/dist-cli/chunks/control-UXY7YQVX.js +2 -0
- package/dist-cli/chunks/{cpu-profile-GEIKHCPC.js → cpu-profile-IKAE3KTY.js} +2 -2
- package/dist-cli/chunks/{daemon-4EBUFN4D.js → daemon-ZUMF53YB.js} +2 -2
- package/dist-cli/chunks/{debug-WGD6XWOF.js → debug-P6KULKKS.js} +3 -3
- package/dist-cli/chunks/{detox-LNKGRZU6.js → detox-SPWAZCYG.js} +2 -2
- package/dist-cli/chunks/{device-AYKXKVIQ.js → device-JWEPK6I2.js} +2 -2
- package/dist-cli/chunks/{diagnose-TMXSDOOC.js → diagnose-IZODTXV2.js} +2 -2
- package/dist-cli/chunks/drivers-MK6WJKBC.js +2 -0
- package/dist-cli/chunks/{electron-QFPF7TBY.js → electron-R5GP6RVB.js} +3 -3
- package/dist-cli/chunks/flow-6O4GEOPJ.js +2 -0
- package/dist-cli/chunks/{hints-MXKRR4TG.js → hints-DYDNYX7N.js} +2 -2
- package/dist-cli/chunks/{home-paths-REMWQDAO.js → home-paths-GLMX5OKL.js} +2 -2
- package/dist-cli/chunks/{inspect-XGSQNFV7.js → inspect-FJOPCTY2.js} +3 -3
- package/dist-cli/chunks/install-A3TUGGHN.js +2 -0
- package/dist-cli/chunks/{install-desktop-NQG3RZSA.js → install-desktop-YPJZMZM5.js} +3 -3
- package/dist-cli/chunks/{keys-5QZWXL3F.js → keys-GSYPHWNY.js} +2 -2
- package/dist-cli/chunks/{launch-SBXOZWKO.js → launch-4G2PKW5X.js} +3 -3
- package/dist-cli/chunks/{login-EACQXE24.js → login-KJQGHA64.js} +4 -4
- package/dist-cli/chunks/{logout-IBQLMUML.js → logout-XM2SYH5C.js} +2 -2
- package/dist-cli/chunks/{maestro-LFYXUX7O.js → maestro-EOWGI7DG.js} +2 -2
- package/dist-cli/chunks/{preview-U4SBOEGQ.js → preview-F73TKK37.js} +2 -2
- package/dist-cli/chunks/{profile-GWS5ECMY.js → profile-22FDKBUO.js} +2 -2
- package/dist-cli/chunks/{react-QDHLMVYL.js → react-5L6VPFUP.js} +2 -2
- package/dist-cli/chunks/{record-BUEUWPDI.js → record-JZXCQ4IN.js} +2 -2
- package/dist-cli/chunks/runtime-EEBX7CFV.js +2 -0
- package/dist-cli/chunks/{runtime-delivery-G7L6RVZ7.js → runtime-delivery-LXUM3R4A.js} +2 -2
- package/dist-cli/chunks/{screenshot-T2HBA3VI.js → screenshot-HDRRG33Q.js} +2 -2
- package/dist-cli/chunks/{screenshot-mode-EG5HMIH3.js → screenshot-mode-WY63LZIX.js} +2 -2
- package/dist-cli/chunks/{screenshots-S52AFHTV.js → screenshots-MPV2ENL5.js} +2 -2
- package/dist-cli/chunks/{server-MFFVYUGG.js → server-5LBMCJ3G.js} +2 -2
- package/dist-cli/chunks/setup-repo-SZSYNKNI.js +2 -0
- package/dist-cli/chunks/{skills-HQGWBS2O.js → skills-BQ73YOBF.js} +2 -2
- package/dist-cli/chunks/{start-E3DRYY7W.js → start-2WU4W6ZU.js} +4 -4
- package/dist-cli/chunks/store-RE45SUBF.js +2 -0
- package/dist-cli/chunks/telemetry-DG6GJLCP.js +2 -0
- package/dist-cli/chunks/{test-ZY3EF62K.js → test-OVO4CQTG.js} +3 -3
- package/dist-cli/chunks/{three-mode-WSPKQCJ5.js → three-mode-BKM3KFM7.js} +2 -2
- package/dist-cli/chunks/{timeline-3XAB5EWZ.js → timeline-MDXGEDQL.js} +2 -2
- package/dist-cli/chunks/{upgrade-WNENPFM5.js → upgrade-JGQABWVF.js} +2 -2
- package/dist-cli/chunks/upload-UJNUA4ZV.js +2 -0
- package/dist-cli/chunks/{web-D2AOZY44.js → web-WYFAYQ72.js} +2 -2
- package/dist-cli/chunks/{what-happened-F43KNSG6.js → what-happened-PZW2KW6A.js} +2 -2
- package/dist-cli/chunks/{whoami-T22VBR7C.js → whoami-7ATWJQS6.js} +2 -2
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +44 -0
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +1770 -0
- package/dist-lib/detox/jest-preset.cjs +50 -0
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +1 -1
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/index.cjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/demo-app-registry.cjs +809 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1269 -0
- package/dist-lib/skills.cjs +8322 -0
- package/dist-lib/vite-base.cjs +3 -3
- package/dist-lib/vite.cjs +1 -1
- package/package.json +39 -10
- package/scripts/demo-app-registry.ts +989 -0
- package/scripts/dev-server-scanner.ts +674 -0
- package/src/agent-daemon-client.ts +390 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +572 -0
- package/src/attached-projects.ts +536 -0
- package/src/auth/shared-session.ts +199 -0
- package/src/backend-origin.ts +49 -0
- package/src/beta.ts +21 -0
- package/src/bridge-constants.ts +10 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +30 -0
- package/src/codex-client.ts +215 -0
- package/src/config.ts +110 -0
- package/src/dev-bundle-resolution.ts +180 -0
- package/src/home-paths.ts +382 -0
- package/src/host/agent-host.ts +576 -0
- package/src/host/bridge-host.ts +2293 -0
- package/src/host/fetch-proxy-handler.ts +288 -0
- package/src/host/fetch-proxy-overrides.ts +39 -0
- package/src/host/open-url.ts +234 -0
- package/src/index.ts +9 -0
- package/src/metro-plugin.ts +207 -0
- package/src/native-dev-bundle-url.ts +62 -0
- package/src/native-seam-manifest.ts +313 -0
- package/src/profiles.ts +179 -0
- package/src/render-mode.ts +27 -0
- package/src/runtime-delivery.ts +334 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +438 -0
- package/src/screenshots/orchestrate.ts +244 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/skills/builtin/a11y-review.ts +126 -0
- package/src/skills/builtin/compat-check.ts +104 -0
- package/src/skills/builtin/perf-profile.ts +84 -0
- package/src/skills/builtin/screenshot-all.ts +46 -0
- package/src/skills/builtin/test-flow.ts +118 -0
- package/src/skills/builtin/visual-diff.ts +94 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/vite-plugin-one.ts +187 -0
- package/src/vite-plugin.ts +1381 -0
- package/src/worklets-babel.ts +132 -0
- package/dist-cli/chunks/auto-bootstrap-FQS4ZD2K.js +0 -2
- package/dist-cli/chunks/beta-VG7CDY2U.js +0 -2
- package/dist-cli/chunks/chunk-2OIBDYHW.js +0 -1
- package/dist-cli/chunks/chunk-6BNLVMXA.js +0 -1
- package/dist-cli/chunks/chunk-6XD6CBJM.js +0 -2
- package/dist-cli/chunks/chunk-CHQTO426.js +0 -1
- package/dist-cli/chunks/chunk-FAPYGVIU.js +0 -4
- package/dist-cli/chunks/chunk-PEHFE3LG.js +0 -64
- package/dist-cli/chunks/chunk-RXH2SLKF.js +0 -2
- package/dist-cli/chunks/chunk-UXQWC5ZR.js +0 -79
- package/dist-cli/chunks/chunk-XFQL74PF.js +0 -5
- package/dist-cli/chunks/cli-version-PWF6I6LY.js +0 -2
- package/dist-cli/chunks/control-UIOXGYXU.js +0 -2
- package/dist-cli/chunks/demo-app-registry-G3BDOFWC.js +0 -2
- package/dist-cli/chunks/drivers-IDQF34HP.js +0 -2
- package/dist-cli/chunks/flow-3JN3Y7RF.js +0 -2
- package/dist-cli/chunks/install-2N3YOOSN.js +0 -2
- package/dist-cli/chunks/runtime-PVB4VGUH.js +0 -2
- package/dist-cli/chunks/setup-repo-YOF7NV5D.js +0 -2
- package/dist-cli/chunks/store-MAI6D3UO.js +0 -2
- package/dist-cli/chunks/telemetry-RCQKCJTH.js +0 -2
- package/dist-cli/chunks/upload-YLJ4RA73.js +0 -2
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { BEZEL_PADDING, SIDE_BUTTON_RESERVE } from 'sootsim-engine/ios/chrome-metrics'
|
|
2
|
+
import { getDevice, type DeviceModel } from 'sootsim-engine/settings'
|
|
3
|
+
|
|
4
|
+
const LEGACY_FRAME_RADIUS = 52
|
|
5
|
+
const LEGACY_SIDE_BEZEL = 18
|
|
6
|
+
const LEGACY_TOP_BEZEL = 42
|
|
7
|
+
const LEGACY_BOTTOM_BEZEL = 96
|
|
8
|
+
const LEGACY_HOME_BUTTON_SIZE = 54
|
|
9
|
+
const LEGACY_HOME_BUTTON_RING = 1.5
|
|
10
|
+
const METALLIC_RING_SHADOW =
|
|
11
|
+
'inset 0 0 0 0.5px #000, inset 0 0 0 2px #757575, inset 0 0 0 5px #212121'
|
|
12
|
+
const FRAME_OUTLINE_BLEED = 1
|
|
13
|
+
|
|
14
|
+
type FrameButtonLayout = {
|
|
15
|
+
side: 'left' | 'right'
|
|
16
|
+
top: number
|
|
17
|
+
height: number
|
|
18
|
+
width: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type LegacyHomeButtonLayout = {
|
|
22
|
+
top: number
|
|
23
|
+
left: number
|
|
24
|
+
size: number
|
|
25
|
+
ring: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface FramedScreenshotLayout {
|
|
29
|
+
model: DeviceModel
|
|
30
|
+
renderScale: number
|
|
31
|
+
outerWidth: number
|
|
32
|
+
outerHeight: number
|
|
33
|
+
logicalOuterWidth: number
|
|
34
|
+
logicalOuterHeight: number
|
|
35
|
+
logicalFrameWidth: number
|
|
36
|
+
logicalFrameHeight: number
|
|
37
|
+
logicalFrameLeft: number
|
|
38
|
+
logicalFrameTop: number
|
|
39
|
+
logicalScreenWidth: number
|
|
40
|
+
logicalScreenHeight: number
|
|
41
|
+
logicalScreenLeft: number
|
|
42
|
+
logicalScreenTop: number
|
|
43
|
+
logicalScreenRadius: number
|
|
44
|
+
logicalFrameRadius: number
|
|
45
|
+
frameBackground: string
|
|
46
|
+
frameOutline: string
|
|
47
|
+
metallicRingShadow: string | null
|
|
48
|
+
buttons: FrameButtonLayout[]
|
|
49
|
+
legacyHomeButton: LegacyHomeButtonLayout | null
|
|
50
|
+
showHomeIndicator: boolean
|
|
51
|
+
logicalHomeIndicatorStripHeight: number
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Exact shell device frame layout for still exports.
|
|
55
|
+
// Mirrors DeviceFrame's bezel/button geometry while intentionally excluding:
|
|
56
|
+
// - the electron simulator top bar
|
|
57
|
+
// - the browser rail / gutter
|
|
58
|
+
// The screen bitmap itself still comes from the raw bridge screenshot.
|
|
59
|
+
export function getFramedScreenshotLayout(model: DeviceModel): FramedScreenshotLayout {
|
|
60
|
+
const spec = getDevice(model)
|
|
61
|
+
const renderScale = spec.scale
|
|
62
|
+
const isLegacyHardware =
|
|
63
|
+
!spec.dynamicIsland && spec.homeIndicatorHeight === 0 && spec.cornerRadius === 0
|
|
64
|
+
const logicalScreenWidth = spec.width
|
|
65
|
+
const logicalScreenHeight = spec.height
|
|
66
|
+
const logicalFrameWidth = isLegacyHardware
|
|
67
|
+
? spec.width + LEGACY_SIDE_BEZEL * 2
|
|
68
|
+
: spec.width + BEZEL_PADDING * 2
|
|
69
|
+
const logicalFrameHeight = isLegacyHardware
|
|
70
|
+
? spec.height + LEGACY_TOP_BEZEL + LEGACY_BOTTOM_BEZEL
|
|
71
|
+
: spec.height + BEZEL_PADDING * 2
|
|
72
|
+
const logicalFrameLeft = SIDE_BUTTON_RESERVE
|
|
73
|
+
const logicalFrameTop = FRAME_OUTLINE_BLEED
|
|
74
|
+
const logicalOuterWidth = logicalFrameWidth + logicalFrameLeft * 2
|
|
75
|
+
const logicalOuterHeight = logicalFrameHeight + FRAME_OUTLINE_BLEED * 2
|
|
76
|
+
const logicalScreenLeft =
|
|
77
|
+
logicalFrameLeft + (isLegacyHardware ? LEGACY_SIDE_BEZEL : BEZEL_PADDING)
|
|
78
|
+
const logicalScreenTop = isLegacyHardware ? LEGACY_TOP_BEZEL : BEZEL_PADDING
|
|
79
|
+
const logicalFrameRadius = isLegacyHardware
|
|
80
|
+
? LEGACY_FRAME_RADIUS
|
|
81
|
+
: spec.cornerRadius + BEZEL_PADDING
|
|
82
|
+
const buttons: FrameButtonLayout[] = [
|
|
83
|
+
{
|
|
84
|
+
side: 'right',
|
|
85
|
+
top: spec.hardwareButtons.lock.top,
|
|
86
|
+
height: spec.hardwareButtons.lock.height,
|
|
87
|
+
width: spec.hardwareButtons.width,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
side: 'left',
|
|
91
|
+
top: spec.hardwareButtons.ringToggle.top,
|
|
92
|
+
height: spec.hardwareButtons.ringToggle.height,
|
|
93
|
+
width: spec.hardwareButtons.width,
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
side: 'left',
|
|
97
|
+
top: spec.hardwareButtons.volumeUp.top,
|
|
98
|
+
height: spec.hardwareButtons.volumeUp.height,
|
|
99
|
+
width: spec.hardwareButtons.width,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
side: 'left',
|
|
103
|
+
top: spec.hardwareButtons.volumeDown.top,
|
|
104
|
+
height: spec.hardwareButtons.volumeDown.height,
|
|
105
|
+
width: spec.hardwareButtons.width,
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
model,
|
|
111
|
+
renderScale,
|
|
112
|
+
outerWidth: Math.round(logicalOuterWidth * renderScale),
|
|
113
|
+
outerHeight: Math.round(logicalOuterHeight * renderScale),
|
|
114
|
+
logicalOuterWidth,
|
|
115
|
+
logicalOuterHeight,
|
|
116
|
+
logicalFrameWidth,
|
|
117
|
+
logicalFrameHeight,
|
|
118
|
+
logicalFrameLeft,
|
|
119
|
+
logicalFrameTop,
|
|
120
|
+
logicalScreenWidth,
|
|
121
|
+
logicalScreenHeight,
|
|
122
|
+
logicalScreenLeft,
|
|
123
|
+
logicalScreenTop,
|
|
124
|
+
logicalScreenRadius: isLegacyHardware ? 0 : spec.cornerRadius,
|
|
125
|
+
logicalFrameRadius,
|
|
126
|
+
frameBackground: isLegacyHardware
|
|
127
|
+
? 'linear-gradient(180deg, #1b1c20 0%, #0f1012 42%, #050608 100%)'
|
|
128
|
+
: '#000000',
|
|
129
|
+
frameOutline: isLegacyHardware ? '0 0 0 1px #1f2125' : '0 0 0 1px #333',
|
|
130
|
+
metallicRingShadow: isLegacyHardware ? null : METALLIC_RING_SHADOW,
|
|
131
|
+
buttons,
|
|
132
|
+
legacyHomeButton: isLegacyHardware
|
|
133
|
+
? {
|
|
134
|
+
top:
|
|
135
|
+
LEGACY_TOP_BEZEL +
|
|
136
|
+
spec.height +
|
|
137
|
+
(LEGACY_BOTTOM_BEZEL - LEGACY_HOME_BUTTON_SIZE) / 2,
|
|
138
|
+
left: spec.width / 2 - LEGACY_HOME_BUTTON_SIZE / 2 + LEGACY_SIDE_BEZEL,
|
|
139
|
+
size: LEGACY_HOME_BUTTON_SIZE,
|
|
140
|
+
ring: LEGACY_HOME_BUTTON_RING,
|
|
141
|
+
}
|
|
142
|
+
: null,
|
|
143
|
+
showHomeIndicator: spec.homeIndicatorHeight > 0,
|
|
144
|
+
logicalHomeIndicatorStripHeight: spec.homeIndicatorHeight,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function renderSideButtonHtml(button: FrameButtonLayout): string {
|
|
149
|
+
const isRight = button.side === 'right'
|
|
150
|
+
const clip = isRight ? 'inset(-3px -3px -3px 0)' : 'inset(-3px 0 -3px -3px)'
|
|
151
|
+
return `
|
|
152
|
+
<div
|
|
153
|
+
aria-hidden="true"
|
|
154
|
+
style="
|
|
155
|
+
position:absolute;
|
|
156
|
+
${isRight ? `right:-${button.width}px;` : `left:-${button.width}px;`}
|
|
157
|
+
top:${button.top}px;
|
|
158
|
+
width:${button.width}px;
|
|
159
|
+
height:${button.height}px;
|
|
160
|
+
background-color:#212121;
|
|
161
|
+
border-top-left-radius:${isRight ? 0 : 1.5}px;
|
|
162
|
+
border-bottom-left-radius:${isRight ? 0 : 1.5}px;
|
|
163
|
+
border-top-right-radius:${isRight ? 1.5 : 0}px;
|
|
164
|
+
border-bottom-right-radius:${isRight ? 1.5 : 0}px;
|
|
165
|
+
box-shadow:
|
|
166
|
+
inset 0 0.5px 0 #616161,
|
|
167
|
+
inset 0 -0.5px 0 #3F3F3F,
|
|
168
|
+
${isRight ? 'inset -0.5px 0 0 #3F3F3F,' : 'inset 0.5px 0 0 #3F3F3F,'}
|
|
169
|
+
0 0 0 0.5px #676767,
|
|
170
|
+
0 0 0 1.5px #000;
|
|
171
|
+
clip-path:${clip};
|
|
172
|
+
pointer-events:none;
|
|
173
|
+
z-index:2;
|
|
174
|
+
"
|
|
175
|
+
></div>
|
|
176
|
+
`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderFrameHtml(
|
|
180
|
+
layout: FramedScreenshotLayout,
|
|
181
|
+
rawImageDataUrl: string,
|
|
182
|
+
): string {
|
|
183
|
+
const buttonsHtml = layout.buttons.map(renderSideButtonHtml).join('')
|
|
184
|
+
|
|
185
|
+
const ringHtml = layout.metallicRingShadow
|
|
186
|
+
? `
|
|
187
|
+
<div
|
|
188
|
+
aria-hidden="true"
|
|
189
|
+
style="
|
|
190
|
+
position:absolute;
|
|
191
|
+
inset:0;
|
|
192
|
+
border-radius:${layout.logicalFrameRadius}px;
|
|
193
|
+
box-shadow:${layout.metallicRingShadow};
|
|
194
|
+
pointer-events:none;
|
|
195
|
+
z-index:10;
|
|
196
|
+
"
|
|
197
|
+
></div>
|
|
198
|
+
`
|
|
199
|
+
: ''
|
|
200
|
+
|
|
201
|
+
const legacyHomeButtonHtml = layout.legacyHomeButton
|
|
202
|
+
? `
|
|
203
|
+
<div
|
|
204
|
+
aria-hidden="true"
|
|
205
|
+
style="
|
|
206
|
+
position:absolute;
|
|
207
|
+
top:${layout.legacyHomeButton.top}px;
|
|
208
|
+
left:${layout.legacyHomeButton.left}px;
|
|
209
|
+
width:${layout.legacyHomeButton.size}px;
|
|
210
|
+
height:${layout.legacyHomeButton.size}px;
|
|
211
|
+
border-radius:50%;
|
|
212
|
+
background:
|
|
213
|
+
radial-gradient(circle at 35% 32%, rgba(62,64,68,0.35) 0%, rgba(17,18,20,0.98) 72%, rgba(8,9,11,1) 100%);
|
|
214
|
+
box-shadow:
|
|
215
|
+
inset 0 0 0 1px rgba(255,255,255,0.06),
|
|
216
|
+
inset 0 0 0 ${layout.legacyHomeButton.ring}px rgba(184,192,204,0.45),
|
|
217
|
+
0 0 0 1px rgba(0,0,0,0.5);
|
|
218
|
+
z-index:3;
|
|
219
|
+
pointer-events:none;
|
|
220
|
+
display:flex;
|
|
221
|
+
align-items:center;
|
|
222
|
+
justify-content:center;
|
|
223
|
+
"
|
|
224
|
+
>
|
|
225
|
+
<div
|
|
226
|
+
style="
|
|
227
|
+
width:16px;
|
|
228
|
+
height:16px;
|
|
229
|
+
border-radius:4px;
|
|
230
|
+
box-shadow:inset 0 0 0 1.5px rgba(188,194,202,0.5);
|
|
231
|
+
"
|
|
232
|
+
></div>
|
|
233
|
+
</div>
|
|
234
|
+
`
|
|
235
|
+
: ''
|
|
236
|
+
|
|
237
|
+
const homeIndicatorHtml = layout.showHomeIndicator
|
|
238
|
+
? `
|
|
239
|
+
<div
|
|
240
|
+
aria-hidden="true"
|
|
241
|
+
style="
|
|
242
|
+
position:absolute;
|
|
243
|
+
left:0;
|
|
244
|
+
right:0;
|
|
245
|
+
bottom:4px;
|
|
246
|
+
height:${layout.logicalHomeIndicatorStripHeight}px;
|
|
247
|
+
display:flex;
|
|
248
|
+
align-items:center;
|
|
249
|
+
justify-content:center;
|
|
250
|
+
pointer-events:none;
|
|
251
|
+
z-index:10;
|
|
252
|
+
"
|
|
253
|
+
>
|
|
254
|
+
<div
|
|
255
|
+
style="
|
|
256
|
+
width:134px;
|
|
257
|
+
height:5px;
|
|
258
|
+
border-radius:2.5px;
|
|
259
|
+
background-color:rgb(150, 150, 150);
|
|
260
|
+
opacity:0.5;
|
|
261
|
+
"
|
|
262
|
+
></div>
|
|
263
|
+
</div>
|
|
264
|
+
`
|
|
265
|
+
: ''
|
|
266
|
+
|
|
267
|
+
return `<!doctype html>
|
|
268
|
+
<html>
|
|
269
|
+
<head>
|
|
270
|
+
<meta charset="utf-8" />
|
|
271
|
+
<style>
|
|
272
|
+
html, body {
|
|
273
|
+
margin: 0;
|
|
274
|
+
width: ${layout.outerWidth}px;
|
|
275
|
+
height: ${layout.outerHeight}px;
|
|
276
|
+
background: transparent;
|
|
277
|
+
}
|
|
278
|
+
body {
|
|
279
|
+
overflow: hidden;
|
|
280
|
+
}
|
|
281
|
+
img {
|
|
282
|
+
display: block;
|
|
283
|
+
width: 100%;
|
|
284
|
+
height: 100%;
|
|
285
|
+
}
|
|
286
|
+
</style>
|
|
287
|
+
</head>
|
|
288
|
+
<body>
|
|
289
|
+
<div
|
|
290
|
+
id="frame-export-root"
|
|
291
|
+
style="
|
|
292
|
+
position:relative;
|
|
293
|
+
width:${layout.outerWidth}px;
|
|
294
|
+
height:${layout.outerHeight}px;
|
|
295
|
+
overflow:hidden;
|
|
296
|
+
background:transparent;
|
|
297
|
+
"
|
|
298
|
+
>
|
|
299
|
+
<div
|
|
300
|
+
id="frame-scale-layer"
|
|
301
|
+
style="
|
|
302
|
+
position:absolute;
|
|
303
|
+
top:0;
|
|
304
|
+
left:0;
|
|
305
|
+
width:${layout.logicalOuterWidth}px;
|
|
306
|
+
height:${layout.logicalOuterHeight}px;
|
|
307
|
+
transform:scale(${layout.renderScale});
|
|
308
|
+
transform-origin:top left;
|
|
309
|
+
overflow:visible;
|
|
310
|
+
"
|
|
311
|
+
>
|
|
312
|
+
<div
|
|
313
|
+
id="frame"
|
|
314
|
+
style="
|
|
315
|
+
position:absolute;
|
|
316
|
+
top:${layout.logicalFrameTop}px;
|
|
317
|
+
left:${layout.logicalFrameLeft}px;
|
|
318
|
+
width:${layout.logicalFrameWidth}px;
|
|
319
|
+
height:${layout.logicalFrameHeight}px;
|
|
320
|
+
border-radius:${layout.logicalFrameRadius}px;
|
|
321
|
+
box-sizing:border-box;
|
|
322
|
+
overflow:visible;
|
|
323
|
+
background:${layout.frameBackground};
|
|
324
|
+
box-shadow:${layout.frameOutline};
|
|
325
|
+
"
|
|
326
|
+
>
|
|
327
|
+
${buttonsHtml}
|
|
328
|
+
${ringHtml}
|
|
329
|
+
${legacyHomeButtonHtml}
|
|
330
|
+
<div
|
|
331
|
+
id="screen"
|
|
332
|
+
style="
|
|
333
|
+
position:absolute;
|
|
334
|
+
top:${layout.logicalScreenTop}px;
|
|
335
|
+
left:${layout.logicalScreenLeft - layout.logicalFrameLeft}px;
|
|
336
|
+
width:${layout.logicalScreenWidth}px;
|
|
337
|
+
height:${layout.logicalScreenHeight}px;
|
|
338
|
+
border-radius:${layout.logicalScreenRadius}px;
|
|
339
|
+
overflow:hidden;
|
|
340
|
+
background:#000;
|
|
341
|
+
"
|
|
342
|
+
>
|
|
343
|
+
<img src="${rawImageDataUrl}" alt="" />
|
|
344
|
+
${homeIndicatorHtml}
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
</body>
|
|
350
|
+
</html>`
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export async function composeFramedScreenshot(
|
|
354
|
+
rawPng: Buffer,
|
|
355
|
+
model: DeviceModel,
|
|
356
|
+
): Promise<Buffer> {
|
|
357
|
+
const { chromium } = await import('playwright')
|
|
358
|
+
const browser = await chromium.launch({ headless: true })
|
|
359
|
+
try {
|
|
360
|
+
const composer = await createFramedScreenshotComposer(browser)
|
|
361
|
+
try {
|
|
362
|
+
return await composer.compose(rawPng, model)
|
|
363
|
+
} finally {
|
|
364
|
+
await composer.close()
|
|
365
|
+
}
|
|
366
|
+
} finally {
|
|
367
|
+
await browser.close()
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
type BrowserLike = {
|
|
372
|
+
newContext: (opts: {
|
|
373
|
+
viewport: { width: number; height: number }
|
|
374
|
+
deviceScaleFactor: number
|
|
375
|
+
}) => Promise<{
|
|
376
|
+
newPage: () => Promise<{
|
|
377
|
+
setContent: (html: string, opts: { waitUntil: 'load' }) => Promise<void>
|
|
378
|
+
waitForTimeout: (ms: number) => Promise<void>
|
|
379
|
+
screenshot: (opts: {
|
|
380
|
+
type: 'png'
|
|
381
|
+
clip: { x: number; y: number; width: number; height: number }
|
|
382
|
+
omitBackground: true
|
|
383
|
+
}) => Promise<Buffer>
|
|
384
|
+
}>
|
|
385
|
+
close: () => Promise<void>
|
|
386
|
+
}>
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
type CachedFramePage = {
|
|
390
|
+
layout: FramedScreenshotLayout
|
|
391
|
+
context: Awaited<ReturnType<BrowserLike['newContext']>>
|
|
392
|
+
page: Awaited<ReturnType<Awaited<ReturnType<BrowserLike['newContext']>>['newPage']>>
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function createFramedScreenshotComposer(browser: BrowserLike) {
|
|
396
|
+
const cache = new Map<DeviceModel, CachedFramePage>()
|
|
397
|
+
|
|
398
|
+
async function getCachedPage(model: DeviceModel): Promise<CachedFramePage> {
|
|
399
|
+
const existing = cache.get(model)
|
|
400
|
+
if (existing) return existing
|
|
401
|
+
const layout = getFramedScreenshotLayout(model)
|
|
402
|
+
const context = await browser.newContext({
|
|
403
|
+
viewport: { width: layout.outerWidth, height: layout.outerHeight },
|
|
404
|
+
deviceScaleFactor: 1,
|
|
405
|
+
})
|
|
406
|
+
const page = await context.newPage()
|
|
407
|
+
const created = { layout, context, page }
|
|
408
|
+
cache.set(model, created)
|
|
409
|
+
return created
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
async compose(rawPng: Buffer, model: DeviceModel): Promise<Buffer> {
|
|
414
|
+
const cached = await getCachedPage(model)
|
|
415
|
+
const rawImageDataUrl = `data:image/png;base64,${rawPng.toString('base64')}`
|
|
416
|
+
await cached.page.setContent(renderFrameHtml(cached.layout, rawImageDataUrl), {
|
|
417
|
+
waitUntil: 'load',
|
|
418
|
+
})
|
|
419
|
+
await cached.page.waitForTimeout(20)
|
|
420
|
+
return (await cached.page.screenshot({
|
|
421
|
+
type: 'png',
|
|
422
|
+
clip: {
|
|
423
|
+
x: 0,
|
|
424
|
+
y: 0,
|
|
425
|
+
width: cached.layout.outerWidth,
|
|
426
|
+
height: cached.layout.outerHeight,
|
|
427
|
+
},
|
|
428
|
+
omitBackground: true,
|
|
429
|
+
})) as Buffer
|
|
430
|
+
},
|
|
431
|
+
async close(): Promise<void> {
|
|
432
|
+
for (const entry of cache.values()) {
|
|
433
|
+
await entry.context.close()
|
|
434
|
+
}
|
|
435
|
+
cache.clear()
|
|
436
|
+
},
|
|
437
|
+
}
|
|
438
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
2
|
+
import { tmpdir } from 'os'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { runFlowPlayback } from '../../cli/commands/flow'
|
|
5
|
+
import {
|
|
6
|
+
composeMarketingScreenshots,
|
|
7
|
+
type MarketingBrowserLike,
|
|
8
|
+
type ComposeResult,
|
|
9
|
+
type ComposeSlideSource,
|
|
10
|
+
} from './compose'
|
|
11
|
+
import { createFramedScreenshotComposer } from './frame-compose'
|
|
12
|
+
import { loadScreenshotsPlan, type NormalizedScreenshotsPlan } from './schema'
|
|
13
|
+
|
|
14
|
+
export interface RunScreenshotsOverrides {
|
|
15
|
+
appTarget?: string | null
|
|
16
|
+
deviceModel?: string | null
|
|
17
|
+
simId?: string | null
|
|
18
|
+
captureOnly?: boolean
|
|
19
|
+
composeOnly?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CaptureResult {
|
|
23
|
+
manifestPath: string
|
|
24
|
+
rawDir: string
|
|
25
|
+
framedDir: string
|
|
26
|
+
rawFiles: string[]
|
|
27
|
+
framedFiles: string[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ScreenshotsRunResult {
|
|
31
|
+
plan: NormalizedScreenshotsPlan
|
|
32
|
+
capture: CaptureResult
|
|
33
|
+
compose: ComposeResult | null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveCapturePathMode(plan: NormalizedScreenshotsPlan): 'plan' | 'flow' {
|
|
37
|
+
if (plan.capture.pathMode === 'plan' || plan.capture.pathMode === 'flow') {
|
|
38
|
+
return plan.capture.pathMode
|
|
39
|
+
}
|
|
40
|
+
return plan.capture.fromDir ? 'flow' : 'plan'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildCaptureFlowArgs(
|
|
44
|
+
plan: NormalizedScreenshotsPlan,
|
|
45
|
+
overrides: RunScreenshotsOverrides,
|
|
46
|
+
): string[] {
|
|
47
|
+
if (!plan.capture.flowPath) {
|
|
48
|
+
throw new Error('capture flow path is required to build flow args')
|
|
49
|
+
}
|
|
50
|
+
const args = [plan.capture.flowPath]
|
|
51
|
+
const pathMode = resolveCapturePathMode(plan)
|
|
52
|
+
args.push('--screenshots', plan.capture.rawDir)
|
|
53
|
+
if (pathMode === 'flow') {
|
|
54
|
+
args.push('--screenshot-paths', 'flow')
|
|
55
|
+
}
|
|
56
|
+
const appTarget = overrides.appTarget ?? plan.appTarget
|
|
57
|
+
const deviceModel = overrides.deviceModel ?? plan.deviceModel
|
|
58
|
+
const simId = overrides.simId ?? plan.capture.simId
|
|
59
|
+
if (appTarget) args.push('--url', appTarget)
|
|
60
|
+
if (deviceModel) args.push('--device', deviceModel)
|
|
61
|
+
if (simId) args.push('--sim', simId)
|
|
62
|
+
if (plan.capture.openInNewSim) args.push('--new')
|
|
63
|
+
return args
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function resolveSlideRawPath(
|
|
67
|
+
plan: NormalizedScreenshotsPlan,
|
|
68
|
+
screenshot: string,
|
|
69
|
+
): string {
|
|
70
|
+
return path.isAbsolute(screenshot)
|
|
71
|
+
? screenshot
|
|
72
|
+
: path.join(plan.capture.rawDir, screenshot)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveCaptureManifestPath(plan: NormalizedScreenshotsPlan): string {
|
|
76
|
+
return path.join(plan.capture.outDir, 'manifest.json')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function runCaptureStage(
|
|
80
|
+
plan: NormalizedScreenshotsPlan,
|
|
81
|
+
overrides: RunScreenshotsOverrides,
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
if (!plan.capture.flowPath) return
|
|
84
|
+
mkdirSync(plan.capture.rawDir, { recursive: true })
|
|
85
|
+
const args = buildCaptureFlowArgs(plan, overrides)
|
|
86
|
+
const exitCode = await runFlowPlayback(args)
|
|
87
|
+
if (exitCode !== 0) {
|
|
88
|
+
throw new Error(`flow capture failed with exit code ${exitCode}`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function buildFramedSources(
|
|
93
|
+
plan: NormalizedScreenshotsPlan,
|
|
94
|
+
browserOverride?: MarketingBrowserLike,
|
|
95
|
+
): Promise<Map<string, string>> {
|
|
96
|
+
const sources = new Map<string, string>()
|
|
97
|
+
mkdirSync(plan.capture.framedDir, { recursive: true })
|
|
98
|
+
const browser =
|
|
99
|
+
browserOverride ??
|
|
100
|
+
(await (async () => {
|
|
101
|
+
const { chromium } = await import('playwright')
|
|
102
|
+
return chromium.launch({ headless: true })
|
|
103
|
+
})())
|
|
104
|
+
try {
|
|
105
|
+
const composer = await createFramedScreenshotComposer(browser)
|
|
106
|
+
try {
|
|
107
|
+
for (const slide of plan.compose.slides) {
|
|
108
|
+
const rawPath = resolveSlideRawPath(plan, slide.screenshot)
|
|
109
|
+
const rawBuffer = readFileSync(rawPath)
|
|
110
|
+
const framedBuffer = await composer.compose(rawBuffer, plan.compose.frame.style)
|
|
111
|
+
const framedPath = path.join(plan.capture.framedDir, `${slide.assetKey}.png`)
|
|
112
|
+
writeFileSync(framedPath, framedBuffer)
|
|
113
|
+
sources.set(slide.id, framedPath)
|
|
114
|
+
}
|
|
115
|
+
} finally {
|
|
116
|
+
await composer.close()
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
if (!browserOverride && typeof browser.close === 'function') {
|
|
120
|
+
await browser.close()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return sources
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function writeCaptureManifest(
|
|
127
|
+
plan: NormalizedScreenshotsPlan,
|
|
128
|
+
framedSources: Map<string, string>,
|
|
129
|
+
effectiveDeviceModel: string | null,
|
|
130
|
+
): CaptureResult {
|
|
131
|
+
const rawFiles: string[] = []
|
|
132
|
+
const framedFiles: string[] = []
|
|
133
|
+
mkdirSync(plan.capture.outDir, { recursive: true })
|
|
134
|
+
const manifestPath = resolveCaptureManifestPath(plan)
|
|
135
|
+
const slides = plan.compose.slides.map((slide) => {
|
|
136
|
+
const rawPath = resolveSlideRawPath(plan, slide.screenshot)
|
|
137
|
+
const framedPath = framedSources.get(slide.id) ?? null
|
|
138
|
+
rawFiles.push(rawPath)
|
|
139
|
+
if (framedPath) framedFiles.push(framedPath)
|
|
140
|
+
return {
|
|
141
|
+
id: slide.id,
|
|
142
|
+
screenshot: slide.screenshot,
|
|
143
|
+
rawPath,
|
|
144
|
+
framedPath,
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
writeFileSync(
|
|
148
|
+
manifestPath,
|
|
149
|
+
JSON.stringify(
|
|
150
|
+
{
|
|
151
|
+
generatedAt: new Date().toISOString(),
|
|
152
|
+
deviceModel: effectiveDeviceModel,
|
|
153
|
+
mode: plan.capture.mode,
|
|
154
|
+
rawDir: plan.capture.rawDir,
|
|
155
|
+
framedDir: plan.capture.framedDir,
|
|
156
|
+
slides,
|
|
157
|
+
},
|
|
158
|
+
null,
|
|
159
|
+
2,
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
return {
|
|
163
|
+
manifestPath,
|
|
164
|
+
rawDir: plan.capture.rawDir,
|
|
165
|
+
framedDir: plan.capture.framedDir,
|
|
166
|
+
rawFiles,
|
|
167
|
+
framedFiles,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resolveComposeSources(
|
|
172
|
+
plan: NormalizedScreenshotsPlan,
|
|
173
|
+
framedSources: Map<string, string>,
|
|
174
|
+
): ComposeSlideSource[] {
|
|
175
|
+
return plan.compose.slides.map((slide) => ({
|
|
176
|
+
slide,
|
|
177
|
+
imagePath:
|
|
178
|
+
plan.compose.frame.show === true
|
|
179
|
+
? (framedSources.get(slide.id) ?? resolveSlideRawPath(plan, slide.screenshot))
|
|
180
|
+
: resolveSlideRawPath(plan, slide.screenshot),
|
|
181
|
+
}))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export async function runScreenshotsPlan(
|
|
185
|
+
planPath: string,
|
|
186
|
+
overrides: RunScreenshotsOverrides = {},
|
|
187
|
+
): Promise<ScreenshotsRunResult> {
|
|
188
|
+
const plan = loadScreenshotsPlan(planPath)
|
|
189
|
+
const effectiveDeviceModel = overrides.deviceModel ?? plan.deviceModel
|
|
190
|
+
if (!overrides.composeOnly) {
|
|
191
|
+
await runCaptureStage(plan, overrides)
|
|
192
|
+
}
|
|
193
|
+
const needsFramedSources =
|
|
194
|
+
plan.capture.mode !== 'raw' ||
|
|
195
|
+
(!overrides.captureOnly && plan.compose.frame.show === true)
|
|
196
|
+
const needsSharedBrowser = needsFramedSources || !overrides.captureOnly
|
|
197
|
+
const sharedBrowser = needsSharedBrowser
|
|
198
|
+
? await (async () => {
|
|
199
|
+
const { chromium } = await import('playwright')
|
|
200
|
+
return chromium.launch({ headless: true })
|
|
201
|
+
})()
|
|
202
|
+
: null
|
|
203
|
+
try {
|
|
204
|
+
const framedSources = needsFramedSources
|
|
205
|
+
? await buildFramedSources(plan, sharedBrowser ?? undefined)
|
|
206
|
+
: new Map()
|
|
207
|
+
const capture = writeCaptureManifest(plan, framedSources, effectiveDeviceModel)
|
|
208
|
+
|
|
209
|
+
if (overrides.captureOnly) {
|
|
210
|
+
return { plan, capture, compose: null }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const composeSources = resolveComposeSources(plan, framedSources)
|
|
214
|
+
const compose = await composeMarketingScreenshots(
|
|
215
|
+
plan.compose,
|
|
216
|
+
composeSources,
|
|
217
|
+
sharedBrowser ?? undefined,
|
|
218
|
+
)
|
|
219
|
+
return { plan, capture, compose }
|
|
220
|
+
} finally {
|
|
221
|
+
if (sharedBrowser && typeof sharedBrowser.close === 'function') {
|
|
222
|
+
await sharedBrowser.close()
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function runScreenshotsPlanFromExistingRaw(
|
|
228
|
+
planPath: string,
|
|
229
|
+
rawDir: string,
|
|
230
|
+
): Promise<ScreenshotsRunResult> {
|
|
231
|
+
const plan = loadScreenshotsPlan(planPath)
|
|
232
|
+
plan.capture.rawDir = rawDir
|
|
233
|
+
plan.capture.flowPath = null
|
|
234
|
+
plan.capture.fromDir = rawDir
|
|
235
|
+
const framedDir = path.join(tmpdir(), 'sootsim-screenshots-framed')
|
|
236
|
+
plan.capture.framedDir = framedDir
|
|
237
|
+
const framedSources = await buildFramedSources(plan)
|
|
238
|
+
const capture = writeCaptureManifest(plan, framedSources, plan.deviceModel)
|
|
239
|
+
const compose = await composeMarketingScreenshots(
|
|
240
|
+
plan.compose,
|
|
241
|
+
resolveComposeSources(plan, framedSources),
|
|
242
|
+
)
|
|
243
|
+
return { plan, capture, compose }
|
|
244
|
+
}
|