gpii-windows
Version:
Components of the GPII personalization infrastructure for use on Microsoft's "Windows" ™
1,530 lines (1,379 loc) • 48.8 kB
JavaScript
/*
* eventLog Tests
*
* Copyright 2017 Raising the Floor - International
*
* Licensed under the New BSD license. You may not use this file except in
* compliance with this License.
*
* The R&D leading to these results received funding from the
* Department of Education - Grant H421A150005 (GPII-APCP). However,
* these results do not necessarily represent the policy of the
* Department of Education, and you should not assume endorsement by the
* Federal Government.
*
* You may obtain a copy of the License at
* https://github.com/GPII/universal/blob/master/LICENSE.txt
*/
;
var fluid = require("gpii-universal"),
fs = require("fs"),
os = require("os"),
net = require("net"),
json5 = require("json5"),
readline = require("readline");
require("../../processHandling/processHandling.js");
var jqUnit = fluid.require("node-jqunit");
var gpii = fluid.registerNamespace("gpii");
fluid.registerNamespace("gpii.tests.metrics");
require("../index.js");
var teardowns = [];
jqUnit.module("gpii.tests.metrics", {
teardown: function () {
while (teardowns.length) {
teardowns.pop()();
}
}
});
// Wrap the gpii.windowsMetrics to disable auto-starting the metrics capturing.
fluid.defaults("gpii.tests.metrics.windowsMetricsWrapper", {
gradeNames: ["fluid.component", "gpii.windowsMetrics", "gpii.eventLog", "gpii.lifecycleManager"],
listeners: {
"onStartMetrics.application": null,
"onStartMetrics.input": null,
"onStopMetrics.application": null,
"onStopMetrics.input": null,
"onCreate": {
funcName: "gpii.tests.metrics.captureLog",
args: ["{that}"]
}
},
invokers: {
"startMessages": "fluid.identity"
}
});
gpii.tests.metrics.defaultKeyboardConfig = fluid.freezeRecursive();
gpii.tests.metrics.configMessageTests = fluid.freezeRecursive([
{
// msg, wParam, lParam
input: [gpii.windows.API_constants.WM_SYSCOLORCHANGE, 1, 2],
expect: {
module: "metrics",
event: "config",
data: {
"wp": 1, "lp": 2, "msg": "WM_SYSCOLORCHANGE"
}
}
},
{
input: [gpii.windows.API_constants.WM_INPUTLANGCHANGE, 3, 4],
expect: {
module: "metrics",
event: "config",
data: {
"wp": 3, "lp": 4, "msg": "WM_INPUTLANGCHANGE"
}
}
},
{
input: [gpii.windows.API_constants.WM_THEMECHANGED, 5, 6],
expect: {
module: "metrics",
event: "config",
data: {
"wp": 5, "lp": 6, "msg": "WM_THEMECHANGED"
}
}
},
{
input: [gpii.windows.API_constants.WM_DISPLAYCHANGE, 32, gpii.windows.makeLong(1, 2)],
expect: {
module: "metrics",
event: "config.resolution",
data: {
"wp": 32, "lp": 131073, "msg": "WM_DISPLAYCHANGE",
width: 1, height: 2
}
}
},
{
input: [gpii.windows.API_constants.WM_DISPLAYCHANGE, 16, gpii.windows.makeLong(3, 4)],
expect: {
module: "metrics",
event: "config.resolution",
data: {
"wp": 16, "lp": 262147, "msg": "WM_DISPLAYCHANGE",
width: 3, height: 4, bpp: 16
}
}
},
{
// wParam=0 is not from spi
input: [gpii.windows.API_constants.WM_SETTINGCHANGE, 0, 123],
expect: {
module: "metrics",
event: "config",
data: {
"wp": 0, "lp": 123, "msg": "WM_SETTINGCHANGE"
}
}
},
{
input: [gpii.windows.API_constants.WM_SETTINGCHANGE, gpii.windows.spi.actions.SPI_SETSTICKYKEYS, 2],
expect: {
module: "metrics",
event: "config.spi",
data: {
"wp": 59, "lp": 2, "msg": "WM_SETTINGCHANGE",
"action": "SPI_SETSTICKYKEYS"
}
}
},
{
// unknown spi uiAction
input: [gpii.windows.API_constants.WM_SETTINGCHANGE, 0x4000, 3],
expect: {
module: "metrics",
event: "config",
data: {
"wp": 0x4000, "lp": 3, "msg": "WM_SETTINGCHANGE"
}
}
}
]);
// Tests for recordKeyTiming: key presses
gpii.tests.metrics.recordKeyTimingKeyTests = fluid.freezeRecursive({
defaultState: {
times: []
},
defaultConfig: {
minSession: 0xffffff,
sessionTimeout: 0xffffff,
maxRecords: 1000
},
testData: [
{ // Normal key press
state: {},
input: {
timestamp: 1,
key: undefined
},
expect: {
module: "metrics",
event: "key-time",
data: {keyTime: 0}
}
},
{ // special key
state: null,
input: {
key: "BACK"
},
expect: {
module: "metrics",
event: "key-time",
data: {keyTime: 0, key: "BACK"}
}
},
// Test that non-special keys don't get leaked.
{ // not a special key (letter)
state: null,
input: {
key: "A"
},
expect: {
module: "metrics",
event: "key-time",
data: {keyTime: 0, key: fluid.NO_VALUE }
}
},
{ // not a special key (symbol)
state: null,
input: {
key: "!"
},
expect: {
module: "metrics",
event: "key-time",
data: {keyTime: 0, key: fluid.NO_VALUE}
}
},
{ // not a special key (numeric string)
state: null,
input: {
key: "1"
},
expect: {
module: "metrics",
event: "key-time",
data: {keyTime: 0, key: fluid.NO_VALUE}
}
},
{ // not a special key (number)
state: null,
input: {
key: 0x41
},
expect: {
module: "metrics",
event: "key-time",
data: {keyTime: 0, key: fluid.NO_VALUE}
}
},
{ // not a special key (string)
state: null,
input: {
key: "ABC"
},
expect: {
module: "metrics",
event: "key-time",
data: {keyTime: 0, key: fluid.NO_VALUE}
}
},
{ // not a special key (virtual key code of a special key)
state: null,
input: {
key: gpii.windows.API_constants.virtualKeyCodes.VK_ESCAPE
},
expect: {
module: "metrics",
event: "key-time",
data: {keyTime: 0, key: fluid.NO_VALUE}
}
}
]
});
// Tests for recordKeyTiming: typing sessions
gpii.tests.metrics.typingSessionTests = fluid.freezeRecursive({
defaultState: {
times: []
},
defaultConfig: {
minSession: 30000,
sessionTimeout: 60000,
minSessionKeys: 3
},
testData: [
{ // Single session (3 keys, 1 minute)
state: {},
input: [
{ timestamp: 1 },
{ timestamp: 20000 },
{ timestamp: 60001 },
{ timestamp: 200000 }
],
expect: [{
module: "metrics",
event: "typing-session",
data: {
duration: 60000,
count: 3,
corrections: 0,
rate: 3
}
}]
},
{ // Single session (10 keys, 5 minutes)
state: {},
input: [
{ timestamp: 1 },
{ timestamp: 60000 },
{ timestamp: 90000 },
{ timestamp: 120000 },
{ timestamp: 150000 },
{ timestamp: 180000 },
{ timestamp: 210000 },
{ timestamp: 240000 },
{ timestamp: 270000 },
{ timestamp: 300001 },
{ timestamp: 9000000 }
],
expect: [{
module: "metrics",
event: "typing-session",
data: {
duration: 300000,
count: 10,
corrections: 0,
rate: 2
}
}]
},
{ // Two sessions
state: {},
input: [
// 1st
{ timestamp: 1 },
{ timestamp: 20000 },
{ timestamp: 60001 },
// 2nd
{ timestamp: 200000 },
{ timestamp: 250000 },
{ timestamp: 300000 },
{ timestamp: 350000 },
{ timestamp: 9000000 }
],
expect: [{
module: "metrics",
event: "typing-session",
data: {
duration: 60000,
count: 3,
corrections: 0,
rate: 3
}
}, {
module: "metrics",
event: "typing-session",
data: {
duration: 150000,
count: 4,
corrections: 0,
rate: 2
}
}]
}
]
});
// Tests for inputHook.
gpii.tests.metrics.inputHookTests = fluid.freezeRecursive([
{ // Invalid nCode.
input: {
nCode: -1,
wParam: gpii.windows.API_constants.WM_KEYUP,
lParam: {
vkCode: "A".charCodeAt(),
scanCode: 0,
flags: 0,
time: 1,
dwExtraInfo: 0
}
},
expect: []
},
{ // First key.
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_KEYUP,
lParam: {
vkCode: "A".charCodeAt(),
scanCode: 0,
flags: 0,
time: 150,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "key-time",
data: { keyTime: 0, key: fluid.NO_VALUE }
}
},
{ // Next key (50ms later).
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_KEYUP,
lParam: {
vkCode: "A".charCodeAt(),
scanCode: 0,
flags: 0,
time: 200,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "key-time",
data: { keyTime: 50, key: fluid.NO_VALUE }
}
},
{ // Non-special non-input key.
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_KEYUP,
lParam: {
vkCode: gpii.windows.API_constants.virtualKeyCodes.VK_HOME,
scanCode: 0,
flags: 0,
time: 200,
dwExtraInfo: 0
}
},
expect: []
},
{ // Special key: back space.
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_KEYUP,
lParam: {
vkCode: gpii.windows.API_constants.virtualKeyCodes.VK_BACK,
scanCode: 0,
flags: 0,
time: 200,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "key-time",
data: { keyTime: 0, key: "BACK" }
}
},
{ // Special key: delete.
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_KEYUP,
lParam: {
vkCode: gpii.windows.API_constants.virtualKeyCodes.VK_DELETE,
scanCode: 0,
flags: 0,
time: 200,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "key-time",
data: { keyTime: 0, key: "DELETE" }
}
},
{ // Special key: escape.
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_KEYUP,
lParam: {
vkCode: gpii.windows.API_constants.virtualKeyCodes.VK_ESCAPE,
scanCode: 0,
flags: 0,
time: 200,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "key-time",
data: { keyTime: 0, key: "ESCAPE" }
}
},
{ // Normal key: 'X'
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_KEYUP,
lParam: {
vkCode: "X".charCodeAt(),
scanCode: 0,
flags: 0,
time: 200,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "key-time",
data: { keyTime: 0, key: fluid.NO_VALUE }
}
},
{ // Special key: left arrow.
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_KEYUP,
lParam: {
vkCode: gpii.windows.API_constants.virtualKeyCodes.VK_LEFT,
scanCode: 0,
flags: 0,
time: 200,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "key-time",
data: { keyTime: 0, key: "LEFT" }
}
},
{ // "system key" (WM_SYSKEYUP is sent when alt is down, should behave the same as WM_KEYUP)
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_SYSKEYUP,
lParam: {
vkCode: gpii.windows.API_constants.virtualKeyCodes.VK_HOME,
scanCode: 0,
flags: 0,
time: 200,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "key-time",
data: { keyTime: 0, key: "HOME" }
}
},
// Mouse events
{ // Click
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_LBUTTONUP,
lParam: {
ptX: 0,
ptY: 0,
mouseData: 0,
flags: 0,
time: 0,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "mouse",
data: { button: 1, distance: 0 }
}
},
{ // Right click
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_RBUTTONUP,
lParam: {
ptX: 0,
ptY: 0,
mouseData: 0,
flags: 0,
time: 0,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "mouse",
data: { button: 2, distance: 0 }
}
},
{ // Click with movement
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_LBUTTONUP,
lParam: {
ptX: 100,
ptY: 200,
mouseData: 0,
flags: 0,
time: 0,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "mouse",
data: { button: 1, distance: 224 }
}
},
{ // Click with movement again - distance should be from the last coordinate.
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_RBUTTONUP,
lParam: {
ptX: 100 + 50,
ptY: 200 + 60,
mouseData: 0,
flags: 0,
time: 0,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "mouse",
// sqrt(50^2 + 60^2) = 78
data: { button: 2, distance: 78 }
}
},
{ // Just movement
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_MOUSEMOVE,
lParam: {
ptX: 150 - 25,
ptY: 260 - 30,
mouseData: 0,
flags: 0,
time: 0,
dwExtraInfo: 0
}
},
// Shouldn't produce anything
expect: []
},
{ // Click + more movement
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_LBUTTONUP,
lParam: {
ptX: 125 + 10,
ptY: 230 + 20,
mouseData: 0,
flags: 0,
time: 0,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "mouse",
// (-25,-30) + (10,20) => 39 + 22 = 61
data: { button: 1, distance: 61 }
}
},
{ // Mouse wheel up
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_MOUSEWHEEL,
lParam: {
ptX: 1,
ptY: 2,
mouseData: gpii.windows.makeLong(0, -120),
flags: 0,
time: 0,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "mouse",
data: { wheel: -1 }
}
},
{ // Mouse wheel down
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_MOUSEWHEEL,
lParam: {
ptX: 1,
ptY: 2,
mouseData: gpii.windows.makeLong(0, 120),
flags: 0,
time: 0,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "mouse",
data: { wheel: 1 }
}
},
{ // Mouse wheel up multiple
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_MOUSEWHEEL,
lParam: {
ptX: 1,
ptY: 2,
mouseData: gpii.windows.makeLong(0, -120 * 5),
flags: 0,
time: 0,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "mouse",
data: { wheel: -5 }
}
},
{ // Mouse wheel down multiple
input: {
nCode: 0,
wParam: gpii.windows.API_constants.WM_MOUSEWHEEL,
lParam: {
ptX: 1,
ptY: 2,
mouseData: gpii.windows.makeLong(0, 120 * 3),
flags: 0,
time: 0,
dwExtraInfo: 0
}
},
expect: {
module: "metrics",
event: "mouse",
data: { wheel: 3 }
}
}
]);
gpii.tests.metrics.allLogLines = [];
/**
* Start a log server, to capture everything that's being logged in this test module. The logs will then be looked at
* in the final test.
* @param {Number} port The TCP port to listen on.
* @return {net.Server} The TCP server.
*/
gpii.tests.metrics.createLogServer = function (port) {
var buffer = "";
var logServer = net.createServer();
logServer.listen(port, "127.0.0.1");
logServer.on("listening", function () {
fluid.log("[test] log server listening");
});
logServer.on("connection", function (socket) {
fluid.log("[test] log server connection");
socket.setEncoding("utf8");
socket.on("data", function (chunk) {
buffer += chunk;
if (buffer.indexOf("\n") >= 0) {
var lines = buffer.split("\n");
buffer = lines.pop();
lines.forEach(function (line) {
gpii.tests.metrics.allLogLines.push(line);
});
}
});
});
logServer.on("close", function () {
});
logServer.on("error", function (err) {
jqUnit.fail(err);
});
return logServer;
};
gpii.tests.metrics.captureLog = function (that) {
that.logHost = "127.0.0.1";
that.logPort = gpii.tests.metrics.logPort;
};
gpii.tests.metrics.logPort = 50000 + Math.floor(Math.random() * 5000);
gpii.tests.metrics.logServer = gpii.tests.metrics.createLogServer(gpii.tests.metrics.logPort);
/**
* Configure a log file that's just used for testing.
* The file will be deleted automatically after the current test completes.
*
* @return {String} The path of the log file.
*/
gpii.tests.metrics.setLogFile = function () {
var previousLogFile = gpii.eventLog.logFilePath;
var logFile = os.tmpdir() + "/gpii-test-metrics-" + Date.now();
teardowns.push(function () {
gpii.eventLog.logFilePath = previousLogFile;
try {
fs.unlinkSync(logFile);
} catch (e) {
// Ignored.
}
});
gpii.eventLog.logFilePath = logFile;
return logFile;
};
/**
* Check if all properties of expected are also in subject and are equal, ignoring any extra ones in subject.
*
* A property's value in the expected object can be the expected value, fluid.VALUE to match any value (just check if
* the property exists), or fluid.NO_VALUE to check the property doesn't exist.
*
* @param {Object} subject The object to check against
* @param {Object} expected The object containing the values to check for.
* @param {Number} maxDepth [Optional] How deep to check.
* @return {Boolean} true if subject matches expected.
*/
gpii.tests.metrics.deepMatch = function (subject, expected, maxDepth) {
var match = false;
if (maxDepth < 0) {
return false;
} else if (!maxDepth && maxDepth !== 0) {
maxDepth = fluid.strategyRecursionBailout;
}
if (!subject) {
return subject === expected;
}
for (var prop in expected) {
if (expected.hasOwnProperty(prop)) {
var exp = expected[prop];
if (exp instanceof RegExp) {
match = exp.test(subject[prop]);
} else if (fluid.isMarker(exp, fluid.VALUE)) {
// match any value
match = subject.hasOwnProperty(prop);
} else if (fluid.isMarker(exp, fluid.NO_VALUE)) {
// match no value
match = !subject.hasOwnProperty(prop);
} else if (fluid.isPrimitive(exp)) {
match = subject[prop] === exp;
} else {
match = gpii.tests.metrics.deepMatch(subject[prop], exp, maxDepth - 1);
}
if (!match) {
break;
}
}
}
return match;
};
/**
* Checks the log file for some lines that are expected. (see deepMatch for the matching rules)
*
* @param {String} logFile The file to read.
* @param {Array<Object>} expected An array of log items to search for (in order of how they should be found).
* @return {Promise} Resolves when all expected lines where found, or rejects if the end of the log is reached first.
*/
gpii.tests.metrics.expectLogLines = function (logFile, expected) {
jqUnit.expect(expected.length);
var promise = fluid.promise();
var reader = readline.createInterface({
input: fs.createReadStream(logFile)
});
var remainingLines = [];
var index = 0;
var currentExpected = expected[index];
var complete = false;
reader.input.on("error", function (err) {
jqUnit.fail(err);
});
reader.on("line", function (line) {
if (complete) {
return;
}
fluid.log(line);
var obj = JSON.parse(line);
remainingLines.push(obj);
if (gpii.tests.metrics.deepMatch(obj, currentExpected)) {
jqUnit.assert("Found a matching line");
index++;
remainingLines = [];
if (index >= expected.length) {
complete = true;
reader.close();
} else {
currentExpected = expected[index];
}
}
});
reader.on("close", function () {
reader.input.close();
if (complete) {
promise.resolve();
} else {
fluid.log("No matching line (index=" + index + "):", currentExpected);
fluid.log("Remaining log lines:", remainingLines);
promise.reject("Unable to find matching log entry " + index);
}
});
return promise;
};
/**
* Completes a test of logging, by checking if the given log file contains the expected lines.
*
* @param {String} logFile The log file.
* @param {Array<Object>} expectedLines An array of log items to search for (in order of how they should be found).
*/
gpii.tests.metrics.completeLogTest = function (logFile, expectedLines) {
gpii.tests.metrics.expectLogLines(logFile, expectedLines).then(function () {
jqUnit.assert("All expected lines should be found.");
jqUnit.start();
}, function (err) {
jqUnit.fail(err);
});
};
jqUnit.test("Testing path generification", function () {
var env = {
"SystemRoot": "C:\\Windows",
"APPDATA": "C:\\Users\\vagrant\\AppData\\Roaming",
"TEMP": "C:\\Users\\vagrant\\AppData\\Local\\Temp",
"LOCALAPPDATA": "C:\\Users\\vagrant\\AppData\\Local",
"HOME": "C:\\Users\\vagrant",
"USERPROFILE": "C:\\Users\\vagrant"
};
var tests = {
"c:\\xyz\\abc": "c:\\xyz\\abc",
"c:\\windows\\notepad.exe": "%SystemRoot%\\notepad.exe",
"c:\\WINDOWS\\NOTEPAD1.EXE": "%SystemRoot%\\NOTEPAD1.EXE",
"c:/windows/notepad2.exe": "%SystemRoot%\\notepad2.exe",
"c:\\users\\vagrant\\file": "%HOME%\\file",
"c:\\users\\vagrant\\\\\\file2": "%HOME%\\file2",
"c:\\users\\moo\\..\\vagrant\\file3": "%HOME%\\file3",
"c:\\windows\\..\\users\\hello\\..\\vagrant\\file3": "%HOME%\\file3",
"c:\\users\\vagrant": "%HOME%",
"c:\\users\\vagrant\\": "%HOME%\\",
"c:\\users\\vagrant\\\\": "%HOME%\\",
"C:\\Users\\vagrant\\AppData\\Roaming": "%APPDATA%",
"C:\\Users\\vagrant\\AppData\\Roaming\\file": "%APPDATA%\\file",
"C:\\Users\\vagrant\\AppData\\Roaming\\..\\file": "%HOME%\\AppData\\file",
"C:\\Users\\vagrant\\AppData\\Roaming\\..\\Local\\file": "%LOCALAPPDATA%\\file"
};
fluid.each(tests, function (expect, test) {
var result = gpii.windows.metrics.genericisePath(test, env);
jqUnit.assertEquals("Path " + test + " should match the expected value", expect, result);
});
});
/**
* Test the application metrics - app launches and window times.
*/
jqUnit.asyncTest("Testing application metrics", function () {
jqUnit.expect(1);
var logFile = gpii.tests.metrics.setLogFile();
var windowsMetrics = gpii.tests.metrics.windowsMetricsWrapper({
members: {
logFilePath: logFile
}
});
// Pick three windows (owned by different processes) that already exist, and pretend they've became active.
var windows = [];
var exes = {};
gpii.windows.enumerateWindows(function (hwnd) {
if (windows.length < 3) {
var exe = gpii.windows.getProcessPath(gpii.windows.getWindowProcessId(hwnd));
if (!exes[exe] && !/ApplicationFrameHost/i.test(exe)) {
exes[exe] = true;
windows.push({
exe: gpii.windows.metrics.genericisePath(exe),
hwnd: hwnd
});
}
}
});
// Mock gpii.windows.isProcessRunning so it looks like the processes are not running when the window is closed.
var oldIsProcessRunning = gpii.windows.isProcessRunning;
gpii.windows.isProcessRunning = function () {
return false;
};
teardowns.push(function () {
gpii.windows.isProcessRunning = oldIsProcessRunning;
});
var activeWindowIndex = 0;
var expectedLines = [];
// Activate the windows a few times.
var activateWindow = function (remaining) {
activeWindowIndex = (activeWindowIndex + 1) % windows.length;
// Pretend the "window has been activated" notification has been received. A fuller test would have sent the
// actual message. However, there isn't a real message loop when running under node (rather than electron).
gpii.windows.metrics.windowMessage(windowsMetrics, 0, gpii.windows.API_constants.WM_SHELLHOOK,
gpii.windows.API_constants.HSHELL_WINDOWACTIVATED, windows[activeWindowIndex].hwnd);
if (remaining > 0) {
if (!windows[activeWindowIndex].done) {
// The launch event is only expected on a window's first occurrence.
expectedLines.push({
module: "metrics",
event: "app-launch",
data: {
exe: windows[activeWindowIndex].exe
}
});
windows[activeWindowIndex].done = true;
}
expectedLines.push({
module: "metrics",
event: "app-active",
data: {
exe: windows[activeWindowIndex].exe,
window: fluid.VALUE
}
});
process.nextTick(activateWindow, remaining - 1);
} else {
// Destroy a window
gpii.windows.metrics.windowMessage(windowsMetrics, 0, gpii.windows.API_constants.WM_SHELLHOOK,
gpii.windows.API_constants.HSHELL_WINDOWDESTROYED, windows[0].hwnd);
expectedLines.push({
module: "metrics",
event: "app-close",
data: {
exe: windows[0].exe,
windowClass: fluid.VALUE
}
});
setTimeout(function () {
windowsMetrics.stopApplicationMetrics();
gpii.tests.metrics.completeLogTest(logFile, expectedLines);
windowsMetrics.destroy();
}, 100);
}
};
// Start monitoring.
windowsMetrics.startApplicationMetrics();
activateWindow(windows.length * 2);
});
jqUnit.asyncTest("Testing system configuration notifications", function () {
jqUnit.expect(2);
var logFile = gpii.tests.metrics.setLogFile();
var windowsMetrics = gpii.tests.metrics.windowsMetricsWrapper({
members: {
logFilePath: logFile
}
});
var untestedMessages = fluid.arrayToHash(gpii.windows.metrics.settingsMessages);
var expectedLines = [];
fluid.each(gpii.tests.metrics.configMessageTests, function (test) {
var msg = test.input[0],
wp = test.input[1],
lp = test.input[2];
gpii.windows.metrics.windowMessage(windowsMetrics, 0, msg, wp, lp);
if (untestedMessages[msg]) {
delete untestedMessages[msg];
}
expectedLines.push(test.expect);
});
jqUnit.assertDeepEq("All messages in gpii.windows.metrics.settingsMessages should be tested", {}, untestedMessages);
gpii.tests.metrics.completeLogTest(logFile, expectedLines);
});
/**
* Test recordKeyTiming
* @param {Object} theTests The tests; gpii.tests.metrics.recordKeyTimingKeyTests or typingSessionTests.
*/
gpii.tests.metrics.recordKeyTimingTests = function (theTests) {
jqUnit.expect(1);
var logFile = gpii.tests.metrics.setLogFile();
var testData = theTests.testData;
var defaultState = theTests.defaultState;
var defaultConfig = theTests.defaultConfig;
var windowsMetrics = gpii.tests.metrics.windowsMetricsWrapper({
members: {
logFilePath: logFile
}
});
var expectedLines = [];
fluid.each(testData, function (test) {
var state = fluid.copy(test.state || defaultState);
state.config = fluid.copy(state.config || defaultConfig);
windowsMetrics.state.input = state;
windowsMetrics.config.input = state.config;
// "press" the keys
fluid.each(fluid.makeArray(test.input), function (input) {
gpii.windows.metrics.recordKeyTiming(windowsMetrics, input.timestamp, input.key);
});
expectedLines.push.apply(expectedLines, fluid.makeArray(test.expect));
});
gpii.tests.metrics.completeLogTest(logFile, expectedLines);
};
jqUnit.asyncTest("Testing keyboard metrics: recordKeyTiming (key presses)", function () {
gpii.tests.metrics.recordKeyTimingTests(gpii.tests.metrics.recordKeyTimingKeyTests);
});
jqUnit.asyncTest("Testing keyboard metrics: recordKeyTiming (typing sessions)", function () {
gpii.tests.metrics.recordKeyTimingTests(gpii.tests.metrics.typingSessionTests);
});
// This also tests recordMouseEvent (indirectly)
jqUnit.asyncTest("Testing input metrics: inputHook", function () {
jqUnit.expect(1);
var logFile = gpii.tests.metrics.setLogFile();
var windowsMetrics = gpii.tests.metrics.windowsMetricsWrapper({
members: {
logFilePath: logFile
}
});
// Get the initial state, so it can be reset (otherwise tests will depend on the previous tests, making it tricky
// to change)
var initialState = fluid.copy(windowsMetrics.state.input);
// Disable typing sessions
windowsMetrics.config.input.minSession = windowsMetrics.config.input.sessionTimeout = -1 >>> 0;
windowsMetrics.config.input.maxRecords = 1;
windowsMetrics.startInputMetrics();
var testData = gpii.tests.metrics.inputHookTests;
var expectedLines = [];
var currentTest = null;
var modifiers = [
{name: "SHIFT", code: gpii.windows.API_constants.virtualKeyCodes.VK_SHIFT},
{name: "CONTROL", code: gpii.windows.API_constants.virtualKeyCodes.VK_CONTROL},
{name: "ALT", code: gpii.windows.API_constants.virtualKeyCodes.VK_MENU}
];
var expectedModifiers = [];
// Tests a single element of the test data.
var testInput = function () {
fluid.each(testData, function (test) {
currentTest = test.input;
gpii.windows.metrics.inputHook(windowsMetrics, currentTest.nCode, currentTest.wParam, currentTest.lParam);
// insert the modifierKeys field to the expect result, if necessary.
var expected = fluid.transform(fluid.makeArray(test.expect), function (expect) {
var togo = fluid.copy(expect);
if (expectedModifiers.length > 0) {
togo.data.modifierKeys = expectedModifiers;
}
return togo;
});
expectedLines.push.apply(expectedLines, expected);
});
};
// Mock windows.getModifierKeys to return the test data (it's better than simulating the key state)
var getModifierKeysOrig = gpii.windows.metrics.getModifierKeys;
teardowns.push(function () {
gpii.windows.metrics.getModifierKeys = getModifierKeysOrig;
});
gpii.windows.metrics.getModifierKeys = function () {
return expectedModifiers;
};
// Run all of the input tests, with different combinations of modifier keys pressed.
for (var pass = 0; pass < Math.pow(2, modifiers.length); pass++) {
expectedModifiers = [];
for (var n = 0; n < modifiers.length; n++) {
var set = (pass >> n) & 1;
if (set) {
expectedModifiers.push(modifiers[n].name);
}
}
windowsMetrics.startInputMetrics();
windowsMetrics.state.input = fluid.copy(initialState);
testInput(expectedModifiers);
}
// The events are put in the log in the next tick (to allow the hook handler to return quickly).
setImmediate(function () {
windowsMetrics.stopInputMetrics();
gpii.tests.metrics.completeLogTest(logFile, expectedLines);
});
});
// Ensure no character keys are whitelisted for being logged.
jqUnit.test("Testing input metrics: inputHook - not logging character keys", function () {
var keyCodes = Object.keys(gpii.windows.metrics.specialKeys);
jqUnit.expect(keyCodes.length + 2);
// Return true if keyCode corresponds to a character key.
var isCharacterKey = function (keyCode) {
var char = gpii.windows.user32.MapVirtualKeyW(keyCode, gpii.windows.API_constants.MAPVK_VK_TO_CHAR);
return char && char >= 0x20;
};
isCharacterKey(gpii.windows.API_constants.virtualKeyCodes.VK_BACK);
// Sanity check.
var keyA = 0x41;
jqUnit.assertTrue("'A' key should be a character key", isCharacterKey(keyA));
jqUnit.assertFalse("'HOME' key should not be a character key",
isCharacterKey(gpii.windows.API_constants.virtualKeyCodes.VK_HOME));
fluid.each(keyCodes, function (key) {
jqUnit.assertFalse("Keys in windows.metrics.specialKeys must all be non-printable: " + gpii.windows.metrics.specialKeys[key],
isCharacterKey(key));
});
});
// Ensure only the value of whitelisted keys are being logged.
jqUnit.asyncTest("Testing input metrics: inputHook - only logging desired keys", function () {
var logFile = gpii.tests.metrics.setLogFile();
var windowsMetrics = gpii.tests.metrics.windowsMetricsWrapper({
members: {
logFilePath: logFile
}
});
// Disable typing sessions
windowsMetrics.config.input.minSession = windowsMetrics.config.input.sessionTimeout = -1 >>> 0;
windowsMetrics.config.input.maxRecords = 1;
windowsMetrics.startInputMetrics();
var lParam = {
flags: 0,
time: 0,
dwExtraInfo: 0
};
var keyCount = 0;
// Send every key.
for (var keyCode = 0; keyCode <= 0xff; keyCode++) {
lParam.vkCode = keyCode;
gpii.windows.metrics.inputHook(windowsMetrics, 0, gpii.windows.API_constants.WM_KEYUP, lParam);
keyCount++;
}
// Log a bad key, to test the test.
windowsMetrics.logMetric("key-time", {
keyTime: 0,
key: "A",
// Used to mark this log entry as a test value.
selfTest: "this is a test"
});
keyCount++;
var expectedCount = 257;
jqUnit.assertEquals("self-test: there should be 257 key presses, including the bad one.", expectedCount, keyCount);
// The events are put in the log in the next tick (to allow the hook handler to return quickly).
setImmediate(function () {
windowsMetrics.stopInputMetrics();
var gotSelfTest = false;
var loggedKeys = {};
var loggedSpecialKeys = [];
var logLines = fs.readFileSync(logFile).toString().trim().split(/[\n\r]+/);
fluid.each(logLines, function (line) {
var logEntry = JSON.parse(line);
if (logEntry.event === "key-time") {
jqUnit.assertNotNull("Key event must have a data field", logEntry.data);
var key = logEntry.data.key;
if (key) {
jqUnit.assertEquals("value of data.key must be a string", "string", typeof key);
jqUnit.assertTrue("value of data.key must not be a number", isNaN(parseInt(key)));
// Failire means there's something wrong with the logging or this test.
jqUnit.assertFalse("Keys should only be logged once.", !!loggedKeys[key]);
loggedKeys[key] = true;
// Make sure the key is supposed to be logged.
var keyCode = fluid.keyForValue(gpii.windows.metrics.specialKeys, key);
var found = keyCode !== undefined;
if (found) {
// Double check for the alpha-numeric keys
/* from WinUser.h:
* VK_0 - VK_9 are the same as ASCII '0' - '9' (0x30 - 0x39)
* 0x3A - 0x40 : unassigned
* VK_A - VK_Z are the same as ASCII 'A' - 'Z' (0x41 - 0x5A)
*/
var isAlphaNum = (keyCode >= 0x30 && keyCode <= 0x5a);
// This means a number/letter key is in the whitelist.
jqUnit.assertFalse("Alphanumeric keys should not be logged: " + key, isAlphaNum);
loggedSpecialKeys.push(key);
} else {
// This key should not be logged.
if (logEntry.data.selfTest === "this is a test" && !gotSelfTest) {
// The self-test key
gotSelfTest = true;
jqUnit.assert("Got self-test key.");
} else {
jqUnit.fail("An undesired key was logged");
}
}
}
}
});
// Make sure the test ran fully.
var allKeys = fluid.values(gpii.windows.metrics.specialKeys);
jqUnit.assertDeepEq("All special keys should have been logged", allKeys, loggedSpecialKeys);
jqUnit.assertTrue("The self-test bad key should have been found", gotSelfTest);
jqUnit.start();
});
});
jqUnit.asyncTest("Testing input metrics: inactivity", function () {
jqUnit.expect(4);
var logFile = gpii.tests.metrics.setLogFile();
var windowsMetrics = gpii.tests.metrics.windowsMetricsWrapper({
members: {
logFilePath: logFile
}
});
// Make the inactivity timeout very quick
windowsMetrics.config.input.inactiveTime = 1;
windowsMetrics.startInputMetrics();
var state = windowsMetrics.state.input;
jqUnit.assertFalse("Should not be inactive at start", state.inactive);
gpii.windows.metrics.userInput(windowsMetrics);
jqUnit.assertFalse("Should not be inactive after first input", state.inactive);
setTimeout(function () {
jqUnit.assertTrue("Should be inactive after timer", state.inactive);
gpii.windows.metrics.userInput(windowsMetrics);
jqUnit.assertFalse("Should not be inactive after last input", state.inactive);
windowsMetrics.stopInputMetrics();
jqUnit.start();
}, 10);
});
jqUnit.asyncTest("Testing version and system info logging", function () {
jqUnit.expect(1);
var logFile = gpii.tests.metrics.setLogFile();
var windowsMetrics = gpii.tests.metrics.windowsMetricsWrapper({
members: {
logFilePath: logFile
}
});
windowsMetrics.events.onStartMetrics.fire();
gpii.tests.metrics.completeLogTest(logFile, [
{
module: "metrics",
event: "version",
data: {
"windowsMetrics": fluid.VALUE,
"gpii-app": fluid.VALUE,
"gpii-windows": fluid.VALUE,
"gpii-universal": fluid.VALUE
}
}, {
module: "metrics",
event: "system-info",
data: {
"cpu": fluid.VALUE,
"cores": /^[1-9][0-9]?$/,
"memory": /^[0-9]+(\.[0-9]+)?$/,
"resolution": /^[0-9]+x[0-9]+$/,
"scale": /^[0-9]+(\.[0-9]+)?$/,
"osBits": /^(32|64)$/,
"osRelease": fluid.VALUE,
"osEdition": /Windows/,
"systemMfr": fluid.VALUE,
"systemName": fluid.VALUE
}
}
]);
});
/**
* Checks the documentation (README.md) by extracting the JSON code regions for the following:
* - Every type of event that is logged, from this test module, is in the document.
* - The all data fields for each event are listed in the document.
* - All documented fields have been used.
*
* This will not expose event data that is both untested and undocumented.
*/
jqUnit.test("Testing metrics documentation", function () {
gpii.tests.metrics.logServer.close();
var docPath = __dirname + "/../README.md";
// ignored events (implemented in gpii-universal)
var ignore = {
"gpii": ["stop"]
};
// Parse the documentation for json blocks
var content = fs.readFileSync(docPath, "utf8");
var reg = /```json5?(((?!```)[\s\S])*)```/g;
var docObjects = [];
var match;
while ((match = reg.exec(content))) {
var json = match[1];
var obj;
try {
obj = json5.parse(json);
} catch (e) {
fluid.log("Unable to parse this: ", json);
jqUnit.fail(e);
throw e;
}
// The values in the data object do not matter (just the existence)
obj.data = fluid.transform(obj.data, function () {
return "";
});
docObjects.push(obj);
}
var documentedEvents = {};
// Gather the keys of the data object for each documented event. The same event may be documented more than once, in
// that case the keys for each one are combined.
fluid.each(docObjects, function (obj) {
var eventData = fluid.get(documentedEvents, [obj.module, obj.event]);
var dataKeys = obj.data ? Object.keys(obj.data) : [];
var keyHash = fluid.arrayToHash(dataKeys);
if (eventData) {
Object.assign(eventData.keys, keyHash);
eventData.documentation = fluid.makeArray(eventData.documentation);
eventData.documentation.push(obj);
} else {
eventData = {
keys: keyHash,
documentation: obj
};
fluid.set(documentedEvents, [obj.module, obj.event], eventData);
}
eventData.remainingKeys = fluid.copy(eventData.keys);
});
// Hack for the selfTest key used in testing of the key timing metrics.
fluid.set(documentedEvents, "metrics.key-time.keys.selfTest", true);
// Check every log entry matches one of the objects in the documentation.
fluid.each(gpii.tests.metrics.allLogLines, function (logLine) {
var logObject = JSON.parse(logLine);
if (!ignore[logObject.module] || !ignore[logObject.module].includes(logObject.event)) {
var eventData = fluid.get(documentedEvents, [logObject.module, logObject.event]);
if (eventData) {
// Check each field in the data object is documented.
var dataKeys = Object.keys(logObject.data || {});
fluid.each(dataKeys, function (key) {
var isDocumented = eventData.keys[key];
if (!isDocumented) {
fluid.log("Log entry:", logObject);
fluid.log("Documented object:", eventData.documentation);
}
jqUnit.assertTrue("All data fields should be documented. field: " + key, isDocumented);
});
// Cross off the documented keys that have been seen.
eventData.remainingKeys = fluid.filterKeys(eventData.remainingKeys, dataKeys, true);
} else {
fluid.log("Undocumented event:", logObject);
}
jqUnit.assertTrue(
"Event '" + logObject.module + ":" + logObject.event + "' should be documented.", !!eventData);
}
});
// Check if documented fields are not being sent. Could be a sign of outdated documentation, or poor testing.
fluid.each(documentedEvents, function (module, moduleName) {
fluid.each(module, function (event, eventName) {
var unused = Object.keys(event.remainingKeys);
if (unused.length > 0) {
fluid.log("Stale documented fields: ", unused);
}
jqUnit.assertTrue("All documented fields should be used. " + moduleName + ":" + eventName,
unused.length === 0);
});
});
jqUnit.assert();
});