summaryrefslogtreecommitdiffstats
path: root/devtools/shared/commands/resource/tests/browser_resources_network_events.js
blob: da355fd023f5e11455535dc55a253c9cfbd56fb5 (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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

// Test the ResourceCommand API around NETWORK_EVENT

const ResourceCommand = require("resource://devtools/shared/commands/resource/resource-command.js");

// We are borrowing tests from the netmonitor frontend
const NETMONITOR_TEST_FOLDER =
  "https://example.com/browser/devtools/client/netmonitor/test/";
const CSP_URL = `${NETMONITOR_TEST_FOLDER}html_csp-test-page.html`;
const JS_CSP_URL = `${NETMONITOR_TEST_FOLDER}js_websocket-worker-test.js`;
const CSS_CSP_URL = `${NETMONITOR_TEST_FOLDER}internal-loaded.css`;

const CSP_BLOCKED_REASON_CODE = 4000;

add_task(async function testContentProcessRequests() {
  info(`Tests for NETWORK_EVENT resources fired from the content process`);

  const expectedAvailable = [
    {
      url: CSP_URL,
      method: "GET",
      isNavigationRequest: true,
      chromeContext: false,
    },
    {
      url: JS_CSP_URL,
      method: "GET",
      blockedReason: CSP_BLOCKED_REASON_CODE,
      isNavigationRequest: false,
      chromeContext: false,
    },
    {
      url: CSS_CSP_URL,
      method: "GET",
      blockedReason: CSP_BLOCKED_REASON_CODE,
      isNavigationRequest: false,
      chromeContext: false,
    },
  ];
  const expectedUpdated = [
    {
      url: CSP_URL,
      method: "GET",
      isNavigationRequest: true,
      chromeContext: false,
    },
    {
      url: JS_CSP_URL,
      method: "GET",
      blockedReason: CSP_BLOCKED_REASON_CODE,
      isNavigationRequest: false,
      chromeContext: false,
    },
    {
      url: CSS_CSP_URL,
      method: "GET",
      blockedReason: CSP_BLOCKED_REASON_CODE,
      isNavigationRequest: false,
      chromeContext: false,
    },
  ];

  await assertNetworkResourcesOnPage(
    CSP_URL,
    expectedAvailable,
    expectedUpdated
  );
});

add_task(async function testCanceledRequest() {
  info(`Tests for NETWORK_EVENT resources with a canceled request`);

  // Do a XHR request that we cancel against a slow loading page
  const requestUrl =
    "https://example.org/document-builder.sjs?delay=1000&html=foo";
  const html =
    "<!DOCTYPE html><script>(" +
    function (xhrUrl) {
      const xhr = new XMLHttpRequest();
      xhr.open("GET", xhrUrl);
      xhr.send(null);
    } +
    ")(" +
    JSON.stringify(requestUrl) +
    ")</script>";
  const pageUrl =
    "https://example.org/document-builder.sjs?html=" + encodeURIComponent(html);

  const expectedAvailable = [
    {
      url: pageUrl,
      method: "GET",
      isNavigationRequest: true,
      chromeContext: false,
    },
    {
      url: requestUrl,
      method: "GET",
      isNavigationRequest: false,
      blockedReason: "NS_BINDING_ABORTED",
      chromeContext: false,
    },
  ];
  const expectedUpdated = [
    {
      url: pageUrl,
      method: "GET",
      isNavigationRequest: true,
      chromeContext: false,
    },
    {
      url: requestUrl,
      method: "GET",
      isNavigationRequest: false,
      blockedReason: "NS_BINDING_ABORTED",
      chromeContext: false,
    },
  ];

  // Register a one-off listener to cancel the XHR request
  // Using XMLHttpRequest's abort() method from the content process
  // isn't reliable and would introduce many race condition in the test.
  // Canceling the request via nsIRequest.cancel privileged method,
  // from the parent process is much more reliable.
  const observer = {
    QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
    observe(subject, topic, data) {
      subject = subject.QueryInterface(Ci.nsIHttpChannel);
      if (subject.URI.spec == requestUrl) {
        subject.cancel(Cr.NS_BINDING_ABORTED);
        Services.obs.removeObserver(observer, "http-on-modify-request");
      }
    },
  };
  Services.obs.addObserver(observer, "http-on-modify-request");

  await assertNetworkResourcesOnPage(
    pageUrl,
    expectedAvailable,
    expectedUpdated
  );
});

add_task(async function testIframeRequest() {
  info(`Tests for NETWORK_EVENT resources with an iframe`);

  // Do a XHR request that we cancel against a slow loading page
  const iframeRequestUrl =
    "https://example.org/document-builder.sjs?html=iframe-request";
  const iframeHtml = `iframe<script>fetch("${iframeRequestUrl}")</script>`;
  const iframeUrl =
    "https://example.org/document-builder.sjs?html=" +
    encodeURIComponent(iframeHtml);
  const html = `top-document<iframe src="${iframeUrl}"></iframe>`;
  const pageUrl =
    "https://example.org/document-builder.sjs?html=" + encodeURIComponent(html);

  const expectedAvailable = [
    {
      url: pageUrl,
      method: "GET",
      chromeContext: false,
      isNavigationRequest: true,
      // The top level navigation request relates to the previous top level target.
      // Unfortunately, it is hard to test because it is racy.
      // The target front might be destroyed and `targetFront.url` will be null.
      // Or not just yet and be equal to "about:blank".
    },
    {
      url: iframeUrl,
      method: "GET",
      isNavigationRequest: false,
      targetFrontUrl: pageUrl,
      chromeContext: false,
    },
    {
      url: iframeRequestUrl,
      method: "GET",
      isNavigationRequest: false,
      targetFrontUrl: iframeUrl,
      chromeContext: false,
    },
  ];
  const expectedUpdated = [
    {
      url: pageUrl,
      method: "GET",
      isNavigationRequest: true,
      chromeContext: false,
    },
    {
      url: iframeUrl,
      method: "GET",
      isNavigationRequest: false,
      chromeContext: false,
    },
    {
      url: iframeRequestUrl,
      method: "GET",
      isNavigationRequest: false,
      chromeContext: false,
    },
  ];

  await assertNetworkResourcesOnPage(
    pageUrl,
    expectedAvailable,
    expectedUpdated
  );
});

async function assertNetworkResourcesOnPage(
  url,
  expectedAvailable,
  expectedUpdated
) {
  // First open a blank document to avoid spawning any request
  const tab = await addTab("about:blank");

  const commands = await CommandsFactory.forTab(tab);
  await commands.targetCommand.startListening();
  const { resourceCommand } = commands;

  const onAvailable = resources => {
    for (const resource of resources) {
      // Immediately assert the resource, as the same resource object
      // will be notified to onUpdated and so if we assert it later
      // we will not highlight attributes that aren't set yet from onAvailable.
      const idx = expectedAvailable.findIndex(e => e.url === resource.url);
      Assert.notEqual(
        idx,
        -1,
        "Found a matching available notification for: " + resource.url
      );
      // Remove the match from the list in case there is many requests with the same url
      const [expected] = expectedAvailable.splice(idx, 1);

      assertResources(resource, expected);
    }
  };
  const onUpdated = updates => {
    for (const { resource } of updates) {
      const idx = expectedUpdated.findIndex(e => e.url === resource.url);
      Assert.notEqual(
        idx,
        -1,
        "Found a matching updated notification for: " + resource.url
      );
      // Remove the match from the list in case there is many requests with the same url
      const [expected] = expectedUpdated.splice(idx, 1);

      assertResources(resource, expected);
    }
  };

  // Start observing for network events before loading the test page
  await resourceCommand.watchResources([resourceCommand.TYPES.NETWORK_EVENT], {
    onAvailable,
    onUpdated,
  });

  // Load the test page that fires network requests
  const onLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
  BrowserTestUtils.startLoadingURIString(gBrowser.selectedBrowser, url);
  await onLoaded;

  // Make sure we processed all the expected request updates
  await waitFor(
    () => !expectedAvailable.length,
    "Wait for all expected available notifications"
  );
  await waitFor(
    () => !expectedUpdated.length,
    "Wait for all expected updated notifications"
  );

  resourceCommand.unwatchResources([resourceCommand.TYPES.NETWORK_EVENT], {
    onAvailable,
    onUpdated,
  });

  await commands.destroy();

  BrowserTestUtils.removeTab(tab);
}

function assertResources(actual, expected) {
  is(
    actual.resourceType,
    ResourceCommand.TYPES.NETWORK_EVENT,
    "The resource type is correct"
  );
  is(
    typeof actual.innerWindowId,
    "number",
    "All requests have an innerWindowId attribute"
  );
  ok(
    actual.targetFront.isTargetFront,
    "All requests have a targetFront attribute"
  );

  for (const name in expected) {
    if (name == "targetFrontUrl") {
      is(
        actual.targetFront.url,
        expected[name],
        "The request matches the right target front"
      );
    } else {
      is(actual[name], expected[name], `The '${name}' attribute is correct`);
    }
  }
}