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); + }); + });