feat(globe): port to CeliumJS
This commit is contained in:
parent
ec03425067
commit
eb7698e034
51 changed files with 3521 additions and 1992 deletions
121
tests/e2e/export.spec.ts
Normal file
121
tests/e2e/export.spec.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { test, expect, login, sceneObjectCount } from './fixtures';
|
||||
|
||||
/**
|
||||
* Prediction export. The UI shell (format select + Export button) existed with
|
||||
* no handler wired at all, so nothing was ever produced.
|
||||
*/
|
||||
|
||||
test.beforeEach(async ({ context }) => {
|
||||
await login(context);
|
||||
});
|
||||
|
||||
/** Run a prediction so there is a result to export. */
|
||||
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);
|
||||
}
|
||||
|
||||
/** Pick a format in the export select and click Export; return the file text. */
|
||||
async function exportAs(
|
||||
page: import('@playwright/test').Page,
|
||||
format: string,
|
||||
): Promise<{ name: string; text: string }> {
|
||||
const group = page.locator('.panel-container-left .input-group', {
|
||||
has: page.getByRole('button', { name: /Экспорт|Export/ }),
|
||||
});
|
||||
await group.locator('select').selectOption(format);
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('download'),
|
||||
group.getByRole('button', { name: /Экспорт|Export/ }).click(),
|
||||
]);
|
||||
const path = await download.path();
|
||||
if (!path) throw new Error('download produced no file');
|
||||
return { name: download.suggestedFilename(), text: readFileSync(path, 'utf8') };
|
||||
}
|
||||
|
||||
test('exports the trajectory as CSV', async ({ page }) => {
|
||||
test.setTimeout(150_000);
|
||||
await runPrediction(page);
|
||||
|
||||
const { name, text } = await exportAs(page, 'CSV');
|
||||
expect(name).toMatch(/\.csv$/);
|
||||
|
||||
const lines = text.trim().split('\n');
|
||||
expect(lines[0]).toBe('datetime,latitude,longitude,altitude');
|
||||
// One row per trajectory point.
|
||||
expect(lines.length).toBeGreaterThan(50);
|
||||
// Every data row: ISO timestamp + three numbers.
|
||||
for (const line of lines.slice(1)) {
|
||||
const cols = line.split(',');
|
||||
expect(cols).toHaveLength(4);
|
||||
expect(cols[0]).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
||||
for (const n of cols.slice(1)) expect(Number.isFinite(Number(n))).toBe(true);
|
||||
}
|
||||
// Altitude must actually rise — proves real data, not zeros.
|
||||
const alts = lines.slice(1).map((l) => Number(l.split(',')[3]));
|
||||
expect(Math.max(...alts)).toBeGreaterThan(10_000);
|
||||
});
|
||||
|
||||
test('exports the trajectory as JSON', async ({ page }) => {
|
||||
test.setTimeout(150_000);
|
||||
await runPrediction(page);
|
||||
|
||||
const { name, text } = await exportAs(page, 'JSON');
|
||||
expect(name).toMatch(/\.json$/);
|
||||
|
||||
const data = JSON.parse(text) as {
|
||||
launch: { latitude: number; longitude: number; datetime: string };
|
||||
burst: { altitude: number };
|
||||
landing: { latitude: number; longitude: number };
|
||||
flight_time: number;
|
||||
trajectory: { datetime: string; latitude: number; longitude: number; altitude: number }[];
|
||||
};
|
||||
expect(data.trajectory.length).toBeGreaterThan(50);
|
||||
expect(Number.isFinite(data.launch.latitude)).toBe(true);
|
||||
expect(data.burst.altitude).toBeGreaterThan(10_000);
|
||||
expect(Number.isFinite(data.landing.longitude)).toBe(true);
|
||||
expect(data.flight_time).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('exports the trajectory as KML with absolute altitude', async ({ page }) => {
|
||||
test.setTimeout(150_000);
|
||||
await runPrediction(page);
|
||||
|
||||
const { name, text } = await exportAs(page, 'KML');
|
||||
expect(name).toMatch(/\.kml$/);
|
||||
|
||||
expect(text).toContain('<?xml version="1.0" encoding="UTF-8"?>');
|
||||
expect(text).toContain('<kml xmlns="http://www.opengis.net/kml/2.2">');
|
||||
// A balloon track is not a ground feature: it must carry its own altitude,
|
||||
// otherwise Google Earth drapes the 30 km arc onto the terrain.
|
||||
expect(text).toContain('<altitudeMode>absolute</altitudeMode>');
|
||||
expect(text).toContain('<LineString>');
|
||||
// Placemarks for the three key events.
|
||||
for (const n of ['Launch', 'Burst', 'Landing']) expect(text).toContain(`<name>${n}</name>`);
|
||||
|
||||
// Coordinates are lon,lat,alt triples — note the order differs from CSV.
|
||||
const coords = /<coordinates>([\s\S]*?)<\/coordinates>/.exec(text);
|
||||
expect(coords).not.toBeNull();
|
||||
const triples = (coords as RegExpExecArray)[1].trim().split(/\s+/);
|
||||
expect(triples.length).toBeGreaterThan(50);
|
||||
const alts = triples.map((tr) => Number(tr.split(',')[2]));
|
||||
expect(Math.max(...alts)).toBeGreaterThan(10_000);
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue