From 0ef85857268f2622a3c248e39cc359ffc9193849 Mon Sep 17 00:00:00 2001
From: Phil Hughes <me@iamphill.com>
Date: Thu, 23 Mar 2017 12:37:24 +0000
Subject: [PATCH] Added tests

---
 app/assets/javascripts/blob/notebook/index.js |  80 +++++++++
 .../javascripts/blob/notebook_viewer.js       |  76 +--------
 spec/javascripts/blob/notebook/index_spec.js  | 159 ++++++++++++++++++
 .../fixtures/notebook_viewer.html.haml        |   1 +
 spec/models/blob_spec.rb                      |  19 +++
 vendor/assets/javascripts/notebooklab.js      |   8 +-
 6 files changed, 265 insertions(+), 78 deletions(-)
 create mode 100644 app/assets/javascripts/blob/notebook/index.js
 create mode 100644 spec/javascripts/blob/notebook/index_spec.js
 create mode 100644 spec/javascripts/fixtures/notebook_viewer.html.haml

diff --git a/app/assets/javascripts/blob/notebook/index.js b/app/assets/javascripts/blob/notebook/index.js
new file mode 100644
index 00000000000..c910ed5b76b
--- /dev/null
+++ b/app/assets/javascripts/blob/notebook/index.js
@@ -0,0 +1,80 @@
+/* eslint-disable no-new */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+import NotebookLab from 'vendor/notebooklab';
+
+Vue.use(VueResource);
+Vue.use(NotebookLab);
+
+export default () => {
+  const el = document.getElementById('js-notebook-viewer');
+
+  new Vue({
+    el,
+    data() {
+      return {
+        error: false,
+        loadError: false,
+        loading: true,
+        json: {},
+      };
+    },
+    template: `
+      <div class="container-fluid md prepend-top-default append-bottom-default">
+        <div
+          class="text-center loading"
+          v-if="loading && !error">
+          <i
+            class="fa fa-spinner fa-spin"
+            aria-hidden="true"
+            aria-label="iPython notebook loading">
+          </i>
+        </div>
+        <notebook-lab
+          v-if="!loading && !error"
+          :notebook="json" />
+        <p
+          class="text-center"
+          v-if="error">
+          <span v-if="loadError">
+            An error occured whilst loading the file. Please try again later.
+          </span>
+          <span v-else>
+            An error occured whilst parsing the file.
+          </span>
+        </p>
+      </div>
+    `,
+    methods: {
+      loadFile() {
+        this.$http.get(el.dataset.endpoint)
+          .then((res) => {
+            this.json = res.json();
+            this.loading = false;
+          })
+          .catch((e) => {
+            if (e.status) {
+              this.loadError = true;
+            }
+
+            this.error = true;
+          });
+      },
+    },
+    mounted() {
+      $('<link>', {
+        rel: 'stylesheet',
+        type: 'text/css',
+        href: gon.katex_css_url,
+      }).appendTo('head');
+
+      if (gon.katex_js_url) {
+        $.getScript(gon.katex_js_url, () => {
+          this.loadFile();
+        });
+      } else {
+        this.loadFile();
+      }
+    },
+  });
+};
diff --git a/app/assets/javascripts/blob/notebook_viewer.js b/app/assets/javascripts/blob/notebook_viewer.js
index 45b838c700f..b7a0a195a92 100644
--- a/app/assets/javascripts/blob/notebook_viewer.js
+++ b/app/assets/javascripts/blob/notebook_viewer.js
@@ -1,75 +1,3 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-import NotebookLab from 'vendor/notebooklab';
+import renderNotebook from './notebook';
 
