stratoflights-web/tests/e2e/profile.spec.ts
2026-08-14 00:01:49 +09:00

89 lines
3.6 KiB
TypeScript

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