mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
Test that a GhostMaps edition or engine change mid-parse shows only the selected edition
This commit is contained in:
1 parent
c34a2c38fc
commit
57f7edc693
1 file changed
+187
@@ -0,0 +1,187 @@
|
|||||||
|
// Edge cases from docs/migration/planet-separation/layers.md (s2-ghostmaps):
|
||||||
|
// change the KMZ edition and the engine before parsing finishes; only the
|
||||||
|
// selected edition is shown, and a bad archive does not erase the working one.
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { createGhostMapsLayer } from './index.js';
|
||||||
|
|
||||||
|
function element(localName, children = [], value = '') {
|
||||||
|
const node = {
|
||||||
|
localName,
|
||||||
|
children,
|
||||||
|
get textContent() {
|
||||||
|
return value + children.map((child) => child.textContent).join('');
|
||||||
|
},
|
||||||
|
getAttribute: () => 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The XML double turns `<kml>NAME</kml>` into one placemark called NAME.
|
||||||
|
function documentFor(xml) {
|
||||||
|
const name = /<kml>(.*)<\/kml>/.exec(xml)?.[1] ?? 'unnamed';
|
||||||
|
return element('document', [
|
||||||
|
element('kml', [
|
||||||
|
element('Document', [
|
||||||
|
element('Placemark', [
|
||||||
|
element('name', [], name),
|
||||||
|
element('Point', [element('coordinates', [], '12,34')]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A stored (uncompressed) KMZ holding doc.kml only. */
|
||||||
|
function kmz(name) {
|
||||||
|
const key = Buffer.from('doc.kml');
|
||||||
|
const bytes = Buffer.from(`<kml>${name}</kml>`);
|
||||||
|
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(0, 42);
|
||||||
|
const central = Buffer.concat([directory, key]);
|
||||||
|
const end = Buffer.alloc(22);
|
||||||
|
end.writeUInt32LE(0x06054b50);
|
||||||
|
end.writeUInt16LE(1, 8);
|
||||||
|
end.writeUInt16LE(1, 10);
|
||||||
|
end.writeUInt32LE(central.length, 12);
|
||||||
|
end.writeUInt32LE(header.length + key.length + bytes.length, 16);
|
||||||
|
return new Uint8Array(Buffer.concat([header, key, bytes, central, end]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function deferred() {
|
||||||
|
let resolve;
|
||||||
|
const promise = new Promise((done) => (resolve = done));
|
||||||
|
return { promise, resolve };
|
||||||
|
}
|
||||||
|
|
||||||
|
const catalog = (version) => ({
|
||||||
|
products: [{ id: 'cip', version, defaultOn: true }],
|
||||||
|
});
|
||||||
|
|
||||||
|
/** A scene-less renderer double that records live paints per engine. */
|
||||||
|
function fakeRenderer(name, live) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
requestRender() {},
|
||||||
|
onClick: () => () => {},
|
||||||
|
draw(id, features) {
|
||||||
|
const paint = {
|
||||||
|
engine: name,
|
||||||
|
id,
|
||||||
|
names: features.map((feature) => feature.name),
|
||||||
|
ready: Promise.resolve(),
|
||||||
|
destroy() {
|
||||||
|
live.delete(paint);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
live.add(paint);
|
||||||
|
return paint;
|
||||||
|
},
|
||||||
|
destroy() {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function harness(t) {
|
||||||
|
const previous = Object.getOwnPropertyDescriptor(globalThis, 'DOMParser');
|
||||||
|
globalThis.DOMParser = class {
|
||||||
|
parseFromString(xml) {
|
||||||
|
return documentFor(xml);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
t.after(() => {
|
||||||
|
if (previous) Object.defineProperty(globalThis, 'DOMParser', previous);
|
||||||
|
else delete globalThis.DOMParser;
|
||||||
|
});
|
||||||
|
t.mock.method(console, 'warn', () => {});
|
||||||
|
const files = new Map();
|
||||||
|
const live = new Set();
|
||||||
|
let listener = null;
|
||||||
|
const layer = createGhostMapsLayer({
|
||||||
|
storage: null,
|
||||||
|
createRenderer: (engine) => fakeRenderer(engine.name, live),
|
||||||
|
source: {
|
||||||
|
getCatalog: async () => catalog('v1'),
|
||||||
|
getFile: (_id, version) => files.get(version).promise,
|
||||||
|
subscribe(onCatalog) {
|
||||||
|
listener = onCatalog;
|
||||||
|
return () => {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
layer.init(null, { engine: { name: 'cesium' } });
|
||||||
|
layer.enable();
|
||||||
|
t.after(() => layer.destroy());
|
||||||
|
const file = (version) => {
|
||||||
|
const pending = deferred();
|
||||||
|
files.set(version, pending);
|
||||||
|
return pending;
|
||||||
|
};
|
||||||
|
const push = (next) => listener(next);
|
||||||
|
return { layer, live, file, push };
|
||||||
|
}
|
||||||
|
|
||||||
|
const names = (layer) => layer.getAnalystRecords().map((record) => record.name);
|
||||||
|
const painted = (live) =>
|
||||||
|
[...live].map((paint) => `${paint.engine}:${paint.names.join(',')}`);
|
||||||
|
|
||||||
|
test('edge:s2-ghostmaps: an edition and engine change mid-parse shows only the selected edition', async (t) => {
|
||||||
|
const { layer, live, file, push } = harness(t);
|
||||||
|
file('v1').resolve(kmz('edition one'));
|
||||||
|
await layer.update();
|
||||||
|
assert.deepEqual(names(layer), ['edition one']);
|
||||||
|
|
||||||
|
// v2 is still downloading when v3 is published and the engine is swapped.
|
||||||
|
const v2 = file('v2');
|
||||||
|
const v3 = file('v3');
|
||||||
|
push(catalog('v2'));
|
||||||
|
await new Promise(setImmediate);
|
||||||
|
push(catalog('v3'));
|
||||||
|
layer.detachRenderer();
|
||||||
|
await layer.attachRenderer(null, { engine: { name: 'navara' } });
|
||||||
|
v2.resolve(kmz('edition two'));
|
||||||
|
v3.resolve(kmz('edition three'));
|
||||||
|
// Let the queued syncs settle.
|
||||||
|
for (let i = 0; i < 20; i += 1) await new Promise(setImmediate);
|
||||||
|
|
||||||
|
assert.deepEqual(names(layer), ['edition three']);
|
||||||
|
assert.deepEqual(painted(live), ['navara:edition three']);
|
||||||
|
assert.equal(layer.getStats().loading, false);
|
||||||
|
assert.equal(layer.getStats().error, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('edge:s2-ghostmaps: a bad archive keeps the working edition on the globe', async (t) => {
|
||||||
|
const { layer, live, file, push } = harness(t);
|
||||||
|
file('v1').resolve(kmz('working edition'));
|
||||||
|
await layer.update();
|
||||||
|
assert.deepEqual(painted(live), ['cesium:working edition']);
|
||||||
|
|
||||||
|
const broken = file('v2');
|
||||||
|
push(catalog('v2'));
|
||||||
|
// The engine changes while the broken file is still on its way.
|
||||||
|
layer.detachRenderer();
|
||||||
|
await layer.attachRenderer(null, { engine: { name: 'navara' } });
|
||||||
|
broken.resolve(new Uint8Array([1, 2, 3, 4]));
|
||||||
|
for (let i = 0; i < 20; i += 1) await new Promise(setImmediate);
|
||||||
|
|
||||||
|
assert.deepEqual(names(layer), ['working edition']);
|
||||||
|
assert.deepEqual(painted(live), ['navara:working edition']);
|
||||||
|
assert.ok(layer.getStats().error, 'the failed edition is reported');
|
||||||
|
const chip = layer.getRowControls().chips[0];
|
||||||
|
assert.equal(chip.state, 'error');
|
||||||
|
});
|
||||||
Reference in new issue
Block a user