<commit_before>describe('The highlight factory', function () {
    var highlight,
        L,
        crsService,
        crsConverter,
        geojson,
        store,
        ACTIONS,
        mockedLeafletMap,
        mockedItems = {
            item_multipolygon: {
                id: 'item_multipolygon',
                geometry: {
                    type: 'MultiPolygon',
                    coordinates: [
                        [[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]],
                        [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]],
                        [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]]
                    ]
                },
                useAutoFocus: true
            },
            item_polygon: {
                id: 'item_polygon',
                geometry: {
                    type: 'Polygon',
                    coordinates: [
                        [
                            [100.0, 0.0], [102.0, 0.0], [102.0, 10.0], [100.0, 10.0], [100.0, 0.0]
                        ]
                    ]
                },
                useAutoFocus: true
            },
            item_point: {
                id: 'item_point',
                geometry: {
                    type: 'Point',
                    coordinates: [100.0, 0.0]
                },
                useAutoFocus: true
            },
            item_marker: {
                id: 'item_marker',
                geometry: {
                    type: 'Point',
                    coordinates: [100.0, 0.0]
                },
                useAutoFocus: false
            },
            item_rotated_marker: {
                id: 'item_rotated_marker',
                geometry: {
                    type: 'Point',
                    coordinates: [100.0, 0.0]
                },
                orientation: 145,
                useAutoFocus: false
            }
        },
        mockedLayer = {
            getBounds: function () {
                return 'FAKE_LAYER_BOUNDS';
            }
        },
        projGeoJsonArguments;

    beforeEach(function () {
        angular.mock.module(
            'dpMap',
            {
                mapConfig: {
                    DEFAULT_ZOOM_HIGHLIGHT: 14
                },
                crsConverter: {
                    rdToWgs84: function (location) {
                        if (location === 'FAKE_POINT_CENTER_RD') {
                            return 'FAKE_POINT_CENTER_WGS84';
                        } else if (location === 'FAKE_POLYGON_CENTER_RD') {
                            return 'FAKE_POLYGON_CENTER_WGS84';
                        } else if (location === 'FAKE_MULTIPOLYGON_CENTER_RD') {
                            return 'FAKE_MULTIPOLYGON_CENTER_WGS84';
                        }
                    }
                },
                geojson: {
                    getCenter: function (geometry) {
                        if (geometry.type === 'Point') {
                            return 'FAKE_POINT_CENTER_RD';
                        } else if (geometry.type === 'Polygon') {
                            return 'FAKE_POLYGON_CENTER_RD';
                        } else if (geometry.type === 'MultiPolygon') {
                            return 'FAKE_MULTIPOLYGON_CENTER_RD';
                        }
                    }
                },
                panning: {
                    getCurrentLocation: function () {
                        return 'FAKE_LOCATION_CENTER';
                    }
                },
                store: {
                    dispatch: function () {}
                }
            },
            function ($provide) {
                $provide.constant('ICON_CONFIG', {
                    item_multipolygon: {
                        foo: 'a'
                    },
                    item_polygon: {
                        foo: 'b'
                    },
                    item_point: {
                        foo: 'b'
                    },
                    item_marker: {
                        foo: 'c'
                    },
                    item_rotated_marker: {
                        foo: 'd'
                    }
                });
            }
        );

        angular.mock.inject(function (_highlight_, _L_, _crsService_, _crsConverter_, _geojson_, _store_, _ACTIONS_) {
            highlight = _highlight_;
            L = _L_;
            crsService = _crsService_;
            crsConverter = _crsConverter_;
            geojson = _geojson_;
            store = _store_;
            ACTIONS = _ACTIONS_;
        });

        mockedLeafletMap = {
            addLayer: function () {},
            removeLayer: function () {},
            fitBounds: function () {},
            getBoundsZoom: function () {},
            getCenter: function () {
                return {
                    lat: 'FAKE_LATITUDE',
                    lng: 'FAKE_LONGITUDE'
                };
            },
            getZoom: function () {}
        };

        spyOn(mockedLeafletMap, 'addLayer');
        spyOn(mockedLeafletMap, 'removeLayer');
        spyOn(mockedLeafletMap, 'fitBounds').and.callThrough();

        L.Proj.geoJson = function () {
            projGeoJsonArguments = arguments;

            return mockedLayer;
        };

        spyOn(L.Proj, 'geoJson').and.callThrough();
        spyOn(L, 'icon').and.returnValue('FAKE_ICON');
        spyOn(L, 'marker');

        spyOn(crsService, 'getRdObject').and.returnValue('FAKE_RD_OBJECT');

        spyOn(crsConverter, 'rdToWgs84').and.callThrough();
        spyOn(geojson, 'getCenter').and.callThrough();

        spyOn(store, 'dispatch');
    });

    afterEach(function () {
        projGeoJsonArguments = undefined;
    });

    it('has an initialize function to set the Leaflet image path for icons to \'assets\'', function () {
        expect(L.Icon.Default.imagePath).not.toBe('assets');

        highlight.initialize();
        expect(L.Icon.Default.imagePath).toBe('assets');
    });

    it('can add a MultiPolygons to the map', function () {
        var item = {
            id: 'item_multipolygon',
            geometry: {
                type: 'MultiPolygon',
                coordinates: [
                    [[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]],
                    [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]],
                    [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]]
                ]
            }
        };

        highlight.addMarker(mockedLeafletMap, item);

        expect(L.Proj.geoJson).toHaveBeenCalledWith(jasmine.objectContaining(item.geometry), jasmine.any(Object));
        expect(mockedLeafletMap.addLayer).toHaveBeenCalledWith(mockedLayer);
    });

    it('has custom styling for MultiPolygons', function () {
        highlight.addMarker(mockedLeafletMap, mockedItems.item_multipolygon);

        // In the real world Leaflet calls the style function
        expect(projGeoJsonArguments[1].style()).toEqual({
            color: 'red',
            fillColor: 'red',
            weight: 2,
            opacity: 1.6,
            fillOpacity: 0.2
        });
    });

    it('can add markers with custom icons to the map', function () {
        var item = {
            id: 'item_marker',
            geometry: {
                type: 'Point',
                coordinates: [100.0, 0.0]
            }
        };

        highlight.addMarker(mockedLeafletMap, mockedItems.item_marker);

        expect(L.Proj.geoJson).toHaveBeenCalledWith(jasmine.objectContaining(item.geometry), jasmine.any(Object));
        projGeoJsonArguments[1].pointToLayer(null, 'FAKE_LATLNG'); // In the real world Leaflet calls this function

        expect(L.icon).toHaveBeenCalledWith({
            foo: 'c'
        });

        expect(L.marker).toHaveBeenCalledWith('FAKE_LATLNG', jasmine.objectContaining({
            icon: 'FAKE_ICON'
        }));

        expect(mockedLeafletMap.addLayer).toHaveBeenCalledWith(mockedLayer);
    });

    it('can add rotated markers to the map', function () {
        highlight.addMarker(mockedLeafletMap, mockedItems.item_rotated_marker);
        projGeoJsonArguments[1].pointToLayer(null, 'FAKE_LATLNG'); // In the real world Leaflet calls this function

        expect(L.marker).toHaveBeenCalledWith('FAKE_LATLNG', {
            icon: 'FAKE_ICON',
            rotationAngle: 145
        });
    });

    it('sets the CRS to RD', function () {
        ['item_multipolygon', 'item_marker', 'item_rotated_marker'].forEach(function (item) {
            highlight.addMarker(mockedLeafletMap, mockedItems[item]);

            expect(L.Proj.geoJson).toHaveBeenCalledWith(
                angular.merge(
                    mockedItems[item].geometry,
                    {
                        crs: 'FAKE_RD_OBJECT'
                    }
                ),
                jasmine.any(Object)
            );
        });
    });

    it('can remove highlighted things from the map', function () {
        ['item_multipolygon', 'item_marker', 'item_rotated_marker'].forEach(function (item) {
            highlight.addMarker(mockedLeafletMap, mockedItems[item]);
            highlight.removeMarker(mockedLeafletMap, mockedItems[item]);

            expect(mockedLeafletMap.removeLayer).toHaveBeenCalledWith(mockedLayer);
        });
    });

    describe('triggers MAP_ZOOM when geometry has been found (center and zoom)', function () {
        it('Points do center automatically but use a default zoom level', function () {
            spyOn(mockedLeafletMap, 'getBoundsZoom').and.returnValue(NaN);
            spyOn(mockedLeafletMap, 'getZoom').and.returnValue(13);

            highlight.addMarker(mockedLeafletMap, mockedItems.item_point);
            expect(mockedLeafletMap.fitBounds).not.toHaveBeenCalled();

            // 14 is the fallback zoom level defined in mapConfig.DEFAULT_ZOOM_HIGHLIGHT
            expect(store.dispatch).toHaveBeenCalledWith({
                type: ACTIONS.MAP_ZOOM,
                payload: {
                    viewCenter: 'FAKE_POINT_CENTER_WGS84',
                    zoom: 14
                }
            });
        });

        it('Points will not zoom out when viewing with a zoom level larger than 14', function () {
            spyOn(mockedLeafletMap, 'getBoundsZoom').and.returnValue(NaN);
            spyOn(mockedLeafletMap, 'getZoom').and.returnValue(15);

            highlight.addMarker(mockedLeafletMap, mockedItems.item_point);
            expect(mockedLeafletMap.fitBounds).not.toHaveBeenCalled();

            // 14 is the fallback zoom level defined in mapConfig.DEFAULT_ZOOM_HIGHLIGHT
            expect(store.dispatch).toHaveBeenCalledWith({
                type: ACTIONS.MAP_ZOOM,
                payload: {
                    viewCenter: 'FAKE_POINT_CENTER_WGS84',
                    zoom: 15
                }
            });
        });

        it('Polygons support autozoom and auto center (without animation)', function () {
            spyOn(mockedLeafletMap, 'getBoundsZoom').and.returnValue(10);

            highlight.addMarker(mockedLeafletMap, mockedItems.item_polygon);

            expect(mockedLeafletMap.fitBounds).toHaveBeenCalledWith('FAKE_LAYER_BOUNDS', {animate: false});
            expect(store.dispatch).toHaveBeenCalledWith({
                type: ACTIONS.MAP_ZOOM,
                payload: {
                    viewCenter: 'FAKE_LOCATION_CENTER',
                    zoom: 10
                }
            });
        });

        it('MultiPolygons support autozoom and auto center (without animation)', function () {
            spyOn(mockedLeafletMap, 'getBoundsZoom').and.returnValue(10);

            highlight.addMarker(mockedLeafletMap, mockedItems.item_multipolygon);

            expect(mockedLeafletMap.fitBounds).toHaveBeenCalledWith('FAKE_LAYER_BOUNDS', {animate: false});
            expect(store.dispatch).toHaveBeenCalledWith({
                type: ACTIONS.MAP_ZOOM,
                payload: {
                    viewCenter: 'FAKE_LOCATION_CENTER',
                    zoom: 10
                }
            });
        });
    });
});
<commit_msg>Unit tests for adding and remove clustered layers.<commit_after>describe('The highlight factory', function () {
    var highlight,
        L,
        crsService,
        crsConverter,
        geojson,
        store,
        ACTIONS,
        mockedLeafletMap,
        mockedItems = {
            item_multipolygon: {
                id: 'item_multipolygon',
                geometry: {
                    type: 'MultiPolygon',
                    coordinates: [
                        [[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]],
                        [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]],
                        [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]]
                    ]
                },
                useAutoFocus: true
            },
            item_polygon: {
                id: 'item_polygon',
                geometry: {
                    type: 'Polygon',
                    coordinates: [
                        [
                            [100.0, 0.0], [102.0, 0.0], [102.0, 10.0], [100.0, 10.0], [100.0, 0.0]
                        ]
                    ]
                },
                useAutoFocus: true
            },
            item_point: {
                id: 'item_point',
                geometry: {
                    type: 'Point',
                    coordinates: [100.0, 0.0]
                },
                useAutoFocus: true
            },
            item_marker: {
                id: 'item_marker',
                geometry: {
                    type: 'Point',
                    coordinates: [100.0, 0.0]
                },
                useAutoFocus: false
            },
            item_rotated_marker: {
                id: 'item_rotated_marker',
                geometry: {
                    type: 'Point',
                    coordinates: [100.0, 0.0]
                },
                orientation: 145,
                useAutoFocus: false
            }
        },
        mockedLayer = {
            getBounds: function () {
                return 'FAKE_LAYER_BOUNDS';
            }
        },
        mockedClusteredLayer = {
            addLayer: angular.noop,
            getBounds: function () {
                return 'FAKE_LAYER_BOUNDS';
            }
        },
        projGeoJsonArguments;

    beforeEach(function () {
        angular.mock.module(
            'dpMap',
            {
                mapConfig: {
                    DEFAULT_ZOOM_HIGHLIGHT: 14
                },
                crsConverter: {
                    rdToWgs84: function (location) {
                        if (location === 'FAKE_POINT_CENTER_RD') {
                            return 'FAKE_POINT_CENTER_WGS84';
                        } else if (location === 'FAKE_POLYGON_CENTER_RD') {
                            return 'FAKE_POLYGON_CENTER_WGS84';
                        } else if (location === 'FAKE_MULTIPOLYGON_CENTER_RD') {
                            return 'FAKE_MULTIPOLYGON_CENTER_WGS84';
                        }
                    }
                },
                geojson: {
                    getCenter: function (geometry) {
                        if (geometry.type === 'Point') {
                            return 'FAKE_POINT_CENTER_RD';
                        } else if (geometry.type === 'Polygon') {
                            return 'FAKE_POLYGON_CENTER_RD';
                        } else if (geometry.type === 'MultiPolygon') {
                            return 'FAKE_MULTIPOLYGON_CENTER_RD';
                        }
                    }
                },
                panning: {
                    getCurrentLocation: function () {
                        return 'FAKE_LOCATION_CENTER';
                    }
                },
                store: {
                    dispatch: angular.noop
                }
            },
            function ($provide) {
                $provide.constant('ICON_CONFIG', {
                    item_multipolygon: {
                        foo: 'a'
                    },
                    item_polygon: {
                        foo: 'b'
                    },
                    item_point: {
                        foo: 'b'
                    },
                    item_marker: {
                        foo: 'c'
                    },
                    item_rotated_marker: {
                        foo: 'd'
                    },
                    detail: {
                        foo: 'e'
                    }
                });

                $provide.constant('CLUSTERED_MARKERS_CONFIG', {
                    looksGoodToMe: true,
                    optimizationLevel: 999
                });
            }
        );

        angular.mock.inject(function (_highlight_, _L_, _crsService_, _crsConverter_, _geojson_, _store_, _ACTIONS_) {
            highlight = _highlight_;
            L = _L_;
            crsService = _crsService_;
            crsConverter = _crsConverter_;
            geojson = _geojson_;
            store = _store_;
            ACTIONS = _ACTIONS_;
        });

        mockedLeafletMap = {
            addLayer: angular.noop,
            removeLayer: angular.noop,
            fitBounds: angular.noop,
            getBoundsZoom: angular.noop,
            getCenter: function () {
                return {
                    lat: 'FAKE_LATITUDE',
                    lng: 'FAKE_LONGITUDE'
                };
            },
            getZoom: angular.noop
        };

        spyOn(mockedLeafletMap, 'addLayer');
        spyOn(mockedLeafletMap, 'removeLayer');
        spyOn(mockedLeafletMap, 'fitBounds').and.callThrough();

        L.Proj.geoJson = function () {
            projGeoJsonArguments = arguments;

            return mockedLayer;
        };

        spyOn(L.Proj, 'geoJson').and.callThrough();
        spyOn(L, 'icon').and.returnValue('FAKE_ICON');
        spyOn(L, 'marker').and.returnValue('FAKE_MARKER');

        spyOn(L, 'markerClusterGroup').and.returnValue(mockedClusteredLayer);
        spyOn(mockedClusteredLayer, 'addLayer');
        spyOn(crsService, 'getRdObject').and.returnValue('FAKE_RD_OBJECT');

        spyOn(crsConverter, 'rdToWgs84').and.callThrough();
        spyOn(geojson, 'getCenter').and.callThrough();

        spyOn(store, 'dispatch');
    });

    afterEach(function () {
        projGeoJsonArguments = undefined;
    });

    it('has an initialize function to set the Leaflet image path for icons to \'assets\'', function () {
        expect(L.Icon.Default.imagePath).not.toBe('assets');

        highlight.initialize();
        expect(L.Icon.Default.imagePath).toBe('assets');
    });

    it('can add a MultiPolygons to the map', function () {
        var item = {
            id: 'item_multipolygon',
            geometry: {
                type: 'MultiPolygon',
                coordinates: [
                    [[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]],
                    [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]],
                    [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]]
                ]
            }
        };

        highlight.addMarker(mockedLeafletMap, item);

        expect(L.Proj.geoJson).toHaveBeenCalledWith(jasmine.objectContaining(item.geometry), jasmine.any(Object));
        expect(mockedLeafletMap.addLayer).toHaveBeenCalledWith(mockedLayer);
    });

    it('has custom styling for MultiPolygons', function () {
        highlight.addMarker(mockedLeafletMap, mockedItems.item_multipolygon);

        // In the real world Leaflet calls the style function
        expect(projGeoJsonArguments[1].style()).toEqual({
            color: 'red',
            fillColor: 'red',
            weight: 2,
            opacity: 1.6,
            fillOpacity: 0.2
        });
    });

    it('can add markers with custom icons to the map', function () {
        var item = {
            id: 'item_marker',
            geometry: {
                type: 'Point',
                coordinates: [100.0, 0.0]
            }
        };

        highlight.addMarker(mockedLeafletMap, mockedItems.item_marker);

        expect(L.Proj.geoJson).toHaveBeenCalledWith(jasmine.objectContaining(item.geometry), jasmine.any(Object));
        projGeoJsonArguments[1].pointToLayer(null, 'FAKE_LATLNG'); // In the real world Leaflet calls this function

        expect(L.icon).toHaveBeenCalledWith({
            foo: 'c'
        });

        expect(L.marker).toHaveBeenCalledWith('FAKE_LATLNG', jasmine.objectContaining({
            icon: 'FAKE_ICON'
        }));

        expect(mockedLeafletMap.addLayer).toHaveBeenCalledWith(mockedLayer);
    });

    it('can add rotated markers to the map', function () {
        highlight.addMarker(mockedLeafletMap, mockedItems.item_rotated_marker);
        projGeoJsonArguments[1].pointToLayer(null, 'FAKE_LATLNG'); // In the real world Leaflet calls this function

        expect(L.marker).toHaveBeenCalledWith('FAKE_LATLNG', {
            icon: 'FAKE_ICON',
            rotationAngle: 145
        });
    });

    it('sets the CRS to RD', function () {
        ['item_multipolygon', 'item_marker', 'item_rotated_marker'].forEach(function (item) {
            highlight.addMarker(mockedLeafletMap, mockedItems[item]);

            expect(L.Proj.geoJson).toHaveBeenCalledWith(
                angular.merge(
                    mockedItems[item].geometry,
                    {
                        crs: 'FAKE_RD_OBJECT'
                    }
                ),
                jasmine.any(Object)
            );
        });
    });

    it('can remove highlighted markers from the map', function () {
        ['item_multipolygon', 'item_marker', 'item_rotated_marker'].forEach(function (item) {
            highlight.addMarker(mockedLeafletMap, mockedItems[item]);
            highlight.removeMarker(mockedLeafletMap, mockedItems[item]);

            expect(mockedLeafletMap.removeLayer).toHaveBeenCalledWith(mockedLayer);
        });
    });

    it('can add clustered markers to the map', function () {
        spyOn(mockedLeafletMap, 'getZoom').and.returnValue(13);

        highlight.addCluster(mockedLeafletMap, [
            [52.1, 4.0],
            [52.2, 4.0],
            [52.3, 4.1]
        ]);

        // Making sure the configuration is used
        expect(L.markerClusterGroup).toHaveBeenCalledWith({
            looksGoodToMe: true,
            optimizationLevel: 999
        });

        // Three icons are created (one for each marker) that share the icon config of a 'detail geometry'
        expect(L.icon).toHaveBeenCalledTimes(3);
        expect(L.icon).toHaveBeenCalledWith({foo: 'e'});

        // Three markers are created with those icons associated to them
        expect(L.marker).toHaveBeenCalledTimes(3);
        expect(L.marker).toHaveBeenCalledWith(
            [52.1, 4.0],
            {
                icon: 'FAKE_ICON'
            }
        );
        expect(L.marker).toHaveBeenCalledWith(
            [52.2, 4.0],
            {
                icon: 'FAKE_ICON'
            }
        );

        expect(L.marker).toHaveBeenCalledWith(
            [52.3, 4.1],
            {
                icon: 'FAKE_ICON'
            }
        );

        // And those three markers are added to a single layer
        expect(mockedClusteredLayer.addLayer).toHaveBeenCalledTimes(3);
        expect(mockedClusteredLayer.addLayer).toHaveBeenCalledWith('FAKE_MARKER');

        // The new clusteredLayer is then added to the map
        expect(mockedLeafletMap.addLayer).toHaveBeenCalledWith(mockedClusteredLayer);
    });

    it('pans and zooms to the clustered markers after adding them to the map', function () {
        spyOn(mockedLeafletMap, 'getZoom').and.returnValue(13);

        highlight.addCluster(mockedLeafletMap, [
            [52.1, 4.0],
            [52.2, 4.0],
            [52.3, 4.1]
        ]);

        expect(store.dispatch).toHaveBeenCalledWith(jasmine.objectContaining({type: ACTIONS.MAP_ZOOM}));
    });

    it('can remove clustered markers from the map', function () {
        spyOn(mockedLeafletMap, 'getZoom').and.returnValue(13);

        // First make sure there is something to delete
        highlight.addCluster(mockedLeafletMap, [
            [52.1, 4.0],
            [52.2, 4.0],
            [52.3, 4.1]
        ]);
        expect(mockedLeafletMap.removeLayer).not.toHaveBeenCalled();

        // Then delete it
        highlight.removeCluster(mockedLeafletMap);
        expect(mockedLeafletMap.removeLayer).toHaveBeenCalledWith(mockedClusteredLayer);
    });

    describe('triggers MAP_ZOOM when geometry has been found (center and zoom)', function () {
        it('Points do center automatically but use a default zoom level', function () {
            spyOn(mockedLeafletMap, 'getBoundsZoom').and.returnValue(NaN);
            spyOn(mockedLeafletMap, 'getZoom').and.returnValue(13);

            highlight.addMarker(mockedLeafletMap, mockedItems.item_point);
            expect(mockedLeafletMap.fitBounds).not.toHaveBeenCalled();

            // 14 is the fallback zoom level defined in mapConfig.DEFAULT_ZOOM_HIGHLIGHT
            expect(store.dispatch).toHaveBeenCalledWith({
                type: ACTIONS.MAP_ZOOM,
                payload: {
                    viewCenter: 'FAKE_POINT_CENTER_WGS84',
                    zoom: 14
                }
            });
        });

        it('Points will not zoom out when viewing with a zoom level larger than 14', function () {
            spyOn(mockedLeafletMap, 'getBoundsZoom').and.returnValue(NaN);
            spyOn(mockedLeafletMap, 'getZoom').and.returnValue(15);

            highlight.addMarker(mockedLeafletMap, mockedItems.item_point);
            expect(mockedLeafletMap.fitBounds).not.toHaveBeenCalled();

            // 14 is the fallback zoom level defined in mapConfig.DEFAULT_ZOOM_HIGHLIGHT
            expect(store.dispatch).toHaveBeenCalledWith({
                type: ACTIONS.MAP_ZOOM,
                payload: {
                    viewCenter: 'FAKE_POINT_CENTER_WGS84',
                    zoom: 15
                }
            });
        });

        it('Polygons support autozoom and auto center (without animation)', function () {
            spyOn(mockedLeafletMap, 'getBoundsZoom').and.returnValue(10);

            highlight.addMarker(mockedLeafletMap, mockedItems.item_polygon);

            expect(mockedLeafletMap.fitBounds).toHaveBeenCalledWith('FAKE_LAYER_BOUNDS', {animate: false});
            expect(store.dispatch).toHaveBeenCalledWith({
                type: ACTIONS.MAP_ZOOM,
                payload: {
                    viewCenter: 'FAKE_LOCATION_CENTER',
                    zoom: 10
                }
            });
        });

        it('MultiPolygons support autozoom and auto center (without animation)', function () {
            spyOn(mockedLeafletMap, 'getBoundsZoom').and.returnValue(10);

            highlight.addMarker(mockedLeafletMap, mockedItems.item_multipolygon);

            expect(mockedLeafletMap.fitBounds).toHaveBeenCalledWith('FAKE_LAYER_BOUNDS', {animate: false});
            expect(store.dispatch).toHaveBeenCalledWith({
                type: ACTIONS.MAP_ZOOM,
                payload: {
                    viewCenter: 'FAKE_LOCATION_CENTER',
                    zoom: 10
                }
            });
        });
    });
});
<|endoftext|>


