mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
- The chip reads GOOGLE STREET VIEW (and its caption link likewise). - Providers declare the imagery filters they honour (`filters`, default pano + since). Street View declares none: every panorama is 360° and it draws no coverage to thin, so its lookups ignore the filters and the panel hides IMAGERY and SINCE while it is the only lit provider. - The legend lists only providers that draw coverage lines, with Selected only beside one: Street View alone shows no legend. - Providers may pick their on-globe marker (`marker`); Street View's is a Google-blue field-of-view cone from a white-ringed dot, in place of GEV's amber pin, and switches back for a Mapillary photo. Co-Authored-By: Claude Opus 5.5 <[email protected]>
294 lines
10 KiB
JavaScript
294 lines
10 KiB
JavaScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
import { createGoogleProvider } from './index.js';
|
||
import {
|
||
GROUND_CLICK_HINT,
|
||
KEY_REJECTED_MESSAGE,
|
||
creatorFrom,
|
||
imageDateMs,
|
||
streetViewUrl,
|
||
} from './policy.js';
|
||
import { PROVIDER_COLORS } from '../../policy.js';
|
||
import { requiresKeyIdFor, validateProviders } from '../../registry.js';
|
||
import { createMapillaryProvider } from '../mapillary/index.js';
|
||
import {
|
||
fakeHost,
|
||
fakeMapsLoader,
|
||
fakeStreetViewLibrary,
|
||
} from '../../../../testSupport/googleStreetViewFakes.mjs';
|
||
|
||
const PANORAMAS = {
|
||
fresh: { lat: 38.5816, lng: -121.4944, imageDate: '2025-06' },
|
||
old: { lat: 38.6, lng: -121.5, imageDate: '2009-03' },
|
||
};
|
||
|
||
function setup({ key = 'browser-key', nearest = () => 'fresh' } = {}) {
|
||
const fake = fakeStreetViewLibrary({ panoramas: PANORAMAS, nearest });
|
||
const loader = fakeMapsLoader(fake.library);
|
||
let apiKey = key;
|
||
const def = createGoogleProvider({ getApiKey: () => apiKey, loader });
|
||
const reported = [];
|
||
let filter = { pano: 'all', sinceMs: null };
|
||
let active = false;
|
||
const context = {
|
||
services: {},
|
||
getFilter: () => filter,
|
||
isActive: () => active,
|
||
notify() {},
|
||
actions: { reportError: (message) => reported.push(message) },
|
||
};
|
||
const created = def.create(context);
|
||
// The core switches it on and off; mirror that for isActive().
|
||
const instance = {
|
||
...created,
|
||
activate() {
|
||
active = true;
|
||
created.activate();
|
||
},
|
||
deactivate() {
|
||
active = false;
|
||
created.deactivate();
|
||
},
|
||
};
|
||
return {
|
||
def,
|
||
instance,
|
||
fake,
|
||
loader,
|
||
reported,
|
||
setFilter: (next) => (filter = next),
|
||
setKey: (next) => (apiKey = next),
|
||
};
|
||
}
|
||
|
||
test('the provider registers beside Mapillary, off by default, gated on the browser Maps key', () => {
|
||
const { def } = setup();
|
||
assert.equal(def.id, 'google');
|
||
assert.equal(def.label, 'GOOGLE STREET VIEW');
|
||
assert.deepEqual(def.filters, [], 'no imagery filter applies');
|
||
assert.deepEqual(def.marker, { kind: 'view-cone', color: '#4285f4' });
|
||
assert.equal(def.requiresKeyId, 'google-maps');
|
||
assert.equal(def.defaultOn, false, 'every panorama is billed');
|
||
assert.equal(def.groundClick, true);
|
||
assert.equal(def.colors.coverage, PROVIDER_COLORS.google);
|
||
assert.match(def.credit.html, /Street View imagery © .*Google/);
|
||
const mapillary = createMapillaryProvider({
|
||
source: {
|
||
getStatus() {},
|
||
getTile() {},
|
||
getSequenceImages() {},
|
||
nearestImages() {},
|
||
},
|
||
});
|
||
const both = validateProviders([mapillary, def]);
|
||
assert.equal(requiresKeyIdFor(both), null, 'each chip gates on its own key');
|
||
assert.throws(() => createGoogleProvider({}), /getApiKey/);
|
||
});
|
||
|
||
test('status follows the browser Maps key', async () => {
|
||
const { instance, setKey } = setup({ key: '' });
|
||
assert.deepEqual(await instance.status(), { configured: false });
|
||
assert.equal(instance.coverageStats().keyRequired, true);
|
||
setKey('browser-key');
|
||
assert.deepEqual(await instance.status(), { configured: true });
|
||
assert.equal(instance.coverageStats().keyRequired, false);
|
||
});
|
||
|
||
test('the nearest lookup asks Google for its own outdoor imagery within 50 m', async () => {
|
||
const { instance, fake } = setup();
|
||
assert.equal(
|
||
await instance.nearestImage({ lat: 38.58, lon: -121.49 }),
|
||
'fresh',
|
||
);
|
||
const [request] = fake.lookups;
|
||
assert.deepEqual(request.location, { lat: 38.58, lng: -121.49 });
|
||
assert.equal(request.radius, 50);
|
||
assert.deepEqual(request.sources, ['google', 'outdoor']);
|
||
assert.equal(request.preference, 'nearest');
|
||
});
|
||
|
||
test('no panorama nearby is null; the imagery filters (Mapillary\u2019s) do not apply', async () => {
|
||
const none = setup({ nearest: () => null });
|
||
assert.equal(await none.instance.nearestImage({ lat: 0, lon: 0 }), null);
|
||
|
||
const { instance, setFilter } = setup({ nearest: () => 'old' }); // 2009
|
||
for (const filter of [
|
||
{ pano: 'all', sinceMs: Date.UTC(2020, 0, 1) },
|
||
{ pano: 'flat', sinceMs: null },
|
||
{ pano: 'pano', sinceMs: Date.UTC(2025, 0, 1) },
|
||
]) {
|
||
setFilter(filter);
|
||
assert.equal(await instance.nearestImage({ lat: 0, lon: 0 }), 'old');
|
||
}
|
||
assert.equal(instance.coverageStats().hint, '', 'off: no hint');
|
||
instance.activate();
|
||
setFilter({ pano: 'flat', sinceMs: null });
|
||
assert.equal(instance.coverageStats().hint, GROUND_CLICK_HINT);
|
||
});
|
||
|
||
test('an aborted lookup gives up before asking Google', async () => {
|
||
const { instance } = setup();
|
||
const controller = new AbortController();
|
||
controller.abort();
|
||
await assert.rejects(
|
||
instance.nearestImage({ lat: 0, lon: 0 }, { signal: controller.signal }),
|
||
{ name: 'AbortError' },
|
||
);
|
||
});
|
||
|
||
test('a key Google refuses reads KEY REJECTED, shows why, and stops lookups', async () => {
|
||
const { instance, loader, reported } = setup();
|
||
instance.activate();
|
||
assert.equal(instance.coverageStats().hint, GROUND_CLICK_HINT);
|
||
loader.refuseKey();
|
||
const stats = instance.coverageStats();
|
||
assert.equal(stats.keyRejected, true);
|
||
assert.equal(stats.error, KEY_REJECTED_MESSAGE);
|
||
assert.equal(stats.hint, '', 'no "click a street" hint over a dead key');
|
||
assert.deepEqual(reported, [KEY_REJECTED_MESSAGE]);
|
||
await assert.rejects(instance.nearestImage({ lat: 0, lon: 0 }), {
|
||
message: KEY_REJECTED_MESSAGE,
|
||
});
|
||
instance.destroy();
|
||
assert.equal(loader.listenerCount(), 0, 'destroy stops listening');
|
||
});
|
||
|
||
test('the hint shows only while the provider is on', () => {
|
||
const { instance } = setup();
|
||
assert.equal(instance.coverageStats().hint, '');
|
||
instance.activate();
|
||
assert.equal(instance.coverageStats().hint, GROUND_CLICK_HINT);
|
||
instance.deactivate();
|
||
assert.equal(instance.coverageStats().hint, '');
|
||
});
|
||
|
||
test('image dates, photographers and deep links read the way Google writes them', () => {
|
||
assert.equal(imageDateMs('2024-05'), Date.UTC(2024, 4, 1));
|
||
assert.equal(imageDateMs('2024-13'), null);
|
||
assert.equal(imageDateMs(undefined), null);
|
||
assert.equal(creatorFrom('© 2024 Google'), 'Google');
|
||
assert.equal(creatorFrom('From the Owner, Photo by: Ada'), 'Ada');
|
||
assert.equal(creatorFrom(''), null);
|
||
assert.equal(
|
||
streetViewUrl('a b', { heading: 90.4, pitch: -9.6 }),
|
||
'https://www.google.com/maps/@?api=1&map_action=pano&pano=a+b&heading=90&pitch=-10',
|
||
);
|
||
});
|
||
|
||
test('a lookup Google never answers fails after the timeout, and a refused key fails it at once', async (t) => {
|
||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||
const { NEAREST_TIMEOUT_MS, NO_ANSWER_MESSAGE } = await import('./policy.js');
|
||
const { instance, fake, loader } = setup();
|
||
fake.library.StreetViewService.prototype.getPanorama = () =>
|
||
new Promise(() => {});
|
||
const hung = instance.nearestImage({ lat: 0, lon: 0 });
|
||
await Promise.resolve();
|
||
await Promise.resolve();
|
||
t.mock.timers.tick(NEAREST_TIMEOUT_MS);
|
||
await assert.rejects(hung, { message: NO_ANSWER_MESSAGE });
|
||
|
||
const refused = instance.nearestImage({ lat: 0, lon: 0 });
|
||
await Promise.resolve();
|
||
loader.refuseKey();
|
||
await assert.rejects(refused, { message: KEY_REJECTED_MESSAGE });
|
||
assert.equal(
|
||
loader.listenerCount(),
|
||
1,
|
||
'only the provider’s own watch is left',
|
||
);
|
||
});
|
||
|
||
test('aborting a lookup in flight gives up at once', async () => {
|
||
const { instance, fake } = setup();
|
||
fake.library.StreetViewService.prototype.getPanorama = () =>
|
||
new Promise(() => {});
|
||
const controller = new AbortController();
|
||
const lookup = instance.nearestImage(
|
||
{ lat: 0, lon: 0 },
|
||
{ signal: controller.signal },
|
||
);
|
||
controller.abort();
|
||
await assert.rejects(lookup, { name: 'AbortError' });
|
||
});
|
||
|
||
test('a lookup cancelled while Google\u2019s script loads asks Google nothing', async (t) => {
|
||
t.mock.timers.enable({ apis: ['setTimeout'] });
|
||
const { NEAREST_TIMEOUT_MS } = await import('./policy.js');
|
||
const fake = fakeStreetViewLibrary({
|
||
panoramas: PANORAMAS,
|
||
nearest: () => 'fresh',
|
||
});
|
||
const loader = fakeMapsLoader(fake.library, { deferred: true });
|
||
const instance = createGoogleProvider({
|
||
getApiKey: () => 'browser-key',
|
||
loader,
|
||
}).create({
|
||
services: {},
|
||
getFilter: () => ({ pano: 'all', sinceMs: null }),
|
||
isActive: () => true,
|
||
notify() {},
|
||
actions: { reportError() {} },
|
||
});
|
||
const flush = async () => {
|
||
for (let i = 0; i < 10; i++) await Promise.resolve();
|
||
};
|
||
// Aborted, then timed out, while the script still loads.
|
||
const controller = new AbortController();
|
||
const aborted = instance.nearestImage(
|
||
{ lat: 0, lon: 0 },
|
||
{ signal: controller.signal },
|
||
);
|
||
controller.abort();
|
||
await assert.rejects(aborted, { name: 'AbortError' });
|
||
const expired = instance.nearestImage({ lat: 0, lon: 0 });
|
||
t.mock.timers.tick(NEAREST_TIMEOUT_MS);
|
||
await assert.rejects(expired, /did not answer/);
|
||
// The script arrives: a current lookup still gets its answer.
|
||
const current = instance.nearestImage({ lat: 0, lon: 0 });
|
||
loader.release();
|
||
await flush();
|
||
assert.equal(await current, 'fresh');
|
||
assert.equal(fake.lookups.length, 1, 'only the current lookup asked Google');
|
||
});
|
||
|
||
test('a Google outage is an error, not "no imagery here"', async () => {
|
||
const { instance, fake } = setup();
|
||
fake.library.StreetViewService.prototype.getPanorama = () =>
|
||
Promise.reject(
|
||
Object.assign(new Error('UNKNOWN_ERROR'), {
|
||
code: 'UNKNOWN_ERROR',
|
||
endpoint: 'STREETVIEW_GET_PANORAMA',
|
||
}),
|
||
);
|
||
await assert.rejects(instance.nearestImage({ lat: 0, lon: 0 }), {
|
||
code: 'UNKNOWN_ERROR',
|
||
});
|
||
});
|
||
|
||
test('a refusal while the provider is off reports no error over another photo', () => {
|
||
const { instance, loader, reported } = setup();
|
||
loader.refuseKey();
|
||
assert.deepEqual(reported, []);
|
||
assert.equal(
|
||
instance.coverageStats().keyRejected,
|
||
true,
|
||
'its chip still says so',
|
||
);
|
||
});
|
||
|
||
test('the panorama a lookup found is dated without a second lookup', async () => {
|
||
const { instance, fake } = setup();
|
||
await instance.nearestImage({ lat: 38.58, lon: -121.49 });
|
||
const host = fakeHost();
|
||
const poses = [];
|
||
instance.viewer.onPose((pose) => poses.push(pose));
|
||
await instance.viewer.mount(host);
|
||
await instance.viewer.open('fresh');
|
||
assert.equal(poses.at(-1).capturedAt, Date.UTC(2025, 5, 1));
|
||
assert.deepEqual(
|
||
fake.lookups.filter((request) => request.pano),
|
||
[],
|
||
'no getPanorama({pano}) for a panorama the lookup already described',
|
||
);
|
||
});
|