<commit_before>define([
    'jquery',
    'underscore',
    'knockout',
    'arches',
    'viewmodels/alert',
    'views/search/base-filter',
    'views/search/time-filter',
    'views/search/term-filter',
    'views/search/map-filter',
    'views/search/resource-type-filter',
    'views/resource/related-resources-manager',
    'views/search/search-results',
    'views/base-manager'
], function($, _, ko, arches, AlertViewModel, BaseFilter, TimeFilter, TermFilter, MapFilter, ResourceTypeFilter, RelatedResourcesManager, SearchResults, BaseManagerView) {
    // a method to track the old and new values of a subscribable
    // from https://github.com/knockout/knockout/issues/914
    //
    // use case:
    // var sub1 = this.FirstName.subscribeChanged(function (newValue, oldValue) {
    //     this.NewValue1(newValue);
    //     this.OldValue1(oldValue);
    // }, this);

    ko.subscribable.fn.subscribeChanged = function (callback, context) {
        var savedValue = this.peek();
        return this.subscribe(function (latestValue) {
            var oldValue = savedValue;
            savedValue = latestValue;
            callback.call(context, latestValue, oldValue);
        });
    };

    var SearchBaseManagerView = BaseManagerView.extend({
        initialize: function(options) {
            this.isNewQuery = true;
            this.viewModel.resultsExpanded = ko.observable(true);

            this.filters = {};
            this.filters.termFilter = new TermFilter();
            this.filters.timeFilter = new TimeFilter({
                termFilter: this.filters.termFilter
            });
            this.filters.resourceTypeFilter = new ResourceTypeFilter({
                termFilter: this.filters.termFilter
            });
            this.filters.mapFilter = new MapFilter({
                resizeOnChange: this.viewModel.resultsExpanded,
                termFilter: this.filters.termFilter
            });
            this.filters.savedSearches = new BaseFilter();
            this.filters.advancedFilter = new BaseFilter();
            this.filters.searchRelatedResources = new BaseFilter()

            _.extend(this.viewModel, this.filters);

            this.viewModel.searchResults = new SearchResults({
                viewModel: this.viewModel
            });

            this.filters.mapFilter.results = this.viewModel.searchResults;

            this.viewModel.relatedResourcesManager = new RelatedResourcesManager({
                searchResults: this.viewModel.searchResults,
                resourceEditorContext: this.viewModel.resourceEditorContext,
                editing_instance_id: this.viewModel.editingInstanceId,
                relationship_types: this.viewModel.relationship_types
            })

            this.viewModel.selectedTab = this.viewModel.resourceEditorContext === true ? ko.observable(this.viewModel.relatedResourcesManager) : ko.observable(this.filters.mapFilter);
            this.filters.mapFilter.results = this.viewModel.searchResults;

            this.queryString = ko.computed(function() {
                // we used to loop through all the filters but
                // that caused this computed to be triggered multiple times
                var params = {};

                this.filters.termFilter.appendFilters(params);
                this.filters.timeFilter.appendFilters(params);
                this.filters.resourceTypeFilter.appendFilters(params);
                this.filters.mapFilter.appendFilters(params);

                params.no_filters = !Object.keys(params).length;
                return params;
            }, this);

            this.filters.termFilter.filter.tags.subscribe(function(tags){
                _.each(tags, function(tag){
                    if(tag.status === 'deleted'){
                        var found = _.find(this.filters.termFilter.filter.tags, function(currentTag){
                           return tag.value.type === currentTag.type;
                        }, this)
                        if(!found){
                            _.each(this.filters, function(filter){
                                if(filter.name === tag.value.type){
                                    filter.clear();
                                }
                            }, this);
                        }

                    }
                }, this)
            }, this, "arrayChange");

            this.restoreState();

            this.viewModel.searchResults.page.subscribe(function(){
                if(this.viewModel.searchResults.userRequestedNewPage()){
                    this.isNewQuery = false;
                    this.viewModel.searchResults.userRequestedNewPage(false);
                    console.log('user requested new page')
                    this.doQuery();
                }
            }, this)

            this.queryString.subscribe(function() {
                console.log('user updated a filter')
                this.isNewQuery = true;
                this.viewModel.searchResults.page(1);
                this.doQuery();
            }, this);

            BaseManagerView.prototype.initialize.call(this, options);
        },

        doQuery: function() {
            console.log('in doQuery');
            var queryString = this.queryString();
            queryString.page = this.viewModel.searchResults.page();
            queryString.include_ids = this.isNewQuery;
            if (this.updateRequest) {
                this.updateRequest.abort();
            }

            this.viewModel.loading(true);

            this.updateRequest = $.ajax({
                type: "GET",
                url: arches.urls.search_results,
                data: queryString,
                context: this,
                success: function(response) {
                    var data = this.viewModel.searchResults.updateResults(response);
                    //this.isNewQuery = true;
                },
                error: function(response, status, error) {
                    if(this.updateRequest.statusText !== 'abort'){
                        this.viewModel.alert(new AlertViewModel('ep-alert-red', arches.graphImportFailed.title, response));
                    }
                },
                complete: function(request, status) {
                    this.viewModel.loading(false);
                    this.updateRequest = undefined;
                    window.history.pushState({}, '', '?' + $.param(queryString).split('+').join('%20'));
                }
            });
        },

        restoreState: function() {
            var query = _.chain(decodeURIComponent(location.search).slice(1).split('&'))
                // Split each array item into [key, value]
                // ignore empty string if search is empty
                .map(function(item) {
                    if (item) return item.split('=');
                })
                // Remove undefined in the case the search is empty
                .compact()
                // Turn [key, value] arrays into object parameters
                .object()
                // Return the value of the chain operation
                .value();

            if ('page' in query) {
                query.page = JSON.parse(query.page);
            } else {
                query.page = 1;
            }
            this.viewModel.searchResults.restoreState(query.page);

            _.each(this.filters, function (filter) {
                filter.restoreState(query)
            });

            this.doQuery();
        },

        clear: function() {
            _.each(this.filters, function (filter) {
                filter.clear();
            });
        }
    });

    return SearchBaseManagerView;
});
<commit_msg>added a trigger to resize related resources table when a user opens related resources in the editor<commit_after>define([
    'jquery',
    'underscore',
    'knockout',
    'arches',
    'viewmodels/alert',
    'views/search/base-filter',
    'views/search/time-filter',
    'views/search/term-filter',
    'views/search/map-filter',
    'views/search/resource-type-filter',
    'views/resource/related-resources-manager',
    'views/search/search-results',
    'views/base-manager'
], function($, _, ko, arches, AlertViewModel, BaseFilter, TimeFilter, TermFilter, MapFilter, ResourceTypeFilter, RelatedResourcesManager, SearchResults, BaseManagerView) {
    // a method to track the old and new values of a subscribable
    // from https://github.com/knockout/knockout/issues/914
    //
    // use case:
    // var sub1 = this.FirstName.subscribeChanged(function (newValue, oldValue) {
    //     this.NewValue1(newValue);
    //     this.OldValue1(oldValue);
    // }, this);

    ko.subscribable.fn.subscribeChanged = function (callback, context) {
        var savedValue = this.peek();
        return this.subscribe(function (latestValue) {
            var oldValue = savedValue;
            savedValue = latestValue;
            callback.call(context, latestValue, oldValue);
        });
    };

    var SearchBaseManagerView = BaseManagerView.extend({
        initialize: function(options) {
            this.isNewQuery = true;
            this.viewModel.resultsExpanded = ko.observable(true);

            this.filters = {};
            this.filters.termFilter = new TermFilter();
            this.filters.timeFilter = new TimeFilter({
                termFilter: this.filters.termFilter
            });
            this.filters.resourceTypeFilter = new ResourceTypeFilter({
                termFilter: this.filters.termFilter
            });
            this.filters.mapFilter = new MapFilter({
                resizeOnChange: this.viewModel.resultsExpanded,
                termFilter: this.filters.termFilter
            });
            this.filters.savedSearches = new BaseFilter();
            this.filters.advancedFilter = new BaseFilter();
            this.filters.searchRelatedResources = new BaseFilter()

            _.extend(this.viewModel, this.filters);

            this.viewModel.searchResults = new SearchResults({
                viewModel: this.viewModel
            });

            this.filters.mapFilter.results = this.viewModel.searchResults;

            this.viewModel.relatedResourcesManager = new RelatedResourcesManager({
                searchResults: this.viewModel.searchResults,
                resourceEditorContext: this.viewModel.resourceEditorContext,
                editing_instance_id: this.viewModel.editingInstanceId,
                relationship_types: this.viewModel.relationship_types
            })

            this.viewModel.selectedTab = this.viewModel.resourceEditorContext === true ? ko.observable(this.viewModel.relatedResourcesManager) : ko.observable(this.filters.mapFilter);
            if (this.viewModel.resourceEditorContext === true) {
                this.viewModel.openRelatedResources.subscribe(function(val) {
                    if (val === true) {
                        var resize = function(){
                            $(window).trigger("resize");
                        }
                        setTimeout(resize, 200);
                    }
                })
            }

            this.filters.mapFilter.results = this.viewModel.searchResults;

            this.queryString = ko.computed(function() {
                // we used to loop through all the filters but
                // that caused this computed to be triggered multiple times
                var params = {};

                this.filters.termFilter.appendFilters(params);
                this.filters.timeFilter.appendFilters(params);
                this.filters.resourceTypeFilter.appendFilters(params);
                this.filters.mapFilter.appendFilters(params);

                params.no_filters = !Object.keys(params).length;
                return params;
            }, this);

            this.filters.termFilter.filter.tags.subscribe(function(tags){
                _.each(tags, function(tag){
                    if(tag.status === 'deleted'){
                        var found = _.find(this.filters.termFilter.filter.tags, function(currentTag){
                           return tag.value.type === currentTag.type;
                        }, this)
                        if(!found){
                            _.each(this.filters, function(filter){
                                if(filter.name === tag.value.type){
                                    filter.clear();
                                }
                            }, this);
                        }

                    }
                }, this)
            }, this, "arrayChange");

            this.restoreState();

            this.viewModel.searchResults.page.subscribe(function(){
                if(this.viewModel.searchResults.userRequestedNewPage()){
                    this.isNewQuery = false;
                    this.viewModel.searchResults.userRequestedNewPage(false);
                    console.log('user requested new page')
                    this.doQuery();
                }
            }, this)

            this.queryString.subscribe(function() {
                console.log('user updated a filter')
                this.isNewQuery = true;
                this.viewModel.searchResults.page(1);
                this.doQuery();
            }, this);

            BaseManagerView.prototype.initialize.call(this, options);
        },

        doQuery: function() {
            console.log('in doQuery');
            var queryString = this.queryString();
            queryString.page = this.viewModel.searchResults.page();
            queryString.include_ids = this.isNewQuery;
            if (this.updateRequest) {
                this.updateRequest.abort();
            }

            this.viewModel.loading(true);

            this.updateRequest = $.ajax({
                type: "GET",
                url: arches.urls.search_results,
                data: queryString,
                context: this,
                success: function(response) {
                    var data = this.viewModel.searchResults.updateResults(response);
                    //this.isNewQuery = true;
                },
                error: function(response, status, error) {
                    if(this.updateRequest.statusText !== 'abort'){
                        this.viewModel.alert(new AlertViewModel('ep-alert-red', arches.graphImportFailed.title, response));
                    }
                },
                complete: function(request, status) {
                    this.viewModel.loading(false);
                    this.updateRequest = undefined;
                    window.history.pushState({}, '', '?' + $.param(queryString).split('+').join('%20'));
                }
            });
        },

        restoreState: function() {
            var query = _.chain(decodeURIComponent(location.search).slice(1).split('&'))
                // Split each array item into [key, value]
                // ignore empty string if search is empty
                .map(function(item) {
                    if (item) return item.split('=');
                })
                // Remove undefined in the case the search is empty
                .compact()
                // Turn [key, value] arrays into object parameters
                .object()
                // Return the value of the chain operation
                .value();

            if ('page' in query) {
                query.page = JSON.parse(query.page);
            } else {
                query.page = 1;
            }
            this.viewModel.searchResults.restoreState(query.page);

            _.each(this.filters, function (filter) {
                filter.restoreState(query)
            });

            this.doQuery();
        },

        clear: function() {
            _.each(this.filters, function (filter) {
                filter.clear();
            });
        }
    });

    return SearchBaseManagerView;
});
<|endoftext|>


