// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { assert } from 'chai';
import { assertScreenshot, dispatchKeyDownEvent, renderElementIntoDOM, } from '../../testing/DOMHelpers.js';
import { setupLocaleHooks } from '../../testing/LocaleHelpers.js';
import * as Models from './models/models.js';
import { StepEditor } from './recorder.js';
import { installMocksForRecordingPlayer } from './testing/RecorderHelpers.js';
function getStepEditedPromise(editor) {
    return new Promise(resolve => {
        editor.onStepEdited = resolve;
    });
}
const triggerMicroTaskQueue = async (n = 1) => {
    while (n > 0) {
        --n;
        await new Promise(resolve => setTimeout(resolve, 0));
    }
};
describe('StepEditor', () => {
    setupLocaleHooks();
    async function renderEditor(step) {
        const editor = new StepEditor.StepEditor();
        editor.step = structuredClone(step);
        renderElementIntoDOM(editor, {});
        await triggerMicroTaskQueue();
        await editor.updateComplete;
        return editor;
    }
    function getInputByAttribute(editor, attribute) {
        const input = editor.contentElement.querySelector(`.attribute[data-attribute="${attribute}"] devtools-suggestion-input`);
        if (!input) {
            throw new Error(`${attribute} devtools-suggestion-input not found`);
        }
        return input;
    }
    function getAllInputValues(editor) {
        const result = [];
        const inputs = editor.contentElement.querySelectorAll('devtools-suggestion-input');
        for (const input of inputs) {
            result.push(input.value);
        }
        return result;
    }
    async function addOptionalField(editor, attribute) {
        const button = editor.contentElement.querySelector(`devtools-button.add-row[data-attribute="${attribute}"]`);
        assert.instanceOf(button, HTMLElement);
        button.click();
        await triggerMicroTaskQueue();
        await editor.updateComplete;
    }
    async function deleteOptionalField(editor, attribute) {
        const button = editor.contentElement.querySelector(`devtools-button.delete-row[data-attribute="${attribute}"]`);
        assert.instanceOf(button, HTMLElement);
        button.click();
        await triggerMicroTaskQueue();
        await editor.updateComplete;
    }
    async function clickFrameLevelButton(editor, className) {
        const button = editor.contentElement.querySelector(`.attribute[data-attribute="frame"] devtools-button${className}`);
        assert.instanceOf(button, HTMLElement);
        button.click();
        await editor.updateComplete;
    }
    async function clickSelectorLevelButton(editor, path, className) {
        const button = editor.contentElement.querySelector(`[data-selector-path="${path.join('.')}"] devtools-button${className}`);
        assert.instanceOf(button, HTMLElement);
        button.click();
        await editor.updateComplete;
    }
    /**
     * Extra button to be able to focus on it in tests to see how
     * the step editor reacts when the focus moves outside of it.
     */
    function createFocusOutsideButton() {
        const button = document.createElement('button');
        button.innerText = 'click';
        renderElementIntoDOM(button, { allowMultipleChildren: true });
        return {
            focus() {
                button.focus();
            },
        };
    }
    beforeEach(() => {
        installMocksForRecordingPlayer();
    });
    it('should render correctly (screenshot)', async () => {
        await renderEditor({
            type: Models.Schema.StepType.Click,
            selectors: [['.cls']],
            offsetX: 1,
            offsetY: 1,
        });
        await assertScreenshot('recorder/StepEditor_click.png');
    });
    it('should edit step type', async () => {
        const editor = await renderEditor({
            type: Models.Schema.StepType.Click,
            selectors: [['.cls']],
            offsetX: 1,
            offsetY: 1,
        });
        const step = getStepEditedPromise(editor);
        const input = getInputByAttribute(editor, 'type');
        input.focus();
        input.value = 'change';
        await input.updateComplete;
        input.dispatchEvent(new KeyboardEvent('keydown', {
            key: 'Enter',
            bubbles: true,
            composed: true,
        }));
        await editor.updateComplete;
        assert.deepEqual(await step, {
            type: Models.Schema.StepType.Change,
            selectors: ['.cls'],
            value: 'Value',
        });
        await editor.updateComplete;
        assert.deepEqual(getAllInputValues(editor), [
            'change',
            '.cls',
            'Value',
        ]);
    });
    it('should edit step type via dropdown', async () => {
        const editor = await renderEditor({ type: Models.Schema.StepType.Scroll });
        const step = getStepEditedPromise(editor);
        const input = getInputByAttribute(editor, 'type');
        input.focus();
        input.value = '';
        await input.updateComplete;
        // Use the drop down.
        input.dispatchEvent(new KeyboardEvent('keydown', {
            key: 'ArrowDown',
            bubbles: true,
            composed: true,
        }));
        input.dispatchEvent(new KeyboardEvent('keydown', {
            key: 'Enter',
            bubbles: true,
            composed: true,
        }));
        await editor.updateComplete;
        assert.deepEqual(await step, {
            type: Models.Schema.StepType.Click,
            selectors: ['.cls'],
            offsetX: 1,
            offsetY: 1,
        });
        await editor.updateComplete;
        assert.deepEqual(getAllInputValues(editor), [
            'click',
            '.cls',
            '1',
            '1',
        ]);
    });
    it('should edit other attributes', async () => {
        const editor = await renderEditor({
            type: Models.Schema.StepType.CustomStep,
            name: 'test',
            parameters: {},
        });
        const step = getStepEditedPromise(editor);
        const input = getInputByAttribute(editor, 'parameters');
        input.focus();
        input.value = '{"custom":"test"}';
        await input.updateComplete;
        input.dispatchEvent(new KeyboardEvent('keydown', {
            key: 'Enter',
            bubbles: true,
            composed: true,
        }));
        await editor.updateComplete;
        assert.deepEqual(await step, {
            type: Models.Schema.StepType.CustomStep,
            name: 'test',
            parameters: { custom: 'test' },
        });
        assert.deepEqual(getAllInputValues(editor), [
            'customStep',
            'test',
            '{"custom":"test"}',
        ]);
    });
    it('should close dropdown on Enter', async () => {
        const editor = await renderEditor({ type: Models.Schema.StepType.Scroll });
        const input = getInputByAttribute(editor, 'type');
        input.focus();
        input.value = '';
        await input.updateComplete;
        const suggestions = input.renderRoot.querySelector('devtools-suggestion-box');
        if (!suggestions) {
            throw new Error('Failed to find element');
        }
        assert.strictEqual(window.getComputedStyle(suggestions).display, 'block');
        input.dispatchEvent(new KeyboardEvent('keydown', {
            key: 'Enter',
            bubbles: true,
            composed: true,
        }));
        assert.strictEqual(window.getComputedStyle(suggestions).display, 'none');
    });
    it('should close dropdown on focus elsewhere', async () => {
        const editor = await renderEditor({ type: Models.Schema.StepType.Scroll });
        const button = createFocusOutsideButton();
        const input = getInputByAttribute(editor, 'type');
        input.focus();
        input.value = '';
        await input.updateComplete;
        const suggestions = input.renderRoot.querySelector('devtools-suggestion-box');
        if (!suggestions) {
            throw new Error('Failed to find element');
        }
        assert.strictEqual(window.getComputedStyle(suggestions).display, 'block');
        button.focus();
        assert.strictEqual(window.getComputedStyle(suggestions).display, 'none');
    });
    it('should add optional fields', async () => {
        const editor = await renderEditor({ type: Models.Schema.StepType.Scroll });
        const step = getStepEditedPromise(editor);
        await addOptionalField(editor, 'x');
        assert.deepEqual(await step, {
            type: Models.Schema.StepType.Scroll,
            x: 0,
        });
        assert.deepEqual(getAllInputValues(editor), ['scroll', '0']);
    });
    it('should add the duration field', async () => {
        const editor = await renderEditor({
            type: Models.Schema.StepType.Click,
            offsetX: 1,
            offsetY: 1,
            selectors: ['.cls'],
        });
        const step = getStepEditedPromise(editor);
        await addOptionalField(editor, 'duration');
        assert.deepEqual(await step, {
            type: Models.Schema.StepType.Click,
            offsetX: 1,
            offsetY: 1,
            selectors: ['.cls'],
            duration: 50,
        });
        assert.deepEqual(getAllInputValues(editor), [
            'click',
            '.cls',
            '1',
            '1',
            '50',
        ]);
    });
    it('should add the parameters field', async () => {
        const editor = await renderEditor({
            type: Models.Schema.StepType.WaitForElement,
            selectors: ['.cls'],
        });
        const step = getStepEditedPromise(editor);
        await addOptionalField(editor, 'properties');
        assert.deepEqual(await step, {
            type: Models.Schema.StepType.WaitForElement,
            selectors: ['.cls'],
            properties: {},
        });
        assert.deepEqual(getAllInputValues(editor), [
            'waitForElement',
            '.cls',
            '{}',
        ]);
    });
    it('should edit timeout fields', async () => {
        const editor = await renderEditor({
            type: Models.Schema.StepType.Navigate,
            url: 'https://example.com',
        });
        const step = getStepEditedPromise(editor);
        await addOptionalField(editor, 'timeout');
        assert.deepEqual(await step, {
            type: Models.Schema.StepType.Navigate,
            url: 'https://example.com',
            timeout: 5000,
        });
        assert.deepEqual(getAllInputValues(editor), [
            'navigate',
            'https://example.com',
            '5000',
        ]);
    });
    it('should delete optional fields', async () => {
        const editor = await renderEditor({
            type: Models.Schema.StepType.Scroll,
            x: 1,
        });
        const step = getStepEditedPromise(editor);
        await deleteOptionalField(editor, 'x');
        assert.deepEqual(await step, { type: Models.Schema.StepType.Scroll });
        assert.deepEqual(getAllInputValues(editor), ['scroll']);
    });
    it('should add/remove frames', async () => {
        const editor = await renderEditor({
            type: Models.Schema.StepType.Scroll,
            frame: [0],
        });
        {
            const step = getStepEditedPromise(editor);
            await clickFrameLevelButton(editor, '.add-frame');
            assert.deepEqual(await step, {
                type: Models.Schema.StepType.Scroll,
                frame: [0, 0],
            });
            assert.deepEqual(getAllInputValues(editor), ['scroll', '0', '0']);
            assert.isTrue(editor.element.shadowRoot?.activeElement?.matches('devtools-suggestion-input[data-path="frame.1"]'));
        }
        {
            const step = getStepEditedPromise(editor);
            await clickFrameLevelButton(editor, '.remove-frame');
            assert.deepEqual(await step, {
                type: Models.Schema.StepType.Scroll,
                frame: [0],
            });
            assert.deepEqual(getAllInputValues(editor), ['scroll', '0']);
            assert.isTrue(editor.element.shadowRoot?.activeElement?.matches('devtools-suggestion-input[data-path="frame.0"]'));
        }
    });
    it('should add/remove selector parts', async () => {
        const editor = await renderEditor({
            type: Models.Schema.StepType.Scroll,
            selectors: [['.part1']],
        });
        {
            const step = getStepEditedPromise(editor);
            await clickSelectorLevelButton(editor, [0, 0], '.add-selector-part');
            assert.deepEqual(await step, {
                type: Models.Schema.StepType.Scroll,
                selectors: [['.part1', '.cls']],
            });
            assert.deepEqual(getAllInputValues(editor), [
                'scroll',
                '.part1',
                '.cls',
            ]);
            assert.isTrue(editor.element.shadowRoot?.activeElement?.matches('devtools-suggestion-input[data-path="selectors.0.1"]'));
        }
        {
            const step = getStepEditedPromise(editor);
            await clickSelectorLevelButton(editor, [0, 0], '.remove-selector-part');
            assert.deepEqual(await step, {
                type: Models.Schema.StepType.Scroll,
                selectors: ['.cls'],
            });
            assert.deepEqual(getAllInputValues(editor), ['scroll', '.cls']);
            assert.isTrue(editor.element.shadowRoot?.activeElement?.matches('devtools-suggestion-input[data-path="selectors.0.0"]'));
        }
    });
    it('should add/remove selectors', async () => {
        const editor = await renderEditor({
            type: Models.Schema.StepType.Scroll,
            selectors: [['.part1']],
        });
        {
            const step = getStepEditedPromise(editor);
            await clickSelectorLevelButton(editor, [0], '.add-selector');
            assert.deepEqual(await step, {
                type: Models.Schema.StepType.Scroll,
                selectors: ['.part1', '.cls'],
            });
            assert.deepEqual(getAllInputValues(editor), [
                'scroll',
                '.part1',
                '.cls',
            ]);
            assert.isTrue(editor.element.shadowRoot?.activeElement?.matches('devtools-suggestion-input[data-path="selectors.1.0"]'));
        }
        {
            const step = getStepEditedPromise(editor);
            await clickSelectorLevelButton(editor, [1], '.remove-selector');
            assert.deepEqual(await step, {
                type: Models.Schema.StepType.Scroll,
                selectors: ['.part1'],
            });
            assert.deepEqual(getAllInputValues(editor), ['scroll', '.part1']);
            assert.isTrue(editor.element.shadowRoot?.activeElement?.matches('devtools-suggestion-input[data-path="selectors.0.0"]'));
        }
    });
    it('should become readonly if disabled', async () => {
        const editor = await renderEditor({
            type: Models.Schema.StepType.Scroll,
            selectors: [['.part1']],
        });
        editor.disabled = true;
        await editor.updateComplete;
        for (const input of editor.contentElement.querySelectorAll('devtools-suggestion-input')) {
            assert.isTrue(input.disabled);
        }
    });
    it('clears text selection when navigating away from devtools-suggestion-input', async () => {
        const editor = await renderEditor({ type: Models.Schema.StepType.Scroll });
        // Clicking on the type devtools-suggestion-input should select the entire text in the field.
        const input = getInputByAttribute(editor, 'type');
        input.focus();
        input.click();
        assert.strictEqual(window.getSelection()?.toString(), 'scroll');
        // Navigating away should remove the selection.
        dispatchKeyDownEvent(input, {
            key: 'Enter',
            bubbles: true,
            composed: true,
        });
        assert.strictEqual(window.getSelection()?.toString(), '');
    });
    it('should add an attribute after another\'s deletion', async () => {
        const editor = await renderEditor({
            type: Models.Schema.StepType.WaitForElement,
            selectors: [['.cls']],
        });
        await addOptionalField(editor, 'operator');
        await deleteOptionalField(editor, 'operator');
        const step = getStepEditedPromise(editor);
        await addOptionalField(editor, 'count');
        assert.deepEqual(await step, {
            type: Models.Schema.StepType.WaitForElement,
            selectors: ['.cls'],
            count: 1,
        });
        assert.deepEqual(getAllInputValues(editor), [
            'waitForElement',
            '.cls',
            '1',
        ]);
    });
    it('should edit asserted events', async () => {
        const editor = await renderEditor({
            type: Models.Schema.StepType.Navigate,
            url: 'www.example.com',
            assertedEvents: [{
                    type: 'navigation',
                    title: 'Test',
                    url: 'www.example.com',
                }],
        });
        const step = getStepEditedPromise(editor);
        const input = getInputByAttribute(editor, 'assertedEvents');
        input.focus();
        input.value = 'None';
        await input.updateComplete;
        input.dispatchEvent(new KeyboardEvent('keydown', {
            key: 'Enter',
            bubbles: true,
            composed: true,
        }));
        await editor.updateComplete;
        assert.deepEqual(await step, {
            type: Models.Schema.StepType.Navigate,
            url: 'www.example.com',
            assertedEvents: [{
                    type: 'navigation',
                    title: 'None',
                    url: 'www.example.com',
                }],
        });
    });
    it('should add/remove attribute assertion', async () => {
        const editor = await renderEditor({
            type: Models.Schema.StepType.WaitForElement,
            selectors: ['.part1'],
            attributes: {
                a: 'b',
            },
        });
        {
            const step = getStepEditedPromise(editor);
            editor.contentElement.querySelectorAll('.add-attribute-assertion')[0]?.click();
            assert.deepEqual(await step, {
                type: Models.Schema.StepType.WaitForElement,
                selectors: ['.part1'],
                attributes: { a: 'b', attribute: 'value' },
            });
            await editor.updateComplete;
            assert.deepEqual(getAllInputValues(editor), [
                'waitForElement',
                '.part1',
                'a',
                'b',
                'attribute',
                'value',
            ]);
        }
        {
            const step = getStepEditedPromise(editor);
            editor.contentElement.querySelectorAll('.remove-attribute-assertion')[1]?.click();
            assert.deepEqual(await step, {
                type: Models.Schema.StepType.WaitForElement,
                selectors: ['.part1'],
                attributes: { a: 'b' },
            });
            await editor.updateComplete;
            assert.deepEqual(getAllInputValues(editor), [
                'waitForElement',
                '.part1',
                'a',
                'b',
            ]);
        }
    });
    it('should edit attribute assertion', async () => {
        const editor = await renderEditor({
            type: Models.Schema.StepType.WaitForElement,
            selectors: ['.part1'],
            attributes: {
                a: 'b',
            },
        });
        const step = getStepEditedPromise(editor);
        const input = getInputByAttribute(editor, 'attributes');
        input.focus();
        input.value = 'innerText';
        await input.updateComplete;
        input.dispatchEvent(new KeyboardEvent('keydown', {
            key: 'Enter',
            bubbles: true,
            composed: true,
        }));
        await editor.updateComplete;
        assert.deepEqual(await step, {
            type: Models.Schema.StepType.WaitForElement,
            selectors: ['.part1'],
            attributes: {
                innerText: 'b',
            },
        });
    });
});
//# sourceMappingURL=StepEditor.test.js.map