<!DOCTYPE html>
<meta charset="utf-8">
<title>Input Event typing tests</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>
<script src="/resources/testdriver-actions.js"></script>
<div id="rich" contenteditable></div>
<textarea id="plain"></textarea>
<script>
let inputEventsLog = [];
const rich = document.getElementById('rich');
const plain = document.getElementById('plain');

function log(event) {
    const clone = new event.constructor(event.type, event);
    clone.state = rich.innerHTML;
    inputEventsLog.push(clone);
}

function resetRich() {
    inputEventsLog = [];
    rich.innerHTML = '';
}

rich.addEventListener('beforeinput', log);
rich.addEventListener('input', log);

promise_test(async function() {
    this.add_cleanup(resetRich);
    rich.focus();
    const message = 'Hello';
    await test_driver.send_keys(rich, message);
    // 10 events (5 beforeinput + 5 input events)
    assert_equals(inputEventsLog.length, 10);
    for (let i = 0; i < inputEventsLog.length; i += 2) {
        const beforeInputEvent = inputEventsLog[i];
        const inputEvent = inputEventsLog[i + 1];
        assert_equals(beforeInputEvent.type, 'beforeinput');
        assert_equals(inputEvent.type, 'input');
        assert_equals(beforeInputEvent.inputType, 'insertText');
        assert_equals(inputEvent.inputType, 'insertText');
        assert_equals(beforeInputEvent.data, inputEvent.data);
        assert_equals(inputEvent.data, message[i / 2]);
        assert_equals(beforeInputEvent.state + message[i / 2], inputEvent.state);
    }
}, 'It triggers beforeinput and input events on text typing');

promise_test(async function() {
    this.add_cleanup(resetRich);
    rich.focus();
    await test_driver.send_keys(rich, "\uE006"); // Return

    assert_equals(inputEventsLog.length, 2);
    const beforeInputEvent = inputEventsLog[0];
    const inputEvent = inputEventsLog[1];
    assert_equals(beforeInputEvent.type, 'beforeinput');
    assert_equals(inputEvent.type, 'input');
    assert_equals(beforeInputEvent.inputType, 'insertParagraph');
    assert_equals(inputEvent.inputType, 'insertParagraph');
    assert_equals(beforeInputEvent.data, inputEvent.data);
}, 'It triggers beforeinput and input events on typing RETURN');

promise_test(async function() {
    this.add_cleanup(resetRich);
    rich.focus();
    await new test_driver.Actions()
        .keyDown('\uE008') // Shift
        .keyDown('\uE006') // Return
        .keyUp('\uE006')
        .keyUp('\uE008')
        .send();

    assert_equals(inputEventsLog.length, 2);
    const [beforeInputEvent, inputEvent] = inputEventsLog;
    assert_equals(beforeInputEvent.type, 'beforeinput');
    assert_equals(inputEvent.type, 'input');
    assert_equals(beforeInputEvent.inputType, 'insertLineBreak');
    assert_equals(inputEvent.inputType, 'insertLineBreak');
    assert_equals(beforeInputEvent.data, inputEvent.data);
}, 'It triggers beforeinput and input events on typing Shift+RETURN');

promise_test(async function() {
    this.add_cleanup(resetRich);
    rich.innerHTML = '<p>Preexisting <i id="caret">c</i>ontent</p>';
    const caret = document.querySelector('#caret');
    await test_driver.click(caret);
    await test_driver.send_keys(caret, "\uE017"); // Delete

    assert_equals(inputEventsLog.length, 2);
    const [beforeInputEvent, inputEvent] = inputEventsLog;
    assert_equals(beforeInputEvent.type, 'beforeinput');
    assert_equals(inputEvent.type, 'input');
    assert_equals(beforeInputEvent.inputType, 'deleteContentForward');
    assert_equals(inputEvent.inputType, 'deleteContentForward');
    assert_equals(beforeInputEvent.data, inputEvent.data);
}, 'It triggers beforeinput and input events on typing DELETE with pre-existing content');

promise_test(async function() {
    this.add_cleanup(resetRich);
    rich.focus();
    await test_driver.send_keys(rich, "\uE017"); // Delete
    assert_equals(inputEventsLog.length, 2);
    const [beforeInputEvent, inputEvent] = inputEventsLog;
    assert_equals(beforeInputEvent.type, 'beforeinput');
    assert_equals(inputEvent.type, 'input');
    assert_equals(beforeInputEvent.inputType, 'deleteContentForward');
    assert_equals(inputEvent.inputType, 'deleteContentForward');
    assert_equals(beforeInputEvent.data, inputEvent.data);
}, 'It triggers beforeinput and input events on typing DELETE with no pre-existing content');

promise_test(async function() {
    this.add_cleanup(resetRich);
    rich.innerHTML = '<p>Preexisting <i id="caret">c</i>ontent</p>';

    await test_driver.click(document.querySelector('#caret'));
    await test_driver.send_keys(rich, "\uE003"); // Back Space

    assert_equals(inputEventsLog.length, 2);
    const [beforeInputEvent, inputEvent] = inputEventsLog;
    assert_equals(beforeInputEvent.type, 'beforeinput');
    assert_equals(inputEvent.type, 'input');
    assert_equals(beforeInputEvent.inputType, 'deleteContentBackward');
    assert_equals(inputEvent.inputType, 'deleteContentBackward');
    assert_equals(beforeInputEvent.data, inputEvent.data);
}, 'It triggers beforeinput and input events on typing BACK_SPACE with pre-existing content');

