leaflet_svelte/tests/e2e/export.spec.ts
2026-08-03 22:12:32 +09:00

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);
});