144 lines
5.4 KiB
TypeScript
144 lines
5.4 KiB
TypeScript
import { test, expect, login, sceneObjectCount } from './fixtures';
|
|
|
|
/**
|
|
* Regression guard for the Mercator -> globe migration.
|
|
*
|
|
* The MapLibre implementation tiled every GeoJSON source through
|
|
* @maplibre/geojson-vt, whose projectY() clamps its result to [0,1] — so any
|
|
* vertex above 85.051129° collapsed onto that parallel and a polar trajectory
|
|
* rendered as a straight line along it. Cesium converts degrees straight to
|
|
* Cartesian3, so latitude must survive intact. If a Mercator-tiled render path
|
|
* is ever reintroduced, this test fails.
|
|
*/
|
|
|
|
const MERCATOR_LIMIT = 85.051129;
|
|
|
|
test.beforeEach(async ({ context }) => {
|
|
await login(context);
|
|
});
|
|
|
|
test('globe preserves latitude above the Mercator limit', async ({ page }) => {
|
|
// Cesium ships ~7 MB of workers/assets; first paint is slower than MapLibre.
|
|
test.setTimeout(90_000);
|
|
|
|
await page.goto('/predict');
|
|
await page
|
|
.locator('.map-container canvas')
|
|
.first()
|
|
.waitFor({ state: 'attached', timeout: 60_000 });
|
|
|
|
// Map.svelte exposes the raw Cesium Viewer on window._lsvMap in dev builds.
|
|
await page.waitForFunction(
|
|
() => (window as unknown as { _lsvMap?: { scene?: unknown } })._lsvMap?.scene !== undefined,
|
|
undefined,
|
|
{ timeout: 60_000 },
|
|
);
|
|
|
|
const latitudes: number[] = await page.evaluate((limit) => {
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
const viewer = (window as any)._lsvMap;
|
|
// Cesium is an ES module, not a global. Reach its constructors through
|
|
// live objects the Viewer already holds: camera.position is a Cartesian3
|
|
// and positionCartographic is a Cartographic, both carrying the statics.
|
|
const Cartesian3: any = viewer.camera.position.constructor;
|
|
const Cartographic: any = viewer.camera.positionCartographic.constructor;
|
|
|
|
const wanted = [89.0, limit + 0.001, 89.99, 90.0];
|
|
const positions = Cartesian3.fromDegreesArray(wanted.flatMap((lat: number) => [10, lat]));
|
|
viewer.entities.add({ polyline: { positions, width: 3 } });
|
|
|
|
return positions.map(
|
|
(p: unknown) => (Cartographic.fromCartesian(p).latitude * 180) / Math.PI,
|
|
);
|
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
}, MERCATOR_LIMIT);
|
|
|
|
expect(latitudes).toHaveLength(4);
|
|
// Every vertex round-trips to the latitude it was given.
|
|
expect(latitudes[0]).toBeCloseTo(89.0, 6);
|
|
expect(latitudes[1]).toBeCloseTo(MERCATOR_LIMIT + 0.001, 6);
|
|
expect(latitudes[2]).toBeCloseTo(89.99, 6);
|
|
expect(latitudes[3]).toBeCloseTo(90.0, 6);
|
|
// And none got pinned to the old Mercator ceiling.
|
|
for (const lat of latitudes.slice(1)) {
|
|
expect(lat).toBeGreaterThan(MERCATOR_LIMIT);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* The flight_path tuples carry altitude as an optional third element
|
|
* (domain/geo.ts: LatLngTuple), filled in by parsePrediction. MapLibre could
|
|
* not use it, so it sat unread; on a globe it must become real geometry —
|
|
* otherwise a 30 km balloon flight renders as a flat line on the ground.
|
|
*/
|
|
test.describe('altitude', () => {
|
|
test.beforeEach(async ({ context }) => {
|
|
await login(context);
|
|
});
|
|
|
|
/** Heights (metres) of every vertex of the rendered workspace path. */
|
|
function pathHeights(page: import('@playwright/test').Page): Promise<number[]> {
|
|
return page.evaluate(() => {
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
const v: any = (window as any)._lsvMap;
|
|
const C: any = v.camera.positionCartographic.constructor;
|
|
const e = v.entities.values.find((x: any) => String(x.id).endsWith('__path'));
|
|
if (!e) return [];
|
|
return e.polyline.positions
|
|
.getValue(v.clock.currentTime)
|
|
.map((p: unknown) => C.fromCartesian(p).height);
|
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
});
|
|
}
|
|
|
|
async function runPrediction(page: import('@playwright/test').Page) {
|
|
await page.goto('/');
|
|
await page.evaluate(() => localStorage.removeItem('workspaces'));
|
|
await page.goto('/predict');
|
|
await page
|
|
.locator('.map-container canvas')
|
|
.first()
|
|
.waitFor({ state: 'attached', timeout: 60_000 });
|
|
await page
|
|
.locator('.panel-container-right')
|
|
.locator('.workspace-row')
|
|
.first()
|
|
.getByRole('button', { name: /Рассчитать|Run/ })
|
|
.click();
|
|
await expect
|
|
.poll(() => sceneObjectCount(page, 'ws/'), {
|
|
timeout: 90_000,
|
|
intervals: [1000, 2000, 3000],
|
|
})
|
|
.toBeGreaterThan(0);
|
|
}
|
|
|
|
test('trajectory vertices carry real altitude, not ground level', async ({ page }) => {
|
|
test.setTimeout(150_000);
|
|
await runPrediction(page);
|
|
|
|
const heights = await pathHeights(page);
|
|
expect(heights.length).toBeGreaterThan(10);
|
|
// Burst is at 30 km; the apex must be somewhere near it, definitely not 0.
|
|
expect(Math.max(...heights)).toBeGreaterThan(10_000);
|
|
// Launch/landing sit on the ground, so the minimum stays low.
|
|
expect(Math.min(...heights)).toBeLessThan(1_000);
|
|
});
|
|
|
|
test('burst marker sits at its altitude, not on the ground', async ({ page }) => {
|
|
test.setTimeout(150_000);
|
|
await runPrediction(page);
|
|
|
|
const burstHeight = await page.evaluate(() => {
|
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
const v: any = (window as any)._lsvMap;
|
|
const C: any = v.camera.positionCartographic.constructor;
|
|
const e = v.entities.values.find((x: any) => String(x.id).endsWith('__burst'));
|
|
if (!e) return null;
|
|
return C.fromCartesian(e.position.getValue(v.clock.currentTime)).height;
|
|
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
});
|
|
expect(burstHeight).not.toBeNull();
|
|
expect(burstHeight as number).toBeGreaterThan(10_000);
|
|
});
|
|
});
|