Skip to content

Commit

Permalink
feat: add clickElemAt & hoverElemAt (resolves #281)
Browse files Browse the repository at this point in the history
feat: add `clickElemAt` & `hoverElemAt`
  • Loading branch information
KearseTrevor authored Nov 15, 2024
2 parents 6396405 + 55277f9 commit c2e2e00
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 59 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,24 @@ it('should click on element', async() => {
});
```

To hover or click on a specific element with offset relative its origin (top-left corner), use `hoverElemAt(elem, x, y)` and `clickElemAt(elem, x, y)`. Useful for reducing shadowRoot snooping and element offset calculations.

```javascript
import { clickElemAt, hoverElemAt } from '@brightspace-ui/testing';

it('should click element with offset from element origin', async() => {
const elem = await fixture(...);
await clickElemAt(elem, 25, 50);
// do assertions
});

it('should hover element with offset from element origin', async() => {
const elem = await fixture(...);
await hoverElemAt(elem, 25, 50);
// do assertions
});
```

Alternatively, to hover over or click at viewport coordinates with the mouse, use `hoverAt(x, y)` or `clickAt(x, y)`:

```javascript
Expand Down Expand Up @@ -593,7 +611,7 @@ my-elem {
my-elem.faded-out {
opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
@media (prefers-reduced-motion: no-preference) {
my-elem {
transition: opacity 0.2s ease-in-out;
}
Expand Down Expand Up @@ -628,7 +646,7 @@ Refer to the [vdiff GitHub Action](https://github.com/BrightspaceUI/actions/tree

### Generating the Goldens

To ensure a consistent environment, goldens checked into source control should be generated by [continuous integration](#continuous-integration).
To ensure a consistent environment, goldens checked into source control should be generated by [continuous integration](#continuous-integration).

However, it can be helpful during development to generate a local version of the goldens to test and preview changes. This can be done by passing the `golden` sub-command:

Expand Down
12 changes: 12 additions & 0 deletions src/browser/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export { setViewport } from './reset.js';
function getElementPosition(elem) {
const { x, y, width, height } = elem.getBoundingClientRect();
return {
left: Math.floor(x + window.scrollX),
top: Math.floor(y + window.scrollY),
x: Math.floor(x + window.scrollX + width / 2),
y: Math.floor(y + window.scrollY + height / 2),
};
Expand All @@ -24,6 +26,11 @@ export async function clickElem(elem) {
return clickAt(position.x, position.y);
}

export async function clickElemAt(elem, offsetX = 0, offsetY = 0) {
const position = getElementPosition(elem);
return clickAt(Math.floor(position.left + offsetX), Math.floor(position.top + offsetY));
}

export async function dragDropElems(elem, toElem) {
const fromPosition = getElementPosition(elem);
const toPosition = getElementPosition(toElem);
Expand All @@ -48,6 +55,11 @@ export async function hoverElem(elem) {
return hoverAt(position.x, position.y);
}

export async function hoverElemAt(elem, offsetX = 0, offsetY = 0) {
const position = getElementPosition(elem);
return hoverAt(Math.floor(position.left + offsetX), Math.floor(position.top + offsetY));
}

export async function sendKeys(action, keys) {
const val = {};
val[action] = keys;
Expand Down
2 changes: 1 addition & 1 deletion src/browser/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import './vdiff.js';

export { assert, aTimeout, defineCE, expect, html, nextFrame, oneDefaultPreventedEvent, oneEvent, waitUntil } from '@open-wc/testing';
export { clickAt, clickElem, dragDropElems, focusElem, hoverAt, hoverElem, sendKeys, sendKeysElem, setViewport } from './commands.js';
export { clickAt, clickElem, clickElemAt, dragDropElems, focusElem, hoverAt, hoverElem, hoverElemAt, sendKeys, sendKeysElem, setViewport } from './commands.js';
export { fixture } from './fixture.js';
export { runConstructor } from './constructor.js';
200 changes: 144 additions & 56 deletions test/browser/commands.test.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,69 @@
import { clickAt, clickElem, dragDropElems, expect, fixture, focusElem, hoverAt, hoverElem, sendKeys, sendKeysElem, setViewport } from '../../src/browser/index.js';
import { clickAt, clickElem, clickElemAt, dragDropElems, expect, fixture, focusElem, hoverAt, hoverElem, hoverElemAt, sendKeys, sendKeysElem, setViewport } from '../../src/browser/index.js';
import { html } from 'lit';
import { spy } from 'sinon';

describe('commands', () => {
const buttonTemplate = html`<button>text</button>`;
const draggableTemplate = html`
<div>
<div id="dest" style="height: 100px; width: 100px;"></div>
<div id="source" draggable="true" style="height: 50px; width: 50px;"></div>
</div>`;
const emptyDivTemplate = html`<div></div>`;
const focusTemplate = html`
<div>
<input type="text">
<button>text</button>
</div>`;

let elem, focusSource, hovered, key, keys;
const clickPos = { x: 0, y: 0 };
const mousePos = { x: 0, y: 0 };

function onClick(e) {
clickPos.x = e.clientX;
clickPos.y = e.clientY;
}

function onKeyDown(e) {
key = e.key;
if (Array.isArray(keys)) {
keys.push(key);
};
}

function onFocus(e) {
focusSource = e.target;
}

function onMouseMove(e) {
mousePos.x = e.clientX;
mousePos.y = e.clientY;
}

function onMouseOver() {
hovered = true;
}

function onMouseOut() {
hovered = false;
}

before(() => {
window.addEventListener('click', onClick);
window.addEventListener('keydown', onKeyDown);
window.addEventListener('mousemove', onMouseMove);
});

describe('click/hover', () => {
after(() => {
window.removeEventListener('click', onClick);
window.removeEventListener('keydown', onKeyDown);
window.removeEventListener('mousemove', onMouseMove);
});

let elem;
describe('click/hover', () => {
beforeEach(async() => {
elem = await fixture(html`<button>text</button>`);
elem = await fixture(buttonTemplate);
});

it('should click on element', async() => {
Expand All @@ -19,81 +74,126 @@ describe('commands', () => {
});

it('should click at position', async() => {
const clickPos = { x: 0, y: 0 };
function onClick(e) {
clickPos.x = e.clientX;
clickPos.y = e.clientY;
}
window.addEventListener('click', onClick);
await clickAt(200, 300);

expect(clickPos.x).to.equal(200);
expect(clickPos.y).to.equal(300);
window.removeEventListener('click', onClick);
});

it('should clickElemAt top-left by default', async() => {
await clickElemAt(elem);

const { x: expectedX, y: expectedY } = elem.getBoundingClientRect();

expect(clickPos.x).to.equal(expectedX);
expect(clickPos.y).to.equal(expectedY);
});

it('should clickElemAt offset from elem origin', async() => {
await clickElemAt(elem, 10, 10);

const { x, y } = elem.getBoundingClientRect();
const expectedX = x + 10;
const expectedY = y + 10;

expect(clickPos.x).to.equal(expectedX);
expect(clickPos.y).to.equal(expectedY);
});

it('should hover over element', async() => {
let hovered = false;
elem.addEventListener('mouseover', () => hovered = true);
elem.addEventListener('mouseout', () => hovered = false);
hovered = false;

elem.addEventListener('mouseover', onMouseOver);
elem.addEventListener('mouseout', onMouseOut);

await hoverElem(elem);
expect(hovered).to.be.true;

elem.removeEventListener('mouseover', onMouseOver);
elem.removeEventListener('mouseout', onMouseOut);
});

it('should hover at position', async() => {
const mousePos = { x: 0, y: 0 };
function onMouseMove(e) {
mousePos.x = e.clientX;
mousePos.y = e.clientY;
}
window.addEventListener('mousemove', onMouseMove);
await hoverAt(50, 100);

expect(mousePos.x).to.equal(50);
expect(mousePos.y).to.equal(100);
window.removeEventListener('mousemove', onMouseMove);
});

it('should hoverElemAt top-left by default', async() => {
await hoverElemAt(elem);

const { x: expectedX, y: expectedY } = elem.getBoundingClientRect();

expect(mousePos.x).to.equal(expectedX);
expect(mousePos.y).to.equal(expectedY);
});

it('should hoverElemAt offset from elem origin', async() => {
await hoverElemAt(elem, 10, 10);

const { x, y } = elem.getBoundingClientRect();
const expectedX = x + 10;
const expectedY = y + 10;

expect(mousePos.x).to.equal(expectedX);
expect(mousePos.y).to.equal(expectedY);
});
});

describe('keyboard/focus', async() => {
let buttonElem, inputElem;

let elem;
beforeEach(async() => {
elem = await fixture(html`<input type="text">`);
elem = await fixture(focusTemplate).then(rootElem => {
buttonElem = rootElem.querySelector('button');
inputElem = rootElem.querySelector('input');
});
buttonElem.addEventListener('focus', onFocus);
inputElem.addEventListener('focus', onFocus);
});

afterEach(() => {
buttonElem.removeEventListener('focus', onFocus);
inputElem.removeEventListener('focus', onFocus);
});

it('should focus on element', async() => {
let focussed = false;
elem.addEventListener('focus', () => focussed = true);
await focusElem(elem);
expect(focussed).to.be.true;
await focusElem(buttonElem);
expect(focusSource).to.equal(buttonElem);

await focusElem(inputElem);
expect(focusSource).to.equal(inputElem);
});

it('should move focus via key press', async() => {
await sendKeys('press', 'Tab');
expect(focusSource).to.equal(inputElem);

await sendKeys('press', 'Tab');
expect(focusSource).to.equal(buttonElem);
});

it('should send keys to element', async() => {
await sendKeysElem(elem, 'type', 'Hello');
expect(elem.value).to.equal('Hello');
keys = [];

await sendKeysElem(inputElem, 'type', 'Hello');
expect(inputElem.value).to.equal('Hello');
expect(keys).to.include('Shift');
expect(focusSource).to.equal(inputElem);
});

it('should send keys to browser', async() => {
let key = undefined;
function onKeyDown(e) {
key = e.key;
}
window.addEventListener('keydown', onKeyDown);
await sendKeys('press', 'Escape');
expect(key).to.equal('Escape');
window.removeEventListener('keydown', onKeyDown);
});

});

describe('drag & drop', () => {

it('should drag & drop element', (done) => {
fixture(html`<div>
<div id="dest" style="height: 100px; width: 100px;"></div>
<div id="source" draggable="true" style="height: 50px; width: 50px;"></div>
</div>`).then(rootElem => {

fixture(draggableTemplate).then(rootElem => {
let dragSource;
const sourceElem = rootElem.querySelector('#source');
sourceElem.addEventListener('dragstart', e => dragSource = e.target);
Expand All @@ -114,35 +214,23 @@ describe('commands', () => {
});

describe('mouseReset', () => {

const mousePos = { x: 0, y: 0 };
function onMouseMove(e) {
mousePos.x = e.clientX;
mousePos.y = e.clientY;
}

const buttonTemplate = html`<button>text</button>`;

let elem;
beforeEach(async() => {
elem = await fixture(buttonTemplate);
window.addEventListener('mousemove', onMouseMove);
});

afterEach(() => {
window.removeEventListener('mousemove', onMouseMove);
});

[
{ command: 'clickElem', action: (elem) => clickElem(elem) },
{ command: 'clickAt', action: () => clickAt(5, 10) },
{ command: 'clickElemAt', action: (elem) => clickElemAt(elem, 10, 10) },
{ command: 'hoverElem', action: (elem) => hoverElem(elem) },
{ command: 'hoverAt', action: () => hoverAt(5, 10) },
{ command: 'hoverElemAt', action: (elem) => hoverElemAt(elem, 10, 10) },
].forEach(({ command, action }) => {
it(`should reset mouse position after ${command}`, async() => {
await action(elem);
expect(mousePos.x).to.not.equal(0);
expect(mousePos.y).to.not.equal(0);

await fixture(buttonTemplate);
expect(mousePos.x).to.equal(0);
expect(mousePos.y).to.equal(0);
Expand All @@ -153,7 +241,7 @@ describe('commands', () => {
describe('viewport', () => {

beforeEach(async() => {
await fixture(html`<div></div>`);
await fixture(emptyDivTemplate);
});

it('should set width and height', async() => {
Expand Down

0 comments on commit c2e2e00

Please sign in to comment.