<commit_before>/**
 * SettingsInactiveModules component.
 *
 * 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.
 */

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import { CORE_MODULES } from '../../googlesitekit/modules/datastore/constants';
import Data from 'googlesitekit-data';
import Layout from '../layout/Layout';
import Notification from '../legacy-notifications/notification';
import SetupModule from './SetupModule';
import { Cell, Grid, Row } from '../../material-components';
import ThumbsUpSVG from '../../../svg/thumbs-up.svg';
const { useSelect } = Data;

const SettingsInactiveModules = () => {
	const modules = useSelect( ( select ) => select( CORE_MODULES ).getModules() );

	if ( ! modules ) {
		return null;
	}

	const inactiveModules = Object.values( modules )
		.filter( ( module ) => ! module.internal && ! module.active )
		.sort( ( module1, module2 ) => module1.sort - module2.sort );

	if ( inactiveModules.length === 0 ) {
		return (
			<Notification
				id="no-more-modules"
				title={ __( 'Congrats, you’ve connected all services!', 'google-site-kit' ) }
				description={ __( 'We’re working on adding new services to Site Kit by Google all the time, so please check back in the future.', 'google-site-kit' ) }
				format="small"
				SmallImageSVG={ ThumbsUpSVG }
				type="win-success"
			/>
		);
	}

	return (
		<Layout
			header
			title={ __( 'Connect More Services to Gain More Insights', 'google-site-kit' ) }
			relative
		>
			<Grid>
				<Row>
					{ inactiveModules.map( ( module ) => (
						<Cell
							key={ module.slug }
							size={ 4 }
						>
							<SetupModule
								slug={ module.slug }
								name={ module.name }
								description={ module.description }
							/>
						</Cell>
					) ) }
				</Row>
			</Grid>
		</Layout>
	);
};

export default SettingsInactiveModules;
<commit_msg>Update SettingsInactiveModules to preserve list based on initial active state.<commit_after>/**
 * SettingsInactiveModules component.
 *
 * 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.
 */

/**
 * WordPress dependencies
 */
import { useEffect, useState } from '@wordpress/element';
import { __ } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import { CORE_MODULES } from '../../googlesitekit/modules/datastore/constants';
import Data from 'googlesitekit-data';
import Layout from '../layout/Layout';
import Notification from '../legacy-notifications/notification';
import SetupModule from './SetupModule';
import { Cell, Grid, Row } from '../../material-components';
import ThumbsUpSVG from '../../../svg/thumbs-up.svg';
const { useSelect } = Data;

const SettingsInactiveModules = () => {
	// We store `initialInactiveSlugs` separately to avoid
	// layout shifts when activating a module as it would otherwise
	// cause the activated module to be removed upon activation.
	const [ initialInactiveSlugs, setInitialInactiveSlugs ] = useState();
	const modules = useSelect( ( select ) => select( CORE_MODULES ).getModules() );
	useEffect( () => {
		// Only set initialInactiveSlugs once, as soon as modules are available.
		if ( ! modules || initialInactiveSlugs !== undefined ) {
			return;
		}

		const inactiveSlugs = Object.keys( modules )
			.filter( ( slug ) => ! modules[ slug ].active );

		setInitialInactiveSlugs( inactiveSlugs );
	}, [ modules ] );

	if ( ! initialInactiveSlugs ) {
		return null;
	}

	const inactiveModules = initialInactiveSlugs
		.map( ( slug ) => modules[ slug ] )
		.filter( ( module ) => ! module.internal )
		.sort( ( a, b ) => a.sort - b.sort )
	;

	if ( inactiveModules.length === 0 ) {
		return (
			<Notification
				id="no-more-modules"
				title={ __( 'Congrats, you’ve connected all services!', 'google-site-kit' ) }
				description={ __( 'We’re working on adding new services to Site Kit by Google all the time, so please check back in the future.', 'google-site-kit' ) }
				format="small"
				SmallImageSVG={ ThumbsUpSVG }
				type="win-success"
			/>
		);
	}

	return (
		<Layout
			header
			title={ __( 'Connect More Services to Gain More Insights', 'google-site-kit' ) }
			relative
		>
			<Grid>
				<Row>
					{ inactiveModules.map( ( module ) => (
						<Cell
							key={ module.slug }
							size={ 4 }
						>
							<SetupModule
								slug={ module.slug }
								name={ module.name }
								description={ module.description }
							/>
						</Cell>
					) ) }
				</Row>
			</Grid>
		</Layout>
	);
};

export default SettingsInactiveModules;
<|endoftext|>


<commit_before>/**
 * `modules/analytics` data store: setup-flow tests.
 *
 * 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.
 */

/**
 * WordPress dependencies
 */
import { createRegistry } from '@wordpress/data';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import { MODULES_ANALYTICS } from './constants';
import { createTestRegistry, unsubscribeFromAll } from 'tests/js/utils';
import * as modulesAnalytics from '../';
import { MODULES_ANALYTICS_4 } from '../../analytics-4/datastore/constants';

const accountID = 'pub-12345678';

const populateAnalyticsDatastore = ( registry ) => {
	registry.dispatch( MODULES_ANALYTICS ).receiveGetProperties(
		[
			{
				// eslint-disable-next-line sitekit/acronym-case
				accountId: accountID,
				id: 'UA-151753095-1',
				name: 'rwh',
			},
			{
				// eslint-disable-next-line sitekit/acronym-case
				accountId: accountID,
				id: 'UA-151753095-1',
				name: 'troubled-tipped.example.com',
			},

		],
		{ accountID }
	);
};

