<commit_before>/**
 * WordPress dependencies
 */
import { activatePlugin, visitAdminPage, createURL } from '@wordpress/e2e-test-utils';

/**
 * Internal dependencies
 */
import {
	deactivateUtilityPlugins,
	resetSiteKit,
	useRequestInterception,
	setSearchConsoleProperty,
	setupAnalytics,
} from '../../../utils';

async function proceedToTagManagerSetup() {
	await visitAdminPage( 'admin.php', 'page=googlesitekit-settings' );
	await page.waitForSelector( '.mdc-tab-bar' );
	await expect( page ).toClick( '.mdc-tab', { text: /connect more services/i } );
	await page.waitForSelector( '.googlesitekit-settings-connect-module--tagmanager' );

	await Promise.all( [
		page.waitForSelector( '.googlesitekit-setup-module__action .mdc-button' ),
		expect( page ).toClick( '.googlesitekit-cta-link', { text: /set up tag manager/i } ),
	] );
}

describe( 'Tag Manager module setup', () => {
	beforeAll( async () => {
		await page.setRequestInterception( true );
		useRequestInterception( ( request ) => {
			if ( request.url().match( 'google-site-kit/v1/data/' ) ) {
				request.respond( {
					status: 200,
				} );
			} else {
				request.continue();
			}
		} );
	} );

	beforeEach( async () => {
		await activatePlugin( 'e2e-tests-auth-plugin' );
		await activatePlugin( 'e2e-tests-site-verification-plugin' );
		await activatePlugin( 'e2e-tests-oauth-callback-plugin' );
		await setSearchConsoleProperty();
		await setupAnalytics();
	} );

	afterEach( async () => {
		await deactivateUtilityPlugins();
		await resetSiteKit();
	} );

	it( 'displays account creation form when user has no Tag Manager account', async () => {
		await activatePlugin( 'e2e-tests-module-setup-tagmanager-api-mock-no-account' );
		await proceedToTagManagerSetup();

		// Intercept the call to window.open and call our API to simulate a created account.
		await page.evaluate( () => {
			window.open = () => {
				window._e2eApiFetch( {
					path: 'google-site-kit/v1/e2e/setup/tagmanager/account-created',
					method: 'post',
				} );
			};
		} );

		// Clicking Create Account button will switch API mock plugins on the server to the one that has accounts.
		await Promise.all( [
			page.waitForResponse( ( res ) => res.url().match( 'google-site-kit/v1/e2e/setup/tagmanager/account-created' ) ),
			expect( page ).toClick( '.mdc-button', { text: /Create an account/i } ),
		] );

		await Promise.all( [
			page.waitForResponse( ( req ) => req.url().match( 'tagmanager/data/accounts' ) ),
			expect( page ).toClick( '.googlesitekit-cta-link', { text: /Re-fetch My Account/i } ),
		] );
		await page.waitForSelector( '.googlesitekit-setup-module__inputs' );

		// Ensure account and container selections are cleared.
		await expect( page ).toMatchElement( '.googlesitekit-tagmanager__select-account .mdc-select__selected-text', { text: '' } );
		await expect( page ).toMatchElement( '.googlesitekit-tagmanager__select-container .mdc-select__selected-text', { text: '' } );

		// Choose an account.
		await expect( page ).toClick( '.googlesitekit-tagmanager__select-account' );
		await expect( page ).toClick( '.mdc-menu-surface--open .mdc-list-item', { text: /test account a/i } );

		// Ensure "Set up a new container" option is present in container select.
		await expect( page ).toClick( '.googlesitekit-tagmanager__select-container' );
		await expect( page ).toMatchElement( '.mdc-menu-surface--open .mdc-list-item', { text: /set up a new container/i } );
		await expect( page ).toClick( '.mdc-menu-surface--open .mdc-list-item', { text: /test container x/i } );

		await page.waitFor( 1000 );
		await expect( page ).toClick( 'button', { text: /confirm \& continue/i } );

		await page.waitForSelector( '.googlesitekit-publisher-win--win-success' );
		await expect( page ).toMatchElement( '.googlesitekit-publisher-win__title', { text: /Congrats on completing the setup for Tag Manager!/i } );

		// Ensure expected tag is placed.
		await Promise.all( [
			page.goto( createURL( '/' ) ),
			page.waitForNavigation(),
		] );
		await expect( page ).toMatchElement( 'script[src^="https://www.googletagmanager.com/gtm.js?id=GTM-ABCXYZ"]' );
	} );

	it( 'displays available accounts and containers for the chosen account', async () => {
		await activatePlugin( 'e2e-tests-module-setup-tagmanager-api-mock' );
		await proceedToTagManagerSetup();

		// Ensure account and container are selected by default.
		await expect( page ).toMatchElement( '.googlesitekit-tagmanager__select-account .mdc-select__selected-text', { text: /test account a/i } );
		await expect( page ).toMatchElement( '.googlesitekit-tagmanager__select-container .mdc-select__selected-text', { text: /test container x/i } );

		// Ensure choosing a different account loads the proper values.
		await expect( page ).toClick( '.googlesitekit-tagmanager__select-account' );
		await Promise.all( [
			page.waitForResponse( ( res ) => res.url().match( 'modules/tagmanager/data' ) ),
			expect( page ).toClick( '.mdc-menu-surface--open .mdc-list-item', { text: /test account b/i } ),
		] );

		// Ensure proper account and container are now selected.
		await expect( page ).toMatchElement( '.googlesitekit-tagmanager__select-account .mdc-select__selected-text', { text: /test account b/i } );
		await expect( page ).toMatchElement( '.googlesitekit-tagmanager__select-container .mdc-select__selected-text', { text: /test container y/i } );

		await page.waitFor( 1000 );
		await expect( page ).toClick( 'button', { text: /confirm \& continue/i } );

		await page.waitForSelector( '.googlesitekit-publisher-win--win-success' );
		await expect( page ).toMatchElement( '.googlesitekit-publisher-win__title', { text: /Congrats on completing the setup for Tag Manager!/i } );

		// Ensure expected tag is placed.
		await Promise.all( [
			page.goto( createURL( '/' ) ),
			page.waitForNavigation(),
		] );
		await expect( page ).toMatchElement( 'script[src^="https://www.googletagmanager.com/gtm.js?id=GTM-BCDWXY"]' );
	} );

	it( 'displays instructions for account creation when "Set up a new account" option is selected', async () => {
		await activatePlugin( 'e2e-tests-module-setup-tagmanager-api-mock' );
		await proceedToTagManagerSetup();

		// Ensure "setup a new account" is an available choice.
		await expect( page ).toClick( '.googlesitekit-tagmanager__select-account' );
		await expect( page ).toMatchElement( '.mdc-menu-surface--open .mdc-list-item', { text: /set up a new account/i } );

		// Choose set up a new account.
		await expect( page ).toClick( '.mdc-menu-surface--open .mdc-list-item', { text: /set up a new account/i } );

		// Ensure instructions are present.
		await expect( page ).toMatchElement( '.googlesitekit-setup-module--tag-manager p', { text: /to create a new account/i } );

		// Ensure buttons are present.
		await expect( page ).toMatchElement( '.googlesitekit-setup-module--tag-manager .mdc-button', { text: /create an account/i } );
		await expect( page ).toMatchElement( '.googlesitekit-setup-module--tag-manager .googlesitekit-cta-link', { text: /re-fetch my account/i } );
	} );
} );
<commit_msg>Update e2e test: container no longer auto selected after changing account.<commit_after>/**
 * WordPress dependencies
 */
import { activatePlugin, visitAdminPage, createURL } from '@wordpress/e2e-test-utils';

/**
 * Internal dependencies
 */
import {
	deactivateUtilityPlugins,
	resetSiteKit,
	useRequestInterception,
	setSearchConsoleProperty,
	setupAnalytics,
} from '../../../utils';

async function proceedToTagManagerSetup() {
	await visitAdminPage( 'admin.php', 'page=googlesitekit-settings' );
	await page.waitForSelector( '.mdc-tab-bar' );
	await expect( page ).toClick( '.mdc-tab', { text: /connect more services/i } );
	await page.waitForSelector( '.googlesitekit-settings-connect-module--tagmanager' );

	await Promise.all( [
		page.waitForSelector( '.googlesitekit-setup-module__action .mdc-button' ),
		expect( page ).toClick( '.googlesitekit-cta-link', { text: /set up tag manager/i } ),
	] );
}

