<commit_before>/* See license.txt for terms of usage */

define([
    "firebug/firebug",
    "firebug/lib/object",
    "firebug/lib/locale",
    "firebug/lib/events",
    "firebug/lib/dom",
    "firebug/lib/domplate",
    "firebug/css/selectorEditor",
],
function(Firebug, Obj, Locale, Events, Dom, Domplate, SelectorEditor) {
with (Domplate) {

// ********************************************************************************************* //
// Constants

const Cc = Components.classes;
const Ci = Components.interfaces;

const prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch2);

// ********************************************************************************************* //
// CSS Selector Panel

/**
 * @panel Selector side panel displaying HTML elements for the current selector,
 * either from the CSS main panel or user entry
 */
function SelectorPanel() {}
SelectorPanel.prototype = Obj.extend(Firebug.Panel,
/** @lends SelectorPanel */
{
    name: "selector",
    parentPanel: "stylesheet",
    title: Locale.$STR("css.selector.Selection"),
    editable: true,

    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
    // Initialization

    initialize: function(context, doc)
    {
        Firebug.Panel.initialize.apply(this, arguments);
    },

    initializeNode: function(oldPanelNode)
    {
        Firebug.Panel.initializeNode.apply(this, arguments);

        this.setSelection = Obj.bind(this.setSelection, this);
        this.clearSelection = Obj.bind(this.clearSelection, this);
        this.lockSelection = Obj.bind(this.lockSelection, this);

        var panelNode = this.mainPanel.panelNode;
        Events.addEventListener(panelNode, "mouseover", this.setSelection, false);
        Events.addEventListener(panelNode, "mouseout", this.clearSelection, false);
        Events.addEventListener(panelNode, "mousedown", this.lockSelection, false);
    },

    destroyNode: function()
    {
        var panelNode = this.mainPanel.panelNode;
        Events.removeEventListener(panelNode, "mouseover", this.setSelection, false);
        Events.removeEventListener(panelNode, "mouseout", this.clearSelection, false);
        Events.removeEventListener(panelNode, "mousedown", this.lockSelection, false);

        Firebug.Panel.destroyNode.apply(this, arguments);
    },

    show: function(state)
    {
        Firebug.Panel.show.apply(this, arguments);

        this.refresh();
    },

    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

    getCSSStyleRule: function(event)
    {
        var object = Firebug.getRepObject(event.target);

        if (object && (object instanceof window.CSSStyleRule))
            return object;
    },

    getCSSRuleElement: function(element)
    {
        while (element && !element.classList.contains("cssRule"))
            element = element.parentNode;

        return element;
    },

    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
    // Selection

    setSelection: function(event)
    {
        var rule = this.getCSSStyleRule(event);

        if (rule)
        {
            // then we have entered a rule element
            var ruleElement = this.getCSSRuleElement(event.target);
            if (ruleElement && ruleElement !== this.lockedElement)
                ruleElement.classList.add("selectedSelectorRule");

            this.selection = rule;
            this.rebuild();
        }
    },

    clearSelection: function(event)
    {
        if (this.selection !== this.lockedSelection)
        {
            this.selection = this.lockedSelection;
            this.rebuild();
        }

        var rule = this.getCSSStyleRule(event);
        if (rule)
        {
            // then we are leaving a rule element that we may have highlighted.
            var ruleElement = this.getCSSRuleElement(event.target);
            if (ruleElement)
                ruleElement.classList.remove("selectedSelectorRule");
        }
    },

    lockSelection: function(event)
    {
        var rule = this.getCSSStyleRule(event);
        if (rule)
        {
            if (this.lockedElement)
                this.lockedElement.classList.remove("lockedSelectorRule");

            this.lockedElement = this.getCSSRuleElement(event.target);

            if (this.lockedElement)
            {
                this.lockedElement.classList.add("lockedSelectorRule");
                this.lockedElement.classList.remove("selectedSelectorRule");
            }

            this.lockedSelection = rule;
        }
    },

    hide: function()
    {
        Firebug.Panel.hide.apply(this, arguments);
    },

    refresh: function()
    {
        var root = this.context.window.document.documentElement;
        this.selection = this.mainPanel.selection;

        // Use trial selector if there is no selection in the CSS panel.
        if (!this.selection)
            this.selection = this.trialSelector;

        this.rebuild(true);
    },

    /**
     * returns an array of Elements matched from selector
     */
    getSelectedElements: function(selectorText)
    {
        var selections = Firebug.currentContext.window.document.querySelectorAll(selectorText);

        // For some reason the return value of querySelectorAll()
        // is not recognized as a NodeList anymore since Firefox 10.0.
        // See issue 5442.
        if (selections)
        {
            var elements = [];
            for (var i=0; i<selections.length; i++)
                elements.push(selections[i]);

            return elements;
        }
        else
        {
            throw new Error("Selection Failed: " + selections);
        }
    },

    /**
     * Build content of the panel. The basic layout of the panel is generated by
     * {@link SelectorTemplate} template.
     */
    rebuild: function()
    {
        if (this.selection)
        {
            try
            {
                var selectorText;

                if (this.selection instanceof window.CSSStyleRule)
                    selectorText = this.selection.selectorText;
                else
                    selectorText = this.selection;

                var elements = this.getSelectedElements(selectorText);
                if (elements && elements.length != 0)
                {
                    SelectorTemplate.tag.replace({object: elements}, this.panelNode);
                    this.showTrialSelector(this.trialSelector);
                    return;
                }
            }
            catch (e)
            {
                var table = SelectorTemplate.tag.replace({object: []}, this.panelNode);
                var tbody = table.lastChild;

                WarningTemplate.selectErrorTag.insertRows({object: e}, tbody.lastChild);
                WarningTemplate.selectErrorTextTag.insertRows({object: e}, tbody.lastChild);

                this.showTrialSelector(this.trialSelector);
                return;
            }
        }

        var table = SelectorTemplate.tag.replace({object: []}, this.panelNode);
        var tbody = table.lastChild;

        if (this.trialSelector)
        {
            WarningTemplate.noSelectionResultsTag.insertRows(
                {object: this.selection}, tbody.lastChild)
        }
        else
        {
            WarningTemplate.noSelectionTag.insertRows(
                {object: this.selection}, tbody.lastChild);
        }

        this.showTrialSelector(this.trialSelector);
    },

    getObjectPath: function(object)
    {
        if (FBTrace.DBG_SELECTOR)
            FBTrace.sysout("css.selector.getObjectPath NOOP", object);
    },

    supportsObject: function(object)
    {
        return 0;
    },

    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

    tryASelector:function(element)
    {
        if (!this.trialSelector)
            this.trialSelector = this.selection ? this.selection.selectorText : "";

        this.editProperty(element, this.trialSelector);
    },

    editProperty: function(row, editValue)
    {
        Firebug.Editor.startEditing(row, editValue);
    },

    getEditor: function(target, value)
    {
        if (!this.editor)
            this.editor = new SelectorEditor(this);

        return this.editor;
    },

    setTrialSelector: function(target, value)
    {
        if (this.lockedElement)
            this.lockedElement.classList.remove("lockedSelectorRule");

        this.trialSelector = value;
        this.selection = this.trialSelector;
        this.lockedElement = target;
        this.lockedSelection = this.selection;
        this.rebuild();
    },

    showTrialSelector: function(trialSelector)
    {
        var show = trialSelector ? true : false;
        Dom.collapse(this.document.getElementById("trialHint"), show);

        var trialSelectorDiv = this.document.getElementById("trialSelector");
        trialSelectorDiv.textContent = trialSelector;
        Dom.collapse(trialSelectorDiv, !show);
    },
});

// ********************************************************************************************* //

var BaseRep = domplate(Firebug.Rep,
{
    // xxxHonza: shouldn't this be in Firebug.Rep?
    getNaturalTag: function(value)
    {
        var rep = Firebug.getRep(value);
        var tag = rep.shortTag ? rep.shortTag : rep.tag;
        return tag;
    }
});

// ********************************************************************************************* //

var TrialRow =
    TR({"class": "watchNewRow", level: 0, onclick: "$onClickEditor"},
        TD({"class": "watchEditCell", colspan: 3},
            DIV({"class": "watchEditBox a11yFocusNoTab", "id": "trialHint",
                role: "button", "tabindex" : "0",
                "aria-label": Locale.$STR("a11y.labels.press enter to add new selector")},
                Locale.$STR("css.selector.TryASelector")
            ),
            DIV({"class": "trialSelector", "id": "trialSelector"}, "")
        )
    );

// ********************************************************************************************* //

/**
 * @domplate: Template for basic layout of the {@link SelectorPanel} panel.
 */
var SelectorTemplate = domplate(BaseRep,
{
    // object will be array of elements CSSStyleRule
    tag:
        TABLE({"class": "cssSelectionTable", cellpadding: 0, cellspacing: 0},
            TBODY({"class": "cssSelectionTBody"},
                TrialRow,
                FOR("element", "$object",
                    TR({"class": "selectionElementRow", _repObject: "$element"},
                        TD({"class": "selectionElement"},
                            TAG( "$element|getNaturalTag", {object: "$element"})
                        )
                    )
                )
            )
        ),

    onClickEditor: function(event)
    {
        var tr = event.currentTarget;
        var panel = Firebug.getElementPanel(tr);
        panel.tryASelector(tr);
    },
});

// ********************************************************************************************* //

var WarningTemplate = domplate(Firebug.Rep,
{
    noSelectionTag:
        TR({"class": "selectorWarning"},
            TD({"class": "selectionElement"}, Locale.$STR("css.selector.noSelection"))
        ),

    noSelectionResultsTag:
        TR({"class": "selectorWarning"},
            TD({"class": "selectionElement"}, Locale.$STR("css.selector.noSelectionResults"))
        ),

    selectErrorTag:
        TR({"class": "selectorWarning"},
            TD({"class": "selectionElement"}, Locale.$STR("css.selector.selectorError"))
        ),

    selectErrorTextTag:
        TR({"class": "selectorWarning"},
            TD({"class": "selectionErrorText selectionElement"},
                SPAN("$object|getErrorMessage")
            )
        ),

    getErrorMessage: function(object)
    {
        if (object.message)
            return object.message;

        return Locale.$STR("css.selector.unknownErrorMessage");
    }
});

// ********************************************************************************************* //
// Registration

Firebug.registerStylesheet("chrome://firebug/skin/selector.css");
Firebug.registerPanel(SelectorPanel);

return SelectorPanel;

// ********************************************************************************************* //
}});
<commit_msg>CSS Selector: remove selection and hover-preview (see issue 5931)<commit_after>/* See license.txt for terms of usage */