const populateAnalytics4Datastore = ( registry ) => {
	registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetProperties( [
		{
			_id: '1000',
			_accountID: '100',
			name: 'properties/1000',
			createTime: '2014-10-02T15:01:23Z',
			updateTime: '2014-10-02T15:01:23Z',
			parent: 'accounts/100',
			displayName: 'Test GA4 Property',
			industryCategory: 'TECHNOLOGY',
			timeZone: 'America/Los_Angeles',
			currencyCode: 'USD',
			deleted: false,
		},
	],
	{ accountID },
	);
	registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetWebDataStreams(
		[
			{
				_id: '2000',
				_propertyID: '1000',
				name: 'properties/1000/webDataStreams/2000',
				// eslint-disable-next-line sitekit/acronym-case
				measurementId: '1A2BCD345E',
				// eslint-disable-next-line sitekit/acronym-case
				firebaseAppId: '',
				createTime: '2014-10-02T15:01:23Z',
				updateTime: '2014-10-02T15:01:23Z',
				defaultUri: 'http://example.com',
				displayName: 'Test GA4 WebDataStream',
			},
		],
		{ propertyID: '12345' }
	);
};

describe( 'modules/analytics setup-flow', () => {
	let registry;

	beforeAll( () => {
		API.setUsingCache( false );
	} );

	afterAll( () => {
		API.setUsingCache( true );
	} );

	afterEach( () => {
		unsubscribeFromAll( registry );
	} );

	describe( 'selectors', () => {
		describe( 'getSetupFlowMode', () => {
			it( 'returns "legacy" if the modules/analytics-4 store is not available ', async () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				// just for this test create new registry with only this module
				const newRegistry = createRegistry();

				modulesAnalytics.registerStore( newRegistry );

				registry = newRegistry;
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( 'legacy' );
			} );

			it( 'should return "legacy" if isAdminAPIWorking() returns false', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				registry.dispatch( MODULES_ANALYTICS_4 ).receiveError(
					new Error( 'foo' ), 'getProperties', [ 'foo', 'bar' ]
				);

				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( false );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( 'legacy' );
			} );

			it( 'should return undefined if isAdminAPIWorking() returns undefined ', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( undefined );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( undefined );
			} );

			it( 'should return undefined if settings are still loading', () => {
				fetchMock.get(
					/^\/google-site-kit\/v1\/modules\/analytics\/data\/settings/,
					{
						body: {
							accountID: 'pub-12345678',
							clientID: 'ca-pub-12345678',
							useSnippet: true,
						},
						status: 200,
					}
				);

				registry = createTestRegistry();

				populateAnalytics4Datastore( registry );
				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( true );

				expect( registry.select( MODULES_ANALYTICS ).getSettings() ).toBe( undefined );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( undefined );
			} );

			it( 'should return "ua" if there is no account selected', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				expect( registry.select( MODULES_ANALYTICS ).getAccountID( accountID ) ).toBe( undefined );

				populateAnalytics4Datastore( registry );
				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( true );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( 'ua' );
			} );

			it( 'should return undefined if selected account returns undefined from GA4 getProperties selector', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				registry.dispatch( MODULES_ANALYTICS ).setAccountID( accountID );
				populateAnalyticsDatastore( registry );

				expect( registry.select( MODULES_ANALYTICS ).getProperties() ).toBe( undefined );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( undefined );
			} );

			it( 'should return "ua" if selected account returns an empty array from GA4 getProperties selector', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				registry.dispatch( MODULES_ANALYTICS ).setAccountID( accountID );
				populateAnalyticsDatastore( registry );

				// For isAdminAPIWorking() to return true:
				// "The state['properties'] object has at least one account with a non-empty array of properties;"
				// See: https://github.com/google/site-kit-wp/issues/3169
				registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetProperties( [
					{
						_id: '1000',
						_accountID: '100',
						name: 'properties/1000',
						createTime: '2014-10-02T15:01:23Z',
						updateTime: '2014-10-02T15:01:23Z',
						parent: 'accounts/100',
						displayName: 'Test GA4 Property',
						industryCategory: 'TECHNOLOGY',
						timeZone: 'America/Los_Angeles',
						currencyCode: 'USD',
						deleted: false,
					},
				],
				// This is a different accountID
				{ accountID: 'bar-1234567' },
				);

				//  Receive empty properties list to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetProperties(
					[],
					{ accountID },
				);
				registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetWebDataStreams(
					[
						{
							_id: '2000',
							_propertyID: '1000',
							name: 'properties/1000/webDataStreams/2000',
							// eslint-disable-next-line sitekit/acronym-case
							measurementId: '1A2BCD345E',
							// eslint-disable-next-line sitekit/acronym-case
							firebaseAppId: '',
							createTime: '2014-10-02T15:01:23Z',
							updateTime: '2014-10-02T15:01:23Z',
							defaultUri: 'http://example.com',
							displayName: 'Test GA4 WebDataStream',
						},
					],
					{ propertyID: '12345' }
				);

				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( true );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( 'ua' );
			} );

			it( 'should return undefined if selected account returns undefined from UA getProperties selector', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				registry.dispatch( MODULES_ANALYTICS ).setAccountID( accountID );

				populateAnalytics4Datastore( registry );

				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( true );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( undefined );
			} );

			it( 'should return "ga4" if selected account returns an empty array from UA getProperties selector', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				registry.dispatch( MODULES_ANALYTICS ).setAccountID( accountID );
				populateAnalytics4Datastore( registry );

				registry.dispatch( MODULES_ANALYTICS ).receiveGetProperties(
					[],
					{ accountID },
				);

				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( true );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( 'ga4' );
			} );

			it( 'should return "ga4-transitional" if both GA4 and UA properties are found for an account', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				registry.dispatch( MODULES_ANALYTICS ).setAccountID( accountID );
				populateAnalytics4Datastore( registry );
				populateAnalyticsDatastore( registry );

				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( true );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( 'ga4-transitional' );
			} );
		} );
	} );
} );
<commit_msg>Use constants in test.<commit_after>/**
 * `modules/analytics` data store: setup-flow tests.
 *
 * 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.
 */

/**
 * WordPress dependencies
 */
import { createRegistry } from '@wordpress/data';

/**
 * Internal dependencies
 */
import API from 'googlesitekit-api';
import {
	MODULES_ANALYTICS,
	SETUP_FLOW_MODE_LEGACY,
	SETUP_FLOW_MODE_UA,
	SETUP_FLOW_MODE_GA4,
	SETUP_FLOW_MODE_GA4_TRANSITIONAL,
} from './constants';
import { createTestRegistry, unsubscribeFromAll } from 'tests/js/utils';
import * as modulesAnalytics from '../';
import { MODULES_ANALYTICS_4 } from '../../analytics-4/datastore/constants';

const accountID = 'pub-12345678';

const populateAnalyticsDatastore = ( registry ) => {
	registry.dispatch( MODULES_ANALYTICS ).receiveGetProperties(
		[
			{
				// eslint-disable-next-line sitekit/acronym-case
				accountId: accountID,
				id: 'UA-151753095-1',
				name: 'rwh',
			},
			{
				// eslint-disable-next-line sitekit/acronym-case
				accountId: accountID,
				id: 'UA-151753095-1',
				name: 'troubled-tipped.example.com',
			},

		],
		{ accountID }
	);
};

const populateAnalytics4Datastore = ( registry ) => {
	registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetProperties( [
		{
			_id: '1000',
			_accountID: '100',
			name: 'properties/1000',
			createTime: '2014-10-02T15:01:23Z',
			updateTime: '2014-10-02T15:01:23Z',
			parent: 'accounts/100',
			displayName: 'Test GA4 Property',
			industryCategory: 'TECHNOLOGY',
			timeZone: 'America/Los_Angeles',
			currencyCode: 'USD',
			deleted: false,
		},
	],
	{ accountID },
	);
	registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetWebDataStreams(
		[
			{
				_id: '2000',
				_propertyID: '1000',
				name: 'properties/1000/webDataStreams/2000',
				// eslint-disable-next-line sitekit/acronym-case
				measurementId: '1A2BCD345E',
				// eslint-disable-next-line sitekit/acronym-case
				firebaseAppId: '',
				createTime: '2014-10-02T15:01:23Z',
				updateTime: '2014-10-02T15:01:23Z',
				defaultUri: 'http://example.com',
				displayName: 'Test GA4 WebDataStream',
			},
		],
		{ propertyID: '12345' }
	);
};

