Skip to content

Commit

Permalink
feedback: Show popular help content when no match or as cold start
Browse files Browse the repository at this point in the history
Show popular help content for the following situations:
- When the feedback tool is opened and no text has been entered [1].
- When some text has been entered but no match found [3].

Screenshots:
[1] https://screenshot.googleplex.com/BSji3GZwnD5CFpg.
[2] https://screenshot.googleplex.com/AttSxNW4bSebCVL (normal case).
[3] https://screenshot.googleplex.com/8cktCLtJqZm6BVF

Bug: b:185624798
Test: browser_tests --gtest_filter=OSFeedbackBrowserTest.*
Change-Id: I40fdf2b7a8f4ee4d27ef74ef1daf98c2363d6505
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3544317
Commit-Queue: Xiangdong Kong <xiangdongkong@google.com>
Auto-Submit: Xiangdong Kong <xiangdongkong@google.com>
Reviewed-by: Gavin Williams <gavinwill@chromium.org>
Commit-Queue: Gavin Williams <gavinwill@chromium.org>
Cr-Commit-Position: refs/heads/main@{#985088}
  • Loading branch information
xiangdong kong authored and Chromium LUCI CQ committed Mar 25, 2022
1 parent 93cfa14 commit ea5a561
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 54 deletions.
14 changes: 14 additions & 0 deletions ash/webui/os_feedback_ui/resources/fake_data.js
Expand Up @@ -10,6 +10,20 @@ import {HelpContentList, HelpContentType, SearchRequest, SearchResponse} from '.
* Fake data used for testing purpose.
*/

/** @type {!HelpContentList} */
export const fakePopularHelpContentList = [
{
title: stringToMojoString16('fake article'),
url: {url: 'https://support.google.com/chromebook/?q=article'},
contentType: HelpContentType.kArticle
},
{
title: stringToMojoString16('fake forum'),
url: {url: 'https://support.google.com/chromebook/?q=forum'},
contentType: HelpContentType.kForum
}
];

/** @type {!HelpContentList} */
export const fakeHelpContentList = [
{
Expand Down
14 changes: 14 additions & 0 deletions ash/webui/os_feedback_ui/resources/feedback_types.js
Expand Up @@ -41,6 +41,20 @@ export const SearchRequest = ash.osFeedbackUi.mojom.SearchRequest;
*/
export const SearchResponse = ash.osFeedbackUi.mojom.SearchResponse;

/**
* Type alias for search result. When isPopularContent is true, the contentList
* contains top popular help contents, i.e. returned where the search query is
* empty. The isQueryEmpty is true when the current query is empty. The
* isPopularContent is true when the current query is not empty and no matches
* are found.
* @typedef {{
* contentList: HelpContentList,
* isQueryEmpty: boolean,
* isPopularContent: boolean
* }}
*/
export let SearchResult;

/**
* Type alias for the HelpContentProviderInterface.
* @typedef {ash.osFeedbackUi.mojom.HelpContentProviderInterface}
Expand Down
5 changes: 2 additions & 3 deletions ash/webui/os_feedback_ui/resources/help_content.html
Expand Up @@ -13,9 +13,8 @@
}
</style>
<div id="helpContentContainer">
<!--TODO(xiangdongkong): use localized strings -->
<p id="helpContentLabel">Suggested help content:</p>
<dom-repeat items="[[helpContentList]]">
<p id="helpContentLabel">[[getLabel_(searchResult)]]</p>
<dom-repeat items="[[searchResult.contentList]]">
<template>
<div class="help-item">
<a href="[[getUrl_(item)]]" target="_blank">
Expand Down
38 changes: 31 additions & 7 deletions ash/webui/os_feedback_ui/resources/help_content.js
Expand Up @@ -6,7 +6,7 @@ import './help_resources_icons.js';
import '//resources/polymer/v3_0/iron-icon/iron-icon.js';
import {mojoString16ToString} from '//resources/ash/common/mojo_utils.js';
import {html, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {HelpContent, HelpContentList, HelpContentType} from './feedback_types.js';
import {HelpContent, HelpContentList, HelpContentType, SearchResult} from './feedback_types.js';

/**
* @const {string}
Expand All @@ -32,15 +32,39 @@ export class HelpContentElement extends PolymerElement {
}

static get properties() {
return {
/**
* An implicit array of help contents to be displayed.
* @type {!HelpContentList}
*/
helpContentList: {type: HelpContentList, value: () => []}
return {searchResult: {type: SearchResult}};
}

constructor() {
super();

/**
* @type {!SearchResult}
*/
this.searchResult = {
contentList: [],
isQueryEmpty: true,
isPopularContent: true
};
}

/**
* Compute the label to use.
* @param {!SearchResult} searchResult
* @returns {string}
* @protected
*/
getLabel_(searchResult) {
// TODO(xiangdongkong): Use localized strings.
if (!searchResult.isPopularContent) {
return 'Suggested help content';
}
if (searchResult.isQueryEmpty) {
return 'Popular help content';
}
return 'No matched results, see popular help content';
}

/**
* Find the icon name to be used for a help content type.
* @param {!HelpContentType} contentType
Expand Down
86 changes: 57 additions & 29 deletions ash/webui/os_feedback_ui/resources/search_page.js
Expand Up @@ -10,7 +10,7 @@ import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import {stringToMojoString16} from 'chrome://resources/ash/common/mojo_utils.js';
import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';

import {HelpContentProviderInterface, SearchRequest, SearchResponse} from './feedback_types.js';
import {HelpContentList, HelpContentProviderInterface, SearchRequest, SearchResponse, SearchResult} from './feedback_types.js';
import {getHelpContentProvider} from './mojo_interface_provider.js';

/**
Expand Down Expand Up @@ -76,13 +76,21 @@ export class SearchPageElement extends PolymerElement {

/** @private {?HTMLIFrameElement} */
this.iframe_ = null;

/**
* The content list received when query is empty.
* @private {!HelpContentList|undefined}
*/
this.popularHelpContentList_;
}

ready() {
super.ready();

this.iframe_ = /** @type {HTMLIFrameElement} */ (
this.shadowRoot.querySelector('iframe'));
// Fetch popular help contents with empty query.
this.fetchHelpContent_(/* query= */ '');
}
/**
*
Expand All @@ -95,40 +103,60 @@ export class SearchPageElement extends PolymerElement {

if (Math.abs(newCharCount - this.lastCharCount_) >= MIN_CHARS_COUNT) {
this.lastCharCount_ = newCharCount;

/** @type {!SearchRequest} */
const request = {
query: stringToMojoString16(newInput),
maxResults: MAX_RESULTS,
};

this.fetchHelpContent_(request);
this.fetchHelpContent_(newInput);
}
}

