master 7642062f0b43 cached
74 files
438.3 KB
180.9k tokens
83 symbols
1 requests
Download .txt
Showing preview only (462K chars total). Download the full file or copy to clipboard to get everything.
Repository: udacity/frontend-grading-engine
Branch: master
Commit: 7642062f0b43
Files: 74
Total size: 438.3 KB

Directory structure:
gitextract_tfrp3au2/

├── .gitignore
├── .jscsrc
├── .jshintrc
├── README.md
├── chromium/
│   ├── README.md
│   ├── browser_action/
│   │   ├── intro.js
│   │   └── outro.js
│   ├── inject/
│   │   ├── intro.js
│   │   └── outro.js
│   ├── manifest.json
│   └── options/
│       ├── intro.js
│       └── outro.js
├── firefox/
│   ├── README.md
│   ├── background.js
│   ├── browser_action/
│   │   ├── intro.js
│   │   └── outro.js
│   ├── inject/
│   │   ├── intro.js
│   │   └── outro.js
│   ├── manifest.json
│   └── options/
│       ├── intro.js
│       └── outro.js
├── gulpfile.js
├── lib/
│   └── components.js
├── license
├── package.json
├── safari/
│   ├── Info.plist
│   ├── README.md
│   ├── background/
│   │   ├── REAMDE.md
│   │   ├── adapter.js
│   │   ├── adapterListener.js
│   │   ├── background.html
│   │   ├── background.js
│   │   ├── helpers.js
│   │   └── registry.js
│   ├── browser_action/
│   │   ├── intro.js
│   │   └── outro.js
│   ├── inject/
│   │   ├── intro.js
│   │   └── outro.js
│   └── options/
│       ├── intro.js
│       └── outro.js
├── sample/
│   ├── index.html
│   ├── test.json
│   └── unit-tests.js
└── src/
    ├── app/
    │   ├── README.md
    │   ├── browser_action/
    │   │   ├── browser_action.html
    │   │   └── browser_action.js
    │   ├── css/
    │   │   ├── common.css
    │   │   ├── fonts/
    │   │   │   └── OFL.txt
    │   │   ├── fonts.css
    │   │   ├── options.css
    │   │   └── ui.css
    │   ├── js/
    │   │   ├── inject/
    │   │   │   ├── StateManager.js
    │   │   │   ├── helpers.js
    │   │   │   └── inject.js
    │   │   └── libs/
    │   │       └── jsgrader.js
    │   ├── options/
    │   │   ├── index.html
    │   │   └── options.js
    │   └── test_widget/
    │       ├── active_test.js
    │       ├── font.js
    │       ├── test_results.js
    │       ├── test_suite.js
    │       └── test_widget.js
    └── js/
        ├── ActiveTest.js
        ├── GradeBook.js
        ├── Queue.js
        ├── README.md
        ├── Suite.js
        ├── TACollectors.js
        ├── TAReporters.js
        ├── Target.js
        ├── helpers.js
        ├── intro.js
        ├── outro.js
        └── registrar.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
build/
.DS_STORE
node_modules/
ext.zip
TODO
GE.min.js
GE.js
.dir-locals.el
\#*\#
GTAGS
GRTAGS
GPATH
experiments/
.tern-port
.tern-project
log.md
build.zip
global.js


================================================
FILE: .jscsrc
================================================
{
  "preset": "google",
  "disallowSpacesInAnonymousFunctionExpression": null,
  "excludeFiles": ["node_modules/**"]
}


================================================
FILE: .jshintrc
================================================
{
  "node": true,
  "browser": true,
  "bitwise": true,
  "camelcase": true,
  "curly": true,
  "eqeqeq": true,
  "immed": true,
  "indent": 2,
  "latedef": true,
  "noarg": true,
  "quotmark": "single",
  "undef": true,
  "unused": true,
  "newcap": false,
  "globals": {
    "Polymer": true,
    "Platform": true,
    "Promise": true,
    "MathAndPhysics": true,
    "CookieManager": true,
    "Model": true,
    "window": true,
    "document": true
  }
}


================================================
FILE: README.md
================================================
# Udacity Feedback Chrome Extension

Immediate, visual feedback about any website's HTML, CSS and JavaScript.

