<commit_before>import test from 'tape'
import {createAction} from 'redux-actions'
import Choko, {BOOT} from '../src/lib/bootstrap'

test('Use redux-actions with redux-promise to fire async side-effect actions in middlwares', (assert) => {
  const AFTER_BOOT = 'choko/core/test/AFTER_BOOT'

  const afterBootAction = createAction(AFTER_BOOT, async (id) => {

    // Mocking an Async IO call, like an API fetch.
    const asyncContentFromIO = await new Promise((resolve, reject) => {
      setTimeout(() => {
        if (id === 'foo') {
          resolve('baz')
        }
        else {
          reject('id is not foo')
        }
      }, 1000)
    })

    // Payload of the action.
    return {
      foo: asyncContentFromIO
    }
  })

  const initialState = {
    foo: 'bar'
  }

  // React to an action with less boilerplaing.
  const handlers = {

    [AFTER_BOOT]: (state, action) => {
      assert.pass('Reducer called')

      return {
        ...state,
        foo: action.payload.foo
      }
    }

  }

  const testModule = {

    // Less boilerplating.
    reducer: handlers,

    middleware({getState, dispatch}) {
      // Note the async keyword.
      return next => async (action) => {

        if (action.type === BOOT) {
          assert.pass('Middleware called')

          // Pause till this async side-effect actions
          // is fully dispatched.
          const sideEffect = await dispatch(afterBootAction('foo'))

          console.log(getState(), 'NEW STATE')
        }

        return next(action)
      }
    }

  }

  const modules = [
    testModule
  ]

  const app = Choko(initialState, modules)

  assert.equal(
    app.store.getState().foo,
    'baz',
    "Async side-effect was handled"
  )

  assert.end()
})<commit_msg>Removed console.log function.<commit_after>import test from 'tape'
import {createAction} from 'redux-actions'
import Choko, {BOOT} from '../src/lib/bootstrap'

test('Use redux-actions with redux-promise to fire async side-effect actions in middlwares', (assert) => {
  const AFTER_BOOT = 'choko/core/test/AFTER_BOOT'

  const afterBootAction = createAction(AFTER_BOOT, async (id) => {

    // Mocking an Async IO call, like an API fetch.
    const asyncContentFromIO = await new Promise((resolve, reject) => {
      setTimeout(() => {
        if (id === 'foo') {
          resolve('baz')
        }
        else {
          reject('id is not foo')
        }
      }, 1000)
    })

    // Payload of the action.
    return {
      foo: asyncContentFromIO
    }
  })

  const initialState = {
    foo: 'bar'
  }

  // React to an action with less boilerplaing.
  const handlers = {

    [AFTER_BOOT]: (state, action) => {
      assert.pass('Reducer called')

      return {
        ...state,
        foo: action.payload.foo
      }
    }

  }

  const testModule = {

    // Less boilerplating.
    reducer: handlers,

    middleware({getState, dispatch}) {
      // Note the async keyword.
      return next => async (action) => {

        if (action.type === BOOT) {
          assert.pass('Middleware called')

          // Pause till this async side-effect actions
          // is fully dispatched.
          const sideEffect = await dispatch(afterBootAction('foo'))
        }

        return next(action)
      }
    }

  }

  const modules = [
    testModule
  ]

  const app = Choko(initialState, modules)

  assert.equal(
    app.store.getState().foo,
    'baz',
    "Async side-effect was handled"
  )

  assert.end()
})<|endoftext|>


<commit_before>import { execSync } from 'child_process';
import fs from 'fs-promise';
import os from 'os';
import path from 'path';

import { expect } from 'chai';
import proxyquire from 'proxyquire';

import installDeps from '../../src/util/install-dependencies';
import readPackageJSON from '../../src/util/read-package-json';

const installer = process.argv.find(arg => arg.startsWith('--installer=')) || '--installer=system default';
const forge = proxyquire.noCallThru().load('../../src/api', {
  './install': async () => {},
});

