Repository: mozilla/nunjucks
Pull request #1193 (2019-02-18 14:30:08)

Update faq.md
Named block end tags _are_ supported in Nunjucks. Added in [v2.4.0](
https://github.com/mozilla/nunjucks/releases/tag/v2.4.0)

## Summary

Proposed change: Remove bullet from FAQ stating that named end block tags aren't supported.

## Checklist

I've completed the checklist below to ensure I didn't forget anything. This makes reviewing this PR as easy as possible for the maintainers. And it gets this change released as soon as possible.

* [x] Proposed change helps towards [*purpose of this project*](https://github.com/mozilla/nunjucks/blob/master/CONTRIBUTING.md#purpose).
* [x] [*Documentation*](https://github.com/mozilla/nunjucks/tree/master/docs/) is added / updated to describe proposed change.
* [x] [*Tests*](https://github.com/mozilla/nunjucks/tree/master/tests) are added / updated to cover proposed change.
* [x] [*Changelog*](https://github.com/mozilla/nunjucks/blob/master/CHANGELOG.md) has an entry for proposed change (if user-facing fix or feature).


Patch:
diff --git a/.travis.yml b/.travis.yml
index 4c3d5e3..4c7eb3b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,9 @@
 language: node_js
 sudo: false
 node_js:
-  - "9"
+  - "11"
+  - "10"
   - "8"
-  - "6"
 install:
   - npm install
   - npm install codecov
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 83907ab..5f44151 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,9 @@
 Changelog
 =========
 
+* Emit 'load' events on `Environment` instances, to allow runtime dependency
+  tracking. Fixes [#1153](https://github.com/mozilla/nunjucks/issues/1153).
+
 3.1.7 (Jan 12 2019)
 ------------------
 
diff --git a/appveyor.yml b/appveyor.yml
index 341eec1..d908fa2 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -5,9 +5,9 @@ init:
 # Test against these versions of Node.js.
 environment:
   matrix:
-    - nodejs_version: "9"
+    - nodejs_version: "11"
+    - nodejs_version: "10"
     - nodejs_version: "8"
-    - nodejs_version: "6"
 
 # Install scripts. (runs after repo cloning)
 install:
diff --git a/docs/api.md b/docs/api.md
index 2bf4f35..8c516a5 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -353,6 +353,23 @@ SafeString (to be documented) if one was passed in, so the output will
 copy the safeness of the input, but this property is helpful in rare
 circumstances.
 {% endapi %}
+
+{% api %}
+'load' event
+env.on('load', function(name, source, loader))
+
+The 'load' event gets emitted whenever a Loader retrieves the source of a
+template. It can be listened to in order to determine template dependencies
+at runtime. The arguments emitted to the callback are:
+
+* **name** *(String)* The template name, as passed to the loader
+* **source** *(Object)* The object that gets returned from Loader.getSource
+  * **src** *(String)* The template source
+  * **path** *(String)* The full path to the template
+  * **noCache** *(Bool)* If `true`, the template wasn't cached.
+* **loader** The Loader instance that triggered the event.
+{% endapi %}
+
 {% raw %}
 
 ## Template
diff --git a/docs/faq.md b/docs/faq.md
index cf57610..ba67914 100644
--- a/docs/faq.md
+++ b/docs/faq.md
@@ -43,7 +43,6 @@ Additionally, there are few jinja2 features not implemented in nunjucks:
 * The special `self` variable
 * `for` does not support `if not` and `else`
 * `if i is divisibleby(3)`-style conditionals
-* Named block end tags: `{% endblock content %}`
 * Sandboxed mode
   * Note: this makes it **unsuitable for applications requiring [user-defined templates](api.html#user-defined-templates-warning)**
 * Line statements: `# for item in seq`
diff --git a/docs/fr/api.md b/docs/fr/api.md
index 847ff34..babac4b 100644
--- a/docs/fr/api.md
+++ b/docs/fr/api.md
@@ -344,6 +344,22 @@ de rares circonstances.
 {% endapi %}
 {% raw %}
 
+{% api %}
+'load' événement
+env.on('load', function(name, source, loader))
+
+Quand une instance 'Loader' charge un template, elle émet l'événement
+'load'. On peut utiliser cette événement pour détérminer les dépendances
+à l'exécution. Ses arguments sont :
+
+* **name** *(String)* Le nom du template
+* **source** *(Object)* Le resultat de l'appel de `Loader.getSource`
+  * **src** *(String)* Le code source du template
+  * **path** *(String)* Le chemin d'accèss du fichier ou l'URL
+  * **noCache** *(Bool)* Si la valeur n'est pas mise en cache.
+* **loader** L'instance du Loader qui a émetté l'événement
+{% endapi %}
+
 ## Template
 
 Un `Template` est un objet qui gère la compilation des chaînes de template
diff --git a/nunjucks/src/compiler.js b/nunjucks/src/compiler.js
index 6ef3c14..431cde9 100644
--- a/nunjucks/src/compiler.js
+++ b/nunjucks/src/compiler.js
@@ -5,7 +5,7 @@ const transformer = require('./transformer');
 const nodes = require('./nodes');
 const {TemplateError} = require('./lib');
 const {Frame} = require('./runtime');
-const Obj = require('./object');
+const {Obj} = require('./object');
 
 // These are all the same for now, but shouldn't be passed straight
 // through
@@ -67,13 +67,13 @@ class Compiler extends Obj {
     lines.forEach((line) => this._emitLine(line));
   }
 
-  _emitFuncBegin(name) {
+  _emitFuncBegin(node, name) {
     this.buffer = 'output';
     this._scopeClosers = '';
-    this._emitLine('function ' + name + '(env, context, frame, runtime, cb) {');
-    this._emitLine('var lineno = null;');
-    this._emitLine('var colno = null;');
-    this._emitLine('var ' + this.buffer + ' = "";');
+    this._emitLine(`function ${name}(env, context, frame, runtime, cb) {`);
+    this._emitLine(`var lineno = ${node.lineno};`);
+    this._emitLine(`var colno = ${node.colno};`);
+    this._emitLine(`var ${this.buffer} = "";`);
     this._emitLine('try {');
   }
 
@@ -1123,7 +1123,7 @@ class Compiler extends Obj {
 
     frame = new Frame();
 
-    this._emitFuncBegin('root');
+    this._emitFuncBegin(node, 'root');
     this._emitLine('var parentTemplate = null;');
     this._compileChildren(node, frame);
     this._emitLine('if(parentTemplate) {');
@@ -1147,7 +1147,7 @@ class Compiler extends Obj {
       }
       blockNames.push(name);
 
-      this._emitFuncBegin(`b_${name}`);
+      this._emitFuncBegin(block, `b_${name}`);
 
       const tmpFrame = new Frame();
       this._emitLine('var frame = frame.push(true);');
diff --git a/nunjucks/src/environment.js b/nunjucks/src/environment.js
index bb1c3b9..1cd7537 100644
--- a/nunjucks/src/environment.js
+++ b/nunjucks/src/environment.js
@@ -8,7 +8,7 @@ const filters = require('./filters');
 const {FileSystemLoader, WebLoader, PrecompiledLoader} = require('./loaders');
 const tests = require('./tests');
 const globals = require('./globals');
-const Obj = require('./object');
+const {Obj, EmitterObj} = require('./object');
 const globalRuntime = require('./runtime');
 const {handleError, Frame} = globalRuntime;
 const expressApp = require('./express-app');
@@ -37,7 +37,7 @@ const noopTmplSrc = {
   }
 };
 
-class Environment extends Obj {
+class Environment extends EmitterObj {
   init(loaders, opts) {
     // The dev flag determines the trace that'll be shown on errors.
     // If set to true, returns the full trace from the error point,
@@ -82,7 +82,7 @@ class Environment extends Obj {
       );
     }
 
-    this.initCache();
+    this._initLoaders();
 
     this.globals = globals();
     this.filters = {};
@@ -95,18 +95,28 @@ class Environment extends Obj {
     lib._entries(tests).forEach(([name, test]) => this.addTest(name, test));
   }
 
-  initCache() {
-    // Caching and cache busting
+  _initLoaders() {
     this.loaders.forEach((loader) => {
+      // Caching and cache busting
       loader.cache = {};
       if (typeof loader.on === 'function') {
-        loader.on('update', (template) => {
-          loader.cache[template] = null;
+        loader.on('update', (name, fullname) => {
+          loader.cache[name] = null;
+          this.emit('update', name, fullname, loader);
+        });
+        loader.on('load', (name, source) => {
+          this.emit('load', name, source, loader);
         });
       }
     });
   }
 
+  invalidateCache() {
+    this.loaders.forEach((loader) => {
+      loader.cache = {};
+    });
+  }
+
   addExtension(name, extension) {
     extension.__name = name;
     this.extensions[name] = extension;
diff --git a/nunjucks/src/lexer.js b/nunjucks/src/lexer.js
index 2e33adb..ef99227 100644
--- a/nunjucks/src/lexer.js
+++ b/nunjucks/src/lexer.js
@@ -375,7 +375,7 @@ class Tokenizer {
 
   _extractString(str) {
     if (this._matches(str)) {
-      this.index += str.length;
+      this.forwardN(str.length);
       return str;
     }
     return null;
diff --git a/nunjucks/src/loader.js b/nunjucks/src/loader.js
index 1ff3b20..139939e 100644
--- a/nunjucks/src/loader.js
+++ b/nunjucks/src/loader.js
@@ -1,23 +1,9 @@
 'use strict';
 
 const path = require('path');
-const Obj = require('./object');
-
-module.exports = class Loader extends Obj {
-  on(name, func) {
-    this.listeners = this.listeners || {};
-    this.listeners[name] = this.listeners[name] || [];
-    this.listeners[name].push(func);
-  }
-
-  emit(name, ...args) {
-    if (this.listeners && this.listeners[name]) {
-      this.listeners[name].forEach((listener) => {
-        listener(...args);
-      });
-    }
-  }
+const {EmitterObj} = require('./object');
 
+module.exports = class Loader extends EmitterObj {
   resolve(from, to) {
     return path.resolve(path.dirname(from), to);
   }
diff --git a/nunjucks/src/node-loaders.js b/nunjucks/src/node-loaders.js
index 4e3d6f9..42405a0 100644
--- a/nunjucks/src/node-loaders.js
+++ b/nunjucks/src/node-loaders.js
@@ -45,7 +45,7 @@ class FileSystemLoader extends Loader {
       watcher.on('all', (event, fullname) => {
         fullname = path.resolve(fullname);
         if (event === 'change' && fullname in this.pathsToNames) {
-          this.emit('update', this.pathsToNames[fullname]);
+          this.emit('update', this.pathsToNames[fullname], fullname);
         }
       });
       watcher.on('error', (error) => {
@@ -76,11 +76,13 @@ class FileSystemLoader extends Loader {
 
     this.pathsToNames[fullpath] = name;
 
-    return {
+    const source = {
       src: fs.readFileSync(fullpath, 'utf-8'),
       path: fullpath,
       noCache: this.noCache
     };
+    this.emit('load', name, source);
+    return source;
   }
 }
 
diff --git a/nunjucks/src/nodes.js b/nunjucks/src/nodes.js
index 65fd2fa..20c60d5 100644
--- a/nunjucks/src/nodes.js
+++ b/nunjucks/src/nodes.js
@@ -1,6 +1,6 @@
 'use strict';
 
-const Obj = require('./object');
+const {Obj} = require('./object');
 
 function traverseAndCheck(obj, type, results) {
   if (obj instanceof type) {
diff --git a/nunjucks/src/object.js b/nunjucks/src/object.js
index 39c49ac..e3a34c6 100644
--- a/nunjucks/src/object.js
+++ b/nunjucks/src/object.js
@@ -1,6 +1,7 @@
 'use strict';
 
 // A simple class system, more documentation to come
+const EventEmitter = require('events');
 const lib = require('./lib');
 
 function parentWrap(parent, prop) {
@@ -59,4 +60,26 @@ class Obj {
   }
 }
 
-module.exports = Obj;
+class EmitterObj extends EventEmitter {
+  constructor(...args) {
+    super();
+    // Unfortunately necessary for backwards compatibility
+    this.init(...args);
+  }
+
+  init() {}
+
+  get typename() {
+    return this.constructor.name;
+  }
+
+  static extend(name, props) {
+    if (typeof name === 'object') {
+      props = name;
+      name = 'anonymous';
+    }
+    return extendClass(this, name, props);
+  }
+}
+
+module.exports = { Obj, EmitterObj };
diff --git a/nunjucks/src/parser.js b/nunjucks/src/parser.js
index 45d0dc5..a98142c 100644
--- a/nunjucks/src/parser.js
+++ b/nunjucks/src/parser.js
@@ -2,7 +2,7 @@
 
 var lexer = require('./lexer');
 var nodes = require('./nodes');
-var Obj = require('./object');
+var Obj = require('./object').Obj;
 var lib = require('./lib');
 
 class Parser extends Obj {
diff --git a/nunjucks/src/web-loaders.js b/nunjucks/src/web-loaders.js
index c592aa5..609e588 100644
--- a/nunjucks/src/web-loaders.js
+++ b/nunjucks/src/web-loaders.js
@@ -45,6 +45,7 @@ class WebLoader extends Loader {
           path: name,
           noCache: !useCache
         };
+        this.emit('load', name, result);
         if (cb) {
           cb(null, result);
         }


Test patch:
diff --git a/tests/api.js b/tests/api.js
index 17d644a..275f2ed 100644
--- a/tests/api.js
+++ b/tests/api.js
@@ -82,5 +82,14 @@
         path: path.resolve(templatesPath, 'string.njk')
       })).to.be('FooTest3BazFizzle');
     });
+
+    it('should emit "load" event on Environment instance', function(done) {
+      var env = new Environment(new Loader(templatesPath));
+      env.on('load', function(name, source) {
+        expect(name).to.equal('item.njk');
+        done();
+      });
+      env.render('item.njk', {});
+    });
   });
 }());
diff --git a/tests/compiler.js b/tests/compiler.js
index a282ecc..c1c38ec 100644
--- a/tests/compiler.js
+++ b/tests/compiler.js
@@ -816,12 +816,35 @@
         tmpl.render({}, function(err, res) {
           expect(res).to.be(undefined);
           expect(err.toString()).to.be([
-            'Template render error: (parse-error.njk) [Line 1, Column 24]',
+            'Template render error: (parse-error.njk) [Line 1, Column 26]',
             '  unexpected token: ,',
           ].join('\n'));
           done();
         });
       });
+
+      it('should include error line when exception raised in user function', function(done) {
+        var tmplStr = [
+          '{% block content %}',
+          '<div>{{ foo() }}</div>',
+          '{% endblock %}',
+        ].join('\n');
+        var env = new Environment(new Loader('tests/templates'));
+        var tmpl = new Template(tmplStr, env, 'user-error.njk');
+
+        function foo() {
+          throw new Error('ERROR');
+        }
+
+        tmpl.render({foo: foo}, function(err, res) {
+          expect(res).to.be(undefined);
+          expect(err.toString()).to.be([
+            'Template render error: (user-error.njk) [Line 1, Column 11]',
+            '  Error: ERROR',
+          ].join('\n'));
+          done();
+        });
+      });
     }
 
     it('should compile string concatenations with tilde', function(done) {
diff --git a/tests/lexer.js b/tests/lexer.js
index 0c406e5..515c46e 100644
--- a/tests/lexer.js
+++ b/tests/lexer.js
@@ -32,6 +32,17 @@
       if (lib.isArray(type)) {
         expect(tok.type).to.be(type[0]);
         expect(tok.value).to.be(type[1]);
+      } else if (lib.isObject(type)) {
+        expect(tok.type).to.be(type.type);
+        if (type.value != null) {
+          expect(tok.value).to.be(type.value);
+        }
+        if (type.lineno != null) {
+          expect(tok.lineno).to.be(type.lineno);
+        }
+        if (type.colno != null) {
+          expect(tok.colno).to.be(type.colno);
+        }
       } else {
         expect(tok.type).to.be(type);
       }
@@ -436,5 +447,289 @@
         lexer.TOKEN_SYMBOL,
         lexer.TOKEN_VARIABLE_END);
     });
+
+    it('should keep track of token positions', function() {
+      hasTokens(lexer.lex('{{ 3 != 4 == 5 <= 6 >= 7 < 8 > 9 }}'),
+        {
+          type: lexer.TOKEN_VARIABLE_START,
+          lineno: 0,
+          colno: 0,
+        },
+        {
+          type: lexer.TOKEN_INT,
+          value: '3',
+          lineno: 0,
+          colno: 3,
+        },
+        {
+          type: lexer.TOKEN_OPERATOR,
+          value: '!=',
+          lineno: 0,
+          colno: 5,
+        },
+        {
+          type: lexer.TOKEN_INT,
+          value: '4',
+          lineno: 0,
+          colno: 8,
+        },
+        {
+          type: lexer.TOKEN_OPERATOR,
+          value: '==',
+          lineno: 0,
+          colno: 10,
+        },
+        {
+          type: lexer.TOKEN_INT,
+          value: '5',
+          lineno: 0,
+          colno: 13,
+        },
+        {
+          type: lexer.TOKEN_OPERATOR,
+          value: '<=',
+          lineno: 0,
+          colno: 15,
+        },
+        {
+          type: lexer.TOKEN_INT,
+          value: '6',
+          lineno: 0,
+          colno: 18,
+        },
+        {
+          type: lexer.TOKEN_OPERATOR,
+          lineno: 0,
+          colno: 20,
+          value: '>=',
+        },
+        {
+          type: lexer.TOKEN_INT,
+          lineno: 0,
+          colno: 23,
+          value: '7',
+        },
+        {
+          type: lexer.TOKEN_OPERATOR,
+          value: '<',
+          lineno: 0,
+          colno: 25,
+        },
+        {
+          type: lexer.TOKEN_INT,
+          value: '8',
+          lineno: 0,
+          colno: 27,
+        },
+        {
+          type: lexer.TOKEN_OPERATOR,
+          value: '>',
+          lineno: 0,
+          colno: 29,
+        },
+        {
+          type: lexer.TOKEN_INT,
+          value: '9',
+          lineno: 0,
+          colno: 31,
+        },
+        {
+          type: lexer.TOKEN_VARIABLE_END,
+          lineno: 0,
+          colno: 33,
+        });
+
+      hasTokens(lexer.lex('{% if something %}{{ value }}{% else %}{{ otherValue }}{% endif %}'),
+        {
+          type: lexer.TOKEN_BLOCK_START,
+          lineno: 0,
+          colno: 0,
+        },
+        {
+          type: lexer.TOKEN_SYMBOL,
+          value: 'if',
+          lineno: 0,
+          colno: 3,
+        },
+        {
+          type: lexer.TOKEN_SYMBOL,
+          value: 'something',
+          lineno: 0,
+          colno: 6,
+        },
+        {
+          type: lexer.TOKEN_BLOCK_END,
+          lineno: 0,
+          colno: 16,
+        },
+        {
+          type: lexer.TOKEN_VARIABLE_START,
+          lineno: 0,
+          colno: 18,
+        },
+        {
+          type: lexer.TOKEN_SYMBOL,
+          value: 'value',
+          lineno: 0,
+          colno: 21,
+        },
+        {
+          type: lexer.TOKEN_VARIABLE_END,
+          lineno: 0,
+          colno: 27,
+        },
+        {
+          type: lexer.TOKEN_BLOCK_START,
+          lineno: 0,
+          colno: 29,
+        },
+        {
+          type: lexer.TOKEN_SYMBOL,
+          value: 'else',
+          lineno: 0,
+          colno: 32,
+        },
+        {
+          type: lexer.TOKEN_BLOCK_END,
+          lineno: 0,
+          colno: 37,
+        },
+        {
+          type: lexer.TOKEN_VARIABLE_START,
+          lineno: 0,
+          colno: 39,
+        },
+        {
+          type: lexer.TOKEN_SYMBOL,
+          value: 'otherValue',
+          lineno: 0,
+          colno: 42,
+        },
+        {
+          type: lexer.TOKEN_VARIABLE_END,
+          lineno: 0,
+          colno: 53,
+        },
+        {
+          type: lexer.TOKEN_BLOCK_START,
+          lineno: 0,
+          colno: 55,
+        },
+        {
+          type: lexer.TOKEN_SYMBOL,
+          value: 'endif',
+          lineno: 0,
+          colno: 58,
+        },
+        {
+          type: lexer.TOKEN_BLOCK_END,
+          lineno: 0,
+          colno: 64,
+        });
+
+      hasTokens(lexer.lex('{% if something %}\n{{ value }}\n{% else %}\n{{ otherValue }}\n{% endif %}'),
+        {
+          type: lexer.TOKEN_BLOCK_START,
+          lineno: 0,
+          colno: 0,
+        },
+        {
+          type: lexer.TOKEN_SYMBOL,
+          value: 'if',
+          lineno: 0,
+          colno: 3,
+        },
+        {
+          type: lexer.TOKEN_SYMBOL,
+          value: 'something',
+          lineno: 0,
+          colno: 6,
+        },
+        {
+          type: lexer.TOKEN_BLOCK_END,
+          lineno: 0,
+          colno: 16,
+        },
+        {
+          type: lexer.TOKEN_DATA,
+          value: '\n',
+        },
+        {
+          type: lexer.TOKEN_VARIABLE_START,
+          lineno: 1,
+          colno: 0,
+        },
+        {
+          type: lexer.TOKEN_SYMBOL,
+          value: 'value',
+          lineno: 1,
+          colno: 3,
+        },
+        {
+          type: lexer.TOKEN_VARIABLE_END,
+          lineno: 1,
+          colno: 9,
+        },
+        {
+          type: lexer.TOKEN_DATA,
+          value: '\n',
+        },
+        {
+          type: lexer.TOKEN_BLOCK_START,
+          lineno: 2,
+          colno: 0,
+        },
+        {
+          type: lexer.TOKEN_SYMBOL,
+          value: 'else',
+          lineno: 2,
+          colno: 3,
+        },
+        {
+          type: lexer.TOKEN_BLOCK_END,
+          lineno: 2,
+          colno: 8,
+        },
+        {
+          type: lexer.TOKEN_DATA,
+          value: '\n',
+        },
+        {
+          type: lexer.TOKEN_VARIABLE_START,
+          lineno: 3,
+          colno: 0,
+        },
+        {
+          type: lexer.TOKEN_SYMBOL,
+          value: 'otherValue',
+          lineno: 3,
+          colno: 3,
+        },
+        {
+          type: lexer.TOKEN_VARIABLE_END,
+          lineno: 3,
+          colno: 14,
+        },
+        {
+          type: lexer.TOKEN_DATA,
+          value: '\n',
+        },
+        {
+          type: lexer.TOKEN_BLOCK_START,
+          lineno: 4,
+          colno: 0,
+        },
+        {
+          type: lexer.TOKEN_SYMBOL,
+          value: 'endif',
+          lineno: 4,
+          colno: 3,
+        },
+        {
+          type: lexer.TOKEN_BLOCK_END,
+          lineno: 4,
+          colno: 9,
+        });
+    });
   });
 }());
diff --git a/tests/loader.js b/tests/loader.js
index 4c31428..c3b9a58 100644
--- a/tests/loader.js
+++ b/tests/loader.js
@@ -22,21 +22,6 @@
   }
 
   describe('loader', function() {
-    it('should have default opts for WebLoader', function() {
-      var webLoader = new WebLoader(templatesPath);
-      expect(webLoader).to.be.a(WebLoader);
-      expect(webLoader.useCache).to.be(false);
-      expect(webLoader.async).to.be(false);
-    });
-
-    if (typeof FileSystemLoader !== 'undefined') {
-      it('should have default opts for FileSystemLoader', function() {
-        var fileSystemLoader = new FileSystemLoader(templatesPath);
-        expect(fileSystemLoader).to.be.a(FileSystemLoader);
-        expect(fileSystemLoader.noCache).to.be(false);
-      });
-    }
-
     it('should allow a simple loader to be created', function() {
       // From Docs: http://mozilla.github.io/nunjucks/api.html#writing-a-loader
       // We should be able to create a loader that only exposes getSource
@@ -82,5 +67,49 @@
         done();
       });
     });
+
+    describe('WebLoader', function() {
+      it('should have default opts for WebLoader', function() {
+        var webLoader = new WebLoader(templatesPath);
+        expect(webLoader).to.be.a(WebLoader);
+        expect(webLoader.useCache).to.be(false);
+        expect(webLoader.async).to.be(false);
+      });
+
+      it('should emit a "load" event', function(done) {
+        var loader = new WebLoader(templatesPath);
+
+        if (typeof window === 'undefined') {
+          this.skip();
+        }
+
+        loader.on('load', function(name, source) {
+          expect(name).to.equal('simple-base.njk');
+          done();
+        });
+
+        loader.getSource('simple-base.njk');
+      });
+    });
+
+    if (typeof FileSystemLoader !== 'undefined') {
+      describe('FileSystemLoader', function() {
+        it('should have default opts', function() {
+          var loader = new FileSystemLoader(templatesPath);
+          expect(loader).to.be.a(FileSystemLoader);
+          expect(loader.noCache).to.be(false);
+        });
+
+        it('should emit a "load" event', function(done) {
+          var loader = new FileSystemLoader(templatesPath);
+          loader.on('load', function(name, source) {
+            expect(name).to.equal('simple-base.njk');
+            done();
+          });
+
+          loader.getSource('simple-base.njk');
+        });
+      });
+    }
   });
 }());