// 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 'chrome-untrusted://compose/app.js';
import {CrFeedbackOption} from '//resources/cr_elements/cr_feedback_buttons/cr_feedback_buttons.js';
import {loadTimeData} from '//resources/js/load_time_data.js';
import type {ComposeAppElement, ComposeAppState} from 'chrome-untrusted://compose/app.js';
import type {ComposeState} from 'chrome-untrusted://compose/compose.mojom-webui.js';
import { CloseReason, StyleModifier, UserFeedback } from 'chrome-untrusted://compose/compose.mojom-webui.js';
import {ComposeApiProxyImpl} from 'chrome-untrusted://compose/compose_api_proxy.js';
import {ComposeStatus} from 'chrome-untrusted://compose/compose_enums.mojom-webui.js';
import {assertDeepEquals, assertEquals, assertFalse, assertStringContains, assertTrue} from 'chrome-untrusted://webui-test/chai_assert.js';
import {flushTasks} from 'chrome-untrusted://webui-test/polymer_test_util.js';
import {isVisible, whenCheck} from 'chrome-untrusted://webui-test/test_util.js';
import {TestComposeApiProxy} from './test_compose_api_proxy.js';
suite('ComposeApp', () => {
let app: ComposeAppElement;
let testProxy: TestComposeApiProxy;
setup(async () => {
testProxy = new TestComposeApiProxy();
ComposeApiProxyImpl.setInstance(testProxy);
document.body.innerHTML = window.trustedTypes!.emptyHTML;
app = document.createElement('compose-app');
document.body.appendChild(app);
await testProxy.whenCalled('requestInitialState');
return flushTasks();
});
function mockInput(input: string) {
app.$.textarea.value = input;
app.$.textarea.dispatchEvent(new CustomEvent('value-changed'));
}
function mockResponse(
result: string = 'some response',
status: ComposeStatus = ComposeStatus.kOk, onDeviceEvaluationUsed = false,
triggeredFromModifier = false): Promise<void> {
testProxy.remote.responseReceived({
status: status,
undoAvailable: false,
redoAvailable: false,
providedByUser: false,
result,
onDeviceEvaluationUsed,
triggeredFromModifier,
});
return testProxy.remote.$.flushForTesting();
}
function mockPartialResponse(result: string = 'partial response'):
Promise<void> {
testProxy.remote.partialResponseReceived({result});
return testProxy.remote.$.flushForTesting();
}
async function initializeNewAppWithFirstRunAndMsbbState(
fre: boolean, msbb: boolean): Promise<ComposeAppElement> {
document.body.innerHTML = window.trustedTypes!.emptyHTML;
testProxy.setOpenMetadata({freComplete: fre, msbbState: msbb});
const newApp = document.createElement('compose-app');
document.body.appendChild(newApp);
await flushTasks();
return newApp;
}
test('SendsInputParams', () => {
assertEquals(2, app.$.textarea.inputParams.minWordLimit);
assertEquals(50, app.$.textarea.inputParams.maxWordLimit);
assertEquals(100, app.$.textarea.inputParams.maxCharacterLimit);
});
test('SubmitsAndAcceptsInput', async () => {
// Starts off with submit enabled even when input is empty.
assertTrue(isVisible(app.$.submitButton));
assertFalse(app.$.submitButton.disabled);
assertFalse(isVisible(app.$.resultContainer));
assertFalse(isVisible(app.$.acceptButton));
// Invalid input keeps submit enabled and error is not visible.
mockInput('Short');
assertFalse(app.$.submitButton.disabled);
assertFalse(isVisible(app.$.textarea.$.tooShortError));
assertFalse(isVisible(app.$.textarea.$.tooLongError));
// Clicking on submit shows error.
app.$.submitButton.click();
assertTrue(app.$.submitButton.disabled);
assertTrue(isVisible(app.$.textarea.$.tooShortError));
assertFalse(isVisible(app.$.textarea.$.tooLongError));
// Inputting valid text enables submit.
mockInput('Here is my input.');
assertFalse(app.$.submitButton.disabled);
// Clicking on submit gets results.
app.$.submitButton.click();
assertTrue(isVisible(app.$.loading));
const args = await testProxy.whenCalled('compose');
await mockResponse();
assertEquals('Here is my input.', args.input);
assertFalse(isVisible(app.$.loading));
assertFalse(isVisible(app.$.submitButton));
assertTrue(app.$.textarea.readonly);
assertTrue(isVisible(app.$.acceptButton));
assertFalse(isVisible(app.$.onDeviceUsedFooter));
// Clicking on accept button calls acceptComposeResult.
app.$.acceptButton.click();
await testProxy.whenCalled('acceptComposeResult');
});
test('OnlyOneErrorShows', async () => {
mockInput('x'.repeat(2501));
app.$.submitButton.click();
assertTrue(app.$.submitButton.disabled);
assertTrue(isVisible(app.$.textarea.$.tooLongError));
assertFalse(isVisible(app.$.textarea.$.tooShortError));
});
test('AcceptButtonText', async () => {
async function initializeNewAppWithTextSelectedState(textSelected: boolean):
Promise<ComposeAppElement> {
document.body.innerHTML = window.trustedTypes!.emptyHTML;
testProxy.setOpenMetadata({textSelected});
const newApp = document.createElement('compose-app');
document.body.appendChild(newApp);
await flushTasks();
return newApp;
}
const appWithTextSelected =
await initializeNewAppWithTextSelectedState(true);
assertStringContains(
appWithTextSelected.$.acceptButton.textContent!, 'Replace');
const appWithNoTextSelected =
await initializeNewAppWithTextSelectedState(false);
assertStringContains(
appWithNoTextSelected.$.acceptButton.textContent!, 'Insert');
});
test('FirstRunAndMsbbStateDetermineViewState', async () => {
// Check correct visibility for FRE view state.
const appWithFirstRunDialog =
await initializeNewAppWithFirstRunAndMsbbState(false, false);
assertTrue(isVisible(appWithFirstRunDialog.$.firstRunDialog));
assertFalse(isVisible(appWithFirstRunDialog.$.freMsbbDialog));
assertFalse(isVisible(appWithFirstRunDialog.$.appDialog));
// Check correct visibility for MSBB view state.
const appWithMSBBDialog =
await initializeNewAppWithFirstRunAndMsbbState(true, false);
assertFalse(isVisible(appWithMSBBDialog.$.firstRunDialog));
assertTrue(isVisible(appWithMSBBDialog.$.freMsbbDialog));
assertFalse(isVisible(appWithMSBBDialog.$.appDialog));
// Check correct visibility for main app view state
const appWithMainDialog =
await initializeNewAppWithFirstRunAndMsbbState(true, true);
assertFalse(isVisible(appWithMainDialog.$.firstRunDialog));
assertFalse(isVisible(appWithMainDialog.$.freMsbbDialog));
assertTrue(isVisible(appWithMainDialog.$.appDialog));
});
test('FirstRunCloseButton', async () => {
const appWithFirstRunDialog =
await initializeNewAppWithFirstRunAndMsbbState(true, false);
appWithFirstRunDialog.$.firstRunCloseButton.click();
// Close reason should match that given to the FRE close button.
const closeReason = await testProxy.whenCalled('closeUi');
assertEquals(CloseReason.kFirstRunCloseButton, closeReason);
});
test('MSBBCloseButton', async () => {
const appWithMsbbDialog =
await initializeNewAppWithFirstRunAndMsbbState(true, false);
appWithMsbbDialog.$.closeButtonMSBB.click();
// Close reason should match that given to the consent close button.
const closeReason = await testProxy.whenCalled('closeUi');
assertEquals(CloseReason.kMSBBCloseButton, closeReason);
});
test('FirstRunOkButtonToMainDialog', async () => {
const appWithFirstRunDialog =
await initializeNewAppWithFirstRunAndMsbbState(false, true);
appWithFirstRunDialog.$.firstRunOkButton.click();
// View state should change from FRE UI to main app UI.
assertFalse(isVisible(appWithFirstRunDialog.$.firstRunDialog));
assertFalse(isVisible(appWithFirstRunDialog.$.freMsbbDialog));
assertTrue(isVisible(appWithFirstRunDialog.$.appDialog));
});
test('FirstRunOkButtonToMSBBDialog', async () => {
const appWithFirstRunDialog =
await initializeNewAppWithFirstRunAndMsbbState(false, false);
appWithFirstRunDialog.$.firstRunOkButton.click();
// View state should change from FRE UI to MSBB UI.
assertFalse(isVisible(appWithFirstRunDialog.$.firstRunDialog));
assertTrue(isVisible(appWithFirstRunDialog.$.freMsbbDialog));
assertFalse(isVisible(appWithFirstRunDialog.$.appDialog));
});
test('InitializesWithState', async () => {
async function initializeNewAppWithState(
state: Partial<ComposeState>,
input?: string): Promise<ComposeAppElement> {
document.body.innerHTML = window.trustedTypes!.emptyHTML;
testProxy.setOpenMetadata({initialInput: input}, state);
const newApp = document.createElement('compose-app');
document.body.appendChild(newApp);
await flushTasks();
return newApp;
}
// Initial input
const appWithInitialInput =
await initializeNewAppWithState({}, 'initial input');
assertEquals('initial input', appWithInitialInput.$.textarea.value);
// Invalid input is sent to textarea but submit is enabled.
const appWithInvalidInput = await initializeNewAppWithState(
{webuiState: JSON.stringify({input: 'short'})});
assertEquals('short', appWithInvalidInput.$.textarea.value);
assertFalse(appWithInvalidInput.$.submitButton.disabled);
// Input with pending response shows loading state.
const appWithLoadingState = await initializeNewAppWithState({
hasPendingRequest: true,
webuiState: JSON.stringify({input: 'some input'}),
});
assertTrue(isVisible(appWithLoadingState.$.loading));
// Input with response shows response.
const appWithResult = await initializeNewAppWithState({
webuiState: JSON.stringify({input: 'some input'}),
hasPendingRequest: false,
response: {
status: ComposeStatus.kOk,
undoAvailable: false,
redoAvailable: false,
providedByUser: false,
result: 'here is a result',
onDeviceEvaluationUsed: false,
triggeredFromModifier: false,
},
});
assertTrue(isVisible(appWithResult.$.resultContainer));
assertStringContains(
appWithResult.$.resultText.$.root.innerText, 'here is a result');
assertTrue(appWithResult.$.undoButton.disabled);
// Input with response with undo available.
const appWithUndo = await initializeNewAppWithState({
webuiState: JSON.stringify({input: 'some input'}),
hasPendingRequest: false,
response: {
status: ComposeStatus.kOk,
undoAvailable: true,
redoAvailable: false,
providedByUser: false,
result: 'here is a result',
onDeviceEvaluationUsed: false,
triggeredFromModifier: false,
},
});
assertFalse(appWithUndo.$.undoButton.disabled);
// Input with positive feedback.
const appWithPositiveFeedback = await initializeNewAppWithState({
webuiState: JSON.stringify({input: 'some input'}),
hasPendingRequest: false,
feedback: UserFeedback.kUserFeedbackPositive,
});
assertEquals(
CrFeedbackOption.THUMBS_UP,
appWithPositiveFeedback.$.feedbackButtons.selectedOption);
// Already has a response but is loading another one.
const appWithResultAndLoading = await initializeNewAppWithState({
webuiState: JSON.stringify({input: 'some input'}),
hasPendingRequest: true,
response: {
status: ComposeStatus.kOk,
undoAvailable: false,
redoAvailable: false,
providedByUser: false,
result: 'here is a result',
onDeviceEvaluationUsed: false,
triggeredFromModifier: false,
},
});
assertTrue(isVisible(appWithResultAndLoading.$.loading));
assertFalse(isVisible(appWithResultAndLoading.$.resultContainer));
// Input with response while editing input shows edit textarea.
const appEditingPrompt = await initializeNewAppWithState({
webuiState: JSON.stringify({
input: 'some input',
isEditingSubmittedInput: true,
editedInput: 'some new input',
}),
hasPendingRequest: false,
response: {
status: ComposeStatus.kOk,
undoAvailable: false,
redoAvailable: false,
providedByUser: false,
result: 'here is a result',
onDeviceEvaluationUsed: false,
triggeredFromModifier: false,
},
});
assertTrue(isVisible(appEditingPrompt.$.editTextarea));
assertEquals('some new input', appEditingPrompt.$.editTextarea.value);
assertEquals(
'hidden',
window.getComputedStyle(appEditingPrompt.$.textarea).visibility);
assertFalse(isVisible(appEditingPrompt.$.loading));
assertEquals(
'hidden',
window.getComputedStyle(appEditingPrompt.$.resultContainer).visibility);
// Input with feedback already filled out.
const appWithFeedback = await initializeNewAppWithState({
feedback: UserFeedback.kUserFeedbackPositive,
});
const feedbackButtons =
appWithFeedback.shadowRoot!.querySelector('cr-feedback-buttons')!;
assertEquals('true', feedbackButtons.$.thumbsUp.ariaPressed);
});
test('SavesState', async () => {
assertEquals(0, testProxy.getCallCount('saveWebuiState'), 'es');
async function assertSavedState(expectedState: ComposeAppState) {
const savedState = await testProxy.whenCalled('saveWebuiState');
assertDeepEquals(expectedState, JSON.parse(savedState));
testProxy.resetResolver('saveWebuiState');
}
// Changing input saves state.
mockInput('Here is my input');
await assertSavedState({input: 'Here is my input'});
// Visibilitychange event saves state.
Object.defineProperty(
document, 'visibilityState', {value: 'hidden', writable: true});
document.dispatchEvent(new CustomEvent('visibilitychange'));
await assertSavedState({input: 'Here is my input'});
// Hitting submit saves state.
app.$.submitButton.click();
await assertSavedState({input: 'Here is my input'});
// Hitting edit button saves state.
app.$.textarea.dispatchEvent(
new CustomEvent('edit-click', {composed: true, bubbles: true}));
await assertSavedState({
editedInput: 'Here is my input',
input: 'Here is my input',
isEditingSubmittedInput: true,
});
// Updating edit textarea saves state.
app.$.editTextarea.value = 'Here is my new input';
app.$.editTextarea.dispatchEvent(new CustomEvent('value-changed'));
await assertSavedState({
editedInput: 'Here is my new input',
input: 'Here is my input',
isEditingSubmittedInput: true,
});
// Canceling reverts state back to before editing.
app.$.cancelEditButton.click();
await assertSavedState({input: 'Here is my input'});
// Submitting edited textarea saves state.
app.$.textarea.dispatchEvent(
new CustomEvent('edit-click', {composed: true, bubbles: true}));
app.$.editTextarea.value = 'Here is my new input!!!!';
app.$.editTextarea.dispatchEvent(new CustomEvent('value-changed'));
testProxy.resetResolver('saveWebuiState');
app.$.submitEditButton.click();
await assertSavedState({input: 'Here is my new input!!!!'});
});
test('DebouncesSavingState', async () => {
mockInput('Here is my input');
mockInput('Here is my input 2');
await flushTasks();
const savedState = await testProxy.whenCalled('saveWebuiState');
assertEquals(1, testProxy.getCallCount('saveWebuiState'));
assertEquals(JSON.stringify({input: 'Here is my input 2'}), savedState);
});
test('DebouncesSavingState', async () => {
mockInput('Here is my input');
mockInput('Here is my input 2');
await flushTasks();
const savedState = await testProxy.whenCalled('saveWebuiState');
assertEquals(1, testProxy.getCallCount('saveWebuiState'));
assertEquals(JSON.stringify({input: 'Here is my input 2'}), savedState);
});
test('CloseButton', async () => {
assertTrue(isVisible(app.$.closeButton));
app.$.closeButton.click();
// Close reason should match that given to the close button.
const closeReason = await testProxy.whenCalled('closeUi');
assertEquals(CloseReason.kCloseButton, closeReason);
});
test('GoBackFromError', async () => {
testProxy.setResponseBeforeError({
hasPendingRequest: false,
response: {
status: ComposeStatus.kOk,
undoAvailable: false,
redoAvailable: false,
providedByUser: false,
result: 'initial result text',
onDeviceEvaluationUsed: false,
triggeredFromModifier: false,
},
webuiState: JSON.stringify({
input: 'initial input',
selectedLength: Number(StyleModifier.kUnset),
selectedTone: Number(StyleModifier.kUnset),
}),
feedback: UserFeedback.kUserFeedbackPositive,
});
// Mock a filtered error response that enables the go back button.
mockInput('Initial input.');
app.$.submitButton.click();
const errorMessage = `filtered error message`;
loadTimeData.overrideValues({['errorFiltered']: errorMessage});
await mockResponse('', ComposeStatus.kFiltered, false, true);
assertTrue(isVisible(app.$.errorFooter));
assertStringContains(app.$.errorFooter.textContent!, errorMessage);
assertTrue(isVisible(app.$.errorGoBackButton));
app.$.errorGoBackButton.click();
await testProxy.whenCalled('recoverFromErrorState');
await flushTasks();
// UI is updated to the mocked last ok response.
assertEquals('initial input', app.$.textarea.value);
assertTrue(isVisible(app.$.resultContainer));
assertStringContains(
app.$.resultText.$.root.innerText, 'initial result text');
});
test('ErrorFooterShowsMessage', async () => {
async function testError(status: ComposeStatus, stringKey: string) {
const errorMessage = `some error ${stringKey}`;
loadTimeData.overrideValues({[stringKey]: errorMessage});
mockInput('Here is my input.');
app.$.submitButton.click();
await testProxy.whenCalled('compose');
await mockResponse('', status);
assertTrue(isVisible(app.$.errorFooter));
assertStringContains(app.$.errorFooter.textContent!, errorMessage);
}
await testError(ComposeStatus.kFiltered, 'errorFiltered');
await testError(ComposeStatus.kRequestThrottled, 'errorRequestThrottled');
await testError(ComposeStatus.kOffline, 'errorOffline');
await testError(ComposeStatus.kRequestTimeout, 'errorTryAgainLater');
await testError(ComposeStatus.kClientError, 'errorTryAgain');
await testError(ComposeStatus.kMisconfiguration, 'errorTryAgain');
await testError(ComposeStatus.kServerError, 'errorTryAgain');
await testError(ComposeStatus.kInvalidRequest, 'errorTryAgain');
await testError(ComposeStatus.kRetryableError, 'errorTryAgain');
await testError(ComposeStatus.kNonRetryableError, 'errorTryAgain');
await testError(ComposeStatus.kDisabled, 'errorTryAgain');
await testError(ComposeStatus.kCancelled, 'errorTryAgain');
await testError(ComposeStatus.kNoResponse, 'errorTryAgain');
});
test('UnsupportedLanguageErrorClickable', async () => {
const errorMessage = `some error ${'errorUnsupportedLanguage'}`;
loadTimeData.overrideValues({['errorUnsupportedLanguage']: errorMessage});
mockInput('Here is my input.');
app.$.submitButton.click();
await testProxy.whenCalled('compose');
await mockResponse('', ComposeStatus.kUnsupportedLanguage);
assertTrue(isVisible(app.$.errorFooter));
// Click on the "Learn more" link part of the error.
(app.$.errorFooter.getElementsByTagName('A')[0] as HTMLElement).click();
await testProxy.whenCalled('openComposeLearnMorePage');
});
test('PermissionDeniedErrorClickable', async () => {
const errorMessage = `some error ${'errorPermissionDenied'}`;
loadTimeData.overrideValues({['errorPermissionDenied']: errorMessage});
mockInput('Here is my input.');
app.$.submitButton.click();
await testProxy.whenCalled('compose');
await mockResponse('', ComposeStatus.kPermissionDenied);
assertTrue(isVisible(app.$.errorFooter));
// Click on the "Sign in" link part of the error.
(app.$.errorFooter.getElementsByTagName('A')[1] as HTMLElement).click();
await testProxy.whenCalled('openSignInPage');
});
test('AllowsEditingPrompt', async () => {
app.$.textarea.dispatchEvent(
new CustomEvent('edit-click', {composed: true, bubbles: true}));
assertTrue(isVisible(app.$.editTextarea));
mockInput('Initial input.');
app.$.submitButton.click();
await testProxy.whenCalled('compose');
await flushTasks();
testProxy.resetResolver('compose');
// Mock clicking edit in the textarea and verify new textarea shows.
app.$.textarea.dispatchEvent(
new CustomEvent('edit-click', {composed: true, bubbles: true}));
await testProxy.whenCalled('logEditInput');
assertTrue(isVisible(app.$.editTextarea));
// Mock updating input and cancelling.
assertEquals('Initial input.', app.$.editTextarea.value);
app.$.editTextarea.value = 'Here is a better input.';
app.$.editTextarea.dispatchEvent(new CustomEvent('value-changed'));
app.$.cancelEditButton.click();
await testProxy.whenCalled('logCancelEdit');
assertFalse(isVisible(app.$.editTextarea));
assertEquals('Initial input.', app.$.textarea.value);
// Mock updating input and submitting.
app.$.textarea.dispatchEvent(
new CustomEvent('edit-click', {composed: true, bubbles: true}));
app.$.editTextarea.value = 'Here is an even better input.';
app.$.editTextarea.dispatchEvent(new CustomEvent('value-changed'));
app.$.submitEditButton.click();
assertFalse(isVisible(app.$.editTextarea));
assertEquals('Here is an even better input.', app.$.textarea.value);
const args = await testProxy.whenCalled('compose');
await mockResponse('new response');
assertEquals('Here is an even better input.', args.input);
assertTrue(args.edited);
assertStringContains(app.$.resultText.$.root.innerText, 'new response');
});
test('Undo', async () => {
// Set up initial state to show undo button and mock up a previous state.
document.body.innerHTML = window.trustedTypes!.emptyHTML;
testProxy.setOpenMetadata({}, {
hasPendingRequest: false,
response: {
status: ComposeStatus.kOk,
undoAvailable: true,
redoAvailable: false,
providedByUser: false,
result: 'here is a result',
onDeviceEvaluationUsed: false,
triggeredFromModifier: false,
},
});
testProxy.setUndoResponse({
hasPendingRequest: false,
response: {
status: ComposeStatus.kOk,
undoAvailable: false,
redoAvailable: false,
providedByUser: false,
result: 'some undone result',
onDeviceEvaluationUsed: false,
triggeredFromModifier: false,
},
webuiState: JSON.stringify({
input: 'my old input',
selectedLength: Number(StyleModifier.kLonger),
selectedTone: Number(StyleModifier.kCasual),
}),
feedback: UserFeedback.kUserFeedbackPositive,
});
const appWithUndo = document.createElement('compose-app');
document.body.appendChild(appWithUndo);
await testProxy.whenCalled('requestInitialState');
// Click undo.
appWithUndo.$.undoButton.click();
await testProxy.whenCalled('undo');
await flushTasks();
// UI is updated.
assertEquals('my old input', appWithUndo.$.textarea.value);
assertTrue(isVisible(appWithUndo.$.resultContainer));
assertStringContains(
appWithUndo.$.resultText.$.root.innerText, 'some undone result');
assertEquals(
StyleModifier.kLonger, Number(appWithUndo.$.lengthMenu.value));
assertEquals(StyleModifier.kCasual, Number(appWithUndo.$.toneMenu.value));
assertEquals(
CrFeedbackOption.THUMBS_UP,
appWithUndo.$.feedbackButtons.selectedOption);
});
test('Redo', async () => {
// Set up initial state to show redo button and mock a forward state to redo
// to.
document.body.innerHTML = window.trustedTypes!.emptyHTML;
testProxy.setOpenMetadata({}, {
hasPendingRequest: false,
response: {
status: ComposeStatus.kOk,
undoAvailable: false,
redoAvailable: true,
providedByUser: false,
result: 'here is a result',
onDeviceEvaluationUsed: false,
triggeredFromModifier: false,
},
});
testProxy.setRedoResponse({
hasPendingRequest: false,
response: {
status: ComposeStatus.kOk,
undoAvailable: false,
redoAvailable: false,
providedByUser: false,
result: 'some future result',
onDeviceEvaluationUsed: false,
triggeredFromModifier: false,
},
webuiState: JSON.stringify({
input: 'some future input',
selectedLength: Number(StyleModifier.kLonger),
selectedTone: Number(StyleModifier.kCasual),
}),
feedback: UserFeedback.kUserFeedbackPositive,
});
const appWithRedo = document.createElement('compose-app');
document.body.appendChild(appWithRedo);
await testProxy.whenCalled('requestInitialState');
// Click redo.
appWithRedo.$.redoButton.click();
await testProxy.whenCalled('redo');
await flushTasks();
// UI is updated.
assertEquals('some future input', appWithRedo.$.textarea.value);
assertTrue(isVisible(appWithRedo.$.resultContainer));
assertStringContains(
appWithRedo.$.resultText.$.root.innerText, 'some future result');
assertEquals(StyleModifier.kLonger, Number(appWithRedo.$.lengthMenu.value));
assertEquals(StyleModifier.kCasual, Number(appWithRedo.$.toneMenu.value));
assertEquals(
CrFeedbackOption.THUMBS_UP,
appWithRedo.$.feedbackButtons.selectedOption);
});
test('Feedback', async () => {
const feedbackButtons =
app.shadowRoot!.querySelector('cr-feedback-buttons')!;
feedbackButtons.dispatchEvent(new CustomEvent('selected-option-changed', {
bubbles: true,
composed: true,
detail: {value: CrFeedbackOption.THUMBS_DOWN},
}));
const args = await testProxy.whenCalled('setUserFeedback');
assertEquals(args.reason, args.UserFeedback);
});
test('PartialResponseIsShown', async () => {
loadTimeData.overrideValues({
enableOnDeviceDogfoodFooter: true,
});
// Make streaming work instantly.
app.$.resultText.enableInstantStreamingForTesting();
// Although streaming happens immediately, it requires a sequence of events.
// Wait for those events to complete.
const wait = async () => {
for (let i = 0; i < 5; i++) {
await flushTasks();
}
};
mockInput('Some fake input.');
app.$.submitButton.click();
await testProxy.whenCalled('compose');
// A partial response is shown.
await mockPartialResponse('partial response here');
await wait();
assertTrue(
isVisible(app.$.resultText.$.partialResultText),
'partial result text should be shown');
assertEquals(app.$.resultText.$.root.innerText, 'partial response');
// The final response hides the partial response text.
await mockResponse(
'some response', ComposeStatus.kOk, /*onDeviceEvaluationUsed=*/
true);
await flushTasks();
assertTrue(
(app as any).showOnDeviceDogfoodFooter_(),
'show footer should be true');
await wait();
assertTrue(
isVisible(app.$.onDeviceUsedFooter),
'on-device footer should be shown');
assertEquals(app.$.resultText.$.root.innerText.trim(), 'some response');
});
});
suite('ComposeAppLegacyUi', () => {
let app: ComposeAppElement;
let testProxy: TestComposeApiProxy;
suiteSetup(function() {
if (loadTimeData.getBoolean('enableRefinedUi')) {
this.skip();
}
});
setup(async () => {
testProxy = new TestComposeApiProxy();
ComposeApiProxyImpl.setInstance(testProxy);
document.body.innerHTML = window.trustedTypes!.emptyHTML;
app = document.createElement('compose-app');
document.body.appendChild(app);
await testProxy.whenCalled('requestInitialState');
return flushTasks();
});
function mockInput(input: string) {
app.$.textarea.value = input;
app.$.textarea.dispatchEvent(new CustomEvent('value-changed'));
}
function mockResponse(
result: string = 'some response',
status: ComposeStatus = ComposeStatus.kOk, onDeviceEvaluationUsed = false,
triggeredFromModifier = false): Promise<void> {
testProxy.remote.responseReceived({
status: status,
undoAvailable: false,
redoAvailable: false,
providedByUser: false,
result,
onDeviceEvaluationUsed,
triggeredFromModifier,
});
return testProxy.remote.$.flushForTesting();
}
test('RefreshesResult', async () => {
// Submit the input once so the refresh button shows up.
mockInput('Input to refresh.');
app.$.submitButton.click();
await mockResponse();
testProxy.resetResolver('rewrite');
assertTrue(
isVisible(app.$.refreshButton), 'Refresh button should be visible.');
// Click the refresh button and assert compose is called with the same args.
app.$.refreshButton.click();
assertTrue(
isVisible(app.$.loading), 'Loading indicator should be visible.');
const args = await testProxy.whenCalled('rewrite');
await mockResponse('Refreshed output.');
assertEquals(StyleModifier.kRetry, args);
// Verify UI has updated with refreshed results.
assertFalse(isVisible(app.$.loading));
assertTrue(
isVisible(app.$.resultContainer),
'App result container should be visible.');
assertStringContains(
app.$.resultText.$.root.innerText, 'Refreshed output.');
});
test('UpdatesScrollableBodyAfterResize', async () => {
assertEquals(app.$.body, app.getContainer());
mockInput('Some fake input.');
app.$.submitButton.click();
// Mock a height on results to get body to scroll. The body should not yet
// be scrollable though because result has not been fetched yet.
app.$.resultContainer.style.minHeight = '500px';
assertFalse(app.$.body.classList.contains('can-scroll'));
await testProxy.whenCalled('compose');
await mockResponse();
await whenCheck(
app.$.body, () => app.$.body.classList.contains('can-scroll'));
assertEquals(220, app.$.body.offsetHeight);
assertTrue(220 < app.$.body.scrollHeight);
// Mock resizing result container down to a 50px height. This should result
// in the body changing height, triggering the updates to the CSS classes.
// At this point, 50px is too short to scroll, so it should not have the
// 'can-scroll' class.
app.$.resultContainer.style.minHeight = '50px';
app.$.resultContainer.style.height = '50px';
app.$.resultContainer.style.overflow = 'hidden';
await whenCheck(
app.$.body, () => !app.$.body.classList.contains('can-scroll'));
});
test('ComposeWithLengthToneOptionResult', async () => {
// Submit the input once so the refresh button shows up.
mockInput('Input to refresh.');
app.$.submitButton.click();
await mockResponse();
testProxy.resetResolver('rewrite');
assertTrue(isVisible(app.$.lengthMenu), 'Length menu should be visible.');
assertEquals(
2, app.$.lengthMenu.querySelectorAll('option:not([disabled])').length);
app.$.lengthMenu.value = `${StyleModifier.kShorter}`;
app.$.lengthMenu.dispatchEvent(new CustomEvent('change'));
const args = await testProxy.whenCalled('rewrite');
await mockResponse();
assertEquals(StyleModifier.kShorter, args);
testProxy.resetResolver('rewrite');
assertTrue(isVisible(app.$.toneMenu), 'Tone menu should be visible.');
assertEquals(
2, app.$.toneMenu.querySelectorAll('option:not([disabled])').length);
app.$.toneMenu.value = `${StyleModifier.kCasual}`;
app.$.toneMenu.dispatchEvent(new CustomEvent('change'));
const args2 = await testProxy.whenCalled('rewrite');
await mockResponse();
assertEquals(StyleModifier.kCasual, args2);
});
});
suite('ComposeAppRefinedUi', () => {
let app: ComposeAppElement;
let testProxy: TestComposeApiProxy;
suiteSetup(function() {
if (!loadTimeData.getBoolean('enableRefinedUi')) {
this.skip();
}
});
setup(async () => {
testProxy = new TestComposeApiProxy();
ComposeApiProxyImpl.setInstance(testProxy);
document.body.innerHTML = window.trustedTypes!.emptyHTML;
app = document.createElement('compose-app');
document.body.appendChild(app);
await testProxy.whenCalled('requestInitialState');
return flushTasks();
});
function mockInput(input: string) {
app.$.textarea.value = input;
app.$.textarea.dispatchEvent(new CustomEvent('value-changed'));
}
function mockResponse(
result: string = 'some response',
status: ComposeStatus = ComposeStatus.kOk, onDeviceEvaluationUsed = false,
triggeredFromModifier = false): Promise<void> {
testProxy.remote.responseReceived({
status: status,
undoAvailable: false,
redoAvailable: false,
providedByUser: false,
result,
onDeviceEvaluationUsed,
triggeredFromModifier,
});
return testProxy.remote.$.flushForTesting();
}
test('RefreshesResult', async () => {
// Submit the input once so that modifier menu is visible.
mockInput('Input to retry.');
app.$.submitButton.click();
await mockResponse();
testProxy.resetResolver('rewrite');
assertTrue(
isVisible(app.$.modifierMenu), 'Modifier menu should be visible.');
// Select the retry option from the modifier menu and assert compose is
// called with the same args.
app.$.modifierMenu.value = `${StyleModifier.kRetry}`;
app.$.modifierMenu.dispatchEvent(new CustomEvent('change'));
assertTrue(
isVisible(app.$.loading), 'Loading indicator should be visible.');
const args = await testProxy.whenCalled('rewrite');
await mockResponse('Refreshed output.');
assertEquals(StyleModifier.kRetry, args);
// Verify UI has updated with refreshed results.
assertFalse(isVisible(app.$.loading));
assertTrue(
isVisible(app.$.resultContainer),
'App result container should be visible.');
assertStringContains(
app.$.resultText.$.root.innerText, 'Refreshed output.');
});
test('UpdatesScrollableResultContainerAfterResize', async () => {
// Assert scrolling container is set correctly.
assertEquals(app.$.resultTextContainer, app.getContainer());
mockInput('Some fake input.');
app.$.submitButton.click();
// The results text should not yet be visible because the result has not
// been fetched yet.
assertFalse(isVisible(app.$.resultTextContainer));
// Results text should be scrollable when a long response is received.
await testProxy.whenCalled('compose');
const longResponse = 'x'.repeat(1000);
await mockResponse(longResponse);
await whenCheck(
app.$.resultTextContainer,
() => app.$.resultTextContainer.classList.contains('can-scroll'));
assertEquals(220, app.$.body.offsetHeight);
assertTrue(
220 < app.$.resultTextContainer.scrollHeight,
'Scroll height (' + app.$.resultTextContainer.scrollHeight +
' should be bigger than 220.');
// Results text should not be scrollable when a short response is received.
app.$.modifierMenu.value = `${StyleModifier.kRetry}`;
app.$.modifierMenu.dispatchEvent(new CustomEvent('change'));
await testProxy.whenCalled('rewrite');
await mockResponse('Refreshed output.');
await whenCheck(
app.$.resultTextContainer,
() => !app.$.resultTextContainer.classList.contains('can-scroll'));
});
test('ComposeWithModifierResult', async () => {
// Submit the input once so that modifier menu is visible.
mockInput('Input to refresh.');
app.$.submitButton.click();
await mockResponse();
testProxy.resetResolver('rewrite');
assertTrue(
isVisible(app.$.modifierMenu), 'Modifier menu should be visible.');
assertEquals(
5,
app.$.modifierMenu.querySelectorAll('option:not([disabled])').length);
app.$.modifierMenu.value = `${StyleModifier.kShorter}`;
app.$.modifierMenu.dispatchEvent(new CustomEvent('change'));
const args = await testProxy.whenCalled('rewrite');
await mockResponse();
assertEquals(StyleModifier.kShorter, args);
});
});