forked from jeffpar/pcjs.v1
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtestctl.js
350 lines (324 loc) · 13.3 KB
/
testctl.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
/**
* @fileoverview TestController Class for SerialPort-based Testing
* @author <a href="mailto:[email protected]">Jeff Parsons</a>
* @copyright © 2012-2020 Jeff Parsons
*
* This file is part of PCjs, a computer emulation software project at <https://www.pcjs.org>.
*
* PCjs 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 3
* of the License, or (at your option) any later version.
*
* PCjs 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 PCjs. If not,
* see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <https://www.pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
/*
* This module provides connectivity between the TestMonitor component and whichever PCx86 SerialPort
* our 'binding' property indicates, if any.
*/
if (typeof module !== "undefined") {
var Str = require("../../shared/lib/strlib");
var Web = require("../../shared/lib/weblib");
var Component = require("../../shared/lib/component");
var PCx86 = require("./defines");
var TestMonitor = require("./testmon");
}
/**
* TestController class
*
* @class TestController
* @property {string|undefined} urlTests
* @property {Object|null} tests
* @property {string|null} consoleBuffer
* @property {HTMLTextAreaElement|null} controlBuffer
* @property {function(...)|null} sendData
* @property {function(number)|null} deliverData
* @property {function(number)|null} deliverInput
* @property {function(Object)|null} deliverTests
* @unrestricted (allows the class to define properties, both dot and named, outside of the constructor)
*/
class TestController extends Component {
/**
* TestController(parms)
*
* @this {TestController}
* @param {Object} parms
*/
constructor(parms)
{
super("TestController", parms);
this.tests = null;
let fLoading = false;
this.urlTests = parms['tests'];
this.consoleBuffer = "";
this.controlBuffer = null;
this.sendData = null;
this.deliverData = this.deliverInput = this.deliverTests = null;
this.sBinding = parms['binding'];
if (this.sBinding) {
this.serialPort = Component.getComponentByID(this.sBinding, this.id);
if (this.serialPort) {
let exports = this.serialPort['exports'];
if (exports) {
let bind = /** @function */ (exports['bind']);
if (bind && bind.call(this.serialPort, this, this.receiveData, true)) {
this.sendData = exports['receiveData'].bind(this.serialPort);
if (this.urlTests) {
this.loadTests(this.urlTests);
fLoading = true;
}
}
}
}
if (!this.sendData) {
Component.warning(this.id + ": binding '" + this.sBinding + "' unavailable");
}
}
if (!fLoading) this.setReady();
}
/**
* loadTests(sURL)
*
* @this {TestController}
* @param {string} sURL
*/
loadTests(sURL)
{
let controller = this;
let sProgress = "Loading " + sURL + "...";
Web.getResource(sURL, null, true, function(sURL, sResponse, nErrorCode) {
controller.doneLoad(sURL, sResponse, nErrorCode);
}, function(nState) {
controller.println(sProgress, Component.PRINT.PROGRESS);
});
}
/**
* doneLoad(sURL, sTestData, nErrorCode)
*
* @this {TestController}
* @param {string} sURL
* @param {string} sTestData
* @param {number} nErrorCode (response from server if anything other than 200)
*/
doneLoad(sURL, sTestData, nErrorCode)
{
if (nErrorCode) {
this.notice("Unable to load tests (error " + nErrorCode + ": " + sURL + ")", nErrorCode < 0);
}
else {
try {
this.tests = /** @type {Object} */ (JSON.parse(sTestData));
if (this.deliverTests) {
this.deliverTests(this.tests);
this.tests = null;
}
Component.addMachineResource(this.idMachine, sURL, sTestData);
} catch (err) {
this.notice("Test parsing error: " + err.message);
}
}
this.setReady();
}
/**
* bindMonitor(monitor, deliverData, deliverInput, deliverTests)
*
* @this {TestController}
* @param {TestMonitor} monitor
* @param {function(number)} deliverData
* @param {function(number)} deliverInput
* @param {function(Object)} deliverTests
*/
bindMonitor(monitor, deliverData, deliverInput, deliverTests)
{
this.deliverData = deliverData.bind(monitor);
this.deliverInput = deliverInput.bind(monitor);
this.deliverTests = deliverTests.bind(monitor);
if (this.tests && this.deliverTests) {
this.deliverTests(this.tests);
this.tests = null;
}
}
/**
* setBinding(sHTMLType, sBinding, control, sValue)
*
* @this {TestController}
* @param {string} sHTMLType is the type of the HTML control (eg, "button", "list", "text", "submit", "textarea", "canvas")
* @param {string} sBinding is the value of the 'binding' parameter stored in the HTML control's "data-value" attribute (eg, "buffer")
* @param {HTMLElement} control is the HTML control DOM object (eg, HTMLButtonElement)
* @param {string} [sValue] optional data value
* @return {boolean} true if binding was successful, false if unrecognized binding request
*/
setBinding(sHTMLType, sBinding, control, sValue)
{
let controller = this;
if (sHTMLType == "textarea" && !this.controlBuffer) {
this.bindings[sBinding] = control;
this.controlBuffer = /** @type {HTMLTextAreaElement} */ (control);
this.consoleBuffer = null; // we currently use one or the other: control or console
/*
* By establishing an onkeypress handler here, we make it possible for DOS commands like
* "CTTY COM1" to more or less work (use "CTTY CON" to restore control to the DOS console).
*/
control.onkeydown = function onKeyDown(event) {
/*
* This is required in addition to onkeypress, because it's the only way to prevent
* BACKSPACE (keyCode 8) from being interpreted by the browser as a "Back" operation;
* moreover, not all browsers generate an onkeypress notification for BACKSPACE.
*
* A related problem exists for Ctrl-key combinations in most Windows-based browsers
* (eg, IE, Edge, Chrome for Windows, etc), because keys like Ctrl-C and Ctrl-S have
* special meanings (eg, Copy, Save). To the extent the browser will allow it, we
* attempt to disable that default behavior when this control receives an onkeydown
* event for one of those keys (probably the only event the browser generates for them).
*/
event = event || window.event;
let keyCode = event.keyCode;
if (keyCode === 0x08 || event.ctrlKey && keyCode >= 0x41 && keyCode <= 0x5A) {
if (event.preventDefault) event.preventDefault();
if (keyCode > 0x40) keyCode -= 0x40;
if (controller.deliverInput) controller.deliverInput(keyCode);
}
return true;
};
control.onkeypress = function onKeyPress(event) {
/*
* Browser-independent keyCode extraction; refer to onKeyPress() and the other key event
* handlers in keyboard.js.
*/
event = event || window.event;
let keyCode = event.which || event.keyCode;
if (controller.deliverInput) controller.deliverInput(keyCode);
/*
* Since we're going to remove the "readonly" attribute from the <textarea> control
* (so that the soft keyboard activates on iOS), instead of calling preventDefault() for
* selected keys (eg, the SPACE key, whose default behavior is to scroll the page), we must
* now call it for *all* keys, so that the keyCode isn't added to the control immediately,
* on top of whatever the machine is echoing back, resulting in double characters.
*/
if (event.preventDefault) event.preventDefault();
return true;
};
/*
* Now that we've added an onkeypress handler that calls preventDefault() for ALL keys, the control
* itself no longer needs the "readonly" attribute; we primarily need to remove it for iOS browsers,
* so that the soft keyboard will activate, but it shouldn't hurt to remove the attribute for all browsers.
*/
control.removeAttribute("readonly");
if (this.sendData) {
let monitor = new TestMonitor();
monitor.bindController(this, this.sendData, this.sendOutput, this.printf, this.sBinding);
}
return true;
}
return false;
}
/**
* sendOutput(data)
*
* @this {TestController}
* @param {number|string|Array} data
*/
sendOutput(data)
{
if (typeof data == "number") {
this.printf("%c", data);
}
else if (typeof data == "string") {
this.printf("%s", data);
}
else {
for (let i = 0; i < data.length; i++) this.printf("[0x%02x]", data[i]);
}
}
/**
* printf(format, ...args)
*
* @this {TestController}
* @param {string|number} format
* @param {...} args
*/
printf(format, ...args)
{
let s = Str.sprintf(format.toString(), ...args);
if (this.controlBuffer != null) {
if (s != '\r') {
if (s == '\b' || s == "\b \b") {
this.controlBuffer.value = this.controlBuffer.value.slice(0, -1);
} else {
this.controlBuffer.value += s;
}
/*
* Prevent the <textarea> from getting too large; otherwise, printing becomes slower and slower.
*/
if (!DEBUG && this.controlBuffer.value.length > 8192) {
this.controlBuffer.value = this.controlBuffer.value.substr(this.controlBuffer.value.length - 4096);
}
this.controlBuffer.scrollTop = this.controlBuffer.scrollHeight;
}
}
if (this.consoleBuffer != null) {
let i = s.lastIndexOf('\n');
if (i >= 0) {
console.log(this.consoleBuffer + s.substr(0, i));
this.consoleBuffer = "";
s = s.substr(i + 1);
}
this.consoleBuffer += s;
}
}
/**
* receiveData(data)
*
* @this {TestController}
* @param {number|string|Array} data
*/
receiveData(data)
{
if (typeof data == "number") {
this.deliverData(data);
}
else if (typeof data == "string") {
for (let i = 0; i < data.length; i++) this.deliverData(data.charCodeAt(i));
}
else {
for (let i = 0; i < data.length; i++) this.deliverData(data[i]);
}
}
/**
* TestController.init()
*
* This function operates on every HTML element of class "TestController", extracting the
* JSON-encoded parameters for the TestController constructor from the element's "data-value"
* attribute, invoking the constructor to create a TestController component, and then binding
* any associated HTML controls to the new component.
*/
static init()
{
let aeTest = Component.getElementsByClass(document, PCx86.APPCLASS, "testctl");
for (let iTest = 0; iTest < aeTest.length; iTest++) {
let eTest = aeTest[iTest];
let parms = Component.getComponentParms(eTest);
let test = new TestController(parms);
Component.bindComponentControls(test, eTest, PCx86.APPCLASS);
}
}
}
/*
* Initialize every TestController module on the page.
*/
Web.onInit(TestController.init);
if (typeof module !== "undefined") module.exports = TestController;