describe( 'Tag Manager module setup', () => {
	beforeAll( async () => {
		await page.setRequestInterception( true );
		useRequestInterception( ( request ) => {
			if ( request.url().match( 'google-site-kit/v1/data/' ) ) {
				request.respond( {
					status: 200,
				} );
			} else {
				request.continue();
			}
		} );
	} );

	beforeEach( async () => {
		await activatePlugin( 'e2e-tests-auth-plugin' );
		await activatePlugin( 'e2e-tests-site-verification-plugin' );
		await activatePlugin( 'e2e-tests-oauth-callback-plugin' );
		await setSearchConsoleProperty();
		await setupAnalytics();
	} );

	afterEach( async () => {
		await deactivateUtilityPlugins();
		await resetSiteKit();
	} );

	it( 'displays account creation form when user has no Tag Manager account', async () => {
		await activatePlugin( 'e2e-tests-module-setup-tagmanager-api-mock-no-account' );
		await proceedToTagManagerSetup();

		// Intercept the call to window.open and call our API to simulate a created account.
		await page.evaluate( () => {
			window.open = () => {
				window._e2eApiFetch( {
					path: 'google-site-kit/v1/e2e/setup/tagmanager/account-created',
					method: 'post',
				} );
			};
		} );

		// Clicking Create Account button will switch API mock plugins on the server to the one that has accounts.
		await Promise.all( [
			page.waitForResponse( ( res ) => res.url().match( 'google-site-kit/v1/e2e/setup/tagmanager/account-created' ) ),
			expect( page ).toClick( '.mdc-button', { text: /Create an account/i } ),
		] );

		await Promise.all( [
			page.waitForResponse( ( req ) => req.url().match( 'tagmanager/data/accounts' ) ),
			expect( page ).toClick( '.googlesitekit-cta-link', { text: /Re-fetch My Account/i } ),
		] );
		await page.waitForSelector( '.googlesitekit-setup-module__inputs' );

		// Ensure account and container selections are cleared.
		await expect( page ).toMatchElement( '.googlesitekit-tagmanager__select-account .mdc-select__selected-text', { text: '' } );
		await expect( page ).toMatchElement( '.googlesitekit-tagmanager__select-container .mdc-select__selected-text', { text: '' } );

		// Choose an account.
		await expect( page ).toClick( '.googlesitekit-tagmanager__select-account' );
		await expect( page ).toClick( '.mdc-menu-surface--open .mdc-list-item', { text: /test account a/i } );

		// Ensure "Set up a new container" option is present in container select.
		await expect( page ).toClick( '.googlesitekit-tagmanager__select-container' );
		await expect( page ).toMatchElement( '.mdc-menu-surface--open .mdc-list-item', { text: /set up a new container/i } );
		await expect( page ).toClick( '.mdc-menu-surface--open .mdc-list-item', { text: /test container x/i } );

		await page.waitFor( 1000 );
		await expect( page ).toClick( 'button', { text: /confirm \& continue/i } );

		await page.waitForSelector( '.googlesitekit-publisher-win--win-success' );
		await expect( page ).toMatchElement( '.googlesitekit-publisher-win__title', { text: /Congrats on completing the setup for Tag Manager!/i } );

		// Ensure expected tag is placed.
		await Promise.all( [
			page.goto( createURL( '/' ) ),
			page.waitForNavigation(),
		] );
		await expect( page ).toMatchElement( 'script[src^="https://www.googletagmanager.com/gtm.js?id=GTM-ABCXYZ"]' );
	} );

	it( 'displays available accounts and containers for the chosen account', async () => {
		await activatePlugin( 'e2e-tests-module-setup-tagmanager-api-mock' );
		await proceedToTagManagerSetup();

		// Ensure account and container are selected by default.
		await expect( page ).toMatchElement( '.googlesitekit-tagmanager__select-account .mdc-select__selected-text', { text: /test account a/i } );
		await expect( page ).toMatchElement( '.googlesitekit-tagmanager__select-container .mdc-select__selected-text', { text: /test container x/i } );

		// Ensure choosing a different account loads the proper values.
		await expect( page ).toClick( '.googlesitekit-tagmanager__select-account' );
		await Promise.all( [
			page.waitForResponse( ( res ) => res.url().match( 'modules/tagmanager/data' ) ),
			expect( page ).toClick( '.mdc-menu-surface--open .mdc-list-item', { text: /test account b/i } ),
		] );

		// Ensure account is selected.
		await expect( page ).toMatchElement( '.googlesitekit-tagmanager__select-account .mdc-select__selected-text', { text: /test account b/i } );

		// Select a container.
		await expect( page ).toClick( '.googlesitekit-tagmanager__select-container' );
		await expect( page ).toClick( '.mdc-menu-surface--open .mdc-list-item', { text: /test container y/i } );

		await page.waitFor( 1000 );
		await expect( page ).toClick( 'button', { text: /confirm \& continue/i } );

		await page.waitForSelector( '.googlesitekit-publisher-win--win-success' );
		await expect( page ).toMatchElement( '.googlesitekit-publisher-win__title', { text: /Congrats on completing the setup for Tag Manager!/i } );

		// Ensure expected tag is placed.
		await Promise.all( [
			page.goto( createURL( '/' ) ),
			page.waitForNavigation(),
		] );
		await expect( page ).toMatchElement( 'script[src^="https://www.googletagmanager.com/gtm.js?id=GTM-BCDWXY"]' );
	} );

	it( 'displays instructions for account creation when "Set up a new account" option is selected', async () => {
		await activatePlugin( 'e2e-tests-module-setup-tagmanager-api-mock' );
		await proceedToTagManagerSetup();

		// Ensure "setup a new account" is an available choice.
		await expect( page ).toClick( '.googlesitekit-tagmanager__select-account' );
		await expect( page ).toMatchElement( '.mdc-menu-surface--open .mdc-list-item', { text: /set up a new account/i } );

		// Choose set up a new account.
		await expect( page ).toClick( '.mdc-menu-surface--open .mdc-list-item', { text: /set up a new account/i } );

		// Ensure instructions are present.
		await expect( page ).toMatchElement( '.googlesitekit-setup-module--tag-manager p', { text: /to create a new account/i } );

		// Ensure buttons are present.
		await expect( page ).toMatchElement( '.googlesitekit-setup-module--tag-manager .mdc-button', { text: /create an account/i } );
		await expect( page ).toMatchElement( '.googlesitekit-setup-module--tag-manager .googlesitekit-cta-link', { text: /re-fetch my account/i } );
	} );
} );
<|endoftext|>


<commit_before>'use babel'

import React from 'react-for-atom'
import Bacon from 'baconjs'
import R from 'ramda'
import classNames from 'classnames'
import Search from './search'
import ScrollableContent from './scrollable-content'
import ResizeHandle from './resize-handle'
import Th from './th'
import Item from './item'
import Summary from './cells/summary'
import DateTime from './cells/date-time'
import BaconMixin from './baconjs-mixin'