define([
    "firebug/firebug",
    "firebug/lib/object",
    "firebug/lib/locale",
    "firebug/lib/events",
    "firebug/lib/dom",
    "firebug/lib/domplate",
    "firebug/css/selectorEditor",
],
function(Firebug, Obj, Locale, Events, Dom, Domplate, SelectorEditor) {
with (Domplate) {

// ********************************************************************************************* //
// Constants

const Cc = Components.classes;
const Ci = Components.interfaces;

const prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch2);

// ********************************************************************************************* //
// CSS Selector Panel

/**
 * @panel Selector side panel displaying HTML elements for the current selector,
 * either from the CSS main panel or user entry
 */
function SelectorPanel() {}
SelectorPanel.prototype = Obj.extend(Firebug.Panel,
/** @lends SelectorPanel */
{
    name: "selector",
    parentPanel: "stylesheet",
    title: Locale.$STR("css.selector.Selection"),
    editable: true,

    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
    // Initialization

    initialize: function(context, doc)
    {
        Firebug.Panel.initialize.apply(this, arguments);
    },

    initializeNode: function(oldPanelNode)
    {
        Firebug.Panel.initializeNode.apply(this, arguments);

        this.setSelection = Obj.bind(this.setSelection, this);
        this.clearSelection = Obj.bind(this.clearSelection, this);
        this.lockSelection = Obj.bind(this.lockSelection, this);

        var panelNode = this.mainPanel.panelNode;
        // See: http://code.google.com/p/fbug/issues/detail?id=5931
        //Events.addEventListener(panelNode, "mouseover", this.setSelection, false);
        //Events.addEventListener(panelNode, "mouseout", this.clearSelection, false);
        //Events.addEventListener(panelNode, "mousedown", this.lockSelection, false);
    },

    destroyNode: function()
    {
        var panelNode = this.mainPanel.panelNode;
        //Events.removeEventListener(panelNode, "mouseover", this.setSelection, false);
        //Events.removeEventListener(panelNode, "mouseout", this.clearSelection, false);
        //Events.removeEventListener(panelNode, "mousedown", this.lockSelection, false);

        Firebug.Panel.destroyNode.apply(this, arguments);
    },

    show: function(state)
    {
        Firebug.Panel.show.apply(this, arguments);

        this.refresh();
    },

    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

    getCSSStyleRule: function(event)
    {
        var object = Firebug.getRepObject(event.target);

        if (object && (object instanceof window.CSSStyleRule))
            return object;
    },

    getCSSRuleElement: function(element)
    {
        while (element && !element.classList.contains("cssRule"))
            element = element.parentNode;

        return element;
    },

    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
    // Selection

    setSelection: function(event)
    {
        var rule = this.getCSSStyleRule(event);

        if (rule)
        {
            // then we have entered a rule element
            var ruleElement = this.getCSSRuleElement(event.target);
            if (ruleElement && ruleElement !== this.lockedElement)
                ruleElement.classList.add("selectedSelectorRule");

            this.selection = rule;
            this.rebuild();
        }
    },

    clearSelection: function(event)
    {
        if (this.selection !== this.lockedSelection)
        {
            this.selection = this.lockedSelection;
            this.rebuild();
        }

        var rule = this.getCSSStyleRule(event);
        if (rule)
        {
            // then we are leaving a rule element that we may have highlighted.
            var ruleElement = this.getCSSRuleElement(event.target);
            if (ruleElement)
                ruleElement.classList.remove("selectedSelectorRule");
        }
    },

    lockSelection: function(event)
    {
        var rule = this.getCSSStyleRule(event);
        if (rule)
        {
            if (this.lockedElement)
                this.lockedElement.classList.remove("lockedSelectorRule");

            this.lockedElement = this.getCSSRuleElement(event.target);

            if (this.lockedElement)
            {
                this.lockedElement.classList.add("lockedSelectorRule");
                this.lockedElement.classList.remove("selectedSelectorRule");
            }

            this.lockedSelection = rule;
        }
    },

    hide: function()
    {
        Firebug.Panel.hide.apply(this, arguments);
    },

    refresh: function()
    {
        var root = this.context.window.document.documentElement;
        this.selection = this.mainPanel.selection;

        // Use trial selector if there is no selection in the CSS panel.
        if (!this.selection)
            this.selection = this.trialSelector;

        this.rebuild(true);
    },

    /**
     * returns an array of Elements matched from selector
     */
    getSelectedElements: function(selectorText)
    {
        var selections = Firebug.currentContext.window.document.querySelectorAll(selectorText);

        // For some reason the return value of querySelectorAll()
        // is not recognized as a NodeList anymore since Firefox 10.0.
        // See issue 5442.
        if (selections)
        {
            var elements = [];
            for (var i=0; i<selections.length; i++)
                elements.push(selections[i]);

            return elements;
        }
        else
        {
            throw new Error("Selection Failed: " + selections);
        }
    },

    /**
     * Build content of the panel. The basic layout of the panel is generated by
     * {@link SelectorTemplate} template.
     */
    rebuild: function()
    {
        if (this.selection)
        {
            try
            {
                var selectorText;

                if (this.selection instanceof window.CSSStyleRule)
                    selectorText = this.selection.selectorText;
                else
                    selectorText = this.selection;

                var elements = this.getSelectedElements(selectorText);
                if (elements && elements.length != 0)
                {
                    SelectorTemplate.tag.replace({object: elements}, this.panelNode);
                    this.showTrialSelector(this.trialSelector);
                    return;
                }
            }
            catch (e)
            {
                var table = SelectorTemplate.tag.replace({object: []}, this.panelNode);
                var tbody = table.lastChild;

                WarningTemplate.selectErrorTag.insertRows({object: e}, tbody.lastChild);
                WarningTemplate.selectErrorTextTag.insertRows({object: e}, tbody.lastChild);

                this.showTrialSelector(this.trialSelector);
                return;
            }
        }

        var table = SelectorTemplate.tag.replace({object: []}, this.panelNode);
        var tbody = table.lastChild;

        if (this.trialSelector)
        {
            WarningTemplate.noSelectionResultsTag.insertRows(
                {object: this.selection}, tbody.lastChild)
        }
        else
        {
            WarningTemplate.noSelectionTag.insertRows(
                {object: this.selection}, tbody.lastChild);
        }

        this.showTrialSelector(this.trialSelector);
    },

    getObjectPath: function(object)
    {
        if (FBTrace.DBG_SELECTOR)
            FBTrace.sysout("css.selector.getObjectPath NOOP", object);
    },

    supportsObject: function(object)
    {
        return 0;
    },

    // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //

    tryASelector:function(element)
    {
        if (!this.trialSelector)
            this.trialSelector = this.selection ? this.selection.selectorText : "";

        this.editProperty(element, this.trialSelector);
    },

    editProperty: function(row, editValue)
    {
        Firebug.Editor.startEditing(row, editValue);
    },

    getEditor: function(target, value)
    {
        if (!this.editor)
            this.editor = new SelectorEditor(this);

        return this.editor;
    },

    setTrialSelector: function(target, value)
    {
        if (this.lockedElement)
            this.lockedElement.classList.remove("lockedSelectorRule");

        this.trialSelector = value;
        this.selection = this.trialSelector;
        this.lockedElement = target;
        this.lockedSelection = this.selection;
        this.rebuild();
    },

    showTrialSelector: function(trialSelector)
    {
        var show = trialSelector ? true : false;
        Dom.collapse(this.document.getElementById("trialHint"), show);

        var trialSelectorDiv = this.document.getElementById("trialSelector");
        trialSelectorDiv.textContent = trialSelector;
        Dom.collapse(trialSelectorDiv, !show);
    },
});

// ********************************************************************************************* //

var BaseRep = domplate(Firebug.Rep,
{
    // xxxHonza: shouldn't this be in Firebug.Rep?
    getNaturalTag: function(value)
    {
        var rep = Firebug.getRep(value);
        var tag = rep.shortTag ? rep.shortTag : rep.tag;
        return tag;
    }
});

// ********************************************************************************************* //

var TrialRow =
    TR({"class": "watchNewRow", level: 0, onclick: "$onClickEditor"},
        TD({"class": "watchEditCell", colspan: 3},
            DIV({"class": "watchEditBox a11yFocusNoTab", "id": "trialHint",
                role: "button", "tabindex" : "0",
                "aria-label": Locale.$STR("a11y.labels.press enter to add new selector")},
                Locale.$STR("css.selector.TryASelector")
            ),
            DIV({"class": "trialSelector", "id": "trialSelector"}, "")
        )
    );

// ********************************************************************************************* //

/**
 * @domplate: Template for basic layout of the {@link SelectorPanel} panel.
 */
var SelectorTemplate = domplate(BaseRep,
{
    // object will be array of elements CSSStyleRule
    tag:
        TABLE({"class": "cssSelectionTable", cellpadding: 0, cellspacing: 0},
            TBODY({"class": "cssSelectionTBody"},
                TrialRow,
                FOR("element", "$object",
                    TR({"class": "selectionElementRow", _repObject: "$element"},
                        TD({"class": "selectionElement"},
                            TAG( "$element|getNaturalTag", {object: "$element"})
                        )
                    )
                )
            )
        ),

    onClickEditor: function(event)
    {
        var tr = event.currentTarget;
        var panel = Firebug.getElementPanel(tr);
        panel.tryASelector(tr);
    },
});

// ********************************************************************************************* //

var WarningTemplate = domplate(Firebug.Rep,
{
    noSelectionTag:
        TR({"class": "selectorWarning"},
            TD({"class": "selectionElement"}, Locale.$STR("css.selector.noSelection"))
        ),

    noSelectionResultsTag:
        TR({"class": "selectorWarning"},
            TD({"class": "selectionElement"}, Locale.$STR("css.selector.noSelectionResults"))
        ),

    selectErrorTag:
        TR({"class": "selectorWarning"},
            TD({"class": "selectionElement"}, Locale.$STR("css.selector.selectorError"))
        ),

    selectErrorTextTag:
        TR({"class": "selectorWarning"},
            TD({"class": "selectionErrorText selectionElement"},
                SPAN("$object|getErrorMessage")
            )
        ),

    getErrorMessage: function(object)
    {
        if (object.message)
            return object.message;

        return Locale.$STR("css.selector.unknownErrorMessage");
    }
});

// ********************************************************************************************* //
// Registration

Firebug.registerStylesheet("chrome://firebug/skin/selector.css");
Firebug.registerPanel(SelectorPanel);

return SelectorPanel;

// ********************************************************************************************* //
}});
<|endoftext|>


