mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
fix(mobile): preserve settings across foldable postures
This commit is contained in:
@@ -228,6 +228,8 @@ Frontend JS modules have `@fileoverview` with `@dependency`/`@loadorder` tags. L
|
||||
|
||||
**Theme skins** (App Settings → Display): the `skin` setting selects a palette via a `data-skin` attribute on `<html>`. Values: `daylight-blue` (default), `daylight-green`, `og` (OG Codeman). CSS lives under `[data-skin="…"]` blocks in `styles.css`. To avoid a flash-of-wrong-theme, an **inline pre-paint script** in `index.html` (`<head>`) reads `localStorage['codeman:skin']` and sets `data-skin` before first paint; `settings-ui.js` `applySkin()` applies it live on save (sets `html[data-skin]` + `window.__codemanSkin`, syncs the standalone `codeman:skin` key with the settings blob, and calls terminal-ui.js `applyTerminalSkin()` to re-theme live terminals). `skin` is a **per-device/client-only** setting — it's destructured OUT of the server payload (settings-ui.js, alongside `localEchoEnabled`/`cjkInputEnabled`/`extendedKeyboardBar`), so it does NOT sync across devices.
|
||||
|
||||
**Foldable settings identity**: responsive layout remains width-driven through `MobileDetection.getDeviceType()`, but the localStorage namespace/defaults use `MobileDetection.isHandheldDevice()` so an Android foldable keeps `codeman-app-settings-mobile` after unfolding past the desktop breakpoint. The stable handheld check prefers explicit phone/tablet/desktop UA tokens, then `navigator.userAgentData.mobile`; Android WebView is covered by the `Mobile` UA fallback. Do not switch per-device settings namespaces from instantaneous viewport width — a posture-triggered WebView reload would lose opt-in UI such as `showResponseViewer` and `extendedKeyboardBar`. Regression profile: `OPPO Find N5 (unfolded)` in `test/mobile/devices.ts`.
|
||||
|
||||
**Respawn presets**: `solo-work` (3s/60min), `subagent-workflow` (45s/240min), `team-lead` (90s/480min), `ralph-todo` (8s/480min), `overnight-autonomous` (10s/480min).
|
||||
|
||||
**Keyboard shortcuts**: Escape (close), Ctrl+? (shortcut overlay), Ctrl/Cmd/Alt+K (session palette), Ctrl+W (kill), Ctrl+Tab (next), Alt+[/] (prev/next tab), Alt+1-9 (switch tab), Ctrl+Shift+{/} (move tab left/right), Shift+Enter or Ctrl+Enter (newline), Ctrl+L (clear), Ctrl+Shift+R (restore size), Ctrl+Shift+V (voice input), Ctrl/Cmd +/- (font), Shift+Wheel (local scrollback when mouse passthrough is active). Rebindable via the registry (see Command palette above).
|
||||
@@ -296,7 +298,7 @@ Raw `npx vitest` skips `config/vitest.config.ts`; always use `npm test --` or pa
|
||||
|
||||
**Ports**: Pick unique ports manually. Search `const PORT =` before adding new tests.
|
||||
|
||||
**Respawn tests**: Use `MockSession` from `test/mocks/index.ts` (defined in `test/mocks/mock-session.ts`). **Route tests**: `app.inject({ method, url, payload })` in `test/routes/` — no live port needed. **Mobile tests**: Playwright suite in `test/mobile/` (135 device profiles). Browser-testing infra and practices: `docs/browser-testing-guide.md`.
|
||||
**Respawn tests**: Use `MockSession` from `test/mocks/index.ts` (defined in `test/mocks/mock-session.ts`). **Route tests**: `app.inject({ method, url, payload })` in `test/routes/` — no live port needed. **Mobile tests**: Playwright suite in `test/mobile/` (136 device profiles). Browser-testing infra and practices: `docs/browser-testing-guide.md`.
|
||||
|
||||
## Debugging
|
||||
|
||||
|
||||
@@ -43,6 +43,35 @@ const MobileDetection = {
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check whether this browser belongs to a handheld device.
|
||||
*
|
||||
* Unlike getDeviceType(), this classification must remain stable when a
|
||||
* foldable changes posture. An unfolded phone can expose a desktop-width
|
||||
* viewport, but it still needs the same per-device settings that were saved
|
||||
* while folded. User-Agent Client Hints are preferred where available; the
|
||||
* legacy token fallback covers Android WebView and iPhone browsers.
|
||||
*/
|
||||
isHandheldDevice() {
|
||||
if (!this.isTouchDevice()) return false;
|
||||
|
||||
const userAgent = navigator.userAgent || '';
|
||||
|
||||
// Prefer explicit UA form-factor signals. Besides matching real browsers,
|
||||
// this avoids Chromium emulation reporting userAgentData.mobile=true for
|
||||
// an iPad/tablet context created with isMobile=true.
|
||||
if (/iPad|Tablet|Silk|PlayBook|Kindle|Windows NT|CrOS|Macintosh/i.test(userAgent)) {
|
||||
return false;
|
||||
}
|
||||
if (/Android/i.test(userAgent) && !/Mobile/i.test(userAgent)) return false;
|
||||
if (/Mobi|iPhone|iPod/i.test(userAgent)) return true;
|
||||
|
||||
const uaDataMobile = navigator.userAgentData?.mobile;
|
||||
if (typeof uaDataMobile === 'boolean') return uaDataMobile;
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/** Check if device is iOS (iPhone, iPad, iPod) */
|
||||
isIOS() {
|
||||
return (
|
||||
|
||||
@@ -1758,17 +1758,21 @@ Object.assign(CodemanApp.prototype, {
|
||||
return settings.ralphTrackerEnabled ?? false;
|
||||
},
|
||||
|
||||
// Get the settings storage key based on device type (mobile vs desktop)
|
||||
// Keep the settings namespace stable across foldable posture changes. Layout
|
||||
// still follows viewport width, but an unfolded phone remains the same
|
||||
// handheld device and must not silently switch to desktop preferences.
|
||||
getSettingsStorageKey() {
|
||||
const isMobile = MobileDetection.getDeviceType() === 'mobile';
|
||||
return isMobile ? 'codeman-app-settings-mobile' : 'codeman-app-settings';
|
||||
const isHandheld =
|
||||
MobileDetection.isHandheldDevice?.() ?? MobileDetection.getDeviceType() === 'mobile';
|
||||
return isHandheld ? 'codeman-app-settings-mobile' : 'codeman-app-settings';
|
||||
},
|
||||
|
||||
// Get default settings based on device type
|
||||
// Note: Notification prefs are handled separately by NotificationManager
|
||||
getDefaultSettings() {
|
||||
const isMobile = MobileDetection.getDeviceType() === 'mobile';
|
||||
if (isMobile) {
|
||||
const isHandheld =
|
||||
MobileDetection.isHandheldDevice?.() ?? MobileDetection.getDeviceType() === 'mobile';
|
||||
if (isHandheld) {
|
||||
// Mobile defaults: minimal UI for small screens
|
||||
return {
|
||||
// Header visibility - hide everything on mobile
|
||||
@@ -2180,7 +2184,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
// so mobile defaults to OFF; the desktop blob is untouched and keeps its value.
|
||||
try {
|
||||
if (
|
||||
MobileDetection.getDeviceType() === 'mobile' &&
|
||||
(MobileDetection.isHandheldDevice?.() ?? MobileDetection.getDeviceType() === 'mobile') &&
|
||||
!localStorage.getItem('codeman:planUsagePerDeviceMigrated')
|
||||
) {
|
||||
const s = this.loadAppSettingsFromStorage();
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
Comprehensive mobile UI testing for Codeman's web interface using Playwright with dual-engine support (Chromium + WebKit).
|
||||
|
||||
**325 tests across 135 devices — all passing.**
|
||||
**326 tests across 136 devices — all passing.**
|
||||
|
||||
## Purpose
|
||||
|
||||
Validates Codeman's mobile UI across 135 devices, covering:
|
||||
Validates Codeman's mobile UI across 136 devices, covering:
|
||||
|
||||
- **Keyboard simulation** — 3-layer approach to emulate virtual keyboards in headless browsers
|
||||
- **Touch/swipe interactions** — CDP trusted events (Chromium) + synthetic fallback (WebKit)
|
||||
@@ -26,7 +26,7 @@ npx vitest run --config test/mobile/vitest.config.ts test/mobile/keyboard.test.t
|
||||
# Quick mode — 6 representative devices, skip full matrix
|
||||
CI_QUICK=1 npx vitest run --config test/mobile/vitest.config.ts
|
||||
|
||||
# Full device matrix only (135 devices)
|
||||
# Full device matrix only (136 devices)
|
||||
npx vitest run --config test/mobile/vitest.config.ts test/mobile/device-matrix.test.ts
|
||||
|
||||
# Update visual baselines (delete old baselines, re-run)
|
||||
@@ -43,7 +43,7 @@ npx vitest run --config test/mobile/vitest.config.ts test/mobile/visual-regressi
|
||||
| `subagent-windows.test.ts` | 3202 | Mobile subagent card dimensions, stacking, interactions |
|
||||
| `settings.test.ts` | 3203 | Settings modal, mobile defaults, persistence |
|
||||
| `layout.test.ts` | 3204 | General mobile layout, fixed elements, device classes |
|
||||
| `device-matrix.test.ts` | 3205 | Cross-device parametric tests (135 devices) |
|
||||
| `device-matrix.test.ts` | 3205 | Cross-device parametric tests (136 devices) |
|
||||
| `visual-regression.test.ts` | 3206 | Screenshot comparison at key breakpoints |
|
||||
| `accessibility.test.ts` | 3207 | WCAG touch targets, zoom, focus, ARIA |
|
||||
|
||||
@@ -58,7 +58,7 @@ npx vitest run --config test/mobile/vitest.config.ts test/mobile/visual-regressi
|
||||
| standard-tablet | 768–834px | ~8 | iPad Mini |
|
||||
| large-tablet | 835px+ | ~5 | iPad Pro 11" |
|
||||
|
||||
135 devices are defined in `devices.ts` — 68 from Playwright's built-in device profiles plus 67 custom entries for newer devices (iPhone 16/17, Pixel 9, Galaxy S25, iPad Air M2, Surface Pro, etc.).
|
||||
136 devices are defined in `devices.ts` — 68 from Playwright's built-in device profiles plus 68 custom entries for newer devices (iPhone 16/17, Pixel 9, Galaxy S25, OPPO Find N5 unfolded, iPad Air M2, Surface Pro, etc.).
|
||||
|
||||
### How Devices Are Differentiated
|
||||
|
||||
@@ -98,7 +98,7 @@ Test File
|
||||
├─ helpers/touch-sim.ts → CDP trusted touch / synthetic fallback
|
||||
├─ helpers/assertions.ts → Layout, CSS, accessibility assertions
|
||||
├─ helpers/visual.ts → pixelmatch screenshot comparison
|
||||
└─ devices.ts → 135-device registry
|
||||
└─ devices.ts → 136-device registry
|
||||
```
|
||||
|
||||
### Keyboard Simulation — 3-Layer Approach
|
||||
|
||||
+168
-157
@@ -1,6 +1,7 @@
|
||||
import { devices as playwrightDevices } from 'playwright';
|
||||
|
||||
export type DeviceCategory = 'small-phone' | 'standard-phone' | 'large-phone' | 'small-tablet' | 'standard-tablet' | 'large-tablet';
|
||||
export type DeviceCategory =
|
||||
'small-phone' | 'standard-phone' | 'large-phone' | 'small-tablet' | 'standard-tablet' | 'large-tablet';
|
||||
|
||||
export interface DeviceEntry {
|
||||
name: string;
|
||||
@@ -55,14 +56,7 @@ function fromPlaywright(name: string): DeviceEntry | null {
|
||||
}
|
||||
|
||||
/** Create a custom DeviceEntry for devices not in Playwright. */
|
||||
function custom(
|
||||
name: string,
|
||||
width: number,
|
||||
height: number,
|
||||
dpr: number,
|
||||
ua: string,
|
||||
isIOS: boolean,
|
||||
): DeviceEntry {
|
||||
function custom(name: string, width: number, height: number, dpr: number, ua: string, isIOS: boolean): DeviceEntry {
|
||||
return {
|
||||
name,
|
||||
category: categoryFor(width),
|
||||
@@ -83,89 +77,89 @@ function custom(
|
||||
|
||||
const PLAYWRIGHT_DEVICE_NAMES = [
|
||||
// Small phones (<375px)
|
||||
'iPhone SE', // 320x568
|
||||
'Galaxy S9+', // 320x658
|
||||
'Nokia Lumia 520', // 320x533
|
||||
'Galaxy S III', // 360x640
|
||||
'Galaxy Note 3', // 360x640
|
||||
'Galaxy Note II', // 360x640
|
||||
'Galaxy S5', // 360x640
|
||||
'Galaxy S8', // 360x740
|
||||
'Galaxy S24', // 360x780
|
||||
'BlackBerry Z30', // 360x640
|
||||
'iPhone SE', // 320x568
|
||||
'Galaxy S9+', // 320x658
|
||||
'Nokia Lumia 520', // 320x533
|
||||
'Galaxy S III', // 360x640
|
||||
'Galaxy Note 3', // 360x640
|
||||
'Galaxy Note II', // 360x640
|
||||
'Galaxy S5', // 360x640
|
||||
'Galaxy S8', // 360x740
|
||||
'Galaxy S24', // 360x780
|
||||
'BlackBerry Z30', // 360x640
|
||||
'Microsoft Lumia 550', // 360x640
|
||||
'Microsoft Lumia 950', // 360x640
|
||||
'Nexus 5', // 360x640
|
||||
'Moto G4', // 360x640
|
||||
'Pixel 4', // 353x745
|
||||
'Nexus 5', // 360x640
|
||||
'Moto G4', // 360x640
|
||||
'Pixel 4', // 353x745
|
||||
|
||||
// Standard phones (375-429px)
|
||||
'iPhone 6', // 375x667
|
||||
'iPhone 7', // 375x667
|
||||
'iPhone 8', // 375x667
|
||||
'iPhone 6', // 375x667
|
||||
'iPhone 7', // 375x667
|
||||
'iPhone 8', // 375x667
|
||||
'iPhone SE (3rd gen)', // 375x667
|
||||
'iPhone X', // 375x812
|
||||
'iPhone 11 Pro', // 375x635
|
||||
'iPhone 12 Mini', // 375x629
|
||||
'iPhone 13 Mini', // 375x629
|
||||
'LG Optimus L70', // 384x640
|
||||
'Nexus 4', // 384x640
|
||||
'iPhone 12', // 390x664
|
||||
'iPhone 12 Pro', // 390x664
|
||||
'iPhone 13', // 390x664
|
||||
'iPhone 13 Pro', // 390x664
|
||||
'iPhone 14', // 390x664
|
||||
'iPhone 14 Pro', // 393x660
|
||||
'iPhone 15', // 393x659
|
||||
'iPhone 15 Pro', // 393x659
|
||||
'Pixel 3', // 393x786
|
||||
'Pixel 5', // 393x727
|
||||
'Pixel 2', // 411x731
|
||||
'Pixel 2 XL', // 411x823
|
||||
'Pixel 7', // 412x839
|
||||
'Pixel 4a (5G)', // 412x765
|
||||
'Nexus 5X', // 412x732
|
||||
'Nexus 6', // 412x732
|
||||
'Nexus 6P', // 412x732
|
||||
'iPhone 6 Plus', // 414x736
|
||||
'iPhone 7 Plus', // 414x736
|
||||
'iPhone 8 Plus', // 414x736
|
||||
'iPhone XR', // 414x896
|
||||
'iPhone 11', // 414x715
|
||||
'iPhone 11 Pro Max', // 414x715
|
||||
'iPhone 12 Pro Max', // 428x746
|
||||
'iPhone 13 Pro Max', // 428x746
|
||||
'iPhone 14 Plus', // 428x746
|
||||
'iPhone X', // 375x812
|
||||
'iPhone 11 Pro', // 375x635
|
||||
'iPhone 12 Mini', // 375x629
|
||||
'iPhone 13 Mini', // 375x629
|
||||
'LG Optimus L70', // 384x640
|
||||
'Nexus 4', // 384x640
|
||||
'iPhone 12', // 390x664
|
||||
'iPhone 12 Pro', // 390x664
|
||||
'iPhone 13', // 390x664
|
||||
'iPhone 13 Pro', // 390x664
|
||||
'iPhone 14', // 390x664
|
||||
'iPhone 14 Pro', // 393x660
|
||||
'iPhone 15', // 393x659
|
||||
'iPhone 15 Pro', // 393x659
|
||||
'Pixel 3', // 393x786
|
||||
'Pixel 5', // 393x727
|
||||
'Pixel 2', // 411x731
|
||||
'Pixel 2 XL', // 411x823
|
||||
'Pixel 7', // 412x839
|
||||
'Pixel 4a (5G)', // 412x765
|
||||
'Nexus 5X', // 412x732
|
||||
'Nexus 6', // 412x732
|
||||
'Nexus 6P', // 412x732
|
||||
'iPhone 6 Plus', // 414x736
|
||||
'iPhone 7 Plus', // 414x736
|
||||
'iPhone 8 Plus', // 414x736
|
||||
'iPhone XR', // 414x896
|
||||
'iPhone 11', // 414x715
|
||||
'iPhone 11 Pro Max', // 414x715
|
||||
'iPhone 12 Pro Max', // 428x746
|
||||
'iPhone 13 Pro Max', // 428x746
|
||||
'iPhone 14 Plus', // 428x746
|
||||
|
||||
// Large phones (430-599px)
|
||||
'iPhone 14 Pro Max', // 430x740
|
||||
'iPhone 15 Plus', // 430x739
|
||||
'iPhone 15 Pro Max', // 430x739
|
||||
'Galaxy A55', // 480x1040
|
||||
'Nokia N9', // 480x854
|
||||
'iPhone 14 Pro Max', // 430x740
|
||||
'iPhone 15 Plus', // 430x739
|
||||
'iPhone 15 Pro Max', // 430x739
|
||||
'Galaxy A55', // 480x1040
|
||||
'Nokia N9', // 480x854
|
||||
|
||||
// Small tablets (600-767px)
|
||||
'Blackberry PlayBook', // 600x1024
|
||||
'Nexus 7', // 600x960
|
||||
'Galaxy Tab S9', // 640x1024
|
||||
'iPad (gen 11)', // 656x944
|
||||
'Galaxy Tab S4', // 712x1138
|
||||
'Nexus 7', // 600x960
|
||||
'Galaxy Tab S9', // 640x1024
|
||||
'iPad (gen 11)', // 656x944
|
||||
'Galaxy Tab S4', // 712x1138
|
||||
|
||||
// Standard tablets (768-834px)
|
||||
'iPad (gen 5)', // 768x1024
|
||||
'iPad (gen 6)', // 768x1024
|
||||
'iPad Mini', // 768x1024
|
||||
'Kindle Fire HDX', // 800x1280
|
||||
'Nexus 10', // 800x1280
|
||||
'iPad (gen 7)', // 810x1080
|
||||
'iPad (gen 5)', // 768x1024
|
||||
'iPad (gen 6)', // 768x1024
|
||||
'iPad Mini', // 768x1024
|
||||
'Kindle Fire HDX', // 800x1280
|
||||
'Nexus 10', // 800x1280
|
||||
'iPad (gen 7)', // 810x1080
|
||||
|
||||
// Large tablets (834px+)
|
||||
'iPad Pro 11', // 834x1194
|
||||
'iPad Pro 11', // 834x1194
|
||||
];
|
||||
|
||||
const playwrightEntries: DeviceEntry[] = PLAYWRIGHT_DEVICE_NAMES
|
||||
.map(n => fromPlaywright(n))
|
||||
.filter((d): d is DeviceEntry => d !== null);
|
||||
const playwrightEntries: DeviceEntry[] = PLAYWRIGHT_DEVICE_NAMES.map((n) => fromPlaywright(n)).filter(
|
||||
(d): d is DeviceEntry => d !== null
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Custom devices — newer models and those missing from Playwright
|
||||
@@ -185,84 +179,101 @@ const ANDROID_TABLET_UA = (androidVer: string, model: string) =>
|
||||
|
||||
const customEntries: DeviceEntry[] = [
|
||||
// ── Small phones (<375px) ──────────────────────────────────────────────
|
||||
custom('iPhone 5', 320, 568, 2, IOS_MOBILE_UA('10_3_4'), true),
|
||||
custom('iPhone 5s', 320, 568, 2, IOS_MOBILE_UA('12_5_7'), true),
|
||||
custom('iPhone 5c', 320, 568, 2, IOS_MOBILE_UA('10_3_3'), true),
|
||||
custom('iPod Touch (7th gen)',320, 568, 2, IOS_MOBILE_UA('15_8'), true),
|
||||
custom('Galaxy Y', 240, 320, 1, ANDROID_MOBILE_UA('2.3.6', 'GT-S5360'), false),
|
||||
custom('Galaxy Ace', 320, 480, 1, ANDROID_MOBILE_UA('2.3.7', 'GT-S5830'), false),
|
||||
custom('Pixel 4a', 353, 745, 2.75, ANDROID_MOBILE_UA('12', 'Pixel 4a'), false),
|
||||
custom('iPhone 5', 320, 568, 2, IOS_MOBILE_UA('10_3_4'), true),
|
||||
custom('iPhone 5s', 320, 568, 2, IOS_MOBILE_UA('12_5_7'), true),
|
||||
custom('iPhone 5c', 320, 568, 2, IOS_MOBILE_UA('10_3_3'), true),
|
||||
custom('iPod Touch (7th gen)', 320, 568, 2, IOS_MOBILE_UA('15_8'), true),
|
||||
custom('Galaxy Y', 240, 320, 1, ANDROID_MOBILE_UA('2.3.6', 'GT-S5360'), false),
|
||||
custom('Galaxy Ace', 320, 480, 1, ANDROID_MOBILE_UA('2.3.7', 'GT-S5830'), false),
|
||||
custom('Pixel 4a', 353, 745, 2.75, ANDROID_MOBILE_UA('12', 'Pixel 4a'), false),
|
||||
|
||||
// ── Standard phones (375-429px) ────────────────────────────────────────
|
||||
custom('iPhone 16', 393, 659, 3, IOS_MOBILE_UA('18_0'), true),
|
||||
custom('iPhone 16 Pro', 402, 674, 3, IOS_MOBILE_UA('18_0'), true),
|
||||
custom('Galaxy S20', 360, 800, 3, ANDROID_MOBILE_UA('12', 'SM-G980F'), false),
|
||||
custom('Galaxy S20 FE', 360, 800, 3, ANDROID_MOBILE_UA('13', 'SM-G780F'), false),
|
||||
custom('Galaxy S21', 360, 800, 3, ANDROID_MOBILE_UA('13', 'SM-G991B'), false),
|
||||
custom('Galaxy S21 FE', 360, 800, 3, ANDROID_MOBILE_UA('14', 'SM-G990B'), false),
|
||||
custom('Galaxy S22', 360, 780, 3, ANDROID_MOBILE_UA('14', 'SM-S901B'), false),
|
||||
custom('Galaxy S23', 360, 780, 3, ANDROID_MOBILE_UA('14', 'SM-S911B'), false),
|
||||
custom('Galaxy S24 FE', 360, 780, 3, ANDROID_MOBILE_UA('14', 'SM-S721B'), false),
|
||||
custom('Galaxy A54', 360, 800, 3, ANDROID_MOBILE_UA('14', 'SM-A546B'), false),
|
||||
custom('Galaxy A34', 360, 800, 2.625, ANDROID_MOBILE_UA('14', 'SM-A346B'), false),
|
||||
custom('Galaxy A14', 384, 854, 1.5, ANDROID_MOBILE_UA('13', 'SM-A145F'), false),
|
||||
custom('Galaxy Z Flip 5', 412, 919, 2.625, ANDROID_MOBILE_UA('14', 'SM-F731B'), false),
|
||||
custom('Galaxy Z Flip 4', 412, 919, 2.625, ANDROID_MOBILE_UA('14', 'SM-F721B'), false),
|
||||
custom('Pixel 6', 412, 915, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 6'), false),
|
||||
custom('Pixel 6a', 412, 892, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 6a'), false),
|
||||
custom('Pixel 7a', 412, 892, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 7a'), false),
|
||||
custom('Pixel 8', 412, 915, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 8'), false),
|
||||
custom('Pixel 8a', 412, 892, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 8a'), false),
|
||||
custom('Pixel 9', 412, 923, 2.75, ANDROID_MOBILE_UA('15', 'Pixel 9'), false),
|
||||
custom('OnePlus 12', 412, 915, 2.625, ANDROID_MOBILE_UA('14', 'CPH2581'), false),
|
||||
custom('OnePlus Nord 3', 412, 915, 2.625, ANDROID_MOBILE_UA('14', 'CPH2491'), false),
|
||||
custom('Xiaomi 14', 393, 873, 2.75, ANDROID_MOBILE_UA('14', '23127PN0CC'), false),
|
||||
custom('Xiaomi Redmi Note 13',393, 873, 2.75, ANDROID_MOBILE_UA('14', '23106RN0DA'), false),
|
||||
custom('Nothing Phone (2)', 412, 915, 2.625, ANDROID_MOBILE_UA('14', 'A065'), false),
|
||||
custom('Sony Xperia 1 V', 411, 960, 2.625, ANDROID_MOBILE_UA('14', 'XQ-DQ72'), false),
|
||||
custom('iPhone 16', 393, 659, 3, IOS_MOBILE_UA('18_0'), true),
|
||||
custom('iPhone 16 Pro', 402, 674, 3, IOS_MOBILE_UA('18_0'), true),
|
||||
custom('Galaxy S20', 360, 800, 3, ANDROID_MOBILE_UA('12', 'SM-G980F'), false),
|
||||
custom('Galaxy S20 FE', 360, 800, 3, ANDROID_MOBILE_UA('13', 'SM-G780F'), false),
|
||||
custom('Galaxy S21', 360, 800, 3, ANDROID_MOBILE_UA('13', 'SM-G991B'), false),
|
||||
custom('Galaxy S21 FE', 360, 800, 3, ANDROID_MOBILE_UA('14', 'SM-G990B'), false),
|
||||
custom('Galaxy S22', 360, 780, 3, ANDROID_MOBILE_UA('14', 'SM-S901B'), false),
|
||||
custom('Galaxy S23', 360, 780, 3, ANDROID_MOBILE_UA('14', 'SM-S911B'), false),
|
||||
custom('Galaxy S24 FE', 360, 780, 3, ANDROID_MOBILE_UA('14', 'SM-S721B'), false),
|
||||
custom('Galaxy A54', 360, 800, 3, ANDROID_MOBILE_UA('14', 'SM-A546B'), false),
|
||||
custom('Galaxy A34', 360, 800, 2.625, ANDROID_MOBILE_UA('14', 'SM-A346B'), false),
|
||||
custom('Galaxy A14', 384, 854, 1.5, ANDROID_MOBILE_UA('13', 'SM-A145F'), false),
|
||||
custom('Galaxy Z Flip 5', 412, 919, 2.625, ANDROID_MOBILE_UA('14', 'SM-F731B'), false),
|
||||
custom('Galaxy Z Flip 4', 412, 919, 2.625, ANDROID_MOBILE_UA('14', 'SM-F721B'), false),
|
||||
custom('Pixel 6', 412, 915, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 6'), false),
|
||||
custom('Pixel 6a', 412, 892, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 6a'), false),
|
||||
custom('Pixel 7a', 412, 892, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 7a'), false),
|
||||
custom('Pixel 8', 412, 915, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 8'), false),
|
||||
custom('Pixel 8a', 412, 892, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 8a'), false),
|
||||
custom('Pixel 9', 412, 923, 2.75, ANDROID_MOBILE_UA('15', 'Pixel 9'), false),
|
||||
custom('OnePlus 12', 412, 915, 2.625, ANDROID_MOBILE_UA('14', 'CPH2581'), false),
|
||||
custom('OnePlus Nord 3', 412, 915, 2.625, ANDROID_MOBILE_UA('14', 'CPH2491'), false),
|
||||
custom('Xiaomi 14', 393, 873, 2.75, ANDROID_MOBILE_UA('14', '23127PN0CC'), false),
|
||||
custom('Xiaomi Redmi Note 13', 393, 873, 2.75, ANDROID_MOBILE_UA('14', '23106RN0DA'), false),
|
||||
custom('Nothing Phone (2)', 412, 915, 2.625, ANDROID_MOBILE_UA('14', 'A065'), false),
|
||||
custom('Sony Xperia 1 V', 411, 960, 2.625, ANDROID_MOBILE_UA('14', 'XQ-DQ72'), false),
|
||||
|
||||
// ── Large phones (430-599px) ───────────────────────────────────────────
|
||||
custom('iPhone 16 Plus', 430, 739, 3, IOS_MOBILE_UA('18_0'), true),
|
||||
custom('iPhone 16 Pro Max', 440, 756, 3, IOS_MOBILE_UA('18_0'), true),
|
||||
custom('Galaxy S20 Ultra', 432, 960, 3, ANDROID_MOBILE_UA('13', 'SM-G988B'), false),
|
||||
custom('Galaxy S21 Ultra', 432, 960, 3, ANDROID_MOBILE_UA('13', 'SM-G998B'), false),
|
||||
custom('Galaxy S22 Ultra', 432, 960, 3, ANDROID_MOBILE_UA('14', 'SM-S908B'), false),
|
||||
custom('Galaxy S23 Ultra', 432, 960, 3, ANDROID_MOBILE_UA('14', 'SM-S918B'), false),
|
||||
custom('Galaxy S24 Ultra', 432, 960, 3, ANDROID_MOBILE_UA('14', 'SM-S928B'), false),
|
||||
custom('Galaxy Z Fold 5', 460, 1016, 2.5, ANDROID_MOBILE_UA('14', 'SM-F946B'), false),
|
||||
custom('Pixel 6 Pro', 440, 990, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 6 Pro'), false),
|
||||
custom('Pixel 7 Pro', 440, 990, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 7 Pro'), false),
|
||||
custom('Pixel 8 Pro', 448, 998, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 8 Pro'), false),
|
||||
custom('Pixel 9 Pro XL', 448, 998, 2.75, ANDROID_MOBILE_UA('15', 'Pixel 9 Pro XL'), false),
|
||||
custom('OnePlus 12 Pro', 440, 990, 2.625, ANDROID_MOBILE_UA('14', 'CPH2583'), false),
|
||||
custom('iPhone 16 Plus', 430, 739, 3, IOS_MOBILE_UA('18_0'), true),
|
||||
custom('iPhone 16 Pro Max', 440, 756, 3, IOS_MOBILE_UA('18_0'), true),
|
||||
custom('Galaxy S20 Ultra', 432, 960, 3, ANDROID_MOBILE_UA('13', 'SM-G988B'), false),
|
||||
custom('Galaxy S21 Ultra', 432, 960, 3, ANDROID_MOBILE_UA('13', 'SM-G998B'), false),
|
||||
custom('Galaxy S22 Ultra', 432, 960, 3, ANDROID_MOBILE_UA('14', 'SM-S908B'), false),
|
||||
custom('Galaxy S23 Ultra', 432, 960, 3, ANDROID_MOBILE_UA('14', 'SM-S918B'), false),
|
||||
custom('Galaxy S24 Ultra', 432, 960, 3, ANDROID_MOBILE_UA('14', 'SM-S928B'), false),
|
||||
custom('Galaxy Z Fold 5', 460, 1016, 2.5, ANDROID_MOBILE_UA('14', 'SM-F946B'), false),
|
||||
custom('Pixel 6 Pro', 440, 990, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 6 Pro'), false),
|
||||
custom('Pixel 7 Pro', 440, 990, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 7 Pro'), false),
|
||||
custom('Pixel 8 Pro', 448, 998, 2.625, ANDROID_MOBILE_UA('14', 'Pixel 8 Pro'), false),
|
||||
custom('Pixel 9 Pro XL', 448, 998, 2.75, ANDROID_MOBILE_UA('15', 'Pixel 9 Pro XL'), false),
|
||||
custom('OnePlus 12 Pro', 440, 990, 2.625, ANDROID_MOBILE_UA('14', 'CPH2583'), false),
|
||||
|
||||
// ── Small tablets (600-767px) ──────────────────────────────────────────
|
||||
custom('Galaxy Tab A8', 600, 1024, 1.5, ANDROID_TABLET_UA('14', 'SM-X200'), false),
|
||||
custom('Galaxy Tab S6 Lite', 600, 1024, 1.5, ANDROID_TABLET_UA('14', 'SM-P613'), false),
|
||||
custom('Galaxy Tab A7 Lite', 600, 960, 1.5, ANDROID_TABLET_UA('13', 'SM-T220'), false),
|
||||
custom('Kindle Fire HD 8', 600, 1024, 1.5, 'Mozilla/5.0 (Linux; Android 11; KFRAPWI) AppleWebKit/537.36 (KHTML, like Gecko) Silk/110.1.4 like Chrome/110.0.5481.154 Safari/537.36', false),
|
||||
custom('Lenovo Tab M10', 600, 1024, 1.5, ANDROID_TABLET_UA('12', 'TB-X606F'), false),
|
||||
custom('Xiaomi Pad 6', 600, 960, 2, ANDROID_TABLET_UA('14', '23043RP34G'), false),
|
||||
custom('Galaxy Tab A8', 600, 1024, 1.5, ANDROID_TABLET_UA('14', 'SM-X200'), false),
|
||||
custom('Galaxy Tab S6 Lite', 600, 1024, 1.5, ANDROID_TABLET_UA('14', 'SM-P613'), false),
|
||||
custom('Galaxy Tab A7 Lite', 600, 960, 1.5, ANDROID_TABLET_UA('13', 'SM-T220'), false),
|
||||
custom(
|
||||
'Kindle Fire HD 8',
|
||||
600,
|
||||
1024,
|
||||
1.5,
|
||||
'Mozilla/5.0 (Linux; Android 11; KFRAPWI) AppleWebKit/537.36 (KHTML, like Gecko) Silk/110.1.4 like Chrome/110.0.5481.154 Safari/537.36',
|
||||
false
|
||||
),
|
||||
custom('Lenovo Tab M10', 600, 1024, 1.5, ANDROID_TABLET_UA('12', 'TB-X606F'), false),
|
||||
custom('Xiaomi Pad 6', 600, 960, 2, ANDROID_TABLET_UA('14', '23043RP34G'), false),
|
||||
|
||||
// ── Standard tablets (768-834px) ───────────────────────────────────────
|
||||
custom('iPad Air (5th gen)', 820, 1180, 2, IPAD_UA('16_0'), true),
|
||||
custom('iPad (9th gen)', 810, 1080, 2, IPAD_UA('16_0'), true),
|
||||
custom('iPad (10th gen)', 820, 1180, 2, IPAD_UA('16_0'), true),
|
||||
custom('iPad Mini (6th gen)', 768, 1024, 2, IPAD_UA('16_0'), true),
|
||||
custom('Galaxy Tab S7', 800, 1280, 2, ANDROID_TABLET_UA('13', 'SM-T870'), false),
|
||||
custom('Galaxy Tab S8', 800, 1280, 2, ANDROID_TABLET_UA('14', 'SM-X700'), false),
|
||||
custom('iPad Air (5th gen)', 820, 1180, 2, IPAD_UA('16_0'), true),
|
||||
custom('iPad (9th gen)', 810, 1080, 2, IPAD_UA('16_0'), true),
|
||||
custom('iPad (10th gen)', 820, 1180, 2, IPAD_UA('16_0'), true),
|
||||
custom('iPad Mini (6th gen)', 768, 1024, 2, IPAD_UA('16_0'), true),
|
||||
custom('Galaxy Tab S7', 800, 1280, 2, ANDROID_TABLET_UA('13', 'SM-T870'), false),
|
||||
custom('Galaxy Tab S8', 800, 1280, 2, ANDROID_TABLET_UA('14', 'SM-X700'), false),
|
||||
|
||||
// ── Large tablets (834px+) ──────────────────────────────────────────────
|
||||
custom('iPad Pro 12.9 (6th gen)', 1024, 1366, 2, IPAD_UA('16_0'), true),
|
||||
custom('iPad Pro 11 (4th gen)', 834, 1194, 2, IPAD_UA('16_0'), true),
|
||||
custom('iPad Air (M2)', 834, 1194, 2, IPAD_UA('17_0'), true),
|
||||
custom('Surface Pro 7', 912, 1368, 2,
|
||||
'Mozilla/5.0 (Windows NT 10.0; ARM; Surface Pro 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.7632.6 Safari/537.36 Edg/145.0.0.0', false),
|
||||
custom('Galaxy Tab S8+', 840, 1344, 2.25, ANDROID_TABLET_UA('14', 'SM-X800'), false),
|
||||
custom('Galaxy Tab S9+', 840, 1344, 2.25, ANDROID_TABLET_UA('14', 'SM-X810'), false),
|
||||
custom('Galaxy Tab S9 Ultra', 900, 1440, 2.25, ANDROID_TABLET_UA('14', 'SM-X910'), false),
|
||||
custom('Pixel Tablet', 888, 1280, 2, ANDROID_TABLET_UA('14', 'GPD8'), false),
|
||||
custom('Lenovo Tab P12 Pro', 900, 1440, 2, ANDROID_TABLET_UA('13', 'TB-Q706F'), false),
|
||||
custom('iPad Pro 11 (4th gen)', 834, 1194, 2, IPAD_UA('16_0'), true),
|
||||
custom('iPad Air (M2)', 834, 1194, 2, IPAD_UA('17_0'), true),
|
||||
custom(
|
||||
'Surface Pro 7',
|
||||
912,
|
||||
1368,
|
||||
2,
|
||||
'Mozilla/5.0 (Windows NT 10.0; ARM; Surface Pro 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.7632.6 Safari/537.36 Edg/145.0.0.0',
|
||||
false
|
||||
),
|
||||
custom('Galaxy Tab S8+', 840, 1344, 2.25, ANDROID_TABLET_UA('14', 'SM-X800'), false),
|
||||
custom('Galaxy Tab S9+', 840, 1344, 2.25, ANDROID_TABLET_UA('14', 'SM-X810'), false),
|
||||
custom('Galaxy Tab S9 Ultra', 900, 1440, 2.25, ANDROID_TABLET_UA('14', 'SM-X910'), false),
|
||||
custom('Pixel Tablet', 888, 1280, 2, ANDROID_TABLET_UA('14', 'GPD8'), false),
|
||||
custom('Lenovo Tab P12 Pro', 900, 1440, 2, ANDROID_TABLET_UA('13', 'TB-Q706F'), false),
|
||||
// Find N5 inner display is 2248x2480 physical pixels. At DPR 2 its full-
|
||||
// resolution CSS viewport crosses Codeman's desktop breakpoint while the
|
||||
// browser remains a mobile/touch device.
|
||||
custom('OPPO Find N5 (unfolded)', 1124, 1240, 2, ANDROID_MOBILE_UA('15', 'CPH2671'), false),
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -276,29 +287,29 @@ export const DEVICE_REGISTRY: DeviceEntry[] = [...playwrightEntries, ...customEn
|
||||
// Per-category exports
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const SMALL_PHONES: DeviceEntry[] = DEVICE_REGISTRY.filter(d => d.category === 'small-phone');
|
||||
export const STANDARD_PHONES: DeviceEntry[] = DEVICE_REGISTRY.filter(d => d.category === 'standard-phone');
|
||||
export const LARGE_PHONES: DeviceEntry[] = DEVICE_REGISTRY.filter(d => d.category === 'large-phone');
|
||||
export const SMALL_TABLETS: DeviceEntry[] = DEVICE_REGISTRY.filter(d => d.category === 'small-tablet');
|
||||
export const STANDARD_TABLETS: DeviceEntry[] = DEVICE_REGISTRY.filter(d => d.category === 'standard-tablet');
|
||||
export const LARGE_TABLETS: DeviceEntry[] = DEVICE_REGISTRY.filter(d => d.category === 'large-tablet');
|
||||
export const SMALL_PHONES: DeviceEntry[] = DEVICE_REGISTRY.filter((d) => d.category === 'small-phone');
|
||||
export const STANDARD_PHONES: DeviceEntry[] = DEVICE_REGISTRY.filter((d) => d.category === 'standard-phone');
|
||||
export const LARGE_PHONES: DeviceEntry[] = DEVICE_REGISTRY.filter((d) => d.category === 'large-phone');
|
||||
export const SMALL_TABLETS: DeviceEntry[] = DEVICE_REGISTRY.filter((d) => d.category === 'small-tablet');
|
||||
export const STANDARD_TABLETS: DeviceEntry[] = DEVICE_REGISTRY.filter((d) => d.category === 'standard-tablet');
|
||||
export const LARGE_TABLETS: DeviceEntry[] = DEVICE_REGISTRY.filter((d) => d.category === 'large-tablet');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Platform exports
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const IOS_DEVICES: DeviceEntry[] = DEVICE_REGISTRY.filter(d => d.isIOS);
|
||||
export const ANDROID_DEVICES: DeviceEntry[] = DEVICE_REGISTRY.filter(d => !d.isIOS);
|
||||
export const IOS_DEVICES: DeviceEntry[] = DEVICE_REGISTRY.filter((d) => d.isIOS);
|
||||
export const ANDROID_DEVICES: DeviceEntry[] = DEVICE_REGISTRY.filter((d) => !d.isIOS);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Representative devices — one per category for quick smoke tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const REPRESENTATIVE_DEVICES: Record<DeviceCategory, DeviceEntry> = {
|
||||
'small-phone': SMALL_PHONES.find(d => d.name === 'iPhone SE')!,
|
||||
'standard-phone': STANDARD_PHONES.find(d => d.name === 'iPhone 14 Pro')!,
|
||||
'large-phone': LARGE_PHONES.find(d => d.name === 'iPhone 15 Pro Max')!,
|
||||
'small-tablet': SMALL_TABLETS.find(d => d.name === 'Nexus 7')!,
|
||||
'standard-tablet': STANDARD_TABLETS.find(d => d.name === 'iPad Mini')!,
|
||||
'large-tablet': LARGE_TABLETS.find(d => d.name === 'iPad Pro 11')!,
|
||||
'small-phone': SMALL_PHONES.find((d) => d.name === 'iPhone SE')!,
|
||||
'standard-phone': STANDARD_PHONES.find((d) => d.name === 'iPhone 14 Pro')!,
|
||||
'large-phone': LARGE_PHONES.find((d) => d.name === 'iPhone 15 Pro Max')!,
|
||||
'small-tablet': SMALL_TABLETS.find((d) => d.name === 'Nexus 7')!,
|
||||
'standard-tablet': STANDARD_TABLETS.find((d) => d.name === 'iPad Mini')!,
|
||||
'large-tablet': LARGE_TABLETS.find((d) => d.name === 'iPad Pro 11')!,
|
||||
};
|
||||
|
||||
@@ -5,10 +5,8 @@ import { PORTS, SELECTORS, KEYBOARD, STORAGE_KEYS, BODY_CLASSES, WAIT } from './
|
||||
import { createTestServer, stopTestServer } from './helpers/server.js';
|
||||
import { createDevicePage, closeAllBrowsers } from './helpers/browser.js';
|
||||
import { showKeyboard, hideKeyboard } from './helpers/keyboard-sim.js';
|
||||
import {
|
||||
assertVisible, assertHidden, getCSSProperty, getCSSNumericValue,
|
||||
} from './helpers/assertions.js';
|
||||
import { REPRESENTATIVE_DEVICES } from './devices.js';
|
||||
import { assertVisible, assertHidden, getCSSProperty, getCSSNumericValue } from './helpers/assertions.js';
|
||||
import { DEVICE_REGISTRY, REPRESENTATIVE_DEVICES } from './devices.js';
|
||||
import type { WebServer } from '../src/web/server.js';
|
||||
|
||||
const PORT = PORTS.SETTINGS;
|
||||
@@ -94,9 +92,7 @@ describe('Settings Modal', () => {
|
||||
if (gearBox && toolbarBox) {
|
||||
// Gear button should be within toolbar's vertical range
|
||||
expect(gearBox.y).toBeGreaterThanOrEqual(toolbarBox.y - 5);
|
||||
expect(gearBox.y + gearBox.height).toBeLessThanOrEqual(
|
||||
toolbarBox.y + toolbarBox.height + 5,
|
||||
);
|
||||
expect(gearBox.y + gearBox.height).toBeLessThanOrEqual(toolbarBox.y + toolbarBox.height + 5);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -304,11 +300,14 @@ describe('Settings Modal', () => {
|
||||
try {
|
||||
// Store a test setting
|
||||
await page.evaluate((key) => {
|
||||
localStorage.setItem(key, JSON.stringify({
|
||||
showFontControls: true,
|
||||
showMonitor: true,
|
||||
subagentTrackingEnabled: true,
|
||||
}));
|
||||
localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify({
|
||||
showFontControls: true,
|
||||
showMonitor: true,
|
||||
subagentTrackingEnabled: true,
|
||||
})
|
||||
);
|
||||
}, STORAGE_KEYS.SETTINGS_MOBILE);
|
||||
|
||||
// Reload page
|
||||
@@ -335,20 +334,32 @@ describe('Settings Modal', () => {
|
||||
|
||||
try {
|
||||
// Store both mobile and desktop settings
|
||||
await page.evaluate(({ mobileKey, desktopKey, notifKey }) => {
|
||||
localStorage.setItem(mobileKey, JSON.stringify({ showFontControls: false }));
|
||||
localStorage.setItem(desktopKey, JSON.stringify({ showFontControls: true }));
|
||||
localStorage.setItem(notifKey, JSON.stringify({ mobileNotif: true }));
|
||||
}, {
|
||||
mobileKey: STORAGE_KEYS.SETTINGS_MOBILE,
|
||||
desktopKey: STORAGE_KEYS.SETTINGS_DESKTOP,
|
||||
notifKey: STORAGE_KEYS.NOTIFICATION_PREFS_MOBILE,
|
||||
});
|
||||
await page.evaluate(
|
||||
({ mobileKey, desktopKey, notifKey }) => {
|
||||
localStorage.setItem(mobileKey, JSON.stringify({ showFontControls: false }));
|
||||
localStorage.setItem(desktopKey, JSON.stringify({ showFontControls: true }));
|
||||
localStorage.setItem(notifKey, JSON.stringify({ mobileNotif: true }));
|
||||
},
|
||||
{
|
||||
mobileKey: STORAGE_KEYS.SETTINGS_MOBILE,
|
||||
desktopKey: STORAGE_KEYS.SETTINGS_DESKTOP,
|
||||
notifKey: STORAGE_KEYS.NOTIFICATION_PREFS_MOBILE,
|
||||
}
|
||||
);
|
||||
|
||||
// Verify they are independent
|
||||
const mobile = await page.evaluate((key) => JSON.parse(localStorage.getItem(key) || '{}'), STORAGE_KEYS.SETTINGS_MOBILE);
|
||||
const desktop = await page.evaluate((key) => JSON.parse(localStorage.getItem(key) || '{}'), STORAGE_KEYS.SETTINGS_DESKTOP);
|
||||
const notif = await page.evaluate((key) => JSON.parse(localStorage.getItem(key) || '{}'), STORAGE_KEYS.NOTIFICATION_PREFS_MOBILE);
|
||||
const mobile = await page.evaluate(
|
||||
(key) => JSON.parse(localStorage.getItem(key) || '{}'),
|
||||
STORAGE_KEYS.SETTINGS_MOBILE
|
||||
);
|
||||
const desktop = await page.evaluate(
|
||||
(key) => JSON.parse(localStorage.getItem(key) || '{}'),
|
||||
STORAGE_KEYS.SETTINGS_DESKTOP
|
||||
);
|
||||
const notif = await page.evaluate(
|
||||
(key) => JSON.parse(localStorage.getItem(key) || '{}'),
|
||||
STORAGE_KEYS.NOTIFICATION_PREFS_MOBILE
|
||||
);
|
||||
|
||||
expect(mobile.showFontControls).toBe(false);
|
||||
expect(desktop.showFontControls).toBe(true);
|
||||
@@ -390,5 +401,53 @@ describe('Settings Modal', () => {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps handheld settings when a foldable unfolds past the desktop breakpoint', async () => {
|
||||
const device = DEVICE_REGISTRY.find((entry) => entry.name === 'OPPO Find N5 (unfolded)')!;
|
||||
const { page, context } = await createDevicePage(device, BASE_URL, 'chromium');
|
||||
|
||||
try {
|
||||
// Seed the preferences while folded, exactly as a phone user does.
|
||||
await page.setViewportSize({ width: 412, height: 915 });
|
||||
await page.evaluate((key) => {
|
||||
localStorage.setItem(
|
||||
key,
|
||||
JSON.stringify({
|
||||
showResponseViewer: true,
|
||||
extendedKeyboardBar: true,
|
||||
})
|
||||
);
|
||||
}, STORAGE_KEYS.SETTINGS_MOBILE);
|
||||
await page.reload({ waitUntil: WAIT.DOM_CONTENT_LOADED });
|
||||
await page.waitForTimeout(WAIT.SSE_CONNECT);
|
||||
|
||||
// Unfolding can reload Android WebView. The viewport now uses desktop
|
||||
// layout, but the physical device and its preferences have not changed.
|
||||
await page.setViewportSize(device.viewport);
|
||||
await page.reload({ waitUntil: WAIT.DOM_CONTENT_LOADED });
|
||||
await page.waitForTimeout(WAIT.SSE_CONNECT);
|
||||
|
||||
const state = await page.evaluate(() => ({
|
||||
deviceType: (window as any).MobileDetection.getDeviceType(),
|
||||
handheld: (window as any).MobileDetection.isHandheldDevice(),
|
||||
storageKey: (window as any).app.getSettingsStorageKey(),
|
||||
responseViewerVisible: !document
|
||||
.querySelector('.btn-response-viewer-header')
|
||||
?.classList.contains('btn-response-viewer-header--hidden'),
|
||||
keyboardExtended: Boolean(document.querySelector('.keyboard-accessory-bar [data-action="arrow-left"]')),
|
||||
}));
|
||||
|
||||
expect(state.deviceType).toBe('desktop');
|
||||
expect(state.handheld).toBe(true);
|
||||
expect(state.storageKey).toBe(STORAGE_KEYS.SETTINGS_MOBILE);
|
||||
expect(state.responseViewerVisible).toBe(true);
|
||||
expect(state.keyboardExtended).toBe(true);
|
||||
|
||||
await showKeyboard(page, KEYBOARD.TYPICAL_IOS_HEIGHT);
|
||||
await assertVisible(page, '.keyboard-accessory-bar');
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user