export default React.createClass({

  mixins: [BaconMixin],

  propTypes: {
    bodyHeightStream: React.PropTypes.object,
    showStream: React.PropTypes.object,
    resultsProp: React.PropTypes.object
  },

  getInitialState () {
    return {
      rowHeight: 25,
      bodyHeight: 100,
      scrollTop: 0,
      // TODO mapped version of titles, calculated widths, createCell
      columns: [
        {
          title: 'Name',
          width: 60,
          createCell: item =>
        }, {
          title: 'Date modified',
          width: 20,
          createCell: item => <DateTime time={item.stat.mtime} />
        }, {
          title: 'Date created',
          width: 20,
          createCell: item => <DateTime time={item.stat.birthtime} />
        }
      ],
      resultsOffset: 0,
      resultsTotal: 0,
      resultsItems: [],
      reverseStripes: true,
      selectedIndex: null
    }
  },

  componentWillMount () {
    let selectIndexBus = new Bacon.Bus()
    let changeBus = new Bacon.Bus()
    let keyDownBus = new Bacon.Bus()
    let bodyHeightBus = new Bacon.Bus()

    let resetStream = keyDownBus.filter(R.propEq('keyCode', 27)).map('') // <esc>
    let searchStream = changeBus.map('.target.value').merge(resetStream)
    let searchProp = searchStream.toProperty('')

    bodyHeightBus.plug(this.props.bodyHeightStream)
    let bodyHeightProp = bodyHeightBus.skipDuplicates().filter(newHeight => newHeight > 0).toProperty(100)

    let selectPrevStream = keyDownBus.filter(R.propEq('keyCode', 38)).doAction('.preventDefault') // <up>
    let selectNextStream = keyDownBus.filter(R.propEq('keyCode', 40)).doAction('.preventDefault') // <down>

    let selectedIndexProp = Bacon.update(
      null,
      [searchStream], R.always(null),
      [selectIndexBus], R.nthArg(-1),
      [selectPrevStream, this.props.resultsProp], (currentIndex, _, results) => {
        // Select prev item; cycle back to last item if current is the first item
        const newIndex = (currentIndex || results.total) - 1
        return newIndex >= 0
          ? newIndex
          : currentIndex
      },
      [selectNextStream, this.props.resultsProp], (currentIndex, _, results) => {
        // Select next item; stop at last item
        const newIndex = R.defaultTo(-1, currentIndex) + 1
        return newIndex < results.total
          ? newIndex
          : currentIndex
      }
    ).skipDuplicates()

    let scrollTopBus = new Bacon.Bus()
    let scrollTopProp = Bacon.update(
      0,
      [scrollTopBus], R.nthArg(-1),
      [searchStream], R.always(0),
      [selectedIndexProp.filter(R.is(Number)).changes()], (scrollTop, index) => {
        // Adjust scrollTop for selected item
        let selectedScrollTop = index * this.state.rowHeight
        if (scrollTop > selectedScrollTop) {
          // selected item is located before the visible bounds
          // from: ..X..[...]..
          // to:   .[X..]......
          return selectedScrollTop
        } else if (scrollTop + this.state.bodyHeight <= selectedScrollTop) {
          // selected item is located after the visible bounds
          // from: ..[...]..X..
          // to:   ......[..X].
          return selectedScrollTop - this.state.bodyHeight + this.state.rowHeight
        } else {
          // selected item is located within the visible bounds, just return the current scrollTop value
          return scrollTop
        }
      }
    )

    this.onStreamsValues(
      bodyHeightProp, scrollTopProp, searchProp, selectedIndexProp, this.props.resultsProp,
      (bodyHeight, scrollTop, searchStr, selectedIndex, results) => {
        this.setState({
          bodyHeight: bodyHeight,
          scrollTop: scrollTop,
          resultsOffset: results.offset,
          resultsTotal: results.total,
          resultsItems: results.items,
          reverseStripes: results.offset % 2 === 1,
          selectedIndex: selectedIndex
        })
      }
    )

    // expose some observables for sub-components
    this._searchStream = searchStream
    this._keyDownBus = keyDownBus
    this._changeBus = changeBus
    this._selectIndexBus = selectIndexBus
    this._scrollTopBus = scrollTopBus
    this._bodyHeightBus = bodyHeightBus
    this._focusBus = new Bacon.Bus()
    this._focusBus.plug(this.props.showStream)

    // also expose some observables for caller (to assign side-effects outside of this scope)
    const calcPagination = R.curry(val => (val / this.state.rowHeight) | 0)
    this.paginationOffsetProp = scrollTopProp.map(calcPagination)
    this.paginationSizeProp = bodyHeightProp.map(R.pipe(calcPagination, R.add(2)))
    this.openStream = keyDownBus.filter(R.propEq('keyCode', 13)) // <enter>
    this.resetStream = resetStream
    this.bodyHeightProp = bodyHeightProp
    this.searchProp = searchProp

    this.selectedItemProp = Bacon
      .combineWith(this.props.resultsProp, selectedIndexProp, (results, index) => results.items[index])
      .filter(R.is(Object))
  },

  render () {
    return (

        <Search showStream={this.props.showStream.merge(this._focusBus)}
            searchStream={this._searchStream}
            keyDownBus={this._keyDownBus}
            changeBus={this._changeBus}
          />

                  {this.state.columns.map((c) =>

                  )}

          <ScrollableContent focusBus={this._focusBus}
              bodyHeight={this.state.bodyHeight} rowHeight={this.state.rowHeight}
              resultsTotal={this.state.resultsTotal} resultsOffset={this.state.resultsOffset}
              scrollTop={this.state.scrollTop} scrollTopBus={this._scrollTopBus}>

                  {this.state.columns.map(c =>

                  )}

                {this.state.resultsItems.map((item, i) => {
                  const index = this.state.resultsOffset + i
                  return (
                    <Item item={item} isSelected={index === this.state.selectedIndex}
                        index={index} selectIndexBus={this._selectIndexBus}>
                      {this.state.columns.map((c) => c.createCell(item))}
                    </Item>
                  )
                })}

          </ScrollableContent>
          <ResizeHandle bodyHeight={this.state.bodyHeight} bodyHeightBus={this._bodyHeightBus} />

    )
  }

})
<commit_msg>:art: Set react key to each iterated item<commit_after>'use babel'

import React from 'react-for-atom'
import Bacon from 'baconjs'
import R from 'ramda'
import classNames from 'classnames'
import Search from './search'
import ScrollableContent from './scrollable-content'
import ResizeHandle from './resize-handle'
import Th from './th'
import Item from './item'
import Summary from './cells/summary'
import DateTime from './cells/date-time'
import BaconMixin from './baconjs-mixin'

export default React.createClass({

  mixins: [BaconMixin],

  propTypes: {
    bodyHeightStream: React.PropTypes.object,
    showStream: React.PropTypes.object,
    resultsProp: React.PropTypes.object
  },

  getInitialState () {
    return {
      rowHeight: 25,
      bodyHeight: 100,
      scrollTop: 0,
      // TODO mapped version of titles, calculated widths, createCell
      columns: [
        {
          title: 'Name',
          width: 60,
          createCell: item =>
        }, {
          title: 'Date modified',
          width: 20,
          createCell: item => <DateTime key='mtime' time={item.stat.mtime} />
        }, {
          title: 'Date created',
          width: 20,
          createCell: item => <DateTime key='birhtime' time={item.stat.birthtime} />
        }
      ],
      resultsOffset: 0,
      resultsTotal: 0,
      resultsItems: [],
      reverseStripes: true,
      selectedIndex: null
    }
  },

  componentWillMount () {
    let selectIndexBus = new Bacon.Bus()
    let changeBus = new Bacon.Bus()
    let keyDownBus = new Bacon.Bus()
    let bodyHeightBus = new Bacon.Bus()

    let resetStream = keyDownBus.filter(R.propEq('keyCode', 27)).map('') // <esc>
    let searchStream = changeBus.map('.target.value').merge(resetStream)
    let searchProp = searchStream.toProperty('')

    bodyHeightBus.plug(this.props.bodyHeightStream)
    let bodyHeightProp = bodyHeightBus.skipDuplicates().filter(newHeight => newHeight > 0).toProperty(100)

    let selectPrevStream = keyDownBus.filter(R.propEq('keyCode', 38)).doAction('.preventDefault') // <up>
    let selectNextStream = keyDownBus.filter(R.propEq('keyCode', 40)).doAction('.preventDefault') // <down>

    let selectedIndexProp = Bacon.update(
      null,
      [searchStream], R.always(null),
      [selectIndexBus], R.nthArg(-1),
      [selectPrevStream, this.props.resultsProp], (currentIndex, _, results) => {
        // Select prev item; cycle back to last item if current is the first item
        const newIndex = (currentIndex || results.total) - 1
        return newIndex >= 0
          ? newIndex
          : currentIndex
      },
      [selectNextStream, this.props.resultsProp], (currentIndex, _, results) => {
        // Select next item; stop at last item
        const newIndex = R.defaultTo(-1, currentIndex) + 1
        return newIndex < results.total
          ? newIndex
          : currentIndex
      }
    ).skipDuplicates()

    let scrollTopBus = new Bacon.Bus()
    let scrollTopProp = Bacon.update(
      0,
      [scrollTopBus], R.nthArg(-1),
      [searchStream], R.always(0),
      [selectedIndexProp.filter(R.is(Number)).changes()], (scrollTop, index) => {
        // Adjust scrollTop for selected item
        let selectedScrollTop = index * this.state.rowHeight
        if (scrollTop > selectedScrollTop) {
          // selected item is located before the visible bounds
          // from: ..X..[...]..
          // to:   .[X..]......
          return selectedScrollTop
        } else if (scrollTop + this.state.bodyHeight <= selectedScrollTop) {
          // selected item is located after the visible bounds
          // from: ..[...]..X..
          // to:   ......[..X].
          return selectedScrollTop - this.state.bodyHeight + this.state.rowHeight
        } else {
          // selected item is located within the visible bounds, just return the current scrollTop value
          return scrollTop
        }
      }
    )

    this.onStreamsValues(
      bodyHeightProp, scrollTopProp, searchProp, selectedIndexProp, this.props.resultsProp,
      (bodyHeight, scrollTop, searchStr, selectedIndex, results) => {
        this.setState({
          bodyHeight: bodyHeight,
          scrollTop: scrollTop,
          resultsOffset: results.offset,
          resultsTotal: results.total,
          resultsItems: results.items,
          reverseStripes: results.offset % 2 === 1,
          selectedIndex: selectedIndex
        })
      }
    )

    // expose some observables for sub-components
    this._searchStream = searchStream
    this._keyDownBus = keyDownBus
    this._changeBus = changeBus
    this._selectIndexBus = selectIndexBus
    this._scrollTopBus = scrollTopBus
    this._bodyHeightBus = bodyHeightBus
    this._focusBus = new Bacon.Bus()
    this._focusBus.plug(this.props.showStream)

    // also expose some observables for caller (to assign side-effects outside of this scope)
    const calcPagination = R.curry(val => (val / this.state.rowHeight) | 0)
    this.paginationOffsetProp = scrollTopProp.map(calcPagination)
    this.paginationSizeProp = bodyHeightProp.map(R.pipe(calcPagination, R.add(2)))
    this.openStream = keyDownBus.filter(R.propEq('keyCode', 13)) // <enter>
    this.resetStream = resetStream
    this.bodyHeightProp = bodyHeightProp
    this.searchProp = searchProp

    this.selectedItemProp = Bacon
      .combineWith(this.props.resultsProp, selectedIndexProp, (results, index) => results.items[index])
      .filter(R.is(Object))
  },

  render () {
    return (

        <Search showStream={this.props.showStream.merge(this._focusBus)}
            searchStream={this._searchStream}
            keyDownBus={this._keyDownBus}
            changeBus={this._changeBus}
          />

                  {this.state.columns.map((c) =>

                  )}

          <ScrollableContent focusBus={this._focusBus}
              bodyHeight={this.state.bodyHeight} rowHeight={this.state.rowHeight}
              resultsTotal={this.state.resultsTotal} resultsOffset={this.state.resultsOffset}
              scrollTop={this.state.scrollTop} scrollTopBus={this._scrollTopBus}>

                  {this.state.columns.map(c =>

                  )}

                {this.state.resultsItems.map((item, i) => {
                  const index = this.state.resultsOffset + i
                  return (
                    <Item key={item.path} item={item} isSelected={index === this.state.selectedIndex}
                        index={index} selectIndexBus={this._selectIndexBus}>
                      {this.state.columns.map((c) => c.createCell(item))}
                    </Item>
                  )
                })}

          </ScrollableContent>
          <ResizeHandle bodyHeight={this.state.bodyHeight} bodyHeightBus={this._bodyHeightBus} />

    )
  }

})
<|endoftext|>