promise_test(async function() {
    this.add_cleanup(resetRich);
    rich.focus();
    await test_driver.send_keys(rich, "\uE003"); // Back Space

    assert_equals(inputEventsLog.length, 2);
    const [beforeInputEvent, inputEvent] = inputEventsLog;
    assert_equals(beforeInputEvent.type, 'beforeinput');
    assert_equals(inputEvent.type, 'input');
    assert_equals(beforeInputEvent.inputType, 'deleteContentBackward');
    assert_equals(inputEvent.inputType, 'deleteContentBackward');
    assert_equals(beforeInputEvent.data, inputEvent.data);
}, 'It triggers beforeinput and input events on typing BACK_SPACE with no pre-existing content');

promise_test(async function() {
    this.add_cleanup(resetRich);
    rich.focus();
    await test_driver.send_keys(rich, "hello");

    // Decide whether to use  Key.COMMAND (mac) or Key.CONTROL (everything else)
    const modifierKey = navigator.platform === "MacIntel" ? '\u2318' : '\uE009';

    // Undo
    await new test_driver.Actions()
        .keyDown(modifierKey)
        .keyDown('z')
        .keyUp('z')
        .keyUp(modifierKey)
        .send();
    // Redo
    await new test_driver.Actions()
        .keyDown(modifierKey)
        .keyDown('\uE008') // Shift
        .keyDown('z')
        .keyUp('z')
        .keyUp('\uE008')
        .keyUp(modifierKey)
        .send();

    // Ignore the initial typing of 'hello'
    const historyInputEventsLog = inputEventsLog.slice(10);

    assert_equals(historyInputEventsLog.length, 4);
    const inputTypes = ['historyUndo', 'historyRedo'];
    for (let i = 0; i < historyInputEventsLog.length; i += 2) {
        // We are increaisng i by 2 as there should always be matching beforeinput and input events.
        const beforeInputEvent = historyInputEventsLog[i];
        const inputEvent = historyInputEventsLog[i + 1];
        assert_equals(beforeInputEvent.type, 'beforeinput');
        assert_equals(inputEvent.type, 'input');
        assert_equals(beforeInputEvent.inputType, inputTypes[i / 2]);
        assert_equals(inputEvent.inputType, inputTypes[i / 2]);
        assert_equals(beforeInputEvent.data, inputEvent.data);
    }
}, 'It triggers beforeinput and input events on typing Undo and Redo key combinations with an existing history');

promise_test(async function() {
    this.add_cleanup(resetRich);
    rich.focus();
    // Decide whether to use  Key.COMMAND (mac) or Key.CONTROL (everything else)
    const modifierKey = navigator.platform === "MacIntel" ? '\u2318' : '\uE009';

    // Undo
    await new test_driver.Actions()
        .keyDown(modifierKey)
        .keyDown('z')
        .keyUp('z')
        .keyUp(modifierKey)
        .send();
    // Redo
    await new test_driver.Actions()
        .keyDown(modifierKey)
        .keyDown('\uE008') // Shift
        .keyDown('z')
        .keyUp('z')
        .keyUp('\uE008')
        .keyUp(modifierKey)
        .send();

    assert_equals(inputEventsLog.length, 4);
    const inputTypes = ['historyUndo', 'historyRedo'];
    for (let i = 0; i < inputEventsLog.length; i += 2) {
        const beforeInputEvent = inputEventsLog[i];
        const inputEvent = inputEventsLog[i + 1];
        assert_equals(beforeInputEvent.type, 'beforeinput');
        assert_equals(inputEvent.type, 'input');
        assert_equals(beforeInputEvent.inputType, inputTypes[i / 2]);
        assert_equals(inputEvent.inputType, inputTypes[i / 2]);
        assert_equals(beforeInputEvent.data, inputEvent.data);
    }
}, 'It triggers beforeinput and input events on typing Undo and Redo key combinations without an existing history');

promise_test(async function() {
    this.add_cleanup(resetRich);
    const expectedResult = [
        // Pressing 'a'.
        'plain-keydown-a',
        'plain-keypress-a',
        'plain-beforeinput-a-null',
        'plain-input-a-null',
        'plain-keyup-a',
        // Pressing Shift-'b'.
        'plain-keydown-B',
        'plain-keypress-B',
        'plain-beforeinput-B-null',
        'plain-input-B-null',
        'plain-keyup-B',
        // Pressing 'c'.
        'rich-keydown-c',
        'rich-keypress-c',
        'rich-beforeinput-c-null',
        'rich-input-c-null',
        'rich-keyup-c',
        // Pressing Shift-'d'.
        'rich-keydown-D',
        'rich-keypress-D',
        'rich-beforeinput-D-null',
        'rich-input-D-null',
        'rich-keyup-D',
    ];
    const result = [];

    for (const eventType of ['beforeinput', 'input', 'keydown', 'keypress', 'keyup']) {
        const listener = event => {
            if (event.key === 'Shift') return;
            const eventInfo = [event.target.id, event.type, event.data || event.key];
            if (event instanceof InputEvent) eventInfo.push(String(event.dataTransfer));
            result.push(eventInfo.join('-'));
        }
        rich.addEventListener(eventType, listener);
        plain.addEventListener(eventType, listener);
    }

    plain.focus();
    await new test_driver.Actions()
        .keyDown('a')
        .keyUp('a')
        .keyDown('\uE008') // Shift
        .keyDown('b')
        .keyUp('b')
        .keyUp('\uE008')
        .send();

    rich.focus();
    await new test_driver.Actions()
        .keyDown('c')
        .keyUp('c')
        .keyDown('\uE008') // Shift
        .keyDown('d')
        .keyUp('d')
        .keyUp('\uE008')
        .send();

    assert_equals(expectedResult.length, result.length);
    expectedResult.forEach((er, index) => assert_equals(er, result[index]));
}, 'InputEvents have correct data/order when typing on textarea and contenteditable');
</script>