describe( 'modules/analytics setup-flow', () => {
	let registry;

	beforeAll( () => {
		API.setUsingCache( false );
	} );

	afterAll( () => {
		API.setUsingCache( true );
	} );

	afterEach( () => {
		unsubscribeFromAll( registry );
	} );

	describe( 'selectors', () => {
		describe( 'getSetupFlowMode', () => {
			it( 'returns "legacy" if the modules/analytics-4 store is not available ', async () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				// just for this test create new registry with only this module
				const newRegistry = createRegistry();

				modulesAnalytics.registerStore( newRegistry );

				registry = newRegistry;
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( SETUP_FLOW_MODE_LEGACY );
			} );

			it( 'should return "legacy" if isAdminAPIWorking() returns false', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				registry.dispatch( MODULES_ANALYTICS_4 ).receiveError(
					new Error( 'foo' ), 'getProperties', [ 'foo', 'bar' ]
				);

				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( false );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( SETUP_FLOW_MODE_LEGACY );
			} );

			it( 'should return undefined if isAdminAPIWorking() returns undefined ', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( undefined );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( undefined );
			} );

			it( 'should return undefined if settings are still loading', () => {
				fetchMock.get(
					/^\/google-site-kit\/v1\/modules\/analytics\/data\/settings/,
					{
						body: {
							accountID: 'pub-12345678',
							clientID: 'ca-pub-12345678',
							useSnippet: true,
						},
						status: 200,
					}
				);

				registry = createTestRegistry();

				populateAnalytics4Datastore( registry );
				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( true );

				expect( registry.select( MODULES_ANALYTICS ).getSettings() ).toBe( undefined );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( undefined );
			} );

			it( 'should return "ua" if there is no account selected', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				expect( registry.select( MODULES_ANALYTICS ).getAccountID( accountID ) ).toBe( undefined );

				populateAnalytics4Datastore( registry );
				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( true );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( SETUP_FLOW_MODE_UA );
			} );

			it( 'should return undefined if selected account returns undefined from GA4 getProperties selector', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				registry.dispatch( MODULES_ANALYTICS ).setAccountID( accountID );
				populateAnalyticsDatastore( registry );

				expect( registry.select( MODULES_ANALYTICS ).getProperties() ).toBe( undefined );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( undefined );
			} );

			it( 'should return "ua" if selected account returns an empty array from GA4 getProperties selector', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				registry.dispatch( MODULES_ANALYTICS ).setAccountID( accountID );
				populateAnalyticsDatastore( registry );

				// For isAdminAPIWorking() to return true:
				// "The state['properties'] object has at least one account with a non-empty array of properties;"
				// See: https://github.com/google/site-kit-wp/issues/3169
				registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetProperties( [
					{
						_id: '1000',
						_accountID: '100',
						name: 'properties/1000',
						createTime: '2014-10-02T15:01:23Z',
						updateTime: '2014-10-02T15:01:23Z',
						parent: 'accounts/100',
						displayName: 'Test GA4 Property',
						industryCategory: 'TECHNOLOGY',
						timeZone: 'America/Los_Angeles',
						currencyCode: 'USD',
						deleted: false,
					},
				],
				// This is a different accountID
				{ accountID: 'bar-1234567' },
				);

				//  Receive empty properties list to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetProperties(
					[],
					{ accountID },
				);
				registry.dispatch( MODULES_ANALYTICS_4 ).receiveGetWebDataStreams(
					[
						{
							_id: '2000',
							_propertyID: '1000',
							name: 'properties/1000/webDataStreams/2000',
							// eslint-disable-next-line sitekit/acronym-case
							measurementId: '1A2BCD345E',
							// eslint-disable-next-line sitekit/acronym-case
							firebaseAppId: '',
							createTime: '2014-10-02T15:01:23Z',
							updateTime: '2014-10-02T15:01:23Z',
							defaultUri: 'http://example.com',
							displayName: 'Test GA4 WebDataStream',
						},
					],
					{ propertyID: '12345' }
				);

				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( true );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( SETUP_FLOW_MODE_UA );
			} );

			it( 'should return undefined if selected account returns undefined from UA getProperties selector', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				registry.dispatch( MODULES_ANALYTICS ).setAccountID( accountID );

				populateAnalytics4Datastore( registry );

				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( true );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( undefined );
			} );

			it( 'should return "ga4" if selected account returns an empty array from UA getProperties selector', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				registry.dispatch( MODULES_ANALYTICS ).setAccountID( accountID );
				populateAnalytics4Datastore( registry );

				registry.dispatch( MODULES_ANALYTICS ).receiveGetProperties(
					[],
					{ accountID },
				);

				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( true );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( SETUP_FLOW_MODE_GA4 );
			} );

			it( 'should return "ga4-transitional" if both GA4 and UA properties are found for an account', () => {
				registry = createTestRegistry();
				// Receive empty settings to prevent unexpected fetch by resolver.
				registry.dispatch( MODULES_ANALYTICS ).receiveGetSettings( {} );
				registry.dispatch( MODULES_ANALYTICS ).setAccountID( accountID );
				populateAnalytics4Datastore( registry );
				populateAnalyticsDatastore( registry );

				expect( registry.select( MODULES_ANALYTICS_4 ).isAdminAPIWorking() ).toBe( true );

				expect( registry.select( MODULES_ANALYTICS ).getSetupFlowMode() ).toBe( SETUP_FLOW_MODE_GA4_TRANSITIONAL );
			} );
		} );
	} );
} );
<|endoftext|>


<commit_before>"use strict";

let _              = require('underscore'),
    Node           = require('../node/node.js'),
    logger         = require('../log_assistant.js').getLogger(),
    TestRoleStep   = require('./test_role_step.js'),
    ScreenshotKeys = require('../enum/screenshot_keys.js');

class TestRoleStepNode extends TestRoleStep {
  /**
   * Initialize the test step
   */
  init () {
    logger.debug('TestRoleStepNode.init:', this.index, this.record._id);
    let self = this;

    // Call the parent constructor
    super.init();

    // Load the node for this step
    self.node = new Node(self.record.data.nodeId, self.record.projectId, self.record.projectVersionId, self.testRole.serverLink);
    self.node.init();
  }

  /**
   * Validate the node
   */
  doStep () {
    logger.debug('TestRoleStepNode.doStep:', this.index, this.record._id);
    let self   = this,
        driver = self.testRole.driver,
        result = {
          ready: {},
          valid: {}
        };

    // Update the context
    self.context.update({ nodeId: self.record.data.node.staticId });
    self.context.milestone({ type: "node", data: self.record.data.node });

    // If this is the first step then we need to bootstrap the browser to the url
    if (self.index == 0) {
      logger.debug("Step 0, navigating to starting point:", {
        serverUrl: self.testRole.testServer && self.testRole.testServer.url,
        nodeUrl  : self.node.record && self.node.record.url
      });
      driver.url(driver.buildUrl(self.testRole.testServer.url, self.node.record.url));
      driver.wait(1000);
    }

    // Inject the helpers
    driver.injectHelpers();

    // save a screenshot of the page
    self.serverLink.saveImage(driver.getScreenshot(), ScreenshotKeys.loading);

    // Wait for the node to be ready
    logger.debug("TestRoleStepNode.doStep waiting for node to be ready", self.dataContext);
    _.keys(self.dataContext).forEach((key) => {
      self.node.addVariable(key, self.dataContext[ key ]);
    });
    result.ready = self.node.checkReady(driver, self.dataContext);
    driver.getClientLogs();

    // Screenshot of the page ready
    if (result.ready.pass) {
      self.serverLink.saveImage(driver.getScreenshot(), ScreenshotKeys.afterLoad);
    } else {
      self.serverLink.saveImage(driver.getScreenshot(), ScreenshotKeys.error);
    }

    // Check that the node is valid
    if (result.ready.pass) {
      logger.debug("TestRoleStepNode.doStep validating node");
      result.valid = self.node.validate(driver, self.dataContext);
      driver.getClientLogs();
    }

    // Store the ready checks
    self.serverLink.saveTestResultStepChecks(self.record._id, [{
      node: self.node.record,
      ready: result.ready.checks,
      validation: result.valid.checks
    }]);

    logger.debug("TestRoleStepNode.doStep complete:", result.ready.pass === true && result.valid.pass === true, result);
    self.context.update({ pass: result.ready.pass === true && result.valid.pass === true });
    return result.ready.pass === true && result.valid.pass === true
  }
}

module.exports = TestRoleStepNode;<commit_msg>Updated TestRoleStepNode to use a static method for better code reuse<commit_after>"use strict";

let _              = require('underscore'),
    Node           = require('../node/node.js'),
    logger         = require('../log_assistant.js').getLogger(),
    TestRoleStep   = require('./test_role_step.js'),
    ScreenshotKeys = require('../enum/screenshot_keys.js');

class TestRoleStepNode extends TestRoleStep {
  /**
   * Initialize the test step
   */
  init () {
    logger.debug('TestRoleStepNode.init:', this.index, this.record._id);
    let self = this;

    // Call the parent constructor
    super.init();

    // Load the node for this step
    self.node = new Node(self.record.data.nodeId, self.record.projectId, self.record.projectVersionId, self.testRole.serverLink);
    self.node.init();
  }