<commit_before>define(["logger", "promise", "gallery", "imageview", "galleryviewsettings", "svg", "backbone"], function(Logger, Promise, Gallery, ImageView, GalleryViewSettings, SVG) {
    var TileSpacing = 2;
    var StandardTileSize = 100;
    var id = 0;
    var renderCount = 0;
    var GalleryView = Backbone.View.extend({
        tagName: "div",
        className: "gallery",
        initialize: function() {
            this.id = id++;
            Logger.log("gv init, model:", this.model);
           this.model.on("all", function(event) {
               // Logger.log("gv model event:", event);
               this.render();
           }.bind(this));
            this.imageViews = {};
            this.visible = true;

            this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
            this.svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
            this.svg.setAttribute("version", "1.1");
            this.svg.classList.add("h-100");
            this.svg.classList.add("w-100");
            this.el.appendChild(this.svg);
            if (this.attributes && this.attributes.showSelected) {
                this.setShowSelected(true);
            }

            var instance = this;
            window.addEventListener("resize", function() {
                window.requestAnimationFrame(render.bind(instance));
            });

            if (this.model) {
                this.render();
            }
        },
        render: render,
        setVisible: function(isVisible) {
            this.visible = isVisible;
            this.render();
            this.trigger("change:isVisible");
        },
        isVisible: function() {
            return this.visible;
        },
        setShowSelected: function(b) {
            Logger.log("gv setShowSelected", b);
            this.showSelected = b;
            this.render();
            this.trigger("change:showSelected");
        },
        isShowSelected: function() {
            return this.showSelected;
        },
        zoom: zoom,
        translate: translate,
        prepend: prepend,
        resetTransformation: resetTransformation,
        setTransformation: setTransformation,
        getDisplayedImages: getDisplayedImages,
        getAllGalleryImages: function() {
            var gallery = this.model;
            var allImages = gallery.get("images");
            return allImages;
        },
        getSelectedGalleryImages: function() {
            var gallery = this.model;
            var selectedImages = gallery.get("selectedImages");
            return selectedImages;
        },
        /*
        * Removes images that have been deleted or should not be displayed from the cache and the DOM
        */
        removeOrphanImages: function(imagesToDisplay) {
            var allImages = this.getAllGalleryImages();
            var imageViewsToRemove = [];
            for (var cid in this.imageViews) {
                var value = this.imageViews[cid];
                if (!(value instanceof ImageView)) continue;

                var imageView = value;
                var image = imageView.model;
                if (allImages.indexOf(image) < 0 || imagesToDisplay.indexOf(image) < 0)
                    imageViewsToRemove.push(imageView);
            }
            imageViewsToRemove.forEach(function(imageView) {
                imageView.el.parentElement.removeChild(imageView.el);
                var image = imageView.model;
                var cid = image.cid;
                delete this.imageViews[cid];
            }.bind(this));
        }
    });

    function render() {
        // Logger.log("gallery view render");
        var currentRenderCount = ++renderCount;

        var el = this.el;
        el.classList.toggle("d-n", !this.visible);

        this.svg.setAttribute("width", "100%");

        // iterate over images
        var showSelected = this.isShowSelected();
        var allGalleryImages = this.getAllGalleryImages();
        var imagesToDisplay = showSelected?
            this.getSelectedGalleryImages() : allGalleryImages;
        // Logger.log(imagesToDisplay.length, " to display", this.id);
        var instance = this;

        var gallerySize;

        var parentNode = this.el.parentNode;
        if (parentNode) {
            var rect = this.el.parentNode.getBoundingClientRect();
            gallerySize = {
                width: rect.right - rect.left,
                height: rect.bottom - rect.top
            };
        }
        else {
            gallerySize = {
                width: 0,
                height: 0
            }
        }
        if (gallerySize.width === 0 || gallerySize.height === 0) {
            // avoid compute errors
            return;
        }

        var viewsToDisplay = imagesToDisplay.map(getImageView.bind(this));

        this.removeOrphanImages(imagesToDisplay);

        var gridAndTileSizePromise = new Promise(function(resolve, reject) {
            try{
            var gridSize;
            var tileSize;
            if (showSelected) {
                // respect ratio, fit to screen
                Promise.all(viewsToDisplay.map(function(imageView) {
                    return imageView.getFullSizePromise();
                })).then(function(fullImageSizes) {
                    try{
                    // stretch
                    gridSize = getGridSize(gallerySize, fullImageSizes);
                    tileSize = getTileSize(gallerySize, gridSize);
                    resolve({
                        gridSize: gridSize,
                        tileSize: tileSize
                    });
                    } catch (e) {
                        Logger.log(e);
                    }
                });
            }
            else {
                var maxGalleryWidth = viewsToDisplay.length * (StandardTileSize + TileSpacing) - TileSpacing;
                if (maxGalleryWidth < gallerySize.width) {
                    // the whole gallery fits in one line. use standard tile size
                    gridSize = {
                        width: viewsToDisplay.length,
                        height: 1
                    }
                    tileSize = {
                        width: StandardTileSize,
                        height: StandardTileSize
                    };
                }
                else {
                    // fixed tile size
                    var gridWidth = Math.floor(gallerySize.width / StandardTileSize);
                    gridSize = {
                        width: gridWidth,
                        height: 1
                    };

                    tileSize = getTileSize(gallerySize, gridSize);
                    tileSize.height = tileSize.width;
                }
                resolve({
                    gridSize: gridSize,
                    tileSize: tileSize
                });
            }
            }
            catch(e) {
                Logger.log(e);
            }
        });
        gridAndTileSizePromise.then(function(result) {
            // for some racing issues, we may be trying to render an old state
            if (currentRenderCount < renderCount) {
                // Logger.log("aborted render, " + currentRenderCount + " vs " + renderCount)
                return;
            }
			try{
			var tileSize = result.tileSize;
			var gridSize = result.gridSize;
            var displaySettings = {
                gridSize: gridSize,
                gallerySize: gallerySize,
                displayedImages: viewsToDisplay
            };

            viewsToDisplay.forEach(function(imageView) {
                imageView.setSize(tileSize);
            });
            var isRedrawRequired = !isSame(this.oldDisplaySettings, displaySettings);
            if (!isRedrawRequired) {
                Logger.log("gallery view. render unnecessary");
                return;
            }
            this.oldDisplaySettings = displaySettings;

            Logger.log("clear gallery view");
    //        Logger.log("grid size" + gridSize);
            var latestRowIndex = -1;
            var row;

            if (!showSelected) {
                // reset scale and translation in displayed tiles
                this.resetTransformation();
            }
            viewsToDisplay.forEach(function(imageView, index) {
                if (imageView.el.parentElement !== this.svg) {
                    this.svg.appendChild(imageView.el);
                }

                imageView.setVisible(false);

                var rowIndex = Math.floor(index / gridSize.width);
                var colIndex = index % gridSize.width;
                latestRowIndex = rowIndex;

                var x = colIndex * (tileSize.width + TileSpacing);
                var y = rowIndex * (tileSize.height + TileSpacing);

                imageView.el.setAttribute("transform", "translate(" + x + ", " + y + ")");
                imageView.setVisible(true);
            }.bind(this));

            var minGalleryHeight = latestRowIndex * StandardTileSize + StandardTileSize;
            if (!showSelected && minGalleryHeight > gallerySize.height) {
                // expand the svg if we're displaying the whole gallery
                this.svg.setAttribute("height", minGalleryHeight);
            }
            else {
                this.svg.setAttribute("height", "100%");
            }
			}
			catch(e) {
				Logger.log(e);
			}
        }.bind(this));
    }

    /**
    * Scales all the displayed images
    * @param s the scale
    * @param c the position of the center
    **/
    function zoom(s, c) {
        this.getDisplayedImages().forEach(function(image) {
            // use the center if center is undefined
            if (c === undefined) {
                var size = image.getSize();
                c = {
                    x: size.width / 2,
                    y: size.height / 2
                };
            }
            var translateMinusC = "translate(" + -c.x + ", " + -c.y + ")";
            var translateC = "translate(" + c.x + ", " + c.y + ")";
            image.setTransformation(translateC +
                         " matrix(" + s + ", 0, 0, " + s + ", 0, 0) " +
                         translateMinusC + " " + image.getTransformation());
        });
    }

    function translate(dx, dy) {
        this.prepend("translate(" + dx + ", " + dy + ")");
    }
    /**
    * Prepends a SVG transformation to all the displayed images
    **/
    function prepend(transformation) {
        this.getDisplayedImages().forEach(function(image) {
            image.setTransformation(transformation + " " + image.getTransformation());
        });
    }

    function setTransformation(transformation) {
        this.getDisplayedImages().forEach(function(image) {
            image.setTransformation(transformation);
        });
    }

    function resetTransformation() {
        this.setTransformation("");
    }

    function getImageView(image) {
        var cid = image.cid;
        var imageView;
        if (!(cid in this.imageViews)) {
            // make the image element
            console.log(this.id, "new image");
            imageView = new ImageView({
                model: image,
                attributes: { thumbnailUpdateEnabled: this.isShowSelected() }
            });
            this.imageViews[image.cid] = imageView;
        }
        imageView = this.imageViews[cid];
        return imageView;
    }

    function getGridSize(gallerySize, imageSizes) {
        // Logger.log("getGridSize " + JSON.stringify(gallerySize) + " " + JSON.stringify(imageSizes));
        var maxUsedSize;
        var usedSize;

        // default value is used only when no image is displayed
        var bestGridSize = {
            width: Number.POSITIVE_INFINITY,
            height: 1
        };
        for (var gridWidth = 1; gridWidth <= imageSizes.length; gridWidth++) {
            for (var gridHeight = Math.ceil(imageSizes.length / gridWidth); gridHeight <= imageSizes.length; gridHeight++) {
                var gridSize = {
                    width: gridWidth,
                    height: gridHeight
                };
                var tileSize = getTileSize(gallerySize, gridSize);

                // compute used size
                usedSize = 0;
                imageSizes.forEach(function(size) {
                    var imageSize = getStretchedSize(size, tileSize);
                    usedSize += imageSize.width * imageSize.height;
                });
                // Logger.log("griddimensions (width, height, usedsize", gridWidth + " " + gridHeight + " " + usedSize);
                if (!maxUsedSize || usedSize > maxUsedSize) {
                    bestGridSize = gridSize;
                    maxUsedSize = usedSize;
                }
            }
        }
        return bestGridSize;
    }

    function getTileSize(bounds, gridSize) {
        var gridWidth = gridSize.width;
        var gridHeight = gridSize.height;
        var tileSize = {
            width: (bounds.width - (gridWidth - 1) * TileSpacing) / gridWidth ,
            height: (bounds.height - (gridHeight - 1) * TileSpacing) / gridHeight
        }
        return tileSize;
    }

    // returns the size of the image stretched to fit within some bounds
    function getStretchedSize(size, tileSize) {
        // Logger.log("getStretchedSize ", JSON.stringify(size), JSON.stringify(tileSize));
        var imageRatio = size.width / size.height;
        // fit width
        var imageSize = {
            width: tileSize.width,
            height: tileSize.width / imageRatio
        };
        // fit height
        var scale = Math.min(1, tileSize.height / imageSize.height);
        imageSize.width *= scale;
        imageSize.height *= scale;
        return imageSize;
    }

    function getDisplayedImages() {
        if (!this.oldDisplaySettings ||
            !this.oldDisplaySettings.displayedImages)
            return [];

        return this.oldDisplaySettings.displayedImages;
    }

    function isSame(displaySettings, oldDisplaySettings) {
        if (!oldDisplaySettings && displaySettings ||
            oldDisplaySettings && !displaySettings)
            return false;
        if (!oldDisplaySettings && !displaySettings)
            return true;

        var isSameGridSize = displaySettings.gridSize.width === oldDisplaySettings.gridSize.width &&
            displaySettings.gridSize.height === oldDisplaySettings.gridSize.height;
        var isSameGallerySize = displaySettings.gallerySize.width === oldDisplaySettings.gallerySize.width &&
            displaySettings.gallerySize.height === oldDisplaySettings.gallerySize.height;
        var arraysIdentical = areArraysIdentical(displaySettings.displayedImages, oldDisplaySettings.displayedImages);
        var r = // compare grid size
            isSameGridSize &&
            // compare gallery size
            isSameGallerySize &&
            arraysIdentical;
        return r;
    }

    function areArraysIdentical(a1, a2) {
        return a1 === a2 ||
            a1.length === a2.length &&
            a1.every(function(obj, index) {
                return obj === a2[index];
            });
    }

    return GalleryView;
});
<commit_msg>fixed gallery rerender on resize<commit_after>define(["logger", "promise", "gallery", "imageview", "galleryviewsettings", "svg", "backbone"], function(Logger, Promise, Gallery, ImageView, GalleryViewSettings, SVG) {
    var TileSpacing = 2;
    var StandardTileSize = 100;
    var id = 0;
    var GalleryView = Backbone.View.extend({
        tagName: "div",
        className: "gallery",
        initialize: function() {
            this.renderCount = 0;
            this.id = id++;
            Logger.log("gv init, model:", this.model);
           this.model.on("all", function(event) {
               // Logger.log("gv model event:", event);
               this.render();
           }.bind(this));
            this.imageViews = {};
            this.visible = true;

            this.svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
            this.svg.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
            this.svg.setAttribute("version", "1.1");
            this.svg.classList.add("h-100");
            this.svg.classList.add("w-100");
            this.el.appendChild(this.svg);
            if (this.attributes && this.attributes.showSelected) {
                this.setShowSelected(true);
            }

            var instance = this;
            window.addEventListener("resize", function() {
                window.requestAnimationFrame(render.bind(instance));
            });

            if (this.model) {
                this.render();
            }
        },
        render: render,
        setVisible: function(isVisible) {
            this.visible = isVisible;
            this.render();
            this.trigger("change:isVisible");
        },
        isVisible: function() {
            return this.visible;
        },
        setShowSelected: function(b) {
            Logger.log("gv setShowSelected", b);
            this.showSelected = b;
            this.render();
            this.trigger("change:showSelected");
        },
        isShowSelected: function() {
            return this.showSelected;
        },
        zoom: zoom,
        translate: translate,
        prepend: prepend,
        resetTransformation: resetTransformation,
        setTransformation: setTransformation,
        getDisplayedImages: getDisplayedImages,
        getAllGalleryImages: function() {
            var gallery = this.model;
            var allImages = gallery.get("images");
            return allImages;
        },
        getSelectedGalleryImages: function() {
            var gallery = this.model;
            var selectedImages = gallery.get("selectedImages");
            return selectedImages;
        },
        /*
        * Removes images that have been deleted or should not be displayed from the cache and the DOM
        */
        removeOrphanImages: function(imagesToDisplay) {
            var allImages = this.getAllGalleryImages();
            var imageViewsToRemove = [];
            for (var cid in this.imageViews) {
                var value = this.imageViews[cid];
                if (!(value instanceof ImageView)) continue;

                var imageView = value;
                var image = imageView.model;
                if (allImages.indexOf(image) < 0 || imagesToDisplay.indexOf(image) < 0)
                    imageViewsToRemove.push(imageView);
            }
            imageViewsToRemove.forEach(function(imageView) {
                imageView.el.parentElement.removeChild(imageView.el);
                var image = imageView.model;
                var cid = image.cid;
                delete this.imageViews[cid];
            }.bind(this));
        }
    });

    function render() {
         Logger.log("gallery view render");
        var currentRenderCount = ++this.renderCount;

        var el = this.el;
        el.classList.toggle("d-n", !this.visible);

        this.svg.setAttribute("width", "100%");

        // iterate over images
        var showSelected = this.isShowSelected();
        var allGalleryImages = this.getAllGalleryImages();
        var imagesToDisplay = showSelected?
            this.getSelectedGalleryImages() : allGalleryImages;
        // Logger.log(imagesToDisplay.length, " to display", this.id);
        var instance = this;

        var gallerySize;

        var parentNode = this.el.parentNode;
        if (parentNode) {
            var rect = this.el.parentNode.getBoundingClientRect();
            gallerySize = {
                width: rect.right - rect.left,
                height: rect.bottom - rect.top
            };
        }
        else {
            gallerySize = {
                width: 0,
                height: 0
            }
        }
        if (gallerySize.width === 0 || gallerySize.height === 0) {
            // avoid compute errors
            return;
        }

        var viewsToDisplay = imagesToDisplay.map(getImageView.bind(this));

        this.removeOrphanImages(imagesToDisplay);

        var gridAndTileSizePromise = new Promise(function(resolve, reject) {
            try{
            var gridSize;
            var tileSize;
            if (showSelected) {
                // respect ratio, fit to screen
                Promise.all(viewsToDisplay.map(function(imageView) {
                    return imageView.getFullSizePromise();
                })).then(function(fullImageSizes) {
                    try{
                    // stretch
                    gridSize = getGridSize(gallerySize, fullImageSizes);
                    tileSize = getTileSize(gallerySize, gridSize);
                    resolve({
                        gridSize: gridSize,
                        tileSize: tileSize
                    });
                    } catch (e) {
                        Logger.log(e);
                    }
                });
            }
            else {
                var maxGalleryWidth = viewsToDisplay.length * (StandardTileSize + TileSpacing) - TileSpacing;
                if (maxGalleryWidth < gallerySize.width) {
                    // the whole gallery fits in one line. use standard tile size
                    gridSize = {
                        width: viewsToDisplay.length,
                        height: 1
                    }
                    tileSize = {
                        width: StandardTileSize,
                        height: StandardTileSize
                    };
                }
                else {
                    // fixed tile size
                    var gridWidth = Math.floor(gallerySize.width / StandardTileSize);
                    gridSize = {
                        width: gridWidth,
                        height: 1
                    };

                    tileSize = getTileSize(gallerySize, gridSize);
                    tileSize.height = tileSize.width;
                }
                resolve({
                    gridSize: gridSize,
                    tileSize: tileSize
                });
            }
            }
            catch(e) {
                Logger.log(e);
            }
        });
        gridAndTileSizePromise.then(function(result) {
            // for some racing issues, we may be trying to render an old state
            if (currentRenderCount < this.renderCount) {
                 Logger.log("aborted render, " + currentRenderCount + " vs " + this.renderCount)
                return;
            }
			try{
			var tileSize = result.tileSize;
			var gridSize = result.gridSize;
            var displaySettings = {
                gridSize: gridSize,
                gallerySize: gallerySize,
                displayedImages: viewsToDisplay
            };

            viewsToDisplay.forEach(function(imageView) {
                imageView.setSize(tileSize);
            });
            var isRedrawRequired = !isSame(this.oldDisplaySettings, displaySettings);
            if (!isRedrawRequired) {
                Logger.log("gallery view. render unnecessary");
                return;
            }
            this.oldDisplaySettings = displaySettings;

            Logger.log("clear gallery view");
    //        Logger.log("grid size" + gridSize);
            var latestRowIndex = -1;
            var row;

            if (!showSelected) {
                // reset scale and translation in displayed tiles
                this.resetTransformation();
            }
            viewsToDisplay.forEach(function(imageView, index) {
                if (imageView.el.parentElement !== this.svg) {
                    this.svg.appendChild(imageView.el);
                }

                imageView.setVisible(false);

                var rowIndex = Math.floor(index / gridSize.width);
                var colIndex = index % gridSize.width;
                latestRowIndex = rowIndex;

                var x = colIndex * (tileSize.width + TileSpacing);
                var y = rowIndex * (tileSize.height + TileSpacing);

                imageView.el.setAttribute("transform", "translate(" + x + ", " + y + ")");
                imageView.setVisible(true);
            }.bind(this));

            var minGalleryHeight = latestRowIndex * StandardTileSize + StandardTileSize;
            if (!showSelected && minGalleryHeight > gallerySize.height) {
                // expand the svg if we're displaying the whole gallery
                this.svg.setAttribute("height", minGalleryHeight);
            }
            else {
                this.svg.setAttribute("height", "100%");
            }
			}
			catch(e) {
				Logger.log(e);
			}
        }.bind(this));
    }

    /**
    * Scales all the displayed images
    * @param s the scale
    * @param c the position of the center
    **/
    function zoom(s, c) {
        this.getDisplayedImages().forEach(function(image) {
            // use the center if center is undefined
            if (c === undefined) {
                var size = image.getSize();
                c = {
                    x: size.width / 2,
                    y: size.height / 2
                };
            }
            var translateMinusC = "translate(" + -c.x + ", " + -c.y + ")";
            var translateC = "translate(" + c.x + ", " + c.y + ")";
            image.setTransformation(translateC +
                         " matrix(" + s + ", 0, 0, " + s + ", 0, 0) " +
                         translateMinusC + " " + image.getTransformation());
        });
    }

    function translate(dx, dy) {
        this.prepend("translate(" + dx + ", " + dy + ")");
    }
    /**
    * Prepends a SVG transformation to all the displayed images
    **/
    function prepend(transformation) {
        this.getDisplayedImages().forEach(function(image) {
            image.setTransformation(transformation + " " + image.getTransformation());
        });
    }

    function setTransformation(transformation) {
        this.getDisplayedImages().forEach(function(image) {
            image.setTransformation(transformation);
        });
    }

    function resetTransformation() {
        this.setTransformation("");
    }

    function getImageView(image) {
        var cid = image.cid;
        var imageView;
        if (!(cid in this.imageViews)) {
            // make the image element
            console.log(this.id, "new image");
            imageView = new ImageView({
                model: image,
                attributes: { thumbnailUpdateEnabled: this.isShowSelected() }
            });
            this.imageViews[image.cid] = imageView;
        }
        imageView = this.imageViews[cid];
        return imageView;
    }

    function getGridSize(gallerySize, imageSizes) {
        // Logger.log("getGridSize " + JSON.stringify(gallerySize) + " " + JSON.stringify(imageSizes));
        var maxUsedSize;
        var usedSize;

        // default value is used only when no image is displayed
        var bestGridSize = {
            width: Number.POSITIVE_INFINITY,
            height: 1
        };
        for (var gridWidth = 1; gridWidth <= imageSizes.length; gridWidth++) {
            for (var gridHeight = Math.ceil(imageSizes.length / gridWidth); gridHeight <= imageSizes.length; gridHeight++) {
                var gridSize = {
                    width: gridWidth,
                    height: gridHeight
                };
                var tileSize = getTileSize(gallerySize, gridSize);

                // compute used size
                usedSize = 0;
                imageSizes.forEach(function(size) {
                    var imageSize = getStretchedSize(size, tileSize);
                    usedSize += imageSize.width * imageSize.height;
                });
                // Logger.log("griddimensions (width, height, usedsize", gridWidth + " " + gridHeight + " " + usedSize);
                if (!maxUsedSize || usedSize > maxUsedSize) {
                    bestGridSize = gridSize;
                    maxUsedSize = usedSize;
                }
            }
        }
        return bestGridSize;
    }

    function getTileSize(bounds, gridSize) {
        var gridWidth = gridSize.width;
        var gridHeight = gridSize.height;
        var tileSize = {
            width: (bounds.width - (gridWidth - 1) * TileSpacing) / gridWidth ,
            height: (bounds.height - (gridHeight - 1) * TileSpacing) / gridHeight
        }
        return tileSize;
    }

    // returns the size of the image stretched to fit within some bounds
    function getStretchedSize(size, tileSize) {
        // Logger.log("getStretchedSize ", JSON.stringify(size), JSON.stringify(tileSize));
        var imageRatio = size.width / size.height;
        // fit width
        var imageSize = {
            width: tileSize.width,
            height: tileSize.width / imageRatio
        };
        // fit height
        var scale = Math.min(1, tileSize.height / imageSize.height);
        imageSize.width *= scale;
        imageSize.height *= scale;
        return imageSize;
    }

    function getDisplayedImages() {
        if (!this.oldDisplaySettings ||
            !this.oldDisplaySettings.displayedImages)
            return [];

        return this.oldDisplaySettings.displayedImages;
    }

    function isSame(displaySettings, oldDisplaySettings) {
        if (!oldDisplaySettings && displaySettings ||
            oldDisplaySettings && !displaySettings)
            return false;
        if (!oldDisplaySettings && !displaySettings)
            return true;

        var isSameGridSize = displaySettings.gridSize.width === oldDisplaySettings.gridSize.width &&
            displaySettings.gridSize.height === oldDisplaySettings.gridSize.height;
        var isSameGallerySize = displaySettings.gallerySize.width === oldDisplaySettings.gallerySize.width &&
            displaySettings.gallerySize.height === oldDisplaySettings.gallerySize.height;
        var arraysIdentical = areArraysIdentical(displaySettings.displayedImages, oldDisplaySettings.displayedImages);
        var r = // compare grid size
            isSameGridSize &&
            // compare gallery size
            isSameGallerySize &&
            arraysIdentical;
        return r;
    }

    function areArraysIdentical(a1, a2) {
        return a1 === a2 ||
            a1.length === a2.length &&
            a1.every(function(obj, index) {
                return obj === a2[index];
            });
    }

    return GalleryView;
});
<|endoftext|>