<commit_before>import {
  props,
  define,
  declarativeClickHandler,
  sanitizeBoltClasses,
  hasNativeShadowDomSupport,
  afterNextRender,
} from '@bolt/core/utils';
import { wire, withHyperHtml } from '@bolt/core/renderers';

import classNames from 'classnames/bind';

import visuallyhiddenUtils from '@bolt/global/styles/07-utilities/_utilities-visuallyhidden.scss';
import styles from './button.scss';

let cx = classNames.bind(styles);

@define
class BoltButton extends withHyperHtml() {
  static is = 'bolt-button';

  static props = {
    color: props.string,
    text: props.string,
    size: props.string,
    rounded: props.boolean,
    iconOnly: props.boolean,
    width: props.string,
    align: props.string,
    transform: props.string,
    disabled: props.boolean,
    target: props.string,
    url: props.string,

    onClick: props.string, // Managed by base class
    onClickTarget: props.string, // Managed by base class
  };

  // https://github.com/WebReflection/document-register-element#upgrading-the-constructor-context
  constructor(self) {
    self = super(self);
    self.useShadow = hasNativeShadowDomSupport;
    return self;
  }

  connecting() {
    const root = this;

    // If the initial <bolt-button> element contains a button or link, break apart the original HTML so we can retain any button or a tags but swap out the inner content with slots.
    this.childNodes.forEach((childElement, i) => {
      if (childElement.tagName === 'BUTTON' || childElement.tagName === 'A') {
        root.rootElement = document.createDocumentFragment();

        // Take any existing buttons and links and move them to the root of the custom element
        while (childElement.firstChild) {
          root.appendChild(childElement.firstChild);
        }

        if (childElement.className) {
          childElement.className = sanitizeBoltClasses(childElement);
        }

        if (
          childElement.getAttribute('is') &&
          childElement.getAttribute('is') === 'shadow-root'
        ) {
          childElement.removeAttribute('is');
        }

        root.rootElement.appendChild(childElement);
      }
    });

    // When possible, use afterNextRender to defer non-critical
    // work until after first paint.
    afterNextRender(this, function() {
      this.addEventListener('click', this.clickHandler);
    });
  }

