Files
gods-eye-view/src/layers/streetLevel/index.test.mjs
T
German KhokhlovandClaude Opus 5.5 00704ddc89 i18n: fix review findings and polish ru/es strings
Fixes the 51 P0–P2 findings from the full review of the i18n branch and
the issues found while checking every new string on screen in ru and es.

- Translate errors and statuses that still reached ru/es in English:
  feed and tile errors, provider statuses, the ALPR chip, the traffic
  mode chip, voice plan step labels, fire feed values, receiver bands,
  scene save reasons and the Cesium attribution link.
- Show preset city names in the UI language (src/ui/placeNames.js);
  data and everything sent to the model stay English.
- Share layer names between the layer panel, HUD and voice card
  (src/ui/layerNames.js).
- Keep long ru/es strings whole: voice card error lines, analyst
  titles, the voice tier status, the Nepal evidence button, the ALPR
  retry chip. English layout is unchanged.
- Ask before a language switch drops unsaved work.
- Catalog fixes: glossary terms (пуск, пресет), gender agreement and
  consistent "peticiones" in es, fewer calques in both languages.
- Fix English text that was itself wrong (e.g. "Exit cockpit to fly to
  a weather", raw tool ids in voice plan steps).

Catalogs: es/ru 2535 messages each, complete.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 23:49:52 +04:00

965 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import assert from 'node:assert/strict';
import test from 'node:test';
import * as Cesium from 'cesium';
import { createStreetLevelLayer } from './index.js';
import { MAPILLARY_CREDIT_HTML } from './providers/mapillary/policy.js';
import { createMapillaryProvider } from './providers/mapillary/index.js';
import { createKartaViewProvider } from './providers/kartaview/index.js';
import { fakeStreetLevelProvider as fakeProvider } from '../../testSupport/streetLevelFakes.mjs';
import { setLocale } from '../../i18n/index.js';
/**
* A stand-in Cesium viewer the layer can be enabled on: a canvas for the
* click handler, camera events, a primitive list and a credit display.
*/
function fakeViewer() {
const credits = [];
/** Cesium's flight bookkeeping: a new flight cancels the current one. */
let flight = null;
const flights = { started: 0, cancelled: 0 };
const stopFlight = () => {
const current = flight;
flight = null;
current?.cancel?.();
return Boolean(current);
};
const canvas = Object.assign(new EventTarget(), {
style: {},
// Keep Cesium's handler on the canvas; there is no real document here.
disableRootEvents: true,
onwheel: null,
});
return {
credits,
flights,
scene: {
canvas,
primitives: { add: (p) => p, remove() {} },
// Enough of a scene for the position marker to clamp to the ground.
frameState: { mode: Cesium.SceneMode.SCENE3D },
updateHeight: () => () => {},
getHeight: () => undefined,
},
camera: {
changed: new Cesium.Event(),
moveStart: new Cesium.Event(),
moveEnd: new Cesium.Event(),
flyToBoundingSphere(sphere, options = {}) {
stopFlight();
flight = options;
flights.started++;
},
cancelFlight() {
if (stopFlight()) flights.cancelled++;
},
},
creditDisplay: {
addStaticCredit: (credit) => credits.push(credit),
removeStaticCredit: (credit) =>
credits.splice(credits.indexOf(credit), 1),
},
};
}
/** A layer over `providers`, initialised and enabled on a stand-in viewer. */
async function enabledLayer(t, providers = [fakeProvider()]) {
const saved = globalThis.document;
// A pose moves the position marker, whose glyph is drawn on a canvas.
const drawing = new Proxy({}, { get: () => () => ({ addColorStop() {} }) });
globalThis.document = Object.assign(new EventTarget(), {
createElement: () => ({ getContext: () => drawing }),
});
const viewer = fakeViewer();
const layer = createStreetLevelLayer({ providers });
layer.init(viewer);
layer.enable(viewer);
t.after(() => {
layer.destroy();
globalThis.document = saved;
});
await settle();
return { layer, viewer };
}
const settle = () => new Promise((resolve) => setTimeout(resolve, 0));
function deferred() {
let resolve;
let reject;
const promise = new Promise((done, fail) => {
resolve = done;
reject = fail;
});
return { promise, resolve, reject };
}
/** A provider whose viewer reports a pose for every image it opens. */
function posingProvider(options) {
const provider = fakeProvider(options);
const create = provider.create;
provider.create = (context) => {
const instance = create(context);
let emit = null;
instance.viewer.onPose = (listener) => {
emit = listener;
return () => {
emit = null;
};
};
instance.viewer.open = async (imageId) => {
provider.calls.open.push(imageId);
emit?.({
providerId: 'mapillary',
imageId,
position: { lon: -121.49, lat: 38.58 },
bearing: 90,
});
};
return instance;
};
return provider;
}
/** nearestImage lookups that answer when the test says, with their signals. */
function slowLookups() {
const lookups = [];
const nearestImage = (point, { signal } = {}) => {
const answer = deferred();
lookups.push({ point, signal, answer });
return answer.promise;
};
return { lookups, nearestImage };
}
/** A map stack controller that can be switched between stacks. */
function fakeMapStack(initial) {
let active = initial;
const listeners = new Set();
return {
getActiveId: () => active,
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
switchTo(id) {
active = id;
for (const listener of listeners) listener();
},
listenerCount: () => listeners.size,
};
}
test('FOLLOW is available only while the Google 3D map stack is active', () => {
const layer = createStreetLevelLayer({ providers: [fakeProvider()] });
assert.equal(
layer.getUIState().street.followAvailable,
false,
'unknown stack: off',
);
const stack = fakeMapStack('esri-imagery');
layer.attachMapStackController(stack);
assert.equal(layer.getUIState().street.followAvailable, false);
layer.setFollow(true);
assert.equal(layer.getUIState().street.follow, false, 'refused on Esri');
stack.switchTo('photoreal');
assert.equal(layer.getUIState().street.followAvailable, true);
layer.setFollow(true);
assert.equal(layer.getUIState().street.follow, true);
stack.switchTo('osm');
assert.equal(layer.getUIState().street.followAvailable, false);
assert.equal(
layer.getUIState().street.follow,
false,
'leaving Google 3D stops following',
);
});
test('the layer lets go of the map stack when destroyed or re-attached', () => {
const layer = createStreetLevelLayer({ providers: [fakeProvider()] });
const first = fakeMapStack('photoreal');
layer.attachMapStackController(first);
assert.equal(first.listenerCount(), 1);
const second = fakeMapStack('photoreal');
layer.attachMapStackController(second);
assert.equal(first.listenerCount(), 0);
assert.equal(second.listenerCount(), 1);
layer.destroy();
assert.equal(second.listenerCount(), 0);
});
test('the CC BY-SA credit shows while a provider is on and goes with it', async (t) => {
const { layer, viewer } = await enabledLayer(t);
const shown = () => viewer.credits.map((credit) => credit.html);
assert.deepEqual(shown(), [MAPILLARY_CREDIT_HTML], 'shown on activate');
assert.match(shown()[0], /CC BY-SA 4\.0/);
layer.setProviderEnabled('mapillary', false);
assert.deepEqual(shown(), [], 'hidden with the provider');
layer.setProviderEnabled('mapillary', true);
assert.deepEqual(shown(), [MAPILLARY_CREDIT_HTML]);
layer.disable();
assert.deepEqual(shown(), [], 'every credit goes with the layer');
});
test('getStats tells a rejected key from a missing one', async (t) => {
const provider = fakeProvider();
const { layer } = await enabledLayer(t, [provider]);
provider.stats.loading = true;
assert.equal(layer.getStats().loadingLabel, 'loading coverage...');
assert.equal(layer.getStats().keyRequired, false);
provider.stats.keyRequired = true;
assert.deepEqual(
[layer.getStats().loadingLabel, layer.getStats().error],
['KEY REQUIRED', 'KEY REQUIRED'],
);
// A rejected key gates the provider exactly like a missing one.
provider.stats.keyRequired = false;
provider.stats.keyRejected = true;
provider.stats.error = 'Mapillary rejected MAPILLARY_CLIENT_TOKEN';
const stats = layer.getStats();
assert.equal(stats.keyRequired, true);
assert.equal(stats.loadingLabel, 'KEY REJECTED');
assert.equal(stats.error, 'Mapillary rejected MAPILLARY_CLIENT_TOKEN');
const ui = layer.getUIState();
assert.equal(ui.keyRequired, true);
assert.equal(ui.keyRejected, true);
assert.equal(ui.providers[0].keyRequired, true);
});
test('switching a provider off deactivates it and closes the image it shows', async (t) => {
const provider = fakeProvider();
const { layer } = await enabledLayer(t, [provider]);
layer.attachViewerHost({});
assert.equal(await layer.openImage('mapillary', 'img1'), true);
assert.deepEqual(provider.calls.open, ['img1']);
assert.equal(layer.getUIState().street.open, true);
layer.setProviderEnabled('mapillary', false);
assert.equal(provider.calls.deactivate, 1);
assert.equal(provider.calls.unmount, 1, 'its viewer is released');
assert.equal(layer.getUIState().street.open, false);
assert.equal(layer.getUIState().street.providerId, null);
});
test('openNearest without the panel reports it instead of loading forever', async (t) => {
const provider = fakeProvider({ nearestImage: async () => 'img1' });
const { layer } = await enabledLayer(t, [provider]);
assert.equal(await layer.openNearest({ lat: 38.58, lon: -121.49 }), false);
const { street } = layer.getUIState();
assert.equal(street.loading, false);
assert.match(street.error, /Open the Street Level panel/);
assert.equal(provider.calls.mount, 0);
});
test('openNearest opens nothing once the layer went off during the lookup', async (t) => {
const answer = deferred();
const provider = fakeProvider({ nearestImage: () => answer.promise });
const { layer } = await enabledLayer(t, [provider]);
layer.attachViewerHost({});
const opening = layer.openNearest({ lat: 38.58, lon: -121.49 });
layer.disable();
answer.resolve('img1');
assert.equal(await opening, false);
assert.equal(provider.calls.mount, 0, 'no viewer was stood up');
const { street } = layer.getUIState();
assert.equal(street.open, false);
assert.equal(street.loading, false);
assert.equal(street.error, null);
});
test('openNearest skips a provider switched off during its lookup', async (t) => {
const answer = deferred();
const provider = fakeProvider({ nearestImage: () => answer.promise });
const { layer } = await enabledLayer(t, [provider]);
layer.attachViewerHost({});
const opening = layer.openNearest({ lat: 38.58, lon: -121.49 });
layer.setProviderEnabled('mapillary', false);
answer.resolve('img1');
assert.equal(await opening, false);
assert.deepEqual(provider.calls.open, []);
assert.equal(layer.getUIState().street.loading, false);
});
test('a provider withdraws only the error it reported', async (t) => {
const provider = fakeProvider();
const { layer } = await enabledLayer(t, [provider]);
const { actions } = provider.context();
actions.reportError('Sequence images unavailable');
assert.equal(layer.getUIState().street.error, 'Sequence images unavailable');
actions.reportError(null);
assert.equal(layer.getUIState().street.error, null, 'withdrawn');
// The viewer's own error is not the provider's to clear.
layer.attachViewerHost(null);
actions.reportError('Sequence images unavailable');
await layer.openImage('mapillary', 'img1');
actions.reportError(null);
assert.match(layer.getUIState().street.error, /Open the Street Level panel/);
});
test('closing the photo stops the globe flying to it', async (t) => {
const { layer, viewer } = await enabledLayer(t, [posingProvider()]);
layer.attachViewerHost({});
assert.equal(await layer.openImage('mapillary', 'img1'), true);
assert.equal(viewer.flights.started, 1, 'the photo is framed');
layer.closeViewer();
assert.equal(viewer.flights.cancelled, 1);
});
test('switching the layer off or destroying it stops the framing flight', async (t) => {
const { layer, viewer } = await enabledLayer(t, [posingProvider()]);
layer.attachViewerHost({});
await layer.openImage('mapillary', 'img1');
layer.disable();
assert.equal(viewer.flights.cancelled, 1, 'layer off');
layer.enable(viewer);
await layer.openImage('mapillary', 'img2');
layer.destroy();
assert.equal(viewer.flights.cancelled, 2, 'destroyed');
});
test('closing the photo leaves a newer navigation flight alone', async (t) => {
const { layer, viewer } = await enabledLayer(t, [posingProvider()]);
layer.attachViewerHost({});
await layer.openImage('mapillary', 'img1');
assert.equal(viewer.flights.started, 1, 'the photo is being framed');
// A search result flies the globe elsewhere before the framing lands.
viewer.camera.flyToBoundingSphere(null, {});
layer.closeViewer();
assert.equal(viewer.flights.cancelled, 0, 'the search flight keeps going');
});
test('an older nearest lookup that answers late cannot replace a newer one', async (t) => {
const { lookups, nearestImage } = slowLookups();
const provider = fakeProvider({ nearestImage });
const { layer } = await enabledLayer(t, [provider]);
layer.attachViewerHost({});
const older = layer.openNearest({ lat: 38.58, lon: -121.49 });
const newer = layer.openNearest({ lat: 38.59, lon: -121.48 });
lookups[1].answer.resolve('newer');
assert.equal(await newer, true);
lookups[0].answer.resolve('older');
assert.equal(await older, false);
assert.deepEqual(provider.calls.open, ['newer']);
assert.equal(lookups[0].signal?.aborted, true, 'its request was aborted');
assert.equal(lookups[1].signal?.aborted, false);
});
test('an image picked during a nearest lookup wins over its late answer', async (t) => {
const { lookups, nearestImage } = slowLookups();
const provider = fakeProvider({ nearestImage });
const { layer } = await enabledLayer(t, [provider]);
layer.attachViewerHost({});
const lookup = layer.openNearest({ lat: 38.58, lon: -121.49 });
// A cone click goes through the provider's openImage action.
assert.equal(await provider.context().actions.openImage('picked'), true);
lookups[0].answer.resolve('nearest');
assert.equal(await lookup, false);
assert.deepEqual(provider.calls.open, ['picked']);
assert.equal(lookups[0].signal?.aborted, true);
});
test('closing the viewer or switching the layer off retires a nearest lookup', async (t) => {
const { lookups, nearestImage } = slowLookups();
const provider = fakeProvider({ nearestImage });
const { layer, viewer } = await enabledLayer(t, [provider]);
layer.attachViewerHost({});
const closed = layer.openNearest({ lat: 38.58, lon: -121.49 });
layer.closeViewer();
// The aborted fetch rejects, as fetch does; that is not the user's error.
lookups[0].answer.reject(new DOMException('aborted', 'AbortError'));
assert.equal(await closed, false);
let { street } = layer.getUIState();
assert.deepEqual(
[street.open, street.loading, street.error],
[false, false, null],
);
assert.equal(lookups[0].signal?.aborted, true, 'closing aborts the request');
const disabled = layer.openNearest({ lat: 38.58, lon: -121.49 });
layer.disable();
layer.enable(viewer);
lookups[1].answer.resolve('img1');
assert.equal(await disabled, false, 'nor does it open once back on');
assert.deepEqual(provider.calls.open, []);
({ street } = layer.getUIState());
assert.deepEqual(
[street.open, street.loading, street.error],
[false, false, null],
);
assert.equal(lookups[1].signal?.aborted, true, 'layer off aborts it too');
});
test('the Mapillary provider hands the lookup signal to its source', async () => {
const requests = [];
const source = {
hasToken: () => true,
getStatus: async () => ({ configured: true }),
getTile: async () => new Uint8Array(0),
getSequenceImages: async () => [],
nearestImages: async (query, options) => {
requests.push(options);
return [{ id: 7, is_pano: false, captured_at: 10 }];
},
};
const instance = createMapillaryProvider({ source }).create({
services: {},
getFilter: () => ({ pano: 'all', sinceMs: null }),
isActive: () => true,
notify() {},
actions: { openImage() {}, reportError() {} },
});
const { signal } = new AbortController();
assert.equal(
await instance.nearestImage({ lat: 1, lon: 2 }, { signal }),
'7',
);
assert.equal(requests[0]?.signal, signal);
});
test('setParams takes "any date" (0 days) and a provider switch over the current values (share-link defaults)', async (t) => {
const provider = fakeProvider();
const { layer } = await enabledLayer(t, [provider]);
layer.setParams({ pano: 'flat', sinceDays: 365 });
assert.equal(layer.getParams().sinceDays, 365);
layer.setParams({ sinceDays: 0 });
assert.deepEqual(layer.getParams(), {
mapillary: true,
pano: 'flat',
sinceDays: 0,
});
assert.equal(
provider.filters.at(-1).sinceMs,
null,
'providers drop the cut-off',
);
layer.setParams({ mapillary: false });
assert.equal(layer.getParams().mapillary, false);
});
/** A layer initialised on a stand-in viewer but never enabled. */
function initialisedLayer(t, providers) {
const saved = globalThis.document;
const drawing = new Proxy({}, { get: () => () => ({ addColorStop() {} }) });
globalThis.document = Object.assign(new EventTarget(), {
createElement: () => ({ getContext: () => drawing }),
});
const viewer = fakeViewer();
const layer = createStreetLevelLayer({ providers });
layer.init(viewer);
t.after(() => {
layer.destroy();
globalThis.document = saved;
});
return { layer, viewer };
}
test('switching a provider on while the layer is off activates nothing (M01)', async (t) => {
const provider = fakeProvider();
const prewarmed = [];
const create = provider.create;
provider.create = (context) => {
const instance = create(context);
instance.viewer.prewarm = async (host) => prewarmed.push(host);
return instance;
};
const { layer, viewer } = initialisedLayer(t, [provider]);
layer.attachViewerHost({});
layer.setProviderEnabled('mapillary', false);
layer.setProviderEnabled('mapillary', true);
layer.setParams({ mapillary: false });
layer.setParams({ mapillary: true });
await new Promise((resolve) => setTimeout(resolve, 40)); // past whenIdle
assert.equal(provider.calls.activate, 0, 'no coverage drawn');
assert.deepEqual(viewer.credits, [], 'no credit shown');
assert.deepEqual(prewarmed, [], 'no viewer stood up');
assert.equal(layer.getUIState().providers[0].on, true, 'the switch is kept');
// Enabling the layer is what activates it, once.
layer.enable(viewer);
assert.equal(provider.calls.activate, 1);
assert.equal(viewer.credits.length, 1);
});
/** The real Google provider over the Street View fakes, beside a Mapillary stand-in. */
async function withStreetView(
t,
{ mapStack = 'photoreal', nearest, mapillaryKeyless = false } = {},
) {
const { createGoogleProvider } = await import('./providers/google/index.js');
const { fakeHost, fakeMapsLoader, fakeStreetViewLibrary } =
await import('../../testSupport/googleStreetViewFakes.mjs');
const fake = fakeStreetViewLibrary({
panoramas: {
'pano-a': { lat: 38.5816, lng: -121.4944, imageDate: '2024-05' },
},
nearest: nearest ?? (() => 'pano-a'),
});
const mapillaryLookups = [];
const mapillary = fakeProvider({
nearestImage: async (point) => {
mapillaryLookups.push(point);
return null;
},
});
mapillary.requiresKeyId = 'mapillary';
if (mapillaryKeyless) {
mapillary.stats.keyRequired = true;
const create = mapillary.create;
mapillary.create = (context) => ({
...create(context),
status: async () => ({ configured: false }),
});
}
const loader = fakeMapsLoader(fake.library);
const google = createGoogleProvider({
getApiKey: () => 'browser-key',
loader,
});
const { layer, viewer } = await enabledLayer(t, [mapillary, google]);
const stack = fakeMapStack(mapStack);
layer.attachMapStackController(stack);
layer.attachViewerHost(fakeHost());
return { layer, viewer, stack, fake, loader, mapillaryLookups };
}
test('Street View starts off, and the layer row names the key of the providers that are on', async (t) => {
const { layer } = await withStreetView(t);
const google = () =>
layer.getUIState().providers.find((p) => p.id === 'google');
assert.equal(google().on, false);
assert.equal(layer.requiresKeyId, 'mapillary');
layer.setProviderEnabled('google', true);
assert.equal(layer.requiresKeyId, 'google-maps', 'Street View alone');
});
test('the layer starts with one source lit: the first that defaults on', async (t) => {
const { layer } = await enabledLayer(t, [
fakeProvider(),
fakeProvider({ id: 'panoramax', pickPrefix: 'pmx:' }),
]);
assert.deepEqual(
layer
.getUIState()
.providers.filter((p) => p.on)
.map((p) => p.id),
['mapillary'],
);
});
test('leaving Google 3D during a Street View lookup ends it without an error or a stuck spinner', async (t) => {
const { layer, stack, fake } = await withStreetView(t);
layer.setProviderEnabled('google', true);
let answer;
fake.library.StreetViewService.prototype.getPanorama = () =>
new Promise((resolve) => {
answer = resolve;
});
const opening = layer.openNearest({ lat: 38.5816, lon: -121.4944 });
await settle();
assert.equal(layer.getUIState().street.loading, true);
stack.switchTo('bing');
answer({ data: { location: { pano: 'pano-a' }, imageDate: '2024-05' } });
assert.equal(await opening, false);
const { street } = layer.getUIState();
assert.equal(street.loading, false, 'OPEN STREET VIEW is usable again');
assert.equal(street.error, null);
assert.equal(street.open, false);
});
test('a source swap during a lookup reports no false "no imagery"', async (t) => {
let answer;
const mapillary = fakeProvider({
nearestImage: () =>
new Promise((resolve) => {
answer = resolve;
}),
});
const other = fakeProvider({ id: 'panoramax', pickPrefix: 'pmx:' });
const { layer } = await enabledLayer(t, [mapillary, other]);
const opening = layer.openNearest({ lat: 1, lon: 2 });
layer.setProviderEnabled('panoramax', true);
answer('img-1');
assert.equal(await opening, false);
assert.equal(layer.getUIState().street.error, null);
assert.equal(layer.getUIState().street.loading, false);
});
test('Street View lit off Google 3D says why the panel is empty', async (t) => {
const { layer, stack } = await withStreetView(t);
layer.setProviderEnabled('google', true);
stack.switchTo('esri-imagery');
assert.match(
layer.getUIState().coverage.hint,
/Google Street View needs the Google 3D map/,
);
layer.setProviderEnabled('mapillary', true);
assert.doesNotMatch(layer.getUIState().coverage.hint, /Google 3D/);
});
test('one imagery source at a time: lighting one darkens the other, either way', async (t) => {
const { layer } = await withStreetView(t);
const lit = () =>
layer
.getUIState()
.providers.filter((p) => p.on)
.map((p) => p.id);
assert.deepEqual(lit(), ['mapillary'], 'Mapillary by default: it is free');
layer.setProviderEnabled('google', true);
assert.deepEqual(lit(), ['google']);
assert.equal(layer.getParams().mapillary, false);
layer.setProviderEnabled('mapillary', true);
assert.deepEqual(lit(), ['mapillary']);
assert.equal(layer.getParams().google, false);
// Params that light one switch to it; reapplying them changes nothing.
layer.setParams({ google: true });
assert.deepEqual(lit(), ['google']);
layer.setParams({ google: true });
assert.deepEqual(lit(), ['google']);
// Params lighting both keep the one already on, however often applied.
for (let i = 0; i < 2; i++) {
layer.setParams({ mapillary: true, providerSwitches: 'google-1' });
assert.deepEqual(lit(), ['google']);
}
layer.setParams({ mapillary: true, google: false });
assert.deepEqual(lit(), ['mapillary']);
// Filter-only params leave the source alone.
layer.setParams({ pano: 'pano' });
assert.deepEqual(lit(), ['mapillary']);
// Darkening the only lit one leaves none: the panel turns the layer off.
layer.setProviderEnabled('mapillary', false);
assert.deepEqual(lit(), []);
});
test('Street View appears only on the Google 3D map: chip, legend, credit, source and images', async (t) => {
const { layer, viewer, stack } = await withStreetView(t, {
mapStack: 'esri-imagery',
});
layer.setProviderEnabled('google', true);
await settle();
const ui = () => layer.getUIState();
const listed = () => ui().providers.map((p) => p.id);
const credited = () =>
viewer.credits.some((credit) => /Street View imagery/.test(credit.html));
assert.deepEqual(listed(), ['mapillary'], 'no Street View chip');
assert.ok(!ui().legend.some((item) => item.key === 'google'));
assert.equal(credited(), false, 'no Google credit on another map');
assert.equal(layer.source, 'Mapillary');
assert.equal(layer.getStats().source, 'Mapillary');
assert.equal(await layer.openImage('google', 'pano-a'), false);
assert.match(ui().street.error, /needs the Google 3D map/);
assert.equal(layer.getParams().google, true, 'the choice is kept');
stack.switchTo('photoreal');
assert.deepEqual(listed(), ['mapillary', 'google']);
assert.equal(credited(), true);
assert.equal(layer.source, 'Mapillary · Google Street View');
assert.equal(await layer.openImage('google', 'pano-a'), true);
assert.equal(ui().street.providerId, 'google');
stack.switchTo('bing');
await settle();
assert.equal(ui().street.open, false, 'closed with the map');
assert.equal(credited(), false);
assert.deepEqual(listed(), ['mapillary']);
});
test('the UI state says when a click (or the button) can open Street View here', async (t) => {
const { layer } = await withStreetView(t);
assert.equal(typeof layer.getUIState().groundClickReady, 'boolean');
});
test('a lookup for the click-to-open providers asks only them and reports "that point"', async (t) => {
const { layer, mapillaryLookups } = await withStreetView(t, {
nearest: () => null,
});
layer.setProviderEnabled('google', true);
await settle();
const opened = await layer.openNearest(
{ lat: 38.58, lon: -121.49 },
{ providerIds: ['google'], frame: false },
);
assert.equal(opened, false);
assert.deepEqual(mapillaryLookups, [], 'Mapillary was not asked');
assert.equal(
layer.getUIState().street.error,
'No street-level imagery within 50 m of that point',
);
});
test('the nearest-image refusal is written in the language of the page', async (t) => {
const { layer } = await withStreetView(t, { nearest: () => null });
layer.setProviderEnabled('google', true);
await settle();
setLocale('es');
try {
await layer.openNearest(
{ lat: 38.58, lon: -121.49 },
{ providerIds: ['google'], frame: false },
);
const error = layer.getUIState().street.error;
assert.notEqual(error, 'No street-level imagery within 50 m of that point');
assert.match(error, /50/, 'keeps the radius');
} finally {
setLocale('en');
}
});
test('an image opened from a ground click leaves the camera where it is', async (t) => {
const { layer, viewer } = await withStreetView(t);
layer.setProviderEnabled('google', true);
await settle();
await layer.openNearest(
{ lat: 38.58, lon: -121.49 },
{ providerIds: ['google'], frame: false },
);
assert.equal(layer.getUIState().street.imageId, 'pano-a');
assert.equal(viewer.flights.started, 0, 'no framing flight');
});
test('off Google 3D not even Google\u2019s script is loaded for Street View', async (t) => {
const { layer, loader } = await withStreetView(t, {
mapStack: 'esri-imagery',
});
layer.setProviderEnabled('google', true);
layer.attachViewerHost({ ownerDocument: { createElement: () => ({}) } });
await new Promise((resolve) => setTimeout(resolve, 100));
assert.deepEqual(loader.imports, [], 'no prewarm, no lookup');
});
test('the button’s “centre of the view” is the ground under the middle of the screen', async (t) => {
const { createStreetLevelLayer: create } = await import('./index.js');
const scenePick = await import('../../data/scenePick.js');
const lookups = [];
const provider = fakeProvider({
nearestImage: async (point) => {
lookups.push(point);
return null;
},
});
const saved = globalThis.document;
const drawing = new Proxy({}, { get: () => () => ({ addColorStop() {} }) });
globalThis.document = Object.assign(new EventTarget(), {
createElement: () => ({ getContext: () => drawing }),
});
const viewer = fakeViewer();
const centre = Cesium.Cartesian3.fromDegrees(-121.4937, 38.5799, 0);
Object.assign(viewer.scene, {
pickPositionSupported: true,
pickPosition: (at) => (at.x === 500 && at.y === 300 ? centre : undefined),
});
viewer.scene.canvas.clientWidth = 1000;
viewer.scene.canvas.clientHeight = 600;
const layer = create({ providers: [provider], services: { scenePick } });
layer.init(viewer);
layer.enable(viewer);
t.after(() => {
layer.destroy();
globalThis.document = saved;
});
await layer.openNearest(undefined, { aim: 'screen-centre' });
assert.equal(lookups.length, 1);
assert.ok(Math.abs(lookups[0].lat - 38.5799) < 1e-9);
assert.ok(Math.abs(lookups[0].lon - -121.4937) < 1e-9);
});
test('the button aimed at the sky opens nothing, and says to aim at a street', async (t) => {
const { createStreetLevelLayer: create } = await import('./index.js');
const scenePick = await import('../../data/scenePick.js');
const lookups = [];
const provider = fakeProvider({
nearestImage: async (point) => {
lookups.push(point);
return null;
},
});
const saved = globalThis.document;
const drawing = new Proxy({}, { get: () => () => ({ addColorStop() {} }) });
globalThis.document = Object.assign(new EventTarget(), {
createElement: () => ({ getContext: () => drawing }),
});
const viewer = fakeViewer();
// Every ground pick misses: the middle of the screen is sky.
Object.assign(viewer.scene, {
pickPositionSupported: true,
pickPosition: () => undefined,
});
viewer.camera.pickEllipsoid = () => undefined;
viewer.scene.canvas.clientWidth = 1000;
viewer.scene.canvas.clientHeight = 600;
const layer = create({ providers: [provider], services: { scenePick } });
layer.init(viewer);
layer.enable(viewer);
t.after(() => {
layer.destroy();
globalThis.document = saved;
});
assert.equal(
await layer.openNearest(undefined, { aim: 'screen-centre' }),
false,
);
assert.deepEqual(lookups, [], 'no lookup at the view centre instead');
const { street } = layer.getUIState();
assert.match(street.error, /Aim the middle of the view at a street/);
assert.equal(street.loading, false);
});
test('with no Mapillary token, the layer points at Street View, on Google 3D only', async (t) => {
const { layer, stack } = await withStreetView(t, { mapillaryKeyless: true });
await settle();
const stats = layer.getStats();
assert.equal(stats.keyRequired, true);
assert.deepEqual(stats.keyAlternatives, [
{ id: 'google', label: 'GOOGLE STREET VIEW', requiresKeyId: 'google-maps' },
]);
assert.deepEqual(layer.getUIState().keyAlternatives, stats.keyAlternatives);
stack.switchTo('esri-imagery');
assert.deepEqual(layer.getStats().keyAlternatives, [], 'not off Google 3D');
// Lit, Street View draws: the layer is no longer key-gated.
stack.switchTo('photoreal');
layer.setProviderEnabled('google', true);
assert.equal(layer.getStats().keyRequired, false);
});
test('an empty provider composition enables and tears down without acquiring imagery', async (t) => {
const { layer, viewer } = await enabledLayer(t, []);
assert.deepEqual(layer.providerIds, []);
assert.deepEqual(layer.getUIState().providers, []);
assert.equal(layer.getUIState().keyRequired, false);
assert.match(
layer.getUIState().coverage.hint,
/No street-level imagery providers/,
);
layer.disable();
layer.enable(viewer);
assert.deepEqual(layer.getUIState().providers, []);
});
test('a failed lookup’s error goes when another source is lit; a provider can open an image without framing', async (t) => {
const failing = fakeProvider({
nearestImage: async () => {
throw new Error('KartaView did not answer in time — try again');
},
});
const other = fakeProvider({ id: 'other', pickPrefix: 'oth:' });
const { layer, viewer } = await enabledLayer(t, [failing, other]);
layer.attachViewerHost({});
await layer.openNearest({ lat: 1, lon: 1 });
assert.match(layer.getUIState().street.error, /did not answer/);
layer.setProviderEnabled('other', true);
await settle();
assert.equal(layer.getUIState().street.error, null);
// A provider's own open (KartaView's ‹ ›) can leave the camera alone.
await other.context().actions.openImage('img-1', { frame: false });
assert.deepEqual(other.calls.open, ['img-1']);
assert.equal(viewer.flights.started, 0);
});
test('a provider’s own open can leave the camera where it is; a default open frames', async (t) => {
const provider = posingProvider();
const { layer, viewer } = await enabledLayer(t, [provider]);
layer.attachViewerHost({});
await provider.context().actions.openImage('img-1', { frame: false });
assert.deepEqual(provider.calls.open, ['img-1']);
assert.equal(viewer.flights.started, 0, 'frame: false keeps the camera');
await provider.context().actions.openImage('img-2');
assert.equal(viewer.flights.started, 1, 'a default open frames the image');
});
test('a provider’s openNearest (a line click) asks only that provider and leaves the camera', async (t) => {
const lit = posingProvider({ nearestImage: async () => 'lit-near' });
const other = fakeProvider({
id: 'other',
pickPrefix: 'oth:',
nearestImage: async () => 'other-near',
});
const { layer, viewer } = await enabledLayer(t, [lit, other]);
layer.attachViewerHost({});
const point = { lat: 38.58, lon: -121.49 };
// The other source is off: its click asks nobody else.
await other.context().actions.openNearest(point);
await settle();
assert.deepEqual(lit.calls.open, [], 'the lit source was not asked');
assert.deepEqual(other.calls.open, []);
await lit.context().actions.openNearest(point);
assert.deepEqual(lit.calls.open, ['lit-near']);
assert.equal(viewer.flights.started, 0, 'the user picked the spot');
});
test('KartaView through the core: its own swatch without Selected, its lines counted as streets', async (t) => {
const kartaView = createKartaViewProvider({
source: {
getTile: async () => ({ lines: [] }),
nearestPhotos: async () => [],
getPhoto: async () => {
throw new Error('KartaView has no such photo');
},
getSequencePhotos: async () => ({ photos: [], hasMore: false }),
probeImage: async () => true,
},
});
const { layer } = await enabledLayer(t, [kartaView]);
layer.setProviderEnabled('kartaview', true);
await settle();
const ui = layer.getUIState();
assert.deepEqual(
ui.legend.map((entry) => entry.key),
['kartaview'],
);
assert.equal(ui.coverage.label?.noun, 'streets');
assert.equal(
ui.providers.find((p) => p.id === 'kartaview').highlightsSequence,
false,
);
});
test('switching source by params (the chip path) stops a running lookup and clears its spinner and error', async (t) => {
const slow = slowLookups();
const looking = fakeProvider({ nearestImage: slow.nearestImage });
const other = fakeProvider({ id: 'other', pickPrefix: 'oth:' });
const { layer } = await enabledLayer(t, [looking, other]);
layer.attachViewerHost({});
const opening = layer.openNearest({ lat: 1, lon: 1 });
await settle();
assert.equal(layer.getUIState().street.loading, true);
layer.setParams({ mapillary: false, other: true });
assert.equal(slow.lookups[0].signal.aborted, true, 'the lookup is stopped');
assert.equal(
layer.getUIState().street.loading,
false,
'no spinner left behind',
);
// The stopped lookup answers late: nothing opens, nothing is reported.
slow.lookups[0].answer.resolve('img1');
assert.equal(await opening, false);
assert.deepEqual(looking.calls.open, []);
assert.equal(layer.getUIState().street.error, null);
});
test('switching source by params clears a failed lookup’s error; reapplying the same switches does not', async (t) => {
const failing = fakeProvider({
nearestImage: async () => {
throw new Error('KartaView did not answer in time — try again');
},
});
const { layer: second } = await enabledLayer(t, [
failing,
fakeProvider({ id: 'other', pickPrefix: 'oth:' }),
]);
second.attachViewerHost({});
await second.openNearest({ lat: 1, lon: 1 });
assert.match(second.getUIState().street.error, /did not answer/);
second.setParams({ mapillary: false, other: true });
assert.equal(second.getUIState().street.error, null);
// Reapplying the same switches (a restore) leaves state alone.
await second.openNearest({ lat: 1, lon: 1 });
const kept = second.getUIState().street.error;
assert.match(kept, /No street-level imagery/);
second.setParams({ mapillary: false, other: true });
assert.equal(second.getUIState().street.error, kept);
});