-Vue.use(VueResource);
-Vue.use(NotebookLab);
-
-document.addEventListener('DOMContentLoaded', () => {
-  const el = document.getElementById('js-notebook-viewer');
-
-  new Vue({
-    el,
-    data() {
-      return {
-        error: false,
-        loadError: false,
-        loading: true,
-        json: {},
-      };
-    },
-    template: `
-      <div class="container-fluid md prepend-top-default append-bottom-default">
-        <div
-          class="text-center loading"
-          v-if="loading && !error">
-          <i
-            class="fa fa-spinner fa-spin"
-            aria-hidden="true"
-            aria-label="iPython notebook loading">
-          </i>
-        </div>
-        <notebook-lab
-          v-if="!loading && !error"
-          :notebook="json" />
-        <p
-          class="text-center"
-          v-if="error">
-          <span v-if="loadError">
-            An error occured whilst loading the file. Please try again later.
-          </span>
-          <span v-else>
-            An error occured whilst parsing the file.
-          </span>
-        </p>
-      </div>
-    `,
-    methods: {
-      loadFile() {
-        this.$http.get(el.dataset.endpoint)
-          .then((res) => {
-            this.json = res.json();
-            this.loading = false;
-          })
-          .catch((e) => {
-            if (e.status) {
-              this.loadError = true;
-            }
-
-            this.error = true;
-          });
-      },
-    },
-    mounted() {
-      $('<link>', {
-        rel: 'stylesheet',
-        type: 'text/css',
-        href: gon.katex_css_url,
-      }).appendTo('head');
-
-      $.getScript(gon.katex_js_url, () => {
-        this.loadFile();
-      });
-    },
-  });
-});
+document.addEventListener('DOMContentLoaded', renderNotebook);
diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js
new file mode 100644
index 00000000000..03539bead29
--- /dev/null
+++ b/spec/javascripts/blob/notebook/index_spec.js
@@ -0,0 +1,159 @@
+import Vue from 'vue';
+import renderNotebook from '~/blob/notebook';
+
+describe('iPython notebook renderer', () => {
+  preloadFixtures('static/notebook_viewer.html.raw');
+
+  beforeEach(() => {
+    loadFixtures('static/notebook_viewer.html.raw');
+  });
+
+  it('shows loading icon', () => {
+    renderNotebook();
+
+    expect(
+      document.querySelector('.loading'),
+    ).not.toBeNull();
+  });
+
+  describe('successful response', () => {
+    const response = (request, next) => {
+      next(request.respondWith(JSON.stringify({
+        cells: [{
+          cell_type: 'markdown',
+          source: ['# test'],
+        }, {
+          cell_type: 'code',
+          execution_count: 1,
+          source: [
+            'def test(str)',
+            '  return str',
+          ],
+          outputs: [],
+        }],
+      }), {
+        status: 200,
+      }));
+    };
+
+    beforeEach((done) => {
+      Vue.http.interceptors.push(response);
+
+      renderNotebook();
+
+      setTimeout(() => {
+        done();
+      });
+    });
+
+    afterEach(() => {
+      Vue.http.interceptors = _.without(
+        Vue.http.interceptors, response,
+      );
+    });
+
+    it('does not show loading icon', () => {
+      expect(
+        document.querySelector('.loading'),
+      ).toBeNull();
+    });
+
+    it('renders the notebook', () => {
+      expect(
+        document.querySelector('.md'),
+      ).not.toBeNull();
+    });
+
+    it('renders the markdown cell', () => {
+      expect(
+        document.querySelector('h1'),
+      ).not.toBeNull();
+
+      expect(
+        document.querySelector('h1').textContent.trim(),
+      ).toBe('test');
+    });
+
+    it('highlights code', () => {
+      expect(
+        document.querySelector('.hljs'),
+      ).not.toBeNull();
+
+      expect(
+        document.querySelector('.python'),
+      ).not.toBeNull();
+    });
+  });
+
+  describe('error in JSON response', () => {
+    const response = (request, next) => {
+      next(request.respondWith('{ "cells": [{"cell_type": "markdown"} }', {
+        status: 200,
+      }));
+    };
+
+    beforeEach((done) => {
+      Vue.http.interceptors.push(response);
+
+      renderNotebook();
+
+      setTimeout(() => {
+        done();
+      });
+    });
+
+    afterEach(() => {
+      Vue.http.interceptors = _.without(
+        Vue.http.interceptors, response,
+      );
+    });
+
+    it('does not show loading icon', () => {
+      expect(
+        document.querySelector('.loading'),
+      ).toBeNull();
+    });
+
+    it('shows error message', () => {
+      expect(
+        document.querySelector('.md').textContent.trim(),
+      ).toBe('An error occured whilst parsing the file.');
+    });
+  });
+
+  describe('error getting file', () => {
+    const response = (request, next) => {
+      next(request.respondWith('', {
+        status: 500,
+      }));
+    };
+
+    beforeEach((done) => {
+      Vue.http.interceptors.push(response);
+
+      renderNotebook();
+
+      setTimeout(() => {
+        done();
+      });
+    });
+
+    afterEach(() => {
+      Vue.http.interceptors = _.without(
+        Vue.http.interceptors, response,
+      );
+    });
+
+    it('does not show loading icon', () => {
+      expect(
+        document.querySelector('.loading'),
+      ).toBeNull();
+    });
+
+    it('shows error message', () => {
+      expect(
+        document.querySelector('.md').textContent.trim(),
+      ).toBe('An error occured whilst loading the file. Please try again later.');
+    });
+  });
+});
diff --git a/spec/javascripts/fixtures/notebook_viewer.html.haml b/spec/javascripts/fixtures/notebook_viewer.html.haml
new file mode 100644
index 00000000000..17a7a9d8f31
--- /dev/null
+++ b/spec/javascripts/fixtures/notebook_viewer.html.haml
@@ -0,0 +1 @@
+.file-content#js-notebook-viewer{ data: { endpoint: '/test' } }
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 94c25a454aa..552229e9b07 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -53,6 +53,20 @@ describe Blob do
     end
   end
 