<commit_before>/* eslint-env jest */
import moment from 'moment'
import MockDate from 'mockdate'
import createCampaignConfiguration from '../createCampaignConfiguration'
import callRedis from '../../../utils/redis'
import CharityModel from '../../charities/CharityModel'
import { getMockUserContext } from '../../test-utils'

jest.mock('../../../utils/redis')
jest.mock('../../charities/CharityModel')
jest.mock('../../../utils/logger')

const mockNow = '2020-05-02T13:59:58.000Z'

beforeEach(() => {
  MockDate.set(moment(mockNow))
})

afterEach(() => {
  jest.clearAllMocks()
  MockDate.reset()
  CharityModel.get.mockReturnValue(null)
})

// Get an example of campaign config input.
const getMockCampaignConfigInput = () => ({
  campaignId: 'myCoolCampaign',
  charityId: '5ae19457-a8a7-4c28-89ee-52179a2fb966',
  content: {
    titleMarkdown: '## Some title',
    descriptionMarkdown: '#### A description goes here.',
  },
  countNewUsers: true,
  countTabsOpened: true,
  endContent: {
    titleMarkdown: '## Another title',
    descriptionMarkdown: '#### Another description goes here.',
  },
  goal: {
    impactUnitSingular: 'Heart',
    impactUnitPlural: 'Hearts',
    impactVerbPastTense: 'raised',
    targetNumber: 10e6,
  },
  showCountdownTimer: true,
  showHeartsDonationButton: true,
  showProgressBar: true,
  time: {
    start: '2020-05-01T18:00:00.000Z',
    end: '2020-05-05T18:00:00.000Z',
  },
})

describe('createCampaignConfiguration', () => {
  it('returns an object with the expected required properties', () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    expect(createCampaignConfiguration(mockCampaignInput)).toEqual({
      campaignId: 'myCoolCampaign',
      content: {
        titleMarkdown: '## Some title',
        descriptionMarkdown: '#### A description goes here.',
      },
      endContent: {
        titleMarkdown: '## Another title',
        descriptionMarkdown: '#### Another description goes here.',
      },
      getCharityData: expect.any(Function),
      goal: {
        getCurrentNumber: expect.any(Function),
        impactUnitSingular: 'Heart',
        impactUnitPlural: 'Hearts',
        impactVerbPastTense: 'raised',
        targetNumber: 10e6,
      },
      incrementNewUserCount: expect.any(Function),
      incrementTabCount: expect.any(Function),
      isActive: expect.any(Function),
      showCountdownTimer: true,
      showHeartsDonationButton: true,
      showProgressBar: true,
      time: {
        start: '2020-05-01T18:00:00.000Z',
        end: '2020-05-05T18:00:00.000Z',
      },
    })
  })

  it('calls Redis as expected when calling incrementNewUserCount and countNewUsers is true', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      campaignId: 'myFunCampaign',
      countNewUsers: true,
    })
    await campaignConfig.incrementNewUserCount()
    expect(callRedis).toHaveBeenCalledWith({
      operation: 'INCR',
      key: 'campaign:myFunCampaign:newUsers',
    })
  })

  it('does not call Redis when calling incrementNewUserCount and countNewUsers is false', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      campaignId: 'myFunCampaign',
      countNewUsers: false,
    })
    await campaignConfig.incrementNewUserCount()
    expect(callRedis).not.toHaveBeenCalled()
  })

  it('does not call Redis when calling incrementNewUserCount and the campaign is no longer active', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      campaignId: 'myFunCampaign',
      countNewUsers: true,
      time: {
        ...mockCampaignInput.time,
        start: '2020-04-21T18:00:00.000Z',
        end: '2020-04-28T18:00:00.000Z', // campaign has ended
      },
    })
    await campaignConfig.incrementNewUserCount()
    expect(callRedis).not.toHaveBeenCalled()
  })

  it('calls Redis as expected when calling incrementTabCount and countTabsOpened is true', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      campaignId: 'myFunCampaign',
      countTabsOpened: true,
    })
    await campaignConfig.incrementTabCount()
    expect(callRedis).toHaveBeenCalledWith({
      operation: 'INCR',
      key: 'campaign:myFunCampaign:tabsOpened',
    })
  })

  it('does not call Redis when calling incrementTabCount and countTabsOpened is false', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      campaignId: 'myFunCampaign',
      countTabsOpened: false,
    })
    await campaignConfig.incrementTabCount()
    expect(callRedis).not.toHaveBeenCalled()
  })

  it('does not call Redis when calling incrementTabCount and the campaign is no longer active', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      campaignId: 'myFunCampaign',
      countTabsOpened: true,
      time: {
        ...mockCampaignInput.time,
        start: '2020-04-21T18:00:00.000Z',
        end: '2020-04-28T18:00:00.000Z', // campaign has ended
      },
    })
    await campaignConfig.incrementTabCount()
    expect(callRedis).not.toHaveBeenCalled()
  })

  it('fetches the charity as expected when calling getCharityData', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      charityId: 'some-charity-id',
    })
    const mockUserContext = getMockUserContext()
    await campaignConfig.getCharityData(mockUserContext)
    expect(CharityModel.get).toHaveBeenCalledWith(
      mockUserContext,
      'some-charity-id'
    )
  })

  it('returns the expected charity data when calling getCharityData', async () => {
    expect.assertions(1)
    CharityModel.get.mockReturnValue({
      id: 'some-charity-id',
      foo: 'bar',
    })
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      charityId: 'some-charity-id',
    })
    const mockUserContext = getMockUserContext()
    const charityData = await campaignConfig.getCharityData(mockUserContext)
    expect(charityData).toEqual({
      id: 'some-charity-id',
      foo: 'bar',
    })
  })

  it('returns null charity data when no charityId is defined in the config', async () => {
    expect.assertions(1)
    CharityModel.get.mockReturnValue({
      id: 'some-charity-id',
      foo: 'bar',
    })
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      charityId: null, // no charity ID defined
    })
    const mockUserContext = getMockUserContext()
    const charityData = await campaignConfig.getCharityData(mockUserContext)
    expect(charityData).toBeNull()
  })

  it('throws if fetching the charity thows', async () => {
    expect.assertions(1)
    const mockErr = new Error('The database has failed us.')
    CharityModel.get.mockImplementation(() => {
      throw mockErr
    })
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      charityId: 'some-charity-id',
    })
    const mockUserContext = getMockUserContext()
    await expect(
      campaignConfig.getCharityData(mockUserContext)
    ).rejects.toEqual(mockErr)
  })
})
<commit_msg>Add tests<commit_after>/* eslint-env jest */
import moment from 'moment'
import MockDate from 'mockdate'
import createCampaignConfiguration from '../createCampaignConfiguration'
import callRedis from '../../../utils/redis'
import CharityModel from '../../charities/CharityModel'
import { getMockUserContext } from '../../test-utils'

