mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
Supersedes #235 by @cora-fresh-labs, ported onto the current component layout with fixes from review. Verified before merge: format, package boundaries, unit suite, build, and the browser tracking suite; a browser harness for the change where one applies; adversarial review applied. Future work (not in this PR): hand-drawn marks do not persist across a reload today, and neither do spoken annotations; there is no storage layer for marks. The natural home is Scenes: a drawing that belongs to a shot and persists per shot, so a scene animation can carry its own marks. Needs an owner. Co-authored-by: Clint Borle <[email protected]>
177 lines
5.9 KiB
JavaScript
177 lines
5.9 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import {
|
||
claimPointer,
|
||
isLeaseCurrent,
|
||
isPointerFree,
|
||
isPointerOwnedBy,
|
||
pointerOwner,
|
||
releasePointer,
|
||
resetPointerOwnership,
|
||
} from './inputOwnership.js';
|
||
|
||
const ROOT = path.resolve(
|
||
path.dirname(fileURLToPath(import.meta.url)),
|
||
'..',
|
||
'..',
|
||
);
|
||
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
||
|
||
test.beforeEach(() => resetPointerOwnership());
|
||
|
||
test('the pointer starts free and a claim hands back a lease', () => {
|
||
assert.equal(isPointerFree(), true);
|
||
assert.equal(pointerOwner(), null);
|
||
|
||
const lease = claimPointer('draw');
|
||
assert.ok(lease, 'a successful claim returns a lease, not a boolean');
|
||
assert.equal(isPointerFree(), false);
|
||
assert.equal(pointerOwner(), 'draw');
|
||
assert.equal(isPointerOwnedBy('draw'), true);
|
||
assert.equal(isPointerOwnedBy('directions'), false);
|
||
assert.equal(isLeaseCurrent(lease), true);
|
||
});
|
||
|
||
test('a claim is never stolen — not even by the same tool name', () => {
|
||
const first = claimPointer('draw');
|
||
assert.ok(first);
|
||
|
||
assert.equal(
|
||
claimPointer('directions'),
|
||
null,
|
||
'another tool must not displace it',
|
||
);
|
||
assert.equal(
|
||
claimPointer('draw'),
|
||
null,
|
||
'a SECOND instance of the same tool is a second owner, not a no-op',
|
||
);
|
||
assert.equal(pointerOwner(), 'draw');
|
||
assert.equal(isLeaseCurrent(first), true);
|
||
});
|
||
|
||
test('a replaced instance cannot free its successor’s claim', () => {
|
||
// The bug this rule exists for. An old Draw instance is torn down while a new
|
||
// one is already running; the old teardown calls releasePointer. Under
|
||
// release-by-name that freed the LIVE claim and every layer started selecting
|
||
// through the new instance's vertices.
|
||
const stale = claimPointer('draw');
|
||
assert.ok(releasePointer(stale), 'the old instance gives the pointer back');
|
||
|
||
const live = claimPointer('draw');
|
||
assert.ok(live, 'the replacement takes it');
|
||
assert.notEqual(live.id, stale.id, 'a fresh claim is a fresh lease');
|
||
|
||
assert.equal(releasePointer(stale), false, 'the stale lease frees nothing');
|
||
assert.equal(pointerOwner(), 'draw', 'the replacement still holds it');
|
||
assert.equal(isLeaseCurrent(stale), false);
|
||
assert.equal(isLeaseCurrent(live), true);
|
||
|
||
assert.equal(releasePointer(live), true);
|
||
assert.equal(isPointerFree(), true);
|
||
});
|
||
|
||
test('release ignores anything that is not the live lease', () => {
|
||
const lease = claimPointer('draw');
|
||
for (const bad of [
|
||
null,
|
||
undefined,
|
||
'draw',
|
||
{},
|
||
{ owner: 'draw', id: lease.id + 99 },
|
||
]) {
|
||
assert.equal(releasePointer(bad), false, JSON.stringify(bad));
|
||
assert.equal(pointerOwner(), 'draw');
|
||
}
|
||
assert.equal(releasePointer(lease), true);
|
||
assert.equal(releasePointer(lease), false, 'releasing twice changes nothing');
|
||
});
|
||
|
||
test('a claim needs a real owner id', () => {
|
||
for (const bad of ['', ' ', null, undefined, 7, {}]) {
|
||
assert.equal(claimPointer(bad), null, JSON.stringify(bad));
|
||
assert.equal(isPointerFree(), true);
|
||
}
|
||
const lease = claimPointer(' draw ');
|
||
assert.ok(lease, 'ids are trimmed, not rejected for padding');
|
||
assert.equal(pointerOwner(), 'draw');
|
||
assert.equal(releasePointer(lease), true);
|
||
});
|
||
|
||
test('reset reports who was holding it, for teardown', () => {
|
||
assert.equal(resetPointerOwnership(), null);
|
||
const lease = claimPointer('draw');
|
||
assert.equal(resetPointerOwnership(), 'draw');
|
||
assert.equal(isPointerFree(), true);
|
||
assert.equal(isLeaseCurrent(lease), false);
|
||
});
|
||
|
||
test('every scene click handler consults ownership before it picks', () => {
|
||
// This is the whole point of a shared contract: a new tool must not have to
|
||
// find and edit each of these. If a layer grows a click handler it has to
|
||
// appear here too.
|
||
const guarded = [
|
||
['src/data/trackingClickGesture.js', 'onClick(click, gesture);'],
|
||
['src/data/localGeojsonCore.js', 'viewer.scene.pick(click.position)'],
|
||
['src/data/cctvGizmo.js', 'pickGizmoPart(event.position)'],
|
||
['src/layers/alpr/presentation.js', 'viewer.scene.pick(click.position)'],
|
||
['src/layers/bikeshare/selection.js', 'viewer.scene.pick(click.position)'],
|
||
['src/layers/firms/selection.js', 'scene.pick(click.position)'],
|
||
[
|
||
'src/layers/installations/selection.js',
|
||
'viewer.scene.pick(click.position)',
|
||
],
|
||
['src/layers/launches/lifecycle.js', 'drillPick(movement.position'],
|
||
['src/layers/radio/interaction.js', 'pickedRadioStationAt(click.position)'],
|
||
[
|
||
'src/layers/satellites/interaction.js',
|
||
'viewer.scene.pick(click.position)',
|
||
],
|
||
[
|
||
'src/layers/submarineCables/interaction.js',
|
||
'viewer.scene.pick(click.position)',
|
||
],
|
||
['src/layers/vessels/selection.js', 'viewer.scene.pick(click.position)'],
|
||
];
|
||
for (const [file, firstPick] of guarded) {
|
||
const source = read(file);
|
||
assert.match(
|
||
source,
|
||
/import \{ isPointerFree \} from '[^']*inputOwnership\.js';/,
|
||
`${file} must consult the shared pointer claim`,
|
||
);
|
||
const guardAt = source.indexOf('if (!isPointerFree()) return;');
|
||
const pickAt = source.indexOf(firstPick);
|
||
assert.ok(guardAt >= 0, `${file} is missing the guard`);
|
||
assert.ok(
|
||
pickAt >= 0,
|
||
`${file}: could not find its pick call (${firstPick})`,
|
||
);
|
||
assert.ok(
|
||
guardAt < pickAt,
|
||
`${file} must yield before it picks, not after`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test('ambient selection handlers never claim the pointer themselves', () => {
|
||
// Claiming from a selection handler would deadlock every other layer the
|
||
// moment a user clicked anything. Only tools claim.
|
||
for (const file of [
|
||
'src/data/trackingClickGesture.js',
|
||
'src/data/localGeojsonCore.js',
|
||
'src/data/cctvGizmo.js',
|
||
'src/layers/vessels/selection.js',
|
||
'src/layers/satellites/interaction.js',
|
||
]) {
|
||
assert.doesNotMatch(
|
||
read(file),
|
||
/claimPointer\(/,
|
||
`${file} must not claim the pointer`,
|
||
);
|
||
}
|
||
});
|