// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { Message, MessageLoop } from '@lumino/messaging';

import { Widget } from '@lumino/widgets';

import { ClientSession, IClientSession } from '@jupyterlab/apputils';

import { CodeEditor, CodeEditorWrapper } from '@jupyterlab/codeeditor';

import {
  Cell,
  CellModel,
  InputPrompt,
  CodeCell,
  CodeCellModel,
  MarkdownCell,
  RawCell,
  RawCellModel,
  MarkdownCellModel,
  CellFooter,
  CellHeader,
  InputArea
} from '@jupyterlab/cells';

import { OutputArea, OutputPrompt } from '@jupyterlab/outputarea';

import {
  createClientSession,
  framePromise,
  NBTestUtils
} from '@jupyterlab/testutils';

const RENDERED_CLASS = 'jp-mod-rendered';
const rendermime = NBTestUtils.defaultRenderMime();

class LogBaseCell extends Cell {
  methods: string[] = [];

  constructor() {
    super({
      model: new CellModel({}),
      contentFactory: NBTestUtils.createBaseCellFactory()
    });
  }

  protected onAfterAttach(msg: Message): void {
    super.onAfterAttach(msg);
    this.methods.push('onAfterAttach');
  }

  protected onActivateRequest(msg: Message): void {
    super.onActivateRequest(msg);
    this.methods.push('onActivateRequest');
  }

  protected onUpdateRequest(msg: Message): void {
    super.onUpdateRequest(msg);
    this.methods.push('onUpdateRequest');
  }
}

class LogCodeCell extends CodeCell {
  methods: string[] = [];

  constructor() {
    super({
      model: new CodeCellModel({}),
      contentFactory: NBTestUtils.createCodeCellFactory(),
      rendermime
    });
  }

  protected onUpdateRequest(msg: Message): void {
    super.onAfterAttach(msg);
    this.methods.push('onUpdateRequest');
  }

  protected onMetadataChanged(model: any, args: any): void {
    super.onMetadataChanged(model, args);
    this.methods.push('onMetadataChanged');
  }
}

class LogMarkdownCell extends MarkdownCell {
  methods: string[] = [];

  protected onUpdateRequest(msg: Message): void {
    super.onAfterAttach(msg);
    this.methods.push('onUpdateRequest');
  }
}

