Include saved places in search results
This commit is contained in:
@@ -230,4 +230,269 @@ module('Integration | Component | search-box', function (hooks) {
|
||||
'transitionTo: search {"queryParams":{"q":"Restaurants","category":"restaurants","selected":null,"lat":"51.5074","lon":"-0.1278"}}',
|
||||
]);
|
||||
});
|
||||
|
||||
test('it includes, deduplicates, and prioritizes saved places in search results', async function (assert) {
|
||||
// Mock MapUi Service
|
||||
class MockMapUiService extends Service {
|
||||
currentCenter = { lat: 52.52, lon: 13.405 };
|
||||
setSearchBoxFocus() {}
|
||||
}
|
||||
this.owner.register('service:map-ui', MockMapUiService);
|
||||
|
||||
// Mock Router Service
|
||||
class MockRouterService extends Service {
|
||||
transitionTo(routeName, id) {
|
||||
assert.step(`transitionTo: ${routeName} ["${id}"]`);
|
||||
}
|
||||
}
|
||||
this.owner.register('service:router', MockRouterService);
|
||||
|
||||
// Mock Storage Service
|
||||
class MockStorageService extends Service {
|
||||
savedPlaces = [
|
||||
{
|
||||
title: 'Awesome Coffee',
|
||||
lat: 52.5,
|
||||
lon: 13.4,
|
||||
osmId: '999',
|
||||
osmType: 'node',
|
||||
},
|
||||
];
|
||||
}
|
||||
this.owner.register('service:storage', MockStorageService);
|
||||
|
||||
// Mock Photon Service
|
||||
class MockPhotonService extends Service {
|
||||
async search(query) {
|
||||
if (query === 'coffee') {
|
||||
return [
|
||||
{
|
||||
title: 'Awesome Coffee',
|
||||
osmId: '999',
|
||||
osmType: 'node',
|
||||
description: 'Duplicate to be removed',
|
||||
},
|
||||
{
|
||||
title: 'Other Coffee',
|
||||
osmId: '888',
|
||||
osmType: 'node',
|
||||
description: 'A different coffee shop',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
this.owner.register('service:photon', MockPhotonService);
|
||||
|
||||
this.noop = () => {};
|
||||
await render(
|
||||
<template><SearchBox @onToggleMenu={{this.noop}} /></template>
|
||||
);
|
||||
|
||||
// Type "coffee" to trigger matches in Category, Saved, and Photon
|
||||
await fillIn('.search-input', 'coffee');
|
||||
await waitFor('.search-results-popover', { timeout: 2000 });
|
||||
|
||||
const resultItems = Array.from(
|
||||
this.element.querySelectorAll('.search-result-item')
|
||||
);
|
||||
|
||||
// Should be exactly 3 items:
|
||||
// 1. Category (Coffee)
|
||||
// 2. Saved (Awesome Coffee)
|
||||
// 3. Photon (Other Coffee)
|
||||
// (The Photon duplicate of "Awesome Coffee" is removed)
|
||||
assert.strictEqual(resultItems.length, 3, 'Renders exactly 3 items');
|
||||
|
||||
// 1. Category
|
||||
assert.ok(
|
||||
resultItems[0].textContent.includes('Coffee'),
|
||||
'First item is the category match'
|
||||
);
|
||||
assert
|
||||
.dom(resultItems[0].querySelector('.result-icon svg'))
|
||||
.hasClass('feather-search', 'Category uses search icon');
|
||||
|
||||
// 2. Saved Place
|
||||
assert.ok(
|
||||
resultItems[1].textContent.includes('Awesome Coffee'),
|
||||
'Second item is the saved place match'
|
||||
);
|
||||
assert.ok(
|
||||
resultItems[1].textContent.includes('Saved place'),
|
||||
'Saved place has correct description text'
|
||||
);
|
||||
assert
|
||||
.dom(resultItems[1].querySelector('.result-icon svg'))
|
||||
.hasClass('feather-bookmark', 'Saved place uses bookmark icon');
|
||||
|
||||
// 3. Photon Match
|
||||
assert.ok(
|
||||
resultItems[2].textContent.includes('Other Coffee'),
|
||||
'Third item is the unique photon result'
|
||||
);
|
||||
|
||||
// Click the Saved Place
|
||||
await click(resultItems[1]);
|
||||
|
||||
assert.verifySteps(['transitionTo: place ["osm:node:999"]']);
|
||||
});
|
||||
|
||||
test('it requires 3 or more characters to match saved places', async function (assert) {
|
||||
// Mock MapUi Service
|
||||
class MockMapUiService extends Service {
|
||||
currentCenter = { lat: 52.52, lon: 13.405 };
|
||||
setSearchBoxFocus() {}
|
||||
}
|
||||
this.owner.register('service:map-ui', MockMapUiService);
|
||||
|
||||
// Mock Router Service
|
||||
class MockRouterService extends Service {
|
||||
transitionTo() {}
|
||||
}
|
||||
this.owner.register('service:router', MockRouterService);
|
||||
|
||||
// Mock Storage Service
|
||||
class MockStorageService extends Service {
|
||||
savedPlaces = [
|
||||
{
|
||||
title: 'Awesome Coffee',
|
||||
lat: 52.5,
|
||||
lon: 13.4,
|
||||
osmId: '999',
|
||||
osmType: 'node',
|
||||
},
|
||||
];
|
||||
}
|
||||
this.owner.register('service:storage', MockStorageService);
|
||||
|
||||
// Mock Photon Service
|
||||
class MockPhotonService extends Service {
|
||||
async search(query) {
|
||||
if (query === 'aw' || query === 'awe') {
|
||||
return [
|
||||
{
|
||||
title: 'Aww Some Place',
|
||||
osmId: '111',
|
||||
osmType: 'node',
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
this.owner.register('service:photon', MockPhotonService);
|
||||
|
||||
this.noop = () => {};
|
||||
await render(
|
||||
<template><SearchBox @onToggleMenu={{this.noop}} /></template>
|
||||
);
|
||||
|
||||
// Type "aw" (2 characters)
|
||||
await fillIn('.search-input', 'aw');
|
||||
await waitFor('.search-results-popover', { timeout: 2000 });
|
||||
|
||||
let resultItems = Array.from(
|
||||
this.element.querySelectorAll('.search-result-item')
|
||||
);
|
||||
|
||||
// Should only show Photon match since 'aw' is < 3 characters
|
||||
assert.strictEqual(
|
||||
resultItems.length,
|
||||
1,
|
||||
'Renders exactly 1 item for 2 chars'
|
||||
);
|
||||
assert.ok(
|
||||
resultItems[0].textContent.includes('Aww Some Place'),
|
||||
'Shows photon match'
|
||||
);
|
||||
assert.notOk(
|
||||
resultItems.some((item) => item.textContent.includes('Awesome Coffee')),
|
||||
'Saved place is NOT shown for 2 char query'
|
||||
);
|
||||
|
||||
// Type "awe" (3 characters)
|
||||
await fillIn('.search-input', 'awe');
|
||||
await waitFor('.search-results-popover', { timeout: 2000 });
|
||||
|
||||
resultItems = Array.from(
|
||||
this.element.querySelectorAll('.search-result-item')
|
||||
);
|
||||
|
||||
// Should now show Saved Place and Photon match
|
||||
assert.strictEqual(
|
||||
resultItems.length,
|
||||
2,
|
||||
'Renders exactly 2 items for 3 chars'
|
||||
);
|
||||
assert.ok(
|
||||
resultItems.some((item) => item.textContent.includes('Awesome Coffee')),
|
||||
'Saved place is now shown'
|
||||
);
|
||||
});
|
||||
|
||||
test('it navigates to internal ID for custom saved places without an OSM ID', async function (assert) {
|
||||
// Mock MapUi Service
|
||||
class MockMapUiService extends Service {
|
||||
currentCenter = { lat: 52.52, lon: 13.405 };
|
||||
setSearchBoxFocus() {}
|
||||
}
|
||||
this.owner.register('service:map-ui', MockMapUiService);
|
||||
|
||||
// Mock Router Service
|
||||
class MockRouterService extends Service {
|
||||
transitionTo(routeName, id) {
|
||||
assert.step(`transitionTo: ${routeName} ["${id}"]`);
|
||||
}
|
||||
}
|
||||
this.owner.register('service:router', MockRouterService);
|
||||
|
||||
// Mock Storage Service (Custom Place)
|
||||
class MockStorageService extends Service {
|
||||
savedPlaces = [
|
||||
{
|
||||
id: 'custom-1234',
|
||||
title: 'My Custom Home',
|
||||
lat: 52.5,
|
||||
lon: 13.4,
|
||||
// Notice NO osmId or osmType
|
||||
},
|
||||
];
|
||||
}
|
||||
this.owner.register('service:storage', MockStorageService);
|
||||
|
||||
// Mock Photon Service
|
||||
class MockPhotonService extends Service {
|
||||
async search() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
this.owner.register('service:photon', MockPhotonService);
|
||||
|
||||
this.noop = () => {};
|
||||
await render(
|
||||
<template><SearchBox @onToggleMenu={{this.noop}} /></template>
|
||||
);
|
||||
|
||||
// Type 3 chars to trigger saved place match
|
||||
await fillIn('.search-input', 'cus');
|
||||
await waitFor('.search-results-popover', { timeout: 2000 });
|
||||
|
||||
const resultItems = Array.from(
|
||||
this.element.querySelectorAll('.search-result-item')
|
||||
);
|
||||
|
||||
// Ensure our custom place is rendered
|
||||
const customResult = resultItems.find((item) =>
|
||||
item.textContent.includes('My Custom Home')
|
||||
);
|
||||
assert.ok(customResult, 'Custom place is rendered');
|
||||
|
||||
// Click it
|
||||
await click(customResult);
|
||||
|
||||
// Verify it navigated using the internal ID, NOT a search query
|
||||
assert.verifySteps(['transitionTo: place ["custom-1234"]']);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user