Files
marco/tests/unit/services/place-name-resolver-test.js
raucao a4a8d123d3 Split Activity timeline in Home and Explore
Adds a new Explore timeline with contributions from anyone on trusted
relays.
2026-09-02 15:06:10 -06:00

277 lines
9.7 KiB
JavaScript

import { module, test } from 'qunit';
import { setupTest } from 'marco/tests/helpers';
import Service from '@ember/service';
function makeEntry(opts = {}) {
const placeIdentifier = opts.placeIdentifier || 'osm:node:123';
const [, osmType, osmId] = placeIdentifier.split(':');
return {
placeIdentifier,
osmType: opts.osmType || osmType,
osmId: opts.osmId || osmId,
placeName: opts.placeName || null,
placeNameLoading: opts.placeNameLoading ?? true,
};
}
class MockStorageService extends Service {
savedPlaces = [];
findPlaceById() {
return null;
}
}
class MockOsmService extends Service {
async getCachedOsmObject() {
return null;
}
async fetchOsmObjectsBatch() {
return new Map();
}
}
function flushPromises() {
return new Promise((resolve) => setTimeout(resolve, 50));
}
module('Unit | Service | place-name-resolver', function (hooks) {
setupTest(hooks);
hooks.beforeEach(function () {
this.owner.register('service:storage', MockStorageService);
this.owner.register('service:osm', MockOsmService);
});
test('resolveBookmark returns bookmark title when found', function (assert) {
const resolver = this.owner.lookup('service:place-name-resolver');
const storage = this.owner.lookup('service:storage');
storage.savedPlaces = [{ id: '123', title: 'Bookmarked Café' }];
storage.findPlaceById = (id) =>
id === '123' ? { id: '123', title: 'Bookmarked Café' } : null;
const name = resolver.resolveBookmark('123');
assert.strictEqual(name, 'Bookmarked Café');
});
test('resolveBookmark returns null when no bookmark', function (assert) {
const resolver = this.owner.lookup('service:place-name-resolver');
const name = resolver.resolveBookmark('999');
assert.notOk(name);
});
test('resolveInBackground resolves names from the persistent name cache', async function (assert) {
const resolver = this.owner.lookup('service:place-name-resolver');
const localForage = this.owner.lookup('service:localForage');
const entry = makeEntry({ placeIdentifier: 'osm:node:999' });
await localForage.set('place-name-cache', 'osm:node:999', 'Cached Park');
await resolver.resolveInBackground([entry]);
assert.strictEqual(entry.placeName, 'Cached Park');
assert.false(entry.placeNameLoading);
});
test('resolved names are persisted to the IndexedDB name cache', async function (assert) {
const resolver = this.owner.lookup('service:place-name-resolver');
const localForage = this.owner.lookup('service:localForage');
const entry = makeEntry({ placeIdentifier: 'osm:node:42' });
resolver.osm.fetchOsmObjectsBatch = async () => {
const map = new Map();
map.set('node:42', { title: 'Test Place' });
return map;
};
await resolver.resolveInBackground([entry]);
await flushPromises();
assert.strictEqual(
await localForage.get('place-name-cache', 'osm:node:42'),
'Test Place',
'Name is persisted to the IndexedDB name cache'
);
});
test('names stored in a previous session are resolved from the IndexedDB cache', async function (assert) {
const resolver = this.owner.lookup('service:place-name-resolver');
const localForage = this.owner.lookup('service:localForage');
const entry = makeEntry({ placeIdentifier: 'osm:node:77' });
await localForage.set('place-name-cache', 'osm:node:77', 'Persisted Place');
await resolver.resolveInBackground([entry]);
assert.strictEqual(entry.placeName, 'Persisted Place');
assert.false(entry.placeNameLoading);
});
test('batch fetch failure applies a fallback name so items do not stay loading', async function (assert) {
const resolver = this.owner.lookup('service:place-name-resolver');
const entry = makeEntry({ placeIdentifier: 'osm:node:555' });
resolver.osm.fetchOsmObjectsBatch = async () => new Map();
await resolver.resolveInBackground([entry]);
await flushPromises();
assert.false(
entry.placeNameLoading,
'Item is no longer loading after batch failure'
);
assert.strictEqual(
entry.placeName,
'OSM node 555',
'Fallback name is applied'
);
});
test('fallback names are NOT persisted to the name cache (so they can be re-fetched next session)', async function (assert) {
const resolver = this.owner.lookup('service:place-name-resolver');
const localForage = this.owner.lookup('service:localForage');
const entry = makeEntry({ placeIdentifier: 'osm:node:666' });
resolver.osm.fetchOsmObjectsBatch = async () => new Map();
await resolver.resolveInBackground([entry]);
await flushPromises();
assert.strictEqual(entry.placeName, 'OSM node 666');
assert.notOk(
await localForage.get('place-name-cache', 'osm:node:666'),
'No fallback name persisted in the IndexedDB name cache'
);
assert.true(
resolver._unresolvable.has('osm:node:666'),
'Item is marked unresolvable for this session'
);
});
test('unresolvable items are not re-fetched within the same session', async function (assert) {
const resolver = this.owner.lookup('service:place-name-resolver');
const entry = makeEntry({ placeIdentifier: 'osm:node:777' });
let fetchCount = 0;
resolver.osm.fetchOsmObjectsBatch = async () => {
fetchCount++;
return new Map();
};
// First call triggers a fetch
await resolver.resolveInBackground([entry]);
await flushPromises();
assert.strictEqual(fetchCount, 1, 'First call triggers a fetch');
assert.strictEqual(entry.placeName, 'OSM node 777');
// Second call should NOT trigger another fetch (already unresolvable)
await resolver.resolveInBackground([entry]);
await flushPromises();
assert.strictEqual(fetchCount, 1, 'Second call does not re-fetch');
assert.strictEqual(
entry.placeName,
'OSM node 777',
'Fallback is shown immediately'
);
});
test('successful batch resolution stores names in the name cache', async function (assert) {
const resolver = this.owner.lookup('service:place-name-resolver');
const localForage = this.owner.lookup('service:localForage');
const entry = makeEntry({ placeIdentifier: 'osm:node:111' });
resolver.osm.fetchOsmObjectsBatch = async () => {
const map = new Map();
map.set('node:111', { title: 'Resolved Café' });
return map;
};
await resolver.resolveInBackground([entry]);
await flushPromises();
assert.strictEqual(entry.placeName, 'Resolved Café');
assert.strictEqual(
await localForage.get('place-name-cache', 'osm:node:111'),
'Resolved Café',
'Name is stored in the name cache'
);
});
test('reset clears per-session state', function (assert) {
const resolver = this.owner.lookup('service:place-name-resolver');
resolver._unresolvable.add('test-id');
resolver._lastBatchSignature = 'sig';
resolver._pendingBatchPromise = Promise.resolve();
resolver.reset();
assert.strictEqual(resolver._unresolvable.size, 0);
assert.strictEqual(resolver._lastBatchSignature, '');
assert.strictEqual(resolver._pendingBatchPromise, null);
});
test('deduplicated batch resolves entries from a second call with same place identifiers', async function (assert) {
const resolver = this.owner.lookup('service:place-name-resolver');
const localForage = this.owner.lookup('service:localForage');
let fetchCount = 0;
resolver.osm.fetchOsmObjectsBatch = async () => {
fetchCount++;
const map = new Map();
map.set('node:222', { title: 'Shared Place' });
return map;
};
// First call with entry1 — triggers a batch fetch
const entry1 = makeEntry({ placeIdentifier: 'osm:node:222' });
const promise1 = resolver.resolveInBackground([entry1]);
await flushPromises();
await promise1;
// Verify the name persisted so the second call's cache check finds it
assert.strictEqual(entry1.placeName, 'Shared Place');
assert.strictEqual(fetchCount, 1, 'only one batch fetch fired');
// Simulate the real-world race: a second set of entries for the same
// place arrives before the cache is consulted. Clear the persistent cache
// to simulate the incognito scenario where the batch hasn't persisted yet.
await localForage.clear('place-name-cache');
// Second call with a fresh entry2 — should resolve from the batch result
// that was already persisted (or re-use the pending batch if still in flight)
const entry2 = makeEntry({ placeIdentifier: 'osm:node:222' });
await resolver.resolveInBackground([entry2]);
assert.strictEqual(
entry2.placeName,
'Shared Place',
'second entry resolves even when batch was from a prior call'
);
});
test('pending batch is shared when second call fires before first completes', async function (assert) {
const resolver = this.owner.lookup('service:place-name-resolver');
let fetchCount = 0;
resolver.osm.fetchOsmObjectsBatch = async () => {
fetchCount++;
const map = new Map();
map.set('node:333', { title: 'Concurrent Place' });
return map;
};
// First call — don't await yet (simulates subscription firing)
const entry1 = makeEntry({ placeIdentifier: 'osm:node:333' });
const promise1 = resolver.resolveInBackground([entry1]);
// Second call with a different entry for the same place — should piggyback
const entry2 = makeEntry({ placeIdentifier: 'osm:node:333' });
const promise2 = resolver.resolveInBackground([entry2]);
await Promise.all([promise1, promise2]);
await flushPromises();
assert.strictEqual(fetchCount, 1, 'only one batch fetch fired');
assert.strictEqual(entry1.placeName, 'Concurrent Place');
assert.strictEqual(entry2.placeName, 'Concurrent Place');
});
});