summaryrefslogtreecommitdiffstats
path: root/devtools/shared/webconsole/test/chrome/common.js
blob: 0e570ba8edf36241511110197aeaf24d41a16405 (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
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

/* exported attachConsole, attachConsoleToTab, attachConsoleToWorker,
   closeDebugger, checkConsoleAPICalls, checkRawHeaders, runTests, nextTest, Ci, Cc,
   withActiveServiceWorker, Services, consoleAPICall, createCommandsForTab, FRACTIONAL_NUMBER_REGEX, DevToolsServer */

const { require } = ChromeUtils.importESModule(
  "resource://devtools/shared/loader/Loader.sys.mjs"
);
const {
  DevToolsServer,
} = require("resource://devtools/server/devtools-server.js");
const {
  CommandsFactory,
} = require("resource://devtools/shared/commands/commands-factory.js");

// timeStamp are the result of a number in microsecond divided by 1000.
// so we can't expect a precise number of decimals, or even if there would
// be decimals at all.
const FRACTIONAL_NUMBER_REGEX = /^\d+(\.\d{1,3})?$/;

function attachConsole(listeners) {
  return _attachConsole(listeners);
}
function attachConsoleToTab(listeners) {
  return _attachConsole(listeners, true);
}
function attachConsoleToWorker(listeners) {
  return _attachConsole(listeners, true, true);
}

var _attachConsole = async function (listeners, attachToTab, attachToWorker) {
  try {
    function waitForMessage(target) {
      return new Promise(resolve => {
        target.addEventListener("message", resolve, { once: true });
      });
    }

    // Fetch the console actor out of the expected target
    // ParentProcessTarget / WorkerTarget / FrameTarget
    let commands, target, worker;
    if (!attachToTab) {
      commands = await CommandsFactory.forMainProcess();
      target = await commands.descriptorFront.getTarget();
    } else {
      commands = await CommandsFactory.forCurrentTabInChromeMochitest();
      // Descriptor's getTarget will only work if the TargetCommand watches for the first top target
      await commands.targetCommand.startListening();
      target = await commands.descriptorFront.getTarget();
      if (attachToWorker) {
        const workerName = "console-test-worker.js#" + new Date().getTime();
        worker = new Worker(workerName);
        await waitForMessage(worker);

        const { workers } = await target.listWorkers();
        target = workers.filter(w => w.url == workerName)[0];
        if (!target) {
          console.error(
            "listWorkers failed. Unable to find the worker actor\n"
          );
          return null;
        }
        // This is still important to attach workers as target is still a descriptor front
        // which "becomes" a target when calling this method:
        await target.morphWorkerDescriptorIntoWorkerTarget();
      }
    }

    // Attach the Target and the target thread in order to instantiate the console client.
    await target.attachThread();

    const webConsoleFront = await target.getFront("console");

    // By default the console isn't listening for anything,
    // request listeners from here
    const response = await webConsoleFront.startListeners(listeners);
    return {
      state: {
        dbgClient: commands.client,
        webConsoleFront,
        actor: webConsoleFront.actor,
        // Keep a strong reference to the Worker to avoid it being
        // GCd during the test (bug 1237492).
        // eslint-disable-next-line camelcase
        _worker_ref: worker,
      },
      response,
    };
  } catch (error) {
    console.error(
      `attachConsole failed: ${error.error} ${error.message} - ` + error.stack
    );
  }
  return null;
};

async function createCommandsForTab() {
  const commands = await CommandsFactory.forMainProcess();
  await commands.targetCommand.startListening();
  return commands;
}

function closeDebugger(state, callback) {
  const onClose = state.dbgClient.close();

  state.dbgClient = null;
  state.client = null;

  if (typeof callback === "function") {
    onClose.then(callback);
  }
  return onClose;
}

function checkConsoleAPICalls(consoleCalls, expectedConsoleCalls) {
  is(
    consoleCalls.length,
    expectedConsoleCalls.length,
    "received correct number of console calls"
  );
  expectedConsoleCalls.forEach(function (message, index) {
    info("checking received console call #" + index);
    checkConsoleAPICall(consoleCalls[index], expectedConsoleCalls[index]);
  });
}

function checkConsoleAPICall(call, expected) {
  is(
    call.arguments?.length || 0,
    expected.arguments?.length || 0,
    "number of arguments"
  );

  checkObject(call, expected);
}

function checkObject(object, expected) {
  if (object && object.getGrip) {
    object = object.getGrip();
  }

  for (const name of Object.keys(expected)) {
    const expectedValue = expected[name];
    const value = object[name];
    checkValue(name, value, expectedValue);
  }
}

function checkValue(name, value, expected) {
  if (expected === null) {
    ok(!value, "'" + name + "' is null");
  } else if (value === undefined) {
    ok(false, "'" + name + "' is undefined");
  } else if (value === null) {
    ok(false, "'" + name + "' is null");
  } else if (
    typeof expected == "string" ||
    typeof expected == "number" ||
    typeof expected == "boolean"
  ) {
    is(value, expected, "property '" + name + "'");
  } else if (expected instanceof RegExp) {
    ok(expected.test(value), name + ": " + expected + " matched " + value);
  } else if (Array.isArray(expected)) {
    info("checking array for property '" + name + "'");
    checkObject(value, expected);
  } else if (typeof expected == "object") {
    info("checking object for property '" + name + "'");
    checkObject(value, expected);
  }
}

function checkHeadersOrCookies(array, expected) {
  const foundHeaders = {};

  for (const elem of array) {
    if (!(elem.name in expected)) {
      continue;
    }
    foundHeaders[elem.name] = true;
    info("checking value of header " + elem.name);
    checkValue(elem.name, elem.value, expected[elem.name]);
  }

  for (const header in expected) {
    if (!(header in foundHeaders)) {
      ok(false, header + " was not found");
    }
  }
}

function checkRawHeaders(text, expected) {
  const headers = text.split(/\r\n|\n|\r/);
  const arr = [];
  for (const header of headers) {
    const index = header.indexOf(": ");
    if (index < 0) {
      continue;
    }
    arr.push({
      name: header.substr(0, index),
      value: header.substr(index + 2),
    });
  }

  checkHeadersOrCookies(arr, expected);
}

var gTestState = {};

function runTests(tests, endCallback) {
  function* driver() {
    let lastResult, sendToNext;
    for (let i = 0; i < tests.length; i++) {
      gTestState.index = i;
      const fn = tests[i];
      info("will run test #" + i + ": " + fn.name);
      lastResult = fn(sendToNext, lastResult);
      sendToNext = yield lastResult;
    }
    yield endCallback(sendToNext, lastResult);
  }
  gTestState.driver = driver();
  return gTestState.driver.next();
}

function nextTest(message) {
  return gTestState.driver.next(message);
}

function withActiveServiceWorker(win, url, scope) {
  const opts = {};
  if (scope) {
    opts.scope = scope;
  }
  return win.navigator.serviceWorker.register(url, opts).then(swr => {
    if (swr.active) {
      return swr;
    }

    // Unfortunately we can't just use navigator.serviceWorker.ready promise
    // here.  If the service worker is for a scope that does not cover the window
    // then the ready promise will never resolve.  Instead monitor the service
    // workers state change events to determine when its activated.
    return new Promise(resolve => {
      const sw = swr.waiting || swr.installing;
      sw.addEventListener("statechange", function stateHandler() {
        if (sw.state === "activated") {
          sw.removeEventListener("statechange", stateHandler);
          resolve(swr);
        }
      });
    });
  });
}

/**
 *
 * @param {Front} consoleFront
 * @param {Function} consoleCall: A function which calls the consoleAPI, e.g. :
 *                         `() => top.console.log("test")`.
 * @returns {Promise} A promise that will be resolved with the packet sent by the server
 *                    in response to the consoleAPI call.
 */
function consoleAPICall(consoleFront, consoleCall) {
  const onConsoleAPICall = consoleFront.once("consoleAPICall");
  consoleCall();
  return onConsoleAPICall;
}