+  describe '#ipython_notebook?' do
+    it 'is falsey when language is not Jupyter Notebook' do
+      git_blob = double(text?: true, language: double(name: 'JSON'))
+
+      expect(described_class.decorate(git_blob)).not_to be_ipython_notebook
+    end
+
+    it 'is truthy when language is Jupyter Notebook' do
+      git_blob = double(text?: true, language: double(name: 'Jupyter Notebook'))
+
+      expect(described_class.decorate(git_blob)).to be_ipython_notebook
+    end
+  end
+
   describe '#video?' do
     it 'is falsey with image extension' do
       git_blob = Gitlab::Git::Blob.new(name: 'image.png')
@@ -116,6 +130,11 @@ describe Blob do
       blob = stubbed_blob
       expect(blob.to_partial_path(project)).to eq 'download'
     end
+
+    it 'handles iPython notebooks' do
+      blob = stubbed_blob(text?: true, ipython_notebook?: true)
+      expect(blob.to_partial_path(project)).to eq 'notebook'
+    end
   end
 
   describe '#size_within_svg_limits?' do
diff --git a/vendor/assets/javascripts/notebooklab.js b/vendor/assets/javascripts/notebooklab.js
index 35e845657bd..9d2aea18c5b 100644
--- a/vendor/assets/javascripts/notebooklab.js
+++ b/vendor/assets/javascripts/notebooklab.js
@@ -414,13 +414,13 @@ exports.default = {
   },
   computed: {
     markdown: function markdown() {
-      var regex = new RegExp(/^\$\$(.*)\$\$$/, 'g');
+      var regex = new RegExp('^\$\$(.*)\$\$$', 'g');
 
       var source = this.cell.source.map(function (line) {
         var matches = regex.exec(line.trim());
 
         // Only render use the Katex library if it is actually loaded
-        if (matches && matches.length > 0 && katex) {
+        if (matches && matches.length > 0 && typeof katex !== 'undefined') {
           return katex.renderToString(matches[1]);
         } else {
           return line;
@@ -3047,7 +3047,7 @@ function escape(html, encode) {
 }
 
 function unescape(html) {
-	// explicitly match decimal, hex, and named HTML entities 
+	// explicitly match decimal, hex, and named HTML entities
   return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/g, function(_, n) {
     n = n.toLowerCase();
     if (n === 'colon') return ':';
@@ -3636,4 +3636,4 @@ module.exports = {
 
 /***/ })
 /******/ ]);
-});
\ No newline at end of file
+});
-- 
GitLab