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