jest.mock('../../../utils/redis')
jest.mock('../../charities/CharityModel')
jest.mock('../../../utils/logger')

const mockNow = '2020-05-02T13:59:58.000Z'

beforeEach(() => {
  MockDate.set(moment(mockNow))
})

afterEach(() => {
  jest.clearAllMocks()
  MockDate.reset()
  CharityModel.get.mockReturnValue(null)
})

// Get an example of campaign config input.
const getMockCampaignConfigInput = () => ({
  campaignId: 'myCoolCampaign',
  charityId: '5ae19457-a8a7-4c28-89ee-52179a2fb966',
  content: {
    titleMarkdown: '## Some title',
    descriptionMarkdown: '#### A description goes here.',
  },
  countNewUsers: true,
  countTabsOpened: true,
  endContent: {
    titleMarkdown: '## Another title',
    descriptionMarkdown: '#### Another description goes here.',
  },
  goal: {
    impactUnitSingular: 'Heart',
    impactUnitPlural: 'Hearts',
    impactVerbPastTense: 'raised',
    targetNumber: 10e6,
  },
  showCountdownTimer: true,
  showHeartsDonationButton: true,
  showProgressBar: true,
  time: {
    start: '2020-05-01T18:00:00.000Z',
    end: '2020-05-05T18:00:00.000Z',
  },
})

describe('createCampaignConfiguration', () => {
  it('returns an object with the expected required properties', () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    expect(createCampaignConfiguration(mockCampaignInput)).toEqual({
      campaignId: 'myCoolCampaign',
      content: {
        titleMarkdown: '## Some title',
        descriptionMarkdown: '#### A description goes here.',
      },
      endContent: {
        titleMarkdown: '## Another title',
        descriptionMarkdown: '#### Another description goes here.',
      },
      getCharityData: expect.any(Function),
      goal: {
        getCurrentNumber: expect.any(Function),
        impactUnitSingular: 'Heart',
        impactUnitPlural: 'Hearts',
        impactVerbPastTense: 'raised',
        targetNumber: 10e6,
      },
      incrementNewUserCount: expect.any(Function),
      incrementTabCount: expect.any(Function),
      isActive: expect.any(Function),
      showCountdownTimer: true,
      showHeartsDonationButton: true,
      showProgressBar: true,
      time: {
        start: '2020-05-01T18:00:00.000Z',
        end: '2020-05-05T18:00:00.000Z',
      },
    })
  })

  it('calls Redis as expected when calling incrementNewUserCount and countNewUsers is true', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      campaignId: 'myFunCampaign',
      countNewUsers: true,
    })
    await campaignConfig.incrementNewUserCount()
    expect(callRedis).toHaveBeenCalledWith({
      operation: 'INCR',
      key: 'campaign:myFunCampaign:newUsers',
    })
  })

  it('does not call Redis when calling incrementNewUserCount and countNewUsers is false', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      campaignId: 'myFunCampaign',
      countNewUsers: false,
    })
    await campaignConfig.incrementNewUserCount()
    expect(callRedis).not.toHaveBeenCalled()
  })

  it('does not call Redis when calling incrementNewUserCount and the campaign is no longer active', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      campaignId: 'myFunCampaign',
      countNewUsers: true,
      time: {
        ...mockCampaignInput.time,
        start: '2020-04-21T18:00:00.000Z',
        end: '2020-04-28T18:00:00.000Z', // campaign has ended
      },
    })
    await campaignConfig.incrementNewUserCount()
    expect(callRedis).not.toHaveBeenCalled()
  })

  it('calls Redis as expected when calling incrementTabCount and countTabsOpened is true', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      campaignId: 'myFunCampaign',
      countTabsOpened: true,
    })
    await campaignConfig.incrementTabCount()
    expect(callRedis).toHaveBeenCalledWith({
      operation: 'INCR',
      key: 'campaign:myFunCampaign:tabsOpened',
    })
  })

  it('does not call Redis when calling incrementTabCount and countTabsOpened is false', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      campaignId: 'myFunCampaign',
      countTabsOpened: false,
    })
    await campaignConfig.incrementTabCount()
    expect(callRedis).not.toHaveBeenCalled()
  })

  it('does not call Redis when calling incrementTabCount and the campaign is no longer active', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      campaignId: 'myFunCampaign',
      countTabsOpened: true,
      time: {
        ...mockCampaignInput.time,
        start: '2020-04-21T18:00:00.000Z',
        end: '2020-04-28T18:00:00.000Z', // campaign has ended
      },
    })
    await campaignConfig.incrementTabCount()
    expect(callRedis).not.toHaveBeenCalled()
  })

  it('fetches the charity as expected when calling getCharityData', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      charityId: 'some-charity-id',
    })
    const mockUserContext = getMockUserContext()
    await campaignConfig.getCharityData(mockUserContext)
    expect(CharityModel.get).toHaveBeenCalledWith(
      mockUserContext,
      'some-charity-id'
    )
  })

  it('returns the expected charity data when calling getCharityData', async () => {
    expect.assertions(1)
    CharityModel.get.mockReturnValue({
      id: 'some-charity-id',
      foo: 'bar',
    })
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      charityId: 'some-charity-id',
    })
    const mockUserContext = getMockUserContext()
    const charityData = await campaignConfig.getCharityData(mockUserContext)
    expect(charityData).toEqual({
      id: 'some-charity-id',
      foo: 'bar',
    })
  })

  it('returns null charity data when no charityId is defined in the config', async () => {
    expect.assertions(1)
    CharityModel.get.mockReturnValue({
      id: 'some-charity-id',
      foo: 'bar',
    })
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      charityId: null, // no charity ID defined
    })
    const mockUserContext = getMockUserContext()
    const charityData = await campaignConfig.getCharityData(mockUserContext)
    expect(charityData).toBeNull()
  })

  it('throws if fetching the charity thows', async () => {
    expect.assertions(1)
    const mockErr = new Error('The database has failed us.')
    CharityModel.get.mockImplementation(() => {
      throw mockErr
    })
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      charityId: 'some-charity-id',
    })
    const mockUserContext = getMockUserContext()
    await expect(
      campaignConfig.getCharityData(mockUserContext)
    ).rejects.toEqual(mockErr)
  })

  it('isActive() returns true if the campaign is still active', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      campaignId: 'myFunCampaign',
      countTabsOpened: true,
      time: {
        ...mockCampaignInput.time,
        start: '2020-05-01T18:00:00.000Z',
        end: '2020-05-05T18:00:00.000Z', // campaign is still active
      },
    })
    expect(campaignConfig.isActive()).toBe(true)
  })

  it('isActive() returns false if the campaign is no longer active', async () => {
    expect.assertions(1)
    const mockCampaignInput = getMockCampaignConfigInput()
    const campaignConfig = createCampaignConfiguration({
      ...mockCampaignInput,
      campaignId: 'myFunCampaign',
      countTabsOpened: true,
      time: {
        ...mockCampaignInput.time,
        start: '2020-04-21T18:00:00.000Z',
        end: '2020-04-28T18:00:00.000Z', // campaign has ended
      },
    })
    expect(campaignConfig.isActive()).toBe(false)
  })
})
<|endoftext|>


<commit_before>var metas = document.getElementsByTagName('meta');
var i;
if (navigator.userAgent.match(/iPhone/i)) {
  for (i=0; i<metas.length; i++) {
    if (metas[i].name == "viewport") {
      metas[i].content = "width=device-width, minimum-scale=1.0, maximum-scale=1.0";
    }
  }
  document.addEventListener("gesturestart", gestureStart, false);
}
function gestureStart() {
  for (i=0; i<metas.length; i++) {
    if (metas[i].name == "viewport") {
      metas[i].content = "width=device-width, minimum-scale=0.25, maximum-scale=1.6";
    }
  }
}<commit_msg>Delete scale.fix.js<commit_after><|endoftext|>


<commit_before>$(function() {
    RED.i18n.init({},function() {
        var options = {
            messageMouseEnter: function(sourceId) {
                window.opener.postMessage({event:"mouseEnter",id:sourceId},'*');
            },
            messageMouseLeave: function(sourceId) {
                window.opener.postMessage({event:"mouseLeave",id:sourceId},'*');
            },
            messageSourceClick: function(sourceId, aliasId, path) {
                window.opener.postMessage({event:"mouseClick",id:sourceId, _alias: aliasId, path: path},'*');
            },
            clear: function() {
                window.opener.postMessage({event:"clear"},'*');
            }
        }

        var uiComponents = RED.debug.init(options);

        $(".red-ui-debug-window").append(uiComponents.content);

        window.addEventListener('message',function(evt) {
            if (evt.data.event === "message") {
                RED.debug.handleDebugMessage(evt.data.msg);
            } else if (evt.data.event === "workspaceChange") {
                RED.debug.refreshMessageList(evt.data.activeWorkspace);
            } else if (evt.data.event === "projectChange") {
                RED.debug.clearMessageList(true);
            }
        },false);
    })
});
<commit_msg>Set apiRootUrl for debug pop-out to load locales properly Fixes #2629, #2630<commit_after>$(function() {
    RED.i18n.init({apiRootUrl:"../../"},function() {
        var options = {
            messageMouseEnter: function(sourceId) {
                window.opener.postMessage({event:"mouseEnter",id:sourceId},'*');
            },
            messageMouseLeave: function(sourceId) {
                window.opener.postMessage({event:"mouseLeave",id:sourceId},'*');
            },
            messageSourceClick: function(sourceId, aliasId, path) {
                window.opener.postMessage({event:"mouseClick",id:sourceId, _alias: aliasId, path: path},'*');
            },
            clear: function() {
                window.opener.postMessage({event:"clear"},'*');
            }
        }

        var uiComponents = RED.debug.init(options);

        $(".red-ui-debug-window").append(uiComponents.content);

        window.addEventListener('message',function(evt) {
            if (evt.data.event === "message") {
                RED.debug.handleDebugMessage(evt.data.msg);
            } else if (evt.data.event === "workspaceChange") {
                RED.debug.refreshMessageList(evt.data.activeWorkspace);
            } else if (evt.data.event === "projectChange") {
                RED.debug.clearMessageList(true);
            }
        },false);
    })
});
<|endoftext|>


