Skip to content

Commit 345acfc

Browse files
committed
fix(vite): android dev overlay improvements
[skip ci]
1 parent 88aea3c commit 345acfc

5 files changed

Lines changed: 644 additions & 2 deletions

File tree

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2+
import { computeAndroidPanelLayout, ensureHmrDevOverlayRuntimeInstalled, hexRgbaToAndroidArgb } from './dev-overlay.js';
3+
import { getGlobalScope } from './global-scope.js';
4+
5+
// Minimal stand-ins for the Android widget surface the host touches. Method
6+
// bodies record just enough to assert attachment and visibility.
7+
class FakeView {
8+
parent: FakeView | null = null;
9+
children: FakeView[] = [];
10+
visibility = 8;
11+
clickable = false;
12+
layoutParams: any = null;
13+
text = '';
14+
translationY = 0;
15+
alpha = 1;
16+
endAction: any = null;
17+
addView(child: FakeView, params?: any) {
18+
child.parent = this;
19+
child.layoutParams = params ?? null;
20+
this.children.push(child);
21+
}
22+
removeView(child: FakeView) {
23+
this.children = this.children.filter((c) => c !== child);
24+
child.parent = null;
25+
}
26+
getParent() {
27+
return this.parent;
28+
}
29+
getWidth() {
30+
return 1080;
31+
}
32+
setClickable(value: boolean) {
33+
this.clickable = value;
34+
}
35+
setFocusable() {}
36+
setVisibility(value: number) {
37+
this.visibility = value;
38+
}
39+
setBackgroundColor() {}
40+
setBackground() {}
41+
setLayoutParams(params: any) {
42+
this.layoutParams = params;
43+
}
44+
setPadding() {}
45+
setElevation() {}
46+
setOrientation() {}
47+
setGravity() {}
48+
setTextSize() {}
49+
setTypeface() {}
50+
setText(value: string) {
51+
this.text = value;
52+
}
53+
setTextColor() {}
54+
setTranslationY(value: number) {
55+
this.translationY = value;
56+
}
57+
setAlpha(value: number) {
58+
this.alpha = value;
59+
}
60+
bringToFront() {}
61+
equals(other: unknown) {
62+
return other === this;
63+
}
64+
animate() {
65+
const chain: any = {
66+
translationY: () => chain,
67+
alpha: () => chain,
68+
setDuration: () => chain,
69+
setInterpolator: () => chain,
70+
withEndAction: (runnable: any) => {
71+
this.endAction = runnable;
72+
return chain;
73+
},
74+
start: () => {
75+
this.endAction?.run?.();
76+
this.endAction = null;
77+
},
78+
};
79+
return chain;
80+
}
81+
}
82+
83+
class FakeLayoutParams {
84+
gravity = 0;
85+
topMargin = 0;
86+
margins: number[] = [];
87+
constructor(
88+
public width: number,
89+
public height: number,
90+
) {}
91+
setMargins(left: number, top: number, right: number, bottom: number) {
92+
this.margins = [left, top, right, bottom];
93+
}
94+
}
95+
96+
class FakeDrawable {
97+
setCornerRadius() {}
98+
setColor() {}
99+
}
100+
101+
class FakeFrameLayout extends FakeView {
102+
static LayoutParams = FakeLayoutParams;
103+
}
104+
class FakeLinearLayout extends FakeView {
105+
static LayoutParams = FakeLayoutParams;
106+
static VERTICAL = 1;
107+
}
108+
class FakeTextView extends FakeView {}
109+
110+
class FakeRunnable {
111+
constructor(private readonly impl: { run: () => void }) {}
112+
run() {
113+
this.impl.run();
114+
}
115+
}
116+
117+
// NativeScript layouts the in-tree host would build if it were reached.
118+
class FakeNsLayout {
119+
children: any[] = [];
120+
addChild(child: any) {
121+
this.children.push(child);
122+
}
123+
}
124+
125+
function installFakeAndroid(decor: FakeView, page: { content: any }) {
126+
const g = getGlobalScope();
127+
g.android = {
128+
widget: { FrameLayout: FakeFrameLayout, LinearLayout: FakeLinearLayout, TextView: FakeTextView },
129+
view: {
130+
View: { VISIBLE: 0, GONE: 8 },
131+
Gravity: { TOP: 48, BOTTOM: 80, CENTER: 17, CENTER_HORIZONTAL: 1 },
132+
animation: { DecelerateInterpolator: class {}, AccelerateInterpolator: class {} },
133+
},
134+
util: { TypedValue: { COMPLEX_UNIT_SP: 2 } },
135+
graphics: { Typeface: { DEFAULT_BOLD: {} }, drawable: { GradientDrawable: FakeDrawable } },
136+
};
137+
g.java = { lang: { Runnable: FakeRunnable } };
138+
const activity = {
139+
getWindow: () => ({ getDecorView: () => decor }),
140+
getResources: () => ({ getDisplayMetrics: () => ({ density: 2 }) }),
141+
};
142+
g.Application = { android: { foregroundActivity: activity }, getRootView: () => page };
143+
g.GridLayout = FakeNsLayout;
144+
g.StackLayout = FakeNsLayout;
145+
g.Label = FakeNsLayout;
146+
}
147+
148+
const GLOBALS = ['__NS_HMR_DEV_OVERLAY__', '__NS_HMR_DEV_OVERLAY_STATE__', '__NS_HMR_ANDROID_SAFE_INSETS__', 'android', 'java', 'Application', 'GridLayout', 'StackLayout', 'Label'];
149+
150+
function clearGlobals() {
151+
const g = getGlobalScope();
152+
for (const key of GLOBALS) {
153+
delete g[key];
154+
}
155+
}
156+
157+
describe('Android overlay colour packing', () => {
158+
it('packs #RRGGBBAA into a signed ARGB int', () => {
159+
expect(hexRgbaToAndroidArgb('#FFFFFFFF')).toBe(-1);
160+
expect(hexRgbaToAndroidArgb('#00000000')).toBe(0);
161+
expect(hexRgbaToAndroidArgb('#FF000080')).toBe((0x80ff0000 | 0) >> 0);
162+
expect(hexRgbaToAndroidArgb('#0e6e2fff')).toBe((0xff0e6e2f | 0) >> 0);
163+
});
164+
165+
it('treats six-digit values as opaque and rejects garbage', () => {
166+
expect(hexRgbaToAndroidArgb('#112233')).toBe(hexRgbaToAndroidArgb('#112233FF'));
167+
expect(hexRgbaToAndroidArgb('nope')).toBe(0);
168+
});
169+
});
170+
171+
describe('Android overlay panel layout', () => {
172+
it('scales the safe-area margins by density and caps the width by the host', () => {
173+
const layout = computeAndroidPanelLayout({ position: 'top', density: 2, hostWidthPx: 500, safeInsets: { top: 24, bottom: 0, left: 0, right: 0 } });
174+
expect(layout.marginPx).toEqual({ top: 64, right: 32, bottom: 0, left: 32 });
175+
expect(layout.widthPx).toBe(500 - 64);
176+
});
177+
178+
it('uses the full panel width when the host has not been measured yet', () => {
179+
const layout = computeAndroidPanelLayout({ position: 'bottom', density: 3, hostWidthPx: 0 });
180+
expect(layout.widthPx).toBe(960);
181+
expect(layout.marginPx.bottom).toBe(24);
182+
});
183+
});
184+
185+
describe('Android overlay host', () => {
186+
beforeEach(clearGlobals);
187+
afterEach(clearGlobals);
188+
189+
it('mounts the chip on the DecorView and leaves the page content untouched', () => {
190+
const decor = new FakeView();
191+
const content = { id: 'app-root' };
192+
const page = { content };
193+
installFakeAndroid(decor, page);
194+
195+
const api = ensureHmrDevOverlayRuntimeInstalled(true);
196+
api.setUpdateStage('received', { detail: 'Updating /src/app/guitar/guitar.component.html' });
197+
198+
expect(page.content).toBe(content);
199+
expect(decor.children).toHaveLength(1);
200+
const root = decor.children[0];
201+
expect(root.visibility).toBe(0);
202+
expect(root.clickable).toBe(false);
203+
const [panel] = root.children;
204+
const [title, status] = panel.children;
205+
expect(title.text).toBe(api.getSnapshot().title);
206+
expect(status.text).toContain('Updating /src/app/guitar/guitar.component.html');
207+
expect(panel.layoutParams.gravity).toBe(48 | 1);
208+
});
209+
210+
it('reuses the mounted chip across stages and hides it on demand', () => {
211+
const decor = new FakeView();
212+
installFakeAndroid(decor, { content: {} });
213+
214+
const api = ensureHmrDevOverlayRuntimeInstalled(true);
215+
api.setUpdateStage('received');
216+
api.setUpdateStage('rebooting');
217+
expect(decor.children).toHaveLength(1);
218+
219+
api.hide('test');
220+
expect(decor.children[0].visibility).toBe(8);
221+
});
222+
223+
it('rebuilds the chip when the Activity hands out a new DecorView', () => {
224+
const firstDecor = new FakeView();
225+
installFakeAndroid(firstDecor, { content: {} });
226+
const api = ensureHmrDevOverlayRuntimeInstalled(true);
227+
api.setUpdateStage('received');
228+
expect(firstDecor.children).toHaveLength(1);
229+
230+
const secondDecor = new FakeView();
231+
getGlobalScope().Application.android.foregroundActivity.getWindow = () => ({ getDecorView: () => secondDecor });
232+
api.setUpdateStage('reimporting');
233+
234+
expect(firstDecor.children).toHaveLength(0);
235+
expect(secondDecor.children).toHaveLength(1);
236+
});
237+
});

0 commit comments

Comments
 (0)