121 lines
4.4 KiB
TypeScript
121 lines
4.4 KiB
TypeScript
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);
|
|
});
|