/**
* @param {!SearchRequest} request
* @param {string} query
* @private
*/
fetchHelpContent_(request) {
this.helpContentProvider_.getHelpContents(request).then(
/** {{response: !SearchResponse}} */ (response) => {
if (!this.iframe_) {
console.warn('untrusted iframe is not found');
return;
}

const data = {
response: response.response,
};

// Wait for the iframe to complete loading before postMessage.
this.iframeLoaded_.then(() => {
// TODO(xiangdongkong): Use Mojo to communicate with untrusted page.
this.iframe_.contentWindow.postMessage(
data, OS_FEEDBACK_UNTRUSTED_ORIGIN);
});
});
async fetchHelpContent_(query) {
if (!this.iframe_) {
console.warn('untrusted iframe is not found');
return;
}

/** @type {!SearchRequest} */
const request = {
query: stringToMojoString16(query),
maxResults: MAX_RESULTS,
};

/** @type boolean */
const isQueryEmpty = (query === '');

/** @type boolean */
let isPopularContent;

/** @type {{response: !SearchResponse}} */
let response;

if (isQueryEmpty) {
// Load popular help content if they are not loaded before.
if (this.popularHelpContentList_ === undefined) {
response = await this.helpContentProvider_.getHelpContents(request);
this.popularHelpContentList_ = response.response.results;
}
isPopularContent = true;
} else {
response = await this.helpContentProvider_.getHelpContents(request);
isPopularContent = (response.response.totalResults === 0);
}

/** @type {!SearchResult} */
const data = {
contentList: /** @type {!HelpContentList} */ (
isPopularContent ? this.popularHelpContentList_ :
response.response.results),
isQueryEmpty: isQueryEmpty,
isPopularContent: isPopularContent
};

// Wait for the iframe to complete loading before postMessage.
await this.iframeLoaded_;
// TODO(xiangdongkong): Use Mojo to communicate with untrusted page.
this.iframe_.contentWindow.postMessage(data, OS_FEEDBACK_UNTRUSTED_ORIGIN);
}
}

Expand Down
Expand Up @@ -22,14 +22,14 @@ function initialize() {
console.error('Unknown origin: ' + event.origin);
return;
}
// After receiving help content sent from parent page, display them.
helpContent.helpContentList = event.data.response.results;
// After receiving search result sent from parent page, display them.
helpContent.searchResult = event.data;

// Post a message to parent to make testing easier.
window.parent.postMessage(
{
id: 'help-content-received-for-testing',
count: event.data.response.results.length,
count: event.data.contentList.length,
},
OS_FEEDBACK_TRUSTED_ORIGIN);
});
Expand Down
93 changes: 82 additions & 11 deletions chrome/test/data/webui/chromeos/os_feedback_ui/help_content_test.js
Expand Up @@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {fakeHelpContentList} from 'chrome://os-feedback/fake_data.js';
import {HelpContentList, HelpContentType} from 'chrome://os-feedback/feedback_types.js';
import {fakeHelpContentList, fakePopularHelpContentList} from 'chrome://os-feedback/fake_data.js';
import {HelpContentList, HelpContentType, SearchResult} from 'chrome://os-feedback/feedback_types.js';
import {HelpContentElement} from 'chrome://os-feedback/help_content.js';