  rendered() {
    const root = this;
    const slots = this.slots;

    // helper function to let you quickly check if an array of elements is inside a component's
    function containsAny(source, target) {
      const result = source.filter(function(item) {
        return target.indexOf(item) > -1;
      });
      return result.length > 0;
    }

    // Automatically re-render if the component's children get externally modified (ex. a new icon gets injected)
    this.observer = new MutationObserver(mutations => {
      mutations.forEach(function(mutation) {
        if (mutation.removedNodes.length > 0) {
          const itemsRemoved = [].slice.call(mutation.removedNodes); // grab items removed + convert to array

          for (let i = 0; i < slots.length; i++) {
            if (containsAny(slots[slot[i]], itemsRemoved)) {
              for (let j = 0; j < itemsRemoved.length; j++) {
                const itemRemoved = itemsRemoved[j];
                slots[slot] = slots[slot].filter(
                  slottedItem => slottedItem !== itemRemoved,
                );
              }
            }
          }
        } else {
          const itemsAdded = [].slice.call(mutation.addedNodes); // grab items added + convert to array

          for (let i = 0; i < itemsAdded.length; i++) {
            const itemAdded = itemsAdded[i];
            const slotName = itemAdded.getAttribute
              ? itemAdded.getAttribute('slot')
              : null;

            if (!slotName) {
              slots.default.push(itemAdded);
            } else if (slots[slotName]) {
              slots[slotName].push(itemAdded);
            } else {
              slots[slotName] = [];
              slots[slotName].push(itemAdded);
            }
          }
        }

        // re-render if Shadow DOM is supported and enabled; temp workaround to dealing w/ components already rendered, but without slot support
        if (hasNativeShadowDomSupport && root.useShadow) {
          root.triggerUpdate();
        }
      });
    });

    this.observer.observe(this, {
      attributes: false,
      childList: true,
      characterData: false,
    });
  }

  disconnecting() {
    this.removeEventListener('click', this.clickHandler);
    this.observer.disconnect();
  }

  // Attach external events declaratively
  clickHandler(event) {
    declarativeClickHandler(this);
  }

  render() {
    const classes = cx('c-bolt-button', {
      'c-bolt-button--rounded': this.props.rounded,
      'c-bolt-button--disabled': this.props.disabled,
      'c-bolt-button--icon-only': this.props.iconOnly,
      'c-bolt-button--center': !this.props.align, // defautl align prop
      [`c-bolt-button--${this.props.align}`]: this.props.align,
      'c-bolt-button--primary': !this.props.color, // default color prop
      [`c-bolt-button--${this.props.color}`]: this.props.color,
      'c-bolt-button--medium': !this.props.size,
      [`c-bolt-button--${this.props.size}`]: this.props.size,
      [`c-bolt-button--${this.props.width}`]: this.props.width,
      [`c-bolt-button--${this.props.transform}`]: this.props.transform,
    });

    // Decide on if the rendered button tag should be a <button> or  tag, based on if a URL exists OR if a link was passed in from the getgo
    const hasUrl = this.props.url.length > 0 && this.props.url !== 'null';

    // Assign default target attribute value if one isn't specified
    const urlTarget = this.props.target && hasUrl ? this.props.target : '_self';

    // The buttonElement to render, based on the initial HTML passed alone.
    let buttonElement = null;
    const self = this;

    const slotMarkup = name => {
      switch (name) {
        case 'before':
        case 'after':
          const iconClasses = cx('c-bolt-button__icon', {
            'is-empty': name in this.slots === false,
          });

          return wire(this)`
            ${
            name in this.slots
              ? this.slot(name)
              : wire(this)`<slot name="${name}" />`
          }`;
        default:
          const itemClasses = cx('c-bolt-button__item', {
            'is-empty': name in this.slots === false,
            'u-bolt-visuallyhidden': this.props.iconOnly,
          });

          return wire(this)`
            ${
            name in this.slots ? this.slot('default') : wire(this)`<slot/>`
          }`;
      }
    };

    const innerSlots = [
      slotMarkup('before'),
      slotMarkup('default'),
      slotMarkup('after'),
    ];

    function renderInnerSlots(elementToAppendTo) {
      // hyperhtml workaround till lit-html in place
      for (var i = 0; i < innerSlots.length; i++) {
        const slotValue = innerSlots[i];
        if (slotValue !== undefined) {
          elementToAppendTo.appendChild(slotValue);
        }
      }
      return elementToAppendTo;
    }

    if (this.rootElement) {
      buttonElement = this.rootElement.firstChild.cloneNode(true);
      // render(inner, buttonElement); // lit-html syntax
      buttonElement.className += ' ' + classes;
    } else if (hasUrl) {
      buttonElement = wire()``;
    } else {
      buttonElement = wire()`<button class="${classes}"></button>`;
    }

    buttonElement = renderInnerSlots(buttonElement);

    return this.html`
      ${this.addStyles([styles, visuallyhiddenUtils])}
      ${buttonElement}
    `;
  }
}

export { BoltButton };
<commit_msg>fix: update button component JS to call the `rendered` method on the base class<commit_after>import {
  props,
  define,
  declarativeClickHandler,
  sanitizeBoltClasses,
  hasNativeShadowDomSupport,
  afterNextRender,
} from '@bolt/core/utils';
import { wire, withHyperHtml } from '@bolt/core/renderers';

import classNames from 'classnames/bind';

import visuallyhiddenUtils from '@bolt/global/styles/07-utilities/_utilities-visuallyhidden.scss';
import styles from './button.scss';

let cx = classNames.bind(styles);

@define
class BoltButton extends withHyperHtml() {
  static is = 'bolt-button';

  static props = {
    color: props.string,
    text: props.string,
    size: props.string,
    rounded: props.boolean,
    iconOnly: props.boolean,
    width: props.string,
    align: props.string,
    transform: props.string,
    disabled: props.boolean,
    target: props.string,
    url: props.string,

    onClick: props.string, // Managed by base class
    onClickTarget: props.string, // Managed by base class
  };

  // https://github.com/WebReflection/document-register-element#upgrading-the-constructor-context
  constructor(self) {
    self = super(self);
    self.useShadow = hasNativeShadowDomSupport;
    return self;
  }

  connecting() {
    const root = this;

    // If the initial <bolt-button> element contains a button or link, break apart the original HTML so we can retain any button or a tags but swap out the inner content with slots.
    this.childNodes.forEach((childElement, i) => {
      if (childElement.tagName === 'BUTTON' || childElement.tagName === 'A') {
        root.rootElement = document.createDocumentFragment();

        // Take any existing buttons and links and move them to the root of the custom element
        while (childElement.firstChild) {
          root.appendChild(childElement.firstChild);
        }

        if (childElement.className) {
          childElement.className = sanitizeBoltClasses(childElement);
        }

        if (
          childElement.getAttribute('is') &&
          childElement.getAttribute('is') === 'shadow-root'
        ) {
          childElement.removeAttribute('is');
        }

        root.rootElement.appendChild(childElement);
      }
    });

    // When possible, use afterNextRender to defer non-critical
    // work until after first paint.
    afterNextRender(this, function() {
      this.addEventListener('click', this.clickHandler);
    });
  }

