blob: a8e229193627b0dc6c2541d4bb1ec02afd723857 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {renderElementIntoDOM} from '../../testing/DOMHelpers.js';
import {setupLocaleHooks} from '../../testing/LocaleHelpers.js';
import * as Lit from '../lit/lit.js';
import * as UI from './legacy.js';
const {html} = Lit;
describe('TextPromptElement', () => {
setupLocaleHooks();
let container: HTMLDivElement;
beforeEach(() => {
container = document.createElement('div');
renderElementIntoDOM(container);
});
function renderPrompt(template: Lit.LitTemplate): UI.TextPrompt.TextPromptElement {
Lit.render(template, container);
const element = container.querySelector('devtools-prompt');
assert.exists(element);
return element;
}
it('renders its contents', async () => {
const prompt = renderPrompt(html`<devtools-prompt>Initial content</devtools-prompt>`);
assert.strictEqual(prompt.innerText, 'Initial content');
});
it('turns into a text input when starting to edit', async () => {
const prompt = renderPrompt(html`<devtools-prompt>Initial content</devtools-prompt>`);
prompt.setAttribute('editing', 'true');
assert.strictEqual(prompt.innerText, '');
assert.strictEqual(prompt.deepInnerText(), 'Initial content');
const placeholder = prompt.shadowRoot?.querySelector('[contenteditable]');
assert.exists(placeholder);
assert.strictEqual(placeholder.textContent, 'Initial content');
assert.strictEqual(window.getSelection()?.toString(), 'Initial content');
});
it('sends commit events', () => {
const prompt = renderPrompt(html`<devtools-prompt>Initial content</devtools-prompt>`);
prompt.setAttribute('editing', 'true');
const listener = sinon.stub<[Event]>();
prompt.addEventListener('commit', listener);
const placeholder = prompt.shadowRoot?.querySelector('[contenteditable]');
assert.exists(placeholder);
placeholder.textContent = 'New content';
placeholder.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter', bubbles: true}));
sinon.assert.calledOnce(listener);
assert.instanceOf(listener.args[0][0], UI.TextPrompt.TextPromptElement.CommitEvent);
assert.strictEqual(listener.args[0][0].detail, 'New content');
});
it('sends cancel events', () => {
const prompt = renderPrompt(html`<devtools-prompt>Initial content</devtools-prompt>`);
prompt.setAttribute('editing', 'true');
const listener = sinon.stub<[Event]>();
prompt.addEventListener('cancel', listener);
const placeholder = prompt.shadowRoot?.querySelector('[contenteditable]');
assert.exists(placeholder);
placeholder.textContent = 'New content';
placeholder.dispatchEvent(new KeyboardEvent('keydown', {key: 'Escape', bubbles: true}));
sinon.assert.calledOnce(listener);
});
it('enters editing mode when connected to the DOM', () => {
const prompt = document.createElement('devtools-prompt') as UI.TextPrompt.TextPromptElement;
prompt.textContent = 'Initial content';
prompt.setAttribute('editing', 'true');
container.appendChild(prompt);
const placeholder = prompt.shadowRoot?.querySelector('[contenteditable]');
assert.exists(placeholder);
assert.strictEqual(placeholder.textContent, 'Initial content');
});
function stubSuggestBox() {
const {resolve, promise} = Promise.withResolvers<UI.SuggestBox.Suggestion[]>();
const suggestBoxStub = sinon.stub(UI.SuggestBox.SuggestBox.prototype, 'updateSuggestions');
suggestBoxStub.callsFake((_, suggestions) => {
suggestBoxStub.restore();
resolve(suggestions);
});
return promise;
}
it('shows completions', async () => {
const prompt = renderPrompt(html`
<devtools-prompt completions=completion-id></devtools-prompt>
<datalist id=completion-id>
<option>suggestion</option>
<option>another</option>
</datalist>`);
const suggestBoxPromise = stubSuggestBox();
prompt.setAttribute('editing', '');
const suggestions = await suggestBoxPromise;
assert.deepEqual(suggestions, [{text: 'suggestion'}, {text: 'another'}]);
});
it('allows attaching a completions datalist just-in-time', async () => {
// Initial render
renderPrompt(html`
<devtools-prompt></devtools-prompt>
<datalist id=completion-id>
<option>suggestion</option>
<option>another</option>
</datalist>`);
// Event handler to attch the completions datalist by setting the completions attribute
const setCompletions = () => renderPrompt(html`
<devtools-prompt
editing
completions=completion-id
></devtools-prompt>
<datalist id=completion-id>
<option>suggestion</option>
<option>another</option>
</datalist>`);
const suggestBoxStub = stubSuggestBox();
// Start editing to trigger the suggest box
renderPrompt(html`
<devtools-prompt
editing
@beforeautocomplete=${setCompletions}
></devtools-prompt>
></devtools-prompt>
<datalist id=completion-id>
<option>suggestion</option>
<option>another</option>
</datalist>`);
const suggestions = await suggestBoxStub;
assert.deepEqual(suggestions, [{text: 'suggestion'}, {text: 'another'}]);
});
it('allows updating completions content just-in-time', async () => {
// Initial render
renderPrompt(html`
<devtools-prompt></devtools-prompt>
<datalist id=completion-id>
<option>suggestion</option>
<option>another</option>
</datalist>`);
// Event handler to update the contents of the completions datalist
const setCompletions = () => renderPrompt(html`
<devtools-prompt
editing
completions=completion-id
></devtools-prompt>
<datalist id=completion-id>
<option>modified and removed</option>
</datalist>`);
const suggestBoxStub = stubSuggestBox();
// Start editing to trigger the suggest box
renderPrompt(html`
<devtools-prompt
editing
completions=completion-id
@beforeautocomplete=${setCompletions}
></devtools-prompt>
></devtools-prompt>
<datalist id=completion-id>
<option>suggestion</option>
<option>another</option>
</datalist>`);
const suggestions = await suggestBoxStub;
assert.deepEqual(suggestions, [{text: 'modified and removed'}]);
});
});
describe('TextPrompt', () => {
setupLocaleHooks();
const suggestions = ['heyoo', 'hey it\'s a suggestion', 'hey another suggestion'].map(s => ({text: s}));
let prompt: UI.TextPrompt.TextPrompt;
let div: HTMLDivElement;
beforeEach(() => {
prompt = new UI.TextPrompt.TextPrompt();
div = document.createElement('div');
renderElementIntoDOM(div);
UI.GlassPane.GlassPane.setContainer(div.ownerDocument.body);
});
it('provides autocomplete suggestions', async () => {
prompt.initialize(async () => suggestions);
prompt.attachAndStartEditing(div);
prompt.setText('hey');
await prompt.complete();
assert.strictEqual(prompt.textWithCurrentSuggestion(), 'heyoo');
});
it('uses a default suggestion for inexact matches', async () => {
prompt.initialize(async () => suggestions);
prompt.attachAndStartEditing(div);
prompt.clearAutocomplete();
prompt.setText('inexactmatch');
await prompt.complete();
assert.strictEqual(prompt.textWithCurrentSuggestion(), 'heyoo');
});
it('uses a default suggestion for a blank prompt', async () => {
prompt.initialize(async () => suggestions);
prompt.attachAndStartEditing(div);
prompt.setText('');
await prompt.complete();
assert.strictEqual(prompt.textWithCurrentSuggestion(), 'heyoo');
});
it('does not suggest for a blank prompt when disabled', async () => {
prompt.initialize(async () => suggestions);
prompt.attachAndStartEditing(div);
prompt.disableDefaultSuggestionForEmptyInput();
prompt.setText('');
await prompt.complete();
assert.strictEqual(prompt.textWithCurrentSuggestion(), '');
});
it('passes expression and query to the suggestion provider', async () => {
let expression, query;
prompt.initialize(async (e, q) => {
expression = e;
query = q;
return suggestions;
});
prompt.attachAndStartEditing(div);
prompt.setText('the expression and query');
await prompt.complete();
assert.strictEqual(prompt.text(), 'the expression and query');
assert.strictEqual(expression, 'the expression and ');
assert.strictEqual(query, 'query');
});
});