mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-13 23:02:22 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f29b0b2639 | ||
|
|
7f18943ec4 | ||
|
|
0325805eb0 |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
- Collapse the CCTV panel when the layer is switched off. It stayed open with
|
||||
an empty frame and a meta line that still described the last camera; the
|
||||
meta line of a disabled layer now reads the catalog count. Contacts, Space
|
||||
Missions and Clear selected data layers leave the panel as it is
|
||||
(asturksever, #1723).
|
||||
@@ -2229,6 +2229,15 @@ shown without a fade once it is decoded (or after one second if its decode never
|
||||
settles), so a new camera's preview is blank only while its frame loads. Disposal
|
||||
releases listeners, subscriptions, image handlers, summary timers and chip-hide timers.
|
||||
|
||||
The panel opens when a camera becomes active and collapses when the layer is
|
||||
switched off, from its own switch or anywhere else. The panel chrome applies
|
||||
the collapse, so a panel the desktop accordion only folded has its saved and
|
||||
shared state closed too. Clear selected data layers and Context isolation
|
||||
(Contacts, Space Missions) switch the layer off without the user asking: the
|
||||
panel and its share preference stay as they are, and the layer comes back to an
|
||||
open panel. A panel opened by hand while the layer is off stays open, and its
|
||||
meta line reads the catalog count, not the last active camera.
|
||||
|
||||
## UI disposal
|
||||
|
||||
UI disposal releases the CCTV subscription, command-dock observer, legacy drag
|
||||
|
||||
@@ -339,6 +339,45 @@ try {
|
||||
'#cctv-frame',
|
||||
(image) => !image.getAttribute('src') && image.dataset.cameraId === '',
|
||||
);
|
||||
// The address bar follows the share state after a short delay: wait for the
|
||||
// CCTV panel token (1 collapsed, 0 open) the step expects.
|
||||
const panelState = async (expectedToken) => {
|
||||
const settled = await page
|
||||
.waitForFunction(
|
||||
(token) =>
|
||||
new RegExp(`(?:^|_)v\\.c\\.${token}(?:_|$)`).test(
|
||||
new URLSearchParams(location.hash.slice(1)).get('ui') || '',
|
||||
),
|
||||
{ timeout: 5000 },
|
||||
expectedToken,
|
||||
)
|
||||
.then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
return page.evaluate(
|
||||
(shared) => ({
|
||||
collapsed: document
|
||||
.getElementById('cctv-panel')
|
||||
.classList.contains('collapsed'),
|
||||
shared,
|
||||
ui: new URLSearchParams(location.hash.slice(1)).get('ui'),
|
||||
}),
|
||||
settled ? expectedToken : null,
|
||||
);
|
||||
};
|
||||
const closedOnDisable = await panelState('1');
|
||||
check(
|
||||
'disable collapses the panel and its share preference',
|
||||
closedOnDisable.collapsed && closedOnDisable.shared === '1',
|
||||
JSON.stringify(closedOnDisable),
|
||||
);
|
||||
// The switch sits inside the collapsed panel: reopen it from the right rail.
|
||||
await page.click('[data-rail-item="cctv-panel"]');
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
!document.getElementById('cctv-panel').classList.contains('collapsed'),
|
||||
);
|
||||
await page.click('#cctv-enable-btn');
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
@@ -351,6 +390,42 @@ try {
|
||||
'disable clears the preview and re-enable reacquires the camera frame',
|
||||
disabledCleanly,
|
||||
);
|
||||
// Contacts switches CCTV off to isolate itself and restores it on the way
|
||||
// out: the open panel and its share preference survive the round trip.
|
||||
const cameraBeforeContacts = await page.evaluate(
|
||||
() =>
|
||||
window.__godsEyeView.styleManager._cctvControls.getState()
|
||||
?.activeCameraId,
|
||||
);
|
||||
const entered = await page.evaluate(() =>
|
||||
window.__godsEyeView.styleManager.setContextMode('flights'),
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => !window.__godsEyeView.dataManager.isEnabled('cctv'),
|
||||
);
|
||||
const duringContacts = await panelState('0');
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.styleManager.setContextMode(null),
|
||||
);
|
||||
await page.waitForFunction(() =>
|
||||
window.__godsEyeView.dataManager.isEnabled('cctv'),
|
||||
);
|
||||
const afterContacts = await panelState('0');
|
||||
check(
|
||||
'Contacts isolation keeps the open CCTV panel and restores its camera',
|
||||
entered?.ok === true &&
|
||||
!duringContacts.collapsed &&
|
||||
duringContacts.shared === '0' &&
|
||||
!afterContacts.collapsed &&
|
||||
afterContacts.shared === '0' &&
|
||||
(await page.evaluate(
|
||||
(cameraId) =>
|
||||
window.__godsEyeView.styleManager._cctvControls.getState()
|
||||
?.activeCameraId === cameraId,
|
||||
cameraBeforeContacts,
|
||||
)),
|
||||
JSON.stringify({ entered, duringContacts, afterContacts }),
|
||||
);
|
||||
await page.$eval('#cctv-frame', (image) =>
|
||||
image.scrollIntoView({ block: 'nearest' }),
|
||||
);
|
||||
|
||||
@@ -1197,6 +1197,11 @@ export class StyleManager extends ShellFacade {
|
||||
toggleEnabled: (...args) => this._toggleCctvEnabled(...args),
|
||||
runExplicitFocus: (...args) => this._runExplicitCctvFocus(...args),
|
||||
setPanelCollapsed: (...args) => this.setPanelCollapsed(...args),
|
||||
// Clear selected data layers and Context isolation switch the layer
|
||||
// off without the user asking: the panel keeps its state for both.
|
||||
preservePanelState: () =>
|
||||
this._preservePanelStateDuringLayerClear ||
|
||||
this._contextSuspendsLayer('cctv'),
|
||||
showToast: (message) => this._showToast(message),
|
||||
syncViewport: () => this._syncCctvPanelViewport(),
|
||||
},
|
||||
|
||||
@@ -54,6 +54,7 @@ export class CctvControls {
|
||||
this._cctvSummaryTypingTimer = null;
|
||||
this._lastCctvSummaryText = '';
|
||||
this._lastSeenCctvActiveId = null;
|
||||
this._wasCctvEnabled = false;
|
||||
this._cctvChipHideTimer = null;
|
||||
this._cctvChipWasBusy = false;
|
||||
// The camera grid chip as last published; its label stays while hidden.
|
||||
|
||||
@@ -513,3 +513,96 @@ test('the panel meta line reads a bearing that rounds to north as HDG 0°', () =
|
||||
assert.match(meta(359.4), /· HDG 359° ·/);
|
||||
assert.match(meta(359.6), /· HDG 0° ·/);
|
||||
});
|
||||
|
||||
/** The panel collapses the test asked for, as `[panelId, collapsed]` pairs. */
|
||||
function collapseCalls(controls) {
|
||||
const calls = [];
|
||||
controls.actions.setPanelCollapsed = (panelId, collapsed) => {
|
||||
calls.push([panelId, collapsed]);
|
||||
// The panel chrome owns the class the controls read back.
|
||||
controls._cctvPanel.classList.toggle('collapsed', collapsed);
|
||||
};
|
||||
return calls;
|
||||
}
|
||||
|
||||
const activeState = {
|
||||
enabled: true,
|
||||
count: 1,
|
||||
cameraIndex: [],
|
||||
activeCameraId: camera.id,
|
||||
activeCamera: camera,
|
||||
};
|
||||
|
||||
test('switching the layer off collapses the panel it opened', (t) => {
|
||||
const { controls } = panel(t);
|
||||
const calls = collapseCalls(controls);
|
||||
controls._renderCctvState(activeState);
|
||||
assert.deepEqual(calls, [['cctv-panel', false]]);
|
||||
// The layer keeps its active camera while off.
|
||||
controls._renderCctvState({ ...activeState, enabled: false });
|
||||
assert.deepEqual(calls.at(-1), ['cctv-panel', true]);
|
||||
// A panel reopened by hand stays open through later notifications while
|
||||
// the layer is off: only the on → off change collapses.
|
||||
controls._cctvPanel.classList.remove('collapsed');
|
||||
controls._renderCctvState({ ...activeState, enabled: false });
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(controls._cctvPanel.classList.contains('collapsed'), false);
|
||||
});
|
||||
|
||||
test('a layer that was never on leaves an open panel alone', (t) => {
|
||||
const { controls } = panel(t);
|
||||
const calls = collapseCalls(controls);
|
||||
controls._renderCctvState({ enabled: false, count: 3, cameraIndex: [] });
|
||||
controls._renderCctvState({ enabled: false, count: 3, cameraIndex: [] });
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test('the data manager switching the layer off collapses the panel too', (t) => {
|
||||
const { controls } = panel(t);
|
||||
const calls = collapseCalls(controls);
|
||||
let on = true;
|
||||
controls.actions.isEnabled = () => on;
|
||||
controls._renderCctvState(activeState);
|
||||
on = false;
|
||||
controls._renderCctvState(activeState);
|
||||
assert.deepEqual(calls.at(-1), ['cctv-panel', true]);
|
||||
});
|
||||
|
||||
test('a layer the application suspends keeps its panel through the round trip', (t) => {
|
||||
const { controls } = panel(t);
|
||||
const calls = collapseCalls(controls);
|
||||
// Clear selected data layers and Context isolation both answer true.
|
||||
let suspended = true;
|
||||
controls.actions.preservePanelState = () => suspended;
|
||||
controls._renderCctvState(activeState);
|
||||
controls._renderCctvState({ ...activeState, enabled: false });
|
||||
assert.equal(controls._cctvPanel.classList.contains('collapsed'), false);
|
||||
suspended = false;
|
||||
controls._renderCctvState(activeState);
|
||||
assert.equal(controls._cctvPanel.classList.contains('collapsed'), false);
|
||||
assert.ok(calls.every(([, collapsed]) => collapsed === false));
|
||||
// The next switch-off is the user's again.
|
||||
controls._renderCctvState({ ...activeState, enabled: false });
|
||||
assert.deepEqual(calls.at(-1), ['cctv-panel', true]);
|
||||
});
|
||||
|
||||
test('a panel the accordion folded still has its saved state closed', (t) => {
|
||||
const { controls } = panel(t);
|
||||
controls._renderCctvState(activeState);
|
||||
const calls = [];
|
||||
controls.actions.setPanelCollapsed = (...args) => calls.push(args);
|
||||
// Folded for layout only: the class is there, the saved state is open. The
|
||||
// panel chrome tells the two apart, so the controls always ask it.
|
||||
controls._cctvPanel.classList.add('collapsed');
|
||||
controls._renderCctvState({ ...activeState, enabled: false });
|
||||
assert.deepEqual(calls, [['cctv-panel', true]]);
|
||||
});
|
||||
|
||||
test('the meta line of a disabled layer does not describe its last camera', () => {
|
||||
const off = { ...activeState, enabled: false, count: 3 };
|
||||
assert.equal(
|
||||
presentCctvPanel(off, false).meta,
|
||||
'3 cameras loaded · enable CCTV to activate',
|
||||
);
|
||||
assert.match(presentCctvPanel(activeState, true).meta, /^Austin · HDG /);
|
||||
});
|
||||
@@ -90,7 +90,9 @@ export function presentCctvPanel(state, enabled) {
|
||||
}
|
||||
|
||||
function metaText(state, enabled, activeCamera, catalogCount) {
|
||||
if (activeCamera) {
|
||||
// The layer keeps its active camera while off: a disabled panel must not
|
||||
// describe a feed it no longer shows.
|
||||
if (enabled && activeCamera) {
|
||||
const provider = cctvSourceLabelText(
|
||||
activeCamera.sourceLabel || activeCamera.provider || 'Configured Source',
|
||||
);
|
||||
@@ -188,6 +190,20 @@ export function _renderCctvState(state) {
|
||||
});
|
||||
}
|
||||
this._lastSeenCctvActiveId = effectiveActiveId;
|
||||
// Switching the layer off closes the panel it opened: nothing is left to
|
||||
// show. Only the on → off change collapses, so a panel the user opens
|
||||
// while the layer is off stays open. Clear selected data layers and
|
||||
// Context isolation keep the panel: the layer comes back with it. The
|
||||
// panel chrome decides whether anything changes: a panel the desktop
|
||||
// accordion only folded still has an open saved state to close.
|
||||
if (
|
||||
this._wasCctvEnabled &&
|
||||
!enabled &&
|
||||
!this.actions.preservePanelState?.()
|
||||
) {
|
||||
this.actions.setPanelCollapsed('cctv-panel', true);
|
||||
}
|
||||
this._wasCctvEnabled = enabled;
|
||||
|
||||
this._updateCctvSyncChip(state?.loading, enabled);
|
||||
|
||||
|
||||
@@ -59,6 +59,15 @@ export class ContextControls {
|
||||
showToast(...args) {
|
||||
if (!this.destroyed) this.actions.showToast(...args);
|
||||
}
|
||||
/**
|
||||
* Whether Context isolation switched this layer off and leaving the mode
|
||||
* restores it. Such a layer is suspended, not switched off by the user.
|
||||
* @param {string} layerId Layer id.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
suspendsLayer(layerId) {
|
||||
return Boolean(this._contextSessionSnapshot?.enabledLayerIds?.has(layerId));
|
||||
}
|
||||
setClearBusy(...args) {
|
||||
if (!this.destroyed) this.actions.setClearBusy(...args);
|
||||
}
|
||||
|
||||
@@ -153,3 +153,39 @@ test('a rejected tracked reaction settles without an unhandled rejection', async
|
||||
await controls._waitForContextLayerSettlement();
|
||||
assert.equal(controls._contextLayerReactionPromises.size, 0);
|
||||
});
|
||||
|
||||
test('a layer Context isolation switches off is suspended until the mode is left', async (t) => {
|
||||
const { controls } = fixture(t);
|
||||
const enabled = new Set(['cctv']);
|
||||
// What the CCTV panel is told at each visibility change of its layer.
|
||||
const seen = [];
|
||||
controls._dataManager = {
|
||||
layers: new Map([['cctv', {}]]),
|
||||
getEnabledLayerIds: () => [...enabled],
|
||||
getLayerParams: () => null,
|
||||
setLayerParams() {},
|
||||
isEnabled: (layerId) => enabled.has(layerId),
|
||||
isEffectivelyEnabled: (layerId) => enabled.has(layerId),
|
||||
async setEnabled(layerId, next) {
|
||||
if (next) enabled.add(layerId);
|
||||
else enabled.delete(layerId);
|
||||
seen.push([layerId, next, controls.suspendsLayer(layerId)]);
|
||||
return true;
|
||||
},
|
||||
async restoreEnabledLayerIds(layerIds) {
|
||||
for (const layerId of layerIds) await this.setEnabled(layerId, true);
|
||||
},
|
||||
};
|
||||
assert.equal(controls.suspendsLayer('cctv'), false);
|
||||
controls._captureContextSessionSnapshot();
|
||||
await controls._clearLayersOutsideContextMode('flights');
|
||||
assert.deepEqual(seen, [['cctv', false, true]]);
|
||||
assert.equal(controls.suspendsLayer('cctv'), true);
|
||||
assert.equal(controls.suspendsLayer('street-level'), false);
|
||||
await controls._restoreContextSession();
|
||||
assert.equal(enabled.has('cctv'), true);
|
||||
assert.equal(controls.suspendsLayer('cctv'), false);
|
||||
// A switch-off after the mode is left is the user's own.
|
||||
await controls._dataManager.setEnabled('cctv', false);
|
||||
assert.deepEqual(seen.at(-1), ['cctv', false, false]);
|
||||
});
|
||||
@@ -891,6 +891,10 @@ export class ShellFacade {
|
||||
return this._contextControls?._preservePanelStateDuringLayerClear ?? false;
|
||||
}
|
||||
|
||||
_contextSuspendsLayer(layerId) {
|
||||
return this._contextControls?.suspendsLayer(layerId) ?? false;
|
||||
}
|
||||
|
||||
_runUserFacingContextAction(...args) {
|
||||
return this._contextControls?._runUserFacingContextAction(...args);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user