191 lines
6.4 KiB
TypeScript
191 lines
6.4 KiB
TypeScript
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 });
|
|
});
|
|
}
|