blob: b03a42181b91af0d7c8f9bdfee7f1b37ac63ec83 [file] [log] [blame]
/**
* @license
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as sinon from 'sinon';
import '../../../test/common-test-setup';
import {fixture, html, assert} from '@open-wc/testing';
import {AbortStop, CursorMoveResult} from '../../../api/core';
import {GrCursorManager} from './gr-cursor-manager';
suite('gr-cursor-manager tests', () => {
let cursor: GrCursorManager;
let list: Element;
setup(async () => {
list = await fixture(html` <ul>
<li>A</li>
<li>B</li>
<li>C</li>
<li>D</li>
</ul>`);
cursor = new GrCursorManager();
cursor.cursorTargetClass = 'targeted';
});
test('core cursor functionality', () => {
// The element is initialized into the proper state.
assert.isArray(cursor.stops);
assert.equal(cursor.stops.length, 0);
assert.equal(cursor.index, -1);
assert.isNotOk(cursor.target);
// Initialize the cursor with its stops.
cursor.stops = [...list.querySelectorAll('li')];
// It should have the stops but it should not be targeting any of them.
assert.isNotNull(cursor.stops);
assert.equal(cursor.stops.length, 4);
assert.equal(cursor.index, -1);
assert.isNotOk(cursor.target);
// Select the third stop.
cursor.setCursor(list.children[2] as HTMLElement);
// It should update its internal state and update the element's class.
assert.equal(cursor.index, 2);
assert.equal(cursor.target, list.children[2] as HTMLElement);
assert.isTrue(list.children[2].classList.contains('targeted'));
assert.isFalse(cursor.isAtStart());
assert.isFalse(cursor.isAtEnd());
// Progress the cursor.
let result = cursor.next();
// Confirm that the next stop is selected and that the previous stop is
// unselected.
assert.equal(result, CursorMoveResult.MOVED);
assert.equal(cursor.index, 3);
assert.equal(cursor.target, list.children[3] as HTMLElement);
assert.isTrue(cursor.isAtEnd());
assert.isFalse(list.children[2].classList.contains('targeted'));
assert.isTrue(list.children[3].classList.contains('targeted'));
// Progress the cursor.
result = cursor.next();
// We should still be at the end.
assert.equal(result, CursorMoveResult.CLIPPED);
assert.equal(cursor.index, 3);
assert.equal(cursor.target, list.children[3] as HTMLElement);
assert.isTrue(cursor.isAtEnd());
// Wind the cursor all the way back to the first stop.
result = cursor.previous();
assert.equal(result, CursorMoveResult.MOVED);
result = cursor.previous();
assert.equal(result, CursorMoveResult.MOVED);
result = cursor.previous();
assert.equal(result, CursorMoveResult.MOVED);
// The element state should reflect the start of the list.
assert.equal(cursor.index, 0);
assert.equal(cursor.target, list.children[0] as HTMLElement);
assert.isTrue(cursor.isAtStart());
assert.isTrue(list.children[0].classList.contains('targeted'));
const newLi = document.createElement('li');
newLi.textContent = 'Z';
list.insertBefore(newLi, list.children[0]);
cursor.stops = [...list.querySelectorAll('li')];
assert.equal(cursor.index, 1);
// De-select all targets.
cursor.unsetCursor();
// There should now be no cursor target.
assert.isFalse(list.children[1].classList.contains('targeted'));
assert.isNotOk(cursor.target);
assert.equal(cursor.index, -1);
});
test('isAtStart() returns true when there are no stops', () => {
cursor.stops = [];
assert.isTrue(cursor.isAtStart());
});
test('isAtEnd() returns true when there are no stops', () => {
cursor.stops = [];
assert.isTrue(cursor.isAtEnd());
});
test('next() goes to first element when no cursor is set', () => {
cursor.stops = [...list.querySelectorAll('li')];
const result = cursor.next();
assert.equal(result, CursorMoveResult.MOVED);
assert.equal(cursor.index, 0);
assert.equal(cursor.target, list.children[0] as HTMLElement);
assert.isTrue(list.children[0].classList.contains('targeted'));
assert.isTrue(cursor.isAtStart());
assert.isFalse(cursor.isAtEnd());
});
test('next() resets the cursor when there are no stops', () => {
cursor.stops = [];
const result = cursor.next();
assert.equal(result, CursorMoveResult.NO_STOPS);
assert.equal(cursor.index, -1);
assert.isNotOk(cursor.target);
assert.isFalse(list.children[1].classList.contains('targeted'));
});
test('previous() goes to last element when no cursor is set', () => {
cursor.stops = [...list.querySelectorAll('li')];
const result = cursor.previous();
assert.equal(result, CursorMoveResult.MOVED);
const lastIndex = list.children.length - 1;
assert.equal(cursor.index, lastIndex);
assert.equal(cursor.target, list.children[lastIndex] as HTMLElement);
assert.isTrue(list.children[lastIndex].classList.contains('targeted'));
assert.isFalse(cursor.isAtStart());
assert.isTrue(cursor.isAtEnd());
});
test('previous() resets the cursor when there are no stops', () => {
cursor.stops = [];
const result = cursor.previous();
assert.equal(result, CursorMoveResult.NO_STOPS);
assert.equal(cursor.index, -1);
assert.isNotOk(cursor.target);
assert.isFalse(list.children[1].classList.contains('targeted'));
});
test('_moveCursor', () => {
// Initialize the cursor with its stops.
cursor.stops = [...list.querySelectorAll('li')];
// Select the first stop.
cursor.setCursor(list.children[0] as HTMLElement);
const getTargetHeight = sinon.stub();
// Move the cursor without an optional get target height function.
cursor._moveCursor(1);
assert.isFalse(getTargetHeight.called);
// Move the cursor with an optional get target height function.
cursor._moveCursor(1, {getTargetHeight});
assert.isTrue(getTargetHeight.called);
});
test('_moveCursor from for invalid index does not check height', () => {
cursor.stops = [];
const getTargetHeight = sinon.stub();
cursor._moveCursor(1, {filter: () => false, getTargetHeight});
assert.isFalse(getTargetHeight.called);
});
test('setCursorAtIndex with noScroll', () => {
sinon.stub(cursor, '_targetIsVisible').callsFake(() => false);
const scrollStub = sinon.stub(window, 'scrollTo');
cursor.stops = [...list.querySelectorAll('li')];
cursor.scrollMode = 'keep-visible';
cursor.setCursorAtIndex(1, true);
assert.isFalse(scrollStub.called);
cursor.setCursorAtIndex(2);
assert.isTrue(scrollStub.called);
});
test('move with filter', () => {
const isLetterB = function (row: HTMLElement) {
return row.textContent === 'B';
};
cursor.stops = [...list.querySelectorAll('li')];
// Start cursor at the first stop.
cursor.setCursor(list.children[0] as HTMLElement);
// Move forward to meet the next condition.
cursor.next({filter: isLetterB});
assert.equal(cursor.index, 1);
// Nothing else meets the condition, should be at last stop.
cursor.next({filter: isLetterB});
assert.equal(cursor.index, 3);
// Should stay at last stop if try to proceed.
cursor.next({filter: isLetterB});
assert.equal(cursor.index, 3);
// Go back to the previous condition met. Should be back at.
// stop 1.
cursor.previous({filter: isLetterB});
assert.equal(cursor.index, 1);
// Go back. No more meet the condition. Should be at stop 0.
cursor.previous({filter: isLetterB});
assert.equal(cursor.index, 0);
});
test('focusOnMove prop', () => {
const listEls = [...list.querySelectorAll('li')];
const listFocusStubs = listEls.map(listEl => sinon.spy(listEl, 'focus'));
cursor.stops = listEls;
cursor.setCursor(list.children[0] as HTMLElement);
cursor.focusOnMove = false;
cursor.next();
assert.equal(listEls[1], cursor.target);
assert.isFalse(listFocusStubs[1].called);
cursor.focusOnMove = true;
cursor.next();
assert.equal(listEls[2], cursor.target);
assert.isTrue(listFocusStubs[2].called);
});
suite('circular options', () => {
const options = {circular: true};
setup(() => {
cursor.stops = [...list.querySelectorAll('li')];
});
test('previous() on first element goes to last element', () => {
cursor.setCursor(list.children[0] as HTMLElement);
cursor.previous(options);
assert.equal(cursor.index, list.children.length - 1);
});
test('next() on last element goes to first element', () => {
cursor.setCursor(list.children[list.children.length - 1] as HTMLElement);
cursor.next(options);
assert.equal(cursor.index, 0);
});
});
suite('_scrollToTarget', () => {
let scrollStub: sinon.SinonStub;
setup(() => {
cursor.stops = [...list.querySelectorAll('li')];
cursor.scrollMode = 'keep-visible';
// There is a target which has a targetNext
cursor.setCursor(list.children[0] as HTMLElement);
cursor._moveCursor(1);
scrollStub = sinon.stub(window, 'scrollTo');
window.innerHeight = 60;
});
test('Called when top and bottom not visible', () => {
sinon.stub(cursor, '_targetIsVisible').returns(false);
cursor._scrollToTarget();
assert.isTrue(scrollStub.called);
});
test('Not called when top and bottom visible', () => {
sinon.stub(cursor, '_targetIsVisible').returns(true);
cursor._scrollToTarget();
assert.isFalse(scrollStub.called);
});
test('Called when top is visible, bottom is not, scroll is lower', () => {
const visibleStub = sinon
.stub(cursor, '_targetIsVisible')
.callsFake(() => visibleStub.callCount === 2);
window.scrollX = 123;
window.scrollY = 15;
window.innerHeight = 1000;
window.pageYOffset = 0;
sinon.stub(cursor, '_calculateScrollToValue').returns(20);
cursor._scrollToTarget();
assert.isTrue(scrollStub.called);
assert.isTrue(scrollStub.calledWithExactly(123, 20));
assert.equal(visibleStub.callCount, 2);
});
test('Called when top is visible, bottom not, scroll is higher', () => {
const visibleStub = sinon
.stub(cursor, '_targetIsVisible')
.callsFake(() => visibleStub.callCount === 2);
window.scrollX = 123;
window.scrollY = 25;
window.innerHeight = 1000;
window.pageYOffset = 0;
sinon.stub(cursor, '_calculateScrollToValue').returns(20);
cursor._scrollToTarget();
assert.isFalse(scrollStub.called);
assert.equal(visibleStub.callCount, 2);
});
test('_calculateScrollToValue', () => {
window.scrollX = 123;
window.scrollY = 25;
window.innerHeight = 300;
window.pageYOffset = 0;
const fakeElement = {offsetHeight: 10} as HTMLElement;
assert.equal(cursor._calculateScrollToValue(1000, fakeElement), 905);
});
});
suite('AbortStops', () => {
test('next() does not skip AbortStops', () => {
cursor.stops = [
document.createElement('li'),
new AbortStop(),
document.createElement('li'),
];
cursor.setCursorAtIndex(0);
const result = cursor.next();
assert.equal(result, CursorMoveResult.ABORTED);
assert.equal(cursor.index, 0);
});
test('setCursorAtIndex() does not target AbortStops', () => {
cursor.stops = [
document.createElement('li'),
new AbortStop(),
document.createElement('li'),
];
cursor.setCursorAtIndex(1);
assert.equal(cursor.index, -1);
});
test('moveToStart() does not target AbortStop', () => {
cursor.stops = [
new AbortStop(),
document.createElement('li'),
document.createElement('li'),
];
cursor.moveToStart();
assert.equal(cursor.index, -1);
});
test('moveToEnd() does not target AbortStop', () => {
cursor.stops = [
document.createElement('li'),
document.createElement('li'),
new AbortStop(),
];
cursor.moveToEnd();
assert.equal(cursor.index, -1);
});
});
});