blob: 7da3a095b408b718c7d4fc333924448d3c2109ed [file] [log] [blame]
// Copyright 2017 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.
const MOVE_TOLERANCE = 20;
const INDICATOR_LENGTH = 4;
/**
* API for touchscreen test.
*/
window.TouchscreenTest = class {
/*
* @param {number} numColumns Number of columns.
* @param {number} numRows Number of rows.
* @param {number} maxRetries Number of retries.
* @param {number} demoIntervalMsecs Interval (ms) to show drawing pattern.
* Non-positive value means no demo.
* @param {boolean} e2eMode Perform end-to-end test or not (for touchscreen).
* @param {boolean} spiralMode Blocks must be drawn in spiral order or not.
*/
constructor(
numColumns, numRows, maxRetries, demoIntervalMsecs, e2eMode, spiralMode) {
this.numColumns = numColumns;
this.numRows = numRows;
this.numBlocks = this.numRows * this.numColumns;
this.maxRetries = maxRetries;
this.e2eMode = e2eMode;
this.spiralMode = spiralMode;
this.expectSequence = [];
this.tries = 0;
this.tryFailed = false;
this.previousBlockIndex = -1;
this.testedBlockCount = 0;
this.demoIntervalMsecs = demoIntervalMsecs;
this.previousX = 0;
this.previousY = 0;
this.setupUI();
if (this.spiralMode) {
this.expectSequence = this.generateTouchSequence();
if (this.demoIntervalMsecs > 0) {
this.showDemoIndicator();
}
}
}
/**
* Initializes fullscreen div elements and sets fullscreen mode.
*
* The touch table contains numRows by numColumns divs.
*/
setupUI() {
if (this.e2eMode) {
const fullScreenElement = document.getElementById('fullscreen');
const handler = (handlerName) => (event) => {
event.preventDefault();
const touchEvent = event.changedTouches[0];
this[handlerName](touchEvent.screenX, touchEvent.screenY);
};
fullScreenElement.addEventListener(
'touchstart', handler('touchStartHandler'));
fullScreenElement.addEventListener(
'touchmove', handler('touchMoveHandler'));
fullScreenElement.addEventListener(
'touchend', handler('touchEndHandler'));
}
const testGrid = document.getElementById('test-grid');
testGrid.style.grid =
`repeat(${this.numRows}, 1fr) / repeat(${this.numColumns}, 1fr)`;
for (let i = 0; i < this.numBlocks; i++) {
const div = document.createElement('div');
div.id = `block-${i}`;
testGrid.appendChild(div);
}
this.restartTest();
window.test.setFullScreen(true);
}
/**
* Creates a touchscreen block test sequence.
*
* It starts from upper-left corner, draws the outer blocks in right, down,
* left, up directions; then draws inner blocks till the center block is
* reached.
*
* @return {Array<{blockIndex: number, directionX: number,
* directionY: number}>}
* Array of touchscreen block test sequence.
*/
generateTouchSequence() {
const steps = [[1, 0], [0, 1], [-1, 0], [0, -1]];
const points = [];
const walked = new Set();
const canWalk = (col, row) =>
(col >= 0 && col < this.numColumns && row >= 0 && row < this.numRows &&
!walked.has(this.colRowToIndex(col, row)));
let col = 0, row = 0, direction = 0;
for (let i = 0; i < this.numBlocks; i++) {
points.push([col, row]);
walked.add(this.colRowToIndex(col, row));
for (const newDirection of [direction, (direction + 1) % 4]) {
const [dCol, dRow] = steps[newDirection];
if (canWalk(col + dCol, row + dRow)) {
col += dCol;
row += dRow;
direction = newDirection;
break;
}
}
}
const result = points.map(([col, row]) => ({
blockIndex: this.colRowToIndex(col, row),
directionX: 0,
directionY: 0
}));
for (let i = 1; i < this.numBlocks; i++) {
const [col0, row0] = points[i - 1];
const [col1, row1] = points[i];
if (col0 !== col1) {
result[i - 1].directionX = result[i].directionX = col1 - col0;
}
if (row0 !== row1) {
result[i - 1].directionY = result[i].directionY = row1 - row0;
}
}
return result;
}
/**
* Converts (col, row) block coordinates to block index.
*
* @param {number} col column index of block.
* @param {number} row row index of block.
* @return {number} block index
*/
colRowToIndex(col, row) {
return col + row * this.numColumns;
}
/**
* Gets block index of the touch event.
*
* @param {number} screenX x-coordinate on screen.
* @param {number} screenY y-coordinate on screen.
* @return {number} Block ID.
*/
getBlockIndex(screenX, screenY) {
const col = Math.floor(screenX / screen.width * this.numColumns);
const row = Math.floor(screenY / screen.height * this.numRows);
return this.colRowToIndex(col, row);
}
/**
* Update previous x, y coordinates.
*
* @param {number} screenX x-coordinate on screen.
* @param {number} screenY y-coordinate on screen.
*/
updatePreviousXY(screenX, screenY) {
this.previousX = screenX;
this.previousY = screenY;
}
/**
* Checks if the moving direction conforms to expectSequence.
*
* On conducting the God's touch test in OQC, circles are supposed to show up
* exactly under the touching finger. If this is not the case, the touchscreen
* is considered bad. It is desirable to catch the mis-location problem too in
* this test. Such a bad panel may be caught in this test when a finger moves
* in some direction, its reported coordinates jump in the other directions
* when the finger moves to near around the problematic spot of the touchscreen.
*
* If directionX == 1, the finger is supposed to move to the right.
* If directionX == -1, the finger is supposed to move to the left.
* If directionX == 0, the finger is supposed to move in a vertical direction.
* The rules apply to directionY in a similar way.
* MOVE_TOLERANCE is used to allow a little deviation.
*
* @param {number} screenX x-coordinate on screen.
* @param {number} screenY y-coordinate on screen.
* @return {boolean} false if the moving direction is not correct.
*/
checkDirection(currentBlockIndex, screenX, screenY) {
if (this.testedBlockCount === this.numBlocks) {
return true;
}
const expectSequenceIndex = this.expectSequence.findIndex(
(ele) => ele.blockIndex === currentBlockIndex);
const diffX = screenX - this.previousX;
const diffY = screenY - this.previousY;
let checkX = false;
let checkY = false;
switch (this.expectSequence[expectSequenceIndex].directionX) {
case 1:
checkX = diffX > -MOVE_TOLERANCE;
break;
case 0:
checkX = Math.abs(diffX) < MOVE_TOLERANCE;
break;
case -1:
checkX = diffX < MOVE_TOLERANCE;
break;
}
switch (this.expectSequence[expectSequenceIndex].directionY) {
case 1:
checkY = diffY > -MOVE_TOLERANCE;
break;
case 0:
checkY = Math.abs(diffY) < MOVE_TOLERANCE;
break;
case -1:
checkY = diffY < MOVE_TOLERANCE;
break;
}
return checkX && checkY;
}
/**
* Fails this try and if #retries is reached, fail the test.
*
* @param {cros.factory.i18n.TranslationDict} message The failed message.
* @param {number} failedBlock The block index which the failure occurs.
*/
failThisTry(message, failedBlock) {
this.prompt(message);
this.setBlockState(failedBlock, 'failed');
// Prevent marking multiple failure for a try.
if (!this.tryFailed) {
this.tryFailed = true;
this.tries++;
if (this.tries > this.maxRetries) {
this.failTest();
}
}
}
/**
* Handles touchstart event.
*
* It checks if the touch starts from block (0, 0).
* If not, prompt operator to do so.
*
* @param {number} screenX x-coordinate on screen.
* @param {number} screenY y-coordinate on screen.
*/
touchStartHandler(screenX, screenY) {
const touchBlockIndex = this.getBlockIndex(screenX, screenY);
this.updatePreviousXY(screenX, screenY);
if (this.spiralMode &&
touchBlockIndex !== this.expectSequence[0].blockIndex) {
this.failThisTry(
_('Please start drawing from upper-left corner.'), touchBlockIndex);
this.previousBlockIndex = touchBlockIndex;
return;
}
// Reset blocks for previous failure.
if (this.tryFailed) {
this.restartTest();
}
}
/**
* Handles touchmove event.
*
* It'll check if the current block is the expected one.
* If not, it'll prompt operator to restart from upper-left block.
*
* @param {number} screenX x-coordinate on screen.
* @param {number} screenY y-coordinate on screen.
*/
touchMoveHandler(screenX, screenY) {
const touchBlockIndex = this.getBlockIndex(screenX, screenY);
if (touchBlockIndex !== this.previousBlockIndex) {
if (this.tryFailed) {
// Failed case. Either out-of-sequence touch or early finger leaving.
// Show stronger prompt for drawing multiple unexpected blocks.
this.failThisTry(
_('Please leave your finger and restart from upper-left block.'),
touchBlockIndex);
} else if (this.spiralMode) {
if (touchBlockIndex ===
this.expectSequence[this.testedBlockCount].blockIndex) {
this.markBlockTested(touchBlockIndex);
} else {
this.failThisTry(
_('Fails to draw blocks in sequence. Please try again.'),
touchBlockIndex);
}
} else {
this.markBlockTested(touchBlockIndex);
}
this.previousBlockIndex = touchBlockIndex;
}
if (!this.tryFailed && this.spiralMode &&
!this.checkDirection(touchBlockIndex, screenX, screenY)) {
// Failed case. Ask the tester to verify with God's touch test.
this.failThisTry(
_('Test failed! ' +
'Please test this panel carefully with Gods Touch test.'),
touchBlockIndex);
}
this.updatePreviousXY(screenX, screenY);
}
/**
* Handles touchend event.
*
* @param {number} screenX x-coordinate on screen.
* @param {number} screenY y-coordinate on screen.
*/
touchEndHandler(screenX, screenY) {
if (this.spiralMode) {
const touchBlockIndex = this.getBlockIndex(screenX, screenY);
if (!this.tryFailed) {
this.failThisTry(
_('Finger leaving too early. Please try again.'), touchBlockIndex);
}
}
}
/**
* Restarts the test.
*
* Resets test properties to default and blocks to untested.
*/
restartTest() {
this.prompt(
this.spiralMode ?
_('Draw blocks from upper-left corner in sequence; Esc to fail.') :
_('Draw blocks in any order; Esc to fail.'));
for (let i = 0; i < this.numBlocks; i++) {
this.setBlockState(i, 'untested');
}
this.previousBlockIndex = -1;
this.testedBlockCount = 0;
this.tryFailed = false;
}
/**
* Shows a hungry snake animation to guide operator to draw test pattern on
* the touchscreen.
*
* It starts at the expected blocks (index 0). It changes the target block's
* CSS to demo-0 (head indicator). Then the indicator block moves forward to
* next expected block after demoIntervalMsecs. As indicator moving forward,
* it had a tail with lighter color. And the block just behind the tail will
* be reset to untested CSS.
*/
async showDemoIndicator() {
for (let head = 0; head < this.numBlocks + INDICATOR_LENGTH; head++) {
for (let i = 0; i < this.numBlocks; i++) {
cros.factory.utils.removeClassesWithPrefix(
this.getBlockDiv(i), 'demo-');
}
for (let indicatorSegment = 0; indicatorSegment < INDICATOR_LENGTH;
indicatorSegment++) {
const index = head - indicatorSegment;
if (index < this.testedBlockCount || index >= this.numBlocks) {
continue;
}
this.getBlockDiv(this.expectSequence[index].blockIndex)
.classList.add(`demo-${indicatorSegment}`);
}
await cros.factory.utils.delay(this.demoIntervalMsecs);
}
}
/**
* Gets a block div from block index.
*
* @param {number} blockIndex
* @return {?Element}
*/
getBlockDiv(blockIndex) {
return document.getElementById(`block-${blockIndex}`);
}
/**
* Sets a block's test state.
*
* @param {number} blockIndex
* @param {boolean} state The state of the block.
*/
setBlockState(blockIndex, state) {
const div = this.getBlockDiv(blockIndex);
cros.factory.utils.removeClassesWithPrefix(div, 'state-');
div.classList.add(`state-${state}`);
}
/**
* Mark a untested block as tested. Also check if the test is completed.
*
* @param {number} blockIndex
*/
markBlockTested(blockIndex) {
if (!this.isBlockTested(blockIndex)) {
this.setBlockState(blockIndex, 'tested');
this.testedBlockCount++;
this.checkTestComplete();
}
}
/**
* Gets whether a block is tested.
*
* @param {number} blockIndex
*/
isBlockTested(blockIndex) {
return this.getBlockDiv(blockIndex).classList.contains('state-tested');
}
/**
* Checks if test is completed.
* */
checkTestComplete() {
if (this.testedBlockCount === this.numBlocks) {
window.test.pass();
}
}
/**
* Fails the test and prints out all the failed items.
*/
failTest() {
const getElementIds = (cls) =>
Array.from(document.getElementsByClassName(cls)).map((ele) => ele.id);
const untestedBlocks = getElementIds('state-untested');
const failedBlocks = getElementIds('state-failed');
let failMessage = 'Touchscreen test failed.';
if (failedBlocks.length) {
failMessage += ` Failed blocks: ${failedBlocks.join()}`;
}
if (untestedBlocks.length) {
failMessage += ` Untested blocks: ${untestedBlocks.join()}`;
}
window.test.fail(failMessage);
}
/**
* Sets prompt message.
*
* @param {cros.factory.i18n.TranslationDict} msg A message object
* containing i18n messages.
*/
prompt(msg) {
goog.dom.safe.setInnerHtml(
document.getElementById('prompt'), cros.factory.i18n.i18nLabel(msg));
}
/**
* Handle goofy touch event.
* @param {string} handlerName The type of handler to trigger.
* @param {number} normalizedX Normalized x coordinate in [0, 1].
* @param {number} normalizedY Normalized y coordinate in [0, 1].
*/
goofyTouchListener(handlerName, normalizedX, normalizedY) {
const screenX = screen.width * normalizedX;
const screenY = screen.height * normalizedY;
this[handlerName](screenX, screenY);
}
};