Skip to content

Commit 0c84248

Browse files
committed
Introduce waitForFocus DOM Helper
Convinient helper that resolves when a target receives focus. Useful for verifying keyboard navigation handling and default focus. Uses the pull based waitUntil helper to support the element not being in the DOM when invoking the helper. Alternatives without this helper is asserting `document.activeElement` is the target. It usually work, but there are cases it focus may happne async. The element isn't in view yet.
1 parent a74b4d4 commit 0c84248

3 files changed

Lines changed: 199 additions & 0 deletions

File tree

addon/src/dom/wait-for-focus.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import waitUntil from '../wait-until.ts';
2+
import getElement from './-get-element.ts';
3+
import {
4+
type IDOMElementDescriptor,
5+
lookupDescriptorData,
6+
} from 'dom-element-descriptors';
7+
import getDescription from './-get-description.ts';
8+
9+
export interface Options {
10+
timeout?: number;
11+
count?: number | null;
12+
timeoutMessage?: string;
13+
}
14+
15+
/**
16+
Used to wait for a particular selector to receive focus.
17+
18+
@param {string|IDOMElementDescriptor} target the selector or DOM element descriptor to wait for
19+
@param {Object} [options] the options to be used
20+
@param {number} [options.timeout=1000] the time to wait (in ms) for a match
21+
@param {string} [options.timeoutMessage='waitForFocus timed out waiting for selector'] the message to use in the reject on timeout
22+
@return {Promise<Element>} resolves when the element received focus
23+
24+
@example
25+
<caption>
26+
Waiting until a selector is rendered:
27+
</caption>
28+
await waitFor('.my-selector', { timeout: 2000 })
29+
*/
30+
export default function waitForFocus(
31+
target: string | IDOMElementDescriptor,
32+
options: Options = {},
33+
): Promise<Element | Element[]> {
34+
return Promise.resolve().then(() => {
35+
if (typeof target !== 'string' && !lookupDescriptorData(target)) {
36+
throw new Error(
37+
'Must pass a selector or DOM element descriptor to `waitFor`.',
38+
);
39+
}
40+
41+
const { timeout = 1000 } = options;
42+
let { timeoutMessage } = options;
43+
44+
if (!timeoutMessage) {
45+
const description = getDescription(target);
46+
timeoutMessage = `waitForFocus timed out waiting for selector "${description}"`;
47+
}
48+
49+
return waitUntil(
50+
() => {
51+
const element = getElement(target);
52+
if (element && element === document.activeElement) {
53+
return document.activeElement as HTMLElement;
54+
}
55+
},
56+
{ timeout, timeoutMessage },
57+
);
58+
});
59+
}

addon/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ export { default as find } from './dom/find.ts';
7474
export { default as findAll } from './dom/find-all.ts';
7575
export { default as typeIn } from './dom/type-in.ts';
7676
export { default as scrollTo } from './dom/scroll-to.ts';
77+
export { default as waitForFocus } from './dom/wait-for-focus.ts';
78+
7779
export type { Target } from './dom/-target.ts';
7880

7981
// Declaration-merge for our internal purposes.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { module, test } from 'qunit';
2+
import {
3+
waitForFocus,
4+
setupContext,
5+
teardownContext,
6+
find,
7+
} from '@ember/test-helpers';
8+
import hasEmberVersion from '@ember/test-helpers/has-ember-version';
9+
import { registerDescriptorData } from 'dom-element-descriptors';
10+
11+
module('DOM Helper: waitForFocus', function (hooks) {
12+
if (!hasEmberVersion(2, 4)) {
13+
return;
14+
}
15+
16+
let context, rootElement;
17+
18+
hooks.beforeEach(function () {
19+
context = {};
20+
rootElement = document.getElementById('ember-testing');
21+
});
22+
23+
hooks.afterEach(async function () {
24+
// only teardown if setupContext was called
25+
if (context.owner) {
26+
await teardownContext(context);
27+
}
28+
document.getElementById('ember-testing').innerHTML = '';
29+
});
30+
31+
class SelectorData {
32+
constructor(selector) {
33+
this.selector = selector;
34+
}
35+
36+
get elements() {
37+
return rootElement.querySelectorAll(this.selector);
38+
}
39+
}
40+
41+
class SelectorDescriptor {
42+
constructor(selector) {
43+
registerDescriptorData(this, new SelectorData(selector));
44+
}
45+
}
46+
47+
test('wait for selector without context set', async function (assert) {
48+
assert.rejects(
49+
waitForFocus('.something'),
50+
/Must setup rendering context before attempting to interact with elements/
51+
);
52+
});
53+
54+
test('wait for focus using descriptor without context set', async function (assert) {
55+
assert.rejects(
56+
waitForFocus('.something'),
57+
/Must setup rendering context before attempting to interact with elements/
58+
);
59+
});
60+
61+
test('wait for focus using descriptor', async function (assert) {
62+
rootElement.innerHTML = `<input class="something">`;
63+
await setupContext(context);
64+
65+
let waitPromise = waitForFocus(new SelectorDescriptor('.something'));
66+
67+
setTimeout(() => {
68+
find('.something').focus();
69+
}, 10);
70+
71+
let element = await waitPromise;
72+
73+
assert.ok(element, 'returns element');
74+
assert.equal(element, find('.something'));
75+
});
76+
77+
test('resolves when the element is already focused', async function (assert) {
78+
rootElement.innerHTML = `<input class="something">`;
79+
await setupContext(context);
80+
81+
find('.something').focus();
82+
83+
let waitPromise = waitForFocus('.something');
84+
let element = await waitPromise;
85+
86+
assert.ok(element, 'returns element');
87+
assert.equal(element, find('.something'));
88+
});
89+
90+
test('wait for focus using selector', async function (assert) {
91+
rootElement.innerHTML = `<input class="something">`;
92+
93+
await setupContext(context);
94+
95+
let waitPromise = waitForFocus('.something');
96+
97+
setTimeout(() => {
98+
find('.something').focus();
99+
}, 10);
100+
101+
let element = await waitPromise;
102+
103+
assert.ok(element, 'returns element');
104+
assert.equal(element, find('.something'));
105+
});
106+
107+
test('wait for selector with timeout', async function (assert) {
108+
assert.expect(2);
109+
110+
await setupContext(context);
111+
112+
let start = Date.now();
113+
try {
114+
await waitForFocus('.something', { timeout: 100 });
115+
} catch (error) {
116+
let end = Date.now();
117+
assert.ok(end - start >= 100, 'timed out after correct time');
118+
assert.equal(
119+
error.message,
120+
'waitForFocus timed out waiting for selector ".something"'
121+
);
122+
}
123+
});
124+
125+
test('wait for selector with timeoutMessage', async function (assert) {
126+
assert.expect(1);
127+
128+
await setupContext(context);
129+
130+
try {
131+
await waitForFocus('.something', {
132+
timeoutMessage: '.something timed out',
133+
});
134+
} catch (error) {
135+
assert.equal(error.message, '.something timed out');
136+
}
137+
});
138+
});

0 commit comments

Comments
 (0)