Bug 2040055 - Show loading state in modal while link is being created r=sharing-reviewers,fluent-reviewers,bolsson,jhirsch
Differential Revision: https://phabricator.services.mozilla.com/D301146
This commit is contained in:
committed by
nbarrett@mozilla.com
parent
8098b7f49c
commit
2047026553
@@ -94,6 +94,7 @@ export function makeShareResult({ share = null } = {}) {
|
||||
url: null,
|
||||
isSchemaValid: null,
|
||||
isSignedIn: null,
|
||||
loadingPromise: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -352,16 +353,29 @@ class ContentSharingUtilsClass {
|
||||
* @param {string} context Used in error logging (e.g. "tabs", "tab group")
|
||||
*/
|
||||
async #createLinkAndOpenModal(shareResult, context) {
|
||||
// Note: the result object contains either the URL or an error. It's safe
|
||||
// to pass into the modal, which handles error UI as needed.
|
||||
shareResult = await this.createShareableLink(shareResult);
|
||||
shareResult.isSignedIn =
|
||||
this.isSignedIn() && shareResult.error !== ERRORS.UNAUTHORIZED;
|
||||
let resolveLoading;
|
||||
const loadingPromise = new Promise(resolve => {
|
||||
resolveLoading = resolve;
|
||||
});
|
||||
|
||||
let window = Services.wm.getMostRecentBrowserWindow();
|
||||
|
||||
// Note: we deliberately do not await the open.
|
||||
window.gDialogBox.open(CONTENT_SHARING_MODAL_URL, shareResult);
|
||||
window.gDialogBox.open(CONTENT_SHARING_MODAL_URL, {
|
||||
...shareResult,
|
||||
loadingPromise,
|
||||
});
|
||||
|
||||
// Note: the result object contains either the URL or an error. It's safe
|
||||
// to pass into the modal, which handles error UI as needed.
|
||||
try {
|
||||
shareResult = await this.createShareableLink(shareResult);
|
||||
shareResult.isSignedIn =
|
||||
this.isSignedIn() && shareResult.error !== ERRORS.UNAUTHORIZED;
|
||||
} finally {
|
||||
// Resolve with a new object so Lit detects the shareResult change
|
||||
resolveLoading({ ...shareResult, loadingPromise: null });
|
||||
}
|
||||
|
||||
if (shareResult.error && !shareResult.isSignedIn) {
|
||||
console.error(
|
||||
`ContentSharingUtils: failed to share ${context}`,
|
||||
|
||||
@@ -65,19 +65,25 @@ export class ContentSharingModal extends MozLitElement {
|
||||
copyButton: "#copy-button",
|
||||
viewPageButton: "#view-page",
|
||||
signInButton: "#sign-in",
|
||||
loadingButton: "#loading-button",
|
||||
tooManyLinks: ".too-many-links",
|
||||
errorMessageBar: "moz-message-bar",
|
||||
};
|
||||
|
||||
async getUpdateComplete() {
|
||||
await super.getUpdateComplete();
|
||||
await this.previewCard.updateComplete;
|
||||
await this.previewCard?.updateComplete;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
this.shareResult = window.arguments?.[0];
|
||||
if (this.shareResult?.loadingPromise) {
|
||||
this.shareResult.loadingPromise.then(result => {
|
||||
this.shareResult = result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
@@ -182,7 +188,23 @@ export class ContentSharingModal extends MozLitElement {
|
||||
);
|
||||
}
|
||||
|
||||
loadingTemplate() {
|
||||
return html`<moz-button-group
|
||||
><moz-button
|
||||
disabled
|
||||
id="loading-button"
|
||||
iconsrc="chrome://global/skin/icons/loading.svg"
|
||||
data-l10n-id="content-sharing-modal-generating-page"
|
||||
type="icon"
|
||||
></moz-button
|
||||
></moz-button-group>`;
|
||||
}
|
||||
|
||||
descriptionActionTemplate() {
|
||||
if (this.shareResult.loadingPromise) {
|
||||
return this.loadingTemplate();
|
||||
}
|
||||
|
||||
// If we got the url or
|
||||
// if there were no errors or
|
||||
// if were not signed in and got an unauthorized error
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
import { HttpServer } from "resource://testing-common/httpd.sys.mjs";
|
||||
import { NetUtil } from "resource://gre/modules/NetUtil.sys.mjs";
|
||||
|
||||
const lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
||||
});
|
||||
|
||||
const SERVER_PATH = "/api/v1/create";
|
||||
const SHARE_PATH = "/share/mockShare001";
|
||||
|
||||
@@ -21,6 +26,7 @@ class ContentSharingMockServerClass {
|
||||
#originalServerUrl = null;
|
||||
#mockResponse = null;
|
||||
#mockResponseStatus = 201;
|
||||
#responseDelay = 0;
|
||||
get url() {
|
||||
return this.#url;
|
||||
}
|
||||
@@ -47,6 +53,13 @@ class ContentSharingMockServerClass {
|
||||
this.#mockResponseStatus = value;
|
||||
}
|
||||
|
||||
get responseDelay() {
|
||||
return this.#responseDelay;
|
||||
}
|
||||
set responseDelay(value) {
|
||||
this.#responseDelay = value;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.#httpServer = new HttpServer();
|
||||
this.#httpServer.registerPathHandler(SERVER_PATH, (req, resp) =>
|
||||
@@ -99,6 +112,7 @@ class ContentSharingMockServerClass {
|
||||
this.#requests = [];
|
||||
this.#mockResponse = { url: this.#mockShareURL };
|
||||
this.#mockResponseStatus = 201;
|
||||
this.#responseDelay = 0;
|
||||
}
|
||||
|
||||
#handleRequest(httpRequest, httpResponse) {
|
||||
@@ -119,14 +133,22 @@ class ContentSharingMockServerClass {
|
||||
this.#requests.push({ body });
|
||||
|
||||
httpResponse.processAsync();
|
||||
httpResponse.setStatusLine("", this.#mockResponseStatus, "");
|
||||
|
||||
if (this.#mockResponseStatus !== 401) {
|
||||
httpResponse.setHeader("Set-Cookie", COOKIE_CONTENTS);
|
||||
const respond = () => {
|
||||
httpResponse.setStatusLine("", this.#mockResponseStatus, "");
|
||||
if (this.#mockResponseStatus !== 401) {
|
||||
httpResponse.setHeader("Set-Cookie", COOKIE_CONTENTS);
|
||||
}
|
||||
httpResponse.setHeader("Content-Type", "application/json", false);
|
||||
httpResponse.write(JSON.stringify(this.#mockResponse));
|
||||
httpResponse.finish();
|
||||
};
|
||||
|
||||
if (this.#responseDelay > 0) {
|
||||
lazy.setTimeout(respond, this.#responseDelay);
|
||||
} else {
|
||||
respond();
|
||||
}
|
||||
httpResponse.setHeader("Content-Type", "application/json", false);
|
||||
httpResponse.write(JSON.stringify(this.#mockResponse));
|
||||
httpResponse.finish();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,12 @@ prefs = [
|
||||
|
||||
["browser_testFeatureDisabled.js"]
|
||||
|
||||
["browser_testPrivateBrowsing.js"]
|
||||
["browser_testLoadingState.js"]
|
||||
|
||||
["browser_testModalErrors.js"]
|
||||
|
||||
["browser_testPrivateBrowsing.js"]
|
||||
|
||||
["browser_testServerErrors.js"]
|
||||
|
||||
["browser_testShareBookmarks.js"]
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
https://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
add_task(async function test_loadingStateWhileCreatingLink() {
|
||||
await withContentSharingMockServer(async server => {
|
||||
let tabs = [
|
||||
BrowserTestUtils.addTab(gBrowser, "https://example.com"),
|
||||
BrowserTestUtils.addTab(gBrowser, "https://example.com?1"),
|
||||
];
|
||||
|
||||
await Promise.all(
|
||||
tabs.map(async tab => {
|
||||
await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
||||
})
|
||||
);
|
||||
|
||||
server.responseDelay = 500;
|
||||
|
||||
const sharePromise = ContentSharingUtils.handleShareTabs(tabs);
|
||||
|
||||
await TestUtils.waitForCondition(() => window.gDialogBox.isOpen);
|
||||
const modalEl = await TestUtils.waitForCondition(() =>
|
||||
window.gDialogBox.dialog.frameContentWindow.document.querySelector(
|
||||
"content-sharing-modal"
|
||||
)
|
||||
);
|
||||
await TestUtils.waitForCondition(() => BrowserTestUtils.isVisible(modalEl));
|
||||
await TestUtils.waitForCondition(() => modalEl.getUpdateComplete);
|
||||
await modalEl.getUpdateComplete();
|
||||
|
||||
Assert.ok(
|
||||
BrowserTestUtils.isVisible(modalEl.loadingButton),
|
||||
"Loading button is visible while API call is in progress"
|
||||
);
|
||||
Assert.ok(
|
||||
!modalEl.copyButton,
|
||||
"Copy button is not rendered during loading"
|
||||
);
|
||||
Assert.ok(
|
||||
!modalEl.signInButton,
|
||||
"Sign in button is not rendered during loading"
|
||||
);
|
||||
Assert.ok(
|
||||
!modalEl.errorMessageBar,
|
||||
"Error message bar is not rendered during loading"
|
||||
);
|
||||
|
||||
await sharePromise;
|
||||
await TestUtils.waitForCondition(
|
||||
() => !modalEl.shareResult?.loadingPromise
|
||||
);
|
||||
await TestUtils.waitForCondition(() => modalEl.getUpdateComplete);
|
||||
await modalEl.getUpdateComplete();
|
||||
|
||||
Assert.ok(
|
||||
BrowserTestUtils.isVisible(modalEl.copyButton),
|
||||
"Copy button is visible after loading completes"
|
||||
);
|
||||
Assert.ok(
|
||||
!modalEl.loadingButton,
|
||||
"Loading button is not rendered after loading completes"
|
||||
);
|
||||
|
||||
window.gDialogBox.dialog.close();
|
||||
gBrowser.removeTabs(tabs);
|
||||
});
|
||||
});
|
||||
@@ -32,6 +32,7 @@ add_task(async function test_tooManyLinks() {
|
||||
url: server.mockResponse.url,
|
||||
isSchemaValid: true,
|
||||
isSignedIn: true,
|
||||
loadingPromise: null,
|
||||
});
|
||||
|
||||
Assert.equal(body.type, "tabs", "Share type is 'tabs'");
|
||||
@@ -91,6 +92,7 @@ add_task(async function test_genericError() {
|
||||
url: null,
|
||||
isSchemaValid: true,
|
||||
isSignedIn: true,
|
||||
loadingPromise: null,
|
||||
});
|
||||
|
||||
Assert.equal(body.type, "tabs", "Share type is 'tabs'");
|
||||
|
||||
@@ -88,6 +88,7 @@ add_task(async function test_createShareableLink() {
|
||||
url: server.mockResponse.url,
|
||||
isSchemaValid: true,
|
||||
isSignedIn: true,
|
||||
loadingPromise: null,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
@@ -64,6 +64,7 @@ add_task(async function test_handleShareTabGroup() {
|
||||
url: server.mockResponse.url,
|
||||
isSchemaValid: true,
|
||||
isSignedIn: true,
|
||||
loadingPromise: null,
|
||||
});
|
||||
|
||||
Assert.equal(body.type, "tab_group", "Share type is 'tab_group'");
|
||||
|
||||
@@ -33,6 +33,7 @@ add_task(async function test_handleShareTabs() {
|
||||
url: server.mockResponse.url,
|
||||
isSchemaValid: true,
|
||||
isSignedIn: true,
|
||||
loadingPromise: null,
|
||||
});
|
||||
|
||||
Assert.equal(body.type, "tabs", "Share type is 'tabs'");
|
||||
|
||||
@@ -63,11 +63,6 @@ async function withContentSharingMockServer(task) {
|
||||
|
||||
async function assertContentSharingModal(window, expected) {
|
||||
Assert.ok(window.gDialogBox.isOpen, "Content sharing modal should be open");
|
||||
Assert.deepEqual(
|
||||
window.gDialogBox.dialog.frameContentWindow.arguments[0],
|
||||
expected,
|
||||
"The window has the expected arguments"
|
||||
);
|
||||
|
||||
// Wait for the modal to be fully rendered
|
||||
const modalEl = await TestUtils.waitForCondition(() =>
|
||||
@@ -76,8 +71,22 @@ async function assertContentSharingModal(window, expected) {
|
||||
)
|
||||
);
|
||||
await TestUtils.waitForCondition(() => BrowserTestUtils.isVisible(modalEl));
|
||||
|
||||
// If the modal is still loading, wait for the loadingPromise to resolve
|
||||
// before asserting on the final shareResult state.
|
||||
if (modalEl.shareResult?.loadingPromise) {
|
||||
await modalEl.shareResult.loadingPromise;
|
||||
await modalEl.getUpdateComplete();
|
||||
}
|
||||
|
||||
await TestUtils.waitForCondition(() => modalEl.getUpdateComplete);
|
||||
await modalEl.getUpdateComplete();
|
||||
|
||||
Assert.deepEqual(
|
||||
modalEl.shareResult,
|
||||
expected,
|
||||
"Modal has the expected share result"
|
||||
);
|
||||
await TestUtils.waitForCondition(
|
||||
() => modalEl.links?.length === Math.min(expected.share.links.length, 3)
|
||||
);
|
||||
|
||||
@@ -22,6 +22,9 @@ content-sharing-modal-view-page =
|
||||
content-sharing-modal-copy-link =
|
||||
.label = Copy link
|
||||
|
||||
content-sharing-modal-generating-page =
|
||||
.label = Generating page…
|
||||
|
||||
content-sharing-modal-link-copied =
|
||||
.label = Link copied
|
||||
|
||||
|
||||
Reference in New Issue
Block a user