blob: 0b923b68f5cc0864654b8848059e9d83ebfcd68a [file] [log] [blame]
// Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* @fileoverview hterm.Terminal unit tests.
*/
hterm.Terminal.Tests = new lib.TestManager.Suite('hterm.Terminal.Tests');
hterm.Terminal.Tests.prototype.setup = function(cx) {
this.setDefaults(cx,
{ visibleColumnCount: 80,
visibleRowCount: 24,
});
};
/**
* Clear out the current document and create a new hterm.Terminal object for
* testing.
*
* Called before each test case in this suite.
*/
hterm.Terminal.Tests.prototype.preamble = function(result, cx) {
var document = cx.window.document;
document.body.innerHTML = '';
var div = this.div = document.createElement('div');
div.style.position = 'absolute';
div.style.height = '100%';
div.style.width = '100%';
document.body.appendChild(div);
cx.window.terminal = this.terminal = new hterm.Terminal();
this.terminal.decorate(div);
this.terminal.setHeight(this.visibleRowCount);
this.terminal.setWidth(this.visibleColumnCount);
MockNotification.start();
};
/**
* Restore any mocked out objects.
*
* Called after each test case in this suite.
*/
hterm.Terminal.Tests.prototype.postamble = function(result, cx) {
MockNotification.stop();
};
/**
* Overridden addTest method.
*
* Every test in this suite needs to wait for the terminal initialization to
* complete asynchronously. Rather than stick a bunch of boilerplate into each
* test case, we use this overridden addTest method to add a proxy around the
* actual test.
*/
hterm.Terminal.Tests.addTest = function(name, callback) {
function testProxy(result, cx) {
var self = this;
setTimeout(function() {
self.terminal.setCursorPosition(0, 0);
callback.apply(self, [result, cx]);
}, 0);
result.requestTime(200);
}
lib.TestManager.Suite.addTest.apply(this, [name, testProxy]);
};
/**
* How long to wait for image display tests to timeout.
*
* Passing tests won't hit this, so having it higher is OK. When Chrome is
* running in the background (e.g. the window/tab isn't focused), then Chrome
* will deprioritize it causing JS/image loading/etc... to take longer.
*/
hterm.Terminal.Tests.DISPLAY_IMAGE_TIMEOUT = 5000;
/**
* Checks that the dimensions of the scrollport match the dimensions of the
* values that the Terminal was constructed with.
*/
hterm.Terminal.Tests.addTest('dimensions', function(result, cx) {
var divSize = hterm.getClientSize(this.div);
var scrollPort = this.terminal.scrollPort_;
var innerWidth = Math.round(
divSize.width - scrollPort.currentScrollbarWidthPx);
result.assertEQ(innerWidth, Math.round(scrollPort.getScreenWidth()));
result.assertEQ(Math.round(divSize.height),
Math.round(scrollPort.getScreenHeight()));
result.assertEQ(Math.floor(innerWidth / scrollPort.characterSize.width),
this.visibleColumnCount);
result.assertEQ(
Math.round(divSize.height / scrollPort.characterSize.height),
this.visibleRowCount);
result.assertEQ(this.terminal.screen_.getWidth(), this.visibleColumnCount);
result.assertEQ(this.terminal.screen_.getHeight(), this.visibleRowCount);
result.pass();
});
/**
* Fill the screen with 'X' characters one character at a time, in a way
* that should stress the cursor positioning code.
*/
hterm.Terminal.Tests.addTest('plaintext-stress-cursor-ltr',
function(result, cx) {
for (var col = 0; col < this.visibleColumnCount; col++) {
for (var row = 0; row < this.visibleRowCount; row++) {
this.terminal.screen_.setCursorPosition(row, col);
this.terminal.screen_.insertString('X');
}
}
result.pass();
});
/**
* Fill the screen with 'X' characters one character at a time, in a way
* that should stress the cursor positioning code and the overwriteString()
* code.
*/
hterm.Terminal.Tests.addTest('plaintext-stress-cursor-rtl',
function(result, cx) {
for (var col = this.visibleColumnCount - 1; col >= 0; col--) {
for (var row = 0; row < this.visibleRowCount; row++) {
this.terminal.screen_.setCursorPosition(row, col);
this.terminal.screen_.overwriteString('X');
}
}
result.pass();
});
/**
* Fill the terminal with a lot of text as quickly as possible.
*
* This test doesn't actually assert anything, but the timing data in the test
* log is useful.
*/
hterm.Terminal.Tests.addTest('plaintext-stress-insert',
function(result, cx) {
var chunkSize = 1000;
var testCount = 10;
var self = this;
function test(count) {
for (var i = count * chunkSize; i < (count + 1) * chunkSize; i++) {
if (i != 0)
self.terminal.newLine();
self.terminal.screen_.insertString(
'line ' + i + ': All work and no play makes jack a dull boy.');
}
if (count + 1 >= testCount) {
result.pass();
} else {
result.requestTime(200);
setTimeout(test, 0, count + 1);
}
}
test(0);
});
/**
* Test that accounting of desktop notifications works, and that they are
* closed under the right circumstances.
*/
hterm.Terminal.Tests.addTest('desktop-notification-bell-test',
function(result, cx) {
this.terminal.desktopNotificationBell_ = true;
// If we have focus, then no notifications should show.
this.terminal.document_.hasFocus = function() { return true; };
// Ring the bell, but nothing shows up.
result.assertEQ(0, this.terminal.bellNotificationList_.length);
result.assertEQ(0, Notification.count);
this.terminal.ringBell();
this.terminal.ringBell();
this.terminal.ringBell();
this.terminal.ringBell();
result.assertEQ(0, this.terminal.bellNotificationList_.length);
result.assertEQ(0, Notification.count);
// If we don't have focus, then notifications should show.
this.terminal.document_.hasFocus = function() { return false; };
// Gaining focus closes all desktop notifications.
result.assertEQ(0, this.terminal.bellNotificationList_.length);
result.assertEQ(0, Notification.count);
this.terminal.ringBell();
result.assertEQ(1, this.terminal.bellNotificationList_.length);
result.assertEQ(1, Notification.count);
this.terminal.ringBell();
result.assertEQ(2, this.terminal.bellNotificationList_.length);
result.assertEQ(2, Notification.count);
this.terminal.onFocusChange_(true);
result.assertEQ(0, this.terminal.bellNotificationList_.length);
result.assertEQ(0, Notification.count);
// A user click closes all desktop notifications.
this.terminal.ringBell();
this.terminal.ringBell();
result.assertEQ(2, this.terminal.bellNotificationList_.length);
result.assertEQ(2, Notification.count);
this.terminal.bellNotificationList_[0].onclick(null);
result.assertEQ(0, this.terminal.bellNotificationList_.length);
result.assertEQ(0, Notification.count);
result.pass();
});
/**
* Test that focus sequences are passed as expected when focus reporting is
* turned on, and nothing is passed when reporting is off.
*/
hterm.Terminal.Tests.addTest('focus-reporting', function(result, cx) {
var resultString = '';
this.terminal.io.sendString = (str) => resultString = str;
this.terminal.interpret('\x1b[?1004h');
this.terminal.onFocusChange_(false);
result.assertEQ(resultString, '\x1b[O');
this.terminal.onFocusChange_(true);
result.assertEQ(resultString, '\x1b[I');
resultString = '';
this.terminal.interpret('\x1b[?1004l');
this.terminal.onFocusChange_(false);
result.assertEQ(resultString, '');
this.terminal.onFocusChange_(true);
result.assertEQ(resultString, '');
result.pass();
});
/**
* Verify saved cursors have per-screen state.
*/
hterm.Terminal.Tests.addTest('per-screen-cursor-state', function(result, cx) {
const terminal = this.terminal;
const vt = terminal.vt;
// Start with the primary screen.
terminal.setAlternateMode(false);
// This should be the default cursor state.
terminal.restoreCursorAndState();
result.assertEQ(0, terminal.getCursorRow());
result.assertEQ(0, terminal.getCursorColumn());
result.assertEQ('G0', vt.GL);
result.assertEQ('G0', vt.GR);
// Change the primary cursor a bit and save it.
vt.GL = 'G1';
vt.GR = 'G2';
terminal.setAbsoluteCursorPosition(3, 4);
result.assertEQ(3, terminal.getCursorRow());
result.assertEQ(4, terminal.getCursorColumn());
terminal.saveCursorAndState();
// Switch to the alternative screen.
terminal.setAlternateMode(true);
// Cursor state should not be changed.
result.assertEQ(3, terminal.getCursorRow());
result.assertEQ(4, terminal.getCursorColumn());
result.assertEQ('G1', vt.GL);
result.assertEQ('G2', vt.GR);
// This should be the default cursor state.
terminal.restoreCursorAndState();
result.assertEQ(0, terminal.getCursorRow());
result.assertEQ(0, terminal.getCursorColumn());
result.assertEQ('G0', vt.GL);
result.assertEQ('G0', vt.GR);
// Change the alternate cursor a bit and save it.
vt.GL = 'G2';
vt.GR = 'G3';
terminal.setAbsoluteCursorPosition(7, 8);
result.assertEQ(7, terminal.getCursorRow());
result.assertEQ(8, terminal.getCursorColumn());
terminal.saveCursorAndState();
// Switch back to the primary scren.
terminal.setAlternateMode(false);
// Cursor state should not be changed.
result.assertEQ(7, terminal.getCursorRow());
result.assertEQ(8, terminal.getCursorColumn());
result.assertEQ('G2', vt.GL);
result.assertEQ('G3', vt.GR);
// This should be the primary cursor state we set up earlier.
terminal.restoreCursorAndState();
result.assertEQ(3, terminal.getCursorRow());
result.assertEQ(4, terminal.getCursorColumn());
result.assertEQ('G1', vt.GL);
result.assertEQ('G2', vt.GR);
// Finally back to the alternate scren.
terminal.setAlternateMode(true);
// Cursor state should not be changed.
result.assertEQ(3, terminal.getCursorRow());
result.assertEQ(4, terminal.getCursorColumn());
result.assertEQ('G1', vt.GL);
result.assertEQ('G2', vt.GR);
// This should be the alternate cursor state we set up earlier.
terminal.restoreCursorAndState();
result.assertEQ(7, terminal.getCursorRow());
result.assertEQ(8, terminal.getCursorColumn());
result.assertEQ('G2', vt.GL);
result.assertEQ('G3', vt.GR);
result.pass();
});
/**
* Check image display handling when disabled.
*/
hterm.Terminal.Tests.addTest('display-img-disabled', function(result, cx) {
this.terminal.allowImagesInline = false;
this.terminal.displayImage({uri: ''});
const text = this.terminal.getRowsText(0, 1);
result.assertEQ('Inline Images Disabled', text);
result.pass();
});
/**
* Check image display handling when not yet decided.
*/
hterm.Terminal.Tests.addTest('display-img-prompt', function(result, cx) {
this.terminal.allowImagesInline = null;
// Search for the block & allow buttons.
this.terminal.displayImage({uri: ''});
const text = this.terminal.getRowsText(0, 1);
result.assert(text.includes('block'));
result.assert(text.includes('allow'));
result.pass();
});
/**
* Check simple image display handling.
*/
hterm.Terminal.Tests.addTest('display-img-normal', function(result, cx) {
this.terminal.allowImagesInline = true;
// This is a 16px x 8px gif.
const data = 'R0lGODdhCAAQAIAAAP///wAAACwAAAAACAAQAAACFkSAhpfMC1uMT1mabHWZy6t1U/htQAEAOw==';
// Callback when loading finishes.
const onLoad = () => {
result.assertEQ(1, this.terminal.getCursorRow());
const row = this.terminal.getRowNode(0);
const container = row.childNodes[1];
const img = container.childNodes[0];
result.assertEQ('center', container.style.textAlign);
result.assertEQ(2, img.clientHeight);
result.pass();
};
// Display an image that only takes up one row.
this.terminal.displayImage({
height: '2px',
inline: true,
align: 'center',
uri: `data:application/octet-stream;base64,${data}`,
}, onLoad, (e) => result.fail(e));
result.requestTime(hterm.Terminal.Tests.DISPLAY_IMAGE_TIMEOUT);
});
/**
* Check handling of image dimensions.
*/
hterm.Terminal.Tests.addTest('display-img-dimensions', function(result, cx) {
this.terminal.allowImagesInline = true;
// This is a 16px x 8px gif.
const data = 'R0lGODdhCAAQAIAAAP///wAAACwAAAAACAAQAAACFkSAhpfMC1uMT1mabHWZy6t1U/htQAEAOw==';
// Callback when loading finishes.
const onLoad = () => {
result.assertEQ(4, this.terminal.getCursorRow());
const row = this.terminal.getRowNode(3);
const container = row.childNodes[1];
const img = container.childNodes[0];
// The image should be 4 rows tall.
result.assert(img.clientHeight ==
this.terminal.scrollPort_.characterSize.height * 4);
// Do a range check for the percentage width.
const bodyWidth = this.terminal.document_.body.clientWidth;
result.assert(img.clientWidth > bodyWidth * 0.70);
result.assert(img.clientWidth < bodyWidth * 0.80);
result.pass();
};
// Display an image that only takes up one row.
this.terminal.displayImage({
height: '4',
width: '75%',
inline: true,
uri: `data:application/octet-stream;base64,${data}`,
}, onLoad, (e) => result.fail(e));
result.requestTime(hterm.Terminal.Tests.DISPLAY_IMAGE_TIMEOUT);
});
/**
* Check handling of max image dimensions.
*/
hterm.Terminal.Tests.addTest('display-img-max-dimensions', function(result, cx) {
this.terminal.allowImagesInline = true;
// This is a 16px x 8px gif.
const data = 'R0lGODdhCAAQAIAAAP///wAAACwAAAAACAAQAAACFkSAhpfMC1uMT1mabHWZy6t1U/htQAEAOw==';
// Callback when loading finishes.
const onLoad = () => {
const rowNum = this.terminal.screen_.getHeight() - 1;
result.assertEQ(rowNum, this.terminal.getCursorRow());
const row = this.terminal.getRowNode(rowNum);
const container = row.childNodes[1];
const img = container.childNodes[0];
// The image should take up the whole screen, but not more.
const body = this.terminal.document_.body;
result.assertEQ(img.clientHeight, body.clientHeight);
result.assertEQ(img.clientWidth, body.clientWidth);
result.pass();
};
// Display an image that only takes up one row.
this.terminal.displayImage({
height: '4000px',
width: '1000',
inline: true,
uri: `data:application/octet-stream;base64,${data}`,
}, onLoad, (e) => result.fail(e));
result.requestTime(hterm.Terminal.Tests.DISPLAY_IMAGE_TIMEOUT);
});
/**
* Check loading of invalid images doesn't wedge the terminal.
*/
hterm.Terminal.Tests.addTest('display-img-invalid', function(result, cx) {
this.terminal.allowImagesInline = true;
// Callback when loading finishes (i.e. failure triggers).
const onError = () => {
// The cursor should not have advanced.
result.assertEQ(0, this.terminal.getCursorRow());
result.pass();
};
// The data is invalid image content.
this.terminal.displayImage({
inline: true,
uri: 'data:application/octet-stream;base64,asdf',
}, () => result.fail('image loading should have failed'), () => {
// We can't seem to run directly from the onError as JS doesn't like to
// throw exceptions in there that our framework catches.
// TODO(vapier): Should figure this out.
setTimeout(onError, 0);
});
result.requestTime(hterm.Terminal.Tests.DISPLAY_IMAGE_TIMEOUT);
});
/**
* Verify turning text blink on/off works.
*
* This test isn't great. Since we use CSS animations for everything, we
* assume that part is working, so we just check the stored timing values.
*/
hterm.Terminal.Tests.addTest('text-blink', function(result, cx) {
// Default blink state is enabled.
this.terminal.setTextBlink();
result.assert('0' != this.terminal.getCssVar('blink-node-duration'));
// Explicitly turn it off.
this.terminal.setTextBlink(false);
result.assertEQ('0', this.terminal.getCssVar('blink-node-duration'));
// Explicitly turn it back on.
this.terminal.setTextBlink(true);
result.assert('0' != this.terminal.getCssVar('blink-node-duration'));
result.pass();
});