blob: 7deae3d718ab14bc1e2dfdf128a747a8a5b426ed [file] [log] [blame]
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../test/common-test-setup';
import {
descendedFromClass,
eventMatchesShortcut,
getComputedStyleValue,
getEventPath,
Key,
Modifier,
querySelectorAll,
shouldSuppress,
strToClassName,
} from './dom-util';
import {mockPromise, pressKey, queryAndAssert} from '../test/test-utils';
import {fixture, assert} from '@open-wc/testing';
import {LitElement, html} from 'lit';
import {customElement} from 'lit/decorators.js';
/**
* You might think that instead of passing in the callback with assertions as a
* parameter that you could as well just `await keyEventOn()` and *then* run
* your assertions. But at that point the event is not "hot" anymore, so most
* likely you want to assert stuff about the event within the callback
* parameter.
*/
function keyEventOn(
el: HTMLElement,
callback: (e: KeyboardEvent) => void,
key = 'k'
): Promise<KeyboardEvent> {
const promise = mockPromise<KeyboardEvent>();
el.addEventListener('keydown', (e: KeyboardEvent) => {
callback(e);
promise.resolve(e);
});
pressKey(el, key);
return promise;
}
@customElement('dom-util-test-element')
export class TestElement extends LitElement {
override render() {
return html`
<div>
<div class="a">
<div class="b">
<div class="c"></div>
</div>
<span class="ss"></span>
</div>
<span class="ss"></span>
</div>
`;
}
}
async function createFixture() {
return await fixture<HTMLElement>(html`
<div id="test" class="a b c d">
<a class="testBtn" style="color:red;"></a>
<dom-util-test-element></dom-util-test-element>
<span class="ss"></span>
</div>
`);
}
suite('dom-util tests', () => {
suite('getEventPath', () => {
test('empty event', () => {
assert.equal(getEventPath(), '');
assert.equal(getEventPath(undefined), '');
assert.equal(getEventPath(new MouseEvent('click')), '');
});
test('event with fake path', () => {
assert.equal(getEventPath(new MouseEvent('click')), '');
const dd = document.createElement('dd');
assert.equal(
getEventPath({...new MouseEvent('click'), composedPath: () => [dd]}),
'dd'
);
});
test('event with fake complicated path', () => {
const dd = document.createElement('dd');
dd.setAttribute('id', 'test');
dd.className = 'a b';
const divNode = document.createElement('DIV');
divNode.id = 'test2';
divNode.className = 'a b c';
assert.equal(
getEventPath({
...new MouseEvent('click'),
composedPath: () => [dd, divNode],
}),
'div#test2.a.b.c>dd#test.a.b'
);
});
test('event with fake target', () => {
const fakeTargetParent1 = document.createElement('dd');
fakeTargetParent1.setAttribute('id', 'test');
fakeTargetParent1.className = 'a b';
const fakeTargetParent2 = document.createElement('DIV');
fakeTargetParent2.id = 'test2';
fakeTargetParent2.className = 'a b c';
fakeTargetParent2.appendChild(fakeTargetParent1);
const fakeTarget = document.createElement('SPAN');
fakeTargetParent1.appendChild(fakeTarget);
assert.equal(
getEventPath({
...new MouseEvent('click'),
composedPath: () => [],
target: fakeTarget,
}),
'div#test2.a.b.c>dd#test.a.b>span'
);
});
test('event with real click', async () => {
const element = await createFixture();
const aLink = queryAndAssert<HTMLAnchorElement>(element, 'a');
let path;
aLink.addEventListener('click', (e: Event) => {
path = getEventPath(e as MouseEvent);
});
aLink.click();
assert.equal(path, 'html>body>div>div#test.a.b.c.d>a.testBtn');
});
});
suite('querySelector and querySelectorAll', () => {
test('query cross shadow dom', async () => {
const element = await createFixture();
const theFirstEl = queryAndAssert(element, '.ss');
const allEls = querySelectorAll(element, '.ss');
assert.equal(allEls.length, 3);
assert.equal(theFirstEl, allEls[0]);
});
});
suite('getComputedStyleValue', () => {
test('color style', async () => {
const element = await createFixture();
const testBtn = queryAndAssert(element, '.testBtn');
assert.equal(getComputedStyleValue('color', testBtn), 'rgb(255, 0, 0)');
});
});
suite('descendedFromClass', () => {
test('descends from itself', async () => {
const element = await createFixture();
const testEl = queryAndAssert(element, 'dom-util-test-element');
assert.isTrue(descendedFromClass(queryAndAssert(testEl, '.c'), 'c'));
assert.isTrue(descendedFromClass(queryAndAssert(testEl, '.b'), 'b'));
assert.isTrue(descendedFromClass(queryAndAssert(testEl, '.a'), 'a'));
});
test('.c in .b in .a', async () => {
const element = await createFixture();
const testEl = queryAndAssert(element, 'dom-util-test-element');
const a = queryAndAssert(testEl, '.a');
const b = queryAndAssert(testEl, '.b');
const c = queryAndAssert(testEl, '.c');
assert.isTrue(descendedFromClass(a, 'a'));
assert.isTrue(descendedFromClass(b, 'a'));
assert.isTrue(descendedFromClass(c, 'a'));
assert.isFalse(descendedFromClass(a, 'b'));
assert.isTrue(descendedFromClass(b, 'b'));
assert.isTrue(descendedFromClass(c, 'b'));
assert.isFalse(descendedFromClass(a, 'c'));
assert.isFalse(descendedFromClass(b, 'c'));
assert.isTrue(descendedFromClass(c, 'c'));
});
test('stops at shadow root', async () => {
const element = await createFixture();
const testEl = queryAndAssert(element, 'dom-util-test-element');
const a = queryAndAssert(testEl, '.a');
// div.d is a parent of testEl, but `descendedFromClass` does not cross
// the shadow root boundary of <dom-util-test-element>. So div.a inside
// the shadow root is not considered to descend from div.d outside of it.
assert.isFalse(descendedFromClass(a, 'd'));
});
test('stops at stop element', async () => {
const element = await createFixture();
const testEl = queryAndAssert(element, 'dom-util-test-element');
assert.isFalse(
descendedFromClass(
queryAndAssert(testEl, '.c'),
'a',
queryAndAssert(testEl, '.b')
)
);
});
});
suite('strToClassName', () => {
test('basic tests', () => {
assert.equal(strToClassName(''), 'generated_');
assert.equal(strToClassName('11'), 'generated_11');
assert.equal(strToClassName('0.123'), 'generated_0_123');
assert.equal(strToClassName('0.123', 'prefix_'), 'prefix_0_123');
assert.equal(strToClassName('0>123', 'prefix_'), 'prefix_0_123');
assert.equal(strToClassName('0<123', 'prefix_'), 'prefix_0_123');
assert.equal(strToClassName('0+1+23', 'prefix_'), 'prefix_0_1_23');
});
});
suite('eventMatchesShortcut', () => {
test('basic tests', () => {
const a = new KeyboardEvent('keydown', {key: 'a'});
const b = new KeyboardEvent('keydown', {key: 'B'});
assert.isTrue(eventMatchesShortcut(a, {key: 'a'}));
assert.isFalse(eventMatchesShortcut(a, {key: 'B'}));
assert.isFalse(eventMatchesShortcut(b, {key: 'a'}));
assert.isTrue(eventMatchesShortcut(b, {key: 'B'}));
});
test('check modifiers for a', () => {
const e = new KeyboardEvent('keydown', {key: 'a'});
const s = {key: 'a'};
assert.isTrue(eventMatchesShortcut(e, s));
const eAlt = new KeyboardEvent('keydown', {key: 'a', altKey: true});
const sAlt = {key: 'a', modifiers: [Modifier.ALT_KEY]};
assert.isFalse(eventMatchesShortcut(eAlt, s));
assert.isFalse(eventMatchesShortcut(e, sAlt));
const eCtrl = new KeyboardEvent('keydown', {key: 'a', ctrlKey: true});
const sCtrl = {key: 'a', modifiers: [Modifier.CTRL_KEY]};
assert.isFalse(eventMatchesShortcut(eCtrl, s));
assert.isFalse(eventMatchesShortcut(e, sCtrl));
const eMeta = new KeyboardEvent('keydown', {key: 'a', metaKey: true});
const sMeta = {key: 'a', modifiers: [Modifier.META_KEY]};
assert.isFalse(eventMatchesShortcut(eMeta, s));
assert.isFalse(eventMatchesShortcut(e, sMeta));
// Do NOT check SHIFT for alphanum keys.
const eShift = new KeyboardEvent('keydown', {key: 'a', shiftKey: true});
const sShift = {key: 'a', modifiers: [Modifier.SHIFT_KEY]};
assert.isTrue(eventMatchesShortcut(eShift, s));
assert.isTrue(eventMatchesShortcut(e, sShift));
});
test('check modifiers for Enter', () => {
const e = new KeyboardEvent('keydown', {key: 'Enter'});
const s = {key: 'Enter'};
assert.isTrue(eventMatchesShortcut(e, s));
const eAlt = new KeyboardEvent('keydown', {key: 'Enter', altKey: true});
const sAlt = {key: 'Enter', modifiers: [Modifier.ALT_KEY]};
assert.isFalse(eventMatchesShortcut(eAlt, s));
assert.isFalse(eventMatchesShortcut(e, sAlt));
const eCtrl = new KeyboardEvent('keydown', {key: 'Enter', ctrlKey: true});
const sCtrl = {key: 'Enter', modifiers: [Modifier.CTRL_KEY]};
assert.isFalse(eventMatchesShortcut(eCtrl, s));
assert.isFalse(eventMatchesShortcut(e, sCtrl));
const eMeta = new KeyboardEvent('keydown', {key: 'Enter', metaKey: true});
const sMeta = {key: 'Enter', modifiers: [Modifier.META_KEY]};
assert.isFalse(eventMatchesShortcut(eMeta, s));
assert.isFalse(eventMatchesShortcut(e, sMeta));
const eShift = new KeyboardEvent('keydown', {
key: 'Enter',
shiftKey: true,
});
const sShift = {key: 'Enter', modifiers: [Modifier.SHIFT_KEY]};
assert.isFalse(eventMatchesShortcut(eShift, s));
assert.isFalse(eventMatchesShortcut(e, sShift));
});
test('check modifiers for [', () => {
const e = new KeyboardEvent('keydown', {key: '['});
const s = {key: '['};
assert.isTrue(eventMatchesShortcut(e, s));
const eCtrl = new KeyboardEvent('keydown', {key: '[', ctrlKey: true});
const sCtrl = {key: '[', modifiers: [Modifier.CTRL_KEY]};
assert.isFalse(eventMatchesShortcut(eCtrl, s));
assert.isFalse(eventMatchesShortcut(e, sCtrl));
const eMeta = new KeyboardEvent('keydown', {key: '[', metaKey: true});
const sMeta = {key: '[', modifiers: [Modifier.META_KEY]};
assert.isFalse(eventMatchesShortcut(eMeta, s));
assert.isFalse(eventMatchesShortcut(e, sMeta));
// Do NOT check SHIFT and ALT for special chars like [.
const eAlt = new KeyboardEvent('keydown', {key: '[', altKey: true});
const sAlt = {key: '[', modifiers: [Modifier.ALT_KEY]};
assert.isTrue(eventMatchesShortcut(eAlt, s));
assert.isTrue(eventMatchesShortcut(e, sAlt));
const eShift = new KeyboardEvent('keydown', {
key: '[',
shiftKey: true,
});
const sShift = {key: '[', modifiers: [Modifier.SHIFT_KEY]};
assert.isTrue(eventMatchesShortcut(eShift, s));
assert.isTrue(eventMatchesShortcut(e, sShift));
});
});
suite('shouldSuppress', () => {
test('do not suppress shortcut event from <div>', async () => {
await keyEventOn(document.createElement('div'), e => {
assert.isFalse(shouldSuppress(e));
});
});
test('suppress shortcut event from <div contenteditable>', async () => {
const el = document.createElement('div');
el.setAttribute('contenteditable', '');
await keyEventOn(el, e => {
assert.isTrue(shouldSuppress(e));
});
});
test('suppress shortcut event from <input>', async () => {
await keyEventOn(document.createElement('input'), e => {
assert.isTrue(shouldSuppress(e));
});
});
test('suppress shortcut event from <textarea>', async () => {
await keyEventOn(document.createElement('textarea'), e => {
assert.isTrue(shouldSuppress(e));
});
});
test('suppress shortcut event from <gr-textarea>', async () => {
await keyEventOn(document.createElement('gr-textarea'), e => {
assert.isTrue(shouldSuppress(e));
});
});
test('do not suppress shortcut event from checkbox <input>', async () => {
const inputEl = document.createElement('input');
inputEl.setAttribute('type', 'checkbox');
await keyEventOn(inputEl, e => {
assert.isFalse(shouldSuppress(e));
});
});
test('suppress "enter" shortcut event from <gr-button>', async () => {
await keyEventOn(
document.createElement('gr-button'),
e => assert.isTrue(shouldSuppress(e)),
Key.ENTER
);
});
test('suppress "enter" shortcut event from <a>', async () => {
await keyEventOn(document.createElement('a'), e => {
assert.isFalse(shouldSuppress(e));
});
await keyEventOn(
document.createElement('a'),
e => assert.isTrue(shouldSuppress(e)),
Key.ENTER
);
});
});
});