Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

quickshell/nuguland/test-fixtures/ui/tasks.mjs

Raw
import assert from 'node:assert/strict';
import Brightness from '../../brightness.js';

export const taskSelection = 'tasks';

export const taskServiceFixtures = Object.freeze([
    'Audio',
    'Bluetooth',
    'Brightness',
    'Clipboard',
    'Launcher',
    'Media',
    'Network',
    'Noko',
    'Niri',
    'Notifications',
    'Power',
    'Tray',
    'Usage',
    'Vault',
].map(name => ({ source: `test-fixtures/ui/task-services/${name}.qml`, target: `${name}.qml` })));

export const reusedTaskServiceFixtures = Object.freeze([
    { source: 'test-fixtures/ui/services/Privilege.qml', target: 'Privilege.qml' },
    { source: 'test-fixtures/ui/services/Resources.qml', target: 'Resources.qml' },
]);

export const taskOverlayFixtures = Object.freeze([...taskServiceFixtures, ...reusedTaskServiceFixtures]);

export const taskEntryFixture = Object.freeze({
    source: 'test-fixtures/ui/tasks.qml',
    target: 'tasks-ui-test.qml',
    replace: Object.freeze({ from: 'import "../.." as N', to: 'import "." as N' }),
});

function within(rect, width, height) {
    return rect && rect.width > 0 && rect.height > 0 && rect.x >= 0 && rect.y >= 0 && rect.x + rect.width <= width && rect.y + rect.height <= height;
}

function center(rect) {
    return [String(Math.round(rect.x + rect.width / 2)), String(Math.round(rect.y + rect.height / 2))];
}

function outsideRect(state) {
    const card = state.popup.card;
    const candidates = [
        { x: state.width - 14, y: state.height - 14, width: 6, height: 6 },
        { x: 8, y: state.height - 14, width: 6, height: 6 },
        { x: state.width - 14, y: 40, width: 6, height: 6 },
    ];
    const result = candidates.find(rect => rect.x + rect.width < card.x || rect.x > card.x + card.width || rect.y + rect.height < card.y || rect.y > card.y + card.height);
    assert(result, 'mapped output must contain an outside-click target');
    assert(within(result, state.width, state.height));
    return result;
}