  rendered() {
    super.rendered();
    const root = this;
    const slots = this.slots;

    // helper function to let you quickly check if an array of elements is inside a component's
    function containsAny(source, target) {
      const result = source.filter(function(item) {
        return target.indexOf(item) > -1;
      });
      return result.length > 0;
    }

    // Automatically re-render if the component's children get externally modified (ex. a new icon gets injected)
    this.observer = new MutationObserver(mutations => {
      mutations.forEach(function(mutation) {
        if (mutation.removedNodes.length > 0) {
          const itemsRemoved = [].slice.call(mutation.removedNodes); // grab items removed + convert to array

          for (let i = 0; i < slots.length; i++) {
            if (containsAny(slots[slot[i]], itemsRemoved)) {
              for (let j = 0; j < itemsRemoved.length; j++) {
                const itemRemoved = itemsRemoved[j];
                slots[slot] = slots[slot].filter(
                  slottedItem => slottedItem !== itemRemoved,
                );
              }
            }
          }
        } else {
          const itemsAdded = [].slice.call(mutation.addedNodes); // grab items added + convert to array

          for (let i = 0; i < itemsAdded.length; i++) {
            const itemAdded = itemsAdded[i];
            const slotName = itemAdded.getAttribute
              ? itemAdded.getAttribute('slot')
              : null;

            if (!slotName) {
              slots.default.push(itemAdded);
            } else if (slots[slotName]) {
              slots[slotName].push(itemAdded);
            } else {
              slots[slotName] = [];
              slots[slotName].push(itemAdded);
            }
          }
        }

        // re-render if Shadow DOM is supported and enabled; temp workaround to dealing w/ components already rendered, but without slot support
        if (hasNativeShadowDomSupport && root.useShadow) {
          root.triggerUpdate();
        }
      });
    });

    this.observer.observe(this, {
      attributes: false,
      childList: true,
      characterData: false,
    });
  }

  disconnecting() {
    this.removeEventListener('click', this.clickHandler);
    this.observer.disconnect();
  }

  // Attach external events declaratively
  clickHandler(event) {
    declarativeClickHandler(this);
  }

  render() {
    const classes = cx('c-bolt-button', {
      'c-bolt-button--rounded': this.props.rounded,
      'c-bolt-button--disabled': this.props.disabled,
      'c-bolt-button--icon-only': this.props.iconOnly,
      'c-bolt-button--center': !this.props.align, // defautl align prop
      [`c-bolt-button--${this.props.align}`]: this.props.align,
      'c-bolt-button--primary': !this.props.color, // default color prop
      [`c-bolt-button--${this.props.color}`]: this.props.color,
      'c-bolt-button--medium': !this.props.size,
      [`c-bolt-button--${this.props.size}`]: this.props.size,
      [`c-bolt-button--${this.props.width}`]: this.props.width,
      [`c-bolt-button--${this.props.transform}`]: this.props.transform,
    });

    // Decide on if the rendered button tag should be a <button> or  tag, based on if a URL exists OR if a link was passed in from the getgo
    const hasUrl = this.props.url.length > 0 && this.props.url !== 'null';

    // Assign default target attribute value if one isn't specified
    const urlTarget = this.props.target && hasUrl ? this.props.target : '_self';

    // The buttonElement to render, based on the initial HTML passed alone.
    let buttonElement = null;
    const self = this;

    const slotMarkup = name => {
      switch (name) {
        case 'before':
        case 'after':
          const iconClasses = cx('c-bolt-button__icon', {
            'is-empty': name in this.slots === false,
          });

          return wire(this)`
            ${
            name in this.slots
              ? this.slot(name)
              : wire(this)`<slot name="${name}" />`
          }`;
        default:
          const itemClasses = cx('c-bolt-button__item', {
            'is-empty': name in this.slots === false,
            'u-bolt-visuallyhidden': this.props.iconOnly,
          });

          return wire(this)`
            ${
            name in this.slots ? this.slot('default') : wire(this)`<slot/>`
          }`;
      }
    };

    const innerSlots = [
      slotMarkup('before'),
      slotMarkup('default'),
      slotMarkup('after'),
    ];

    function renderInnerSlots(elementToAppendTo) {
      // hyperhtml workaround till lit-html in place
      for (var i = 0; i < innerSlots.length; i++) {
        const slotValue = innerSlots[i];
        if (slotValue !== undefined) {
          elementToAppendTo.appendChild(slotValue);
        }
      }
      return elementToAppendTo;
    }

    if (this.rootElement) {
      buttonElement = this.rootElement.firstChild.cloneNode(true);
      // render(inner, buttonElement); // lit-html syntax
      buttonElement.className += ' ' + classes;
    } else if (hasUrl) {
      buttonElement = wire()``;
    } else {
      buttonElement = wire()`<button class="${classes}"></button>`;
    }

    buttonElement = renderInnerSlots(buttonElement);

    return this.html`
      ${this.addStyles([styles, visuallyhiddenUtils])}
      ${buttonElement}
    `;
  }
}

export { BoltButton };
<|endoftext|>


<commit_before>import React from 'react';
import { Comments } from "meteor/example-forum";
import { addCallback, runCallbacksAsync } from 'meteor/vulcan:core';
import { convertFromRaw } from 'draft-js';
import { draftToHTML } from '../../editor/utils.js';
import { preProcessLatex } from '../../editor/server/utils.js';
import htmlToText from 'html-to-text';
import { createError } from 'apollo-errors';

// function commentsSoftRemoveChildrenComments(comment) {
//     const childrenComments = Comments.find({parentCommentId: comment._id}).fetch();
//     childrenComments.forEach(childComment => {
//       editMutation({
//         documentId: childComment._id,
//         set: {deleted:true},
//         unset: {}
//       }).then(()=>console.log('comment softRemoved')).catch(/* error */);
//     });
// }

Comments.convertFromContentAsync = async function(content) {
  content = await preProcessLatex(content);
  return Comments.convertFromContent(content)
}

/*
 * @summary Takes in a content field, returns object with {htmlBody, body, excerpt}
*/

Comments.convertFromContent = (content) => {
  const contentState = convertFromRaw(content);
  return {
    htmlBody: draftToHTML(contentState),
    body: contentState.getPlainText(),
  }
}

/*
 * @summary Input is html, returns object with {body, excerpt}
*/

Comments.convertFromHTML = (html) => {
  const body = htmlToText.fromString(html);
  return {
    body
  }
}

function CommentsEditSoftDeleteCallback (comment, oldComment) {
  if (comment.deleted && !oldComment.deleted) {
    runCallbacksAsync('comments.softDelete.async', comment);
  }
}
addCallback("comments.edit.async", CommentsEditSoftDeleteCallback);

function CommentsNewHTMLSerializeCallback (comment) {
  if (comment.content) {
    const newFields = Comments.convertFromContent(comment.content);
    comment = {...comment, ...newFields}
  } else if (comment.htmlBody) {
    const newFields = Comments.convertFromHTML(comment.content);
    comment = {...comment, ...newFields}
  }
  return comment
}

addCallback("comments.new.sync", CommentsNewHTMLSerializeCallback);

function CommentsEditHTMLSerializeCallback (modifier, comment) {
  if (modifier.$set && modifier.$set.content) {
    const newFields = Comments.convertFromContent(modifier.$set.content)
    modifier.$set = {...modifier.$set, ...newFields}
  } else if (modifier.$set && modifier.$set.htmlBody) {
    const newFields = Comments.convertFromHTML(modifier.$set.htmlBody);
    modifier.$set = {...modifier.$set, ...newFields}
  }
  return modifier
}

addCallback("comments.edit.sync", CommentsEditHTMLSerializeCallback);

function NewCommentsEmptyCheck (comment, user) {
  if (!comment.htmlBody && !convertFromRaw(comment.content).hasText()) {
    const EmptyCommentError = createError('comments.comment_empty_error', {message: 'comments.comment_empty_error'});
    throw new EmptyCommentError({data: {break: true, value: comment}});
  }
  return comment;
}

function EditCommentsEmptyCheck (modifier, user) {
  const EmptyCommentError = createError('comments.comment_empty_error', {message: 'comments.comment_empty_error'});
  if (modifier && modifier.unset && (modifier.unset.htmlBody || modifier.unset.content)) {
    throw new EmptyCommentError({data: {break: true, value: modifier}});
  }
  if (modifier.set && !modifier.set.htmlBody && !convertFromRaw(modifier.set.content).hasText()) {
    throw new EmptyCommentError({data: {break: true, value: modifier}});
  }
  return modifier;
}

addCallback("comments.new.validate", NewCommentsEmptyCheck);
addCallback("comments.edit.validate", EditCommentsEmptyCheck);

export async function CommentsHTMLSerializeCallbackAsync (comment) {
  if (comment.content) {
    const newFields = await Comments.convertFromContentAsync(comment.content);
    Comments.update({_id: comment._id}, {$set: newFields})
  } else if (comment.htmlBody) {
    const newFields = Comments.convertFromHTML(comment.content);
    Comments.update({_id: comment._id}, {$set: newFields})
  }
}

addCallback("comments.edit.async", CommentsHTMLSerializeCallbackAsync);
addCallback("comments.new.async", CommentsHTMLSerializeCallbackAsync);
<commit_msg>Initial pass on callback<commit_after>import React from 'react';
import { Comments, Posts } from "meteor/example-forum";
import { addCallback, runCallbacksAsync, newMutation } from 'meteor/vulcan:core';
import Users from "meteor/vulcan:users";
import { convertFromRaw, ContentState } from 'draft-js';
import { draftToHTML } from '../../editor/utils.js';
import { preProcessLatex } from '../../editor/server/utils.js';
import htmlToText from 'html-to-text';
import { createError } from 'apollo-errors';
import Messages from '../messages/collection.js';

// function commentsSoftRemoveChildrenComments(comment) {
//     const childrenComments = Comments.find({parentCommentId: comment._id}).fetch();
//     childrenComments.forEach(childComment => {
//       editMutation({
//         documentId: childComment._id,
//         set: {deleted:true},
//         unset: {}
//       }).then(()=>console.log('comment softRemoved')).catch(/* error */);
//     });
// }

const getLessWrongAccount = () => {
  let account = Users.findOne({username: "LessWrong"});
  if (!account) {
    const userData = {
      username: "LessWrong",
      email: "lesswrong@lesswrong.com",
    }
    account = newMutation({
      collection: Users,
      userData,
      validate: true
    })
  }
  return account;
}

Comments.convertFromContentAsync = async function(content) {
  content = await preProcessLatex(content);
  return Comments.convertFromContent(content)
}

/*
 * @summary Takes in a content field, returns object with {htmlBody, body, excerpt}
*/

Comments.convertFromContent = (content) => {
  const contentState = convertFromRaw(content);
  return {
    htmlBody: draftToHTML(contentState),
    body: contentState.getPlainText(),
  }
}

/*
 * @summary Input is html, returns object with {body, excerpt}
*/

Comments.convertFromHTML = (html) => {
  const body = htmlToText.fromString(html);
  return {
    body
  }
}

function CommentsEditSoftDeleteCallback (comment, oldComment) {
  if (comment.deleted && !oldComment.deleted) {
    runCallbacksAsync('comments.softDelete.async', comment);
  }
}
addCallback("comments.edit.async", CommentsEditSoftDeleteCallback);

function CommentsNewHTMLSerializeCallback (comment) {
  if (comment.content) {
    const newFields = Comments.convertFromContent(comment.content);
    comment = {...comment, ...newFields}
  } else if (comment.htmlBody) {
    const newFields = Comments.convertFromHTML(comment.content);
    comment = {...comment, ...newFields}
  }
  return comment
}

addCallback("comments.new.sync", CommentsNewHTMLSerializeCallback);

function CommentsEditHTMLSerializeCallback (modifier, comment) {
  if (modifier.$set && modifier.$set.content) {
    const newFields = Comments.convertFromContent(modifier.$set.content)
    modifier.$set = {...modifier.$set, ...newFields}
  } else if (modifier.$set && modifier.$set.htmlBody) {
    const newFields = Comments.convertFromHTML(modifier.$set.htmlBody);
    modifier.$set = {...modifier.$set, ...newFields}
  }
  return modifier
}

