feat: mile-marker posts travel with the road and carry the route log
This commit is contained in:
@@ -1,132 +1,332 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { SectionId } from '@/content/types';
|
||||
import { SceneResourceBag } from '@/scene/resources';
|
||||
import {
|
||||
MILESTONE_FLASH_MAX_RATE,
|
||||
createMilestoneRoad,
|
||||
createMilestones,
|
||||
MILESTONE_ASSIGN_Z,
|
||||
MILESTONE_FAR_Z,
|
||||
MILESTONE_FLASH_MAX_RATE,
|
||||
MILESTONE_NEAR_Z,
|
||||
MILESTONE_PASS_Z,
|
||||
MILESTONE_SIDE,
|
||||
milestoneLayout,
|
||||
milestoneZ,
|
||||
MILESTONE_SLOTS,
|
||||
MILESTONE_SPACING,
|
||||
milestoneEntries,
|
||||
milestoneRangeFade,
|
||||
milestoneSlot,
|
||||
type MilestonePost,
|
||||
} from '@/scene/world/milestones';
|
||||
import { MILESTONE_COUNTS, passPoint } from '@/journey/route-log';
|
||||
|
||||
const sweep = (from: number, to: number, steps = 200): number[] =>
|
||||
Array.from({ length: steps + 1 }, (_, i) => from + ((to - from) * i) / steps);
|
||||
const DT = 1 / 60;
|
||||
|
||||
describe('mile marker layout', () => {
|
||||
it('rolls every post toward the camera monotonically with progress, in order', () => {
|
||||
for (const section of ['experience', 'profile'] as const) {
|
||||
/** Drives the road for `seconds`: progress and road speed (m/s) per frame. */
|
||||
function drive(
|
||||
road: ReturnType<typeof createMilestoneRoad>,
|
||||
seconds: number,
|
||||
frame: (t: number) => { section: SectionId; p: number | null; speed: number },
|
||||
start = { distance: 0 },
|
||||
each?: (posts: readonly MilestonePost[], t: number, p: number | null) => void,
|
||||
) {
|
||||
for (let i = 0; i <= Math.round(seconds / DT); i += 1) {
|
||||
const t = i * DT;
|
||||
const { section, p, speed } = frame(t);
|
||||
start.distance += speed * DT;
|
||||
const posts = road.step({ section, p, distance: start.distance, pulse: 0, deltaSeconds: DT });
|
||||
each?.(posts, t, p);
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
/** Progress at which each feature post (by entry) went past the pass line. */
|
||||
function passes(
|
||||
road: ReturnType<typeof createMilestoneRoad>,
|
||||
seconds: number,
|
||||
frame: (t: number) => { section: SectionId; p: number | null; speed: number },
|
||||
) {
|
||||
const seen: Array<{ entry: number; p: number | null; t: number }> = [];
|
||||
let last: readonly number[] = [];
|
||||
drive(road, seconds, frame, { distance: 0 }, (posts, t, p) => {
|
||||
posts.forEach((post, k) => {
|
||||
if (post.entry >= 0 && post.presence > 0 && (last[k] ?? Infinity) < MILESTONE_PASS_Z && post.z >= MILESTONE_PASS_Z) {
|
||||
seen.push({ entry: post.entry, p, t });
|
||||
}
|
||||
});
|
||||
last = posts.map((post) => post.z);
|
||||
});
|
||||
return seen;
|
||||
}
|
||||
|
||||
describe('mile marker lattice', () => {
|
||||
it('spaces the posts evenly along the road, inside the drawn range', () => {
|
||||
expect(MILESTONE_NEAR_Z - MILESTONE_FAR_Z).toBe(MILESTONE_SPACING * MILESTONE_SLOTS);
|
||||
for (const distance of [0, 17.3, 59.99, 60, 1234.5, 98765.4]) {
|
||||
const zs = Array.from({ length: MILESTONE_SLOTS }, (_, k) => milestoneSlot(distance, k).z).sort((a, b) => a - b);
|
||||
for (const z of zs) {
|
||||
expect(z).toBeGreaterThanOrEqual(MILESTONE_FAR_Z);
|
||||
expect(z).toBeLessThan(MILESTONE_NEAR_Z);
|
||||
}
|
||||
for (let i = 1; i < zs.length; i += 1) expect(zs[i]! - zs[i - 1]!).toBeCloseTo(MILESTONE_SPACING, 6);
|
||||
}
|
||||
// A post is born in the haze and leaves behind the camera, never in view.
|
||||
expect(milestoneRangeFade(MILESTONE_FAR_Z)).toBe(0);
|
||||
expect(milestoneRangeFade(MILESTONE_NEAR_Z - 0.01)).toBe(0);
|
||||
expect(milestoneRangeFade(MILESTONE_PASS_Z)).toBe(1);
|
||||
});
|
||||
|
||||
it('moves every post with the travel distance, metre for metre, and numbers them along the route', () => {
|
||||
for (let k = 0; k < MILESTONE_SLOTS; k += 1) {
|
||||
const a = milestoneSlot(1000, k);
|
||||
const b = milestoneSlot(1007, k);
|
||||
if (b.id === a.id) expect(b.z - a.z).toBeCloseTo(7, 6);
|
||||
else expect(b.id).toBeGreaterThan(a.id);
|
||||
}
|
||||
// The farther post is the later one on the route.
|
||||
const now = Array.from({ length: MILESTONE_SLOTS }, (_, k) => milestoneSlot(500, k)).sort((x, y) => x.z - y.z);
|
||||
for (let i = 1; i < now.length; i += 1) expect(now[i]!.id).toBeLessThan(now[i - 1]!.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mile markers on the road', () => {
|
||||
it('keep streaming past while the reader is still (constant progress)', () => {
|
||||
const road = createMilestoneRoad(true);
|
||||
const state = drive(road, 2, () => ({ section: 'experience', p: 0.42, speed: 18 }));
|
||||
const before = road.posts().map((post) => ({ ...post }));
|
||||
drive(road, 1, () => ({ section: 'experience', p: 0.42, speed: 18 }), state);
|
||||
const after = road.posts();
|
||||
let moved = 0;
|
||||
after.forEach((post, k) => {
|
||||
const was = before[k]!;
|
||||
// Each post either moved 18 m toward the camera or wrapped to the far end.
|
||||
if (post.z > was.z) {
|
||||
expect(post.z - was.z).toBeCloseTo(18 + 18 * DT, 1);
|
||||
moved += 1;
|
||||
} else {
|
||||
expect(post.z).toBeLessThan(MILESTONE_FAR_Z + 20);
|
||||
}
|
||||
});
|
||||
expect(moved).toBeGreaterThanOrEqual(MILESTONE_SLOTS - 1);
|
||||
// Visible plain posts on the free shoulder.
|
||||
const shown = after.filter((post) => post.presence > 0.5);
|
||||
expect(shown.length).toBeGreaterThanOrEqual(2);
|
||||
expect(shown.every((post) => post.x === MILESTONE_SIDE.experience)).toBe(true);
|
||||
});
|
||||
|
||||
it('stands on the free shoulder: right in Experience, left in Profile, nowhere else', () => {
|
||||
expect(MILESTONE_SIDE.experience).toBeGreaterThan(8);
|
||||
expect(MILESTONE_SIDE.profile).toBeLessThan(-8);
|
||||
const road = createMilestoneRoad(true);
|
||||
const state = drive(road, 3, () => ({ section: 'profile', p: 0.5, speed: 30 }));
|
||||
expect(road.posts().filter((post) => post.presence > 0).every((post) => post.x === MILESTONE_SIDE.profile)).toBe(true);
|
||||
drive(road, 1.5, () => ({ section: 'infrastructure', p: null, speed: 30 }), state);
|
||||
expect(road.posts().every((post) => post.presence === 0)).toBe(true);
|
||||
const start = createMilestoneRoad(true);
|
||||
drive(start, 3, () => ({ section: 'start', p: null, speed: 30 }));
|
||||
expect(start.posts().every((post) => post.presence === 0)).toBe(true);
|
||||
});
|
||||
|
||||
it('fades in and out over time at chapter changes instead of popping', () => {
|
||||
const road = createMilestoneRoad(true);
|
||||
const state = drive(road, 3, () => ({ section: 'start', p: null, speed: 18 }));
|
||||
let first = -1;
|
||||
drive(road, 0, () => ({ section: 'profile', p: 0.01, speed: 18 }), state, (posts) => {
|
||||
first = Math.max(...posts.map((post) => post.presence));
|
||||
});
|
||||
expect(first).toBeLessThan(0.2);
|
||||
drive(road, 1.2, () => ({ section: 'profile', p: 0.05, speed: 18 }), state);
|
||||
expect(Math.max(...road.posts().map((post) => post.presence))).toBeGreaterThan(0.8);
|
||||
// Into Experience: Profile's posts fade (no frame drops more than ~10%).
|
||||
let previous = road.posts().map((post) => post.presence);
|
||||
let biggestDrop = 0;
|
||||
drive(road, 1, () => ({ section: 'experience', p: 0.01, speed: 18 }), state, (posts) => {
|
||||
posts.forEach((post, k) => {
|
||||
if (post.x === MILESTONE_SIDE.profile) biggestDrop = Math.max(biggestDrop, previous[k]! - post.presence);
|
||||
});
|
||||
previous = posts.map((post) => post.presence);
|
||||
});
|
||||
expect(biggestDrop).toBeLessThan(0.12);
|
||||
expect(road.posts().filter((post) => post.presence > 0).every((post) => post.x === MILESTONE_SIDE.experience)).toBe(true);
|
||||
});
|
||||
|
||||
it('sends each log entry past the camera about when its plate crosses the line (reading pace)', () => {
|
||||
for (const [section, rate, speed] of [
|
||||
['experience', 0.025, 40],
|
||||
['experience', 0.012, 18],
|
||||
['profile', 0.03, 60],
|
||||
] as const) {
|
||||
const count = MILESTONE_COUNTS[section];
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
let previous = Number.NEGATIVE_INFINITY;
|
||||
for (const p of sweep(0, 1)) {
|
||||
const z = milestoneZ(p, i, count);
|
||||
expect(z).toBeGreaterThan(previous);
|
||||
previous = z;
|
||||
}
|
||||
// At the camera's near edge exactly as its plate crosses the line.
|
||||
expect(milestoneZ(passPoint(i, count), i, count)).toBeCloseTo(MILESTONE_PASS_Z);
|
||||
expect(MILESTONE_PASS_Z).toBeCloseTo((MILESTONE_FAR_Z + MILESTONE_NEAR_Z) / 2);
|
||||
// Still on the near side of the camera (z ~25) and large in frame.
|
||||
expect(MILESTONE_PASS_Z).toBeLessThan(20);
|
||||
// Earlier posts are always nearer.
|
||||
if (i > 0) expect(milestoneZ(0.5, i - 1, count)).toBeGreaterThan(milestoneZ(0.5, i, count));
|
||||
const road = createMilestoneRoad(true);
|
||||
const seen = passes(road, 1 / rate, (t) => ({ section, p: Math.min(1, t * rate), speed }));
|
||||
expect(seen.map((pass) => pass.entry)).toEqual(Array.from({ length: count }, (_, i) => i));
|
||||
for (const pass of seen) {
|
||||
expect(Math.abs(pass.p! - passPoint(pass.entry, count))).toBeLessThan(0.3 / count);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('stands on the free shoulder: right of the road in Experience, left in Profile', () => {
|
||||
expect(MILESTONE_SIDE.experience).toBeGreaterThan(8);
|
||||
expect(MILESTONE_SIDE.profile).toBeLessThan(-8);
|
||||
const posts = milestoneLayout('experience', 0.5).filter((post) => post.presence > 0);
|
||||
expect(posts.length).toBeGreaterThan(0);
|
||||
expect(posts.every((post) => post.section === 'experience' && post.x === MILESTONE_SIDE.experience)).toBe(true);
|
||||
it('sends the entry the reader stopped on, once, and keeps the shoulder moving', () => {
|
||||
const road = createMilestoneRoad(true);
|
||||
// A jump that lands early in entry 2 of 4, then nothing.
|
||||
const p = passPoint(2, 4) - 0.08;
|
||||
const seen = passes(road, 40, (t) => ({ section: 'experience', p: t < 0.3 ? (t / 0.3) * p : p, speed: 18 }));
|
||||
expect(seen.map((pass) => pass.entry)).toEqual([2]);
|
||||
expect(seen[0]!.t).toBeLessThan(15);
|
||||
});
|
||||
|
||||
it('is hidden outside its chapters and fades at the chapter edges', () => {
|
||||
for (const section of ['start', 'infrastructure', 'toolkit', 'contact'] as const) {
|
||||
expect(milestoneLayout(section, 0.5).every((post) => post.presence === 0)).toBe(true);
|
||||
it('runs the road forward when scrolling back: rewound entries come up the road again', () => {
|
||||
const road = createMilestoneRoad(true);
|
||||
const rate = 0.03;
|
||||
const back = 1 / rate;
|
||||
const progress = (t: number) => (t < back ? t * rate : Math.max(0, 1 - (t - back) * rate));
|
||||
const seen: Array<{ entry: number; reverse: boolean }> = [];
|
||||
let last: number[] = [];
|
||||
let ids: number[] = [];
|
||||
drive(road, back * 2, (t) => ({ section: 'experience', p: progress(t), speed: 30 }), { distance: 0 }, (posts, t) => {
|
||||
posts.forEach((post, k) => {
|
||||
// Never toward the horizon, except the wrap to the far end (a new post).
|
||||
if (last[k] !== undefined && ids[k] === post.number) expect(post.z).toBeGreaterThanOrEqual(last[k]!);
|
||||
if (post.entry >= 0 && post.presence > 0 && (last[k] ?? Infinity) < MILESTONE_PASS_Z && post.z >= MILESTONE_PASS_Z) {
|
||||
seen.push({ entry: post.entry, reverse: t >= back });
|
||||
}
|
||||
});
|
||||
last = posts.map((post) => post.z);
|
||||
ids = posts.map((post) => post.number);
|
||||
});
|
||||
expect(seen.filter((pass) => !pass.reverse).map((pass) => pass.entry)).toEqual([0, 1, 2, 3]);
|
||||
// The last entry has only just gone by; the others come back in reverse order.
|
||||
expect(seen.filter((pass) => pass.reverse).map((pass) => pass.entry)).toEqual([2, 1, 0]);
|
||||
});
|
||||
|
||||
it('only hands an entry to a post still far up the road, in the haze', () => {
|
||||
const road = createMilestoneRoad(true);
|
||||
const born = new Map<number, number>();
|
||||
drive(road, 30, (t) => ({ section: 'experience', p: Math.min(1, t * 0.03), speed: 30 }), { distance: 0 }, (posts) => {
|
||||
posts.forEach((post) => {
|
||||
if (post.entry >= 0 && !born.has(post.number)) born.set(post.number, post.z);
|
||||
});
|
||||
});
|
||||
expect(born.size).toBeGreaterThan(0);
|
||||
for (const z of born.values()) expect(z).toBeLessThan(MILESTONE_ASSIGN_Z);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mile marker light', () => {
|
||||
it('warms a feature lamp as it approaches and leaves plain posts dark', () => {
|
||||
const road = createMilestoneRoad(true);
|
||||
const lamps = new Map<number, Array<{ z: number; lamp: number }>>();
|
||||
drive(road, 20, () => ({ section: 'experience', p: passPoint(1, 4) - 0.1, speed: 18 }), { distance: 0 }, (posts) => {
|
||||
for (const post of posts) {
|
||||
if (post.entry < 0) expect(post.lamp).toBe(0);
|
||||
else if (post.presence > 0) lamps.set(post.number, [...(lamps.get(post.number) ?? []), { z: post.z, lamp: post.lamp }]);
|
||||
}
|
||||
});
|
||||
const track = [...lamps.values()][0]!;
|
||||
const approach = track.filter((sample) => sample.z < MILESTONE_PASS_Z);
|
||||
for (let i = 1; i < approach.length; i += 1) expect(approach[i]!.lamp).toBeGreaterThanOrEqual(approach[i - 1]!.lamp);
|
||||
expect(approach.at(-1)!.lamp).toBeGreaterThan(0.8);
|
||||
});
|
||||
|
||||
it('glints once as a feature post passes, never twice inside 0.6 s, and never during a jump', () => {
|
||||
const road = createMilestoneRoad(true);
|
||||
const count = MILESTONE_COUNTS.experience;
|
||||
let glints: number[] = [];
|
||||
let previous = 0;
|
||||
drive(road, 1 / 0.02, (t) => ({ section: 'experience', p: Math.min(1, t * 0.02), speed: 30 }), { distance: 0 }, (posts, t) => {
|
||||
const level = Math.max(...posts.map((post) => post.glint));
|
||||
if (level === 1 && previous < 1) glints.push(t);
|
||||
previous = level;
|
||||
});
|
||||
expect(glints.length).toBe(count);
|
||||
for (let i = 1; i < glints.length; i += 1) expect(glints[i]! - glints[i - 1]!).toBeGreaterThanOrEqual(0.6);
|
||||
// A navigator jump: the whole chapter in 0.6 s at full road speed.
|
||||
const jump = createMilestoneRoad(true);
|
||||
glints = [];
|
||||
let peak = 0;
|
||||
const state = drive(jump, 4, () => ({ section: 'profile', p: 0.02, speed: 18 }));
|
||||
drive(jump, 0.6, (t) => ({ section: 'experience', p: Math.min(1, t / 0.6), speed: 120 }), state, (posts) => {
|
||||
peak = Math.max(peak, ...posts.map((post) => post.glint));
|
||||
});
|
||||
expect(peak).toBe(0);
|
||||
expect(MILESTONE_FLASH_MAX_RATE).toBeGreaterThan(0.5);
|
||||
});
|
||||
|
||||
it('low tier and touch: no glint and no pulse swell', () => {
|
||||
const road = createMilestoneRoad(false);
|
||||
let peak = 0;
|
||||
let lamp = 0;
|
||||
for (let i = 0; i < 60 * 30; i += 1) {
|
||||
const posts = road.step({ section: 'experience', p: Math.min(1, i / (60 * 30)), distance: i * 0.5, pulse: 1, deltaSeconds: DT });
|
||||
peak = Math.max(peak, ...posts.map((post) => post.glint));
|
||||
lamp = Math.max(lamp, ...posts.map((post) => post.lamp));
|
||||
}
|
||||
expect(milestoneLayout('experience', null).every((post) => post.presence === 0)).toBe(true);
|
||||
expect(milestoneLayout('experience', 0).every((post) => post.presence === 0)).toBe(true);
|
||||
expect(milestoneLayout('profile', 1).every((post) => post.presence === 0)).toBe(true);
|
||||
const inside = milestoneLayout('profile', 0.5);
|
||||
expect(inside.filter((post) => post.section === 'experience').every((post) => post.presence === 0)).toBe(true);
|
||||
expect(inside.some((post) => post.section === 'profile' && post.presence === 1)).toBe(true);
|
||||
});
|
||||
|
||||
it('is symmetric in reverse: the same progress gives the same frame either way', () => {
|
||||
const ps = sweep(0, 1);
|
||||
const forward = ps.map((p) => milestoneLayout('experience', p));
|
||||
const backward = [...ps].reverse().map((p) => milestoneLayout('experience', p)).reverse();
|
||||
expect(backward).toEqual(forward);
|
||||
expect(peak).toBe(0);
|
||||
expect(lamp).toBeGreaterThan(0.5);
|
||||
expect(lamp).toBeLessThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mile marker mesh', () => {
|
||||
it('draws seven instances in one mesh, visible only in Experience and Profile', () => {
|
||||
it('draws the lattice in one mesh, visible only in Experience and Profile', () => {
|
||||
const resources = new SceneResourceBag();
|
||||
const markers = createMilestones(resources, true);
|
||||
expect(markers.mesh.count).toBe(7);
|
||||
expect(markers.pool.count).toBe(7);
|
||||
markers.update('start', null, 0, 1 / 60);
|
||||
expect(markers.mesh.count).toBe(MILESTONE_SLOTS);
|
||||
expect(markers.pool.count).toBe(MILESTONE_SLOTS);
|
||||
markers.update({ section: 'start', p: null, distance: 0, pulse: 0, deltaSeconds: DT });
|
||||
expect(markers.mesh.visible).toBe(false);
|
||||
expect(markers.pool.visible).toBe(false);
|
||||
markers.update('experience', 0.4, 0.5, 1 / 60);
|
||||
expect(markers.mesh.visible).toBe(true);
|
||||
expect(markers.pool.visible).toBe(true);
|
||||
markers.update('infrastructure', null, 0, 1 / 60);
|
||||
expect(markers.mesh.visible).toBe(false);
|
||||
expect(markers.pool.visible).toBe(false);
|
||||
resources.dispose();
|
||||
});
|
||||
|
||||
it('flashes a post as its plate crosses the line, either way, and decays in 300 ms', () => {
|
||||
const resources = new SceneResourceBag();
|
||||
const markers = createMilestones(resources, true);
|
||||
const second = passPoint(1, 4);
|
||||
markers.update('experience', second - 0.02, 0, 1 / 60);
|
||||
markers.update('experience', second - 0.01, 0, 1 / 60);
|
||||
expect(markers.flashes()[1]).toBe(0);
|
||||
markers.update('experience', second + 0.01, 0, 1 / 60);
|
||||
expect(markers.flashes()[1]).toBe(1);
|
||||
for (let i = 0; i < 20; i += 1) markers.update('experience', second + 0.01, 0, 1 / 60);
|
||||
expect(markers.flashes()[1]).toBe(0);
|
||||
// Back the other way (after the 0.6 s gap) flashes again.
|
||||
for (let i = 0; i < 30; i += 1) markers.update('experience', second + 0.01, 0, 1 / 60);
|
||||
markers.update('experience', second - 0.01, 0, 1 / 60);
|
||||
expect(markers.flashes()[1]).toBe(1);
|
||||
resources.dispose();
|
||||
});
|
||||
|
||||
it('does not flash posts swept past by a navigator jump or a fling', () => {
|
||||
const resources = new SceneResourceBag();
|
||||
const markers = createMilestones(resources, true);
|
||||
// A navigator long-scroll crosses the whole chapter in about 0.6 s.
|
||||
let peak = 0;
|
||||
for (let frame = 0; frame <= 36; frame += 1) {
|
||||
markers.update('experience', frame / 36, 0, 1 / 60);
|
||||
peak = Math.max(peak, ...markers.flashes());
|
||||
let featured = false;
|
||||
let pooled = false;
|
||||
for (let i = 1; i < 60 * 12; i += 1) {
|
||||
markers.update({ section: 'experience', p: 0.4, distance: i * 0.3, pulse: 0.5, deltaSeconds: DT });
|
||||
featured ||= markers.posts().some((post) => post.entry >= 0);
|
||||
pooled ||= markers.pool.visible;
|
||||
}
|
||||
expect(peak).toBe(0);
|
||||
// Parked, then a single wheel tick at reading pace still flashes the post it crosses.
|
||||
for (let i = 0; i < 90; i += 1) markers.update('experience', passPoint(3, 4) + 0.01, 0, 1 / 60);
|
||||
markers.update('experience', passPoint(3, 4) - 0.01, 0, 1 / 60);
|
||||
expect(markers.flashes()[3]).toBe(1);
|
||||
expect(MILESTONE_FLASH_MAX_RATE).toBeGreaterThan(0.5);
|
||||
expect(markers.mesh.visible).toBe(true);
|
||||
expect(featured).toBe(true);
|
||||
expect(pooled).toBe(true);
|
||||
for (let i = 0; i < 60; i += 1) {
|
||||
markers.update({ section: 'infrastructure', p: null, distance: 300 + i * 0.3, pulse: 0, deltaSeconds: DT });
|
||||
}
|
||||
expect(markers.mesh.visible).toBe(false);
|
||||
expect(markers.pool.visible).toBe(false);
|
||||
resources.dispose();
|
||||
});
|
||||
|
||||
it('never flashes on the low tier or touch', () => {
|
||||
it('never lights pools on the low tier', () => {
|
||||
const resources = new SceneResourceBag();
|
||||
const markers = createMilestones(resources, false);
|
||||
const first = passPoint(0, 3);
|
||||
markers.update('profile', first - 0.01, 1, 1 / 60);
|
||||
markers.update('profile', first + 0.01, 1, 1 / 60);
|
||||
expect(markers.flashes().every((level) => level === 0)).toBe(true);
|
||||
let pooled = false;
|
||||
let featured = false;
|
||||
for (let i = 1; i < 60 * 15; i += 1) {
|
||||
markers.update({ section: 'profile', p: 0.5, distance: i * 0.3, pulse: 1, deltaSeconds: DT });
|
||||
pooled ||= markers.pool.visible;
|
||||
featured ||= markers.posts().some((post) => post.entry >= 0 && post.lamp > 0.5);
|
||||
}
|
||||
expect(markers.mesh.visible).toBe(true);
|
||||
expect(featured).toBe(true);
|
||||
expect(pooled).toBe(false);
|
||||
resources.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mile marker plates', () => {
|
||||
it('letters the feature plates from the inert input snapshot, and survives without one', () => {
|
||||
const content = JSON.parse(readFileSync(resolve(process.cwd(), 'content/public/portfolio-content.json'), 'utf8')) as unknown;
|
||||
const page = document.implementation.createHTMLDocument('plates');
|
||||
const script = page.createElement('script');
|
||||
script.id = 'portfolio-inputs';
|
||||
script.type = 'application/json';
|
||||
script.textContent = JSON.stringify({ content });
|
||||
page.body.append(script);
|
||||
const items = milestoneEntries('experience', page);
|
||||
expect(items).toHaveLength(MILESTONE_COUNTS.experience);
|
||||
expect(items[1]).toMatchObject({ compactDate: 'JUN→AUG 2025', caption: 'CAMERA CONSULTANȚILOR FISCALI' });
|
||||
expect(milestoneEntries('profile', page)).toHaveLength(MILESTONE_COUNTS.profile);
|
||||
script.textContent = '{not json';
|
||||
expect(milestoneEntries('experience', page)).toEqual([]);
|
||||
expect(milestoneEntries('experience', document.implementation.createHTMLDocument('none'))).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user