summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/extensions/test/browser/browser_html_scroll_restoration.js
blob: 8376782762330127180a7a863c5b7ffd17f5e137 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
/* eslint max-len: ["error", 80] */
"use strict";

const { AddonTestUtils } = ChromeUtils.importESModule(
  "resource://testing-common/AddonTestUtils.sys.mjs"
);

AddonTestUtils.initMochitest(this);
const server = AddonTestUtils.createHttpServer();
const TEST_API_URL = `http://localhost:${server.identity.primaryPort}/discoapi`;

const EXT_ID_EXTENSION = "extension@example.com";
const EXT_ID_THEME = "theme@example.com";

let requestCount = 0;
server.registerPathHandler("/discoapi", (request, response) => {
  // This test is expected to load the results only once, and then cache the
  // results.
  is(++requestCount, 1, "Expect only one discoapi request");

  let results = {
    results: [
      {
        addon: {
          authors: [{ name: "Some author" }],
          current_version: {
            files: [{ platform: "all", url: "data:," }],
          },
          url: "data:,",
          guid: "recommendation@example.com",
          type: "extension",
        },
      },
    ],
  };
  response.write(JSON.stringify(results));
});

add_setup(async function () {
  await SpecialPowers.pushPrefEnv({
    set: [["extensions.getAddons.discovery.api_url", TEST_API_URL]],
  });

  let mockProvider = new MockProvider();
  mockProvider.createAddons([
    {
      id: EXT_ID_EXTENSION,
      name: "Mock 1",
      type: "extension",
      userPermissions: {
        origins: ["<all_urls>"],
        permissions: ["tabs"],
      },
    },
    {
      id: EXT_ID_THEME,
      name: "Mock 2",
      type: "theme",
    },
  ]);
});

async function switchToView(win, type, param = "") {
  let loaded = waitForViewLoad(win);
  win.gViewController.loadView(`addons://${type}/${param}`);
  await loaded;
  await waitForStableLayout(win);
}

// delta = -1 = go back.
// delta = +1 = go forwards.
async function historyGo(win, delta, expectedViewType) {
  let loaded = waitForViewLoad(win);
  win.history.go(delta);
  await loaded;
  is(
    win.gViewController.currentViewId,
    expectedViewType,
    "Expected view after history navigation"
  );
  await waitForStableLayout(win);
}

async function waitForStableLayout(win) {
  // In the test, it is important that the layout is fully stable before we
  // consider the view loaded, because those affect the offset calculations.
  await TestUtils.waitForCondition(
    () => isLayoutStable(win),
    "Waiting for layout to stabilize"
  );
}

function isLayoutStable(win) {
  // <message-bar> elements may affect the layout of a page, and therefore we
  // should check whether its embedded style sheet has finished loading.
  for (let bar of win.document.querySelectorAll("message-bar")) {
    // Check for the existence of a CSS property from message-bar.css.
    if (!win.getComputedStyle(bar).getPropertyValue("--message-bar-icon-url")) {
      return false;
    }
  }
  return true;
}

function getScrollOffset(win) {
  let { scrollTop: top, scrollLeft: left } = win.document.documentElement;
  return { top, left };
}

// Scroll an element into view. The purpose of this is to simulate a real-world
// scenario where the user has moved part of the UI is in the viewport.
function scrollTopLeftIntoView(elem) {
  elem.scrollIntoView({ block: "start", inline: "start" });
  // Sanity check: In this test, a large padding has been added to the top and
  // left of the document. So when an element has been scrolled into view, the
  // top and left offsets must be non-zero.
  assertNonZeroScrollOffsets(getScrollOffset(elem.ownerGlobal));
}

function assertNonZeroScrollOffsets(offsets) {
  ok(offsets.left, "Should have scrolled to the right");
  ok(offsets.top, "Should have scrolled down");
}