export async function runTasks({ ipc, snapshot, input, key, click, check, screenshot, run, delay }) {
    for (const helper of [ipc, snapshot, input, key, click, check, screenshot, run, delay])
        assert.equal(typeof helper, 'function', 'tasks runner requires the parent UI helper contract');

    function assertGeometry(state, primary, name) {
        assert(state.width >= 520 && state.height >= 260, 'output is below the production popup minimum');
        assert(within(state.popup.card, state.width, state.height), `${name} popup card outside output`);
        assert(within(state.panel, state.width, state.height), `${name} production panel outside output`);
        assert(within(primary, state.width, state.height), `${name} primary control outside output`);
    }

    function confirmationFits(value) {
        const region = value.power.confirmation;
        const action = value.power.poweroff?.rect;
        return region && action && region.y >= action.y + action.height && region.y + region.height <= value.panel.y + value.panel.height;
    }

    async function open(name, primary) {
        const before = snapshot();
        assert(within(before.chips[name].rect, before.width, before.height), `${name} Bar chip outside output`);
        click(before.chips[name].rect);
        let previousGeometry = '';
        const state = await check(`${name} opens from the real Bar`, value => {
            if (value.popup.name !== name || value.popup.phase !== 'open' || !value.popup.windowActive || value.layoutFrames < 4 || !primary(value)) return false;
            const geometry = JSON.stringify([value.popup.card, value.panel, primary(value)]);
            const settled = geometry === previousGeometry;
            previousGeometry = geometry;
            return settled;
        });
        assertGeometry(state, primary(state), name);
        return state;
    }

    let state = await check('task fixture maps the configured output and real Bar', value => value.width > 0 && value.height > 0 && Object.values(value.chips).every(chip => within(chip.rect, value.width, value.height)));

    state = await check('horizontal clock stays at the far right with widgets before it', value => value.clock && value.clock.x + value.clock.width === value.width - 10 && Object.values(value.chips).every(chip => chip.rect.x + chip.rect.width <= value.clock.x - 12));
    await screenshot('tasks-clock-right');
    click(state.clock);
    await check('rightmost clock opens its calendar by pointer', value => value.popup.name === 'clock' && value.popup.phase === 'open' && value.popup.windowActive);
    key('Escape');
    await check('Escape dismisses the calendar opened from the rightmost clock', value => !value.popup.open);
    assert.equal(state.barEdge.height, 1, 'horizontal Bar accent is a hairline');
    assert.equal(state.barEdge.width, state.width);
    state = await open('clipboard', value => value.clipboard.search?.rect);
    assert(state.clipboard.search.focused, 'clipboard search must receive initial focus');
    input('type', 'needle');
    state = await check('clipboard query filters without exposing editable text', value => value.popup.name === 'clipboard' && value.clipboard.queryKnown && value.clipboard.selectedKnown && value.clipboard.visibleRows === 1);
    click(state.clipboard.copy.rect);
    state = await check('clipboard copy preserves the active filter', value => value.clipboard.copies === 1 && value.clipboard.queryKnown && value.clipboard.visibleRows === 1);
    click(state.clipboard.pin.rect);
    state = await check('clipboard pin preserves query and selected identity', value => value.clipboard.pins === 1 && value.clipboard.queryKnown && value.clipboard.selectedKnown && value.clipboard.visibleRows === 1);
    ipc('external', 'clipboard-update');
    state = await check('external clipboard update remains filtered after copy and pin', value => value.clipboard.externalUpdates === 1 && value.clipboard.queryKnown && value.clipboard.visibleRows === 2);
    await screenshot('tasks-clipboard-filtered');

    input('mousemove', ...center(state.chips.launcher.rect));
    await delay(120);
    state = await check('real Bar hover does not switch the active clipboard task', value => value.chips.launcher.hovered && value.popup.name === 'clipboard' && value.clipboard.queryKnown);
    click(state.chips.launcher.rect);
    state = await check('Bar activation switches from clipboard to launcher', value => value.popup.name === 'launcher' && value.popup.phase === 'open' && value.launcher.search?.rect);
    assertGeometry(state, state.launcher.search.rect, 'launcher');
    assert(state.launcher.search.focused, 'launcher search must receive initial focus');
    input('type', 'terminal');
    state = await check('launcher draft filters without exposing editable text', value => value.launcher.queryKnown && value.launcher.visibleRows === 1);
    await screenshot('tasks-launcher-draft');

    click(state.chips.clipboard.rect);
    state = await check('switching restores clipboard query and results', value => value.popup.name === 'clipboard' && value.clipboard.queryKnown && value.clipboard.visibleRows === 2);
    click(state.chips.clipboard.rect);
    await check('same Bar chip closes clipboard without discarding its draft', value => !value.popup.open && value.popup.lastCloseReason === 'bar-toggle' && value.clipboard.queryKnown);
    state = await open('clipboard', value => value.clipboard.search?.rect);
    assert(state.clipboard.queryKnown && state.clipboard.visibleRows === 2, 'clipboard draft must survive close and reopen');
    click(state.chips.launcher.rect);
    state = await check('switching back restores the launcher draft', value => value.popup.name === 'launcher' && value.launcher.queryKnown && value.launcher.visibleRows === 1);
    click(state.chips.launcher.rect);
    await check('same Bar chip closes launcher without discarding its draft', value => !value.popup.open && value.popup.lastCloseReason === 'bar-toggle' && value.launcher.queryKnown);
    state = await open('launcher', value => value.launcher.search?.rect);
    assert(state.launcher.queryKnown && state.launcher.visibleRows === 1, 'launcher draft must survive close and reopen');

    state = await open('audio', value => value.audio.slider?.rect);
    const slider = state.audio.slider.rect;
    input('mousemove', String(Math.round(slider.x + slider.width * 0.2)), String(Math.round(slider.y + slider.height / 2)));
    input('mousedown', '1');
    input('mousemove', String(Math.round(slider.x + slider.width * 0.58)), String(Math.round(slider.y + slider.height / 2)));
    state = await check('audio slider drag writes while retaining the real delegate', value => value.audio.pressed && value.audio.sameSlider && value.audio.writes > 0);
    const writesDuringDrag = state.audio.writes;
    ipc('external', 'audio-update');
    await check('external audio update does not interrupt the held drag', value => value.audio.pressed && value.audio.sameSlider && value.audio.externalUpdates === 1 && value.audio.writes === writesDuringDrag);
    input('mousemove', String(Math.round(slider.x + slider.width * 0.82)), String(Math.round(slider.y + slider.height / 2)));
    input('mouseup', '1');
    state = await check('audio drag completes after the external update', value => !value.audio.pressed && value.audio.sameSlider && value.audio.writes > writesDuringDrag && value.audio.volume > 0.8);
    await screenshot('tasks-audio-drag-update');
    ipc('external', 'audio-many');
    for (let i = 0; i < 18; i++) {
        key('Tab');
        state = await check(`audio keyboard traversal keeps control visible ${i}`, value => {
            const f = value.audio.focusedRect, p = value.panel;
            return f && p && f.y >= p.y && f.y + f.height <= p.y + p.height;
        });
    }
    assert(state.audio.scrollY > 0, 'long audio list must actually scroll');
    const retainedAudioScroll = state.audio.scrollY;
    await screenshot('tasks-audio-keyboard-scroll');

    state = await open('media', value => value.media.play?.rect);
    assert.equal(snapshot().popup.name, 'media');
    assert.equal(state.media.previous.enabled, false);
    assert.equal(state.media.next.enabled, true);
    assert.equal(state.media.play.enabled, true);
    assert.equal(state.media.shuffle.enabled, true);
    assert.equal(state.media.repeat.enabled, true);
    click(state.media.next.rect);
    state = await check('media enabled next action dispatches once by pointer', value => value.media.actions === 1);
    click(state.media.play.rect);
    key('space');
    state = await check('media play control supports pointer and keyboard activation', value => value.media.actions === 3 && value.media.lastWasPlay);
    ipc('external', 'media-restrict');
    state = await check('media capabilities disable every unsupported transport', value => value.media.restricted && !value.media.previous.enabled && !value.media.next.enabled && !value.media.play.enabled && !value.media.shuffle.enabled && !value.media.repeat.enabled);
    click(state.media.next.rect);
    await delay(100);
    assert.equal(snapshot().media.actions, 3, 'disabled media action must reject pointer activation');
    await screenshot('tasks-media-capabilities');
    ipc('external', 'media-empty');
    state = await check('last player disappearing shrinks media and removes dead controls', value => value.media.emptyState && !value.media.play && value.popup.card.width === 352 && value.popup.card.height === 88);
    assertGeometry(state, state.media.emptyState, 'empty media');
    await screenshot('tasks-media-empty');
    key('Escape');
    await check('Escape dismisses empty media', value => !value.popup.open);
    await open('media', value => value.media.emptyState);
    ipc('external', 'media-populated');
    await check('player arrival restores media controls and size without reopening', value => value.media.play && !value.media.emptyState && value.popup.card.height === 404);

    state = await open('tray', value => value.tray.primary?.rect);
    click(state.tray.primary.rect);
    state = await check('tray primary action is keyboard focusable after pointer activation', value => value.tray.primaryCount === 1 && value.tray.primary.focused);
    key('Return');
    state = await check('tray card activates with Enter', value => value.tray.primaryCount === 2);
    key('Tab');
    state = await check('Tab reaches tray secondary action', value => value.tray.secondary.focused);
    key('space');
    state = await check('tray secondary action activates with Space', value => value.tray.secondaryCount === 1);
    key('Tab');
    state = await check('Tab reaches tray menu action', value => value.tray.menu.focused);
    key('Return');
    await check('tray menu action activates with Enter', value => value.tray.menuCount === 1);
    await screenshot('tasks-tray-keyboard');
    ipc('external', 'tray-empty');
    state = await check('last tray item disappearing shrinks tray and removes actions', value => value.tray.emptyState && !value.tray.primary && !value.tray.menu && value.popup.card.width === 352 && value.popup.card.height === 88);
    assertGeometry(state, state.tray.emptyState, 'empty tray');
    await screenshot('tasks-tray-empty');
    click(outsideRect(state));
    await check('outside click dismisses empty tray', value => !value.popup.open && value.popup.lastCloseReason === 'outside');
    await open('tray', value => value.tray.emptyState);
    ipc('external', 'tray-populated');
    await check('tray item arrival restores actions and size without reopening', value => value.tray.primary && !value.tray.emptyState && value.popup.card.height === 424);

    state = await open('power', value => value.power.reboot?.rect);
    assert.equal(state.popup.card.width, 352);
    assert.equal(state.popup.card.height, 232);
    assert.equal(state.power.battery.height, 48);
    await screenshot('tasks-power-compact');
    input('mousemove', ...center(state.power.reboot.rect));
    await check('power action hover shows its contextual tooltip', value => value.power.rebootTooltip);
    await screenshot('tasks-power-tooltip');
    click(state.power.reboot.rect);
    state = await check('power destructive action arms confirmation', value => value.power.armedAction === 'reboot' && value.power.arms === 1 && confirmationFits(value));
    await screenshot('tasks-power-armed');
    click(state.chips.audio.rect);
    state = await check('switching panels cancels power confirmation', value => value.popup.name === 'audio' && !value.power.armed && value.power.cancels === 1 && value.power.confirms === 0);
    assert.equal(state.audio.scrollY, retainedAudioScroll, 'audio scroll survives switching away and back');
    state = await open('power', value => value.power.poweroff?.rect);
    assert.equal(state.power.armed, false, 'power must reopen unarmed after switching');
    click(state.power.poweroff.rect);
    state = await check('power can arm again after switch cancellation', value => value.power.armedAction === 'poweroff' && value.power.arms === 2 && confirmationFits(value));
    click(outsideRect(state));
    state = await check('outside dismissal cancels power confirmation', value => !value.popup.open && value.popup.lastCloseReason === 'outside' && !value.power.armed && value.power.cancels === 2);
    state = await open('power', value => value.power.reboot?.rect);
    assert.equal(state.power.armed, false, 'power must reopen unarmed after outside dismissal');
    const immediateBefore = state.power.immediate;
    for (const [index, name] of ['lock', 'suspend', 'monitor-off'].entries()) {
        click(state.power.actions.find(action => action.name === name).control.rect);
        key('space');
        state = await check(`${name} accepts pointer and Space exactly once each`, value => value.power.immediate === immediateBefore + (index + 1) * 2 && !value.power.armed);
    }
    key('Tab');
    state = await check('Tab reaches logout in the compact action grid', value => value.power.actions.find(action => action.name === 'logout').control.focused);
    key('Return');
    state = await check('Enter arms logout without executing it', value => value.power.armedAction === 'logout' && value.power.confirms === 0 && value.popup.card.height === 348 && confirmationFits(value));
    assert(within(state.power.confirmation, state.width, state.height));
    await screenshot('tasks-power-logout');
    click(state.power.cancel.rect);
    state = await check('cancel shrinks power and returns focus to the armed action', value => !value.power.armed && value.popup.card.height === 232 && value.power.actions.find(action => action.name === 'logout').control.focused && value.power.confirms === 0);
    key('Tab');
    key('Return');
    state = await check('keyboard activation arms reboot', value => value.power.armedAction === 'reboot' && confirmationFits(value));
    key('Tab');
    key('Tab');
    await check('Tab reaches cancel before confirmation', value => value.power.cancel.focused);
    key('Tab');
    await check('Tab reaches explicit destructive confirmation', value => value.power.confirm.focused);
    key('shift+Tab');
    await check('reverse Tab returns to cancel', value => value.power.cancel.focused);
    key('Tab');
    key('space');
    state = await check('Space confirms only the armed action once and restores focus', value => value.power.confirms === 1 && !value.power.armed && value.power.reboot.focused && value.popup.card.height === 232);
    ipc('external', 'power-desktop');
    state = await check('desktop power has no blank battery region', value => !value.power.battery && value.popup.card.height === 172);
    await screenshot('tasks-power-desktop');
    ipc('external', 'power-devices');
    state = await check('external batteries fit two rows even without a main battery', value => value.power.devices?.height === 72 && value.popup.card.height === 256);
    await screenshot('tasks-power-devices');
    ipc('external', 'power-no-devices');
    ipc('external', 'power-laptop');
    await check('laptop power returns to its compact resting size', value => value.power.battery && !value.power.devices && value.popup.card.height === 232);

    state = await open('connectivity', value => value.connectivity.row?.rect);
    click(state.connectivity.row.rect);
    state = await check('secured network expands a focused masked editor', value => value.connectivity.expanded && value.connectivity.input?.rect && value.connectivity.input.focused && value.connectivity.inputMasked);
    input('type', 'fixture-only');
    state = await check('network secret remains represented only as masked presence', value => value.connectivity.hasInput && value.connectivity.inputMasked && value.connectivity.sameInput);
    ipc('external', 'network-update');
    state = await check('live network update preserves masked input and row identity', value => value.connectivity.generation === 1 && value.connectivity.expanded && value.connectivity.hasInput && value.connectivity.inputMasked && value.connectivity.sameInput && value.connectivity.connects === 0);
    await screenshot('tasks-connectivity-masked-update');

    state = await open('notifications', value => value.notifications.input?.rect);
    click(state.notifications.input.rect);
    state = await check('pointer focuses the notification reply editor', value => value.notifications.input.focused);
    input('type', 'fixture-only');
    state = await check('notification reply draft is tracked as a boolean only', value => value.notifications.hasInput && value.notifications.knownDraft && value.notifications.edits > 0);
    ipc('external', 'notification-update');
    state = await check('external notification update preserves the live reply editor', value => value.notifications.externalUpdates === 1 && value.notifications.hasInput && value.notifications.knownDraft && value.notifications.sameInput);
    click(state.chips.notifications.rect);
    await check('closing notifications retains the unsent reply draft', value => !value.popup.open && value.notifications.knownDraft);
    state = await open('notifications', value => value.notifications.input?.rect);
    assert(state.notifications.hasInput && state.notifications.knownDraft, 'notification draft must survive reopen');
    click(state.notifications.input.rect);
    await check('reopened notification draft accepts keyboard focus', value => value.notifications.input.focused);
    key('Return');
    state = await check('failed reply keeps the draft and exposes retryable failure', value => value.notifications.sends === 1 && value.notifications.failed && value.notifications.hasInput && value.notifications.knownDraft);
    await screenshot('tasks-notification-reply-retry');
    key('Escape');
    await check('Escape dismisses an active reply without discarding it', value => !value.popup.open && value.notifications.knownDraft);
    state = await open('brightness', value => value.brightness.slider?.rect);
    click({ ...state.brightness.slider.rect, width: state.brightness.slider.rect.width * 1.5 });
    state = await check('brightness pointer updates the display', value => value.brightness.writes === 1 && value.brightness.level > 0.7);
    key('Right');
    state = await check('brightness keyboard updates the same display', value => value.brightness.writes === 2);
    ipc('external', 'brightness-update');
    state = await check('brightness follows external updates after local actions without writeback', value => value.brightness.value === 0.25 && value.brightness.writes === 2);
    await screenshot('tasks-brightness-live');
    ipc('external', 'brightness-unavailable');
    state = await check('unavailable display disables its slider', value => !value.brightness.slider.enabled);
    click(state.brightness.slider.rect);
    assert.equal(snapshot().brightness.writes, 2);
    await screenshot('tasks-brightness-unavailable');

    for (const docked of [false, true]) {
        const outputs = [{ name: 'LVDS-1', model: 'internal panel' }];
        if (docked) outputs.push({ name: 'HDMI-A-1', model: 'external monitor' });
        const displays = Brightness.displayRows({ present: true, level: 0.5 }, outputs, {});
        ipc('external', 'brightness-state:' + JSON.stringify({
            displays,
            global: Brightness.globalState(displays),
            setup: { needed: displays.some(row => row.kind === 'ddc' && !row.controllable), available: true },
        }));
        await check(docked ? 'docked laptop offers DDC only for its external monitor' : 'internal laptop panel has backlight control without DDC setup', value =>
            value.brightness.builtIn?.enabled && value.brightness.setupVisible === docked &&
            value.brightness.displayIds.join(',') === (docked ? 'backlight,ddc:HDMI-A-1' : 'backlight'));
        await screenshot(docked ? 'tasks-brightness-docked' : 'tasks-brightness-laptop');
    }
    ipc('external', 'brightness-state:null');

    state = await open('noko', value => value.noko.capture?.rect);
    click(state.noko.capture.rect);
    state = await check('Noko capture button starts the fixture action', value => value.noko.listening && value.noko.triggers === 1);
    key('space');
    state = await check('Noko capture supports keyboard stop', value => !value.noko.listening && value.noko.triggers === 2);
    key('Tab');
    await check('Noko history copy is keyboard reachable', value => value.noko.copy?.focused);
    key('space');
    await check('Noko copy displays completion', value => value.noko.copied);
    await check('Noko copy reaches the native sandbox clipboard', () => run('wl-paste', ['--list-types']).split('\n').includes('text/plain') && run('wl-paste', ['--no-newline', '--type', 'text/plain']) === 'synthetic capture 0');
    for (let i = 0; i < 8; i++) {
        key('Tab');
        await check(`Noko history keyboard focus remains visible ${i}`, value => {
            const f = value.noko.focusedRect, v = value.noko.viewport;
            return f && v && f.y >= v.y && f.y + f.height <= v.y + v.height;
        });
    }
    const nokoScroll = snapshot().noko.scrollY;
    assert(nokoScroll > 0, 'Noko history actually crosses its viewport boundary');
    await screenshot('tasks-noko-history');
    state = await open('vault', value => value.vault.search?.rect);
    await check('vault search receives focus', value => value.vault.search.focused);
    for (let i = 0; i < 14; i++) {
        key('Down');
        state = await check(`vault selected result remains visible ${i}`, value => {
            const r = value.vault.selected, v = value.vault.viewport;
            return value.vault.selectedId === `item-${i + 1}` && r && v && r.y >= v.y && r.y + r.height <= v.y + v.height;
        });
    }
    assert(state.vault.scrollY > 0, 'vault selection actually crosses viewport boundary');
    await screenshot('tasks-vault-keyboard');
    input('type', 'absent');
    await check('zero vault matches clear stale selection and detail', value => value.vault.count === 0 && value.vault.detailEmpty && value.vault.selectedId === '' && value.vault.queryKnown);
    await screenshot('tasks-vault-empty');
    key('ctrl+a');
    key('BackSpace');
    await check('clearing vault search restores a valid selection', value => value.vault.count === 20 && value.vault.selectedId === 'item-0');
    for (let i = 0; i < 12; i++) key('Down');
    state = await check('vault navigation stores its scroll position', value => value.vault.selectedId === 'item-12' && value.vault.scrollY > 0);
    const vaultScroll = state.vault.scrollY;
    click(state.chips.vault.rect);
    await check('vault closes', value => !value.popup.open);
    state = await open('vault', value => value.vault.search?.rect);
    assert.equal(state.vault.selectedId, 'item-12');
    assert.equal(state.vault.scrollY, vaultScroll, 'vault restores scroll after Loader recreation');
    key('Escape');
    await check('Escape closes vault search', value => !value.popup.open);
    ipc('external', 'vertical');
    await check('real Bar moves to vertical layout', value => value.orientation === 'vertical' && Object.values(value.chips).every(chip => within(chip.rect, value.width, value.height)));
    await check('vertical clock retains its centre and widget-clearance rule', value => value.clock && value.clock.x + value.clock.width / 2 === value.barWidth / 2 && value.clock.y === Math.min(value.height / 2 - value.clock.height / 2, Math.min(...Object.values(value.chips).map(chip => chip.rect.y)) - value.clock.height - 12));
    await check('vertical Bar accent remains a right-edge hairline after orientation change', value => value.barEdge.width === 1 && value.barEdge.height === value.height && value.barEdge.x === value.barWidth - 1);
    for (const [name, primary] of [
        ['clipboard', value => value.clipboard.search?.rect],
        ['launcher', value => value.launcher.search?.rect],
        ['audio', value => value.audio.slider?.rect],
        ['media', value => value.media.play?.rect],
        ['tray', value => value.tray.primary?.rect],
        ['power', value => value.power.reboot?.rect],
        ['connectivity', value => value.connectivity.row?.rect],
        ['notifications', value => value.notifications.input?.rect],
        ['brightness', value => value.brightness.slider?.rect],
        ['vault', value => value.vault.search?.rect],
        ['noko', value => value.noko.capture?.rect],
    ]) {
        state = await open(name, primary);
        if (name === 'clipboard') assert(state.clipboard.queryKnown && state.clipboard.visibleRows === 2);
        if (name === 'launcher') assert(state.launcher.queryKnown && state.launcher.visibleRows === 1);
        if (name === 'connectivity') assert(state.connectivity.expanded && !state.connectivity.hasInput, 'SSID survives but credential input does not');
        if (name === 'notifications') assert(state.notifications.knownDraft && state.notifications.hasInput);
        if (name === 'noko') assert.equal(state.noko.scrollY, nokoScroll, 'Noko history restores actual viewport position after orientation change');
        if (name === 'vault') {
            const r = state.vault.selected, v = state.vault.viewport;
            assert.equal(state.vault.scrollY, vaultScroll);
            assert(r && v && r.y >= v.y && r.y + r.height <= v.y + v.height, 'vault selection is visibly restored after orientation change');
        }
        await screenshot(`tasks-${name}-vertical`);
        if (name === 'media' || name === 'tray') {
            ipc('external', `${name}-empty`);
            state = await check(`${name} empty popup stays compact in vertical layout`, value => value[name].emptyState && value.popup.card.width === 352 && value.popup.card.height === 88);
            assertGeometry(state, state[name].emptyState, `vertical empty ${name}`);
            await screenshot(`tasks-${name}-empty-vertical`);
            ipc('external', `${name}-populated`);
            await check(`${name} repopulates in vertical layout`, value => !value[name].emptyState && primary(value));
        }
    }
}