Files
gods-eye-view/src/ui/layerKeyRequirement.test.mjs
T
Said TurkseverandClaude Opus 5.5 e823e15025 fix(street-level): point a key-gated layer at Street View; button beside the hint
Review of #966: with a Google Maps key but no Mapillary token, switching
Street Level on read KEY REQUIRED / "Needs MAPILLARY_CLIENT_TOKEN" with
nothing about Street View, which stayed off (billing), so the layer looked
broken.

- While the switched-on providers lack their keys, the layer reports the
  switched-off ones that would draw with a key already set
  (keyAlternatives: Street View on Google 3D with a Google key). The
  panel's KEY REQUIRED tooltip, its coverage line and the layer row's
  tooltip then read "Needs MAPILLARY_CLIENT_TOKEN — add it in Provider
  Settings, or light STREET VIEW to use your Google Maps key"
  (keySetupWithAlternatives). Off Google 3D the plain KEY REQUIRED stays.
  Street View stays off by default.
- OPEN STREET VIEW moves beside the coverage hint: at the default panel
  width the PROVIDERS row has no room for it, so it wrapped onto a line of
  its own.
- The panel gate's corner drag names what lay under the pointer when it
  fails.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 00:05:58 +01:00

160 lines
5.2 KiB
JavaScript

// NAMING THE MISSING KEY — a control a provider key holds back must say which
// key, or the operator is left with a dead row and no next step. The three
// halves are pinned here: the layer declares which key it needs and reports
// that it is missing, the manager publishes that on the row, and the panel
// turns the pair into guidance that names the environment variable.
//
// Run with: npm test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { layerKeyRequirementTooltip } from './layerPanel.js';
import { DataLayerManager } from '../data/manager.js';
import { KEY_SETUP_KEYS } from '../keySetupCore.mjs';
import { createFirmsState } from '../layers/firms/state.js';
import { createModel } from '../layers/firms/model.js';
import { createQueries } from '../layers/firms/queries.js';
/** The fire layer's query half, with the scene services it never calls stubbed. */
function firmsQueries() {
const services = {
render: { governorRequestRender() {} },
anchors: { fireAnchorHeight: () => 0, FIRE_ANCHOR_LIFT_M: 0 },
overlays: {},
sprites: {},
context: {},
picking: {},
focus: {},
};
const config = { id: 'local-firms', name: 'FIRMS Active Fires' };
const layerState = createFirmsState({ services, config });
const components = {};
const context = {
layerState,
services,
components,
config,
feed: { getSnapshot: async () => ({}) },
};
components.model = createModel(context);
return { layerState, methods: createQueries(context).methods };
}
test('the fire layer names the key it needs and reports when it is missing', () => {
const { layerState, methods } = firmsQueries();
assert.equal(methods.requiresKeyId, 'firms');
assert.ok(
KEY_SETUP_KEYS.some((entry) => entry.id === 'firms'),
'the declared id must exist in the key registry',
);
assert.equal(
methods.getStats().keyRequired,
false,
'no key is missing until the feed says so',
);
layerState._keyRequired = true;
const stats = methods.getStats();
assert.equal(stats.keyRequired, true);
assert.equal(
stats.error,
'KEY REQUIRED',
'the human string the row already shows is unchanged',
);
});
test('the guidance names the environment variable and where to set it', () => {
const text = layerKeyRequirementTooltip({
requiresKeyId: 'firms',
stats: { keyRequired: true },
});
assert.match(text, /FIRMS_MAP_KEY/);
assert.match(text, /Provider Settings/);
});
test('guidance appears only for a key that is actually missing and actually named', () => {
for (const layer of [
undefined,
{},
{ requiresKeyId: 'firms' },
{ requiresKeyId: 'firms', stats: {} },
{ requiresKeyId: 'firms', stats: { keyRequired: false } },
// Truthy but not the boolean the contract asks for: a stray string must
// not be read as "the key is missing".
{ requiresKeyId: 'firms', stats: { keyRequired: 'yes' } },
// Missing key, but the layer never said which one.
{ stats: { keyRequired: true } },
{ requiresKeyId: '', stats: { keyRequired: true } },
{ requiresKeyId: ' ', stats: { keyRequired: true } },
// An id the registry does not know must not be guessed at: guidance
// naming the wrong variable sends the operator to the wrong provider.
{ requiresKeyId: 'not-a-provider', stats: { keyRequired: true } },
]) {
assert.equal(
layerKeyRequirementTooltip(layer),
'',
`${JSON.stringify(layer)} must produce no guidance`,
);
}
});
test('the manager publishes the declared key id on the row', async () => {
const manager = new DataLayerManager();
const base = {
name: 'Test layer',
icon: '•',
source: 'Test',
init() {},
enable() {},
disable() {},
update() {},
destroy() {},
};
manager.register({
...base,
id: 'gated',
requiresKeyId: 'firms',
getStats: () => ({ count: 0, keyRequired: true }),
});
manager.register({
...base,
id: 'ungated',
getStats: () => ({ count: 3, lastUpdate: Date.now() }),
});
// The manager consults a layer's getStats() only once it is initialized, so
// the rows are read the way the panel reads them: after the layers are on.
await manager.setEnabled('gated', true);
await manager.setEnabled('ungated', true);
const rows = manager.getAll();
const gated = rows.find((row) => row.id === 'gated');
const ungated = rows.find((row) => row.id === 'ungated');
assert.equal(gated.requiresKeyId, 'firms');
assert.equal(gated.stats.keyRequired, true);
assert.match(layerKeyRequirementTooltip(gated), /FIRMS_MAP_KEY/);
assert.equal(
ungated.requiresKeyId,
null,
'a layer that needs no key declares none',
);
assert.equal(layerKeyRequirementTooltip(ungated), '');
});
test('a key-gated layer with another source ready says how to use it', () => {
const text = layerKeyRequirementTooltip({
requiresKeyId: 'mapillary',
stats: {
keyRequired: true,
keyAlternatives: [
{ id: 'google', label: 'STREET VIEW', requiresKeyId: 'google-maps' },
],
},
});
assert.equal(
text,
'Needs MAPILLARY_CLIENT_TOKEN — add it in Provider Settings, or light STREET VIEW to use your Google Maps key',
);
});