addCallback("comments.edit.sync", CommentsEditHTMLSerializeCallback);

function NewCommentsEmptyCheck (comment, user) {
  if (!comment.htmlBody && !convertFromRaw(comment.content).hasText()) {
    const EmptyCommentError = createError('comments.comment_empty_error', {message: 'comments.comment_empty_error'});
    throw new EmptyCommentError({data: {break: true, value: comment}});
  }
  return comment;
}

function EditCommentsEmptyCheck (modifier, user) {
  const EmptyCommentError = createError('comments.comment_empty_error', {message: 'comments.comment_empty_error'});
  if (modifier && modifier.unset && (modifier.unset.htmlBody || modifier.unset.content)) {
    throw new EmptyCommentError({data: {break: true, value: modifier}});
  }
  if (modifier.set && !modifier.set.htmlBody && !convertFromRaw(modifier.set.content).hasText()) {
    throw new EmptyCommentError({data: {break: true, value: modifier}});
  }
  return modifier;
}

addCallback("comments.new.validate", NewCommentsEmptyCheck);
addCallback("comments.edit.validate", EditCommentsEmptyCheck);

export async function CommentsHTMLSerializeCallbackAsync (comment) {
  if (comment.content) {
    const newFields = await Comments.convertFromContentAsync(comment.content);
    Comments.update({_id: comment._id}, {$set: newFields})
  } else if (comment.htmlBody) {
    const newFields = Comments.convertFromHTML(comment.content);
    Comments.update({_id: comment._id}, {$set: newFields})
  }
}

addCallback("comments.edit.async", CommentsHTMLSerializeCallbackAsync);
addCallback("comments.new.async", CommentsHTMLSerializeCallbackAsync);

export async function CommentsDeleteSendPMAsync (comment) {
  if (comment.content) {
    const originalPost = Posts.findOne(comment.postId);
    const lwAccount = getLessWrongAccount();

    const conversationData = {
      participantIds: [comment.userId],
      title: `Comment deleted on ${originalPost.title}`
    }
    const conversation = newMutation({
      collection: Users,
      document: conversationData,
      currentUser: lwAccount,
      validate: true,
    });

    const firstMessageContent =
        `One of your comments on ${originalPost.title} has been removed by
        the author or a moderator. We posted the content below.`

    const firstMessageData = {
      userId: lwAccount._id,
      content: ContentState.createFromText(firstMessageContent),
      conversationId: conversation._id
    }

    const secondMessageData = {
      userId: lwAccount._id,
      content: comment.content,
      conversationId: conversation._id
    }

    newMutation({
      collection: Messages,
      document: firstMessageData,
      currentUser: lwAccount,
      validate: true
    })

    newMutation({
      collection: Messages,
      document: secondMessageData,
      currentUser: lwAccount,
      validate: true
    })
  }
}

addCallback("comments.softDelete.async", CommentsDeleteSendPMAsync);
<|endoftext|>


<commit_before>import test from 'ava';
import trbl from '../lib/trbl';
import plugin from '..';
import {processCSSFactory} from '../../../../util/testHelpers';

const {passthroughCSS, processCSS} = processCSSFactory(plugin);

const wsc = [{
    property: 'width',
    fixture: '1px',
}, {
    property: 'style',
    fixture: 'solid',
}, {
    property: 'color',
    fixture: 'red',
}];

wsc.forEach(({property, fixture}) => {
    test(
        `should merge to form a border-trbl-${property} definition`,
        processCSS,
        [
            `h1{`,
            `border-${trbl[0]}-${property}:${fixture};`,
            `border-${trbl[1]}-${property}:${fixture};`,
            `border-${trbl[2]}-${property}:${fixture};`,
            `border-${trbl[3]}-${property}:${fixture}`,
            `}`,
        ].join(''),
        `h1{border-${property}:${fixture}}`
    );
});

trbl.forEach(direction => {
    const value = wsc.reduce((list, {fixture}) => [...list, fixture], []);
    test(
        `should merge to form a border-${direction} definition`,
        processCSS,
        [
            `h1{`,
            `border-${direction}-width:${value[0]};`,
            `border-${direction}-style:${value[1]};`,
            `border-${direction}-color:${value[2]}`,
            `}`,
        ].join(''),
        `h1{border-${direction}:${value[0]} ${value[1]} ${value[2]}}`
    );
});

test(
    'should merge identical border values',
    processCSS,
    'h1{border-top:1px solid black;border-bottom:1px solid black;border-left:1px solid black;border-right:1px solid black}',
    'h1{border:1px solid black}',
);

test(
    'should merge identical border values with !important',
    processCSS,
    'h1{border-top:1px solid black!important;border-bottom:1px solid black!important;border-left:1px solid black!important;border-right:1px solid black!important}',
    'h1{border:1px solid black!important}'
);

test(
    'should not merge identical border values with mixed !important',
    passthroughCSS,
    'h1{border-top:1px solid black;border-bottom:1px solid black;border-left:1px solid black!important;border-right:1px solid black!important}'
);

test(
    'should merge border values',
    processCSS,
    'h1{border-color:red;border-width:1px;border-style:dashed}',
    'h1{border:1px dashed red}'
);

test(
    'should merge border values with !important',
    processCSS,
    'h1{border-color:red!important;border-width:1px!important;border-style:dashed!important}',
    'h1{border:1px dashed red!important}'
);

test(
    'should merge border values with identical values for all sides',
    processCSS,
    'h1{border-color:red red red red;border-width:1px 1px 1px 1px;border-style:solid solid solid solid}',
    'h1{border:1px solid red}'
);

test(
    'should merge border value shorthands',
    processCSS,
    'h1{border-color:red blue red blue;border-style:solid;border-width:10px 20px 10px 20px}',
    'h1{border-color:red blue;border-style:solid;border-width:10px 20px}'
);

test(
    'should not merge border values with mixed !important',
    passthroughCSS,
    'h1{border-color:red;border-width:1px!important;border-style:dashed!important}'
);

test(
    'should not merge border values with more than 3 values',
    passthroughCSS,
    'h1{border-color:red;border-style:dashed;border-width:1px 5px}'
);

test(
    'should not merge rules with the inherit keyword',
    processCSS,
    'h1{border-width:3px;border-style:solid;border-color:inherit}',
    'h1{border:3px solid;border-color:inherit}'
);

test(
    'should not crash on comments',
    processCSS,
    'h1{\n  border-width:3px;/* 1 */\n  border-style:solid;/* 2 */\n  border-color:red;/* 3 */}',
    'h1{/* 1 *//* 2 */\n  border:3px solid red;/* 3 */}'
);

test(
    'should not convert border: 0 to border-width: 0',
    passthroughCSS,
    'h1{border:0}'
);

test(
    'should not merge border-left values with mixed !important',
    passthroughCSS,
    'h1{border-left-color:red;border-left-width:1px!important;border-left-style:dashed!important}'
);

test(
    'should minimize default border values',
    processCSS,
    'h1{border:medium none currentColor}',
    'h1{border:medium}'
);

test(
    'should optimize border merging for length',
    processCSS,
    'h1{border:1px solid #ddd;border-bottom:1px solid #fff}',
    'h1{border:1px solid;border-color:#ddd #ddd #fff}'
);

test(
    'should not mangle borders',
    passthroughCSS,
    'hr{display:block;height:1px;border:0;border-top:1px solid #ddd}'
);

test(
    'should use shorter equivalent rules',
    processCSS,
    'h1{border:5px solid;border-color:#222 transparent transparent}',
    'h1{border:5px solid transparent;border-top:5px solid #222}'
);

test(
    'should merge redundant values',
    processCSS,
    'h1{border-width:5px 5px 0;border-bottom-width:0}',
    'h1{border-width:5px 5px 0}'
);

test(
    'should merge redundant values (2)',
    processCSS,
    'h1{border-width:5px 5px 0;border-bottom-width:10px}',
    'h1{border-width:5px 5px 10px}'
);

test(
    'should merge redundant values (3)',
    processCSS,
    'h1{border:1px solid #ddd;border-bottom-color:transparent}',
    'h1{border:1px solid;border-color:#ddd #ddd transparent}'
);

test(
    'should merge redundant values (4)',
    processCSS,
    'h1{border:1px solid #ddd;border-bottom-style:dotted}',
    'h1{border:1px #ddd;border-style:solid solid dotted}'
);

test(
    'should merge redundant values (5)',
    processCSS,
    'h1{border:1px solid #ddd;border-bottom-width:5px}',
    'h1{border:solid #ddd;border-width:1px 1px 5px}'
);

test(
    'should merge redundant border-spacing values',
    processCSS,
    'h1{border-spacing:10px 10px;}',
    'h1{border-spacing:10px;}'
);

test(
    'should not merge different border-spacing values',
    passthroughCSS,
    'h1{border-spacing:10px 50px;}'
);

test(
    'should produce the minimum css necessary',
    passthroughCSS,
    'h1{border-width:0;border-top:1px solid #e1e1e1}'
);

test(
    'should produce the minimum css necessary (2)',
    processCSS,
    'h1{border-color:rgba(0,0,0,.2);border-right-style:solid;border-right-width:1px}',
    'h1{border-right:1px solid;border-color:rgba(0,0,0,.2)}'
);

test(
    'should produce the minimum css necessary (3)',
    processCSS,
    'h1{border-top:0 solid transparent;border-right:4em solid transparent;border-bottom:4em solid transparent;border-left:0 solid transparent;border-right-color:inherit}',
    'h1{border-color:transparent;border-style:solid;border-width:0 4em 4em 0;border-right-color:inherit}'
);

test(
    'should produce the minimum css necessary (4)',
    processCSS,
    'h1{border:none;border-top:1px solid #d4d4d5;border-right:1px solid #d4d4d5}',
    'h1{border:1px solid #d4d4d5;border-bottom:none;border-left:none}'
);

test(
    'should produce the minimum css necessary (5)',
    processCSS,
    'h1{border-spacing:50px 50px;border-top:0 solid transparent;border-right:4em solid transparent;border-bottom:4em solid transparent;border-left:0 solid transparent;border-right-color:inherit}',
    'h1{border-spacing:50px;border-color:transparent;border-style:solid;border-width:0 4em 4em 0;border-right-color:inherit}'
);

test(
    'should not merge declarations with hacks',
    processCSS,
    'h1{border-color:red red red red;_border-width:1px 1px 1px 1px;border-style:solid solid solid solid}',
    'h1{border-color:red;_border-width:1px 1px 1px 1px;border-style:solid}'
);

test(
    'should not merge fallback colours',
    passthroughCSS,
    'h1{border-color:#ddd;border-color:rgba(0,0,0,.15)}'
);

test(
    'should not merge fallback colours with shorthand property',
    processCSS,
    'h1{border:1px solid #ccc;border:1px solid rgba(0,0,0,.2)}',
    'h1{border:1px solid #ccc;border:1px solid rgba(0,0,0,.2)}'
);

test(
    'should merge together all initial values',
    processCSS,
    'h1{border-color:initial;border-width:initial;border-style:initial}',
    'h1{border:initial}'
);

test(
    'should merge together all inherit values',
    processCSS,
    'h1{border-color:inherit;border-width:inherit;border-style:inherit}',
    'h1{border:inherit}'
);
test(
    'should preserve nesting level',
    processCSS,
    'section{h1{border-color:red;border-width:1px;border-style:solid}}',
    'section{h1{border:1px solid red}}'
);

test(
    'should not merge custom properties',
    passthroughCSS,
    ':root{--my-border-width:2px;--my-border-style:solid;--my-border-color:#fff;}'
);

