Skip to content

Commit 10941c3

Browse files
committed
test: add android host measure test
Signed-off-by: kryptocodes <srivatsantb@gmail.com>
1 parent d833f50 commit 10941c3

1 file changed

Lines changed: 166 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// host-measure-android.test.mjs — what measure() reports on a browser that
2+
// resizes the LAYOUT viewport when the soft keyboard opens (host/popcorn-host.js).
3+
//
4+
// Why this file exists: the iOS keyboard-gap fix changed measure() from
5+
//
6+
// occluded = innerHeight - vv.height - vv.offsetTop
7+
// to
8+
// occluded = max(layoutBaselineHeight, innerHeight) - vv.height
9+
//
10+
// and neither half of that is platform-gated. The latched baseline is there
11+
// because Safari eventually shrinks innerHeight down to the already-shrunk
12+
// visualViewport height while the keyboard is still docked — but an Android
13+
// browser without the VirtualKeyboard API shrinks BOTH viewports for real, and
14+
// on that shape a latched baseline could invent a keyboard that is not there,
15+
// or report occlusion against a frame that has already made room for the keys.
16+
// Chrome takes the VirtualKeyboard branch and never reaches this code; Firefox
17+
// for Android, Android WebViews, and any iframe embedded without the
18+
// virtual-keyboard permission do reach it.
19+
//
20+
// So: three properties per platform shape — the reported occlusion, whether the
21+
// iframe gets pinned, and whether the baseline can be relearned back down.
22+
//
23+
// No installGlobals here: this is the HOST script, which has no platform profile
24+
// of its own — it branches on the viewport numbers it is handed, which is exactly
25+
// what a test can vary.
26+
import { test } from 'node:test';
27+
import assert from 'node:assert/strict';
28+
import { makeHostWindow } from './host-stub.mjs';
29+
30+
const VIEWPORT = { w: 411, h: 732 };
31+
const KEYBOARD = 332; // 732 - 400, a plausible Pixel-class IME
32+
33+
/** A layered (position:fixed, full-viewport) embed with a live measurer. */
34+
function measurer(attachOpts) {
35+
const h = makeHostWindow({
36+
iframeRect: { left: 0, top: 0, width: VIEWPORT.w, height: VIEWPORT.h },
37+
viewport: VIEWPORT,
38+
});
39+
h.PopcornHost.layer(h.iframe);
40+
const host = h.PopcornHost.attach(h.iframe, Object.assign({ childOrigin: 'https://pod.test' }, attachOpts));
41+
return { h, host };
42+
}
43+
44+
/** The newest geometry the host posted down to the viewer. */
45+
function geometry(h) {
46+
for (let i = h.posted.length - 1; i >= 0; i--) {
47+
if (h.posted[i].type === 'POPCORN_HOST_GEOMETRY') return h.posted[i];
48+
}
49+
return null;
50+
}
51+
52+
/** Re-measure the way the browser does: mutate the viewport, fire resize. */
53+
function resize(h, { inner, visual, offsetTop } = {}) {
54+
if (inner !== undefined) h.win.innerHeight = inner;
55+
if (visual !== undefined) h.win.visualViewport.height = visual;
56+
if (offsetTop !== undefined) h.win.visualViewport.offsetTop = offsetTop;
57+
h.fireWindow('resize');
58+
return geometry(h);
59+
}
60+
61+
test('a layout-resize keyboard (both viewports shrink) is NOT reported as occlusion', () => {
62+
const { h, host } = measurer();
63+
// Android's default: the keyboard takes the space out of the layout viewport,
64+
// so the embedded viewer is already only 400px tall. Reporting 332px of
65+
// occlusion on top of that would lift the remote field twice.
66+
const g = resize(h, { inner: 400, visual: 400 });
67+
assert.equal(g.occludedBottom, 0, 'no phantom keyboard from the latched baseline');
68+
assert.equal(g.visibleHeight, 400);
69+
assert.equal(g.framePinned, 0, 'nothing to pin — the layout already made room');
70+
assert.equal(h.iframe.style.height, '100%', 'the embedder layout contract is untouched');
71+
host.destroy();
72+
});
73+
74+
test('the baseline relearns DOWNWARD, so the shrunken layout is not a keyboard forever', () => {
75+
const { h, host } = measurer();
76+
resize(h, { inner: 400, visual: 400 });
77+
assert.equal(geometry(h).layoutBaselineHeight, 400, 'baseline followed the layout down');
78+
// Restoring the full viewport must not read as a keyboard in either direction.
79+
const g = resize(h, { inner: 732, visual: 732 });
80+
assert.equal(g.occludedBottom, 0);
81+
assert.equal(g.visibleHeight, 732);
82+
host.destroy();
83+
});
84+
85+
test('either viewport update order is safe (innerHeight can land first)', () => {
86+
const { h, host } = measurer();
87+
// A resize event can fire between the two updates. The intermediate state
88+
// (small layout viewport, stale visual viewport) must not produce occlusion.
89+
let g = resize(h, { inner: 400 });
90+
assert.equal(g.occludedBottom, 0, 'a stale visual viewport is not a keyboard');
91+
g = resize(h, { visual: 400 });
92+
assert.equal(g.occludedBottom, 0, 'and the settled layout-resize state still is not');
93+
assert.equal(h.iframe.style.height, '100%');
94+
host.destroy();
95+
});
96+
97+
test('the VirtualKeyboard API outranks the layout baseline (Android Chrome path)', () => {
98+
const { h, host } = measurer();
99+
// Chromium with allow="virtual-keyboard" reports the rect explicitly, and it
100+
// does so WITHOUT shrinking either viewport (overlays-content). The baseline
101+
// latch must not touch this branch.
102+
h.win.navigator.virtualKeyboard = { boundingRect: { height: 300 } };
103+
let g = resize(h, {});
104+
assert.equal(g.occludedBottom, 300, 'the keyboard rect is used verbatim');
105+
assert.equal(g.visibleHeight, 432, 'visible height comes off innerHeight, not the baseline');
106+
assert.equal(g.framePinned, 1, 'an overlaying keyboard does pin the frame');
107+
108+
h.win.navigator.virtualKeyboard = { boundingRect: { height: 0 } };
109+
g = resize(h, {});
110+
assert.equal(g.occludedBottom, 0, 'an empty rect falls through to a clean dismissal');
111+
assert.equal(h.iframe.style.height, '100%');
112+
host.destroy();
113+
});
114+
115+
test('a visual-only shrink IS the keyboard, on any platform', () => {
116+
const { h, host } = measurer();
117+
// interactive-widget=resizes-visual (and every iOS build): the layout viewport
118+
// keeps its height, only the visual one shrinks. This is the case the lift is
119+
// for, and the case the baseline latch has to keep alive.
120+
const g = resize(h, { visual: 400 });
121+
assert.equal(g.occludedBottom, KEYBOARD);
122+
assert.equal(g.visibleHeight, 400);
123+
assert.equal(g.framePinned, 1);
124+
assert.equal(h.iframe.style.height, '732px', 'the frame is pinned to its pre-keyboard box');
125+
host.destroy();
126+
});
127+
128+
test('the baseline survives innerHeight collapsing mid-session (the iOS late shrink)', () => {
129+
const { h, host } = measurer();
130+
resize(h, { visual: 400 });
131+
assert.equal(geometry(h).occludedBottom, KEYBOARD);
132+
// Safari now drops innerHeight to the already-shrunk visual height while the
133+
// keys are still up. Without the latch this reads as a dismissal and the black
134+
// gap comes back; the answer must not change.
135+
resize(h, { inner: 400 });
136+
const g = geometry(h);
137+
assert.equal(g.occludedBottom, KEYBOARD, 'still a keyboard, not a dismissal');
138+
assert.equal(h.iframe.style.height, '732px', 'and the frame stays pinned');
139+
host.destroy();
140+
});
141+
142+
test('KNOWN GAP: with pinning off, a late innerHeight collapse double-counts', () => {
143+
// The latch and the pin are one mechanism: the frame is held at its
144+
// pre-keyboard height, so reporting occlusion against it is correct. Turn the
145+
// pin off (pinKeyboardHeight:false, or an iframe that is not position:fixed)
146+
// and the two halves disagree — the frame follows the shrinking layout while
147+
// the host still reports a full keyboard height, so the viewer lifts inside a
148+
// box that already ends above the keys.
149+
//
150+
// Reachable only when the visual viewport shrinks BEFORE innerHeight, which is
151+
// the iOS ordering; a layout-resize browser (tests above) shrinks both at once
152+
// and never latches. Locked down as current behavior, not as desired behavior.
153+
const { h, host } = measurer({ pinKeyboardHeight: false });
154+
resize(h, { visual: 400 });
155+
assert.equal(h.iframe.style.height, '100%', 'pinning is off, so the box is left alone');
156+
// The layout viewport (and with it the fixed frame) now collapses onto the
157+
// keyboard edge. 390 rather than 400 only so the sample clears the 8px
158+
// send-dedupe and the assertions can read the fresh numbers.
159+
h.iframe._rect = { left: 0, top: 0, width: VIEWPORT.w, height: 390 };
160+
resize(h, { inner: 390, visual: 390 });
161+
const g = geometry(h);
162+
assert.equal(g.occludedBottom, 342, 'a full keyboard height is still claimed');
163+
assert.equal(g.framePinned, 0);
164+
assert.equal(g.frameHeight, 390, 'against a frame that is already keyboard-sized');
165+
host.destroy();
166+
});

0 commit comments

Comments
 (0)