diff --git a/core/code/search.js b/core/code/search.js index 275351531..a75769e7c 100644 --- a/core/code/search.js +++ b/core/code/search.js @@ -1,4 +1,4 @@ -/* global L -- eslint */ +/* global IITC -- eslint */ /** * Provides functionality for the search system within the application. @@ -6,516 +6,138 @@ * You can implement your own result provider by listening to the search hook: * ```window.addHook('search', function(query) {});```. * - * The `query` object has the following members: - * - `term`: The term for which the user has searched. - * - `confirmed`: A boolean indicating if the user has pressed enter after searching. - * You should not search online or do heavy processing unless the user has confirmed the search term. - * - `addResult(result)`: A method to add a result to the query. - * - * The `result` object can have the following members (`title` is required, as well as one of `position` and `bounds`): - * - `title`: The label for this result. Will be interpreted as HTML, so make sure to escape properly. - * - `description`: Secondary information for this result. Will be interpreted as HTML, so make sure to escape properly. - * - `position`: A L.LatLng object describing the position of this result. - * - `bounds`: A L.LatLngBounds object describing the bounds of this result. - * - `layer`: An ILayer to be added to the map when the user selects this search result. - * Will be generated if not set. Set to `null` to prevent the result from being added to the map. - * - `icon`: A URL to an icon to display in the result list. Should be 12x12 pixels. - * - `onSelected(result, event)`: A handler to be called when the result is selected. - * May return `true` to prevent the map from being repositioned. You may reposition the map yourself or do other work. - * - `onRemove(result)`: A handler to be called when the result is removed from the map - * (because another result has been selected or the search was cancelled by the user). - * @namespace window.search - */ -window.search = { - lastSearch: null, -}; - -/** - * Represents a search query. - * - * @memberof window.search - * @class - * @name window.search.Query - * @param {string} term - The search term. - * @param {boolean} confirmed - Indicates if the search is confirmed (e.g., by pressing Enter). + * @example + * // Adding a search result + * window.addHook('search', function(query) { + * query.addResult({ + * title: 'My Result', + * position: L.latLng(0, 0) + * }); + * }); + * + * @namespace IITC.search + * @memberof IITC */ -window.search.Query = function (term, confirmed) { - this.term = term; - this.confirmed = confirmed; - this.init(); -}; /** - * Initializes the search query, setting up the DOM elements and triggering the 'search' hook. - * - * @function + * @memberOf IITC.search + * @typedef {Object} SearchQuery + * @property {string} term - The term for which the user has searched. + * @property {boolean} confirmed - Indicates if the user has pressed enter after searching. + * You should not search online or do heavy processing unless the user has confirmed the search term. + * @property {IITC.search.Query.addResult} addResult - Method to add a result to the query. + * @property {IITC.search.Query.addPortalResult} addPortalResult - Method to add a portal to the query. */ -window.search.Query.prototype.init = function () { - this.results = []; - - this.container = $('
' + ' ' + ' ' + diff --git a/test/search_query.spec.js b/test/search_query.spec.js new file mode 100644 index 000000000..a7305283a --- /dev/null +++ b/test/search_query.spec.js @@ -0,0 +1,176 @@ +import { describe, it, beforeEach } from 'mocha'; +import { expect } from 'chai'; + +/* global IITC, L */ +/* eslint-disable no-unused-expressions */ + +if (!globalThis.window) globalThis.window = {}; +if (!globalThis.L) globalThis.L = {}; +if (!globalThis.IITC) globalThis.IITC = {}; +if (!globalThis.IITC.search) globalThis.IITC.search = {}; +globalThis.IITC.search.Query = {}; +import('../core/code/search_query.js'); + +describe('IITC.search.Query', () => { + let query; + let fakeMap; + + beforeEach(() => { + // Mock objects and methods + fakeMap = { + addTo: () => {}, + addLayer: () => {}, + }; + + globalThis.window = { + ...globalThis.window, + ...{ + map: fakeMap, + runHooks: () => {}, + isSmartphone: () => false, + TEAM_SHORTNAMES: { NEUTRAL: 'NEU', ENLIGHTENED: 'ENL' }, + COLORS: { NEUTRAL: '#CCC', ENLIGHTENED: '#008000' }, + teamStringToId: (team) => (team === 'ENLIGHTENED' ? 'ENLIGHTENED' : 'NEUTRAL'), + }, + }; + + globalThis.L = { + ...globalThis.L, + ...{ + LatLng: class {}, + latLng: (lat, lng) => new L.LatLng(lat, lng), + layerGroup: () => fakeMap, + marker: () => fakeMap, + divIcon: { + coloredSvg: () => {}, + }, + }, + }; + + globalThis.IITC.search.QueryResultsView = class { + constructor(term, confirmed) { + this.term = term; + this.confirmed = confirmed; + } + renderResults() {} + }; + + query = new IITC.search.Query('test', true); + }); + + // Test for initialization + it('should initialize with an empty results array', () => { + expect(query.results).to.be.an('array').that.is.empty; + }); + + // Test for the addResult method + it('should add a result to the results array with addResult', () => { + const mockResult = { title: 'Test Result', position: new L.LatLng(0, 0) }; + + query.addResult(mockResult); + + expect(query.results).to.have.lengthOf(1); + expect(query.results[0]).to.deep.equal(mockResult); + + let renderCalled = false; + query.renderResults = () => { + renderCalled = true; + }; + query.addResult(mockResult); + expect(renderCalled).to.be.true; + }); + + // Test for the addPortalResult method + it('should add a portal result to the results array with addPortalResult', () => { + const portalData = { + title: 'Test Portal', + team: 'ENLIGHTENED', + level: 8, + health: 100, + resCount: 8, + latE6: 50000000, + lngE6: 100000000, + }; + + query.addPortalResult(portalData, 'abc123'); + + expect(query.results).to.have.lengthOf(1); + const addedResult = query.results[0]; + + expect(addedResult.title).to.equal('Test Portal'); + expect(addedResult.description).to.contain('ENL'); + expect(addedResult.description).to.contain('L8'); + expect(addedResult.description).to.contain('100%'); + expect(addedResult.description).to.contain('8 Resonators'); + expect(addedResult.icon).to.be.a('string').that.contains('data:image/svg+xml;base64'); + }); + + // Test for hover interaction handling + it('should start hover interaction and add layer to map', () => { + const mockResult = { title: 'Hover Result', layer: null, position: new L.LatLng(0, 0) }; + let layerAdded = false; + + fakeMap.addLayer = () => { + layerAdded = true; + }; + query.onResultHoverStart(mockResult); + + expect(layerAdded).to.be.true; + }); + + it('should handle Space key press for selecting a result', () => { + let eventHandled = false; + const mockEvent = { key: ' ', preventDefault: () => {} }; + const result = { title: 'Test Result' }; + + query.onResultSelected = () => { + eventHandled = true; + }; + + query.handleKeyPress(mockEvent, result); + expect(eventHandled).to.be.true; + }); + + it('should remove hover interaction layer from map', () => { + const mockResult = { layer: fakeMap }; + let layerRemoved = false; + + query.hoverResult = mockResult; + fakeMap.removeLayer = () => { + layerRemoved = true; + }; + + query.removeHoverResult(); + expect(layerRemoved).to.be.true; + }); + + // Test for selecting a result + it('should select a result and adjust the map view', () => { + const mockResult = { + title: 'Selected Result', + position: new L.LatLng(0, 0), + onSelected: () => false, + }; + let viewSet = false; + + fakeMap.setView = () => { + viewSet = true; + }; + query.onResultSelected(mockResult, { type: 'click' }); + + expect(viewSet).to.be.true; + expect(query.selectedResult).to.equal(mockResult); + }); + + it('should prevent map repositioning if onSelected returns true', () => { + const mockResult = { title: 'Selected Result', onSelected: () => true }; + let viewSet = false; + + fakeMap.setView = () => { + viewSet = true; + }; + query.onResultSelected(mockResult, { type: 'click' }); + + expect(viewSet).to.be.false; + }); +});