<commit_before>import { observer } from 'binary-common-utils/lib/observer';
import CustomApi from 'binary-common-utils/lib/customApi';
import { getToken } from 'binary-common-utils/lib/storageManager';
import _ from 'underscore';
import config from '../../common/const';
import PurchaseCtrl from './purchaseCtrl';
import _Symbol from './symbol';
import { expectNumber, expectBarrierOffset } from '../../common/expect';
import { RuntimeError } from '../../common/error';

const decorateTradeOptions = (tradeOption, otherOptions = {}) => {
  const option = {
    duration_unit: tradeOption.duration_unit,
    basis: tradeOption.basis,
    currency: tradeOption.currency,
    symbol: tradeOption.symbol,
    ...otherOptions,
  };
  option.duration = expectNumber('duration', tradeOption.duration, RuntimeError);
  option.amount = expectNumber('amount', tradeOption.amount, RuntimeError).toFixed(2);
  if ('prediction' in tradeOption) {
    option.barrier = expectNumber('prediction', tradeOption.prediction, RuntimeError);
  }
  if ('barrierOffset' in tradeOption) {
    option.barrier = expectBarrierOffset(tradeOption.barrierOffset, RuntimeError);
  }
  if ('secondBarrierOffset' in tradeOption) {
    option.barrier2 = expectBarrierOffset(tradeOption.secondBarrierOffset, RuntimeError);
  }
  return option;
};

export default class Bot {
  constructor(api = null) {
    this.ticks = [];
    this.candles = [];
    this.candleInterval = 60;
    this.running = false;
    this.currentToken = '';
    this.balanceStr = '';
    this.currentSymbol = '';
    this.unregisterOnFinish = [];
    this.totalProfit = 0;
    this.totalRuns = 0;
    this.totalStake = 0;
    this.totalPayout = 0;
    this.balance = 0;
    this.api = (api === null) ? new CustomApi() : api;
    this.symbol = new _Symbol(this.api);
    this.initPromise = this.symbol.initPromise;
  }
  start(token, tradeOption, strategy, duringPurchase, finish, sameTrade) {
    if (!this.running || sameTrade) {
      this.running = true;
      if (this.purchaseCtrl) {
        this.purchaseCtrl.destroy();
      }
      this.purchaseCtrl = new PurchaseCtrl(this.api, strategy, duringPurchase, finish);
      this.tradeOption = tradeOption;
      observer.emit('log.bot.start', {
        again: !!sameTrade,
      });
      const accountName = getToken(token).account_name;
      if (typeof amplitude !== 'undefined') {
        amplitude.getInstance().setUserId(accountName);
      }
      if (typeof trackJs !== 'undefined') {
        trackJs.configure({
          userId: accountName,
        });
      }
      if (sameTrade) {
        this.startTrading();
      } else {
        const promises = [];
        if (this.currentToken !== token) {
          promises.push(this.login(token));
        }
        if (!_.isEmpty(this.tradeOption)) {
          if (this.tradeOption.symbol !== this.currentSymbol) {
            promises.push(this.subscribeToTickHistory());
            promises.push(this.subscribeToCandles());
          } else if (this.tradeOption.candleInterval !== this.candleInterval) {
            this.candleInterval = this.tradeOption.candleInterval;
            promises.push(this.subscribeToCandles());
          }
        }
        Promise.all(promises).then(() => {
          this.startTrading();
        });
      }
    }
  }
  login(token) {
    return new Promise((resolve) => {
      const apiAuthorize = () => {
        observer.emit('log.bot.login', {
          lastToken: this.currentToken,
          token,
        });
        this.currentToken = token;
        this.subscribeToBalance();
        this.observeStreams();
        resolve();
      };
      observer.register('api.authorize', apiAuthorize, true, {
        type: 'authorize',
        unregister: [['api.authorize', apiAuthorize]],
      }, true);
      this.api.authorize(token);
    });
  }
  setTradeOptions() {
    if (!_.isEmpty(this.tradeOption)) {
      this.pip = this.symbol.activeSymbols.getSymbols()[this.tradeOption.symbol].pip;
      const opposites = config.opposites[this.tradeOption.condition];
      this.candleInterval = this.tradeOption.candleInterval;
      this.tradeOptions = [];
      for (const key of Object.keys(opposites)) {
        this.tradeOptions.push(decorateTradeOptions(this.tradeOption, {
          contract_type: Object.keys(opposites[key])[0],
        }));
      }
    } else {
      this.tradeOptions = [];
    }
  }
  subscribeToBalance() {
    const apiBalance = (balance) => {
      this.balance = balance.balance;
      this.balanceStr = `${Number(balance.balance).toFixed(2)} ${balance.currency}`;
      observer.emit('bot.tradeInfo', {
        balance: this.balanceStr,
      });
    };
    observer.register('api.balance', apiBalance, false, {
      type: 'balance',
      unregister: [['api.balance', apiBalance]],
    });
    this.api.balance();
  }
  subscribeToCandles() {
    return new Promise((resolve) => {
      const apiCandles = (candles) => {
        this.candles = candles;
        resolve();
      };
      observer.register('api.candles', apiCandles, true, {
        type: 'candles',
        unregister: ['api.ohlc', 'api.candles', 'api.tick', 'bot.tickUpdate'],
      });
      this.api.originalApi.unsubscribeFromAllCandles().then(() => 0, () => 0);
      this.api.history(this.tradeOption.symbol, {
        end: 'latest',
        count: 600,
        granularity: this.candleInterval,
        style: 'candles',
        subscribe: 1,
      });
    });
  }
  subscribeToTickHistory() {
    return new Promise((resolve) => {
      const apiHistory = (history) => {
        this.currentSymbol = this.tradeOption.symbol;
        this.ticks = history;
        resolve();
      };
      observer.register('api.history', apiHistory, true, {
        type: 'history',
        unregister: [['api.history', apiHistory], 'api.tick',
          'bot.tickUpdate', 'api.ohlc', 'api.candles'],
      }, true);
      this.api.originalApi.unsubscribeFromAllTicks().then(() => 0, () => 0);
      this.api.history(this.tradeOption.symbol, {
        end: 'latest',
        count: 600,
        subscribe: 1,
      });
    });
  }
  observeTicks() {
    if (!observer.isRegistered('api.tick')) {
      const apiTick = (tick) => {
        this.ticks = [...this.ticks, tick];
        this.ticks.splice(0, 1);
        if (this.running) {
          this.purchaseCtrl.updateTicks({
            ticks: this.ticks,
            candles: this.candles,
          });
        }
        observer.emit('bot.tickUpdate', {
          ticks: this.ticks,
          candles: this.candles,
          pip: this.pip,
        });
      };
      observer.register('api.tick', apiTick);
    }
  }
  observeOhlc() {
    if (!observer.isRegistered('api.ohlc')) {
      const apiOHLC = (candle) => {
        if (this.candles.length && this.candles.slice(-1)[0].epoch === candle.epoch) {
          this.candles = [...this.candles.slice(0, -1), candle];
        } else {
          this.candles = [...this.candles, candle];
          this.candles.splice(0, 1);
        }
      };
      observer.register('api.ohlc', apiOHLC);
    }
  }
  observeStrategy() {
    if (!observer.isRegistered('strategy.ready')) {
      const strategyReady = () => {
        if (this.running) {
          observer.emit('bot.waiting_for_purchase');
        }
      };
      observer.register('strategy.ready', strategyReady);
    }
  }
  observeTradeUpdate() {
    if (!observer.isRegistered('strategy.tradeUpdate')) {
      const strategyTradeUpdate = (contract) => {
        if (this.running) {
          observer.emit('bot.tradeUpdate', contract);
        }
      };
      observer.register('strategy.tradeUpdate', strategyTradeUpdate);
    }
  }
  observeStreams() {
    this.observeTradeUpdate();
    this.observeStrategy();
    this.observeTicks();
    this.observeOhlc();
  }
  subscribeProposal(tradeOption) {
    const apiProposal = (proposal) => {
      if (this.running) {
        observer.emit('log.bot.proposal', proposal);
        this.purchaseCtrl.updateProposal(proposal);
      }
    };
    observer.register('api.proposal', apiProposal, false, {
      type: 'proposal',
      unregister: [
        ['api.proposal', apiProposal],
        'strategy.ready',
        'bot.waiting_for_purchase',
      ],
    });
    this.unregisterOnFinish.push(['api.proposal', apiProposal]);
    this.api.proposal(tradeOption);
  }
  subscribeProposals() {
    this.setTradeOptions();
    observer.unregisterAll('api.proposal');
    this.api.originalApi.unsubscribeFromAllProposals().then(() => {
      for (const to of this.tradeOptions) {
        this.subscribeProposal(to);
      }
    }, () => 0);
  }
  waitForStrategyFinish() {
    const strategyFinish = (contract) => {
      this.botFinish(contract);
    };
    observer.register('strategy.finish', strategyFinish, true, null, true);
    this.unregisterOnFinish.push(['strategy.finish', strategyFinish]);
  }
  waitForTradePurchase() {
    const tradePurchase = () => {
      this.totalRuns += 1;
      observer.emit('bot.tradeInfo', {
        totalRuns: this.totalRuns,
      });
    };
    observer.register('trade.purchase', tradePurchase, true, null, true);
    this.unregisterOnFinish.push(['trade.purchase', tradePurchase]);
  }
  startTrading() {
    this.waitForStrategyFinish();
    this.waitForTradePurchase();
    this.subscribeProposals();
  }
  updateTotals(contract) {
    const profit = +(Number(contract.sell_price) - Number(contract.buy_price)).toFixed(2);
    const user = getToken(this.currentToken);
    observer.emit('log.revenue', {
      user,
      profit,
      contract,
    });

    this.totalProfit = +(this.totalProfit + profit).toFixed(2);
    this.totalStake = +(this.totalStake + Number(contract.buy_price)).toFixed(2);
    this.totalPayout = +(this.totalPayout + Number(contract.sell_price)).toFixed(2);

    observer.emit('bot.tradeInfo', {
      totalProfit: this.totalProfit,
      totalStake: this.totalStake,
      totalPayout: this.totalPayout,
    });
  }
  botFinish(contract) {
    for (const obs of this.unregisterOnFinish) {
      observer.unregisterAll(...obs);
    }
    this.unregisterOnFinish = [];
    this.updateTotals(contract);
    observer.emit('bot.finish', contract);
    // order matters
    this.running = false;
    this.purchaseCtrl.destroy();
    this.purchaseCtrl = null;
  //
  }
  stop(contract) {
    if (!this.running) {
      observer.emit('bot.stop', contract);
      return;
    }
    for (const obs of this.unregisterOnFinish) {
      observer.unregisterAll(...obs);
    }
    this.unregisterOnFinish = [];
    // order matters
    this.running = false;
    if (this.purchaseCtrl) {
      this.purchaseCtrl.destroy();
      this.purchaseCtrl = null;
    }
    //
    this.api.originalApi.unsubscribeFromAllProposals().then(() => 0, () => 0);
    if (contract) {
      observer.emit('log.bot.stop', contract);
    }
    observer.emit('bot.stop', contract);
  }
}

