Source code
Revision control
Copy as Markdown
Other Tools
Test Info:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Test that compositionend is fired when EditContext is deactivated</title>
<script src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
</head>
<body>
<div id="hostParent"><div id="host"></div></div>
<script>
'use strict';
// According to the spec, compositionend should always be fired asynchronously.
// However, Chromium fires it synchronously in many cases, and it probably
// makes more sense that way:
const host = document.getElementById('host');
const hostParent = document.getElementById('hostParent');
const editContext = new EditContext();
function compositionUpdate(newString) {
synthesizeCompositionChange({
composition: {
string: newString,
clauses: [
{length: newString.length, attr: COMPOSITION_ATTR_RAW_CLAUSE },
]
},
caret: {
start: newString.length,
length: 0,
},
key: {
key: 'a',
}
});
}
async function checkCompositionEnd(func, expectToFire, description) {
let fired = false;
const {promise, resolve} = Promise.withResolvers();
const element = document.activeElement;
const target = element.editContext || element;
target.addEventListener('compositionend', () => {
if (target instanceof EditContext) {
// Even if func() changes element's editContext,
// compositionend should be fired (step 4.1) before setting
// the editContext to the new value (step 7).
is(element.editContext, target,
"element.editContext should be the original EditContext when compositionend is fired.");
}
fired = true;
resolve();
}, {once: true});
compositionUpdate('a');
await func();
if (expectToFire === 'sync') {
ok(fired, `${description}: Should fire compositionend synchronously.`);
} else if (expectToFire === 'async') {
ok(!fired, `${description}: Should not fire compositionend synchronously.`);
await promise;
ok(fired, `${description}: Should fire compositionend asynchronously.`);
} else if (expectToFire === false) {
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
ok(!fired, `${description}: Should not fire compositionend.`);
} else {
ok(false, 'invalid value of expectToFire');
}
}
async function setupEditContext() {
await SimpleTest.promiseFocus();
host.removeAttribute('contenteditable');
hostParent.removeAttribute('contenteditable');
host.editContext = editContext;
hostParent.editContext = null;
hostParent.append(host);
document.body.append(hostParent);
host.focus();
getSelection().collapse(host);
}
async function setupContentEditable() {
await SimpleTest.promiseFocus();
host.setAttribute('contenteditable', '');
hostParent.removeAttribute('contenteditable');
host.editContext = null;
hostParent.editContext = null;
hostParent.append(host);
document.body.append(hostParent);
host.focus();
getSelection().collapse(host);
}
add_task(async () => {
await setupEditContext();
await checkCompositionEnd(() => host.blur(), 'sync',
'EditContext associated element is blurred');
});
add_task(async () => {
await setupEditContext();
await checkCompositionEnd(() => host.remove(), 'sync',
'EditContext associated element is removed');
});
add_task(async () => {
await setupEditContext();
await checkCompositionEnd(() => hostParent.remove(), 'sync',
'Parent of EditContext associated element is removed');
});
add_task(async () => {
await setupEditContext();
// editContext "wins over" contenteditable attribute, so this
// doesn't deactivate the EditContext...
await checkCompositionEnd(() => host.setAttribute('contenteditable', ''), false,
'EditContext associated element gets contenteditable attribute');
});
add_task(async () => {
await setupEditContext();
// ...however, if an ancestor has contenteditable, that has precedence
// over the descendant's editContext.
await checkCompositionEnd(() => hostParent.setAttribute('contenteditable', ''), 'async',
'Parent of EditContext associated element gets contenteditable attribute');
});
add_task(async () => {
await setupEditContext();
await checkCompositionEnd(() => host.editContext = null, 'sync',
'EditContext is detached from associated element');
});
add_task(async () => {
await setupEditContext();
await checkCompositionEnd(() => host.editContext = new EditContext(), 'sync',
'New EditContext is attached to associated element');
});
add_task(async () => {
await setupEditContext();
await checkCompositionEnd(() => hostParent.editContext = new EditContext(), 'sync',
'New EditContext is attached to parent of associated element');
});
add_task(async () => {
await setupContentEditable();
await checkCompositionEnd(() => host.editContext = new EditContext(), 'sync',
'EditContext is attached to contenteditable element.');
});
add_task(async () => {
await setupContentEditable();
await checkCompositionEnd(() => hostParent.editContext = new EditContext(), 'sync',
'EditContext is attached to parent of contenteditable element.');
});
</script>
</body>
</html>