import { test, expect, type Page } from '@playwright/test'; async function login(page: Page) { await page.goto('/login/'); await page.fill('#lg-username', 'demo'); await page.fill('#lg-password', 'demo'); await page.click('button[type=submit]'); await expect(page.getByTestId('nav-menu')).toBeVisible(); } /** The builder is desktop-only, so pin a wide viewport before revealing it. */ async function openBuilder(page: Page) { await page.setViewportSize({ width: 1400, height: 900 }); await login(page); await page.selectOption('#cp-profile', 'custom_profile'); await expect(page.getByTestId('profile-builder')).toBeVisible(); } test('the builder is revealed only by the custom profile', async ({ page }) => { await page.setViewportSize({ width: 1400, height: 900 }); await login(page); // Progressive disclosure: the standard profile shows no stage list. await expect(page.getByTestId('profile-builder')).toHaveCount(0); await page.selectOption('#cp-profile', 'custom_profile'); await expect(page.getByTestId('profile-builder')).toBeVisible(); // It starts from the standard flight expressed as stages. await expect(page.getByTestId('stage-card')).toHaveCount(2); }); test('stages can be added, reordered and removed', async ({ page }) => { await openBuilder(page); const names = () => page.getByTestId('stage-name').evaluateAll((els) => els.map((e) => (e as HTMLInputElement).value) ); expect(await names()).toEqual(['ascent', 'descent']); await page.getByTestId('add-stage').click(); await expect(page.getByTestId('stage-card')).toHaveCount(3); // Reordering is by buttons so it stays keyboard-reachable. await page.getByTestId('stage-down').first().click(); expect(await names()).toEqual(['descent', 'ascent', 'stage-3']); await page.getByTestId('stage-delete').last().click(); await expect(page.getByTestId('stage-card')).toHaveCount(2); }); test('a stage with no exit is reported as invalid', async ({ page }) => { await openBuilder(page); await expect(page.getByTestId('profile-problems')).toHaveCount(0); // Remove the ascent stage's only "advance when" condition. await page.getByTestId('advance-row').first().getByRole('button').click(); await expect(page.getByTestId('profile-problems')).toContainText(/условия выхода|no exit/i); }); test('an abort constraint must point at an existing stage', async ({ page }) => { await openBuilder(page); await page.getByTestId('add-abort').first().click(); // It defaults to falling back to the first stage, which is itself — pick another. const fallback = page.getByTestId('abort-fallback').first(); await fallback.selectOption('descent'); await expect(page.getByTestId('profile-problems')).toHaveCount(0); await fallback.selectOption(''); await expect(page.getByTestId('profile-problems')).toContainText(/Резервная|fallback/i); }); test('a custom profile runs through the v2 endpoint and draws a trajectory', async ({ page }) => { await openBuilder(page); // Change the ascent rate through the stage, not the flat form. await page.getByTestId('stage-rate').first().fill('7'); const v2 = page.waitForRequest( (r) => r.url().includes('/api/predictions/v2/') && r.method() === 'POST' ); await page.getByTestId('run-btn').click(); const request = await v2; // The request carries the stage list in the predictor's own shape. const body = request.postDataJSON(); expect(body.profile).toHaveLength(2); expect(body.profile[0].model).toMatchObject({ type: 'constant_rate', rate: 7 }); expect(body.profile[1].model.type).toBe('parachute_descent'); expect(body.launch).toHaveProperty('time'); await expect(page.getByTestId('flight-time')).toBeVisible({ timeout: 15000 }); });