function checkScrollOffset(win, expected, msg = "") {
  let actual = getScrollOffset(win);
  let fuzz = AppConstants.platform == "macosx" ? 3 : 1;
  isfuzzy(actual.top, expected.top, fuzz, `Top scroll offset - ${msg}`);
  isfuzzy(actual.left, expected.left, fuzz, `Left scroll offset - ${msg}`);
}

add_task(async function test_scroll_restoration() {
  let win = await loadInitialView("discover");

  // Wait until the recommendations have been loaded. These are cached after
  // the first load, so we only need to wait once, at the start of the test.
  await win.document.querySelector("recommended-addon-list").cardsReady;

  // Force scrollbar to appear, by adding enough space at the top and left.
  win.document.body.style.paddingTop = "200vh";
  win.document.body.style.paddingLeft = "100vw";
  win.document.body.style.width = "200vw";

  checkScrollOffset(win, { top: 0, left: 0 }, "initial page load");

  scrollTopLeftIntoView(win.document.querySelector("recommended-addon-card"));
  let discoOffsets = getScrollOffset(win);
  assertNonZeroScrollOffsets(discoOffsets);

  // Switch from disco pane to extension list

  await switchToView(win, "list", "extension");
  checkScrollOffset(win, { top: 0, left: 0 }, "initial extension list");

  scrollTopLeftIntoView(getAddonCard(win, EXT_ID_EXTENSION));
  let extListOffsets = getScrollOffset(win);
  assertNonZeroScrollOffsets(extListOffsets);

  // Switch from extension list to details view.

  let loaded = waitForViewLoad(win);
  getAddonCard(win, EXT_ID_EXTENSION).click();
  await loaded;

  checkScrollOffset(win, { top: 0, left: 0 }, "initial details view");
  scrollTopLeftIntoView(getAddonCard(win, EXT_ID_EXTENSION));
  let detailsOffsets = getScrollOffset(win);
  assertNonZeroScrollOffsets(detailsOffsets);

  // Switch from details view back to extension list.

  await historyGo(win, -1, "addons://list/extension");
  checkScrollOffset(win, extListOffsets, "back to extension list");

  // Now scroll to the bottom-right corner, so we can check whether the scroll
  // offset is correctly restored when the extension view is loaded, even when
  // the recommendations are loaded after the initial render.
  ok(
    win.document.querySelector("recommended-addon-card"),
    "Recommendations have already been loaded"
  );
  win.document.body.scrollIntoView({ block: "end", inline: "end" });
  extListOffsets = getScrollOffset(win);
  assertNonZeroScrollOffsets(extListOffsets);

  // Switch back from the extension list to the details view.

  await historyGo(win, +1, `addons://detail/${EXT_ID_EXTENSION}`);
  checkScrollOffset(win, detailsOffsets, "details view with default tab");

  // Switch from the default details tab to the permissions tab.
  // (this does not change the history).
  win.document.querySelector(".tab-button[name='permissions']").click();

  // Switch back from the details view to the extension list.

  await historyGo(win, -1, "addons://list/extension");
  checkScrollOffset(win, extListOffsets, "bottom-right of extension list");
  ok(
    win.document.querySelector("recommended-addon-card"),
    "Recommendations should have been loaded again"
  );

  // Switch back from extension list to the details view.

  await historyGo(win, +1, `addons://detail/${EXT_ID_EXTENSION}`);
  // Scroll offsets are not remembered for the details view, because at the
  // time of leaving the details view, the non-default tab was selected.
  checkScrollOffset(win, { top: 0, left: 0 }, "details view, non-default tab");

  // Switch back from the details view to the disco pane.

  await historyGo(win, -2, "addons://discover/");
  checkScrollOffset(win, discoOffsets, "after switching back to disco pane");

  // Switch from disco pane to theme list.

  // Verifies that the extension list and theme lists are independent.
  await switchToView(win, "list", "theme");
  checkScrollOffset(win, { top: 0, left: 0 }, "initial theme list");

  await closeView(win);
});