From 669cad74f6ce3a63251a87086cc400be1a7d054f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Minh=20Nguy=E1=BB=85n?= Date: Sat, 18 Jul 2015 01:14:17 -0500 Subject: [PATCH 1/4] Add wikidata asynchronously based on wikipedia ref #2680 --- index.html | 1 + js/id/services/wikidata.js | 22 ++++++++++++++++++++++ js/id/ui/preset/wikipedia.js | 20 ++++++++++++++++---- test/index.html | 1 + test/spec/ui/preset/wikipedia.js | 4 +++- 5 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 js/id/services/wikidata.js diff --git a/index.html b/index.html index 80d184a2f..cb539b9cc 100644 --- a/index.html +++ b/index.html @@ -43,6 +43,7 @@ + diff --git a/js/id/services/wikidata.js b/js/id/services/wikidata.js new file mode 100644 index 000000000..0718e2fdd --- /dev/null +++ b/js/id/services/wikidata.js @@ -0,0 +1,22 @@ +iD.services.wikidata = function() { + var wiki = {}, + endpoint = 'https://www.wikidata.org/w/api.php?'; + + // Given a Wikipedia language and article title, return an array of + // corresponding Wikidata entities. + wiki.itemsByTitle = function(lang, title, callback) { + lang = lang || 'en'; + d3.jsonp(endpoint + iD.util.qsString({ + action: 'wbgetentities', + format: 'json', + sites: lang.replace(/-/g, '_') + 'wiki', + titles: title, + languages: 'en', // shrink response by filtering to one language + callback: '{callback}' + }), function(data) { + callback(title, data.entities || {}); + }); + }; + + return wiki; +}; diff --git a/js/id/ui/preset/wikipedia.js b/js/id/ui/preset/wikipedia.js index b14ac9655..0d2317160 100644 --- a/js/id/ui/preset/wikipedia.js +++ b/js/id/ui/preset/wikipedia.js @@ -1,6 +1,7 @@ iD.ui.preset.wikipedia = function(field, context) { var dispatch = d3.dispatch('change'), wikipedia = iD.services.wikipedia(), + wikidata = iD.services.wikidata(), link, entity, lang, title; function i(selection) { @@ -88,7 +89,8 @@ iD.ui.preset.wikipedia = function(field, context) { var value = title.value(), m = value.match(/https?:\/\/([-a-z]+)\.wikipedia\.org\/(?:wiki|\1-[-a-z]+)\/([^#]+)(?:#(.+))?/), l = m && _.find(iD.data.wikipedia, function(d) { return m[1] === d[2]; }), - anchor; + anchor, + syncTags = {}; if (l) { // Normalize title http://www.mediawiki.org/wiki/API:Query#Title_normalization @@ -107,9 +109,19 @@ iD.ui.preset.wikipedia = function(field, context) { title.value(value); } - var t = {}; - t[field.key] = value ? language()[2] + ':' + value : undefined; - dispatch.change(t); + syncTags[field.key] = value ? language()[2] + ':' + value : undefined; + dispatch.change(syncTags); + + if (value && language()[2]) { + wikidata.itemsByTitle(language()[2], value, function (title, entities) { + var qids = entities && Object.keys(entities), + asyncTags = {}; + asyncTags.wikidata = qids && _.find(qids, function (id) { + return id.match(/^Q\d+$/); + }); + dispatch.change(asyncTags); + }); + } } i.tags = function(tags) { diff --git a/test/index.html b/test/index.html index 15775fc36..24aa35963 100644 --- a/test/index.html +++ b/test/index.html @@ -48,6 +48,7 @@ + diff --git a/test/spec/ui/preset/wikipedia.js b/test/spec/ui/preset/wikipedia.js index f90ab6e09..fc3ff1b41 100644 --- a/test/spec/ui/preset/wikipedia.js +++ b/test/spec/ui/preset/wikipedia.js @@ -27,7 +27,9 @@ describe('iD.ui.preset.wikipedia', function() { happen.once(selection.selectAll('.wiki-lang').node(), {type: 'change'}); wikipedia.on('change', function(tags) { - expect(tags).to.eql({wikipedia: 'de:Title'}); + expect(tags).to.satisfy(function (tags) { + return tags.wikipedia === 'de:Title' || 'wikidata' in tags; + }); }); selection.selectAll('.wiki-title').value('Title'); From 04462750911f2103abc5aec276fa1b9ec9eae473 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 25 May 2016 21:46:14 -0400 Subject: [PATCH 2/4] Rename field function --- js/id/ui/preset/localized.js | 12 +++++++----- js/id/ui/preset/textarea.js | 8 ++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/js/id/ui/preset/localized.js b/js/id/ui/preset/localized.js index 657331fbe..f41d2c191 100644 --- a/js/id/ui/preset/localized.js +++ b/js/id/ui/preset/localized.js @@ -4,7 +4,7 @@ iD.ui.preset.localized = function(field, context) { input, localizedInputs, wikiTitles, entity; - function i(selection) { + function localized(selection) { input = selection.selectAll('.localized-main') .data([0]); @@ -203,7 +203,7 @@ iD.ui.preset.localized = function(field, context) { .value(function(d) { return d.value; }); } - i.tags = function(tags) { + localized.tags = function(tags) { // Fetch translations from wikipedia if (tags.wikipedia && !wikiTitles) { wikiTitles = {}; @@ -228,13 +228,15 @@ iD.ui.preset.localized = function(field, context) { localizedInputs.call(render, postfixed.reverse()); }; - i.focus = function() { + localized.focus = function() { input.node().focus(); }; - i.entity = function(_) { + localized.entity = function(_) { + if (!arguments.length) return entity; entity = _; + return localized; }; - return d3.rebind(i, dispatch, 'on'); + return d3.rebind(localized, dispatch, 'on'); }; diff --git a/js/id/ui/preset/textarea.js b/js/id/ui/preset/textarea.js index b609aed72..92d18aa63 100644 --- a/js/id/ui/preset/textarea.js +++ b/js/id/ui/preset/textarea.js @@ -2,7 +2,7 @@ iD.ui.preset.textarea = function(field) { var dispatch = d3.dispatch('change'), input; - function i(selection) { + function textarea(selection) { input = selection.selectAll('textarea') .data([0]); @@ -25,13 +25,13 @@ iD.ui.preset.textarea = function(field) { }; } - i.tags = function(tags) { + textarea.tags = function(tags) { input.value(tags[field.key] || ''); }; - i.focus = function() { + textarea.focus = function() { input.node().focus(); }; - return d3.rebind(i, dispatch, 'on'); + return d3.rebind(textarea, dispatch, 'on'); }; From 0135fbe4a6135574f2a55d61ecbb836350451013 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 25 May 2016 21:46:38 -0400 Subject: [PATCH 3/4] Update async wikidata tag if graph unchanged --- Makefile | 1 + data/presets.yaml | 2 +- data/presets/fields.json | 4 +++ data/presets/fields/wikipedia.json | 1 + js/id/ui/preset/wikipedia.js | 41 +++++++++++++++++++++--------- test/spec/ui/preset/wikipedia.js | 19 ++++++++------ 6 files changed, 47 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index 55ed971e2..c22d5a80a 100644 --- a/Makefile +++ b/Makefile @@ -67,6 +67,7 @@ dist/iD.js: \ js/id/services/mapillary.js \ js/id/services/nominatim.js \ js/id/services/taginfo.js \ + js/id/services/wikidata.js \ js/id/services/wikipedia.js \ js/id/util.js \ js/id/util/session_mutex.js \ diff --git a/data/presets.yaml b/data/presets.yaml index 4c0534523..137cddb63 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -1045,7 +1045,7 @@ en: # 'width=*' label: Width (Meters) wikipedia: - # 'wikipedia=*' + # 'wikipedia=*, wikidata=*' label: Wikipedia presets: address: diff --git a/data/presets/fields.json b/data/presets/fields.json index ee2a6a2f1..203c632a5 100644 --- a/data/presets/fields.json +++ b/data/presets/fields.json @@ -1414,6 +1414,10 @@ }, "wikipedia": { "key": "wikipedia", + "keys": [ + "wikipedia", + "wikidata" + ], "type": "wikipedia", "icon": "wikipedia", "universal": true, diff --git a/data/presets/fields/wikipedia.json b/data/presets/fields/wikipedia.json index 8f4a67d85..466c95520 100644 --- a/data/presets/fields/wikipedia.json +++ b/data/presets/fields/wikipedia.json @@ -1,5 +1,6 @@ { "key": "wikipedia", + "keys": ["wikipedia", "wikidata"], "type": "wikipedia", "icon": "wikipedia", "universal": true, diff --git a/js/id/ui/preset/wikipedia.js b/js/id/ui/preset/wikipedia.js index 0d2317160..9dca790e0 100644 --- a/js/id/ui/preset/wikipedia.js +++ b/js/id/ui/preset/wikipedia.js @@ -4,7 +4,7 @@ iD.ui.preset.wikipedia = function(field, context) { wikidata = iD.services.wikidata(), link, entity, lang, title; - function i(selection) { + function wiki(selection) { var langcombo = d3.combobox() .fetcher(function(value, cb) { var v = value.toLowerCase(); @@ -109,22 +109,37 @@ iD.ui.preset.wikipedia = function(field, context) { title.value(value); } - syncTags[field.key] = value ? language()[2] + ':' + value : undefined; + syncTags.wikipedia = value ? language()[2] + ':' + value : undefined; + syncTags.wikidata = undefined; + dispatch.change(syncTags); + + // attempt asynchronous update of wikidata tag.. if (value && language()[2]) { - wikidata.itemsByTitle(language()[2], value, function (title, entities) { - var qids = entities && Object.keys(entities), - asyncTags = {}; - asyncTags.wikidata = qids && _.find(qids, function (id) { + var initGraph = context.graph(); + wikidata.itemsByTitle(language()[2], value, function (title, data) { + var qids = data && Object.keys(data), + currGraph = context.graph(); + + // only do this if graph is unchanged.. + if (initGraph !== currGraph) return; + + var currEntity = context.entity(entity.id), + currTags = _.clone(currEntity.tags); + + currTags.wikidata = qids && _.find(qids, function (id) { return id.match(/^Q\d+$/); }); - dispatch.change(asyncTags); + + var annotation = t('operations.change_tags.annotation'); + context.overwrite(iD.actions.ChangeTags(currEntity.id, currTags), annotation); + dispatch.change(currTags); }); } } - i.tags = function(tags) { + wiki.tags = function(tags) { var value = tags[field.key] || '', m = value.match(/([^:]+):([^#]+)(?:#(.+))?/), l = m && _.find(iD.data.wikipedia, function(d) { return m[1] === d[2]; }), @@ -143,7 +158,7 @@ iD.ui.preset.wikipedia = function(field, context) { } } link.attr('href', 'https://' + m[1] + '.wikipedia.org/wiki/' + - m[2].replace(/ /g, '_') + (anchor ? ('#' + anchor) : '')); + m[2].replace(/ /g, '_') + (anchor ? ('#' + anchor) : '')); // unrecognized value format } else { @@ -155,13 +170,15 @@ iD.ui.preset.wikipedia = function(field, context) { } }; - i.entity = function(_) { + wiki.entity = function(_) { + if (!arguments.length) return entity; entity = _; + return wiki; }; - i.focus = function() { + wiki.focus = function() { title.node().focus(); }; - return d3.rebind(i, dispatch, 'on'); + return d3.rebind(wiki, dispatch, 'on'); }; diff --git a/test/spec/ui/preset/wikipedia.js b/test/spec/ui/preset/wikipedia.js index fc3ff1b41..721050035 100644 --- a/test/spec/ui/preset/wikipedia.js +++ b/test/spec/ui/preset/wikipedia.js @@ -1,13 +1,16 @@ describe('iD.ui.preset.wikipedia', function() { - var selection, field; + var entity, context, selection, field, entity; beforeEach(function() { + entity = iD.Node({id: 'n12345'}); + context = iD(); + context.history().merge([entity]); selection = d3.select(document.createElement('div')); - field = iD().presets(iD.data.presets).presets().field('wikipedia'); + field = context.presets(iD.data.presets).presets().field('wikipedia'); }); it('recognizes lang:title format', function() { - var wikipedia = iD.ui.preset.wikipedia(field, {}); + var wikipedia = iD.ui.preset.wikipedia(field, context).entity(entity); selection.call(wikipedia); wikipedia.tags({wikipedia: 'en:Title'}); expect(selection.selectAll('.wiki-lang').value()).to.equal('English'); @@ -16,11 +19,11 @@ describe('iD.ui.preset.wikipedia', function() { }); it('sets a new value', function() { - var wikipedia = iD.ui.preset.wikipedia(field, {}); + var wikipedia = iD.ui.preset.wikipedia(field, context).entity(entity); selection.call(wikipedia); wikipedia.on('change', function(tags) { - expect(tags).to.eql({wikipedia: undefined}); + expect(tags).to.eql({wikipedia: undefined, wikidata: undefined}); }); selection.selectAll('.wiki-lang').value('Deutsch'); @@ -37,7 +40,7 @@ describe('iD.ui.preset.wikipedia', function() { }); it('recognizes pasted URLs', function() { - var wikipedia = iD.ui.preset.wikipedia(field, {}); + var wikipedia = iD.ui.preset.wikipedia(field, context).entity(entity); selection.call(wikipedia); selection.selectAll('.wiki-title').value('http://de.wikipedia.org/wiki/Title'); @@ -48,10 +51,10 @@ describe('iD.ui.preset.wikipedia', function() { }); it('preserves existing language', function() { - selection.call(iD.ui.preset.wikipedia(field, {})); + selection.call(iD.ui.preset.wikipedia(field, context).entity(entity)); selection.selectAll('.wiki-lang').value('Deutsch'); - var wikipedia = iD.ui.preset.wikipedia(field, {}); + var wikipedia = iD.ui.preset.wikipedia(field, context).entity(entity); selection.call(wikipedia); wikipedia.tags({}); From db6234b2fc25402a7f59436c21a64e5328190d4d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 26 May 2016 16:16:37 -0400 Subject: [PATCH 4/4] Initiate async wikidata lookup onchange only (not onblur), add tests --- js/id/ui/preset/wikipedia.js | 54 ++++++++----- test/spec/ui/preset/wikipedia.js | 130 ++++++++++++++++++++++++++----- 2 files changed, 145 insertions(+), 39 deletions(-) diff --git a/js/id/ui/preset/wikipedia.js b/js/id/ui/preset/wikipedia.js index 9dca790e0..b129eb9ba 100644 --- a/js/id/ui/preset/wikipedia.js +++ b/js/id/ui/preset/wikipedia.js @@ -55,7 +55,7 @@ iD.ui.preset.wikipedia = function(field, context) { title .call(titlecombo) - .on('blur', change) + .on('blur', blur) .on('change', change); link = selection.selectAll('a.wiki-link') @@ -82,10 +82,14 @@ iD.ui.preset.wikipedia = function(field, context) { function changeLang() { lang.value(language()[1]); - change(); + change(true); } - function change() { + function blur() { + change(true); + } + + function change(skipWikidata) { var value = title.value(), m = value.match(/https?:\/\/([-a-z]+)\.wikipedia\.org\/(?:wiki|\1-[-a-z]+)\/([^#]+)(?:#(.+))?/), l = m && _.find(iD.data.wikipedia, function(d) { return m[1] === d[2]; }), @@ -110,33 +114,43 @@ iD.ui.preset.wikipedia = function(field, context) { } syncTags.wikipedia = value ? language()[2] + ':' + value : undefined; - syncTags.wikidata = undefined; + if (!skipWikidata) { + syncTags.wikidata = undefined; + } dispatch.change(syncTags); + if (skipWikidata || !value || !language()[2]) return; + // attempt asynchronous update of wikidata tag.. - if (value && language()[2]) { - var initGraph = context.graph(); - wikidata.itemsByTitle(language()[2], value, function (title, data) { - var qids = data && Object.keys(data), - currGraph = context.graph(); + var initEntityId = entity.id, + initWikipedia = context.entity(initEntityId).tags.wikipedia; - // only do this if graph is unchanged.. - if (initGraph !== currGraph) return; + wikidata.itemsByTitle(language()[2], value, function (title, data) { + // 1. most recent change was a tag change + var annotation = t('operations.change_tags.annotation'), + currAnnotation = context.history().undoAnnotation(); + if (currAnnotation !== annotation) return; - var currEntity = context.entity(entity.id), - currTags = _.clone(currEntity.tags); + // 2. same entity exists and still selected + var selectedIds = context.selectedIDs(), + currEntityId = selectedIds.length > 0 && selectedIds[0]; + if (currEntityId !== initEntityId) return; - currTags.wikidata = qids && _.find(qids, function (id) { - return id.match(/^Q\d+$/); - }); + // 3. wikipedia value has not changed + var currTags = _.clone(context.entity(currEntityId).tags), + qids = data && Object.keys(data); + if (initWikipedia !== currTags.wikipedia) return; - var annotation = t('operations.change_tags.annotation'); - context.overwrite(iD.actions.ChangeTags(currEntity.id, currTags), annotation); - dispatch.change(currTags); + // ok to coalesce the update of wikidata tag into the previous tag change + currTags.wikidata = qids && _.find(qids, function (id) { + return id.match(/^Q\d+$/); }); - } + + context.overwrite(iD.actions.ChangeTags(currEntityId, currTags), annotation); + dispatch.change(currTags); + }); } wiki.tags = function(tags) { diff --git a/test/spec/ui/preset/wikipedia.js b/test/spec/ui/preset/wikipedia.js index 721050035..b0d5764c6 100644 --- a/test/spec/ui/preset/wikipedia.js +++ b/test/spec/ui/preset/wikipedia.js @@ -1,16 +1,46 @@ describe('iD.ui.preset.wikipedia', function() { - var entity, context, selection, field, entity; + var entity, context, selection, field, wikiDelay, selectedId; + + function wikidataStub() { + wikidataStub.itemsByTitle = function(lang, title, callback) { + var data = {Q216353: {id: 'Q216353'}}; + if (wikiDelay) { + window.setTimeout(function () { callback(title, data); }, wikiDelay); + } + else { + callback(title, data); + } + } + return wikidataStub; + } + + function changeTags(changed) { + var annotation = t('operations.change_tags.annotation'); + var tags = _.extend({}, entity.tags, changed); + context.perform(iD.actions.ChangeTags(entity.id, tags), annotation); + } beforeEach(function() { entity = iD.Node({id: 'n12345'}); + selectedId = entity.id; context = iD(); context.history().merge([entity]); selection = d3.select(document.createElement('div')); field = context.presets(iD.data.presets).presets().field('wikipedia'); + wikiDelay = 0; + + sinon.stub(iD.services, 'wikidata', wikidataStub); + sinon.stub(context, 'selectedIDs', function() { return [selectedId]; }); }); + afterEach(function() { + iD.services.wikidata.restore(); + context.selectedIDs.restore(); + }); + + it('recognizes lang:title format', function() { - var wikipedia = iD.ui.preset.wikipedia(field, context).entity(entity); + var wikipedia = iD.ui.preset.wikipedia(field, context); selection.call(wikipedia); wikipedia.tags({wikipedia: 'en:Title'}); expect(selection.selectAll('.wiki-lang').value()).to.equal('English'); @@ -18,46 +48,108 @@ describe('iD.ui.preset.wikipedia', function() { expect(selection.selectAll('.wiki-link').attr('href')).to.equal('https://en.wikipedia.org/wiki/Title'); }); - it('sets a new value', function() { + it('sets language, value, wikidata', function() { var wikipedia = iD.ui.preset.wikipedia(field, context).entity(entity); + wikipedia.on('change', changeTags); selection.call(wikipedia); - wikipedia.on('change', function(tags) { - expect(tags).to.eql({wikipedia: undefined, wikidata: undefined}); - }); - + var spy = sinon.spy(); + wikipedia.on('change.spy', spy); selection.selectAll('.wiki-lang').value('Deutsch'); - happen.once(selection.selectAll('.wiki-lang').node(), {type: 'change'}); - - wikipedia.on('change', function(tags) { - expect(tags).to.satisfy(function (tags) { - return tags.wikipedia === 'de:Title' || 'wikidata' in tags; - }); - }); + happen.once(selection.selectAll('.wiki-lang').node(), { type: 'change' }); + happen.once(selection.selectAll('.wiki-lang').node(), { type: 'blur' }); + expect(spy.callCount).to.equal(2); + expect(spy.firstCall).to.have.been.calledWith({ wikipedia: undefined }); // on change + expect(spy.secondCall).to.have.been.calledWith({ wikipedia: undefined }); // on blur + spy = sinon.spy(); + wikipedia.on('change.spy', spy); selection.selectAll('.wiki-title').value('Title'); - happen.once(selection.selectAll('.wiki-title').node(), {type: 'change'}); + happen.once(selection.selectAll('.wiki-title').node(), { type: 'change' }); + happen.once(selection.selectAll('.wiki-title').node(), { type: 'blur' }); + expect(spy.callCount).to.equal(3); + expect(spy.firstCall).to.have.been.calledWith({ wikipedia: 'de:Title', wikidata: undefined }); // on change + expect(spy.secondCall).to.have.been.calledWith({ wikipedia: 'de:Title', wikidata: 'Q216353' }); // wikidata async + expect(spy.thirdCall).to.have.been.calledWith({ wikipedia: 'de:Title' }); // on blur }); it('recognizes pasted URLs', function() { var wikipedia = iD.ui.preset.wikipedia(field, context).entity(entity); + wikipedia.on('change', changeTags); selection.call(wikipedia); selection.selectAll('.wiki-title').value('http://de.wikipedia.org/wiki/Title'); - happen.once(selection.selectAll('.wiki-title').node(), {type: 'change'}); - + happen.once(selection.selectAll('.wiki-title').node(), { type: 'change' }); expect(selection.selectAll('.wiki-lang').value()).to.equal('Deutsch'); expect(selection.selectAll('.wiki-title').value()).to.equal('Title'); }); it('preserves existing language', function() { - selection.call(iD.ui.preset.wikipedia(field, context).entity(entity)); + selection.call(iD.ui.preset.wikipedia(field, context)); selection.selectAll('.wiki-lang').value('Deutsch'); - var wikipedia = iD.ui.preset.wikipedia(field, context).entity(entity); + var wikipedia = iD.ui.preset.wikipedia(field, context); selection.call(wikipedia); wikipedia.tags({}); expect(selection.selectAll('.wiki-lang').value()).to.equal('Deutsch'); }); + + it('does not set delayed wikidata tag if wikipedia field has changed', function(done) { + var wikipedia = iD.ui.preset.wikipedia(field, context).entity(entity); + wikipedia.on('change', changeTags); + selection.call(wikipedia); + wikiDelay = 20; + + var spy = sinon.spy(); + wikipedia.on('change.spy', spy); + selection.selectAll('.wiki-lang').value('Deutsch'); + selection.selectAll('.wiki-title').value('Skip'); + happen.once(selection.selectAll('.wiki-title').node(), { type: 'change' }); + happen.once(selection.selectAll('.wiki-title').node(), { type: 'blur' }); + + window.setTimeout(function() { + selection.selectAll('.wiki-title').value('Title'); + happen.once(selection.selectAll('.wiki-title').node(), { type: 'change' }); + happen.once(selection.selectAll('.wiki-title').node(), { type: 'blur' }); + }, 10); + + window.setTimeout(function() { + expect(spy.callCount).to.equal(5); + expect(spy.getCall(0)).to.have.been.calledWith({ wikipedia: 'de:Skip', wikidata: undefined }); // 'Skip' on change + expect(spy.getCall(1)).to.have.been.calledWith({ wikipedia: 'de:Skip' }); // 'Skip' on blur + expect(spy.getCall(2)).to.have.been.calledWith({ wikipedia: 'de:Title', wikidata: undefined }); // 'Title' on change +10ms + expect(spy.getCall(3)).to.have.been.calledWith({ wikipedia: 'de:Title' }); // 'Title' on blur +10ms + // skip delayed wikidata for 'Skip' // 'Skip' wikidata +20ms + expect(spy.getCall(4)).to.have.been.calledWith({ wikipedia: 'de:Title', wikidata: 'Q216353' }); // 'Title' wikidata +40ms + done(); + }, 50); + }); + + it('does not set delayed wikidata tag if selected entity has changed', function(done) { + var wikipedia = iD.ui.preset.wikipedia(field, context).entity(entity); + wikipedia.on('change', changeTags); + selection.call(wikipedia); + wikiDelay = 20; + + var spy = sinon.spy(); + wikipedia.on('change.spy', spy); + selection.selectAll('.wiki-lang').value('Deutsch'); + selection.selectAll('.wiki-title').value('Title'); + happen.once(selection.selectAll('.wiki-title').node(), { type: 'change' }); + happen.once(selection.selectAll('.wiki-title').node(), { type: 'blur' }); + + window.setTimeout(function() { + selectedId = 'w-123'; // user clicked on something else.. + }, 10); + + window.setTimeout(function() { + expect(spy.callCount).to.equal(2); + expect(spy.getCall(0)).to.have.been.calledWith({ wikipedia: 'de:Title', wikidata: undefined }); // 'Title' on change + expect(spy.getCall(1)).to.have.been.calledWith({ wikipedia: 'de:Title' }); // 'Title' on blur + // wikidata tag not changed because another entity is now selected + done(); + }, 50); + }); + });