test(
    'should not merge custom properties with variables',
    passthroughCSS,
    ':root{--my-border-width:var(--my-border-width);--my-border-style:var(--my-border-style);--my-border-color:var(--my-border-color);}'
);
<commit_msg>tests: merge `border` and `border-width` (`border-*-width`) (#502)<commit_after>import test from 'ava';
import trbl from '../lib/trbl';
import plugin from '..';
import {processCSSFactory} from '../../../../util/testHelpers';

const {passthroughCSS, processCSS} = processCSSFactory(plugin);

const wsc = [{
    property: 'width',
    fixture: '1px',
}, {
    property: 'style',
    fixture: 'solid',
}, {
    property: 'color',
    fixture: 'red',
}];

wsc.forEach(({property, fixture}) => {
    test(
        `should merge to form a border-trbl-${property} definition`,
        processCSS,
        [
            `h1{`,
            `border-${trbl[0]}-${property}:${fixture};`,
            `border-${trbl[1]}-${property}:${fixture};`,
            `border-${trbl[2]}-${property}:${fixture};`,
            `border-${trbl[3]}-${property}:${fixture}`,
            `}`,
        ].join(''),
        `h1{border-${property}:${fixture}}`
    );
});

trbl.forEach(direction => {
    const value = wsc.reduce((list, {fixture}) => [...list, fixture], []);
    test(
        `should merge to form a border-${direction} definition`,
        processCSS,
        [
            `h1{`,
            `border-${direction}-width:${value[0]};`,
            `border-${direction}-style:${value[1]};`,
            `border-${direction}-color:${value[2]}`,
            `}`,
        ].join(''),
        `h1{border-${direction}:${value[0]} ${value[1]} ${value[2]}}`
    );
});

test(
    'should merge identical border values',
    processCSS,
    'h1{border-top:1px solid black;border-bottom:1px solid black;border-left:1px solid black;border-right:1px solid black}',
    'h1{border:1px solid black}',
);

test(
    'should merge identical border values with !important',
    processCSS,
    'h1{border-top:1px solid black!important;border-bottom:1px solid black!important;border-left:1px solid black!important;border-right:1px solid black!important}',
    'h1{border:1px solid black!important}'
);

test(
    'should not merge identical border values with mixed !important',
    passthroughCSS,
    'h1{border-top:1px solid black;border-bottom:1px solid black;border-left:1px solid black!important;border-right:1px solid black!important}'
);

test(
    'should merge border values',
    processCSS,
    'h1{border-color:red;border-width:1px;border-style:dashed}',
    'h1{border:1px dashed red}'
);

test(
    'should merge border values with !important',
    processCSS,
    'h1{border-color:red!important;border-width:1px!important;border-style:dashed!important}',
    'h1{border:1px dashed red!important}'
);

test(
    'should merge border values with identical values for all sides',
    processCSS,
    'h1{border-color:red red red red;border-width:1px 1px 1px 1px;border-style:solid solid solid solid}',
    'h1{border:1px solid red}'
);

test(
    'should merge border value shorthands',
    processCSS,
    'h1{border-color:red blue red blue;border-style:solid;border-width:10px 20px 10px 20px}',
    'h1{border-color:red blue;border-style:solid;border-width:10px 20px}'
);

test(
    'should not merge border values with mixed !important',
    passthroughCSS,
    'h1{border-color:red;border-width:1px!important;border-style:dashed!important}'
);

test(
    'should not merge border values with more than 3 values',
    passthroughCSS,
    'h1{border-color:red;border-style:dashed;border-width:1px 5px}'
);

test(
    'should not merge rules with the inherit keyword',
    processCSS,
    'h1{border-width:3px;border-style:solid;border-color:inherit}',
    'h1{border:3px solid;border-color:inherit}'
);

test(
    'should not crash on comments',
    processCSS,
    'h1{\n  border-width:3px;/* 1 */\n  border-style:solid;/* 2 */\n  border-color:red;/* 3 */}',
    'h1{/* 1 *//* 2 */\n  border:3px solid red;/* 3 */}'
);

test(
    'should not convert border: 0 to border-width: 0',
    passthroughCSS,
    'h1{border:0}'
);

test(
    'should not merge border-left values with mixed !important',
    passthroughCSS,
    'h1{border-left-color:red;border-left-width:1px!important;border-left-style:dashed!important}'
);

test(
    'should minimize default border values',
    processCSS,
    'h1{border:medium none currentColor}',
    'h1{border:medium}'
);

test(
    'should optimize border merging for length',
    processCSS,
    'h1{border:1px solid #ddd;border-bottom:1px solid #fff}',
    'h1{border:1px solid;border-color:#ddd #ddd #fff}'
);

test(
    'should not mangle borders',
    passthroughCSS,
    'hr{display:block;height:1px;border:0;border-top:1px solid #ddd}'
);

test(
    'should use shorter equivalent rules',
    processCSS,
    'h1{border:5px solid;border-color:#222 transparent transparent}',
    'h1{border:5px solid transparent;border-top:5px solid #222}'
);

test(
    'should merge redundant values',
    processCSS,
    'h1{border-width:5px 5px 0;border-bottom-width:0}',
    'h1{border-width:5px 5px 0}'
);

test(
    'should merge redundant values (2)',
    processCSS,
    'h1{border-width:5px 5px 0;border-bottom-width:10px}',
    'h1{border-width:5px 5px 10px}'
);

test(
    'should merge redundant values (3)',
    processCSS,
    'h1{border:1px solid #ddd;border-bottom-color:transparent}',
    'h1{border:1px solid;border-color:#ddd #ddd transparent}'
);

test(
    'should merge redundant values (4)',
    processCSS,
    'h1{border:1px solid #ddd;border-bottom-style:dotted}',
    'h1{border:1px #ddd;border-style:solid solid dotted}'
);

test(
    'should merge redundant values (5)',
    processCSS,
    'h1{border:1px solid #ddd;border-bottom-width:5px}',
    'h1{border:solid #ddd;border-width:1px 1px 5px}'
);

test(
    'should merge redundant border-spacing values',
    processCSS,
    'h1{border-spacing:10px 10px;}',
    'h1{border-spacing:10px;}'
);

test(
    'should not merge different border-spacing values',
    passthroughCSS,
    'h1{border-spacing:10px 50px;}'
);

test(
    'should merge border and border-width values',
    processCSS,
    'h1{border:0 solid rgba(0, 0, 0, 0.2);border-width:1px;}',
    'h1{border:1px solid rgba(0, 0, 0, 0.2);}'
);

test(
    'should merge border and multiple border-*-width values',
    processCSS,
    'h1{border:0 solid rgba(0, 0, 0, 0.2);border-top-width:1px;border-right-width:1px;border-bottom-width:1px;border-left-width:1px;}',
    'h1{border:1px solid rgba(0, 0, 0, 0.2);}'
);

test(
    'should produce the minimum css necessary',
    passthroughCSS,
    'h1{border-width:0;border-top:1px solid #e1e1e1}'
);

test(
    'should produce the minimum css necessary (2)',
    processCSS,
    'h1{border-color:rgba(0,0,0,.2);border-right-style:solid;border-right-width:1px}',
    'h1{border-right:1px solid;border-color:rgba(0,0,0,.2)}'
);

test(
    'should produce the minimum css necessary (3)',
    processCSS,
    'h1{border-top:0 solid transparent;border-right:4em solid transparent;border-bottom:4em solid transparent;border-left:0 solid transparent;border-right-color:inherit}',
    'h1{border-color:transparent;border-style:solid;border-width:0 4em 4em 0;border-right-color:inherit}'
);

test(
    'should produce the minimum css necessary (4)',
    processCSS,
    'h1{border:none;border-top:1px solid #d4d4d5;border-right:1px solid #d4d4d5}',
    'h1{border:1px solid #d4d4d5;border-bottom:none;border-left:none}'
);

test(
    'should produce the minimum css necessary (5)',
    processCSS,
    'h1{border-spacing:50px 50px;border-top:0 solid transparent;border-right:4em solid transparent;border-bottom:4em solid transparent;border-left:0 solid transparent;border-right-color:inherit}',
    'h1{border-spacing:50px;border-color:transparent;border-style:solid;border-width:0 4em 4em 0;border-right-color:inherit}'
);

test(
    'should not merge declarations with hacks',
    processCSS,
    'h1{border-color:red red red red;_border-width:1px 1px 1px 1px;border-style:solid solid solid solid}',
    'h1{border-color:red;_border-width:1px 1px 1px 1px;border-style:solid}'
);

test(
    'should not merge fallback colours',
    passthroughCSS,
    'h1{border-color:#ddd;border-color:rgba(0,0,0,.15)}'
);

test(
    'should not merge fallback colours with shorthand property',
    processCSS,
    'h1{border:1px solid #ccc;border:1px solid rgba(0,0,0,.2)}',
    'h1{border:1px solid #ccc;border:1px solid rgba(0,0,0,.2)}'
);

test(
    'should merge together all initial values',
    processCSS,
    'h1{border-color:initial;border-width:initial;border-style:initial}',
    'h1{border:initial}'
);

test(
    'should merge together all inherit values',
    processCSS,
    'h1{border-color:inherit;border-width:inherit;border-style:inherit}',
    'h1{border:inherit}'
);

test(
    'should preserve nesting level',
    processCSS,
    'section{h1{border-color:red;border-width:1px;border-style:solid}}',
    'section{h1{border:1px solid red}}'
);

test(
    'should not merge custom properties',
    passthroughCSS,
    ':root{--my-border-width:2px;--my-border-style:solid;--my-border-color:#fff;}'
);

test(
    'should not merge custom properties with variables',
    passthroughCSS,
    ':root{--my-border-width:var(--my-border-width);--my-border-style:var(--my-border-style);--my-border-color:var(--my-border-color);}'
);
<|endoftext|>


<commit_before>// @remove-file-on-eject
/**
 * Copyright (c) 2015-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
'use strict';

const fs = require('fs');
const chalk = require('chalk');
const paths = require('../../config/paths');

module.exports = (resolve, rootDir, isEjecting) => {
  // Use this instead of `paths.testsSetup` to avoid putting
  // an absolute filename into configuration after ejecting.
  const setupTestsFile = fs.existsSync(paths.testsSetup)
    ? '<rootDir>/src/setupTests.js'
    : undefined;

  // TODO: I don't know if it's safe or not to just use / as path separator
  // in Jest configs. We need help from somebody with Windows to determine this.
  const config = {
    collectCoverageFrom: ['src/**/*.{js,jsx,mjs}'],
    setupFiles: [resolve('config/polyfills.js')],
    setupTestFrameworkScriptFile: setupTestsFile,
    testMatch: [
      '<rootDir>/src/**/__tests__/**/*.{js,jsx,mjs}',
      '<rootDir>/src/**/?(*.)(spec|test).{js,jsx,mjs}',
    ],
    testEnvironment: 'node',
    testURL: 'http://localhost',
    transform: {
      '^.+\\.(js|jsx|mjs)$': isEjecting
        ? '<rootDir>/node_modules/babel-jest'
        : resolve('config/jest/babelTransform.js'),
      '^.+\\.css$': resolve('config/jest/cssTransform.js'),
      '^(?!.*\\.(js|jsx|mjs|css|json)$)': resolve(
        'config/jest/fileTransform.js'
      ),
    },
    transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs)$'],
    moduleNameMapper: {
      '^react-native$': 'react-native-web',
    },
    moduleFileExtensions: [
      'web.js',
      'mjs',
      'js',
      'json',
      'web.jsx',
      'jsx',
      'node',
    ],
  };
  if (rootDir) {
    config.rootDir = rootDir;
  }
  const overrides = Object.assign({}, require(paths.appPackageJson).jest);
  const supportedKeys = [
    'collectCoverageFrom',
    'coverageReporters',
    'coverageThreshold',
    'snapshotSerializers',
    'watchPathIgnorePatterns',
  ];
  if (overrides) {
    supportedKeys.forEach(key => {
      if (overrides.hasOwnProperty(key)) {
        config[key] = overrides[key];
        delete overrides[key];
      }
    });
    const unsupportedKeys = Object.keys(overrides);
    if (unsupportedKeys.length) {
      console.error(
        chalk.red(
          'Out of the box, Create React App only supports overriding ' +
            'these Jest options:\n\n' +
            supportedKeys.map(key => chalk.bold('  \u2022 ' + key)).join('\n') +
            '.\n\n' +
            'These options in your package.json Jest configuration ' +
            'are not currently supported by Create React App:\n\n' +
            unsupportedKeys
              .map(key => chalk.bold('  \u2022 ' + key))
              .join('\n') +
            '\n\nIf you wish to override other Jest options, you need to ' +
            'eject from the default setup. You can do so by running ' +
            chalk.bold('npm run eject') +
            ' but remember that this is a one-way operation. ' +
            'You may also file an issue with Create React App to discuss ' +
            'supporting more options out of the box.\n'
        )
      );
      process.exit(1);
    }
  }
  return config;
};
<commit_msg>Add feature #3116 extended Jest config (#3802)<commit_after>// @remove-file-on-eject
/**
 * Copyright (c) 2015-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */
