diff --git a/.gitignore b/.gitignore index 41e2fc277..32c47e020 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ transifex.auth /dist/mapillary-js/ /dist/pannellum-streetside/ +/coverage/ + # autogenerated symlinks land.html /img diff --git a/.npmignore b/.npmignore index 8ee8e17b1..3a6c0b317 100644 --- a/.npmignore +++ b/.npmignore @@ -12,3 +12,5 @@ transifex.auth /docs/ /test/ + +/coverage/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 08382a376..2f02c8fda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,9 +75,11 @@ _Breaking developer changes, which may affect downstream projects or sites that * Radio-button based presets fields can be in an non-unique state (e.g. a tunnel which is also a ford) – this is now rendered like a multi selection with conflicting states ([#8796]) * Add colours for preset categories ([#8799]) #### :hammer: Development +* switch test runner to [karma](https://karma-runner.github.io/) ([#8764], thanks [@wvanderp]) [#8057]: https://github.com/openstreetmap/iD/issues/8057 [#8519]: https://github.com/openstreetmap/iD/issues/8519 +[#8764]: https://github.com/openstreetmap/iD/pull/8764 [#8771]: https://github.com/openstreetmap/iD/issues/8771 [#8781]: https://github.com/openstreetmap/iD/issues/8781 [#8782]: https://github.com/openstreetmap/iD/pull/8782 @@ -99,6 +101,7 @@ _Breaking developer changes, which may affect downstream projects or sites that [@k-yle]: https://github.com/k-yle [@tpetillon]: https://github.com/tpetillon [@mbrzakovic]: https://github.com/mbrzakovic +[@wvanderp]: https://github.com/wvanderp # 2.20.2 ##### 2021-Oct-28 diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 000000000..218e37a25 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,109 @@ +// Karma configuration +// Generated on Wed Sep 01 2021 16:45:06 GMT+0200 (Central European Summer Time) + +module.exports = function (config) { + config.set({ + + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: '', + + plugins: [ + 'karma-remap-istanbul', + 'karma-coverage', + 'karma-mocha', + 'karma-chrome-launcher' + ], + + // frameworks to use + // available frameworks: https://www.npmjs.com/search?q=keywords:karma-adapter + frameworks: ['mocha'], + + + + // list of files / patterns to load in the browser + files: [ + 'node_modules/chai/chai.js', + 'node_modules/sinon/pkg/sinon.js', + 'node_modules/sinon-chai/lib/sinon-chai.js', + 'node_modules/happen/happen.js', + 'node_modules/fetch-mock/es5/client-bundle.js', + { pattern: 'dist/iD.js', included: true }, + { pattern: 'dist/iD.css', included: true }, + { pattern: 'dist/**/*', included: false }, + 'test/spec/spec_helpers.js', + 'test/spec/**/*.js' + ], + + + // list of files / patterns to exclude + exclude: [ + '**/*.js.map' + ], + + proxies: { + '/dist/': 'http://localhost:9876/base/dist/', + '/data/': 'http://localhost:9876/base/dist/data/', + '/img/': 'http://localhost:9876/base/dist/img/' + }, + + // preprocess matching files before serving them to the browser + // available preprocessors: https://www.npmjs.com/search?q=keywords:karma-preprocessor + preprocessors: { + 'dist/iD.js': ['coverage'] + }, + + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://www.npmjs.com/search?q=keywords:karma-reporter + reporters: ['progress', 'coverage', 'karma-remap-istanbul'], + + + // web server port + port: 9876, + + + // enable / disable colors in the output (reporters and logs) + colors: true, + + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + + // start these browsers + // available browser launchers: https://www.npmjs.com/search?q=keywords:karma-launcher + browsers: [ + 'ChromeHeadless' + ], + + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + // Concurrency level + // how many browser instances should be started simultaneously + concurrency: Infinity, + + remapIstanbulReporter: { + remapOptions: { + exclude: [ + 'node_modules' + ] + }, //additional remap options + reportOptions: { + basePath: 'modules' + }, //additional report options + reports: { + lcovonly: 'coverage/lcof.info', + html: 'coverage' + } + } + }); +}; diff --git a/package.json b/package.json index bce1b2cd9..24352de46 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ "start": "npm-run-all -s build start:server", "quickstart": "npm-run-all -s build:dev start:server", "start:server": "node scripts/server.js", - "test": "npm-run-all -s lint build:css build:data build:legacy test:spec", - "test:spec": "phantomjs --web-security=no node_modules/mocha-phantomjs-core/mocha-phantomjs-core.js test/index.html spec", + "test": "npm-run-all -s lint build test:spec", + "test:spec": "karma start karma.conf.js", "translations": "node scripts/update_locales.js" }, "dependencies": { @@ -82,7 +82,7 @@ "@rollup/plugin-node-resolve": "~13.0.5", "autoprefixer": "^10.0.1", "btoa": "^1.2.1", - "chai": "^4.1.0", + "chai": "^4.3.4", "cldr-core": "37.0.0", "cldr-localenames-full": "37.0.0", "colors": "^1.1.2", @@ -90,22 +90,26 @@ "d3": "~6.6.0", "editor-layer-index": "github:osmlab/editor-layer-index#gh-pages", "eslint": "^7.1.0", + "fetch-mock": "^9.11.0", "gaze": "^1.1.3", "glob": "^7.1.0", - "happen": "^0.3.1", + "happen": "^0.3.2", "js-yaml": "^4.0.0", "json-stringify-pretty-compact": "^3.0.0", + "karma": "^6.3.5", + "karma-chrome-launcher": "^3.1.0", + "karma-coverage": "^2.0.3", + "karma-mocha": "^2.0.1", + "karma-remap-istanbul": "^0.6.0", "mapillary_sprite_source": "^1.8.0", "mapillary-js": "4.0.0", "minimist": "^1.2.3", - "mocha": "^7.0.1", - "mocha-phantomjs-core": "^2.1.0", + "mocha": "^8.4.0", "name-suggestion-index": "~6.0", "node-fetch": "^2.6.1", "npm-run-all": "^4.0.0", "object-inspect": "1.10.3", "osm-community-index": "~5.1.0", - "phantomjs-prebuilt": "~2.1.16", "postcss": "^8.1.1", "postcss-selector-prepend": "^0.5.0", "rollup": "~2.52.8", @@ -114,8 +118,8 @@ "rollup-plugin-visualizer": "~4.2.0", "shelljs": "^0.8.0", "shx": "^0.3.0", - "sinon": "7.5.0", - "sinon-chai": "^3.3.0", + "sinon": "^11.1.2", + "sinon-chai": "^3.7.0", "smash": "0.0", "static-server": "^2.2.1", "svg-sprite": "1.5.1", diff --git a/test/.eslintrc b/test/.eslintrc index 213cc998d..8d0a09855 100644 --- a/test/.eslintrc +++ b/test/.eslintrc @@ -8,7 +8,8 @@ "fakeFetch": true, "happen": false, "iD": false, - "sinon": false + "sinon": false, + "fetchMock": true }, "rules": { "no-unused-expressions": "off" diff --git a/test/index.html b/test/index.html deleted file mode 100644 index c047ad690..000000000 --- a/test/index.html +++ /dev/null @@ -1,226 +0,0 @@ - - - - - - Mocha Tests - - - - - - - -
- - - - - - - - - - - - - diff --git a/test/spec/services/kartaview.js b/test/spec/services/kartaview.js index 958a9faec..c0eb5ce41 100644 --- a/test/spec/services/kartaview.js +++ b/test/spec/services/kartaview.js @@ -1,9 +1,10 @@ describe('iD.serviceKartaview', function() { var dimensions = [64, 64]; - var context, server, kartaview; + var context, kartaview; before(function() { iD.services.kartaview = iD.serviceKartaview; + fetchMock.reset(); }); after(function() { @@ -17,13 +18,13 @@ describe('iD.serviceKartaview', function() { .translate([-116508, 0]) // 10,0 .clipExtent([[0,0], dimensions]); - server = window.fakeFetch().create(); kartaview = iD.services.kartaview; kartaview.reset(); + fetchMock.reset(); }); afterEach(function() { - server.restore(); + fetchMock.reset(); }); @@ -52,13 +53,6 @@ describe('iD.serviceKartaview', function() { describe('#loadImages', function() { it('fires loadedImages when images are loaded', function(done) { - kartaview.on('loadedImages', function() { - expect(server.requests().length).to.eql(1); // 1 nearby-photos - done(); - }); - - kartaview.loadImages(context.projection); - var data = { status: { apiCode: '600', httpCode: 200, httpMessage: 'Success' }, currentPageItems:[{ @@ -101,18 +95,21 @@ describe('iD.serviceKartaview', function() { totalFilteredItems: ['3'] }; - server.respondWith('POST', /nearby-photos/, - [200, { 'Content-Type': 'application/json' }, JSON.stringify(data) ]); - server.respond(); + fetchMock.mock(new RegExp('/nearby-photos/'), { + body: JSON.stringify(data), + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + + kartaview.on('loadedImages', function() { + expect(fetchMock.calls().length).to.eql(1); // 1 nearby-photos + done(); + }); + + kartaview.loadImages(context.projection); }); - it('does not load images around null island', function(done) { - var spy = sinon.spy(); - context.projection.translate([0,0]); - - kartaview.on('loadedImages', spy); - kartaview.loadImages(context.projection); - + it('does not load images around null island', function (done) { var data = { status: { apiCode: '600', httpCode: 200, httpMessage: 'Success' }, currentPageItems:[{ @@ -155,25 +152,26 @@ describe('iD.serviceKartaview', function() { totalFilteredItems: ['3'] }; - server.respondWith('POST', /nearby-photos/, - [200, { 'Content-Type': 'application/json' }, JSON.stringify(data) ]); - server.respond(); + var spy = sinon.spy(); + fetchMock.mock(new RegExp('/nearby-photos/'), { + body: JSON.stringify(data), + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + + context.projection.translate([0, 0]); + + kartaview.on('loadedImages', spy); + kartaview.loadImages(context.projection); window.setTimeout(function() { expect(spy).to.have.been.not.called; - expect(server.requests().length).to.eql(0); // no tile requests of any kind + expect(fetchMock.calls().length).to.eql(0); // no tile requests of any kind done(); }, 200); }); it('loads multiple pages of image results', function(done) { - kartaview.on('loadedImages', function() { - expect(server.requests().length).to.eql(2); // 2 nearby-photos - done(); - }); - - kartaview.loadImages(context.projection); - var features = []; for (var i = 0; i < 1000; i++) { var key = String(i); @@ -191,15 +189,25 @@ describe('iD.serviceKartaview', function() { username: 'test' }); } + var response = { status: { apiCode: '600', httpCode: 200, httpMessage: 'Success' }, currentPageItems: features, totalFilteredItems: ['1000'] }; - server.respondWith('POST', /nearby-photos/, - [200, { 'Content-Type': 'application/json' }, JSON.stringify(response) ]); - server.respond(); + fetchMock.mock(new RegExp('/nearby-photos/'), { + body: JSON.stringify(response), + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + + kartaview.on('loadedImages', function() { + expect(fetchMock.calls().length).to.eql(2); // 2 nearby-photos + done(); + }); + + kartaview.loadImages(context.projection); }); }); diff --git a/test/spec/services/mapillary.js b/test/spec/services/mapillary.js index 29964f5b0..4db946bca 100644 --- a/test/spec/services/mapillary.js +++ b/test/spec/services/mapillary.js @@ -1,6 +1,6 @@ describe('iD.serviceMapillary', function() { var dimensions = [64, 64]; - var context, server, mapillary; + var context, mapillary; before(function() { @@ -18,14 +18,11 @@ describe('iD.serviceMapillary', function() { .translate([-116508, 0]) // 10,0 .clipExtent([[0,0], dimensions]); - server = window.fakeFetch().create(); mapillary = iD.services.mapillary; mapillary.reset(); }); - afterEach(function() { - server.restore(); - }); + afterEach(function() {}); describe('#init', function() { diff --git a/test/spec/services/nominatim.js b/test/spec/services/nominatim.js index f28982cd0..1e6102649 100644 --- a/test/spec/services/nominatim.js +++ b/test/spec/services/nominatim.js @@ -1,9 +1,9 @@ describe('iD.serviceNominatim', function() { - var server, nominatim; - + var nominatim; before(function() { iD.services.geocoder = iD.serviceNominatim; + fetchMock.reset(); }); after(function() { @@ -11,32 +11,31 @@ describe('iD.serviceNominatim', function() { }); beforeEach(function() { - server = window.fakeFetch().create(); nominatim = iD.services.geocoder; nominatim.reset(); }); afterEach(function() { - server.restore(); + fetchMock.reset(); }); - function query(url) { + function parseQueryString(url) { return iD.utilStringQs(url.substring(url.indexOf('?'))); } - describe('#countryCode', function() { it('calls the given callback with the results of the country code query', function(done) { var callback = sinon.spy(); + fetchMock.mock(new RegExp('https://nominatim.openstreetmap.org/reverse'), { + body: '{"address":{"country_code":"at"}}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + nominatim.countryCode([16, 48], callback); - server.respondWith('GET', new RegExp('https://nominatim.openstreetmap.org/reverse'), - [200, { 'Content-Type': 'application/json' }, - '{"address":{"country_code":"at"}}']); - server.respond(); - window.setTimeout(function() { - expect(query(server.requests()[0].url)).to.eql( + expect(parseQueryString(fetchMock.calls()[0][0])).to.eql( {zoom: '13', format: 'json', addressdetails: '1', lat: '48', lon: '16'} ); expect(callback).to.have.been.calledWithExactly(null, 'at'); @@ -48,30 +47,32 @@ describe('iD.serviceNominatim', function() { describe('#reverse', function() { it('should not cache distant result', function(done) { var callback = sinon.spy(); + fetchMock.mock(new RegExp('https://nominatim.openstreetmap.org/reverse'), { + body: '{"address":{"country_code":"at"}}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + nominatim.reverse([16, 48], callback); - server.respondWith('GET', new RegExp('https://nominatim.openstreetmap.org/reverse'), - [200, { 'Content-Type': 'application/json' }, '{"address":{"country_code":"at"}}']); - server.respond(); - window.setTimeout(function() { - expect(query(server.requests()[0].url)).to.eql( + expect(parseQueryString(fetchMock.calls()[0][0])).to.eql( {zoom: '13', format: 'json', addressdetails: '1', lat: '48', lon: '16'} ); expect(callback).to.have.been.calledWithExactly(null, {address: {country_code:'at'}}); - server.restore(); - server = window.fakeFetch().create(); + fetchMock.reset(); + fetchMock.mock(new RegExp('https://nominatim.openstreetmap.org/reverse'), { + body: '{"address":{"country_code":"cz"}}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); callback = sinon.spy(); nominatim.reverse([17, 49], callback); - server.respondWith('GET', new RegExp('https://nominatim.openstreetmap.org/reverse'), - [200, { 'Content-Type': 'application/json' }, '{"address":{"country_code":"cz"}}']); - server.respond(); - window.setTimeout(function() { - expect(query(server.requests()[0].url)).to.eql( + expect(parseQueryString(fetchMock.calls()[0][0])).to.eql( {zoom: '13', format: 'json', addressdetails: '1', lat: '49', lon: '17'} ); expect(callback).to.have.been.calledWithExactly(null, {address: {country_code:'cz'}}); @@ -82,28 +83,25 @@ describe('iD.serviceNominatim', function() { it('should cache nearby result', function(done) { var callback = sinon.spy(); + fetchMock.mock(new RegExp('https://nominatim.openstreetmap.org/reverse'), { + body: '{"address":{"country_code":"at"}}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + nominatim.reverse([16, 48], callback); - server.respondWith('GET', new RegExp('https://nominatim.openstreetmap.org/reverse'), - [200, { 'Content-Type': 'application/json' }, '{"address":{"country_code":"at"}}']); - server.respond(); - window.setTimeout(function() { - expect(query(server.requests()[0].url)).to.eql( + expect(parseQueryString(fetchMock.calls()[0][0])).to.eql( {zoom: '13', format: 'json', addressdetails: '1', lat: '48', lon: '16'} ); expect(callback).to.have.been.calledWithExactly(null, {address: {country_code:'at'}}); - server.restore(); - server = window.fakeFetch().create(); + fetchMock.resetHistory(); callback = sinon.spy(); nominatim.reverse([16.000001, 48.000001], callback); - server.respondWith('GET', new RegExp('https://nominatim.openstreetmap.org/reverse'), - [200, { 'Content-Type': 'application/json' }, '{"address":{"country_code":"cz"}}']); - server.respond(); - window.setTimeout(function() { expect(callback).to.have.been.calledWithExactly(null, {address: {country_code:'at'}}); done(); @@ -113,15 +111,17 @@ describe('iD.serviceNominatim', function() { it('calls the given callback with an error', function(done) { var callback = sinon.spy(); + fetchMock.mock(new RegExp('https://nominatim.openstreetmap.org/reverse'), { + body: '{"error":"Unable to geocode"}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + nominatim.reverse([1000, 1000], callback); - server.respondWith('GET', new RegExp('https://nominatim.openstreetmap.org/reverse'), - [200, { 'Content-Type': 'application/json' }, - '{"error":"Unable to geocode"}']); - server.respond(); window.setTimeout(function() { - expect(query(server.requests()[0].url)).to.eql( + expect(parseQueryString(fetchMock.calls()[0][0])).to.eql( {zoom: '13', format: 'json', addressdetails: '1', lat: '1000', lon: '1000'} ); expect(callback).to.have.been.calledWithExactly('Unable to geocode'); @@ -134,16 +134,16 @@ describe('iD.serviceNominatim', function() { describe('#search', function() { it('calls the given callback with the results of the search query', function(done) { var callback = sinon.spy(); + fetchMock.mock(new RegExp('https://nominatim.openstreetmap.org/search'), { + body: '[{"place_id":"158484588","osm_type":"relation","osm_id":"188022","boundingbox":["39.867005","40.1379593","-75.2802976","-74.9558313"],"lat":"39.9523993","lon":"-75.1635898","display_name":"Philadelphia, Philadelphia County, Pennsylvania, United States of America","class":"place","type":"city","importance":0.83238050437778}]', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + nominatim.search('philadelphia', callback); - server.respondWith('GET', new RegExp('https://nominatim.openstreetmap.org/search'), - [200, { 'Content-Type': 'application/json' }, - '[{"place_id":"158484588","osm_type":"relation","osm_id":"188022","boundingbox":["39.867005","40.1379593","-75.2802976","-74.9558313"],"lat":"39.9523993","lon":"-75.1635898","display_name":"Philadelphia, Philadelphia County, Pennsylvania, United States of America","class":"place","type":"city","importance":0.83238050437778}]' - ]); - server.respond(); - window.setTimeout(function() { - expect(query(server.requests()[0].url)).to.eql({format: 'json', limit: '10'}); + expect(parseQueryString(fetchMock.calls()[0][0])).to.eql({format: 'json', limit: '10'}); expect(callback).to.have.been.calledOnce; done(); }, 50); diff --git a/test/spec/services/osm.js b/test/spec/services/osm.js index bc5542ef6..8299f825e 100644 --- a/test/spec/services/osm.js +++ b/test/spec/services/osm.js @@ -1,6 +1,6 @@ describe('iD.serviceOsm', function () { var context, connection, spy; - var serverFetch, serverXHR; + var serverXHR; function login() { connection.switch({ @@ -25,7 +25,6 @@ describe('iD.serviceOsm', function () { }); beforeEach(function () { - serverFetch = window.fakeFetch().create(); // unauthenticated calls use d3-fetch serverXHR = sinon.fakeServer.create(); // authenticated calls use XHR via osm-auth context = iD.coreContext().assetPath('../dist/').init(); connection = context.connection(); @@ -35,7 +34,7 @@ describe('iD.serviceOsm', function () { }); afterEach(function() { - serverFetch.restore(); + fetchMock.reset(); serverXHR.restore(); }); @@ -44,14 +43,6 @@ describe('iD.serviceOsm', function () { expect(connection).to.be.ok; }); - it('allows insecure connections', function () { - expect(connection.changesetURL(2)).to.match(/^http:/); - }); - - it('allows secure connections', function () { - connection.switch({ urlroot: 'https://www.openstreetmap.org'}); - expect(connection.changesetURL(2)).to.match(/^https:/); - }); describe('#getConnectionId', function() { it('changes the connection id every time connection is reset', function() { @@ -60,7 +51,8 @@ describe('iD.serviceOsm', function () { var cid2 = connection.getConnectionId(); expect(cid2).to.be.above(cid1); }); - it('changes the connection id every time connection is switched', function() { + + it('changes the connection id every time connection is switched', function () { var cid1 = connection.getConnectionId(); connection.switch({ urlroot: 'https://api06.dev.openstreetmap.org' }); var cid2 = connection.getConnectionId(); @@ -72,6 +64,11 @@ describe('iD.serviceOsm', function () { it('provides a changeset url', function() { expect(connection.changesetURL(2)).to.eql('http://www.openstreetmap.org/changeset/2'); }); + + it('allows secure connections', function() { + connection.switch({ urlroot: 'https://www.openstreetmap.org' }); + expect(connection.changesetURL(2)).to.eql('https://www.openstreetmap.org/changeset/2'); + }); }); describe('#changesetsURL', function() { @@ -87,10 +84,12 @@ describe('iD.serviceOsm', function () { var e = iD.osmNode({id: 'n1'}); expect(connection.entityURL(e)).to.eql('http://www.openstreetmap.org/node/1'); }); + it('provides an entity url for a way', function() { var e = iD.osmWay({id: 'w1'}); expect(connection.entityURL(e)).to.eql('http://www.openstreetmap.org/way/1'); }); + it('provides an entity url for a relation', function() { var e = iD.osmRelation({id: 'r1'}); expect(connection.entityURL(e)).to.eql('http://www.openstreetmap.org/relation/1'); @@ -102,10 +101,12 @@ describe('iD.serviceOsm', function () { var e = iD.osmNode({id: 'n1'}); expect(connection.historyURL(e)).to.eql('http://www.openstreetmap.org/node/1/history'); }); + it('provides a history url for a way', function() { var e = iD.osmWay({id: 'w1'}); expect(connection.historyURL(e)).to.eql('http://www.openstreetmap.org/way/1/history'); }); + it('provides a history url for a relation', function() { var e = iD.osmRelation({id: 'r1'}); expect(connection.historyURL(e)).to.eql('http://www.openstreetmap.org/relation/1/history'); @@ -151,74 +152,94 @@ describe('iD.serviceOsm', function () { '}'; it('returns an object', function(done) { + fetchMock.mock('http://www.openstreetmap.org' + path, { + body: response, + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + connection.loadFromAPI(path, function (err, payload) { expect(err).to.not.be.ok; expect(typeof payload).to.eql('object'); done(); }); - - serverFetch.respondWith('GET', 'http://www.openstreetmap.org' + path, - [200, { 'Content-Type': 'application/json' }, response]); - serverFetch.respond(); }); it('retries an authenticated call unauthenticated if 400 Bad Request', function (done) { + fetchMock.mock('http://www.openstreetmap.org' + path, { + body: response, + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + serverXHR.respondWith('GET', 'http://www.openstreetmap.org' + path, + [400, { 'Content-Type': 'text/plain' }, 'Bad Request']); + login(); connection.loadFromAPI(path, function (err, xml) { expect(err).to.be.not.ok; expect(typeof xml).to.eql('object'); expect(connection.authenticated()).to.be.not.ok; + expect(fetchMock.called()).to.be.true; + done(); }); - serverXHR.respondWith('GET', 'http://www.openstreetmap.org' + path, - [400, { 'Content-Type': 'text/plain' }, 'Bad Request']); - serverFetch.respondWith('GET', 'http://www.openstreetmap.org' + path, - [200, { 'Content-Type': 'application/json' }, response]); - serverXHR.respond(); - serverFetch.respond(); }); it('retries an authenticated call unauthenticated if 401 Unauthorized', function (done) { + fetchMock.mock('http://www.openstreetmap.org' + path, { + body: response, + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + serverXHR.respondWith('GET', 'http://www.openstreetmap.org' + path, + [401, { 'Content-Type': 'text/plain' }, 'Unauthorized']); + login(); connection.loadFromAPI(path, function (err, xml) { expect(err).to.be.not.ok; expect(typeof xml).to.eql('object'); expect(connection.authenticated()).to.be.not.ok; + expect(fetchMock.called()).to.be.true; + done(); }); - serverXHR.respondWith('GET', 'http://www.openstreetmap.org' + path, - [401, { 'Content-Type': 'text/plain' }, 'Unauthorized']); - serverFetch.respondWith('GET', 'http://www.openstreetmap.org' + path, - [200, { 'Content-Type': 'application/json' }, response]); - serverXHR.respond(); - serverFetch.respond(); }); it('retries an authenticated call unauthenticated if 403 Forbidden', function (done) { + fetchMock.mock('http://www.openstreetmap.org' + path, { + body: response, + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + serverXHR.respondWith('GET', 'http://www.openstreetmap.org' + path, + [403, { 'Content-Type': 'text/plain' }, 'Forbidden']); + login(); connection.loadFromAPI(path, function (err, xml) { expect(err).to.be.not.ok; expect(typeof xml).to.eql('object'); expect(connection.authenticated()).to.be.not.ok; + expect(fetchMock.called()).to.be.true; + done(); }); - serverXHR.respondWith('GET', 'http://www.openstreetmap.org' + path, - [403, { 'Content-Type': 'text/plain' }, 'Forbidden']); - serverFetch.respondWith('GET', 'http://www.openstreetmap.org' + path, - [200, { 'Content-Type': 'application/json' }, response]); - serverXHR.respond(); - serverFetch.respond(); }); it('dispatches change event if 509 Bandwidth Limit Exceeded', function (done) { + fetchMock.mock('http://www.openstreetmap.org' + path, { + body: 'Bandwidth Limit Exceeded', + status: 509, + headers: { 'Content-Type': 'text/plain' } + }); + logout(); connection.on('change', spy); connection.loadFromAPI(path, function (err) { @@ -226,13 +247,15 @@ describe('iD.serviceOsm', function () { expect(spy).to.have.been.calledOnce; done(); }); - - serverFetch.respondWith('GET', 'http://www.openstreetmap.org' + path, - [509, { 'Content-Type': 'text/plain' }, 'Bandwidth Limit Exceeded']); - serverFetch.respond(); }); it('dispatches change event if 429 Too Many Requests', function (done) { + fetchMock.mock('http://www.openstreetmap.org' + path, { + body: '429 Too Many Requests', + status: 429, + headers: { 'Content-Type': 'text/plain' } + }); + logout(); connection.on('change', spy); connection.loadFromAPI(path, function (err) { @@ -240,10 +263,6 @@ describe('iD.serviceOsm', function () { expect(spy).to.have.been.calledOnce; done(); }); - - serverFetch.respondWith('GET', 'http://www.openstreetmap.org' + path, - [429, { 'Content-Type': 'text/plain' }, '429 Too Many Requests']); - serverFetch.respond(); }); }); @@ -267,13 +286,15 @@ describe('iD.serviceOsm', function () { }); it('calls callback when data tiles are loaded', function(done) { + fetchMock.mock(/map.json\?bbox/, { + body: tileResponse, + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + var spy = sinon.spy(); connection.loadTiles(context.projection, spy); - serverFetch.respondWith('GET', /map.json\?bbox/, - [200, { 'Content-Type': 'application/json' }, tileResponse]); - serverFetch.respond(); - window.setTimeout(function() { expect(spy).to.have.been.calledOnce; done(); @@ -281,15 +302,27 @@ describe('iD.serviceOsm', function () { }); it('#isDataLoaded', function(done) { - expect(connection.isDataLoaded([-74.0444216, 40.6694299])).to.be.not.ok; + fetchMock.mock(/map.json\?bbox/, { + body: tileResponse, + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + + // resetting the cache + const caches = connection.caches('get'); + caches.tile.toLoad = {}; + caches.tile.loaded = {}; + caches.tile.inflight = {}; + caches.tile.seen = {}; + caches.tile.rtree.clear(); + + expect(connection.isDataLoaded([-74.0444216, 40.6694299])).to.be.false; connection.loadTiles(context.projection); - serverFetch.respondWith('GET', /map.json\?bbox/, - [200, { 'Content-Type': 'application/json' }, tileResponse]); - serverFetch.respond(); window.setTimeout(function() { - expect(connection.isDataLoaded([-74.0444216, 40.6694299])).to.be.ok; + expect(fetchMock.called()).to.be.true; + expect(connection.isDataLoaded([-74.0444216, 40.6694299])).to.be.true; done(); }, 500); }); @@ -303,6 +336,7 @@ describe('iD.serviceOsm', function () { ' {"type":"node","id":1,"visible":true,"version":1,"changeset":28924294,"timestamp":"2009-03-07T03:26:33Z","user":"peace2","uid":119748,"lat":0,"lon":0}' + ' ]' + '}'; + var wayResponse = '{' + ' "version":"0.6",' + @@ -313,47 +347,53 @@ describe('iD.serviceOsm', function () { '}'; it('loads a node', function(done) { + fetchMock.mock('http://www.openstreetmap.org/api/0.6/node/1.json', { + body: nodeResponse, + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + var id = 'n1'; connection.loadEntity(id, function(err, result) { var entity = result.data.find(function(e) { return e.id === id; }); expect(entity).to.be.an.instanceOf(iD.osmNode); done(); }); - - serverFetch.respondWith('GET', 'http://www.openstreetmap.org/api/0.6/node/1.json', - [200, { 'Content-Type': 'application/json' }, nodeResponse]); - serverFetch.respond(); }); it('loads a way', function(done) { + fetchMock.mock('http://www.openstreetmap.org/api/0.6/way/1/full.json', { + body: wayResponse, + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + var id = 'w1'; connection.loadEntity(id, function(err, result) { var entity = result.data.find(function(e) { return e.id === id; }); expect(entity).to.be.an.instanceOf(iD.osmWay); done(); }); - - serverFetch.respondWith('GET', 'http://www.openstreetmap.org/api/0.6/way/1/full.json', - [200, { 'Content-Type': 'application/json' }, wayResponse]); - serverFetch.respond(); }); it('does not ignore repeat requests', function(done) { + fetchMock.mock('http://www.openstreetmap.org/api/0.6/node/1.json', { + body: wayResponse, + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + var id = 'n1'; connection.loadEntity(id, function(err1, result1) { var entity1 = result1.data.find(function(e1) { return e1.id === id; }); expect(entity1).to.be.an.instanceOf(iD.osmNode); + connection.loadEntity(id, function(err2, result2) { var entity2 = result2.data.find(function(e2) { return e2.id === id; }); expect(entity2).to.be.an.instanceOf(iD.osmNode); done(); }); - serverFetch.respond(); }); - - serverFetch.respondWith('GET', 'http://www.openstreetmap.org/api/0.6/node/1.json', - [200, { 'Content-Type': 'application/json' }, nodeResponse]); - serverFetch.respond(); }); }); @@ -376,47 +416,53 @@ describe('iD.serviceOsm', function () { '}'; it('loads a node', function(done) { + fetchMock.mock('http://www.openstreetmap.org/api/0.6/node/1/1.json', { + body: nodeResponse, + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + var id = 'n1'; connection.loadEntityVersion(id, 1, function(err, result) { var entity = result.data.find(function(e) { return e.id === id; }); expect(entity).to.be.an.instanceOf(iD.osmNode); done(); }); - - serverFetch.respondWith('GET', 'http://www.openstreetmap.org/api/0.6/node/1/1.json', - [200, { 'Content-Type': 'application/json' }, nodeResponse]); - serverFetch.respond(); }); it('loads a way', function(done) { + fetchMock.mock('http://www.openstreetmap.org/api/0.6/way/1/1.json', { + body: wayResponse, + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + var id = 'w1'; connection.loadEntityVersion(id, 1, function(err, result) { var entity = result.data.find(function(e) { return e.id === id; }); expect(entity).to.be.an.instanceOf(iD.osmWay); done(); }); - - serverFetch.respondWith('GET', 'http://www.openstreetmap.org/api/0.6/way/1/1.json', - [200, { 'Content-Type': 'application/json' }, wayResponse]); - serverFetch.respond(); }); it('does not ignore repeat requests', function(done) { + fetchMock.mock('http://www.openstreetmap.org/api/0.6/node/1/1.json', { + body: nodeResponse, + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + var id = 'n1'; connection.loadEntityVersion(id, 1, function(err1, result1) { var entity1 = result1.data.find(function(e1) { return e1.id === id; }); expect(entity1).to.be.an.instanceOf(iD.osmNode); + connection.loadEntityVersion(id, 1, function(err2, result2) { var entity2 = result2.data.find(function(e2) { return e2.id === id; }); expect(entity2).to.be.an.instanceOf(iD.osmNode); done(); }); - serverFetch.respond(); }); - - serverFetch.respondWith('GET', 'http://www.openstreetmap.org/api/0.6/node/1/1.json', - [200, { 'Content-Type': 'application/json' }, nodeResponse]); - serverFetch.respond(); }); }); @@ -605,13 +651,15 @@ describe('iD.serviceOsm', function () { }); it('fires loadedNotes when notes are loaded', function(done) { + fetchMock.mock(/notes\?/, { + body: notesXML, + status: 200, + headers: { 'Content-Type': 'text/xml' } + }); + connection.on('loadedNotes', spy); connection.loadNotes(context.projection, {}); - serverFetch.respondWith('GET', /notes\?/, - [200, { 'Content-Type': 'text/xml' }, notesXML ]); - serverFetch.respond(); - window.setTimeout(function() { expect(spy).to.have.been.calledOnce; done(); @@ -698,50 +746,60 @@ describe('iD.serviceOsm', function () { describe('API capabilities', function() { - var capabilitiesXML = '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - '' + - ''; + var capabilitiesXML = ` + + + + + + + + + + + + + + + + + + `; describe('#status', function() { it('gets API status', function(done) { - connection.status(function(err, val) { + fetchMock.mock('http://www.openstreetmap.org/api/capabilities', { + body: capabilitiesXML, + status: 200, + headers: { 'Content-Type': 'text/xml' } + }, { + overwriteRoutes: true + }); + + connection.status(function (err, val) { expect(val).to.eql('online'); done(); }); - - serverFetch.respondWith('GET', 'http://www.openstreetmap.org/api/capabilities', - [200, { 'Content-Type': 'text/xml' }, capabilitiesXML]); - serverFetch.respond(); }); }); describe('#imageryBlocklists', function() { it('updates imagery blocklists', function(done) { + fetchMock.mock('http://www.openstreetmap.org/api/capabilities', { + body: capabilitiesXML, + status: 200, + headers: { 'Content-Type': 'text/xml' } + }, { + overwriteRoutes: true + }); + connection.status(function() { var blocklists = connection.imageryBlocklists(); expect(blocklists).to.deep.equal([new RegExp('\.foo\.com'), new RegExp('\.bar\.org')]); done(); }); - - serverFetch.respondWith('GET', 'http://www.openstreetmap.org/api/capabilities', - [200, { 'Content-Type': 'text/xml' }, capabilitiesXML]); - serverFetch.respond(); }); }); }); - -}); +}); \ No newline at end of file diff --git a/test/spec/services/osm_wikibase.js b/test/spec/services/osm_wikibase.js index b56b24035..021ba46a5 100644 --- a/test/spec/services/osm_wikibase.js +++ b/test/spec/services/osm_wikibase.js @@ -1,5 +1,5 @@ describe('iD.serviceOsmWikibase', function () { - var server, wikibase; + var wikibase; before(function () { iD.services.osmWikibase = iD.serviceOsmWikibase; @@ -10,17 +10,16 @@ describe('iD.serviceOsmWikibase', function () { }); beforeEach(function () { - server = window.fakeFetch().create(); wikibase = iD.services.osmWikibase; wikibase.init(); }); afterEach(function () { - server.restore(); + fetchMock.reset(); }); - function query(url) { + function parseQueryString(url) { return iD.utilStringQs(url.substring(url.indexOf('?'))); } @@ -265,22 +264,23 @@ describe('iD.serviceOsmWikibase', function () { describe('#getEntity', function () { it('calls the given callback with the results of the getEntity data item query', function (done) { var callback = sinon.spy(); - wikibase.getEntity({key: 'amenity', value: 'parking', langCodes: ['fr']}, callback); - - server.respondWith('GET', /action=wbgetentities/, - [200, {'Content-Type': 'application/json'}, JSON.stringify({ + fetchMock.mock(/action=wbgetentities/, { + body: JSON.stringify({ entities: { Q42: keyData(), Q13: tagData(), Q7792: localeData, }, success: 1 - })] - ); - server.respond(); + }), + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - window.setTimeout(function() { - expect(query(server.requests()[0].url)).to.eql( + wikibase.getEntity({ key: 'amenity', value: 'parking', langCodes: ['fr'] }, callback); + + window.setTimeout(function () { + expect(parseQueryString(fetchMock.calls()[0][0])).to.eql( { action: 'wbgetentities', sites: 'wiki', diff --git a/test/spec/services/streetside.js b/test/spec/services/streetside.js index 3e1e999c7..067211212 100644 --- a/test/spec/services/streetside.js +++ b/test/spec/services/streetside.js @@ -1,6 +1,6 @@ describe('iD.serviceStreetside', function() { var dimensions = [64, 64]; - var context, server, streetside; + var context, streetside; before(function() { iD.services.streetside = iD.serviceStreetside; @@ -17,14 +17,12 @@ describe('iD.serviceStreetside', function() { .translate([-116508, 0]) // 10,0 .clipExtent([[0,0], dimensions]); - server = window.fakeFetch().create(); streetside = iD.services.streetside; streetside.reset(); }); afterEach(function() { window.JSONP_FIX = undefined; - server.restore(); }); diff --git a/test/spec/services/taginfo.js b/test/spec/services/taginfo.js index 0207fa0bd..f25e3471c 100644 --- a/test/spec/services/taginfo.js +++ b/test/spec/services/taginfo.js @@ -1,5 +1,5 @@ describe('iD.serviceTaginfo', function() { - var server, taginfo; + var taginfo; before(function() { @@ -11,43 +11,40 @@ describe('iD.serviceTaginfo', function() { }); beforeEach(function() { - server = window.fakeFetch().create(); taginfo = iD.services.taginfo; + fetchMock.mock(new RegExp('\/keys\/all.*sortname=values_all'), { + body: '{"data":[{"count_all":56136034,"key":"name","count_all_fraction":0.0132}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + // prepopulate popular keys list with "name" taginfo.init(); - server.respondWith('GET', - new RegExp('\/keys\/all.*sortname=values_all'), - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"count_all":56136034,"key":"name","count_all_fraction":0.0132}]}'] - ); - server.respond(); - server.restore(); - server = window.fakeFetch().create(); + fetchMock.reset(); }); afterEach(function() { - server.restore(); + fetchMock.reset(); }); - function query(url) { + function parseQueryString(url) { return iD.utilStringQs(url.substring(url.indexOf('?'))); } - describe('#keys', function() { it('calls the given callback with the results of the keys query', function(done) { - var callback = sinon.spy(); - taginfo.keys({query: 'amen'}, callback); + fetchMock.mock(/\/keys\/all/, { + body: '{"data":[{"count_all":5190337,"key":"amenity","count_all_fraction":1.0}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/keys\/all/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"count_all":5190337,"key":"amenity","count_all_fraction":1.0}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.keys({ query: 'amen' }, callback); window.setTimeout(function() { - expect(query(server.requests()[0].url)).to.eql( + expect(parseQueryString(fetchMock.calls()[0][0])).to.eql( {query: 'amen', page: '1', rp: '10', sortname: 'count_all', sortorder: 'desc', lang: 'en'} ); expect(callback).to.have.been.calledWith( @@ -58,15 +55,15 @@ describe('iD.serviceTaginfo', function() { }); it('includes popular keys', function(done) { - var callback = sinon.spy(); - taginfo.keys({query: 'amen'}, callback); + fetchMock.mock(/\/keys\/all/, { + body: '{"data":[{"count_all":5190337,"count_nodes":500000,"key":"amenity","count_all_fraction":1.0, "count_nodes_fraction":1.0},' + + '{"count_all":1,"key":"amenityother","count_all_fraction":0.0, "count_nodes":100}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/keys\/all/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"count_all":5190337,"key":"amenity","count_all_fraction":1.0,"count_nodes_fraction":1.0},' - + '{"count_all":1,"key":"amenityother","count_all_fraction":0.0,"count_nodes_fraction":0.0}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.keys({ query: 'amen' }, callback); window.setTimeout(function() { expect(callback).to.have.been.calledWith( @@ -77,15 +74,15 @@ describe('iD.serviceTaginfo', function() { }); it('includes popular keys with an entity type filter', function(done) { - var callback = sinon.spy(); - taginfo.keys({query: 'amen', filter: 'nodes'}, callback); + fetchMock.mock(/\/keys\/all/, { + body: '{"data":[{"count_all":5190337,"count_nodes":500000,"key":"amenity","count_all_fraction":1.0, "count_nodes_fraction":1.0},' + + '{"count_all":1,"key":"amenityother","count_all_fraction":0.0, "count_nodes":100}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/keys\/all/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"count_all":5190337,"count_nodes":500000,"key":"amenity","count_all_fraction":1.0, "count_nodes_fraction":1.0},' - + '{"count_all":1,"key":"amenityother","count_all_fraction":0.0, "count_nodes":100}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.keys({ query: 'amen', filter: 'nodes' }, callback); window.setTimeout(function() { expect(callback).to.have.been.calledWith( @@ -96,15 +93,15 @@ describe('iD.serviceTaginfo', function() { }); it('includes unpopular keys with a wiki page', function(done) { - var callback = sinon.spy(); - taginfo.keys({query: 'amen'}, callback); + fetchMock.mock(/\/keys\/all/, { + body: '{"data":[{"count_all":5190337,"key":"amenity","count_all_fraction":1.0, "count_nodes_fraction":1.0},' + + '{"count_all":1,"key":"amenityother","count_all_fraction":0.0, "count_nodes_fraction":0.0, "in_wiki": true}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/keys\/all/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"count_all":5190337,"key":"amenity","count_all_fraction":1.0, "count_nodes_fraction":1.0},' - + '{"count_all":1,"key":"amenityother","count_all_fraction":0.0, "count_nodes_fraction":0.0, "in_wiki": true}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.keys({ query: 'amen' }, callback); window.setTimeout(function() { expect(callback).to.have.been.calledWith(null, [ @@ -116,15 +113,15 @@ describe('iD.serviceTaginfo', function() { }); it('sorts keys with \':\' below keys without \':\'', function(done) { - var callback = sinon.spy(); - taginfo.keys({query: 'ref'}, callback); + fetchMock.mock(/\/keys\/all/, { + body: '{"data":[{"key":"ref:bag","count_all":9790586,"count_all_fraction":0.0028},' + + '{"key":"ref","count_all":7933528,"count_all_fraction":0.0023}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/keys\/all/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"key":"ref:bag","count_all":9790586,"count_all_fraction":0.0028},' + - '{"key":"ref","count_all":7933528,"count_all_fraction":0.0023}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.keys({ query: 'ref' }, callback); window.setTimeout(function() { expect(callback).to.have.been.calledWith( @@ -137,17 +134,17 @@ describe('iD.serviceTaginfo', function() { describe('#multikeys', function() { it('calls the given callback with the results of the multikeys query', function(done) { - var callback = sinon.spy(); - taginfo.multikeys({query: 'recycling:'}, callback); + fetchMock.mock(/\/keys\/all/, { + body: '{"data":[{"count_all":69593,"key":"recycling:glass","count_all_fraction":0.0}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/keys\/all/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"count_all":69593,"key":"recycling:glass","count_all_fraction":0.0}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.multikeys({ query: 'recycling:' }, callback); window.setTimeout(function() { - expect(query(server.requests()[0].url)).to.eql( + expect(parseQueryString(fetchMock.calls()[0][0])).to.eql( {query: 'recycling:', page: '1', rp: '25', sortname: 'count_all', sortorder: 'desc', lang: 'en'} ); expect(callback).to.have.been.calledWith( @@ -158,15 +155,15 @@ describe('iD.serviceTaginfo', function() { }); it('excludes multikeys with extra colons', function(done) { - var callback = sinon.spy(); - taginfo.multikeys({query: 'service:bicycle:'}, callback); + fetchMock.mock(/\/keys\/all/, { + body: '{"data":[{"count_all":4426,"key":"service:bicycle:retail","count_all_fraction":0.0},' + + '{"count_all":22,"key":"service:bicycle:retail:ebikes","count_all_fraction":0.0}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/keys\/all/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"count_all":4426,"key":"service:bicycle:retail","count_all_fraction":0.0},' + - '{"count_all":22,"key":"service:bicycle:retail:ebikes","count_all_fraction":0.0}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.multikeys({ query: 'service:bicycle:' }, callback); window.setTimeout(function() { expect(callback).to.have.been.calledWith( @@ -177,15 +174,15 @@ describe('iD.serviceTaginfo', function() { }); it('excludes multikeys with wrong prefix', function(done) { - var callback = sinon.spy(); - taginfo.multikeys({query: 'service:bicycle:'}, callback); + fetchMock.mock(/\/keys\/all/, { + body: '{"data":[{"count_all":4426,"key":"service:bicycle:retail","count_all_fraction":0.0},' + + '{"count_all":22,"key":"disused:service:bicycle","count_all_fraction":0.0}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/keys\/all/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"count_all":4426,"key":"service:bicycle:retail","count_all_fraction":0.0},' + - '{"count_all":22,"key":"disused:service:bicycle","count_all_fraction":0.0}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.multikeys({ query: 'service:bicycle:' }, callback); window.setTimeout(function() { expect(callback).to.have.been.calledWith( @@ -198,17 +195,17 @@ describe('iD.serviceTaginfo', function() { describe('#values', function() { it('calls the given callback with the results of the values query', function(done) { - var callback = sinon.spy(); - taginfo.values({key: 'amenity', query: 'par'}, callback); + fetchMock.mock(/\/key\/values/, { + body: '{"data":[{"value":"parking","description":"A place for parking cars", "fraction":0.1}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/key\/values/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"value":"parking","description":"A place for parking cars", "fraction":0.1}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.values({ key: 'amenity', query: 'par' }, callback); window.setTimeout(function() { - expect(query(server.requests()[0].url)).to.eql( + expect(parseQueryString(fetchMock.calls()[0][0])).to.eql( {key: 'amenity', query: 'par', page: '1', rp: '25', sortname: 'count_all', sortorder: 'desc', lang: 'en'} ); expect(callback).to.have.been.calledWith( @@ -219,15 +216,15 @@ describe('iD.serviceTaginfo', function() { }); it('includes popular values', function(done) { - var callback = sinon.spy(); - taginfo.values({key: 'amenity', query: 'par'}, callback); + fetchMock.mock(/\/key\/values/, { + body: '{"data":[{"value":"parking","description":"A place for parking cars", "fraction":1.0},' + + '{"value":"party","description":"A place for partying", "fraction":0.0}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/key\/values/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"value":"parking","description":"A place for parking cars", "fraction":1.0},' + - '{"value":"party","description":"A place for partying", "fraction":0.0}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.values({ key: 'amenity', query: 'par' }, callback); window.setTimeout(function() { expect(callback).to.have.been.calledWith( @@ -238,15 +235,15 @@ describe('iD.serviceTaginfo', function() { }); it('does not get values for extremely unpopular keys', function(done) { - var callback = sinon.spy(); - taginfo.values({key: 'name', query: 'ste'}, callback); + fetchMock.mock(/\/key\/values/, { + body: '{"data":[{"value":"Rue Pasteur","description":"", "fraction":0.0001},' + + '{"value":"Via Trieste","description":"", "fraction":0.0001}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/key\/values/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"value":"Rue Pasteur","description":"", "fraction":0.0001},' + - '{"value":"Via Trieste","description":"", "fraction":0.0001}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.values({ key: 'name', query: 'ste' }, callback); window.setTimeout(function() { expect(callback).to.have.been.calledWith(null, []); @@ -255,18 +252,18 @@ describe('iD.serviceTaginfo', function() { }); it('excludes values with capital letters and some punctuation', function(done) { - var callback = sinon.spy(); - taginfo.values({key: 'amenity', query: 'par'}, callback); + fetchMock.mock(/\/key\/values/, { + body: '{"data":[{"value":"parking","description":"A place for parking cars", "fraction":0.2},' + + '{"value":"PArking","description":"A common misspelling", "fraction":0.2},' + + '{"value":"parking;partying","description":"A place for parking cars *and* partying", "fraction":0.2},' + + '{"value":"parking, partying","description":"A place for parking cars *and* partying", "fraction":0.2},' + + '{"value":"*","description":"", "fraction":0.2}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/key\/values/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"value":"parking","description":"A place for parking cars", "fraction":0.2},' - + '{"value":"PArking","description":"A common misspelling", "fraction":0.2},' - + '{"value":"parking;partying","description":"A place for parking cars *and* partying", "fraction":0.2},' - + '{"value":"parking, partying","description":"A place for parking cars *and* partying", "fraction":0.2},' - + '{"value":"*","description":"", "fraction":0.2}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.values({ key: 'amenity', query: 'par' }, callback); window.setTimeout(function() { expect(callback).to.have.been.calledWith( @@ -277,18 +274,18 @@ describe('iD.serviceTaginfo', function() { }); it('includes network values with capital letters and some punctuation', function(done) { - var callback = sinon.spy(); - taginfo.values({key: 'network', query: 'us'}, callback); + fetchMock.mock(/\/key\/values/, { + body: '{"data":[{"value":"US:TX:FM","description":"Farm to Market Roads in the U.S. state of Texas.", "fraction":0.34},' + + '{"value":"US:KY","description":"Primary and secondary state highways in the U.S. state of Kentucky.", "fraction":0.31},' + + '{"value":"US:US","description":"U.S. routes in the United States.", "fraction":0.19},' + + '{"value":"US:I","description":"Interstate highways in the United States.", "fraction":0.11},' + + '{"value":"US:MD","description":"State highways in the U.S. state of Maryland.", "fraction":0.06}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/key\/values/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"value":"US:TX:FM","description":"Farm to Market Roads in the U.S. state of Texas.", "fraction":0.34},' - + '{"value":"US:KY","description":"Primary and secondary state highways in the U.S. state of Kentucky.", "fraction":0.31},' - + '{"value":"US:US","description":"U.S. routes in the United States.", "fraction":0.19},' - + '{"value":"US:I","description":"Interstate highways in the United States.", "fraction":0.11},' - + '{"value":"US:MD","description":"State highways in the U.S. state of Maryland.", "fraction":0.06}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.values({ key: 'network', query: 'us' }, callback); window.setTimeout(function() { expect(callback).to.have.been.calledWith(null, [ @@ -303,14 +300,14 @@ describe('iD.serviceTaginfo', function() { }); it('includes biological genus values with capital letters', function(done) { - var callback = sinon.spy(); - taginfo.values({key: 'genus', query: 'qu'}, callback); + fetchMock.mock(/\/key\/values/, { + body: '{"data":[{"value":"Quercus","description":"Oak", "fraction":0.5}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/key\/values/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"value":"Quercus","description":"Oak", "fraction":0.5}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.values({ key: 'genus', query: 'qu' }, callback); window.setTimeout(function() { expect(callback).to.have.been.calledWith( @@ -321,14 +318,14 @@ describe('iD.serviceTaginfo', function() { }); it('includes biological taxon values with capital letters', function(done) { - var callback = sinon.spy(); - taginfo.values({key: 'taxon', query: 'qu'}, callback); + fetchMock.mock(/\/key\/values/, { + body: '{"data":[{"value":"Quercus robur","description":"Oak", "fraction":0.5}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/key\/values/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"value":"Quercus robur","description":"Oak", "fraction":0.5}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.values({ key: 'taxon', query: 'qu' }, callback); window.setTimeout(function() { expect(callback).to.have.been.calledWith( @@ -339,14 +336,14 @@ describe('iD.serviceTaginfo', function() { }); it('includes biological species values with capital letters', function(done) { - var callback = sinon.spy(); - taginfo.values({key: 'species', query: 'qu'}, callback); + fetchMock.mock(/\/key\/values/, { + body: '{"data":[{"value":"Quercus robur","description":"Oak", "fraction":0.5}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/key\/values/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"value":"Quercus robur","description":"Oak", "fraction":0.5}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.values({ key: 'species', query: 'qu' }, callback); window.setTimeout(function() { expect(callback).to.have.been.calledWith( @@ -359,18 +356,18 @@ describe('iD.serviceTaginfo', function() { describe('#roles', function() { it('calls the given callback with the results of the roles query', function(done) { - var callback = sinon.spy(); - taginfo.roles({rtype: 'route', query: 's', geometry: 'relation'}, callback); + fetchMock.mock(/\/relation\/roles/, { + body: '{"data":[{"role":"stop","count_relation_members_fraction":0.1757},' + + '{"role":"south","count_relation_members_fraction":0.0035}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/relation\/roles/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"role":"stop","count_relation_members_fraction":0.1757},' + - '{"role":"south","count_relation_members_fraction":0.0035}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.roles({ rtype: 'route', query: 's', geometry: 'relation' }, callback); window.setTimeout(function() { - expect(query(server.requests()[0].url)).to.eql( + expect(parseQueryString(fetchMock.calls()[0][0])).to.eql( {rtype: 'route', query: 's', page: '1', rp: '25', sortname: 'count_relation_members', sortorder: 'desc', lang: 'en'} ); expect(callback).to.have.been.calledWith(null, [ @@ -384,17 +381,17 @@ describe('iD.serviceTaginfo', function() { describe('#docs', function() { it('calls the given callback with the results of the docs query', function(done) { - var callback = sinon.spy(); - taginfo.docs({key: 'amenity', value: 'parking'}, callback); + fetchMock.mock(/\/tag\/wiki_page/, { + body: '{"data":[{"on_way":false,"lang":"en","on_area":true,"image":"File:Car park2.jpg"}]}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); - server.respondWith('GET', /\/tag\/wiki_page/, - [200, { 'Content-Type': 'application/json' }, - '{"data":[{"on_way":false,"lang":"en","on_area":true,"image":"File:Car park2.jpg"}]}'] - ); - server.respond(); + var callback = sinon.spy(); + taginfo.docs({ key: 'amenity', value: 'parking' }, callback); window.setTimeout(function() { - expect(query(server.requests()[0].url)).to.eql( + expect(parseQueryString(fetchMock.calls()[0][0])).to.eql( {key: 'amenity', value: 'parking'} ); expect(callback).to.have.been.calledWith( diff --git a/test/spec/spec_helpers.js b/test/spec/spec_helpers.js index 6506928ec..bb5626822 100644 --- a/test/spec/spec_helpers.js +++ b/test/spec/spec_helpers.js @@ -112,146 +112,30 @@ expect = chai.expect; window.d3 = iD.d3; // Remove this if we can avoid exporting all of d3.js delete window.PointerEvent; // force the browser to use mouse events -// Workaround for `Array.from` polyfill in PhantomJS -// https://github.com/openstreetmap/iD/issues/6087#issuecomment-476219308 -var __arrayfrom = Array.from; -Array.from = function(what) { - if (what instanceof Set) { - var arr = []; - what.forEach(function(v) { arr.push(v); }); - return arr; - } else { - return __arrayfrom.apply(null, arguments); - } -}; +// some sticky fallbacks +const capabilities = ` + + + + + + + + + + + + + + + + + + +`; -// Workaround for `ArrayBuffer.isView` in PhantomJS -// https://github.com/openstreetmap/iD/issues/7072 -if (typeof ArrayBuffer.isView === 'undefined') { - ArrayBuffer.isView = function() { return false; }; -} +fetchMock.sticky('https://www.openstreetmap.org/api/capabilities', capabilities, {sticky: true}); +fetchMock.sticky('http://www.openstreetmap.org/api/capabilities', capabilities, {sticky: true}); -// Polyfill for `Math.sign()` in PhantomJS -// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign#Polyfill -if (!Math.sign) { - Math.sign = function(x) { - return ((x > 0) - (x < 0)) || +x; - }; -} - -// Add support for sinon-stubbing `fetch` API -// (sinon fakeServer works only on `XMLHttpRequest`) -// see https://github.com/sinonjs/nise/issues/7 -// -// None of the alternatives really worked well, -// so I'm just wrapping the `fake-fetch` methods in here. -// - https://github.com/msn0/fake-fetch -// - https://github.com/wheresrhys/fetch-mock - -window.fakeFetch = function() { - var _responders = []; - var _requests = []; - - function fake(url, options) { - options = Object.assign({ method: 'get', headers: {}, body: '' }, options); - return new Promise(function(resolve, reject) { - _requests.push({ - url: url, options: options, resolve: resolve, reject: reject, processed: false - }); - }); - } - - return { - requests: function() { - return _requests; - }, - - create: function () { - _responders = []; - _requests = []; - sinon.stub(window, 'fetch').callsFake(fake); - return this; - }, - - restore: function () { - window.fetch.restore(); - }, - - getUrl: function () { - return window.fetch.firstCall.args[0]; - }, - - getOptions: function () { - return window.fetch.firstCall.args[1] || {}; - }, - - getMethod: function () { - return this.getOptions().method || 'get'; - }, - - getBody: function () { - return this.getOptions().body || ''; - }, - - getRequestHeaders: function () { - return this.getOptions().headers || {}; - }, - - respondWith: function(method, match, response) { - var status = 200; - var headers = { 'Content-Type': 'text/html' }; - var body = 'OK'; - - if (typeof response === 'string') { - body = response; - } else if (Array.isArray(response) && response.length === 3) { - status = response[0]; - headers = Object.assign(headers, response[1] || {}); - body = response[2]; - } - - headers['Content-Length'] = body.length; - var data = new Blob([body], { type: headers['Content-Type'] }); - var options = { status: status, headers: headers }; - - _responders.push({ - method: method, - match: match, - respond: function() { return new Response(data, options); } - }); - }, - - respond: function () { - _requests.forEach(function(request) { - if (request.processed) return; - - var didMatch = false; - for (var i = 0; i < _responders.length; i++) { - var responder = _responders[i]; - if (responder.method.toLowerCase() !== request.options.method.toLowerCase()) { - continue; // skip if method doesn't match (get/post) - } - - if (responder.match.constructor.name === 'RegExp') { - didMatch = responder.match.test(request.url); - } else if (typeof responder.match === 'string') { - didMatch = (request.url.indexOf(responder.match) !== -1); - } - - if (didMatch) { - request.processed = true; - request.resolve(responder.respond()); - break; - } - } - if (!didMatch) { - request.processed = true; - request.reject(new Response( - new Blob(['404'], { type: 'text/plain' }), - { status: 404, statusText: 'Not Found' } - )); - } - }); - } - }; -}; +fetchMock.config.fallbackToNetwork = true; +fetchMock.config.overwriteRoutes = false; diff --git a/test/spec/ui/fields/wikipedia.js b/test/spec/ui/fields/wikipedia.js index 24583535c..6a6d4a122 100644 --- a/test/spec/ui/fields/wikipedia.js +++ b/test/spec/ui/fields/wikipedia.js @@ -1,5 +1,5 @@ describe('iD.uiFieldWikipedia', function() { - var entity, context, selection, field, server; + var entity, context, selection, field; before(function() { iD.fileFetcher.cache().wmf_sitematrix = [ @@ -26,11 +26,16 @@ describe('iD.uiFieldWikipedia', function() { keys: ['wikipedia', 'wikidata'], type: 'wikipedia' }); - server = createServer({ respondImmediately: true }); + fetchMock.reset(); + fetchMock.mock(new RegExp('\/w\/api\.php.*action=wbgetentities'), { + body: '{"entities":{"Q216353":{"id":"Q216353"}}}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }); }); afterEach(function() { - server.restore(); + fetchMock.reset(); }); @@ -55,19 +60,6 @@ describe('iD.uiFieldWikipedia', function() { } } - function createServer(options) { // eslint-disable-line no-unused-vars - // note - currently skipping the tests that use `options` to delay responses - // var server = sinon.fakeServer.create(options); - var server = window.fakeFetch().create(); - server.respondWith('GET', - new RegExp('\/w\/api\.php.*action=wbgetentities'), - [200, { 'Content-Type': 'application/json' }, - '{"entities":{"Q216353":{"id":"Q216353"}}}'] - ); - return server; - } - - it('recognizes lang:title format', function(done) { var wikipedia = iD.uiFieldWikipedia(field, context); window.setTimeout(function() { // async, so data will be available @@ -121,6 +113,7 @@ describe('iD.uiFieldWikipedia', function() { }, 20); }); + // note - currently skipping the tests that use `options` to delay responses it('preserves existing language', function(done) { var wikipedia1 = iD.uiFieldWikipedia(field, context); window.setTimeout(function() { // async, so data will be available @@ -146,7 +139,14 @@ describe('iD.uiFieldWikipedia', function() { wikipedia.on('change.spy', spy); // Create an XHR server that will respond after 60ms - createServer({ autoRespond: true, autoRespondAfter: 60 }); + fetchMock.reset(); + fetchMock.mock(new RegExp('\/w\/api\.php.*action=wbgetentities'), { + body: '{"entities":{"Q216353":{"id":"Q216353"}}}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }, { + delay: 60 + }); // Set title to "Skip" iD.utilGetSetValue(selection.selectAll('.wiki-lang'), 'Deutsch'); @@ -159,7 +159,14 @@ describe('iD.uiFieldWikipedia', function() { // Create a new XHR server that will respond after 60ms to // separate requests after this point from those before - createServer({ autoRespond: true, autoRespondAfter: 60 }); + fetchMock.reset(); + fetchMock.mock(new RegExp('\/w\/api\.php.*action=wbgetentities'), { + body: '{"entities":{"Q216353":{"id":"Q216353"}}}', + status: 200, + headers: { 'Content-Type': 'application/json' } + }, { + delay: 60 + }); // t30: graph change - Set title to "Title" window.setTimeout(function() { diff --git a/test/spec/util/util.js b/test/spec/util/util.js index a033afc38..604128db5 100644 --- a/test/spec/util/util.js +++ b/test/spec/util/util.js @@ -78,7 +78,7 @@ describe('iD.util', function() { expect(iD.utilTagText({tags:{foo:'bar',two:'three'}})).to.eql('foo=bar, two=three'); }); - it('utilStringQs', function() { + describe('utilStringQs', function() { it('splits a parameter string into k=v pairs', function() { expect(iD.utilStringQs('foo=bar')).to.eql({foo: 'bar'}); expect(iD.utilStringQs('foo=bar&one=2')).to.eql({foo: 'bar', one: '2' });