chrome-devtools-frontend
Version:
Chrome DevTools UI
712 lines (617 loc) • 19.3 kB
text/typescript
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {
dispatchKeyDownEvent,
getEventPromise,
renderElementIntoDOM,
} from '../../../testing/DOMHelpers.js';
// eslint-disable-next-line rulesdir/es-modules-import
import * as EnvironmentHelpers from '../../../testing/EnvironmentHelpers.js';
import type * as SuggestionInput from '../../../ui/components/suggestion_input/suggestion_input.js';
import * as Models from '../models/models.js';
// eslint-disable-next-line rulesdir/es-modules-import
import * as RecorderHelpers from '../testing/RecorderHelpers.js';
import type * as Components from './components.js';
const {describeWithLocale} = EnvironmentHelpers;
function getStepEditedPromise(editor: Components.StepEditor.StepEditor) {
return getEventPromise<Components.StepEditor.StepEditedEvent>(
editor,
'stepedited',
)
.then(({data}) => data);
}
const triggerMicroTaskQueue = async (n = 1) => {
while (n > 0) {
--n;
await new Promise(resolve => setTimeout(resolve, 0));
}
};
describeWithLocale('StepEditor', () => {
async function renderEditor(
step: Models.Schema.Step,
): Promise<Components.StepEditor.StepEditor> {
const editor = document.createElement('devtools-recorder-step-editor');
editor.step = structuredClone(step) as typeof editor.step;
renderElementIntoDOM(editor, {});
await editor.updateComplete;
return editor;
}
function getInputByAttribute(
editor: Components.StepEditor.StepEditor,
attribute: string,
): SuggestionInput.SuggestionInput.SuggestionInput {
const input = editor.renderRoot.querySelector(
`.attribute[data-attribute="${attribute}"] devtools-suggestion-input`,
);
if (!input) {
throw new Error(`${attribute} devtools-suggestion-input not found`);
}
return input as SuggestionInput.SuggestionInput.SuggestionInput;
}
function getAllInputValues(
editor: Components.StepEditor.StepEditor,
): string[] {
const result = [];
const inputs = editor.renderRoot.querySelectorAll(
'devtools-suggestion-input',
);
for (const input of inputs) {
result.push(input.value);
}
return result;
}
async function addOptionalField(
editor: Components.StepEditor.StepEditor,
attribute: string,
): Promise<void> {
const button = editor.renderRoot.querySelector(
`devtools-button.add-row[data-attribute="${attribute}"]`,
);
assert.instanceOf(button, HTMLElement);
button.click();
await triggerMicroTaskQueue();
await editor.updateComplete;
}
async function deleteOptionalField(
editor: Components.StepEditor.StepEditor,
attribute: string,
): Promise<void> {
const button = editor.renderRoot.querySelector(
`devtools-button.delete-row[data-attribute="${attribute}"]`,
);
assert.instanceOf(button, HTMLElement);
button.click();
await triggerMicroTaskQueue();
await editor.updateComplete;
}
async function clickFrameLevelButton(
editor: Components.StepEditor.StepEditor,
className: string,
): Promise<void> {
const button = editor.renderRoot.querySelector(
`.attribute[data-attribute="frame"] devtools-button${className}`,
);
assert.instanceOf(button, HTMLElement);
button.click();
await editor.updateComplete;
}
async function clickSelectorLevelButton(
editor: Components.StepEditor.StepEditor,
path: number[],
className: string,
): Promise<void> {
const button = editor.renderRoot.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(() => {
RecorderHelpers.installMocksForRecordingPlayer();
});
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',
});
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,
});
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.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.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.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.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.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.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.renderRoot.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' as Models.Schema.AssertedEventType,
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' as Models.Schema.AssertedEventType,
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.renderRoot.querySelectorAll<HTMLElement>('.add-attribute-assertion')[0]?.click();
assert.deepEqual(await step, {
type: Models.Schema.StepType.WaitForElement,
selectors: ['.part1'],
attributes: {a: 'b', attribute: 'value'},
});
assert.deepEqual(getAllInputValues(editor), [
'waitForElement',
'.part1',
'a',
'b',
'attribute',
'value',
]);
}
{
const step = getStepEditedPromise(editor);
editor.renderRoot.querySelectorAll<HTMLElement>('.remove-attribute-assertion')[1]?.click();
assert.deepEqual(await step, {
type: Models.Schema.StepType.WaitForElement,
selectors: ['.part1'],
attributes: {a: 'b'},
});
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',
},
});
});
});