← back to Desktop Dotbar

test/floating-pos.test.js

67 lines

'use strict';
// TK-12225: floatingPosFor must survive macOS renumbering a display's id (seen: 5 -> 6 across a
// reboot). Isolated from the real bar via DOTBAR_TEST_CFG so this never touches the live .barcfg.
process.env.DOTBAR_TEST_CFG = '.tmp-floating-pos-test.barcfg';
const path = require('node:path');
const fs = require('node:fs');
const test = require('node:test');
const assert = require('node:assert');
const dm = require('../electron-main.js');

const TMP_CFG = path.join(__dirname, '..', process.env.DOTBAR_TEST_CFG);
// writeCfg() debounces 200ms; flush synchronously first so no pending timer recreates the
// scratch file after we delete it (must never leak into the live .barcfg either way).
test.after(() => { try { dm.flushCfg(); } catch {} try { fs.unlinkSync(TMP_CFG); } catch {} });

const display = (id, x, y, w, h) => ({ id, workArea: { x, y, width: w, height: h } });
function resetCfg() { dm.cfg.pos = {}; dm.cfg.floating = true; }

test('missing id + orphan pos -> returns the orphan, clamped, and re-keys it to the new id', () => {
  resetCfg();
  dm.cfg.pos['5'] = { x: 4139, y: 95, w: 149, h: 985, t: 1000 };   // old display id, no longer connected
  const newDisplay = display(6, 2294, 0, 1920, 1080);              // same monitor, renumbered 5 -> 6
  const p = dm.floatingPosFor(newDisplay, [6]);                    // only id 6 is live now
  assert.ok(p, 'must fall back to the orphaned pos instead of returning null');
  assert.equal(p.w, 149);
  assert.equal(p.h, 985);
  assert.ok(p.x >= newDisplay.workArea.x && p.x + p.w <= newDisplay.workArea.x + newDisplay.workArea.width,
    'clamped into the new display work area');
  assert.ok(dm.cfg.pos['6'], 're-keyed under the new display id so it sticks');
  assert.equal(dm.cfg.pos['5'], undefined, 'orphan entry consumed, not duplicated');
});

test('a pos belonging to a display that IS currently connected is never stolen for a different display', () => {
  resetCfg();
  dm.cfg.pos['2'] = { x: 4139, y: 95, w: 149, h: 985, t: 1000 };   // display 2 is still plugged in
  const otherDisplay = display(7, 0, 0, 1920, 1080);               // a different display with no saved pos
  const p = dm.floatingPosFor(otherDisplay, [2, 7]);               // both 2 and 7 are live
  assert.equal(p, null, 'display 2\'s pos must not be handed to display 7 while 2 is still connected');
  assert.ok(dm.cfg.pos['2'], 'the connected display\'s saved pos is left untouched');
});

test('multiple orphans -> the most recently written one (highest t) wins', () => {
  resetCfg();
  dm.cfg.pos['5'] = { x: 100, y: 100, w: 149, h: 985, t: 1000 };   // stale
  dm.cfg.pos['9'] = { x: 200, y: 200, w: 149, h: 985, t: 5000 };   // most recent
  const newDisplay = display(6, 0, 0, 1920, 1080);
  const p = dm.floatingPosFor(newDisplay, [6]);                    // neither 5 nor 9 is connected
  assert.equal(p.x, 200, 'picked the higher-t orphan (9), not the stale one (5)');
  assert.equal(dm.cfg.pos['9'], undefined, 'the winning orphan is consumed');
  assert.ok(dm.cfg.pos['5'], 'the losing orphan is left in place (not silently dropped)');
});

// Negative/regression guard: if the orphan-fallback is ripped back out to the original
// `return p ? clampPosToDisplay(p, display) : null;`, the two assertions below go RED because
// cfg.pos[display.id] is never set for a renumbered display — proving this suite actually
// exercises the fallback rather than passing vacuously.
test('negative: without the fallback this would be null / wrong coords (fault-injection check)', () => {
  resetCfg();
  dm.cfg.pos['5'] = { x: 321, y: 654, w: 149, h: 985, t: 42 };
  const newDisplay = display(6, 0, 0, 1920, 1080);
  const naive = dm.cfg.pos && dm.cfg.pos[newDisplay.id];           // what the OLD one-line impl would see
  assert.equal(naive, undefined, 'sanity: the id-only lookup the old code used finds nothing');
  const p = dm.floatingPosFor(newDisplay, [6]);
  assert.ok(p, 'the fallback must still produce a position');
  assert.equal(p.x, 321, 'coords must come from the real orphan, not a made-up default');
});