<commit_before>import { withPluginApi, decorateCooked } from "discourse/lib/plugin-api";
import { Tag } from "discourse/lib/to-markdown";
import ComposerController from "discourse/controllers/composer";

function spoil($elem) {
  $(".spoiler", $elem)
    .removeClass("spoiler")
    .addClass("spoiled")
    .spoil();
}

function initializeSpoiler(api) {
  api.decorateCooked(spoil);

  api.addToolbarPopupMenuOptionsCallback(() => {
    return {
      action: "insertSpoiler",
      icon: "magic",
      label: "spoiler.title"
    };
  });

  ComposerController.reopen({
    actions: {
      insertSpoiler() {
        this.get("toolbarEvent").applySurround(
          "[spoiler]",
          "[/spoiler]",
          "spoiler_text",
          { multiline: false }
        );
      }
    }
  });

  if (Tag) {
    Tag.prototype.decorate = function(text) {
      const attr = this.element.attributes;
      if (attr.class === "spoiled") {
        this.prefix = "[spoiler]";
        this.suffix = "[/spoiler]";
      }

      if (this.prefix || this.suffix) {
        text = [this.prefix, text, this.suffix].join("");
      }

      if (this.inline) {
        text = " " + text + " ";
      }

      return text;
    };

    Tag.block = function(name, prefix, suffix) {
      return class extends Tag {
        constructor() {
          super(name, prefix, suffix);
          this.gap = "\n\n";
        }

        decorate(text) {
          const attr = this.element.attributes;
          const parent = this.element.parent;

          if (this.name === "p" && parent && parent.name === "li") {
            // fix for google docs
            this.gap = "";
          }

          if (this.name === "div" && attr.class === "spoiled") {
            this.prefix = "[spoiler]";
            this.suffix = "[/spoiler]";
            text = text.trim();
          }

          return `${this.gap}${this.prefix}${text}${this.suffix}${this.gap}`;
        }
      };
    };
  }
}

export default {
  name: "apply-spoilers",
  initialize(container) {
    const siteSettings = container.lookup("site-settings:main");
    if (siteSettings.spoiler_enabled) {
      withPluginApi("0.5", initializeSpoiler);
    }
  }
};
<commit_msg>Remove unused variable decorateCooked<commit_after>import { withPluginApi } from "discourse/lib/plugin-api";
import { Tag } from "discourse/lib/to-markdown";
import ComposerController from "discourse/controllers/composer";

function spoil($elem) {
  $(".spoiler", $elem)
    .removeClass("spoiler")
    .addClass("spoiled")
    .spoil();
}

function initializeSpoiler(api) {
  api.decorateCooked(spoil);

  api.addToolbarPopupMenuOptionsCallback(() => {
    return {
      action: "insertSpoiler",
      icon: "magic",
      label: "spoiler.title"
    };
  });

  ComposerController.reopen({
    actions: {
      insertSpoiler() {
        this.get("toolbarEvent").applySurround(
          "[spoiler]",
          "[/spoiler]",
          "spoiler_text",
          { multiline: false }
        );
      }
    }
  });

  if (Tag) {
    Tag.prototype.decorate = function(text) {
      const attr = this.element.attributes;
      if (attr.class === "spoiled") {
        this.prefix = "[spoiler]";
        this.suffix = "[/spoiler]";
      }

      if (this.prefix || this.suffix) {
        text = [this.prefix, text, this.suffix].join("");
      }

      if (this.inline) {
        text = " " + text + " ";
      }

      return text;
    };

    Tag.block = function(name, prefix, suffix) {
      return class extends Tag {
        constructor() {
          super(name, prefix, suffix);
          this.gap = "\n\n";
        }

        decorate(text) {
          const attr = this.element.attributes;
          const parent = this.element.parent;

          if (this.name === "p" && parent && parent.name === "li") {
            // fix for google docs
            this.gap = "";
          }

          if (this.name === "div" && attr.class === "spoiled") {
            this.prefix = "[spoiler]";
            this.suffix = "[/spoiler]";
            text = text.trim();
          }

          return `${this.gap}${this.prefix}${text}${this.suffix}${this.gap}`;
        }
      };
    };
  }
}

export default {
  name: "apply-spoilers",
  initialize(container) {
    const siteSettings = container.lookup("site-settings:main");
    if (siteSettings.spoiler_enabled) {
      withPluginApi("0.5", initializeSpoiler);
    }
  }
};
<|endoftext|>


<commit_before>/**
 * User Input Keywords.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import classnames from 'classnames';

/**
 * WordPress dependencies
 */
import { useCallback, useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { ENTER } from '@wordpress/keycodes';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import { CORE_USER } from '../../googlesitekit/datastore/user/constants';
import { Cell, Input, TextField } from '../../material-components';
import Button from '../Button';
import CloseIcon from '../../../svg/close.svg';
import { COMMA } from '../../util/key-codes';
const { useSelect, useDispatch } = Data;

export default function UserInputKeywords( { slug, max } ) {
	const keywordsContainer = useRef();

	const values = useSelect( ( select ) => select( CORE_USER ).getUserInputSetting( slug ) || [] );
	const { setUserInputSetting } = useDispatch( CORE_USER );

	// Add an empty string if the values array is empty.
	if ( values.length === 0 ) {
		values.push( '' );
	}

	// Need to make sure that dependencies list always has the same number of elements.
	const dependencies = values.concat( Array( max ) ).slice( 0, max );

	const updateKeywords = useCallback( ( keywords ) => {
		const EOT = String.fromCharCode( 4 );
		let newKeywords = keywords
			// Trim keywords to allow no empty spaces at the beginning and at max one space at the end.
			.map( ( keyword ) => keyword.replace( /(\S)\s+$/, '$1 ' ).replace( /^\s+\S/, '' ) )
			// EOT is added to the end to properly combine two sequential empty spaces at the end.
			.concat( [ '', EOT ] )
			.join( EOT )
			.replace( new RegExp( `${ EOT }{3,}`, 'g' ), EOT ); // Combine two sequential empty spaces into one.

		if ( newKeywords === EOT ) {
			newKeywords = [ '' ];
		} else {
			newKeywords = newKeywords.split( EOT ).slice( 0, max );
		}

		setUserInputSetting( slug, newKeywords );
	}, [ slug ] );

	const onKeywordChange = useCallback( ( index, { target } ) => {
		if ( target.value[ target.value.length - 1 ] === ',' ) {
			return;
		}

		updateKeywords( [
			...values.slice( 0, index ),
			target.value,
			...values.slice( index + 1 ),
		] );
	}, dependencies );

	const onKeyDown = useCallback( ( index, { keyCode } ) => {
		const nonEmptyValues = values.filter( ( value ) => value.length > 0 );
		if ( ( keyCode === ENTER || keyCode === COMMA ) && nonEmptyValues.length < max ) {
			updateKeywords( [
				...values.slice( 0, index + 1 ),
				'',
				...values.slice( index + 1 ),
			] );

			setTimeout( () => {
				const input = keywordsContainer.current.querySelector( `#${ slug }-keyword-${ index + 1 }` );
				if ( input ) {
					input.focus();
				}
			}, 50 );
		}
	}, [ keywordsContainer.current, ...dependencies ] );

	const onKeywordDelete = useCallback( ( index ) => {
		updateKeywords( [
			...values.slice( 0, index ),
			...values.slice( index + 1 ),
		] );
	}, dependencies );

	return (
		<Cell lgStart={ 6 } lgSize={ 6 } mdSize={ 8 } smSize={ 4 }>

				{ values.map( ( value, i ) => (

 i + 1 || value.length > 0,
						} ) }
					>
						<TextField
							label={ i + 1 === values.length ? __( 'Enter minimum one (1), maximum three (3) terms', 'google-site-kit' ) : '' }
							noLabel
						>
							<Input
								id={ `${ slug }-keyword-${ i }` }
								value={ value }
								size={ value.length > 0 ? value.length : undefined }
								onChange={ onKeywordChange.bind( null, i ) }
								onKeyDown={ onKeyDown.bind( null, i ) }
							/>
						</TextField>

						{ ( value.length > 0 || i + 1 < values.length ) && (
							<Button
								text
								icon={ <CloseIcon width="14" height="14" /> }
								onClick={ onKeywordDelete.bind( null, i ) }
							/>
						) }

				) ) }

				{ __( 'Separate with commas or the Enter key', 'google-site-kit' ) }

		</Cell>
	);
}

UserInputKeywords.propTypes = {
	slug: PropTypes.string.isRequired,
	max: PropTypes.number,
};

UserInputKeywords.defaultProps = {
	max: 1,
};
<commit_msg>Add hidden label to user input search terms.<commit_after>/**
 * User Input Keywords.
 *
 * Site Kit by Google, Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import classnames from 'classnames';

/**
 * WordPress dependencies
 */
