feat(globe): port to CeliumJS
This commit is contained in:
parent
ec03425067
commit
eb7698e034
51 changed files with 3521 additions and 1992 deletions
191
tests/e2e/polar.spec.ts
Normal file
191
tests/e2e/polar.spec.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import { test, expect, login } from './fixtures';
|
||||
|
||||
/**
|
||||
* Task 6 verification: end-to-end polar behaviour against the REAL stack.
|
||||
*
|
||||
* Unlike the rest of the suite, this spec expects Django on :8000 and the Go
|
||||
* predictor on :8080 with a loaded GFS dataset (./run-stack.py). It drives a
|
||||
* launch at high latitude through the UI, then reads back the coordinates the
|
||||
* globe actually rendered — the numbers the predictor returned, not synthetic
|
||||
* ones.
|
||||
*
|
||||
* Run with:
|
||||
* npx playwright test tests/e2e/polar.spec.ts --reporter=list
|
||||
*/
|
||||
|
||||
const MERCATOR_LIMIT = 85.051129;
|
||||
|
||||
/**
|
||||
* Must be reached as localhost, not 127.0.0.1. Django's CSRF_TRUSTED_ORIGINS
|
||||
* defaults to `http://localhost:5173`, so a 127.0.0.1 origin gets 403 on any
|
||||
* POST (including /api/predictions/). run-stack.py binds Vite to 127.0.0.1 and
|
||||
* does not set CSRF_TRUSTED_ORIGINS, so the trusted spelling is the one to use.
|
||||
*/
|
||||
test.use({ baseURL: 'http://localhost:5173' });
|
||||
|
||||
/** Seed one workspace whose launch point sits at the given latitude. */
|
||||
async function seedLaunch(page: import('@playwright/test').Page, lat: number, lng: number) {
|
||||
await page.goto('/');
|
||||
await page.evaluate(
|
||||
({ lat, lng }) => {
|
||||
const ws = {
|
||||
id: crypto.randomUUID(),
|
||||
name: `polar-${lat}`,
|
||||
color: '#dc3545',
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
flightParameters: {
|
||||
ascent_rate: 5.0,
|
||||
burst_altitude: 30000.0,
|
||||
dataset: '',
|
||||
descent_rate: 5.0,
|
||||
format: 'json',
|
||||
launch_altitude: 0.0,
|
||||
launch_latitude: lat,
|
||||
launch_longitude: lng,
|
||||
profile: 'standard_profile',
|
||||
version: 2,
|
||||
},
|
||||
launchDate: new Date().toISOString().split('T')[0],
|
||||
launchTime: '12:00:00',
|
||||
result: null,
|
||||
bboxMargin: 10,
|
||||
bboxVisible: false,
|
||||
};
|
||||
localStorage.setItem('workspaces', JSON.stringify({ items: [ws], activeId: ws.id }));
|
||||
},
|
||||
{ lat, lng },
|
||||
);
|
||||
}
|
||||
|
||||
/** Every latitude Cesium is holding in a rendered workspace polyline. */
|
||||
function renderedLatitudes(page: import('@playwright/test').Page): Promise<number[]> {
|
||||
return page.evaluate(() => {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const viewer: any = (window as any)._lsvMap;
|
||||
if (!viewer?.entities) return [];
|
||||
const Cartographic: any = viewer.camera.positionCartographic.constructor;
|
||||
const out: number[] = [];
|
||||
for (const e of viewer.entities.values) {
|
||||
if (typeof e.id !== 'string' || !e.id.startsWith('ws/')) continue;
|
||||
const positions = e.polyline?.positions?.getValue?.(viewer.clock.currentTime);
|
||||
if (!positions) continue;
|
||||
for (const p of positions) {
|
||||
out.push((Cartographic.fromCartesian(p).latitude * 180) / Math.PI);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This spec needs the real predictor + Django, unlike the rest of the suite
|
||||
* which runs against the mock plugin. Probe for it and skip rather than fail
|
||||
* confusingly when only the mock dev server is up.
|
||||
*/
|
||||
let stackUp = false;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
try {
|
||||
const res = await fetch('http://127.0.0.1:8080/ready');
|
||||
stackUp = res.ok;
|
||||
} catch {
|
||||
stackUp = false;
|
||||
}
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ context }) => {
|
||||
test.skip(!stackUp, 'requires the real stack — start it with ./run-stack.py');
|
||||
await login(context);
|
||||
});
|
||||
|
||||
for (const lat of [89.5, 89.99]) {
|
||||
test(`polar launch at ${lat}N renders unclamped on the globe`, async ({ page }) => {
|
||||
test.setTimeout(180_000);
|
||||
|
||||
await seedLaunch(page, lat, 0.1);
|
||||
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();
|
||||
|
||||
// Wait for the real predictor round-trip to paint something.
|
||||
await expect
|
||||
.poll(async () => (await renderedLatitudes(page)).length, {
|
||||
timeout: 150_000,
|
||||
intervals: [2000, 3000, 5000],
|
||||
})
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
const lats = await renderedLatitudes(page);
|
||||
const maxLat = Math.max(...lats);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
` [${lat}N] vertices=${lats.length} max=${maxLat.toFixed(5)} min=${Math.min(...lats).toFixed(5)}`,
|
||||
);
|
||||
|
||||
// Nothing may be NaN, and the track must reach above the Mercator ceiling
|
||||
// that the previous renderer could not represent.
|
||||
expect(lats.every((v) => Number.isFinite(v))).toBe(true);
|
||||
expect(maxLat).toBeGreaterThan(MERCATOR_LIMIT);
|
||||
// And no vertex may sit exactly on the old clamp value, which would mean
|
||||
// something re-introduced Mercator tiling.
|
||||
expect(lats.some((v) => Math.abs(v - MERCATOR_LIMIT) < 1e-6)).toBe(false);
|
||||
|
||||
// Nothing on /predict auto-frames the result (fitBounds is only wired on
|
||||
// the tracking page), so frame the track's own bounds to capture it —
|
||||
// the same Rectangle maths CesiumMap.fitBounds uses.
|
||||
await page.evaluate(() => {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const v: any = (window as any)._lsvMap;
|
||||
const Cartographic: any = v.camera.positionCartographic.constructor;
|
||||
const Rectangle: any = v.camera.computeViewRectangle().constructor;
|
||||
const lats: number[] = [];
|
||||
const lngs: number[] = [];
|
||||
for (const e of v.entities.values) {
|
||||
if (typeof e.id !== 'string' || !e.id.startsWith('ws/')) continue;
|
||||
const ps = e.polyline?.positions?.getValue?.(v.clock.currentTime);
|
||||
if (!ps) continue;
|
||||
for (const p of ps) {
|
||||
const c = Cartographic.fromCartesian(p);
|
||||
lats.push((c.latitude * 180) / Math.PI);
|
||||
lngs.push((c.longitude * 180) / Math.PI);
|
||||
}
|
||||
}
|
||||
const rect = Rectangle.fromDegrees(
|
||||
Math.min(...lngs),
|
||||
Math.min(...lats),
|
||||
Math.max(...lngs),
|
||||
Math.max(...lats),
|
||||
);
|
||||
v.camera.flyTo({ destination: rect, duration: 0 });
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
});
|
||||
// Let the camera move and tile streaming settle before capturing.
|
||||
await page.waitForTimeout(6000);
|
||||
const cam = await page.evaluate(() => {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const v: any = (window as any)._lsvMap;
|
||||
const c = v.camera.positionCartographic;
|
||||
return {
|
||||
lng: (c.longitude * 180) / Math.PI,
|
||||
lat: (c.latitude * 180) / Math.PI,
|
||||
height: c.height,
|
||||
tilesLoaded: v.scene.globe.tilesLoaded,
|
||||
};
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
});
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(` [${lat}N] camera ${JSON.stringify(cam)}`);
|
||||
await page.screenshot({ path: `test-results/polar-${lat}N.png`, fullPage: false });
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue