<commit_before>"use strict";

const {strict: assert} = require("assert");

const {stub_templates} = require("../zjsunit/handlebars");
const {mock_cjs, mock_esm, set_global, zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
const $ = require("../zjsunit/zjquery");

const noop = () => {};
stub_templates(() => "<stub>");

const page_params = set_global("page_params", {});
mock_cjs("jquery", $);
const typeahead_helper = mock_esm("../../static/js/typeahead_helper");
const ui = mock_esm("../../static/js/ui", {
    get_scroll_element: noop,
});

mock_esm("../../static/js/hash_util", {
    stream_edit_uri: noop,
    by_stream_uri: noop,
});
mock_esm("../../static/js/hashchange", {update_browser_history: noop});
mock_esm("../../static/js/list_widget", {
    create: () => ({init: noop}),
});
mock_esm("../../static/js/settings_notifications", {
    get_notifications_table_row_data: noop,
});
mock_esm("../../static/js/stream_color", {
    set_colorpicker_color: noop,
});

const peer_data = zrequire("peer_data");
const people = zrequire("people");
const stream_data = zrequire("stream_data");
const stream_edit = zrequire("stream_edit");
const stream_pill = zrequire("stream_pill");
const user_pill = zrequire("user_pill");

stream_edit.__Rewire__("sort_but_pin_current_user_on_top", noop);

const jill = {
    email: "jill@zulip.com",
    user_id: 10,
    full_name: "Jill Hill",
};
const mark = {
    email: "mark@zulip.com",
    user_id: 20,
    full_name: "Marky Mark",
};
const fred = {
    email: "fred@zulip.com",
    user_id: 30,
    full_name: "Fred Flintstone",
};
const me = {
    email: "me@example.com",
    user_id: 40,
    full_name: "me",
};

page_params.user_id = me.user_id;
const persons = [jill, mark, fred, me];
for (const person of persons) {
    people.add_active_user(person);
}

const denmark = {
    stream_id: 1,
    name: "Denmark",
    subscribed: true,
    render_subscribers: true,
    should_display_subscription_button: true,
};
peer_data.set_subscribers(denmark.stream_id, [me.user_id, mark.user_id]);

const sweden = {
    stream_id: 2,
    name: "Sweden",
    subscribed: false,
};
peer_data.set_subscribers(sweden.stream_id, [mark.user_id, jill.user_id]);

const subs = [denmark, sweden];
for (const sub of subs) {
    stream_data.add_sub(sub);
}

function test_ui(label, f) {
    run_test(label, (override) => {
        stream_edit.initialize();
        f(override);
    });
}

test_ui("subscriber_pills", (override) => {
    const subscriptions_table_selector = "#subscriptions_table";
    const input_field_stub = $.create(".input");

    input_field_stub.before = () => {};

    const sub_settings_selector = `#subscription_overlay .subscription_settings[data-stream-id='${CSS.escape(
        denmark.stream_id,
    )}']`;
    const $sub_settings_container = $.create(sub_settings_selector);
    $sub_settings_container.find = noop;
    $sub_settings_container.find = () => input_field_stub;

    const pill_container_stub = $.create(sub_settings_selector + " .pill-container");
    pill_container_stub.find = () => input_field_stub;

    const $subscription_settings = $.create(".subscription_settings");
    $subscription_settings.addClass = noop;
    $subscription_settings.closest = () => $subscription_settings;
    $subscription_settings.attr("data-stream-id", denmark.stream_id);
    $subscription_settings.length = 0;

    const $add_subscribers_form = $.create(".subscriber_list_add form");
    $add_subscribers_form.closest = () => $subscription_settings;

    let template_rendered = false;
    ui.get_content_element = () => {
        template_rendered = true;
        return {html: noop};
    };

    let expected_user_ids = [];
    let input_typeahead_called = false;
    let add_subscribers_request = false;
    override(stream_edit, "invite_user_to_stream", (user_ids, sub) => {
        assert.equal(sub.stream_id, denmark.stream_id);
        assert.deepEqual(user_ids.sort(), expected_user_ids.sort());
        add_subscribers_request = true;
    });

    input_field_stub.typeahead = (config) => {
        assert.equal(config.items, 5);
        assert(config.fixed);
        assert(config.dropup);
        assert(config.stopAdvance);

        assert.equal(typeof config.source, "function");
        assert.equal(typeof config.highlighter, "function");
        assert.equal(typeof config.matcher, "function");
        assert.equal(typeof config.sorter, "function");
        assert.equal(typeof config.updater, "function");

        const fake_this = {
            query: "#Denmark",
        };

        (function test_highlighter() {
            const fake_stream = $.create("fake-stream");
            typeahead_helper.render_stream = () => fake_stream;
            assert.equal(config.highlighter.call(fake_this, denmark), fake_stream);
        })();

        (function test_matcher() {
            let result = config.matcher.call(fake_this, denmark);
            assert(result);

            result = config.matcher.call(fake_this, sweden);
            assert(!result);
        })();

        (function test_sorter() {
            let sort_streams_called = false;
            typeahead_helper.sort_streams = () => {
                sort_streams_called = true;
            };
            config.sorter.call(fake_this);
            assert(sort_streams_called);
        })();

        (function test_updater() {
            function number_of_pills() {
                const pills = stream_edit.pill_widget.items();
                return pills.length;
            }

            assert.equal(number_of_pills(), 0);
            config.updater.call(fake_this, denmark);
            assert.equal(number_of_pills(), 1);
            fake_this.query = me.email;
            config.updater.call(fake_this, me);
            assert.equal(number_of_pills(), 2);
            fake_this.query = "#Denmark";
        })();

        (function test_source() {
            const result = config.source.call(fake_this);
            const taken_ids = stream_pill.get_stream_ids(stream_edit.pill_widget);
            const stream_ids = Array.from(result, (stream) => stream.stream_id).sort();
            let expected_ids = Array.from(subs, (stream) => stream.stream_id).sort();
            expected_ids = expected_ids.filter((id) => !taken_ids.includes(id));
            assert.deepEqual(stream_ids, expected_ids);
        })();

        input_typeahead_called = true;
    };

    // Initialize pill widget upon displaying subscription settings page.
    const stream_row_handler = $(subscriptions_table_selector).get_on_handler(
        "click",
        ".stream-row",
    );

    let fake_this = $subscription_settings;
    let event = {target: fake_this};
    stream_row_handler.call(fake_this, event);
    assert(template_rendered);
    assert(input_typeahead_called);

    let add_subscribers_handler = $(subscriptions_table_selector).get_on_handler(
        "submit",
        ".subscriber_list_add form",
    );

    fake_this = $add_subscribers_form;
    fake_this.closest = () => $subscription_settings;
    event = {
        target: fake_this,
        preventDefault: () => {},
    };

    // We cannot subscribe ourselves (`me`) as
    // we are already subscribed to denmark stream.
    const potential_denmark_stream_subscribers = Array.from(
        peer_data.get_subscribers(denmark.stream_id),
    ).filter((id) => id !== me.user_id);

    // denmark.stream_id is stubbed. Thus request is
    // sent to add all subscribers of stream Denmark.
    expected_user_ids = potential_denmark_stream_subscribers;
    add_subscribers_handler(event);

    add_subscribers_handler = $(subscriptions_table_selector).get_on_handler(
        "keyup",
        ".subscriber_list_add form",
    );
    event.which = 13;

    // Only Denmark stream pill is created and a
    // request is sent to add all it's subscribers.
    override(user_pill, "get_user_ids", () => []);
    expected_user_ids = potential_denmark_stream_subscribers;
    add_subscribers_handler(event);

    // No request is sent when there are no users to subscribe.
    stream_pill.get_user_ids = () => [];
    add_subscribers_request = false;
    add_subscribers_handler(event);
    assert(!add_subscribers_request);

    // No request is sent if we try to subscribe ourselves
    // only and are already subscribed to the stream.
    override(user_pill, "get_user_ids", () => [me.user_id]);
    add_subscribers_handler(event);
    assert(!add_subscribers_request);

    // Denmark stream pill and fred and mark user pills are created.
    // But only one request for mark is sent even though a mark user
    // pill is created and mark is also a subscriber of Denmark stream.
    override(user_pill, "get_user_ids", () => [mark.user_id, fred.user_id]);
    stream_pill.get_user_ids = () => peer_data.get_subscribers(denmark.stream_id);
    expected_user_ids = potential_denmark_stream_subscribers.concat(fred.user_id);
    add_subscribers_handler(event);
});
<commit_msg>node tests: Avoid __Rewire__ in stream_edit.<commit_after>"use strict";

const {strict: assert} = require("assert");

const {stub_templates} = require("../zjsunit/handlebars");
const {mock_cjs, mock_esm, set_global, zrequire} = require("../zjsunit/namespace");
const {run_test} = require("../zjsunit/test");
const $ = require("../zjsunit/zjquery");

const noop = () => {};
stub_templates(() => "<stub>");

const page_params = set_global("page_params", {});
mock_cjs("jquery", $);
const typeahead_helper = mock_esm("../../static/js/typeahead_helper");
const ui = mock_esm("../../static/js/ui", {
    get_scroll_element: noop,
});

mock_esm("../../static/js/hash_util", {
    stream_edit_uri: noop,
    by_stream_uri: noop,
});
mock_esm("../../static/js/hashchange", {update_browser_history: noop});
mock_esm("../../static/js/list_widget", {
    create: () => ({init: noop}),
});
mock_esm("../../static/js/settings_notifications", {
    get_notifications_table_row_data: noop,
});
mock_esm("../../static/js/stream_color", {
    set_colorpicker_color: noop,
});

const peer_data = zrequire("peer_data");
const people = zrequire("people");
const stream_data = zrequire("stream_data");
const stream_edit = zrequire("stream_edit");
const stream_pill = zrequire("stream_pill");
const user_pill = zrequire("user_pill");

const jill = {
    email: "jill@zulip.com",
    user_id: 10,
    full_name: "Jill Hill",
};
const mark = {
    email: "mark@zulip.com",
    user_id: 20,
    full_name: "Marky Mark",
};
const fred = {
    email: "fred@zulip.com",
    user_id: 30,
    full_name: "Fred Flintstone",
};
const me = {
    email: "me@example.com",
    user_id: 40,
    full_name: "me",
};

page_params.user_id = me.user_id;
const persons = [jill, mark, fred, me];
for (const person of persons) {
    people.add_active_user(person);
}

const denmark = {
    stream_id: 1,
    name: "Denmark",
    subscribed: true,
    render_subscribers: true,
    should_display_subscription_button: true,
};
peer_data.set_subscribers(denmark.stream_id, [me.user_id, mark.user_id]);

const sweden = {
    stream_id: 2,
    name: "Sweden",
    subscribed: false,
};
peer_data.set_subscribers(sweden.stream_id, [mark.user_id, jill.user_id]);

const subs = [denmark, sweden];
for (const sub of subs) {
    stream_data.add_sub(sub);
}

function test_ui(label, f) {
    run_test(label, (override) => {
        stream_edit.initialize();
        f(override);
    });
}

test_ui("subscriber_pills", (override) => {
    override(stream_edit, "sort_but_pin_current_user_on_top", noop);

    const subscriptions_table_selector = "#subscriptions_table";
    const input_field_stub = $.create(".input");

    input_field_stub.before = () => {};

    const sub_settings_selector = `#subscription_overlay .subscription_settings[data-stream-id='${CSS.escape(
        denmark.stream_id,
    )}']`;
    const $sub_settings_container = $.create(sub_settings_selector);
    $sub_settings_container.find = noop;
    $sub_settings_container.find = () => input_field_stub;

    const pill_container_stub = $.create(sub_settings_selector + " .pill-container");
    pill_container_stub.find = () => input_field_stub;

    const $subscription_settings = $.create(".subscription_settings");
    $subscription_settings.addClass = noop;
    $subscription_settings.closest = () => $subscription_settings;
    $subscription_settings.attr("data-stream-id", denmark.stream_id);
    $subscription_settings.length = 0;

    const $add_subscribers_form = $.create(".subscriber_list_add form");
    $add_subscribers_form.closest = () => $subscription_settings;

    let template_rendered = false;
    ui.get_content_element = () => {
        template_rendered = true;
        return {html: noop};
    };

    let expected_user_ids = [];
    let input_typeahead_called = false;
    let add_subscribers_request = false;
    override(stream_edit, "invite_user_to_stream", (user_ids, sub) => {
        assert.equal(sub.stream_id, denmark.stream_id);
        assert.deepEqual(user_ids.sort(), expected_user_ids.sort());
        add_subscribers_request = true;
    });

    input_field_stub.typeahead = (config) => {
        assert.equal(config.items, 5);
        assert(config.fixed);
        assert(config.dropup);
        assert(config.stopAdvance);

        assert.equal(typeof config.source, "function");
        assert.equal(typeof config.highlighter, "function");
        assert.equal(typeof config.matcher, "function");
        assert.equal(typeof config.sorter, "function");
        assert.equal(typeof config.updater, "function");

        const fake_this = {
            query: "#Denmark",
        };

        (function test_highlighter() {
            const fake_stream = $.create("fake-stream");
            typeahead_helper.render_stream = () => fake_stream;
            assert.equal(config.highlighter.call(fake_this, denmark), fake_stream);
        })();

        (function test_matcher() {
            let result = config.matcher.call(fake_this, denmark);
            assert(result);

            result = config.matcher.call(fake_this, sweden);
            assert(!result);
        })();

        (function test_sorter() {
            let sort_streams_called = false;
            typeahead_helper.sort_streams = () => {
                sort_streams_called = true;
            };
            config.sorter.call(fake_this);
            assert(sort_streams_called);
        })();

        (function test_updater() {
            function number_of_pills() {
                const pills = stream_edit.pill_widget.items();
                return pills.length;
            }

            assert.equal(number_of_pills(), 0);
            config.updater.call(fake_this, denmark);
            assert.equal(number_of_pills(), 1);
            fake_this.query = me.email;
            config.updater.call(fake_this, me);
            assert.equal(number_of_pills(), 2);
            fake_this.query = "#Denmark";
        })();

        (function test_source() {
            const result = config.source.call(fake_this);
            const taken_ids = stream_pill.get_stream_ids(stream_edit.pill_widget);
            const stream_ids = Array.from(result, (stream) => stream.stream_id).sort();
            let expected_ids = Array.from(subs, (stream) => stream.stream_id).sort();
            expected_ids = expected_ids.filter((id) => !taken_ids.includes(id));
            assert.deepEqual(stream_ids, expected_ids);
        })();

        input_typeahead_called = true;
    };

    // Initialize pill widget upon displaying subscription settings page.
    const stream_row_handler = $(subscriptions_table_selector).get_on_handler(
        "click",
        ".stream-row",
    );

    let fake_this = $subscription_settings;
    let event = {target: fake_this};
    stream_row_handler.call(fake_this, event);
    assert(template_rendered);
    assert(input_typeahead_called);

    let add_subscribers_handler = $(subscriptions_table_selector).get_on_handler(
        "submit",
        ".subscriber_list_add form",
    );

    fake_this = $add_subscribers_form;
    fake_this.closest = () => $subscription_settings;
    event = {
        target: fake_this,
        preventDefault: () => {},
    };

    // We cannot subscribe ourselves (`me`) as
    // we are already subscribed to denmark stream.
    const potential_denmark_stream_subscribers = Array.from(
        peer_data.get_subscribers(denmark.stream_id),
    ).filter((id) => id !== me.user_id);

    // denmark.stream_id is stubbed. Thus request is
    // sent to add all subscribers of stream Denmark.
    expected_user_ids = potential_denmark_stream_subscribers;
    add_subscribers_handler(event);

    add_subscribers_handler = $(subscriptions_table_selector).get_on_handler(
        "keyup",
        ".subscriber_list_add form",
    );
    event.which = 13;

    // Only Denmark stream pill is created and a
    // request is sent to add all it's subscribers.
    override(user_pill, "get_user_ids", () => []);
    expected_user_ids = potential_denmark_stream_subscribers;
    add_subscribers_handler(event);

    // No request is sent when there are no users to subscribe.
    stream_pill.get_user_ids = () => [];
    add_subscribers_request = false;
    add_subscribers_handler(event);
    assert(!add_subscribers_request);

    // No request is sent if we try to subscribe ourselves
    // only and are already subscribed to the stream.
    override(user_pill, "get_user_ids", () => [me.user_id]);
    add_subscribers_handler(event);
    assert(!add_subscribers_request);

    // Denmark stream pill and fred and mark user pills are created.
    // But only one request for mark is sent even though a mark user
    // pill is created and mark is also a subscriber of Denmark stream.
    override(user_pill, "get_user_ids", () => [mark.user_id, fred.user_id]);
    stream_pill.get_user_ids = () => peer_data.get_subscribers(denmark.stream_id);
    expected_user_ids = potential_denmark_stream_subscribers.concat(fred.user_id);
    add_subscribers_handler(event);
});
<|endoftext|>


<commit_before>'use strict'
var Hub = require('../../lib')
var hub = global.hub = new Hub()

require('./style.less')

hub.set({
  adapter: {
    inject: require('../../lib/protocol/websocket'),
    websocket: 'ws://localhost:50500'
  },
  shows: {
    // 977: {

    // }
  }
})

var Element = require('vigour-element')
var Prop = require('vigour-element/lib/property')
var app = require('vigour-element/lib/app')

var A = new Element({
  $: true,
  text: { $: 'title' },
  button: {
    type: 'button',
    text: 'yo button',
    on: {
      click () {
        if (hub.blurf.origin === hub.blarf) {
          hub.blurf.val = false //.remove()
        } else {
          hub.set({'blarf': {
            title: 'its a blurf!'
          }})
          hub.blurf.val = hub.blarf
        }
      }
    }
  }
}).Constructor

var PAGE = new Element({
  $: 'shows', // so nested on collection listeners are rly fucked
  shows: {
    $collection: true,
    ChildConstructor: new Element({
      css: 'show',
      text: { $: 'title' },
      // image: {
      //   // url(data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7)
      //   $prepend: 'data:image/jpg;base64,',
      //   $: 'thumb'
      //   // val: 'R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7'
      // },
      seasons: {
        $collection: 'seasons',
        ChildConstructor: new Element({
          css: 'season',
          text: { $prepend: 'season: ', $: 'number' },
          episodes: {
            $collection: 'episodes',
            ChildConstructor: new Element({
              css: 'episode',
              text: { $: 'number' },
              // img: {
              //   type: 'img',
              //   src: {
              //     $: 'img',
              //     $transform (val) {
              //       if (typeof val === 'string') {
              //         var ret = `https://imgmtvplay-a.akamaihd.net/image/20/20?url=http://images.mtvnn.com/${val}/original`
              //         return ret
              //       }
              //       return
              //     }
              //   }
              // }
            })
          }
        })
      }
    })
  }
}).Constructor

app.set({
  c: new PAGE(hub),
  // b: new A(hub.get('blurf', {})),
  marcus: {
    text: 'lalala'
  }
})

hub.on(() => {
  console.log('incoming!')
  // console.time(1)
  // if (typeof window !== void 0) {
    // window.requestAnimationFrame(function () {
      // console.timeEnd(1)
    // })
  // }
})

module.exports = app
<commit_msg>update<commit_after>'use strict'
var Hub = require('../../lib')
var hub = global.hub = new Hub()

require('./style.less')

hub.set({
  adapter: {
    inject: require('../../lib/protocol/websocket'),
    websocket: 'ws://localhost:3031'
  },
  shows: {
    // 977: {

    // }
  }
})

var Element = require('vigour-element')
var Prop = require('vigour-element/lib/property')
var app = require('vigour-element/lib/app')

var A = new Element({
  $: true,
  text: { $: 'title' },
  button: {
    type: 'button',
    text: 'yo button',
    on: {
      click () {
        if (hub.blurf.origin === hub.blarf) {
          hub.blurf.val = false //.remove()
        } else {
          hub.set({'blarf': {
            title: 'its a blurf!'
          }})
          hub.blurf.val = hub.blarf
        }
      }
    }
  }
}).Constructor

var PAGE = new Element({
  $: 'shows', // so nested on collection listeners are rly fucked
  shows: {
    $collection: true,
    ChildConstructor: new Element({
      css: 'show',
      text: { $: 'title' },
      // image: {
      //   // url(data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7)
      //   $prepend: 'data:image/jpg;base64,',
      //   $: 'thumb'
      //   // val: 'R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7'
      // },
      seasons: {
        $collection: 'seasons',
        ChildConstructor: new Element({
          css: 'season',
          text: { $prepend: 'season: ', $: 'number' },
          episodes: {
            $collection: 'episodes',
            ChildConstructor: new Element({
              css: 'episode',
              text: { $: 'number' },
              // img: {
              //   type: 'img',
              //   src: {
              //     $: 'img',
              //     $transform (val) {
              //       if (typeof val === 'string') {
              //         var ret = `https://imgmtvplay-a.akamaihd.net/image/20/20?url=http://images.mtvnn.com/${val}/original`
              //         return ret
              //       }
              //       return
              //     }
              //   }
              // }
            })
          }
        })
      }
    })
  }
}).Constructor

app.set({
  c: new PAGE(hub),
  // b: new A(hub.get('blurf', {})),
  marcus: {
    text: 'lalala'
  }
})

hub.on(() => {
  console.log('incoming!')
  // console.time(1)
  // if (typeof window !== void 0) {
    // window.requestAnimationFrame(function () {
      // console.timeEnd(1)
    // })
  // }
})

module.exports = app
<|endoftext|>


<commit_before>import { LitElement, html } from '/node_modules/@polymer/lit-element/lit-element.js'
import { repeat } from '/node_modules/lit-html/directives/repeat.js'
import { connect } from 'pwa-helpers/connect-mixin.js'
import { store } from '../../reduxStore.js'
import { playTimeline, resetTimeline, setChannels, resetUniverseAndFixtures } from '../../actions/index.js'
import { batch, clearBatch, fixtureBatch, modvData } from '/src/utils/index.js'
import '../timeline-scene/index.js'
import { getTimelineScenes, getAnimations, getTimeline, getFixtures, getModv } from '../../selectors/index.js'
import * as Fixtures from '../../utils/dmx-fixtures.js'

import '../luminave-elements/button.js'

/*
 * Handle the elements in a timeline
 */
class TimelineManager extends connect(store)(LitElement) {
  constructor() {
    super()

    // Max length of a scene
    this.measures = 20

    this.timeoutId = undefined

    this.progress = 0

    document.addEventListener('keypress', e => {
      const { code } = e
      // console.log(`Keypress code: ${code}`)

      // Start playback when active element is the body
      if (code === 'Space' && e.target === document.body) {
        e.preventDefault()
        this.handlePlay()
      }
    })
  }

  static get properties() {
    return {
      bpm: { type: Number },
      progress: { type: Number },
      timelineScenes: { type: Array },
      animationManager: { type: Array },
      isPlaying: { type: Boolean },
      allFixtures: { type: Array },
      timelineFixtures: { type: Object },
      modvConnected: { type: Boolean }
    }
  }

  // Start / stop the playback loop whenever isPlaying is changed
  set playing(value) {
    // Only update the value if it's actually different
    if (this.isPlaying !== value) {
      this.isPlaying = value
      this.observePlaying()
    }
  }

  // Compute the timelineFixtures whenever the fixtures on the state are changing
  set fixtures(value) {
    if (!Object.is(this.allFixtures, value)) {
      this.allFixtures = value

      // List of fixtures
      this.timelineFixtures = this.computeFixtures(value)
    }
  }

  _stateChanged(state) {
    this._page = state.app.page
    this.bpm = state.bpm
    this.modvConnected = getModv(state).connected
    this.timelineScenes = getTimelineScenes(state)
    this.animationManager = getAnimations(state)
    this.fixtures = getFixtures(state)

    // @TODO: What is the performance difference when checking if the value has changed here
    // vs changed in the setter?
    this.playing = getTimeline(state).playing
  }

  handlePlay() {
    store.dispatch(playTimeline(!this.isPlaying))
  }

  handleReset() {
    store.dispatch(resetTimeline())
    store.dispatch(resetUniverseAndFixtures(0))
  }

  // @TODO: Save the current progress into state
  // store.dispatch(setTimelineProgress(this.progress))
  observePlaying() {
    if (this.isPlaying) {
      console.log('playing')
      this.time = new Date()

      this.loop()
    } else {
      console.log('stopped')

      clearTimeout(this.timeoutId)
    }
  }

  normalizeProgress(progress) {
    return progress.toFixed(2)
  }

  loop() {
    if (this.isPlaying) {
      this.duration = ~~(60 / this.bpm * 1000 * this.measures)

      // const {time, duration, paused} = this.state
      const now = new Date()
      const loopEnd = now - this.time > this.duration

      const timeCounter = (now - this.time) / this.duration

      if (loopEnd) {
        this.time = now
      }

      this.progress = this.normalizeProgress(timeCounter * this.measures)

      for (const fixtureId in fixtureBatch) {
        const interpolatedProperties = fixtureBatch[fixtureId].properties
        const fixture = this.timelineFixtures[fixtureId]

          for (const propertyName in interpolatedProperties) {
            // @TODO: only set properties that the fixture understands
            fixture[propertyName] = interpolatedProperties[propertyName]
          }

          // Overwrite the color of every fixture when a connection to modV was established
          if (this.modvConnected && fixture.hasOwnProperty('color')) {

            // @TODO: When modvConnected, but no color value yet, we still set the color

            // Set a specific color from modV
            if (fixture.hasOwnProperty('modvColor')) {

              // @TODO: Fix precision error = No interpolation for values that don't change
              fixture.modvColor = Math.round(fixture.modvColor)

              const color = modvData.colors.slice((fixture.modvColor - 1) * 3, ((fixture.modvColor - 1) * 3) + 3)

              // Set the color
              // @TODO: This is still overwriting the color that was previously set on the device
              // which means it's totally useless
              fixture.color = color.length === 3
                ? color
                : fixture.color

            // Use average color from modV
            } else {
              fixture.color = modvData.average
            }
          }
      }

      // Update the channels of universe 0 with the batch of values collected for the fixtures
      store.dispatch(setChannels(0, [...batch]))

      // Reset the batch so that if a scene is done the values for the attachted fixtures are also reset
      clearBatch()

      // Send the universe to the UsbDmxManager
      window.dispatchEvent(new CustomEvent('send-universe-to-usb-dmx-controller', { detail: { now } }))

      // Send the universe to the FivetwelveManager
      window.dispatchEvent(new CustomEvent('send-universe-to-fivetwelve', { detail: { now } }))

      this.timeoutId = setTimeout(() => {
        // Get the next frame
        requestAnimationFrame(this.loop.bind(this))
      }, 1000 / 30)

    }
  }

  computeFixtures(allFixtures) {
    // Clear fixtures
    const fixtures = {}

    // Get the fixtures based on the fixtureId that is attachted to the scene
    allFixtures.forEach(fixture => {

      fixtures[fixture.id] =
        new Fixtures[fixture.type]({
          address: fixture.address,
          universe: fixture.universe
        })

    })

    return fixtures
  }

  render() {
    const { progress, isPlaying } = this

    // Label for the play-button
    const playLabel = isPlaying
      ? 'Pause'
      : 'Play'

    return html`

        <luminave-button class="primary" @click="${() => this.handlePlay()}">${playLabel}</luminave-button>
        ${progress}
        <luminave-button @click="${() => this.handleReset()}">Reset</luminave-button>

        <ui-spacer></ui-spacer>

            ${repeat(this.timelineScenes, scene => html`

                <timeline-scene .scene=${scene} progress=${progress}></timeline-scene>

            `)}

    `
  }
}

customElements.define('timeline-manager', TimelineManager)
<commit_msg>refactor(timeline-manager): Remove unused code<commit_after>import { LitElement, html } from '/node_modules/@polymer/lit-element/lit-element.js'
import { repeat } from '/node_modules/lit-html/directives/repeat.js'
import { connect } from 'pwa-helpers/connect-mixin.js'
import { store } from '../../reduxStore.js'
import { playTimeline, resetTimeline, setChannels, resetUniverseAndFixtures } from '../../actions/index.js'
import { batch, clearBatch, fixtureBatch, modvData } from '/src/utils/index.js'
import '../timeline-scene/index.js'
import { getTimelineScenes, getAnimations, getTimeline, getFixtures, getModv } from '../../selectors/index.js'
import * as Fixtures from '../../utils/dmx-fixtures.js'

import '../luminave-elements/button.js'

/*
 * Handle the elements in a timeline
 */
class TimelineManager extends connect(store)(LitElement) {
  constructor() {
    super()

    // Max length of a scene
    this.measures = 20

    this.timeoutId = undefined

    this.progress = 0

    document.addEventListener('keypress', e => {
      const { code } = e
      // console.log(`Keypress code: ${code}`)

      // Start playback when active element is the body
      if (code === 'Space' && e.target === document.body) {
        e.preventDefault()
        this.handlePlay()
      }
    })
  }

  static get properties() {
    return {
      bpm: { type: Number },
      progress: { type: Number },
      timelineScenes: { type: Array },
      animationManager: { type: Array },
      isPlaying: { type: Boolean },
      allFixtures: { type: Array },
      timelineFixtures: { type: Object },
      modvConnected: { type: Boolean }
    }
  }

  // Start / stop the playback loop whenever isPlaying is changed
  set playing(value) {
    // Only update the value if it's actually different
    if (this.isPlaying !== value) {
      this.isPlaying = value
      this.observePlaying()
    }
  }

  // Compute the timelineFixtures whenever the fixtures on the state are changing
  set fixtures(value) {
    if (!Object.is(this.allFixtures, value)) {
      this.allFixtures = value

      // List of fixtures
      this.timelineFixtures = this.computeFixtures(value)
    }
  }

  _stateChanged(state) {
    this._page = state.app.page
    this.bpm = state.bpm
    this.modvConnected = getModv(state).connected
    this.timelineScenes = getTimelineScenes(state)
    this.animationManager = getAnimations(state)
    this.fixtures = getFixtures(state)

    // @TODO: What is the performance difference when checking if the value has changed here
    // vs changed in the setter?
    this.playing = getTimeline(state).playing
  }

  handlePlay() {
    store.dispatch(playTimeline(!this.isPlaying))
  }

  handleReset() {
    store.dispatch(resetTimeline())
    store.dispatch(resetUniverseAndFixtures(0))
  }

  // @TODO: Save the current progress into state
  // store.dispatch(setTimelineProgress(this.progress))
  observePlaying() {
    if (this.isPlaying) {
      console.log('playing')
      this.time = new Date()

      this.loop()
    } else {
      console.log('stopped')

      clearTimeout(this.timeoutId)
    }
  }

  normalizeProgress(progress) {
    return progress.toFixed(2)
  }

  loop() {
    if (this.isPlaying) {
      this.duration = ~~(60 / this.bpm * 1000 * this.measures)

      const now = new Date()
      const loopEnd = now - this.time > this.duration

      const timeCounter = (now - this.time) / this.duration

      if (loopEnd) {
        this.time = now
      }

      this.progress = this.normalizeProgress(timeCounter * this.measures)

      for (const fixtureId in fixtureBatch) {
        const interpolatedProperties = fixtureBatch[fixtureId].properties
        const fixture = this.timelineFixtures[fixtureId]

          for (const propertyName in interpolatedProperties) {
            // @TODO: only set properties that the fixture understands
            fixture[propertyName] = interpolatedProperties[propertyName]
          }

          // Overwrite the color of every fixture when a connection to modV was established
          if (this.modvConnected && fixture.hasOwnProperty('color')) {

            // @TODO: When modvConnected, but no color value yet, we still set the color

            // Set a specific color from modV
            if (fixture.hasOwnProperty('modvColor')) {

              // @TODO: Fix precision error = No interpolation for values that don't change
              fixture.modvColor = Math.round(fixture.modvColor)

              const color = modvData.colors.slice((fixture.modvColor - 1) * 3, ((fixture.modvColor - 1) * 3) + 3)

              // Set the color
              // @TODO: This is still overwriting the color that was previously set on the device
              // which means it's totally useless
              fixture.color = color.length === 3
                ? color
                : fixture.color

            // Use average color from modV
            } else {
              fixture.color = modvData.average
            }
          }
      }

      // Update the channels of universe 0 with the batch of values collected for the fixtures
      store.dispatch(setChannels(0, [...batch]))

      // Reset the batch so that if a scene is done the values for the attachted fixtures are also reset
      clearBatch()

      // Send the universe to the UsbDmxManager
      window.dispatchEvent(new CustomEvent('send-universe-to-usb-dmx-controller', { detail: { now } }))

      // Send the universe to the FivetwelveManager
      window.dispatchEvent(new CustomEvent('send-universe-to-fivetwelve', { detail: { now } }))

      this.timeoutId = setTimeout(() => {
        // Get the next frame
        requestAnimationFrame(this.loop.bind(this))
      }, 1000 / 30)

    }
  }

  computeFixtures(allFixtures) {
    // Clear fixtures
    const fixtures = {}

    // Get the fixtures based on the fixtureId that is attachted to the scene
    allFixtures.forEach(fixture => {

      fixtures[fixture.id] =
        new Fixtures[fixture.type]({
          address: fixture.address,
          universe: fixture.universe
        })

    })

    return fixtures
  }

  render() {
    const { progress, isPlaying } = this

    // Label for the play-button
    const playLabel = isPlaying
      ? 'Pause'
      : 'Play'

    return html`

        <luminave-button class="primary" @click="${() => this.handlePlay()}">${playLabel}</luminave-button>
        ${progress}
        <luminave-button @click="${() => this.handleReset()}">Reset</luminave-button>

        <ui-spacer></ui-spacer>

            ${repeat(this.timelineScenes, scene => html`

                <timeline-scene .scene=${scene} progress=${progress}></timeline-scene>

            `)}

    `
  }
}

customElements.define('timeline-manager', TimelineManager)
<|endoftext|>


<commit_before>/**
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
const fs = require('fs');
const os = require('os');
const rm = require('rimraf').sync;
const path = require('path');
const {helper} = require('../lib/helper');
const mkdtempAsync = helper.promisify(fs.mkdtemp);
const readFileAsync = helper.promisify(fs.readFile);
const statAsync = helper.promisify(fs.stat);
const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
const utils = require('./utils');

module.exports.addTests = function({testRunner, expect, PROJECT_ROOT, defaultBrowserOptions}) {
  const {describe, xdescribe, fdescribe} = testRunner;
  const {it, fit, xit} = testRunner;
  const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
  const puppeteer = require(PROJECT_ROOT);

  describe('Puppeteer', function() {
    describe('BrowserFetcher', function() {
      it('should download and extract linux binary', async({server}) => {
        const downloadsFolder = await mkdtempAsync(TMP_FOLDER);
        const browserFetcher = puppeteer.createBrowserFetcher({
          platform: 'linux',
          path: downloadsFolder,
          host: server.PREFIX
        });
        let revisionInfo = browserFetcher.revisionInfo('123456');
        server.setRoute(revisionInfo.url.substring(server.PREFIX.length), (req, res) => {
          server.serveFile(req, res, '/chromium-linux.zip');
        });

        expect(revisionInfo.local).toBe(false);
        expect(browserFetcher.platform()).toBe('linux');
        expect(await browserFetcher.canDownload('100000')).toBe(false);
        expect(await browserFetcher.canDownload('123456')).toBe(true);

        revisionInfo = await browserFetcher.download('123456');
        expect(revisionInfo.local).toBe(true);
        expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe('LINUX BINARY\n');
        const expectedPermissions = os.platform() === 'win32' ? 0666 : 0755;
        expect((await statAsync(revisionInfo.executablePath)).mode & 0777).toBe(expectedPermissions);
        expect(await browserFetcher.localRevisions()).toEqual(['123456']);
        await browserFetcher.remove('123456');
        expect(await browserFetcher.localRevisions()).toEqual([]);
        rm(downloadsFolder);
      });
    });
    describe('AppMode', function() {
      it('should work', async() => {
        const options = Object.assign({appMode: true}, defaultBrowserOptions);
        const browser = await puppeteer.launch(options);
        const page = await browser.newPage();
        expect(await page.evaluate('11 * 11')).toBe(121);
        await page.close();
        await browser.close();
      });
    });

    describe('Puppeteer.launch', function() {
      it('should reject all promises when browser is closed', async() => {
        const browser = await puppeteer.launch(defaultBrowserOptions);
        const page = await browser.newPage();
        let error = null;
        const neverResolves = page.evaluate(() => new Promise(r => {})).catch(e => error = e);
        await browser.close();
        await neverResolves;
        expect(error.message).toContain('Protocol error');
      });
      it('should reject if executable path is invalid', async({server}) => {
        let waitError = null;
        const options = Object.assign({}, defaultBrowserOptions, {executablePath: 'random-invalid-path'});
        await puppeteer.launch(options).catch(e => waitError = e);
        expect(waitError.message.startsWith('Failed to launch chrome! spawn random-invalid-path ENOENT')).toBe(true);
      });
      it('userDataDir option', async({server}) => {
        const userDataDir = await mkdtempAsync(TMP_FOLDER);
        const options = Object.assign({userDataDir}, defaultBrowserOptions);
        const browser = await puppeteer.launch(options);
        // Open a page to make sure its functional.
        await browser.newPage();
        expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
        await browser.close();
        expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
        rm(userDataDir);
      });
      it('userDataDir argument', async({server}) => {
        const userDataDir = await mkdtempAsync(TMP_FOLDER);
        const options = Object.assign({}, defaultBrowserOptions);
        options.args = [`--user-data-dir=${userDataDir}`].concat(options.args);
        const browser = await puppeteer.launch(options);
        expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
        await browser.close();
        expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
        rm(userDataDir);
      });
      it('userDataDir option should restore state', async({server}) => {
        const userDataDir = await mkdtempAsync(TMP_FOLDER);
        const options = Object.assign({userDataDir}, defaultBrowserOptions);
        const browser = await puppeteer.launch(options);
        const page = await browser.newPage();
        await page.goto(server.EMPTY_PAGE);
        await page.evaluate(() => localStorage.hey = 'hello');
        await browser.close();

        const browser2 = await puppeteer.launch(options);
        const page2 = await browser2.newPage();
        await page2.goto(server.EMPTY_PAGE);
        expect(await page2.evaluate(() => localStorage.hey)).toBe('hello');
        await browser2.close();
        rm(userDataDir);
      });
      it('userDataDir option should restore cookies', async({server}) => {
        const userDataDir = await mkdtempAsync(TMP_FOLDER);
        const options = Object.assign({userDataDir}, defaultBrowserOptions);
        const browser = await puppeteer.launch(options);
        const page = await browser.newPage();
        await page.goto(server.EMPTY_PAGE);
        await page.evaluate(() => document.cookie = 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
        await browser.close();

        const browser2 = await puppeteer.launch(options);
        const page2 = await browser2.newPage();
        await page2.goto(server.EMPTY_PAGE);
        expect(await page2.evaluate(() => document.cookie)).toBe('doSomethingOnlyOnce=true');
        await browser2.close();
        rm(userDataDir);
      });
      it('should return the default chrome arguments', async() => {
        const args = puppeteer.defaultArgs();
        expect(args).toContain('--no-first-run');
      });
      it('should dump browser process stderr', async({server}) => {
        const dumpioTextToLog = 'MAGIC_DUMPIO_TEST';
        let dumpioData = '';
        const {spawn} = require('child_process');
        const options = Object.assign({}, defaultBrowserOptions, {dumpio: true});
        const res = spawn('node',
            [path.join(__dirname, 'fixtures', 'dumpio.js'), PROJECT_ROOT, JSON.stringify(options), server.EMPTY_PAGE, dumpioTextToLog]);
        res.stderr.on('data', data => dumpioData += data.toString('utf8'));
        await new Promise(resolve => res.on('close', resolve));

        expect(dumpioData).toContain(dumpioTextToLog);
      });
      it('should close the browser when the node process closes', async({ server }) => {
        const {spawn, execSync} = require('child_process');
        const res = spawn('node', [path.join(__dirname, 'fixtures', 'closeme.js'), PROJECT_ROOT, JSON.stringify(defaultBrowserOptions)]);
        let wsEndPointCallback;
        const wsEndPointPromise = new Promise(x => wsEndPointCallback = x);
        let output = '';
        res.stdout.on('data', data => {
          output += data;
          if (output.indexOf('\n'))
            wsEndPointCallback(output.substring(0, output.indexOf('\n')));
        });
        const browser = await puppeteer.connect({ browserWSEndpoint: await wsEndPointPromise });
        const promises = [
          new Promise(resolve => browser.once('disconnected', resolve)),
          new Promise(resolve => res.on('close', resolve))];
        if (process.platform === 'win32')
          execSync(`taskkill /pid ${res.pid} /T /F`);
        else
          process.kill(res.pid);
        await Promise.all(promises);
      });
      it('should support the pipe option', async() => {
        const options = Object.assign({pipe: true}, defaultBrowserOptions);
        const browser = await puppeteer.launch(options);
        expect((await browser.pages()).length).toBe(1);
        expect(browser.wsEndpoint()).toBe('');
        const page = await browser.newPage();
        expect(await page.evaluate('11 * 11')).toBe(121);
        await page.close();
        await browser.close();
      });
      it('should support the pipe argument', async() => {
        const options = Object.assign({}, defaultBrowserOptions);
        options.ignoreDefaultArgs = true;
        options.args = ['--remote-debugging-pipe'].concat(options.args);
        const browser = await puppeteer.launch(options);
        expect(browser.wsEndpoint()).toBe('');
        const page = await browser.newPage();
        expect(await page.evaluate('11 * 11')).toBe(121);
        await page.close();
        await browser.close();
      });
      it('should work with no default arguments', async() => {
        const options = Object.assign({}, defaultBrowserOptions);
        options.ignoreDefaultArgs = true;
        const browser = await puppeteer.launch(options);
        const page = await browser.newPage();
        expect(await page.evaluate('11 * 11')).toBe(121);
        await page.close();
        await browser.close();
      });
      it('should have default url when launching browser', async function() {
        const browser = await puppeteer.launch(defaultBrowserOptions);
        const pages = (await browser.pages()).map(page => page.url());
        expect(pages).toEqual(['about:blank']);
        await browser.close();
      });
      it('should have custom url when launching browser', async function({server}) {
        const customUrl = server.PREFIX + '/empty.html';
        const options = Object.assign({}, defaultBrowserOptions);
        options.args = [customUrl].concat(options.args);
        const browser = await puppeteer.launch(options);
        const pages = await browser.pages();
        expect(pages.length).toBe(1);
        if (pages[0].url() !== customUrl)
          await pages[0].waitForNavigation();
        expect(pages[0].url()).toBe(customUrl);
        await browser.close();
      });
    });
    describe('Puppeteer.connect', function() {
      it('should be able to connect multiple times to the same browser', async({server}) => {
        const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
        const browser = await puppeteer.connect({
          browserWSEndpoint: originalBrowser.wsEndpoint()
        });
        const page = await browser.newPage();
        expect(await page.evaluate(() => 7 * 8)).toBe(56);
        browser.disconnect();

        const secondPage = await originalBrowser.newPage();
        expect(await secondPage.evaluate(() => 7 * 6)).toBe(42, 'original browser should still work');
        await originalBrowser.close();
      });
      it('should support ignoreHTTPSErrors option', async({httpsServer}) => {
        const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
        const browserWSEndpoint = originalBrowser.wsEndpoint();

        const browser = await puppeteer.connect({browserWSEndpoint, ignoreHTTPSErrors: true});
        const page = await browser.newPage();
        let error = null;
        const response = await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e);
        expect(error).toBe(null);
        expect(response.ok()).toBe(true);
        expect(response.securityDetails()).toBeTruthy();
        expect(response.securityDetails().protocol()).toBe('TLS 1.2');
        await page.close();
        await browser.close();
      });
      it('should be able to reconnect to a disconnected browser', async({server}) => {
        const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
        const browserWSEndpoint = originalBrowser.wsEndpoint();
        const page = await originalBrowser.newPage();
        await page.goto(server.PREFIX + '/frames/nested-frames.html');
        originalBrowser.disconnect();

        const browser = await puppeteer.connect({browserWSEndpoint});
        const pages = await browser.pages();
        const restoredPage = pages.find(page => page.url() === server.PREFIX + '/frames/nested-frames.html');
        expect(utils.dumpFrames(restoredPage.mainFrame())).toBeGolden('reconnect-nested-frames.txt');
        expect(await restoredPage.evaluate(() => 7 * 8)).toBe(56);
        await browser.close();
      });
    });
    describe('Puppeteer.executablePath', function() {
      it('should work', async({server}) => {
        const executablePath = puppeteer.executablePath();
        expect(fs.existsSync(executablePath)).toBe(true);
      });
    });
  });

  describe('Browser.Events.disconnected', function() {
    it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async() => {
      const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
      const browserWSEndpoint = originalBrowser.wsEndpoint();
      const remoteBrowser1 = await puppeteer.connect({browserWSEndpoint});
      const remoteBrowser2 = await puppeteer.connect({browserWSEndpoint});

      let disconnectedOriginal = 0;
      let disconnectedRemote1 = 0;
      let disconnectedRemote2 = 0;
      originalBrowser.on('disconnected', () => ++disconnectedOriginal);
      remoteBrowser1.on('disconnected', () => ++disconnectedRemote1);
      remoteBrowser2.on('disconnected', () => ++disconnectedRemote2);

      await Promise.all([
        utils.waitEvent(remoteBrowser2, 'disconnected'),
        remoteBrowser2.disconnect(),
      ]);

      expect(disconnectedOriginal).toBe(0);
      expect(disconnectedRemote1).toBe(0);
      expect(disconnectedRemote2).toBe(1);

      await Promise.all([
        utils.waitEvent(remoteBrowser1, 'disconnected'),
        utils.waitEvent(originalBrowser, 'disconnected'),
        originalBrowser.close(),
      ]);

      expect(disconnectedOriginal).toBe(1);
      expect(disconnectedRemote1).toBe(1);
      expect(disconnectedRemote2).toBe(1);
    });
  });

};
<commit_msg>test: fix appveyour flakiness (#2779)<commit_after>/**
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
const fs = require('fs');
const os = require('os');
const path = require('path');
const {helper} = require('../lib/helper');
const rmAsync = helper.promisify(require('rimraf'));
const mkdtempAsync = helper.promisify(fs.mkdtemp);
const readFileAsync = helper.promisify(fs.readFile);
const statAsync = helper.promisify(fs.stat);
const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
const utils = require('./utils');

module.exports.addTests = function({testRunner, expect, PROJECT_ROOT, defaultBrowserOptions}) {
  const {describe, xdescribe, fdescribe} = testRunner;
  const {it, fit, xit} = testRunner;
  const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;
  const puppeteer = require(PROJECT_ROOT);

  describe('Puppeteer', function() {
    describe('BrowserFetcher', function() {
      it('should download and extract linux binary', async({server}) => {
        const downloadsFolder = await mkdtempAsync(TMP_FOLDER);
        const browserFetcher = puppeteer.createBrowserFetcher({
          platform: 'linux',
          path: downloadsFolder,
          host: server.PREFIX
        });
        let revisionInfo = browserFetcher.revisionInfo('123456');
        server.setRoute(revisionInfo.url.substring(server.PREFIX.length), (req, res) => {
          server.serveFile(req, res, '/chromium-linux.zip');
        });

        expect(revisionInfo.local).toBe(false);
        expect(browserFetcher.platform()).toBe('linux');
        expect(await browserFetcher.canDownload('100000')).toBe(false);
        expect(await browserFetcher.canDownload('123456')).toBe(true);

        revisionInfo = await browserFetcher.download('123456');
        expect(revisionInfo.local).toBe(true);
        expect(await readFileAsync(revisionInfo.executablePath, 'utf8')).toBe('LINUX BINARY\n');
        const expectedPermissions = os.platform() === 'win32' ? 0666 : 0755;
        expect((await statAsync(revisionInfo.executablePath)).mode & 0777).toBe(expectedPermissions);
        expect(await browserFetcher.localRevisions()).toEqual(['123456']);
        await browserFetcher.remove('123456');
        expect(await browserFetcher.localRevisions()).toEqual([]);
        await rmAsync(downloadsFolder);
      });
    });
    describe('AppMode', function() {
      it('should work', async() => {
        const options = Object.assign({appMode: true}, defaultBrowserOptions);
        const browser = await puppeteer.launch(options);
        const page = await browser.newPage();
        expect(await page.evaluate('11 * 11')).toBe(121);
        await page.close();
        await browser.close();
      });
    });

    describe('Puppeteer.launch', function() {
      it('should reject all promises when browser is closed', async() => {
        const browser = await puppeteer.launch(defaultBrowserOptions);
        const page = await browser.newPage();
        let error = null;
        const neverResolves = page.evaluate(() => new Promise(r => {})).catch(e => error = e);
        await browser.close();
        await neverResolves;
        expect(error.message).toContain('Protocol error');
      });
      it('should reject if executable path is invalid', async({server}) => {
        let waitError = null;
        const options = Object.assign({}, defaultBrowserOptions, {executablePath: 'random-invalid-path'});
        await puppeteer.launch(options).catch(e => waitError = e);
        expect(waitError.message.startsWith('Failed to launch chrome! spawn random-invalid-path ENOENT')).toBe(true);
      });
      it('userDataDir option', async({server}) => {
        const userDataDir = await mkdtempAsync(TMP_FOLDER);
        const options = Object.assign({userDataDir}, defaultBrowserOptions);
        const browser = await puppeteer.launch(options);
        // Open a page to make sure its functional.
        await browser.newPage();
        expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
        await browser.close();
        expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
        // This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778
        await rmAsync(userDataDir).catch(e => {});
      });
      it('userDataDir argument', async({server}) => {
        const userDataDir = await mkdtempAsync(TMP_FOLDER);
        const options = Object.assign({}, defaultBrowserOptions);
        options.args = [`--user-data-dir=${userDataDir}`].concat(options.args);
        const browser = await puppeteer.launch(options);
        expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
        await browser.close();
        expect(fs.readdirSync(userDataDir).length).toBeGreaterThan(0);
        // This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778
        await rmAsync(userDataDir).catch(e => {});
      });
      it('userDataDir option should restore state', async({server}) => {
        const userDataDir = await mkdtempAsync(TMP_FOLDER);
        const options = Object.assign({userDataDir}, defaultBrowserOptions);
        const browser = await puppeteer.launch(options);
        const page = await browser.newPage();
        await page.goto(server.EMPTY_PAGE);
        await page.evaluate(() => localStorage.hey = 'hello');
        await browser.close();

        const browser2 = await puppeteer.launch(options);
        const page2 = await browser2.newPage();
        await page2.goto(server.EMPTY_PAGE);
        expect(await page2.evaluate(() => localStorage.hey)).toBe('hello');
        await browser2.close();
        // This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778
        await rmAsync(userDataDir).catch(e => {});
      });
      it('userDataDir option should restore cookies', async({server}) => {
        const userDataDir = await mkdtempAsync(TMP_FOLDER);
        const options = Object.assign({userDataDir}, defaultBrowserOptions);
        const browser = await puppeteer.launch(options);
        const page = await browser.newPage();
        await page.goto(server.EMPTY_PAGE);
        await page.evaluate(() => document.cookie = 'doSomethingOnlyOnce=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
        await browser.close();

        const browser2 = await puppeteer.launch(options);
        const page2 = await browser2.newPage();
        await page2.goto(server.EMPTY_PAGE);
        expect(await page2.evaluate(() => document.cookie)).toBe('doSomethingOnlyOnce=true');
        await browser2.close();
        // This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778
        await rmAsync(userDataDir).catch(e => {});
      });
      it('should return the default chrome arguments', async() => {
        const args = puppeteer.defaultArgs();
        expect(args).toContain('--no-first-run');
      });
      it('should dump browser process stderr', async({server}) => {
        const dumpioTextToLog = 'MAGIC_DUMPIO_TEST';
        let dumpioData = '';
        const {spawn} = require('child_process');
        const options = Object.assign({}, defaultBrowserOptions, {dumpio: true});
        const res = spawn('node',
            [path.join(__dirname, 'fixtures', 'dumpio.js'), PROJECT_ROOT, JSON.stringify(options), server.EMPTY_PAGE, dumpioTextToLog]);
        res.stderr.on('data', data => dumpioData += data.toString('utf8'));
        await new Promise(resolve => res.on('close', resolve));

        expect(dumpioData).toContain(dumpioTextToLog);
      });
      it('should close the browser when the node process closes', async({ server }) => {
        const {spawn, execSync} = require('child_process');
        const res = spawn('node', [path.join(__dirname, 'fixtures', 'closeme.js'), PROJECT_ROOT, JSON.stringify(defaultBrowserOptions)]);
        let wsEndPointCallback;
        const wsEndPointPromise = new Promise(x => wsEndPointCallback = x);
        let output = '';
        res.stdout.on('data', data => {
          output += data;
          if (output.indexOf('\n'))
            wsEndPointCallback(output.substring(0, output.indexOf('\n')));
        });
        const browser = await puppeteer.connect({ browserWSEndpoint: await wsEndPointPromise });
        const promises = [
          new Promise(resolve => browser.once('disconnected', resolve)),
          new Promise(resolve => res.on('close', resolve))];
        if (process.platform === 'win32')
          execSync(`taskkill /pid ${res.pid} /T /F`);
        else
          process.kill(res.pid);
        await Promise.all(promises);
      });
      it('should support the pipe option', async() => {
        const options = Object.assign({pipe: true}, defaultBrowserOptions);
        const browser = await puppeteer.launch(options);
        expect((await browser.pages()).length).toBe(1);
        expect(browser.wsEndpoint()).toBe('');
        const page = await browser.newPage();
        expect(await page.evaluate('11 * 11')).toBe(121);
        await page.close();
        await browser.close();
      });
      it('should support the pipe argument', async() => {
        const options = Object.assign({}, defaultBrowserOptions);
        options.ignoreDefaultArgs = true;
        options.args = ['--remote-debugging-pipe'].concat(options.args);
        const browser = await puppeteer.launch(options);
        expect(browser.wsEndpoint()).toBe('');
        const page = await browser.newPage();
        expect(await page.evaluate('11 * 11')).toBe(121);
        await page.close();
        await browser.close();
      });
      it('should work with no default arguments', async() => {
        const options = Object.assign({}, defaultBrowserOptions);
        options.ignoreDefaultArgs = true;
        const browser = await puppeteer.launch(options);
        const page = await browser.newPage();
        expect(await page.evaluate('11 * 11')).toBe(121);
        await page.close();
        await browser.close();
      });
      it('should have default url when launching browser', async function() {
        const browser = await puppeteer.launch(defaultBrowserOptions);
        const pages = (await browser.pages()).map(page => page.url());
        expect(pages).toEqual(['about:blank']);
        await browser.close();
      });
      it('should have custom url when launching browser', async function({server}) {
        const customUrl = server.PREFIX + '/empty.html';
        const options = Object.assign({}, defaultBrowserOptions);
        options.args = [customUrl].concat(options.args);
        const browser = await puppeteer.launch(options);
        const pages = await browser.pages();
        expect(pages.length).toBe(1);
        if (pages[0].url() !== customUrl)
          await pages[0].waitForNavigation();
        expect(pages[0].url()).toBe(customUrl);
        await browser.close();
      });
    });
    describe('Puppeteer.connect', function() {
      it('should be able to connect multiple times to the same browser', async({server}) => {
        const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
        const browser = await puppeteer.connect({
          browserWSEndpoint: originalBrowser.wsEndpoint()
        });
        const page = await browser.newPage();
        expect(await page.evaluate(() => 7 * 8)).toBe(56);
        browser.disconnect();

        const secondPage = await originalBrowser.newPage();
        expect(await secondPage.evaluate(() => 7 * 6)).toBe(42, 'original browser should still work');
        await originalBrowser.close();
      });
      it('should support ignoreHTTPSErrors option', async({httpsServer}) => {
        const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
        const browserWSEndpoint = originalBrowser.wsEndpoint();

        const browser = await puppeteer.connect({browserWSEndpoint, ignoreHTTPSErrors: true});
        const page = await browser.newPage();
        let error = null;
        const response = await page.goto(httpsServer.EMPTY_PAGE).catch(e => error = e);
        expect(error).toBe(null);
        expect(response.ok()).toBe(true);
        expect(response.securityDetails()).toBeTruthy();
        expect(response.securityDetails().protocol()).toBe('TLS 1.2');
        await page.close();
        await browser.close();
      });
      it('should be able to reconnect to a disconnected browser', async({server}) => {
        const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
        const browserWSEndpoint = originalBrowser.wsEndpoint();
        const page = await originalBrowser.newPage();
        await page.goto(server.PREFIX + '/frames/nested-frames.html');
        originalBrowser.disconnect();

        const browser = await puppeteer.connect({browserWSEndpoint});
        const pages = await browser.pages();
        const restoredPage = pages.find(page => page.url() === server.PREFIX + '/frames/nested-frames.html');
        expect(utils.dumpFrames(restoredPage.mainFrame())).toBeGolden('reconnect-nested-frames.txt');
        expect(await restoredPage.evaluate(() => 7 * 8)).toBe(56);
        await browser.close();
      });
    });
    describe('Puppeteer.executablePath', function() {
      it('should work', async({server}) => {
        const executablePath = puppeteer.executablePath();
        expect(fs.existsSync(executablePath)).toBe(true);
      });
    });
  });

  describe('Browser.Events.disconnected', function() {
    it('should be emitted when: browser gets closed, disconnected or underlying websocket gets closed', async() => {
      const originalBrowser = await puppeteer.launch(defaultBrowserOptions);
      const browserWSEndpoint = originalBrowser.wsEndpoint();
      const remoteBrowser1 = await puppeteer.connect({browserWSEndpoint});
      const remoteBrowser2 = await puppeteer.connect({browserWSEndpoint});

      let disconnectedOriginal = 0;
      let disconnectedRemote1 = 0;
      let disconnectedRemote2 = 0;
      originalBrowser.on('disconnected', () => ++disconnectedOriginal);
      remoteBrowser1.on('disconnected', () => ++disconnectedRemote1);
      remoteBrowser2.on('disconnected', () => ++disconnectedRemote2);

      await Promise.all([
        utils.waitEvent(remoteBrowser2, 'disconnected'),
        remoteBrowser2.disconnect(),
      ]);

      expect(disconnectedOriginal).toBe(0);
      expect(disconnectedRemote1).toBe(0);
      expect(disconnectedRemote2).toBe(1);

      await Promise.all([
        utils.waitEvent(remoteBrowser1, 'disconnected'),
        utils.waitEvent(originalBrowser, 'disconnected'),
        originalBrowser.close(),
      ]);

      expect(disconnectedOriginal).toBe(1);
      expect(disconnectedRemote1).toBe(1);
      expect(disconnectedRemote2).toBe(1);
    });
  });

};
<|endoftext|>


<commit_before>'use babel'

import {Point, CompositeDisposable} from 'atom'
import {diffLines} from 'diff'
import MarkerIndex from 'marker-index'
import temp from 'temp'

class ReviewCommentTracker {
  constructor (editor) {
    this.editor = editor
    this.invalidComments = new Map
    this.subscriptions = new CompositeDisposable()

    // TODO: change to onDidStopChanging
    this.editor.onDidSave(() => {
      let invalidComments = this.invalidComments
      this.invalidComments = new Map
      invalidComments.forEach(({fileContents, rowInFileContents}, id) => {
        this.track(id, fileContents, rowInFileContents)
      })
    })
  }

  destroy () {
    this.subscriptions.dispose()
  }

  track (id, fileContents, rowInFileContents) {
    let index = new MarkerIndex()
    index.insert(id, {row: rowInFileContents, column: 0}, {row: rowInFileContents + 1, column: 0})
    index.setExclusive(id, true)
    let currentRow = 0
    for (let change of diffLines(fileContents, this.editor.getText())) {
      if (change.added) {
        let {inside} = index.splice({row: currentRow, column: 0}, {row: 0, column: 0}, {row: change.count, column: 0})
        if (inside.has(id)) {
          this.invalidComments.set(id, {fileContents, rowInFileContents})
          return
        }
        // TODO: add specs
        // currentRow += change.count
      } else if (change.removed) {
        let {inside} = index.splice({row: currentRow, column: 0}, {row: change.count, column: 0}, {row: 0, column: 0})
        if (inside.has(id)) {
          this.invalidComments.set(id, {fileContents, rowInFileContents})
          return
        }
        // TODO: add specs
        // currentRow = Math.max(0, currentRow - change.count)
      } else {
        currentRow += change.count
      }
    }

    let marker = this.editor.markBufferRange(index.getRange(id), {reversed: true, invalidate: 'inside'})
    let subscription = marker.onDidChange(({isValid}) => {
      if (!isValid) {
        this.invalidComments.set(id, {fileContents, rowInFileContents})
        marker.destroy()
        subscription.dispose()
      }
    })
    this.editor.decorateMarker(marker, {type: 'block', position: 'after'})
  }
}

describe('ReviewCommentTracker', () => {
  let editor, foo

  beforeEach(() => {
    editor = atom.workspace.buildTextEditor()
    foo = new ReviewCommentTracker(editor)
  })

  it("adds a decoration on the same row when the buffers' contents match", () => {
    editor.setText('abc\ndef\nghi')

    foo.track(1, 'abc\ndef\nghi', 1)

    let decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(1)
    expect(decorations[0].getMarker().getHeadBufferPosition()).toEqual([1, 0])
  })

  it("adds a decoration on a translated row in the current buffer corresponding to row in the original buffer", () => {
    editor.setText('def\nghi\nABC\nDEF\nlmn\nopq\nrst')

    foo.track(1, 'abc\ndef\nghi\nlmn\nopq\nrst', 3)

    let decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(1)
    expect(decorations[0].getMarker().getHeadBufferPosition()).toEqual([4, 0])

    editor.setCursorBufferPosition([4, 0])
    editor.insertNewline()

    decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(1)
    expect(decorations[0].getMarker().getHeadBufferPosition()).toEqual([5, 0])
  })

  it("doesn't add a decoration when the comment position doesn't exist anymore, and adds it back on save if it becomes valid again", () => {
    editor.setText('def\nghi\nABC\nDEF\nGHI\nopq\nrst')

    foo.track(1, 'abc\ndef\nghi\nlmn\nopq\nrst', 3)

    let decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(0)

    editor.setSelectedBufferRange([[4, 0], [4, 3]])
    editor.insertText('lmn')
    editor.saveAs(temp.path())

    decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(1)
    expect(decorations[0].getMarker().getHeadBufferPosition()).toEqual([4, 0])
  })

  it("removes decorations as soon as they become invalid and adds them back on save if they become valid again", () => {
    editor.setText('def\nghi\nABC\nDEF\nlmn\nopq\nrst')

    foo.track(1, 'abc\ndef\nghi\nlmn\nopq\nrst', 3)
    editor.setCursorBufferPosition([4, 0])
    editor.deleteLine()

    let decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(0)

    editor.undo()
    editor.saveAs(temp.path())

    decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(1)
    expect(decorations[0].getMarker().getHeadBufferPosition()).toEqual([4, 0])

    editor.redo()

    decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(0)

    editor.save()

    decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(0)

    editor.undo()
    editor.save()

    decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(1)
    expect(decorations[0].getMarker().getHeadBufferPosition()).toEqual([4, 0])
  })
})
<commit_msg>Make test more thorough for invalid comments<commit_after>'use babel'

import {Point, CompositeDisposable} from 'atom'
import {diffLines} from 'diff'
import MarkerIndex from 'marker-index'
import temp from 'temp'

class ReviewCommentTracker {
  constructor (editor) {
    this.editor = editor
    this.invalidComments = new Map
    this.subscriptions = new CompositeDisposable()

    // TODO: change to onDidStopChanging
    this.editor.onDidSave(() => {
      let invalidComments = this.invalidComments
      this.invalidComments = new Map
      invalidComments.forEach(({fileContents, rowInFileContents}, id) => {
        this.track(id, fileContents, rowInFileContents)
      })
    })
  }

  destroy () {
    this.subscriptions.dispose()
  }

  track (id, fileContents, rowInFileContents) {
    let index = new MarkerIndex()
    index.insert(id, {row: rowInFileContents, column: 0}, {row: rowInFileContents + 1, column: 0})
    index.setExclusive(id, true)
    let currentRow = 0
    for (let change of diffLines(fileContents, this.editor.getText())) {
      if (change.added) {
        let {inside} = index.splice({row: currentRow, column: 0}, {row: 0, column: 0}, {row: change.count, column: 0})
        if (inside.has(id)) {
          this.invalidComments.set(id, {fileContents, rowInFileContents})
          return
        }
        currentRow += change.count
      } else if (change.removed) {
        let {inside} = index.splice({row: currentRow, column: 0}, {row: change.count, column: 0}, {row: 0, column: 0})
        if (inside.has(id)) {
          this.invalidComments.set(id, {fileContents, rowInFileContents})
          return
        }
      } else {
        currentRow += change.count
      }
    }

    let marker = this.editor.markBufferRange(index.getRange(id), {reversed: true, invalidate: 'inside'})
    let subscription = marker.onDidChange(({isValid}) => {
      if (!isValid) {
        this.invalidComments.set(id, {fileContents, rowInFileContents})
        marker.destroy()
        subscription.dispose()
      }
    })
    this.editor.decorateMarker(marker, {type: 'block', position: 'after'})
  }
}

describe('ReviewCommentTracker', () => {
  let editor, foo

  beforeEach(() => {
    editor = atom.workspace.buildTextEditor()
    foo = new ReviewCommentTracker(editor)
  })

  it("adds a decoration on the same row when the buffers' contents match", () => {
    editor.setText('abc\ndef\nghi')

    foo.track(1, 'abc\ndef\nghi', 1)

    let decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(1)
    expect(decorations[0].getMarker().getHeadBufferPosition()).toEqual([1, 0])
  })

  it("adds a decoration on a translated row in the current buffer corresponding to row in the original buffer", () => {
    editor.setText('def\nghi\nABC\nDEF\nlmn\nopq\nrst')

    foo.track(1, 'abc\ndef\nghi\nlmn\nopq\nrst', 3)

    let decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(1)
    expect(decorations[0].getMarker().getHeadBufferPosition()).toEqual([4, 0])

    editor.setCursorBufferPosition([4, 0])
    editor.insertNewline()

    decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(1)
    expect(decorations[0].getMarker().getHeadBufferPosition()).toEqual([5, 0])
  })

  it("doesn't add a decoration when the comment position doesn't exist anymore, and adds it back on save if it becomes valid again", () => {
    editor.setText('def\nABC\nDEF\nghi\nGHI\nJKL\nMNO\nopq\nrst')

    foo.track(1, 'abc\ndef\nghi\nlmn\nopq\nrst', 3)

    let decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(0)

    editor.setSelectedBufferRange([[6, 0], [6, 3]])
    editor.insertText('lmn')
    editor.saveAs(temp.path())

    decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(1)
    expect(decorations[0].getMarker().getHeadBufferPosition()).toEqual([6, 0])
  })

  it("removes decorations as soon as they become invalid and adds them back on save if they become valid again", () => {
    editor.setText('def\nghi\nABC\nDEF\nlmn\nopq\nrst')

    foo.track(1, 'abc\ndef\nghi\nlmn\nopq\nrst', 3)
    editor.setCursorBufferPosition([4, 0])
    editor.deleteLine()

    let decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(0)

    editor.undo()
    editor.saveAs(temp.path())

    decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(1)
    expect(decorations[0].getMarker().getHeadBufferPosition()).toEqual([4, 0])

    editor.redo()

    decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(0)

    editor.save()

    decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(0)

    editor.undo()
    editor.save()

    decorations = editor.getDecorations({type: 'block'})
    expect(decorations.length).toBe(1)
    expect(decorations[0].getMarker().getHeadBufferPosition()).toEqual([4, 0])
  })
})
<|endoftext|>


<commit_before>/* eslint-disable no-param-reassign */
import { max } from "d3-array";
import {getTraitFromNode, getDivFromNode} from "../../../util/treeMiscHelpers";

/** get a string to be used as the DOM element ID
 * Note that this cannot have any "special" characters
 */
export const getDomId = (type, strain) => {
  // Replace non-alphanumeric characters with dashes (probably unnecessary)
  const name = `${type}_${strain}`.replace(/(\W+)/g, '-');
  return CSS.escape(name);
};

/**
 * computes a measure of the total number of leaves for each node in
 * the tree, weighting leaves differently if they are inView.
 * Note: function is recursive
 * @param {PhyloNode} node -- root node of the tree
 * @returns {undefined}
 * @sideEffects sets `node.leafCount` {number} for all nodes
 */
export const addLeafCount = (node) => {
  const terminal = !node.n.hasChildren;
  if (terminal && node.inView) {
    node.leafCount = 1;
  } else if (terminal && !node.inView) {
    node.leafCount = 0.15;
  } else {
    node.leafCount = 0;
    for (const child of node.n.children) {
      const phyloChild = child.shell;
      addLeafCount(phyloChild);
      node.leafCount += phyloChild.leafCount;
    }
  }
};

/**
 * this function takes a call back and applies it recursively
 * to all child nodes, including internal nodes
 * @param {PhyloNode} node
 * @param {Function} func - function to apply to each children. Is passed a single argument, the <PhyloNode> of the children.
 */
export const applyToChildren = (phyloNode, func) => {
  func(phyloNode);
  const node = phyloNode.n;
  if ((!node.hasChildren) || (node.children === undefined)) { // in case clade set by URL, terminal hasn't been set yet!
    return;
  }
  for (const child of node.children) {
    applyToChildren(child.shell, func);
  }
};

/** setDisplayOrderRecursively
 * @param {PhyloNode} node
 * @param {int} yCounter
 */
export const setDisplayOrderRecursively = (node, yCounter) => {
  const children = node.n.children; // (redux) tree node
  if (children && children.length) {
    for (let i = children.length - 1; i >= 0; i--) {
      yCounter = setDisplayOrderRecursively(children[i].shell, yCounter);
    }
  } else {
    node.displayOrder = ++yCounter;
    node.displayOrderRange = [yCounter, yCounter];
    return yCounter;
  }
  /* if here, then all children have displayOrders, but we dont. */
  node.displayOrder = children.reduce((acc, d) => acc + d.shell.displayOrder, 0) / children.length;
  node.displayOrderRange = [children[0].shell.displayOrder, children[children.length - 1].shell.displayOrder];
  return yCounter;
};

/** setDisplayOrder
 * given nodes, this fn sets <phyloNode>.displayOrder for each node
 * Nodes are the phyloTree nodes (i.e. node.n is the redux node)
 * Nodes must have parent child links established (via createChildrenAndParents)
 * PhyloTree can subsequently use this information. Accessed by prototypes
 * rectangularLayout, radialLayout, createChildrenAndParents
 * side effects: <phyloNode>.displayOrder (i.e. in the redux node) and <phyloNode>.displayOrderRange
 * @param {Array<PhyloNode>} nodes
 */
export const setDisplayOrder = (nodes) => setDisplayOrderRecursively(nodes[0], 0);

export const formatDivergence = (divergence) => {
  return divergence > 1 ?
    Math.round((divergence + Number.EPSILON) * 1000) / 1000 :
    divergence > 0.01 ?
      Math.round((divergence + Number.EPSILON) * 10000) / 10000 :
      divergence.toExponential(3);
};

/** get the idx of the zoom node (i.e. the in-view root node).
 * This differs depending on which tree is in view so it's helpful to access it
 * by reaching into phyotree to get it
 */
export const getIdxOfInViewRootNode = (node) => {
  return node.shell.that.zoomNode.n.arrayIdx;
};

/**
 * Are the provided nodes within some divergence / time of each other?
 * NOTE: `otherNode` is always closer to the root in the tree than `node`
 */
function isWithinBranchTolerance(node, otherNode, distanceMeasure) {
  if (distanceMeasure === "num_date") {
    /* We calculate the threshold by reaching into phylotree to extract the date range of the dataset
    and then split the data into ~50 slices. This could be refactored to not reach into phylotree. */
    const tolerance = (node.shell.that.dateRange[1]-node.shell.that.dateRange[0])/50;
    return (getTraitFromNode(node, "num_date") - tolerance < getTraitFromNode(otherNode, "num_date"));
  }
  /* Compute the divergence tolerance similarly to above. This uses the approach used to compute the
  x-axis grid within phyotree, and could be refactored into a helper function. Note that we don't store
  the maximum divergence on the tree so we use the in-view max instead */
  const tolerance = (node.shell.that.xScale.domain()[1] - node.shell.that.nodes[0].depth)/50;
  return (getDivFromNode(node) - tolerance < getDivFromNode(otherNode));
}

/**
 * Given a `node`, get the parent, grandparent etc node which is beyond some
 * branch length threshold (either divergence or time). This is useful for finding the node
 * beyond a polytomy, or polytomy-like structure
 * @param {object} node - tree node
 * @param {string} getParentBeyondPolytomy -- 'num_date' or 'div'
 * @returns {object} the closest node up the tree (towards the root) which is beyond
 * some threshold
 */
export const getParentBeyondPolytomy = (node, distanceMeasure) => {
  let potentialNode = node.parent;
  while (isWithinBranchTolerance(node, potentialNode, distanceMeasure)) {
    if (potentialNode === potentialNode.parent) break; // root node of tree
    potentialNode = potentialNode.parent;
  }
  return potentialNode;
};

/**
 * Prior to Jan 2020, the divergence measure was always "subs per site per year"
 * however certain datasets changed this to "subs per year" across entire sequence.
 * This distinction is not set in the JSON, so in order to correctly display the rate
 * we will "guess" this here. A future augur update will export this in a JSON key,
 * removing the need to guess.
 */
export function guessAreMutationsPerSite(scale) {
  const maxDivergence = max(scale.domain());
  return maxDivergence <= 5;
}
<commit_msg>[explode] add padding between subtrees<commit_after>/* eslint-disable no-param-reassign */
import { max } from "d3-array";
import {getTraitFromNode, getDivFromNode} from "../../../util/treeMiscHelpers";

/** get a string to be used as the DOM element ID
 * Note that this cannot have any "special" characters
 */
export const getDomId = (type, strain) => {
  // Replace non-alphanumeric characters with dashes (probably unnecessary)
  const name = `${type}_${strain}`.replace(/(\W+)/g, '-');
  return CSS.escape(name);
};

/**
 * computes a measure of the total number of leaves for each node in
 * the tree, weighting leaves differently if they are inView.
 * Note: function is recursive
 * @param {PhyloNode} node -- root node of the tree
 * @returns {undefined}
 * @sideEffects sets `node.leafCount` {number} for all nodes
 */
export const addLeafCount = (node) => {
  const terminal = !node.n.hasChildren;
  if (terminal && node.inView) {
    node.leafCount = 1;
  } else if (terminal && !node.inView) {
    node.leafCount = 0.15;
  } else {
    node.leafCount = 0;
    for (const child of node.n.children) {
      const phyloChild = child.shell;
      addLeafCount(phyloChild);
      node.leafCount += phyloChild.leafCount;
    }
  }
};

/**
 * this function takes a call back and applies it recursively
 * to all child nodes, including internal nodes
 * @param {PhyloNode} node
 * @param {Function} func - function to apply to each children. Is passed a single argument, the <PhyloNode> of the children.
 */
export const applyToChildren = (phyloNode, func) => {
  func(phyloNode);
  const node = phyloNode.n;
  if ((!node.hasChildren) || (node.children === undefined)) { // in case clade set by URL, terminal hasn't been set yet!
    return;
  }
  for (const child of node.children) {
    applyToChildren(child.shell, func);
  }
};

/** setDisplayOrderRecursively
 * @param {PhyloNode} node
 * @param {int} yCounter
 * @sideeffect modifies node.displayOrder and node.displayOrderRange
 * @returns {int} current yCounter after assignment to the tree originating from `node`
 */
export const setDisplayOrderRecursively = (node, yCounter) => {
  const children = node.n.children; // (redux) tree node
  if (children && children.length) {
    for (let i = children.length - 1; i >= 0; i--) {
      yCounter = setDisplayOrderRecursively(children[i].shell, yCounter);
    }
  } else {
    node.displayOrder = ++yCounter;
    node.displayOrderRange = [yCounter, yCounter];
    return yCounter;
  }
  /* if here, then all children have displayOrders, but we dont. */
  node.displayOrder = children.reduce((acc, d) => acc + d.shell.displayOrder, 0) / children.length;
  node.displayOrderRange = [children[0].shell.displayOrder, children[children.length - 1].shell.displayOrder];
  return yCounter;
};

/**
 * heuristic function to return the appropriate spacing between subtrees for a given tree
 * the returned value is to be interpreted as a count of the number of tips that would
 * otherwise fit in the gap
 */
function _getSpaceBetweenSubtrees(numSubtrees, numTips) {
  if (numSubtrees===1 || numTips<10) {
    return 0;
  }
  if (numSubtrees*2 > numTips) {
    return 0;
  }
  return numTips/20; /* note that it's not actually 5% of vertical space,
                     as the final max yCount = numTips + numSubtrees*numTips/20 */
}

/** setDisplayOrder
 * given nodes, this fn sets <phyloNode>.displayOrder for each node
 * Nodes are the phyloTree nodes (i.e. node.n is the redux node)
 * Nodes must have parent child links established (via createChildrenAndParents)
 * PhyloTree can subsequently use this information. Accessed by prototypes
 * rectangularLayout, radialLayout, createChildrenAndParents
 * side effects: <phyloNode>.displayOrder (i.e. in the redux node) and <phyloNode>.displayOrderRange
 * @param {Array<PhyloNode>} nodes
 * @returns {undefined}
 */
export const setDisplayOrder = (nodes) => {
  const numSubtrees = nodes[0].n.children.filter((n) => n.fullTipCount!==0).length;
  const numTips = nodes[0].n.fullTipCount;
  const spaceBetweenSubtrees = _getSpaceBetweenSubtrees(numSubtrees, numTips);
  let yCounter = 0;
  /* iterate through each subtree, and add padding between each */
  for (const subtree of nodes[0].n.children) {
    yCounter = setDisplayOrderRecursively(nodes[subtree.arrayIdx], yCounter);
    yCounter+=spaceBetweenSubtrees;
  }
  /* note that nodes[0] is a dummy node holding each subtree */
  nodes[0].displayOrder = undefined;
  nodes[0].displayOrderRange = [undefined, undefined];
};

export const formatDivergence = (divergence) => {
  return divergence > 1 ?
    Math.round((divergence + Number.EPSILON) * 1000) / 1000 :
    divergence > 0.01 ?
      Math.round((divergence + Number.EPSILON) * 10000) / 10000 :
      divergence.toExponential(3);
};

/** get the idx of the zoom node (i.e. the in-view root node).
 * This differs depending on which tree is in view so it's helpful to access it
 * by reaching into phyotree to get it
 */
export const getIdxOfInViewRootNode = (node) => {
  return node.shell.that.zoomNode.n.arrayIdx;
};

/**
 * Are the provided nodes within some divergence / time of each other?
 * NOTE: `otherNode` is always closer to the root in the tree than `node`
 */
function isWithinBranchTolerance(node, otherNode, distanceMeasure) {
  if (distanceMeasure === "num_date") {
    /* We calculate the threshold by reaching into phylotree to extract the date range of the dataset
    and then split the data into ~50 slices. This could be refactored to not reach into phylotree. */
    const tolerance = (node.shell.that.dateRange[1]-node.shell.that.dateRange[0])/50;
    return (getTraitFromNode(node, "num_date") - tolerance < getTraitFromNode(otherNode, "num_date"));
  }
  /* Compute the divergence tolerance similarly to above. This uses the approach used to compute the
  x-axis grid within phyotree, and could be refactored into a helper function. Note that we don't store
  the maximum divergence on the tree so we use the in-view max instead */
  const tolerance = (node.shell.that.xScale.domain()[1] - node.shell.that.nodes[0].depth)/50;
  return (getDivFromNode(node) - tolerance < getDivFromNode(otherNode));
}

/**
 * Given a `node`, get the parent, grandparent etc node which is beyond some
 * branch length threshold (either divergence or time). This is useful for finding the node
 * beyond a polytomy, or polytomy-like structure
 * @param {object} node - tree node
 * @param {string} getParentBeyondPolytomy -- 'num_date' or 'div'
 * @returns {object} the closest node up the tree (towards the root) which is beyond
 * some threshold
 */
export const getParentBeyondPolytomy = (node, distanceMeasure) => {
  let potentialNode = node.parent;
  while (isWithinBranchTolerance(node, potentialNode, distanceMeasure)) {
    if (potentialNode === potentialNode.parent) break; // root node of tree
    potentialNode = potentialNode.parent;
  }
  return potentialNode;
};

/**
 * Prior to Jan 2020, the divergence measure was always "subs per site per year"
 * however certain datasets changed this to "subs per year" across entire sequence.
 * This distinction is not set in the JSON, so in order to correctly display the rate
 * we will "guess" this here. A future augur update will export this in a JSON key,
 * removing the need to guess.
 */
export function guessAreMutationsPerSite(scale) {
  const maxDivergence = max(scale.domain());
  return maxDivergence <= 5;
}
<|endoftext|>


<commit_before>// put decelerations here that you don't want overriden by "should"
// You need to do should(<obj>) to use these
var should = require('should'); // overrides Object.prototype

// put declerations here that you want to use like <obj>.should
var QueryGenerator = require('../lib/generators/queryGenerator');

describe('QueryGenerator', function () {
    describe('ctor', function () {

    });

    describe('#startsWith', function () {
        it('should produce a multi match query of type phrase prefix on the passed in fields', function (done) {
            var expected = { multi_match: { query: 'test', fields: ['field1', 'field2'], type: 'phrase_prefix'}};

            new QueryGenerator(function (actual) {
                actual.should.eql(expected);
                done();
            }, ['field1', 'field2']).startsWith('test');
        });
    });
});<commit_msg>Add test<commit_after>// put decelerations here that you don't want overriden by "should"
// You need to do should(<obj>) to use these
var should = require('should'); // overrides Object.prototype

// put declerations here that you want to use like <obj>.should
var QueryGenerator = require('../lib/generators/queryGenerator');

describe('QueryGenerator', function () {
    describe('ctor', function () {

    });

    describe('#startsWith', function () {
        it('should produce a multi match query of type phrase prefix on the passed in fields', function (done) {
            var expected = { multi_match: { query: 'test', fields: ['field1', 'field2'], type: 'phrase_prefix'}};

            new QueryGenerator(function (actual) {
                actual.should.eql(expected);
                done();
            }, ['field1', 'field2']).startsWith('test');
        });
    });

    describe('#beMissing', function () {
        it('should produce a must not exists query on the passed in fields', function (done) {
            var expected = {
                bool: {
                    must_not: {
                        exists: {
                            field: ['field1', 'field2']
                        }
                    }
                }
            };

            new QueryGenerator(function (actual) {
                actual.should.eql(expected);
                done();
            }, ['field1', 'field2']).beMissing();
        });
    });
});<|endoftext|>