<commit_before>import hbs from 'htmlbars-inline-precompile';
import { module } from 'qunit';
import { render, click, triggerEvent, getContext } from '@ember/test-helpers';
import { setupRenderingTest } from 'ember-qunit';
import { test, testBS3, testBS4, delay } from '../../helpers/bootstrap-test';
import { skip } from 'qunit';
import setupNoDeprecations from '../../helpers/setup-no-deprecations';

const TRANSITION_DURATION = 50;

function clickIndicator(index) {
  return click(`.carousel-indicators li:nth-child(${index})`);
}

function clickToNextSlide() {
  let rootEl = getContext().element;
  let control = rootEl.querySelector('.carousel-control.right');
  if (control !== null) {
    return click('.carousel-control.right');
  }
  control = rootEl.querySelector('.carousel-control-next');
  if (control !== null) {
    return click('.carousel-control-next');
  }
  return null;
}

function clickToPrevSlide() {
  let rootEl = getContext().element;
  let control = rootEl.querySelector('.carousel-control.left');
  if (control !== null) {
    return click('.carousel-control.left');
  }
  control = rootEl.querySelector('.carousel-control-prev');
  if (control !== null) {
    return click('.carousel-control-prev');
  }
  return null;
}

function getActivatedSlide(index) {
  let rootEl = getContext().element;
  return rootEl.querySelector(`.carousel-inner div:nth-child(${index}).active`);
}