* Not sure what this is? Try the [walkthrough](http://labs.udacity.com/udacity-feedback-extension/).
* Just want to install? Visit the [Chrome Web Store](https://chrome.google.com/webstore/detail/udacity-front-end-feedbac/melpgahbngpgnbhhccnopmlmpbmdaeoi).

## Installing from Source

1. Clone this repo
2. `npm install` - install dependencies
3. `gulp watch` or just `gulp` - build the grading engine
4. [Load in Chrome](https://developer.chrome.com/extensions/getstarted#unpacked)
  * Open the Extensions window (`chrome://extensions`)
  * Check 'Developer Mode'
  * Click 'Load unpacked extension...'
  * Select `ext/`

[**More on development**](#how-udacity-feedback-works)

## Loading Tests

### On sites you own

Add the following meta tag:

```html
<meta name="udacity-grader" content="relative_path_to_tests.json">
```

There are two optional attributes: `libraries` and `unit-tests`. `libraries` is always optional and `unit-tests` is only necessary for JS quizzes. More on JS tests [here](#js-tests).

### On sites you don't own

Click on the Udacity browser action. Choose 'Load tests' and navigate to a JSON.

## API

### JSON

Typical structure is an array of:

    suite
    |_name
    |_code
    |_tests
      |_description
      |_definition
      | |_nodes
      | |_collector
      | |_reporter
      |
      |_[flags]

Example:

```javascript
[{
  "name": "Learning Udacity Feedback",
  "code": "This can be an encouraging message",
  "tests": [
    {
      "description": "Test 1 has correct bg color",
      "definition": {
        "nodes": ".test1",
        "cssProperty": "backgroundColor",
        "equals": "rgb(204, 204, 255)"
      }
    }
  ]
},
{
  "name": "More 'dacity Feedback",
  "code": "Some message",
  "tests": [
    {
      "description": "Test 2 says 'Hello, world!'",
      "definition": {
        "nodes": ".test2",
        "get": "innerHTML",
        "hasSubstring": "^Hello, world!$"
      }
    },
    {
      "description": "Test 3 has two columns",
      "definition": {
        "nodes": ".test4",
        "get": "count",
        "equals": 2
      }
    },
    {
      "description": "Test 4 has been dispatched",
      "definition": {
        "waitForEvent": "ud-test",
        "exists": true
      },
      "flags": {
        "noRepeat": true
      }
    }
  ]
}]
```

*Note that the feedback JSON must be an array of objects*

* `"name"`: the name of the suite. The word "Test" or "Tests" gets appended when this name shows up in the widget as a heading for its child tests.
* `"code"`: a message to display when all tests in the suite pass. Why is it called a code? It's sometimes the code I make students copy and paste into a quiz on the Udacity site to prove they finished the quiz.
* `"tests"`: an array of test objects
  * `"description"`: shows up in the test widget. Try to keep titles short, as long titles don't wrap well in the current version.
  * `"definition"`: an object with collector and reporter properties. More on this below.
  * `"flags"`: optional flags to alter the way a test is run. The most common is `noRepeat`, which ensures that a test runs only once rather than repeatedly.

### How to write a `"definition"`

Think about this sentence as you write tests:

> I want the nodes of [selector] to have [some property] that [compares to some value].

#### 1) Start with `"nodes"`. Most* tests against the DOM need some nodes to examine. This is the start of a "collector".

```javascript
"definition": {
  "nodes": "selector",
  ...
}
```

**Exceptions: collecting a user-agent string, device pixel ratio, or in conjunction with `"waitForEvent"`.*

#### 2) Decide what value you want to collect and test.

**CSS:**

```javascript
"definition": {
  "nodes": ".anything"
  "cssProperty": "backgroundColor",
  ...
}
```

The `"cssProperty"` can be the camelCase version of [any CSS property](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Properties_Reference). `"cssProperty"` takes advantage of `window.getComputedStyle()`.

* Colors will be returned in the form of `"rgb(255, 255, 255)"`. Note the spaces.
* All `width`, `margin`, etc measurements are returned as pixel values, not percentages.

**Attribute:**

```javascript
"definition": {
  "nodes": "input"
  "attribute": "for",
  ...
}
```

Any attribute works.

**Absolute Position:**

```javascript
"definition": {
  "nodes": ".left-nav"
  "absolutePosition": "side",
  ...
}
```

Side must be one of: `top`, `left`, `bottom`, or `right`. Currently, the position returned is relative to the **viewport**.

**Count, innerHTML, ChildPosition, UAString (user-agent string), DPR (device pixel ratio):**

```javascript
"definition": {
  "nodes": ".box"
  "get": "count",
  ...
}
```

These tests (`"count"`, `"innerHTML"`, `"childPositions"`, `"UAString"`, `"DPR"`) use `"get"` and they are the only tests that use `"get"`. Remember the asterix from earlier about the necessity of `"nodes"`? Device pixel ratios and user-agent strings are exceptions - you can `"get": "UAString"` or `"get": "DPR"` without `"nodes"`.

"Child position? I haven't seen anything about children." - a question you might be asking yourself. Let me answer it.

**Children**

```javascript
"definition": {
  "nodes": ".flex-container",
  "children": "div",
  "absolutePosition": "side",
  ...
}
```

`"children"` is a deep children selector.

In this example, it was used to select all the divs inside a flex container. Now, reporters will run tests against all of the child divs, not the parent flex container.

#### 3) Decide how you want to grade the values you just collected. This is a "reporter".

**Equals**

```javascript
"definition": {
  "nodes": ".flex",
  "get": "count",
  "equals": 4
}
```
 
or

```javascript
"definition": {
  "nodes": "input.billing-address",
  "attribute": "for",
  "equals": "billing-address"
}
```

or

```javascript
"definition": {
  "nodes": ".inline",
  "cssProperty": "display",
  "equals": [
    "inline",
    "inline-block"
  ]
}

```

Set the `"equals"` value to a string, a number, or an array of strings and numbers.

This test looks for a strict equality match of either the value or one of the values in the array. In the first example, the test passes when the count of nodes returned by the selector equals four. In the second, the `for` attribute of `<input class="billing-address">` must be set to `"billing-address"`. In the third example, the test passes if `display` is either `inline` or `inline-block`.

If you want to compare strings and would prefer to use regex, try `"hasSubstring"`.

**Exists**

```javascript
"definition": {
  "nodes": "input.billing-address",
  "attribute": "for",
  "exists": true
}
```

In this example, rather than looking for a specific `for`, I'm just checking to see that it exists at all. The value doesn't matter. If `"exists": false`, then the test will only pass if the attribute does not exist.

**Comparison**

```javascript
"definition": {
  "nodes": ".flex",
  "get": "count",
  "isLessThan": 4
}
```

`"isLessThan"` and `"isGreaterThan"` share identical behavior.

```javascript
  "definition": {
    "nodes": ".flex",
    "get": "count",
    "isInRange": {
      "lower": 4,
      "upper": 10
    }
  }
```

Set an `"upper"` and a `"lower"` value for `"isInRange"`.

**Substrings**

```javascript
"definition": {
  "nodes": ".text",
  "get": "innerHTML",
  "hasSubstring": "([A-Z])\w+"
}
```

Run regex tests against strings with `"hasSubstring"`. If one or more match groups are returned, the test passes.

**NOTE**: you must escape `\`s! eg. `\wHello` will break, but `\\wHello` will work.

You can pass an array to `"hasSubstring"` if you want to match one regex out of many.

```javascript
"definition": {
  "nodes": ".text",
  "get": "innerHTML",
  "hasSubstring": ["([A-Z])\w+", "([a-z])\w+"]
}
```

There is an alternate syntax with optional configs for `"hasSubstring"`.

```javascript
"definition": {
  "nodes": ".text",
  "get": "innerHTML",
  "hasSubstring": {
    "expected": [
      "([A-Z])\\w+",
      "$another^"
    ],
    "minValues": 1,
    "maxValues": 2
  }
}
```

This test checks that either one or both of the expected values are found.

If you set `"hasSubstring"` to an object with an array of `"expected"` regexes, you can optionally use `"minValues"` and `"maxValues"` to determine how many of the expected values need to match in order for the test to pass. Unless otherwise specified, only one value from the `"expected"` array will need to be matched in order for the test to pass.

**Utility Properties - `not`, `limit`**

```javascript
"definition": {
  "nodes": ".text",
  "get": "innerHTML",
  "not": true,
  "hasSubstring": "([A-Z])\\w+"
}
```

Switch behavior with `"not"`. A failing test will now pass and vice versa.

```javascript
"definition": {
  "nodes": ".title",
  "cssProperty": "marginTop",
  "limit": 1,
  "equals": 10
}
```

Currently, the values supported by `"limit"` are any number >= 1, `"all"` (default), and `"some"`.

Remember, by default every node collected by `"nodes"` or `"children"` must pass the test specified. To change that, use `"limit"`. If it is any number, `0 < numberCorrect <= limit` values must pass in order for the test to pass. If more than one value passes, the test fails. In the case of `"some"`, one or more values must pass in order for the test to pass.

**Flags**

```javascript
"definition": {
  "nodes": ".small",
  "cssProperty": "borderLeft",
  "equals": 10
},
"flags": {
  "noRepeat": true
}
```

Options here currently include `"noRepeat"` and `"alwaysRun"`.

By default, a test runs every 1000ms until it either passes or encounters an error. If `"noRepeat"` is set, the test only runs once when the widget loads and does not rerun every 1000ms. If `"alwaysRun"`, the test continues to run even after it passes.

###<a name="js-tests"></a> JavaScript Tests

```javascript
"definition": {
  "waitForEvent": "custom-event",
  "exists": true
}
```

For security reasons, you can only run JavaScript tests against pages that you control. You can trigger tests by dispatching custom events from inside your application or from a script linked in the `unit-tests` attribute of the meta tag. Set `"waitForEvent"` to a custom event. As soon as the custom event is detected, the test passes.

Example of a custom event:

```javascript
window.dispatchEvent(new CustomEvent('ud-test', {'detail': 'passed'}));
```

I like to use the [jsgrader library](https://github.com/udacity/js-grader) for writing JS tests because it supports grading checkpoints (logic to say "stop grading if this test fails"). You can use it too by setting `libraries="jsgrader"` in the meta tag.].

### Test States and Debugging

![wrong answer, right answer, error](images/wrong-right-error.png)

Green tests with ✓ have passed, red tests with ✗ have failed and yellow tests with ?? have some kind of error. If there is an error, run `UdacityFEGradingEngine.debug();` from the console to see why the yellow tests are erring.

You can find an options page in chrome://extensions. Use the options page to see and modify the list of domains on which the extension will run.

## How Udacity Feedback Works

At the core of Udacity Feedback is the grading engine. The grading engine performs two tasks: collecting information from the DOM and reporting on it. Each test creates its own instance of the grading engine which queries the DOM once a second (unless otherwise specified).

### Overview of the source code:

* **TA** (Teaching Assistant). The TA orchestrates the DOM querying and comparison logic of the grading engine. There is a collection aspect (src/js/TACollectors.js) and a reporting aspect (src/js/TAReporters.js). Collectors pull info from the DOM. Reporters are responsible for the logic of evaluating the information. The TA executes tests as a series of async methods pulled from a Queue.
* **Gradebook**. Every TA has an instance of a Gradebook, which determines the pass/fail state of a test. Some tests have multiple parts (eg. examining every element of some class to ensure that all have a blue background - each element is a part of the test). The Gradebook compares the parts to the comparison functions as set by the TA and decides if the test has passed or failed.
* **Target**. A Target represents a single piece of information pulled from the DOM. *Almost* every Target has an associated `element` and some `value`. Targets may include child Targets. Tests that result in multiple pieces of information create a tree of Targets (sometimes called a 'Bullseye' in comments).
* **Suite** and **ActiveTest**. An individual test (ie. one line in the widget) is an instance of an ActiveTest. ActiveTests are organized into Suites. Each Suite comes with its own name, which is displayed above its set of tests in the widget.
* **Registrar**. This file contains the logic for creating new tests when the Feedback is turned on and removing tests when the Feedback is turned off.
* The `<test-widget>` and everything inside of it were built as custom elements with HTML imports.

### Development Workflow

1. Run `gulp watch` from `/`
2. Make changes.
3. Open `/sample/index.html` to run regression testing.
4. If you're adding a new feature:
  * Add new passing and failing tests to `/sample/tests.json` (and modify `/sample/index.html` if necessary).
  * Update this README to reflect changes (include examples!).
5. Submit a pull request!

Did you read this far? You're awesome :)

 # Archival Note 
 This repository is deprecated; therefore, we are going to archive it. However, learners will be able to fork it to their personal Github account but cannot submit PRs to this repository. If you have any issues or suggestions to make, feel free to: 
- Utilize the https://knowledge.udacity.com/ forum to seek help on content-specific issues. 
- Submit a support ticket along with the link to your forked repository if (learners are) blocked for other reasons. Here are the links for the [retail consumers](https://udacity.zendesk.com/hc/en-us/requests/new) and [enterprise learners](https://udacityenterprise.zendesk.com/hc/en-us/requests/new?ticket_form_id=360000279131).

================================================
FILE: chromium/README.md
================================================
## Chromium interface
### Description
This directory contains files needed to build the extension for Chromium based browsers (Google Chrome).

### Prerequisites
Chrome natively support the WebExtensions API. Really old versions may even be supported.

### License
This directory and the whole project is subject to the [GPLv3 License](../license).


================================================
FILE: chromium/browser_action/intro.js
================================================
/**
 * @fileOverview This file contains the Chromium opening statements for the browser action script prepended to the main file.
 * @name intro.js<browser_action>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

// intro.js<browser_action> ends here


================================================
FILE: chromium/browser_action/outro.js
================================================
/**
 * @fileOverview This file contains the Chromium closing statements for the browser action script appended to the main file.
 * @name outro.js<browser_action>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

// outro.js<browser_action> ends here
// browser_action.js ends here


================================================
FILE: chromium/inject/intro.js
================================================
/**
 * @fileOverview This file contains the Chromium opening statements for the content script prepended to the main file.
 * @name intro.js<inject>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

// intro.js<browser_action> ends here


================================================
FILE: chromium/inject/outro.js
================================================
/**
 * @fileOverview This file contains the Chromium closing statements for the content script appended to the main file.
 * @name intro.js<inject>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

// outro.js<inject> ends here
// inject.js ends here


================================================
FILE: chromium/manifest.json
================================================
{
  "name": "Udacity Front End Feedback",
  "short_name": "Udacity Feedback",
  "version": "0.3.0",
  "manifest_version": 2,
  "description": "Immediate, visual feedback about any website's HTML, CSS and JavaScript",
  "homepage_url": "http://github.com/udacity/frontend-grading-engine",
  "icons": {
    "16": "icons/icon.png",
    "32": "icons/Icon-32.png",
    "48": "icons/Icon-48.png",
    "64": "icons/Icon-64.png",
    "128": "icons/Icon-128.png"
  },
  "options_page": "app/options/index.html",
  "browser_action": {
    "default_icon": "icons/Icon-48.png",
    "default_title": "Udacity Feedback",
    "default_popup": "app/browser_action/browser_action.html"
  },
  "permissions": [
    "tabs",
    "storage"
  ],
  "content_scripts": [
    {
      "matches": [
        "http://*/*",
        "https://*/*",
        "file://*/*"
      ],
      "js": [
        "app/js/inject.js"
      ]
    }
  ],
  "web_accessible_resources": [
    "app/js/*.js",
    "app/js/libs/GE.js",
    "app/js/libs/jsgrader.js",
    "app/templates/templates.js",
    "lib/components.js"
  ]
}


================================================
FILE: chromium/options/intro.js
================================================
/**
 * @fileOverview This file contains the Chromium opening statements for the options page script prepended to the main file.
 * @name intro.js<options>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

var browserName = 'Chrome';

// intro.js<options> ends here


================================================
FILE: chromium/options/outro.js
================================================
/**
 * @fileOverview This file contains the Chromium closing statements for the options page script appended to the main file.
 * @name outro.js<options>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

// outro.js<options> ends here
// options.js ends here



================================================
FILE: firefox/README.md
================================================
## Firefox interface
### Description
This directory contains files needed to build the extension for Firefox based browsers (may include IceWeasel or IceCat).

While the WebExtensions API is well supported in Chromium, Firefox needs to have specific quirks.

### Incompatibilities
Because Firefox doesn’t yet support some features and that backward compatibility was preferred, here are some issues:

* Firefox doesn’t support the `sync` `StorageArea` (i.e. `chrome.storage.sync.*`). A wrapper was made to instead `local` `StorageArea`.
  - Before version 48, Firefox didn’t support the `chrome.storage` API in _content scripts_. The above wrapper is replaced with a message channel between the _event page_ (background page) and the _content script_.
* Before version 48, Firefox didn’t support the _options page_ (or `options\_ui`). An icon is added in the _action page_ to access a page to modify the settings. That’s currently the only way to access the options prior to version 48.

* Firefox and Chromium don’t seem to handle promises the same way. For example, Firefox couldn’t load the JSON tests before the execution of unit tests.

### Prerequisites
Firefox 45 or higher is needed to support WebExtensions.

### Building
* TODO

## License
This directory and the whole project is subject to the [GPLv3 License](../license).


================================================
FILE: firefox/background.js
================================================
/*global chrome */

/**
 * @fileOverview This file adds support for the {@link chrome.storage.local} API in Firefox. This API isn’t implemented until Firefox version 48 for content-scripts.
 * @name background.js<firefox>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
  // debugger;
  // console.group();
  // console.log("sendResponse = ", sendResponse.toString());
  // console.log("sender = ", sender);
  // console.log("message = ", message);
  // console.groupEnd();

  if(!message)  {
    Promise.reject();
  }

  switch(message.type) {
  case 'chrome.storage.local.get':
    chrome.storage.local.get(message.data, function(response) {
      // debugger;
      sendResponse(response);
    });
    break;

  case 'chrome.storage.local.set':
    chrome.storage.local.set(message.data, function(response) {
      // debugger;
      response = chrome.runtime.lastError ? {status: 1, error: chrome.runtime.lastError.message} : {status: 0};
      sendResponse(response);
    });
    break;
  };
  return true;
});

// background.js<firefox> ends here


================================================
FILE: firefox/browser_action/intro.js
================================================
/*global chrome */

/**
 * @fileOverview This file contains the Firefox opening statements for the browser action script prepended to the main file.
 * @name intro.js<browser_action>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

chrome.runtime.openOptionsPage = function() {
  chrome.tabs.create({url: chrome.extension.getURL('app/options/index.html')});
};

// intro.js<browser_action> ends here


================================================
FILE: firefox/browser_action/outro.js
================================================
/**
 * @fileOverview This file contains the Firefox closing statements for the content script appended to the main file.
 * @name outro.js<browser_action>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

// outro.js<browser_action> ends here
// browser_action.js ends here


================================================
FILE: firefox/inject/intro.js
================================================
/*global chrome */

/**
 * @fileOverview This file contains the Firefox opening statements for the content script prepended to the main file.
 * @name intro.js<inject>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

// chrome = browser;
chrome.storage = {
  sync: {
    /**
     * Gets one or more items from storage.
     * @param {string|string[]|object} [keys] - A single key to get, list of keys to get, or a dictionary specifying default values (see description of the object). An empty list or object will return an empty result object. Pass in null to get the entire contents of storage
     * @param {function} callback - Callback with storage items, or on failure (in which case runtime.lastError will be set).
     */
    get: function(keys, callback) {
      // debugger;
      // console.log(callback);
      // console.log(callback.toString());
      var message = {};
      message.data = keys;
      message.type = 'chrome.storage.local.get';

      chrome.runtime.sendMessage(null, message, {}, localHandleGet);

      function localHandleGet(response) {
        // debugger;
        // console.log('localHandleGet');
        callback(response);
      }
    },
    set: function(object, callback) {
      // debugger;
      // console.log("chrome.storage.sync.set object = ", object);
      var message = {};

      message.type = 'chrome.storage.local.set';
      message.data = object;
      chrome.runtime.sendMessage(null, message, {}, localHandleSet);
      function localHandleSet(response) {
        // debugger;
        // console.log('localHandleSet');
        if(response.status) {
          throw new Error('Error: ' + response.message);
        }
        callback();
      }
    }
  }
};

// intro.js<inject> ends here


================================================
FILE: firefox/inject/outro.js
================================================
/**
 * @fileOverview This file contains the Firefox closing statements for the content script appended to the main file.
 * @name intro.js<inject>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

// outro.js<inject> ends here
// inject.js ends here


================================================
FILE: firefox/manifest.json
================================================
{
  "name": "Udacity Front End Feedback",
  "version": "0.3.0",
  "applications": {
    "gecko": {
      "id": "udacityfeedback@udacity.com",
      "strict_min_version": "45.0"
    }
  },
  "manifest_version": 2,
  "description": "Immediate, visual feedback about any website's HTML, CSS and JavaScript",
  "homepage_url": "http://github.com/udacity/frontend-grading-engine",
  "icons": {
    "16": "icons/icon.png",
    "32": "icons/Icon-32.png",
    "48": "icons/Icon-48.png",
    "64": "icons/Icon-64.png",
    "128": "icons/Icon-128.png"
  },
  "options_ui": {
    "page": "app/options/index.html"
  },
  "browser_action": {
    "default_icon": "icons/Icon-48.png",
    "default_title": "Udacity Feedback",
    "default_popup": "app/browser_action/browser_action.html"
  },
  "background": {
    "scripts": ["background.js"]
  },
  "permissions": [
    "tabs",
    "storage"
  ],
  "content_scripts": [
    {
      "matches": [
        "http://*/*",
        "https://*/*",
        "file://*/*"
      ],
      "js": [
        "app/js/inject.js"
      ]
    }
  ],
  "web_accessible_resources": [
    "app/css/common.css",
    "app/css/fonts/fontawesome-webfont.ttf",
    "app/js/libs/GE.js",
    "app/js/libs/jsgrader.js",
    "app/templates/templates.js",
    "lib/components.js"
  ]
}


================================================
FILE: firefox/options/intro.js
================================================
/*global chrome, browser */

/**
 * @fileOverview This file contains the Firefox opening statements for the options page script prepended to the main file.
 * @name intro.js<options>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

var browserName = 'Firefox';

chrome = browser;
chrome.storage = {
  sync: {
    /**
     * Gets one or more items from storage.
     * @param {string|string[]|object} [keys] - A single key to get, list of keys to get, or a dictionary specifying default values (see description of the object). An empty list or object will return an empty result object. Pass in null to get the entire contents of storage
     * @param {function} callback - Callback with storage items, or on failure (in which case runtime.lastError will be set).
     * @returns {}
     */
    get: function(keys, callback) {
      // debugger;
      // console.log(callback);
      // console.log(callback.toString());
      var message = {};
      message.data = keys;
      message.type = 'chrome.storage.local.get';

      chrome.runtime.sendMessage(null, message, {}, localHandleGet);

      function localHandleGet(response) {
        // debugger;
        // console.log('localHandleGet');
        callback(response);
      }
    },
    set: function(object, callback) {
      // debugger;
      // console.log("chrome.storage.sync.set object = ", object);
      var message = {};

      message.type = 'chrome.storage.local.set';
      message.data = object;
      chrome.runtime.sendMessage(null, message, {}, localHandleSet);
      function localHandleSet(response) {
        // debugger;
        // console.log('localHandleSet');
        if(response.status) {
          throw new Error('Error: ' + response.message);
        }
        callback();
      }
    }
  }
};

// intro.js<options> ends here


================================================
FILE: firefox/options/outro.js
================================================
/**
 * @fileOverview This file contains the Firefox closing statements for the options page script appended to the main file.
 * @name outro.js<options>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

// outro.js<options> ends here
// options.js ends here


================================================
FILE: gulpfile.js
================================================
var gulp = require('gulp');
var gulpsync = require('gulp-sync')(gulp);
var watch = require('gulp-watch');
var concat = require('gulp-concat');
var debug = require('gulp-debug');
var batch = require('gulp-batch');
// var uglify = require('gulp-uglify');
var clean = require('gulp-clean');
var mv = require('mv');

var currentBrowser;

var build = './build/%target%/ext/';

var log = function(message) {
  console.log('\x1b[37;46m####       ' + message + '\x1b[0;m');
};

function setBrowser(browser) {
  currentBrowser = browser;
  return log('Set ' + currentBrowser + ' as the current browser');
}

var pageFiles = {
  gradingEngine: {
    src: [
      'src/js/intro.js',
      'src/js/helpers.js',
      'src/js/Queue.js',
      'src/js/Target.js',
      'src/js/GradeBook.js',
      'src/js/TACollectors.js',
      'src/js/TAReporters.js',
      'src/js/ActiveTest.js',
      'src/js/Suite.js',
      'src/js/registrar.js',
      'src/js/outro.js'
    ],
    libraries: {
      src: 'src/app/js/libs/*.js',
      dest: build + 'app/js/libs/'
    },
    concat: 'GE.js',
    dest: build + 'app/js/libs/'
  },
  libraries: {
    src: 'lib/*',
    dest: build + 'lib/'
  },
  background: {
    js: {
      src: [
        '%target%/background.js'
      ],
      concat: 'background.js'
    },
    dest: build
  },
  inject: {
    src: [
      '%target%/inject/intro.js',
      'src/app/js/inject/helpers.js',
      'src/app/js/inject/StateManager.js',
      'src/app/js/inject/inject.js',
      '%target%/inject/outro.js'
    ],
    concat: 'inject.js',
    dest: build + 'app/js/'
  },
  templates: {
    src: [
      'src/app/test_widget/font.js',
      'src/app/test_widget/test_suite.js',
      'src/app/test_widget/test_results.js',
      'src/app/test_widget/active_test.js',
      'src/app/test_widget/test_widget.js'
    ],
    concat: 'templates.js',
    dest: build + 'app/templates/'
  },
  // Safari background script
  globalPage: {
    js: {
      src: [
        '%target%/background/helpers.js',
        '%target%/background/registry.js',
        '%target%/background/adapter.js',
        '%target%/background/adapterListener.js',
        '%target%/background/background.js'
      ],
      concat: 'background.js'
    },
    html: {
      src: [
        '%target%/background/background.html'
      ],
      concat: 'background.html'
    },
    dest: build
  }
};

var gradingEngine = pageFiles.gradingEngine;
// Third-party libraries
var libraries = pageFiles.libraries;
var geLibs = gradingEngine.libraries;
var background = pageFiles.background;
var global = pageFiles.globalPage;
var inject = pageFiles.inject;
var templates = pageFiles.templates;

var browserPageFiles = {
  pageAction: {
    html: 'src/app/browser_action/browser_action.html',
    js: {
      src: [
        '%target%/browser_action/intro.js',
        'src/app/browser_action/browser_action.js',
        '%target%/browser_action/outro.js'
      ],
      concat: 'browser_action.js'
    },
    dest: build + 'app/browser_action/'
  },
  pageOptions: {
    html: 'src/app/options/index.html',
    js: {
      src: [
        '%target%/options/intro.js',
        'src/app/options/options.js',
        '%target%/options/outro.js'
      ],
      concat: 'options.js'
    },
    dest: build + 'app/options/'
  }
};
var pageAction = browserPageFiles.pageAction;
var pageOptions = browserPageFiles.pageOptions;

var iconFiles = {
  src: [
    'src/icons/icon.png',
    'src/icons/Icon-32.png',
    'src/icons/Icon-48.png',
    'src/icons/Icon-64.png',
    'src/icons/Icon-128.png'
  ],
  dest: build + 'icons/'
};

var styleFiles = {
  src: [
    'src/app/css/common.css',
    'src/app/css/fonts.css',
    'src/app/css/ui.css',
    'src/app/css/options.css'
  ],
  dest: build + 'app/css/'
};

var fontFiles = {
  src: 'src/app/css/fonts/fontawesome-webfont.ttf',
  dest: build + 'app/css/fonts/'
};

// Files to watch
var allFiles = gradingEngine.src.concat(templates.src, inject.src, background.js.src, global.js.src);

// "GE" = Build the GradingEngine library.
gulp.task('GE', function() {
  return gulp.src(gradingEngine.src)
    .pipe(concat(gradingEngine.concat))
    .pipe(gulp.dest(gradingEngine.dest))
    .pipe(debug({title: 'built the grading engine:'}));
});

// "GE_libs" = Copy libraries of the Grading Engine.
gulp.task('GE_libs', function() {
  return gulp.src(geLibs.src)
    .pipe(gulp.dest(geLibs.dest))
    .pipe(debug({title: 'copied grading engine libraries:'}));
});

// "libraries" = Copy third-party libraries.
gulp.task('libraries', function() {
  return gulp.src(libraries.src)
    .pipe(gulp.dest(libraries.dest))
    .pipe(debug({title: 'copied libraries:'}));
});

// "templates" = Generate the native templates. There were
// previously Web Templates.
gulp.task('templates', function() {
  return gulp.src(templates.src)
    .pipe(concat(templates.concat))
    .pipe(gulp.dest(templates.dest))
    .pipe(debug({title: 'built templates: '}));
});

// "inject" = Generate the inject script for the current browser and copy.
gulp.task('inject', function() {
  var files = inject.src.map(function(x) {
    return x.replace('%target%', currentBrowser);
  });
  return gulp.src(files)
    .pipe(concat(inject.concat))
    .pipe(gulp.dest(inject.dest))
    .pipe(debug({title: 'built inject.js:'}));
});

/*** PAGEACTION ***/
// "_pageAction_html" = Copy HTML options page.
gulp.task('_pageAction_html', function() {
  return gulp.src(pageAction.html)
    .pipe(gulp.dest(pageAction.dest))
    .pipe(debug({title: 'copied action page:'}));
});

// "_pageAction_js" = Generate the pageAction script for the current browser and copy.
gulp.task('_pageAction_js', function() {
  var files = pageAction.js.src.map(function(x) {
    return x.replace('%target%', currentBrowser);
  });
  return gulp.src(files)
    .pipe(concat(pageAction.js.concat))
    .pipe(gulp.dest(pageAction.dest))
    .pipe(debug({title: 'built action page script:'}));
});

// "pageAction" = Copy the `browser_action` files.
gulp.task('pageAction', ['_pageAction_html', '_pageAction_js']);
/*** PAGEACTION ends here ***/

/*** PAGEOPTIONS ***/
// "_pageOptions_html" = Copy HTML options page.
gulp.task('_pageOptions_html', function() {
  return gulp.src(pageOptions.html)
    .pipe(gulp.dest(pageOptions.dest))
    .pipe(debug({title: 'copied options page:'}));
});

// "_pageOptions_js" = Generate the pageAction script for the current browser and copy.
gulp.task('_pageOptions_js', function() {
  var files = pageOptions.js.src.map(function(x) {
    return x.replace('%target%', currentBrowser);
  });
  return gulp.src(files)
    .pipe(concat(pageOptions.js.concat))
    .pipe(gulp.dest(pageOptions.dest))
    .pipe(debug({title: 'built options page script:'}));
});

// "pageOptions" = Copy the options page.
gulp.task('pageOptions', ['_pageOptions_html', '_pageOptions_js']);
/*** PAGEOPTIONS ends here ***/


// "icons" = Copy icons.
gulp.task('icons', function() {
  if(currentBrowser === 'safari') {
    iconFiles.src.push('safari/toolbar_icon.png');
    iconFiles.dest = build;
  }
  return gulp.src(iconFiles.src)
    .pipe(gulp.dest(iconFiles.dest))
    .pipe(debug({title: 'copied icons:'}));
});

// "styles" = Copy styles.
gulp.task('styles', function() {
  return gulp.src(styleFiles.src)
    .pipe(gulp.dest(styleFiles.dest))
    .pipe(debug({title: 'copied styles:'}));
});

// "fonts" = Copy fonts.
gulp.task('fonts', function() {
  return gulp.src(fontFiles.src)
    .pipe(gulp.dest(fontFiles.dest))
    .pipe(debug({title: 'copied fonts:'}));
});

// "assets" = Executes tasks for static assets.
gulp.task('assets', ['icons', 'styles', 'fonts']);

// "app" = Executes tasks for the app (view).
gulp.task('app', ['templates', 'inject', 'pageAction', 'pageOptions', 'assets']);

// "extension" = Executes tasks that are mostly not browser specific.
gulp.task('extension', ['app', 'GE', 'GE_libs', 'libraries']);

// "background-script" = Copy the background script for the
// `currentBrowser` (if any).
gulp.task('background-script', function() {
  var _background = currentBrowser === 'safari' ? global : background;
  _background.js.src = _background.js.src.map(function(x) {
    return x.replace('%target%', currentBrowser);
  });
  log(_background);
  return gulp.src(_background.js.src)
    .pipe(concat(_background.js.concat))
    .pipe(gulp.dest(_background.dest))
    .pipe(debug({title: 'copied ' + currentBrowser + '’s background script:'}));
});

// "background-page" = Copy the background page for the
// `currentBrowser` (if any).
gulp.task('background-page', function() {
  var _background = currentBrowser === 'safari' ? global : background;
  _background.html.src = _background.html.src.map(function(x) {
    return x.replace('%target%', currentBrowser);
  });
  return gulp.src(_background.html.src)
    .pipe(concat(_background.html.concat))
    .pipe(gulp.dest(_background.dest))
    .pipe(debug({title: 'copied ' + currentBrowser + '’s background page:'}));
});

// "manifest" = Copy the manifest for the current browser
gulp.task('manifest', function() {
  // Safari doesn’t have a `manifest.json`, but an `Info.plist`
  var manifest = currentBrowser === 'safari' ? 'Info.plist' : 'manifest.json';
  return gulp.src(currentBrowser + '/' + manifest)
    .pipe(gulp.dest(build))
    .pipe(debug({title: 'copied ' + currentBrowser +'’s manifest:'}));
});

// "_chromium" = Sets currentBrowser to chromium.
gulp.task('_chromium', function() {
  return setBrowser('chromium');
});

// "_firefox" = Sets currentBrowser to firefox
gulp.task('_firefox', function() {
  return setBrowser('firefox');
});

// "_safari" = Sets currentBrowser to safari
gulp.task('_safari', function() {
  return setBrowser('safari');
});

// "move-build" = Move build files to its target directory.
gulp.task('move-build', function() {
  var browserBuild = build.replace('%target%/ext/', currentBrowser + '/');
  mv(build.replace('ext/', ''), browserBuild, {mkdirp: true}, function(err) {
    console.log(err);
  });
  return log('Moved ' + currentBrowser + ' files to: ' + browserBuild);
});

// "chromium" = Run chromim dependencies to build the extension.
gulp.task('chromium', gulpsync.sync(['_chromium', ['manifest', 'extension'], 'move-build']));

// "firefox" = Run Firefox dependencies to build the extension.
gulp.task('firefox', gulpsync.sync(['_firefox', ['manifest', 'background-script', 'extension'], 'move-build']));

// "safari" = Run Safari dependencies to build the extension.
gulp.task('safari', gulpsync.sync(['_safari', ['manifest', 'background-script', 'background-page', 'extension'], 'move-build']));

// "clean" = Clean the build directory. Otherwise `mv` would throw an error.
gulp.task('clean', function() {
  log('Cleaned the build directory');
  return gulp.src('./build/', {read: false})
    .pipe(clean())
    .pipe(debug({title: 'cleaned ' + build}));
});

gulp.task('default', gulpsync.sync(['clean', 'firefox', 'chromium', 'safari']));

gulp.task('watch', function() {
  gulp.start('default');
  watch(allFiles, batch(function(events, done) {
    gulp.start('default', done);
  }));
});


================================================
FILE: lib/components.js
================================================
/*global MutationObserver */
/**
 * @fileOverview This file contains the /components/ module for emulating Web Components behavior.
 * @name components.js<src>
 * @author Etienne Prud’homme
 * @version 1.0.0
 * @link https://github.com/notetiene/components
 * @license GPLv3
 */
var components=function(){var e={},t=document.createRange();t.selectNode(document.body);var r=function(t,r,n){if(t.constructor!==String||""===t)throw new Error("Cannot register the component. The name must be a String.");if(r.constructor!==String||""===r)throw new Error("Cannot register "+t+" component. The template must be a String.");if(void 0!==e[t]&&e.hasOwnProperty(t))throw new Error(t+" is an already registered component.");var o=n||{};Object.defineProperty(e,t,{enumerable:!1,configurable:!1,writable:!1,value:{template:r,proto:o}})},n=function(r){if(!e.hasOwnProperty(r))throw new Error("“"+r+"” is not a registered component");for(var n=e[r],o=t.createContextualFragment(n.template),a=null,i=0,c=o.childNodes.length;i<c;i++)if(8!==o.childNodes[i].nodeType){a=o.childNodes[i];break}if(null===a)throw new Error("The “"+r+"” component doesn’t contain a valid node.");var l=n.proto.attachedCallback;if(l instanceof Function){var s=new MutationObserver(function(e){for(var t=0,r=e.length;t<r;t++)"childList"===e[t].type&&0===o.childNodes.length&&l.call(a)});s.observe(o,{childList:!0})}var u=n.proto.attributeChangedCallback;if(u instanceof Function){var d=new MutationObserver(function(e){for(var t=0,r=e.length;t<r;t++)"attributes"===e[t].type&&u.call(a)});d.observe(a,{attributes:!0})}return o};return{registerElement:r,createElement:n}}();

================================================
FILE: license
================================================
Copyright (c) 2015 Cameron Pittman

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. I
NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHEHERIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

================================================
FILE: package.json
================================================
{
  "name": "frontend-grading-engine",
  "version": "1.0.0",
  "description": "Udacity Front-End Grading Engine",
  "main": "Gruntfile.js",
  "dependencies": {},
  "devDependencies": {
    "gulp": "^3.9.0",
    "gulp-batch": "^1.0.5",
    "gulp-clean": "^0.3.2",
    "gulp-concat": "^2.6.0",
    "gulp-debug": "^2.0.1",
    "gulp-install": "^0.4.0",
    "gulp-sync": "^0.1.4",
    "gulp-uglify": "^1.4.2",
    "gulp-watch": "^4.3.5",
    "mv": "^2.1.1"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/udacity/frontend-grading-engine.git"
  },
  "author": "Cameron Pittman",
  "license": "MIT",
  "bugs": {
    "url": "https://github.com/udacity/frontend-grading-engine/issues"
  },
  "homepage": "https://github.com/udacity/frontend-grading-engine"
}


================================================
FILE: safari/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>Builder Version</key>
        <string>11601.7.7</string>
        <key>CFBundleDisplayName</key>
        <string>Udacity Feedback</string>
        <key>CFBundleIdentifier</key>
        <string>com.udacity.frontend-grading-engine</string>
        <key>CFBundleInfoDictionaryVersion</key>
        <string>6.0</string>
        <key>CFBundleShortVersionString</key>
        <string>0.3.0</string>
        <key>CFBundleVersion</key>
        <string>0.3.0</string>
        <key>Update From Gallery</key>
        <true/>
        <key>Chrome</key>
        <dict>
            <key>Global Page</key>
            <string>background.html</string>
            <key>Popovers</key>
            <array>
                <dict>
                    <key>Filename</key>
                    <string>app/browser_action/browser_action.html</string>
                    <key>Identifier</key>
                    <string>browserActionPage</string>
                </dict>
            </array>
            <key>Toolbar Items</key>
            <array>
                <dict>
                    <key>Identifier</key>
                    <string>actionPageButton</string>
                    <key>Image</key>
                    <string>toolbar_icon.png</string>
                    <key>Include By Default</key>
                    <true/>
                    <key>Label</key>
                    <string>Udacity Feedback</string>
                    <key>Popover</key>
                    <string>browserActionPage</string>
                    <key>Tool Tip</key>
                    <string>Udacity Feedback</string>
                </dict>
            </array>
        </dict>
        <key>Content</key>
        <dict>
            <key>Scripts</key>
            <dict>
                <key>End</key>
                <array>
                    <string>app/js/inject.js</string>
                </array>
            </dict>
        </dict>
        <key>DeveloperIdentifier</key>
        <string>AT7S4C746C</string>
        <key>ExtensionInfoDictionaryVersion</key>
        <string>1.0</string>
        <key>Permissions</key>
        <dict>
            <key>Website Access</key>
            <dict>
                <key>Include Secure Pages</key>
                <true/>
                <key>Level</key>
                <string>All</string>
            </dict>
        </dict>
    </dict>
</plist>


================================================
FILE: safari/README.md
================================================
## Safari Interface
### Description
This directory contains files needed to build the extension for Safari. The goal is to make a WebExtension [adapter](https://en.wikipedia.org/wiki/Adapter_pattern) to reduce modification of the current code base.

### Architecture
#### Execution Context
##### Injected Scripts
The extension API of Safari differs substantially from the WebExtension API. The extension architecture is similar to other browsers in that it allows scripts (injected script) to be injected with a limited API access. In that matter, it could be compared to _content scripts_. There’s one script instance per tab (depending on injection condition). Only injected scripts can have access to the DOM while getting a different global execution context other than the `window` object.

##### Global Page
The extension allows to have an execution context with full access to the safari API (global page). However, this global execution context doesn’t have access to a webpage content (which is injected scripts’ role). The role of a _global page_ is to share its access of the API on demand. Injected scripts have to send a serialized message to the global page to trigger a custom action. Because it’s a message passing system, passed functions won’t be executed in their canonical way (not in their String representation).

#### User Interface Components
Safari has UI components that are similar to what WebExtension provides (e.g. _browser action_), but outside of the _browser action_ component, they differ significantly in their behaviour.
Popovers have the same behaviour as _browser action_ page would. The global page _global executing context_ can be accessed with the `safari.extension.globalPage.contentWindow` namespace. To avoid making two adapters, the _global page_ adapter will be used.

#### Safari Incompatibilities
##### Storage
Safari doesn’t support extension storage. Instead, we need to use the `safari.extension.settings` property to set an inner object representing the storage object to use.
##### Tabs and Windows
Safari doesn’t provide a method to uniquely identify a window or a tab. The adapter adds a module called `registry` that assign an ID to each window or tab. It also provides several functions to retrieve a list of tabs or windows.
### License
This directory and the whole project is subject to the [GPLv3 License](../license).


================================================
FILE: safari/background/REAMDE.md
================================================
## Safari Global Page

The files in this directory get concatenated into `global.js`. It is somehow
similar to what the WebExtensions background page does.

Because the Safari uses its own extension API, most of the work to port this
extension to Safari is in providing a WebExtensions adapter.


================================================
FILE: safari/background/adapter.js
================================================
/*global registry, safari, extensionLog */

/**
 * @fileOverview This file contains the adaptee (Adapter inner working) for emulating the WebExtension API.
 * @name adapter.js<background>
 * @author Etienne Prud’homme
 * @license GPLv3
 *
 * Injected Scripts don’t have access to the `chrome.*` API with the exception
 * of:
 * * `extension` (`getURL`, `inIncognitoContext`, `lastError`, `onRequest`,
 *    `sendRequest`)
 * * `i18n`
 * * `runtime` (`connect`, `getManifest`, `getURL`, `id`, `onConnect`,
 *    `onMessage`, `sendMessage`)
 * * `storage`
 *
 * This is wĥy this background script is created.
 */

/**
 * Adaptee that translates chrome method behavior to safari.
 * @namespace
 * @property {error} wrapper.runtime.lastError - Set for the lifetime of a callback if an ansychronous extension api has resulted in an error. If no error has occured lastError will be undefined.
 */
var wrapper = {
  storage: {
    sync: {
      /**
       * Emulates the chrome storage behavior (getter) by using the {@link safari.extension.settings} mechanism.
       * @param {string|string[]|object} keys - A single key to get, list of keys to get, or a dictionary specifying default values (see description of the object). An empty list or object will return an empty result object. Pass in null to get the entire contents of storage.
       * @todo transform to a Promise
       * @returns {object} Object with items in their key-value mappings.
       * @throws {error} Error in the {@link keys} argument and sets {@link wrapper.runtime.lastError}.
       */
      get: function(keys) {
        var i, len, key, items = {};
        try {
          if(!keys) {
            if(keys === null) {
              items = safari.extension.settings;
            } else {
              // Only `null` can return values, otherwise it’s an empty Object
              items = {};
            }
          } else if(keys instanceof String || typeof keys === 'string') {
            items[keys] = safari.extension.settings[keys];
          } else if(keys instanceof Array && keys.length > 0) {
            items = {};

            for(i=0, len=keys.length; i<len; i++) {
              key = keys[i];
              if(!(key instanceof String || typeof key === 'string')) {
                extensionLog(new Error('An item of the `keys` array wasn’t a String'));
              }
              items[key] = safari.extension.settings[key];
            }
          } else {
            // Otherwise it can be any Objects with properties as keys.
            items = {};
            // Only a coincidence if they got the same names.
            var value, keysArray = Object.keys(keys);

            if(keysArray.length === 0) {
              extensionLog(new Error('The `keys` object does not contain any property on its own'));
            }

            for(i=0, len=keysArray.length; i<len; i++) {
              key = keysArray[i];
              value = safari.extension.settings[key];
              // Return the default value if the key isn’t present in settings
              items[key] = value !== undefined ? value : keys[key];
            }
          }
        } catch(e) {
          wrapper.runtime.lastError = e;
          items = -1;
        }
        return items;
      },
      /**
       * Emulates the chrome storage behavior (setter) by using the {@link safari.extension.settings} mechanism.
       * @param {} keys - An object which gives each key/value pair to update storage with. Any other key/value pairs in storage will not be affected.
       * Primitive values such as numbers will serialize as expected. Values with a typeof `object` and `function` will typically serialize to `{}`, with the exception of `Array` (serializes as expected), Date, and Regex (serialize using their `String` representation).
       * @returns {int} 0 on success and -1 on error.
       * @throws {error} Error in the {@link keys} argument and sets {@link wrapper.runtime.lastError}.
       */
      set: function(keys) {
        try {
          if(!keys || keys instanceof String || typeof keys === 'string' || keys instanceof Array) {
            extensionLog(new Error('The `keys` argument is not a valid Object with keys/properties'));
          }

          var key, i, len, keysArray = Object.keys(keys);

          if(keysArray.length === 0) {
            extensionLog(new Error('The `keys` object does not contain any property on its own'));
          }

          for(i=0, len=keysArray.length; i<len; i++) {
            key = keysArray[i];
            safari.extension.settings[key] = keys[key];
          }
        } catch (e) {
          wrapper.runtime.lastError = e;
          return -1;
        }
        return 0;
      }
    }
  },
  runtime: {
    lastError: null
  },
  tabs: {
    /**
     * Sends a single message to the content script(s) in the specified tab,
     * with an optional callback to run when a response is sent back. The
     * {@link injected.runtime.onMessage} event is fired in each content script
     * running in the specified tab for the current extension.
     * @param {int} tabId - The tab to send the message to.
     * @param {*} message - Any object that can be serialized.
     * @returns {Promise} A promise to be fulfilled when it has been received.
     */
    sendMessage: function(tabId, message, options, channel) {
      try {
        var tab = registry.getTabById(tabId);

        channel = channel || Math.floor(Math.random() * 100000000);
        tab.page.dispatchMessage('injected.runtime.onMessage', JSON.stringify({message: message, channel: channel}));
      } catch(e) {
        return Promise.reject(e.message);
      }
      return new Promise(function(resolve, reject) {
        tab.addEventListener('message', function handler(event) {
          var message = JSON.parse(event.message);
          // The injected script response
          if(event.name === 'injected.runtime.onMessage~response' &&
             parseInt(message.channel) === (0 - channel)) {
            tab.removeEventListener('message', handler, false);
            resolve(message.response);
          }
        }, false);
      });
    },

    /**
     * Gets all tabs that have the specified properties, or all tabs if no
     * properties are specified.
     * @param {object} queryInfo
     * @param {bool} [queryInfo.active] - TODO Whether the tabs are active in
     * their windows. (Does not necessarily mean the window is focused.)
     * @param {bool} [queryInfo.currentWindow] - TODO Whether the tabs are in
     * the /current window/. Note: the current window doesn’t mean it’s the
     * active one. It means that the window is currently executing.
     * @returns {int|Object[]} Result of the query of -1 on error.
     */
    query: function(queryInfo) {
      var windows, tabs;
      try {
        windows = safari.application.browserWindows;
        tabs = [];

        if(Object.prototype.toString.call(queryInfo) === '[object Object]') {
          // queryInfo.currentWindow
          if(queryInfo.currentWindow) {
            /** Because there’s no way I know to select the window currently
             * running in Safari, the active window (or
             * {@link lastFocusedWindow} one if null) will be used instead. If
             * someone successfully thriggers an action page that isn’t focused,
             * it’s an undefined behavior.
             */
            windows = registry.getActiveWindow();
            if(windows === null) {
              windows = registry.getLastFocused();
            }
            // Put it in an array
            windows = [windows];
          }

          // queryInfo.active
          if(queryInfo.active === true) {
            tabs = makeTabType(activeTabs(windows));
          } else {
            tabs = makeTabType(getTabs(windows));
          }
        } else {
          extensionLog(new Error('No valid query is specified'));
        }

        /**
         * Gets all active {@link SafariBrowserTab} from given an array
         * {@link SafariBrowserWindow}.=
         * @param {SafariBrowserWindow[]} windows - Windows to get all active
         * tabs.
         * @returns {SafariBrowserTabs[]} Array of active tabs.
         */
        function activeTabs(windows) {
          var resultTabs = [], index, i, len;

          for(i=0, len=windows.length; i<len; i++) {
            // It makes a copy of the object
            resultTabs.push(windows[i].activeTab);
          }
          return resultTabs;
        }

        /**
         * Gets all tabs from given windows.
         * @param {SafariBrowserWindow[]} windows - An array of
         * {@link SafariBrowserWindow}.
         * @returns {SafariBrowserTab[]} List of tabs from {@link windows}.
         */
        function getTabs(windows) {
          var i, len, u, u_len, windowTabs, index, resultTabs = [];
          for(i=0, len=windows.length; i<len; i++) {
            windowTabs = windows[i].tabs;
            for(u=0, u_len=windowTabs.length; u<u_len; u++) {
              resultTabs.push(windowTabs[u]);
            }
          }
          return resultTabs;
        }

        /**
         * Makes a Tab type.
         * @param {SafariBrowserTab[]} tabs - Array of SafariBowserTab.
         * @returns {Tab[]} Chrome formatted Tab type.
         */
        function makeTabType(tabs){
          var resultTabs = [], i, len, currentTab, tab;

          for(i=0, len=tabs.length; i<len; i++) {
            tab = tabs[i];
            currentTab = {
              id: tab.id
              // All other parts may change
            };
            resultTabs.push(currentTab);
          }
          return resultTabs;
        }
      } catch(e) {
        wrapper.runtime.lastError = e;
        return -1;
      }
      return tabs;
    }
  }
};

// adapter.js<background> ends here


================================================
FILE: safari/background/adapterListener.js
================================================
/*global wrapper, safari */

/**
 * @fileOverview This file contains the adapter listener (i.e. message
 * passing part) for injected scripts since most of the adaptee
 * methods need higher priviledge (but allowed from a global page).
 * @name adapterListener.js<background>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

// Listens to the client adapter
safari.application.addEventListener('message', function(event) {
  var status = -1;
  var message = JSON.parse(event.message);

  // Safari uses ev.name for the name of the event while using
  // /message/ for communication between scripts.
  switch(event.name) {
  case 'wrapper.storage.sync.get':
    // Returns -1 on error otherwise the response
    status = wrapper.storage.sync.get(message.keys);
    respondBack('injected.storage.sync.get', status);
    break;
  case 'wrapper.storage.sync.set':
    // Returns -1 on error otherwise the response
    status = wrapper.storage.sync.set(message.keys);
    respondBack('injected.storage.sync.set', status);
    break;
  case 'wrapper.runtime.sendMessage':
    // TODO
    // Returns -1 on error otherwise the response
    status = wrapper.runtime.sendMessage();
    respondBack('injected.runtime.sendMessage', status);
    break;
  case 'wrapper.tabs.query':
    // Returns -1 on error otherwise the responsenn
    status = wrapper.tabs.query(message.query);

    // Note: The docs don’t officially specify throwing lastError
    respondBack('injected.tabs.query', status);
    break;
  }

  /**
   * Function that sends back the result of the request and also take
   * cares of status codes.
   * @param {string} channel - The name of the request receiver.
   * @param {int|Object} status - The response of a query. On error,
   * it should be `-1`.
   */
  function respondBack(channel, status) {
    var response;
    if(status === -1) {
      response = {name: 'error', response: wrapper.runtime.lastError.message};
    } else {
      response = {name: 'ok', response: status};
    }
    event.target.page.dispatchMessage(channel, JSON.stringify(response));
  }
  // Since its lifetime is for a callback
  wrapper.runtime.lastError = undefined;
}, false);

// adapterListener.js<background> ends here


================================================
FILE: safari/background/background.html
================================================
<!doctype html>
<html>
  <head>
    <title>Background Page</title>
    <meta charset="utf-8">
    <script src="background.js"></script>
  </head>
  <body></body>
</html>


================================================
FILE: safari/background/background.js
================================================
/*global safari, SafariBrowserTab, SafariBrowserWindow, wrapper, extensionLog */

/**
 * @fileOverview This file adds support for the Chrome API in the global page
 * script context.
 * @name background.js<safari>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

// Initializes the logs if not created
safari.extension.settings.logs = safari.extension.settings.logs || [];

/**
 * Chrome adapter module for the global page context.
 * @returns {object} The chrome namespace.
 * @throws {Error} TO FIX. We should set `lastError` instead.
 */
var global = (function() {
  var exports = {
    tabs: {
      /**
       * Sends a single message to the content script(s) in the specified tab,
       * with an optional callback to run when a response is sent back. The
       * {@link global.runtime.onMessage} event is fired in each content script
       * running in the specified tab for the current extension.
       * @param {int} tabId - The tab to send the message to.
       * @param {*} message - Any object that can be serialized.
       * @param {object} [options]
       * @param {int} [options.frameId] - Send a message to a specific frame
       * identified by {@link frameId} instead of all frames in the tabn.
       * @param {global.tabs.sendMessage~responseCallback} [responseCallback] -
       * Function called when there’s a response. Note: The response can be any
       * object.
       */
      sendMessage: function(tabId, message, options, responseCallback) {
        var channel = Math.floor(Math.random() * 100000000);
        wrapper.tabs.sendMessage(tabId, message, options, channel)
          .then(function(response) {
            if(typeof(responseCallback) === typeof(Function)) {
              responseCallback(response);
            }
          }).catch(function(reason) {
            extensionLog(reason);
          });
      },
      /**
       * @namespace
       * @property {int} [id] - The ID of the tab. Tab IDs are unique within a
       * browser session. Under some circumstances a Tab may not be assigned an
       * ID, for example when querying foreign tabs using the sessions API, in
       * which case a session ID may be present.

       * @property {int} index - The zero-based index of the tab within its
       * window.
       * @property {int} windowId - The ID of the window the tab is contained
       * within.
       * @property {int} [openerTabId] - The ID of the tab that opened this tab,
       * if any. This property is only present if the opener tab still exists.
       * @property {bool} highlighted - Whether the tab is highlighted.
       * @property {bool} active - Whether the tab is active in its
       * window. (Does not necessarily mean the window is focused.)
       * @property {bool} pinned - Whether the tab is pinned.
       * @property {string} [url] - The URL the tab is displaying. This property
       * is only present if the extension’s manifest includes the “tabs”
       * permission.
       * @property {string} [title] - The title of the tab. This property is
       * only present if the extension’s manifest includes the “tabs”
       * permission.
       * @property {string} [favIconUrl] - The URL of the tab's favicon. This
       * property is only present if the extension's manifest includes the
       * "tabs" permission. It may also be an empty string if the tab is
       * loading.
       * @property {string} [status] - Either loading or complete.
       * @property {bool} incognito - Whether the tab is in an incognito window.
       * @property {int} width - The width of the tab in pixels.
       * @property {int} height - The height of the tab in pixels.
       * @property {string} sessionId - The session ID used to uniquely identify
       * a Tab obtained from the sessions API.
       */
      Tab: {
        id: null,
        index: null,
        windowId: null,
        openerTabId: null,
        highlighted: null,
        active: null,
        pinned: null,
        url: null,
        title: null,
        favIconUrl: null,
        status: null,
        incognito: null,
        width: null,
        height: null,
        sessionId: null
      },
      /**
       * Whether the tabs have completed loading.
       * @readonly
       * @enum {string}n
       */
      tabStatus: {
        loading: 'loading',
        complete: 'complete'
      },
      /**
       * The type of window.
       * @readonly
       * @enum {string}
       */
      windowType: {
        normal: 'normal',
        popup: 'popup',
        panel: 'panel',
        app: 'app',
        devtool: 'devtool'
      },
      /**
       * Gets all tabs that have the specified properties, or all tabs if no
       * properties are specified.
       * @param {object} queryInfo
       * @param {bool} [queryInfo.active] - Whether the tabs are active in their
       * windows.
       * @todo @param {bool} [queryInfo.pinned] - Whether the tabs are pinned.
       * @todo @param {bool} [queryInfo.highlighted] - Whether the tabs are
       * highlighted.
       * @param {bool} [queryInfo.currentWindow] - Whether the tabs are in the
       * /current window/.
       * @todo @param {bool} [queryInfo.lastFocusedWindow] - Whether the tabs
       * are in the last focused window.
       * @todo @param {tabStatus} [queryInfo.status] - Whether the tabs have
       * completed loading.
       * @todo @param {string} [queryInfo.title] - Match page titles against a
       * pattern.
       * @todo @param {string|string[]} [queryInfo.url] - Match tabs against one
       * or more /URL patterns/. Note that fragment identifiers are not matched.
       * @param {int} [queryInfo.windowId] - The ID of the parent window, or
       * {@link global.windows.WINDOW_ID_CURRENT} for the current window.
       * @todo @param {windowType} [queryInfo.windowType] - The type of window
       * the tabs are in.
       * @todo @param {int} [queryInfo.index] - The position of the tabs within
       * their windows.
       * @param {global.tabs.query~callback} callback - Threats returned tabs.
       */
      query: function(queryInfo, callback) {
        try {
          var values = wrapper.tabs.query(queryInfo);
          if(typeof(callback) === typeof(Function)) {
            callback(values);
          } else {
            console.warn('No callback function is provided');
          }
        }
        catch(e) {
          throw e;
        }
      }
    },
    runtime: {
      /**
       * Get keys from the `Info.plist` file. Only `version` is currently supported.
       * @returns {Object} Object containing the manifest properties.
       */
      getManifest: function() {
        return {
          version: safari.extension.displayVersion
        };
      },
      lastError: null,
      openOptionsPage: function() {
        // Find the active popover
        var popovers = safari.extension.popovers;

        // TODO: Find the last active popover
        for(var i=popovers.length; --i >= 0;) {
          // If none is found to be visible, te index 0 is taken
          if(popovers[i].visible === true || i === 0) {
            // Note: contentWindow is referring to the popover window itself
            popovers[i].contentWindow.location.href = safari.extension.baseURI + 'app/options/index.html';
          }
        }
      }
    },
    storage: {
      sync: {
          /**
           * Gets one or more items from storage.
           * @param {string|string[]|object} [keys] - A single key to get, list
           * of keys to get, or a dictionary specifying default values (see
           * description of the object). An empty list or object will return an
           * empty result object. Pass in null to get the entire contents of
           * storage.
           * @param {injected.storage.sync.get~callback} callback - Callback
           * with storage items, or on failure (in which case
           * {@link injected.runtime.lastError} will be set).
           * @returns {object} Object with items in their key-value mappings.
           */
          get: function(keys, callback) {
            var values = wrapper.storage.sync.get(keys);
            if(typeof (callback) === typeof(Function)) {
              callback(values);
            }
          },
          /**
           * Sets multiple items.
           * @param {object} keys - An object which gives each key/value pair to
           * update storage with. Any other key/value pairs in storage will not
           * be affected.
           *
           * Primitive values such as numbers will serialize as expected. Values
           * with a typeof `object` and `function` will typically serialize to
           * `{}`, with the exception of `Array` (serializes as expected), Date,
           * and Regex (serialize using their `String` representation).
           * @param {injected.storage.sync.set~callback} [callback] - Callback
           * on success, or on failure (in which case
           * {@link injected.runtime.lastError} will be set).
           */
          set: function(keys, callback) {
            wrapper.storage.sync.set(keys);
            if(typeof(callback) === typeof(Function)) {
              callback();
            }
          }
      }
    }
  };

  // The module was intialized
  exports.initialized = true;
  return exports;
})();

var chrome = global;

// background.js<safari>


================================================
FILE: safari/background/helpers.js
================================================
/*global safari */

/**
 * @fileOverview This file adds utility functions for the Safari global page.
 * @name helpers.js<background>
 * @author Etienne Prud’homme
 * @license GPL
 */

/**
 * Store logging informations in the extension settings.
 * @param {string|error} message - The message to log as a String or an Error.
 * @throws {Error} Error in the arguments of the function (not a String nor an
 * Error).
 */
function extensionLog(log) {
  // Cache logs to append a single log
  var logs = safari.extension.settings.logs;
  var stack, logMessage;

  if(log instanceof Error) {
    logMessage = log.message;
    stack = log.stack;
  } else if (logMessage instanceof String || typeof logMessage === 'string') {
    logMessage = log;
    stack = new Error().stack;
  } else {
    // Log error of itself
    extensionLog('Invalid log type: ' + log.toString());
    throw new Error('Extension logging error');
  }

  // Adding the new log
  logs.push({
    message: logMessage,
    stack: stack,
    timestamp: Date.now() / 1000
  });

  // Record the new logs
  safari.extension.settings.logs = logs;
  // This should be in the Background script and shouldn’t conflict with page
  // scripts
  console.warn(log);

  // Actually throw that error
  if(log instanceof Error) {
    throw log;
  }
}

// helpers.js<background> ends here


================================================
FILE: safari/background/registry.js
================================================
/**
 * @fileOverview This adds a module to record windows and tabs in
 * Safari. Otherwise, there’s no way to select a tab (or window) with an ID.
 * @name registry.js<background>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

/**
 * Registers Tabs and Windows.
 */
var registry = (function() {
  var _windows = {
    activeWindow: null,
    lastFocusedWindow: null
  };
  var _tabs = {};
  var exports = {};

  /**
   * Returns registered windows from {@link _windows}.
   * @returns {SafariBrowserWindow[]} Registered windows.
   */
  exports.getWindows = function() {
    return _windows;
  };

  /**
   * Returns registered tabs from {@link _tabs}.
   * @returns {SafariBrowserTab[]} Registered tabs.
   */
  exports.getTabs = function() {
    return _tabs;
  };

  /**
   * Returns the window registered as `active`. It may not conform to the Chrome
   * specs.
   * @returns {SafariBrowserWindow} The active window.
   */
  exports.getActiveWindow = function() {
    return _windows.activeWindow;
  };

  /**
   * Returns the last window that had focus (activated from Safari
   * specifications).
   * @returns {SafariBrowserWindow} The last window that had focus.
   */
  exports.getLastFocused = function() {
    return _windows.lastFocusedWindow;
  };

  /**
   * Returns the {@link SafariBrowserTab} corresponding to a given ID.
   * @param {string|int} id - The ID of the registered tab.
   * @returns {SafariBrowserTab} The tab that has the given ID or -1 on error.
   */
  exports.getTabById = function(id) {
    var tab;
    try {
      if(_tabs.hasOwnProperty(id)) {
        tab = _tabs[id];
      } else {
        extensionLog('Invalid tab id: ' + id);
      }
    } catch(e) {
      return -1;
    }
    return tab;
  };
  /**
   * Returns a random property that isn’t found in an Object.
   * @param {object} obj - Object to find uniqueness of a property name.
   * @param {object} [options] - Options for generating the property.
   * @param {int} [options.precision=100000000] - Number that will be multiplied
   * to a random number between 0 and 1.
   * @param {int|string} [options.prefix=0] - Number or string that will add the
   * property to itself. It may add the number vlaue or concatenate the String.
   * @returns {int} Unique Identifier.
   */
  function getUniqueProperty(obj, options) {
    var prop,
        _options = options || {},
        precision = _options.precision || 100000000,
        prefix = _options.prefix || 0;

    do {
      prop = prefix + Math.floor(Math.random() * precision);
    } while(obj.hasOwnProperty(prop) === true);
    return prop;
  }

  // Windows
  /**
   * Register a given window by assigning a new random id. When the window is
   * closed, it removes the id from available windows.
   * @todo Check if tabs from the registry are also removed when the window is
   * closed.
   * @param {SafariBrowserWindow} _window - The new window to register.
   */
  function registerWindow(_window) {
    var id = '';
    if(_window.id === undefined) {
      id = getUniqueProperty(_windows);
      // Registered windows
      _windows[id] = _window;
      _window.id = id;

      // _window.addEventListener('close', function handler() {
      //   _window.removeEventListener('close', handler, false);
      //   delete _windows[id];
      // }, false);
    }
  }

  /**
   * Removes the given {@link SafariBrowserWindow} from the registered windows
   * in {@link _windows}.
   * @param {SafariBrowserWindow} _window - The window to remove.
   */
  function removeWindow(_window) {
    var id = _window.id;
    delete _windows[id];
  }

  /**
   * Register all available windows with a new random unique id. Its purpose is
   * to be called on the extension startup.
   */
  function registerWindows() {
    var browserWindows = safari.application.browserWindows;
    var id, activeWindow;

    for(var i=0, len=browserWindows.length; i<len; i++) {
      registerWindow(browserWindows[i]);
    }
    activeWindow = safari.application.activeBrowserWindow;
    _windows.activeWindow = activeWindow;
    _windows.lastFocusedWindow = activeWindow;
  }
  // Windows ends here

  // Tabs

  /**
   * Register a given tab by assigning a new random id in the tabs
   * registry. When the tab is closed, it removes the id from the tabs registry.
   * @param {SafariBrowserTab} _tab - The new tab to register.
   */
  function registerTab(_tab) {
    var id;
    if(_tab.id === undefined) {
      id = getUniqueProperty(_tabs);
      // Registered tabs
      _tabs[id] = _tab;
      _tab.id = id;

      // // Removes the id from {@link _tabs}
      // _tab.addEventListener('close', function handler() {
      //   _tab.removeEventListener('close', handler, false);
      //   delete _tabs[id];
      // }, false);
    }
  }

  /**
   * Removes the given {@link SafariBrowserTab} from the registered windows in
   * {@link _tabs}.
   * @param {SafariBrowserTab} tab - The Tab to remove.
   */
  function removeTab(tab) {
    var id = tab.id;
    delete _tabs[id];
  }

  /**
   * Search for {@link SafariBrowserTab} without an id and set a new random
   * unique id.
   */
  function registerTabs() {
    var status = 0,
        windows = safari.application.browserWindows;

    var i, u, tabsLen, windowsLen, windowTabs;
    // Concat tabs from different windows

    // For each window
    for(i=0, windowsLen=windows.length; i<windowsLen; i++) {
      windowTabs = windows[i].tabs;
      // For each tabs in the window
      for(u=0, tabsLen=windowTabs.length; u<tabsLen; u++) {
        registerTab(windowTabs[u]);
      }
    }
  }
  // Tabs ends here

  // There’s no way to specify for windows or tabs (it must be guessed). It
  // seems that when a window is created it first fires the event for the tab
  // and then the window.
  safari.application.addEventListener('open', function(ev) {
    // If a new window was created
    if(ev.target instanceof SafariBrowserWindow) {
      registerWindow(ev.target);
    } else if (ev.target instanceof SafariBrowserTab) {
      registerTab(ev.target);
    } else {
      extensionLog('Got something else than a Tab or Window');
    }
  }, true);

  safari.application.addEventListener('close', function(ev) {
    // If a window is about to close
    if(ev.target instanceof SafariBrowserWindow) {
      removeWindow(ev.target);
    } else if (ev.target instanceof SafariBrowserTab) {
      // If a tab is about to close
      removeTab(ev.target);
    } else {
      extensionLog('Got something else than a Tab or Window');
    }
  }, true);

  safari.application.addEventListener('activate', function(ev) {
    if(ev.target instanceof SafariBrowserWindow) {
      _windows.activeWindow = ev.target;
      // TODO: What about when it’s closed?
      _windows.lastFocusedWindow = ev.target;
    }
  }, true);

  safari.application.addEventListener('deactivate', function(ev) {
    if(ev.target instanceof SafariBrowserWindow) {
      _windows.activeWindow = null;
    }
    console.log(ev);
  }, true);

  registerTabs();
  registerWindows();
  return exports;
})();

// registry.js<background> ends here


================================================
FILE: safari/browser_action/intro.js
================================================
/**
 * @fileOverview This file contains the opening statements of
 * `browser_action.js` for the Safari Browser. With this file, a function will
 * wrap the original `browser_action.js`. It prevents executing the code until
 * the `chrome` namespace is fully loaded.
 * @name intro.js<action_page>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

var chrome;

/**
 * Blocks execution until the Chrome namespace is fully loaded.
 */
/* jshint ignore:start */
function waitChromeNS() {
/* jshint ignore:end */


// intro.js<popover> ends here


================================================
FILE: safari/browser_action/outro.js
================================================
/*global waitChromeNS, chrome, safari, checkSiteStatus */

/**
 * @fileOverview This file contains the Safari closing statements for the
 * browser action page appended to the main file. This file contains a function
 * executed each 100ms to check if the `chrome` module/namespace is fully
 * loaded. If it’s already initialized, it assign the _globalPage_ namespace as
 * `chrome`, otherwise it initializes it.
 * @name outro.js<browser_action>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

  var main = document.getElementById('main');
  main.style.width = '400px';

  var label = document.getElementById('ud-label-loader');
  label.remove();

  safari.application.addEventListener('popover', function() {
    checkSiteStatus();
  });
  /* jshint ignore:start */
}
/* jshint ignore:end */

var handler = window.setInterval(function() {
  try {
    // If the module isn’t initialized, intialize it
    if(typeof(safari.extension.globalPage.contentWindow.chrome) === typeof(Function)) {
      chrome = safari.extension.globalPage.contentWindow.chrome();
      window.clearInterval(handler);
      waitChromeNS();
    } else {
      // If the module is initialized, assign the module
      if(safari.extension.globalPage.contentWindow.chrome.initialized === true) {
        chrome = safari.extension.globalPage.contentWindow.chrome;
        window.clearInterval(handler);
        waitChromeNS();
      } else {
        return;
      }
    }
  } catch(e) {
    return;
  }
}, 100);

// outro.js<browser_action> ends here
// browser_action.js ends here


================================================
FILE: safari/inject/intro.js
================================================
/*global safari */

/**
 * @fileOverview This file contains the opening statements of `inject.js` for
 * Safari.
 * @name intro.js<safari>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

/* jshint ignore:start */
// Injected scripts in Safari get also injected in iFrames
if (window.top === window) {
  /* jshint ignore: end*/
  var injected = (function(){
    var pageListener = [];
    /**
     * @namespace
     * @property {object} injected.runtime.lastError - This will be defined
     * during an API method callback if there was an error
     * @property {string} [injected.runtime.lastError.message] - Details about
     * the error which occurred.
     */
    var exports =  {
      runtime: {
        /**
         * Sends a single message to event listeners within the extension/app or
         * a different extension/app. Similar to
         * {@link injected.runtime.onMessage} but only sends a single message,
         * with an optional response. If sending to your extension, the
         * {@link injected.runtime.onMessage} event will be fired in each page,
         * or {@link injected.runtime.onMessageExternal}, if a different
         * extension. Note that extensions cannot send messages to content
         * scripts using this method. To send messages to content scripts, use
         * {@link injected.tabs.sendMessage.}
         * @param {string} [extensionId] - The ID of the extension to send the
         * message to. If `undefined` or `null`, the current extension is used.
         * @param {*} message - The message to sent.
         * @param {object} [options]
         * @todo @param {bool} [options.includeTlsChannelId] - Whether the TLS
         * channel ID will be passed into onMessageExternal for processes that
         * are listening for the connection event.
         * @param {injected.runtime.sendMessage~callback} [callback] - Function
         * called when there’s a response. Note: The response can be any object.
         */
        sendMessage: function(extensionId, message, options, callback) {

        },
        lastError: null,
        /**
         * An object containing information about the script context that sent a
         * message or request.
         * @namespace
         * @property {injected.tabs.Tab} [tab] - The {@link injected.tabs.Tab}
         * which opened the connection, if any. This property will only be
         * present when the connection was opened from a tab (including content
         * scripts), and only if the receiver is an extension, not an app.
         */
        MessageSender: {
          tab: null,
          frameId: null
          // id: null,
          // url: null,
          // tlsChannelId: null
        },
        onMessage: {
          /**
           * Fired when a message is sent from either an extension process or a
           * content script.
           * @param {injected.runtime.onMessage.addListener~callback} callback -
           */
          addListener: function(callback) {
            pageListener.push(callback);
          }
        }
      },
      extension: {
        /**
         * Converts a relative path within an extension install directory to a
         * fully-qualified URL.
         * @param {string} path - A path to a resource within an extension
         * expressed relative to its install directory.
         * @returns {string} The fully-qualified URL.
         */
        getURL: function(url) {
          return safari.extension.baseURI + url;
        }
      },
      storage: {
        sync: {
          /**
           * Gets one or more items from storage.
           * @param {string|string[]|object} [keys] - A single key to get, list
           * of keys to get, or a dictionary specifying default values (see
           * description of the object). An empty list or object will return an
           * empty result object. Pass in null to get the entire contents of
           * storage.
           * @param {injected.storage.sync.get~callback} callback - Callback
           * with storage items, or on failure (in which case
           * {@link injected.runtime.lastError} will be set).
           * @returns {object} Object with items in their key-value mappings.
           */
          get: function(keys, callback) {
            askAdapter('wrapper.storage.sync.get', {keys: keys})
              .then(function(values) {
                if(callback instanceof Function) {
                  console.log('In `injected.storage.sync.get` returning: ' + values.toString());
                  callback(values);
                }
              }).catch(function(error) {
                throw new Error(error);
              });
          },
          /**
           * Sets multiple items.
           * @param {object} keys - An object which gives each key/value pair to
           * update storage with. Any other key/value pairs in storage will not
           * be affected.
           *
           * Primitive values such as numbers will serialize as expected. Values
           * with a typeof `object` and `function` will typically serialize to
           * `{}`, with the exception of `Array` (serializes as expected), Date,
           * and Regex (serialize using their `String` representation).
           * @param {injected.storage.sync.set~callback} [callback] - Callback
           * on success, or on failure (in which case
           * {@link injected.runtime.lastError} will be set).
           */
          set: function(keys, callback) {
            // Send the request
            askAdapter('wrapper.storage.sync.set', {keys: keys})
              .then(function() {
                if(callback instanceof Function) {
                  callback();
                }
              }).catch(function(error) {
                throw new Error(error);
              });
          }
        }
      }
    };

    /**
     * Serialize an Object to be supported by Safari when sent as JSON.
     * @param {object} obj - Any object that will be converted to JSON. It
     * supports RegExp and Date.
     * @returns {string} The JSON string.
     */
    function serialize(obj) {
      // var topObject = true;

      return JSON.stringify(obj, function(key, value) {
        // if(topObject) {
        //   topObject = false;
        //   return value;
        // }

        if (value instanceof String || value instanceof RegExp || value instanceof Date) {
          return value.toString();
        } else if(value instanceof Array) {
          return value;
        }
        //  else if(value instanceof Object && !topObject) {
        //   return {};
        // }

        return value;
      });
    }

    /**
     * Sends a message to the adapter function and return a Promise that
     * resolves when the response is received.
     * @param {} channel
     * @param {} message
     * @returns {Promise} A Promise that resolves when the response is received.
     */
    function askAdapter(channel, message) {
      return new Promise(function(resolve, reject) {
        // Register the response receiver before sending the message (else it
        // won’t be fired)
        safari.self.addEventListener('message', function responseHandler(ev) {
          var data = JSON.parse(ev.message);
          channel = channel.replace('wrapper.', 'injected.');
          // Remove self since just ask --> response
          safari.self.removeEventListener('message', responseHandler);
          if(ev.name === channel) {
            if(data.name === 'ok') {
              resolve(data.response);
            } else if(data.name === 'error') {
              // runtime.lastError
              reject(data.response);
            } else {
              reject('The Global Page sent an invalid response');
            }
          }
        }, false);
        sendMessageToAdapter(channel, message);
      });
    }

    function sendMessageToAdapter(channel, message) {
      var JSONmessage = serialize(message);
      safari.self.tab.dispatchMessage(channel, JSONmessage);
    }

    // The injected script receives messages
    safari.self.addEventListener('message', function handler(event) {
      var data = JSON.parse(event.message);
      // To prevent listening to the same event. It may be useless, but Safari
      // always have weird behavior.
      var channel = 0 - parseInt(data.channel);

      /**
       * Set to true when a listener call {@link sendResponse}. The Chrome
       * specs only allow a call to that function.
       */
      var activeResponse = false;

      // The injected script receives messages from a sendMessage method
      if(event.name === 'injected.runtime.onMessage' &&
         pageListener.length > 0) {
        // For each listener, pass the message
        for(var i=0, len=pageListener.length; i<len; i++) {
          // TODO: Add support to the MessageSender field
          pageListener[i](data.message, undefined, sendResponse);
        }

        // If no listeners called sendResponse
        if(activeResponse !== true) {
          sendResponse();
        }
      }

      /**
       * Sends a response from the listener. Can only be called once.
       * @param {*} response - Any JSON-ifiable objects.
       */
      function sendResponse(response) {
        if(activeResponse !== true) {
          activeResponse = true;
          safari.self.tab.dispatchMessage('injected.runtime.onMessage~response', JSON.stringify({response: response, channel: channel}));
        }
      }
    }, false);

    return exports;
    /**
     * Callback when there’s a message sent to the extension channel (can be
     * both the extension or a /content-script/).
     * @callback injected.runtime.onMessage.addListener~callback
     * @param {*} message - The message sent by the calling script.
     * @param {MessageSender} sender - The sender.
     * @param {function} sendResponse - Function to call (at most once) when
     * there’s a response. The argument should be any JSON-ifiable object. If
     * there’s more than one {@link injected.runtime.onMessage} listener in the
     * same document, then only one may send a response.
     */

    /**

     * Callback  to  pass   the  received  a  response   when  executing
     * {@link injected.runtime.senMessage}.
     * @callback injected.runtime.sendMessage~responseCallback

     * @param {*} response - The JSON response object sent by the handler of the
     * message. If an error occurs while connecting to the extension, the
     * callback will be called with no arguments and
     * {@link injected.runtime.lastError} will be set to the error message.
     */

    /**
     * Callback when getting storage items, or on failure (in which case
     * {@link injected.runtime.lastError} will be set).
     * @callback injected.storage.sync.get~callback
     * @param {object} items - Object with items in their key-value mappings.
     */

    /**
     * Callback when setting storage items, or on failure (in which case
     * {@link injected.runtime.lastError} will be set).
     * @callback injected.storage.sync.set~callback
     */

    /**
     * Callback to process tabs returned by {@link injected.tabs.query}.
     * @callback injected.tabs.query~callback
     * @param {Tab[]} results - The results of the tabs query.
     */

    /**
     * Callback to pass the received a response when executing
     * {@link injected.tabs.senMessage}.
     * @callback injected.tabs.sendMessage~responseCallback

     * @param {*} response - The JSON response object sent by the handler of the
     * message. If an error occurs while connecting to the extension, the
     * callback will be called with no arguments and
     * {@link injected.runtime.lastError} will be set to the error message.
     */
  })();

  chrome = injected;

// intro.js<safari> ends here


================================================
FILE: safari/inject/outro.js
================================================
/**
 * @fileOverview This file contains the closing statements of `inject.js` for
 * Safari.
 * @name outro.js<Safari>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

/* jshint ignore:start */
}
/* jshint ignore:end */

// outro.js<safari> ends here
// inject.js<safari> ends here


================================================
FILE: safari/options/intro.js
================================================
/**
 * @fileOverview This file contains the Firefox opening statements for the options page script prepended to the main file.
 * @name intro.js<options>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

var chrome;

var browserName = 'Safari';
/**
 * Blocks execution until the Chrome namespace is fully loaded.
 */
/* jshint ignore:start */
function waitChromeNS() {
/* jshint ignore:end */

// intro.js<options> ends here


================================================
FILE: safari/options/outro.js
================================================
/*global waitChromeNS, chrome, safari */

/**
 * @fileOverview This file contains the Firefox closing statements for the options page script appended to the main file.
 * @name outro.js<options>
 * @author Etienne Prud’homme
 * @license GPLv3
 */

  var backButton = document.getElementById('back-button');
  backButton.style.display = 'block';

  backButton.addEventListener('click', function handler(event) {
    window.history.back();
  });

  // When the popover is closed (it actually loses focus, but it’s still there)
  window.addEventListener('blur', function() {
    window.location.reload();
  });
/* jshint ignore:start */
}
/* jshint ignore:end */

var handler = window.setInterval(function() {
  try {
    // If the module isn’t initialized, intialize it
    if(typeof(safari.extension.globalPage.contentWindow.chrome) === typeof(Function)) {
      chrome = safari.extension.globalPage.contentWindow.chrome();
      window.clearInterval(handler);
      waitChromeNS();
    } else {
      // If the module is initialized, assign the module
      if(safari.extension.globalPage.contentWindow.chrome.initialized === true) {
        chrome = safari.extension.globalPage.contentWindow.chrome;
        window.clearInterval(handler);
        waitChromeNS();
      } else {
        return;
      }
    }
  } catch(e) {
    return;
  }
}, 100);

// outro.js<options> ends here
// options.js ends here


================================================
FILE: sample/index.html
================================================
<!doctype html>
<head>
  <title>Sample Tests</title>
  <meta name="udacity-grader" content="test.json" libraries="jsgrader" unit-tests="unit-tests.js">
  <meta charset="UTF-8">
  <style>
    body {
      width: 100%;
    }
    .box {
      background-color: #fcc;
    }
    .flex-container {
      display: flex;
      flex-wrap: wrap;
    }
    .flex {
      margin-right: 10px;
      margin-bottom: 10px;
    }

    .flex:last-of-type {
      margin-right: 0px;
    }

    .flex1 {
      background-color: #ffc;
      min-height: 30px;
      min-width: calc( (100% - 10px) / 4);
    }
    .flex2 {
      background-color: #fcc;
      min-height: 30px;
      min-width: calc( (100% - 10px) / 4);
    }
    .flex3 {
      background-color: #cff;
      min-height: 30px;
      min-width: calc( (100% - 10px) / 4);
    }
    .flex4 {
      background-color: #fcf;
      min-height: 30px;
      min-width: calc( (100% - 10px) / 4);
    }
    .flex5 {
      background-color: #cfc;
      min-height: 30px;
      min-width: calc( (100% - 10px) / 4);
    }
    .three-quarters {
      width: 75%;
    }
    #udacity {
      height: 100px;
      width: auto;
    }
    .box:nth-child(even) {
      background-color: #cfc;
    }
    .abs {
      position: absolute;
      top: 30px;
      left: 0px;
    }
    .box4 {
      position: absolute;
      /* Not all user-agents will make it 8px wide (font) */
      width: 8px;
      left: calc(100% - 8px);
      /*bottom: 0;*/
    }
    .center-box-container {
      background-color: #fcf;
      width: 300px;
      height: 50px;
    }
    .center {
      width: 100px;
      margin-left: auto;
      margin-right: auto;
      text-align: center;
    }
    .output {
      background: #ddd !important;
      border: 1px solid black;
      height: 5em;
      overflow-y: scroll;
      font-family: monospace;
      padding: 0px 8px;
    }
    .flex-container {
      display: flex;
      justify-content: space-between;
    }
  </style>
<head>
<body>
  <h1>Grading Engine Test!</h1>
  <img id="udacity" src="udacity.jpg" alt="udacity">
  <div class="box box1 three-quarters">75% width</div>
  <div class="box box2">b</div>
  <div class="box box3 innerHTML">this is the correct innerHTML</div>
  <div class="box box4">d</div>
  <div class="box box5" test>has test attr</div>
  <div class="box box6">f</div>
  <div class="box box7 abs">looking at this one’s abs pos</div>
  <div class="box box8">h</div>
  <div class="box box9">i</div>
  <div class="box box10">j</div>
  <div class="center-box-container">
    <div class="box box11 center">center</div>
  </div>
  <p>Hello, I’m a <code>p</code> tag.</p>
  <div class="flex-container">
    <div class="flex flex1"></div>
    <div class="flex flex2"></div>
    <div class="flex flex3"></div>
    <div class="flex flex4">This box is not the last child</div>
    <div class="flex flex5">Last child of this container</div>
  </div>
  <div class="box output">INIT</div>
  <script>
    function getDateTime() {
      var d = new Date();
      var date = '';

      var year = d.getFullYear().toString();
      var month = (+d.getMonth() + 1).toString();
      var day = d.getDate().toString();
      var hours = d.getHours().toString();
      var minutes = d.getMinutes().toString();
      var seconds = d.getSeconds().toString();

      if (month.length < 2) {
        month = '0' + month;
      }
      if (day.length < 2) {
        month = '0' + month;
      }
      if (hours.length < 2) {
        month = '0' + month;
      }
      if (minutes.length < 2) {
        month = '0' + month;
      }
      if (seconds.length < 2) {
        month = '0' + month;
      }
      date = year + month + day + hours + minutes + seconds;
      return date;
    }
    function log(string) {
      let div = document.createElement('div');
      div.classList.add('flex-container');
      let textSpan = document.createElement('span');
      textSpan.innerText = string;

      let dateSpan = document.createElement('span');
      dateSpan.innerText = getDateTime();
      div.appendChild(textSpan);
      div.appendChild(dateSpan);

      output.insertBefore(div, output.firstChild);
    }
    const output = document.querySelector('.output');
    window.addEventListener('ud-test-pass', e => log(e.detail));
  </script>
</body>


================================================
FILE: sample/test.json
================================================
[{
  "name": "Meta Info",
  "code": "Got the meta stuff",
  "tests": [
    {
      "description": "&lt;meta&gt; has name set to udacity-grader",
      "definition": {
        "nodes": "meta",
        "limit": 1,
        "attribute": "name",
        "equals": "udacity-grader"
      }
    },
    {
      "description": "&lt;meta&gt; has a content tag with a link to json",
      "definition": {
        "nodes": "meta",
        "limit": 1,
        "attribute": "content",
        "hasSubstring": ".json"
      },
      "flags": {
        "optional": true
      }
    }
  ]
},
{
  "name": "These should pass",
  "code": "you should see this",
  "tests": [
    {
      "description": "There are 19 divs",
      "definition": {
        "nodes": "div",
        "get": "count",
        "equals": 19
      }
    },
    {
      "description": "Width check - 75% of page width is between 1 and 10000 px",
      "definition": {
        "nodes": ".three-quarters",
        "cssProperty": "width",
        "isInRange": {
          "lower": 1,
          "upper": 10000
        }
      }
    },
    {
      "description": "There is a Udacity logo",
      "definition": {
        "nodes": "img#udacity",
        "exists": true
      }
    },
    {
      "description": "One div has test attr",
      "definition": {
        "nodes": "div",
        "limit": 1,
        "attribute": "test",
        "exists": true
      }
    },
    {
      "description": "The innerHTML div has correct innerHTML",
      "definition": {
        "nodes": "div.innerHTML",
        "get": "innerHTML",
        "equals": "this is the correct innerHTML"
      }
    },
    {
      "description": "The UA string has Mozilla in it",
      "definition": {
        "get": "UAString",
        "hasSubstring": "Mozilla"
      }
    },
    {
      "description": "page body is bigger than 400px",
      "definition": {
        "nodes": "body",
        "cssProperty": "width",
        "isGreaterThan": 400
      }
    },
    {
      "description": "the random div is in the right spot",
      "definition": {
        "nodes": ".abs",
        "absolutePosition": "top",
        "equals": 30
      }
    },
    {
      "description": "the random div has absolute position left of 0",
      "definition": {
        "nodes": ".abs",
        "absolutePosition": "left",
        "equals": 0
      }
    },
    {
      "description": "the right div is on the far right",
      "definition": {
        "nodes": ".box4",
        "absolutePosition": "right",
        "equals": "max"
      }
    },
    {
      "description": "the box with just a b in it has a b in it",
      "definition": {
        "nodes": ".box2",
        "get": "innerHTML",
        "equals": ["b", "d"]
      }
    },
    {
      "description": "listening to events",
      "definition": {
        "waitForEvent": "ud-test",
        "exists": true
      }, "flags": {
        "noRepeat": true
      }
    },
    {
      "description": "DPR is equal to 2 (optional)",
      "definition": {
        "get": "DPR",
        "equals": 2
      },
      "flags": {
        "optional": true
      }
    },
    {
      "description": "All divs have class box OR flex",
      "definition": {
        "nodes": "div",
        "attribute": "class",
        "hasSubstring": {
          "expected": [".*flex.*", ".*box.*"]
        }
      }
    },
    {
      "description": "Some divs have class box",
      "definition": {
        "nodes": "div",
        "limit": "some",
        "attribute": "class",
        "hasSubstring": ".*box.*"
      }
    },
    {
      "description": "The output box is showing events resulting from tests passing",
      "definition": {
        "nodes": ".output",
        "get": "innerHTML",
        "exists": true
      }
    },
    {
      "description": "The center box is on the middle",
      "definition": {
        "nodes": ".center",
        "cssProperty": "marginLeft",
        "equals": "100px"
      }
    },
    {
      "description": "The center box doesn’t have a margin at bottom",
      "definition": {
        "nodes": ".center",
        "cssProperty": "marginBottom",
        "equals": "0px"
      }
    },
    {
      "description": "The green box is the last child of “.flex-container”",
      "definition": {
        "nodes": ".flex-container",
        "children": ".flex:last-child",
        "attribute": "class",
        "hasSubstring": "flex5"
      }
    },
    {
      "description": "The magenta box is not the last child of “.flex-container”",
      "definition": {
        "nodes": ".flex-container",
        "children": ".flex:last-child",
        "attribute": "class",
        "not": true,
        "hasSubstring": "flex4"
      }
    },
      {
      "description": "The number of “.flex-container:last-child” is 0",
      "definition": {
        "nodes": ".flex-container",
        "children": ".flex-4:last-child",
        "get": "count",
        "equals": 0
      }
    }
  ]
},
{
  "name": "These should fail",
  "code": "it’s bad if you see this",
  "tests": [
    {
      "description": "There are 15 divs (just delete one)",
      "definition": {
        "nodes": "div",
        "get": "count",
        "equals": 15
      }
    },
    {
      "description": "Width check - 75% of page width is between 900 and 1000 px (just resize)",
      "definition": {
        "nodes": ".three-quarters",
        "cssProperty": "width",
        "isInRange": {
          "lower": 800,
          "upper": 900
        }
      }
    },
    {
      "description": "No Udacity logo (delete the Udacity logo)",
      "definition": {
        "nodes": "img#udacity",
        "exists": false
      }
    },
    {
      "description": "No divs have test attr",
      "definition": {
        "nodes": "div",
        "limit": 1,
        "attribute": "test",
        "exists": false
      }
    },
    {
      "description": "The innerHTML div has incorrect innerHTML",
      "definition": {
        "nodes": "div.innerHTML",
        "get": "innerHTML",
        "not": true,
        "equals": "this is the correct innerHTML"
      }
    },
    {
      "description": "The UA string has iPad in it",
      "definition": {
        "get": "UAString",
        "hasSubstring": "iPad"
      }
    },
    {
      "description": "page body is less than 400px",
      "definition": {
        "nodes": "body",
        "cssProperty": "width",
        "isLessThan": 400
      }
    },
    {
      "description": "the random div is in the wrong spot",
      "definition": {
        "nodes": ".abs",
        "absolutePosition": "left",
        "equals": 401
      }
    },
    {
      "description": "the box with just a b in it doesn’t have a c or a p in it",
      "definition": {
        "nodes": ".box2",
        "get": "innerHTML",
        "equals": ["c", "p"]
      }
    },
    {
      "description": "the random div has absolute position left greater than 1",
      "definition": {
        "nodes": ".abs",
        "absolutePosition": "left",
        "isGreaterThan": 1
      }
    },
    {
      "description": "DPR is equal to 0.5",
      "definition": {
        "get": "DPR",
        "equals": 0.5
      },
      "flags": {
        "optional": true
      }
    },
    {
      "description": "All divs have class box AND flex",
      "definition": {
        "nodes": "div",
        "attribute": "class",
        "hasSubstring": {
          "expected": [".*flex.*", ".*box.*"],
          "minValues": 2
        }
      }
    },
    {
      "description": "The output box is not showing events resulting from tests passing",
      "definition": {
        "nodes": ".output",
        "get": "innerHTML",
        "hasSubstring": "$^"
      }
    },
    {
      "description": "The magenta box is the last child of “.flex-container”",
      "definition": {
        "nodes": ".flex-container",
        "children": ".flex:last-child",
        "attribute": "class",
        "hasSubstring": "flex4"
      }
    }
  ]
},
{
  "name": "These tests should err",
  "code": "ERROR ERROR",
  "tests": [
    {
      "description": "absolute positions",
      "definition": {
        "nodes": "body",
        "absolutePositions": "left",
        "equals": 1
      }
    },
    {
      "description": "bad counts",
      "definition": {
        "nodes": "body",
        "get": "counts",
        "equals": 1
      }
    }
  ]
},
{
  "name": "This should err",
  "code": "ERROR ERROR",
  "tests": [
    {
      "description": "absolute positionsss",
      "definitions": {
        "nodes": "body",
        "absolutePositions": "left",
        "equals": 1
      }
    }
  ]
},
{
  "name": "Just one",
  "code": "thing",
  "tests": [
    {
      "description": "There is a body?",
      "definition": {
        "nodes": "body",
        "get": "count",
        "equals": 1
      }
    }
  ]
}]


================================================
FILE: sample/unit-tests.js
================================================
/**
 * @fileOverview This file contains unit tests to be loaded.
 * @name unit-tests.js<sample>
 * @author Cameron Pittman
 * @license GPLv3
 */

console.log('dispatch event');
window.dispatchEvent(new CustomEvent('ud-test', {'detail': 'passed'}));

// unit-test.js<sample> ends here


================================================
FILE: src/app/README.md
================================================
# User interface (view)
## Description
This directory contains files needed to implements the user-interface. It doesn’t contain grading logic (it’s what we can call the Front-End interface).

## License
This directory and the whole project is subject to the [GPLv3 License](../license).


================================================
FILE: src/app/browser_action/browser_action.html
================================================
<!doctype html>
<html>
  <head>
    <style type="text/css">
     #main {
         padding: 1em;
         height: 100%;
         width: 800px;
         font-family: Helvetica, Ubuntu, Arial, sans-serif;
     }
     .alert {
         color: #78f;
         display: none;
     }
     .loader {
         margin-bottom: 1em;
     }
     .loader-description {
         margin-bottom: 1em;
     }
     .autorun.disabled {
         color: #ccc;
     }
     #ud-label-loader.disabled {
         color: #ccc;
     }
     .warning-block {
         width: 80%;
         color: #e00;
         font-weight: bold;
     }
     .warning-icon {
         color: #e9d236;
     }
    </style>
    <link rel="stylesheet" href="../css/common.css" />
    <link rel="stylesheet" href="../css/fonts.css" />
    <link rel="stylesheet" href="../css/ui.css" />
    <meta charset="UTF-8">
  </head>
  <body>
    <div id="main">
      <h3>Udacity Feedback Settings<span id="configs" class="button fa" title="Configure">&#xf013;</span></h3>
      <div class="loader">
        <div class="loader-description">
          Want to write tests? Here’s the <a href="https://github.com/udacity/frontend-grading-engine" target="_blank">API</a>.
        </div>
        <label id="ud-label-loader" for="ud-file-loader"><strong>Load tests from file</strong>
          <input id="ud-file-loader" type="file" accept=".json">
        </label>
        <div class="alert"></div>
      </div>
      <div class="autorun">
        <div class="warning-block" style="display: none"><span class="button fa warning-icon">&#xf071</span><span id="warning-text"></div>
        <form>
          <label for="allow-feedback">Allow feedback on this domain
            <input id="allow-feedback" type="checkbox" value="yes">
          </label>
        </form>
      </div>
    </div>
    <script src="browser_action.js"></script>
  </body>
</html>


================================================
FILE: src/app/browser_action/browser_action.js
================================================
/*global FileReader, chrome */

/**
 * @fileOverview This file contains the browser_action logic.
 * @name browser_action.js<browser_action>
 * @author Cameron Pittman
 * @author Etienne Prud’homme
 * @license GPLv3
 */

// http://html5rocks.com/en/tutorials/file/dndfiles/
/**
 * This function DOESN’T WORK because the browser action closes when the window looses focus. Handle the Drag-and-drop of custom JSON files.
 * @param {DragEvent} evt - The Drag-and-drop event.
 */
function handleFileSelect(evt) {
  var files = evt.target.files;
  var file = files[0];
  var reader = new FileReader();
  var alert = document.querySelector('.alert');
  alert.style.display = 'block';

  reader.onload = function (file) {
    sendDataToTab(file.target.result, 'json');
  };

  reader.onerror = function (e) {
    alert.style.display = 'block';
    alert.textContent = 'Error. Cannot load file.';
    console.log(e);
  };

  if (file.type && (file.type.match('application/json') || file.type.match('text/json'))) {
    alert.textContent = 'JSON found!';
    reader.readAsText(file);
  } else {
    alert.textContent = 'File found';
    alert.style.color = '#a48700';
    reader.readAsText(file);
  }
}

/**
 * Custom function for sending messages to the current tab.
 * @param {*} data - Any message or data that can be serialized
 * @param {string} type - The type of the message.
 * @param {function} [callback] - The function that will receive the response.
 */
function sendDataToTab(data, type, callback) {
  // debugger;
  // get the current tab then send data to it
  chrome.tabs.query({active: true, currentWindow: true}, fireOffData);

  // actually post data to a tab
  /**
   * Sends the message to the current tab.
   * @param {chrome.tabs.Tab[]} arrayOfTabs - An array of tabs.
   */
  function fireOffData (arrayOfTabs) {
    var activeTab = arrayOfTabs[0];
    var activeTabId = activeTab.id;
    var message = {'data': data, 'type': type};
    chrome.tabs.sendMessage(activeTabId, message, {}, function (response) {
      if (callback) {
        callback(response);
      }
    });
  }
}

var allowFeedback = document.querySelector('#allow-feedback');
allowFeedback.onchange = function () {
  if (!this.checked) {
    sendDataToTab('off', 'on-off');
  } else if (this.checked) {
    sendDataToTab('on', 'on-off');
  }
};

document.querySelector('#ud-file-loader').addEventListener('change', handleFileSelect, false);

/**
 * Adds a custom warning message and disable the checkbox.
 * @param {string} message - The custom message.
 * @param {string} type - The type of warning.
 * @param {object} options - Object containing options.
 * @param {bool} options.enableCheckbox - When using the `checkbox`,
 * {@link type}, it enables toggling the checkbox. Otherwise it does nothing.
 * @param {bool} options.checked - When using the `checkbox` {@link type}, it
 * checks the checkbox. Otherwise it does nothing.
 * @param {bool} options.removeFileInput - When using the `fileInput`
 * {@link type}, it removes the file input.
 * @param {bool} options.disableFileInput - When using the `fileInput`,
 * {@link type}, it disables the file input.
 */
function addWarning(message, type, options) {
  options = options || {};

  var fileInput, label;
  var form = document.getElementsByClassName('autorun')[0];
  document.getElementById('warning-text').textContent = message;
  document.getElementsByClassName('warning-block')[0].style.display = 'block';

  if(type === 'disable') {
    document.getElementsByClassName('loader')[0].remove();
  } else if(type === 'checkbox') {
    if(options.enableCheckbox !== true) {
      form.classList.add('disabled');
      allowFeedback.disabled = true;
    }
    if(options.checked === true) {
      allowFeedback.checked = true;
    }
  } else if(type === 'fileInput') {
    fileInput = document.getElementById('ud-file-loader');

    if(options.removeFileInput === true) {
      label = document.getElementById('ud-label-loader');
      label.remove();
    } else if(options.disableFileInput === true) {
      fileInput.disabled = false;
    } else {
      label = document.getElementById('ud-label-loader');
      label.classList.add('disabled');
      fileInput.disabled = true;
    }
  }
}

/**
 * Makes checkbox `checked` if the website is allowed.
 */
function checkSiteStatus () {
  // talk to background script
  sendDataToTab(true, 'background-wake', function (response) {
    switch(response) {
    case true:
      allowFeedback.checked = true;
      break;
    case false:
      allowFeedback.checked = false;
      break;
    case 'chrome_local_exception':
      addWarning('Chrome doesn’t support loading local files automatically', 'checkbox', {enableCheckbox: false, checked: false});
      break;
    case 'unknown_protocol':
      addWarning('Unsupported protocol. Supported protocols are: http, https and (local) file', 'checkbox', {enableCheckbox: false, checked: false});
      break;
    case 'invalid_origin':
      addWarning('The linked JSON page isn’t at the same origin and directory as the document', 'checkbox', {enableCheckbox: false, checked: false});
      break;
    case undefined:
      // response is undefined if there’s no content-script active (so it’s an unsupported URL scheme)
      addWarning('Unsupported URL scheme. Supported URL schemes are: http://, https://, or file://', 'disable', {});
      break;
    default:
      break;
    }
  });
}

/**
 * Adds the gear EventListener for opening configurations.
 */
function initDisplay() {
  var configs = document.getElementById('configs');
  configs.addEventListener('click', function handler() {
    chrome.runtime.openOptionsPage();
  });
}

// Firefox dev edition seems to load a browser action script asynchronously (while having the async property set to false). Pretending it’s not a bug, that may be a workaround for future Firefox releases.
window.addEventListener('DOMContentLoaded', function(event) {
  checkSiteStatus();
  initDisplay();
});

// browser_action.js<browser_action> ends here


================================================
FILE: src/app/css/common.css
================================================
/**
 * This file was taken in part from uBlock Origin. It contains styles common
 * to the extension.
 */

* {
  box-sizing: border-box;
}

body {
  background-color: #fafbfc;
  color: #4f4f4f;
  font: 14px/1.3 sans-serif;
}

a {
  color: #02b3e4;
  text-decoration: none;
  font-weight: 600;
}

code {
  background-color: #f9f2f4;
  border-radius: 4px;
  color: #c7254e;
  font-family: Lucida Console,Monaco,Courier,monospace;
}

/* common.css<css> ends here */

================================================
FILE: src/app/css/fonts/OFL.txt
================================================
Copyright (c) <dates>, <Copyright Holder> (<URL|email>),
with Reserved Font Name <Reserved Font Name>.
Copyright (c) <dates>, <additional Copyright Holder> (<URL|email>),
with Reserved Font Name <additional Reserved Font Name>.
Copyright (c) <dates>, <additional Copyright Holder> (<URL|email>).

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL


-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------

PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.

The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded, 
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.

DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.

"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).

"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).

"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.

"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.

PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:

1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.

2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.

3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.

4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.

5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.

TERMINATION
This license becomes null and void if any of the above conditions are
not met.

DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.


================================================
FILE: src/app/css/fonts.css
================================================
/**
 * This file contains custom fonts for the extension.
 */

@font-face {
  font-family: 'FontAwesome';
  font-weight: normal;
  font-style: normal;
  src: url('fonts/fontawesome-webfont.ttf') format('truetype');
}

@font-face {
  font-family: 'Source Sans Pro';
  font-style: normal;
  font-weight: 400;
  src: local('Source Sans Pro'), local('SourceSansPro-Regular'), url('fonts/SourceSansPro-Regular.ttf') format('truetype');
}

/* fonts.css<css> ends here */

================================================
FILE: src/app/css/options.css
================================================
/**
 * This file contains styles for the options page.
 */

table {
  border: 3px solid #2c3b48;
  border-spacing: 0;
  color: #444;
  line-height: 37px;
  margin: 0 auto;
  width: 100%;
  max-width: 620px;
}

.whitelist-title {
  background-color: #2c3b48;
  border: none;
  color: #fff;
}

tbody tr:hover {
  background-color: #cce2e9;
}

.whitelist-type th {
  background-color: #7d97ad;
  /* border-bottom: 3px solid #ccc; */
  color: #2c3b48;
  padding-left: 1em;
  text-align: left;
}

.whitelist-row td {
  border-bottom: 1px solid #00b4e4;
}

.whitelist-placeholder {
  text-align: center;
}
.whitelist-placeholder:hover {
  background: none;
}

.chromium-message {
  text-align: left;
  padding: 1em 1em 0 1em;
  line-height: 1em;
}

tbody .whitelist-row:last-child td {
  border-bottom: none;
}

.entry {
  padding-left: 0.5em;
}

#add-entry input {
  width: 100%;
  height: 100%;
}

.remove {
  text-align: right;
}

.hiddenFileInput {
  height: 0;
  visibility: hidden;
  width: 0;
}

#back-button {
  display: none;
  left: .5em;
  position: absolute;
  top: .5em;
}

main, footer {
  border: 3px solid #2c3b48;
  border-spacing: 0;
  color: #444;
  line-height: 37px;
  margin-left: auto;
  margin-right: auto;
  margin-top: 2em;
  width: 100%;
  max-width: 620px;

}

.title {
  background-color: #2c3b48;
  border: none;
  color: #fff;
  font-weight: 700;
  margin: 0;
  text-align: center;
}

.description {
  background-color: #7d97ad;
  color: #2c3b48;
  font-weight: 700;
  padding-left: 1em;
  margin: 0;
  text-align: left;
}

.description a {
  color: #2c3b48;
}

.about {
  line-height: 2em;
  padding: 0 1em;
  text-align: left;
}

/* options.css<css> ends here */

================================================
FILE: src/app/css/ui.css
================================================
/**
 * This file contains the User Interface styles of the extension.
 */

button.custom {
  padding: 0.6em 1em;
  border: 1px solid transparent;
  border-color: #ccc #ccc #bbb #bbb;
  border-radius: 3px;
  background-color: hsl(216, 0%, 75%);
  background-image: linear-gradient(#f2f2f2, #dddddd);
  background-repeat: repeat-x;
  color: #000;
  opacity: 0.8;
}

button.custom.disabled,
button.custom[disabled] {
  border-color: #ddd #ddd hsl(36, 0%, 85%);
  background-color: hsl(36, 0%, 72%);
  background-image: linear-gradient(#f2f2f2, #dddddd);
  color: #666;
  opacity: 0.6;
  pointer-events: none;
}

button.custom:hover {
  opacity: 1.0;
}

button.important {
  padding: 0.6em 1em;
  border: 1px solid transparent;
  border-color: #ffcc7f #ffcc7f hsl(36, 100%, 73%);
  border-radius: 3px;
  background-color: hsl(36, 100%, 75%);
  background-image: linear-gradient(#ffdca8, #ffcc7f);
  background-repeat: repeat-x;
  color: #222;
  opacity: 0.8;
}

button.important:hover {
  opacity: 1.0;
}

/* Not to be confused with button */
.button {
  border: none;
  box-sizing: border-box;
  cursor: pointer;
  display: inline-block;
  font-size: 150%;
  margin: 0;
  padding: 8px;
}

.fa {
  display: inline-block;
  font-family: FontAwesome;
  font-style: normal;
  font-weight: normal;
  line-height: 1;
  vertical-align: middle;
}

/* ui.css<css> ends here */

================================================
FILE: src/app/js/inject/StateManager.js
================================================
/*global removeFileNameFromPath, importFeedbackWidget, injectGradingEngine, loadLibraries, loadJSONTestsFromFile, registerTestSuites, turnOn, waitForTestRegistrations, loadUnitTests, chrome, injectedElementsOnPage, injectIntoDocument, importComponentsLibrary, removeInjectedFromDocument, removeFromDocument */

/**
 * @fileOverview This file contains the StateManager Class.
 * @name StateManager.js<inject>
 * @author Cameron Pittman
 * @author Etienne Prud’homme
 * @license GPLv3
 * @todo Add a warning if the widget fails to initialize.
 */

/**
 * State of the current Document.
 * @returns {Promise}
 * @throws {Error} An error coming from a Promise.
 */
function StateManager() {
  this.whitelist = {remote: [], local: []};
  this.hostIsAllowed = false;
  this.host = window.location.origin;
  this.isChromium = window.navigator.vendor.toLocaleLowerCase().indexOf('google') !== -1;

  if(this.host.search(/^(?:https?:)\/\/[^\s\.]/) !== -1)
  {
    this.type = 'remote';
  } else if(this.host === 'null' || this.host.search('file://') !== -1) {
    if(window.location.protocol === 'file:') {
      this.host = removeFileNameFromPath(window.location.pathname);
      this.type = 'local';
    } else {
      throw new Error('Unknown URL formatting error');
    }
  } else {
    throw new Error('Unknown URL formatting error');
  }

  this.geInjected = false;

  var currentlyInjecting = false;

  /**
   * Run a sequence of Promises to activate the Grading Engine.
   * @returns {Promise}
   * @throws {Error} An error coming from a Promise.
   */
  function runLoadSequence() {
    var self = this;
    if (!currentlyInjecting || self.geInjected) {
      currentlyInjecting = true;
      return importComponentsLibrary()
        .then(importFeedbackWidget())
        .then(injectGradingEngine)
        .then(loadLibraries)
        .then(loadJSONTestsFromFile)
        .then(registerTestSuites)
        .then(turnOn)
      // This is to prevent UnitTests and other things in the page to execute before all tests are registered
        .then(waitForTestRegistrations)
        .then(loadUnitTests)
        .then(function() {
          self.geInjected = true;
          currentlyInjecting = false;
          return Promise.resolve();
        }, function(e) {
          // debugger;
          console.log(e);
          throw new Error('Something went wrong loading Udacity Feedback. Please reload.');
        });
    } else {
      return Promise.resolve();
    }
  }

  /**
   * Checks if the host is allowed to execute the Grading Engine (and arbitrary tests).
   * @returns {Promise}
   */
  this.isSiteOnWhitelist = function() {
    var self = this;
    self.isAllowed = false;
    return new Promise(function(resolve, reject) {
      var type = self.type;
      chrome.storage.sync.get('whitelist', function(response) {
        self.whitelist = response.whitelist || {remote: [], local: []};
        // console.log(self.whitelist);
        if (!(self.whitelist[type] instanceof Array)) {
          self.whitelist[type] = [self.whitelist[type]];
        }
        if (self.whitelist[type].indexOf(self.host) > -1) {
          self.isAllowed = true;
        } else {
          self.isAllowed = false;
        }
        resolve(self.isAllowed);
      });
    });
  };

  /**
   * Adds the current Document host to the whitelist (local storage).
   * @returns {Promise}
   */
  this.addSiteToWhitelist = function() {
    var self = this;
    return new Promise(function(resolve, reject) {
      var type = self.type;
      if(!type) {
        reject();
      }
      var index = self.whitelist[type].indexOf(self.host);
      if (index === -1) {
        self.whitelist[type].push(self.host);
      }
      self.isAllowed = true;

      var data = {whitelist: self.whitelist};
      chrome.storage.sync.set(data, function() {
        // debugger;
        resolve();
      });
    });
  };

  /**
   * Removes the current document host from the whitelist (local storage).
   * @param {string} site - unused
   * @returns {Promise}
   */
  this.removeSiteFromWhitelist = function(site) {
    var self = this;
    return new Promise(function(resolve, reject) {
      var type = self.type;
      var index = self.whitelist[type].indexOf(self.host);
      if (index > -1) {
        self.whitelist[type].splice(index, 1);
      }
      self.isAllowed = false;
      var data = {whitelist: self.whitelist};
      chrome.storage.sync.set(data, function() {
        // debugger;
        resolve();
      });
    });
  };

  /**
   * Getter for {@link isAllowed} property. This property shows if the website is on the whitelist.
   * @returns {boolean} The {@link isAllowed} property.
   }
   */
  this.getIsAllowed = function() {
    if(this.isChromium && this.type === 'local') {
      return 'chrome_local_exception';
    }
    return this.isAllowed;
  };

  /**
   * Method that activates the {@link runLoadSequence}.
   * @returns {Promise}
   */
  this.turnOn = function() {
    var self = this;
    var g = document.querySelector('#ud-grader-options');
    if (g) {
      document.head.removeChild(g);
    }
    if (!self.geInjected) {
      return runLoadSequence().then(function() {
        Promise.resolve(true);
      });
    } else {
      return Promise.resolve(true);
    }
  };

  /**
   * Method that desactivates the `test-widget`.
   * @returns {Promise}
   * @throws {it’s cool} do nothing
   */
  this.turnOff = function() {
    var self = this;

    removeFromDocument('ud-grader-options');
    return injectIntoDocument('script', {
      id: 'ud-grader-options',
      // Reviewer: This is safe to pass.
      innerHTML: 'UdacityFEGradingEngine.turnOff();' +
        'delete window.UdacityFEGradingEngine;' +
        'window.addEventListener("killUdacityFEGradingEngine", function handler() {' +
        '  window.removeEventListener("killUdacityFEGradingEngine", handler, false);' +
        '  window.dispatchEvent(new Event("killedGradingEngine"));' +
        '}, false);'
    }, 'head')
      .then(function() {
        return new Promise(function(resolve, reject) {
          window.addEventListener('killedGradingEngine', function handler() {
            window.removeEventListener('killedGradingEngine', handler, false);
            resolve();
          }, false);
          window.dispatchEvent(new Event('killUdacityFEGradingEngine'));
        });
      })
      .then(function() {
        removeInjectedFromDocument();
        // wish I could unregister <test-widget>, but it doesn’t look like it’s possible at the moment
        self.geInjected = false;
      })
      .catch(function(e) {
        throw e;
      });
  };
}

// StateManager.js<inject> ends here


================================================
FILE: src/app/js/inject/helpers.js
================================================
/*global injectedElementsOnPage */

/**
 * @fileOverview This file contains various functions that aren’t specific to the current extension.
 * @name helpers.js<inject>
 * @author Cameron Pittman
 * @author Etienne Prud’homme
 * @license GPLv3
 */

/**
 * Adds elements to the main page.
 * @param  {String} tag       Type of element
 * @param  {Object} data      Key/value pairs you want to be assigned to as newTag[key] = value
 * @param  {Object} [location]  Set to “head” if you want the element to end up there. Default is body
 * @return {Promise}
 */
function injectIntoDocument(tag, data, location) {
  // debugger;
  location = location || 'body';
  return new Promise(function(resolve, reject) {
    var newTag = document.createElement(tag);
    // Firefox fix because it considers dynamically injected scripts as async
    if(tag === 'script') {
      newTag.async = false;
      newTag.setAttribute('charset', 'utf-8');
    }

    if (data) {
      for (var prop in data) {
        newTag[prop] = data[prop];
      }
    }

    if (!newTag.id) {
      newTag.id = 'ud-' + Math.floor(Math.random() * 100000000).toString();
    }

    // for later removal
    injectedElementsOnPage.push(newTag.id);

    newTag.onload = function(e) {
      resolve(e);
    };
    newTag.onerror = function(e) {
      reject(e);
    };
    if (tag === 'script' && !newTag.src && (newTag.text || newTag.innerHTML)) {
      resolve();
    }
    if (location === 'head') {
      document.head.appendChild(newTag);
    } else {
      document.body.appendChild(newTag);
    }
  });
}

/**
 * Removes all injected elements from the document.
 */
function removeInjectedFromDocument() {
  injectedElementsOnPage.forEach(function(item) {
    var element = document.getElementById(item);
    var parent;

    if(element !== null) {
      parent = element.parentNode;
      parent.removeChild(element);
    }
  });
  injectedElementsOnPage = [];
}


/**
 * Removes a single resource from {@link injectedElementsOnPage}.
 * @param {String} id - The ID of the element.
 */
function removeFromDocument(id) {
  var element = document.getElementById(id);
  var parent;
  injectedElementsOnPage.splice(injectedElementsOnPage.indexOf(id), 1);

  if(element !== null) {
    parent = element.parentNode;
    parent.removeChild(element);
  }
}

/**
 * Removes a file name from a given path. It return the basename.
 * @param {string} path - The file path.
 * @returns {string} The basename of the path.
 */
function removeFileNameFromPath(path) {
  path = path.substr(0, path.lastIndexOf('/') + 1);

  // If there’s a hashtag present, it can simulate a path
  if(path.indexOf('#') !== -1) {
    // Remove another URL part until there’s no hashtags
    path = removeFileNameFromPath(path);
  }
  return path;
}

// helpers.js<inject> ends here


================================================
FILE: src/app/js/inject/inject.js
================================================
/*global removeFileNameFromPath, injectIntoDocument, chrome, StateManager */

/**
 * @fileoverview This file manages the injection of several JavaScript files. It contains most procedure for injecting those files, but doesn’t handle the conditional injection part.
 * @name inject.js<inject>
 * @author Cameron Pittman
 * @author Etienne Prud’homme
 * @license GPLv3
 */

/**
 * List of items id that were injected in the page. It is used to later remove them.
 * @type {string[]}
 */
var injectedElementsOnPage = [];

var runtimeError = null;

/**
 * The meta tag that is used to load and activate a file of tests.
 * @type {Element}
 */
var metaTag = document.querySelector('meta[name="udacity-grader"]');

function importComponentsLibrary() {
  var cScript = document.querySelector('script#components-lib');

  if(!cScript) {
    return injectIntoDocument('script', {
      src: chrome.extension.getURL('lib/components.js'),
      id: 'components-lib'
    }, 'head');
  } else {
    return Promise.resolve();
  }
}

/**
 * Finds Web Components templates.
 * @returns {Promise}
 */
function importFeedbackWidget() {
  var twScript = document.querySelector('script#udacity-test-widget');

  if (!twScript) {
    return injectIntoDocument('script', {
      src: chrome.extension.getURL('app/templates/templates.js'),
      id: 'udacity-test-widget'
    }, 'head');
  } else {
    return Promise.resolve();
  }
}

/**
 * Inject the Grading Engine inside the current Document.
 * @returns {Promise}
 */
function injectGradingEngine() {
  return injectIntoDocument('script', {
    src: chrome.extension.getURL('app/js/libs/GE.js'),
    id: 'udacity-front-end-feedback'
  }, 'head');
}

/**
 * Load custom libraries for the Grading Engine (i.e. jsgrader.js). Currently only `jsgrader.js` is supported and allowed in the manifest.
 * @returns {Promise}
 */
function loadLibraries() {
  if (metaTag) {
    var libraries = metaTag.getAttribute('libraries');
  }

  if (libraries) {
    libraries = libraries.split(' ');
  } else {
    return Promise.resolve();
  }

  var loadedLibs = 0;
  return Promise.all(
    libraries.map(function(lib) {
      return injectIntoDocument('script', {src: chrome.extension.getURL('app/js/libs/' + lib + '.js')}, 'head');
    })
  );
}

/**
 * Adds a unique GET ID in order to make the browser ignore the cache.
 * @param {String} url - A valid absolute URL.
 * @returns {String} The absolute URL and a unique GET ID.
 */
function appendIDToURL(url) {
  var _url = new URL(url);
  var searchParams = _url.searchParams;
  var paramName = 'udacityNoCache';

  while(searchParams.has(paramName)) {
    paramName += Math.floor(Math.random() * 10).toString();
  }

  searchParams.set(paramName, Math.floor(Math.random() * 100000000000).toString());
  _url.searchParams = searchParams.toString;
  return _url.toString();
}

/**
 * Loads asynchronously the JSON file containing the tests.
 * @returns {Promise}
 */
function loadJSONTestsFromFile() {
  if (metaTag) {
    return new Promise(function(resolve, reject) {
      // http://stackoverflow.com/a/14274828
      var xmlhttp = new XMLHttpRequest();
      // The complete path to the document excluding the file name (http://example.com/mydir/ for http://example.com/mydir/file.html)
      var documentBase = removeFileNameFromPath(document.URL);
      var url = metaTag.content;
      var fileBase = '';

      // If it’s not an absolute URL
      if(url.search(/^(?:https?|file):\/\//) === -1) {
        // If it’s protocol relative URL (i.e. //example.com)
        if(url.search(/^\/\//) !== -1) {
          // The window must at least use one of those protocols
          switch(window.location.protocol) {
          case 'http:':
          case 'https:':
          case 'file:':
            url = window.location.protocol + url;
            break;
          default:
            runtimeError = 'unknown_protocol';
            console.warn('Unknown URL protocol. Supported protocols are: http, https and (local) file');
            reject(false);
          }
        } else {
          // it’s probably a relative path (may be garbage)
          url = documentBase + url;
        }
      }

      url = appendIDToURL(url);

      // Extract the file path (http://example.com/mydir/ for http://example.com/mydir/file.html)
      fileBase = url.substr(0, url.lastIndexOf('/') + 1);

      if(fileBase !== documentBase) {
        runtimeError = 'invalid_origin';
        console.warn('Invalid JSON file origin');
        reject(false);
      }

      xmlhttp.onreadystatechange = function() {
        if (xmlhttp.status === 200 && xmlhttp.readyState === 4) {
          // DANGER! Checks if that it wasn’t a redirection
          if(xmlhttp.responseURL !== url) {
            runtimeError = 'redirection';
            console.warn('The JSON request received a redirection. Possible cross-origin request attempt');
            reject(false);
          }
          resolve(xmlhttp.responseText);
        } else if (xmlhttp.status >= 400) {
          reject(false);
        }
      };
      xmlhttp.open('GET', url, true);
      xmlhttp.send();
    });
  } else {
    return Promise.resolve(false);
  }
}

// You don’t have access to the GE here, but you can inject a script into the document that does.
/**
 * Register test suites from the JSON data.
 * @param {string} json - JSON containing tests for the Grading Engine.
 * @returns {Promise}
 * @throws {Error} Errors about the JSON file.
 */
function registerTestSuites(json) {
  if (!json) {
    return Promise.resolve();
  }
  var errorMsg = null;
  // validating the JSON
  try {
    if (json.length > 0) {
      JSON.parse(json);
    }
  } catch (e) {
    if (json.indexOf('\\') > -1) {
      errorMsg = 'Are you trying to use “\\” in a RegEx? Try using \\\\ instead.';
    } else {
      errorMsg = 'Invalid JSON file format.';
    }
  }
  try {
    json = JSON.stringify(json);
  } catch (e) {
    errorMsg = 'Invalid JSON format.';
  }

  if (errorMsg) {
    alert(errorMsg);
    throw new Error(errorMsg);
  } else {
    return injectIntoDocument('script', {text: 'UdacityFEGradingEngine.registerSuites(' + json + ');'}, 'head');
  }
}

/**
 * Checks and injects custom Unit Tests.
 * @returns {Promise}
 */
function loadUnitTests() {
  var unitTests = null;
  if (metaTag) {
    unitTests = metaTag.getAttribute('unit-tests');
  }
  if (!unitTests) {
    return Promise.resolve();
  }

  return injectIntoDocument('script', {src: unitTests, defer: 'defer'});
}

/**
 * Activates the Grading Engine by injecting itself in the Document. Not to be confused with {@link StateManager.turnOn}. This method is called from {@link StateManager~runLoadSequence}.
 * @returns {Promise}
 */
function turnOn() {
  // console.log('Turned on from turnOn()');
  return injectIntoDocument('script', {
    id: 'ud-grader-options',
    // Reviewer: Because we need to access the window script context, it’s
    // necessary to inject the script that way. A content-script doesn’t have
    // access to the window scripting context.
    innerHTML: 'UdacityFEGradingEngine.turnOn();'
  }, 'head');
}

/**
 * Stops {@link StateManager~runLoadSequence} until all tests are loaded. This is necessary because the Grading Engine is activated thought the page context. It isn’t a content script like this file.
 * @todo Add a timeout. If (for some reason) the event is never fired, it would probably block the widget.
 * @returns {Promise} A `Promise` that fulfills when all tests are loaded
 */
function waitForTestRegistrations() {
  return new Promise(function(resolve, reject) {
    window.addEventListener('tests-registered', function(data) {
      // console.log('tests-registered received');
      return resolve();
    });
  });
}

// StateManager() was here

var stateManager = new StateManager();

/**
 * Wait for messages from browser action.
 * @param {Object} message - Object containing a `data` and a `type` property.
 * @param {MessageSender} sender - Information about the Script context.
 * @param {function} sendResponse - Function to call when a response is received.
 */
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
  switch (message.type) {
  case 'json':
    // A JSON test file was passed to the action page
    registerTestSuites(message.data);
    break;
  case 'on-off':
    // The action page checkbox was toggled
    if (message.data === 'on') {
      stateManager.addSiteToWhitelist()
        .then(stateManager.turnOn);
    } else if (message.data === 'off') {
      stateManager.removeSiteFromWhitelist()
        .then(stateManager.turnOff);
    }
    break;
  case 'background-wake':
    if(runtimeError) {
      sendResponse(runtimeError);
    }
    // The action page is requesting infos about the current host
    sendResponse(stateManager.getIsAllowed());
    break;
  default:
    // Just in case of future bad implementation
    console.warn('invalid message type for: %s from %s', message, sender);
    break;
  }
});

/**
 * for first load
 */
window.addEventListener('GE-on', function() {
  if (stateManager.isAllowed) {
    stateManager.turnOn();
  }
});

// Check if the site is on the Whitelist on page load
stateManager.isSiteOnWhitelist()
  .then(function(isAllowed) {
    if (isAllowed) {
      stateManager.turnOn();
    }
  });

// inject.js<inject> ends here


================================================
FILE: src/app/js/libs/jsgrader.js
================================================
/**
 * @fileOverview This file contains the JSGrader library to test the JavaScript context.
 * @name jsgrader.js<libs>
 * @author Cameron Pittman
 * @license GPLv3
 */
var Grader = (function() {

  // http://stackoverflow.com/questions/1068834/object-comparison-in-javascript?lq=1
  function deepCompare () {
    var i, l, leftChain, rightChain;

    function compare2Objects (x, y) {
      var p;

      // remember that NaN === NaN returns false
      // and isNaN(undefined) returns true
      if (isNaN(x) && isNaN(y) && typeof x === 'number' && typeof y === 'number') {
        return true;
      }

      // Compare primitives and functions.
      // Check if both arguments link to the same object.
      // Especially useful on step when comparing prototypes
      if (x === y) {
        return true;
      }

      // Works in case when functions are created in constructor.
      // Comparing dates is a common scenario. Another built-ins?
      // We can even handle functions passed across iframes
      if ((typeof x === 'function' && typeof y === 'function') ||
        (x instanceof Date && y instanceof Date) ||
        (x instanceof RegExp && y instanceof RegExp) ||
        (x instanceof String && y instanceof String) ||
        (x instanceof Number && y instanceof Number)) {
          return x.toString() === y.toString();
      }

      // At last checking prototypes as good a we can
      if (!(x instanceof Object && y instanceof Object)) {
        return false;
      }

      if (x.isPrototypeOf(y) || y.isPrototypeOf(x)) {
        return false;
      }

      if (x.constructor !== y.constructor) {
        return false;
      }

      if (x.prototype !== y.prototype) {
        return false;
      }

      // Check for infinitive linking loops
      if (leftChain.indexOf(x) > -1 || rightChain.indexOf(y) > -1) {
         return false;
      }

      // Quick checking of one object beeing a subset of another.
      // todo: cache the structure of arguments[0] for performance
      for (p in y) {
        if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
          return false;
        }
        else if (typeof y[p] !== typeof x[p]) {
          return false;
        }
      }

      for (p in x) {
        if (y.hasOwnProperty(p) !== x.hasOwnProperty(p)) {
          return false;
        }
        else if (typeof y[p] !== typeof x[p]) {
          return false;
        }

        switch (typeof (x[p])) {
          case 'object':
          case 'function':
            leftChain.push(x);
            rightChain.push(y);

            if (!compare2Objects (x[p], y[p])) {
              return false;
            }

            leftChain.pop();
            rightChain.pop();
            break;

          default:
            if (x[p] !== y[p]) {
              return false;
            }
            break;
        }
      }

      return true;
    }

    if (arguments.length < 1) {
      return true; //Die silently? Don’t know how to handle such case, please help...
      // throw "Need two or more arguments to compare";
    }

    for (i = 1, l = arguments.length; i < l; i++) {

      leftChain = []; //Todo: this can be cached
      rightChain = [];

      if (!compare2Objects(arguments[0], arguments[i])) {
        return false;
      }
    }
    return true;
  }

  function Queue (grader) {
    this.grader = grader;
    this.gradingSteps = [];
    this.flushing = false;
    this.alwaysGo = false;
  };

  Queue.prototype = {
    add: function(callback, messages, keepGoing) {
      if (keepGoing !== false) {
        keepGoing = true;
      }

      if (!callback) {
        throw new Error("Every test added to the queue must have a valid function.");
      }

      this.gradingSteps.push({
        callback: callback,
        isCorrect: false,
        wrongMessage: messages.wrongMessage || null,
        comment: messages.comment || null,
        category: messages.category || null,
        keepGoing: keepGoing
      });
    },

    _flush: function () {
      if (!this.flushing) {
        this.flushing = true;
      }
      this.step();
    },

    clear: function () {
      this.flushing = false;
      this.gradingSteps = [];
      this.grader.endTests();
    },

    step: function () {
      var self = this;
      if (this.gradingSteps.length === 0) {
        this.clear();
      }

      function executeInPromise (fn) {
        return new Promise(function (resolve, reject) {
          if (fn) {
            try {
              var result = fn();
            } catch (e) {
              self.clear();
              console.log(e);
            }
          }
          resolve(result);
        });
      };

      function takeNextStep (test, result) {
        test.isCorrect = result;

        self.registerResults(test);

        if (test.isCorrect || test.keepGoing || self.alwaysGo) {
          self.step();
        } else {
          self.clear();
        }
      };

      if (this.flushing) {
        var test = this.gradingSteps.shift();

        if (this.grader.async) {
          executeInPromise(test.callback).then(function (resolve) {
            takeNextStep(test, resolve);
          });
        } else if (!this.grader.async) {
          try {
            var result = test.callback();
          } catch (e) {
            console.log(e);
            throw new Error();
          }
          takeNextStep(test, result);
        }

      }
    },

    registerResults: function (test) {
      this.grader.registerResults(test);
    }
  };

  function Grader (type, categoryMessages) {
    var self = this;
    this.specificFeedback = [];
    this.comments = [];
    this.isCorrect = false;
    this.correctHasChanged = false;
    this.queue = new Queue(self);
    this.async = false;
    this.categoryMessages = null;
    this.generalFeedback = [];
    this.onresult = function () {};

    for (n in arguments) {
      switch (typeof arguments[n]) {
        case 'string':
          if (arguments[n] === 'async') {
            this.async = true;
          } else if (arguments[n] === 'sync') {
            this.async = false;
          } else {
            throw new Error("Invalid type argument in Grader constructor");
          }
          break;
        case 'object':
          this.categoryMessages = arguments[n];
          break;
        default:
          throw new TypeError("Invalid argument in Grader constructor");
          break;
      }
    }
  };

  Grader.prototype = {
    addTest: function (callback, messages, keepGoing) {
      this.queue.add(callback, messages, keepGoing);
    },

    runTests: function (options) {
      if (options) {
        this.queue.alwaysGo = options.ignoreCheckpoints || false;
      }
      this.queue._flush();
    },

    endTests: function () {
      if (this.queue.flushing) {
        this.queue.clear();
      } else {
        var results = this.gatherResults();
        this.onresult(results);
      }
    },

    registerResults: function (test) {
      this.generateSpecificFeedback(test);
      this.generateGeneralFeedback(test);
      this.setCorrect(test);
    },

    generateSpecificFeedback: function (test) {
      if (!test.isCorrect && test.wrongMessage) {
        this.addSpecificFeedback(test.wrongMessage);
      } else if (test.isCorrect && test.comment) {
        this.addComment(test.comment);
      }
    },

    generateGeneralFeedback: function (test) {
      if (!test.isCorrect && test.category) {
        if (this.generalFeedback.indexOf(this.categoryMessages[test.category]) === -1) {
          this.generalFeedback.push(this.categoryMessages[test.category]);
        }
      }
    },

    setCorrect: function (test) {
      if (this.correctHasChanged) {
        this.isCorrect = this.isCorrect && test.isCorrect;
      } else {
        this.correctHasChanged = true;
        this.isCorrect = test.isCorrect;
      }
    },

    addSpecificFeedback: function (feedback) {
      this.specificFeedback.push(feedback);
    },

    addComment: function (feedback) {
      this.comments.push(feedback);
    },

    gatherResults: function () {
      var self = this;
      return {
        isCorrect: self.isCorrect,
        testFeedback: self.specificFeedback.concat(self.generalFeedback),
        testComments: self.comments
      };
    },

    getFormattedWrongMessages: function (separator) {
      var allMessages, message;

      allMessages = this.specificFeedback.concat(this.generalFeedback);
      message = allMessages.join(separator);

      return message;
    },

    getFormattedComments: function (separator) {
      return this.comments.join(separator);
    },

    isType: function (value, expectedType) {
      var isCorrect = false;

      if (typeof value !== expectedType) {

        if (typeof value === 'function') {
          value = value.name;
        };

        isCorrect = false;
      } else if (typeof value === expectedType){
        isCorrect = true;
      }
      return isCorrect;
    },

    isInstance: function (value, expectedInstance) {
      var isCorrect = false;

      if (value instanceof expectedInstance !== true) {

        isCorrect = false;
      } else if (value instanceof expectedInstance === true){
        isCorrect = true;
      }
      return isCorrect;
    },

    isValue: function (value1, value2) {
      var isCorrect = false;

      if (!deepCompare(value1, value2)) {
        isCorrect = false;
      } else if (deepCompare(value1, value2)) {
        isCorrect = true;
      }
      return isCorrect;
    },

    isInRange: function (value, lower, upper) {
      var isCorrect = false;

      if (typeof value !== 'number' || isNaN(value)) {
        isCorrect = false;
      } else if (value > upper || value < lower) {
        isCorrect = false;

      } else if (value < upper || value > lower) {
        isCorrect = true;
      }
      return isCorrect;
    },

    isSet: function (value) {
      var isCorrect = false;

      if (value === undefined) {
        isCorrect = false;

      } else {
        isCorrect = true;
      }
      return isCorrect;
    },

    isjQuery: function (elem) {
      // could use obj.jquery, which will only return true if it is a jquery object
      var isjQ = false;
      if (elem instanceof $) {
        isjQ = true;
      }
      return isjQ;
    },

    hasCorrectTag: function (elem, tag) {
      if (!this.isjQuery(elem)) {
        elem = $(elem);
      }
      var hasTag = false;
      if (elem.is(tag)) {
        hasTag = true;
      }
      return hasTag;
    },

    hasCorrectClass: function (elem, className) {
      if (!this.isjQuery(elem)) {
        elem = $(elem);
      }
      var hasClass = false;
      if (elem.hasClass(className)) {
        hasClass = true;
      }
      return hasClass;
    },

    hasCorrectId: function (elem, id) {
      if (!this.isjQuery(elem)) {
        elem = $(elem);
      }
      if (elem.is('#' + id)) return true;
      return false;
    },

    hasCorrectText: function (elem, text) {
      if (!this.isjQuery(elem)) {
        elem = $(elem);
      }
      var hasText = false;
      var re = new RegExp(text);
      if (elem.text().match(re)) {
        hasText = true;
      }
      return hasText;
    },

    hasAttr: function (elem, attrName, correctAttr) {
      var isCorrect = false;
      if (!this.isjQuery(elem)) {
        elem = $(elem);
      }
      if (correctAttr && elem.attr(attrName) === correctAttr) {
        isCorrect = true;
      } else if (!correctAttr && elem.attr(attrName)) {
        isCorrect = true;
      }
      return isCorrect;
    },

    hasCorrectLength: function (elems, _length) {
      if (!this.isjQuery(elems)) {
        elems = $(elems);
      }
      var correctLength = false;
      var cLength = elems.length;
      if (cLength === _length) {
        correctLength = true;
      }
      return correctLength;
    },

    isCorrectElem: function (elem, correctElem) {
      if (!this.isjQuery(elem)) {
        elem = $(elem);
      }
      var is = false;
      if (elem.is(correctElem)) {
        is = true;
      }
      return is;
    },

    isCorrectCollection: function (collection, correctCollection) {
      if (!this.isjQuery(elem)) {
        elem = $(elem);
      }
      var is = false;
      if (collection.is(correctCollection)) {
        is = true;
      }
      return is;
    },

    hasCorrectStyle: function (elem, cssProperty, _correctStyle) {
      if (!this.isjQuery(elem)) {
        elem = $(elem);
      }
      var hasCorrectStyle = false;
      var currentStyle = elem.css(cssProperty);
      if (currentStyle  === _correctStyle) {
        hasCorrectStyle = true;
      }
      return hasCorrectStyle;
    },

    doesExistInParent: function (elem, parentElem) {
      if (!this.isjQuery(elem)) {
        elem = $(elem);
      }
      if (!this.isjQuery(parentElem)) {
        parentElem = $(parentElem);
      }
      var inParent = false;
      if (parentElem.find(elem).length > 0) {
        inParent = true;
      }
      return inParent;
    },

    elemDoesExist: function (elem) {
      if (!this.isjQuery(elem)) {
        elem = $(elem);
      }
      var exists = false;
      if (elem.length > 0) {
        exists = true;
      }
      return exists;
    },

    areSiblings: function (elem1, elem2) {
      if (!this.isjQuery(elem1)) {
        elem1 = $(elem1);
      }
      if (!this.isjQuery(elem2)) {
        elem2 = $(elem2);
      }
      var siblingLove = false;
      if (elem1.siblings(elem2).length > 0) {
        siblingLove = true;
      }
      return siblingLove;
    },

    isImmediateChild: function (elem, parentElem) {
      var isCorrect = false;
      if (this.isjQuery(elem)) {
        throw new Error("elem needs to be a string for Grader.isImmediateChild()");
      }
      if (!this.isjQuery(parentElem)) {
        parentElem = $(parentElem);
      }
      if (parentElem.children(elem).length > 0) {
        isCorrect = true;
      }
      return isCorrect;
    },

    hasParent: function (elem, parentElem) {
      var isCorrect = false;
      if (this.isjQuery(parentElem)) {
        throw new Error("parentElem needs to be a string for Grader.hasParent()");
      }
      if (!this.isjQuery(elem)) {
        elem = $(elem);
      }
      if (elem.closest(parentElem).length > 0) {
        isCorrect = true;
      }
      return isCorrect;
    },

    sendResultsToExecutor: function () {
      var output = {
        isCorrect: false,
        test_feedback: "",
        test_comments: "",
        congrats: ""
      };

      for (arg in arguments) {
        var thisIsCorrect = arguments[arg].isCorrect;
        var thisTestFeedback = arguments[arg].getFormattedWrongMessages();
        var thisTestComment = arguments[arg].getFormattedComments();
        if (typeof thisIsCorrect !== 'boolean') {
          thisIsCorrect = false;
        }

        switch (arg) {
          case '0':
            output.congrats = arguments[arg];
          case '1':
            output.isCorrect = thisIsCorrect;
            output.test_feedback = thisTestFeedback;
            output.test_comments = thisTestComment;
            break;
          default:
            output.isCorrect = thisIsCorrect && output.isCorrect;
            if (output.test_feedback !== "") {
              output.test_feedback = [output.test_feedback, thisTestFeedback].join('\n');
            } else {
              output.test_feedback = thisTestFeedback;
            }

            if (output.test_comments !== "") {
              output.test_comments = [output.test_comments, thisTestFeedback].join('\n');
            } else {
              output.test_comments = thisTestComment;
            }
            break;
        }
      }
      output = JSON.stringify(output);
      console.info("UDACITY_RESULT:" + output);
    }
  };
  return Grader;
})();

// jsgrader.js<libs> ends here


================================================
FILE: src/app/options/index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <title>Udacity Feedback</title>
    <style>
     #whitelist-entry-template {
         display: none;
     }
    </style>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="../css/common.css" />
    <link rel="stylesheet" href="../css/fonts.css" />
    <link rel="stylesheet" href="../css/options.css" />
    <link rel="stylesheet" href="../css/ui.css" />
  </head>
  <body>
    <table class="whitelist">
      <colgroup><col><col></colgroup>
      <thead>
        <tr class="whitelist-title">
          <th colspan="2"><span id="back-button" class="button fa">&#xf112;</span>Whitelist</th>
        </tr>
        <tr class="whitelist-type">
          <th colspan="2">Remote hosts<span id="remote-add" class="add-remote-entry button fa" title="Add remote host">&#xf196;</span></th>
        </tr>
      </thead>
      <tbody id="remote-whitelist">
        <tr id="whitelist-entry-template" class="whitelist-row">
          <td class="entry">host</td>
          <td class="remove"><span class="remove-entry button fa" title="Remove">&#xf00d;</span></td>
        </tr>
        <tr class="whitelist-placeholder">
          <td colspan="2"><span class="whitelist-message">Nothing to show here</span><span class="button fa">&#xf119;</span></td>
        </tr>
      </tbody>
      <thead>
        <tr class="whitelist-type">
          <th colspan="2">Local directories<span id="local-add" class="add-remote-entry button fa" title="Add local directory">&#xf196;</span></th>
        </tr>
      </thead>
      <tbody id="local-whitelist">
        <tr class="whitelist-placeholder">
          <td colspan="2"><span class="whitelist-message">Nothing to show here</span><span class="button fa">&#xf119;</span></td>
        </tr>
      </tbody>
    </table>
    <main>
      <p class="title">Help</p>
      <p class="description">Usage</p>
      <p class="about">Not sure what how to make it work? Try the <a href="http://labs.udacity.com/udacity-feedback-extension/">walkthrough</a>.</p>
      <p class="description">Reporting bugs</p>
      <p class="about">Because this extension tries to supports multiple browsers, it’s not unusual to find bugs when a new browser version rolls out. For any suggestions or bug reports, please make a <a href="https://github.com/udacity/frontend-grading-engine/issues" href="">new issue</a> on the GitHub platform. Make sure to include your browser version and the currently installed version of the extension.</p>
      <p class="description">Contact</p>
      <p class="about">You can contact us at <code>udacityfeedback@udacity.com</code>.<p>
    </main>
    <footer>
      <p class="title">About</p>
      <p class="description"><a title="About the extension" href="https://github.com/udacity/frontend-grading-engine">Udacity Front End Feedback<span class="button fa">&#xf08e;</span></a></p>
      <p class="about">Immediate, visual feedback about any website’s HTML, CSS and JavaScript.</p>
      <p class="about">Version <code id="extension-version"></code> for <span id="browser-name"></span></p>
    </footer>
    <script src="options.js"></script>
  </body>
</html>


================================================
FILE: src/app/options/options.js
================================================
/*global chrome, browserName */

/**
 * @fileOverview This file contains the option page for adding/removing websites from the whitelist.
 * @name options.js<options>
 * @author Cameron Pittman
 * @author Etienne Prud’homme
 * @license GPLv3
 * @todo remove trailing / from URLs
 */

var remoteWhitelist = document.querySelector('#remote-whitelist');
var localWhitelist = document.querySelector('#local-whitelist');
var isChromium = window.navigator.vendor.toLocaleLowerCase().indexOf('google') !== -1;

function StateManager() {
  this.whitelist = {remote: [], local: []};
};

StateManager.prototype = {
  /**
   * Get the whitelist from the storage.
   * @returns {Promise} A promise that resolves when the data is received.
   */
  getWhitelist: function() {
    var self = this;
    return new Promise(function (resolve, reject) {
      chrome.storage.sync.get('whitelist', function (response) {
        self.whitelist = response.whitelist || {remote: [], local: []};

        if (!(self.whitelist.remote instanceof Array ||
              Object.prototype.toString.call(self.whitelist.remote) === '[object Array]')) {
          self.whitelist.remote = [self.whitelist.remote];
        }
        if (!(self.whitelist.local instanceof Array ||
              Object.prototype.toString.call(self.whitelist.remote) === '[object Array]')) {
          self.whitelist.local = [self.whitelist.local];
        }
        resolve(self.whitelist);
      });
    });
  },
  /**
   * Add a given site to the stored whitelist and the {@link StateManager.whitelist}.
   * @param {string} site - A URL to add to the whitelist.
   * @param {string} type - The type of site. It either be: `remote` or `local`.
   * @returns {Promise} A promise that resolves when the data is set.
   */
  addSiteToWhitelist: function(site, type) {
    var self = this;
    return new Promise(function (resolve, reject) {
      if(type === 'remote') {
        if(site.search(/^(?:https?:)\/\/[^\s\.]/) === -1) {
          reject('The site is not a valid URL. The URL must at least contains the http:// or https:// scheme');
        }
        resolve();
      } else if(type === 'local') {
        if(site.search(/^file:\/\/\/?[^\s\.]/) === -1) {
          reject('The site is not a valid local URL. The URL must at least contains the file:// scheme');
        }
        resolve();
      } else {
        reject('type');
      }}).then(function() {

        var index = self.whitelist[type].indexOf(site);
        if (index === -1) {
          self.whitelist[type].push(site);
        }
        self.isAllowed = true;
        var data = {whitelist: {remote: self.whitelist.remote, local: self.whitelist.local}};
        chrome.storage.sync.set(data, function () {
          Promise.resolve();
        });
      });
  },
  /**
   * Remove a given site from the stored whitelist and the {@link StateManager.whitelist}.
   * @param {string} site - A URL to remove from the whitelist.
   * @param {string} type - The type of site. It either be: `remote` or `local`.
   * @returns {Promise} A promise when the data is set.
   */
  removeSiteFromWhitelist: function(site, type) {
    var self = this;
    return new Promise(function (resolve, reject) {
      if(type !== 'remote' && type !== 'local') {
        reject('type');
      }

      var index = self.whitelist[type].indexOf(site);
      if (index > -1) {
        self.whitelist[type].splice(index, 1);
      }
      self.isAllowed = false;
      var data = {whitelist: {remote: self.whitelist.remote, local: self.whitelist.local}};
      chrome.storage.sync.set(data, function () {
        resolve();
      });
    });
  }
};

/**
 * Adds buttons to add entries.
 */
function initDisplay() {
  var manifest = chrome.runtime.getManifest();
  var extensionVersion = document.getElementById('extension-version');
  extensionVersion.textContent = manifest.version;
  document.getElementById('browser-name').textContent = browserName;

  var remoteAdd = document.getElementById('remote-add');
  remoteAdd.addEventListener('click', function handler(event) {
    newInputEntry('remote');
  });

  var localAdd = document.getElementById('local-add');
  if(localAdd !== null) {
    localAdd.addEventListener('click', function handler(event) {
      newInputEntry('local');
    });
  }
}

/**
 * Removes entries from the whitelist table.
 */
function cleanDisplay() {
  var entryCollection = document.getElementsByClassName('whitelist-row');
  var entries = [], i, len;

  // An HTMLCollection would remove its item if we used
  // `entryCollection[i].remove()` thus decreasing the lenght. That’s why we
  // convert the collection to an Array
  for(i=0, len=entryCollection.length; i<len; i++) {
    if(entryCollection[i].id !== 'whitelist-entry-template') {
      entries.push(entryCollection[i]);
    }
  }

  for(i=0, len=entries.length; i<len; i++) {
    entries[i].remove();
  }
}

/**
 * Updates the whitelist table.
 */
function refreshDisplay() {
  cleanDisplay();

  refreshSection('remote');
  refreshSection('local');

  function refreshSection(type) {
    var whitelist = stateManager.whitelist[type];
    var isEmpty = true,
        newTypeEntry = type === 'remote' ? newRemoteEntry : newLocalEntry,
        whitelistElem;

    for(var i=0, len=whitelist.length; i<len; i++) {
      if(whitelist[i]) {
        newTypeEntry(whitelist[i]);
        isEmpty = false;
      }
    }

    var placeholderDisplay = isEmpty ? 'table-row' : 'none';
    whitelistElem = document.getElementById(type + '-whitelist');
    whitelistElem.getElementsByClassName('whitelist-placeholder')[0].style.display = placeholderDisplay;
  }
}

/**
 * Return a new entry for the whitelist created from a template. The entry should isn’t attached to the document.
 * @param {string} data - The text node of the entry (`.entry`).
 * @param {string} type - The type of entry. It either be: `add-entry`, `remote` or `local`.
 * @returns {HTMLElement} The newly created entry.
 */
function newEntry(data, type) {
  var template = document.getElementById('whitelist-entry-template');
  var entry = template.cloneNode(true);
  entry.removeAttribute('id');

  if(type === 'add-entry') {
    entry.id = 'add-entry';
  }

  entry.getElementsByClassName('entry')[0].textContent = data;
  entry.getElementsByClassName('remove-entry')[0].addEventListener('click', function handler(event) {
    event.preventDefault();
    entry.remove();
    window.dispatchEvent(new CustomEvent('remove', {detail: {type: type, data: data}}));
  });
  return entry;
}

/**
 * Adds and attach a new entry in the **remote** section of the whitelist table.
 * @param {string} url - The URL (text) of the entry.
 * @returns {HTMLElement} A reference to the newly attached element.
 */
function newRemoteEntry(url) {
  return remoteWhitelist.appendChild(newEntry(url, 'remote'));
}

/**
 * Adds and attach a new entry in the **local** section of the whitelist table.
 * @param {string} url - The URL (text) of the entry.
 * @returns {HTMLElement} A reference to the newly attached element.
 */
function newLocalEntry(url) {
  return localWhitelist.appendChild(newEntry(url, 'local'));
}

/**
 * Creates an new empty entry with a text input to fill and remove an existing one if already present.
 * @param {string} type - The type of entry for the whitelist. It can either be: `local` or `remote`
 * @todo
 */
function newInputEntry(type) {
  var emptyEntry = document.getElementById('add-entry');

  if(emptyEntry !== null) {
    emptyEntry.remove();
  }

  var input = document.createElement('input');
  emptyEntry = newEntry('', 'add-entry');

  if(type === 'local') {
    // TODO: How to handle directories?
    input.className = 'local-add-input';
    emptyEntry = localWhitelist.appendChild(emptyEntry);
  } else if(type === 'remote') {
    emptyEntry = remoteWhitelist.appendChild(emptyEntry);
    input.className = 'remote-add-input';
  } else {
    throw new TypeError('The type argument isn’t valid');
  }

  // Actually attach the input
  emptyEntry.getElementsByClassName('entry')[0].appendChild(input);

  // TODO: Check correct values
  input.addEventListener('keyup', function handler(event) {
    if (event.keyCode === 13) {
      if(event.target.value) {
        var site = event.target.value;
        stateManager.addSiteToWhitelist(site, type)
          .then(refreshDisplay)
          .then(function() {
            emptyEntry.remove();
          })
          .catch(function(message) {
            if(message === 'type') {
              message = 'Unknown error';
            }
            // TODO: Implement something less annoying
            window.alert(message);
          });
      }
    }
  }, false);
  input.focus();
  // console.log(emptyEntry);
}

/**
 * Adds a warning to Chromium/Chrome users that loading a local file can’t work without doing it manually.
 */
function chromiumInit() {
  if(isChromium) {
    var localPlaceholder = document.querySelector('#local-whitelist td .whitelist-message');
    localPlaceholder.textContent = 'Chrome doesn’t support loading local files asynchronously. You must manually load the test file. Sorry for the inconvenience ';
    localPlaceholder.parentElement.classList = localPlaceholder.parentElement.classList + ' chromium-message';
    // Removes the plus sign
    document.getElementById('local-add').remove();
  }
}

var stateManager = new StateManager();
stateManager.getWhitelist()
  .then(refreshDisplay)
  .then(initDisplay)
  .then(chromiumInit);

window.addEventListener('remove', function handler(event) {
  stateManager.removeSiteFromWhitelist(event.detail.data, event.detail.type);
  refreshDisplay();
}, false);

// options.js<options> ends here


================================================
FILE: src/app/test_widget/active_test.js
================================================
/*global components */

/**
 * @fileOverview This file registers the `active-test` component. This file doesn’t depend on other components.
 * @name active_test.js<test_widget>
 * @author Etienne Prud’homme
 * @license MIT
 */

/**
 * Registers the `active-test` component.
 */
(function() {
  'use strict';
  var self = null;

  var proto = {};

  var template = '<div class="active-test">' +
        '  <div class="flex-container">' +
        '    <div class="mark incorrect"><span class="test-desc"></span></div>' +
        '  </div>' +
        '</div>' +
        '<!-- active-test ends here -->';

  /**
   * Function to mark a test as `Passed`.
   * @param {HTMLElement} markRightOrWrong - The element containing the mark.
   * @private
   */
  function _testHasPassed(markRightOrWrong) {
    markRightOrWrong.classList.remove('incorrect');
    markRightOrWrong.classList.remove('error');
    markRightOrWrong.classList.add('correct');
  }

  /**
   * Function to mark a test as `Failed`.
   * @param {HTMLElement} markRightOrWrong - The element containing the mark.
   * @private
   */
  function _testHasFailed(markRightOrWrong) {
    markRightOrWrong.classList.add('incorrect');
    markRightOrWrong.classList.remove('correct');
    markRightOrWrong.classList.remove('error');
  }

  /**
   * Function to mark a test as `Erred` (is not valid).
   * @param {HTMLElement} markRightOrWrong - The element containing the mark.
   * @private
   */
  function _testHasErred(markRightOrWrong) {
    markRightOrWrong.classList.remove('correct');
    markRightOrWrong.classList.remove('incorrect');
    markRightOrWrong.classList.add('error');
  }

  /**
   * Main function for updating member elements.
   */
  function updateView() {
    var testPassed, testDescription;
    try {
      testDescription = self.dataset.description;
      testPassed = self.dataset.testPassed;
    } catch (e) {
      console.warn(e);
    }

    var markRightOrWrong = self.querySelector('.mark');
    var descriptionDisplay = self.querySelector('.test-desc');

    // Simple fix for backward compatibility
    descriptionDisplay.textContent = testDescription.replace(/&lt;|&gt;/g, function(match) {
      return {'&lt;': '<', '&gt;': '>'}[match];
    });

    if (testPassed === 'true') {
      _testHasPassed(markRightOrWrong);
    } else if (testPassed === 'false') {
      _testHasFailed(markRightOrWrong);
    } else if (testPassed === 'error') {
      _testHasErred(markRightOrWrong);
    }
  }

  /**
   * Called when the element gets attached to the document
   */
  proto.attachedCallback = function() {
    self = this;
    self.dataset.testPassed = false;
    updateView();
  };

  /**
   * Called when any attribute on the element changes
   */
  proto.attributeChangedCallback = function () {
    self = this;
    updateView();
  };

  components.registerElement('active-test', template, proto);
})();

// active_test.js<test_widget> ends here


================================================
FILE: src/app/test_widget/font.js
================================================
/**
 * @fileOverview This file contains the "Source Sans Pro" font as base64. Because the `template.js` file is an injected script, it doesn’t have access to the extension path. Furthermore, sharing the path could be dangerous.
 * @name font.js<test_widget>
 * @author Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.

 This Font Software is licensed under the SIL Open Font License, Version 1.1.
 * @license GPLv3
 */

var sourceSansProFont = 'AAEAAAATAQAABAAwQkFTRYsZlLEAAilAAAAAOkRTSUexF+HqAAIpfAAAIFhHREVGWIRZ9wABwEAAAAEoR1BPUw6dvuMAAcFoAABMKkdTVUJ+lJvfAAINlAAAG6xPUy8yWrSUWwAAAbgAAABgY21hcG4XREUAABOEAAAKKGN2dCANmQD6AAAfbAAAAChmcGdtBlmcNwAAHawAAAFzZ2FzcP//AAMAAcA4AAAACGdseWbpcaFDAAAoTAABMtRoZWFk/hSz4gAAATwAAAA2aGhlYQejBwsAAAF0AAAAJGhtdHi/qKz/AAACGAAAEWxsb2NhuTYHZgAAH5QAAAi4bWF4cAZ1AkAAAAGYAAAAIG5hbWWdGO0SAAFbIAAAPLJwb3N03+u9sAABl9QAAChicHJlcJYE+usAAB8gAAAASwABAAAAAQzMpu0hD18PPPUACQPoAAAAAM2XgKUAAAAAzZfjFv9A/r0EiAO4AAAACQACAAAAAAAAAAEAAAPY/u8AAASq/0D/FgSIAAEAAAAAAAAAAAAAAAAAAARbAAEAAARbAFoABwBxAAUAAQAAAAAACgAAAgABcwADAAEAAwHHAZAABQAAAooCWAAAAEsCigJYAAABXgAyASAAAAILBQMDBAMCAgQgAAAHAAAAAQAAAAAAAAAAQURCRQBAAAD+/wLu/wYAAAPYAREgAAGTAAAAAAHgApQAAAAgAAMCjQBZAAAAAADKAAAAygAAAiAAAwJMAFoCOwA0AmcAWgIPAFoB7gBaAmkANAKMAFoBBwBaAeAAHwJDAFoB5gBaAtcAWgKHAFoCmAA0AkAAWgKYADQCRQBaAhYAKgIYABwChQBXAgMAAAMSABcCAQAPAdz//wIbAC0CAAA6AisAUgHIAC4CKwAvAfAALgEkAB4B+AAtAiAAUgD2AEMA9//YAe8AUgD/AFIDPQBSAiMAUgIeAC4CKwBSAiYALwFbAFIBowAcAVIAGAIgAEsB0wAMAs4AGAG+AA4B0wAMAakAHwIgAAMCIAADAiAAAwIgAAMCIAADAiAAAwIgAAMCIAADAiAAAwIgAAMCIAADAiAAAwIgAAMCIAADAiAAAwIgAAMCIAADAiAAAwIgAAMCIAADAiAAAwIgAAMDNgAIAjsANAI7ADQCOwA0AjsANAI7ADQCZwBaAmcAWgJnAFoCfgAhAg8AWgIPAFoCDwBaAg8AWgIPAFoCDwBaAg8AWgIPAFoCDwBaAg8AWgIPAFoCDwBaAg8AWgIPAFoCDwBaAg8AWgIPAFoCaQA0AmkANAJpADQCaQA0AmkANAJpADQCaQA0AowAWgKMAFoCjABaAq8AIAEHAAABBwBQAQf/+wEH//IBB//8AQcABwEHAEoBB//7AQcAQwEHAE4BBwArAeAAHwJDAFoB5gBTAeYAWgHmAFoB5gBaAeYAWgHmAAoB5gBaAekADQLXAFoChwBaAocAWgKHAFoChwBaAocAWgKHAFoChwBaApgANAKYADQCmAA0ApgANAKYADQCmAA0ApgANAKYADQCmAA0ApgANAKYADQCmAA0ApgANAKYADQCmAA0ApgAMgNPADQCmAA2ApgANgKYADYCmAA2ApgANgKYADYCmAA0AkUAWgJFAFoCRQBaAkUAWgJFAFoCRQBaAhYAKgIWACoCFgAqAhYAKgIWACoCFgAqAhYAKgKbAFsCGAAcAhgAHAIYABwCGAAcAhgAHAKFAFcChQBXAoUAVwKFAFcChQBXAoUAVwKFAFcChQBXAoUAVwKFAFcChQBXAoUAVwKFAFcChQBXAoUAVwKFAFcChQBXApMAVwKTAFcCkwBXApMAVwKTAFcCkwBXAxIAFwMSABcDEgAXAxIAFwHc//8B3P//Adz//wHc//8B3P//Adz//wHc//8B3P//AhsALQIbAC0CGwAtAhsALQJ+ACECRwBaApMAOgIAADoCAAA6AgAAOgIAADoCAAA6AgAAOgIAADoCAAA6AgAAOgIAADoCAAA6AgAAOgIAADoCAAA6AgAAOgIAADoCAAA6AgAAOgIAADoCAAA6AgAAOgIAADoDEQA6AcgALgHIAC4ByAAuAcgALgHIAC4CPQAvAisALwIrAC8CKwAvAfAALgHwAC4B8AAuAfAALgHwAC4B8AAuAfAALgHwAC4B8AAuAfAALgHwAC4B8AAuAfAALgHwAC4B8AAuAfAALgHwAC4B+AAtAfgALQH4AC0B+AAtAfgALQH4AC0B+AAtAiD/9AIgAFICIABSAiAACAD2AAwA9gA6APb/+gD2//AA9v/0APYAAAD2//oA9gA7APYAQwD2ACYA9gAmAPYAUgD3/9gB7wBSAe8AUgD/AEQBCABSAWoAUgD/AFIA/wBSAP///wD/AC4BBgAXAz0AUgIjAFICIwBSAiMAUgIjAFICIwBSAiMAUgIjAFIDBwA/Ah4ALgIeAC4CHgAuAh4ALgIeAC4CHgAuAh4ALgIeAC4CHgAuAh4ALgIeAC4CHgAuAh4ALgIeAC4CHgAuAh4ALgNHAC4CHgAuAh4ALgIeAC4CHgAuAh4ALgIeAC4CHgAuAVsAUgFbACQBWwBSAVsAQwFbAEMBW//9AaMAHAGjABwBowAcAaMAHAGjABwBowAcAaMAHAJAAFIBUgAYAVIAGAFSABgBUgAYAVIAGAFSAAkCIABLAiAASwIgAEsCIABLAiAASwIgAEsCIABLAiAASwIgAEsCIABLAiAASwIgAEsCIABLAiAASwIgAEsCIABLAiAASwIgAEsCIABLAiAASwIgAEsCIABLAiAASwLOABgCzgAYAs4AGALOABgB0wAMAdMADAHTAAwB0wAMAdMADAHTAAwB0wAMAdMADAGpAB8BqQAfAakAHwGpAB8CIQA1AisAUgD3/9gCKwAvAfAAJQIvADICPgAeAiwAHgIjAB4CUgAeAVgALgFYACkBWAAuAVgAJAFYABsBWAAlAVgALgFYAC4BWAAkAVgALgFYAC4BWAAuAisALwIrAC8CKwAvAisALwIrAC8CKwAvAisALwIrAC8CKwAvAisALwIrAC8CKwAvAisALwIrAC8CKwAvAisALwIrAC8CKwAvAisALwIrAC8CKwAvAisALwIrAC8CLwAyAi8AMgIvADICLwAyAi8AMgIvADICLwAyAi8AMgD2AFIA9gBFAQgAUgFqAFIA9gAmAPYARQD2AAAA9v//AQEAFwIsAB4CYQAgAfEALAHxAE8B8QAkAfEAGgHxABEB8QAZAfEAMAHxACwB8QApAfEAKAIaADcBcQAyAfUAJQHxABoCBwAiAfEAGQINAD0B6wAsAg0ANwINADQB8QAsAfEATwHxACQB8QAaAfEAEQHxABkB8QAxAfEALAHxACkB8QAeAgYANAFxADIB8QApAfEAGgIGABkB8QAZAgYAOQHpACwCBwAxAgYAJgD5AEEA+QAvAPkAQQD5AC8DtABeASEAVQEhAFUBqQAmAakAMAD5AFABqQBQAPkAOQD5AD8BqQA5AakAPwD5AD8BqQA/AQ8ALQEPADYBrQAtAa0ANgE3ACkBNwApAeAAKQMgACkB8QApAyAAKQD5AEEBMAAoAfQADAEvAFIBLwAmAS8AXgEvAB8BLwAiAS8AHwFeAAoA8QBcAV4ADgDxAFwBogA6AcYANgHGADYB8QAtAjAAKQLoADEC5wAxAacAFwJ9AAMCfQAbA08AMwMOADMB8QAjAW8AIwFvAFcBbwAoAW8AIwFvACoBbwAjAW8ALQFvADIBbwAtAW8AJwDtAEEA7QAnALEAKwCxACEBbwAjAW8AVwFvACgBbwAjAW8AKgFvACMBbwAtAW8AMgFvAC0BbwAnAO0AQQDtACcAsQArALEAIQFvACMBbwBXAW8AKAFvACMBbwAqAW8AIwFvAC0BbwAyAW8ALQFvACcA7QBBAO0AJwCxACsAsQAhAW8AIwFvAFcBbwAoAW8AIwFvACoBbwAjAW8ALQFvADIBbwAtAW8AJwDtAEEA7QAnALEAKwCxACEBWQAlAXYAIQFtAB4BWQAlAXYANAEyAB4BdgAhAVAAHADJABMBVwAeAW4ANACmACoAqf/mAVEANACuADQCLwA0AXEANAFtAB4BdgA0AXYAIQDwADQBGwATAOgAEAFyADIBQQAIAecAEAEzAAgBPwAIASIAFQFQABwBUAAcAVAAGQF2ACEBeQAiAKYANAFLACkB8QAaAfEANAHxADUB8QAXAfEAFwHxAD0B8QASAfEAPQHxADUB8QALAfEACgHxAEQB8QAKAfEALwHxAD0B8QBIAfEAFwBW/1kAVv9ZAFb/WQM4ACMEqgAjAw0AQAMoAEADHAAjAyQAQAM1ACkDJABAAzUAIwM1ACMDIQAfAfEAIgHxACIB8QAyAfEAIgH2ALwB8QAiAfEAIgHxACIB8QAiAfEAIgHxACIB8QA8AfEAIgHxACQB8QAkAfEAIgIxABgDDgAoAiYAUgIQACgBTAA0Ai0AKQJQAB4CrgArAfgAFgKhAFkBmQAVAyAALgJrABoCawAqAmsAJwJrACoBMgA2ATIADAG8ACkBeAA2AjsAFwI7ABcCOwA5AjsAOQI7ABcCOwAXAjsAEgI7ABIDHwBKAx8ASgJkAAAB9wAdAgUAOAD5AFEBqgBRAPkAOQD5AD8AlgAeAKIAFAIeAKACHgDOAh4AjgIeAI4AcgAWARUAEADhAC4A4QAAAHIAFgIeAIQCHgCIAh4AlAIeAJECHgCyAh4ArQIeANkCHgDAAh4AzQAA/5EAAP99AAD/vwAA/80AAP9/AAD/eAAA/3UAAP9v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AaIB4AIWAnQCsALQAwIDSgNwA9QEJAR4BMAFMgWGBfQGIAZmBqIHGAd0B7IH6AheCNoJKgmiCgQKUgsCC1ALhAvMDBQMRgzGDRwNcA3sDmIOqg8WD2APtg/yEGgQwhEYEU4RWhFmEXIRfhGKEZYRohGuEboRxhHSEd4R6hH2EgISEhIeEioSNhJCElISxhMqEzYTQhNOE1oTZhNyE34TihOSE54TqhO2E8ITzhPaE+YT8hP+FAoUFhQiFC4UOhRGFFYUuhTGFNIU3hTqFPYVAhUOFRoVJhUyFZoVphWyFb4VyhXWFeIV7hX6FgYWEhZcFmgWdBaAFowWmBakFrAWwBbMFwwXGBckFzAXPBdIF1QXYBdsF3gXhBeQF5wXqBe0F8AXzBfYF+QX8Bf8GAgYFBgkGLAZBhl2GYIZjhmaGaYZshoyGj4aShpWGmIachp+GooalhqiGq4auhrGGtIbPBtIG1QbYBtsG3gbhBuQG5wbqBu0G8AbzBvYG+Qb8Bv8HAgcFBwgHCwcOByqHQodFh0iHS4dOh1GHVIdXh1qHXYdgh2OHZodph2yHb4dyh3WHeId7h36HgYeXh6iHwIfDh8aHyYfMh8+H0ofVh9iH24feh+GH5Ifnh+qH7Yfxh/SH94f6h/2IAYgkiFIIVQhYCFsIXghhCGQIZwhqCIgIiwiOCJEIlAiXCJoInQigCKMIpgipCKwIrwiyCLUIuQjZiNyI34jiiOWI6IjriO6I8Yj0iPeJDYkQiROJFokZiRyJH4kiiSWJKIk9iU2JVYlYiVuJbIlviXKJdYl4iXuJf4mCiZYJmQmcCZ8JogmlCagJqwmuCbEJtAm3CboJvQnACcMJxgnJCcwJzwnSCdUJ2AnbCd8J+gogCjwKPwpCCkUKSApLCmmKbIpvinKKdYp5inyKf4qCioWKiIqLio6KkYqxCrQKtwq6Cr0KwArDCsYKyQrMCs8K0grVCtgK2wreCuEK5ArnCuoK7QrwCvMLDIsliyiLK4suizGLNIs3izqLPYtAi0OLRotJi0yLT4tSi1WLWItbi16LYYtki4ULnourC8IL2gv2jBSMF4wajDsMSIxLjE6MUYxUjFeMWoxdjGCMY4xmjH4MgAyDDIYMiQyMDI8MkgyVDJgMmwyeDKEMpAynDKoMrQyxDLQMtwy6DL0MwQzfjOGM5IznjOqM7YzwjPO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
Download .txt
gitextract_tfrp3au2/

├── .gitignore
├── .jscsrc
├── .jshintrc
├── README.md
├── chromium/
│   ├── README.md
│   ├── browser_action/
│   │   ├── intro.js
│   │   └── outro.js
│   ├── inject/
│   │   ├── intro.js
│   │   └── outro.js
│   ├── manifest.json
│   └── options/
│       ├── intro.js
│       └── outro.js
├── firefox/
│   ├── README.md
│   ├── background.js
│   ├── browser_action/
│   │   ├── intro.js
│   │   └── outro.js
│   ├── inject/
│   │   ├── intro.js
│   │   └── outro.js
│   ├── manifest.json
│   └── options/
│       ├── intro.js
│       └── outro.js
├── gulpfile.js
├── lib/
│   └── components.js
├── license
├── package.json
├── safari/
│   ├── Info.plist
│   ├── README.md
│   ├── background/
│   │   ├── REAMDE.md
│   │   ├── adapter.js
│   │   ├── adapterListener.js
│   │   ├── background.html
│   │   ├── background.js
│   │   ├── helpers.js
│   │   └── registry.js
│   ├── browser_action/
│   │   ├── intro.js
│   │   └── outro.js
│   ├── inject/
│   │   ├── intro.js
│   │   └── outro.js
│   └── options/
│       ├── intro.js
│       └── outro.js
├── sample/
│   ├── index.html
│   ├── test.json
│   └── unit-tests.js
└── src/
    ├── app/
    │   ├── README.md
    │   ├── browser_action/
    │   │   ├── browser_action.html
    │   │   └── browser_action.js
    │   ├── css/
    │   │   ├── common.css
    │   │   ├── fonts/
    │   │   │   └── OFL.txt
    │   │   ├── fonts.css
    │   │   ├── options.css
    │   │   └── ui.css
    │   ├── js/
    │   │   ├── inject/
    │   │   │   ├── StateManager.js
    │   │   │   ├── helpers.js
    │   │   │   └── inject.js
    │   │   └── libs/
    │   │       └── jsgrader.js
    │   ├── options/
    │   │   ├── index.html
    │   │   └── options.js
    │   └── test_widget/
    │       ├── active_test.js
    │       ├── font.js
    │       ├── test_results.js
    │       ├── test_suite.js
    │       └── test_widget.js
    └── js/
        ├── ActiveTest.js
        ├── GradeBook.js
        ├── Queue.js
        ├── README.md
        ├── Suite.js
        ├── TACollectors.js
        ├── TAReporters.js
        ├── Target.js
        ├── helpers.js
        ├── intro.js
        ├── outro.js
        └── registrar.js
Download .txt
SYMBOL INDEX (83 symbols across 24 files)

FILE: firefox/inject/intro.js
  function localHandleGet (line 28) | function localHandleGet(response) {
  function localHandleSet (line 42) | function localHandleSet(response) {

FILE: firefox/options/intro.js
  function localHandleGet (line 31) | function localHandleGet(response) {
  function localHandleSet (line 45) | function localHandleSet(response) {

FILE: gulpfile.js
  function setBrowser (line 19) | function setBrowser(browser) {

FILE: safari/background/adapter.js
  function activeTabs (line 198) | function activeTabs(windows) {
  function getTabs (line 214) | function getTabs(windows) {
  function makeTabType (line 230) | function makeTabType(tabs){

FILE: safari/background/adapterListener.js
  function respondBack (line 52) | function respondBack(channel, status) {

FILE: safari/background/helpers.js
  function extensionLog (line 16) | function extensionLog(log) {

FILE: safari/background/registry.js
  function getUniqueProperty (line 82) | function getUniqueProperty(obj, options) {
  function registerWindow (line 102) | function registerWindow(_window) {
  function removeWindow (line 122) | function removeWindow(_window) {
  function registerWindows (line 131) | function registerWindows() {
  function registerTab (line 151) | function registerTab(_tab) {
  function removeTab (line 172) | function removeTab(tab) {
  function registerTabs (line 181) | function registerTabs() {

FILE: safari/inject/intro.js
  function serialize (line 149) | function serialize(obj) {
  function askAdapter (line 178) | function askAdapter(channel, message) {
  function sendMessageToAdapter (line 202) | function sendMessageToAdapter(channel, message) {
  function sendResponse (line 239) | function sendResponse(response) {

FILE: src/app/browser_action/browser_action.js
  function handleFileSelect (line 16) | function handleFileSelect(evt) {
  function sendDataToTab (line 49) | function sendDataToTab(data, type, callback) {
  function addWarning (line 96) | function addWarning(message, type, options) {
  function checkSiteStatus (line 133) | function checkSiteStatus () {
  function initDisplay (line 165) | function initDisplay() {

FILE: src/app/js/inject/StateManager.js
  function StateManager (line 17) | function StateManager() {

FILE: src/app/js/inject/helpers.js
  function injectIntoDocument (line 18) | function injectIntoDocument(tag, data, location) {
  function removeInjectedFromDocument (line 62) | function removeInjectedFromDocument() {
  function removeFromDocument (line 80) | function removeFromDocument(id) {
  function removeFileNameFromPath (line 96) | function removeFileNameFromPath(path) {

FILE: src/app/js/inject/inject.js
  function importComponentsLibrary (line 25) | function importComponentsLibrary() {
  function importFeedbackWidget (line 42) | function importFeedbackWidget() {
  function injectGradingEngine (line 59) | function injectGradingEngine() {
  function loadLibraries (line 70) | function loadLibraries() {
  function appendIDToURL (line 94) | function appendIDToURL(url) {
  function loadJSONTestsFromFile (line 112) | function loadJSONTestsFromFile() {
  function registerTestSuites (line 183) | function registerTestSuites(json) {
  function loadUnitTests (line 218) | function loadUnitTests() {
  function turnOn (line 234) | function turnOn() {
  function waitForTestRegistrations (line 250) | function waitForTestRegistrations() {

FILE: src/app/js/libs/jsgrader.js
  function deepCompare (line 10) | function deepCompare () {
  function Queue (line 123) | function Queue (grader) {
  function executeInPromise (line 169) | function executeInPromise (fn) {
  function takeNextStep (line 183) | function takeNextStep (test, result) {
  function Grader (line 220) | function Grader (type, categoryMessages) {

FILE: src/app/options/options.js
  function StateManager (line 16) | function StateManager() {
  function initDisplay (line 106) | function initDisplay() {
  function cleanDisplay (line 128) | function cleanDisplay() {
  function refreshDisplay (line 149) | function refreshDisplay() {
  function newEntry (line 180) | function newEntry(data, type) {
  function newRemoteEntry (line 203) | function newRemoteEntry(url) {
  function newLocalEntry (line 212) | function newLocalEntry(url) {
  function newInputEntry (line 221) | function newInputEntry(type) {
  function chromiumInit (line 272) | function chromiumInit() {

FILE: src/app/test_widget/active_test.js
  function _testHasPassed (line 31) | function _testHasPassed(markRightOrWrong) {
  function _testHasFailed (line 42) | function _testHasFailed(markRightOrWrong) {
  function _testHasErred (line 53) | function _testHasErred(markRightOrWrong) {
  function updateView (line 62) | function updateView() {

FILE: src/app/test_widget/test_suite.js
  function updateView (line 26) | function updateView() {

FILE: src/js/ActiveTest.js
  function ActiveTest (line 36) | function ActiveTest(rawTest) {

FILE: src/js/GradeBook.js
  function GradeBook (line 11) | function GradeBook() {

FILE: src/js/Queue.js
  function Queue (line 13) | function Queue() {
  function executeInPromise (line 39) | function executeInPromise(fn) {

FILE: src/js/Suite.js
  function Suite (line 10) | function Suite(rawSuite) {
  function createTestElement (line 110) | function createTestElement(newTest) {

FILE: src/js/TACollectors.js
  function TA (line 17) | function TA(description) {
  function visitDfs (line 196) | function visitDfs (node, callback) {
  function getMarginSide (line 451) | function getMarginSide(marginName) {
  function getDisplayType (line 611) | function getDisplayType (element) {
  function isValidSide (line 616) | function isValidSide(side) {
  function getOffsetBySide (line 624) | function getOffsetBySide(element, sideName) {

FILE: src/js/Target.js
  function Target (line 15) | function Target() {

FILE: src/js/helpers.js
  function arrEquals (line 9) | function arrEquals(array1, array2) {
  function getDomNodeArray (line 35) | function getDomNodeArray(selector, parent) {
  function executeFunctionByName (line 62) | function executeFunctionByName(functionName, context) {
  function getUnitlessMeasurement (line 77) | function getUnitlessMeasurement(measurement) {

FILE: src/js/registrar.js
  function registerSuite (line 59) | function registerSuite(rawSuite) {
  function startTests (line 85) | function startTests() {
  function registerSuites (line 119) | function registerSuites(suitesJSON) {
  function turnOn (line 136) | function turnOn() {
  function turnOff (line 155) | function turnOff () {
  function debug (line 166) | function debug() {
Condensed preview — 74 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (464K chars).
[
  {
    "path": ".gitignore",
    "chars": 165,
    "preview": "build/\n.DS_STORE\nnode_modules/\next.zip\nTODO\nGE.min.js\nGE.js\n.dir-locals.el\n\\#*\\#\nGTAGS\nGRTAGS\nGPATH\nexperiments/\n.tern-p"
  },
  {
    "path": ".jscsrc",
    "chars": 119,
    "preview": "{\n  \"preset\": \"google\",\n  \"disallowSpacesInAnonymousFunctionExpression\": null,\n  \"excludeFiles\": [\"node_modules/**\"]\n}\n"
  },
  {
    "path": ".jshintrc",
    "chars": 458,
    "preview": "{\n  \"node\": true,\n  \"browser\": true,\n  \"bitwise\": true,\n  \"camelcase\": true,\n  \"curly\": true,\n  \"eqeqeq\": true,\n  \"immed"
  },
  {
    "path": "README.md",
    "chars": 14260,
    "preview": "# Udacity Feedback Chrome Extension\n\nImmediate, visual feedback about any website's HTML, CSS and JavaScript.\n\n* Not sur"
  },
  {
    "path": "chromium/README.md",
    "chars": 349,
    "preview": "## Chromium interface\n### Description\nThis directory contains files needed to build the extension for Chromium based bro"
  },
  {
    "path": "chromium/browser_action/intro.js",
    "chars": 255,
    "preview": "/**\n * @fileOverview This file contains the Chromium opening statements for the browser action script prepended to the m"
  },
  {
    "path": "chromium/browser_action/outro.js",
    "chars": 285,
    "preview": "/**\n * @fileOverview This file contains the Chromium closing statements for the browser action script appended to the ma"
  },
  {
    "path": "chromium/inject/intro.js",
    "chars": 240,
    "preview": "/**\n * @fileOverview This file contains the Chromium opening statements for the content script prepended to the main fil"
  },
  {
    "path": "chromium/inject/outro.js",
    "chars": 254,
    "preview": "/**\n * @fileOverview This file contains the Chromium closing statements for the content script appended to the main file"
  },
  {
    "path": "chromium/manifest.json",
    "chars": 1078,
    "preview": "{\n  \"name\": \"Udacity Front End Feedback\",\n  \"short_name\": \"Udacity Feedback\",\n  \"version\": \"0.3.0\",\n  \"manifest_version\""
  },
  {
    "path": "chromium/options/intro.js",
    "chars": 268,
    "preview": "/**\n * @fileOverview This file contains the Chromium opening statements for the options page script prepended to the mai"
  },
  {
    "path": "chromium/options/outro.js",
    "chars": 263,
    "preview": "/**\n * @fileOverview This file contains the Chromium closing statements for the options page script appended to the main"
  },
  {
    "path": "firefox/README.md",
    "chars": 1334,
    "preview": "## Firefox interface\n### Description\nThis directory contains files needed to build the extension for Firefox based brows"
  },
  {
    "path": "firefox/background.js",
    "chars": 1137,
    "preview": "/*global chrome */\n\n/**\n * @fileOverview This file adds support for the {@link chrome.storage.local} API in Firefox. Thi"
  },
  {
    "path": "firefox/browser_action/intro.js",
    "chars": 404,
    "preview": "/*global chrome */\n\n/**\n * @fileOverview This file contains the Firefox opening statements for the browser action script"
  },
  {
    "path": "firefox/browser_action/outro.js",
    "chars": 277,
    "preview": "/**\n * @fileOverview This file contains the Firefox closing statements for the content script appended to the main file."
  },
  {
    "path": "firefox/inject/intro.js",
    "chars": 1752,
    "preview": "/*global chrome */\n\n/**\n * @fileOverview This file contains the Firefox opening statements for the content script prepen"
  },
  {
    "path": "firefox/inject/outro.js",
    "chars": 253,
    "preview": "/**\n * @fileOverview This file contains the Firefox closing statements for the content script appended to the main file."
  },
  {
    "path": "firefox/manifest.json",
    "chars": 1290,
    "preview": "{\n  \"name\": \"Udacity Front End Feedback\",\n  \"version\": \"0.3.0\",\n  \"applications\": {\n    \"gecko\": {\n      \"id\": \"udacityf"
  },
  {
    "path": "firefox/options/intro.js",
    "chars": 1814,
    "preview": "/*global chrome, browser */\n\n/**\n * @fileOverview This file contains the Firefox opening statements for the options page"
  },
  {
    "path": "firefox/options/outro.js",
    "chars": 261,
    "preview": "/**\n * @fileOverview This file contains the Firefox closing statements for the options page script appended to the main "
  },
  {
    "path": "gulpfile.js",
    "chars": 11102,
    "preview": "var gulp = require('gulp');\nvar gulpsync = require('gulp-sync')(gulp);\nvar watch = require('gulp-watch');\nvar concat = r"
  },
  {
    "path": "lib/components.js",
    "chars": 1630,
    "preview": "/*global MutationObserver */\n/**\n * @fileOverview This file contains the /components/ module for emulating Web Component"
  },
  {
    "path": "license",
    "chars": 1054,
    "preview": "Copyright (c) 2015 Cameron Pittman\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this"
  },
  {
    "path": "package.json",
    "chars": 854,
    "preview": "{\n  \"name\": \"frontend-grading-engine\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Udacity Front-End Grading Engine\",\n  \"mai"
  },
  {
    "path": "safari/Info.plist",
    "chars": 2558,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "safari/README.md",
    "chars": 2380,
    "preview": "## Safari Interface\n### Description\nThis directory contains files needed to build the extension for Safari. The goal is "
  },
  {
    "path": "safari/background/REAMDE.md",
    "chars": 295,
    "preview": "## Safari Global Page\n\nThe files in this directory get concatenated into `global.js`. It is somehow\nsimilar to what the "
  },
  {
    "path": "safari/background/adapter.js",
    "chars": 9849,
    "preview": "/*global registry, safari, extensionLog */\n\n/**\n * @fileOverview This file contains the adaptee (Adapter inner working) "
  },
  {
    "path": "safari/background/adapterListener.js",
    "chars": 2220,
    "preview": "/*global wrapper, safari */\n\n/**\n * @fileOverview This file contains the adapter listener (i.e. message\n * passing part)"
  },
  {
    "path": "safari/background/background.html",
    "chars": 170,
    "preview": "<!doctype html>\n<html>\n  <head>\n    <title>Background Page</title>\n    <meta charset=\"utf-8\">\n    <script src=\"backgroun"
  },
  {
    "path": "safari/background/background.js",
    "chars": 9388,
    "preview": "/*global safari, SafariBrowserTab, SafariBrowserWindow, wrapper, extensionLog */\n\n/**\n * @fileOverview This file adds su"
  },
  {
    "path": "safari/background/helpers.js",
    "chars": 1338,
    "preview": "/*global safari */\n\n/**\n * @fileOverview This file adds utility functions for the Safari global page.\n * @name helpers.j"
  },
  {
    "path": "safari/background/registry.js",
    "chars": 7117,
    "preview": "/**\n * @fileOverview This adds a module to record windows and tabs in\n * Safari. Otherwise, there’s no way to select a t"
  },
  {
    "path": "safari/browser_action/intro.js",
    "chars": 545,
    "preview": "/**\n * @fileOverview This file contains the opening statements of\n * `browser_action.js` for the Safari Browser. With th"
  },
  {
    "path": "safari/browser_action/outro.js",
    "chars": 1558,
    "preview": "/*global waitChromeNS, chrome, safari, checkSiteStatus */\n\n/**\n * @fileOverview This file contains the Safari closing st"
  },
  {
    "path": "safari/inject/intro.js",
    "chars": 11843,
    "preview": "/*global safari */\n\n/**\n * @fileOverview This file contains the opening statements of `inject.js` for\n * Safari.\n * @nam"
  },
  {
    "path": "safari/inject/outro.js",
    "chars": 286,
    "preview": "/**\n * @fileOverview This file contains the closing statements of `inject.js` for\n * Safari.\n * @name outro.js<Safari>\n "
  },
  {
    "path": "safari/options/intro.js",
    "chars": 428,
    "preview": "/**\n * @fileOverview This file contains the Firefox opening statements for the options page script prepended to the main"
  },
  {
    "path": "safari/options/outro.js",
    "chars": 1405,
    "preview": "/*global waitChromeNS, chrome, safari */\n\n/**\n * @fileOverview This file contains the Firefox closing statements for the"
  },
  {
    "path": "sample/index.html",
    "chars": 4299,
    "preview": "<!doctype html>\n<head>\n  <title>Sample Tests</title>\n  <meta name=\"udacity-grader\" content=\"test.json\" libraries=\"jsgrad"
  },
  {
    "path": "sample/test.json",
    "chars": 8808,
    "preview": "[{\n  \"name\": \"Meta Info\",\n  \"code\": \"Got the meta stuff\",\n  \"tests\": [\n    {\n      \"description\": \"&lt;meta&gt; has name"
  },
  {
    "path": "sample/unit-tests.js",
    "chars": 284,
    "preview": "/**\n * @fileOverview This file contains unit tests to be loaded.\n * @name unit-tests.js<sample>\n * @author Cameron Pittm"
  },
  {
    "path": "src/app/README.md",
    "chars": 288,
    "preview": "# User interface (view)\n## Description\nThis directory contains files needed to implements the user-interface. It doesn’t"
  },
  {
    "path": "src/app/browser_action/browser_action.html",
    "chars": 1885,
    "preview": "<!doctype html>\n<html>\n  <head>\n    <style type=\"text/css\">\n     #main {\n         padding: 1em;\n         height: 100%;\n "
  },
  {
    "path": "src/app/browser_action/browser_action.js",
    "chars": 6036,
    "preview": "/*global FileReader, chrome */\n\n/**\n * @fileOverview This file contains the browser_action logic.\n * @name browser_actio"
  },
  {
    "path": "src/app/css/common.css",
    "chars": 462,
    "preview": "/**\n * This file was taken in part from uBlock Origin. It contains styles common\n * to the extension.\n */\n\n* {\n  box-siz"
  },
  {
    "path": "src/app/css/fonts/OFL.txt",
    "chars": 4599,
    "preview": "Copyright (c) <dates>, <Copyright Holder> (<URL|email>),\nwith Reserved Font Name <Reserved Font Name>.\nCopyright (c) <da"
  },
  {
    "path": "src/app/css/fonts.css",
    "chars": 464,
    "preview": "/**\n * This file contains custom fonts for the extension.\n */\n\n@font-face {\n  font-family: 'FontAwesome';\n  font-weight:"
  },
  {
    "path": "src/app/css/options.css",
    "chars": 1689,
    "preview": "/**\n * This file contains styles for the options page.\n */\n\ntable {\n  border: 3px solid #2c3b48;\n  border-spacing: 0;\n  "
  },
  {
    "path": "src/app/css/ui.css",
    "chars": 1364,
    "preview": "/**\n * This file contains the User Interface styles of the extension.\n */\n\nbutton.custom {\n  padding: 0.6em 1em;\n  borde"
  },
  {
    "path": "src/app/js/inject/StateManager.js",
    "chars": 6684,
    "preview": "/*global removeFileNameFromPath, importFeedbackWidget, injectGradingEngine, loadLibraries, loadJSONTestsFromFile, regist"
  },
  {
    "path": "src/app/js/inject/helpers.js",
    "chars": 2816,
    "preview": "/*global injectedElementsOnPage */\n\n/**\n * @fileOverview This file contains various functions that aren’t specific to th"
  },
  {
    "path": "src/app/js/inject/inject.js",
    "chars": 9354,
    "preview": "/*global removeFileNameFromPath, injectIntoDocument, chrome, StateManager */\n\n/**\n * @fileoverview This file manages the"
  },
  {
    "path": "src/app/js/libs/jsgrader.js",
    "chars": 15879,
    "preview": "/**\n * @fileOverview This file contains the JSGrader library to test the JavaScript context.\n * @name jsgrader.js<libs>\n"
  },
  {
    "path": "src/app/options/index.html",
    "chars": 3217,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title>Udacity Feedback</title>\n    <style>\n     #whitelist-entry-template {\n       "
  },
  {
    "path": "src/app/options/options.js",
    "chars": 9692,
    "preview": "/*global chrome, browserName */\n\n/**\n * @fileOverview This file contains the option page for adding/removing websites fr"
  },
  {
    "path": "src/app/test_widget/active_test.js",
    "chars": 2938,
    "preview": "/*global components */\n\n/**\n * @fileOverview This file registers the `active-test` component. This file doesn’t depend o"
  },
  {
    "path": "src/app/test_widget/font.js",
    "chars": 200630,
    "preview": "/**\n * @fileOverview This file contains the \"Source Sans Pro\" font as base64. Because the `template.js` file is an injec"
  },
  {
    "path": "src/app/test_widget/test_results.js",
    "chars": 3077,
    "preview": "/*global components */\n\n/**\n * @fileOverview This file registers the `test-results` component. This component is the mai"
  },
  {
    "path": "src/app/test_widget/test_suite.js",
    "chars": 2507,
    "preview": "/*global components */\n\n/**\n * @fileOverview This file registers the `test-suite` component. {@link test-results} and {@"
  },
  {
    "path": "src/app/test_widget/test_widget.js",
    "chars": 9973,
    "preview": "/*global MutationObserver, components, sourceSansProFont */\n\n/**\n * @fileOverview This file provides the test widget mod"
  },
  {
    "path": "src/js/ActiveTest.js",
    "chars": 5221,
    "preview": "/*global TA */\n\n/**\n * @fileOverview This file contains the prototype of a single running test.\n * @name ActiveTest.js<j"
  },
  {
    "path": "src/js/GradeBook.js",
    "chars": 3919,
    "preview": "/**\n * @fileOverview The GradeBook maintains and reports on the state of a set of questions registered by the TA. The Gr"
  },
  {
    "path": "src/js/Queue.js",
    "chars": 1401,
    "preview": "/**\n * @fileOverview This file contains a `queue` Data Structure implementation for chaining promises.\n * @see http://ww"
  },
  {
    "path": "src/js/README.md",
    "chars": 318,
    "preview": "## Front-End Grading Engine\nThe files in this directory get concatenated into `ext/app/js/libs/GE.min.js`. See the build"
  },
  {
    "path": "src/js/Suite.js",
    "chars": 4116,
    "preview": "/*global ActiveTest, components */\n\n/**\n * @fileOverview This file contains the constructor for a `Suite` of tests.\n * @"
  },
  {
    "path": "src/js/TACollectors.js",
    "chars": 21138,
    "preview": "/*global Target, GradeBook, Queue, getDomNodeArray */\n\n/**\n * @fileOverview The Teaching Assistant (TA) is responsible f"
  },
  {
    "path": "src/js/TAReporters.js",
    "chars": 13354,
    "preview": "/**\n * @fileOverview Reporters live on the TA and are responsible for:\n   * giving the GradeBook instructions for evalua"
  },
  {
    "path": "src/js/Target.js",
    "chars": 1840,
    "preview": "/**\n * @fileOverview Targets are:\n *  • nested into a tree-like structure called a bullseye\n *  • usually mapped 1:1 wit"
  },
  {
    "path": "src/js/helpers.js",
    "chars": 2656,
    "preview": "/**\n * @fileOverview This file contains helpers for the Grading Engine.\n * @name helpers.js<js>\n * @author Cameron Pittm"
  },
  {
    "path": "src/js/intro.js",
    "chars": 503,
    "preview": "/**\n * @fileOverview Udacity’s library for immediate front-end feedback.\n * @name intro.js<js>\n * @author Cameron Pittma"
  },
  {
    "path": "src/js/outro.js",
    "chars": 345,
    "preview": "/**\n * @fileOverview This file contains the closing statements of the Grading Engine.\n * @name outro.js<js>\n * @author C"
  },
  {
    "path": "src/js/registrar.js",
    "chars": 4576,
    "preview": "/*global testWidget, Suite, testResults */\n\n/**\n * @fileOverview  Expose functions that create and monitor tests.\n * @na"
  }
]

About this extraction

This page contains the full source code of the udacity/frontend-grading-engine GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 74 files (438.3 KB), approximately 180.9k tokens, and a symbol index with 83 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!