describe('cells/widget', () => {
  const editorFactory = NBTestUtils.editorFactory;

  describe('Cell', () => {
    const contentFactory = NBTestUtils.createBaseCellFactory();
    const model = new CellModel({});

    describe('#constructor()', () => {
      it('should create a base cell widget', () => {
        const widget = new Cell({ model, contentFactory }).initializeState();
        expect(widget).toBeInstanceOf(Cell);
      });

      it('should accept a custom contentFactory', () => {
        const contentFactory = NBTestUtils.createBaseCellFactory();
        const widget = new Cell({ model, contentFactory }).initializeState();
        expect(widget).toBeInstanceOf(Cell);
      });

      it('shoule accept a custom editorConfig', () => {
        const editorConfig: Partial<CodeEditor.IConfig> = {
          insertSpaces: false,
          matchBrackets: false
        };
        const widget = new Cell({
          editorConfig,
          model,
          contentFactory
        }).initializeState();
        expect(widget.editor.getOption('insertSpaces')).toEqual(false);
        expect(widget.editor.getOption('matchBrackets')).toEqual(false);
        expect(widget.editor.getOption('lineNumbers')).toEqual(
          CodeEditor.defaultConfig.lineNumbers
        );
      });
    });

    describe('#model', () => {
      it('should be the model used by the widget', () => {
        const model = new CellModel({});
        const widget = new Cell({ model, contentFactory }).initializeState();
        expect(widget.model).toEqual(model);
      });
    });

    describe('#editorWidget', () => {
      it('should be a code editor widget', () => {
        const widget = new Cell({ model, contentFactory }).initializeState();
        expect(widget.editorWidget).toBeInstanceOf(CodeEditorWrapper);
      });
    });

    describe('#editor', () => {
      it('should be a cell editor', () => {
        const widget = new Cell({ model, contentFactory }).initializeState();
        expect(widget.editor.uuid).toBeTruthy();
      });
    });

    describe('#inputArea', () => {
      it('should be the input area for the cell', () => {
        const widget = new Cell({ model }).initializeState();
        expect(widget.inputArea).toBeInstanceOf(InputArea);
      });
    });

    describe('#readOnly', () => {
      it('should be a boolean', () => {
        const widget = new Cell({ model, contentFactory }).initializeState();
        expect(typeof widget.readOnly).toEqual('boolean');
      });

      it('should default to false', () => {
        const widget = new Cell({ model, contentFactory }).initializeState();
        expect(widget.readOnly).toEqual(false);
      });

      it('should be settable', () => {
        const widget = new Cell({
          model,
          contentFactory
        }).initializeState();
        widget.readOnly = true;
        expect(widget.readOnly).toEqual(true);
      });

      it('should ignore being set to the same value', async () => {
        const widget = new LogBaseCell().initializeState();
        widget.readOnly = true;
        widget.readOnly = true;
        await framePromise();
        expect(widget.methods).toEqual(['onUpdateRequest']);
      });

      it('should reflect model metadata', () => {
        model.metadata.set('editable', false);

        const widget = new Cell({
          model,
          contentFactory
        }).initializeState();
        expect(widget.readOnly).toEqual(true);
      });
    });

    describe('#inputCollapsed', () => {
      it('should be the view state of the input being collapsed', () => {
        const widget = new LogBaseCell().initializeState();
        expect(widget.inputHidden).toEqual(false);
        widget.inputHidden = true;
        expect(widget.inputHidden).toEqual(true);
      });
    });

    describe('#loadEditableState()', () => {
      it('should load the editable state from the model', () => {
        const model = new CellModel({});
        const widget = new Cell({ model, contentFactory }).initializeState();
        expect(widget.readOnly).toEqual(false);

        model.metadata.set('editable', false);
        widget.loadEditableState();
        expect(widget.readOnly).toEqual(true);

        model.metadata.set('editable', true);
        widget.loadEditableState();
        expect(widget.readOnly).toEqual(false);
      });
    });

    describe('#saveEditableState()', () => {
      it('should save the editable state to the model', () => {
        const model = new CellModel({});
        const widget = new Cell({ model, contentFactory }).initializeState();
        expect(widget.readOnly).toEqual(false);

        widget.readOnly = true;
        widget.saveEditableState();
        expect(model.metadata.get('editable')).toEqual(false);

        widget.readOnly = false;
        widget.saveEditableState();
        // Default values are not saved explicitly
        expect(model.metadata.get('editable')).toEqual(undefined);
      });
    });

    describe('#syncEditable', () => {
      it('should control automatic syncing of editable state with model', () => {
        const model = new CellModel({});
        const widget = new Cell({ model, contentFactory }).initializeState();
        expect(widget.syncEditable).toEqual(false);
        expect(widget.readOnly).toEqual(false);

        // Not synced if setting widget attribute
        widget.readOnly = true;
        expect(model.metadata.get('editable')).toEqual(undefined);

        // Not synced if setting metadata attribute
        model.metadata.set('editable', true);
        expect(widget.readOnly).toEqual(true);

        widget.syncEditable = true;

        // Setting sync does an initial sync from model to view. This also sets
        // the metadata to undefined if it is the default value.
        expect(model.metadata.get('editable')).toEqual(undefined);
        expect(widget.readOnly).toEqual(false);

        // Synced if setting widget attribute
        widget.readOnly = true;
        expect(model.metadata.get('editable')).toEqual(false);

        // Synced if setting metadata attribute
        model.metadata.set('editable', true);
        expect(widget.readOnly).toEqual(false);
      });
    });

    describe('#loadCollapseState()', () => {
      it('should load the input collapse state from the model', () => {
        const model = new CellModel({});
        const widget = new Cell({ model, contentFactory }).initializeState();
        expect(widget.inputHidden).toEqual(false);

        model.metadata.set('jupyter', { source_hidden: true });
        widget.loadCollapseState();
        expect(widget.inputHidden).toEqual(true);

        model.metadata.set('jupyter', { source_hidden: false });
        widget.loadCollapseState();
        expect(widget.inputHidden).toEqual(false);
      });
    });

    describe('#saveCollapseState()', () => {
      it('should save the collapse state to the model', () => {
        const model = new CellModel({});
        const widget = new Cell({ model, contentFactory }).initializeState();
        expect(widget.inputHidden).toEqual(false);

        widget.inputHidden = true;
        widget.saveCollapseState();
        expect(model.metadata.get('jupyter')).toEqual({
          source_hidden: true
        });

        widget.inputHidden = false;
        widget.saveCollapseState();
        // Default values are not saved explicitly
        expect(model.metadata.get('jupyter')).toEqual(undefined);
      });
    });

    describe('#syncCollapse', () => {
      it('should control automatic syncing of collapse state with model', () => {
        const model = new CellModel({});
        const widget = new Cell({ model, contentFactory }).initializeState();
        expect(widget.syncCollapse).toEqual(false);
        expect(widget.inputHidden).toEqual(false);

        // Not synced if setting widget attribute
        widget.inputHidden = true;
        expect(model.metadata.get('jupyter')).toEqual(undefined);

        // Not synced if setting metadata attribute
        model.metadata.set('jupyter', { source_hidden: false });
        expect(widget.inputHidden).toEqual(true);

        widget.syncCollapse = true;

        // Setting sync does an initial sync from model to view. This also sets
        // the metadata to undefined if it is the default value.
        expect(model.metadata.get('jupyter')).toEqual(undefined);
        expect(widget.inputHidden).toEqual(false);

        // Synced if setting widget attribute
        widget.inputHidden = true;
        expect(model.metadata.get('jupyter')).toEqual({
          source_hidden: true
        });

        // Synced if setting metadata attribute
        model.metadata.set('jupyter', {});
        expect(widget.inputHidden).toEqual(false);
      });
    });

    describe('#onActivateRequest()', () => {
      it('should focus the cell editor', async () => {
        const widget = new LogBaseCell().initializeState();
        Widget.attach(widget, document.body);
        widget.activate();
        await framePromise();
        expect(widget.methods).toContain('onActivateRequest');
        await framePromise();
        expect(widget.editor.hasFocus()).toEqual(true);
        widget.dispose();
      });
    });

    describe('#setPrompt()', () => {
      it('should not throw an error (full test in input area)', () => {
        const widget = new Cell({ model, contentFactory }).initializeState();
        expect(() => {
          widget.setPrompt(void 0);
        }).not.toThrow();
        expect(() => {
          widget.setPrompt(null);
        }).not.toThrow();
        expect(() => {
          widget.setPrompt('');
        }).not.toThrow();
        expect(() => {
          widget.setPrompt('null');
        }).not.toThrow();
        expect(() => {
          widget.setPrompt('test');
        }).not.toThrow();
      });
    });

    describe('#dispose()', () => {
      it('should dispose of the resources held by the widget', () => {
        const widget = new Cell({ model, contentFactory }).initializeState();
        widget.dispose();
        expect(widget.isDisposed).toEqual(true);
      });

      it('should be safe to call multiple times', () => {
        const widget = new Cell({ model, contentFactory }).initializeState();
        widget.dispose();
        widget.dispose();
        expect(widget.isDisposed).toEqual(true);
      });
    });

    describe('#onAfterAttach()', () => {
      it('should run when widget is attached', () => {
        const widget = new LogBaseCell().initializeState();
        expect(widget.methods).not.toContain('onAfterAttach');
        Widget.attach(widget, document.body);
        expect(widget.methods).toContain('onAfterAttach');
        widget.dispose();
      });
    });

    describe('#onUpdateRequest()', () => {
      it('should update the widget', () => {
        const widget = new LogBaseCell().initializeState();
        expect(widget.methods).not.toContain('onUpdateRequest');
        MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
        expect(widget.methods).toContain('onUpdateRequest');
      });
    });

    describe('#.defaultContentFactory', () => {
      it('should be a contentFactory', () => {
        expect(Cell.defaultContentFactory).toBeInstanceOf(Cell.ContentFactory);
      });
    });

    describe('.ContentFactory', () => {
      describe('#constructor', () => {
        it('should create a ContentFactory', () => {
          const factory = new Cell.ContentFactory({ editorFactory });
          expect(factory).toBeInstanceOf(Cell.ContentFactory);
        });
      });

      describe('#editorFactory', () => {
        it('should be the editor factory used by the content factory', () => {
          const factory = new Cell.ContentFactory({ editorFactory });
          expect(factory.editorFactory).toEqual(editorFactory);
        });
      });

      describe('#createCellHeader()', () => {
        it('should create a new cell header', () => {
          const factory = new Cell.ContentFactory();
          expect(factory.createCellHeader()).toBeInstanceOf(CellHeader);
        });
      });

      describe('#createCellFooter()', () => {
        it('should create a new cell footer', () => {
          const factory = new Cell.ContentFactory();
          expect(factory.createCellFooter()).toBeInstanceOf(CellFooter);
        });
      });

      describe('#createOutputPrompt()', () => {
        it('should create a new output prompt', () => {
          const factory = new Cell.ContentFactory();
          expect(factory.createOutputPrompt()).toBeInstanceOf(OutputPrompt);
        });
      });

      describe('#createInputPrompt()', () => {
        it('should create a new input prompt', () => {
          const factory = new Cell.ContentFactory();
          expect(factory.createInputPrompt()).toBeInstanceOf(InputPrompt);
        });
      });
    });
  });

  describe('CodeCell', () => {
    const contentFactory = NBTestUtils.createCodeCellFactory();
    const model = new CodeCellModel({});

    describe('#constructor()', () => {
      it('should create a code cell widget', () => {
        const widget = new CodeCell({ model, rendermime, contentFactory });
        widget.initializeState();
        expect(widget).toBeInstanceOf(CodeCell);
      });

      it('should accept a custom contentFactory', () => {
        const contentFactory = NBTestUtils.createCodeCellFactory();
        const widget = new CodeCell({ model, contentFactory, rendermime });
        widget.initializeState();
        expect(widget).toBeInstanceOf(CodeCell);
      });
    });

    describe('#outputArea', () => {
      it('should be the output area used by the cell', () => {
        const widget = new CodeCell({ model, rendermime });
        widget.initializeState();
        expect(widget.outputArea).toBeInstanceOf(OutputArea);
      });
    });

    describe('#outputCollapsed', () => {
      it('should initialize from the model', () => {
        const collapsedModel = new CodeCellModel({});
        let widget = new CodeCell({ model: collapsedModel, rendermime });
        widget.initializeState();
        expect(widget.outputHidden).toEqual(false);

        collapsedModel.metadata.set('collapsed', true);
        widget = new CodeCell({ model: collapsedModel, rendermime });
        widget.initializeState();
        expect(widget.outputHidden).toEqual(true);

        collapsedModel.metadata.delete('collapsed');
        collapsedModel.metadata.set('jupyter', { outputs_hidden: true });
        widget = new CodeCell({ model: collapsedModel, rendermime });
        widget.initializeState();
        expect(widget.outputHidden).toEqual(true);
      });

      it('should be the view state of the output being collapsed', () => {
        const widget = new CodeCell({ model, rendermime });
        widget.initializeState();
        expect(widget.outputHidden).toEqual(false);
        widget.outputHidden = true;
        expect(widget.outputHidden).toEqual(true);
      });
    });

    describe('#outputsScrolled', () => {
      it('should initialize from the model', () => {
        const model = new CodeCellModel({});
        let widget = new CodeCell({ model, rendermime });
        widget.initializeState();
        expect(widget.outputsScrolled).toEqual(false);

        model.metadata.set('scrolled', false);
        widget = new CodeCell({ model, rendermime });
        widget.initializeState();
        expect(widget.outputsScrolled).toEqual(false);

        model.metadata.set('scrolled', 'auto');
        widget = new CodeCell({ model, rendermime });
        widget.initializeState();
        expect(widget.outputsScrolled).toEqual(false);

        model.metadata.set('scrolled', true);
        widget = new CodeCell({ model, rendermime });
        widget.initializeState();
        expect(widget.outputsScrolled).toEqual(true);
      });
    });

    describe('#loadScrolledState()', () => {
      it('should load the output scrolled state from the model', () => {
        const model = new CodeCellModel({});
        let widget = new CodeCell({ model, rendermime });
        widget.initializeState();
        expect(widget.outputsScrolled).toEqual(false);

        model.metadata.set('scrolled', true);
        widget.loadScrolledState();
        expect(widget.outputsScrolled).toEqual(true);

        model.metadata.set('scrolled', false);
        widget.loadScrolledState();
        expect(widget.outputsScrolled).toEqual(false);
      });
    });

    describe('#saveScrolledState()', () => {
      it('should save the collapse state to the model', () => {
        const model = new CodeCellModel({});
        let widget = new CodeCell({ model, rendermime });
        widget.initializeState();
        expect(widget.outputsScrolled).toEqual(false);

        widget.outputsScrolled = true;
        widget.saveScrolledState();
        expect(model.metadata.get('scrolled')).toEqual(true);

        widget.outputsScrolled = false;
        widget.saveScrolledState();
        // Default values are not saved explicitly
        expect(model.metadata.get('scrolled')).toEqual(undefined);
      });
    });

    describe('#syncScrolled', () => {
      it('should control automatic syncing of scrolled state with model', () => {
        const model = new CodeCellModel({});
        let widget = new CodeCell({ model, rendermime });
        widget.initializeState();
        expect(widget.syncScrolled).toEqual(false);
        expect(widget.outputsScrolled).toEqual(false);

        // Not synced if setting widget attribute
        widget.outputsScrolled = true;
        expect(model.metadata.get('scrolled')).toEqual(undefined);

        // Not synced if setting metadata attribute
        model.metadata.set('scrolled', false);
        expect(widget.outputsScrolled).toEqual(true);

        widget.syncScrolled = true;

        // Setting sync does an initial sync from model to view. This also sets
        // the metadata to undefined if it is the default value.
        expect(model.metadata.get('scrolled')).toEqual(undefined);
        expect(widget.outputsScrolled).toEqual(false);

        // Synced if setting widget attribute
        widget.outputsScrolled = true;
        expect(model.metadata.get('scrolled')).toEqual(true);

        // Synced if setting metadata attribute
        model.metadata.set('scrolled', false);
        expect(widget.outputsScrolled).toEqual(false);
      });
    });

    describe('#loadCollapseState()', () => {
      it('should load the output collapse state from the model', () => {
        const model = new CodeCellModel({});
        let widget = new CodeCell({ model, rendermime });
        widget.initializeState();
        widget.loadCollapseState();
        expect(widget.outputHidden).toEqual(false);

        model.metadata.set('collapsed', true);
        widget.loadCollapseState();
        expect(widget.outputHidden).toEqual(true);

        model.metadata.set('collapsed', false);
        widget.loadCollapseState();
        expect(widget.outputHidden).toEqual(false);
      });
    });

    describe('#saveCollapseState()', () => {
      it('should save the collapse state to the model `collapsed` metadata', () => {
        const model = new CodeCellModel({});
        let widget = new CodeCell({ model, rendermime });
        widget.initializeState();
        expect(widget.outputHidden).toEqual(false);

        widget.outputHidden = true;
        widget.saveCollapseState();
        expect(model.metadata.get('collapsed')).toEqual(true);

        // Default values are not saved explicitly
        widget.outputHidden = false;
        widget.saveCollapseState();
        expect(model.metadata.get('collapsed')).toEqual(undefined);

        // Default values are explicitly deleted
        model.metadata.set('collapsed', false);
        widget.outputHidden = false;
        widget.saveCollapseState();
        expect(model.metadata.get('collapsed')).toEqual(undefined);
      });
    });

    describe('#syncCollapse', () => {
      it('should control automatic syncing of collapse state with model', () => {
        const model = new CodeCellModel({});
        let widget = new CodeCell({ model, rendermime });
        widget.initializeState();
        expect(widget.syncCollapse).toEqual(false);
        expect(widget.outputHidden).toEqual(false);

        // Not synced if setting widget attribute
        widget.outputHidden = true;
        expect(model.metadata.get('collapsed')).toEqual(undefined);

        // Not synced if setting metadata attribute
        model.metadata.set('collapsed', false);
        expect(widget.outputHidden).toEqual(true);

        widget.syncCollapse = true;

        // Setting sync does an initial sync from model to view.
        expect(model.metadata.get('collapsed')).toEqual(undefined);
        expect(widget.outputHidden).toEqual(false);

        // Synced if setting widget attribute
        widget.outputHidden = true;
        expect(model.metadata.get('collapsed')).toEqual(true);

        // Synced if setting metadata attribute
        model.metadata.set('collapsed', false);
        expect(widget.outputHidden).toEqual(false);

        // Synced if deleting collapsed metadata attribute
        widget.outputHidden = true;
        expect(model.metadata.get('collapsed')).toEqual(true);
        model.metadata.delete('collapsed');
        expect(widget.outputHidden).toEqual(false);
      });
    });

    describe('#dispose()', () => {
      it('should dispose of the resources held by the widget', () => {
        const widget = new CodeCell({ model, rendermime, contentFactory });
        widget.initializeState();
        widget.dispose();
        expect(widget.isDisposed).toEqual(true);
      });

      it('should be safe to call multiple times', () => {
        const widget = new CodeCell({ model, rendermime, contentFactory });
        widget.initializeState();
        widget.dispose();
        widget.dispose();
        expect(widget.isDisposed).toEqual(true);
      });
    });

    describe('#onUpdateRequest()', () => {
      it('should update the widget', () => {
        const widget = new LogCodeCell().initializeState();
        expect(widget.methods).not.toContain('onUpdateRequest');
        MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
        expect(widget.methods).toContain('onUpdateRequest');
      });
    });

    describe('#onMetadataChanged()', () => {
      it('should fire when model metadata changes', () => {
        const method = 'onMetadataChanged';
        const widget = new LogCodeCell().initializeState();
        expect(widget.methods).not.toContain(method);
        widget.model.metadata.set('foo', 1);
        expect(widget.methods).toContain(method);
      });
    });

    describe('.execute()', () => {
      let session: IClientSession;

      beforeEach(async () => {
        session = await createClientSession();
        await (session as ClientSession).initialize();
        await session.kernel.ready;
      });

      afterEach(() => {
        return session.shutdown();
      });

      it('should fulfill a promise if there is no code to execute', async () => {
        const widget = new CodeCell({ model, rendermime, contentFactory });
        widget.initializeState();
        try {
          await CodeCell.execute(widget, session);
        } catch (error) {
          throw error;
        }
      });

      it('should fulfill a promise if there is code to execute', async () => {
        const widget = new CodeCell({ model, rendermime, contentFactory });
        widget.initializeState();
        let originalCount: number;
        widget.model.value.text = 'foo';
        originalCount = widget.model.executionCount;
        await CodeCell.execute(widget, session);
        const executionCount = widget.model.executionCount;
        expect(executionCount).not.toEqual(originalCount);
      });

      const TIMING_KEYS = [
        'iopub.execute_input',
        'shell.execute_reply.started',
        'shell.execute_reply',
        'iopub.status.busy',
        'iopub.status.idle'
      ];

      it('should not save timing info by default', async () => {
        const widget = new CodeCell({ model, rendermime, contentFactory });
        await CodeCell.execute(widget, session);
        expect(widget.model.metadata.get('execution')).toBeUndefined();
      });
      it('should save timing info if requested', async () => {
        const widget = new CodeCell({ model, rendermime, contentFactory });
        await CodeCell.execute(widget, session, { recordTiming: true });
        expect(widget.model.metadata.get('execution')).toBeDefined();
        const timingInfo = widget.model.metadata.get('execution') as any;
        for (const key of TIMING_KEYS) {
          expect(timingInfo[key]).toBeDefined();
        }
      });

      it('should set the cell prompt properly while executing', async () => {
        const widget = new CodeCell({ model, rendermime, contentFactory });
        widget.initializeState();
        widget.model.value.text = 'foo';
        const future1 = CodeCell.execute(widget, session);
        expect(widget.promptNode.textContent).toEqual('[*]:');
        const future2 = CodeCell.execute(widget, session);
        expect(widget.promptNode.textContent).toEqual('[*]:');
        await expect(future1).rejects.toThrow('Canceled');
        expect(widget.promptNode.textContent).toEqual('[*]:');
        let msg = await future2;
        expect(msg).not.toBeUndefined;

        // The `if` is a Typescript type guard so that msg.content works below.
        if (msg) {
          expect(widget.promptNode.textContent).toEqual(
            `[${msg.content.execution_count}]:`
          );
        }
      });
    });
  });

  describe('MarkdownCell', () => {
    const contentFactory = NBTestUtils.createBaseCellFactory();
    const model = new MarkdownCellModel({});

    describe('#constructor()', () => {
      it('should create a markdown cell widget', () => {
        const widget = new MarkdownCell({ model, rendermime, contentFactory });
        widget.initializeState();
        expect(widget).toBeInstanceOf(MarkdownCell);
      });

      it('should accept a custom contentFactory', () => {
        const widget = new MarkdownCell({ model, rendermime, contentFactory });
        widget.initializeState();
        expect(widget).toBeInstanceOf(MarkdownCell);
      });

      it('should set the default mimetype to text/x-ipythongfm', () => {
        const widget = new MarkdownCell({ model, rendermime, contentFactory });
        widget.initializeState();
        expect(widget.model.mimeType).toEqual('text/x-ipythongfm');
      });
    });

    describe('#rendered', () => {
      it('should default to true', async () => {
        const widget = new MarkdownCell({ model, rendermime, contentFactory });
        widget.initializeState();
        Widget.attach(widget, document.body);
        expect(widget.rendered).toEqual(true);
        await framePromise();
        expect(widget.node.classList.contains(RENDERED_CLASS)).toEqual(true);
      });

      it('should unrender the widget', async () => {
        const widget = new MarkdownCell({ model, rendermime, contentFactory });
        widget.initializeState();
        Widget.attach(widget, document.body);
        widget.rendered = false;
        await framePromise();
        expect(widget.node.classList.contains(RENDERED_CLASS)).toEqual(false);
        widget.dispose();
      });
    });

    describe('#dispose()', () => {
      it('should dispose of the resources held by the widget', () => {
        const widget = new MarkdownCell({ model, rendermime, contentFactory });
        widget.initializeState();
        widget.dispose();
        expect(widget.isDisposed).toEqual(true);
      });

      it('should be safe to call multiple times', () => {
        const widget = new MarkdownCell({ model, rendermime, contentFactory });
        widget.initializeState();
        widget.dispose();
        widget.dispose();
        expect(widget.isDisposed).toEqual(true);
      });
    });

    describe('#onUpdateRequest()', () => {
      it('should update the widget', () => {
        const widget = new LogMarkdownCell({
          model,
          rendermime,
          contentFactory
        }).initializeState();
        expect(widget.methods).not.toContain('onUpdateRequest');
        MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
        expect(widget.methods).toContain('onUpdateRequest');
      });
    });
  });

  describe('RawCell', () => {
    const contentFactory = NBTestUtils.createBaseCellFactory();

    describe('#constructor()', () => {
      it('should create a raw cell widget', () => {
        const model = new RawCellModel({});
        const widget = new RawCell({ model, contentFactory }).initializeState();
        expect(widget).toBeInstanceOf(RawCell);
      });
    });
  });

  describe('CellHeader', () => {
    describe('#constructor()', () => {
      it('should create a new cell header', () => {
        expect(new CellHeader()).toBeInstanceOf(CellHeader);
      });
    });
  });

  describe('CellFooter', () => {
    describe('#constructor()', () => {
      it('should create a new cell footer', () => {
        expect(new CellFooter()).toBeInstanceOf(CellFooter);
      });
    });
  });
});
