// room-window.test.mjs — when the call window's controls show // (2026-09-28), gated without a browser. // // node --test test/browser/room-window.test.mjs // // The same trick as room-layout.test.mjs: the rule is lifted out of the // shipped call-window.js by name, and its constants are held equal to // the Mac's ControlsVisibility (CallChrome.swift), so the web and the // Mac cannot drift apart in silence. So is Grid's fit (call-layout.js's // gridColumns, the Mac's GridFit.columns) and where a dropped self view // settles (pipCorner, PiPLayout.corner). import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; const read = (rel) => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8'); const windowJS = read('../../internal/calls/static/call-window.js'); const roomJS = read('../../internal/calls/static/room.js'); const layoutJS = read('../../internal/calls/static/call-layout.js'); const swift = read('../../native/apple/Sources/CallsKit/CallChrome.swift'); const css = read('../../internal/calls/static/calls.css'); function lift(source, name) { const start = source.indexOf('function ' + name + '('); assert.notEqual(start, -1, name + ' is not there any more — this test is stale, not passing'); let depth = 0; for (let i = source.indexOf('{', start); i < source.length; i++) { if (source[i] === '{') depth += 1; else if (source[i] === '}') { depth -= 1; if (depth === 0) return source.slice(start, i + 1); } } assert.fail('unbalanced braces reading ' + name); } function jsConst(name) { const m = windowJS.match(new RegExp('const ' + name + ' = ([0-9.]+);')); assert.ok(m, name + ' is not a numeric const in call-window.js any more'); return Number(m[1]); } function swiftConst(name) { const m = swift.match(new RegExp('static let ' + name + ': TimeInterval = ([0-9.]+)')); assert.ok(m, name + ' is not in CallChrome.swift any more'); return Number(m[1]); } const CONSTS = ['CONTROLS_POINTER_DELAY', 'CONTROLS_TOUCH_DELAY', 'CONTROLS_EXIT_DELAY'] .map((n) => 'const ' + n + ' = ' + jsConst(n) + ';').join('\n'); // eslint-disable-next-line no-new-func const m = new Function(CONSTS + '\n' + ['controlsDelay', 'controlsStart', 'controlsHeld', 'controlsStep'] .map((n) => lift(windowJS, n)).join('\n') + '; return { controlsStart, controlsStep, controlsHeld };')(); // eslint-disable-next-line no-new-func const layout = new Function(['gridColumns', 'gridTileWidth', 'gridFit', 'pipCorner'] .map((n) => lift(layoutJS, n)).join('\n') + '; return { gridColumns, gridFit, pipCorner };')(); const run = (s, events) => events.reduce((st, [t, ev]) => m.controlsStep(st, ev, t), s); test('the delays are the Mac\'s', () => { assert.equal(jsConst('CONTROLS_POINTER_DELAY'), swiftConst('pointerDelay')); assert.equal(jsConst('CONTROLS_TOUCH_DELAY'), swiftConst('touchDelay')); assert.equal(jsConst('CONTROLS_EXIT_DELAY'), swiftConst('pointerExitDelay')); }); test('the fades are the Mac\'s: 0.35 s going, 0.2 s coming back', () => { const out = css.match(/--calls-fade-out: ([0-9.]+)s;/); const back = css.match(/--calls-fade-in: ([0-9.]+)s;/); assert.ok(out && back, 'calls.css has no fade durations'); assert.equal(Number(out[1]), swiftConst('fadeOutDuration')); assert.equal(Number(back[1]), swiftConst('fadeInDuration')); }); test('they start visible and go 3 s later', () => { const s = m.controlsStart('pointer', 100); assert.equal(s.visible, true); assert.equal(s.hideAt, 103); assert.equal(m.controlsStep(s, { type: 'tick' }, 102.9).visible, true); assert.equal(m.controlsStep(s, { type: 'tick' }, 103).visible, false); }); test('moving the pointer shows them and starts the wait again', () => { const s = run(m.controlsStart('pointer', 0), [[4, { type: 'tick' }], [5, { type: 'move' }]]); assert.equal(s.visible, true); assert.equal(s.hideAt, 8); assert.equal(s.pointerInside, true); assert.equal(run(s, [[7, { type: 'move' }], [9, { type: 'tick' }]]).visible, true); assert.equal(run(s, [[7, { type: 'move' }], [10, { type: 'tick' }]]).visible, false); }); test('leaving the window hides them 1.5 s later, never later than they were due', () => { let s = run(m.controlsStart('pointer', 0), [[0.5, { type: 'move' }], [1, { type: 'exit' }]]); assert.equal(s.hideAt, 2.5); assert.equal(s.pointerInside, false); s = run(m.controlsStart('pointer', 0), [[0, { type: 'move' }], [2.5, { type: 'exit' }]]); assert.equal(s.hideAt, 3, 'due at 3 already, so leaving at 2.5 does not put it off to 4'); }); test('a hold keeps them up; letting go starts the usual wait', () => { let s = run(m.controlsStart('pointer', 0), [[1, { type: 'hold', hold: 'menuOpen', on: true }]]); assert.equal(s.hideAt, null); assert.equal(run(s, [[60, { type: 'tick' }]]).visible, true); s = run(s, [[60, { type: 'hold', hold: 'menuOpen', on: false }]]); assert.equal(s.hideAt, 63); // A hold shows hidden controls. s = run(m.controlsStart('pointer', 0), [[5, { type: 'tick' }]]); assert.equal(s.visible, false); assert.equal(run(s, [[6, { type: 'hold', hold: 'keyboardFocus', on: true }]]).visible, true); }); test('leaving the window lets go of the pointer on the bar, but not of other holds', () => { let s = run(m.controlsStart('pointer', 0), [[0, { type: 'move' }], [0, { type: 'hold', hold: 'pointerOverControls', on: true }], [1, { type: 'exit' }]]); assert.equal(m.controlsHeld(s), false); assert.equal(s.hideAt, 2.5); s = run(m.controlsStart('pointer', 0), [[0, { type: 'hold', hold: 'menuOpen', on: true }], [1, { type: 'exit' }]]); assert.equal(s.hideAt, null); }); test('a tap toggles, unless they are held', () => { let s = m.controlsStart('touch', 0); s = m.controlsStep(s, { type: 'tap' }, 1); assert.equal(s.visible, false); s = m.controlsStep(s, { type: 'tap' }, 2); assert.equal(s.visible, true); assert.equal(s.hideAt, 5); s = run(s, [[2, { type: 'hold', hold: 'menuOpen', on: true }], [3, { type: 'tap' }]]); assert.equal(s.visible, true, 'a held tap only starts the wait again'); }); test('activity (Tab, a click) shows them', () => { const s = run(m.controlsStart('pointer', 0), [[4, { type: 'tick' }], [5, { type: 'activity' }]]); assert.equal(s.visible, true); assert.equal(s.hideAt, 8); }); test('Grid fits as the Mac\'s GridFit does', () => { // Two 16:9 tiles in a laptop's window: one above the other is larger. assert.equal(layout.gridColumns(2, 1264, 784, 16 / 9, 4), 1); // In a wide, short one, side by side. assert.equal(layout.gridColumns(2, 1600, 500, 16 / 9, 4), 2); assert.equal(layout.gridColumns(4, 1400, 860, 16 / 9, 4), 2); assert.equal(layout.gridColumns(1, 1400, 860, 16 / 9, 4), 1); const small = layout.gridFit(8, 400, 300, 72); assert.equal(small.tileW, 200, 'no tile under 200 wide: it scrolls instead'); const fit = layout.gridFit(4, 1400, 860, 72); assert.ok(fit.tileW * fit.columns + (fit.columns - 1) * 4 <= 1392); assert.ok(fit.tileH * fit.rows + (fit.rows - 1) * 4 <= 852); assert.equal(fit.lastRow, 2); }); test('a dropped self view settles where it was headed', () => { assert.equal(layout.pipCorner({ x: 100, y: 100 }, null, 1000, 600), 'top-left'); assert.equal(layout.pipCorner({ x: 900, y: 500 }, null, 1000, 600), 'bottom-right'); assert.equal(layout.pipCorner({ x: 450, y: 500 }, { x: 700, y: 100 }, 1000, 600), 'top-right', 'a flick carries it'); }); test('every state the title pill carried is a notice now (2026-10-02)', () => { // The pill said Connecting, Waiting to be let in, The room is full, // Reconnecting and Call ended over the title. It is gone; the status // notice (#status, room.js's statusLine) says each of them, and the // call being over is stop()'s sentence with its way out. // eslint-disable-next-line no-new-func const statusLine = new Function(lift(roomJS, 'statusLine') + '; return statusLine;')(); assert.deepEqual(statusLine({ admitted: false }, false, 0, true), ['Connecting to the call.', false]); assert.deepEqual(statusLine({ admitted: false, peer: 'p', joined: true }, false, 0, false), ['Waiting for someone to let you in.', false]); assert.deepEqual(statusLine({ admitted: false, peer: 'p', joined: true }, false, 0, true), ['Connecting to the call.', false], 'a member is never waiting'); assert.deepEqual(statusLine({ full: true }, false, 0, true), ['The room is full.', false]); assert.deepEqual(statusLine({ admitted: true }, true, 1, true), ['Reconnecting…', false]); assert.deepEqual(statusLine({ admitted: true }, false, 0, true), ['You are the only one here.', true]); assert.deepEqual(statusLine({ admitted: true }, false, 2, true), ['', false], 'in the meeting: nothing to say'); assert.ok(/This meeting has ended/.test(roomJS), 'the call being over is said'); assert.ok(!/callState|call-state/.test(windowJS), 'and no pill is drawn'); }); // 2026-10-03, Till on an iPhone: "kept cutting in and out". Safari there // stops the camera, the microphone and the sound with the tab in the // background or the screen locked, so the room says so, once a session. test('appleTouch: iPhone and iPad, in any browser there, and nothing else', () => { const appleTouch = new Function(lift(windowJS, 'appleTouch') + '; return appleTouch;')(); const iphone = 'Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Mobile/15E148 Safari/604.1'; const crios = 'Mozilla/5.0 (iPhone; CPU iPhone OS 26_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/140.0 Mobile/15E148 Safari/604.1'; const mac = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.0 Safari/605.1.15'; assert.equal(appleTouch({ platform: 'iPhone', userAgent: iphone, maxTouchPoints: 5 }), true); assert.equal(appleTouch({ platform: 'iPhone', userAgent: crios, maxTouchPoints: 5 }), true, 'Chrome on iPhone is Safari underneath'); assert.equal(appleTouch({ platform: 'MacIntel', userAgent: mac, maxTouchPoints: 5 }), true, 'an iPad asking for the desktop site'); assert.equal(appleTouch({ platform: 'MacIntel', userAgent: mac, maxTouchPoints: 0 }), false, 'a Mac'); assert.equal(appleTouch({ platform: 'Linux armv8l', userAgent: 'Mozilla/5.0 (Linux; Android 15) Chrome/140 Mobile', maxTouchPoints: 5 }), false); assert.equal(appleTouch(undefined), false); });