export const bot = process.browser ? new Bot() : null;
<commit_msg>Catch runtime errors in the start promise<commit_after>import { observer } from 'binary-common-utils/lib/observer';
import CustomApi from 'binary-common-utils/lib/customApi';
import { getToken } from 'binary-common-utils/lib/storageManager';
import _ from 'underscore';
import config from '../../common/const';
import PurchaseCtrl from './purchaseCtrl';
import _Symbol from './symbol';
import { expectNumber, expectBarrierOffset } from '../../common/expect';
import { RuntimeError } from '../../common/error';

const decorateTradeOptions = (tradeOption, otherOptions = {}) => {
  const option = {
    duration_unit: tradeOption.duration_unit,
    basis: tradeOption.basis,
    currency: tradeOption.currency,
    symbol: tradeOption.symbol,
    ...otherOptions,
  };
  option.duration = expectNumber('duration', tradeOption.duration, RuntimeError);
  option.amount = expectNumber('amount', tradeOption.amount, RuntimeError).toFixed(2);
  if ('prediction' in tradeOption) {
    option.barrier = expectNumber('prediction', tradeOption.prediction, RuntimeError);
  }
  if ('barrierOffset' in tradeOption) {
    option.barrier = expectBarrierOffset(tradeOption.barrierOffset, RuntimeError);
  }
  if ('secondBarrierOffset' in tradeOption) {
    option.barrier2 = expectBarrierOffset(tradeOption.secondBarrierOffset, RuntimeError);
  }
  return option;
};

export default class Bot {
  constructor(api = null) {
    this.ticks = [];
    this.candles = [];
    this.candleInterval = 60;
    this.running = false;
    this.currentToken = '';
    this.balanceStr = '';
    this.currentSymbol = '';
    this.unregisterOnFinish = [];
    this.totalProfit = 0;
    this.totalRuns = 0;
    this.totalStake = 0;
    this.totalPayout = 0;
    this.balance = 0;
    this.api = (api === null) ? new CustomApi() : api;
    this.symbol = new _Symbol(this.api);
    this.initPromise = this.symbol.initPromise;
  }
  start(token, tradeOption, strategy, duringPurchase, finish, sameTrade) {
    if (!this.running || sameTrade) {
      this.running = true;
      if (this.purchaseCtrl) {
        this.purchaseCtrl.destroy();
      }
      this.purchaseCtrl = new PurchaseCtrl(this.api, strategy, duringPurchase, finish);
      this.tradeOption = tradeOption;
      observer.emit('log.bot.start', {
        again: !!sameTrade,
      });
      const accountName = getToken(token).account_name;
      if (typeof amplitude !== 'undefined') {
        amplitude.getInstance().setUserId(accountName);
      }
      if (typeof trackJs !== 'undefined') {
        trackJs.configure({
          userId: accountName,
        });
      }
      if (sameTrade) {
        this.startTrading();
      } else {
        const promises = [];
        if (this.currentToken !== token) {
          promises.push(this.login(token));
        }
        if (!_.isEmpty(this.tradeOption)) {
          if (this.tradeOption.symbol !== this.currentSymbol) {
            promises.push(this.subscribeToTickHistory());
            promises.push(this.subscribeToCandles());
          } else if (this.tradeOption.candleInterval !== this.candleInterval) {
            this.candleInterval = this.tradeOption.candleInterval;
            promises.push(this.subscribeToCandles());
          }
        }
        Promise.all(promises).then(() => {
          this.startTrading();
        }).catch((error) => {
          if (error.name === 'RuntimeError') {
            // pass
          } else {
            throw error;
          }
        });
      }
    }
  }
  login(token) {
    return new Promise((resolve) => {
      const apiAuthorize = () => {
        observer.emit('log.bot.login', {
          lastToken: this.currentToken,
          token,
        });
        this.currentToken = token;
        this.subscribeToBalance();
        this.observeStreams();
        resolve();
      };
      observer.register('api.authorize', apiAuthorize, true, {
        type: 'authorize',
        unregister: [['api.authorize', apiAuthorize]],
      }, true);
      this.api.authorize(token);
    });
  }
  setTradeOptions() {
    if (!_.isEmpty(this.tradeOption)) {
      this.pip = this.symbol.activeSymbols.getSymbols()[this.tradeOption.symbol].pip;
      const opposites = config.opposites[this.tradeOption.condition];
      this.candleInterval = this.tradeOption.candleInterval;
      this.tradeOptions = [];
      for (const key of Object.keys(opposites)) {
        this.tradeOptions.push(decorateTradeOptions(this.tradeOption, {
          contract_type: Object.keys(opposites[key])[0],
        }));
      }
    } else {
      this.tradeOptions = [];
    }
  }
  subscribeToBalance() {
    const apiBalance = (balance) => {
      this.balance = balance.balance;
      this.balanceStr = `${Number(balance.balance).toFixed(2)} ${balance.currency}`;
      observer.emit('bot.tradeInfo', {
        balance: this.balanceStr,
      });
    };
    observer.register('api.balance', apiBalance, false, {
      type: 'balance',
      unregister: [['api.balance', apiBalance]],
    });
    this.api.balance();
  }
  subscribeToCandles() {
    return new Promise((resolve) => {
      const apiCandles = (candles) => {
        this.candles = candles;
        resolve();
      };
      observer.register('api.candles', apiCandles, true, {
        type: 'candles',
        unregister: ['api.ohlc', 'api.candles', 'api.tick', 'bot.tickUpdate'],
      });
      this.api.originalApi.unsubscribeFromAllCandles().then(() => 0, () => 0);
      this.api.history(this.tradeOption.symbol, {
        end: 'latest',
        count: 600,
        granularity: this.candleInterval,
        style: 'candles',
        subscribe: 1,
      });
    });
  }
  subscribeToTickHistory() {
    return new Promise((resolve) => {
      const apiHistory = (history) => {
        this.currentSymbol = this.tradeOption.symbol;
        this.ticks = history;
        resolve();
      };
      observer.register('api.history', apiHistory, true, {
        type: 'history',
        unregister: [['api.history', apiHistory], 'api.tick',
          'bot.tickUpdate', 'api.ohlc', 'api.candles'],
      }, true);
      this.api.originalApi.unsubscribeFromAllTicks().then(() => 0, () => 0);
      this.api.history(this.tradeOption.symbol, {
        end: 'latest',
        count: 600,
        subscribe: 1,
      });
    });
  }
  observeTicks() {
    if (!observer.isRegistered('api.tick')) {
      const apiTick = (tick) => {
        this.ticks = [...this.ticks, tick];
        this.ticks.splice(0, 1);
        if (this.running) {
          this.purchaseCtrl.updateTicks({
            ticks: this.ticks,
            candles: this.candles,
          });
        }
        observer.emit('bot.tickUpdate', {
          ticks: this.ticks,
          candles: this.candles,
          pip: this.pip,
        });
      };
      observer.register('api.tick', apiTick);
    }
  }
  observeOhlc() {
    if (!observer.isRegistered('api.ohlc')) {
      const apiOHLC = (candle) => {
        if (this.candles.length && this.candles.slice(-1)[0].epoch === candle.epoch) {
          this.candles = [...this.candles.slice(0, -1), candle];
        } else {
          this.candles = [...this.candles, candle];
          this.candles.splice(0, 1);
        }
      };
      observer.register('api.ohlc', apiOHLC);
    }
  }
  observeStrategy() {
    if (!observer.isRegistered('strategy.ready')) {
      const strategyReady = () => {
        if (this.running) {
          observer.emit('bot.waiting_for_purchase');
        }
      };
      observer.register('strategy.ready', strategyReady);
    }
  }
  observeTradeUpdate() {
    if (!observer.isRegistered('strategy.tradeUpdate')) {
      const strategyTradeUpdate = (contract) => {
        if (this.running) {
          observer.emit('bot.tradeUpdate', contract);
        }
      };
      observer.register('strategy.tradeUpdate', strategyTradeUpdate);
    }
  }
  observeStreams() {
    this.observeTradeUpdate();
    this.observeStrategy();
    this.observeTicks();
    this.observeOhlc();
  }
  subscribeProposal(tradeOption) {
    const apiProposal = (proposal) => {
      if (this.running) {
        observer.emit('log.bot.proposal', proposal);
        this.purchaseCtrl.updateProposal(proposal);
      }
    };
    observer.register('api.proposal', apiProposal, false, {
      type: 'proposal',
      unregister: [
        ['api.proposal', apiProposal],
        'strategy.ready',
        'bot.waiting_for_purchase',
      ],
    });
    this.unregisterOnFinish.push(['api.proposal', apiProposal]);
    this.api.proposal(tradeOption);
  }
  subscribeProposals() {
    this.setTradeOptions();
    observer.unregisterAll('api.proposal');
    this.api.originalApi.unsubscribeFromAllProposals().then(() => {
      for (const to of this.tradeOptions) {
        this.subscribeProposal(to);
      }
    }, () => 0);
  }
  waitForStrategyFinish() {
    const strategyFinish = (contract) => {
      this.botFinish(contract);
    };
    observer.register('strategy.finish', strategyFinish, true, null, true);
    this.unregisterOnFinish.push(['strategy.finish', strategyFinish]);
  }
  waitForTradePurchase() {
    const tradePurchase = () => {
      this.totalRuns += 1;
      observer.emit('bot.tradeInfo', {
        totalRuns: this.totalRuns,
      });
    };
    observer.register('trade.purchase', tradePurchase, true, null, true);
    this.unregisterOnFinish.push(['trade.purchase', tradePurchase]);
  }
  startTrading() {
    this.waitForStrategyFinish();
    this.waitForTradePurchase();
    this.subscribeProposals();
  }
  updateTotals(contract) {
    const profit = +(Number(contract.sell_price) - Number(contract.buy_price)).toFixed(2);
    const user = getToken(this.currentToken);
    observer.emit('log.revenue', {
      user,
      profit,
      contract,
    });

    this.totalProfit = +(this.totalProfit + profit).toFixed(2);
    this.totalStake = +(this.totalStake + Number(contract.buy_price)).toFixed(2);
    this.totalPayout = +(this.totalPayout + Number(contract.sell_price)).toFixed(2);

    observer.emit('bot.tradeInfo', {
      totalProfit: this.totalProfit,
      totalStake: this.totalStake,
      totalPayout: this.totalPayout,
    });
  }
  botFinish(contract) {
    for (const obs of this.unregisterOnFinish) {
      observer.unregisterAll(...obs);
    }
    this.unregisterOnFinish = [];
    this.updateTotals(contract);
    observer.emit('bot.finish', contract);
    // order matters
    this.running = false;
    this.purchaseCtrl.destroy();
    this.purchaseCtrl = null;
  //
  }
  stop(contract) {
    if (!this.running) {
      observer.emit('bot.stop', contract);
      return;
    }
    for (const obs of this.unregisterOnFinish) {
      observer.unregisterAll(...obs);
    }
    this.unregisterOnFinish = [];
    // order matters
    this.running = false;
    if (this.purchaseCtrl) {
      this.purchaseCtrl.destroy();
      this.purchaseCtrl = null;
    }
    //
    this.api.originalApi.unsubscribeFromAllProposals().then(() => 0, () => 0);
    if (contract) {
      observer.emit('log.bot.stop', contract);
    }
    observer.emit('bot.stop', contract);
  }
}