  /**
   * Validate the node
   */
  doStep () {
    logger.debug('TestRoleStepNode.doStep:', this.index, this.record._id);
    let self   = this,
        result, pass;

    // Validate the node
    result = TestRoleStepNode.doNodeStep(self, self.node);

    // Store the ready checks
    self.serverLink.saveTestResultStepChecks(self.record._id, [{
      node: self.node.record,
      ready: result.ready.checks,
      validation: result.valid.checks
    }]);

    pass = result.ready.pass === true && result.valid.pass === true;
    logger.debug("TestRoleStepAction.doStep complete: ready=", result.ready.pass === true, 'valid=', result.valid.pass === true, 'result=', result);
    self.context.update({ pass: pass });

    if(!pass){
      if(result.ready.error){
        throw result.ready.error
      } else if(result.valid.error){
        throw result.valid.error
      } else {
        throw new Error('Action failed to reach destination');
      }
    }
  }

  /**
   * Static method so that logic can be shared with navigate steps
   * @param {TestRoleStep} step
   * @param {Node} node The node object
   */
  static doNodeStep(step, node){
    logger.debug('TestRoleStepNode.doNodeStep:', step.index, node.record._id);
    let driver = step.testRole.driver,
        result = {
          ready: {},
          valid: {}
        };

    // Update the context
    step.context.update({ nodeId: node.record.staticId });
    step.context.milestone({ type: "node", data: node.record });

    // If this is the first step then we need to bootstrap the browser to the url
    if (step.index === 0 && !step.navigated) {
      logger.debug("Step 0, navigating to starting point:", {
        serverUrl: step.testRole.testServer && step.testRole.testServer.url,
        nodeUrl  : node.record.url
      });
      driver.url(driver.buildUrl(step.testRole.testServer.url, node.record.url));
      step.navigated = true;
    }

    // save a screenshot of the page
    step.serverLink.saveImage(driver.getScreenshot(), ScreenshotKeys.loading);

    // Wait for the node to be ready
    logger.debug("TestRoleStepNode.doStep waiting for node to be ready", step.dataContext);
    _.keys(step.dataContext).forEach((key) => {
      node.addVariable(key, step.dataContext[ key ]);
    });
    result.ready = node.checkReady(driver, step.dataContext);
    driver.getClientLogs();

    // Screenshot of the page ready
    if (result.ready.pass) {
      step.serverLink.saveImage(driver.getScreenshot(), ScreenshotKeys.afterLoad);
    } else {
      step.serverLink.saveImage(driver.getScreenshot(), ScreenshotKeys.error);
    }

    // Check that the node is valid
    if (result.ready.pass) {
      logger.debug("TestRoleStepNode.doStep validating node");
      result.valid = node.validate(driver, step.dataContext);
      driver.getClientLogs();
    }

    return result;
  }
}

module.exports = TestRoleStepNode;<|endoftext|>


<commit_before>/**
 * Backbone-tastypie.js 0.2.0
 * (c) 2011 Paul Uithol
 *
 * Backbone-tastypie may be freely distributed under the MIT license.
 * Add or override Backbone.js functionality, for compatibility with django-tastypie.
 * Depends on Backbone (and thus on Underscore as well): https://github.com/documentcloud/backbone.
 */
( function( root, factory ) {
	// Set up Backbone-relational for the environment. Start with AMD.
	if ( typeof define === 'function' && define.amd ) {
		define( [ 'exports', 'backbone', 'underscore' ], factory );
	}
	// Next for Node.js or CommonJS.
	else if ( typeof exports !== 'undefined' ) {
		factory( exports, require( 'backbone' ), require( 'underscore' ) );
	}
	// Finally, as a browser global. Use `root` here as it references `window`.
	else {
		factory( root, root.Backbone, root._ );
	}
}( this, function( exports, Backbone, _ ) {
	"use strict";

	Backbone.Tastypie = {
		apiKey: {
			username: '',
			key: ''
		},
		constructSetUrl: function( ids ) {
			return 'set/' + ids.join( ';' ) + '/';
		},
		csrfToken: '',
		defaultOptions: {},
		doGetOnEmptyPostResponse: true,
		doGetOnEmptyPutResponse: false,
		idAttribute: 'resource_uri'
	};

	Backbone.Model.prototype.idAttribute = Backbone.Tastypie.idAttribute;

	/**
	 * Override Backbone's sync function, to do a GET upon receiving a HTTP CREATED.
	 * This requires 2 requests to do a create, so you may want to use some other method in production.
	 * Modified from http://joshbohde.com/blog/backbonejs-and-django
	 */
	Backbone.oldSync = Backbone.sync;
	Backbone.sync = function( method, model, options ) {
		var headers = {},
			options = _.defaults( options || {}, Backbone.Tastypie.defaultOptions );

		if ( Backbone.Tastypie.apiKey && Backbone.Tastypie.apiKey.username ) {
			headers[ 'Authorization' ] = 'ApiKey ' + Backbone.Tastypie.apiKey.username + ':' + Backbone.Tastypie.apiKey.key;
		}

		if ( Backbone.Tastypie.csrfToken ) {
			headers[ 'X-CSRFToken' ] = Backbone.Tastypie.csrfToken;
		}

		// Keep `headers` for a potential second request
		headers = _.extend( headers, options.headers );
		options.headers = headers;

		if ( ( method === 'create' && Backbone.Tastypie.doGetOnEmptyPostResponse ) ||
			( method === 'update' && Backbone.Tastypie.doGetOnEmptyPutResponse ) ) {
			var dfd = new $.Deferred();

			// Set up 'success' handling
			var success = options.success;
			dfd.done( function( resp, textStatus, xhr ) {
				_.isFunction( success ) && success( resp );
			});

			options.success = function( resp, textStatus, xhr ) {
				// If create is successful but doesn't return a response, fire an extra GET.
				// Otherwise, resolve the deferred (which triggers the original 'success' callbacks).
				if ( !resp && ( xhr.status === 201 || xhr.status === 202 || xhr.status === 204 ) ) { // 201 CREATED, 202 ACCEPTED or 204 NO CONTENT; response null or empty.
					options = _.defaults( {
							url: xhr.getResponseHeader( 'Location' ) || model.url(),
							headers: headers,
							success: dfd.resolve,
							error: dfd.reject
						},
						Backbone.Tastypie.defaultOptions
					);
					return Backbone.ajax( options );
				}
				else {
					return dfd.resolveWith( options.context || options, [ resp, textStatus, xhr ] );
				}
			};

			// Set up 'error' handling
			var error = options.error;
			dfd.fail( function( xhr, textStatus, errorThrown ) {
				_.isFunction( error ) && error( xhr.responseText );
			});

			options.error = function( xhr, textStatus, errorText ) {
				dfd.rejectWith( options.context || options, [ xhr, textStatus, xhr.responseText ] );
			};

			// Create the request, and make it accessibly by assigning it to the 'request' property on the deferred
			dfd.request = Backbone.oldSync( method, model, options );
			return dfd;
		}

		return Backbone.oldSync( method, model, options );
	};

	Backbone.Model.prototype.url = function() {
		// Use the 'resource_uri' if possible
		var url = this.get( 'resource_uri' );

		// If there's no idAttribute, use the 'urlRoot'. Fallback to try to have the collection construct a url.
		// Explicitly add the 'id' attribute if the model has one.
		if ( !url ) {
			url = _.result( this, 'urlRoot' ) || ( this.collection && _.result( this.collection, 'url' ) );

			if ( url && this.has( 'id' ) ) {
				url = addSlash( url ) + this.get( 'id' );
			}
		}

		url = url && addSlash( url );

		return url || null;
	};

	/**
	 * Return the first entry in 'data.objects' if it exists and is an array, or else just plain 'data'.
	 *
	 * @param {object} data
	 */
	Backbone.Model.prototype.parse = function( data ) {
		return data && data.objects && ( _.isArray( data.objects ) ? data.objects[ 0 ] : data.objects ) || data;
	};

	/**
	 * Return 'data.objects' if it exists.
	 * If present, the 'data.meta' object is assigned to the 'collection.meta' var.
	 *
	 * @param {object} data
	 */
	Backbone.Collection.prototype.parse = function( data ) {
		if ( data && data.meta ) {
			this.meta = data.meta;
		}

		return data && data.objects || data;
	};

	/**
	 * Construct a url for the collection, or for a set of models in the collection if the `models` param is used.
	 * Will attempt to use its own `urlRoot` first; if that doesn't yield a result, attempts to use the `urlRoot`
	 * on models in the collection.
	 *
	 * @param {Backbone.Model[]} [models]
	 */
	Backbone.Collection.prototype.url = function( models ) {
		var url = _.result( this, 'urlRoot' );

		// If the collection doesn't specify an url, try to obtain one from a model in the collection
		if ( !url ) {
			var model = this.models.length && this.models[ 0 ];
			url = model && _.result( model, 'urlRoot' );
		}

		if ( !url ) {
			url = _.result( this.model.prototype, 'urlRoot' );
		}

		url = url && addSlash( url );

		// Build a url to retrieve a set of models. This assume the last part of each model's idAttribute
		// (set to 'resource_uri') contains the model's id.
		if ( models && models.length ) {
			var ids = _.map( models, function( model ) {
				var parts = _.compact( model.url().split( '/' ) );
				return parts[ parts.length - 1 ];
			});
			url += Backbone.Tastypie.constructSetUrl( ids );
		}

		return url || null;
	};

	var addSlash = function( str ) {
		return str + ( ( str.length > 0 && str.charAt( str.length - 1 ) === '/' ) ? '' : '/' );
	};
}));
<commit_msg>Change lazy loading url building so we can build urls directly from strings.<commit_after>/**
 * Backbone-tastypie.js 0.2.0
 * (c) 2011 Paul Uithol
 *
 * Backbone-tastypie may be freely distributed under the MIT license.
 * Add or override Backbone.js functionality, for compatibility with django-tastypie.
 * Depends on Backbone (and thus on Underscore as well): https://github.com/documentcloud/backbone.
 */
