Pārlūkot izejas kodu

Merge pull request #1352 from blink1073/shell-tests

Application Shell Tests
Afshin Darian 8 gadi atpakaļ
vecāks
revīzija
aba05ded38

+ 2 - 0
src/application/index.ts

@@ -18,6 +18,8 @@ import {
 } from './shell';
 
 export { ModuleLoader } from './loader';
+export { ApplicationShell } from './shell';
+
 
 /**
  * The type for all JupyterLab plugins.

+ 159 - 156
src/application/shell.ts

@@ -70,18 +70,6 @@ const SIDEBAR_CLASS = 'jp-SideBar';
 const CURRENT_CLASS = 'jp-mod-current';
 
 
-/**
- * The options for adding a widget to a side area of the shell.
- */
-export
-interface ISideAreaOptions {
-  /**
-   * The rank order of the widget among its siblings.
-   */
-  rank?: number;
-}
-
-
 /**
  * The application shell for JupyterLab.
  */
@@ -99,8 +87,8 @@ class ApplicationShell extends Widget {
     let hboxPanel = this._hboxPanel = new BoxPanel();
     let dockPanel = this._dockPanel = new DockPanel();
     let hsplitPanel = this._hsplitPanel = new SplitPanel();
-    let leftHandler = this._leftHandler = new SideBarHandler('left');
-    let rightHandler = this._rightHandler = new SideBarHandler('right');
+    let leftHandler = this._leftHandler = new Private.SideBarHandler('left');
+    let rightHandler = this._rightHandler = new Private.SideBarHandler('right');
     let rootLayout = new BoxLayout();
 
     topPanel.id = 'jp-top-panel';
@@ -169,9 +157,6 @@ class ApplicationShell extends Widget {
 
   /**
    * The current widget in the shell's main area.
-   *
-   * #### Notes
-   * This property is read-only.
    */
   get currentWidget(): Widget {
     return this._dockPanel.currentWidget;
@@ -211,7 +196,7 @@ class ApplicationShell extends Widget {
    * #### Notes
    * Widgets must have a unique `id` property, which will be used as the DOM id.
    */
-  addToTopArea(widget: Widget, options: ISideAreaOptions = {}): void {
+  addToTopArea(widget: Widget, options: ApplicationShell.ISideAreaOptions = {}): void {
     if (!widget.id) {
       console.error('widgets added to app shell must have unique id property');
       return;
@@ -226,7 +211,7 @@ class ApplicationShell extends Widget {
    * #### Notes
    * Widgets must have a unique `id` property, which will be used as the DOM id.
    */
-  addToLeftArea(widget: Widget, options: ISideAreaOptions = {}): void {
+  addToLeftArea(widget: Widget, options: ApplicationShell.ISideAreaOptions = {}): void {
     if (!widget.id) {
       console.error('widgets added to app shell must have unique id property');
       return;
@@ -241,7 +226,7 @@ class ApplicationShell extends Widget {
    * #### Notes
    * Widgets must have a unique `id` property, which will be used as the DOM id.
    */
-  addToRightArea(widget: Widget, options: ISideAreaOptions = {}): void {
+  addToRightArea(widget: Widget, options: ApplicationShell.ISideAreaOptions = {}): void {
     if (!widget.id) {
       console.error('widgets added to app shell must have unique id property');
       return;
@@ -304,7 +289,7 @@ class ApplicationShell extends Widget {
   }
 
   /**
-   * Close all tracked widgets.
+   * Close all widgets in the main area.
    */
   closeAll(): void {
     each(toArray(this._dockPanel.widgets()), widget => { widget.close(); });
@@ -314,8 +299,8 @@ class ApplicationShell extends Widget {
   private _hboxPanel: BoxPanel;
   private _dockPanel: DockPanel;
   private _hsplitPanel: SplitPanel;
-  private _leftHandler: SideBarHandler;
-  private _rightHandler: SideBarHandler;
+  private _leftHandler: Private.SideBarHandler;
+  private _rightHandler: Private.SideBarHandler;
 }
 
 
@@ -324,172 +309,190 @@ defineSignal(ApplicationShell.prototype, 'currentChanged');
 
 
 /**
- * A class which manages a side bar and related stacked panel.
+ * The namespace for `ApplicationShell` class statics.
  */
-class SideBarHandler {
-  /**
-   * Construct a new side bar handler.
-   */
-  constructor(side: string) {
-    this._side = side;
-    this._sideBar = new TabBar({
-      insertBehavior: 'none',
-      removeBehavior: 'none',
-      allowDeselect: true
-    });
-    this._stackedPanel = new StackedPanel();
-    this._sideBar.hide();
-    this._stackedPanel.hide();
-    this._sideBar.currentChanged.connect(this._onCurrentChanged, this);
-    this._stackedPanel.widgetRemoved.connect(this._onWidgetRemoved, this);
-  }
-
-  /**
-   * Get the tab bar managed by the handler.
-   */
-  get sideBar(): TabBar {
-    return this._sideBar;
-  }
-
+export
+namespace ApplicationShell {
   /**
-   * Get the stacked panel managed by the handler
+   * The options for adding a widget to a side area of the shell.
    */
-  get stackedPanel(): StackedPanel {
-    return this._stackedPanel;
+  export
+  interface ISideAreaOptions {
+    /**
+     * The rank order of the widget among its siblings.
+     */
+    rank?: number;
   }
+}
 
-  /**
-   * Activate a widget residing in the side bar by ID.
-   *
-   * @param id - The widget's unique ID.
-   */
-  activate(id: string): void {
-    let widget = this._findWidgetByID(id);
-    if (widget) {
-      this._sideBar.currentTitle = widget.title;
-      widget.activate();
-    }
-  }
 
+namespace Private {
+  export
   /**
-   * Collapse the sidebar so no items are expanded.
+   * An object which holds a widget and its sort rank.
    */
-  collapse(): void {
-    this._sideBar.currentTitle = null;
-  }
+  interface IRankItem {
+    /**
+     * The widget for the item.
+     */
+    widget: Widget;
 
-  /**
-   * Add a widget and its title to the stacked panel and side bar.
-   *
-   * If the widget is already added, it will be moved.
-   */
-  addWidget(widget: Widget, rank: number): void {
-    widget.parent = null;
-    widget.hide();
-    let item = { widget, rank };
-    let index = this._findInsertIndex(item);
-    this._items.insert(index, item);
-    this._stackedPanel.insertWidget(index, widget);
-    this._sideBar.insertTab(index, widget.title);
-    this._refreshVisibility();
+    /**
+     * The sort rank of the widget.
+     */
+    rank: number;
   }
 
   /**
-   * Find the insertion index for a rank item.
+   * A less-than comparison function for side bar rank items.
    */
-  private _findInsertIndex(item: Private.IRankItem): number {
-    return upperBound(this._items, item, Private.itemCmp);
+  export
+  function itemCmp(first: IRankItem, second: IRankItem): number {
+    return first.rank - second.rank;
   }
 
   /**
-   * Find the index of the item with the given widget, or `-1`.
+   * A class which manages a side bar and related stacked panel.
    */
-  private _findWidgetIndex(widget: Widget): number {
-    return findIndex(this._items, item => item.widget === widget);
-  }
+  export
+  class SideBarHandler {
+    /**
+     * Construct a new side bar handler.
+     */
+    constructor(side: string) {
+      this._side = side;
+      this._sideBar = new TabBar({
+        insertBehavior: 'none',
+        removeBehavior: 'none',
+        allowDeselect: true
+      });
+      this._stackedPanel = new StackedPanel();
+      this._sideBar.hide();
+      this._stackedPanel.hide();
+      this._sideBar.currentChanged.connect(this._onCurrentChanged, this);
+      this._stackedPanel.widgetRemoved.connect(this._onWidgetRemoved, this);
+    }
 
-  /**
-   * Find the widget which owns the given title, or `null`.
-   */
-  private _findWidgetByTitle(title: Title): Widget {
-    let item = find(this._items, value => value.widget.title === title);
-    return item ? item.widget : null;
-  }
+    /**
+     * Get the tab bar managed by the handler.
+     */
+    get sideBar(): TabBar {
+      return this._sideBar;
+    }
 
-  /**
-   * Find the widget with the given id, or `null`.
-   */
-  private _findWidgetByID(id: string): Widget {
-    let item = find(this._items, value => value.widget.id === id);
-    return item ? item.widget : null;
-  }
+    /**
+     * Get the stacked panel managed by the handler
+     */
+    get stackedPanel(): StackedPanel {
+      return this._stackedPanel;
+    }
 
-  /**
-   * Refresh the visibility of the side bar and stacked panel.
-   */
-  private _refreshVisibility(): void {
-    this._sideBar.setHidden(this._sideBar.titles.length === 0);
-    this._stackedPanel.setHidden(this._sideBar.currentTitle === null);
-  }
+    /**
+     * Activate a widget residing in the side bar by ID.
+     *
+     * @param id - The widget's unique ID.
+     */
+    activate(id: string): void {
+      let widget = this._findWidgetByID(id);
+      if (widget) {
+        this._sideBar.currentTitle = widget.title;
+        widget.activate();
+      }
+    }
 
-  /**
-   * Handle the `currentChanged` signal from the sidebar.
-   */
-  private _onCurrentChanged(sender: TabBar, args: TabBar.ICurrentChangedArgs): void {
-    let oldWidget = this._findWidgetByTitle(args.previousTitle);
-    let newWidget = this._findWidgetByTitle(args.currentTitle);
-    if (oldWidget) {
-      oldWidget.hide();
+    /**
+     * Collapse the sidebar so no items are expanded.
+     */
+    collapse(): void {
+      this._sideBar.currentTitle = null;
     }
-    if (newWidget) {
-      newWidget.show();
+
+    /**
+     * Add a widget and its title to the stacked panel and side bar.
+     *
+     * If the widget is already added, it will be moved.
+     */
+    addWidget(widget: Widget, rank: number): void {
+      widget.parent = null;
+      widget.hide();
+      let item = { widget, rank };
+      let index = this._findInsertIndex(item);
+      this._items.insert(index, item);
+      this._stackedPanel.insertWidget(index, widget);
+      this._sideBar.insertTab(index, widget.title);
+      this._refreshVisibility();
     }
-    if (newWidget) {
-      document.body.setAttribute(`data-${this._side}Area`, newWidget.id);
-    } else {
-      document.body.removeAttribute(`data-${this._side}Area`);
+
+    /**
+     * Find the insertion index for a rank item.
+     */
+    private _findInsertIndex(item: Private.IRankItem): number {
+      return upperBound(this._items, item, Private.itemCmp);
     }
-    this._refreshVisibility();
-  }
 
-  /*
-   * Handle the `widgetRemoved` signal from the stacked panel.
-   */
-  private _onWidgetRemoved(sender: StackedPanel, widget: Widget): void {
-    this._items.removeAt(this._findWidgetIndex(widget));
-    this._sideBar.removeTab(widget.title);
-    this._refreshVisibility();
-  }
+    /**
+     * Find the index of the item with the given widget, or `-1`.
+     */
+    private _findWidgetIndex(widget: Widget): number {
+      return findIndex(this._items, item => item.widget === widget);
+    }
 
-  private _side: string;
-  private _sideBar: TabBar;
-  private _stackedPanel: StackedPanel;
-  private _items = new Vector<Private.IRankItem>();
-}
+    /**
+     * Find the widget which owns the given title, or `null`.
+     */
+    private _findWidgetByTitle(title: Title): Widget {
+      let item = find(this._items, value => value.widget.title === title);
+      return item ? item.widget : null;
+    }
 
+    /**
+     * Find the widget with the given id, or `null`.
+     */
+    private _findWidgetByID(id: string): Widget {
+      let item = find(this._items, value => value.widget.id === id);
+      return item ? item.widget : null;
+    }
 
-namespace Private {
-  export
-  /**
-   * An object which holds a widget and its sort rank.
-   */
-  interface IRankItem {
     /**
-     * The widget for the item.
+     * Refresh the visibility of the side bar and stacked panel.
      */
-    widget: Widget;
+    private _refreshVisibility(): void {
+      this._sideBar.setHidden(this._sideBar.titles.length === 0);
+      this._stackedPanel.setHidden(this._sideBar.currentTitle === null);
+    }
 
     /**
-     * The sort rank of the widget.
+     * Handle the `currentChanged` signal from the sidebar.
      */
-    rank: number;
-  }
+    private _onCurrentChanged(sender: TabBar, args: TabBar.ICurrentChangedArgs): void {
+      let oldWidget = this._findWidgetByTitle(args.previousTitle);
+      let newWidget = this._findWidgetByTitle(args.currentTitle);
+      if (oldWidget) {
+        oldWidget.hide();
+      }
+      if (newWidget) {
+        newWidget.show();
+      }
+      if (newWidget) {
+        document.body.setAttribute(`data-${this._side}Area`, newWidget.id);
+      } else {
+        document.body.removeAttribute(`data-${this._side}Area`);
+      }
+      this._refreshVisibility();
+    }
 
-  /**
-   * A less-than comparison function for side bar rank items.
-   */
-  export
-  function itemCmp(first: IRankItem, second: IRankItem): number {
-    return first.rank - second.rank;
+    /*
+     * Handle the `widgetRemoved` signal from the stacked panel.
+     */
+    private _onWidgetRemoved(sender: StackedPanel, widget: Widget): void {
+      this._items.removeAt(this._findWidgetIndex(widget));
+      this._sideBar.removeTab(widget.title);
+      this._refreshVisibility();
+    }
+
+    private _side: string;
+    private _sideBar: TabBar;
+    private _stackedPanel: StackedPanel;
+    private _items = new Vector<Private.IRankItem>();
   }
 }

+ 1 - 1
test/src/application/loader.spec.ts

@@ -5,7 +5,7 @@ import expect = require('expect.js');
 
 import {
   ModuleLoader
-} from '../../../lib/application/loader';
+} from '../../../lib/application';
 
 
 describe('ModuleLoader', () => {

+ 316 - 0
test/src/application/shell.spec.ts

@@ -0,0 +1,316 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import expect = require('expect.js');
+
+import {
+  Message
+} from 'phosphor/lib/core/messaging';
+
+import {
+  Widget
+} from 'phosphor/lib/ui/widget';
+
+import {
+  simulate
+} from 'simulate-event';
+
+import {
+  ApplicationShell
+} from '../../../lib/application';
+
+
+class ContentWidget extends Widget {
+
+  activated = false;
+
+  onActivateRequest(msg: Message): void {
+    this.activated = true;
+  }
+}
+
+
+describe('ApplicationShell', () => {
+
+  let shell: ApplicationShell;
+
+  beforeEach(() => {
+    shell = new ApplicationShell();
+    Widget.attach(shell, document.body);
+  });
+
+  afterEach(() => {
+    shell.dispose();
+  });
+
+  describe('#constructor()', () => {
+
+    it('should create an ApplicationShell instance', () => {
+      expect(shell).to.be.an(ApplicationShell);
+    });
+
+  });
+
+  describe('#currentWidget', () => {
+
+    it('should be the current widget in the shell main area', () => {
+      expect(shell.currentWidget).to.be(null);
+      let widget = new Widget();
+      widget.node.tabIndex = -1;
+      widget.id = 'foo';
+      shell.addToMainArea(widget);
+      expect(shell.currentWidget).to.be(null);
+      simulate(widget.node, 'focus');
+      expect(shell.currentWidget).to.be(widget);
+    });
+
+  });
+
+  describe('#mainAreaIsEmpty', () => {
+
+    it('should test whether the main area is empty', () => {
+      expect(shell.mainAreaIsEmpty).to.be(true);
+      let widget = new Widget();
+      widget.id = 'foo';
+      shell.addToMainArea(widget);
+      expect(shell.mainAreaIsEmpty).to.be(false);
+    });
+
+  });
+
+  describe('#topAreaIsEmpty', () => {
+
+    it('should test whether the top area is empty', () => {
+      expect(shell.topAreaIsEmpty).to.be(true);
+      let widget = new Widget();
+      widget.id = 'foo';
+      shell.addToTopArea(widget);
+      expect(shell.topAreaIsEmpty).to.be(false);
+    });
+
+  });
+
+  describe('#leftAreaIsEmpty', () => {
+
+    it('should test whether the left area is empty', () => {
+      expect(shell.leftAreaIsEmpty).to.be(true);
+      let widget = new Widget();
+      widget.id = 'foo';
+      shell.addToLeftArea(widget);
+      expect(shell.leftAreaIsEmpty).to.be(false);
+    });
+
+  });
+
+  describe('#rightAreaIsEmpty', () => {
+
+    it('should test whether the right area is empty', () => {
+      expect(shell.rightAreaIsEmpty).to.be(true);
+      let widget = new Widget();
+      widget.id = 'foo';
+      shell.addToRightArea(widget);
+      expect(shell.rightAreaIsEmpty).to.be(false);
+    });
+
+  });
+
+  describe('#addToTopArea()', () => {
+
+    it('should add a widget to the top area', () => {
+      let widget = new Widget();
+      widget.id = 'foo';
+      shell.addToTopArea(widget);
+      expect(shell.topAreaIsEmpty).to.be(false);
+    });
+
+    it('should be a no-op if the widget has no id', () => {
+      let widget = new Widget();
+      shell.addToTopArea(widget);
+      expect(shell.topAreaIsEmpty).to.be(true);
+    });
+
+    it('should accept options', () => {
+      let widget = new Widget();
+      widget.id = 'foo';
+      shell.addToTopArea(widget, { rank: 10 });
+      expect(shell.topAreaIsEmpty).to.be(false);
+    });
+
+  });
+
+  describe('#addToLeftArea()', () => {
+
+    it('should add a widget to the left area', () => {
+      let widget = new Widget();
+      widget.id = 'foo';
+      shell.addToLeftArea(widget);
+      expect(shell.leftAreaIsEmpty).to.be(false);
+    });
+
+    it('should be a no-op if the widget has no id', () => {
+      let widget = new Widget();
+      shell.addToLeftArea(widget);
+      expect(shell.leftAreaIsEmpty).to.be(true);
+    });
+
+    it('should accept options', () => {
+      let widget = new Widget();
+      widget.id = 'foo';
+      shell.addToLeftArea(widget, { rank: 10 });
+      expect(shell.leftAreaIsEmpty).to.be(false);
+    });
+
+  });
+
+  describe('#addToRightArea()', () => {
+
+    it('should add a widget to the right area', () => {
+      let widget = new Widget();
+      widget.id = 'foo';
+      shell.addToRightArea(widget);
+      expect(shell.rightAreaIsEmpty).to.be(false);
+    });
+
+    it('should be a no-op if the widget has no id', () => {
+      let widget = new Widget();
+      shell.addToRightArea(widget);
+      expect(shell.rightAreaIsEmpty).to.be(true);
+    });
+
+    it('should accept options', () => {
+      let widget = new Widget();
+      widget.id = 'foo';
+      shell.addToRightArea(widget, { rank: 10 });
+      expect(shell.rightAreaIsEmpty).to.be(false);
+    });
+
+  });
+
+  describe('#addToMainArea()', () => {
+
+    it('should add a widget to the main area', () => {
+      let widget = new Widget();
+      widget.id = 'foo';
+      shell.addToMainArea(widget);
+      expect(shell.mainAreaIsEmpty).to.be(false);
+    });
+
+    it('should be a no-op if the widget has no id', () => {
+      let widget = new Widget();
+      shell.addToMainArea(widget);
+      expect(shell.mainAreaIsEmpty).to.be(true);
+    });
+
+  });
+
+  describe('#activateLeft()', () => {
+
+    it('should activate a widget in the left area', () => {
+      let widget = new Widget();
+      widget.id = 'foo';
+      shell.addToLeftArea(widget);
+      expect(widget.isVisible).to.be(false);
+      shell.activateLeft('foo');
+      expect(widget.isVisible).to.be(true);
+    });
+
+    it('should be a no-op if the widget is not in the left area', () => {
+      let widget = new Widget();
+      widget.id = 'foo';
+      expect(widget.isVisible).to.be(false);
+      shell.activateLeft('foo');
+      expect(widget.isVisible).to.be(false);
+    });
+
+  });
+
+  describe('#activateRight()', () => {
+
+    it('should activate a widget in the right area', () => {
+      let widget = new Widget();
+      widget.id = 'foo';
+      shell.addToRightArea(widget);
+      expect(widget.isVisible).to.be(false);
+      shell.activateRight('foo');
+      expect(widget.isVisible).to.be(true);
+    });
+
+    it('should be a no-op if the widget is not in the right area', () => {
+      let widget = new Widget();
+      widget.id = 'foo';
+      expect(widget.isVisible).to.be(false);
+      shell.activateRight('foo');
+      expect(widget.isVisible).to.be(false);
+    });
+
+  });
+
+  describe('#activateMain()', () => {
+
+    it('should activate a widget in the main area', (done) => {
+      let widget = new ContentWidget();
+      widget.id = 'foo';
+      shell.addToMainArea(widget);
+      shell.activateMain('foo');
+      requestAnimationFrame(() => {
+        expect(widget.activated).to.be(true);
+        done();
+      });
+    });
+
+    it('should be a no-op if the widget is not in the main area', (done) => {
+      let widget = new ContentWidget();
+      widget.id = 'foo';
+      shell.activateMain('foo');
+      requestAnimationFrame(() => {
+        expect(widget.activated).to.be(false);
+        done();
+      });
+    });
+
+  });
+
+  describe('#collapseLeft()', () => {
+
+    it('should collapse all widgets in the left area', () => {
+      let widget = new Widget();
+      widget.id = 'foo';
+      shell.addToLeftArea(widget);
+      shell.activateLeft('foo');
+      expect(widget.isVisible).to.be(true);
+      shell.collapseLeft();
+      expect(widget.isVisible).to.be(false);
+    });
+
+  });
+
+  describe('#collapseRight()', () => {
+
+    it('should collapse all widgets in the right area', () => {
+      let widget = new Widget();
+      widget.id = 'foo';
+      shell.addToRightArea(widget);
+      shell.activateRight('foo');
+      expect(widget.isVisible).to.be(true);
+      shell.collapseRight();
+      expect(widget.isVisible).to.be(false);
+    });
+
+  });
+
+  describe('#closeAll()', () => {
+
+    it('should close all of the widgets in the main area', () => {
+      let foo = new Widget();
+      foo.id = 'foo';
+      shell.addToMainArea(foo);
+      let bar = new Widget();
+      bar.id = 'bar';
+      shell.addToMainArea(bar);
+      shell.closeAll();
+      expect(foo.isAttached).to.be(false);
+      expect(bar.isAttached).to.be(false);
+    });
+
+  });
+});

+ 1 - 0
test/src/index.ts

@@ -2,6 +2,7 @@
 // Distributed under the terms of the Modified BSD License.
 
 import './application/loader.spec';
+import './application/shell.spec';
 
 import './commandlinker/commandlinker.spec';