mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 15:22:21 +00:00
- Keep the open card across a widened reload only when its index still holds the same report; the server may answer a pinned version with newer bytes. - Narrowing to a window the loaded data already covers clears a failed wider reload, so the row no longer stays in error. - Label the every-age option "All" (contextual) so the six-option switch fits the row in every locale; tooltip and info line keep "All markers". Co-Authored-By: Claude Opus 5.5 <[email protected]>
612 lines
20 KiB
JavaScript
612 lines
20 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import * as Cesium from 'cesium';
|
|
import { createGhostMapsLayer } from './index.js';
|
|
import { GHOSTMAPS_PRODUCTS, GHOSTMAPS_LATEST_DROP } from './catalog.js';
|
|
|
|
function element(localName, children = [], value = '', attributes = {}) {
|
|
const node = {
|
|
localName,
|
|
children,
|
|
get textContent() {
|
|
return value + children.map((child) => child.textContent).join('');
|
|
},
|
|
getAttribute: (key) => attributes[key] ?? null,
|
|
getElementsByTagName(name) {
|
|
return children.flatMap((child) => [
|
|
...(name === '*' || name === child.localName ? [child] : []),
|
|
...child.getElementsByTagName(name),
|
|
]);
|
|
},
|
|
};
|
|
for (const child of children) child.parentElement = node;
|
|
return node;
|
|
}
|
|
|
|
function documentFixture(reports = [{ name: 'Test report' }]) {
|
|
return element('document', [
|
|
element('kml', [
|
|
element('Document', [
|
|
element(
|
|
'Style',
|
|
[
|
|
element('IconStyle', [
|
|
element('Icon', [element('href', [], 'icon.png')]),
|
|
]),
|
|
],
|
|
'',
|
|
{ id: 'pin' },
|
|
),
|
|
...reports.map(({ name, description = '', geometry }) =>
|
|
element('Placemark', [
|
|
element('name', [], name),
|
|
element('description', [], description),
|
|
element('styleUrl', [], '#pin'),
|
|
geometry || element('Point', [element('coordinates', [], '12,34')]),
|
|
]),
|
|
),
|
|
]),
|
|
]),
|
|
]);
|
|
}
|
|
|
|
// A stored KMZ with a local icon; the XML DOM double supplies its one point.
|
|
function fixtureKmz() {
|
|
const files = [
|
|
['doc.kml', '<kml/>'],
|
|
['icon.png', 'packed icon'],
|
|
];
|
|
const local = [];
|
|
const central = [];
|
|
let offset = 0;
|
|
for (const [name, value] of files) {
|
|
const key = Buffer.from(name);
|
|
const bytes = Buffer.from(value);
|
|
const header = Buffer.alloc(30);
|
|
header.writeUInt32LE(0x04034b50);
|
|
header.writeUInt32LE(bytes.length, 18);
|
|
header.writeUInt32LE(bytes.length, 22);
|
|
header.writeUInt16LE(key.length, 26);
|
|
const directory = Buffer.alloc(46);
|
|
directory.writeUInt32LE(0x02014b50);
|
|
directory.writeUInt32LE(bytes.length, 20);
|
|
directory.writeUInt32LE(bytes.length, 24);
|
|
directory.writeUInt16LE(key.length, 28);
|
|
directory.writeUInt32LE(offset, 42);
|
|
local.push(header, key, bytes);
|
|
central.push(directory, key);
|
|
offset += header.length + key.length + bytes.length;
|
|
}
|
|
const directory = Buffer.concat(central);
|
|
const end = Buffer.alloc(22);
|
|
end.writeUInt32LE(0x06054b50);
|
|
end.writeUInt16LE(files.length, 8);
|
|
end.writeUInt16LE(files.length, 10);
|
|
end.writeUInt32LE(directory.length, 12);
|
|
end.writeUInt32LE(offset, 16);
|
|
return new Uint8Array(Buffer.concat([...local, directory, end]));
|
|
}
|
|
|
|
function fixture(
|
|
t,
|
|
{
|
|
beforeFileReturns,
|
|
onCreateUrl,
|
|
reports,
|
|
now,
|
|
products,
|
|
controls = {},
|
|
} = {},
|
|
) {
|
|
const previous = Object.getOwnPropertyDescriptor(globalThis, 'DOMParser');
|
|
globalThis.DOMParser = class {
|
|
parseFromString() {
|
|
return documentFixture(reports);
|
|
}
|
|
};
|
|
t.after(() => {
|
|
if (previous) Object.defineProperty(globalThis, 'DOMParser', previous);
|
|
else delete globalThis.DOMParser;
|
|
});
|
|
const created = [];
|
|
const revoked = [];
|
|
t.mock.method(URL, 'createObjectURL', () => {
|
|
const url = `blob:local-${created.length}`;
|
|
created.push(url);
|
|
onCreateUrl?.(layer);
|
|
return url;
|
|
});
|
|
t.mock.method(URL, 'revokeObjectURL', (url) => revoked.push(url));
|
|
t.mock.method(console, 'warn', () => {});
|
|
const mounted = new Set();
|
|
const viewer = {
|
|
scene: { requestRender() {} },
|
|
dataSources: {
|
|
async add(dataSource) {
|
|
mounted.add(dataSource);
|
|
return dataSource;
|
|
},
|
|
remove(dataSource) {
|
|
return mounted.delete(dataSource);
|
|
},
|
|
},
|
|
};
|
|
const layer = createGhostMapsLayer({
|
|
storage: null,
|
|
now,
|
|
...controls,
|
|
source: {
|
|
getCatalog: async () => ({
|
|
products: products ?? [
|
|
{ id: 'cip', version: 'a'.repeat(40), defaultOn: true },
|
|
],
|
|
}),
|
|
getFile: async () => {
|
|
beforeFileReturns?.(layer);
|
|
return fixtureKmz();
|
|
},
|
|
},
|
|
});
|
|
layer.init(viewer);
|
|
layer.enable();
|
|
t.after(() => layer.destroy());
|
|
return { layer, viewer, mounted, created, revoked };
|
|
}
|
|
|
|
test('cancelling a file read does not allocate packed-image URLs', async (t) => {
|
|
const { layer, viewer, mounted, created, revoked } = fixture(t, {
|
|
beforeFileReturns: (layer) => queueMicrotask(() => layer.disable()),
|
|
});
|
|
await layer.update(viewer);
|
|
assert.equal(created.length, 0);
|
|
assert.equal(revoked.length, 0);
|
|
assert.equal(mounted.size, 0);
|
|
});
|
|
|
|
test('cancellation during parsing releases every packed-image URL', async (t) => {
|
|
const { layer, viewer, mounted, created, revoked } = fixture(t, {
|
|
onCreateUrl: (layer) => layer.disable(),
|
|
});
|
|
await layer.update(viewer);
|
|
assert.equal(created.length, 1);
|
|
assert.deepEqual(revoked, created);
|
|
assert.equal(mounted.size, 0);
|
|
});
|
|
|
|
test('entity construction failure releases parsed image URLs', async (t) => {
|
|
const { layer, viewer, mounted, created, revoked } = fixture(t);
|
|
t.mock.method(Cesium.EntityCollection.prototype, 'add', () => {
|
|
throw new Error('fixture entity failure');
|
|
});
|
|
await layer.update(viewer);
|
|
assert.equal(created.length, 1);
|
|
assert.deepEqual(revoked, created);
|
|
assert.equal(mounted.size, 0);
|
|
assert.equal(layer.getStats().error, 'fixture entity failure');
|
|
});
|
|
|
|
test('successful load keeps icons until the product is disabled', async (t) => {
|
|
const { layer, viewer, mounted, created, revoked } = fixture(t);
|
|
await layer.update(viewer);
|
|
assert.equal(created.length, 1);
|
|
assert.equal(revoked.length, 0);
|
|
assert.equal(mounted.size, 1);
|
|
assert.equal(layer.getStats().count, 1);
|
|
layer.disable();
|
|
assert.deepEqual(revoked, created);
|
|
assert.equal(mounted.size, 0);
|
|
});
|
|
|
|
const dated = (name, value, label = 'Date') => ({
|
|
name,
|
|
description: `<table><tr><td>${label}</td><td>${value}</td></tr></table>`,
|
|
});
|
|
|
|
const ageChip = (layer, days) =>
|
|
layer
|
|
.getRowControls()
|
|
.segmented.options.find(({ id }) => id === `age-${days ?? 'all'}`);
|
|
|
|
test('age controls default to seven days and widening restores filtered and expired reports', async (t) => {
|
|
let at = Date.UTC(2026, 10, 3, 12);
|
|
const { layer, viewer, mounted } = fixture(t, {
|
|
now: () => at,
|
|
reports: [
|
|
dated('Old', '10/1/2026'),
|
|
dated('Ten days', '10/24/2026'),
|
|
dated('Recent', '11/2/2026'),
|
|
{ name: 'Undated' },
|
|
],
|
|
});
|
|
await layer.update(viewer);
|
|
assert.equal(layer.getStats().ageDays, 7);
|
|
assert.equal(ageChip(layer, 7).active, true);
|
|
assert.equal(layer.getStats().count, 2);
|
|
await ageChip(layer, 14).onClick();
|
|
assert.equal(layer.getStats().count, 3);
|
|
await ageChip(layer, null).onClick();
|
|
assert.equal(layer.getStats().count, 4);
|
|
at += 100 * 86400_000;
|
|
await layer.update(viewer);
|
|
assert.equal(layer.getStats().count, 4, 'all markers never ages out');
|
|
await ageChip(layer, 1).onClick();
|
|
assert.equal(layer.getStats().count, 1);
|
|
assert.equal(layer.getAnalystRecords()[0].name, 'Undated');
|
|
await ageChip(layer, null).onClick();
|
|
assert.equal(layer.getStats().count, 4);
|
|
assert.equal(mounted.size, 1, 'filter changes never duplicate data sources');
|
|
const controls = layer.getRowControls();
|
|
assert.equal(
|
|
controls.segmented.options.filter(({ active }) => active).length,
|
|
1,
|
|
);
|
|
assert.equal(
|
|
controls.chips.some(({ id }) => id.startsWith('age-')),
|
|
false,
|
|
'age options are one switch, not product chips',
|
|
);
|
|
assert.equal(ageChip(layer, null).active, true);
|
|
});
|
|
|
|
test('age preferences persist independently of product choices and reject invalid storage', async (t) => {
|
|
const values = new Map([['gev.ghostmaps.products', '["cip"]']]);
|
|
const storage = {
|
|
getItem: (key) => values.get(key) ?? null,
|
|
setItem: (key, value) => values.set(key, value),
|
|
};
|
|
await t.test('save All markers without changing products', async (t) => {
|
|
const { layer, viewer } = fixture(t, { controls: { storage } });
|
|
await layer.update(viewer);
|
|
await ageChip(layer, null).onClick();
|
|
assert.equal(values.get('gev.ghostmaps.ageDays'), 'null');
|
|
assert.equal(values.get('gev.ghostmaps.products'), '["cip"]');
|
|
});
|
|
await t.test('restore All markers and save thirty days', async (t) => {
|
|
const { layer } = fixture(t, { controls: { storage } });
|
|
assert.equal(layer.getStats().ageDays, null);
|
|
await ageChip(layer, 30).onClick();
|
|
assert.equal(values.get('gev.ghostmaps.ageDays'), '30');
|
|
});
|
|
await t.test('restore thirty days', (t) => {
|
|
const { layer } = fixture(t, { controls: { storage } });
|
|
assert.equal(layer.getStats().ageDays, 30);
|
|
});
|
|
for (const value of ['garbage', '"7"', '-1', '0', '2', '{}']) {
|
|
await t.test(`reject ${value}`, (t) => {
|
|
values.set('gev.ghostmaps.ageDays', value);
|
|
const { layer } = fixture(t, { controls: { storage } });
|
|
assert.equal(layer.getStats().ageDays, 7);
|
|
});
|
|
}
|
|
});
|
|
|
|
test('blocked storage still permits changing the age within the session', async (t) => {
|
|
const storage = {
|
|
getItem() {
|
|
throw new Error('blocked');
|
|
},
|
|
setItem() {
|
|
throw new Error('blocked');
|
|
},
|
|
};
|
|
const { layer, viewer } = fixture(t, { controls: { storage } });
|
|
await layer.update(viewer);
|
|
assert.equal(layer.getStats().ageDays, 7);
|
|
await ageChip(layer, null).onClick();
|
|
assert.equal(layer.getStats().ageDays, null);
|
|
});
|
|
|
|
test('a filter changed during a pending load never installs the superseded filter', async (t) => {
|
|
let changed = false;
|
|
const { layer, viewer, mounted, revoked, created } = fixture(t, {
|
|
now: () => Date.UTC(2026, 10, 3),
|
|
reports: [dated('Old', '10/1/2026')],
|
|
beforeFileReturns: (layer) => {
|
|
if (changed) return;
|
|
changed = true;
|
|
ageChip(layer, null).onClick();
|
|
},
|
|
});
|
|
await layer.update(viewer);
|
|
await layer.update(viewer);
|
|
assert.equal(layer.getStats().count, 1);
|
|
assert.equal(layer.getStats().ageDays, null);
|
|
assert.equal(mounted.size, 1);
|
|
assert.equal(
|
|
revoked.length,
|
|
created.length - 1,
|
|
'discarded load releases icons',
|
|
);
|
|
});
|
|
|
|
test('age filtering and All markers apply to every conflict-map product, including optional maps', async (t) => {
|
|
for (const product of [...GHOSTMAPS_PRODUCTS, GHOSTMAPS_LATEST_DROP]) {
|
|
await t.test(product.id, async (t) => {
|
|
const { layer, viewer, mounted } = fixture(t, {
|
|
now: () => Date.UTC(2026, 10, 3, 12),
|
|
products: [{ ...product, version: 'a'.repeat(40) }],
|
|
reports: [
|
|
dated(
|
|
'Old event',
|
|
'10/1/2026',
|
|
product.id === 'cip' ? 'Date of Event' : 'Date',
|
|
),
|
|
dated('Recent event', '11/2/2026'),
|
|
{ name: 'Undated reference feature' },
|
|
],
|
|
});
|
|
await layer.update(viewer);
|
|
if (!product.defaultOn) {
|
|
layer
|
|
.getRowControls()
|
|
.chips.find(({ id }) => id === product.id)
|
|
.onClick();
|
|
await layer.update(viewer);
|
|
}
|
|
assert.deepEqual(layer.getStats().products, [product.id]);
|
|
assert.equal(layer.getStats().count, 2);
|
|
assert.deepEqual(
|
|
layer.getAnalystRecords().map(({ name }) => name),
|
|
['Recent event', 'Undated reference feature'],
|
|
);
|
|
await ageChip(layer, null).onClick();
|
|
assert.equal(
|
|
layer.getStats().count,
|
|
3,
|
|
"All markers restores this product's old events",
|
|
);
|
|
await ageChip(layer, 7).onClick();
|
|
assert.equal(layer.getStats().count, 2);
|
|
assert.equal(mounted.size, 1);
|
|
});
|
|
}
|
|
});
|
|
|
|
test('default products filter old event dates while keeping recent, future and undated markers', async (t) => {
|
|
const { layer, viewer, mounted } = fixture(t, {
|
|
now: () => Date.UTC(2026, 10, 3, 12),
|
|
reports: [
|
|
dated('Old event', '10/20/2026'),
|
|
dated('Recent event', '11/2/2026'),
|
|
dated('At cutoff', '10/27/2026 12:00:00 PM'),
|
|
dated('Before cutoff', '10/27/2026 11:59:59 AM'),
|
|
dated('Future event', '11/4/2026'),
|
|
{ name: 'Undated site' },
|
|
dated('Blank date', '<Null>'),
|
|
dated('Invalid date', '2/30/2026'),
|
|
dated('Only a report date', '10/1/2026', 'Date Of Report'),
|
|
{
|
|
name: 'Old event reported recently',
|
|
description:
|
|
'<table><tr><td>Date of Event</td><td>10/1/2026</td></tr><tr><td>Date Of Report</td><td>11/2/2026</td></tr></table>',
|
|
},
|
|
],
|
|
});
|
|
await layer.update(viewer);
|
|
assert.deepEqual(layer.getStats().products, ['cip']);
|
|
assert.deepEqual(
|
|
layer.getAnalystRecords().map((r) => r.name),
|
|
[
|
|
'Recent event',
|
|
'At cutoff',
|
|
'Future event',
|
|
'Undated site',
|
|
'Blank date',
|
|
'Invalid date',
|
|
'Only a report date',
|
|
],
|
|
);
|
|
assert.equal(layer.getStats().count, 7);
|
|
assert.equal([...mounted][0].entities.values.length, 7);
|
|
// Filtering cannot shift pick ids onto a different report.
|
|
assert.ok([...mounted][0].entities.getById('ghostmaps:cip:1:0'));
|
|
assert.equal(
|
|
[...mounted][0].entities.getById('ghostmaps:cip:0:0'),
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
test('unchanged cached products age out on the timer, clear selected cards and keep surviving picks', async (t) => {
|
|
t.mock.timers.enable({ apis: ['setInterval'] });
|
|
let at = Date.UTC(2026, 10, 3, 12);
|
|
let click;
|
|
const cards = [];
|
|
const { layer, viewer, mounted } = fixture(t, {
|
|
now: () => at,
|
|
reports: [
|
|
dated('Expiring', '10/27/2026 12:00:00 PM'),
|
|
dated('Surviving', '11/2/2026'),
|
|
],
|
|
controls: {
|
|
overlayHost: { setEntries: (_id, entries) => cards.push(entries) },
|
|
screenSpaceEventHandlerFactory: () => ({
|
|
setInputAction: (fn) => {
|
|
click = fn;
|
|
},
|
|
destroy() {},
|
|
}),
|
|
picking: { resolvePickId: (picked) => picked },
|
|
},
|
|
});
|
|
await layer.update(viewer);
|
|
const ds = [...mounted][0];
|
|
viewer.scene.pick = () => 'ghostmaps:cip:0:0';
|
|
click({ position: { x: 0, y: 0 } });
|
|
assert.equal(cards.at(-1).length, 1);
|
|
at += 60_000;
|
|
t.mock.timers.tick(60_000);
|
|
assert.equal(layer.getStats().count, 1);
|
|
assert.equal(ds.entities.values.length, 1);
|
|
assert.equal(ds.entities.getById('ghostmaps:cip:0:0'), undefined);
|
|
assert.ok(ds.entities.getById('ghostmaps:cip:1:0'));
|
|
assert.deepEqual(cards.at(-1), []);
|
|
await layer.update(viewer);
|
|
assert.equal([...mounted][0], ds, 'same-version KMZ is not reloaded');
|
|
viewer.scene.pick = () => 'ghostmaps:cip:1:0';
|
|
click({ position: { x: 0, y: 0 } });
|
|
assert.match(cards.at(-1)[0].title, /Surviving/);
|
|
layer.disable();
|
|
at += 14 * 86400_000;
|
|
t.mock.timers.tick(60_000);
|
|
assert.equal(mounted.size, 0);
|
|
layer.enable();
|
|
await layer.update(viewer);
|
|
assert.equal(layer.getStats().count, 0);
|
|
});
|
|
|
|
test('expiry removes every geometry and polygon outline belonging to an old report', async (t) => {
|
|
t.mock.timers.enable({ apis: ['setInterval'] });
|
|
// 10/27 is a whole UTC day: at 7d it expires when 11/3 ends.
|
|
let at = Date.UTC(2026, 10, 4) - 30_000;
|
|
const polygon = element('Polygon', [
|
|
element('outerBoundaryIs', [
|
|
element('LinearRing', [
|
|
element('coordinates', [], '12,34 13,34 13,35 12,34'),
|
|
]),
|
|
]),
|
|
]);
|
|
const { layer, viewer, mounted } = fixture(t, {
|
|
now: () => at,
|
|
reports: [
|
|
{
|
|
...dated('Multi-part', '10/27/2026'),
|
|
geometry: element('MultiGeometry', [
|
|
element('Point', [element('coordinates', [], '12,34')]),
|
|
element('LineString', [element('coordinates', [], '12,34 13,35')]),
|
|
polygon,
|
|
]),
|
|
},
|
|
],
|
|
});
|
|
await layer.update(viewer);
|
|
const ds = [...mounted][0];
|
|
assert.equal(ds.entities.values.length, 4);
|
|
at += 60_000;
|
|
t.mock.timers.tick(60_000);
|
|
assert.equal(ds.entities.values.length, 0);
|
|
assert.equal(layer.getStats().count, 0);
|
|
});
|
|
|
|
test('narrowing then returning to the previous window restores its reports without a duplicate source', async (t) => {
|
|
const { layer, viewer, mounted } = fixture(t, {
|
|
now: () => Date.UTC(2026, 10, 3, 12),
|
|
reports: [dated('Today', '11/3/2026'), dated('Three days', '10/31/2026')],
|
|
});
|
|
await layer.update(viewer);
|
|
assert.equal(layer.getStats().count, 2);
|
|
await ageChip(layer, 1).onClick();
|
|
assert.equal(layer.getStats().count, 1);
|
|
await ageChip(layer, 7).onClick();
|
|
await layer.update(viewer);
|
|
assert.equal(layer.getStats().count, 2, '7d must show both reports again');
|
|
assert.equal(mounted.size, 1);
|
|
});
|
|
|
|
test('two quick clicks, 1d then 7d, restore the report', async (t) => {
|
|
const at = Date.UTC(2026, 10, 3, 12);
|
|
const { layer, viewer } = fixture(t, {
|
|
now: () => at,
|
|
reports: [dated('Today', '11/3/2026'), dated('Three days', '10/31/2026')],
|
|
});
|
|
await layer.update(viewer);
|
|
const a = ageChip(layer, 1).onClick();
|
|
const b = ageChip(layer, 7).onClick();
|
|
await a;
|
|
await b;
|
|
await layer.update(viewer);
|
|
assert.equal(layer.getStats().count, 2);
|
|
});
|
|
|
|
test('narrowing the window keeps the open card of a surviving marker', async (t) => {
|
|
let click;
|
|
const cards = [];
|
|
const { layer, viewer } = fixture(t, {
|
|
now: () => Date.UTC(2026, 10, 3, 12),
|
|
reports: [dated('Recent', '11/2/2026'), dated('Older', '10/20/2026')],
|
|
controls: {
|
|
overlayHost: { setEntries: (_id, entries) => cards.push(entries) },
|
|
screenSpaceEventHandlerFactory: () => ({
|
|
setInputAction: (fn) => {
|
|
click = fn;
|
|
},
|
|
destroy() {},
|
|
}),
|
|
picking: { resolvePickId: (picked) => picked },
|
|
},
|
|
});
|
|
await layer.update(viewer);
|
|
await ageChip(layer, 30).onClick();
|
|
assert.equal(layer.getStats().count, 2);
|
|
viewer.scene.pick = () => 'ghostmaps:cip:0:0';
|
|
click({ position: { x: 0, y: 0 } });
|
|
assert.equal(cards.at(-1).length, 1);
|
|
await ageChip(layer, 3).onClick();
|
|
assert.equal(layer.getStats().count, 1);
|
|
assert.equal(
|
|
cards.at(-1).length,
|
|
1,
|
|
'card of the surviving marker stays open',
|
|
);
|
|
});
|
|
|
|
test('a report dated yesterday is shown by 1d one hour into today (UTC)', async (t) => {
|
|
const { layer, viewer } = fixture(t, {
|
|
now: () => Date.UTC(2026, 10, 6, 1),
|
|
reports: [dated('Yesterday', '11/5/2026')],
|
|
});
|
|
await layer.update(viewer);
|
|
await ageChip(layer, 1).onClick();
|
|
assert.equal(layer.getStats().count, 1);
|
|
});
|
|
|
|
test('a widened reload that returns a different report at the selected index closes its card', async (t) => {
|
|
let click;
|
|
const cards = [];
|
|
const reports = [dated('Recent', '11/2/2026'), dated('Older', '10/20/2026')];
|
|
const { layer, viewer } = fixture(t, {
|
|
now: () => Date.UTC(2026, 10, 3, 12),
|
|
reports,
|
|
controls: {
|
|
overlayHost: { setEntries: (_id, entries) => cards.push(entries) },
|
|
screenSpaceEventHandlerFactory: () => ({
|
|
setInputAction: (fn) => {
|
|
click = fn;
|
|
},
|
|
destroy() {},
|
|
}),
|
|
picking: { resolvePickId: (picked) => picked },
|
|
},
|
|
});
|
|
await layer.update(viewer);
|
|
viewer.scene.pick = () => 'ghostmaps:cip:0:0';
|
|
click({ position: { x: 0, y: 0 } });
|
|
assert.equal(cards.at(-1).length, 1);
|
|
// The server answers the pinned version with a newer edition's bytes.
|
|
reports[0] = dated('Replacement', '11/2/2026');
|
|
await ageChip(layer, 30).onClick();
|
|
assert.equal(layer.getStats().count, 2);
|
|
assert.equal(
|
|
cards.at(-1).length,
|
|
0,
|
|
'a different report must not keep the old card',
|
|
);
|
|
});
|
|
|
|
test('narrowing to a window the loaded data covers clears a failed wider reload', async (t) => {
|
|
let fail = false;
|
|
const { layer, viewer } = fixture(t, {
|
|
now: () => Date.UTC(2026, 10, 3, 12),
|
|
reports: [dated('Recent', '11/2/2026'), dated('Older', '10/20/2026')],
|
|
beforeFileReturns: () => {
|
|
if (fail) throw new Error('network down');
|
|
},
|
|
});
|
|
await layer.update(viewer);
|
|
fail = true;
|
|
await ageChip(layer, 30).onClick();
|
|
assert.ok(layer.getStats().error, 'the wider reload failed');
|
|
await ageChip(layer, 3).onClick();
|
|
assert.equal(layer.getStats().error, null);
|
|
assert.equal(layer.getStats().count, 1);
|
|
});
|