'use strict';

const fs = require('fs');
const chalk = require('chalk');
const paths = require('../../config/paths');

module.exports = (resolve, rootDir, isEjecting) => {
  // Use this instead of `paths.testsSetup` to avoid putting
  // an absolute filename into configuration after ejecting.
  const setupTestsFile = fs.existsSync(paths.testsSetup)
    ? '<rootDir>/src/setupTests.js'
    : undefined;

  // TODO: I don't know if it's safe or not to just use / as path separator
  // in Jest configs. We need help from somebody with Windows to determine this.
  const config = {
    collectCoverageFrom: ['src/**/*.{js,jsx,mjs}'],
    setupFiles: [resolve('config/polyfills.js')],
    setupTestFrameworkScriptFile: setupTestsFile,
    testMatch: [
      '<rootDir>/src/**/__tests__/**/*.{js,jsx,mjs}',
      '<rootDir>/src/**/?(*.)(spec|test).{js,jsx,mjs}',
    ],
    testEnvironment: 'node',
    testURL: 'http://localhost',
    transform: {
      '^.+\\.(js|jsx|mjs)$': isEjecting
        ? '<rootDir>/node_modules/babel-jest'
        : resolve('config/jest/babelTransform.js'),
      '^.+\\.css$': resolve('config/jest/cssTransform.js'),
      '^(?!.*\\.(js|jsx|mjs|css|json)$)': resolve(
        'config/jest/fileTransform.js'
      ),
    },
    transformIgnorePatterns: ['[/\\\\]node_modules[/\\\\].+\\.(js|jsx|mjs)$'],
    moduleNameMapper: {
      '^react-native$': 'react-native-web',
    },
    moduleFileExtensions: [
      'web.js',
      'mjs',
      'js',
      'json',
      'web.jsx',
      'jsx',
      'node',
    ],
  };
  if (rootDir) {
    config.rootDir = rootDir;
  }
  const overrides = Object.assign({}, require(paths.appPackageJson).jest);
  const supportedKeys = [
    'collectCoverageFrom',
    'coverageReporters',
    'coverageThreshold',
    'resetMocks',
    'resetModules',
    'snapshotSerializers',
    'watchPathIgnorePatterns',
  ];
  if (overrides) {
    supportedKeys.forEach(key => {
      if (overrides.hasOwnProperty(key)) {
        config[key] = overrides[key];
        delete overrides[key];
      }
    });
    const unsupportedKeys = Object.keys(overrides);
    if (unsupportedKeys.length) {
      console.error(
        chalk.red(
          'Out of the box, Create React App only supports overriding ' +
            'these Jest options:\n\n' +
            supportedKeys.map(key => chalk.bold('  \u2022 ' + key)).join('\n') +
            '.\n\n' +
            'These options in your package.json Jest configuration ' +
            'are not currently supported by Create React App:\n\n' +
            unsupportedKeys
              .map(key => chalk.bold('  \u2022 ' + key))
              .join('\n') +
            '\n\nIf you wish to override other Jest options, you need to ' +
            'eject from the default setup. You can do so by running ' +
            chalk.bold('npm run eject') +
            ' but remember that this is a one-way operation. ' +
            'You may also file an issue with Create React App to discuss ' +
            'supporting more options out of the box.\n'
        )
      );
      process.exit(1);
    }
  }
  return config;
};
<|endoftext|>


<commit_before>module.exports = {
    '/': {
      backLink: '../intro/what-you-need'
    },
    '/passport-offices': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
    },
    '/book-time': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
    },
    '/book-appointment-2_mobile': {
      backLink: '../intro/what-you-need'
    },
    '/book-appointment-2': {
      backLink: '../intro/what-you-need'
    },
    '/book-appointment-3': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
    },
    '/confirmation-scenario-1': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
    },
    '/confirmation-scenario-2': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
    },
    '/responsive': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
    },
    '/london-week-2-selected': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
  },
  '/london-week-1-selected': {
    fields: ['passport-damaged'],
      backLink: './',
      next: '/passport-damaged' /* if No is selected */
},
    '/peterborough-week-1': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
  },
  '/peterborough-week-2': {
    fields: ['passport-damaged'],
      backLink: './',
      next: '/passport-damaged' /* if No is selected */
    },
    '/newport-week-1': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
  },
  '/newport-week-1-selected': {
    fields: ['passport-damaged'],
      backLink: './',
      next: '/passport-damaged' /* if No is selected */
},
    '/london-week-1': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
  },
  '/london-week-2': {
    fields: ['passport-damaged'],
      backLink: './',
      next: '/passport-damaged' /* if No is selected */
  },
  '/london-week-3': {
    fields: ['passport-damaged'],
      backLink: './',
      next: '/passport-damaged' /* if No is selected */
  },
    '/passport-damaged': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/uncancelled' /* if No is selected */
    },
    '/uncancelled': {
        controller: require('../../../controllers/go-overseas'),
        fields: ['uncancelled'],
        backLink: './passport-damaged',
        next: '/../intro',
        nextAlt: '../overseas'
    }
};
<commit_msg>Update steps<commit_after>module.exports = {
    '/': {
      backLink: '../intro/what-you-need'
    },
    '/passport-offices': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
    },
    '/book-time': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
    },
    '/book-appointment-2': {
      backLink: '../intro/what-you-need'
    },
    '/book-appointment-3': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
    },
    '/book-appointment-2_mobile': {
      backLink: '../intro/what-you-need'
    },
    '/confirmation-scenario-1': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
    },
    '/confirmation-scenario-2': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
    },
    '/responsive': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
    },
    '/london-week-2-selected': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
  },
  '/london-week-1-selected': {
    fields: ['passport-damaged'],
      backLink: './',
      next: '/passport-damaged' /* if No is selected */
},
    '/peterborough-week-1': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
  },
  '/peterborough-week-2': {
    fields: ['passport-damaged'],
      backLink: './',
      next: '/passport-damaged' /* if No is selected */
    },
    '/newport-week-1': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
  },
  '/newport-week-1-selected': {
    fields: ['passport-damaged'],
      backLink: './',
      next: '/passport-damaged' /* if No is selected */
},
    '/london-week-1': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/passport-damaged' /* if No is selected */
  },
  '/london-week-2': {
    fields: ['passport-damaged'],
      backLink: './',
      next: '/passport-damaged' /* if No is selected */
  },
  '/london-week-3': {
    fields: ['passport-damaged'],
      backLink: './',
      next: '/passport-damaged' /* if No is selected */
  },
    '/passport-damaged': {
      fields: ['passport-damaged'],
        backLink: './',
        next: '/uncancelled' /* if No is selected */
    },
    '/uncancelled': {
        controller: require('../../../controllers/go-overseas'),
        fields: ['uncancelled'],
        backLink: './passport-damaged',
        next: '/../intro',
        nextAlt: '../overseas'
    }
};
<|endoftext|>


<commit_before>import React from 'react';
import SPELLS from 'common/SPELLS';

import StatisticBox from 'interface/others/StatisticBox';
import SpellIcon from 'common/SpellIcon';
import Analyzer from 'parser/core/Analyzer';
import SpellUsable from 'parser/shared/modules/SpellUsable';
import HIT_TYPES from 'game/HIT_TYPES';
import { formatNumber, formatPercentage } from 'common/format';

import GrandCrusader from '../core/GrandCrusader';

const REDUCTION_TIME_REGULAR = 2000; // ms
const REDUCTION_TIME_CRIT = 4000; // ms

/**
 * Judgment
 * Judges the target dealing (250% of Spell power) Holy damage, and reducing the remaining cooldown on Shield of the Righteous by 2 sec, or 4 sec on a critical strike.
 */
class Judgment extends Analyzer {
  _totalCdr = 0;
  _wastedCdr = 0;

  _casts = 0;
  _crits = 0;

  static dependencies = {
    spellUsable: SpellUsable,
    gc: GrandCrusader,
  };

  on_byPlayer_damage(event) {
    const spellId = event.ability.guid;
    if (spellId !== SPELLS.JUDGMENT_CAST_PROTECTION.id) {
      return;
    }

    this._casts += 1;

    const isCrit = event.hitType === HIT_TYPES.CRIT || event.hitType === HIT_TYPES.BLOCKED_CRIT;
    this._crits += isCrit ? 1 : 0;

    if (this.spellUsable.isOnCooldown(SPELLS.SHIELD_OF_THE_RIGHTEOUS.id)) {
      // Nope, I did not verify if blocked crits count as crits for this trait, I just assumed it. Please do test if you can and report back or fix this comment.
      // Confirmed: Blocked crits count as crits.
      const reduction = isCrit ? REDUCTION_TIME_CRIT : REDUCTION_TIME_REGULAR;
      const actualReduction = this.spellUsable.reduceCooldown(SPELLS.SHIELD_OF_THE_RIGHTEOUS.id, reduction);
      this._totalCdr += actualReduction;
      this._wastedCdr += reduction - actualReduction;
    }
  }

  get cdrPercentage() {
    return this._totalCdr / (this.owner.fightDuration + this._totalCdr);
  }

  // cdr percentage without extra casts from Crusader's Judgment
  get baseCdr() {
    if(!this.selectedCombatant.hasTalent(SPELLS.CRUSADERS_JUDGMENT_TALENT.id)) {
      return this._totalCdr;
    }

    return (1 - this.gc._totalResets / this._casts) * this._totalCdr;
  }

  get baseCdrPercentage() {
    return this.baseCdr / (this.owner.fightDuration + this.baseCdr);
  }

  statistic() {
    const cjTooltip = this.selectedCombatant.hasTalent(SPELLS.CRUSADERS_JUDGMENT_TALENT.id) ?
      `<br/>Without the Crusader's Judgment talent, this would have been roughly <b>${formatPercentage(this.baseCdrPercentage)}%</b> (or ${formatNumber(this.baseCdr / 1000)}s).` : '';
    return (
      <StatisticBox
        icon={<SpellIcon id={SPELLS.SHIELD_OF_THE_RIGHTEOUS.id} />}
        label="Effective SotR CDR"
        value={`${formatPercentage(this.cdrPercentage)}%`}
        tooltip={`Your Judgment casts reduced the cooldown of Shield of the Righteous by ${formatNumber(this._totalCdr / 1000)}s over ${formatNumber(this._casts)} casts, ${formatNumber(this._crits)} of which were critical strikes.
            ${cjTooltip}

            You wasted ${formatNumber(this._wastedCdr / 1000)}s of cooldown reduction.`}
          />
    );
  }
}

