breakManager: Add new state machine for screen time/health breaks
This implements health break reminder support in gnome-shell. It depends on a few bits and bobs from other modules: - New settings schemas in gsettings-desktop-schemas (released in 47.beta, which Mutter already depends on) - A settings UI in gnome-control-center - User documentation in gnome-user-docs It implements the design from https://gitlab.gnome.org/Teams/Design/settings-mockups/-/blob/master/wellbeing/wellbeing.png. The core of the implementation is `BreakManager`, which is a state machine which uses the Mutter `IdleMonitor` to track whether the user is, or should be, in a screen time break. The `BreakDispatcher` is based on top of this, and controls showing notifications, countdown timers, screen fades, the lock shield, etc. to make the user aware of upcoming or due breaks, as per their notification preferences. Unit tests are included to check that `BreakManager` works. These provide mock implementations of basic GLib clock functions, the `IdleMonitor` and `Gio.Settings` in order to test the state machine in faster-than-real-time. Signed-off-by: Philip Withnall <pwithnall@gnome.org> See: https://gitlab.gnome.org/Teams/Design/initiatives/-/issues/130 Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3251>
This commit is contained in:
parent
03e3d9f435
commit
df3b4d302d
@ -4,6 +4,7 @@ dbus_interfaces = [
|
||||
'org.gnome.Shell.PadOsd.xml',
|
||||
'org.gnome.Shell.Screencast.xml',
|
||||
'org.gnome.Shell.Screenshot.xml',
|
||||
'org.gnome.Shell.ScreenTime.xml',
|
||||
'org.gnome.ShellSearchProvider.xml',
|
||||
'org.gnome.ShellSearchProvider2.xml'
|
||||
]
|
||||
|
38
data/dbus-interfaces/org.gnome.Shell.ScreenTime.xml
Normal file
38
data/dbus-interfaces/org.gnome.Shell.ScreenTime.xml
Normal file
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE node PUBLIC
|
||||
'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN'
|
||||
'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd'>
|
||||
<!--
|
||||
SPDX-License-Identifier: GPL-3.0-or-later
|
||||
SPDX-FileCopyrightText: 2024 GNOME Foundation, Inc.
|
||||
-->
|
||||
<node>
|
||||
|
||||
<!--
|
||||
org.gnome.Shell.ScreenTime:
|
||||
@short_description: Screen Time interface
|
||||
|
||||
The interface used to access Screen Time and Break Reminders usage data.
|
||||
-->
|
||||
<interface name="org.gnome.Shell.ScreenTime">
|
||||
|
||||
<!--
|
||||
State:
|
||||
|
||||
The state of the break manager.
|
||||
- 0: Break reminders are disabled.
|
||||
- 1: Break reminders are enabled, user is active, no break is needed yet.
|
||||
- 2: A break is needed and the user is taking it.
|
||||
- 3: A break is needed but the user is still active.
|
||||
-->
|
||||
<property name="State" type="u" access="read"/>
|
||||
|
||||
<!--
|
||||
LastBreakEndTime:
|
||||
|
||||
The time (in wall clock seconds since the Unix epoch) when the most
|
||||
recent break ended. If there have been no breaks so far, zero is
|
||||
returned.
|
||||
-->
|
||||
<property name="LastBreakEndTime" type="t" access="read"/>
|
||||
</interface>
|
||||
</node>
|
@ -51,6 +51,7 @@
|
||||
<file preprocess="xml-stripblanks">org.gnome.Shell.PortalHelper.xml</file>
|
||||
<file preprocess="xml-stripblanks">org.gnome.Shell.Screencast.xml</file>
|
||||
<file preprocess="xml-stripblanks">org.gnome.Shell.Screenshot.xml</file>
|
||||
<file preprocess="xml-stripblanks">org.gnome.Shell.ScreenTime.xml</file>
|
||||
<file preprocess="xml-stripblanks">org.gnome.Shell.Wacom.PadOsd.xml</file>
|
||||
<file preprocess="xml-stripblanks">org.gnome.Shell.WeatherIntegration.xml</file>
|
||||
<file preprocess="xml-stripblanks">org.gnome.Shell.xml</file>
|
||||
|
@ -53,6 +53,7 @@
|
||||
<file preprocess="xml-stripblanks">scalable/status/screen-privacy-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/status/switch-on-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/status/switch-off-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/status/wellbeing-symbolic.svg</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
|
||||
|
11
data/icons/scalable/status/wellbeing-symbolic.svg
Normal file
11
data/icons/scalable/status/wellbeing-symbolic.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
SPDX-License-Identifier: CC0-1.0
|
||||
SPDX-FileCopyrightText: 2024 Jakub Steiner
|
||||
-->
|
||||
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
|
||||
<g fill="#222222">
|
||||
<path d="m 3.3125 13.683594 c 0.011719 0.007812 0.023438 0.015625 0.039062 0.019531 l 4.195313 2.179687 c 0.289063 0.152344 0.636719 0.152344 0.925781 0 l 4.152344 -2.179687 c 0.019531 -0.007813 0.039062 -0.019531 0.058594 -0.03125 c 3.988281 -2.449219 2.25 -7.671875 -1.703125 -7.671875 c -1.011719 0 -1.75 0.441406 -2.238281 1.128906 c -0.894532 1.257813 -0.691407 1.277344 -1.621094 0.042969 c -0.554688 -0.730469 -1.320313 -1.203125 -2.128906 -1.171875 c -3.996094 0 -5.652344 5.390625 -1.679688 7.683594 z m 7.667969 -5.6875 c 1.726562 0 3.132812 2.449218 0.65625 3.96875 l 0.058593 -0.035156 l -4.152343 2.179687 h 0.925781 l -4.195312 -2.179687 l 0.039062 0.023437 c -2.386719 -1.378906 -1.097656 -3.957031 0.679688 -3.957031 c 1.378906 0 2.039062 0.945312 2.320312 1.535156 c 0.117188 0.25 0.398438 0.46875 0.675781 0.46875 h 0.011719 c 0.277344 0 0.558594 -0.21875 0.675781 -0.46875 c 0.28125 -0.589844 0.945313 -1.535156 2.304688 -1.535156 z m 0 0"/>
|
||||
<path d="m 8 4 c -0.546875 0 -1 -0.453125 -1 -1 s 0.453125 -1 1 -1 s 1 0.453125 1 1 s -0.453125 1 -1 1 z m 0 -4 c -1.660156 0 -3 1.339844 -3 3 s 1.339844 3 3 3 s 3 -1.339844 3 -3 s -1.339844 -3 -3 -3 z m 0 0"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
@ -35,7 +35,7 @@ $osd_levelbar_height:6px;
|
||||
}
|
||||
|
||||
// Monitor number label
|
||||
.osd-monitor-label {
|
||||
.osd-monitor-label, .osd-break-countdown-label {
|
||||
background-color: -st-accent-color;
|
||||
color: -st-accent-fg-color;
|
||||
border-radius: $modal_radius;
|
||||
|
@ -15,6 +15,7 @@
|
||||
<file>extensions/sharedInternals.js</file>
|
||||
|
||||
<file>misc/animationUtils.js</file>
|
||||
<file>misc/breakManager.js</file>
|
||||
<file>misc/config.js</file>
|
||||
<file>misc/dateUtils.js</file>
|
||||
<file>misc/dbusErrors.js</file>
|
||||
|
1439
js/misc/breakManager.js
Normal file
1439
js/misc/breakManager.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@ import St from 'gi://St';
|
||||
|
||||
import * as AccessDialog from './accessDialog.js';
|
||||
import * as AudioDeviceSelection from './audioDeviceSelection.js';
|
||||
import * as BreakManager from '../misc/breakManager.js';
|
||||
import * as Config from '../misc/config.js';
|
||||
import * as Components from './components.js';
|
||||
import * as CtrlAltTab from './ctrlAltTab.js';
|
||||
@ -89,6 +90,9 @@ export let inputMethod = null;
|
||||
export let introspectService = null;
|
||||
export let locatePointer = null;
|
||||
export let endSessionDialog = null;
|
||||
export let breakManager = null;
|
||||
export let screenTimeDBus = null;
|
||||
export let breakManagerDispatcher = null;
|
||||
|
||||
let _startDate;
|
||||
let _defaultCssStylesheet = null;
|
||||
@ -244,6 +248,11 @@ async function _initializeUI() {
|
||||
|
||||
introspectService = new Introspect.IntrospectService();
|
||||
|
||||
// Set up the global default break reminder manager and its D-Bus interface
|
||||
breakManager = new BreakManager.BreakManager();
|
||||
screenTimeDBus = new ShellDBus.ScreenTimeDBus(breakManager);
|
||||
breakManagerDispatcher = new BreakManager.BreakDispatcher(breakManager);
|
||||
|
||||
layoutManager.init();
|
||||
overview.init();
|
||||
|
||||
|
@ -15,6 +15,7 @@ import {ControlsState} from './overviewControls.js';
|
||||
|
||||
const GnomeShellIface = loadInterfaceXML('org.gnome.Shell');
|
||||
const ScreenSaverIface = loadInterfaceXML('org.gnome.ScreenSaver');
|
||||
const ScreenTimeIface = loadInterfaceXML('org.gnome.Shell.ScreenTime');
|
||||
|
||||
export class GnomeShell {
|
||||
constructor() {
|
||||
@ -542,3 +543,30 @@ export class ScreenSaverDBus {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class ScreenTimeDBus {
|
||||
constructor(breakManager) {
|
||||
this._manager = breakManager;
|
||||
|
||||
this._manager.connect('notify::state', this._onNotify.bind(this));
|
||||
this._manager.connect('notify::last-break-end-time', this._onNotify.bind(this));
|
||||
|
||||
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(ScreenTimeIface, this);
|
||||
this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell/ScreenTime');
|
||||
}
|
||||
|
||||
_onNotify() {
|
||||
// We always want to notify the two properties together, as clients need
|
||||
// both values to be useful. GJS will combine the two emissions for us.
|
||||
this._dbusImpl.emit_property_changed('State', new GLib.Variant('u', this.State));
|
||||
this._dbusImpl.emit_property_changed('LastBreakEndTime', new GLib.Variant('t', this.LastBreakEndTime));
|
||||
}
|
||||
|
||||
get State() {
|
||||
return this._manager.state;
|
||||
}
|
||||
|
||||
get LastBreakEndTime() {
|
||||
return this._manager.lastBreakEndTime;
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ gjs_req = '>= 1.73.1'
|
||||
gtk_req = '>= 4.0'
|
||||
mutter_req = '>= 47.0'
|
||||
polkit_req = '>= 0.100'
|
||||
schemas_req = '>= 47.alpha'
|
||||
schemas_req = '>= 47.beta'
|
||||
systemd_req = '>= 246'
|
||||
ibus_req = '>= 1.5.19'
|
||||
gnome_desktop_req = '>= 40'
|
||||
|
@ -12,6 +12,7 @@ js/dbusServices/extensions/ui/extension-error-page.ui
|
||||
js/gdm/authPrompt.js
|
||||
js/gdm/loginDialog.js
|
||||
js/gdm/util.js
|
||||
js/misc/breakManager.js
|
||||
js/misc/systemActions.js
|
||||
js/misc/util.js
|
||||
js/misc/dateUtils.js
|
||||
|
@ -20,6 +20,7 @@ unit_testenv.append('GI_TYPELIB_PATH', shell_typelib_path, separator: ':')
|
||||
unit_testenv.append('GI_TYPELIB_PATH', st_typelib_path, separator: ':')
|
||||
|
||||
unit_tests = [
|
||||
'breakManager',
|
||||
'extensionUtils',
|
||||
'highlighter',
|
||||
'injectionManager',
|
||||
|
513
tests/unit/breakManager.js
Normal file
513
tests/unit/breakManager.js
Normal file
@ -0,0 +1,513 @@
|
||||
// Copyright 2024 GNOME Foundation, Inc.
|
||||
//
|
||||
// This is a GNOME Shell component to support break reminders and screen time
|
||||
// statistics.
|
||||
//
|
||||
// Licensed under the GNU General Public License Version 2
|
||||
//
|
||||
// This program is free software; you can redistribute it and/or
|
||||
// modify it under the terms of the GNU General Public License
|
||||
// as published by the Free Software Foundation; either version 2
|
||||
// of the License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program; if not, write to the Free Software
|
||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import 'resource:///org/gnome/shell/ui/environment.js';
|
||||
import GLib from 'gi://GLib';
|
||||
|
||||
import * as BreakManager from 'resource:///org/gnome/shell/misc/breakManager.js';
|
||||
|
||||
// Convenience alias
|
||||
const {BreakState} = BreakManager;
|
||||
|
||||
// A harness for testing the BreakManager class. It simulates the passage of
|
||||
// time, maintaining an internal ordered queue of events, and providing three
|
||||
// groups of mock functions which the BreakManager uses to interact with it:
|
||||
// a mock version of the IdleMonitor, mock versions of GLib’s clock and timeout
|
||||
// functions, and a mock version of Gio.Settings.
|
||||
//
|
||||
// The internal ordered queue of events is sorted by time (in seconds since an
|
||||
// arbitrary epoch; the tests arbitrarily start from 100s to avoid potential
|
||||
// issues around time zero). On each _tick(), the next event is shifted off the
|
||||
// head of the queue and processed. An event might be a simulated idle watch
|
||||
// (mocking the user being idle), a simulated active watch, a scheduled timeout,
|
||||
// or an assertion function for actually running test assertions.
|
||||
//
|
||||
// The simulated clock jumps from the scheduled time of one event to the
|
||||
// scheduled time of the next. This way, we can simulate half an hour of active
|
||||
// time (waiting for the next rest break to be due) on the computer instantly.
|
||||
class TestHarness {
|
||||
constructor(settings) {
|
||||
this._currentTimeSecs = 100;
|
||||
this._nextSourceId = 1;
|
||||
this._events = [];
|
||||
this._idleWatch = null;
|
||||
this._activeWatch = null;
|
||||
this._settings = settings;
|
||||
}
|
||||
|
||||
_allocateSourceId() {
|
||||
const sourceId = this._nextSourceId;
|
||||
this._nextSourceId++;
|
||||
return sourceId;
|
||||
}
|
||||
|
||||
_removeEventBySourceId(sourceId) {
|
||||
const idx = this._events.findIndex(a => {
|
||||
return a.sourceId === sourceId;
|
||||
});
|
||||
console.assert(idx !== -1);
|
||||
this._events.splice(idx, 1);
|
||||
}
|
||||
|
||||
_insertEvent(event) {
|
||||
this._events.push(event);
|
||||
this._events.sort((a, b) => {
|
||||
return a.time - b.time;
|
||||
});
|
||||
return event;
|
||||
}
|
||||
|
||||
// Add a timeout event to the event queue. It will be scheduled at the
|
||||
// current simulated time plus `intervalSecs`. `callback` will be invoked
|
||||
// when the event is processed.
|
||||
addTimeoutEvent(intervalSecs, callback) {
|
||||
return this._insertEvent({
|
||||
type: 'timeout',
|
||||
time: this._currentTimeSecs + intervalSecs,
|
||||
callback,
|
||||
sourceId: this._allocateSourceId(),
|
||||
intervalSecs,
|
||||
});
|
||||
}
|
||||
|
||||
// Add an idle watch event to the event queue. This simulates the user
|
||||
// becoming idle (no keyboard or mouse input) at time `timeSecs`.
|
||||
addIdleEvent(timeSecs) {
|
||||
return this._insertEvent({
|
||||
type: 'idle',
|
||||
time: timeSecs,
|
||||
});
|
||||
}
|
||||
|
||||
// Add an active watch event to the event queue. This simulates the user
|
||||
// becoming active (using the keyboard or mouse after a period of
|
||||
// inactivity) at time `timeSecs`.
|
||||
addActiveEvent(timeSecs) {
|
||||
return this._insertEvent({
|
||||
type: 'active',
|
||||
time: timeSecs,
|
||||
});
|
||||
}
|
||||
|
||||
// Add a delay action invocation to the event queue. This simulates the user
|
||||
// invoking the ‘delay’ action (typically via a notification) at time
|
||||
// `timeSecs`.
|
||||
addDelayAction(timeSecs, breakManager) {
|
||||
return this._insertEvent({
|
||||
type: 'action',
|
||||
time: timeSecs,
|
||||
callback: () => {
|
||||
breakManager.delayBreak();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Add a skip action invocation to the event queue. This simulates the user
|
||||
// invoking the ‘skip’ action (typically via a notification) at time
|
||||
// `timeSecs`.
|
||||
addSkipAction(timeSecs, breakManager) {
|
||||
return this._insertEvent({
|
||||
type: 'action',
|
||||
time: timeSecs,
|
||||
callback: () => {
|
||||
breakManager.skipBreak();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Add a take action invocation to the event queue. This simulates the user
|
||||
// invoking the ‘take’ action (typically via a notification) at time
|
||||
// `timeSecs`.
|
||||
addTakeAction(timeSecs, breakManager) {
|
||||
return this._insertEvent({
|
||||
type: 'action',
|
||||
time: timeSecs,
|
||||
callback: () => {
|
||||
breakManager.takeBreak();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Add an assertion event to the event queue. This is a callback which is
|
||||
// invoked when the simulated clock reaches `timeSecs`. The callback can
|
||||
// contain whatever test assertions you like.
|
||||
addAssertionEvent(timeSecs, callback) {
|
||||
return this._insertEvent({
|
||||
type: 'assertion',
|
||||
time: timeSecs,
|
||||
callback,
|
||||
});
|
||||
}
|
||||
|
||||
// Add a state assertion event to the event queue. This is a specialised
|
||||
// form of `addAssertionEvent()` which asserts that the `BreakManager.state`
|
||||
// equals `state` at time `timeSecs`.
|
||||
expectState(timeSecs, breakManager, expectedState) {
|
||||
return this.addAssertionEvent(timeSecs, () => {
|
||||
expect(BreakManager.breakStateToString(breakManager.state))
|
||||
.withContext(`${timeSecs}s state`)
|
||||
.toEqual(BreakManager.breakStateToString(expectedState));
|
||||
});
|
||||
}
|
||||
|
||||
// Add a state assertion event to the event queue. This is a specialised
|
||||
// form of `addAssertionEvent()` which asserts that the given `BreakManager`
|
||||
// properties equal the expected values at time `timeSecs`.
|
||||
expectProperties(timeSecs, breakManager, expectedProperties) {
|
||||
return this.addAssertionEvent(timeSecs, () => {
|
||||
for (const [name, expectedValue] of Object.entries(expectedProperties)) {
|
||||
expect(breakManager[name])
|
||||
.withContext(`${timeSecs}s ${name}`)
|
||||
.toEqual(expectedValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_popEvent() {
|
||||
return this._events.shift();
|
||||
}
|
||||
|
||||
// Get a mock clock object for use in the `BreakManager` under test.
|
||||
// This provides a basic implementation of GLib’s clock and timeout
|
||||
// functions which use the simulated clock and event queue.
|
||||
get mockClock() {
|
||||
return {
|
||||
getRealTimeSecs: () => {
|
||||
return this._currentTimeSecs;
|
||||
},
|
||||
timeoutAddSeconds: (priority, intervalSecs, callback) => {
|
||||
return this.addTimeoutEvent(intervalSecs, callback).sourceId;
|
||||
},
|
||||
sourceRemove: sourceId => {
|
||||
this._removeEventBySourceId(sourceId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Get a mock idle monitor object for use in the `BreakManager` under test.
|
||||
// This provides a basic implementation of the `IdleMonitor` which uses the
|
||||
// simulated clock and event queue.
|
||||
get mockIdleMonitor() {
|
||||
return {
|
||||
add_idle_watch: (waitMsec, callback) => {
|
||||
console.assert(this._idleWatch === null);
|
||||
this._idleWatch = {
|
||||
waitMsec,
|
||||
callback,
|
||||
};
|
||||
return 1;
|
||||
},
|
||||
|
||||
add_user_active_watch: callback => {
|
||||
console.assert(this._activeWatch === null);
|
||||
this._activeWatch = callback;
|
||||
return 2;
|
||||
},
|
||||
|
||||
remove_watch: id => {
|
||||
console.assert(id === 1 || id === 2);
|
||||
if (id === 1)
|
||||
this._idleWatch = null;
|
||||
else if (id === 2)
|
||||
this._activeWatch = null;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Get a mock settings factory for use in the `BreakManager` under test.
|
||||
// This is an object providing a couple of constructors for `Gio.Settings`
|
||||
// objects. Each constructor returns a basic implementation of
|
||||
// `Gio.Settings` which uses the settings dictionary passed to `TestHarness`
|
||||
// in its constructor.
|
||||
// This necessarily has an extra layer of indirection because there are
|
||||
// multiple ways to construct a `Gio.Settings`.
|
||||
get mockSettingsFactory() {
|
||||
return {
|
||||
new: schemaId => {
|
||||
return {
|
||||
connect: (unusedSignalName, unusedCallback) => {
|
||||
/* no-op for mock purposes */
|
||||
return 1;
|
||||
},
|
||||
get_boolean: key => {
|
||||
return this._settings[schemaId][key];
|
||||
},
|
||||
get_strv: key => {
|
||||
return this._settings[schemaId][key];
|
||||
},
|
||||
get_uint: key => {
|
||||
return this._settings[schemaId][key];
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
newWithPath: (schemaId, unusedPath) => {
|
||||
return {
|
||||
connect: (unusedSignalName, unusedCallback) => {
|
||||
/* no-op for mock purposes */
|
||||
return 1;
|
||||
},
|
||||
get_boolean: key => {
|
||||
return this._settings[schemaId][key];
|
||||
},
|
||||
get_strv: key => {
|
||||
return this._settings[schemaId][key];
|
||||
},
|
||||
get_uint: key => {
|
||||
return this._settings[schemaId][key];
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
_tick() {
|
||||
const event = this._popEvent();
|
||||
if (!event)
|
||||
return false;
|
||||
this._currentTimeSecs = event.time;
|
||||
|
||||
switch (event.type) {
|
||||
case 'timeout':
|
||||
if (event.callback()) {
|
||||
event.time += event.intervalSecs;
|
||||
this._insertEvent(event);
|
||||
}
|
||||
break;
|
||||
case 'idle':
|
||||
if (this._idleWatch)
|
||||
this._idleWatch.callback();
|
||||
break;
|
||||
case 'active':
|
||||
if (this._activeWatch) {
|
||||
this._activeWatch();
|
||||
this._activeWatch = null; // one-shot
|
||||
}
|
||||
break;
|
||||
case 'action':
|
||||
event.callback();
|
||||
break;
|
||||
case 'assertion':
|
||||
event.callback();
|
||||
break;
|
||||
default:
|
||||
console.assert(false, 'not reached');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Run the test in a loop, blocking until all events are processed or an
|
||||
// exception is raised.
|
||||
run() {
|
||||
const loop = new GLib.MainLoop(null, false);
|
||||
let innerException = null;
|
||||
|
||||
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
||||
try {
|
||||
if (this._tick())
|
||||
return GLib.SOURCE_CONTINUE;
|
||||
loop.quit();
|
||||
return GLib.SOURCE_REMOVE;
|
||||
} catch (e) {
|
||||
// Quit the main loop then re-raise the exception
|
||||
loop.quit();
|
||||
innerException = e;
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
});
|
||||
|
||||
loop.run();
|
||||
|
||||
// Did we exit with an exception?
|
||||
if (innerException)
|
||||
throw innerException;
|
||||
}
|
||||
}
|
||||
|
||||
describe('Break manager', () => {
|
||||
it('can be disabled via GSettings', () => {
|
||||
const harness = new TestHarness({
|
||||
'org.gnome.desktop.break-reminders': {
|
||||
'selected-breaks': [],
|
||||
},
|
||||
});
|
||||
const breakManager = new BreakManager.BreakManager(harness.mockClock, harness.mockIdleMonitor, harness.mockSettingsFactory);
|
||||
|
||||
harness.addActiveEvent(101);
|
||||
harness.expectState(102, breakManager, BreakState.DISABLED);
|
||||
harness.addIdleEvent(130);
|
||||
harness.expectState(135, breakManager, BreakState.DISABLED);
|
||||
|
||||
harness.run();
|
||||
});
|
||||
|
||||
// A simple test which simulates the user being active briefly, taking a short
|
||||
// break before one is due, and then being active again until their next break
|
||||
// is overdue.
|
||||
it('tracks a single break type', () => {
|
||||
const harness = new TestHarness({
|
||||
'org.gnome.desktop.break-reminders': {
|
||||
'selected-breaks': ['movement'],
|
||||
},
|
||||
'org.gnome.desktop.break-reminders.movement': {
|
||||
'duration-seconds': 300, /* 5 minutes */
|
||||
'interval-seconds': 1800, /* 30 minutes */
|
||||
'delay-seconds': 300, /* 5 minutes */
|
||||
'notify': true,
|
||||
'play-sound': false,
|
||||
'fade-screen': false,
|
||||
},
|
||||
});
|
||||
const breakManager = new BreakManager.BreakManager(harness.mockClock, harness.mockIdleMonitor, harness.mockSettingsFactory);
|
||||
|
||||
harness.addActiveEvent(101);
|
||||
harness.expectState(102, breakManager, BreakState.ACTIVE);
|
||||
harness.addIdleEvent(130);
|
||||
harness.expectState(135, breakManager, BreakState.IDLE);
|
||||
harness.addActiveEvent(200); // cut the break short before its duration
|
||||
harness.expectState(201, breakManager, BreakState.ACTIVE);
|
||||
harness.expectProperties(2001, breakManager, { // break is due after 30 mins
|
||||
'state': BreakState.BREAK_DUE,
|
||||
'currentBreakType': 'movement',
|
||||
'currentBreakStartTime': 0,
|
||||
'lastBreakEndTime': 100,
|
||||
});
|
||||
harness.addIdleEvent(2005);
|
||||
harness.expectProperties(2006, breakManager, {
|
||||
'state': BreakState.IN_BREAK,
|
||||
'currentBreakType': 'movement',
|
||||
'currentBreakStartTime': 1900,
|
||||
'lastBreakEndTime': 0,
|
||||
});
|
||||
harness.expectState(2195, breakManager, BreakState.IN_BREAK); // near the end of the break
|
||||
harness.expectState(2210, breakManager, BreakState.IDLE); // just after the end of the break
|
||||
harness.addActiveEvent(2320);
|
||||
harness.expectProperties(2321, breakManager, {
|
||||
'state': BreakState.ACTIVE,
|
||||
'currentBreakType': null,
|
||||
'currentBreakStartTime': 0,
|
||||
'lastBreakEndTime': 2320,
|
||||
});
|
||||
harness.addIdleEvent(4100); // start the next break a little early
|
||||
harness.expectState(4101, breakManager, BreakState.IDLE);
|
||||
harness.expectState(4121, breakManager, BreakState.IN_BREAK);
|
||||
harness.addActiveEvent(4420);
|
||||
harness.expectProperties(4421, breakManager, {
|
||||
'state': BreakState.ACTIVE,
|
||||
'currentBreakType': null,
|
||||
'currentBreakStartTime': 0,
|
||||
'lastBreakEndTime': 4420,
|
||||
});
|
||||
|
||||
harness.run();
|
||||
});
|
||||
|
||||
// Test requesting to delay a break.
|
||||
it('supports delaying a break', () => {
|
||||
const harness = new TestHarness({
|
||||
'org.gnome.desktop.break-reminders': {
|
||||
'selected-breaks': ['movement'],
|
||||
},
|
||||
'org.gnome.desktop.break-reminders.movement': {
|
||||
'duration-seconds': 300, /* 5 minutes */
|
||||
'interval-seconds': 1800, /* 30 minutes */
|
||||
'delay-seconds': 300, /* 5 minutes */
|
||||
'notify': true,
|
||||
'play-sound': false,
|
||||
'fade-screen': false,
|
||||
},
|
||||
});
|
||||
const breakManager = new BreakManager.BreakManager(harness.mockClock, harness.mockIdleMonitor, harness.mockSettingsFactory);
|
||||
|
||||
harness.addActiveEvent(101);
|
||||
harness.expectState(102, breakManager, BreakState.ACTIVE);
|
||||
harness.expectProperties(1901, breakManager, { // break is due after 30 mins
|
||||
'state': BreakState.BREAK_DUE,
|
||||
'currentBreakType': 'movement',
|
||||
'currentBreakStartTime': 0,
|
||||
'lastBreakEndTime': 100,
|
||||
});
|
||||
harness.addDelayAction(1902, breakManager);
|
||||
harness.expectProperties(1903, breakManager, { // break is delayed by 5 mins
|
||||
'state': BreakState.ACTIVE,
|
||||
'currentBreakType': null,
|
||||
'currentBreakStartTime': 0,
|
||||
'lastBreakEndTime': 400,
|
||||
});
|
||||
harness.expectProperties(2201, breakManager, { // break is due after another 5 mins
|
||||
'state': BreakState.BREAK_DUE,
|
||||
'currentBreakType': 'movement',
|
||||
'currentBreakStartTime': 0,
|
||||
'lastBreakEndTime': 400,
|
||||
});
|
||||
harness.addIdleEvent(2202);
|
||||
harness.expectState(2203, breakManager, BreakState.IN_BREAK);
|
||||
|
||||
harness.run();
|
||||
});
|
||||
|
||||
// Test requesting to skip a break.
|
||||
it('supports skipping a break', () => {
|
||||
const harness = new TestHarness({
|
||||
'org.gnome.desktop.break-reminders': {
|
||||
'selected-breaks': ['movement'],
|
||||
},
|
||||
'org.gnome.desktop.break-reminders.movement': {
|
||||
'duration-seconds': 300, /* 5 minutes */
|
||||
'interval-seconds': 1800, /* 30 minutes */
|
||||
'delay-seconds': 300, /* 5 minutes */
|
||||
'notify': true,
|
||||
'play-sound': false,
|
||||
'fade-screen': false,
|
||||
},
|
||||
});
|
||||
const breakManager = new BreakManager.BreakManager(harness.mockClock, harness.mockIdleMonitor, harness.mockSettingsFactory);
|
||||
|
||||
harness.addActiveEvent(101);
|
||||
harness.expectState(102, breakManager, BreakState.ACTIVE);
|
||||
harness.expectProperties(1901, breakManager, { // break is due after 30 mins
|
||||
'state': BreakState.BREAK_DUE,
|
||||
'currentBreakType': 'movement',
|
||||
'currentBreakStartTime': 0,
|
||||
'lastBreakEndTime': 100,
|
||||
});
|
||||
harness.addSkipAction(1902, breakManager);
|
||||
harness.expectProperties(1903, breakManager, { // break is skipped for 30 mins
|
||||
'state': BreakState.ACTIVE,
|
||||
'currentBreakType': null,
|
||||
'currentBreakStartTime': 0,
|
||||
'lastBreakEndTime': 1902,
|
||||
});
|
||||
harness.expectProperties(3703, breakManager, { // break is due after another 30 mins
|
||||
'state': BreakState.BREAK_DUE,
|
||||
'currentBreakType': 'movement',
|
||||
'currentBreakStartTime': 0,
|
||||
'lastBreakEndTime': 1902,
|
||||
});
|
||||
harness.addIdleEvent(3704);
|
||||
harness.expectState(3704, breakManager, BreakState.IN_BREAK);
|
||||
|
||||
harness.run();
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user