export const bot = process.browser ? new Bot() : null;
<|endoftext|>


<commit_before>/**
 * Copyright (C) 2020 3D Repo Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */

const fs = require('fs');
const readline = require('readline');
const { config, configPath } = require('../lib/config');
const { ERRCODE_BUNDLE_GEN_FAIL, ERRCODE_UNITY_LICENCE_INVALID, ERRCODE_REPO_LICENCE_INVALID } = require('../constants/errorCodes');
const run = require('../lib/runCommand');
const logger = require('../lib/logger');
const { getCurrentDateTimeAsString } = require('../lib/utils');

const UnityHandler = {};

const logLabel = { label: 'UNITY' };

const checkLicenceError = (log) => new Promise((resolve) => {
	// We want to check if the unity process failed with licensing error.
	// This seems to fail with an error code on windows, but returns success on linux
	// So the only way is to look through the log and grep for the error message - super ad hoc!
	const readStream = fs.createReadStream(log);
	const rl = readline.createInterface({
		input: readStream,
	});
	let licenseError = false;

	rl.on('line', (line) => {
		if (
			line.includes('Current license is invalid and cannot be activated')
				|| line.includes('Unity has not been activated with a valid License')
				|| line.includes('Failed to activate/update license')
		) {
			licenseError = true;
			rl.close();
		}
	});

	rl.on('close', () => {
		resolve(licenseError);
	});
});

UnityHandler.generateAssetBundles = async (database, model, rid, logDir, processInformation) => {
	const unityCommand = config.unity.batPath;
	const dateStr = getCurrentDateTimeAsString();
	const unityLog = `${logDir}/unity_${dateStr}.log`;
	const unityCmdParams = [
		config.unity.project,
		configPath,
		database,
		model,
		rid,
		unityLog,
	];

	if (config.repoLicense) {
		process.env.REPO_LICENSE = config.repoLicense;
		process.env.REPO_INSTANCE_ID = config.instanceId;
	}

	try {
		const retVal = await run(unityCommand, unityCmdParams, { logLabel }, processInformation);
		if (await checkLicenceError(unityLog)) {
			throw ERRCODE_UNITY_LICENCE_INVALID;
		}

		return retVal;
	} catch (err) {
		logger.info(`Failed to execute unity command: ${err}`, logLabel);
		switch (err) {
			case ERRCODE_UNITY_LICENCE_INVALID:
			case ERRCODE_REPO_LICENCE_INVALID:
				throw err;
			default:
				throw await checkLicenceError(unityLog) ? ERRCODE_UNITY_LICENCE_INVALID : ERRCODE_BUNDLE_GEN_FAIL;
		}
	}
};

UnityHandler.validateUnityConfigurations = () => {
	if (!(config.unity && config.unity.project)) {
		logger.error('unity.project is not specified!', logLabel);
		return false;
	}

	if (!config.unity.project) {
		logger.error('unity.project is not specified!', logLabel);
		return false;
	}
	return true;
};

module.exports = UnityHandler;
<commit_msg>ISSUE #566 capture timeout error and ensure it is not obscured by generic bundle failure<commit_after>/**
 * Copyright (C) 2020 3D Repo Ltd
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 */

const fs = require('fs');
const readline = require('readline');
const { config, configPath } = require('../lib/config');
const { ERRCODE_TIMEOUT, ERRCODE_BUNDLE_GEN_FAIL, ERRCODE_UNITY_LICENCE_INVALID, ERRCODE_REPO_LICENCE_INVALID } = require('../constants/errorCodes');
const run = require('../lib/runCommand');
const logger = require('../lib/logger');
const { getCurrentDateTimeAsString } = require('../lib/utils');

const UnityHandler = {};

const logLabel = { label: 'UNITY' };

const checkLicenceError = (log) => new Promise((resolve) => {
	// We want to check if the unity process failed with licensing error.
	// This seems to fail with an error code on windows, but returns success on linux
	// So the only way is to look through the log and grep for the error message - super ad hoc!
	const readStream = fs.createReadStream(log);
	const rl = readline.createInterface({
		input: readStream,
	});
	let licenseError = false;

	rl.on('line', (line) => {
		if (
			line.includes('Current license is invalid and cannot be activated')
				|| line.includes('Unity has not been activated with a valid License')
				|| line.includes('Failed to activate/update license')
		) {
			licenseError = true;
			rl.close();
		}
	});

	rl.on('close', () => {
		resolve(licenseError);
	});
});