function waitTransitionTime(interval = 450) {
  return delay(TRANSITION_DURATION + interval);
}

module('Integration | Component | bs-carousel', function(hooks) {
  setupRenderingTest(hooks);
  setupNoDeprecations(hooks);

  hooks.beforeEach(function() {
    this.set('interval', 300);
    this.set('transitionDuration', TRANSITION_DURATION);
    this.stopCarousel = () => {
      this.set('interval', 0);
    }
  });

  // Markup

  test('carousel has correct markup', async function(assert) {
    await render(hbs`{{bs-carousel interval=0}}`);

    assert.dom('.carousel').exists('has carousel class');
    assert.dom('.carousel').hasClass('slide', 'has slide class');
    assert.dom('.carousel-inner').exists({ count: 1 }, 'has carousel-inner');
  });

  testBS3('carousel has correct controls markup', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 as |car|}}{{/bs-carousel}}`);

    let left = this.element.querySelector('.left');
    let right = this.element.querySelector('.right');
    assert.dom(left).hasClass('carousel-control', 'has left control class names');
    assert.dom(right).hasClass('carousel-control', 'has right control class names');
    assert.ok(left.querySelector('.sr-only'), 'left control has sr-only');
    assert.ok(right.querySelector('.sr-only'), 'right control has sr-only');
  });

  testBS4('carousel has correct controls markup', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 as |car|}}{{/bs-carousel}}`);

    let prev = this.element.querySelectorAll('.carousel-control-prev');
    let next = this.element.querySelectorAll('.carousel-control-next');
    assert.equal(prev.length, 1, 'has left control class names');
    assert.equal(next.length, 1, 'has right control class names');
    assert.ok(prev[0].querySelector('.sr-only'), 'left control has sr-only');
    assert.ok(next[0].querySelector('.sr-only'), 'right control has sr-only');
  });

  testBS3('carousel has correct indicators and slides markup', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);

    assert.dom('.carousel-indicators li').exists({ count: 2 }, 'has right number of indicators');
    assert.dom('.carousel-inner .item').exists({ count: 2 }, 'has right number of slides');
  });

  testBS4('carousel has correct indicators and slides markup', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);

    assert.dom('.carousel-indicators li').exists({ count: 2 }, 'has right number of indicators');
    assert.dom('.carousel-inner .carousel-item').exists({ count: 2 }, 'has right number of slides');
  });

  // Parameters

  testBS4('carousel transition="fade" has correct markup', async function(assert) {
    await render(hbs`{{bs-carousel interval=0 transition='fade'}}`);
    assert.dom('.carousel').hasClass('carousel-fade', 'has carousel-fade class');
  });

  test('carousel autoPlay=false must not start sliding', async function(assert) {
    await render(hbs`{{#bs-carousel autoPlay=false interval=300 transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    await waitTransitionTime();
    assert.ok(getActivatedSlide(1), 'autoPlay has correct behavior');
  });

  // very flakey :-(
  skip('carousel autoPlay=true must start sliding', async function(assert) {
    render(hbs`{{#bs-carousel autoPlay=true interval=interval transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    await waitTransitionTime();
    assert.ok(getActivatedSlide(2), 'autoPlay has correct behavior');
    this.stopCarousel();
  });

  test('carousel calls onSlideChanged when slide changes', async function(assert) {
    let action = this.spy();
    this.set('action', action);
    await render(hbs`{{#bs-carousel onSlideChanged=this.action wrap=false transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    assert.ok(action.notCalled, 'onSlideChanged action has not been called.');
    clickToNextSlide();
    await waitTransitionTime();
    assert.ok(action.calledWith(1), 'onSlideChanged action has been called.');
  });

  test('carousel wrap=false must not cross lower bound', async function(assert) {
    await render(hbs`{{#bs-carousel wrap=false transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    clickToPrevSlide();
    await waitTransitionTime();
    assert.ok(getActivatedSlide(1), 'wrap has correct behavior');
  });

  test('carousel wrap=false must not cross upper bound', async function(assert) {
    await render(
      hbs`{{#bs-carousel wrap=false index=1 transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`
    );
    clickToNextSlide();
    await waitTransitionTime();
    assert.ok(getActivatedSlide(2), 'wrap has correct behavior');
  });

  test('carousel wrap=true must cross lower bound', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 wrap=true transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    await clickToPrevSlide();
    assert.ok(getActivatedSlide(2), 'wrap has correct behavior');
  });

  test('carousel wrap=true must cross upper bound', async function(assert) {
    await render(
      hbs`{{#bs-carousel interval=0 wrap=true index=1 transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`
    );
    await clickToNextSlide();
    assert.ok(getActivatedSlide(1), 'wrap has correct behavior');
  });

  test('carousel index=N specifies starting slide', async function(assert) {
    await render(hbs`{{#bs-carousel index=1 as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    assert.ok(getActivatedSlide(2), 'index has correct behavior');
  });

  test('carousel interval=0 must not trigger automatic sliding', async function(assert) {
    await render(hbs`{{#bs-carousel autoPlay=true interval=0 transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    await waitTransitionTime();
    assert.ok(getActivatedSlide(1), 'interval has correct behavior');
  });

  // very flakey :-(
  skip('carousel ltr=false does right-to-left automatic sliding', async function(assert) {
    render(
      hbs`{{#bs-carousel autoPlay=true interval=interval transitionDuration=transitionDuration ltr=false as |car|}}{{car.slide}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`
    );
    await waitTransitionTime();
    assert.ok(getActivatedSlide(3), 'ltr has correct behavior');
    this.stopCarousel();
  });

  // very flakey :-(
  skip('carousel ltr=true does left-to-right automatic sliding', async function(assert) {
    render(
      hbs`{{#bs-carousel autoPlay=true interval=interval transitionDuration=transitionDuration ltr=true as |car|}}{{car.slide}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`
    );
    await waitTransitionTime();
    assert.ok(getActivatedSlide(2), 'ltr has correct behavior');
    this.stopCarousel();
  });

  test('carousel pauseOnMouseEnter=false must not pause automatic sliding', async function(assert) {
    render(
      hbs`{{#bs-carousel autoPlay=true interval=interval transitionDuration=transitionDuration pauseOnMouseEnter=false as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`
    );
    await waitTransitionTime(10);
    triggerEvent('.carousel', 'mouseenter');
    await waitTransitionTime();
    assert.ok(getActivatedSlide(2), 'pauseOnMouseEnter has correct behavior');
    this.stopCarousel();
  });

  test('carousel pauseOnMouseEnter=true must pause automatic sliding', async function(assert) {
    render(
      hbs`{{#bs-carousel autoPlay=true interval=interval transitionDuration=transitionDuration pauseOnMouseEnter=true as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`
    );
    await waitTransitionTime(10);
    triggerEvent('.carousel', 'mouseenter');
    await waitTransitionTime();
    assert.ok(getActivatedSlide(1), 'pauseOnMouseEnter has correct behavior');
    this.stopCarousel();
  });

  // User clicks

  test('carousel has functional right control', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    await clickToNextSlide();
    assert.ok(getActivatedSlide(2), 'right control changes slide');
  });

  test('carousel has functional left control', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 transitionDuration=transitionDuration index=1 as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    await clickToPrevSlide();
    assert.ok(getActivatedSlide(1), 'left control changes slide');
  });

  test('carousel has functional indicators', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    await clickIndicator(2);
    assert.dom('.carousel-indicators > li:nth-child(2).active').exists('indicators changes indicator index');
    assert.ok(getActivatedSlide(2), 'indicators changes slide index');
  });
});
<commit_msg>Adjust assert position in carousel test<commit_after>import hbs from 'htmlbars-inline-precompile';
import { module } from 'qunit';
import { render, click, triggerEvent, getContext } from '@ember/test-helpers';
import { setupRenderingTest } from 'ember-qunit';
import { test, testBS3, testBS4, delay } from '../../helpers/bootstrap-test';
import { skip } from 'qunit';
import setupNoDeprecations from '../../helpers/setup-no-deprecations';

const TRANSITION_DURATION = 50;

function clickIndicator(index) {
  return click(`.carousel-indicators li:nth-child(${index})`);
}

function clickToNextSlide() {
  let rootEl = getContext().element;
  let control = rootEl.querySelector('.carousel-control.right');
  if (control !== null) {
    return click('.carousel-control.right');
  }
  control = rootEl.querySelector('.carousel-control-next');
  if (control !== null) {
    return click('.carousel-control-next');
  }
  return null;
}

function clickToPrevSlide() {
  let rootEl = getContext().element;
  let control = rootEl.querySelector('.carousel-control.left');
  if (control !== null) {
    return click('.carousel-control.left');
  }
  control = rootEl.querySelector('.carousel-control-prev');
  if (control !== null) {
    return click('.carousel-control-prev');
  }
  return null;
}

function getActivatedSlide(index) {
  let rootEl = getContext().element;
  return rootEl.querySelector(`.carousel-inner div:nth-child(${index}).active`);
}

function waitTransitionTime(interval = 450) {
  return delay(TRANSITION_DURATION + interval);
}

module('Integration | Component | bs-carousel', function(hooks) {
  setupRenderingTest(hooks);
  setupNoDeprecations(hooks);

  hooks.beforeEach(function() {
    this.set('interval', 300);
    this.set('transitionDuration', TRANSITION_DURATION);
    this.stopCarousel = () => {
      this.set('interval', 0);
    }
  });

  // Markup

  test('carousel has correct markup', async function(assert) {
    await render(hbs`{{bs-carousel interval=0}}`);

    assert.dom('.carousel').exists('has carousel class');
    assert.dom('.carousel').hasClass('slide', 'has slide class');
    assert.dom('.carousel-inner').exists({ count: 1 }, 'has carousel-inner');
  });

  testBS3('carousel has correct controls markup', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 as |car|}}{{/bs-carousel}}`);

    let left = this.element.querySelector('.left');
    let right = this.element.querySelector('.right');
    assert.dom(left).hasClass('carousel-control', 'has left control class names');
    assert.dom(right).hasClass('carousel-control', 'has right control class names');
    assert.ok(left.querySelector('.sr-only'), 'left control has sr-only');
    assert.ok(right.querySelector('.sr-only'), 'right control has sr-only');
  });

  testBS4('carousel has correct controls markup', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 as |car|}}{{/bs-carousel}}`);

    let prev = this.element.querySelectorAll('.carousel-control-prev');
    let next = this.element.querySelectorAll('.carousel-control-next');
    assert.equal(prev.length, 1, 'has left control class names');
    assert.equal(next.length, 1, 'has right control class names');
    assert.ok(prev[0].querySelector('.sr-only'), 'left control has sr-only');
    assert.ok(next[0].querySelector('.sr-only'), 'right control has sr-only');
  });

  testBS3('carousel has correct indicators and slides markup', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);

    assert.dom('.carousel-indicators li').exists({ count: 2 }, 'has right number of indicators');
    assert.dom('.carousel-inner .item').exists({ count: 2 }, 'has right number of slides');
  });

  testBS4('carousel has correct indicators and slides markup', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);

    assert.dom('.carousel-indicators li').exists({ count: 2 }, 'has right number of indicators');
    assert.dom('.carousel-inner .carousel-item').exists({ count: 2 }, 'has right number of slides');
  });

  // Parameters

  testBS4('carousel transition="fade" has correct markup', async function(assert) {
    await render(hbs`{{bs-carousel interval=0 transition='fade'}}`);
    assert.dom('.carousel').hasClass('carousel-fade', 'has carousel-fade class');
  });

  test('carousel autoPlay=false must not start sliding', async function(assert) {
    await render(hbs`{{#bs-carousel autoPlay=false interval=300 transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    await waitTransitionTime();
    assert.ok(getActivatedSlide(1), 'autoPlay has correct behavior');
  });

  // very flakey :-(
  skip('carousel autoPlay=true must start sliding', async function(assert) {
    render(hbs`{{#bs-carousel autoPlay=true interval=interval transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    await waitTransitionTime();
    assert.ok(getActivatedSlide(2), 'autoPlay has correct behavior');
    this.stopCarousel();
  });

  test('carousel calls onSlideChanged when slide changes', async function(assert) {
    let action = this.spy();
    this.set('action', action);
    await render(hbs`{{#bs-carousel onSlideChanged=this.action wrap=false transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    clickToNextSlide();
    assert.ok(action.notCalled, 'onSlideChanged action has not been called.');
    await waitTransitionTime();
    assert.ok(action.calledWith(1), 'onSlideChanged action has been called.');
  });

  test('carousel wrap=false must not cross lower bound', async function(assert) {
    await render(hbs`{{#bs-carousel wrap=false transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    clickToPrevSlide();
    await waitTransitionTime();
    assert.ok(getActivatedSlide(1), 'wrap has correct behavior');
  });

  test('carousel wrap=false must not cross upper bound', async function(assert) {
    await render(
      hbs`{{#bs-carousel wrap=false index=1 transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`
    );
    clickToNextSlide();
    await waitTransitionTime();
    assert.ok(getActivatedSlide(2), 'wrap has correct behavior');
  });

  test('carousel wrap=true must cross lower bound', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 wrap=true transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    await clickToPrevSlide();
    assert.ok(getActivatedSlide(2), 'wrap has correct behavior');
  });

  test('carousel wrap=true must cross upper bound', async function(assert) {
    await render(
      hbs`{{#bs-carousel interval=0 wrap=true index=1 transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`
    );
    await clickToNextSlide();
    assert.ok(getActivatedSlide(1), 'wrap has correct behavior');
  });

  test('carousel index=N specifies starting slide', async function(assert) {
    await render(hbs`{{#bs-carousel index=1 as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    assert.ok(getActivatedSlide(2), 'index has correct behavior');
  });

  test('carousel interval=0 must not trigger automatic sliding', async function(assert) {
    await render(hbs`{{#bs-carousel autoPlay=true interval=0 transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    await waitTransitionTime();
    assert.ok(getActivatedSlide(1), 'interval has correct behavior');
  });

  // very flakey :-(
  skip('carousel ltr=false does right-to-left automatic sliding', async function(assert) {
    render(
      hbs`{{#bs-carousel autoPlay=true interval=interval transitionDuration=transitionDuration ltr=false as |car|}}{{car.slide}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`
    );
    await waitTransitionTime();
    assert.ok(getActivatedSlide(3), 'ltr has correct behavior');
    this.stopCarousel();
  });

  // very flakey :-(
  skip('carousel ltr=true does left-to-right automatic sliding', async function(assert) {
    render(
      hbs`{{#bs-carousel autoPlay=true interval=interval transitionDuration=transitionDuration ltr=true as |car|}}{{car.slide}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`
    );
    await waitTransitionTime();
    assert.ok(getActivatedSlide(2), 'ltr has correct behavior');
    this.stopCarousel();
  });

  test('carousel pauseOnMouseEnter=false must not pause automatic sliding', async function(assert) {
    render(
      hbs`{{#bs-carousel autoPlay=true interval=interval transitionDuration=transitionDuration pauseOnMouseEnter=false as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`
    );
    await waitTransitionTime(10);
    triggerEvent('.carousel', 'mouseenter');
    await waitTransitionTime();
    assert.ok(getActivatedSlide(2), 'pauseOnMouseEnter has correct behavior');
    this.stopCarousel();
  });

  test('carousel pauseOnMouseEnter=true must pause automatic sliding', async function(assert) {
    render(
      hbs`{{#bs-carousel autoPlay=true interval=interval transitionDuration=transitionDuration pauseOnMouseEnter=true as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`
    );
    await waitTransitionTime(10);
    triggerEvent('.carousel', 'mouseenter');
    await waitTransitionTime();
    assert.ok(getActivatedSlide(1), 'pauseOnMouseEnter has correct behavior');
    this.stopCarousel();
  });

  // User clicks

  test('carousel has functional right control', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    await clickToNextSlide();
    assert.ok(getActivatedSlide(2), 'right control changes slide');
  });

  test('carousel has functional left control', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 transitionDuration=transitionDuration index=1 as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    await clickToPrevSlide();
    assert.ok(getActivatedSlide(1), 'left control changes slide');
  });

  test('carousel has functional indicators', async function(assert) {
    await render(hbs`{{#bs-carousel interval=0 transitionDuration=transitionDuration as |car|}}{{car.slide}}{{car.slide}}{{/bs-carousel}}`);
    await clickIndicator(2);
    assert.dom('.carousel-indicators > li:nth-child(2).active').exists('indicators changes indicator index');
    assert.ok(getActivatedSlide(2), 'indicators changes slide index');
  });
});
<|endoftext|>


<commit_before>'use strict';

const nunjucks = require('nunjucks');
const path = require('path');
const env = new nunjucks.Environment(
    new nunjucks.FileSystemLoader(['./src/'], {
        noCache: true
    })
);

function renderHtml(data) {
    return new Promise((resolve, reject) => {
        env.render(path.join(__dirname, '../src/templates/product.html'), data, (err, html) => {
            err ? reject(err) : resolve(html);
        });
    });
}

module.exports = renderHtml;
<commit_msg>fix(render-html): adjust path<commit_after>'use strict';

const nunjucks = require('nunjucks');
const path = require('path');
const env = new nunjucks.Environment(
    new nunjucks.FileSystemLoader([path.join(__dirname, '/../src/')], {
        noCache: true
    })
);

function renderHtml(data) {
    return new Promise((resolve, reject) => {
        env.render('templates/product.html', data, (err, html) => {
            err ? reject(err) : resolve(html);
        });
    });
}

module.exports = renderHtml;
<|endoftext|>


<commit_before>import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';

moduleForComponent('main-footer', 'Integration | Component | main footer', {
  integration: true
});

test('it renders', function(assert) {

  // Set any properties with this.set('myProperty', 'value');
  // Handle any actions with this.on('myAction', function(val) { ... });

  this.render(hbs`{{main-footer}}`);

  assert.equal(this.$().text().trim(), '');

  // Template block usage:
  this.render(hbs`
    {{#main-footer}}
      template block text
    {{/main-footer}}
  `);

  assert.equal(this.$().text().trim(), 'template block text');
});
<commit_msg>Discard footer component integration test<commit_after><|endoftext|>