repositories / dotfiles
dotfiles
bugabingas dorkfiles
owned by admin
quickshell/nuguland/test-fixtures/ui/tasks.mjs
Rawimport 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));
}
}
}