mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 15:22:21 +00:00
The voice card showed English for show_street_level failures the catalog did not know: a missing key, a map or source that could not be switched on, the layer, invalid coordinates, an imagery fallback and the filter clause. They now translate like the other action errors (exact messages and patterns in speech.js), in all twelve languages; the spoken line stays English, and the filter clause names the filters as their chips do.
782 lines
23 KiB
JavaScript
782 lines
23 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {
|
|
SPEECH_BUILDERS,
|
|
attachVoiceResult,
|
|
planStepLabel,
|
|
progressLine,
|
|
progressStepLabel,
|
|
spokenLabel,
|
|
} from './speech.js';
|
|
import { presentResult } from './resultDisplay.js';
|
|
import { countWords, findHedges } from './speechLint.js';
|
|
import { setLocale } from '../i18n/index.js';
|
|
|
|
function envelope(name, result, args) {
|
|
return attachVoiceResult(name, result, args);
|
|
}
|
|
|
|
function assertSpeakable(say) {
|
|
assert.ok(countWords(say) <= 25, `too long: ${say}`);
|
|
assert.deepEqual(findHedges(say), [], `hedged: ${say}`);
|
|
}
|
|
|
|
test('layer toggles confirm the resulting state and fold only material feed tags', () => {
|
|
const on = envelope(
|
|
'set_layer_visibility',
|
|
{
|
|
ok: true,
|
|
action: 'set_layer_visibility',
|
|
layerId: 'flights',
|
|
label: 'Flights',
|
|
enabled: true,
|
|
feedState: 'stale',
|
|
},
|
|
{ layerId: 'flights', enabled: true },
|
|
);
|
|
assert.equal(on.say, 'Flights on, stale.');
|
|
const fallback = envelope(
|
|
'set_layer_visibility',
|
|
{
|
|
ok: true,
|
|
label: 'Flights',
|
|
enabled: true,
|
|
feedState: 'fallback',
|
|
source: 'adsb.lol',
|
|
},
|
|
{ enabled: true },
|
|
);
|
|
assert.equal(
|
|
fallback.say,
|
|
'Flights on.',
|
|
'a healthy fallback feed is valid data, shown not spoken',
|
|
);
|
|
assert.deepEqual(fallback.display.sources, [{ label: 'adsb.lol' }]);
|
|
const unavailable = envelope(
|
|
'set_layer_visibility',
|
|
{
|
|
ok: true,
|
|
label: 'Active Fires',
|
|
enabled: true,
|
|
feedState: 'unavailable',
|
|
},
|
|
{ enabled: true },
|
|
);
|
|
assert.equal(
|
|
unavailable.say,
|
|
'Active Fires on, but its feed is unavailable.',
|
|
);
|
|
const off = envelope(
|
|
'set_layer_visibility',
|
|
{ ok: true, label: 'Flights', enabled: false },
|
|
{ enabled: false },
|
|
);
|
|
assert.equal(off.say, 'Flights off.');
|
|
const failed = envelope(
|
|
'set_layer_visibility',
|
|
{
|
|
ok: false,
|
|
layerId: 'traffic',
|
|
error: 'Could not enable the requested layer',
|
|
},
|
|
{ layerId: 'traffic', enabled: true },
|
|
);
|
|
assert.equal(failed.say, "Couldn't turn on traffic.");
|
|
assert.equal(failed.ok, false, 'the envelope never changes the outcome');
|
|
});
|
|
|
|
test('aircraft identity always covers operator, type and route from returned fields only', () => {
|
|
const full = envelope('get_entity_context', {
|
|
ok: true,
|
|
selected: {
|
|
id: 'a1b2c3',
|
|
layerId: 'flights',
|
|
name: 'UAL428',
|
|
source: 'OpenSky',
|
|
properties: {
|
|
callsign: 'UAL428',
|
|
operator: 'United Airlines',
|
|
type: 'B738',
|
|
routeOrigin: 'KIAH',
|
|
routeDestination: 'KSFO',
|
|
registration: 'N12345',
|
|
},
|
|
},
|
|
});
|
|
assert.equal(
|
|
full.identityLine,
|
|
'UAL428, United Airlines, B738, KIAH to KSFO.',
|
|
);
|
|
assert.equal(
|
|
full.say,
|
|
null,
|
|
'the identity line is not forced onto other questions',
|
|
);
|
|
assert.ok(
|
|
full.display.lines.includes('Registration N12345'),
|
|
'registration is shown, not spoken',
|
|
);
|
|
assert.deepEqual(full.referents, [
|
|
{ n: 1, id: 'a1b2c3', label: 'UAL428', layerId: 'flights' },
|
|
]);
|
|
|
|
const sparse = envelope('get_entity_context', {
|
|
ok: true,
|
|
selected: {
|
|
id: 'ae01',
|
|
layerId: 'military',
|
|
name: 'RCH123',
|
|
properties: { callsign: 'RCH123' },
|
|
},
|
|
});
|
|
assert.equal(
|
|
sparse.identityLine,
|
|
'RCH123, operator unknown, type unknown, no route on file.',
|
|
);
|
|
assertSpeakable(sparse.identityLine);
|
|
|
|
const routeString = envelope('get_entity_context', {
|
|
ok: true,
|
|
selected: {
|
|
id: 'x',
|
|
layerId: 'flights',
|
|
properties: {
|
|
callsign: 'DAL1',
|
|
operator: 'Delta',
|
|
type: 'A321',
|
|
route: 'KATL-KLAX',
|
|
},
|
|
},
|
|
});
|
|
assert.equal(
|
|
routeString.identityLine,
|
|
'DAL1, Delta, A321, KATL to KLAX.',
|
|
'endpoint codes stay verbatim',
|
|
);
|
|
|
|
const place = envelope('get_entity_context', {
|
|
ok: true,
|
|
selected: {
|
|
id: 'dc',
|
|
layerId: 'local-datacenters',
|
|
name: 'Austin DC-7',
|
|
properties: {},
|
|
},
|
|
});
|
|
assert.equal(
|
|
place.say,
|
|
null,
|
|
'non-aircraft answers are composed by the model',
|
|
);
|
|
assert.equal(place.display.title, 'Austin DC-7');
|
|
const inView = { ok: true, scope: 'in_view', visible: [] };
|
|
assert.equal(
|
|
envelope('get_entity_context', inView),
|
|
inView,
|
|
'no selection leaves the result unchanged',
|
|
);
|
|
});
|
|
|
|
test('nearest-aircraft results name the aircraft, distance and altitude briefly', () => {
|
|
const ok = envelope('select_nearest_aircraft', {
|
|
ok: true,
|
|
action: 'select_nearest_aircraft',
|
|
location: 'Austin',
|
|
layerId: 'flights',
|
|
label: 'SWA1940',
|
|
feed: { state: 'nominal', source: 'OpenSky', count: 212 },
|
|
aircraft: {
|
|
id: 'a0b1',
|
|
callsign: 'SWA1940',
|
|
altitudeM: 9449,
|
|
distanceKm: 12.4,
|
|
},
|
|
});
|
|
assert.equal(ok.say, 'Selected SWA1940, 12 km from Austin, 31,000 feet.');
|
|
assertSpeakable(ok.say);
|
|
assert.equal(ok.referents[0].id, 'a0b1');
|
|
|
|
const empty = envelope('select_nearest_aircraft', {
|
|
ok: false,
|
|
stage: 'nearest',
|
|
layerId: 'flights',
|
|
location: { label: 'Austin' },
|
|
feed: { state: 'nominal' },
|
|
});
|
|
assert.equal(empty.say, 'No airborne aircraft loaded near Austin yet.');
|
|
const down = envelope('select_nearest_aircraft', {
|
|
ok: false,
|
|
stage: 'nearest',
|
|
feed: { state: 'unavailable', source: 'OpenSky' },
|
|
});
|
|
assert.equal(down.say, 'The aircraft feed is unavailable.');
|
|
const stale = envelope('select_nearest_aircraft', {
|
|
ok: true,
|
|
location: 'Austin',
|
|
feed: { state: 'stale' },
|
|
aircraft: { callsign: 'AAL1', distanceKm: 3.26 },
|
|
});
|
|
assert.equal(stale.say, 'Selected AAL1, 3.3 km from Austin, stale.');
|
|
});
|
|
|
|
test('nearest-aircraft results leave out an altitude the feed did not report', () => {
|
|
// gevActions sends `altitudeM: null` for an airborne military contact
|
|
// without a barometric altitude; that is unknown, not sea level.
|
|
const unknown = envelope('select_nearest_aircraft', {
|
|
ok: true,
|
|
location: 'Ramstein',
|
|
layerId: 'military',
|
|
feed: { state: 'nominal' },
|
|
aircraft: { callsign: 'RCH123', altitudeM: null, distanceKm: 8.2 },
|
|
});
|
|
assert.equal(unknown.say, 'Selected RCH123, 8.2 km from Ramstein.');
|
|
assert.deepEqual(unknown.display.lines, ['8.2\u00a0km from Ramstein']);
|
|
|
|
// A reported sea-level altitude is still spoken.
|
|
const seaLevel = envelope('select_nearest_aircraft', {
|
|
ok: true,
|
|
location: 'Ramstein',
|
|
layerId: 'military',
|
|
feed: { state: 'nominal' },
|
|
aircraft: { callsign: 'RCH123', altitudeM: 0, distanceKm: 8.2 },
|
|
});
|
|
assert.equal(seaLevel.say, 'Selected RCH123, 8.2 km from Ramstein, 0 feet.');
|
|
assert.deepEqual(seaLevel.display.lines, [
|
|
'8.2\u00a0km from Ramstein',
|
|
'Altitude 0 feet',
|
|
]);
|
|
});
|
|
|
|
test('frame_overhead keeps the essential scope audible and lists referents', () => {
|
|
const framed = envelope('frame_overhead', {
|
|
ok: true,
|
|
layerId: 'flights',
|
|
radiusKm: 150,
|
|
count: 14,
|
|
detectionEnabled: true,
|
|
nearest: [
|
|
{ id: 'a', label: 'UAL1' },
|
|
{ id: 'b', label: null },
|
|
{ id: null, label: null },
|
|
],
|
|
});
|
|
assert.equal(framed.say, 'Framed 14 aircraft within 150 km.');
|
|
assert.deepEqual(
|
|
framed.referents.map((r) => r.label),
|
|
['UAL1', 'b'],
|
|
);
|
|
const capped = envelope('frame_overhead', {
|
|
ok: true,
|
|
layerId: 'ais-live-vessels',
|
|
radiusKm: 120,
|
|
count: 80,
|
|
nearest: [],
|
|
});
|
|
assert.equal(capped.say, 'Framed at least 80 ships within 120 km.');
|
|
const off = envelope('frame_overhead', {
|
|
ok: false,
|
|
layerId: 'satellites',
|
|
error: 'The satellites layer is not enabled',
|
|
});
|
|
assert.equal(off.say, 'The satellites layer is off. Turn it on?');
|
|
const empty = envelope('frame_overhead', {
|
|
ok: false,
|
|
layerId: 'flights',
|
|
radiusKm: 150,
|
|
count: 0,
|
|
error: 'No flights within 150 km of the current view',
|
|
});
|
|
assert.equal(empty.say, 'No aircraft within 150 km.');
|
|
// The runner's real refusal envelope never claims the sky is empty.
|
|
const refused = envelope('frame_overhead', {
|
|
ok: false,
|
|
action: 'frame_overhead',
|
|
error: 'Camera navigation is unavailable in the current view',
|
|
});
|
|
assert.equal(refused.say, "Couldn't frame the contacts right now.");
|
|
const noPolicy = envelope('frame_overhead', {
|
|
ok: false,
|
|
action: 'frame_overhead',
|
|
error: 'Camera navigation policy unavailable',
|
|
});
|
|
assert.doesNotMatch(noPolicy.say, /^No /);
|
|
const cancelled = { ok: false, cancelled: true, action: 'frame_overhead' };
|
|
assert.equal(envelope('frame_overhead', cancelled), cancelled);
|
|
});
|
|
|
|
test('fly_to confirms the destination; cancellation stays silent', () => {
|
|
assert.equal(
|
|
envelope('fly_to_location', { ok: true, label: 'Tokyo' }).say,
|
|
'Flying to Tokyo.',
|
|
);
|
|
assert.equal(
|
|
envelope('fly_to_location', { ok: true, label: 'Austin', arrived: true })
|
|
.say,
|
|
'Over Austin.',
|
|
);
|
|
assert.equal(
|
|
envelope('fly_to_location', { ok: false, cancelled: true, label: 'Austin' })
|
|
.say,
|
|
undefined,
|
|
'a cancelled call keeps its exact outcome',
|
|
);
|
|
assert.equal(
|
|
envelope('fly_to_location', { ok: false, query: 'Atlantis' }).say,
|
|
"Couldn't find Atlantis.",
|
|
);
|
|
});
|
|
|
|
test('view state speaks enabled layers only and is a silent lookup by default', () => {
|
|
const view = envelope('get_current_view_state', {
|
|
ok: true,
|
|
camera: { heightM: 40210 },
|
|
style: 'thermal',
|
|
layers: [
|
|
{
|
|
id: 'flights',
|
|
name: 'Flights',
|
|
enabled: true,
|
|
count: 120,
|
|
feedState: 'stale',
|
|
},
|
|
{
|
|
id: 'local-firms',
|
|
name: 'Fires',
|
|
enabled: true,
|
|
count: 8,
|
|
feedState: 'nominal',
|
|
},
|
|
],
|
|
});
|
|
assert.equal(
|
|
view.say,
|
|
'Camera 40 km up. Thermal style. Flights and Fires on. Flights stale.',
|
|
);
|
|
assert.equal(view.schedule, 'silent');
|
|
assertSpeakable(view.say);
|
|
});
|
|
|
|
test('annotation speech names only what failed and never announces drawing', () => {
|
|
const clean = envelope('annotate_map', {
|
|
ok: true,
|
|
drawn: 2,
|
|
failed: 0,
|
|
items: [
|
|
{ ok: true, id: 'anno-1', label: 'Capitol' },
|
|
{ ok: true, id: 'anno-2', target: 'Zilker Park' },
|
|
],
|
|
});
|
|
assert.equal(clean.say, null);
|
|
assert.equal(clean.display.title, 'Marked 2 places');
|
|
assert.deepEqual(
|
|
clean.referents.map((r) => r.label),
|
|
['Capitol', 'Zilker Park'],
|
|
);
|
|
const partial = envelope('annotate_map', {
|
|
ok: true,
|
|
partial: true,
|
|
failedLabels: ['Flibbergibbet Building'],
|
|
items: [{ ok: true, label: 'Capitol' }, { ok: false }],
|
|
});
|
|
assert.equal(partial.say, "Couldn't place Flibbergibbet Building.");
|
|
const route = envelope('annotate_map', {
|
|
ok: true,
|
|
routeFallback: true,
|
|
items: [{ ok: true, label: 'A → B' }],
|
|
});
|
|
assert.equal(
|
|
route.say,
|
|
'That line is straight-line distance, not a street route.',
|
|
);
|
|
const pending = envelope('annotate_map', {
|
|
ok: true,
|
|
outlinePending: true,
|
|
items: [{ ok: true, label: 'Presidio' }],
|
|
});
|
|
assert.equal(pending.say, null);
|
|
assert.deepEqual(pending.display.notes, ['Tracing outlines']);
|
|
});
|
|
|
|
test('analyst speech preserves exact counts, lower bounds and partial status', () => {
|
|
const raw = {
|
|
ok: true,
|
|
action: 'analyst_query',
|
|
count: 3,
|
|
complete: true,
|
|
scopeLabel: 'anywhere in the loaded data',
|
|
coverage: { layersQueried: [{ layerKey: 'flights' }] },
|
|
};
|
|
assert.equal(
|
|
attachVoiceResult('analyst_query', raw).say,
|
|
'3 aircraft anywhere in the loaded data.',
|
|
);
|
|
assert.equal(
|
|
attachVoiceResult('analyst_query', {
|
|
...raw,
|
|
count: 250_000,
|
|
complete: false,
|
|
partial: true,
|
|
unanswered: ['military'],
|
|
}).say,
|
|
'At least 250,000 aircraft anywhere in the loaded data. Partial; military not answered.',
|
|
);
|
|
assert.equal(
|
|
attachVoiceResult('analyst_query', {
|
|
...raw,
|
|
count: 7,
|
|
coverage: {
|
|
layersQueried: [{ layerKey: 'flights' }, { layerKey: 'military' }],
|
|
},
|
|
}).say,
|
|
'7 aircraft anywhere in the loaded data.',
|
|
'combined Contacts aircraft layers retain their shared semantic noun',
|
|
);
|
|
for (const [feedState, spoken] of [
|
|
['stale', '3 aircraft anywhere in the loaded data, stale.'],
|
|
['degraded', '3 aircraft anywhere in the loaded data, degraded.'],
|
|
[
|
|
'unavailable',
|
|
'Feed unavailable anywhere in the loaded data; no authoritative count.',
|
|
],
|
|
]) {
|
|
const result = attachVoiceResult('analyst_query', {
|
|
...raw,
|
|
feedState,
|
|
feedProvenance: { overall: feedState },
|
|
});
|
|
assert.equal(result.say, spoken);
|
|
assert.equal(
|
|
presentResult('analyst_query', result).display.chips[0].label,
|
|
feedState,
|
|
'speech and card carry the same material feed state',
|
|
);
|
|
}
|
|
const longScope = `within the selected operational corridor ${'north-east '.repeat(8).trim()}`;
|
|
const longResult = attachVoiceResult('analyst_query', {
|
|
...raw,
|
|
scopeLabel: longScope,
|
|
});
|
|
assert.equal(
|
|
presentResult('analyst_query', longResult).display.title,
|
|
longResult.say.replace(/[.]$/, ''),
|
|
'card and speech consume the exact same long headline',
|
|
);
|
|
assert.ok(
|
|
longResult.say.length <= 64,
|
|
'the shared headline fits the voice card without a second truncation',
|
|
);
|
|
assert.match(longResult.say, /…$/);
|
|
});
|
|
|
|
test('builders never break an action and callers can override analyst speech', () => {
|
|
assert.equal(typeof SPEECH_BUILDERS.analyst_query, 'function');
|
|
const raw = { ok: true, action: 'analyst_query', count: 3 };
|
|
const hooked = attachVoiceResult(
|
|
'analyst_query',
|
|
raw,
|
|
{},
|
|
{ analyst_query: (r) => ({ say: `${r.count} found.` }) },
|
|
);
|
|
assert.equal(hooked.say, '3 found.');
|
|
const throwing = attachVoiceResult(
|
|
'x',
|
|
raw,
|
|
{},
|
|
{
|
|
x: () => {
|
|
throw new Error('boom');
|
|
},
|
|
},
|
|
);
|
|
assert.equal(throwing, raw);
|
|
assert.equal(attachVoiceResult('set_layer_visibility', null), null);
|
|
});
|
|
|
|
test('labels are bounded and cleaned before they are spoken', () => {
|
|
assert.equal(
|
|
spokenLabel(' Texas\u0000 State\n Capitol '),
|
|
'Texas State Capitol',
|
|
);
|
|
assert.equal(spokenLabel('x'.repeat(80)).length, 48);
|
|
assert.equal(
|
|
planStepLabel('annotate_map', {
|
|
annotations: [
|
|
{ target: 'Capitol' },
|
|
{ label: 'Zilker' },
|
|
{ target: 'UT' },
|
|
],
|
|
}),
|
|
'Mark Capitol and Zilker +1',
|
|
);
|
|
assert.equal(
|
|
planStepLabel('fly_to_location', { query: 'Tokyo' }),
|
|
'Fly to Tokyo',
|
|
);
|
|
assert.equal(
|
|
progressStepLabel('resolve', 'Capitol'),
|
|
'Finding places: Capitol',
|
|
);
|
|
for (const step of [
|
|
'resolve',
|
|
'search',
|
|
'fly',
|
|
'layer',
|
|
'refresh',
|
|
'nearest',
|
|
'still',
|
|
]) {
|
|
const line = progressLine(step, 'Austin');
|
|
assertSpeakable(line);
|
|
assert.ok(countWords(line) <= 12);
|
|
}
|
|
assert.equal(
|
|
progressLine('outline', 'Presidio'),
|
|
null,
|
|
'background steps stay on screen',
|
|
);
|
|
});
|
|
|
|
test('the street-level card says "the view centre" in the interface language', () => {
|
|
setLocale('de');
|
|
try {
|
|
const out = envelope('show_street_level', {
|
|
ok: true,
|
|
providerName: 'Mapillary',
|
|
label: 'the view centre',
|
|
atViewCentre: true,
|
|
});
|
|
assert.equal(out.display.lines[0], 'die Bildmitte');
|
|
assert.match(out.say, /the view centre/, 'speech stays English');
|
|
} finally {
|
|
setLocale('en');
|
|
}
|
|
});
|
|
|
|
test('street-level failure cards are in the interface language; speech stays English', () => {
|
|
setLocale('de');
|
|
try {
|
|
const card = (error, extra = {}) =>
|
|
envelope('show_street_level', {
|
|
ok: false,
|
|
action: 'show_street_level',
|
|
providerName: 'Google Street View',
|
|
label: 'Paris',
|
|
error,
|
|
...extra,
|
|
});
|
|
const lines = (error, extra) => card(error, extra).display.lines;
|
|
assert.deepEqual(lines('Google Street View needs its key', { stage: 'key' }), [
|
|
'Google Street View braucht seinen Schlüssel',
|
|
]);
|
|
assert.deepEqual(
|
|
lines('Google Street View needs the photoreal map, which is not available', { stage: 'map' }),
|
|
['Google Street View braucht die Google 3D-Karte, die nicht verfügbar ist'],
|
|
);
|
|
assert.deepEqual(lines('Mapillary could not be switched on', { stage: 'provider' }), [
|
|
'Mapillary konnte nicht eingeschaltet werden',
|
|
]);
|
|
assert.deepEqual(lines('Street Level could not be enabled', { stage: 'layer' }), [
|
|
'Straßenebene konnte nicht eingeschaltet werden',
|
|
]);
|
|
assert.deepEqual(
|
|
lines('Latitude must be within ±90° and longitude within ±180°', { stage: 'location' }),
|
|
['Der Breitengrad muss innerhalb von ±90° und der Längengrad innerhalb von ±180° liegen'],
|
|
);
|
|
assert.deepEqual(lines('Mapillary imagery could not be opened', { stage: 'imagery' }), [
|
|
'Bilder von Mapillary konnten nicht geöffnet werden',
|
|
]);
|
|
// The filter clause wraps the reason, already in the interface language,
|
|
// and names the filters as their chips do.
|
|
const filtered = card('No Mapillary imagery near Paris with the 360° and SINCE filters on', {
|
|
stage: 'imagery',
|
|
noImagery: true,
|
|
filter: { label: '360° and SINCE filters' },
|
|
});
|
|
assert.deepEqual(filtered.display.lines, [
|
|
'Keine Bilder von Mapillary in der Nähe von Paris (aktive Filter: 360°, SEIT)',
|
|
]);
|
|
assert.match(filtered.say, /with the 360° and SINCE filters on/, 'speech stays English');
|
|
} finally {
|
|
setLocale('en');
|
|
}
|
|
});
|
|
|
|
test('street-level speech names the source and the place, and says which step failed', () => {
|
|
const ok = envelope('show_street_level', {
|
|
ok: true,
|
|
action: 'show_street_level',
|
|
provider: 'google',
|
|
providerName: 'Google Street View',
|
|
label: '10th Street, Sacramento',
|
|
title: '1000 10th St, Sacramento, CA',
|
|
capturedAt: Date.UTC(2024, 4, 1), // Google dates: the first of the month, UTC
|
|
});
|
|
assert.equal(ok.say, 'Google Street View at 1000 10th St, Sacramento, CA.');
|
|
assertSpeakable(ok.say);
|
|
assert.equal(ok.display.title, 'Google Street View');
|
|
assert.equal(ok.display.lines[1], 'Captured May 2024');
|
|
|
|
const stages = {
|
|
location: "Couldn't find Nowhere.",
|
|
map: "Google Street View needs the Google 3D map, which isn't available.",
|
|
key: 'Google Street View needs its key.',
|
|
layer: "Couldn't turn on Street Level.",
|
|
// A service or viewer failure, not missing coverage.
|
|
imagery: 'Google Street View could not open imagery near Nowhere.',
|
|
};
|
|
for (const [stage, say] of Object.entries(stages)) {
|
|
const out = envelope('show_street_level', {
|
|
ok: false,
|
|
stage,
|
|
label: 'Nowhere',
|
|
providerName: 'Google Street View',
|
|
error: 'detail',
|
|
});
|
|
assert.equal(out.say, say, stage);
|
|
assertSpeakable(out.say);
|
|
assert.deepEqual(out.display.lines, ['detail']);
|
|
}
|
|
const empty = envelope('show_street_level', {
|
|
ok: false,
|
|
stage: 'imagery',
|
|
noImagery: true,
|
|
label: 'Nowhere',
|
|
providerName: 'Mapillary',
|
|
});
|
|
assert.equal(empty.say, 'No Mapillary imagery near Nowhere.');
|
|
const sky = envelope('show_street_level', {
|
|
ok: false,
|
|
stage: 'imagery',
|
|
aim: 'sky',
|
|
atViewCentre: true,
|
|
label: 'the view centre',
|
|
providerName: 'Mapillary',
|
|
});
|
|
assert.equal(sky.say, 'Aim the middle of the view at a street first.');
|
|
assertSpeakable(sky.say);
|
|
const armed = envelope('show_street_level', {
|
|
ok: true,
|
|
armed: true,
|
|
providerName: 'Google Street View',
|
|
label: '10th St, Sacramento',
|
|
});
|
|
assert.equal(armed.say, 'Google Street View is ready at 10th St, Sacramento: press OPEN to load it.');
|
|
assertSpeakable(armed.say);
|
|
assert.deepEqual(armed.display.lines, ['10th St, Sacramento', 'Press OPEN to load the photo']);
|
|
const unfound = envelope('show_street_level', {
|
|
ok: false,
|
|
stage: 'location',
|
|
location: { ok: false, action: 'fly_to_location', label: 'Nowhere Town' },
|
|
});
|
|
assert.equal(unfound.say, "Couldn't find Nowhere Town.");
|
|
assert.equal(unfound.display.title, 'Street Level');
|
|
const refused = envelope('show_street_level', {
|
|
ok: false,
|
|
stage: 'key',
|
|
keyRejected: true,
|
|
providerName: 'Google Street View',
|
|
});
|
|
assert.equal(refused.say, 'Google Street View refused its key.');
|
|
assert.equal(
|
|
envelope('show_street_level', { ok: false, cancelled: true }).say,
|
|
undefined,
|
|
);
|
|
assert.equal(
|
|
planStepLabel('show_street_level', {
|
|
query: 'Eiffel Tower',
|
|
provider: 'google',
|
|
}),
|
|
'Street View at Eiffel Tower',
|
|
);
|
|
assert.equal(
|
|
planStepLabel('show_street_level', {
|
|
query: 'Times Square',
|
|
provider: 'mapillary',
|
|
}),
|
|
'Mapillary at Times Square',
|
|
);
|
|
assert.equal(
|
|
progressStepLabel('imagery', 'Eiffel Tower'),
|
|
'Opening imagery: Eiffel Tower',
|
|
);
|
|
assert.equal(progressLine('map', 'photoreal'), 'Switching to Google 3D.');
|
|
assert.equal(
|
|
progressLine('imagery', 'Eiffel Tower'),
|
|
'Looking for imagery at Eiffel Tower.',
|
|
);
|
|
});
|
|
|
|
test('the capture month is a UTC month whatever the timezone, and labels name the place', async () => {
|
|
const { execFileSync } = await import('node:child_process');
|
|
const speech = new URL('./speech.js', import.meta.url).href;
|
|
// Google dates a panorama 2024-05 as the first of the month, UTC. West of
|
|
// Greenwich that instant is still April; the card must still say May.
|
|
const out = execFileSync(
|
|
process.execPath,
|
|
[
|
|
'--input-type=module',
|
|
'-e',
|
|
`import { attachVoiceResult } from ${JSON.stringify(speech)};
|
|
const r = attachVoiceResult('show_street_level', { ok: true, providerName: 'Google Street View', label: 'x', capturedAt: Date.UTC(2024, 4, 1) });
|
|
process.stdout.write(r.display.lines[1]);`,
|
|
],
|
|
{ env: { ...process.env, TZ: 'America/Los_Angeles' }, encoding: 'utf8' },
|
|
);
|
|
assert.equal(out, 'Captured May 2024');
|
|
const { narrationLabel } = await import('./speech.js');
|
|
assert.equal(narrationLabel('show_street_level', { query: 'Eiffel Tower' }), 'Eiffel Tower');
|
|
assert.equal(narrationLabel('show_street_level', {}), '');
|
|
assert.equal(
|
|
planStepLabel('show_street_level', { latitude: 1, longitude: 2 }),
|
|
'Street level at this spot',
|
|
);
|
|
assert.equal(planStepLabel('show_street_level', {}), 'Street level at the view centre');
|
|
});
|
|
|
|
test('the plan step names KartaView as a source', () => {
|
|
assert.equal(
|
|
planStepLabel('show_street_level', {
|
|
query: 'Alexanderplatz',
|
|
provider: 'kartaview',
|
|
}),
|
|
'KartaView at Alexanderplatz',
|
|
);
|
|
});
|
|
|
|
test('an empty street-level search names the imagery filter that hid the photos', () => {
|
|
const out = envelope('show_street_level', {
|
|
ok: false,
|
|
stage: 'imagery',
|
|
label: 'Alexanderplatz',
|
|
providerName: 'KartaView',
|
|
filter: { pano: 'pano', label: '360° filter' },
|
|
});
|
|
assert.equal(
|
|
out.say,
|
|
'No KartaView imagery near Alexanderplatz with the 360° filter on.',
|
|
);
|
|
assertSpeakable(out.say);
|
|
});
|
|
|
|
test('the plan step names Panoramax as a source', () => {
|
|
assert.equal(
|
|
planStepLabel('show_street_level', {
|
|
query: 'Montmartre',
|
|
provider: 'panoramax',
|
|
}),
|
|
'Panoramax at Montmartre',
|
|
);
|
|
});
|
|
|
|
test('progress step labels join through the catalog colon', () => {
|
|
setLocale('fr');
|
|
try {
|
|
assert.equal(
|
|
progressStepLabel('resolve', 'Capitol'),
|
|
'Recherche de lieux\u202f: Capitol',
|
|
);
|
|
} finally {
|
|
setLocale('en');
|
|
}
|
|
});
|