tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

testHelpers.js (8290B)


      1 // META: script=/resources/testharness.js
      2 
      3 /**
      4 * Awaitable which returns when error occurs or the test sends back its name.
      5 * @param {*} testId - name to distinguish the test
      6 * @returns promise which resolves if test sends back its name, otherwise rejects.
      7 */
      8 const waitOutcomesForNames = async nameList => {
      9  return Promise.allSettled(
     10    nameList.map(
     11      name =>
     12        new Promise((resolve, reject) => {
     13          const bc = new BroadcastChannel(name);
     14          bc.onmessage = e => {
     15            try {
     16              if (e.data.message.startsWith(name)) {
     17                resolve("ok");
     18              } else {
     19                reject(JSON.stringify(e.data));
     20              }
     21            } catch (err) {
     22              reject(err.message);
     23            }
     24          };
     25        })
     26    )
     27  );
     28 };
     29 
     30 const expectNamesForTestWindow = (names, windowPath) => {
     31  return () => {
     32    return new Promise((resolve, reject) => {
     33      try {
     34        waitOutcomesForNames(names).then(res => {
     35          try {
     36            if (
     37              res.every(
     38                elem => elem.status === "fulfilled" && elem.value == "ok"
     39              )
     40            ) {
     41              resolve();
     42            } else {
     43              reject(res.find(elem => elem.status === "rejected").reason);
     44            }
     45          } catch (err) {
     46            reject(err.message);
     47          }
     48        });
     49        window.open(windowPath);
     50      } catch (err) {
     51        reject(err.message);
     52      }
     53    });
     54  };
     55 };
     56 
     57 /**
     58 * Tests whether the event is an automated summary from a known window.
     59 *
     60 * @param {*} e - event
     61 * @returns true if event seems to be an automated summary with a recognized name, otherwise false
     62 */
     63 const isSummary = e => {
     64  const wrappers = ["Write wrapper", "Read wrapper"];
     65  const hasName =
     66    !!e.data.tests && 1 === e.data.tests.length && !!e.data.tests[0].name;
     67  if (!hasName) {
     68    return false;
     69  }
     70  return wrappers.includes(e.data.tests[0].name);
     71 };
     72 
     73 /**
     74 * Returns expected error message when access to a storage API is denied.
     75 *
     76 * @param {*} api Shorthand for the storage API type to be tested
     77 * @returns Expected error message when access is denied.
     78 */
     79 const getAccessErrorForAPI = (api, testId) => {
     80  if (api === "IDB") {
     81    const storeName = "testObjectStore" + testId;
     82    return (
     83      "IDBDatabase.transaction: '" +
     84      storeName +
     85      "' is not a known object store name"
     86    );
     87  } else if (api === "FS") {
     88    return "Entry not found"; //"Security error when calling GetDirectory";
     89  }
     90  throw Error("Unknown API!");
     91 };
     92 
     93 const childListeners = new Map();
     94 const readPromises = new Map();
     95 const readListeners = new Map();
     96 
     97 function createMotherListener(
     98  defaultHandler = e => {
     99    childListeners.values().next().value(e);
    100    const msg =
    101      "Unexpectedly, default handler called: " + JSON.stringify(e.data);
    102    console.log(msg);
    103  }
    104 ) {
    105  // The core 'messageHub' listener that checks for a matching child ID
    106  function messageHubListener(event) {
    107    console.log("We got message with data " + JSON.stringify(event.data));
    108    const id = event.data.id;
    109    if (id) {
    110      if (event.data.message == "read loaded") {
    111        if (readListeners.has(id)) {
    112          if (!readPromises.has(id)) {
    113            throw new Error("Read window lifecycle issue");
    114          }
    115          readListeners.get(id)(event);
    116        } else {
    117          readPromises[id] = new Promise(resolve => resolve());
    118        }
    119        return;
    120      } else if (childListeners.has(id)) {
    121        childListeners.get(id)(event);
    122        return;
    123      }
    124    }
    125    defaultHandler(event);
    126  }
    127 
    128  // Start listening to window messages right away
    129  window.addEventListener("message", messageHubListener);
    130 
    131  // Return an API to register new child listeners
    132  return {
    133    registerWindow(t, testId, testAPI, expectation, setup) {
    134      if (childListeners.has(testId)) {
    135        throw new Error(`Window ID "${testId}" is already registered.`);
    136      }
    137      const handler = getWindowTestListener(
    138        t,
    139        testAPI,
    140        testId,
    141        expectation,
    142        "window",
    143        setup
    144      );
    145      childListeners.set(testId, handler);
    146      console.log("Registered window id " + testId);
    147    },
    148    registerWorker(t, testId, testAPI, expectation, setup) {
    149      if (childListeners.has(testId)) {
    150        throw new Error(`Worker ID "${testId}" is already registered.`);
    151      }
    152      const handler = getWindowTestListener(
    153        t,
    154        testAPI,
    155        testId,
    156        expectation,
    157        "worker",
    158        setup
    159      );
    160      childListeners.set(testId, handler);
    161      console.log("Registered worker id " + testId);
    162    },
    163    registerReadWindow(testId) {
    164      if (readPromises.has(testId)) {
    165        return;
    166      }
    167      readPromises.set(
    168        testId,
    169        new Promise((resolve, reject) => {
    170          readListeners.set(testId, e => {
    171            if (e.data.id != testId) {
    172              reject("Expected read id " + testId + ", actual " + e.data.id);
    173            }
    174            resolve();
    175          });
    176        })
    177      );
    178      console.log("Registered read window id " + testId);
    179    },
    180    async getReadWindow(testId) {
    181      if (!readPromises.has(testId)) {
    182        throw new Error("Read window lifecycle issue");
    183      }
    184 
    185      return readPromises.get(testId);
    186    },
    187  };
    188 }
    189 
    190 /**
    191 * Requires that writeWindows and readWindows are defined in the calling context.
    192 * @param {*} t - test object provided by the wpt test harness
    193 * @param {*} testId - name to distinguish the test
    194 * @param {*} expectation - final message from the tested iframe
    195 * @returns test step function to be used as a window listener
    196 */
    197 const getWindowTestListener = (
    198  t,
    199  testAPI,
    200  testId,
    201  expectation,
    202  contextType,
    203  setup
    204 ) => {
    205  const bc = new BroadcastChannel(testId);
    206 
    207  assert_true(["window", "worker"].includes(contextType));
    208 
    209  const readFrame = "read-frame-" + contextType;
    210 
    211  assert_true(["allow", "deny"].includes(expectation));
    212 
    213  const expectedMessage =
    214    expectation == "allow" ? testId : getAccessErrorForAPI(testAPI, testId);
    215 
    216  const ownedReadWindow = (s => {
    217    if (s.readWindows) {
    218      return s.readWindows.get(testId);
    219    }
    220    return null;
    221  })(setup);
    222 
    223  const ownedWriteWindows = setup.writeWindows;
    224 
    225  return t.step_func(e => {
    226    const here = {};
    227 
    228    try {
    229      console.log("Test listener received " + JSON.stringify(e.data));
    230      here.ownedReadWindow = ownedReadWindow;
    231      here.writeWindows = ownedWriteWindows;
    232      here.bc = bc;
    233      here.api = testAPI;
    234      here.id = testId;
    235      here.expected = expectedMessage;
    236      here.readFrame = readFrame;
    237 
    238      // Test summary is automatically sent to parent window
    239      if (isSummary(e)) {
    240        const maybeError = e.data.tests[0].message;
    241        if (maybeError) {
    242          here.bc.postMessage({ id: here.id, message: maybeError });
    243          throw new Error(maybeError);
    244        }
    245      } else {
    246        // Otherwise it should follow this protocol.
    247        if (e.data.id !== here.id) {
    248          const msg = "id " + here.id + " ignores message for id " + e.data.id;
    249          console.log(msg);
    250          here.bc.postMessage({ id: here.id, message: msg });
    251          throw new Error(msg);
    252        }
    253 
    254        assert_true(!!e.data.message);
    255        if (e.data.message === "write loaded") {
    256          const msg = { id: here.id, message: here.id, type: here.api };
    257          here.writeWindows.get(here.id).postMessage(msg, "*");
    258        } else if (e.data.message === "write done") {
    259          assert_true(!!e.data.expected); // What we wrote to the database
    260          const msg = {
    261            id: here.id,
    262            message: here.id,
    263            expected: e.data.expected, // What was written to storage
    264            outcome: here.expected, // What the iframe should send to its parent
    265            frame: here.readFrame, // Should it go to the worker or window iframe?
    266            type: here.api, // Which storage API should be tested?
    267          };
    268 
    269          here.ownedReadWindow.postMessage(msg, "*");
    270        } else {
    271          assert_equals(e.data.message, here.expected);
    272          here.bc.postMessage({ id: here.id, message: here.id });
    273          t.done();
    274        }
    275      }
    276    } catch (err) {
    277      here.bc.postMessage({ id: here.id, message: err.message });
    278      throw err;
    279    }
    280  });
    281 };