describe(`electron-forge API (with installer=${installer.substr(12)})`, () => {
  let dirID = Date.now();
  const forLintingMethod = (lintStyle) => {
    describe(`init (with lintStyle=${lintStyle})`, () => {
      let dir;

      before(async () => {
        dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}`);
        dirID += 1;
        await fs.remove(dir);
        await forge.init({
          dir,
          lintstyle: lintStyle,
        });
      });

      it('should create a new folder with a npm module inside', async () => {
        expect(await fs.exists(dir), 'the target dir should have been created').to.equal(true);
        expect(await fs.exists(path.resolve(dir, 'package.json')), 'the package.json file should exist').to.equal(true);
      });

      it('should have initialized a git repository', async () => {
        expect(await fs.exists(path.resolve(dir, '.git')), 'the .git folder should exist').to.equal(true);
      });

      it('should have installed the initial node_modules', async () => {
        expect(await fs.exists(path.resolve(dir, 'node_modules')), 'node_modules folder should exist').to.equal(true);
        expect(await fs.exists(path.resolve(dir, 'node_modules/electron-prebuilt-compile')), 'electron-prebuilt-compile should exist').to.equal(true);
        expect(await fs.exists(path.resolve(dir, 'node_modules/babel-core')), 'babel-core should exist').to.equal(true);
      });

      describe('lint', () => {
        it('should initially pass the linting process', () => forge.lint({ dir }));
      });

      after(() => fs.remove(dir));
    });
  };
  forLintingMethod('airbnb');
  forLintingMethod('standard');

  describe('init (with custom templater)', () => {
    let dir;

    before(async () => {
      dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}`);
      dirID += 1;
      await fs.remove(dir);
      execSync('npm link', {
        cwd: path.resolve(__dirname, '../fixture/custom_init'),
      });
      await forge.init({
        dir,
        template: 'dummy',
      });
    });

    it('should create dot files correctly', async () => {
      expect(await fs.exists(dir), 'the target dir should have been created').to.equal(true);
      expect(await fs.exists(path.resolve(dir, '.bar')), 'the .bar file should exist').to.equal(true);
    });

    it('should create deep files correctly', async () => {
      expect(await fs.exists(path.resolve(dir, 'src/foo.js')), 'the src/foo.js file should exist').to.equal(true);
    });

    describe('lint', () => {
      it('should initially pass the linting process', () => forge.lint({ dir }));
    });

    after(async () => {
      await fs.remove(dir);
      execSync('npm unlink', {
        cwd: path.resolve(__dirname, '../fixture/custom_init'),
      });
    });
  });

  describe('init (with built-in templater)', () => {
    let dir;

    before(async () => {
      dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}`);
      dirID += 1;
      await fs.remove(dir);
    });

    it('should succeed in initializing', async () => {
      await forge.init({
        dir,
        template: 'react-typescript',
      });
    });

    it('should succeed in initializing an already initialized directory', async () => {
      await forge.init({
        dir,
        template: 'react-typescript',
      });
    });

    it('should add a dependency on react', async () => {
      expect(Object.keys(require(path.resolve(dir, 'package.json')).dependencies)).to.contain('react');
    });

    after(async () => {
      await fs.remove(dir);
    });
  });

  describe('init (with a nonexistent templater)', () => {
    let dir;

    before(async () => {
      dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}`);
      dirID += 1;
      await fs.remove(dir);
    });

    it('should fail in initializing', async () => {
      await expect(forge.init({
        dir,
        template: 'does-not-exist',
      })).to.eventually.be.rejectedWith('Failed to locate custom template');
    });

    after(async () => {
      await fs.remove(dir);
    });
  });

  describe('after init', () => {
    let dir;

    before(async () => {
      dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}/electron-forge-test`);
      dirID += 1;
      await forge.init({ dir });

      const packageJSON = await readPackageJSON(dir);
      packageJSON.name = 'testapp';
      packageJSON.productName = 'Test App';
      packageJSON.config.forge.electronPackagerConfig.asar = false;
      packageJSON.config.forge.windowsStoreConfig.packageName = 'TestApp';
      packageJSON.homepage = 'http://www.example.com/';
      packageJSON.author = 'Test Author';
      await fs.writeFile(path.resolve(dir, 'package.json'), JSON.stringify(packageJSON, null, 2));
    });

    it('can package without errors', async () => {
      await forge.package({ dir });
    });

    it('can package to outDir without errors', async () => {
      const outDir = `${dir}/foo`;

      expect(await fs.exists(outDir)).to.equal(false);

      await forge.package({ dir, outDir });

      expect(await fs.exists(outDir)).to.equal(true);
    });

    it('can package without errors with native pre-gyp deps installed', async () => {
      await installDeps(dir, ['ref']);
      await forge.package({ dir });
    });

    describe('after package', () => {
      let resourcesPath = 'Test App.app/Contents/Resources';
      if (process.platform !== 'darwin') {
        resourcesPath = 'resources';
      }

      it('should have deleted the forge config from the packaged app', async () => {
        const cleanPackageJSON = await readPackageJSON(
          path.resolve(dir, 'out', `Test App-${process.platform}-${process.arch}`, resourcesPath, 'app')
        );
        expect(cleanPackageJSON).to.not.have.deep.property('config.forge');
      });

      it('should not affect the actual forge config', async () => {
        const normalPackageJSON = await readPackageJSON(dir);
        expect(normalPackageJSON).to.have.deep.property('config.forge');
      });

      let targets = [];
      if (fs.existsSync(path.resolve(__dirname, `../../src/makers/${process.platform}`))) {
        targets = fs.readdirSync(path.resolve(__dirname, `../../src/makers/${process.platform}`)).map(file => path.parse(file).name);
      }
      const genericTargets = fs.readdirSync(path.resolve(__dirname, '../../src/makers/generic')).map(file => path.parse(file).name);
      const testMakeTarget = async function testMakeTarget(target, options = {}) {
        await describe(`make (with target=${target})`, async () => {
          await before(async () => {
            const packageJSON = await readPackageJSON(dir);
            packageJSON.config.forge.make_targets[process.platform] = [target];
            await fs.writeFile(path.resolve(dir, 'package.json'), JSON.stringify(packageJSON));
          });

          await it(`successfully makes for config: ${JSON.stringify(options, 2)}`, async () => {
            await forge.make(options);
          });
        });
      };

      [].concat(targets).concat(genericTargets).forEach(async (target) => {
        await testMakeTarget(target, { dir, skipPackage: true });
      });

      testMakeTarget('zip', { dir, skipPackage: true, outDir: `${dir}/foo` });
    });

    after(() => fs.remove(dir));
  });
});
<commit_msg>fix(tests): stop awaiting mocha and ensure we clean up out dirs<commit_after>import { execSync } from 'child_process';
import fs from 'fs-promise';
import os from 'os';
import path from 'path';

import { expect } from 'chai';
import proxyquire from 'proxyquire';

import installDeps from '../../src/util/install-dependencies';
import readPackageJSON from '../../src/util/read-package-json';

const installer = process.argv.find(arg => arg.startsWith('--installer=')) || '--installer=system default';
const forge = proxyquire.noCallThru().load('../../src/api', {
  './install': async () => {},
});

describe(`electron-forge API (with installer=${installer.substr(12)})`, () => {
  let dirID = Date.now();
  const forLintingMethod = (lintStyle) => {
    describe(`init (with lintStyle=${lintStyle})`, () => {
      let dir;

      before(async () => {
        dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}`);
        dirID += 1;
        await fs.remove(dir);
        await forge.init({
          dir,
          lintstyle: lintStyle,
        });
      });

      it('should create a new folder with a npm module inside', async () => {
        expect(await fs.exists(dir), 'the target dir should have been created').to.equal(true);
        expect(await fs.exists(path.resolve(dir, 'package.json')), 'the package.json file should exist').to.equal(true);
      });

      it('should have initialized a git repository', async () => {
        expect(await fs.exists(path.resolve(dir, '.git')), 'the .git folder should exist').to.equal(true);
      });

      it('should have installed the initial node_modules', async () => {
        expect(await fs.exists(path.resolve(dir, 'node_modules')), 'node_modules folder should exist').to.equal(true);
        expect(await fs.exists(path.resolve(dir, 'node_modules/electron-prebuilt-compile')), 'electron-prebuilt-compile should exist').to.equal(true);
        expect(await fs.exists(path.resolve(dir, 'node_modules/babel-core')), 'babel-core should exist').to.equal(true);
      });

      describe('lint', () => {
        it('should initially pass the linting process', () => forge.lint({ dir }));
      });

      after(() => fs.remove(dir));
    });
  };
  forLintingMethod('airbnb');
  forLintingMethod('standard');

  describe('init (with custom templater)', () => {
    let dir;

    before(async () => {
      dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}`);
      dirID += 1;
      await fs.remove(dir);
      execSync('npm link', {
        cwd: path.resolve(__dirname, '../fixture/custom_init'),
      });
      await forge.init({
        dir,
        template: 'dummy',
      });
    });

    it('should create dot files correctly', async () => {
      expect(await fs.exists(dir), 'the target dir should have been created').to.equal(true);
      expect(await fs.exists(path.resolve(dir, '.bar')), 'the .bar file should exist').to.equal(true);
    });

    it('should create deep files correctly', async () => {
      expect(await fs.exists(path.resolve(dir, 'src/foo.js')), 'the src/foo.js file should exist').to.equal(true);
    });

    describe('lint', () => {
      it('should initially pass the linting process', () => forge.lint({ dir }));
    });

    after(async () => {
      await fs.remove(dir);
      execSync('npm unlink', {
        cwd: path.resolve(__dirname, '../fixture/custom_init'),
      });
    });
  });

  describe('init (with built-in templater)', () => {
    let dir;

    before(async () => {
      dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}`);
      dirID += 1;
      await fs.remove(dir);
    });

    it('should succeed in initializing', async () => {
      await forge.init({
        dir,
        template: 'react-typescript',
      });
    });

    it('should succeed in initializing an already initialized directory', async () => {
      await forge.init({
        dir,
        template: 'react-typescript',
      });
    });

    it('should add a dependency on react', async () => {
      expect(Object.keys(require(path.resolve(dir, 'package.json')).dependencies)).to.contain('react');
    });

    after(async () => {
      await fs.remove(dir);
    });
  });

  describe('init (with a nonexistent templater)', () => {
    let dir;

    before(async () => {
      dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}`);
      dirID += 1;
      await fs.remove(dir);
    });

    it('should fail in initializing', async () => {
      await expect(forge.init({
        dir,
        template: 'does-not-exist',
      })).to.eventually.be.rejectedWith('Failed to locate custom template');
    });

    after(async () => {
      await fs.remove(dir);
    });
  });

  describe('after init', () => {
    let dir;

    before(async () => {
      dir = path.resolve(os.tmpdir(), `electron-forge-test-${dirID}/electron-forge-test`);
      dirID += 1;
      await forge.init({ dir });

      const packageJSON = await readPackageJSON(dir);
      packageJSON.name = 'testapp';
      packageJSON.productName = 'Test App';
      packageJSON.config.forge.electronPackagerConfig.asar = false;
      packageJSON.config.forge.windowsStoreConfig.packageName = 'TestApp';
      packageJSON.homepage = 'http://www.example.com/';
      packageJSON.author = 'Test Author';
      await fs.writeFile(path.resolve(dir, 'package.json'), JSON.stringify(packageJSON, null, 2));
    });

    it('can package to outDir without errors', async () => {
      const outDir = `${dir}/foo`;

      expect(await fs.exists(outDir)).to.equal(false);

      await forge.package({ dir, outDir });

      expect(await fs.exists(outDir)).to.equal(true);
    });

    it('can make from custom outDir without errors', async () => {
      const packageJSON = await readPackageJSON(dir);
      packageJSON.config.forge.make_targets[process.platform] = ['zip'];
      await fs.writeFile(path.resolve(dir, 'package.json'), JSON.stringify(packageJSON));

      await forge.make({ dir, skipPackage: true, outDir: `${dir}/foo` });

      // Cleanup here to ensure things dont break in the make tests
      await fs.remove(path.resolve(dir, 'foo'));
      await fs.remove(path.resolve(dir, 'out'));
    });

    it('can package without errors with native pre-gyp deps installed', async () => {
      await installDeps(dir, ['ref']);
      await forge.package({ dir });
      await fs.remove(path.resolve(dir, 'node_modules/ref'));
    });

    it('can package without errors', async () => {
      await forge.package({ dir });
    });

    describe('after package', () => {
      let resourcesPath = 'Test App.app/Contents/Resources';
      if (process.platform !== 'darwin') {
        resourcesPath = 'resources';
      }

      it('should have deleted the forge config from the packaged app', async () => {
        const cleanPackageJSON = await readPackageJSON(
          path.resolve(dir, 'out', `Test App-${process.platform}-${process.arch}`, resourcesPath, 'app')
        );
        expect(cleanPackageJSON).to.not.have.deep.property('config.forge');
      });

      it('should not affect the actual forge config', async () => {
        const normalPackageJSON = await readPackageJSON(dir);
        expect(normalPackageJSON).to.have.deep.property('config.forge');
      });

      let targets = [];
      if (fs.existsSync(path.resolve(__dirname, `../../src/makers/${process.platform}`))) {
        targets = fs.readdirSync(path.resolve(__dirname, `../../src/makers/${process.platform}`)).map(file => path.parse(file).name);
      }
      const genericTargets = fs.readdirSync(path.resolve(__dirname, '../../src/makers/generic')).map(file => path.parse(file).name);

      const testMakeTarget = function testMakeTarget(target, ...options) {
        describe(`make (with target=${target})`, async () => {
          before(async () => {
            const packageJSON = await readPackageJSON(dir);
            packageJSON.config.forge.make_targets[process.platform] = [target];
            await fs.writeFile(path.resolve(dir, 'package.json'), JSON.stringify(packageJSON));
          });

          options.forEach((optionsFetcher) => {
            it(`successfully makes for config: ${JSON.stringify(optionsFetcher(), 2)}`, async () => {
              await forge.make(optionsFetcher());
            });
          });
        });
      };

      [].concat(targets).concat(genericTargets).forEach((target) => {
        const testOptions = [() => ({ dir, skipPackage: true })];
        testMakeTarget(target, ...testOptions);
      });
    });

    after(() => fs.remove(dir));
  });
});
<|endoftext|>


<commit_before>'use strict';

const Promise = require('bluebird');
const BRIXX = require('brixx');
const sinon = require('sinon');
const TEST = require('./');
const LongPollMixin = require('../lib/sqs-long-poll');

TEST.suite(function runTwiceWithoutError(suite) {
	const createLongPoll = BRIXX.factory(LongPollMixin);
	const queueURL = 'http://some.domain.net/url';
	const sqs = Object.freeze({
		receiveMessage: sinon.spy(function mockReceiveMessage(params, callback) {
			// Call the callback async like the real thing.
			setTimeout(callback, 12);
		})
	});
	const subject = createLongPoll({
		sqs: sqs,
		queueURL: queueURL
	});
	const messages = [];

	suite.before(function () {
		return new Promise(function (resolve, reject) {
			const timeout = setTimeout(function () {
				reject(new Error('timeout: runTwiceWithoutError'));
			}, 60);

			subject.on('message:received', function (message) {
				messages.push(message);
				if (messages.length >= 2) {
					clearTimeout(timeout);
					subject.stop();
					resolve();
				}
			});

			subject.start();
		});
	});

	suite.test(function (test) {
		test('receiveMessage called twice', function (t) {
			t.plan(1);
			t.equal(sqs.receiveMessage.callCount, 2);
		});
	});
});
<commit_msg>SQS long poll tests<commit_after>'use strict';

const Promise = require('bluebird');
const BRIXX = require('brixx');
const sinon = require('sinon');
const TEST = require('./');
const LongPollMixin = require('../lib/sqs-long-poll');
const createLongPoll = BRIXX.factory(LongPollMixin);
const queueURL = 'http://some.domain.net/url';
const Body = '{"pattern":1,"payload":1}';

TEST.suite(function runTwiceWithoutError(suite) {
	const sqs = Object.freeze({
		receiveMessage: sinon.spy(function mockReceiveMessage(params, callback) {
			// Call the callback async like the real thing.
			setTimeout(function () {
				callback(null, {
					Body: Body
				});
			}, 12);
		})
	});
	const errorHandler = sinon.spy();
	const subject = createLongPoll({
		sqs: sqs,
		queueURL: queueURL
	});
	const messages = [];

	suite.before(function () {
		return new Promise(function (resolve, reject) {
			const timeout = setTimeout(function () {
				reject(new Error('timeout: runTwiceWithoutError'));
			}, 60);

			subject.on('error', errorHandler);

			subject.on('message:received', function (message) {
				messages.push(message);
				if (messages.length >= 2) {
					clearTimeout(timeout);
					subject.stop();
					resolve();
				}
			});

			subject.start();
		});
	});

	suite.test(function (test) {
		const args = sqs.receiveMessage.args;
		const paramCalls = Object.freeze(args.map(function (args) {
			return args[0];
		}));

		test('receiveMessage called twice', function (t) {
			t.plan(1);
			t.equal(sqs.receiveMessage.callCount, 2);
		});
		test('receiveMessage called with parameters', function (t) {
			t.plan(6);
			paramCalls.forEach(function (params) {
				t.equal(params.QueueURL, queueURL);
				t.equal(params.MaxNumberOfMessages, 1);
				t.equal(params.WaitTimeSeconds, 20);
			});
		});
		test('receives messages', function (t) {
			const keys = [
				'MessageId',
				'ReceiptHandle',
				'MD5OfBody',
				'Body',
				'Attributes',
				'pattern',
				'payload'
			];
			t.plan(14);
			messages.forEach(function (message) {
				Object.keys(message.toJSON()).forEach(function (key) {
					t.ok(keys.indexOf(key) >= 0, 'key: ' + key);
				});
			});
		});
		test('error is not emitted', function (t) {
			t.plan(1);
			t.equal(errorHandler.callCount, 0);
		});
	});
});

TEST.suite(function runTwiceWithError(suite) {
	const error = new Error('runTwiceWithError');
	const sqs = Object.freeze({
		receiveMessage: sinon.spy(function mockReceiveMessage(params, callback) {
			// Call the callback async like the real thing.
			setTimeout(function () {
				callback(error);
			}, 12);
		})
	});
	const messageReceivedHandler = sinon.spy();
	const subject = createLongPoll({
		sqs: sqs,
		queueURL: queueURL
	});
	const errors = [];

	suite.before(function () {
		return new Promise(function (resolve) {
			setTimeout(function () {
				resolve();
			}, 36);

			subject.on('message:received', messageReceivedHandler);

			subject.on('error', function (error) {
				errors.push(error);
			});

			subject.start();
		});
	});

	suite.test(function (test) {
		test('receiveHandler is never called', function (t) {
			t.plan(1);
			t.equal(messageReceivedHandler.callCount, 0);
		});
		test('error handler called once', function (t) {
			t.plan(1);
			t.equal(errors.length, 1);
		});
		test('error handler called with an error', function (t) {
			t.plan(1);
			t.equal(errors[0], error, 'throw the error');
		});
	});
});
<|endoftext|>


<commit_before>angular.module('formDataObject', [])

// Transforms JSON to multipart/form-data
.factory('FormDataObject', function() {
  return function(data) {
    var fd = new FormData();
    angular.forEach(data, function(value, key) {
      if (key == "images") {
        for (var i = 0; i < value.length; i++) {
          fd.append("image", value[i]);
        }
      } else {
        fd.append(key, value);
      }
    });
    return fd;
  };
});
<commit_msg>Edited formDataObject.js to send multiple files to match the API key:value pair.<commit_after>angular.module('formDataObject', [])

// Transforms JSON to multipart/form-data
.factory('FormDataObject', function() {
  return function(data) {
    var fd = new FormData();
    angular.forEach(data, function(value, key) {
      if (key == "images[]") {
        for (var i = 0; i < value.length; i++) {
          fd.append("images[]", value[i]);
        }
      } else {
        fd.append(key, value);
      }
    });
    return fd;
  };
});
<|endoftext|>


<commit_before>import BackToTopicIcon from '../../../../static/asset/article-back-to-topic.svg'
import Link from 'react-router/lib/Link'
import PropTypes from 'prop-types'
import React from 'react'
import styled from 'styled-components'
import { BookmarkWidget } from '@twreporter/registration'
import { LINK_PREFIX } from '../../../constants/link-prefix'
import { colors, typography }  from '../../../themes/common-variables'
import { articleLayout } from '../../../themes/layout'

const toolsOffset = 20

const Container = styled.div`
  display: inline-block;
  position: fixed;
  right: 50%;
  bottom: 50%;
  color: ${colors.secondaryColor};
  font-size: ${typography.font.size.xSmall};
  z-index: 999;
  transform: translate(${(articleLayout.desktop.width.large - toolsOffset)/2}px, -50%);
  visibility: ${(props) => props.toShow ? 'visible' : 'hidden'};
  opacity: ${(props) => props.toShow ? 1 : 0};
  transition: opacity 0.5s linear;

  a, a:visited, a:link, a:active {
    color: ${colors.secondaryColor};
    font-size: ${typography.font.size.xSmall};
  }
`

const WidgetContainer = styled.div`
  box-sizing: border-box;
  margin-top: 10px;
  text-align: center;
`

const IconContainer = styled.div`
  width: 100%;
  text-align: center;
  svg {
    width: 20px;
  }
`

const BackToTopicContainer = styled.div`
  span {
    display: block;
    margin-bottom: 10px;
    font-weight: $font-weight-normal;
  }
`

const BackToTopic = (props) => (
  <Link to={`${LINK_PREFIX.TOPICS}${props.topicSlug}`} title={props.topicTitle}>
    <BackToTopicContainer>
      回到專題
      <IconContainer>
        <BackToTopicIcon />
      </IconContainer>
    </BackToTopicContainer>
  </Link>
)

BackToTopic.propTypes = {
  topicSlug: PropTypes.string.isRequired,
  topicTitle: PropTypes.string.isRequired
}

class DesktopArticleTools extends React.PureComponent {
  render() {
    const { topicTitle, topicSlug, toShow, bookmarkData, slug } = this.props
    return (
      <Container
        toShow={toShow}
      >
        {!topicSlug ? null : <BackToTopic topicSlug={topicSlug} topicTitle={topicTitle} />}
        <WidgetContainer>
          <BookmarkWidget
            bookmarkData={bookmarkData}
            slug={slug}
          />
        </WidgetContainer>
      </Container>
    )
  }
}

DesktopArticleTools.propTypes = {
  toShow: PropTypes.bool.isRequired,
  topicSlug: PropTypes.string,
  topicTitle: PropTypes.string,
  bookmarkData: PropTypes.object.isRequired,
  slug: PropTypes.string.isRequired
}

DesktopArticleTools.defaultProps = {
  topicSlug: '',
  topicTitle: ''
}

export default DesktopArticleTools
<commit_msg>fix(ui): quick fix for desktop tool position<commit_after>import BackToTopicIcon from '../../../../static/asset/article-back-to-topic.svg'
import Link from 'react-router/lib/Link'
import PropTypes from 'prop-types'
import React from 'react'
import styled from 'styled-components'
import { BookmarkWidget } from '@twreporter/registration'
import { LINK_PREFIX } from '../../../constants/link-prefix'
import { colors, typography }  from '../../../themes/common-variables'
import { articleLayout } from '../../../themes/layout'

const toolsOffset = 20

const Container = styled.div`
  display: inline-block;
  position: fixed;
  right: 50%;
  bottom: 50%;
  color: ${colors.secondaryColor};
  font-size: ${typography.font.size.xSmall};
  z-index: 999;
  transform: translate(${(articleLayout.desktop.width.large - toolsOffset)/2 + 20}px, -50%);
  visibility: ${(props) => props.toShow ? 'visible' : 'hidden'};
  opacity: ${(props) => props.toShow ? 1 : 0};
  transition: opacity 0.5s linear;

  a, a:visited, a:link, a:active {
    color: ${colors.secondaryColor};
    font-size: ${typography.font.size.xSmall};
  }
`

const WidgetContainer = styled.div`
  box-sizing: border-box;
  margin-top: 10px;
  text-align: center;
`

const IconContainer = styled.div`
  width: 100%;
  text-align: center;
  svg {
    width: 20px;
  }
`

const BackToTopicContainer = styled.div`
  span {
    display: block;
    margin-bottom: 10px;
    font-weight: $font-weight-normal;
  }
`

const BackToTopic = (props) => (
  <Link to={`${LINK_PREFIX.TOPICS}${props.topicSlug}`} title={props.topicTitle}>
    <BackToTopicContainer>
      回到專題
      <IconContainer>
        <BackToTopicIcon />
      </IconContainer>
    </BackToTopicContainer>
  </Link>
)

BackToTopic.propTypes = {
  topicSlug: PropTypes.string.isRequired,
  topicTitle: PropTypes.string.isRequired
}

class DesktopArticleTools extends React.PureComponent {
  render() {
    const { topicTitle, topicSlug, toShow, bookmarkData, slug } = this.props
    return (
      <Container
        toShow={toShow}
      >
        {!topicSlug ? null : <BackToTopic topicSlug={topicSlug} topicTitle={topicTitle} />}
        <WidgetContainer>
          <BookmarkWidget
            bookmarkData={bookmarkData}
            slug={slug}
          />
        </WidgetContainer>
      </Container>
    )
  }
}

DesktopArticleTools.propTypes = {
  toShow: PropTypes.bool.isRequired,
  topicSlug: PropTypes.string,
  topicTitle: PropTypes.string,
  bookmarkData: PropTypes.object.isRequired,
  slug: PropTypes.string.isRequired
}

DesktopArticleTools.defaultProps = {
  topicSlug: '',
  topicTitle: ''
}

export default DesktopArticleTools
<|endoftext|>


<commit_before>/*
Copyright 2017 Vector Creations Ltd

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.
*/

import React from 'react';
import sdk from '../../../index';
import classnames from 'classnames';

/*
 * A dialog for confirming a redaction.
 */
export default React.createClass({
    displayName: 'ConfirmRedactDialog',
    propTypes: {
        onFinished: React.PropTypes.func.isRequired,
    },

    defaultProps: {
        danger: false,
    },

    onOk: function() {
        this.props.onFinished(true);
    },

    onCancel: function() {
        this.props.onFinished(false);
    },

    render: function() {
        const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

        const title = "Confirm Redaction";

        const confirmButtonClass = classnames({
            'mx_Dialog_primary': true,
            'danger': false,
        });

        return (
            <BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
                onEnterPressed={ this.onOk }
                title={title}
            >

                    Are you sure you wish to redact this event?
                    Note that if you redact a room name or topic change, it could undo the change.

                    <button className={confirmButtonClass} onClick={this.onOk}>
                        Redact
                    </button>

                    <button onClick={this.onCancel}>
                        Cancel
                    </button>

            </BaseDialog>
        );
    },
});
<commit_msg>clarify that redact === delete<commit_after>/*
Copyright 2017 Vector Creations Ltd

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.
*/

import React from 'react';
import sdk from '../../../index';
import classnames from 'classnames';

/*
 * A dialog for confirming a redaction.
 */
export default React.createClass({
    displayName: 'ConfirmRedactDialog',
    propTypes: {
        onFinished: React.PropTypes.func.isRequired,
    },

    defaultProps: {
        danger: false,
    },

    onOk: function() {
        this.props.onFinished(true);
    },

    onCancel: function() {
        this.props.onFinished(false);
    },

    render: function() {
        const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

        const title = "Confirm Redaction";

        const confirmButtonClass = classnames({
            'mx_Dialog_primary': true,
            'danger': false,
        });

        return (
            <BaseDialog className="mx_ConfirmUserActionDialog" onFinished={this.props.onFinished}
                onEnterPressed={ this.onOk }
                title={title}
            >

                    Are you sure you wish to redact (delete) this event?
                    Note that if you redact a room name or topic change, it could undo the change.

                    <button className={confirmButtonClass} onClick={this.onOk}>
                        Redact
                    </button>

                    <button onClick={this.onCancel}>
                        Cancel
                    </button>

            </BaseDialog>
        );
    },
});
<|endoftext|>


<commit_before>/**
 * conjoon
 * (c) 2002-2009 siteartwork.de/conjoon.org
 * licensing@conjoon.org
 *
 * $Author$
 * $Id$
 * $Date$
 * $Revision$
 * $LastChangedDate$
 * $LastChangedBy$
 * $URL$
 */

Ext.namespace('com.conjoon.groupware.email');

com.conjoon.groupware.email.NodeEditor = function(treePanel, config) {

    Ext.apply(this, config);

    /**
     * @ext-bug beta1 cancelOnEsc is ignored in the onSpecialKey implementation
     *                of the TreeEditor, make it custom
     */
    this.addEvents({
        'escapekey'    : true,
        'enterkey'     : true
    });

    /**
     * @ext-bug beta1 Applying these values in the config gets ignored
     */
    this.shadow        = 'drop';
    this.revertInvalid = false;

    /**
     * The editor for the tree's nodes.
     * The beforeNodeClick method gets assigned to an empty function
     * so cliking on a node won'
     */
    com.conjoon.groupware.email.NodeEditor.superclass.constructor.call(this, treePanel, {
        shadow        : false,
        validator     : this.isNodeNameValid.createDelegate(this),
        allowBlank    : false,
        revertInvalid : false,
        cancelOnEsc   : false
    });

    // should prevent a selected node from being edited when
    // click on selected node occurs
    this.beforeNodeClick = Ext.emptyFn;

    // install listeners
    // looks like we can skip the call to onbeforecomplete, since first the
    // beforehide will be called
    this.on('beforecomplete', this.onBeforeEditingComplete, this);
    this.on('escapekey',      this.onEditorEscape, this);
    this.on('enterkey',       this.onEditorEnter, this);
    this.on('complete',       this.onEditorComplete, this);
    this.on('show',           this.onEditorShow,  this);
    this.on('beforehide',     this.onBeforeHide,  this);
    this.on('hide',           this.onEditorHide,  this);

};

Ext.extend(com.conjoon.groupware.email.NodeEditor, Ext.tree.TreeEditor, {

// ------------------------- Members -------------------------------------------
    /**
     * Edit modes
     */
    NONE : 0,
    EDIT : 1,
    SAVE : 2,

    /**
     * The current mode the editor is in. Equals to SAVE, EDIT or NONE
     */
    editMode : this.NONE,

// ----------------------------- Methods ---------------------------------------

    initEditor : function(tree)
    {
        com.conjoon.groupware.email.NodeEditor.superclass.initEditor.call(this, tree);
        // needed to overwrite since the mousdwon wad not in the ext2.0rc1
        // this is needed since a node which is currently being edited must not be draggable,
        // but without this custom implementation the tree would allow a node being
        // currently edited to be dragged around
        tree.getTreeEl().on('mousedown', this.hide, this);
    },

    /**
     * Validator for the node editor.
     */
    isNodeNameValid : function(value)
    {
        if (!this.editNode) {
            return true;
        }

        var value = value.trim();

        if (value == "") {
            return false;
        }

        // editnode still available?
        if (!this.editNode.parentNode) {
            return true;
        }

        return this.tree.isNodeNameAvailable(this.editNode.parentNode, this.editNode, value);
    },

    /**
     * Returns <tt>true</tt> if a node is being edited and the editor
     * waits for valid data.
     */
    isEditPending : function()
    {
        return (this.editNode != null && !this.field.isValid());
    },

    /**
     *
     */
    triggerEdit : function(node, mode)
    {
        this.editMode = mode;
        /**
         * @ext-bug2.2
         * see http://www.extjs.com/forum/showthread.php?t=41432
         */
        var as = this.tree.autoScroll;
        this.tree.autoScroll = false;
        com.conjoon.groupware.email.NodeEditor.superclass.triggerEdit.call(this, node);
        this.tree.autoScroll = as;
    },

    /**
     *
     */
    resetEdit : function(value, startValue)
    {
        this.field.setValue(startValue);
        this.field.selectText();
        this.field.focus();
        this.isWarning = false;
    },

    // private
    updateNode : function(ed, value)
    {
        com.conjoon.groupware.email.NodeEditor.superclass.updateNode.call(this, ed, value.trim());
    },

// ------------------------------ Listeners ------------------------------------
    /**
     * Method occurs no matter what. Tree calls it. Check if editor exists and
     * react.
     */
    onBeforeHide : function()
    {
        if (this.isWarning || this.isEditPending()) {
            return false;
        }

        return true;
    },

    /**
     * Checks wether the node name is valid and starts a XMLHttpRequest to save
     * the changes.
     *
     * @param {Ext.tree.TreeEditor}
     * @param mixed value
     * @param mixed startValue
     */
    isWarning : false,
    onBeforeEditingComplete : function(editor, value, startValue)
    {
        var msg = Ext.MessageBox;

        if (this.isWarning) {
            return false;
        }

        if (!this.field.isValid()) {
            this.isWarning = true;
            this.tree.valueInvalid(editor, value, startValue);
            return false;
        }

        return true;
    },

    /**
     * Starts saving the newly added/edited node respecting SAVE or EDIT mode.
     * The complete event gets fired even when a node was edited and it's value
     * did not change when requesting another components focus using mouse/keyboard.
     * However, if we are in EDIT mode, comparing the actual value with the start
     * value will tell us if we should proceed in requesting a XMLHttpRequest for
     * this. In SAVE mode the node was newly added, thus requesting a XMLHttpRequest
     * object is needed in every case.
     *
     *
     */
    onEditorComplete : function(editor, value, startValue)
    {
        if (this.editMode == this.NONE ||
           (this.editMode == this.EDIT && value.trim() === startValue.trim())) {
            return;
        }

        this.tree.saveNode({
            mode   : (this.editMode == this.EDIT ? 'edit' : 'add'),
            parent : this.editNode.parentNode.id,
            child  : {
                id         : this.editNode.id,
                value      : value,
                startValue : startValue
            }
        });

        this.editMode  = this.NONE;
        this.isWarning = false;
    },

    /**
    *
    */
    onEditorShow : function()
    {
        //alert(this.editMode+": SHOW");
    },

    /**
    *
    */
    onEditorHide : function()
    {
        //alert(this.editMode+": HIDE");
    },

    /**
     *
     */
    onEditorEnter : function(field, e)
    {
         e.stopEvent();
         this.completeEdit();
    },

    /**
     * When the user presses escapes he usually wants to revert any changes and
     * does not go into save mode. However, we hate to check if the node gets
     * edited or if it is a new node that needs to get saved.
     * In edit mode, we simply revert the changes and hide the editor, not invoking
     * any event.
     * In save mode however, we invoke the completeEdit method after we reverted
     * the fields value.
     *
     *
     */
    onEditorEscape : function(field, e)
    {
        switch (this.editMode) {
            case this.SAVE:
                e.stopEvent();
                this.field.setValue(this.startValue);
                this.completeEdit();
            return;
            case this.EDIT:
                e.stopEvent();
                this.suspendEvents();
                this.cancelEdit();
                this.resumeEvents();
            return;
        }
    },

    /**
     *
     */
    onSpecialKey : function(field, e)
    {
        if (e.getKey() == e.ENTER && !e.hasModifier()) {
            this.fireEvent('enterkey', field, e);
        } else if (e.getKey() == e.ESC) {
            this.fireEvent('escapekey', field, e);
        }
    }

});
<commit_msg> - enhancement: removed workaround for "jumpy page" problem (closes CN-116)<commit_after>/**
 * conjoon
 * (c) 2002-2009 siteartwork.de/conjoon.org
 * licensing@conjoon.org
 *
 * $Author$
 * $Id$
 * $Date$
 * $Revision$
 * $LastChangedDate$
 * $LastChangedBy$
 * $URL$
 */

Ext.namespace('com.conjoon.groupware.email');

com.conjoon.groupware.email.NodeEditor = function(treePanel, config) {

    Ext.apply(this, config);

    /**
     * @ext-bug beta1 cancelOnEsc is ignored in the onSpecialKey implementation
     *                of the TreeEditor, make it custom
     */
    this.addEvents({
        'escapekey'    : true,
        'enterkey'     : true
    });

    /**
     * @ext-bug beta1 Applying these values in the config gets ignored
     */
    this.shadow        = 'drop';
    this.revertInvalid = false;

    /**
     * The editor for the tree's nodes.
     * The beforeNodeClick method gets assigned to an empty function
     * so cliking on a node won'
     */
    com.conjoon.groupware.email.NodeEditor.superclass.constructor.call(this, treePanel, {
        shadow        : false,
        validator     : this.isNodeNameValid.createDelegate(this),
        allowBlank    : false,
        revertInvalid : false,
        cancelOnEsc   : false
    });

    // should prevent a selected node from being edited when
    // click on selected node occurs
    this.beforeNodeClick = Ext.emptyFn;

    // install listeners
    // looks like we can skip the call to onbeforecomplete, since first the
    // beforehide will be called
    this.on('beforecomplete', this.onBeforeEditingComplete, this);
    this.on('escapekey',      this.onEditorEscape, this);
    this.on('enterkey',       this.onEditorEnter, this);
    this.on('complete',       this.onEditorComplete, this);
    this.on('show',           this.onEditorShow,  this);
    this.on('beforehide',     this.onBeforeHide,  this);
    this.on('hide',           this.onEditorHide,  this);

};

Ext.extend(com.conjoon.groupware.email.NodeEditor, Ext.tree.TreeEditor, {

// ------------------------- Members -------------------------------------------
    /**
     * Edit modes
     */
    NONE : 0,
    EDIT : 1,
    SAVE : 2,

    /**
     * The current mode the editor is in. Equals to SAVE, EDIT or NONE
     */
    editMode : this.NONE,

// ----------------------------- Methods ---------------------------------------

    initEditor : function(tree)
    {
        com.conjoon.groupware.email.NodeEditor.superclass.initEditor.call(this, tree);
        // needed to overwrite since the mousdwon wad not in the ext2.0rc1
        // this is needed since a node which is currently being edited must not be draggable,
        // but without this custom implementation the tree would allow a node being
        // currently edited to be dragged around
        tree.getTreeEl().on('mousedown', this.hide, this);
    },

    /**
     * Validator for the node editor.
     */
    isNodeNameValid : function(value)
    {
        if (!this.editNode) {
            return true;
        }

        var value = value.trim();

        if (value == "") {
            return false;
        }

        // editnode still available?
        if (!this.editNode.parentNode) {
            return true;
        }

        return this.tree.isNodeNameAvailable(this.editNode.parentNode, this.editNode, value);
    },

    /**
     * Returns <tt>true</tt> if a node is being edited and the editor
     * waits for valid data.
     */
    isEditPending : function()
    {
        return (this.editNode != null && !this.field.isValid());
    },

    /**
     *
     */
    triggerEdit : function(node, mode)
    {
        this.editMode = mode;

        com.conjoon.groupware.email.NodeEditor.superclass.triggerEdit.call(this, node);
    },

    /**
     *
     */
    resetEdit : function(value, startValue)
    {
        this.field.setValue(startValue);
        this.field.selectText();
        this.field.focus();
        this.isWarning = false;
    },

    // private
    updateNode : function(ed, value)
    {
        com.conjoon.groupware.email.NodeEditor.superclass.updateNode.call(this, ed, value.trim());
    },

// ------------------------------ Listeners ------------------------------------
    /**
     * Method occurs no matter what. Tree calls it. Check if editor exists and
     * react.
     */
    onBeforeHide : function()
    {
        if (this.isWarning || this.isEditPending()) {
            return false;
        }

        return true;
    },

    /**
     * Checks wether the node name is valid and starts a XMLHttpRequest to save
     * the changes.
     *
     * @param {Ext.tree.TreeEditor}
     * @param mixed value
     * @param mixed startValue
     */
    isWarning : false,
    onBeforeEditingComplete : function(editor, value, startValue)
    {
        var msg = Ext.MessageBox;

        if (this.isWarning) {
            return false;
        }

        if (!this.field.isValid()) {
            this.isWarning = true;
            this.tree.valueInvalid(editor, value, startValue);
            return false;
        }

        return true;
    },

    /**
     * Starts saving the newly added/edited node respecting SAVE or EDIT mode.
     * The complete event gets fired even when a node was edited and it's value
     * did not change when requesting another components focus using mouse/keyboard.
     * However, if we are in EDIT mode, comparing the actual value with the start
     * value will tell us if we should proceed in requesting a XMLHttpRequest for
     * this. In SAVE mode the node was newly added, thus requesting a XMLHttpRequest
     * object is needed in every case.
     *
     *
     */
    onEditorComplete : function(editor, value, startValue)
    {
        if (this.editMode == this.NONE ||
           (this.editMode == this.EDIT && value.trim() === startValue.trim())) {
            return;
        }

        this.tree.saveNode({
            mode   : (this.editMode == this.EDIT ? 'edit' : 'add'),
            parent : this.editNode.parentNode.id,
            child  : {
                id         : this.editNode.id,
                value      : value,
                startValue : startValue
            }
        });

        this.editMode  = this.NONE;
        this.isWarning = false;
    },

    /**
    *
    */
    onEditorShow : function()
    {
        //alert(this.editMode+": SHOW");
    },

    /**
    *
    */
    onEditorHide : function()
    {
        //alert(this.editMode+": HIDE");
    },

    /**
     *
     */
    onEditorEnter : function(field, e)
    {
         e.stopEvent();
         this.completeEdit();
    },

    /**
     * When the user presses escapes he usually wants to revert any changes and
     * does not go into save mode. However, we hate to check if the node gets
     * edited or if it is a new node that needs to get saved.
     * In edit mode, we simply revert the changes and hide the editor, not invoking
     * any event.
     * In save mode however, we invoke the completeEdit method after we reverted
     * the fields value.
     *
     *
     */
    onEditorEscape : function(field, e)
    {
        switch (this.editMode) {
            case this.SAVE:
                e.stopEvent();
                this.field.setValue(this.startValue);
                this.completeEdit();
            return;
            case this.EDIT:
                e.stopEvent();
                this.suspendEvents();
                this.cancelEdit();
                this.resumeEvents();
            return;
        }
    },

    /**
     *
     */
    onSpecialKey : function(field, e)
    {
        if (e.getKey() == e.ENTER && !e.hasModifier()) {
            this.fireEvent('enterkey', field, e);
        } else if (e.getKey() == e.ESC) {
            this.fireEvent('escapekey', field, e);
        }
    }

});
<|endoftext|>


<commit_before>function cloneAndCompare(item) {
	var copy = item.clone();
	equals(function() {
		return item.parent == copy.parent;
	}, true);
	equals(function() {
		return item.nextSibling == copy;
	}, true);
	if (item.name) {
		equals(function() {
			return copy.parent.children[copy.name] == copy;
		}, true);
	}
	compareItems(copy, item, true);
	// Remove the cloned item to restore the document:
	copy.remove();
}

test('Path#clone()', function() {
	var path = new Path([10, 20], [30, 40]);
	path.closed = true;
	path.name = 'test';
	path.style = {
		strokeCap: 'round',
		strokeJoin: 'round',
		dashOffset: 10,
		dashArray: [10, 2, 10],
		fillColor: new RGBColor(0, 0, 1),
		strokeColor: new RGBColor(0, 0, 1),
		miterLimit: 5
	};
	path.clockwise = false;
	path.opacity = 0.5;
	path.locked = true;
	path.visible = false;
	path.blendMode = 'multiply';
	path.clipMask = true;
	path.selected = true;
	cloneAndCompare(path);
});

test('Path#clone() with GradientColor', function() {
	var colors = ['red', 'green', 'black'];
	var gradient = new Gradient(colors, 'radial');
	var color = new GradientColor(gradient, [0, 0], [20, 20], [10, 10]);

	var path = new Path([10, 20], [30, 40]);
	path.fillColor = color;
	cloneAndCompare(path);
});

test('CompoundPath#clone()', function() {
	var path1 = new Path.Rectangle([200, 200], [100, 100]);
	var path2 = new Path.Rectangle([50, 50], [200, 200]);
	var compound = new CompoundPath(path1, path2);
	cloneAndCompare(compound);
});

test('Layer#clone()', function() {
	var path = new Path.Rectangle([200, 200], [100, 100]);
	cloneAndCompare(paper.project.activeLayer);
});

test('Layer#clone() - check activeLayer', function() {
	var project = paper.project,
		activeLayer = project.activeLayer,
		layer = activeLayer.clone();
	equals(function() {
		return layer == project.activeLayer;
	}, true);
	equals(function() {
		return activeLayer != project.activeLayer;
	}, true);
});

test('Group#clone()', function() {
	var path = new Path.Circle([150, 150], 60);
	path.style = {
		strokeCap: 'round',
		strokeJoin: 'round',
		dashOffset: 10,
		dashArray: [10, 2, 10],
		fillColor: new RGBColor(0, 0, 1),
		strokeColor: new RGBColor(0, 0, 1),
		miterLimit: 5
	};
	var secondPath = new Path.Circle([175, 175], 85);
	var group = new Group([path, secondPath]);
	cloneAndCompare(group);
});

test('PointText#clone()', function() {
	var pointText = new PointText(new Point(50, 50));
	pointText.content = 'test';
	pointText.position += 100;
	pointText.characterStyle = {
		font: 'serif',
		fontSize: 20
	};
	pointText.paragraphStyle.justification = 'center';
	cloneAndCompare(pointText);
});

test('PlacedSymbol#clone()', function() {
	var path = new Path.Circle([150, 150], 60);
	var symbol = new Symbol(path);
	var placedSymbol = new PlacedSymbol(symbol);
	placedSymbol.position = [100, 100];
	placedSymbol.rotate(90);
	cloneAndCompare(placedSymbol);
});

test('Symbol#clone()', function() {
	var path = new Path.Circle([150, 150], 60);
	path.style = {
		strokeCap: 'round',
		strokeJoin: 'round',
		dashOffset: 10,
		dashArray: [10, 2, 10],
		fillColor: new RGBColor(0, 0, 1),
		strokeColor: new RGBColor(0, 0, 1),
		miterLimit: 5
	};
	path.selected = true;
	var symbol = new Symbol(path);
	var copy = symbol.clone();
	compareItems(copy.definition, symbol.definition);
	equals(function() {
		return symbol.project == copy.project;
	}, true);
	equals(function() {
		return paper.project.symbols.length == 2;
	}, true);
});

test('Raster#clone()', function() {
	var path = new Path.Circle([150, 150], 60);
	path.style = {
		fillColor: new RGBColor(0, 0, 1),
		strokeColor: new RGBColor(0, 0, 1)
	};
	var raster = path.rasterize();
	raster.opacity = 0.5;
	raster.locked = true;
	raster.visible = false;
	raster.blendMode = 'multiply';
	raster.rotate(20).translate(100);
	cloneAndCompare(raster);
});

test('Group with clipmask', function() {
	var path = new Path.Circle([100, 100], 30),
		path2 = new Path.Circle([100, 100], 20),
		group = new Group([path, path2]);
	group.clipped = true;
	cloneAndCompare(group);
});<commit_msg>Remove use of characterStyle / paragraphStyle where not necessary.<commit_after>function cloneAndCompare(item) {
	var copy = item.clone();
	equals(function() {
		return item.parent == copy.parent;
	}, true);
	equals(function() {
		return item.nextSibling == copy;
	}, true);
	if (item.name) {
		equals(function() {
			return copy.parent.children[copy.name] == copy;
		}, true);
	}
	compareItems(copy, item, true);
	// Remove the cloned item to restore the document:
	copy.remove();
}

test('Path#clone()', function() {
	var path = new Path([10, 20], [30, 40]);
	path.closed = true;
	path.name = 'test';
	path.style = {
		strokeCap: 'round',
		strokeJoin: 'round',
		dashOffset: 10,
		dashArray: [10, 2, 10],
		fillColor: new RGBColor(0, 0, 1),
		strokeColor: new RGBColor(0, 0, 1),
		miterLimit: 5
	};
	path.clockwise = false;
	path.opacity = 0.5;
	path.locked = true;
	path.visible = false;
	path.blendMode = 'multiply';
	path.clipMask = true;
	path.selected = true;
	cloneAndCompare(path);
});

test('Path#clone() with GradientColor', function() {
	var colors = ['red', 'green', 'black'];
	var gradient = new Gradient(colors, 'radial');
	var color = new GradientColor(gradient, [0, 0], [20, 20], [10, 10]);

	var path = new Path([10, 20], [30, 40]);
	path.fillColor = color;
	cloneAndCompare(path);
});

test('CompoundPath#clone()', function() {
	var path1 = new Path.Rectangle([200, 200], [100, 100]);
	var path2 = new Path.Rectangle([50, 50], [200, 200]);
	var compound = new CompoundPath(path1, path2);
	cloneAndCompare(compound);
});

test('Layer#clone()', function() {
	var path = new Path.Rectangle([200, 200], [100, 100]);
	cloneAndCompare(paper.project.activeLayer);
});

test('Layer#clone() - check activeLayer', function() {
	var project = paper.project,
		activeLayer = project.activeLayer,
		layer = activeLayer.clone();
	equals(function() {
		return layer == project.activeLayer;
	}, true);
	equals(function() {
		return activeLayer != project.activeLayer;
	}, true);
});

test('Group#clone()', function() {
	var path = new Path.Circle([150, 150], 60);
	path.style = {
		strokeCap: 'round',
		strokeJoin: 'round',
		dashOffset: 10,
		dashArray: [10, 2, 10],
		fillColor: new RGBColor(0, 0, 1),
		strokeColor: new RGBColor(0, 0, 1),
		miterLimit: 5
	};
	var secondPath = new Path.Circle([175, 175], 85);
	var group = new Group([path, secondPath]);
	cloneAndCompare(group);
});

test('PointText#clone()', function() {
	var pointText = new PointText(new Point(50, 50));
	pointText.content = 'test';
	pointText.position += 100;
	pointText.characterStyle = {
		font: 'serif',
		fontSize: 20
	};
	pointText.justification = 'center';
	cloneAndCompare(pointText);
});

test('PlacedSymbol#clone()', function() {
	var path = new Path.Circle([150, 150], 60);
	var symbol = new Symbol(path);
	var placedSymbol = new PlacedSymbol(symbol);
	placedSymbol.position = [100, 100];
	placedSymbol.rotate(90);
	cloneAndCompare(placedSymbol);
});

test('Symbol#clone()', function() {
	var path = new Path.Circle([150, 150], 60);
	path.style = {
		strokeCap: 'round',
		strokeJoin: 'round',
		dashOffset: 10,
		dashArray: [10, 2, 10],
		fillColor: new RGBColor(0, 0, 1),
		strokeColor: new RGBColor(0, 0, 1),
		miterLimit: 5
	};
	path.selected = true;
	var symbol = new Symbol(path);
	var copy = symbol.clone();
	compareItems(copy.definition, symbol.definition);
	equals(function() {
		return symbol.project == copy.project;
	}, true);
	equals(function() {
		return paper.project.symbols.length == 2;
	}, true);
});

test('Raster#clone()', function() {
	var path = new Path.Circle([150, 150], 60);
	path.style = {
		fillColor: new RGBColor(0, 0, 1),
		strokeColor: new RGBColor(0, 0, 1)
	};
	var raster = path.rasterize();
	raster.opacity = 0.5;
	raster.locked = true;
	raster.visible = false;
	raster.blendMode = 'multiply';
	raster.rotate(20).translate(100);
	cloneAndCompare(raster);
});

test('Group with clipmask', function() {
	var path = new Path.Circle([100, 100], 30),
		path2 = new Path.Circle([100, 100], 20),
		group = new Group([path, path2]);
	group.clipped = true;
	cloneAndCompare(group);
});<|endoftext|>


<commit_before>goog.module('measurementLibrary.eventProcessor.GoogleAnalyticsEventProcessor');

const uniqueId = goog.require('measurementLibrary.eventProcessor.generateUniqueId');
const logging = goog.require('measurementLibrary.logging');

/**
 * Parameters that are to be set at the top level of the JSON
 * POST body instead of as event parameters.
 * @const {!Object<string,string>}
 */
const TOP_LEVEL_PARAMS = {
  'client_id': 'clientId',
  'user_id': 'userId',
  'timestamp_micros': 'timestampMircos',
  'user_properties': 'userProperties',
  'non_personalized_ads': 'nonPersonalizedAds',
};

/**
 * Default client ID expires value.
 * @const {number}
 */
const DEFAULT_CLIENT_ID_EXPIRES = 2 * 365 * 24 * 60 * 60;

/**
 * Returns the default Google Analytics measurement URL
 * depending on if debug mode has been enabled.
 * When in debug mode, events get sent to the debug Google Analytics
 * server which returns descriptive error messages if an event
 * is incorrectly formatted.
 * @return {string}
 */
function getDefaultMeasurementUrl() {
  return logging.DEBUG ?
  'https://www.google-analytics.com/debug/mp/collect' :
  'https://www.google-analytics.com/mp/collect';
}

/**
 * A class that processes events pushed to the data layer
 * by constructing and sending Google Analytics events via
 * Measurement Protocol (App + Web).
 * For its Google Analytic events, it generates a unique client ID
 * and stores it in the long term storage model provided.
 * @implements {EventProcessor}
 */
class GoogleAnalyticsEventProcessor {
  /**
   * Constructs a Google Analytics Event Processor using the following
   * optional arguments within the options obejct:
   *
   * api_secret: Google Analytics API Secret. If provided, will be attached to
   *     query parameter of the same name when sending events.
   * measurement_id: Property to be measured Google Analytics Identifier. If
   *     provided, will be attached to query parameter of the same name when
   *     sending events.
   * measurement_url: URL endpoint to send events to. Defaults to
   *     Google Analytics collection endpoint.
   * client_id_expires: Number of seconds to store the client ID in long term
   *     storage. Defaults to two years.
   * automatic_params: Array of event parameters that will be searched for in
   *     the global data model and pulled into all events if found.
   * @param {{
   *     'api_secret': (string|undefined),
   *     'measurement_id': (string|undefined),
   *     'measurement_url': (string|undefined),
   *     'client_id_expires': (number|undefined),
   *     'automatic_params': (!Array<string>|undefined),
   * }=} optionsObject
   */
  constructor({
      'api_secret': apiSecret,
      'measurement_id': measurementId,
      'measurement_url': measurementUrl,
      'client_id_expires': clientIdExpires,
      'automatic_params': userAutomaticParams,
    } = {}) {
    if (apiSecret !== undefined && typeof apiSecret !== 'string') {
      logging.log(
        'Provided API Secret is not a valid string.',
        logging.LogLevel.ERROR
      );
      apiSecret = undefined;
    }
    if (measurementId !== undefined && typeof measurementId !== 'string') {
      logging.log(
        'Provided Measurement ID is not a valid string.',
        logging.LogLevel.ERROR
      );
      measurementId = undefined;
    }
    if (typeof measurementUrl !== 'string') {
      if (measurementUrl !== undefined) {
        logging.log(
          'Provided Measurement URL is not a valid string. ' +
              'Using default URL instead.',
          logging.LogLevel.ERROR
        );
      }
      measurementUrl = getDefaultMeasurementUrl();
      if (measurementId === undefined || apiSecret === undefined) {
        logging.log(
          'Sending events to Google Analytics without measurement ID or ' +
              'API secret will lead to events being dropped.',
          logging.LogLevel.ERROR
        );
      }
    }
    if (isNaN(Number(clientIdExpires))) {
      if (clientIdExpires !== undefined) {
        logging.log(
          'Provided Client ID Expires is not a valid number. ' +
              'Using default value instead.',
          logging.LogLevel.ERROR
        );
      }
      clientIdExpires = DEFAULT_CLIENT_ID_EXPIRES;
    } else {
      clientIdExpires = Number(clientIdExpires);
    }
    if (Object.prototype.toString.call(userAutomaticParams) !==
        '[object Array]') {
      if (userAutomaticParams !== undefined) {
        logging.log(
          'Provided Automatic Parameters are not contained within an array ' +
              'Discarding provided value.',
          logging.LogLevel.ERROR
        );
      }
      userAutomaticParams = [];
    }

    /**
     * Parameters that are important to all events and will be searched for
     * globally in the data model.
     * @private @const {!Object<string, string>}
     */
    this.automaticParams_ = {
      'page_path': 'page_path',
      'page_location': 'page_location',
      'page_title': 'page_title',
      'user_id': 'userId',
      'client_id': 'clientId',
    };

    // Add user provided params to automatic param list
    for (let i = 0; i < userAutomaticParams.length; ++i) {
      this.automaticParams_[userAutomaticParams[i]] = userAutomaticParams[i];
    }

    /**
     * An API secret that can be generated via Google Analytics UI
     * NOTE: This processor makes public network requests using this
     * API secret. To avoid spam, regenerate API secrets regularly.
     * @private @const {string|undefined}
     */
    this.apiSecret_ = apiSecret;

    /**
     * The identifier for property being measured. Can be found using
     * the Google Analytics UI.
     * @private @const {string|undefined}
     */
    this.measurementId_ = measurementId;

    /**
     * URL to send network requests to. Defaults to the Google Analytics
     * collection endpoint. Data must be sent via HTTPS.
     * @private @const {string}
     */
    this.measurementUrl_ = measurementUrl;

    /**
     * How long the client ID key should be stored in long term storage
     * measured in seconds. Defaults to two years.
     * @private @const {number}
     */
    this.clientIdExpires_ = clientIdExpires;
  }

  /**
   * Names the event processor so multiple processors
   * can share previous configuration.
   * @return {string}
   * @export
   */
  static getName() {
    return 'googleAnalytics';
  }

  /**
   * Gets value stored in the global model under a given key.
   * Only accesses the globel model if the given key is an automatic parameter.
   * @param {string} key
   * @param {{get:function(string):*, set:function(string, *)}} modelInterface
   *    An interface to load or save short term page data from the data layer.
   * @return {*} value
   * @private
   */
  getFromGlobalScope_(key, modelInterface) {
    if (this.automaticParams_[key]) {
      return modelInterface.get(key);
    }
    return undefined;
  }

  /**
   * Gets the ID associated with the current client.
   * First queries the global model followed by long term storage if not yet
   * found for `clientId`. If no previous ID exists, a new one is generated
   * and stored for future use in both the global model and long term storage.
   * @param {!StorageInterface} storageInterface An interface to an object to
   *    load or save persistent data with.
   * @param {{get:function(string):*, set:function(string, *)}} modelInterface
   *    An interface to load or save short term page data from the data layer.
   * @return {string}
   * @private
   */
  getClientId_(storageInterface, modelInterface) {
    let clientId = this.getFromGlobalScope_('client_id', modelInterface);
    if (!clientId || typeof clientId !== 'string') {
      clientId = storageInterface.load('client_id');
      if (!clientId || typeof clientId !== 'string') {
        clientId = uniqueId.generateUniqueId();
        storageInterface.save('client_id', clientId, this.clientIdExpires_);
      }
      modelInterface.set('client_id', clientId);
    }
    return clientId;
  }

  /**
   * Builds the POST request URL using `measurement_id` and `api_secret`
   * query parameters if available
   * @return {string}
   * @private
   */
  buildRequestUrl_() {
    const paramsArray = [];
    if (this.measurementId_) {
      paramsArray.push(
        `measurement_id=${encodeURIComponent(this.measurementId_)}`
      );
    }
    if (this.apiSecret_) {
      paramsArray.push(
        `api_secret=${encodeURIComponent(this.apiSecret_)}`
      );
    }
    if (paramsArray.length > 0) {
      const params = paramsArray.join('&');
      return `${this.measurementUrl_}?${params}`;
    }
    return this.measurementUrl_;
  }

  /**
   * Adds a key value pair to JSON POST request if value is defined.
   * If the key is a top level parameter, the pair will be added at the topmost
   * level of the JSON.
   * If not, the pair is interpreted as an event parameter and added to the
   * params object within the JSON.
   * @param {!Object<string, *>} json
   * @param {string} key
   * @param {*} value
   * @private
   */
  addKeyValuePairToJson_(json, key, value) {
    if (value !== undefined) {
      if (TOP_LEVEL_PARAMS[key]) {
        json[TOP_LEVEL_PARAMS[key]] = value;
      } else {
        json['events'][0]['params'][key] = value;
      }
    }
  }

  /**
   * Processes events pushed to the data layer by constructing and sending JSON
   * POST requests to Google Analytics.
   * Follows Measurement Protocol (App + Web).
   * @param {!StorageInterface} storageInterface An interface to an object to
   *    load or save persistent data with.
   * @param {{get:function(string):*, set:function(string, *)}} modelInterface
   *    An interface to load or save short term page data from the data layer.
   * @param {string} eventName The name of the event passed to the data layer.
   * @param {!Object<string, *>=} eventOptions The events passed to the data
   *     layer.
   * @export
   */
  processEvent(storageInterface, modelInterface, eventName, eventOptions) {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', this.buildRequestUrl_());

    const json = {
      'events': [{'name': eventName, 'params': {}}],
    };

    for (const key in this.automaticParams_) {
      let value;
      if (key === 'client_id') {
        value = this.getClientId_(storageInterface, modelInterface);
      } else {
        value = this.getFromGlobalScope_(key, modelInterface);
      }
      this.addKeyValuePairToJson_(json, key, value);
    }

    for (const key in eventOptions) {
      this.addKeyValuePairToJson_(json, key, eventOptions[key]);
    }

    if (logging.DEBUG) {
      xhr.onload = () => {
        logging.log(xhr.responseText, logging.LogLevel.INFO);
      };
    }

    xhr.send(JSON.stringify(json));
  }

  /**
   * Checks if the key being set by the user is client ID and if so returns
   * the client ID expiry.
   * @param {string} key The location at which to store data in the model.
   *    Dot notation is used to access a nested value (i.e. 'employees.jim'
   *    is the key 'jim' in the nested 'employees' object).
   * @param {*} value The data to store.
   * @return {number} How long the key should be stored in seconds.
   *     If -1, then the default value saved in storage will be used.
   *     If 0, the data is not saved to long term storage at all.
   *     If Number.POSITIVE_INFINITY, data will be stored forever.
   * @export
   */
  persistTime(key, value) {
    if (key === 'client_id') {
      return this.clientIdExpires_;
    } else {
      return -1;
    }
  }
}

exports = GoogleAnalyticsEventProcessor;
<commit_msg>Address Change requests<commit_after>goog.module('measurementLibrary.eventProcessor.GoogleAnalyticsEventProcessor');

const uniqueId = goog.require('measurementLibrary.eventProcessor.generateUniqueId');
const logging = goog.require('measurementLibrary.logging');

/**
 * A map of model key names to JSON field names for parameters that
 * are to be set at the top level of the JSON POST body instead of as
 * event parameters.
 * @const {!Object<string, string>}
 */
const TOP_LEVEL_PARAMS = {
  'client_id': 'clientId',
  'user_id': 'userId',
  'timestamp_micros': 'timestampMircos',
  'user_properties': 'userProperties',
  'non_personalized_ads': 'nonPersonalizedAds',
};

/**
 * Default client ID expires value.
 * @const {number}
 */
const DEFAULT_CLIENT_ID_EXPIRES = 2 * 365 * 24 * 60 * 60;

/**
 * Returns the default Google Analytics measurement URL
 * depending on if debug mode has been enabled.
 * When in debug mode, events get sent to the debug Google Analytics
 * server which returns descriptive error messages if an event
 * is incorrectly formatted.
 * @return {string}
 */
function getDefaultMeasurementUrl() {
  return logging.DEBUG ?
  'https://www.google-analytics.com/debug/mp/collect' :
  'https://www.google-analytics.com/mp/collect';
}

/**
 * A class that processes events pushed to the data layer
 * by constructing and sending Google Analytics events via
 * Measurement Protocol (App + Web).
 * For its Google Analytic events, it generates a unique client ID
 * and stores it in the long term storage model provided.
 * @implements {EventProcessor}
 */
class GoogleAnalyticsEventProcessor {
  /**
   * Constructs a Google Analytics Event Processor using the following
   * optional arguments within the options obejct:
   *
   * api_secret: Google Analytics API Secret. If provided, will be attached to
   *     query parameter of the same name when sending events.
   * measurement_id: Property to be measured Google Analytics Identifier. If
   *     provided, will be attached to query parameter of the same name when
   *     sending events.
   * measurement_url: URL endpoint to send events to. Defaults to
   *     Google Analytics collection endpoint.
   * client_id_expires: Number of seconds to store the client ID in long term
   *     storage. Defaults to two years.
   * automatic_params: Array of event parameters that will be searched for in
   *     the global data model and pulled into all events if found.
   * @param {{
   *     'api_secret': (string|undefined),
   *     'measurement_id': (string|undefined),
   *     'measurement_url': (string|undefined),
   *     'client_id_expires': (number|undefined),
   *     'automatic_params': (!Array<string>|undefined),
   * }=} optionsObject
   */
  constructor({
      'api_secret': apiSecret,
      'measurement_id': measurementId,
      'measurement_url': measurementUrl,
      'client_id_expires': clientIdExpires,
      'automatic_params': userAutomaticParams,
    } = {}) {
    if (apiSecret !== undefined && typeof apiSecret !== 'string') {
      logging.log(
        'Provided API Secret is not a valid string.',
        logging.LogLevel.ERROR
      );
      apiSecret = undefined;
    }
    if (measurementId !== undefined && typeof measurementId !== 'string') {
      logging.log(
        'Provided Measurement ID is not a valid string.',
        logging.LogLevel.ERROR
      );
      measurementId = undefined;
    }
    if (typeof measurementUrl !== 'string') {
      if (measurementUrl !== undefined) {
        logging.log(
          'Provided Measurement URL is not a valid string. ' +
              'Using default URL instead.',
          logging.LogLevel.ERROR
        );
      }
      measurementUrl = getDefaultMeasurementUrl();
      if (measurementId === undefined || apiSecret === undefined) {
        logging.log(
          'Sending events to Google Analytics without measurement ID or ' +
              'API secret will lead to events being dropped.',
          logging.LogLevel.ERROR
        );
      }
    }
    if (isNaN(Number(clientIdExpires))) {
      if (clientIdExpires !== undefined) {
        logging.log(
          'Provided Client ID Expires is not a valid number. ' +
              'Using default value instead.',
          logging.LogLevel.ERROR
        );
      }
      clientIdExpires = DEFAULT_CLIENT_ID_EXPIRES;
    } else {
      clientIdExpires = Number(clientIdExpires);
    }
    if (Object.prototype.toString.call(userAutomaticParams) !==
        '[object Array]') {
      if (userAutomaticParams !== undefined) {
        logging.log(
          'Provided Automatic Parameters are not contained within an array ' +
              'Discarding provided value.',
          logging.LogLevel.ERROR
        );
      }
      userAutomaticParams = [];
    }

    /**
     * A map of model key names to JSON field names for Parameters that are
     * important to all events and will be searched for globally in the data
     * model.
     * @private @const {!Object<string, string>}
     */
    this.automaticParams_ = {
      'page_path': 'page_path',
      'page_location': 'page_location',
      'page_title': 'page_title',
      'user_id': 'userId',
      'client_id': 'clientId',
    };

    // Add user provided params to automatic param list
    for (let i = 0; i < userAutomaticParams.length; ++i) {
      this.automaticParams_[userAutomaticParams[i]] = userAutomaticParams[i];
    }

    /**
     * An API secret that can be generated via Google Analytics UI
     * NOTE: This processor makes public network requests using this
     * API secret. To avoid spam, regenerate API secrets regularly.
     * @private @const {string|undefined}
     */
    this.apiSecret_ = apiSecret;

    /**
     * The identifier for property being measured. Can be found using
     * the Google Analytics UI.
     * @private @const {string|undefined}
     */
    this.measurementId_ = measurementId;

    /**
     * URL to send network requests to. Defaults to the Google Analytics
     * collection endpoint. Data must be sent via HTTPS.
     * @private @const {string}
     */
    this.measurementUrl_ = measurementUrl;

    /**
     * How long the client ID key should be stored in long term storage
     * measured in seconds. Defaults to two years.
     * @private @const {number}
     */
    this.clientIdExpires_ = clientIdExpires;
  }

  /**
   * Names the event processor so multiple processors
   * can share previous configuration.
   * @return {string}
   * @export
   */
  static getName() {
    return 'googleAnalytics';
  }

  /**
   * Gets value stored in the global model under a given key.
   * Only accesses the globel model if the given key is an automatic parameter.
   * @param {string} key
   * @param {{get:function(string):*, set:function(string, *)}} modelInterface
   *    An interface to load or save short term page data from the data layer.
   * @return {*} value
   * @private
   */
  getFromGlobalScope_(key, modelInterface) {
    if (this.automaticParams_[key]) {
      return modelInterface.get(key);
    }
    return undefined;
  }

  /**
   * Gets the ID associated with the current client.
   * First queries the global model followed by long term storage if not yet
   * found for `clientId`. If no previous ID exists, a new one is generated
   * and stored for future use in both the global model and long term storage.
   * @param {!StorageInterface} storageInterface An interface to an object to
   *    load or save persistent data with.
   * @param {{get:function(string):*, set:function(string, *)}} modelInterface
   *    An interface to load or save short term page data from the data layer.
   * @return {string}
   * @private
   */
  getClientId_(storageInterface, modelInterface) {
    let clientId = this.getFromGlobalScope_('client_id', modelInterface);
    if (!clientId || typeof clientId !== 'string') {
      clientId = storageInterface.load('client_id');
      if (!clientId || typeof clientId !== 'string') {
        clientId = uniqueId.generateUniqueId();
        storageInterface.save('client_id', clientId, this.clientIdExpires_);
      }
      modelInterface.set('client_id', clientId);
    }
    return clientId;
  }

  /**
   * Builds the POST request URL using `measurement_id` and `api_secret`
   * query parameters if available
   * @return {string}
   * @private
   */
  buildRequestUrl_() {
    const paramsArray = [];
    if (this.measurementId_) {
      paramsArray.push(
        `measurement_id=${encodeURIComponent(this.measurementId_)}`
      );
    }
    if (this.apiSecret_) {
      paramsArray.push(
        `api_secret=${encodeURIComponent(this.apiSecret_)}`
      );
    }
    if (paramsArray.length > 0) {
      const params = paramsArray.join('&');
      return `${this.measurementUrl_}?${params}`;
    }
    return this.measurementUrl_;
  }

  /**
   * Adds a key value pair to JSON POST request if value is defined.
   * If the key is a top level parameter, the pair will be added at the topmost
   * level of the JSON.
   * If not, the pair is interpreted as an event parameter and added to the
   * params object within the JSON.
   * @param {!Object<string, *>} json
   * @param {string} key
   * @param {*} value
   * @private
   */
  addKeyValuePairToJson_(json, key, value) {
    if (value !== undefined) {
      if (TOP_LEVEL_PARAMS[key]) {
        json[TOP_LEVEL_PARAMS[key]] = value;
      } else {
        json['events'][0]['params'][key] = value;
      }
    }
  }

  /**
   * Processes events pushed to the data layer by constructing and sending JSON
   * POST requests to Google Analytics.
   * Follows Measurement Protocol (App + Web).
   * @param {!StorageInterface} storageInterface An interface to an object to
   *    load or save persistent data with.
   * @param {{get:function(string):*, set:function(string, *)}} modelInterface
   *    An interface to load or save short term page data from the data layer.
   * @param {string} eventName The name of the event passed to the data layer.
   * @param {!Object<string, *>=} eventOptions The events passed to the data
   *     layer.
   * @export
   */
  processEvent(storageInterface, modelInterface, eventName, eventOptions) {
    const xhr = new XMLHttpRequest();
    xhr.open('POST', this.buildRequestUrl_());

    const json = {
      'events': [{'name': eventName, 'params': {}}],
    };

    for (const key in this.automaticParams_) {
      let value;
      if (key === 'client_id') {
        value = this.getClientId_(storageInterface, modelInterface);
      } else {
        value = this.getFromGlobalScope_(key, modelInterface);
      }
      this.addKeyValuePairToJson_(json, key, value);
    }

    for (const key in eventOptions) {
      this.addKeyValuePairToJson_(json, key, eventOptions[key]);
    }

    if (logging.DEBUG) {
      xhr.onload = () => {
        logging.log(xhr.responseText, logging.LogLevel.INFO);
      };
    }

    xhr.send(JSON.stringify(json));
  }

  /**
   * Checks if the key being set by the user is client ID and if so returns
   * the client ID expiry.
   * @param {string} key The location at which to store data in the model.
   *    Dot notation is used to access a nested value (i.e. 'employees.jim'
   *    is the key 'jim' in the nested 'employees' object).
   * @param {*} value The data to store.
   * @return {number} How long the key should be stored in seconds.
   *     If -1, then the default value saved in storage will be used.
   *     If 0, the data is not saved to long term storage at all.
   *     If Number.POSITIVE_INFINITY, data will be stored forever.
   * @export
   */
  persistTime(key, value) {
    if (key === 'client_id') {
      return this.clientIdExpires_;
    } else {
      return -1;
    }
  }
}

exports = GoogleAnalyticsEventProcessor;
<|endoftext|>


<commit_before>var assert = require("assert");
var test = require("./testing").test;

var htmlWriter = require("../lib/html-writer");

describe('html-writer', function() {
    test('can generate simple paragraph', function() {
        var writer = htmlWriter.writer();
        writer.open("p");
        writer.text("Hello");
        writer.close("p");
        return assert.equal("
Hello
", writer.asString());
    });
})
<commit_msg>Add test for nested HTML<commit_after>var assert = require("assert");
var test = require("./testing").test;

var htmlWriter = require("../lib/html-writer");

describe('html-writer', function() {
    test('can generate simple paragraph', function() {
        var writer = htmlWriter.writer();
        writer.open("p");
        writer.text("Hello");
        writer.close("p");
        return assert.equal("
Hello
", writer.asString());
    });

    test('can nest elements', function() {
        var writer = htmlWriter.writer();
        writer.open("ul");
        writer.open("li");
        writer.text("One");
        writer.close("li");
        writer.open("li");
        writer.text("Two");
        writer.close("li");
        writer.close("ul");
        return assert.equal("

One

Two

", writer.asString());
    });
})
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';

import createContextSync from './createContextSync';

const contextHOC = (Provider, Consumer, Child) =>
  function ContextWrapper({ children, ...rest }) {
    return (
      <Provider {...rest}>
        <Consumer>
          {context => <Child context={context}>{children}</Child>}
        </Consumer>
      </Provider>
    );
  };

function contextProviderEmitter(namespace, Provider, Consumer) {
  const { update } = createContextSync(namespace);

  class ContextProviderEmitter extends React.PureComponent {
    componentDidMount() {
      update(this.props.context);
    }

    componentDidUpdate() {
      update(this.props.context);
    }

    render() {
      const { children, context } = this.props;
      if (typeof children === 'function') {
        return children(context);
      }
      return children;
    }
  }

  ContextProviderEmitter.propTypes = {
    context: PropTypes.object.isRequired,
    children: PropTypes.node.isRequired
  };

  // eslint-disable-next-line no-class-assign
  ContextProviderEmitter = contextHOC(
    Provider,
    Consumer,
    ContextProviderEmitter
  );

  ContextProviderEmitter.displayName = `ContextProviderEmitter(${namespace})`;

  return ContextProviderEmitter;
}

export default contextProviderEmitter;
<commit_msg>Fix proptypes for docs.<commit_after>import React from 'react';
import PropTypes from 'prop-types';

import createContextSync from './createContextSync';

const contextHOC = (Provider, Consumer, Child) =>
  function ContextWrapper({ children, ...rest }) {
    return (
      <Provider {...rest}>
        <Consumer>
          {context => <Child context={context}>{children}</Child>}
        </Consumer>
      </Provider>
    );
  };

function contextProviderEmitter(namespace, Provider, Consumer) {
  const { update } = createContextSync(namespace);

  class ContextProviderEmitter extends React.PureComponent {
    componentDidMount() {
      update(this.props.context);
    }

    componentDidUpdate() {
      update(this.props.context);
    }

    render() {
      const { children, context } = this.props;
      if (typeof children === 'function') {
        return children(context);
      }
      return children;
    }
  }

  ContextProviderEmitter.propTypes = {
    context: PropTypes.object.isRequired,
    children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired
  };

  // eslint-disable-next-line no-class-assign
  ContextProviderEmitter = contextHOC(
    Provider,
    Consumer,
    ContextProviderEmitter
  );

  ContextProviderEmitter.displayName = `ContextProviderEmitter(${namespace})`;

  return ContextProviderEmitter;
}

export default contextProviderEmitter;
<|endoftext|>


<commit_before>var CodeEditorComponent = function() {

  var initCodeMirror = function(textarea) {
    var codemirror_opts = {
      autofocus: false,
      lineNumbers: true,
      lineWrapping: true
    };

    var codemirror = CodeMirror.fromTextArea(textarea, codemirror_opts);
    codemirror.setOption("mode", this.getAttribute("data-language"));

    // FIXME horrible horrible hack to work around a bug in codemirror where
    // the editor can't be rendered properly before the browser has actually
    // rendered the backing textarea. yield to the browser and poll for
    // completed rendering
    var poll = (function(base){
      return function() {
        if ($(".CodeMirror-gutter", base).offsetWidth == 0) {
          codemirror.refresh();
          window.setTimeout(poll, 1);
        }
      };
    })(this);
    poll();

    return codemirror;
  }

  var setupKeyPressHandlers = function() {
    var base = this;

    var cmd_pressed = false;
    this.addEventListener('keydown', function(e) {
      if (e.keyCode == 17 || e.keyCode == 91) {
        cmd_pressed = true;
      }
    }, false);

    this.addEventListener('keyup', function(e) {
      if (e.keyCode == 17 || e.keyCode == 91) {
        cmd_pressed = false;
      }
    }, false);

    this.addEventListener('keydown', function(e) {
      if (e.keyCode == 13 && cmd_pressed) {
        e.preventDefault();
        base.execute.call(base);
      }
    }, false);
  };

  this.createdCallback = function() {
    var tpl = $.getTemplate("widgets/z-codeeditor", "z-codeeditor-base-tpl");

    var textarea = document.createElement("textarea");
    textarea.setAttribute("autofocus", "autofocus");
    this.appendChild(textarea);

    var codemirror = initCodeMirror.call(this, textarea);
    setupKeyPressHandlers.call(this);

    this.getValue = function() {
      return codemirror.getValue();
    };

    this.setValue = function(value) {
      if (!value) {return;}
      codemirror.setValue(value);
    }

    this.execute = function() {
      var ev = new Event('execute');
      ev.value = this.getValue();
      this.dispatchEvent(ev);
    }

    this.setupResizing = function(resizer) {
      var gutters = this.querySelector(".CodeMirror-gutters");
      var benchmark_y;
      var _this = this;

      resizer.addEventListener('dragstart', function(e) {
        this.style.background = "transparent";
        this.style.border = "none";
        benchmark_y = e.clientY;
      }, false);

      resizer.addEventListener('drag', function(elem) {
        return function(e) {
          e.preventDefault();
          this.style.background = "";
          this.style.border = "";
          var offset = benchmark_y - e.clientY;
          elem.style.height = (elem.offsetHeight - offset) + "px";
          gutters.style.height = (elem.offsetHeight - offset) + "px";
          benchmark_y = e.clientY;
        };
      });
    }
  };
};

var proto = Object.create(HTMLElement.prototype);
CodeEditorComponent.apply(proto);
document.registerElement("z-codeeditor", { prototype: proto });
<commit_msg>pass 'this' explicitly to closure<commit_after>var CodeEditorComponent = function() {

  var initCodeMirror = function(textarea) {
    var codemirror_opts = {
      autofocus: false,
      lineNumbers: true,
      lineWrapping: true
    };

    var codemirror = CodeMirror.fromTextArea(textarea, codemirror_opts);
    codemirror.setOption("mode", this.getAttribute("data-language"));

    // FIXME horrible horrible hack to work around a bug in codemirror where
    // the editor can't be rendered properly before the browser has actually
    // rendered the backing textarea. yield to the browser and poll for
    // completed rendering
    var poll = (function(base){
      return function() {
        if ($(".CodeMirror-gutter", base).offsetWidth == 0) {
          codemirror.refresh();
          window.setTimeout(poll, 1);
        }
      };
    })(this);
    poll();

    return codemirror;
  }

  var setupKeyPressHandlers = function() {
    var base = this;

    var cmd_pressed = false;
    this.addEventListener('keydown', function(e) {
      if (e.keyCode == 17 || e.keyCode == 91) {
        cmd_pressed = true;
      }
    }, false);

    this.addEventListener('keyup', function(e) {
      if (e.keyCode == 17 || e.keyCode == 91) {
        cmd_pressed = false;
      }
    }, false);

    this.addEventListener('keydown', function(e) {
      if (e.keyCode == 13 && cmd_pressed) {
        e.preventDefault();
        base.execute.call(base);
      }
    }, false);
  };

  this.createdCallback = function() {
    var tpl = $.getTemplate("widgets/z-codeeditor", "z-codeeditor-base-tpl");

    var textarea = document.createElement("textarea");
    textarea.setAttribute("autofocus", "autofocus");
    this.appendChild(textarea);

    var codemirror = initCodeMirror.call(this, textarea);
    setupKeyPressHandlers.call(this);

    this.getValue = function() {
      return codemirror.getValue();
    };

    this.setValue = function(value) {
      if (!value) {return;}
      codemirror.setValue(value);
    }

    this.execute = function() {
      var ev = new Event('execute');
      ev.value = this.getValue();
      this.dispatchEvent(ev);
    }

    this.setupResizing = function(resizer) {
      var gutters = this.querySelector(".CodeMirror-gutters");
      var benchmark_y;

      resizer.addEventListener('dragstart', function(e) {
        this.style.background = "transparent";
        this.style.border = "none";
        benchmark_y = e.clientY;
      }, false);

      resizer.addEventListener('drag', (function(elem) {
        return function(e) {
          e.preventDefault();
          this.style.background = "";
          this.style.border = "";
          var offset = benchmark_y - e.clientY;
          elem.style.height = (elem.offsetHeight - offset) + "px";
          gutters.style.height = (elem.offsetHeight - offset) + "px";
          benchmark_y = e.clientY;
        }
      })(this));
    }
  };
};

var proto = Object.create(HTMLElement.prototype);
CodeEditorComponent.apply(proto);
document.registerElement("z-codeeditor", { prototype: proto });
<|endoftext|>


<commit_before>var CodeEditorComponent = function() {

  var initCodeMirror = function(textarea) {
    var codemirror_opts = {
      autofocus: false,
      lineNumbers: true,
      lineWrapping: true
    };

    var codemirror = CodeMirror.fromTextArea(textarea, codemirror_opts);

    // FIXME horrible horrible hack to work around a bug in codemirror where
    // the editor can't be rendered properly before the browser has actually
    // rendered the backing textarea. yield to the browser and poll for
    // completed rendering
    var poll = (function(base){
      return function() {
        if ($(".CodeMirror-gutter", base).offsetWidth == 0) {
          codemirror.refresh();
          window.setTimeout(poll, 1);
        }
      };
    })(this);
    poll();

    return codemirror;
  }

  var setupKeyPressHandlers = function() {
    var base = this;

    var cmd_pressed = false;
    this.addEventListener('keydown', function(e) {
      if (e.keyCode == 17 || e.keyCode == 91) {
        cmd_pressed = true;
      }
    }, false);

    this.addEventListener('keyup', function(e) {
      if (e.keyCode == 17 || e.keyCode == 91) {
        cmd_pressed = false;
      }
    }, false);

    this.addEventListener('keydown', function(e) {
      if (e.keyCode == 13 && cmd_pressed) {
        e.preventDefault();
        base.execute.call(base);
      }
    }, false);
  };

  this.createdCallback = function() {
    var tpl = $.getTemplate("widgets/z-codeeditor", "z-codeeditor-base-tpl");

    var textarea = document.createElement("textarea");
    textarea.setAttribute("autofocus", "autofocus");
    this.appendChild(textarea);

    var codemirror = initCodeMirror.call(this, textarea);
    setupKeyPressHandlers.call(this);

    this.getValue = function() {
      return codemirror.getValue();
    };

    this.setValue = function(value) {
      if (!value) {return;}
      codemirror.setValue(value);
    }

    this.execute = function() {
      var ev = new Event('execute');
      ev.value = this.getValue();
      this.dispatchEvent(ev);
    }
  };
};

var proto = Object.create(HTMLElement.prototype);
CodeEditorComponent.apply(proto);
document.registerElement("z-codeeditor", { prototype: proto });
<commit_msg>fix syntax highlighting<commit_after>var CodeEditorComponent = function() {

  var initCodeMirror = function(textarea) {
    var codemirror_opts = {
      autofocus: false,
      lineNumbers: true,
      lineWrapping: true
    };

    var codemirror = CodeMirror.fromTextArea(textarea, codemirror_opts);
    codemirror.setOption("mode", this.getAttribute("data-language"));

    // FIXME horrible horrible hack to work around a bug in codemirror where
    // the editor can't be rendered properly before the browser has actually
    // rendered the backing textarea. yield to the browser and poll for
    // completed rendering
    var poll = (function(base){
      return function() {
        if ($(".CodeMirror-gutter", base).offsetWidth == 0) {
          codemirror.refresh();
          window.setTimeout(poll, 1);
        }
      };
    })(this);
    poll();

    return codemirror;
  }

  var setupKeyPressHandlers = function() {
    var base = this;

    var cmd_pressed = false;
    this.addEventListener('keydown', function(e) {
      if (e.keyCode == 17 || e.keyCode == 91) {
        cmd_pressed = true;
      }
    }, false);

    this.addEventListener('keyup', function(e) {
      if (e.keyCode == 17 || e.keyCode == 91) {
        cmd_pressed = false;
      }
    }, false);

    this.addEventListener('keydown', function(e) {
      if (e.keyCode == 13 && cmd_pressed) {
        e.preventDefault();
        base.execute.call(base);
      }
    }, false);
  };

  this.createdCallback = function() {
    var tpl = $.getTemplate("widgets/z-codeeditor", "z-codeeditor-base-tpl");

    var textarea = document.createElement("textarea");
    textarea.setAttribute("autofocus", "autofocus");
    this.appendChild(textarea);

    var codemirror = initCodeMirror.call(this, textarea);
    setupKeyPressHandlers.call(this);

    this.getValue = function() {
      return codemirror.getValue();
    };

    this.setValue = function(value) {
      if (!value) {return;}
      codemirror.setValue(value);
    }

    this.execute = function() {
      var ev = new Event('execute');
      ev.value = this.getValue();
      this.dispatchEvent(ev);
    }
  };
};

var proto = Object.create(HTMLElement.prototype);
CodeEditorComponent.apply(proto);
document.registerElement("z-codeeditor", { prototype: proto });
<|endoftext|>