import { useCallback, useRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { ENTER } from '@wordpress/keycodes';

/**
 * Internal dependencies
 */
import Data from 'googlesitekit-data';
import { CORE_USER } from '../../googlesitekit/datastore/user/constants';
import { Cell, Input, TextField } from '../../material-components';
import Button from '../Button';
import CloseIcon from '../../../svg/close.svg';
import { COMMA } from '../../util/key-codes';
const { useSelect, useDispatch } = Data;

export default function UserInputKeywords( { slug, max } ) {
	const keywordsContainer = useRef();

	const values = useSelect( ( select ) => select( CORE_USER ).getUserInputSetting( slug ) || [] );
	const { setUserInputSetting } = useDispatch( CORE_USER );

	// Add an empty string if the values array is empty.
	if ( values.length === 0 ) {
		values.push( '' );
	}

	// Need to make sure that dependencies list always has the same number of elements.
	const dependencies = values.concat( Array( max ) ).slice( 0, max );

	const updateKeywords = useCallback( ( keywords ) => {
		const EOT = String.fromCharCode( 4 );
		let newKeywords = keywords
			// Trim keywords to allow no empty spaces at the beginning and at max one space at the end.
			.map( ( keyword ) => keyword.replace( /(\S)\s+$/, '$1 ' ).replace( /^\s+\S/, '' ) )
			// EOT is added to the end to properly combine two sequential empty spaces at the end.
			.concat( [ '', EOT ] )
			.join( EOT )
			.replace( new RegExp( `${ EOT }{3,}`, 'g' ), EOT ); // Combine two sequential empty spaces into one.

		if ( newKeywords === EOT ) {
			newKeywords = [ '' ];
		} else {
			newKeywords = newKeywords.split( EOT ).slice( 0, max );
		}

		setUserInputSetting( slug, newKeywords );
	}, [ slug ] );

	const onKeywordChange = useCallback( ( index, { target } ) => {
		if ( target.value[ target.value.length - 1 ] === ',' ) {
			return;
		}

		updateKeywords( [
			...values.slice( 0, index ),
			target.value,
			...values.slice( index + 1 ),
		] );
	}, dependencies );

	const onKeyDown = useCallback( ( index, { keyCode } ) => {
		const nonEmptyValues = values.filter( ( value ) => value.length > 0 );
		if ( ( keyCode === ENTER || keyCode === COMMA ) && nonEmptyValues.length < max ) {
			updateKeywords( [
				...values.slice( 0, index + 1 ),
				'',
				...values.slice( index + 1 ),
			] );

			setTimeout( () => {
				const input = keywordsContainer.current.querySelector( `#${ slug }-keyword-${ index + 1 }` );
				if ( input ) {
					input.focus();
				}
			}, 50 );
		}
	}, [ keywordsContainer.current, ...dependencies ] );

	const onKeywordDelete = useCallback( ( index ) => {
		updateKeywords( [
			...values.slice( 0, index ),
			...values.slice( index + 1 ),
		] );
	}, dependencies );

	return (
		<Cell lgStart={ 6 } lgSize={ 6 } mdSize={ 8 } smSize={ 4 }>

				{ values.map( ( value, i ) => (

 i + 1 || value.length > 0,
						} ) }
					>
						<label
							htmlFor={ `${ slug }-keyword-${ i }` }
							className="screen-reader-text"
						>
							{ __( 'Enter minimum one (1), maximum three (3) terms', 'google-site-kit' ) }

						<TextField
							label={ values.length === 1 && values[ 0 ] === ''
								? __( 'Enter minimum one (1), maximum three (3) terms', 'google-site-kit' )
								: '' }
							noLabel
						>
							<Input
								id={ `${ slug }-keyword-${ i }` }
								value={ value }
								size={ value.length > 0 ? value.length : undefined }
								onChange={ onKeywordChange.bind( null, i ) }
								onKeyDown={ onKeyDown.bind( null, i ) }
							/>
						</TextField>

						{ ( value.length > 0 || i + 1 < values.length ) && (
							<Button
								text
								icon={ <CloseIcon width="14" height="14" /> }
								onClick={ onKeywordDelete.bind( null, i ) }
							/>
						) }

				) ) }

				{ __( 'Separate with commas or the Enter key', 'google-site-kit' ) }

		</Cell>
	);
}

UserInputKeywords.propTypes = {
	slug: PropTypes.string.isRequired,
	max: PropTypes.number,
};

UserInputKeywords.defaultProps = {
	max: 1,
};
<|endoftext|>


<commit_before>/**
 * Copyright 2018 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const { makeUserDataDir, removeUserDataDir } = require('./utils');

/**
 * @type {TestSuite}
 */
