Files
DankMaterialShell/quickshell/tests/tst_OutputCycleState.qml
T
Connor Welsh ba2b4a4d5d outputs: only re-enable displays the cycle itself turned off (#3377)
The cycle fallback fired from a never-cycled state. With one display
enabled and another marked off in settings, a DPMS standby that drops
the DisplayPort link looked like an unplug, and DMS sent an On to the
disabled display over niri IPC. It stayed on until the next config
reload and the workspaces round tripped through it.

Track each output's enabled state the first time a cycle touches it,
drop the entry once it is back at that state, and only rescue outputs
that were on before the cycle disabled them. Outputs that were already
off are never enabled automatically.
2026-09-09 13:02:21 -04:00

204 lines
8.6 KiB
QML

import QtQuick
import QtTest
import "../Services/OutputCycleState.js" as OutputCycleState
TestCase {
name: "OutputCycleState"
function output(id, enabled) {
return { id: id, enabled: enabled };
}
function test_disabledTargetStartsPendingCycle() {
const result = OutputCycleState.requestCycle(OutputCycleState.emptyState(), [
output("HDMI-A-1", false),
output("eDP-1", true)
], "eDP-1");
compare(result.status, "accepted");
compare(result.state.pending, "HDMI-A-1");
compare(result.intents.length, 1);
compare(result.intents[0].id, "HDMI-A-1");
verify(result.intents[0].enabled);
}
function test_confirmedTargetDisablesEveryOtherEnabledOutput() {
const state = { ring: ["HDMI-A-1", "eDP-1", "DP-1"], selected: "eDP-1", pending: "HDMI-A-1" };
const result = OutputCycleState.handleOutputsChanged(state, [
output("DP-1", true),
output("HDMI-A-1", true),
output("eDP-1", true)
]);
compare(result.state.pending, "");
compare(result.state.selected, "HDMI-A-1");
compare(result.intents.length, 2);
compare(result.intents[0].id, "DP-1");
verify(!result.intents[0].enabled);
compare(result.intents[1].id, "eDP-1");
verify(!result.intents[1].enabled);
}
function test_alreadyEnabledTargetDisablesPeersImmediately() {
const result = OutputCycleState.requestCycle(OutputCycleState.emptyState(), [
output("DP-1", true),
output("eDP-1", true)
], "DP-1");
compare(result.status, "accepted");
compare(result.state.selected, "eDP-1");
compare(result.intents.length, 1);
compare(result.intents[0].id, "DP-1");
verify(!result.intents[0].enabled);
}
function test_emptyMapRetainsState() {
const state = { ring: ["HDMI-A-1", "eDP-1"], selected: "HDMI-A-1", pending: "" };
const result = OutputCycleState.handleOutputsChanged(state, []);
compare(result.state.ring, state.ring);
compare(result.state.selected, state.selected);
compare(result.state.pending, state.pending);
compare(result.intents.length, 0);
}
function test_emptyMapRetainsPendingCycleUntilTargetConfirms() {
const state = { ring: ["HDMI-A-1", "eDP-1"], selected: "eDP-1", pending: "HDMI-A-1" };
const empty = OutputCycleState.handleOutputsChanged(state, []);
compare(empty.state.ring, state.ring);
compare(empty.state.selected, "eDP-1");
compare(empty.state.pending, "HDMI-A-1");
compare(empty.intents.length, 0);
const confirmed = OutputCycleState.handleOutputsChanged(empty.state, [
output("HDMI-A-1", true),
output("eDP-1", true)
]);
compare(confirmed.state.pending, "");
compare(confirmed.state.selected, "HDMI-A-1");
compare(confirmed.intents.length, 1);
compare(confirmed.intents[0].id, "eDP-1");
verify(!confirmed.intents[0].enabled);
}
function test_unplugPreservesAllEnabledOutputs() {
const state = { ring: ["DP-1", "HDMI-A-1", "eDP-1"], selected: "HDMI-A-1", pending: "" };
const result = OutputCycleState.handleOutputsChanged(state, [
output("DP-1", true),
output("eDP-1", true)
]);
compare(result.state.pending, "");
compare(result.intents, []);
}
function test_disabledFallbackOnlyEnablesPreviousOutput() {
const state = { ring: ["DP-1", "HDMI-A-1", "eDP-1"], selected: "HDMI-A-1", pending: "", original: { "DP-1": true, "eDP-1": true } };
const result = OutputCycleState.handleOutputsChanged(state, [
output("DP-1", false),
output("eDP-1", false)
]);
compare(result.state.pending, "");
compare(result.state.selected, "DP-1");
compare(result.intents.length, 1);
compare(result.intents[0].id, "DP-1");
verify(result.intents[0].enabled);
}
function test_unplugWithoutCyclePreservesWorkingDisplay() {
let state = OutputCycleState.handleOutputsChanged(OutputCycleState.emptyState(), [
output("DP-1", true), output("DP-2", false), output("eDP-1", false)
]).state;
state = OutputCycleState.handleOutputsChanged(state, [
output("DP-1", true), output("DP-2", true), output("eDP-1", false)
]).state;
const unplugged = OutputCycleState.handleOutputsChanged(state, [
output("DP-2", true), output("eDP-1", false)
]);
compare(unplugged.intents, []);
compare(unplugged.state.pending, "");
const repeated = OutputCycleState.handleOutputsChanged(unplugged.state, [
output("DP-2", true), output("eDP-1", false)
]);
compare(repeated.intents, []);
}
function test_fallbackConfirmationDoesNotDisableNewlyEnabledPeer() {
const heads = [output("DP-1", true), output("DP-2", false), output("eDP-1", false)];
const initial = OutputCycleState.handleOutputsChanged(OutputCycleState.emptyState(), heads).state;
const cycle = OutputCycleState.requestCycle(initial, heads, "DP-1");
compare(cycle.intents, [{ id: "DP-2", enabled: true }]);
const cycled = OutputCycleState.handleOutputsChanged(cycle.state, [
output("DP-1", true), output("DP-2", true), output("eDP-1", false)
]);
compare(cycled.intents, [{ id: "DP-1", enabled: false }]);
const unplugged = OutputCycleState.handleOutputsChanged(cycled.state, [
output("DP-1", false), output("eDP-1", false)
]);
compare(unplugged.intents, [{ id: "DP-1", enabled: true }]);
const confirmed = OutputCycleState.handleOutputsChanged(unplugged.state, [
output("DP-1", true), output("eDP-1", false)
]);
compare(confirmed.intents, []);
compare(confirmed.state.pending, "");
compare(confirmed.state.original, { "DP-2": false });
}
function test_unplugWithoutCycleLeavesDisabledOutputAlone() {
const state = OutputCycleState.handleOutputsChanged(OutputCycleState.emptyState(), [
output("DP-2", true), output("HDMI-A-2", false)
]).state;
const asleep = OutputCycleState.handleOutputsChanged(state, [output("HDMI-A-2", false)]);
compare(asleep.intents, []);
const awake = OutputCycleState.handleOutputsChanged(asleep.state, [
output("DP-2", true), output("HDMI-A-2", false)
]);
compare(awake.intents, []);
}
function test_cycleRoundTripForgetsOutputDmsEnabled() {
let state = OutputCycleState.handleOutputsChanged(OutputCycleState.emptyState(), [
output("DP-2", true), output("HDMI-A-2", false)
]).state;
state = OutputCycleState.requestCycle(state, [output("DP-2", true), output("HDMI-A-2", false)], "DP-2").state;
state = OutputCycleState.handleOutputsChanged(state, [output("DP-2", true), output("HDMI-A-2", true)]).state;
state = OutputCycleState.handleOutputsChanged(state, [output("DP-2", false), output("HDMI-A-2", true)]).state;
state = OutputCycleState.requestCycle(state, [output("DP-2", false), output("HDMI-A-2", true)], "HDMI-A-2").state;
state = OutputCycleState.handleOutputsChanged(state, [output("DP-2", true), output("HDMI-A-2", true)]).state;
state = OutputCycleState.handleOutputsChanged(state, [output("DP-2", true), output("HDMI-A-2", false)]).state;
compare(state.original, {});
const asleep = OutputCycleState.handleOutputsChanged(state, [output("HDMI-A-2", false)]);
compare(asleep.intents, []);
}
function test_missingPendingTargetCancelsWithoutIntent() {
const state = { ring: ["HDMI-A-1", "eDP-1"], selected: "eDP-1", pending: "HDMI-A-1" };
const result = OutputCycleState.handleOutputsChanged(state, [output("eDP-1", true)]);
compare(result.state.pending, "");
compare(result.intents.length, 0);
}
function test_pendingCycleIsBusy() {
const state = { ring: ["HDMI-A-1", "eDP-1"], selected: "eDP-1", pending: "HDMI-A-1" };
const result = OutputCycleState.requestCycle(state, [output("HDMI-A-1", false), output("eDP-1", true)], "eDP-1");
compare(result.status, "busy");
compare(result.intents.length, 0);
}
function test_singleOutputIsNoop() {
const state = OutputCycleState.emptyState();
const result = OutputCycleState.requestCycle(state, [output("eDP-1", true)], "eDP-1");
compare(result.status, "no-op");
compare(result.intents.length, 0);
}
}