( function( root, factory ) {
	// Set up Backbone-relational for the environment. Start with AMD.
	if ( typeof define === 'function' && define.amd ) {
		define( [ 'exports', 'backbone', 'underscore' ], factory );
	}
	// Next for Node.js or CommonJS.
	else if ( typeof exports !== 'undefined' ) {
		factory( exports, require( 'backbone' ), require( 'underscore' ) );
	}
	// Finally, as a browser global. Use `root` here as it references `window`.
	else {
		factory( root, root.Backbone, root._ );
	}
}( this, function( exports, Backbone, _ ) {
	"use strict";

	Backbone.Tastypie = {
		apiKey: {
			username: '',
			key: ''
		},
		constructSetUrl: function( ids ) {
			return 'set/' + ids.join( ';' ) + '/';
		},
		csrfToken: '',
		defaultOptions: {},
		doGetOnEmptyPostResponse: true,
		doGetOnEmptyPutResponse: false,
		idAttribute: 'resource_uri'
	};

	Backbone.Model.prototype.idAttribute = Backbone.Tastypie.idAttribute;

	/**
	 * Override Backbone's sync function, to do a GET upon receiving a HTTP CREATED.
	 * This requires 2 requests to do a create, so you may want to use some other method in production.
	 * Modified from http://joshbohde.com/blog/backbonejs-and-django
	 */
	Backbone.oldSync = Backbone.sync;
	Backbone.sync = function( method, model, options ) {
		var headers = {},
			options = _.defaults( options || {}, Backbone.Tastypie.defaultOptions );

		if ( Backbone.Tastypie.apiKey && Backbone.Tastypie.apiKey.username ) {
			headers[ 'Authorization' ] = 'ApiKey ' + Backbone.Tastypie.apiKey.username + ':' + Backbone.Tastypie.apiKey.key;
		}

		if ( Backbone.Tastypie.csrfToken ) {
			headers[ 'X-CSRFToken' ] = Backbone.Tastypie.csrfToken;
		}

		// Keep `headers` for a potential second request
		headers = _.extend( headers, options.headers );
		options.headers = headers;

		if ( ( method === 'create' && Backbone.Tastypie.doGetOnEmptyPostResponse ) ||
			( method === 'update' && Backbone.Tastypie.doGetOnEmptyPutResponse ) ) {
			var dfd = new $.Deferred();

			// Set up 'success' handling
			var success = options.success;
			dfd.done( function( resp, textStatus, xhr ) {
				_.isFunction( success ) && success( resp );
			});

			options.success = function( resp, textStatus, xhr ) {
				// If create is successful but doesn't return a response, fire an extra GET.
				// Otherwise, resolve the deferred (which triggers the original 'success' callbacks).
				if ( !resp && ( xhr.status === 201 || xhr.status === 202 || xhr.status === 204 ) ) { // 201 CREATED, 202 ACCEPTED or 204 NO CONTENT; response null or empty.
					options = _.defaults( {
							url: xhr.getResponseHeader( 'Location' ) || model.url(),
							headers: headers,
							success: dfd.resolve,
							error: dfd.reject
						},
						Backbone.Tastypie.defaultOptions
					);
					return Backbone.ajax( options );
				}
				else {
					return dfd.resolveWith( options.context || options, [ resp, textStatus, xhr ] );
				}
			};

			// Set up 'error' handling
			var error = options.error;
			dfd.fail( function( xhr, textStatus, errorThrown ) {
				_.isFunction( error ) && error( xhr.responseText );
			});

			options.error = function( xhr, textStatus, errorText ) {
				dfd.rejectWith( options.context || options, [ xhr, textStatus, xhr.responseText ] );
			};

			// Create the request, and make it accessibly by assigning it to the 'request' property on the deferred
			dfd.request = Backbone.oldSync( method, model, options );
			return dfd;
		}

		return Backbone.oldSync( method, model, options );
	};

	Backbone.Model.prototype.url = function() {
		// Use the 'resource_uri' if possible
		var url = this.get( 'resource_uri' );

		// If there's no idAttribute, use the 'urlRoot'. Fallback to try to have the collection construct a url.
		// Explicitly add the 'id' attribute if the model has one.
		if ( !url ) {
			url = _.result( this, 'urlRoot' ) || ( this.collection && _.result( this.collection, 'url' ) );

			if ( url && this.has( 'id' ) ) {
				url = addSlash( url ) + this.get( 'id' );
			}
		}

		url = url && addSlash( url );

		return url || null;
	};

	/**
	 * Return the first entry in 'data.objects' if it exists and is an array, or else just plain 'data'.
	 *
	 * @param {object} data
	 */
	Backbone.Model.prototype.parse = function( data ) {
		return data && data.objects && ( _.isArray( data.objects ) ? data.objects[ 0 ] : data.objects ) || data;
	};

	/**
	 * Return 'data.objects' if it exists.
	 * If present, the 'data.meta' object is assigned to the 'collection.meta' var.
	 *
	 * @param {object} data
	 */
	Backbone.Collection.prototype.parse = function( data ) {
		if ( data && data.meta ) {
			this.meta = data.meta;
		}

		return data && data.objects || data;
	};

	/**
	 * Construct a url for the collection, or for a set of models in the collection if the `models` param is used.
	 * Will attempt to use its own `urlRoot` first; if that doesn't yield a result, attempts to use the `urlRoot`
	 * on models in the collection.
	 *
	 * @param {Backbone.Model[]|string[]} [models]
	 */
	Backbone.Collection.prototype.url = function( models ) {
		var url = _.result( this, 'urlRoot' );

		// If the collection doesn't specify an url, try to obtain one from a model in the collection
		if ( !url ) {
			var model = this.models.length && this.models[ 0 ];
			url = model && _.result( model, 'urlRoot' );
		}

		if ( !url ) {
			url = _.result( this.model.prototype, 'urlRoot' );
		}

		url = url && addSlash( url );

		// Build a url to retrieve a set of models. This assume the last part of each model's idAttribute contains
		// the model's id. Will work when idAttribute is set to 'resource_uri' (default), but for a plain 'id' as well.
		if ( models && models.length ) {
			var ids = _.map( models, function( model ) {
				var id = model instanceof Backbone.Model ? model.url() : model,
					parts = _.compact( id.split( '/' ) );
				return parts[ parts.length - 1 ];
			});
			url += Backbone.Tastypie.constructSetUrl( ids );
		}

		return url || null;
	};

	var addSlash = function( str ) {
		return str + ( ( str.length > 0 && str.charAt( str.length - 1 ) === '/' ) ? '' : '/' );
	};
}));
<|endoftext|>


<commit_before>/**
 * Backbone-tastypie.js 0.1
 * (c) 2011 Paul Uithol
 *
 * Backbone-tastypie may be freely distributed under the MIT license.
 * Add or override Backbone.js functionality, for compatibility with django-tastypie.
 */