import {assertEquals, assertFalse, assertTrue} from '../../chai_assert.js';
Expand All @@ -22,15 +22,26 @@ export function helpContentTestSuite() {
helpContentElement = null;
});

/** @param {!HelpContentList} contentList */
function initializeHelpContentElement(contentList) {
/**
* @param {!HelpContentList} contentList
* @param {boolean} isQueryEmpty
* @param {boolean} isPopularContent
*
*/
function initializeHelpContentElement(
contentList, isQueryEmpty, isPopularContent) {
assertFalse(!!helpContentElement);
helpContentElement =
/** @type {!HelpContentElement} */ (
document.createElement('help-content'));
assertTrue(!!helpContentElement);

helpContentElement.helpContentList = contentList;
helpContentElement.searchResult = {
contentList: contentList,
isQueryEmpty: isQueryEmpty,
isPopularContent: isPopularContent
};

document.body.appendChild(helpContentElement);

return flushTasks();
Expand Down Expand Up @@ -62,23 +73,64 @@ export function helpContentTestSuite() {
}
}

/** Test that expected html elements are in the element. */
test('HelpContentLoaded', async () => {
await initializeHelpContentElement(fakeHelpContentList);
// Verify that all popular help content are displayed.
function verifyPopularHelpContent() {
assertEquals(2, getElement('dom-repeat').items.length);
const helpLinks =
helpContentElement.shadowRoot.querySelectorAll('.help-item a');
assertEquals(2, helpLinks.length);

// Verify the help links are displayed in order with correct title, url and
// icon.
assertEquals('fake article', helpLinks[0].innerText);
assertEquals(
'https://support.google.com/chromebook/?q=article', helpLinks[0].href);
verifyIconName(helpLinks[0], fakePopularHelpContentList[0].contentType);

assertEquals('fake forum', helpLinks[1].innerText);
assertEquals(
'https://support.google.com/chromebook/?q=forum', helpLinks[1].href);
verifyIconName(helpLinks[1], fakePopularHelpContentList[1].contentType);
}

/**
* Test that expected HTML elements are in the element when query is empty.
*/
test('ColdStart', async () => {
await initializeHelpContentElement(
fakePopularHelpContentList, /* isQueryEmpty= */ true,
/* isPopularContent= */ true);

// Verify the title is in the helpContentElement.
const title = getElement('#helpContentLabel');
assertTrue(!!title);
assertEquals('Suggested help content:', title.textContent);
assertEquals('Popular help content', title.textContent);

verifyPopularHelpContent();
});

/**
* Test that expected HTML elements are in the element when the query is not
* empty and there are matches.
*/
test('SuggestedHelpContentLoaded', async () => {
await initializeHelpContentElement(
fakeHelpContentList, /* isQueryEmpty =*/ false,
/* isPopularContent =*/ false);

// Verify the title is in the helpContentElement.
const title = getElement('#helpContentLabel');
assertTrue(!!title);
assertEquals('Suggested help content', title.textContent);

// Verify the help content is populated with correct number of items.
assertEquals(5, getElement('dom-repeat').items.length);
const helpLinks =
helpContentElement.shadowRoot.querySelectorAll('.help-item a');
assertEquals(5, helpLinks.length);

// Verify the help links are displayed in order with correct title and
// url.
// Verify the help links are displayed in order with correct title, url and
// icon.
assertEquals('Fix connection problems', helpLinks[0].innerText);
assertEquals(
'https://support.google.com/chromebook/?q=6318213', helpLinks[0].href);
Expand Down Expand Up @@ -111,4 +163,23 @@ export function helpContentTestSuite() {
'https://support.google.com/chromebook/?q=22864239', helpLinks[4].href);
verifyIconName(helpLinks[4], fakeHelpContentList[4].contentType);
});


/**
* Test that expected HTML elements are in the element when query is not empty
* and there are no matches.
*/
test('NoMatches', async () => {
await initializeHelpContentElement(
fakePopularHelpContentList, /* isQueryEmpty= */ false,
/* isPopularContent= */ true);

// Verify the title is in the helpContentElement.
const title = getElement('#helpContentLabel');
assertTrue(!!title);
assertEquals(
'No matched results, see popular help content', title.textContent);

verifyPopularHelpContent();
});
}
Expand Up @@ -142,7 +142,9 @@ export function searchPageTestSuite() {
});

const data = {
response: fakeSearchResponse,
contentList: fakeSearchResponse.results,
isQueryEmpty: true,
isPopularContent: true
};
iframe.contentWindow.postMessage(data, OS_FEEDBACK_UNTRUSTED_ORIGIN);

Expand Down

0 comments on commit ea5a561

Please sign in to comment.