diff --git a/app/models/map.rb b/app/models/map.rb index dd5e5604..76fddcd8 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -42,6 +42,7 @@ class Map < ApplicationRecord after_create :after_created after_update :after_updated after_save :update_deferring_topics_and_synapses, if: :permission_changed? + before_destroy :before_destroyed delegate :count, to: :topics, prefix: :topic # same as `def topic_count; topics.count; end` delegate :count, to: :synapses, prefix: :synapse @@ -158,4 +159,10 @@ class Map < ApplicationRecord end end handle_asynchronously :after_updated_async + + def before_destroyed + Map.where(source_id: id).find_each do |forked_map| + forked_map.update(source_id: nil) + end + end end diff --git a/app/models/mapping.rb b/app/models/mapping.rb index a49555a3..270d3574 100644 --- a/app/models/mapping.rb +++ b/app/models/mapping.rb @@ -37,8 +37,8 @@ class Mapping < ApplicationRecord 'map_' + map.id.to_s, type: 'synapseAdded', synapse: mappable.filtered, - topic1: mappable.topic1.filtered, - topic2: mappable.topic2.filtered, + topic1: mappable.topic1&.filtered, + topic2: mappable.topic2&.filtered, mapping_id: id ) meta = { 'mapping_id': id } diff --git a/app/views/notifications/index.html.erb b/app/views/notifications/index.html.erb index e965f481..ec9987f6 100644 --- a/app/views/notifications/index.html.erb +++ b/app/views/notifications/index.html.erb @@ -37,13 +37,13 @@ map = notification.notified_object.map %> added topic <%= topic.name %> to map <%= map.name %> <% when TOPIC_CONNECTED_1 %> - <% topic1 = notification.notified_object.topic1 - topic2 = notification.notified_object.topic2 %> - connected <%= topic1.name %> to <%= topic2.name %> + <% topic1 = notification.notified_object&.topic1 %> + <% topic2 = notification.notified_object&.topic2 %> + connected <%= topic1&.name %> to <%= topic2&.name %> <% when TOPIC_CONNECTED_2 %> - <% topic1 = notification.notified_object.topic1 - topic2 = notification.notified_object.topic2 %> - connected <%= topic2.name %> to <%= topic1.name %> + <% topic1 = notification.notified_object&.topic1 %> + <% topic2 = notification.notified_object&.topic2 %> + connected <%= topic2&.name %> to <%= topic1&.name %> <% when MESSAGE_FROM_DEVS %> <%= notification.subject %> <% end %> diff --git a/config/initializers/version.rb b/config/initializers/version.rb index 42f89153..ccdeb899 100644 --- a/config/initializers/version.rb +++ b/config/initializers/version.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true -METAMAPS_VERSION = '3.4' +METAMAPS_VERSION = '3.4.1' METAMAPS_BUILD = `git log -1 --pretty=%H`.chomp[0..11].freeze METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad'`.split(' ').values_at(1, 2, 4).join(' ').freeze diff --git a/frontend/test/.eslintrc.js b/frontend/test/.eslintrc.js new file mode 100644 index 00000000..8d883376 --- /dev/null +++ b/frontend/test/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + "rules": { + "no-unused-expressions": "off" + } +} diff --git a/frontend/test/Metamaps/Import.spec.js b/frontend/test/Metamaps/Import.spec.js index 7e4ff0ab..7f438af2 100644 --- a/frontend/test/Metamaps/Import.spec.js +++ b/frontend/test/Metamaps/Import.spec.js @@ -1,10 +1,8 @@ /* global describe, it */ -import chai from 'chai' +import { expect } from 'chai' -import Import from '../src/Metamaps/Import' - -const { expect } = chai +import Import from '../../src/Metamaps/Import.js' describe('Metamaps.Import.js', function() { it('has a topic whitelist', function() { diff --git a/frontend/test/Metamaps/Util.spec.js b/frontend/test/Metamaps/Util.spec.js index 80108ee2..3cfe05d8 100644 --- a/frontend/test/Metamaps/Util.spec.js +++ b/frontend/test/Metamaps/Util.spec.js @@ -1,10 +1,8 @@ /* global describe, it */ -import chai from 'chai' +import { expect } from 'chai' -import Util from '../src/Metamaps/Util' - -const { expect } = chai +import Util from '../../src/Metamaps/Util' describe('Metamaps.Util.js', function() { describe('splitLine', function() { diff --git a/frontend/test/components/ImportDialogBox.spec.js b/frontend/test/components/ImportDialogBox.spec.js deleted file mode 100644 index f14e04b3..00000000 --- a/frontend/test/components/ImportDialogBox.spec.js +++ /dev/null @@ -1,50 +0,0 @@ -/* global describe, it */ -import React from 'react' -import TestUtils from 'react-addons-test-utils' // ES6 -import ImportDialogBox from '../../src/components/ImportDialogBox.js' -import Dropzone from 'react-dropzone' -import chai from 'chai' - -const { expect } = chai - -describe('ImportDialogBox', function() { - it('has an Export CSV button', function(done) { - const onExport = format => { - if (format === 'csv') done() - } - const detachedComp = TestUtils.renderIntoDocument() - const button = TestUtils.findRenderedDOMComponentWithClass(detachedComp, 'export-csv') - const buttonNode = React.findDOMNode(button) - expect(button).to.exist; - TestUtils.Simulate.click(buttonNode) - }) - - it('has an Export JSON button', function(done) { - const onExport = format => { - if (format === 'json') done() - } - const detachedComp = TestUtils.renderIntoDocument() - const button = TestUtils.findRenderedDOMComponentWithClass(detachedComp, 'export-json') - const buttonNode = React.findDOMNode(button) - expect(button).to.exist; - TestUtils.Simulate.click(buttonNode) - }) - - it('has a Download screenshot button', function(done) { - const downloadScreenshot = () => { done() } - const detachedComp = TestUtils.renderIntoDocument() - const button = TestUtils.findRenderedDOMComponentWithClass(detachedComp, 'download-screenshot') - const buttonNode = React.findDOMNode(button) - expect(button).to.exist; - TestUtils.Simulate.click(buttonNode) - }) - - it('has a file uploader', function(done) { - const uploadedFile = { file: 'mock a file' } - const onFileAdded = file => { if (file === uploadedFile) done() } - const detachedComp = TestUtils.renderIntoDocument( {}} onFileAdded={onFileAdded} />) - const dropzone = TestUtils.findRenderedComponentWithType(detachedComp, Dropzone) - expect(dropzone).to.exist; - dropzone.props.onDropAccepted([uploadedFile], { preventDefault: () => {} }) - }) -}) diff --git a/frontend/test/components/MapView/ImportDialogBox.spec.js b/frontend/test/components/MapView/ImportDialogBox.spec.js new file mode 100644 index 00000000..7151230e --- /dev/null +++ b/frontend/test/components/MapView/ImportDialogBox.spec.js @@ -0,0 +1,58 @@ +/* global describe, it */ +import React from 'react' +import ImportDialogBox from '../../../src/components/MapView/ImportDialogBox.js' +import Dropzone from 'react-dropzone' +import { expect } from 'chai' +import { shallow } from 'enzyme' +import sinon from 'sinon' + +describe('ImportDialogBox', function() { + const csvExport = sinon.spy() + const jsonExport = sinon.spy() + const onExport = format => () => { + if (format === 'csv') { + csvExport() + } else if (format === 'json') { + jsonExport() + } + } + + const testExportButton = ({ description, cssClass, exporter }) => { + it(description, () => { + const wrapper = shallow() + const button = wrapper.find(cssClass) + expect(button).to.exist + button.simulate('click') + expect(exporter).to.have.property('callCount', 1) + }) + } + testExportButton({ + description: 'has an Export CSV button', + cssClass: '.export-csv', + exporter: csvExport + }) + testExportButton({ + description: 'has an Export JSON button', + cssClass: '.export-json', + exporter: jsonExport + }) + + it('has a Download screenshot button', () => { + const downloadScreenshot = sinon.spy() + const wrapper = shallow( null} downloadScreenshot={downloadScreenshot} />) + const button = wrapper.find('.download-screenshot') + expect(button).to.exist + button.simulate('click') + expect(downloadScreenshot).to.have.property('callCount', 1) + }) + + it('has a file uploader', () => { + const uploadedFile = {} + const onFileAdded = sinon.spy() + const wrapper = shallow( null} onFileAdded={onFileAdded} />) + const dropzone = wrapper.find(Dropzone) + dropzone.props().onDropAccepted([uploadedFile], { preventDefault: () => {} }) + expect(onFileAdded).to.have.property('callCount', 1) + expect(onFileAdded.calledWith(uploadedFile)).to.equal(true) + }) +}) diff --git a/frontend/test/components/common/InfoAndHelp.spec.js b/frontend/test/components/common/InfoAndHelp.spec.js new file mode 100644 index 00000000..9880af0f --- /dev/null +++ b/frontend/test/components/common/InfoAndHelp.spec.js @@ -0,0 +1,101 @@ +/* global describe, it */ +import React from 'react' +import { expect } from 'chai' +import { shallow } from 'enzyme' +import sinon from 'sinon' + +import InfoAndHelp from '../../../src/components/common/InfoAndHelp.js' +import MapInfoBox from '../../../src/components/MapView/MapInfoBox.js' + +function assertTooltip({ wrapper, description, cssClass, tooltipText, callback }) { + it(description, function() { + expect(wrapper.find(cssClass)).to.exist + expect(wrapper.find(`${cssClass} .tooltipsAbove`).text()).to.equal(tooltipText) + wrapper.find(cssClass).simulate('click') + expect(callback).to.have.property('callCount', 1) + }) +} + +function assertContent({ currentUser, map }) { + const onInfoClick = sinon.spy() + const onHelpClick = sinon.spy() + const onStarClick = sinon.spy() + const wrapper = shallow( + ) + + if (map) { + it('renders MapInfoBox', () => expect(wrapper.find(MapInfoBox)).to.exist) + assertTooltip({ + wrapper, + description: 'renders Map Info icon', + cssClass: '.mapInfoIcon', + tooltipText: 'Map Info', + callback: onInfoClick + }) + } else { + it('does not render MapInfoBox', () => expect(wrapper.find(MapInfoBox).length).to.equal(0)) + it('does not render Map Info icon', () => expect(wrapper.find('.mapInfoIcon').length).to.equal(0)) + } + + if (map && currentUser) { + it('renders Star icon', () => { + expect(wrapper.find('.starMap')).to.exist + wrapper.find('.starMap').simulate('click') + expect(onStarClick).to.have.property('callCount', 1) + }) + } else { + it('does not render the Star icon', () => expect(wrapper.find('.starMap').length).to.equal(0)) + } + + // common content + assertTooltip({ + wrapper, + description: 'renders Help icon', + cssClass: '.openCheatsheet', + tooltipText: 'Help', + callback: onHelpClick + }) + it('renders clearfloat at the end', function() { + const clearfloat = wrapper.find('.clearfloat') + expect(clearfloat).to.exist + expect(wrapper.find('.infoAndHelp').children().last()).to.eql(clearfloat) + }) +} + +function assertStarLogic({ mapIsStarred }) { + const onMapStar = sinon.spy() + const onMapUnstar = sinon.spy() + const wrapper = shallow( + ) + const starWrapper = wrapper.find('.starMap') + starWrapper.simulate('click') + it(mapIsStarred ? 'has unstar content' : 'has star content', () => { + expect(starWrapper.hasClass('starred')).to.equal(mapIsStarred) + expect(starWrapper.find('.tooltipsAbove').text()).to.equal(mapIsStarred ? 'Unstar' : 'Star') + expect(onMapStar).to.have.property('callCount', mapIsStarred ? 0 : 1) + expect(onMapUnstar).to.have.property('callCount', mapIsStarred ? 1 : 0) + }) +} + +describe('InfoAndHelp', function() { + describe('no currentUser, map is present', function() { + assertContent({ currentUser: null, map: {} }) + }) + describe('currentUser is present, map is present', function() { + assertContent({ currentUser: {}, map: {} }) + }) + describe('no currentUser, no map', function() { + assertContent({ currentUser: null, map: null }) + }) + assertStarLogic({ mapIsStarred: true }) + assertStarLogic({ mapIsStarred: false }) +}) diff --git a/frontend/test/support/dom.js b/frontend/test_support/dom.js similarity index 93% rename from frontend/test/support/dom.js rename to frontend/test_support/dom.js index af2c1bf9..5dd4a9ef 100644 --- a/frontend/test/support/dom.js +++ b/frontend/test_support/dom.js @@ -5,12 +5,12 @@ const win = doc.defaultView global.document = doc global.window = win -// take all properties of the window object and also attach it to the +// take all properties of the window object and also attach it to the // mocha global object propagateToGlobal(win) // from mocha-jsdom https://github.com/rstacruz/mocha-jsdom/blob/master/index.js#L80 -function propagateToGlobal (window) { +function propagateToGlobal(window) { for (let key in window) { if (!window.hasOwnProperty(key)) continue if (key in global) continue diff --git a/package.json b/package.json index 8525a725..dfa67ded 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "webpack", "build:watch": "webpack --watch", - "test": "mocha-webpack --webpack-config webpack.test.config.js --require frontend/test/support/dom.js frontend/test", + "test": "mocha-webpack --webpack-config webpack.test.config.js --require frontend/test_support/dom.js --recursive frontend/test", "eslint": "eslint frontend", "eslint:fix": "eslint --fix frontend" }, @@ -58,6 +58,7 @@ "babel-eslint": "^7.1.1", "chai": "^3.5.0", "circular-dependency-plugin": "^2.0.0", + "enzyme": "^2.8.2", "eslint": "^3.11.1", "eslint-config-standard": "^6.2.1", "eslint-plugin-promise": "^3.4.0", @@ -66,7 +67,8 @@ "jsdom": "^9.11.0", "mocha": "^3.2.0", "mocha-webpack": "^0.7.0", - "react-addons-test-utils": "^15.4.2" + "react-addons-test-utils": "^15.5.1", + "sinon": "^2.2.0" }, "optionalDependencies": { "raml2html": "4.0.5" diff --git a/webpack.test.config.js b/webpack.test.config.js index 518835d4..e4bd3ae3 100644 --- a/webpack.test.config.js +++ b/webpack.test.config.js @@ -1,5 +1,15 @@ const config = require('./webpack.config') config.target = 'node' +config.externals = config.externals.concat([ + 'react/lib/ExecutionEnvironment', + 'react/lib/ReactContext', + 'react/addons', + 'react-test-renderer/shallow', + 'react-dom/test-utils', + 'canvas', + 'bufferutil', + 'utf-8-validate' +]) module.exports = config