UnityHandler.generateAssetBundles = async (database, model, rid, logDir, processInformation) => {
	const unityCommand = config.unity.batPath;
	const dateStr = getCurrentDateTimeAsString();
	const unityLog = `${logDir}/unity_${dateStr}.log`;
	const unityCmdParams = [
		config.unity.project,
		configPath,
		database,
		model,
		rid,
		unityLog,
	];

	if (config.repoLicense) {
		process.env.REPO_LICENSE = config.repoLicense;
		process.env.REPO_INSTANCE_ID = config.instanceId;
	}

	try {
		const retVal = await run(unityCommand, unityCmdParams, { logLabel }, processInformation);
		if (await checkLicenceError(unityLog)) {
			throw ERRCODE_UNITY_LICENCE_INVALID;
		}

		return retVal;
	} catch (err) {
		logger.info(`Failed to execute unity command: ${err}`, logLabel);
		switch (err) {
			case ERRCODE_UNITY_LICENCE_INVALID:
			case ERRCODE_REPO_LICENCE_INVALID:
			case ERRCODE_TIMEOUT:
				throw err;
			default:
				throw await checkLicenceError(unityLog) ? ERRCODE_UNITY_LICENCE_INVALID : ERRCODE_BUNDLE_GEN_FAIL;
		}
	}
};

UnityHandler.validateUnityConfigurations = () => {
	if (!(config.unity && config.unity.project)) {
		logger.error('unity.project is not specified!', logLabel);
		return false;
	}

	if (!config.unity.project) {
		logger.error('unity.project is not specified!', logLabel);
		return false;
	}
	return true;
};

module.exports = UnityHandler;
<|endoftext|>


<commit_before>'use strict';

// JointJS v2.0.0 Performance tips

// Overall goals
// -------------
// 1. reduce number of DOM elements
// 2. avoid asking the browser for element bounding boxes as much as possible

// Number of elements 0 - ?
var COUNT = 500;
// Async rendering true/false
// true: does not block the UI
var ASYNC = false;

var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({
    el: document.getElementById('canvas'),
    width: COUNT / 2 * 110,
    height: 500,
    model: graph,
    async: ASYNC
});

var Shape = joint.dia.Element.define('Shape', {
    size: {
        width: 100,
        height: 50
    },
    attrs: {
        body: {
            // Using of special 'ref-like` attributes it's not generally the most
            // performer. In this particular case it's different though.
            // If the `ref` attribute is not defined all the metrics (width, height, x, y)
            // are taken from the model. There is no need to ask the browser for
            // an element bounding box.
            // All calculation are done just in Javascript === very fast.
            refWidth: '100%',
            refHeight: '100%',
            stroke: 'red',
            strokeWidth: 2,
            fill: 'lightgray',
            rx: 5,
            ry: 5
        },
        label: {
            fill: 'black',
            // Please see the `ref-width` & `ref-height` comment.
            refX: '50%',
            refY: '50%',
            // Do not use special attribute `x-alignment` when not necessary.
            // It calls getBBox() on the SVGText element internally. Measuring text
            // in the browser is usually the slowest.
            // `text-anchor` attribute does the same job here (works for the text elements only).
            textAnchor: 'middle',
            // Do not use special attribute `y-alignment` for text vertical positioning. See above.
            textVerticalAnchor: 'middle'
        }
    },
    z: 2
}, {
    // if markup does not change during the application life time, define it on the prototype (i.e. not in the defaults above)
    markup: [{
        tagName: 'rect',
        selector: 'body'
    }, {
        tagName: 'text',
        selector: 'label'
    }]
});

var Link = joint.dia.Link.define('Link', {
    z: 1,
    attrs: {
        line: {
            connection: true,
            // SVG Markers are pretty fast. Let's take advantage of this.
            targetMarker: {
                type: 'path',
                fill: 'green',
                stroke: 'none',
                d: 'M 10 -10 0 0 10 10 z'
            }
        }
    }
}, {
    markup: [{
        tagName: 'path',
        selector: 'line',
        attributes: {
            // Here comes SVG attributes, for which values won't change during the application life time.
            // These are specs SVG attributes. Do not add special attributes (e.g. targetMarker, fill: { /* gradient */ })).
            // These attributes are set during render, and never touched again during updates.
            'stroke': 'green',
            'stroke-width': 2,
        }
    }]
});

var el = new Shape();
var l = new Link();

var cells = [];

Array.from({ length: COUNT / 2 }).forEach(function(_, n) {
    var a = el.clone().position(n * 110, 100).attr('label/text', n + 1);
    var b = el.clone().position(n * 100, 300).attr('label/text', n + 1 + (COUNT / 2));
    var ab = l.clone().prop('source/id', a.id).prop('target/id', b.id);
    cells.push(a, b, ab);
});

var startTime = new Date();

function showResult() {
    var duration = (new Date() - startTime) / 1000;
    document.getElementById('perf').textContent = (COUNT + ' elements and ' + COUNT / 2 + ' links rendered in ' + duration + 's');
}

// Prefer resetCells() over `addCells()` to add elements in bulk.
// SVG as oppose to HTML does not know `z-index` attribute.
// The "z" coordinate is determined by the order of the sibling elements. The JointJS
// paper makes sure the DOM elements are sorted based on the "z" stored on each element model.
graph.resetCells(cells);

if (ASYNC) {
    paper.on('render:done', showResult);
} else {
    showResult();
}

// There is still room for improvements in JointJS from the performance perspective.
// We're definitely going to address this in the upcoming releases.
<commit_msg>demo.Performance: update shapes demo (#1373)<commit_after>'use strict';

// JointJS Performance tips

// Overall goals
// -------------
// 1. reduce number of DOM elements (See async.html demo)
// 2. avoid asking the browser for element bounding boxes as much as possible

// Number of elements 0 - ?
var COUNT = 500;
// Async rendering true/false
// true: does not block the UI
var ASYNC = false;

var graph = new joint.dia.Graph;
var paper = new joint.dia.Paper({
    el: document.getElementById('canvas'),
    width: COUNT / 2 * 110,
    height: 500,
    model: graph,
    async: ASYNC,
    frozen: true,
    sorting: joint.dia.Paper.sorting.APPROX
});

var Shape = joint.dia.Element.define('Shape', {
    size: {
        width: 100,
        height: 50
    },
    attrs: {
        body: {
            // Using of special 'ref-like` attributes it's not generally the most
            // efficient. In this particular case it's different though.
            // If the `ref` attribute is not defined all the metrics (width, height, x, y)
            // are taken from the model. There is no need to ask the browser for
            // an element bounding box.
            // All calculation are done just in Javascript === very fast.
            refWidth: '100%',
            refHeight: '100%',
            stroke: 'red',
            strokeWidth: 2,
            fill: 'lightgray',
            rx: 5,
            ry: 5
        },
        label: {
            fill: 'black',
            // Please see the `ref-width` & `ref-height` comment.
            refX: '50%',
            refY: '50%',
            // Do not use special attribute `x-alignment` when not necessary.
            // It calls getBBox() on the SVGText element internally. Measuring text
            // in the browser is usually the slowest.
            // `text-anchor` attribute does the same job here (works for the text elements only).
            textAnchor: 'middle',
            // Do not use special attribute `y-alignment` for text vertical positioning. See above.
            textVerticalAnchor: 'middle'
        }
    },
    z: 2
}, {
    // if markup does not change during the application life time, define it on the prototype (i.e. not in the defaults above)
    markup: [{
        tagName: 'rect',
        selector: 'body'
    }, {
        tagName: 'text',
        selector: 'label'
    }]
});

var Link = joint.dia.Link.define('Link', {
    z: 1,
    attrs: {
        line: {
            connection: true,
            // SVG Markers are pretty fast. Let's take advantage of this.
            targetMarker: {
                type: 'path',
                fill: 'green',
                stroke: 'none',
                d: 'M 10 -10 0 0 10 10 z'
            }
        }
    }
}, {
    markup: [{
        tagName: 'path',
        selector: 'line',
        attributes: {
            // Here comes SVG attributes, for which values won't change during the application life time.
            // These are specs SVG attributes. Do not add special attributes (e.g. targetMarker, fill: { /* gradient */ })).
            // These attributes are set during render, and never touched again during updates.
            'stroke': 'green',
            'stroke-width': 2,
        }
    }]
});

var el = new Shape();
var l = new Link();

var cells = [];

Array.from({ length: COUNT / 2 }).forEach(function(_, n) {
    var a = el.clone().position(n * 110, 100);
    var b = el.clone().position(n * 100, 300);
    // Since at this point the elements are not in the graph / are not rendered
    // we can change the text label silently (instead of calling attr(), that would deep clone all `attrs`)
    a.attributes.attrs.label.text = n + 1;
    b.attributes.attrs.label.text = n + 1 + (COUNT / 2);
    var ab = l.clone().set({
        source: { id: a.id },
        target: { id: b.id }
    });
    cells.push(a, b, ab);
});

var startTime = new Date();

function showResult() {
    var duration = (new Date() - startTime) / 1000;
    document.getElementById('perf').textContent = (COUNT + ' elements and ' + COUNT / 2 + ' links rendered in ' + duration + 's');
}

// Prefer resetCells() over `addCells()` to add elements in bulk.
// SVG as oppose to HTML does not know `z-index` attribute.
// The "z" coordinate is determined by the order of the sibling elements. The JointJS
// paper makes sure the DOM elements are sorted based on the "z" stored on each element model.
graph.resetCells(cells, {
    sort: false // do not sort cells in the graph
});

paper.unfreeze({
    afterRender: function() {
        showResult();
        paper.unfreeze();
    }
});
<|endoftext|>