export default Judgment;
<commit_msg>updated crusader's judgment tooltip text following @Pewtro's comment<commit_after>import React from 'react';
import SPELLS from 'common/SPELLS';

import StatisticBox from 'interface/others/StatisticBox';
import SpellIcon from 'common/SpellIcon';
import Analyzer from 'parser/core/Analyzer';
import SpellUsable from 'parser/shared/modules/SpellUsable';
import HIT_TYPES from 'game/HIT_TYPES';
import { formatNumber, formatPercentage } from 'common/format';

import GrandCrusader from '../core/GrandCrusader';

const REDUCTION_TIME_REGULAR = 2000; // ms
const REDUCTION_TIME_CRIT = 4000; // ms

/**
 * Judgment
 * Judges the target dealing (250% of Spell power) Holy damage, and reducing the remaining cooldown on Shield of the Righteous by 2 sec, or 4 sec on a critical strike.
 */
class Judgment extends Analyzer {
  _totalCdr = 0;
  _wastedCdr = 0;

  _casts = 0;
  _crits = 0;

  static dependencies = {
    spellUsable: SpellUsable,
    gc: GrandCrusader,
  };

  on_byPlayer_damage(event) {
    const spellId = event.ability.guid;
    if (spellId !== SPELLS.JUDGMENT_CAST_PROTECTION.id) {
      return;
    }

    this._casts += 1;

    const isCrit = event.hitType === HIT_TYPES.CRIT || event.hitType === HIT_TYPES.BLOCKED_CRIT;
    this._crits += isCrit ? 1 : 0;

    if (this.spellUsable.isOnCooldown(SPELLS.SHIELD_OF_THE_RIGHTEOUS.id)) {
      // Nope, I did not verify if blocked crits count as crits for this trait, I just assumed it. Please do test if you can and report back or fix this comment.
      // Confirmed: Blocked crits count as crits.
      const reduction = isCrit ? REDUCTION_TIME_CRIT : REDUCTION_TIME_REGULAR;
      const actualReduction = this.spellUsable.reduceCooldown(SPELLS.SHIELD_OF_THE_RIGHTEOUS.id, reduction);
      this._totalCdr += actualReduction;
      this._wastedCdr += reduction - actualReduction;
    }
  }

  get cdrPercentage() {
    return this._totalCdr / (this.owner.fightDuration + this._totalCdr);
  }

  // cdr percentage without extra casts from Crusader's Judgment
  get baseCdr() {
    if(!this.selectedCombatant.hasTalent(SPELLS.CRUSADERS_JUDGMENT_TALENT.id)) {
      return this._totalCdr;
    }

    return (1 - this.gc._totalResets / this._casts) * this._totalCdr;
  }

  get baseCdrPercentage() {
    return this.baseCdr / (this.owner.fightDuration + this.baseCdr);
  }

  statistic() {
    const cjTooltip = this.selectedCombatant.hasTalent(SPELLS.CRUSADERS_JUDGMENT_TALENT.id) ?
      `<br/>Without the Crusader's Judgment talent, your effective SotR CDR would have been roughly <b>${formatPercentage(this.baseCdrPercentage)}%</b> (or ${formatNumber(this.baseCdr / 1000)}s).` : '';
    return (
      <StatisticBox
        icon={<SpellIcon id={SPELLS.SHIELD_OF_THE_RIGHTEOUS.id} />}
        label="Effective SotR CDR"
        value={`${formatPercentage(this.cdrPercentage)}%`}
        tooltip={`Your Judgment casts reduced the cooldown of Shield of the Righteous by ${formatNumber(this._totalCdr / 1000)}s over ${formatNumber(this._casts)} casts, ${formatNumber(this._crits)} of which were critical strikes.
            ${cjTooltip}

            You wasted ${formatNumber(this._wastedCdr / 1000)}s of cooldown reduction.`}
          />
    );
  }
}

export default Judgment;
<|endoftext|>


<commit_before>/* @flow */

// Provides transition support for list items.
// supports move transitions using the FLIP technique.

// Because the vdom's children update algorithm is "unstable" - i.e.
// it doesn't guarantee the relative positioning of removed elements,
// we force transition-group to update its children into two passes:
// in the first pass, we remove all nodes that need to be removed,
// triggering their leaving transition; in the second pass, we insert/move
// into the final disired state. This way in the second pass removed
// nodes will remain where they should be.

import { warn, extend } from 'core/util/index'
import { transitionProps, extractTransitionData } from './transition'
import {
  hasTransition,
  addTransitionClass,
  removeTransitionClass,
  getTransitionInfo,
  transitionEndEvent
} from '../transition-util'

const props = extend({
  tag: String,
  moveClass: String
}, transitionProps)

delete props.mode

export default {
  props,

  render (h: Function) {
    const tag = this.tag || this.$vnode.data.tag || 'span'
    const map = Object.create(null)
    const prevChildren = this.prevChildren = this.children
    const rawChildren = this.$slots.default || []
    const children = this.children = []
    const transitionData = extractTransitionData(this)

    for (let i = 0; i < rawChildren.length; i++) {
      const c = rawChildren[i]
      if (c.tag) {
        if (c.key != null) {
          children.push(c)
          map[c.key] = c
          ;(c.data || (c.data = {})).transition = transitionData
        } else if (process.env.NODE_ENV !== 'production') {
          const opts = c.componentOptions
          const name = opts
            ? (opts.Ctor.options.name || opts.tag)
            : c.tag
          warn(`<transition-group> children must be keyed: <${name}>`)
        }
      }
    }

    if (prevChildren) {
      const kept = []
      const removed = []
      for (let i = 0; i < prevChildren.length; i++) {
        const c = prevChildren[i]
        c.data.pos = c.elm.getBoundingClientRect()
        if (map[c.key]) {
          kept.push(c)
        } else {
          removed.push(c)
        }
      }
      this.kept = h(tag, null, kept)
      this.removed = removed
    }

    return h(tag, null, children)
  },

  beforeUpdate () {
    // force removing pass
    this.__patch__(
      this._vnode,
      this.kept,
      false, // hydrating
      true // removeOnly (!important, avoids unnecessary moves)
    )
    this._vnode = this.kept
  },

  updated () {
    const children = this.prevChildren
    const moveClass = this.moveClass || (this.name + '-move')
    if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
      return
    }

    children.forEach(c => {
      const oldPos = c.data.pos
      const newPos = c.elm.getBoundingClientRect()
      const dx = oldPos.left - newPos.left
      const dy = oldPos.top - newPos.top
      if (dx || dy) {
        c.data.moved = true
        const s = c.elm.style
        s.transform = s.WebkitTransform = `translate(${dx}px,${dy}px)`
        s.transitionDuration = '0s'
      }
    })

    // force reflow to put everything in position
    const f = document.body.offsetHeight // eslint-disable-line

    children.forEach(c => {
      if (c.data.moved) {
        const el = c.elm
        /* istanbul ignore if */
        if (el._pendingMoveCb) {
          el._pendingMoveCb()
        }
        const s = el.style
        addTransitionClass(el, moveClass)
        s.transform = s.WebkitTransform = s.transitionDuration = ''
        el.addEventListener(transitionEndEvent, el._pendingMoveCb = function cb () {
          el.removeEventListener(transitionEndEvent, cb)
          el._pendingMoveCb = null
          removeTransitionClass(el, moveClass)
        })
      }
    })
  },

  methods: {
    hasMove (el: Element, moveClass: string): boolean {
      /* istanbul ignore if */
      if (!hasTransition) {
        return false
      }
      if (this._hasMove != null) {
        return this._hasMove
      }
      addTransitionClass(el, moveClass)
      const info = getTransitionInfo(el)
      removeTransitionClass(el, moveClass)
      return (this._hasMove = info.hasTransform)
    }
  }
}
<commit_msg>adjust transition-group pending move callback execution timing<commit_after>/* @flow */

// Provides transition support for list items.
// supports move transitions using the FLIP technique.

// Because the vdom's children update algorithm is "unstable" - i.e.
// it doesn't guarantee the relative positioning of removed elements,
// we force transition-group to update its children into two passes:
// in the first pass, we remove all nodes that need to be removed,
// triggering their leaving transition; in the second pass, we insert/move
// into the final disired state. This way in the second pass removed
// nodes will remain where they should be.

import { warn, extend } from 'core/util/index'
import { transitionProps, extractTransitionData } from './transition'
import {
  hasTransition,
  addTransitionClass,
  removeTransitionClass,
  getTransitionInfo,
  transitionEndEvent
} from '../transition-util'

const props = extend({
  tag: String,
  moveClass: String
}, transitionProps)

delete props.mode

export default {
  props,

  render (h: Function) {
    const tag = this.tag || this.$vnode.data.tag || 'span'
    const map = Object.create(null)
    const prevChildren = this.prevChildren = this.children
    const rawChildren = this.$slots.default || []
    const children = this.children = []
    const transitionData = extractTransitionData(this)

    for (let i = 0; i < rawChildren.length; i++) {
      const c = rawChildren[i]
      if (c.tag) {
        if (c.key != null) {
          children.push(c)
          map[c.key] = c
          ;(c.data || (c.data = {})).transition = transitionData
        } else if (process.env.NODE_ENV !== 'production') {
          const opts = c.componentOptions
          const name = opts
            ? (opts.Ctor.options.name || opts.tag)
            : c.tag
          warn(`<transition-group> children must be keyed: <${name}>`)
        }
      }
    }

    if (prevChildren) {
      const kept = []
      const removed = []
      for (let i = 0; i < prevChildren.length; i++) {
        const c = prevChildren[i]
        c.data.pos = c.elm.getBoundingClientRect()
        if (map[c.key]) {
          kept.push(c)
        } else {
          removed.push(c)
        }
      }
      this.kept = h(tag, null, kept)
      this.removed = removed
    }

    return h(tag, null, children)
  },

  beforeUpdate () {
    // force removing pass
    this.__patch__(
      this._vnode,
      this.kept,
      false, // hydrating
      true // removeOnly (!important, avoids unnecessary moves)
    )
    this._vnode = this.kept
  },

  updated () {
    const children = this.prevChildren
    const moveClass = this.moveClass || (this.name + '-move')
    if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
      return
    }

    children.forEach(c => {
      if (c.elm._moveCb) {
        c.elm._moveCb()
      }
      const oldPos = c.data.pos
      const newPos = c.data.pos = c.elm.getBoundingClientRect()
      const dx = oldPos.left - newPos.left
      const dy = oldPos.top - newPos.top
      if (dx || dy) {
        c.data.moved = true
        const s = c.elm.style
        s.transform = s.WebkitTransform = `translate(${dx}px,${dy}px)`
        s.transitionDuration = '0s'
      }
    })

    // force reflow to put everything in position
    const f = document.body.offsetHeight // eslint-disable-line

    children.forEach(c => {
      if (c.data.moved) {
        const el = c.elm
        const s = el.style
        addTransitionClass(el, moveClass)
        s.transform = s.WebkitTransform = s.transitionDuration = ''
        el._moveDest = c.data.pos
        el.addEventListener(transitionEndEvent, el._moveCb = function cb () {
          el.removeEventListener(transitionEndEvent, cb)
          el._moveCb = null
          removeTransitionClass(el, moveClass)
        })
      }
    })
  },

  methods: {
    hasMove (el: Element, moveClass: string): boolean {
      /* istanbul ignore if */
      if (!hasTransition) {
        return false
      }
      if (this._hasMove != null) {
        return this._hasMove
      }
      addTransitionClass(el, moveClass)
      const info = getTransitionInfo(el)
      removeTransitionClass(el, moveClass)
      return (this._hasMove = info.hasTransform)
    }
  }
}
<|endoftext|>