everyscreen 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +241 -2
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +2958 -0
- package/dist/commands-logic-DVWfSe3O.mjs +217 -0
- package/dist/dashboard/assets/button-N-Nucq_K.js +1 -0
- package/dist/dashboard/assets/cn-DMYzmXtt.js +1 -0
- package/dist/dashboard/assets/device-status-logic-Silf3YXA.js +1 -0
- package/dist/dashboard/assets/devices-5pEUg6rA.js +1 -0
- package/dist/dashboard/assets/dist-CnPFbxes.js +1 -0
- package/dist/dashboard/assets/focus._deviceId-DGel-kWw.js +1 -0
- package/dist/dashboard/assets/frame-layout-logic-H_x71NvW.js +1 -0
- package/dist/dashboard/assets/index-DNHZ-i1B.css +2 -0
- package/dist/dashboard/assets/index-DwPQ0eY_.js +10 -0
- package/dist/dashboard/assets/modal-dialog-BaO9xkRr.js +1 -0
- package/dist/dashboard/assets/rolldown-runtime-WkwZ5CU-.js +1 -0
- package/dist/dashboard/assets/routes-CUFa9tL0.js +1 -0
- package/dist/dashboard/assets/run-origin-badge-BeFqg0AC.js +1 -0
- package/dist/dashboard/assets/runs-queries-BYBE7R9S.js +2 -0
- package/dist/dashboard/assets/runs._runId-CokpJ1Rr.js +1 -0
- package/dist/dashboard/assets/runs.index-ZsynzkKU.js +1 -0
- package/dist/dashboard/assets/schemas-BPFqmBjG.js +55 -0
- package/dist/dashboard/assets/screen-boundary-BauyD9pu.js +1 -0
- package/dist/dashboard/assets/status-chip-yqTTTuyZ.js +1 -0
- package/dist/dashboard/assets/tile-status-chip-DisgbNnH.js +1 -0
- package/dist/dashboard/assets/toggle-nciug_di.js +1 -0
- package/dist/dashboard/assets/use-run-totals-B3G4yamQ.js +1 -0
- package/dist/dashboard/assets/useNavigate-8mkM-ffY.js +1 -0
- package/dist/dashboard/assets/useParams-D2lK5BEg.js +1 -0
- package/dist/dashboard/index.html +29 -0
- package/dist/devices-store-edScJDie.mjs +1228 -0
- package/dist/playwright-logic-QFlBFspE.d.mts +138 -0
- package/dist/playwright.d.mts +2 -0
- package/dist/playwright.mjs +18 -0
- package/dist/read-hub-file-N4rZ-6Wr.mjs +180 -0
- package/dist/report-hub-trouble-BjbcuK21.d.mts +2 -0
- package/dist/report-hub-trouble-CaUiyteI.mjs +41 -0
- package/dist/reporter.d.mts +47 -0
- package/dist/reporter.mjs +250 -0
- package/dist/run-one-at-a-time-BeV_a3Fm.mjs +48 -0
- package/dist/runs-store-Bd_ITPFj.mjs +283 -0
- package/dist/test.d.mts +11 -0
- package/dist/test.mjs +69 -0
- package/package.json +106 -4
- package/src/dashboard/ui/theme.css +28 -0
- package/src/skill/SKILL.md +15 -0
|
@@ -0,0 +1,1228 @@
|
|
|
1
|
+
import { _ as DEVICES_FILE_PATH, g as CPU_TIERS } from "./commands-logic-DVWfSe3O.mjs";
|
|
2
|
+
import { i as writeStoredFile, r as readStoredFile, t as runOneAtATime } from "./run-one-at-a-time-BeV_a3Fm.mjs";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { devices } from "playwright";
|
|
7
|
+
import { parse, stringify } from "yaml";
|
|
8
|
+
// src/devices/default-grid-ids.ts
|
|
9
|
+
const DEFAULT_GRID_IDS = [
|
|
10
|
+
"iphone-se",
|
|
11
|
+
"iphone-17",
|
|
12
|
+
"iphone-17-pro-max",
|
|
13
|
+
"galaxy-s25",
|
|
14
|
+
"pixel-10",
|
|
15
|
+
"xiaomi-15",
|
|
16
|
+
"ipad-mini",
|
|
17
|
+
"ipad-pro-11",
|
|
18
|
+
"galaxy-tab-s9",
|
|
19
|
+
"macbook-air-13",
|
|
20
|
+
"windows-laptop-1366",
|
|
21
|
+
"linux-1920"
|
|
22
|
+
];
|
|
23
|
+
// src/devices/device-schema.ts
|
|
24
|
+
const BROWSER_NAMES = [
|
|
25
|
+
"chromium",
|
|
26
|
+
"webkit",
|
|
27
|
+
"firefox"
|
|
28
|
+
];
|
|
29
|
+
const FRAME_SHAPES = [
|
|
30
|
+
"home-button",
|
|
31
|
+
"dynamic-island",
|
|
32
|
+
"punch-hole",
|
|
33
|
+
"tablet",
|
|
34
|
+
"laptop",
|
|
35
|
+
"kiosk"
|
|
36
|
+
];
|
|
37
|
+
const VIEWPORT_MODES = ["full-screen", "browser"];
|
|
38
|
+
const DEVICE_NAME_PATTERN = /[\p{L}\p{N}]/u;
|
|
39
|
+
const MAX_DEVICE_NAME_LENGTH = 40;
|
|
40
|
+
const MIN_DEVICE_SIZE = 240;
|
|
41
|
+
const MAX_DEVICE_SIZE = 3840;
|
|
42
|
+
const MIN_PIXEL_RATIO = 1;
|
|
43
|
+
const MAX_PIXEL_RATIO = 4;
|
|
44
|
+
const DEFAULT_PIXEL_RATIO = 1;
|
|
45
|
+
const isOriginRelativeRoute = (route) => route.startsWith("/") && !route.startsWith("//");
|
|
46
|
+
const createSizeSchema = (flagName) => {
|
|
47
|
+
const sizeMessage = `--${flagName} must be a whole number from ${MIN_DEVICE_SIZE} to ${MAX_DEVICE_SIZE}`;
|
|
48
|
+
return z.number({ error: sizeMessage }).int({ error: sizeMessage }).min(MIN_DEVICE_SIZE, { error: sizeMessage }).max(MAX_DEVICE_SIZE, { error: sizeMessage });
|
|
49
|
+
};
|
|
50
|
+
const PIXEL_RATIO_MESSAGE = `--pixel-ratio must be a number from ${MIN_PIXEL_RATIO} to ${MAX_PIXEL_RATIO}`;
|
|
51
|
+
const customDeviceSchema = z.object({
|
|
52
|
+
name: z.string({ error: "needs --name <device name>" }).max(MAX_DEVICE_NAME_LENGTH, { error: `--name must be ${MAX_DEVICE_NAME_LENGTH} characters or less` }).regex(DEVICE_NAME_PATTERN, "--name needs a letter or digit"),
|
|
53
|
+
width: createSizeSchema("width"),
|
|
54
|
+
height: createSizeSchema("height"),
|
|
55
|
+
pixelRatio: z.number({ error: PIXEL_RATIO_MESSAGE }).min(MIN_PIXEL_RATIO, { error: PIXEL_RATIO_MESSAGE }).max(MAX_PIXEL_RATIO, { error: PIXEL_RATIO_MESSAGE }).default(DEFAULT_PIXEL_RATIO),
|
|
56
|
+
hasTouch: z.boolean().default(false),
|
|
57
|
+
frameShape: z.enum(FRAME_SHAPES).default("kiosk"),
|
|
58
|
+
browserName: z.enum(BROWSER_NAMES).default("chromium")
|
|
59
|
+
});
|
|
60
|
+
const devicesFileSchema = z.object({
|
|
61
|
+
devices: z.array(customDeviceSchema).default([]),
|
|
62
|
+
routes: z.array(z.string().refine(isOriginRelativeRoute, "Use a path that starts with a single /")).default([]),
|
|
63
|
+
grid: z.array(z.string()).default(DEFAULT_GRID_IDS),
|
|
64
|
+
viewportModes: z.record(z.string(), z.enum(VIEWPORT_MODES)).default({})
|
|
65
|
+
});
|
|
66
|
+
const gridUpdateSchema = z.object({ deviceIds: z.array(z.string().min(1)) });
|
|
67
|
+
// src/devices/desktop-user-agent-logic.ts
|
|
68
|
+
const REGISTRY_PLATFORM_PART = /\(Windows NT [^)]+\)/;
|
|
69
|
+
const PLATFORM_PARTS = {
|
|
70
|
+
macos: "(Macintosh; Intel Mac OS X 10_15_7)",
|
|
71
|
+
linux: "(X11; Linux x86_64)"
|
|
72
|
+
};
|
|
73
|
+
const createDesktopUserAgent = (desktopPlatform, registryUserAgent) => {
|
|
74
|
+
if (desktopPlatform === "windows") return registryUserAgent;
|
|
75
|
+
return registryUserAgent.replace(REGISTRY_PLATFORM_PART, PLATFORM_PARTS[desktopPlatform]);
|
|
76
|
+
};
|
|
77
|
+
// src/devices/frame-shape-insets.ts
|
|
78
|
+
const NO_INSETS = {
|
|
79
|
+
top: 0,
|
|
80
|
+
bottom: 0,
|
|
81
|
+
left: 0,
|
|
82
|
+
right: 0
|
|
83
|
+
};
|
|
84
|
+
const FRAME_SHAPE_INSETS = {
|
|
85
|
+
"home-button": NO_INSETS,
|
|
86
|
+
"dynamic-island": {
|
|
87
|
+
top: 59,
|
|
88
|
+
bottom: 34,
|
|
89
|
+
left: 0,
|
|
90
|
+
right: 0
|
|
91
|
+
},
|
|
92
|
+
"punch-hole": {
|
|
93
|
+
top: 24,
|
|
94
|
+
bottom: 0,
|
|
95
|
+
left: 0,
|
|
96
|
+
right: 0
|
|
97
|
+
},
|
|
98
|
+
tablet: NO_INSETS,
|
|
99
|
+
laptop: NO_INSETS,
|
|
100
|
+
kiosk: NO_INSETS
|
|
101
|
+
};
|
|
102
|
+
// src/devices/own-devices.ts
|
|
103
|
+
const ANDROID_USER_AGENT = devices["Pixel 10"].userAgent.replace(/Android [\d.]+; [^)]+\)/, "Android 10; K)");
|
|
104
|
+
const IPAD_USER_AGENT = devices["iPad Pro 11"].userAgent;
|
|
105
|
+
const REGISTRY_DESKTOP_USER_AGENT = devices["Desktop Chrome"].userAgent;
|
|
106
|
+
const UNMEASURED_CUTOUT = {
|
|
107
|
+
kind: "punch-hole-center",
|
|
108
|
+
width: null,
|
|
109
|
+
height: null,
|
|
110
|
+
top: null,
|
|
111
|
+
left: null
|
|
112
|
+
};
|
|
113
|
+
const NO_CUTOUT$1 = {
|
|
114
|
+
...UNMEASURED_CUTOUT,
|
|
115
|
+
kind: "none"
|
|
116
|
+
};
|
|
117
|
+
const NOTCH$1 = {
|
|
118
|
+
...UNMEASURED_CUTOUT,
|
|
119
|
+
kind: "notch"
|
|
120
|
+
};
|
|
121
|
+
const ANDROID_BASE$1 = {
|
|
122
|
+
registryName: null,
|
|
123
|
+
browserName: "chromium",
|
|
124
|
+
frameShape: "punch-hole",
|
|
125
|
+
cpuSlowdown: CPU_TIERS.phone,
|
|
126
|
+
platform: "android",
|
|
127
|
+
maker: "samsung",
|
|
128
|
+
userAgent: ANDROID_USER_AGENT,
|
|
129
|
+
isMobile: true,
|
|
130
|
+
hasTouch: true
|
|
131
|
+
};
|
|
132
|
+
const IPAD_BASE = {
|
|
133
|
+
registryName: null,
|
|
134
|
+
browserName: "webkit",
|
|
135
|
+
frameShape: "tablet",
|
|
136
|
+
cpuSlowdown: CPU_TIERS.tablet,
|
|
137
|
+
platform: "ipados",
|
|
138
|
+
maker: "apple",
|
|
139
|
+
userAgent: IPAD_USER_AGENT,
|
|
140
|
+
isMobile: true,
|
|
141
|
+
hasTouch: true
|
|
142
|
+
};
|
|
143
|
+
const MAC_BASE = {
|
|
144
|
+
registryName: null,
|
|
145
|
+
browserName: "chromium",
|
|
146
|
+
frameShape: "laptop",
|
|
147
|
+
cpuSlowdown: CPU_TIERS.desktop,
|
|
148
|
+
platform: "macos",
|
|
149
|
+
maker: "apple",
|
|
150
|
+
userAgent: createDesktopUserAgent("macos", REGISTRY_DESKTOP_USER_AGENT),
|
|
151
|
+
isMobile: false,
|
|
152
|
+
hasTouch: false
|
|
153
|
+
};
|
|
154
|
+
const WINDOWS_BASE = {
|
|
155
|
+
...MAC_BASE,
|
|
156
|
+
platform: "windows",
|
|
157
|
+
maker: "generic",
|
|
158
|
+
userAgent: createDesktopUserAgent("windows", REGISTRY_DESKTOP_USER_AGENT)
|
|
159
|
+
};
|
|
160
|
+
const LINUX_BASE = {
|
|
161
|
+
...MAC_BASE,
|
|
162
|
+
platform: "linux",
|
|
163
|
+
maker: "generic",
|
|
164
|
+
userAgent: createDesktopUserAgent("linux", REGISTRY_DESKTOP_USER_AGENT)
|
|
165
|
+
};
|
|
166
|
+
const createOwnDevice = (base, spec) => ({
|
|
167
|
+
...base,
|
|
168
|
+
id: spec.id,
|
|
169
|
+
name: spec.name,
|
|
170
|
+
fullScreen: spec.size,
|
|
171
|
+
browserViewport: null,
|
|
172
|
+
deviceScaleFactor: spec.deviceScaleFactor,
|
|
173
|
+
cornerRadius: spec.cornerRadius,
|
|
174
|
+
cutout: spec.cutout,
|
|
175
|
+
safeAreaInsets: spec.safeAreaInsets
|
|
176
|
+
});
|
|
177
|
+
const createTabletSpec = (identity, size) => ({
|
|
178
|
+
...identity,
|
|
179
|
+
size,
|
|
180
|
+
deviceScaleFactor: 2,
|
|
181
|
+
cutout: NO_CUTOUT$1,
|
|
182
|
+
safeAreaInsets: FRAME_SHAPE_INSETS.tablet
|
|
183
|
+
});
|
|
184
|
+
const createPhoneSpec = (identity, screen) => ({
|
|
185
|
+
...identity,
|
|
186
|
+
size: screen.size,
|
|
187
|
+
deviceScaleFactor: screen.deviceScaleFactor,
|
|
188
|
+
cornerRadius: null,
|
|
189
|
+
cutout: UNMEASURED_CUTOUT,
|
|
190
|
+
safeAreaInsets: FRAME_SHAPE_INSETS["punch-hole"]
|
|
191
|
+
});
|
|
192
|
+
const createDesktopSpec = (identity, size) => ({
|
|
193
|
+
...identity,
|
|
194
|
+
size,
|
|
195
|
+
deviceScaleFactor: 1,
|
|
196
|
+
cornerRadius: 0,
|
|
197
|
+
cutout: NO_CUTOUT$1,
|
|
198
|
+
safeAreaInsets: FRAME_SHAPE_INSETS.laptop
|
|
199
|
+
});
|
|
200
|
+
const IPAD_DEVICES = [
|
|
201
|
+
createOwnDevice(IPAD_BASE, createTabletSpec({
|
|
202
|
+
id: "ipad-mini",
|
|
203
|
+
name: "iPad mini",
|
|
204
|
+
cornerRadius: null
|
|
205
|
+
}, {
|
|
206
|
+
width: 744,
|
|
207
|
+
height: 1133
|
|
208
|
+
})),
|
|
209
|
+
createOwnDevice(IPAD_BASE, createTabletSpec({
|
|
210
|
+
id: "ipad-air-11",
|
|
211
|
+
name: "iPad Air 11",
|
|
212
|
+
cornerRadius: 18
|
|
213
|
+
}, {
|
|
214
|
+
width: 820,
|
|
215
|
+
height: 1180
|
|
216
|
+
})),
|
|
217
|
+
createOwnDevice(IPAD_BASE, createTabletSpec({
|
|
218
|
+
id: "ipad-air-13",
|
|
219
|
+
name: "iPad Air 13",
|
|
220
|
+
cornerRadius: 18
|
|
221
|
+
}, {
|
|
222
|
+
width: 1024,
|
|
223
|
+
height: 1366
|
|
224
|
+
})),
|
|
225
|
+
createOwnDevice(IPAD_BASE, createTabletSpec({
|
|
226
|
+
id: "ipad-pro-13",
|
|
227
|
+
name: "iPad Pro 13",
|
|
228
|
+
cornerRadius: null
|
|
229
|
+
}, {
|
|
230
|
+
width: 1032,
|
|
231
|
+
height: 1376
|
|
232
|
+
}))
|
|
233
|
+
];
|
|
234
|
+
const SAMSUNG_DEVICES = [
|
|
235
|
+
createOwnDevice(ANDROID_BASE$1, createPhoneSpec({
|
|
236
|
+
id: "galaxy-s25",
|
|
237
|
+
name: "Galaxy S25"
|
|
238
|
+
}, {
|
|
239
|
+
size: {
|
|
240
|
+
width: 360,
|
|
241
|
+
height: 780
|
|
242
|
+
},
|
|
243
|
+
deviceScaleFactor: 3
|
|
244
|
+
})),
|
|
245
|
+
createOwnDevice(ANDROID_BASE$1, createPhoneSpec({
|
|
246
|
+
id: "galaxy-s25-plus",
|
|
247
|
+
name: "Galaxy S25+"
|
|
248
|
+
}, {
|
|
249
|
+
size: {
|
|
250
|
+
width: 412,
|
|
251
|
+
height: 891
|
|
252
|
+
},
|
|
253
|
+
deviceScaleFactor: 3.5
|
|
254
|
+
})),
|
|
255
|
+
createOwnDevice(ANDROID_BASE$1, createPhoneSpec({
|
|
256
|
+
id: "galaxy-s25-ultra",
|
|
257
|
+
name: "Galaxy S25 Ultra"
|
|
258
|
+
}, {
|
|
259
|
+
size: {
|
|
260
|
+
width: 412,
|
|
261
|
+
height: 891
|
|
262
|
+
},
|
|
263
|
+
deviceScaleFactor: 3.5
|
|
264
|
+
})),
|
|
265
|
+
createOwnDevice(ANDROID_BASE$1, createPhoneSpec({
|
|
266
|
+
id: "galaxy-s26",
|
|
267
|
+
name: "Galaxy S26"
|
|
268
|
+
}, {
|
|
269
|
+
size: {
|
|
270
|
+
width: 360,
|
|
271
|
+
height: 780
|
|
272
|
+
},
|
|
273
|
+
deviceScaleFactor: 3
|
|
274
|
+
})),
|
|
275
|
+
createOwnDevice(ANDROID_BASE$1, createPhoneSpec({
|
|
276
|
+
id: "galaxy-s26-plus",
|
|
277
|
+
name: "Galaxy S26+"
|
|
278
|
+
}, {
|
|
279
|
+
size: {
|
|
280
|
+
width: 412,
|
|
281
|
+
height: 891
|
|
282
|
+
},
|
|
283
|
+
deviceScaleFactor: 3.5
|
|
284
|
+
})),
|
|
285
|
+
createOwnDevice(ANDROID_BASE$1, createPhoneSpec({
|
|
286
|
+
id: "galaxy-s26-ultra",
|
|
287
|
+
name: "Galaxy S26 Ultra"
|
|
288
|
+
}, {
|
|
289
|
+
size: {
|
|
290
|
+
width: 412,
|
|
291
|
+
height: 891
|
|
292
|
+
},
|
|
293
|
+
deviceScaleFactor: 3.5
|
|
294
|
+
}))
|
|
295
|
+
];
|
|
296
|
+
const XIAOMI_BASE = {
|
|
297
|
+
...ANDROID_BASE$1,
|
|
298
|
+
maker: "xiaomi"
|
|
299
|
+
};
|
|
300
|
+
const ONEPLUS_BASE = {
|
|
301
|
+
...ANDROID_BASE$1,
|
|
302
|
+
maker: "oneplus"
|
|
303
|
+
};
|
|
304
|
+
const XIAOMI_DEVICES = [
|
|
305
|
+
createOwnDevice(XIAOMI_BASE, {
|
|
306
|
+
...createPhoneSpec({
|
|
307
|
+
id: "xiaomi-14",
|
|
308
|
+
name: "Xiaomi 14"
|
|
309
|
+
}, {
|
|
310
|
+
size: {
|
|
311
|
+
width: 400,
|
|
312
|
+
height: 890
|
|
313
|
+
},
|
|
314
|
+
deviceScaleFactor: 3
|
|
315
|
+
}),
|
|
316
|
+
cornerRadius: 41.33,
|
|
317
|
+
safeAreaInsets: {
|
|
318
|
+
...FRAME_SHAPE_INSETS["punch-hole"],
|
|
319
|
+
top: 49
|
|
320
|
+
}
|
|
321
|
+
}),
|
|
322
|
+
createOwnDevice(XIAOMI_BASE, createPhoneSpec({
|
|
323
|
+
id: "xiaomi-15",
|
|
324
|
+
name: "Xiaomi 15"
|
|
325
|
+
}, {
|
|
326
|
+
size: {
|
|
327
|
+
width: 400,
|
|
328
|
+
height: 890
|
|
329
|
+
},
|
|
330
|
+
deviceScaleFactor: 3
|
|
331
|
+
})),
|
|
332
|
+
createOwnDevice(XIAOMI_BASE, createPhoneSpec({
|
|
333
|
+
id: "redmi-note-13",
|
|
334
|
+
name: "Redmi Note 13"
|
|
335
|
+
}, {
|
|
336
|
+
size: {
|
|
337
|
+
width: 393,
|
|
338
|
+
height: 873
|
|
339
|
+
},
|
|
340
|
+
deviceScaleFactor: 2.748
|
|
341
|
+
})),
|
|
342
|
+
createOwnDevice(XIAOMI_BASE, createPhoneSpec({
|
|
343
|
+
id: "redmi-note-14",
|
|
344
|
+
name: "Redmi Note 14"
|
|
345
|
+
}, {
|
|
346
|
+
size: {
|
|
347
|
+
width: 393,
|
|
348
|
+
height: 873
|
|
349
|
+
},
|
|
350
|
+
deviceScaleFactor: 2.748
|
|
351
|
+
}))
|
|
352
|
+
];
|
|
353
|
+
const ONEPLUS_13 = createOwnDevice(ONEPLUS_BASE, {
|
|
354
|
+
id: "oneplus-13",
|
|
355
|
+
name: "OnePlus 13",
|
|
356
|
+
size: {
|
|
357
|
+
width: 412,
|
|
358
|
+
height: 905
|
|
359
|
+
},
|
|
360
|
+
deviceScaleFactor: 3.5,
|
|
361
|
+
cornerRadius: 32,
|
|
362
|
+
cutout: {
|
|
363
|
+
kind: "punch-hole-center",
|
|
364
|
+
width: 19.43,
|
|
365
|
+
height: 19.43,
|
|
366
|
+
top: 13.14,
|
|
367
|
+
left: 196
|
|
368
|
+
},
|
|
369
|
+
safeAreaInsets: {
|
|
370
|
+
...FRAME_SHAPE_INSETS["punch-hole"],
|
|
371
|
+
top: 45.67
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
const MAC_DEVICES = [
|
|
375
|
+
createOwnDevice(MAC_BASE, {
|
|
376
|
+
...createDesktopSpec({
|
|
377
|
+
id: "macbook-air-13",
|
|
378
|
+
name: "MacBook Air 13"
|
|
379
|
+
}, {
|
|
380
|
+
width: 1470,
|
|
381
|
+
height: 956
|
|
382
|
+
}),
|
|
383
|
+
cornerRadius: null,
|
|
384
|
+
cutout: NOTCH$1
|
|
385
|
+
}),
|
|
386
|
+
createOwnDevice(MAC_BASE, {
|
|
387
|
+
...createDesktopSpec({
|
|
388
|
+
id: "macbook-air-15",
|
|
389
|
+
name: "MacBook Air 15"
|
|
390
|
+
}, {
|
|
391
|
+
width: 1710,
|
|
392
|
+
height: 1107
|
|
393
|
+
}),
|
|
394
|
+
cornerRadius: null,
|
|
395
|
+
cutout: NOTCH$1
|
|
396
|
+
}),
|
|
397
|
+
createOwnDevice(MAC_BASE, {
|
|
398
|
+
...createDesktopSpec({
|
|
399
|
+
id: "macbook-pro-14",
|
|
400
|
+
name: "MacBook Pro 14"
|
|
401
|
+
}, {
|
|
402
|
+
width: 1512,
|
|
403
|
+
height: 982
|
|
404
|
+
}),
|
|
405
|
+
cornerRadius: null,
|
|
406
|
+
cutout: NOTCH$1
|
|
407
|
+
}),
|
|
408
|
+
createOwnDevice(MAC_BASE, {
|
|
409
|
+
...createDesktopSpec({
|
|
410
|
+
id: "macbook-pro-16",
|
|
411
|
+
name: "MacBook Pro 16"
|
|
412
|
+
}, {
|
|
413
|
+
width: 1728,
|
|
414
|
+
height: 1117
|
|
415
|
+
}),
|
|
416
|
+
cornerRadius: null,
|
|
417
|
+
cutout: NOTCH$1
|
|
418
|
+
})
|
|
419
|
+
];
|
|
420
|
+
const MONITOR_DEVICES = [
|
|
421
|
+
createOwnDevice(WINDOWS_BASE, createDesktopSpec({
|
|
422
|
+
id: "windows-laptop-1366",
|
|
423
|
+
name: "Windows laptop 1366"
|
|
424
|
+
}, {
|
|
425
|
+
width: 1366,
|
|
426
|
+
height: 768
|
|
427
|
+
})),
|
|
428
|
+
createOwnDevice(WINDOWS_BASE, createDesktopSpec({
|
|
429
|
+
id: "windows-laptop-1536",
|
|
430
|
+
name: "Windows laptop 1536"
|
|
431
|
+
}, {
|
|
432
|
+
width: 1536,
|
|
433
|
+
height: 864
|
|
434
|
+
})),
|
|
435
|
+
createOwnDevice(WINDOWS_BASE, createDesktopSpec({
|
|
436
|
+
id: "monitor-1920",
|
|
437
|
+
name: "Windows monitor 1920"
|
|
438
|
+
}, {
|
|
439
|
+
width: 1920,
|
|
440
|
+
height: 1080
|
|
441
|
+
})),
|
|
442
|
+
createOwnDevice(WINDOWS_BASE, createDesktopSpec({
|
|
443
|
+
id: "monitor-2560",
|
|
444
|
+
name: "Windows monitor 2560"
|
|
445
|
+
}, {
|
|
446
|
+
width: 2560,
|
|
447
|
+
height: 1440
|
|
448
|
+
})),
|
|
449
|
+
createOwnDevice(LINUX_BASE, createDesktopSpec({
|
|
450
|
+
id: "linux-1920",
|
|
451
|
+
name: "Linux monitor 1920"
|
|
452
|
+
}, {
|
|
453
|
+
width: 1920,
|
|
454
|
+
height: 1080
|
|
455
|
+
}))
|
|
456
|
+
];
|
|
457
|
+
const LEGACY_DEVICES = [createOwnDevice(MAC_BASE, {
|
|
458
|
+
...createDesktopSpec({
|
|
459
|
+
id: "laptop-1440",
|
|
460
|
+
name: "Laptop 1440"
|
|
461
|
+
}, {
|
|
462
|
+
width: 1440,
|
|
463
|
+
height: 900
|
|
464
|
+
}),
|
|
465
|
+
cutout: NO_CUTOUT$1
|
|
466
|
+
}), createOwnDevice({
|
|
467
|
+
...LINUX_BASE,
|
|
468
|
+
frameShape: "kiosk",
|
|
469
|
+
hasTouch: true
|
|
470
|
+
}, createDesktopSpec({
|
|
471
|
+
id: "store-kiosk",
|
|
472
|
+
name: "Store kiosk"
|
|
473
|
+
}, {
|
|
474
|
+
width: 1080,
|
|
475
|
+
height: 1920
|
|
476
|
+
}))];
|
|
477
|
+
const OWN_DEVICES = [
|
|
478
|
+
...IPAD_DEVICES,
|
|
479
|
+
...SAMSUNG_DEVICES,
|
|
480
|
+
...XIAOMI_DEVICES,
|
|
481
|
+
ONEPLUS_13,
|
|
482
|
+
...MAC_DEVICES,
|
|
483
|
+
...MONITOR_DEVICES,
|
|
484
|
+
...LEGACY_DEVICES
|
|
485
|
+
];
|
|
486
|
+
// src/devices/registry-logic.ts
|
|
487
|
+
const pickRegistryScreen = (registrySizes) => registrySizes.screen ?? registrySizes.viewport;
|
|
488
|
+
const createCatalogDeviceFromRegistry = (identity, registryName, registryDevice) => ({
|
|
489
|
+
...identity,
|
|
490
|
+
registryName,
|
|
491
|
+
fullScreen: pickRegistryScreen(registryDevice),
|
|
492
|
+
browserViewport: registryDevice.viewport,
|
|
493
|
+
userAgent: registryDevice.userAgent,
|
|
494
|
+
deviceScaleFactor: registryDevice.deviceScaleFactor,
|
|
495
|
+
isMobile: registryDevice.isMobile,
|
|
496
|
+
hasTouch: registryDevice.hasTouch
|
|
497
|
+
});
|
|
498
|
+
// src/devices/registry-devices.ts
|
|
499
|
+
const IPHONE_BASE = {
|
|
500
|
+
browserName: "webkit",
|
|
501
|
+
frameShape: "dynamic-island",
|
|
502
|
+
cpuSlowdown: CPU_TIERS.phone,
|
|
503
|
+
platform: "ios",
|
|
504
|
+
maker: "apple"
|
|
505
|
+
};
|
|
506
|
+
const ANDROID_BASE = {
|
|
507
|
+
browserName: "chromium",
|
|
508
|
+
frameShape: "punch-hole",
|
|
509
|
+
cpuSlowdown: CPU_TIERS.phone,
|
|
510
|
+
platform: "android",
|
|
511
|
+
maker: "google"
|
|
512
|
+
};
|
|
513
|
+
const SAMSUNG_BASE = {
|
|
514
|
+
...ANDROID_BASE,
|
|
515
|
+
maker: "samsung"
|
|
516
|
+
};
|
|
517
|
+
const TABLET_BASE = {
|
|
518
|
+
browserName: "webkit",
|
|
519
|
+
frameShape: "tablet",
|
|
520
|
+
cpuSlowdown: CPU_TIERS.tablet,
|
|
521
|
+
platform: "ipados",
|
|
522
|
+
maker: "apple"
|
|
523
|
+
};
|
|
524
|
+
const DYNAMIC_ISLAND = {
|
|
525
|
+
kind: "dynamic-island",
|
|
526
|
+
width: null,
|
|
527
|
+
height: null,
|
|
528
|
+
top: null,
|
|
529
|
+
left: null
|
|
530
|
+
};
|
|
531
|
+
const NOTCH = {
|
|
532
|
+
...DYNAMIC_ISLAND,
|
|
533
|
+
kind: "notch"
|
|
534
|
+
};
|
|
535
|
+
const UNMEASURED_PUNCH_HOLE = {
|
|
536
|
+
...DYNAMIC_ISLAND,
|
|
537
|
+
kind: "punch-hole-center"
|
|
538
|
+
};
|
|
539
|
+
const NO_CUTOUT = {
|
|
540
|
+
...DYNAMIC_ISLAND,
|
|
541
|
+
kind: "none"
|
|
542
|
+
};
|
|
543
|
+
const ISLAND_INSETS_IPHONE_15 = {
|
|
544
|
+
top: 59,
|
|
545
|
+
bottom: 34,
|
|
546
|
+
left: 0,
|
|
547
|
+
right: 0
|
|
548
|
+
};
|
|
549
|
+
const ISLAND_INSETS_IPHONE_16_PRO = {
|
|
550
|
+
...ISLAND_INSETS_IPHONE_15,
|
|
551
|
+
top: 62
|
|
552
|
+
};
|
|
553
|
+
const ISLAND_INSETS_IPHONE_AIR = {
|
|
554
|
+
...ISLAND_INSETS_IPHONE_15,
|
|
555
|
+
top: 68
|
|
556
|
+
};
|
|
557
|
+
const NOTCH_INSETS_IPHONE_13 = {
|
|
558
|
+
...ISLAND_INSETS_IPHONE_15,
|
|
559
|
+
top: 47
|
|
560
|
+
};
|
|
561
|
+
const createPunchHole = (hole) => ({
|
|
562
|
+
kind: "punch-hole-center",
|
|
563
|
+
width: hole.diameter,
|
|
564
|
+
height: hole.diameter,
|
|
565
|
+
top: hole.top,
|
|
566
|
+
left: hole.left
|
|
567
|
+
});
|
|
568
|
+
const createTopInset = (top) => ({
|
|
569
|
+
...FRAME_SHAPE_INSETS["punch-hole"],
|
|
570
|
+
top
|
|
571
|
+
});
|
|
572
|
+
const REGISTRY_DEVICE_SPECS = [
|
|
573
|
+
{
|
|
574
|
+
registryName: "iPhone SE (3rd gen)",
|
|
575
|
+
identity: {
|
|
576
|
+
...IPHONE_BASE,
|
|
577
|
+
id: "iphone-se",
|
|
578
|
+
name: "iPhone SE",
|
|
579
|
+
frameShape: "home-button",
|
|
580
|
+
cpuSlowdown: CPU_TIERS.budgetPhone,
|
|
581
|
+
cornerRadius: null,
|
|
582
|
+
cutout: {
|
|
583
|
+
...NO_CUTOUT,
|
|
584
|
+
kind: "home-button"
|
|
585
|
+
},
|
|
586
|
+
safeAreaInsets: FRAME_SHAPE_INSETS["home-button"]
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
registryName: "iPhone 15",
|
|
591
|
+
identity: {
|
|
592
|
+
...IPHONE_BASE,
|
|
593
|
+
id: "iphone-15",
|
|
594
|
+
name: "iPhone 15",
|
|
595
|
+
cornerRadius: 55,
|
|
596
|
+
cutout: DYNAMIC_ISLAND,
|
|
597
|
+
safeAreaInsets: ISLAND_INSETS_IPHONE_15
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
{
|
|
601
|
+
registryName: "iPhone 15 Plus",
|
|
602
|
+
identity: {
|
|
603
|
+
...IPHONE_BASE,
|
|
604
|
+
id: "iphone-15-plus",
|
|
605
|
+
name: "iPhone 15 Plus",
|
|
606
|
+
cornerRadius: 55,
|
|
607
|
+
cutout: DYNAMIC_ISLAND,
|
|
608
|
+
safeAreaInsets: ISLAND_INSETS_IPHONE_15
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
registryName: "iPhone 15 Pro",
|
|
613
|
+
identity: {
|
|
614
|
+
...IPHONE_BASE,
|
|
615
|
+
id: "iphone-15-pro",
|
|
616
|
+
name: "iPhone 15 Pro",
|
|
617
|
+
cornerRadius: 55,
|
|
618
|
+
cutout: DYNAMIC_ISLAND,
|
|
619
|
+
safeAreaInsets: ISLAND_INSETS_IPHONE_15
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
{
|
|
623
|
+
registryName: "iPhone 15 Pro Max",
|
|
624
|
+
identity: {
|
|
625
|
+
...IPHONE_BASE,
|
|
626
|
+
id: "iphone-15-pro-max",
|
|
627
|
+
name: "iPhone 15 Pro Max",
|
|
628
|
+
cornerRadius: 55,
|
|
629
|
+
cutout: DYNAMIC_ISLAND,
|
|
630
|
+
safeAreaInsets: ISLAND_INSETS_IPHONE_15
|
|
631
|
+
}
|
|
632
|
+
},
|
|
633
|
+
{
|
|
634
|
+
registryName: "iPhone 16",
|
|
635
|
+
identity: {
|
|
636
|
+
...IPHONE_BASE,
|
|
637
|
+
id: "iphone-16",
|
|
638
|
+
name: "iPhone 16",
|
|
639
|
+
cornerRadius: 55,
|
|
640
|
+
cutout: DYNAMIC_ISLAND,
|
|
641
|
+
safeAreaInsets: ISLAND_INSETS_IPHONE_15
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
registryName: "iPhone 16 Plus",
|
|
646
|
+
identity: {
|
|
647
|
+
...IPHONE_BASE,
|
|
648
|
+
id: "iphone-16-plus",
|
|
649
|
+
name: "iPhone 16 Plus",
|
|
650
|
+
cornerRadius: 55,
|
|
651
|
+
cutout: DYNAMIC_ISLAND,
|
|
652
|
+
safeAreaInsets: ISLAND_INSETS_IPHONE_15
|
|
653
|
+
}
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
registryName: "iPhone 16 Pro",
|
|
657
|
+
identity: {
|
|
658
|
+
...IPHONE_BASE,
|
|
659
|
+
id: "iphone-16-pro",
|
|
660
|
+
name: "iPhone 16 Pro",
|
|
661
|
+
cornerRadius: 62,
|
|
662
|
+
cutout: DYNAMIC_ISLAND,
|
|
663
|
+
safeAreaInsets: ISLAND_INSETS_IPHONE_16_PRO
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
registryName: "iPhone 16 Pro Max",
|
|
668
|
+
identity: {
|
|
669
|
+
...IPHONE_BASE,
|
|
670
|
+
id: "iphone-16-pro-max",
|
|
671
|
+
name: "iPhone 16 Pro Max",
|
|
672
|
+
cornerRadius: 62,
|
|
673
|
+
cutout: DYNAMIC_ISLAND,
|
|
674
|
+
safeAreaInsets: ISLAND_INSETS_IPHONE_16_PRO
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
registryName: "iPhone 16e",
|
|
679
|
+
identity: {
|
|
680
|
+
...IPHONE_BASE,
|
|
681
|
+
id: "iphone-16e",
|
|
682
|
+
name: "iPhone 16e",
|
|
683
|
+
cornerRadius: 47.33,
|
|
684
|
+
cutout: NOTCH,
|
|
685
|
+
safeAreaInsets: NOTCH_INSETS_IPHONE_13
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
registryName: "iPhone 17",
|
|
690
|
+
identity: {
|
|
691
|
+
...IPHONE_BASE,
|
|
692
|
+
id: "iphone-17",
|
|
693
|
+
name: "iPhone 17",
|
|
694
|
+
cornerRadius: 62,
|
|
695
|
+
cutout: DYNAMIC_ISLAND,
|
|
696
|
+
safeAreaInsets: ISLAND_INSETS_IPHONE_16_PRO
|
|
697
|
+
}
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
registryName: "iPhone Air",
|
|
701
|
+
identity: {
|
|
702
|
+
...IPHONE_BASE,
|
|
703
|
+
id: "iphone-air",
|
|
704
|
+
name: "iPhone Air",
|
|
705
|
+
cornerRadius: 62,
|
|
706
|
+
cutout: DYNAMIC_ISLAND,
|
|
707
|
+
safeAreaInsets: ISLAND_INSETS_IPHONE_AIR
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
registryName: "iPhone 17 Pro",
|
|
712
|
+
identity: {
|
|
713
|
+
...IPHONE_BASE,
|
|
714
|
+
id: "iphone-17-pro",
|
|
715
|
+
name: "iPhone 17 Pro",
|
|
716
|
+
cornerRadius: 62,
|
|
717
|
+
cutout: DYNAMIC_ISLAND,
|
|
718
|
+
safeAreaInsets: ISLAND_INSETS_IPHONE_16_PRO
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
registryName: "iPhone 17 Pro Max",
|
|
723
|
+
identity: {
|
|
724
|
+
...IPHONE_BASE,
|
|
725
|
+
id: "iphone-17-pro-max",
|
|
726
|
+
name: "iPhone 17 Pro Max",
|
|
727
|
+
cornerRadius: 62,
|
|
728
|
+
cutout: DYNAMIC_ISLAND,
|
|
729
|
+
safeAreaInsets: ISLAND_INSETS_IPHONE_16_PRO
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
registryName: "iPhone 17e",
|
|
734
|
+
identity: {
|
|
735
|
+
...IPHONE_BASE,
|
|
736
|
+
id: "iphone-17e",
|
|
737
|
+
name: "iPhone 17e",
|
|
738
|
+
cornerRadius: null,
|
|
739
|
+
cutout: NOTCH,
|
|
740
|
+
safeAreaInsets: NOTCH_INSETS_IPHONE_13
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
registryName: "Pixel 8",
|
|
745
|
+
identity: {
|
|
746
|
+
...ANDROID_BASE,
|
|
747
|
+
id: "pixel-8",
|
|
748
|
+
name: "Pixel 8",
|
|
749
|
+
cornerRadius: 38.86,
|
|
750
|
+
cutout: createPunchHole({
|
|
751
|
+
diameter: 27.62,
|
|
752
|
+
top: 11.24,
|
|
753
|
+
left: 191.81
|
|
754
|
+
}),
|
|
755
|
+
safeAreaInsets: createTopInset(50.29)
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
registryName: "Pixel 8 Pro",
|
|
760
|
+
identity: {
|
|
761
|
+
...ANDROID_BASE,
|
|
762
|
+
id: "pixel-8-pro",
|
|
763
|
+
name: "Pixel 8 Pro",
|
|
764
|
+
cornerRadius: 30.33,
|
|
765
|
+
cutout: createPunchHole({
|
|
766
|
+
diameter: 28.83,
|
|
767
|
+
top: 10.58,
|
|
768
|
+
left: 209.58
|
|
769
|
+
}),
|
|
770
|
+
safeAreaInsets: createTopInset(50.33)
|
|
771
|
+
}
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
registryName: "Pixel 8a",
|
|
775
|
+
identity: {
|
|
776
|
+
...ANDROID_BASE,
|
|
777
|
+
id: "pixel-8a",
|
|
778
|
+
name: "Pixel 8a",
|
|
779
|
+
cornerRadius: 48.76,
|
|
780
|
+
cutout: createPunchHole({
|
|
781
|
+
diameter: 25.69,
|
|
782
|
+
top: 13.33,
|
|
783
|
+
left: 192.69
|
|
784
|
+
}),
|
|
785
|
+
safeAreaInsets: createTopInset(46.1)
|
|
786
|
+
}
|
|
787
|
+
},
|
|
788
|
+
{
|
|
789
|
+
registryName: "Pixel 9",
|
|
790
|
+
identity: {
|
|
791
|
+
...ANDROID_BASE,
|
|
792
|
+
id: "pixel-9",
|
|
793
|
+
name: "Pixel 9",
|
|
794
|
+
cornerRadius: 44,
|
|
795
|
+
cutout: createPunchHole({
|
|
796
|
+
diameter: 28,
|
|
797
|
+
top: 14.83,
|
|
798
|
+
left: 165.83
|
|
799
|
+
}),
|
|
800
|
+
safeAreaInsets: createTopInset(57.67)
|
|
801
|
+
}
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
registryName: "Pixel 9 Pro",
|
|
805
|
+
identity: {
|
|
806
|
+
...ANDROID_BASE,
|
|
807
|
+
id: "pixel-9-pro",
|
|
808
|
+
name: "Pixel 9 Pro",
|
|
809
|
+
cornerRadius: 52.33,
|
|
810
|
+
cutout: createPunchHole({
|
|
811
|
+
diameter: 32.67,
|
|
812
|
+
top: 17.67,
|
|
813
|
+
left: 197
|
|
814
|
+
}),
|
|
815
|
+
safeAreaInsets: createTopInset(68)
|
|
816
|
+
}
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
registryName: "Pixel 9 Pro XL",
|
|
820
|
+
identity: {
|
|
821
|
+
...ANDROID_BASE,
|
|
822
|
+
id: "pixel-9-pro-xl",
|
|
823
|
+
name: "Pixel 9 Pro XL",
|
|
824
|
+
cornerRadius: 51,
|
|
825
|
+
cutout: createPunchHole({
|
|
826
|
+
diameter: 31.33,
|
|
827
|
+
top: 17.5,
|
|
828
|
+
left: 208.33
|
|
829
|
+
}),
|
|
830
|
+
safeAreaInsets: createTopInset(66.33)
|
|
831
|
+
}
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
registryName: "Pixel 10",
|
|
835
|
+
identity: {
|
|
836
|
+
...ANDROID_BASE,
|
|
837
|
+
id: "pixel-10",
|
|
838
|
+
name: "Pixel 10",
|
|
839
|
+
cornerRadius: null,
|
|
840
|
+
cutout: UNMEASURED_PUNCH_HOLE,
|
|
841
|
+
safeAreaInsets: FRAME_SHAPE_INSETS["punch-hole"]
|
|
842
|
+
}
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
registryName: "Pixel 10 Pro",
|
|
846
|
+
identity: {
|
|
847
|
+
...ANDROID_BASE,
|
|
848
|
+
id: "pixel-10-pro",
|
|
849
|
+
name: "Pixel 10 Pro",
|
|
850
|
+
cornerRadius: null,
|
|
851
|
+
cutout: UNMEASURED_PUNCH_HOLE,
|
|
852
|
+
safeAreaInsets: FRAME_SHAPE_INSETS["punch-hole"]
|
|
853
|
+
}
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
registryName: "Pixel 10 Pro XL",
|
|
857
|
+
identity: {
|
|
858
|
+
...ANDROID_BASE,
|
|
859
|
+
id: "pixel-10-pro-xl",
|
|
860
|
+
name: "Pixel 10 Pro XL",
|
|
861
|
+
cornerRadius: null,
|
|
862
|
+
cutout: UNMEASURED_PUNCH_HOLE,
|
|
863
|
+
safeAreaInsets: FRAME_SHAPE_INSETS["punch-hole"]
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
registryName: "Galaxy S24",
|
|
868
|
+
identity: {
|
|
869
|
+
...SAMSUNG_BASE,
|
|
870
|
+
id: "galaxy-s24",
|
|
871
|
+
name: "Galaxy S24",
|
|
872
|
+
cornerRadius: null,
|
|
873
|
+
cutout: UNMEASURED_PUNCH_HOLE,
|
|
874
|
+
safeAreaInsets: FRAME_SHAPE_INSETS["punch-hole"]
|
|
875
|
+
}
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
registryName: "Galaxy A55",
|
|
879
|
+
identity: {
|
|
880
|
+
...SAMSUNG_BASE,
|
|
881
|
+
id: "galaxy-a55",
|
|
882
|
+
name: "Galaxy A55",
|
|
883
|
+
cornerRadius: null,
|
|
884
|
+
cutout: UNMEASURED_PUNCH_HOLE,
|
|
885
|
+
safeAreaInsets: FRAME_SHAPE_INSETS["punch-hole"]
|
|
886
|
+
}
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
registryName: "Galaxy Z Fold 7",
|
|
890
|
+
identity: {
|
|
891
|
+
...SAMSUNG_BASE,
|
|
892
|
+
id: "galaxy-z-fold-7",
|
|
893
|
+
name: "Galaxy Z Fold 7 (open)",
|
|
894
|
+
frameShape: "tablet",
|
|
895
|
+
cpuSlowdown: CPU_TIERS.tablet,
|
|
896
|
+
cornerRadius: null,
|
|
897
|
+
cutout: UNMEASURED_PUNCH_HOLE,
|
|
898
|
+
safeAreaInsets: FRAME_SHAPE_INSETS.tablet
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
registryName: "Galaxy Z Fold 7 Cover",
|
|
903
|
+
identity: {
|
|
904
|
+
...SAMSUNG_BASE,
|
|
905
|
+
id: "galaxy-z-fold-7-cover",
|
|
906
|
+
name: "Galaxy Z Fold 7 (cover)",
|
|
907
|
+
cornerRadius: null,
|
|
908
|
+
cutout: UNMEASURED_PUNCH_HOLE,
|
|
909
|
+
safeAreaInsets: FRAME_SHAPE_INSETS["punch-hole"]
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
registryName: "Galaxy Z Flip 7",
|
|
914
|
+
identity: {
|
|
915
|
+
...SAMSUNG_BASE,
|
|
916
|
+
id: "galaxy-z-flip-7",
|
|
917
|
+
name: "Galaxy Z Flip 7 (open)",
|
|
918
|
+
cornerRadius: null,
|
|
919
|
+
cutout: UNMEASURED_PUNCH_HOLE,
|
|
920
|
+
safeAreaInsets: FRAME_SHAPE_INSETS["punch-hole"]
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
{
|
|
924
|
+
registryName: "Galaxy Z Flip 7 Cover",
|
|
925
|
+
identity: {
|
|
926
|
+
...SAMSUNG_BASE,
|
|
927
|
+
id: "galaxy-z-flip-7-cover",
|
|
928
|
+
name: "Galaxy Z Flip 7 (cover)",
|
|
929
|
+
cornerRadius: null,
|
|
930
|
+
cutout: NO_CUTOUT,
|
|
931
|
+
safeAreaInsets: FRAME_SHAPE_INSETS.tablet
|
|
932
|
+
}
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
registryName: "iPad Pro 11",
|
|
936
|
+
identity: {
|
|
937
|
+
...TABLET_BASE,
|
|
938
|
+
id: "ipad-pro-11",
|
|
939
|
+
name: "iPad Pro 11",
|
|
940
|
+
cornerRadius: 18,
|
|
941
|
+
cutout: NO_CUTOUT,
|
|
942
|
+
safeAreaInsets: FRAME_SHAPE_INSETS.tablet
|
|
943
|
+
}
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
registryName: "iPad (gen 11)",
|
|
947
|
+
identity: {
|
|
948
|
+
...TABLET_BASE,
|
|
949
|
+
id: "ipad-11",
|
|
950
|
+
name: "iPad (11th gen)",
|
|
951
|
+
cornerRadius: null,
|
|
952
|
+
cutout: NO_CUTOUT,
|
|
953
|
+
safeAreaInsets: FRAME_SHAPE_INSETS.tablet
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
registryName: "Galaxy Tab S9",
|
|
958
|
+
identity: {
|
|
959
|
+
...SAMSUNG_BASE,
|
|
960
|
+
id: "galaxy-tab-s9",
|
|
961
|
+
name: "Galaxy Tab S9",
|
|
962
|
+
frameShape: "tablet",
|
|
963
|
+
cpuSlowdown: CPU_TIERS.tablet,
|
|
964
|
+
cornerRadius: null,
|
|
965
|
+
cutout: NO_CUTOUT,
|
|
966
|
+
safeAreaInsets: FRAME_SHAPE_INSETS.tablet
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
];
|
|
970
|
+
const createRegistryCatalogDevice = (deviceSpec) => {
|
|
971
|
+
const registryDevice = devices[deviceSpec.registryName];
|
|
972
|
+
if (registryDevice === void 0) throw new Error(`Playwright has no device ${deviceSpec.registryName}; update the catalog`);
|
|
973
|
+
return createCatalogDeviceFromRegistry(deviceSpec.identity, deviceSpec.registryName, registryDevice);
|
|
974
|
+
};
|
|
975
|
+
// src/devices/device-catalog.ts
|
|
976
|
+
const DEVICE_CATALOG = [...REGISTRY_DEVICE_SPECS.map(createRegistryCatalogDevice), ...OWN_DEVICES];
|
|
977
|
+
// src/devices/devices-logic.ts
|
|
978
|
+
const UNKNOWN_DEVICE_STATUS = 422;
|
|
979
|
+
const NON_ID_CHARACTERS = /[^\p{L}\p{N}]+/gu;
|
|
980
|
+
const EDGE_DASHES = /^-|-$/g;
|
|
981
|
+
const pickDeviceId = (deviceName) => deviceName.toLowerCase().replace(NON_ID_CHARACTERS, "-").replace(EDGE_DASHES, "");
|
|
982
|
+
const hasDeviceId = (customDevice, deviceId) => pickDeviceId(customDevice.name) === deviceId;
|
|
983
|
+
const createPresetFromCustomDevice = (customDevice) => {
|
|
984
|
+
const isTouchDevice = customDevice.hasTouch;
|
|
985
|
+
const isFirefoxDevice = customDevice.browserName === "firefox";
|
|
986
|
+
const isMobile = isTouchDevice && !isFirefoxDevice;
|
|
987
|
+
const cpuSlowdown = isTouchDevice ? CPU_TIERS.phone : CPU_TIERS.desktop;
|
|
988
|
+
const { frameShape } = customDevice;
|
|
989
|
+
const safeAreaInsets = FRAME_SHAPE_INSETS[frameShape];
|
|
990
|
+
const statusBarHeight = pickStatusBarHeight({
|
|
991
|
+
frameShape,
|
|
992
|
+
platform: null,
|
|
993
|
+
cutout: null,
|
|
994
|
+
safeAreaInsets
|
|
995
|
+
});
|
|
996
|
+
const hasStatusBar = statusBarHeight > 0;
|
|
997
|
+
const screen = {
|
|
998
|
+
width: customDevice.width,
|
|
999
|
+
height: customDevice.height
|
|
1000
|
+
};
|
|
1001
|
+
return {
|
|
1002
|
+
id: pickDeviceId(customDevice.name),
|
|
1003
|
+
name: customDevice.name,
|
|
1004
|
+
browserName: customDevice.browserName,
|
|
1005
|
+
frameShape,
|
|
1006
|
+
cpuSlowdown,
|
|
1007
|
+
platform: null,
|
|
1008
|
+
maker: "generic",
|
|
1009
|
+
cornerRadius: null,
|
|
1010
|
+
cutout: null,
|
|
1011
|
+
safeAreaInsets,
|
|
1012
|
+
isCustom: true,
|
|
1013
|
+
viewportMode: "full-screen",
|
|
1014
|
+
use: {
|
|
1015
|
+
viewport: {
|
|
1016
|
+
width: screen.width,
|
|
1017
|
+
height: screen.height - statusBarHeight
|
|
1018
|
+
},
|
|
1019
|
+
...hasStatusBar && { screen },
|
|
1020
|
+
deviceScaleFactor: customDevice.pixelRatio,
|
|
1021
|
+
isMobile,
|
|
1022
|
+
hasTouch: customDevice.hasTouch
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
};
|
|
1026
|
+
const STATUS_BAR_HEIGHTS_BY_SHAPE = {
|
|
1027
|
+
"home-button": 20,
|
|
1028
|
+
"dynamic-island": 59,
|
|
1029
|
+
"punch-hole": 24,
|
|
1030
|
+
tablet: 24,
|
|
1031
|
+
laptop: 0,
|
|
1032
|
+
kiosk: 0
|
|
1033
|
+
};
|
|
1034
|
+
const STATUS_BAR_HEIGHTS_BY_CUTOUT = {
|
|
1035
|
+
none: null,
|
|
1036
|
+
notch: 47,
|
|
1037
|
+
"dynamic-island": 59,
|
|
1038
|
+
"punch-hole-center": null,
|
|
1039
|
+
"punch-hole-corner": null,
|
|
1040
|
+
"home-button": 20
|
|
1041
|
+
};
|
|
1042
|
+
const STATUS_BAR_PLATFORMS = [
|
|
1043
|
+
"ios",
|
|
1044
|
+
"ipados",
|
|
1045
|
+
"android"
|
|
1046
|
+
];
|
|
1047
|
+
const PHONE_SHAPES = [
|
|
1048
|
+
"home-button",
|
|
1049
|
+
"dynamic-island",
|
|
1050
|
+
"punch-hole"
|
|
1051
|
+
];
|
|
1052
|
+
const pickStatusBarHeight = (deviceIdentity) => {
|
|
1053
|
+
const { frameShape, platform, cutout, safeAreaInsets } = deviceIdentity;
|
|
1054
|
+
const isPlatformKnown = platform !== null;
|
|
1055
|
+
const isMobilePlatform = isPlatformKnown && STATUS_BAR_PLATFORMS.includes(platform);
|
|
1056
|
+
const isPhoneShape = PHONE_SHAPES.includes(frameShape);
|
|
1057
|
+
if (!(!(frameShape === "laptop" || frameShape === "kiosk") && (isPlatformKnown ? isMobilePlatform : isPhoneShape))) return 0;
|
|
1058
|
+
const publishedHeight = Math.round(safeAreaInsets.top);
|
|
1059
|
+
if (publishedHeight > 0) return publishedHeight;
|
|
1060
|
+
return (cutout === null ? null : STATUS_BAR_HEIGHTS_BY_CUTOUT[cutout.kind]) ?? STATUS_BAR_HEIGHTS_BY_SHAPE[frameShape];
|
|
1061
|
+
};
|
|
1062
|
+
const pickModeViewport = (catalogDevice, viewportMode) => {
|
|
1063
|
+
switch (viewportMode) {
|
|
1064
|
+
case "full-screen": {
|
|
1065
|
+
const { width, height } = catalogDevice.fullScreen;
|
|
1066
|
+
return {
|
|
1067
|
+
width,
|
|
1068
|
+
height: height - pickStatusBarHeight(catalogDevice)
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
case "browser": return catalogDevice.browserViewport ?? catalogDevice.fullScreen;
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
const createPresetFromCatalogDevice = (catalogDevice, viewportMode) => {
|
|
1075
|
+
return {
|
|
1076
|
+
id: catalogDevice.id,
|
|
1077
|
+
name: catalogDevice.name,
|
|
1078
|
+
browserName: catalogDevice.browserName,
|
|
1079
|
+
frameShape: catalogDevice.frameShape,
|
|
1080
|
+
cpuSlowdown: catalogDevice.cpuSlowdown,
|
|
1081
|
+
platform: catalogDevice.platform,
|
|
1082
|
+
maker: catalogDevice.maker,
|
|
1083
|
+
cornerRadius: catalogDevice.cornerRadius,
|
|
1084
|
+
cutout: catalogDevice.cutout,
|
|
1085
|
+
safeAreaInsets: catalogDevice.safeAreaInsets,
|
|
1086
|
+
isCustom: false,
|
|
1087
|
+
viewportMode,
|
|
1088
|
+
use: {
|
|
1089
|
+
viewport: pickModeViewport(catalogDevice, viewportMode),
|
|
1090
|
+
screen: catalogDevice.fullScreen,
|
|
1091
|
+
userAgent: catalogDevice.userAgent,
|
|
1092
|
+
deviceScaleFactor: catalogDevice.deviceScaleFactor,
|
|
1093
|
+
isMobile: catalogDevice.isMobile,
|
|
1094
|
+
hasTouch: catalogDevice.hasTouch
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
};
|
|
1098
|
+
const findCatalogPreset = (deviceId, viewportMode) => {
|
|
1099
|
+
const catalogDevice = DEVICE_CATALOG.find(({ id }) => id === deviceId);
|
|
1100
|
+
if (catalogDevice === void 0) throw new Error(`No catalog device ${deviceId}`);
|
|
1101
|
+
return createPresetFromCatalogDevice(catalogDevice, viewportMode);
|
|
1102
|
+
};
|
|
1103
|
+
const pickGridPreset = (devicesFile, gridId) => {
|
|
1104
|
+
const customDevice = devicesFile.devices.find((device) => hasDeviceId(device, gridId));
|
|
1105
|
+
if (customDevice !== void 0) return createPresetFromCustomDevice(customDevice);
|
|
1106
|
+
return findCatalogPreset(gridId, devicesFile.viewportModes[gridId] ?? "full-screen");
|
|
1107
|
+
};
|
|
1108
|
+
const pickGridDevices = (devicesFile) => {
|
|
1109
|
+
const gridPresets = devicesFile.grid.map((gridId) => pickGridPreset(devicesFile, gridId));
|
|
1110
|
+
const gridIds = gridPresets.map(({ id }) => id);
|
|
1111
|
+
const otherCustomPresets = devicesFile.devices.map(createPresetFromCustomDevice).filter((customPreset) => !gridIds.includes(customPreset.id));
|
|
1112
|
+
return [...gridPresets, ...otherCustomPresets];
|
|
1113
|
+
};
|
|
1114
|
+
const listCatalogReplies = (devicesFile) => {
|
|
1115
|
+
const customIds = devicesFile.devices.map((customDevice) => pickDeviceId(customDevice.name));
|
|
1116
|
+
return DEVICE_CATALOG.map((catalogDevice) => ({
|
|
1117
|
+
...catalogDevice,
|
|
1118
|
+
isHiddenByCustomDevice: customIds.includes(catalogDevice.id)
|
|
1119
|
+
}));
|
|
1120
|
+
};
|
|
1121
|
+
const checkNewDevice = (customDevice, takenDevices) => {
|
|
1122
|
+
const newDeviceId = pickDeviceId(customDevice.name);
|
|
1123
|
+
if (takenDevices.some((takenDevice) => takenDevice.id === newDeviceId)) throw new HTTPException(400, { message: `a device with id ${newDeviceId} already exists · use another name` });
|
|
1124
|
+
return customDevice;
|
|
1125
|
+
};
|
|
1126
|
+
const createMissingCustomDeviceMessage = (devicesFile, deviceId) => {
|
|
1127
|
+
const customIds = devicesFile.devices.map((customDevice) => pickDeviceId(customDevice.name));
|
|
1128
|
+
if (customIds.length === 0) return `no custom device ${deviceId} · there are no custom devices`;
|
|
1129
|
+
return `no custom device ${deviceId} · custom devices: ${customIds.join(", ")}`;
|
|
1130
|
+
};
|
|
1131
|
+
const checkDeviceRemoval = (devicesFile, deviceId) => {
|
|
1132
|
+
if (!devicesFile.devices.some((customDevice) => hasDeviceId(customDevice, deviceId))) throw new HTTPException(400, { message: createMissingCustomDeviceMessage(devicesFile, deviceId) });
|
|
1133
|
+
const keptDevices = devicesFile.devices.filter((customDevice) => !hasDeviceId(customDevice, deviceId));
|
|
1134
|
+
const isCatalogDevice = DEVICE_CATALOG.some(({ id }) => id === deviceId);
|
|
1135
|
+
const keptGrid = devicesFile.grid.filter((gridId) => isCatalogDevice || gridId !== deviceId);
|
|
1136
|
+
return {
|
|
1137
|
+
...devicesFile,
|
|
1138
|
+
devices: keptDevices,
|
|
1139
|
+
grid: keptGrid
|
|
1140
|
+
};
|
|
1141
|
+
};
|
|
1142
|
+
const listUnknownIds = (devicesFile, deviceIds) => {
|
|
1143
|
+
const knownIds = [...DEVICE_CATALOG.map(({ id }) => id), ...devicesFile.devices.map((customDevice) => pickDeviceId(customDevice.name))];
|
|
1144
|
+
return deviceIds.filter((deviceId) => !knownIds.includes(deviceId));
|
|
1145
|
+
};
|
|
1146
|
+
const checkDeviceIds = (devicesFile) => {
|
|
1147
|
+
const fieldIds = {
|
|
1148
|
+
grid: devicesFile.grid,
|
|
1149
|
+
viewportModes: Object.keys(devicesFile.viewportModes)
|
|
1150
|
+
};
|
|
1151
|
+
for (const [field, deviceIds] of Object.entries(fieldIds)) {
|
|
1152
|
+
const [unknownId] = listUnknownIds(devicesFile, deviceIds);
|
|
1153
|
+
if (unknownId !== void 0) throw new HTTPException(UNKNOWN_DEVICE_STATUS, { message: `${field}: no device with id ${unknownId}` });
|
|
1154
|
+
}
|
|
1155
|
+
return devicesFile;
|
|
1156
|
+
};
|
|
1157
|
+
const pickDeviceViewport = (devicePreset) => {
|
|
1158
|
+
const { viewport } = devicePreset.use;
|
|
1159
|
+
if (viewport === void 0 || viewport === null) throw new Error(`Device ${devicePreset.id} has no viewport`);
|
|
1160
|
+
return {
|
|
1161
|
+
width: viewport.width,
|
|
1162
|
+
height: viewport.height
|
|
1163
|
+
};
|
|
1164
|
+
};
|
|
1165
|
+
const pickDeviceScreen = (devicePreset) => {
|
|
1166
|
+
const { screen } = devicePreset.use;
|
|
1167
|
+
if (screen === void 0) return pickDeviceViewport(devicePreset);
|
|
1168
|
+
return {
|
|
1169
|
+
width: screen.width,
|
|
1170
|
+
height: screen.height
|
|
1171
|
+
};
|
|
1172
|
+
};
|
|
1173
|
+
const replaceGrid = (devicesFile, deviceIds) => {
|
|
1174
|
+
const uniqueIds = [...new Set(deviceIds)];
|
|
1175
|
+
return checkDeviceIds({
|
|
1176
|
+
...devicesFile,
|
|
1177
|
+
grid: uniqueIds
|
|
1178
|
+
});
|
|
1179
|
+
};
|
|
1180
|
+
const pickGridChanges = (runningDeviceIds, gridDevices) => {
|
|
1181
|
+
const gridDeviceIds = gridDevices.map(({ id }) => id);
|
|
1182
|
+
return {
|
|
1183
|
+
addDevices: gridDevices.filter(({ id }) => !runningDeviceIds.includes(id)),
|
|
1184
|
+
removeDeviceIds: runningDeviceIds.filter((runningDeviceId) => !gridDeviceIds.includes(runningDeviceId))
|
|
1185
|
+
};
|
|
1186
|
+
};
|
|
1187
|
+
// src/devices/devices-store.ts
|
|
1188
|
+
const BAD_DEVICES_FILE_STATUS = 422;
|
|
1189
|
+
const rejectDevicesFile = (reason, cause) => {
|
|
1190
|
+
throw new HTTPException(BAD_DEVICES_FILE_STATUS, {
|
|
1191
|
+
message: `${DEVICES_FILE_PATH} is not valid: ${reason}`,
|
|
1192
|
+
cause
|
|
1193
|
+
});
|
|
1194
|
+
};
|
|
1195
|
+
const parseYaml = (devicesYaml) => {
|
|
1196
|
+
try {
|
|
1197
|
+
return parse(devicesYaml);
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
const [firstLine] = (error instanceof Error ? error.message : "bad YAML").split("\n");
|
|
1200
|
+
return rejectDevicesFile(firstLine, error);
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
const parseDevicesYaml = (devicesYaml) => {
|
|
1204
|
+
const parsedYaml = parseYaml(devicesYaml);
|
|
1205
|
+
const parsedFile = devicesFileSchema.safeParse(parsedYaml ?? {});
|
|
1206
|
+
if (parsedFile.success) return checkDeviceIds(parsedFile.data);
|
|
1207
|
+
const [firstIssue] = parsedFile.error.issues;
|
|
1208
|
+
const badField = firstIssue.path.join(".");
|
|
1209
|
+
return rejectDevicesFile(`${badField || "file"}: ${firstIssue.message}`, parsedFile.error);
|
|
1210
|
+
};
|
|
1211
|
+
const readDevicesYaml = async (devicesFilePath) => {
|
|
1212
|
+
const devicesYaml = await readStoredFile(devicesFilePath);
|
|
1213
|
+
if (devicesYaml === null) return devicesFileSchema.parse({});
|
|
1214
|
+
return parseDevicesYaml(devicesYaml);
|
|
1215
|
+
};
|
|
1216
|
+
const readDevicesFile = async (rootFolder) => {
|
|
1217
|
+
const devicesFilePath = join(rootFolder, DEVICES_FILE_PATH);
|
|
1218
|
+
return readDevicesYaml(devicesFilePath);
|
|
1219
|
+
};
|
|
1220
|
+
const updateDevicesFile = async (rootFolder, changeDevicesFile) => {
|
|
1221
|
+
const devicesFilePath = join(rootFolder, DEVICES_FILE_PATH);
|
|
1222
|
+
return runOneAtATime(devicesFilePath, async () => {
|
|
1223
|
+
const changedFile = changeDevicesFile(await readDevicesYaml(devicesFilePath));
|
|
1224
|
+
await writeStoredFile(devicesFilePath, stringify(changedFile));
|
|
1225
|
+
return changedFile;
|
|
1226
|
+
});
|
|
1227
|
+
};
|
|
1228
|
+
export { gridUpdateSchema as _, checkNewDevice as a, listCatalogReplies as c, pickGridChanges as d, pickGridDevices as f, devicesFileSchema as g, customDeviceSchema as h, checkDeviceRemoval as i, pickDeviceScreen as l, DEVICE_CATALOG as m, readDevicesFile as n, createPresetFromCustomDevice as o, replaceGrid as p, updateDevicesFile as r, findCatalogPreset as s, parseDevicesYaml as t, pickDeviceViewport as u };
|