(function( undefined ) {
	"use strict";

	// Backbone.noConflict support. Save local copy of Backbone object.
	var Backbone;

	// CommonJS shim
	if ( typeof window === 'undefined' ) {
		Backbone = require( 'backbone' );
	}
	else {
		Backbone = window.Backbone;
	}

	Backbone.Tastypie = {
		doGetOnEmptyPostResponse: true,
		doGetOnEmptyPutResponse: false,
		apiKey: {
			username: '',
			key: ''
		},
		csrfToken: ''
	};

	/**
	 * Override Backbone's sync function, to do a GET upon receiving a HTTP CREATED.
	 * This requires 2 requests to do a create, so you may want to use some other method in production.
	 * Modified from http://joshbohde.com/blog/backbonejs-and-django
	 */
	Backbone.oldSync = Backbone.sync;
	Backbone.sync = function( method, model, options ) {
		var headers = '';

		if ( Backbone.Tastypie.apiKey && Backbone.Tastypie.apiKey.username.length ) {
			headers = _.extend( {
				'Authorization': 'ApiKey ' + Backbone.Tastypie.apiKey.username + ':' + Backbone.Tastypie.apiKey.key
			}, options.headers );
			options.headers = headers;
		}

		if ( Backbone.Tastypie.csrfToken && Backbone.Tastypie.csrfToken.length ) {
			headers = _.extend( {
				'X-CSRFToken': Backbone.Tastypie.csrfToken
			}, options.headers );
			options.headers = headers;
		}

		if ( ( method === 'create' && Backbone.Tastypie.doGetOnEmptyPostResponse ) ||
			( method === 'update' && Backbone.Tastypie.doGetOnEmptyPutResponse ) ) {
			var dfd = new $.Deferred();

			// Set up 'success' handling
			dfd.done( options.success );
			options.success = function( resp, status, xhr ) {
				// If create is successful but doesn't return a response, fire an extra GET.
				// Otherwise, resolve the deferred (which triggers the original 'success' callbacks).
				if ( !resp && ( xhr.status === 201 || xhr.status === 202 || xhr.status === 204 ) ) { // 201 CREATED, 202 ACCEPTED or 204 NO CONTENT; response null or empty.
					var location = xhr.getResponseHeader( 'Location' ) || model.id;
					return $.ajax( {
						   url: location,
						   headers: headers,
						   success: dfd.resolve,
						   error: dfd.reject
						});
				}
				else {
					return dfd.resolveWith( options.context || options, [ resp, status, xhr ] );
				}
			};

			// Set up 'error' handling
			dfd.fail( options.error );
			options.error = function( model, xhr, options ) {
				dfd.rejectWith( options.context || options, [ model, xhr, options ] );
			};

			// Make the request, make it accessibly by assigning it to the 'request' property on the deferred
			dfd.request = Backbone.oldSync( method, model, options );
			return dfd;
		}

		return Backbone.oldSync( method, model, options );
	};

	Backbone.Model.prototype.idAttribute = 'resource_uri';

	Backbone.Model.prototype.url = function() {
		// Use the id if possible
		var url = this.id;

		// If there's no idAttribute, use the 'urlRoot'. Fallback to try to have the collection construct a url.
		// Explicitly add the 'id' attribute if the model has one.
		if ( !url ) {
			url = _.isFunction( this.urlRoot ) ? this.urlRoot() : this.urlRoot;
			url = url || this.collection && ( _.isFunction( this.collection.url ) ? this.collection.url() : this.collection.url );

			if ( url && this.has( 'id' ) ) {
				url = addSlash( url ) + this.get( 'id' );
			}
		}

		url = url && addSlash( url );

		return url || null;
	};

	/**
	 * Return the first entry in 'data.objects' if it exists and is an array, or else just plain 'data'.
	 */
	Backbone.Model.prototype.parse = function( data ) {
		return data && data.objects && ( _.isArray( data.objects ) ? data.objects[ 0 ] : data.objects ) || data;
	};

	/**
	 * Return 'data.objects' if it exists.
	 * If present, the 'data.meta' object is assigned to the 'collection.meta' var.
	 */
	Backbone.Collection.prototype.parse = function( data ) {
		if ( data && data.meta ) {
			this.meta = data.meta;
		}

		return data && data.objects;
	};

	Backbone.Collection.prototype.url = function( models ) {
		var url = this.urlRoot || ( models && models.length && models[0].urlRoot );
		url = url && addSlash( url );

		// Build a url to retrieve a set of models. This assume the last part of each model's idAttribute
		// (set to 'resource_uri') contains the model's id.
		if ( models && models.length ) {
			var ids = _.map( models, function( model ) {
					var parts = _.compact( model.id.split( '/' ) );
					return parts[ parts.length - 1 ];
				});
			url += 'set/' + ids.join( ';' ) + '/';
		}

		return url || null;
	};

	var addSlash = function( str ) {
		return str + ( ( str.length > 0 && str.charAt( str.length - 1 ) === '/' ) ? '' : '/' );
	}
})();
<commit_msg>Allow Collection#url use `urlRoot` as a function (both on itself, and on Model)<commit_after>/**
 * Backbone-tastypie.js 0.1
 * (c) 2011 Paul Uithol
 *
 * Backbone-tastypie may be freely distributed under the MIT license.
 * Add or override Backbone.js functionality, for compatibility with django-tastypie.
 */
(function( undefined ) {
	"use strict";

	// Backbone.noConflict support. Save local copy of Backbone object.
	var Backbone;

	// CommonJS shim
	if ( typeof window === 'undefined' ) {
		Backbone = require( 'backbone' );
	}
	else {
		Backbone = window.Backbone;
	}

	Backbone.Tastypie = {
		doGetOnEmptyPostResponse: true,
		doGetOnEmptyPutResponse: false,
		apiKey: {
			username: '',
			key: ''
		},
		csrfToken: ''
	};

	/**
	 * Override Backbone's sync function, to do a GET upon receiving a HTTP CREATED.
	 * This requires 2 requests to do a create, so you may want to use some other method in production.
	 * Modified from http://joshbohde.com/blog/backbonejs-and-django
	 */
	Backbone.oldSync = Backbone.sync;
	Backbone.sync = function( method, model, options ) {
		var headers = '';

		if ( Backbone.Tastypie.apiKey && Backbone.Tastypie.apiKey.username.length ) {
			headers = _.extend( {
				'Authorization': 'ApiKey ' + Backbone.Tastypie.apiKey.username + ':' + Backbone.Tastypie.apiKey.key
			}, options.headers );
			options.headers = headers;
		}

		if ( Backbone.Tastypie.csrfToken && Backbone.Tastypie.csrfToken.length ) {
			headers = _.extend( {
				'X-CSRFToken': Backbone.Tastypie.csrfToken
			}, options.headers );
			options.headers = headers;
		}

		if ( ( method === 'create' && Backbone.Tastypie.doGetOnEmptyPostResponse ) ||
			( method === 'update' && Backbone.Tastypie.doGetOnEmptyPutResponse ) ) {
			var dfd = new $.Deferred();

			// Set up 'success' handling
			dfd.done( options.success );
			options.success = function( resp, status, xhr ) {
				// If create is successful but doesn't return a response, fire an extra GET.
				// Otherwise, resolve the deferred (which triggers the original 'success' callbacks).
				if ( !resp && ( xhr.status === 201 || xhr.status === 202 || xhr.status === 204 ) ) { // 201 CREATED, 202 ACCEPTED or 204 NO CONTENT; response null or empty.
					var location = xhr.getResponseHeader( 'Location' ) || model.id;
					return $.ajax( {
						   url: location,
						   headers: headers,
						   success: dfd.resolve,
						   error: dfd.reject
						});
				}
				else {
					return dfd.resolveWith( options.context || options, [ resp, status, xhr ] );
				}
			};

			// Set up 'error' handling
			dfd.fail( options.error );
			options.error = function( model, xhr, options ) {
				dfd.rejectWith( options.context || options, [ model, xhr, options ] );
			};

			// Make the request, make it accessibly by assigning it to the 'request' property on the deferred
			dfd.request = Backbone.oldSync( method, model, options );
			return dfd;
		}

		return Backbone.oldSync( method, model, options );
	};

	Backbone.Model.prototype.idAttribute = 'resource_uri';

	Backbone.Model.prototype.url = function() {
		// Use the id if possible
		var url = this.id;

		// If there's no idAttribute, use the 'urlRoot'. Fallback to try to have the collection construct a url.
		// Explicitly add the 'id' attribute if the model has one.
		if ( !url ) {
			url = _.isFunction( this.urlRoot ) ? this.urlRoot() : this.urlRoot;
			url = url || this.collection && ( _.isFunction( this.collection.url ) ? this.collection.url() : this.collection.url );

			if ( url && this.has( 'id' ) ) {
				url = addSlash( url ) + this.get( 'id' );
			}
		}

		url = url && addSlash( url );

		return url || null;
	};

	/**
	 * Return the first entry in 'data.objects' if it exists and is an array, or else just plain 'data'.
	 */
	Backbone.Model.prototype.parse = function( data ) {
		return data && data.objects && ( _.isArray( data.objects ) ? data.objects[ 0 ] : data.objects ) || data;
	};

	/**
	 * Return 'data.objects' if it exists.
	 * If present, the 'data.meta' object is assigned to the 'collection.meta' var.
	 */
	Backbone.Collection.prototype.parse = function( data ) {
		if ( data && data.meta ) {
			this.meta = data.meta;
		}

		return data && data.objects;
	};

	Backbone.Collection.prototype.url = function( models ) {
		var url = _.isFunction( this.urlRoot ) ? this.urlRoot() : this.urlRoot;
		// If the collection doesn't specify an url, try to obtain one from a model in the collection
		if ( !url ) {
			model = models && models.length && models[ 0 ];
			url = model && ( _.isFunction( model.urlRoot ) ? model.urlRoot() : model.urlRoot );
		}
		url = url && addSlash( url );

		// Build a url to retrieve a set of models. This assume the last part of each model's idAttribute
		// (set to 'resource_uri') contains the model's id.
		if ( models && models.length ) {
			var ids = _.map( models, function( model ) {
					var parts = _.compact( model.id.split( '/' ) );
					return parts[ parts.length - 1 ];
				});
			url += 'set/' + ids.join( ';' ) + '/';
		}

		return url || null;
	};

	var addSlash = function( str ) {
		return str + ( ( str.length > 0 && str.charAt( str.length - 1 ) === '/' ) ? '' : '/' );
	}
})();
<|endoftext|>


