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(''); expect(text).toContain(''); // 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('absolute'); expect(text).toContain(''); // Placemarks for the three key events. for (const n of ['Launch', 'Burst', 'Landing']) expect(text).toContain(`${n}`); // Coordinates are lon,lat,alt triples — note the order differs from CSV. const coords = /([\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); });