module.exports.describe = function({testRunner, expect, browserType, defaultBrowserOptions, FFOX, CHROMIUM, WEBKIT, WIN}) {
  const {describe, xdescribe, fdescribe} = testRunner;
  const {it, fit, xit, dit} = testRunner;
  const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;

  const headfulOptions = Object.assign({}, defaultBrowserOptions, {
    headless: false
  });
  const headlessOptions = Object.assign({}, defaultBrowserOptions, {
    headless: true
  });

  describe('Headful', function() {
    it('should have default url when launching browser', async function() {
      const userDataDir = await makeUserDataDir();
      const browserContext = await browserType.launchPersistentContext(userDataDir, headfulOptions);
      const urls = browserContext.pages().map(page => page.url());
      expect(urls).toEqual(['about:blank']);
      await browserContext.close();
      await removeUserDataDir(userDataDir);
    });
    it.slow().fail(WIN && CHROMIUM)('headless should be able to read cookies written by headful', async({server}) => {
      // see https://github.com/microsoft/playwright/issues/717
      const userDataDir = await makeUserDataDir();
      // Write a cookie in headful chrome
      const headfulContext = await browserType.launchPersistentContext(userDataDir, headfulOptions);
      const headfulPage = await headfulContext.newPage();
      await headfulPage.goto(server.EMPTY_PAGE);
      await headfulPage.evaluate(() => document.cookie = 'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
      await headfulContext.close();
      // Read the cookie from headless chrome
      const headlessContext = await browserType.launchPersistentContext(userDataDir, headlessOptions);
      const headlessPage = await headlessContext.newPage();
      await headlessPage.goto(server.EMPTY_PAGE);
      const cookie = await headlessPage.evaluate(() => document.cookie);
      await headlessContext.close();
      // This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778
      await removeUserDataDir(userDataDir);
      expect(cookie).toBe('foo=true');
    });
    it.fail(FFOX)('should close browser with beforeunload page', async({server}) => {
      const userDataDir = await makeUserDataDir();
      const browserContext = await browserType.launchPersistentContext(userDataDir, headfulOptions);
      const page = await browserContext.newPage();
      await page.goto(server.PREFIX + '/beforeunload.html');
      // We have to interact with a page so that 'beforeunload' handlers
      // fire.
      await page.click('body');
      await browserContext.close();
      await removeUserDataDir(userDataDir);
    });
  });
};
<commit_msg>test(firefox): fix should close browser with beforeunload page (#1532)<commit_after>/**
 * Copyright 2018 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const { makeUserDataDir, removeUserDataDir } = require('./utils');

/**
 * @type {TestSuite}
 */
module.exports.describe = function({testRunner, expect, browserType, defaultBrowserOptions, FFOX, CHROMIUM, WEBKIT, WIN}) {
  const {describe, xdescribe, fdescribe} = testRunner;
  const {it, fit, xit, dit} = testRunner;
  const {beforeAll, beforeEach, afterAll, afterEach} = testRunner;

  const headfulOptions = Object.assign({}, defaultBrowserOptions, {
    headless: false
  });
  const headlessOptions = Object.assign({}, defaultBrowserOptions, {
    headless: true
  });

  describe('Headful', function() {
    it('should have default url when launching browser', async function() {
      const userDataDir = await makeUserDataDir();
      const browserContext = await browserType.launchPersistentContext(userDataDir, headfulOptions);
      const urls = browserContext.pages().map(page => page.url());
      expect(urls).toEqual(['about:blank']);
      await browserContext.close();
      await removeUserDataDir(userDataDir);
    });
    it.slow().fail(WIN && CHROMIUM)('headless should be able to read cookies written by headful', async({server}) => {
      // see https://github.com/microsoft/playwright/issues/717
      const userDataDir = await makeUserDataDir();
      // Write a cookie in headful chrome
      const headfulContext = await browserType.launchPersistentContext(userDataDir, headfulOptions);
      const headfulPage = await headfulContext.newPage();
      await headfulPage.goto(server.EMPTY_PAGE);
      await headfulPage.evaluate(() => document.cookie = 'foo=true; expires=Fri, 31 Dec 9999 23:59:59 GMT');
      await headfulContext.close();
      // Read the cookie from headless chrome
      const headlessContext = await browserType.launchPersistentContext(userDataDir, headlessOptions);
      const headlessPage = await headlessContext.newPage();
      await headlessPage.goto(server.EMPTY_PAGE);
      const cookie = await headlessPage.evaluate(() => document.cookie);
      await headlessContext.close();
      // This might throw. See https://github.com/GoogleChrome/puppeteer/issues/2778
      await removeUserDataDir(userDataDir);
      expect(cookie).toBe('foo=true');
    });
    it('should close browser with beforeunload page', async({server}) => {
      const userDataDir = await makeUserDataDir();
      const browserContext = await browserType.launchPersistentContext(userDataDir, headfulOptions);
      const page = await browserContext.newPage();
      await page.goto(server.PREFIX + '/beforeunload.html');
      // We have to interact with a page so that 'beforeunload' handlers
      // fire.
      await page.click('body');
      await browserContext.close();
      await removeUserDataDir(userDataDir);
    });
  });
};
<|endoftext|>


<commit_before>//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Draws "labels" using svg:text and d3plus.utils.wordwrap
//------------------------------------------------------------------------------
d3plus.shape.labels = function(vars,selection,enter,exit) {

  //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  // Label Exiting
  //----------------------------------------------------------------------------
  remove = function(text) {
    text
      .transition().duration(vars.style.timing.transitions)
      .attr("opacity",0)
      .remove()
  }

  //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  // Label Styling
  //----------------------------------------------------------------------------
  style = function(text,wrap) {

    function x_pos(t) {

      var align = vars.style.labels.align,
          tspan = this.tagName == "tspan",
          share = tspan ? this.parentNode.className.baseVal == "share" : this.className.baseVal == "share",
          width = d3.select(this).node().getBBox().width,
          rtl = vars.style.labels.dir == "rtl"

      if (align == "middle" || share) {
        var pos = t.x-width/2
      }
      else if ((align == "end" && !rtl) || (align == "start" && rtl)) {
        var pos = t.x+t.w/2-width
      }
      else {
        var pos = t.x-t.w/2
      }

      if (tspan) {
        if (align == "middle") {
          if (vars.style.labels.dir == "rtl") {
            pos -= (width-this.offsetWidth)/2
          }
          else {
            pos += (width-this.offsetWidth)/2
          }
        }
        else if (align == "end") {
          if (vars.style.labels.dir == "rtl") {
            pos -= (width-this.offsetWidth)
          }
          else {
            pos += (width-this.offsetWidth)
          }
        }
      }

      if (vars.style.labels.dir == "rtl") {
        pos += width
      }

      return pos

    }

    function y_pos(t) {

      if (d3.select(this).select("tspan").empty()) {
        return 0
      }
      else {

        var align = vars.style.labels.align,
            height = d3.select(this).node().getBBox().height,
            diff = parseFloat(d3.select(this).style("font-size"),10)/5

        if (this.className.baseVal == "share") {
          var pheight = d3.select(this.parentNode).datum().d3plus.height
          if (align == "end") {
            var y = t.y-pheight/2+diff/2
          }
          else {
            var y = t.y+pheight/2-height-diff/2
          }
        }
        else {

          if (align == "middle") {
            var y = t.y-height/2-diff/2
          }
          else if (align == "end") {
            var y = t.y+t.h/2-height+diff/2
          }
          else {
            var y = t.y-t.h/2-diff
          }

        }

        return y

      }
    }

    text
      .style("font-weight",vars.style.font.weight)
      .attr("font-family",vars.style.font.family)
      .attr("text-anchor","start")
      .attr("fill", function(t){
        return d3plus.color.text(t.color);
      })
      .attr("x",x_pos)
      .attr("y",y_pos)
      .each(function(t){

        if (wrap) {

          if (t.text) {

            d3plus.utils.wordwrap({
              "text": vars.format(t.text*100,"share")+"%",
              "parent": this,
              "width": t.w,
              "height": t.h,
              "resize": true,
              "font_max": 70
            })

          }
          else {

            if (vars.style.labels.align != "middle") {
              var height = t.h-t.share
            }
            else {
              var height = t.h
            }

            d3plus.utils.wordwrap({
              "text": t.names,
              "parent": this,
              "width": t.w,
              "height": height,
              "resize": true
            })

          }

        }

      })
      .attr("x",x_pos)
      .attr("y",y_pos)
      .selectAll("tspan")
        .attr("x",x_pos)
  }

  //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  // Loop through each selection and analyze the labels
  //----------------------------------------------------------------------------
  if (vars.labels.default) {

    selection.each(function(d){

      var disabled = d.d3plus && "label" in d.d3plus && !d.d3plus.label,
          stat = d.d3plus && "static" in d.d3plus && d.d3plus.static
          label = d.d3plus_label,
          share = d.d3plus_share,
          names = d3plus.variable.text(vars,d),
          group = d3.select(this),
          share_size = 0,
          fill = d3plus.apps[vars.type.default].fill

      if (["line","area"].indexOf(vars.shape.default) >= 0) {
        var background = true
      }
      else {
        var active = vars.active.key ? d.d3plus[vars.active.key] : d.d3plus.active,
            temp = vars.temp.key ? d.d3plus[vars.temp.key] : d.d3plus.temp,
            total = vars.total.key ? d.d3plus[vars.total.key] : d.d3plus.total,
            background = (!temp && !active) || (active == total)
      }

      if (!disabled && (background || !fill) && !stat) {

        if (share && share.w >= 20 && share.h >= 10 && d.d3plus.share && vars.style.labels.align != "middle") {

          share.text = d.d3plus.share
          share.color = d3plus.shape.color(d,vars)

          var text = group.selectAll("text.share")
            .data([share],function(t){
              return t.w+""+t.h+""+t.text
            })

          text
            .transition().duration(vars.style.timing.transitions/2)
            .call(style,true)

          text.enter().insert("text",".mouse")
            .attr("class","share")
            .attr("opacity",0)
            .call(style,true)

          text
            .transition().duration(vars.style.timing.transitions/2)
            .delay(vars.style.timing.transitions/2)
            .attr("opacity",0.5)

          share_size = text.node().getBBox().height

          text.exit()
            .call(remove)

        }
        else {
          group.selectAll("text.share")
            .call(remove)
        }

        if (label && label.w >= 20 && label.h >= 10 && names.length) {

          label.names = names
          label.color = d3plus.shape.color(d,vars)
          label.share = share_size

          var text = group.selectAll("text.label")
            .data([label],function(t){
              return t.w+""+t.h+""+t.names.join("")
            })

          text
            .transition().duration(vars.style.timing.transitions/2)
            .call(style,true)

          text.enter().insert("text",".mouse")
            .attr("class","label")
            .attr("opacity",0)
            .call(style,true)

          text
            .transition().duration(vars.style.timing.transitions/2)
            .delay(vars.style.timing.transitions/2)
              .attr("opacity",1)

          text.exit()
            .call(remove)

        }
        else {
          group.selectAll("text.label")
            .call(remove)
        }

      }
      else {
        group.selectAll("text")
          .call(remove)
      }
    })

  }
  else {

    selection.selectAll("text")
      .call(remove)

  }

}
<commit_msg>support for visualization-defined label rotation, color, and alignment<commit_after>//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// Draws "labels" using svg:text and d3plus.utils.wordwrap
//------------------------------------------------------------------------------
d3plus.shape.labels = function(vars,selection,enter,exit) {

  //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  // Label Exiting
  //----------------------------------------------------------------------------
  remove = function(text) {
    text
      .transition().duration(vars.style.timing.transitions)
      .attr("opacity",0)
      .remove()
  }

  //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  // Label Styling
  //----------------------------------------------------------------------------
  style = function(text,wrap) {

    function x_pos(t) {

      var align = t.anchor || vars.style.labels.align,
          tspan = this.tagName == "tspan",
          share = tspan ? this.parentNode.className.baseVal == "share" : this.className.baseVal == "share",
          width = d3.select(this).node().getBBox().width,
          rtl = vars.style.labels.dir == "rtl"

      if (align == "middle" || share) {
        var pos = t.x-width/2
      }
      else if ((align == "end" && !rtl) || (align == "start" && rtl)) {
        var pos = t.x+t.w/2-width
      }
      else {
        var pos = t.x-t.w/2
      }

      if (tspan) {
        if (align == "middle") {
          if (vars.style.labels.dir == "rtl") {
            pos -= (width-this.offsetWidth)/2
          }
          else {
            pos += (width-this.offsetWidth)/2
          }
        }
        else if (align == "end") {
          if (vars.style.labels.dir == "rtl") {
            pos -= (width-this.offsetWidth)
          }
          else {
            pos += (width-this.offsetWidth)
          }
        }
      }

      if (vars.style.labels.dir == "rtl") {
        pos += width
      }

      return pos

    }

    function y_pos(t) {

      if (d3.select(this).select("tspan").empty()) {
        return 0
      }
      else {

        var align = vars.style.labels.align,
            height = d3.select(this).node().getBBox().height,
            diff = parseFloat(d3.select(this).style("font-size"),10)/5

        if (this.className.baseVal == "share") {
          var pheight = d3.select(this.parentNode).datum().d3plus.height
          if (align == "end") {
            var y = t.y-pheight/2+diff/2
          }
          else {
            var y = t.y+pheight/2-height-diff/2
          }
        }
        else {

          if (align == "middle" || t.valign == "center") {
            var y = t.y-height/2-diff/2
          }
          else if (align == "end") {
            var y = t.y+t.h/2-height+diff/2
          }
          else {
            var y = t.y-t.h/2-diff
          }

        }

        return y

      }
    }

    text
      .style("font-weight",vars.style.font.weight)
      .attr("font-family",vars.style.font.family)
      .attr("text-anchor","start")
      .attr("fill", function(t){
        return t.color;
      })
      .attr("x",x_pos)
      .attr("y",y_pos)
      .attr("transform",function(t){
        var a = t.angle || 0
        return "rotate("+a+",0,0)"
      })
      .each(function(t){

        if (wrap) {

          if (t.text) {

            d3plus.utils.wordwrap({
              "text": vars.format(t.text*100,"share")+"%",
              "parent": this,
              "width": t.w,
              "height": t.h,
              "resize": t.resize,
              "font_max": 70
            })

          }
          else {

            if (vars.style.labels.align != "middle") {
              var height = t.h-t.share
            }
            else {
              var height = t.h
            }

            d3plus.utils.wordwrap({
              "text": t.names,
              "parent": this,
              "width": t.w,
              "height": height,
              "resize": t.resize
            })

          }

        }

      })
      .attr("x",x_pos)
      .attr("y",y_pos)
      .attr("transform",function(t){
        var a = t.angle || 0
        return "rotate("+a+",0,0)"
      })
      .selectAll("tspan")
        .attr("x",x_pos)
  }

  //^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  // Loop through each selection and analyze the labels
  //----------------------------------------------------------------------------
  if (vars.labels.default) {

    selection.each(function(d){

      var disabled = d.d3plus && "label" in d.d3plus && !d.d3plus.label,
          stat = d.d3plus && "static" in d.d3plus && d.d3plus.static
          label = d.d3plus_label,
          share = d.d3plus_share,
          names = d3plus.variable.text(vars,d),
          group = d3.select(this),
          share_size = 0,
          fill = d3plus.apps[vars.type.default].fill

      if (["line","area"].indexOf(vars.shape.default) >= 0) {
        var background = true
      }
      else {
        var active = vars.active.key ? d.d3plus[vars.active.key] : d.d3plus.active,
            temp = vars.temp.key ? d.d3plus[vars.temp.key] : d.d3plus.temp,
            total = vars.total.key ? d.d3plus[vars.total.key] : d.d3plus.total,
            background = (!temp && !active) || (active == total)
      }

      if (!disabled && (background || !fill) && !stat) {

        if (share && share.w >= 20 && share.h >= 10 && d.d3plus.share && vars.style.labels.align != "middle") {

          share.text = d.d3plus.share
          if (!share.color) {
            share.color = d3plus.color.text(d3plus.shape.color(d,vars))
          }
          if (!("resize" in share)) {
            share.resize = true
          }

          var text = group.selectAll("text.share")
            .data([share],function(t){
              return t.w+""+t.h+""+t.text
            })

          text
            .transition().duration(vars.style.timing.transitions/2)
            .call(style,true)

          text.enter().insert("text",".mouse")
            .attr("class","share")
            .attr("opacity",0)
            .call(style,true)

          text
            .transition().duration(vars.style.timing.transitions/2)
            .delay(vars.style.timing.transitions/2)
            .attr("opacity",0.5)

          share_size = text.node().getBBox().height

          text.exit()
            .call(remove)

        }
        else {
          group.selectAll("text.share")
            .call(remove)
        }

        if (label && label.w >= 20 && label.h >= 10 && names.length) {

          label.names = names
          if (!label.color) {
            label.color = d3plus.color.text(d3plus.shape.color(d,vars))
          }
          if (!("resize" in label)) {
            label.resize = true
          }
          label.share = share_size

          var text = group.selectAll("text.label")
            .data([label],function(t){
              return t.w+""+t.h+""+t.names.join("")
            })

          text
            .transition().duration(vars.style.timing.transitions/2)
            .call(style,true)

          text.enter().insert("text",".mouse")
            .attr("font-size",vars.style.labels.font.size)
            .attr("class","label")
            .attr("opacity",0)
            .call(style,true)

          text
            .transition().duration(vars.style.timing.transitions/2)
            .delay(vars.style.timing.transitions/2)
              .attr("opacity",1)

          text.exit()
            .call(remove)

        }
        else {
          group.selectAll("text.label")
            .call(remove)
        }

      }
      else {
        group.selectAll("text")
          .call(remove)
      }
    })

  }
  else {

    selection.selectAll("text")
      .call(remove)

  }

}
<|endoftext|>


<commit_before>export default
	[function(){
		return {
            truncateCrumbs: function(){
                let breadCrumbBarWidth = $('#bread_crumb').outerWidth();
                let menuLinkWidth = $('.BreadCrumb-menuLinkHolder').outerWidth();
                let availableWidth = breadCrumbBarWidth - menuLinkWidth;
                let $breadcrumbClone = $('.BreadCrumb-list').clone().appendTo('#bread_crumb_width_checker');
                let $breadcrumbCloneItems = $breadcrumbClone.find('.BreadCrumb-item');
                // 40px for the padding on the breadcrumb bar and a few extra pixels for rounding
				let breadcrumbBarPadding = 45;
                let expandedBreadcrumbWidth = breadcrumbBarPadding;
                let crumbs = [];
                $breadcrumbCloneItems.css('max-width', 'none');
                $breadcrumbCloneItems.each(function(index, item){
                    let crumbWidth = $(item).outerWidth();
                    expandedBreadcrumbWidth += crumbWidth;
                    crumbs.push({
                        index: index,
                        origWidth: crumbWidth
                    });
                });
                // Remove the clone from the dom
                $breadcrumbClone.remove();console.log(availableWidth);
                if(expandedBreadcrumbWidth > availableWidth) {
                    let widthToTrim = expandedBreadcrumbWidth - availableWidth;
                    // Sort the crumbs from biggest to smallest
                    let sortedCrumbs = _.sortByOrder(crumbs, ["origWidth"], ["desc"]);
                    let maxWidth;
                    for(let i=0; i<sortedCrumbs.length; i++) {
                        if(sortedCrumbs[i+1]) {
                            // This isn't the smallest crumb
                            if(sortedCrumbs[i-1]) {
                                // This isn't the biggest crumb
                                let potentialCrumbsToTrim = i+1;
                                if(potentialCrumbsToTrim*(sortedCrumbs[i].origWidth - sortedCrumbs[i+1].origWidth) > widthToTrim) {
                                    // If we trim down the biggest (i+1) crumbs equally then we can make it fit
                                    maxWidth = maxWidth - (widthToTrim/potentialCrumbsToTrim);
                                    break;
                                }
                                else {
                                    // Trim this biggest crumb down to the next biggest
                                    widthToTrim = widthToTrim - (sortedCrumbs[i].origWidth - sortedCrumbs[i+1].origWidth);
                                    maxWidth = sortedCrumbs[i].origWidth;
                                }
                            }
                            else {
                                // This is the biggest crumb
                                if(sortedCrumbs[i].origWidth - widthToTrim > sortedCrumbs[i+1].origWidth) {
                                    maxWidth = sortedCrumbs[i].origWidth - widthToTrim;
                                    break;
                                }
                                else {
                                    // Trim this biggest crumb down to the next biggest
                                    widthToTrim = widthToTrim - (sortedCrumbs[i].origWidth - sortedCrumbs[i+1].origWidth);
                                    maxWidth = sortedCrumbs[i+1].origWidth;
                                }
                            }
                        }
                        else {
                            // This is the smallest crumb
                            if(sortedCrumbs[i-1]) {
                                // We've gotten all the way down to the smallest crumb without being able to reasonably trim
                                // the previous crumbs.  Go ahead and trim all of them equally.
                                maxWidth = (availableWidth-breadcrumbBarPadding)/(i+1);
                            }
                            else {
                                // There's only one breadcrumb so trim this one down
                                maxWidth = sortedCrumbs[i].origWidth - widthToTrim;
                            }
                        }
                    }
                    $('.BreadCrumb-item').css('max-width', maxWidth);
                }
                else {
                    $('.BreadCrumb-item').css('max-width', 'none');
                }
            }
		};
	}];
<commit_msg>removing unnecessary console.log statement<commit_after>export default
	[function(){
		return {
            truncateCrumbs: function(){
                let breadCrumbBarWidth = $('#bread_crumb').outerWidth();
                let menuLinkWidth = $('.BreadCrumb-menuLinkHolder').outerWidth();
                let availableWidth = breadCrumbBarWidth - menuLinkWidth;
                let $breadcrumbClone = $('.BreadCrumb-list').clone().appendTo('#bread_crumb_width_checker');
                let $breadcrumbCloneItems = $breadcrumbClone.find('.BreadCrumb-item');
                // 40px for the padding on the breadcrumb bar and a few extra pixels for rounding
				let breadcrumbBarPadding = 45;
                let expandedBreadcrumbWidth = breadcrumbBarPadding;
                let crumbs = [];
                $breadcrumbCloneItems.css('max-width', 'none');
                $breadcrumbCloneItems.each(function(index, item){
                    let crumbWidth = $(item).outerWidth();
                    expandedBreadcrumbWidth += crumbWidth;
                    crumbs.push({
                        index: index,
                        origWidth: crumbWidth
                    });
                });
                // Remove the clone from the dom
                $breadcrumbClone.remove();
                if(expandedBreadcrumbWidth > availableWidth) {
                    let widthToTrim = expandedBreadcrumbWidth - availableWidth;
                    // Sort the crumbs from biggest to smallest
                    let sortedCrumbs = _.sortByOrder(crumbs, ["origWidth"], ["desc"]);
                    let maxWidth;
                    for(let i=0; i<sortedCrumbs.length; i++) {
                        if(sortedCrumbs[i+1]) {
                            // This isn't the smallest crumb
                            if(sortedCrumbs[i-1]) {
                                // This isn't the biggest crumb
                                let potentialCrumbsToTrim = i+1;
                                if(potentialCrumbsToTrim*(sortedCrumbs[i].origWidth - sortedCrumbs[i+1].origWidth) > widthToTrim) {
                                    // If we trim down the biggest (i+1) crumbs equally then we can make it fit
                                    maxWidth = maxWidth - (widthToTrim/potentialCrumbsToTrim);
                                    break;
                                }
                                else {
                                    // Trim this biggest crumb down to the next biggest
                                    widthToTrim = widthToTrim - (sortedCrumbs[i].origWidth - sortedCrumbs[i+1].origWidth);
                                    maxWidth = sortedCrumbs[i].origWidth;
                                }
                            }
                            else {
                                // This is the biggest crumb
                                if(sortedCrumbs[i].origWidth - widthToTrim > sortedCrumbs[i+1].origWidth) {
                                    maxWidth = sortedCrumbs[i].origWidth - widthToTrim;
                                    break;
                                }
                                else {
                                    // Trim this biggest crumb down to the next biggest
                                    widthToTrim = widthToTrim - (sortedCrumbs[i].origWidth - sortedCrumbs[i+1].origWidth);
                                    maxWidth = sortedCrumbs[i+1].origWidth;
                                }
                            }
                        }
                        else {
                            // This is the smallest crumb
                            if(sortedCrumbs[i-1]) {
                                // We've gotten all the way down to the smallest crumb without being able to reasonably trim
                                // the previous crumbs.  Go ahead and trim all of them equally.
                                maxWidth = (availableWidth-breadcrumbBarPadding)/(i+1);
                            }
                            else {
                                // There's only one breadcrumb so trim this one down
                                maxWidth = sortedCrumbs[i].origWidth - widthToTrim;
                            }
                        }
                    }
                    $('.BreadCrumb-item').css('max-width', maxWidth);
                }
                else {
                    $('.BreadCrumb-item').css('max-width', 'none');
                }
            }
		};
	}];
<|endoftext|>


<commit_before>/* global test, expect, beforeEach */
import { kea, getContext, resetContext } from '../../src'

import './helper/jsdom'
import React from 'react'
import PropTypes from 'prop-types'
import { Provider } from 'react-redux'
import { render, screen } from '@testing-library/react'

beforeEach(() => {
  resetContext({ createStore: true })
})

test('connect works as an object', () => {
  const { store } = getContext()
  const connectedLogic = kea({
    actions: () => ({
      updateDescription: (description) => ({ description }),
    }),

    reducers: ({ actions }) => ({
      description: [
        'default',
        PropTypes.string,
        {
          [actions.updateDescription]: (_, payload) => payload.description,
        },
      ],
    }),
  })

  const logic = kea({
    connect: {
      values: [connectedLogic, ['description']],
    },

    actions: () => ({
      updateName: (name) => ({ name }),
    }),

    reducers: ({ actions }) => ({
      name: [
        'chirpy',
        PropTypes.string,
        {
          [actions.updateName]: (state, payload) => payload.name,
        },
      ],
    }),

    selectors: ({ selectors }) => ({
      capitalizedName: [
        () => [selectors.name],
        (name) => {
          return name
            .trim()
            .split(' ')
            .map((k) => `${k.charAt(0).toUpperCase()}${k.slice(1).toLowerCase()}`)
            .join(' ')
        },
        PropTypes.string,
      ],
    }),
  })

  const SampleComponent = ({ id, name, capitalizedName, description, actions: { updateName } }) => (

{id}

{name}

{capitalizedName}

{description}

        updateName

  )

  const ConnectedComponent = logic(SampleComponent)

  expect(store.getState()).toEqual({ kea: {} })

  render(
    <Provider store={store}>
      <ConnectedComponent id={12} />
    </Provider>,
  )

  expect(store.getState()).toEqual({ kea: { logic: { 1: { name: 'chirpy' }, 2: { description: 'default' } } } })

  expect(screen.getByTestId('id')).toHaveTextContent('12')
  expect(screen.getByTestId('name')).toHaveTextContent('chirpy')
  expect(screen.getByTestId('capitalizedName')).toHaveTextContent('Chirpy')
  expect(screen.getByTestId('description')).toHaveTextContent('default')

  expect(store.getState()).toEqual({ kea: { logic: { 1: { name: 'chirpy' }, 2: { description: 'default' } } } })

  logic.actions.updateName('somename')
  connectedLogic.actions.updateDescription('new description')

  expect(store.getState()).toEqual({
    kea: { logic: { 1: { name: 'somename' }, 2: { description: 'new description' } } },
  })

  expect(screen.getByTestId('id')).toHaveTextContent('12')
  expect(screen.getByTestId('name')).toHaveTextContent('somename')
  expect(screen.getByTestId('capitalizedName')).toHaveTextContent('Somename')
  expect(screen.getByTestId('description')).toHaveTextContent('new description')
})

test('connect works as a function', () => {
  const { store } = getContext()
  const connectedLogic = kea({
    actions: () => ({
      updateDescription: (description) => ({ description }),
    }),

    reducers: ({ actions }) => ({
      description: [
        'default',
        PropTypes.string,
        {
          [actions.updateDescription]: (_, payload) => payload.description,
        },
      ],
    }),
  })

  const logic = kea({
    connect: () => ({
      values: [connectedLogic, ['description']],
    }),

    actions: () => ({
      updateName: (name) => ({ name }),
    }),

    reducers: ({ actions }) => ({
      name: [
        'chirpy',
        PropTypes.string,
        {
          [actions.updateName]: (state, payload) => payload.name,
        },
      ],
    }),

    selectors: ({ selectors }) => ({
      capitalizedName: [
        () => [selectors.name],
        (name) => {
          return name
            .trim()
            .split(' ')
            .map((k) => `${k.charAt(0).toUpperCase()}${k.slice(1).toLowerCase()}`)
            .join(' ')
        },
        PropTypes.string,
      ],
    }),
  })

  const SampleComponent = ({ id, name, capitalizedName, description, actions: { updateName } }) => (

{id}

{name}

{capitalizedName}

{description}

        updateName

  )

  const ConnectedComponent = logic(SampleComponent)

  expect(store.getState()).toEqual({ kea: {} })

  render(
    <Provider store={store}>
      <ConnectedComponent id={12} />
    </Provider>,
  )

  expect(store.getState()).toEqual({ kea: { logic: { 1: { name: 'chirpy' }, 2: { description: 'default' } } } })

  expect(screen.getByTestId('id')).toHaveTextContent('12')
  expect(screen.getByTestId('name')).toHaveTextContent('chirpy')
  expect(screen.getByTestId('capitalizedName')).toHaveTextContent('Chirpy')
  expect(screen.getByTestId('description')).toHaveTextContent('default')

  expect(store.getState()).toEqual({ kea: { logic: { 1: { name: 'chirpy' }, 2: { description: 'default' } } } })

  store.dispatch(logic.actionCreators.updateName('somename'))
  store.dispatch(connectedLogic.actionCreators.updateDescription('new description'))

  expect(store.getState()).toEqual({
    kea: { logic: { 1: { name: 'somename' }, 2: { description: 'new description' } } },
  })

  expect(screen.getByTestId('id')).toHaveTextContent('12')
  expect(screen.getByTestId('name')).toHaveTextContent('somename')
  expect(screen.getByTestId('capitalizedName')).toHaveTextContent('Somename')
  expect(screen.getByTestId('description')).toHaveTextContent('new description')
})

test('props cascade when connecting', () => {
  const { store } = getContext()

  const connectedLogic = kea({
    actions: () => ({
      updateDescription: (description) => ({ description }),
    }),

    reducers: ({ actions, props }) => ({
      description: [
        props.defaultDescription || '',
        PropTypes.string,
        {
          [actions.updateDescription]: (_, payload) => payload.description,
        },
      ],
    }),
  })

  const logic = kea({
    connect: {
      values: [connectedLogic, ['description']],
    },

    actions: () => ({
      updateName: (name) => ({ name }),
    }),

    reducers: ({ actions, props }) => ({
      name: [
        `chirpy-${props.id}`,
        PropTypes.string,
        {
          [actions.updateName]: (state, payload) => payload.name,
        },
      ],
    }),

    selectors: ({ selectors }) => ({
      capitalizedName: [
        () => [selectors.name],
        (name) => {
          return name
            .trim()
            .split(' ')
            .map((k) => `${k.charAt(0).toUpperCase()}${k.slice(1).toLowerCase()}`)
            .join(' ')
        },
        PropTypes.string,
      ],
    }),
  })

  const SampleComponent = ({ id, name, capitalizedName, description, actions: { updateName } }) => (

{id}

{name}

{capitalizedName}

{description}

        updateName

  )

  const ConnectedComponent = logic(SampleComponent)

  expect(store.getState()).toEqual({ kea: {} })

  render(
    <Provider store={store}>
      <ConnectedComponent id={12} defaultDescription="this is a bird" />
    </Provider>,
  )

  expect(store.getState()).toEqual({
    kea: { logic: { 1: { name: 'chirpy-12' }, 2: { description: 'this is a bird' } } },
  })

  expect(screen.getByTestId('id')).toHaveTextContent('12')
  expect(screen.getByTestId('name')).toHaveTextContent('chirpy-12')
  expect(screen.getByTestId('capitalizedName')).toHaveTextContent('Chirpy-12')
  expect(screen.getByTestId('description')).toHaveTextContent('this is a bird')

  logic.actions.updateName('somename')
  connectedLogic.actions.updateDescription('new description')

  expect(store.getState()).toEqual({
    kea: { logic: { 1: { name: 'somename' }, 2: { description: 'new description' } } },
  })

  expect(screen.getByTestId('id')).toHaveTextContent('12')
  expect(screen.getByTestId('name')).toHaveTextContent('somename')
  expect(screen.getByTestId('capitalizedName')).toHaveTextContent('Somename')
  expect(screen.getByTestId('description')).toHaveTextContent('new description')
})

test('can connect logic without values/actions', () => {
  const { store } = getContext()

  const connectedLogic = kea({
    actions: () => ({
      updateDescription: (description) => ({ description }),
    }),

    reducers: ({ actions, props }) => ({
      description: [
        'default',
        PropTypes.string,
        {
          [actions.updateDescription]: (_, payload) => payload.description,
        },
      ],
    }),
  })

  const logic = kea({
    connect: {
      logic: [connectedLogic],
    },

    actions: () => ({
      updateName: (name) => ({ name }),
    }),

    reducers: ({ actions, props }) => ({
      name: [
        `chirpy-${props.id}`,
        PropTypes.string,
        {
          [actions.updateName]: (state, payload) => payload.name,
        },
      ],
    }),
  })

  logic({ id: 12 }).mount()

  expect(logic.values.name).toEqual('chirpy-12')
  expect(connectedLogic.values.description).toEqual('default')
})

test('can connect logic via array', () => {
  const { store } = getContext()

  const connectedLogic = kea({
    actions: () => ({
      updateDescription: (description) => ({ description }),
    }),

    reducers: ({ actions, props }) => ({
      description: [
        'default',
        PropTypes.string,
        {
          [actions.updateDescription]: (_, payload) => payload.description,
        },
      ],
    }),
  })

  const logic = kea({
    connect: [connectedLogic],

    actions: () => ({
      updateName: (name) => ({ name }),
    }),

    reducers: ({ actions, props }) => ({
      name: [
        `chirpy-${props.id}`,
        PropTypes.string,
        {
          [actions.updateName]: (state, payload) => payload.name,
        },
      ],
    }),
  })

  logic({ id: 12 }).mount()

  expect(logic.values.name).toEqual('chirpy-12')
  expect(connectedLogic.values.description).toEqual('default')
})

describe('can connect in a loop', () => {
  const runTest = (buildTheLogic = true) => {
    let logic1
    let logic2

    logic1 = kea({
      connect: () => ({
        values: [buildTheLogic ? logic2() : logic2, ['value2 as connectValue2']],
        actions: [buildTheLogic ? logic2() : logic2, ['action2']],
        logic: [buildTheLogic ? logic2() : logic2],
      }),
      actions: { action1: true },
      reducers: { value1: ['string1', { action1: () => 'action1', action2: () => 'action2' }] },
      selectors: () => ({ value2: [() => [(state) => logic2.selectors.value2(state)], (value2) => value2] }),
      // selectors: () => ({ value2: [() => [logic2.selectors.value2], (value2) => value2] }),
    })

    logic2 = kea({
      connect: () => ({
        values: [buildTheLogic ? logic1() : logic1, ['value1 as connectValue1']],
        actions: [buildTheLogic ? logic1() : logic1, ['action1']],
        logic: [buildTheLogic ? logic1() : logic1],
      }),
      actions: { action2: true },
      reducers: { value2: ['string2', { action1: () => 'action1', action2: () => 'action2' }] },
      selectors: () => ({ value1: [() => [(state) => logic1.selectors.value1(state)], (value1) => value1] }),
      // selectors: () => ({ value1: [() => [logic1.selectors.value1], (value1) => value1] }),
    })

    expect(() => {
      logic1.mount()
    }).not.toThrow()

    // own reducers
    expect(logic1.values.value1).toEqual('string1')
    expect(logic2.values.value2).toEqual('string2')

    // connected values
    expect(logic1.values.connectValue2).toEqual('string2')
    expect(logic2.values.connectValue1).toEqual('string1')

    logic2.actions.action1()

    expect(logic1.values.value1).toEqual('action1')
    expect(logic2.values.value2).toEqual('action1')
    expect(logic1.values.connectValue2).toEqual('action1')
    expect(logic2.values.connectValue1).toEqual('action1')

    logic1.actions.action2()

    expect(logic1.values.value1).toEqual('action2')
    expect(logic2.values.value2).toEqual('action2')
    expect(logic1.values.connectValue2).toEqual('action2')
    expect(logic2.values.connectValue1).toEqual('action2')
  }

  test('built logic', () => runTest(true))
  test('logic wrapper', () => runTest(false))
})
<commit_msg>one extra check<commit_after>/* global test, expect, beforeEach */
import { kea, getContext, resetContext } from '../../src'

import './helper/jsdom'
import React from 'react'
import PropTypes from 'prop-types'
import { Provider } from 'react-redux'
import { render, screen } from '@testing-library/react'

beforeEach(() => {
  resetContext({ createStore: true })
})

test('connect works as an object', () => {
  const { store } = getContext()
  const connectedLogic = kea({
    actions: () => ({
      updateDescription: (description) => ({ description }),
    }),

    reducers: ({ actions }) => ({
      description: [
        'default',
        PropTypes.string,
        {
          [actions.updateDescription]: (_, payload) => payload.description,
        },
      ],
    }),
  })

  const logic = kea({
    connect: {
      values: [connectedLogic, ['description']],
    },

    actions: () => ({
      updateName: (name) => ({ name }),
    }),

    reducers: ({ actions }) => ({
      name: [
        'chirpy',
        PropTypes.string,
        {
          [actions.updateName]: (state, payload) => payload.name,
        },
      ],
    }),

    selectors: ({ selectors }) => ({
      capitalizedName: [
        () => [selectors.name],
        (name) => {
          return name
            .trim()
            .split(' ')
            .map((k) => `${k.charAt(0).toUpperCase()}${k.slice(1).toLowerCase()}`)
            .join(' ')
        },
        PropTypes.string,
      ],
    }),
  })

  const SampleComponent = ({ id, name, capitalizedName, description, actions: { updateName } }) => (

{id}

{name}

{capitalizedName}

{description}

        updateName

  )

  const ConnectedComponent = logic(SampleComponent)

  expect(store.getState()).toEqual({ kea: {} })

  render(
    <Provider store={store}>
      <ConnectedComponent id={12} />
    </Provider>,
  )

  expect(store.getState()).toEqual({ kea: { logic: { 1: { name: 'chirpy' }, 2: { description: 'default' } } } })

  expect(screen.getByTestId('id')).toHaveTextContent('12')
  expect(screen.getByTestId('name')).toHaveTextContent('chirpy')
  expect(screen.getByTestId('capitalizedName')).toHaveTextContent('Chirpy')
  expect(screen.getByTestId('description')).toHaveTextContent('default')

  expect(store.getState()).toEqual({ kea: { logic: { 1: { name: 'chirpy' }, 2: { description: 'default' } } } })

  logic.actions.updateName('somename')
  connectedLogic.actions.updateDescription('new description')

  expect(store.getState()).toEqual({
    kea: { logic: { 1: { name: 'somename' }, 2: { description: 'new description' } } },
  })

  expect(screen.getByTestId('id')).toHaveTextContent('12')
  expect(screen.getByTestId('name')).toHaveTextContent('somename')
  expect(screen.getByTestId('capitalizedName')).toHaveTextContent('Somename')
  expect(screen.getByTestId('description')).toHaveTextContent('new description')
})

test('connect works as a function', () => {
  const { store } = getContext()
  const connectedLogic = kea({
    actions: () => ({
      updateDescription: (description) => ({ description }),
    }),

    reducers: ({ actions }) => ({
      description: [
        'default',
        PropTypes.string,
        {
          [actions.updateDescription]: (_, payload) => payload.description,
        },
      ],
    }),
  })

  const logic = kea({
    connect: () => ({
      values: [connectedLogic, ['description']],
    }),

    actions: () => ({
      updateName: (name) => ({ name }),
    }),

    reducers: ({ actions }) => ({
      name: [
        'chirpy',
        PropTypes.string,
        {
          [actions.updateName]: (state, payload) => payload.name,
        },
      ],
    }),

    selectors: ({ selectors }) => ({
      capitalizedName: [
        () => [selectors.name],
        (name) => {
          return name
            .trim()
            .split(' ')
            .map((k) => `${k.charAt(0).toUpperCase()}${k.slice(1).toLowerCase()}`)
            .join(' ')
        },
        PropTypes.string,
      ],
    }),
  })

  const SampleComponent = ({ id, name, capitalizedName, description, actions: { updateName } }) => (

{id}

{name}

{capitalizedName}

{description}

        updateName

  )

  const ConnectedComponent = logic(SampleComponent)

  expect(store.getState()).toEqual({ kea: {} })

  render(
    <Provider store={store}>
      <ConnectedComponent id={12} />
    </Provider>,
  )

  expect(store.getState()).toEqual({ kea: { logic: { 1: { name: 'chirpy' }, 2: { description: 'default' } } } })

  expect(screen.getByTestId('id')).toHaveTextContent('12')
  expect(screen.getByTestId('name')).toHaveTextContent('chirpy')
  expect(screen.getByTestId('capitalizedName')).toHaveTextContent('Chirpy')
  expect(screen.getByTestId('description')).toHaveTextContent('default')

  expect(store.getState()).toEqual({ kea: { logic: { 1: { name: 'chirpy' }, 2: { description: 'default' } } } })

  store.dispatch(logic.actionCreators.updateName('somename'))
  store.dispatch(connectedLogic.actionCreators.updateDescription('new description'))

  expect(store.getState()).toEqual({
    kea: { logic: { 1: { name: 'somename' }, 2: { description: 'new description' } } },
  })

  expect(screen.getByTestId('id')).toHaveTextContent('12')
  expect(screen.getByTestId('name')).toHaveTextContent('somename')
  expect(screen.getByTestId('capitalizedName')).toHaveTextContent('Somename')
  expect(screen.getByTestId('description')).toHaveTextContent('new description')
})

test('props cascade when connecting', () => {
  const { store } = getContext()

  const connectedLogic = kea({
    actions: () => ({
      updateDescription: (description) => ({ description }),
    }),

    reducers: ({ actions, props }) => ({
      description: [
        props.defaultDescription || '',
        PropTypes.string,
        {
          [actions.updateDescription]: (_, payload) => payload.description,
        },
      ],
    }),
  })

  const logic = kea({
    connect: {
      values: [connectedLogic, ['description']],
    },

    actions: () => ({
      updateName: (name) => ({ name }),
    }),

    reducers: ({ actions, props }) => ({
      name: [
        `chirpy-${props.id}`,
        PropTypes.string,
        {
          [actions.updateName]: (state, payload) => payload.name,
        },
      ],
    }),

    selectors: ({ selectors }) => ({
      capitalizedName: [
        () => [selectors.name],
        (name) => {
          return name
            .trim()
            .split(' ')
            .map((k) => `${k.charAt(0).toUpperCase()}${k.slice(1).toLowerCase()}`)
            .join(' ')
        },
        PropTypes.string,
      ],
    }),
  })

  const SampleComponent = ({ id, name, capitalizedName, description, actions: { updateName } }) => (

{id}

{name}

{capitalizedName}

{description}

        updateName

  )

  const ConnectedComponent = logic(SampleComponent)

  expect(store.getState()).toEqual({ kea: {} })

  render(
    <Provider store={store}>
      <ConnectedComponent id={12} defaultDescription="this is a bird" />
    </Provider>,
  )

  expect(store.getState()).toEqual({
    kea: { logic: { 1: { name: 'chirpy-12' }, 2: { description: 'this is a bird' } } },
  })

  expect(screen.getByTestId('id')).toHaveTextContent('12')
  expect(screen.getByTestId('name')).toHaveTextContent('chirpy-12')
  expect(screen.getByTestId('capitalizedName')).toHaveTextContent('Chirpy-12')
  expect(screen.getByTestId('description')).toHaveTextContent('this is a bird')

  logic.actions.updateName('somename')
  connectedLogic.actions.updateDescription('new description')

  expect(store.getState()).toEqual({
    kea: { logic: { 1: { name: 'somename' }, 2: { description: 'new description' } } },
  })

  expect(screen.getByTestId('id')).toHaveTextContent('12')
  expect(screen.getByTestId('name')).toHaveTextContent('somename')
  expect(screen.getByTestId('capitalizedName')).toHaveTextContent('Somename')
  expect(screen.getByTestId('description')).toHaveTextContent('new description')
})

test('can connect logic without values/actions', () => {
  const { store } = getContext()

  const connectedLogic = kea({
    actions: () => ({
      updateDescription: (description) => ({ description }),
    }),

    reducers: ({ actions, props }) => ({
      description: [
        'default',
        PropTypes.string,
        {
          [actions.updateDescription]: (_, payload) => payload.description,
        },
      ],
    }),
  })

  const logic = kea({
    connect: {
      logic: [connectedLogic],
    },

    actions: () => ({
      updateName: (name) => ({ name }),
    }),

    reducers: ({ actions, props }) => ({
      name: [
        `chirpy-${props.id}`,
        PropTypes.string,
        {
          [actions.updateName]: (state, payload) => payload.name,
        },
      ],
    }),
  })

  logic({ id: 12 }).mount()

  expect(logic.values.name).toEqual('chirpy-12')
  expect(connectedLogic.values.description).toEqual('default')
})

test('can connect logic via array', () => {
  const { store } = getContext()

  const connectedLogic = kea({
    actions: () => ({
      updateDescription: (description) => ({ description }),
    }),

    reducers: ({ actions, props }) => ({
      description: [
        'default',
        PropTypes.string,
        {
          [actions.updateDescription]: (_, payload) => payload.description,
        },
      ],
    }),
  })

  const logic = kea({
    connect: [connectedLogic],

    actions: () => ({
      updateName: (name) => ({ name }),
    }),

    reducers: ({ actions, props }) => ({
      name: [
        `chirpy-${props.id}`,
        PropTypes.string,
        {
          [actions.updateName]: (state, payload) => payload.name,
        },
      ],
    }),
  })

  logic({ id: 12 }).mount()

  expect(logic.values.name).toEqual('chirpy-12')
  expect(connectedLogic.values.description).toEqual('default')
})

describe('can connect in a loop', () => {
  const runTest = (buildTheLogic = true) => {
    let logic1
    let logic2

    logic1 = kea({
      connect: () => ({
        values: [buildTheLogic ? logic2() : logic2, ['value2 as connectValue2']],
        actions: [buildTheLogic ? logic2() : logic2, ['action2']],
        logic: [buildTheLogic ? logic2() : logic2],
      }),
      actions: { action1: true },
      reducers: { value1: ['string1', { action1: () => 'action1', action2: () => 'action2' }] },
      selectors: () => ({ value2: [() => [(state) => logic2.selectors.value2(state)], (value2) => value2] }),
      // selectors: () => ({ value2: [() => [logic2.selectors.value2], (value2) => value2] }),
    })

    logic2 = kea({
      connect: () => ({
        values: [buildTheLogic ? logic1() : logic1, ['value1 as connectValue1']],
        actions: [buildTheLogic ? logic1() : logic1, ['action1']],
        logic: [buildTheLogic ? logic1() : logic1],
      }),
      actions: { action2: true },
      reducers: { value2: ['string2', { action1: () => 'action1', action2: () => 'action2' }] },
      selectors: () => ({ value1: [() => [(state) => logic1.selectors.value1(state)], (value1) => value1] }),
      // selectors: () => ({ value1: [() => [logic1.selectors.value1], (value1) => value1] }),
    })

    let unmount
    expect(() => {
      unmount = logic1.mount()
    }).not.toThrow()

    // own reducers
    expect(logic1.values.value1).toEqual('string1')
    expect(logic2.values.value2).toEqual('string2')

    // connected values
    expect(logic1.values.connectValue2).toEqual('string2')
    expect(logic2.values.connectValue1).toEqual('string1')

    logic2.actions.action1()

    expect(logic1.values.value1).toEqual('action1')
    expect(logic2.values.value2).toEqual('action1')
    expect(logic1.values.connectValue2).toEqual('action1')
    expect(logic2.values.connectValue1).toEqual('action1')

    logic1.actions.action2()

    expect(logic1.values.value1).toEqual('action2')
    expect(logic2.values.value2).toEqual('action2')
    expect(logic1.values.connectValue2).toEqual('action2')
    expect(logic2.values.connectValue1).toEqual('action2')

    unmount()

    expect(logic1.isMounted()).toBeFalsy()
    expect(logic2.isMounted()).toBeFalsy()
  }

  test('built logic', () => runTest(true))
  test('logic wrapper', () => runTest(false))
})
<|endoftext|>


<commit_before>var sys = require('sys'),
   http = require('http'),
 assert = require('assert'),
   path = require('path'),
 events = require('events'),
    url = require('url');

var vows = require('vows');

require.paths.unshift(__dirname, path.join(__dirname, '..'));

var journey = require('lib/journey');

var resources = {
    "home": {
        index: function (res) {
            res.send("honey I'm home!");
        },
        room: function (res, params) {
            assert.equal(params.candles, "lit");
            assert.equal(params.slippers, "on");
            res.send(params);
        }
    },
    "picnic": {
        fail: function () {
            throw "fail!";
        }
    },
    "kitchen": {},
    "recipies": {}
};

//
// Initialize the router
//
var router = new(journey.Router)(function (map) {
    this.route('GET', 'picnic/fail').bind(resources.picnic.fail);

    //map.root.bind(function (res) { res.send("Welcome to the Root") });
    map.get('/home/room').bind(resources.home.room);
    map.get('/undefined').bind();
    map.get('/unbound');

    map.root.bind(function (res) { return resources.home.index(res) });

    map.get('/').bind(function (res) { res.send(200) });
    map.get('/twice').bind(function (res) { res.send("twice") });
    map.get(/twice/).bind(function (res) { res.send(302) });

    map.path('/domain', function () {
        this.path('/v1', function () {
            this.root.bind(function (res) { res.send({ root: true, version: 1 }) });
            this.get().bind(function (res) { res.send({ version: 1 }) });
            this.get('/info').bind(function (res) {
                res.send(200, {}, ['info']);
            });
            this.path('/empty', function () {});
        });
    });

    map.route(['GET', 'PUT'], /^(\w+)$/).
        bind(function (res, r) { return resources[r].index(res) });
    map.route('GET', /^(\w+)\/([0-9]+)$/).
        bind(function (res, r, k) { return resources[r].get(res, k) });
    map.route('PUT', /^(\w+)\/([0-9]+)$/, { payload: true }).
        bind(function (res, r, k) { return resources[r].update(res, k) });
    map.route('POST', /^(\w+)$/, { payload: true }).
        bind(function (res, r, doc) { return resources[r].create(res, doc) });
    map.route('DELETE', /^(\w+)\/([0-9]+)$/).
        bind(function (res, r, k) { return resources[r].destroy(res, k) });

    map.put('home/assert', { assert: function (res, body) { return body.length === 9; } }).
        bind(function (res) { res.send(200, {"Content-Type":"text/html"}, "OK"); });
});

var mock = require('lib/journey/mock-request').mock(router);

var get = mock.get,
    del = mock.del,
   post = mock.post,
    put = mock.put;

journey.env = 'test';

vows.describe('Journey').addVows({
    //
    // SUCCESSFUL (2xx)
    //
    "A valid HTTP request": {
        topic: function () { return get('/', { accept: "application/json" }) },

        "returns a 200": function (res) {
            assert.equal(res.status, 200);
        },
        "returns a body": function (res) {
            assert.equal(res.body.journey, "honey I'm home!");
        }
    },

    "A valid request with multiple Accept types": {
        topic: function () { return get('/', { accept: "text/plain;q=10, application/json" }) },

        "returns a 200": function (res) {
            assert.equal(res.status, 200);
        },
        "returns a body": function (res) {
            assert.equal(res.body.journey, "honey I'm home!");
        }
    },

    "A request with uri parameters": {
        topic: function () {
            // URI parameters get parsed into a javascript object, and are passed to the
            // function handler like so:
            return get('/home/room?slippers=on&candles=lit');
        },

        "returns a 200": function (res) {
            assert.equal(res.status, 200);
        },
        "gets parsed into an object": function (res) {
            assert.equal(res.body.slippers, 'on');
            assert.equal(res.body.candles, 'lit');
        }
    },
    "A request without uri parameters": {
        topic: function () {
            var promise = new(events.EventEmitter);
            router.routes.unshift({
                pattern: /^noparams$/,
                method: 'GET', handler: function (res) {
                    promise.emit('success', arguments);
                }, success: undefined, constraints: {}
            });
            router.route(mock.mockRequest('GET', '/noparams', {}));
            return promise;
        },
        "should pass an empty params object": function (args) {
            var params = args[args.length - 1];
            assert.isObject(params);
            assert.equal(Object.keys(params).length, 0);
        }
    },

    "A request with two matching routes": {
        topic: function () {
            return get('/twice');
        },

        "returns a 200": function (res) {
            assert.equal(res.status, 200);
        },
        "returns the first matching route": function (res) {
            assert.equal(res.body.journey, 'twice');
        }
    },

    // Here, we're sending a POST request; the input is parsed into an object, and passed
    // to the function handler as a parameter.
    // We expect Journey to respond with a 201 'Created', if the request was successful.
    "A POST request": {
        "with a JSON body": {
            topic: function () {
                resources["kitchen"].create = function (res, input) {
                    res.send("cooking-time: " + (input['chicken'].length + input['fries'].length) + 'min');
                };
                return post('/kitchen', null, JSON.stringify(
                    {"chicken":"roasted", "fries":"golden"}
                ));
            },
            "returns a 201": function (res) {
                assert.equal(res.status, 201);
            },
            "gets parsed into an object": function (res) {
                assert.equal(res.body.journey, 'cooking-time: 13min');
            }
        },
        "with a query-string body": {
            topic: function () {
                resources["kitchen"].create = function (res, input) {
                    res.send("cooking-time: "         +
                            (input['chicken'].length  +
                             input['fries'].length)   + 'min');
                };
                return post('/kitchen', {accept: 'application/json'},
                                        "chicken=roasted&fries=golden");
            },
            "returns a 201": function (res) {
                assert.equal(res.status, 201);
            },
            "gets parsed into an object": function (res) {
                assert.equal(res.body.journey, 'cooking-time: 13min');
            }
        }
    },

    //
    // CLIENT ERRORS (4xx)
    //

    // Journey being a JSON only server, asking for text/html returns 'Not Acceptable'
    "A request for text/html": {
        topic: function () {
            return get('/', { accept: "text/html" });
        },
        "returns a 406": function (res) { assert.equal(res.status, 406) }
    },
    // This request doesn't have a matching route, it'll therefore return a 404.
    "A request which doesn't match anything": {
        topic: function () {
            return del('/hello/world');
        },
        "returns a 404": function (res) {
            assert.equal(res.status, 404);
        }
    },
    // This request contains malformed JSON data, the server replies
    // with a 400 'Bad Request'
    "An invalid request": {
        topic: function () {
            return post('/malformed', null, "{bad: json}");
        },
        "returns a 400": function (res) {
            assert.equal(res.status, 400);
        }
    },
    // Trying to access an undefined function will result in a 500,
    // as long as the uri format is valid
    "A route binded to an undefined function": {
        topic: function () {
            return get('/undefined');
        },
        "returns a 500": function (res) {
            assert.equal(res.status, 500);
        }
    },
    // Trying to access an unbound route, will result in a 501 'Not Implemented'
    "An unbound route": {
        topic: function () {
            return get('/unbound');
        },
        "returns a 501": function (res) {
            assert.equal(res.status, 501);
        }
    },
    // Here, we're trying to use the DELETE method on /
    // Of course, we haven't allowed this, so Journey responds with a
    // 405 'Method not Allowed', and returns the allowed methods
    "A request with an unsupported method": {
        topic: function () {
            return del('/');
        },
        "returns a 405": function (res) {
            assert.equal(res.status, 405);
        },
        "sets the 'allowed' header correctly": function (res) {
            assert.equal(res.headers.allow, 'GET');
        }
    },

    //
    // SERVER ERRORS (5xx)
    //

    // The code in `picnic.fail` throws an exception, so we return a
    // 500 'Internal Server Error'
    "A request to a controller with an error in it": {
        topic: function () {
            return get('/picnic/fail');
        },
        "returns a 500": function (res) {
            assert.equal(res.status, 500);
        }
    },
}).addVows({
    "Scoped routes": {
        "A request to a scope with no routes": {
            topic: function () {
                return get('/domain/v1/empty');
            },
            "returns a 404": function (res) {
                assert.equal(res.status, 404);
            }
        },
        "A request to a scoped route's root": {
            topic: function () {
                return get('/domain/v1/');
            },
            "returns a 200": function (res) {
                assert.equal(res.status, 200);
            },
            "calls the correct route": function (res) {
                assert.equal(res.body.version, 1);
                assert.equal(res.body.root, true);
            }
        },
        "A request to a scoped route's base route": {
            topic: function () {
                return get('/domain/v1');
            },
            "returns a 200": function (res) {
                assert.equal(res.status, 200);
            },
            "calls the correct route": function (res) {
                assert.equal(res.body.version, 1);
                assert.isUndefined(res.body.root);
            }
        },
        "A request to a scoped route": {
            topic: function () {
                return get('/domain/v1/info');
            },
            "returns a 200": function (res) {
                assert.equal(res.status, 200);
            },
            "returns a body": function (res) {
                assert.equal(res.body[0], 'info');
            }
        }
    }
});

<commit_msg>(test) updated for vows 0.3.0<commit_after>var sys = require('sys'),
   http = require('http'),
 assert = require('assert'),
   path = require('path'),
 events = require('events'),
    url = require('url');

var vows = require('vows');

require.paths.unshift(__dirname, path.join(__dirname, '..'));

var journey = require('lib/journey');

var resources = {
    "home": {
        index: function (res) {
            res.send("honey I'm home!");
        },
        room: function (res, params) {
            assert.equal(params.candles, "lit");
            assert.equal(params.slippers, "on");
            res.send(params);
        }
    },
    "picnic": {
        fail: function () {
            throw "fail!";
        }
    },
    "kitchen": {},
    "recipies": {}
};

//
// Initialize the router
//
var router = new(journey.Router)(function (map) {
    this.route('GET', 'picnic/fail').bind(resources.picnic.fail);

    //map.root.bind(function (res) { res.send("Welcome to the Root") });
    map.get('/home/room').bind(resources.home.room);
    map.get('/undefined').bind();
    map.get('/unbound');

    map.root.bind(function (res) { return resources.home.index(res) });

    map.get('/').bind(function (res) { res.send(200) });
    map.get('/twice').bind(function (res) { res.send("twice") });
    map.get(/twice/).bind(function (res) { res.send(302) });

    map.path('/domain', function () {
        this.path('/v1', function () {
            this.root.bind(function (res) { res.send({ root: true, version: 1 }) });
            this.get().bind(function (res) { res.send({ version: 1 }) });
            this.get('/info').bind(function (res) {
                res.send(200, {}, ['info']);
            });
            this.path('/empty', function () {});
        });
    });

    map.route(['GET', 'PUT'], /^(\w+)$/).
        bind(function (res, r) { return resources[r].index(res) });
    map.route('GET', /^(\w+)\/([0-9]+)$/).
        bind(function (res, r, k) { return resources[r].get(res, k) });
    map.route('PUT', /^(\w+)\/([0-9]+)$/, { payload: true }).
        bind(function (res, r, k) { return resources[r].update(res, k) });
    map.route('POST', /^(\w+)$/, { payload: true }).
        bind(function (res, r, doc) { return resources[r].create(res, doc) });
    map.route('DELETE', /^(\w+)\/([0-9]+)$/).
        bind(function (res, r, k) { return resources[r].destroy(res, k) });

    map.put('home/assert', { assert: function (res, body) { return body.length === 9; } }).
        bind(function (res) { res.send(200, {"Content-Type":"text/html"}, "OK"); });
});

var mock = require('lib/journey/mock-request').mock(router);

var get = mock.get,
    del = mock.del,
   post = mock.post,
    put = mock.put;

journey.env = 'test';

vows.describe('Journey').addVows({
    //
    // SUCCESSFUL (2xx)
    //
    "A valid HTTP request": {
        topic: function () { return get('/', { accept: "application/json" }) },

        "returns a 200": function (res) {
            assert.equal(res.status, 200);
        },
        "returns a body": function (res) {
            assert.equal(res.body.journey, "honey I'm home!");
        }
    },

    "A valid request with multiple Accept types": {
        topic: function () { return get('/', { accept: "text/plain;q=10, application/json" }) },

        "returns a 200": function (res) {
            assert.equal(res.status, 200);
        },
        "returns a body": function (res) {
            assert.equal(res.body.journey, "honey I'm home!");
        }
    },

    "A request with uri parameters": {
        topic: function () {
            // URI parameters get parsed into a javascript object, and are passed to the
            // function handler like so:
            return get('/home/room?slippers=on&candles=lit');
        },

        "returns a 200": function (res) {
            assert.equal(res.status, 200);
        },
        "gets parsed into an object": function (res) {
            assert.equal(res.body.slippers, 'on');
            assert.equal(res.body.candles, 'lit');
        }
    },
    "A request without uri parameters": {
        topic: function () {
            var promise = new(events.EventEmitter);
            router.routes.unshift({
                pattern: /^noparams$/,
                method: 'GET', handler: function (res) {
                    promise.emit('success', arguments);
                }, success: undefined, constraints: {}
            });
            router.route(mock.mockRequest('GET', '/noparams', {}));
            return promise;
        },
        "should pass an empty params object": function (args) {
            var params = args[args.length - 1];
            assert.isObject(params);
            assert.equal(Object.keys(params).length, 0);
        }
    },

    "A request with two matching routes": {
        topic: function () {
            return get('/twice');
        },

        "returns a 200": function (res) {
            assert.equal(res.status, 200);
        },
        "returns the first matching route": function (res) {
            assert.equal(res.body.journey, 'twice');
        }
    },

    // Here, we're sending a POST request; the input is parsed into an object, and passed
    // to the function handler as a parameter.
    // We expect Journey to respond with a 201 'Created', if the request was successful.
    "A POST request": {
        "with a JSON body": {
            topic: function () {
                resources["kitchen"].create = function (res, input) {
                    res.send("cooking-time: " + (input['chicken'].length + input['fries'].length) + 'min');
                };
                return post('/kitchen', null, JSON.stringify(
                    {"chicken":"roasted", "fries":"golden"}
                ));
            },
            "returns a 201": function (res) {
                assert.equal(res.status, 201);
            },
            "gets parsed into an object": function (res) {
                assert.equal(res.body.journey, 'cooking-time: 13min');
            }
        },
        "with a query-string body": {
            topic: function () {
                resources["kitchen"].create = function (res, input) {
                    res.send("cooking-time: "         +
                            (input['chicken'].length  +
                             input['fries'].length)   + 'min');
                };
                return post('/kitchen', {accept: 'application/json'},
                                        "chicken=roasted&fries=golden");
            },
            "returns a 201": function (res) {
                assert.equal(res.status, 201);
            },
            "gets parsed into an object": function (res) {
                assert.equal(res.body.journey, 'cooking-time: 13min');
            }
        }
    },

    //
    // CLIENT ERRORS (4xx)
    //

    // Journey being a JSON only server, asking for text/html returns 'Not Acceptable'
    "A request for text/html": {
        topic: function () {
            return get('/', { accept: "text/html" });
        },
        "returns a 406": function (res) { assert.equal(res.status, 406) }
    },
    // This request doesn't have a matching route, it'll therefore return a 404.
    "A request which doesn't match anything": {
        topic: function () {
            return del('/hello/world');
        },
        "returns a 404": function (res) {
            assert.equal(res.status, 404);
        }
    },
    // This request contains malformed JSON data, the server replies
    // with a 400 'Bad Request'
    "An invalid request": {
        topic: function () {
            return post('/malformed', null, "{bad: json}");
        },
        "returns a 400": function (res) {
            assert.equal(res.status, 400);
        }
    },
    // Trying to access an undefined function will result in a 500,
    // as long as the uri format is valid
    "A route binded to an undefined function": {
        topic: function () {
            return get('/undefined');
        },
        "returns a 500": function (res) {
            assert.equal(res.status, 500);
        }
    },
    // Trying to access an unbound route, will result in a 501 'Not Implemented'
    "An unbound route": {
        topic: function () {
            return get('/unbound');
        },
        "returns a 501": function (res) {
            assert.equal(res.status, 501);
        }
    },
    // Here, we're trying to use the DELETE method on /
    // Of course, we haven't allowed this, so Journey responds with a
    // 405 'Method not Allowed', and returns the allowed methods
    "A request with an unsupported method": {
        topic: function () {
            return del('/');
        },
        "returns a 405": function (res) {
            assert.equal(res.status, 405);
        },
        "sets the 'allowed' header correctly": function (res) {
            assert.equal(res.headers.allow, 'GET');
        }
    },

    //
    // SERVER ERRORS (5xx)
    //

    // The code in `picnic.fail` throws an exception, so we return a
    // 500 'Internal Server Error'
    "A request to a controller with an error in it": {
        topic: function () {
            return get('/picnic/fail');
        },
        "returns a 500": function (res) {
            assert.equal(res.status, 500);
        }
    },
}).addVows({
    "Scoped routes": {
        "A request to a scope with no routes": {
            topic: function () {
                return get('/domain/v1/empty');
            },
            "returns a 404": function (res) {
                assert.equal(res.status, 404);
            }
        },
        "A request to a scoped route's root": {
            topic: function () {
                return get('/domain/v1/');
            },
            "returns a 200": function (res) {
                assert.equal(res.status, 200);
            },
            "calls the correct route": function (res) {
                assert.equal(res.body.version, 1);
                assert.equal(res.body.root, true);
            }
        },
        "A request to a scoped route's base route": {
            topic: function () {
                return get('/domain/v1');
            },
            "returns a 200": function (res) {
                assert.equal(res.status, 200);
            },
            "calls the correct route": function (res) {
                assert.equal(res.body.version, 1);
                assert.isUndefined(res.body.root);
            }
        },
        "A request to a scoped route": {
            topic: function () {
                return get('/domain/v1/info');
            },
            "returns a 200": function (res) {
                assert.equal(res.status, 200);
            },
            "returns a body": function (res) {
                assert.equal(res.body[0], 'info');
            }
        }
    }
}).export(module);

<|endoftext|>


<commit_before>require('coffee-script/register') /* so we can require coffeescript */
require('coffee-cache')  /* so coffeescript doesn't get recompiled every time we require it */

exports['compute-client'] = require('./compute-client.coffee')
exports['compute-server'] = require('./compute-server.coffee')
exports.hub        = require('./hub.coffee')
exports.rethink    = require('./rethink.coffee')
exports.smc_gcloud = require('./smc_gcloud.coffee')
<commit_msg>cache storage<commit_after>require('coffee-script/register') /* so we can require coffeescript */
require('coffee-cache')  /* so coffeescript doesn't get recompiled every time we require it */

exports['compute-client'] = require('./compute-client.coffee')
exports['compute-server'] = require('./compute-server.coffee')
exports.hub        = require('./hub.coffee')
exports.rethink    = require('./rethink.coffee')
exports.smc_gcloud = require('./smc_gcloud.coffee')
exports.storage    = require('./storage.coffee')
<|endoftext|>


<commit_before>/*eslint-env node*/
var webpackConfig = require("../webpack.config.js");
var _ = require("lodash");

function base() {
  return {
    frameworks: ["mocha"],
    files: ["tests.webpack.js"],
    preprocessors: {
      "tests.webpack.js": [ "webpack", "sourcemap" ]
    },
    webpack: webpackConfig,
    webpackServer: {
      noInfo: true
    }
  };
}

function local() {
  return _.extend(base(), {
    browsers: ["Chrome"],
    reporters: ["mocha"]
  });
}

// function sauceLabs() {
//   var customLaunchers = {
//     sl_chrome: {
//       base: "SauceLabs",
//       browserName: "chrome"
//     },
//     sl_firefox: {
//       base: "SauceLabs",
//       browserName: "firefox"
//     },
//     sl_safari: {
//       base: "SauceLabs",
//       browserName: "safari",
//       platform: "OS X 10.10"
//     },
//     sl_iphone_ios_8: {
//       base: "SauceLabs",
//       browserName: "iphone",
//       version: "8.4",
//       deviceName: "iPhone 6 Plus",
//       deviceOrientation: "portrait"
//     },
//     sl_ie_11: {
//       base: "SauceLabs",
//       browserName: "internet explorer",
//       platform: "Windows 8.1",
//       version: "11"
//     }
//   };

//   return _.extend(base(), {
//     sauceLabs: {
//       testName: "Konbini Tests"
//     },
//     browserDisconnectTimeout: 10000,
//     browserDisconnectTolerance: 1,
//     browserNoActivityTimeout: 4 * 60 * 1000,
//     captureTimeout: 4 * 60 * 1000,
//     customLaunchers: customLaunchers,
//     browsers: Object.keys(customLaunchers),
//     reporters: ["dots", "saucelabs"],
//     singleRun: true
//   });
// }

module.exports = function (config) {
  //if (process.env.CONTINUOUS_INTEGRATION) {
    // We are on travis
    //config.set(sauceLabs());
  //} else {
  //config.set(local());
  //}
};
<commit_msg>just ask austin how to do the tests2<commit_after>/*eslint-env node*/
var webpackConfig = require("../webpack.config.js");
// var _ = require("lodash");

// function base() {
//   return {
//     frameworks: ["mocha"],
//     files: ["tests.webpack.js"],
//     preprocessors: {
//       "tests.webpack.js": [ "webpack", "sourcemap" ]
//     },
//     webpack: webpackConfig,
//     webpackServer: {
//       noInfo: true
//     }
//   };
// }

// function local() {
//   return _.extend(base(), {
//     browsers: ["Chrome"],
//     reporters: ["mocha"]
//   });
// }

// function sauceLabs() {
//   var customLaunchers = {
//     sl_chrome: {
//       base: "SauceLabs",
//       browserName: "chrome"
//     },
//     sl_firefox: {
//       base: "SauceLabs",
//       browserName: "firefox"
//     },
//     sl_safari: {
//       base: "SauceLabs",
//       browserName: "safari",
//       platform: "OS X 10.10"
//     },
//     sl_iphone_ios_8: {
//       base: "SauceLabs",
//       browserName: "iphone",
//       version: "8.4",
//       deviceName: "iPhone 6 Plus",
//       deviceOrientation: "portrait"
//     },
//     sl_ie_11: {
//       base: "SauceLabs",
//       browserName: "internet explorer",
//       platform: "Windows 8.1",
//       version: "11"
//     }
//   };

//   return _.extend(base(), {
//     sauceLabs: {
//       testName: "Konbini Tests"
//     },
//     browserDisconnectTimeout: 10000,
//     browserDisconnectTolerance: 1,
//     browserNoActivityTimeout: 4 * 60 * 1000,
//     captureTimeout: 4 * 60 * 1000,
//     customLaunchers: customLaunchers,
//     browsers: Object.keys(customLaunchers),
//     reporters: ["dots", "saucelabs"],
//     singleRun: true
//   });
// }

module.exports = function (config) {
  //if (process.env.CONTINUOUS_INTEGRATION) {
    // We are on travis
    //config.set(sauceLabs());
  //} else {
  //config.set(local());
  //}
};
<|endoftext|>