<commit_before>/** @license Hyphenopoly_Loader 0.1(beta) - client side hyphenation for webbrowsers
 *  ©2016  Mathias Nater, Zürich (mathiasnater at gmail dot com)
 *  https://github.com/mnater/Hyphenopoly
 *
 *  Released under the MIT license
 *  http://mnater.github.io/Hyphenopoly/LICENSE.txt
 */
/*jslint browser*/
/*global window, Hyphenopoly*/
(function H9YL() {
    "use strict";
    var d = document;

    function scriptLoader(loadTarget, messageTarget) {
        var loadedScripts = {};
        return function loadScript(path, filename, msg) {
            var script;
            if (!loadedScripts[filename]) {
                script = loadTarget.createElement("script");
                loadedScripts[filename] = true;
                script.src = path + filename;
                script.addEventListener("load", function () {
                    messageTarget.evt(msg);
                });
                loadTarget.head.appendChild(script);
                //loadTarget.getElementsByTagName("head")[0].appendChild(script);
            }
        };
    }

    function makeTests() {
        var results = {
            needsPolyfill: false,
            languages: {}
        };

        var tester = (function () {
            var fakeBody;
            function createTest(lang) {
                if (!fakeBody) {
                    fakeBody = d.createElement("body");
                }
                var testDiv = d.createElement("div");
                var s = testDiv.style;
                testDiv.lang = lang;
                testDiv.id = lang;
                s.visibility = "hidden";
                s.MozHyphens = "auto";
                s["-webkit-hyphens"] = "auto";
                s["-ms-hyphens"] = "auto";
                s.hyphens = "auto";
                s.width = "48px";
                s.fontSize = "12px";
                s.lineHeight = "12px";
                s.border = "none";
                s.padding = "0";
                s.wordWrap = "normal";
                testDiv.appendChild(d.createTextNode(Hyphenopoly.require[lang]));
                fakeBody.appendChild(testDiv);
            }
            function appendTests(target) {
                if (!!fakeBody) {
                    target.appendChild(fakeBody);
                }
            }
            function clearTests() {
                if (!!fakeBody) {
                    fakeBody.parentNode.removeChild(fakeBody);
                }
            }
            return {
                createTest: createTest,
                appendTests: appendTests,
                clearTests: clearTests
            };
        }());

        var myScriptLoader;
        function loadBothScripts(lang) {
            if (!myScriptLoader) {
                myScriptLoader = scriptLoader(document, Hyphenopoly);
            }
            myScriptLoader(Hyphenopoly.paths.maindir, "Hyphenopoly.js", ["void"]);
            myScriptLoader(Hyphenopoly.paths.patterndir, lang + ".js", ["loaded", lang]);
        }

        Object.keys(Hyphenopoly.require).forEach(function (lang) {
            if (Hyphenopoly.require[lang] === "FORCEHYPHENOPOLY") {
                results.needsPolyfill = true;
                results.languages[lang] = "H9Y";
                loadBothScripts(lang);
            } else {
                tester.createTest(lang);
            }
        });
        tester.appendTests(d.documentElement);
        Object.keys(Hyphenopoly.require).forEach(function (lang) {
            if (Hyphenopoly.require[lang] !== "FORCEHYPHENOPOLY") {
                if (d.getElementById(lang).offsetHeight > 12) {
                    results.needsPolyfill = results.needsPolyfill || false;
                    results.languages[lang] = "CSS";
                } else {
                    results.needsPolyfill = true;
                    results.languages[lang] = "H9Y";
                    loadBothScripts(lang);
                }
            }
        });
        tester.clearTests();
        return results;
    }

    function run() {
        var result = makeTests();
        var H = Hyphenopoly;
        if (!H.setup.hasOwnProperty("timeout")) {
            H.setup.timeout = 1000;
        }
        if (!H.setup.hasOwnProperty("onTimeOut")) {
            H.setup.onTimeOut = function () {
                window.console.warn("Hyphenopolys 'flash of unhyphenated content'-prevention timed out after " + H.setup.timeout + "ms");
            };
        }
        if (result.needsPolyfill) {
            d.documentElement.style.visibility = "hidden";

            H.setup.timeOutHandler = window.setTimeout(function () {
                d.documentElement.style.visibility = "visible";
                H.setup.onTimeOut();
            }, H.setup.timeout);

            H.testResults = result;
            H.languages = {};
            H.evtList = [];
            H.evt = function (m) {
                H.evtList.push(m);
            };
            d.addEventListener("DOMContentLoaded", function DCL() {
                H.evt(["DOMContentLoaded"]);
            });

        } else {
            window.Hyphenopoly = null;
        }
    }

    run();

}());<commit_msg>Make it shorter<commit_after>/** @license Hyphenopoly_Loader 0.1(beta) - client side hyphenation for webbrowsers
 *  ©2016  Mathias Nater, Zürich (mathiasnater at gmail dot com)
 *  https://github.com/mnater/Hyphenopoly
 *
 *  Released under the MIT license
 *  http://mnater.github.io/Hyphenopoly/LICENSE.txt
 */
/*jslint browser*/
/*global window, Hyphenopoly*/
(function H9YL() {
    "use strict";
    var d = document;

    function scriptLoader(loadTarget, messageTarget) {
        var loadedScripts = {};
        return function loadScript(path, filename, msg) {
            var script;
            if (!loadedScripts[filename]) {
                script = loadTarget.createElement("script");
                loadedScripts[filename] = true;
                script.src = path + filename;
                script.addEventListener("load", function () {
                    messageTarget.evt(msg);
                });
                loadTarget.head.appendChild(script);
                //loadTarget.getElementsByTagName("head")[0].appendChild(script);
            }
        };
    }

    function makeTests() {
        var results = {
            needsPolyfill: false,
            languages: {}
        };

        var tester = (function () {
            var fakeBody;
            function createTest(lang) {
                if (!fakeBody) {
                    fakeBody = d.createElement("body");
                }
                var testDiv = d.createElement("div");
                testDiv.lang = lang;
                testDiv.id = lang;
                testDiv.style.cssText = "visibility:hidden;-moz-hyphens:auto;-webkit-hyphens:auto;-ms-hyphens:auto;hyphens:auto;width:48px;font-size:12px;line-height:12px;boder:none;padding:0;word-wrap:normal";
                testDiv.appendChild(d.createTextNode(Hyphenopoly.require[lang]));
                fakeBody.appendChild(testDiv);
            }
            function appendTests(target) {
                if (!!fakeBody) {
                    target.appendChild(fakeBody);
                }
            }
            function clearTests() {
                if (!!fakeBody) {
                    fakeBody.parentNode.removeChild(fakeBody);
                }
            }
            return {
                createTest: createTest,
                appendTests: appendTests,
                clearTests: clearTests
            };
        }());

        var myScriptLoader;
        function loadBothScripts(lang) {
            if (!myScriptLoader) {
                myScriptLoader = scriptLoader(document, Hyphenopoly);
            }
            myScriptLoader(Hyphenopoly.paths.maindir, "Hyphenopoly.js", ["void"]);
            myScriptLoader(Hyphenopoly.paths.patterndir, lang + ".js", ["loaded", lang]);
        }

        Object.keys(Hyphenopoly.require).forEach(function (lang) {
            if (Hyphenopoly.require[lang] === "FORCEHYPHENOPOLY") {
                results.needsPolyfill = true;
                results.languages[lang] = "H9Y";
                loadBothScripts(lang);
            } else {
                tester.createTest(lang);
            }
        });
        tester.appendTests(d.documentElement);
        Object.keys(Hyphenopoly.require).forEach(function (lang) {
            if (Hyphenopoly.require[lang] !== "FORCEHYPHENOPOLY") {
                if (d.getElementById(lang).offsetHeight > 12) {
                    results.needsPolyfill = results.needsPolyfill || false;
                    results.languages[lang] = "CSS";
                } else {
                    results.needsPolyfill = true;
                    results.languages[lang] = "H9Y";
                    loadBothScripts(lang);
                }
            }
        });
        tester.clearTests();
        return results;
    }

    function run() {
        var H = Hyphenopoly;
        H.testResults = makeTests();
        if (!H.setup.hasOwnProperty("timeout")) {
            H.setup.timeout = 1000;
        }
        if (!H.setup.hasOwnProperty("onTimeOut")) {
            H.setup.onTimeOut = function () {
                window.console.warn("Hyphenopolys 'flash of unhyphenated content'-prevention timed out after " + H.setup.timeout + "ms");
            };
        }
        if (H.testResults.needsPolyfill) {
            d.documentElement.style.visibility = "hidden";

            H.setup.timeOutHandler = window.setTimeout(function () {
                d.documentElement.style.visibility = "visible";
                H.setup.onTimeOut();
            }, H.setup.timeout);
            H.languages = {};
            H.evtList = [];
            H.evt = function (m) {
                H.evtList.push(m);
            };
            d.addEventListener("DOMContentLoaded", function DCL() {
                H.evt(["DOMContentLoaded"]);
            });

        } else {
            window.Hyphenopoly = null;
        }
    }

    run();

}());<|endoftext|>