604 lines
28 KiB
JavaScript
604 lines
28 KiB
JavaScript
const { test, expect } = require('@playwright/test');
|
|
const { login, localized, uniqueName } = require('./helpers');
|
|
|
|
async function dismissOnboardingIfOpen(page) {
|
|
await page.locator('#crank-onboarding-panel').waitFor({ state: 'attached' });
|
|
await page.locator('[data-testid="onboarding-dismiss"]').waitFor({ state: 'attached' });
|
|
await page.evaluate(() => {
|
|
const dismiss = document.querySelector('[data-testid="onboarding-dismiss"]');
|
|
if (dismiss) dismiss.click();
|
|
});
|
|
await expect(page.locator('#crank-onboarding-panel')).toBeHidden();
|
|
}
|
|
|
|
test('operations page shows demo catalog and filter works', async ({ page }) => {
|
|
await login(page);
|
|
await expect(page.locator('.page-heading')).toHaveText(localized('Operations', 'Операции'));
|
|
await expect(page.locator('.ws-switcher-trigger')).toBeVisible();
|
|
await expect(page.locator('#ws-dropdown')).toHaveCount(0);
|
|
await expect(page.getByText(localized('Loading operations', 'Загрузка операций'))).toBeHidden();
|
|
expect(await page.locator('tbody tr').count()).toBeGreaterThanOrEqual(1);
|
|
await page.getByPlaceholder(localized('Search operations', 'Поиск операций')).fill('frankfurter');
|
|
await expect(page.locator('tbody tr')).toHaveCount(1);
|
|
await expect(page.locator('tbody tr').first()).toContainText(/frankfurter_latest_rate/i);
|
|
});
|
|
|
|
test('operations page imports OpenAPI methods as drafts', async ({ page }) => {
|
|
await login(page);
|
|
await dismissOnboardingIfOpen(page);
|
|
const operationId = uniqueName('get_rates');
|
|
const statusOperationId = `${operationId}_status`;
|
|
await page.getByRole('button', { name: localized('Import OpenAPI', 'Импорт OpenAPI') }).click();
|
|
await expect(page.locator('#openapi-import-modal')).toBeVisible();
|
|
const sourceCanary = 'openapi-source-canary-not-for-dom';
|
|
await page.locator('#openapi-import-file').setInputFiles({
|
|
name: 'demo-openapi.yaml',
|
|
mimeType: 'application/octet-stream',
|
|
buffer: Buffer.from(`
|
|
openapi: 3.0.3
|
|
info:
|
|
title: Demo Import API
|
|
description: ${sourceCanary}
|
|
servers:
|
|
paths:
|
|
/v1/rates/{date}:
|
|
post:
|
|
operationId: ${operationId}
|
|
summary: Получить курсы валют
|
|
description: Курсы.
|
|
tags: [currency]
|
|
parameters:
|
|
- name: date
|
|
in: path
|
|
required: true
|
|
schema: { type: string }
|
|
- name: base
|
|
in: query
|
|
required: true
|
|
schema: { type: string }
|
|
- name: X-API-Version
|
|
in: header
|
|
required: false
|
|
schema: { type: string }
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [symbols]
|
|
properties:
|
|
symbols: { type: string }
|
|
responses:
|
|
'200':
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
base: { type: string }
|
|
/v1/status:
|
|
get:
|
|
operationId: ${statusOperationId}
|
|
summary: Проверить статус
|
|
tags: [service]
|
|
responses:
|
|
'200':
|
|
description: OK
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
status: { type: string }
|
|
`),
|
|
});
|
|
const previewRequest = page.waitForRequest((request) => request.method() === 'POST'
|
|
&& request.url().includes('/imports/openapi/preview'));
|
|
await page.locator('#openapi-import-preview').click();
|
|
const request = await previewRequest;
|
|
expect(request.headers()['content-type']).toMatch(/^multipart\/form-data; boundary=/i);
|
|
await expect(page.locator('#openapi-import-preview-panel')).toBeVisible();
|
|
await expect(page.locator('.openapi-import-document-findings')).toContainText(/base URL/i);
|
|
await expect(page.locator('.openapi-import-group').filter({ hasText: 'currency' })).toBeVisible();
|
|
const importedOperation = page.locator('.openapi-import-operation').filter({ hasText: 'Получить курсы валют' });
|
|
await expect(importedOperation).toBeVisible();
|
|
await expect(page.locator('.openapi-import-operation').filter({ hasText: 'Проверить статус' })).toBeVisible();
|
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('Path');
|
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('date');
|
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('Query');
|
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('base');
|
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('Header');
|
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('X-API-Version');
|
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('Body');
|
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText('symbols');
|
|
await expect(importedOperation.locator('.openapi-import-mapping-preview')).toContainText(localized('Response', 'Ответ'));
|
|
await expect(page.locator('#openapi-import-selection')).toContainText(localized('Selected: 2', 'Выбрано: 2'));
|
|
await page.locator('#openapi-import-search').fill('status');
|
|
await expect(page.locator('.openapi-import-operation:visible')).toHaveCount(1);
|
|
await expect(page.locator('.openapi-import-operation:visible').filter({ hasText: 'Проверить статус' })).toBeVisible();
|
|
await page.locator('#openapi-import-clear-visible').click();
|
|
await expect(page.locator('#openapi-import-selection')).toContainText(localized('Selected: 1', 'Выбрано: 1'));
|
|
await page.locator('#openapi-import-search').fill('');
|
|
await page.locator('#openapi-import-method-filter').selectOption('POST');
|
|
await expect(page.locator('.openapi-import-operation:visible')).toHaveCount(1);
|
|
await expect(page.locator('.openapi-import-operation:visible').filter({ hasText: 'Получить курсы валют' })).toBeVisible();
|
|
await page.locator('#openapi-import-method-filter').selectOption('');
|
|
await page.locator('#openapi-import-server-custom').fill('https://api.example.test');
|
|
await page.locator('#openapi-import-create').click();
|
|
await expect(page.locator('#openapi-import-result')).toContainText(localized('Import result', 'Результат импорта'));
|
|
await expect(page.locator('.openapi-import-result-table')).toBeVisible();
|
|
await expect(page.locator('.openapi-import-result-row').filter({ hasText: operationId })).toContainText('POST /v1/rates/{date}');
|
|
await expect(page.locator('.openapi-import-result-row').filter({ hasText: operationId })).toContainText(localized('tool description is too short', 'Описание инструмента слишком короткое'));
|
|
await expect(page.locator('.openapi-import-result-row').filter({ hasText: operationId })).toContainText(localized('Fix in wizard', 'Исправить в мастере'));
|
|
await expect(page.locator('.openapi-import-primary-result a')).toHaveAttribute(
|
|
'href',
|
|
/\/wizard\/\?mode=edit&operationId=op_/
|
|
);
|
|
await expect(page.locator('tbody')).toContainText(new RegExp(operationId, 'i'));
|
|
await expect(page.locator('tbody')).not.toContainText(new RegExp(statusOperationId, 'i'));
|
|
await expect(page.locator('body')).not.toContainText(sourceCanary);
|
|
});
|
|
|
|
test('OpenAPI upload rejects invalid files locally and restores focus after Escape', async ({ page }) => {
|
|
await login(page);
|
|
await dismissOnboardingIfOpen(page);
|
|
const trigger = page.getByRole('button', { name: localized('Import OpenAPI', 'Импорт OpenAPI') });
|
|
await trigger.focus();
|
|
await trigger.click();
|
|
await expect(page.locator('#openapi-import-file-select')).toBeFocused();
|
|
await page.keyboard.press('Shift+Tab');
|
|
await expect(page.locator('[data-openapi-close]').last()).toBeFocused();
|
|
await page.keyboard.press('Shift+Tab');
|
|
await expect(page.locator('#openapi-import-reset')).toBeFocused();
|
|
await page.locator('#openapi-import-file-select').focus();
|
|
await expect(page.locator('#openapi-import-status')).toHaveAttribute('role', 'status');
|
|
await expect(page.locator('#openapi-import-status')).toHaveAttribute('aria-live', 'polite');
|
|
let previewRequests = 0;
|
|
page.on('request', (request) => {
|
|
if (request.url().includes('/imports/openapi/preview')) previewRequests += 1;
|
|
});
|
|
await page.locator('#openapi-import-preview').click();
|
|
expect(previewRequests).toBe(0);
|
|
|
|
const longFileName = `${'a'.repeat(130)}.yaml`;
|
|
await page.locator('#openapi-import-file').setInputFiles({
|
|
name: longFileName,
|
|
mimeType: 'application/yaml',
|
|
buffer: Buffer.from('openapi: 3.0.3'),
|
|
});
|
|
await expect(page.locator('#openapi-import-file-name')).toContainText('… · 14 B');
|
|
await page.evaluate(() => window.setLang('en'));
|
|
await expect(page.locator('#openapi-import-file-name')).toContainText('… · 14 B');
|
|
await expect(page.locator('#openapi-import-server-custom')).toHaveAttribute('aria-label', 'Custom Base URL');
|
|
await page.evaluate(() => window.setLang('ru'));
|
|
await expect(page.locator('#openapi-import-file-name')).toContainText('… · 14 B');
|
|
await expect(page.locator('#openapi-import-server-custom')).toHaveAttribute('aria-label', 'Свой Base URL');
|
|
|
|
const invalidFiles = [
|
|
{
|
|
file: { name: 'wrong-extension.txt', mimeType: 'application/yaml', buffer: Buffer.from('openapi: 3.0.3') },
|
|
message: localized('Choose one .yaml', 'Выберите один файл'),
|
|
},
|
|
{
|
|
file: { name: 'wrong-type.yaml', mimeType: 'text/plain', buffer: Buffer.from('openapi: 3.0.3') },
|
|
message: localized('matching type', 'подходящим типом'),
|
|
},
|
|
{
|
|
file: { name: 'empty.yaml', mimeType: 'application/yaml', buffer: Buffer.alloc(0) },
|
|
message: localized('file is empty', 'файл пуст'),
|
|
},
|
|
{
|
|
file: { name: 'too-large.yaml', mimeType: 'application/yaml', buffer: Buffer.alloc(256 * 1024 + 1, 'x') },
|
|
message: localized('larger than 256 KiB', 'больше 256 KiB'),
|
|
},
|
|
];
|
|
|
|
for (const invalid of invalidFiles) {
|
|
await page.locator('#openapi-import-file').setInputFiles(invalid.file);
|
|
await expect(page.locator('#openapi-import-status')).toContainText(invalid.message);
|
|
await expect(page.locator('#openapi-import-status')).toBeFocused();
|
|
await page.locator('#openapi-import-preview').click();
|
|
expect(previewRequests).toBe(0);
|
|
}
|
|
|
|
await page.keyboard.press('Escape');
|
|
await expect(trigger).toBeFocused();
|
|
});
|
|
|
|
test('OpenAPI upload recovers from pagehide and a preview server error', async ({ page }) => {
|
|
await login(page);
|
|
await dismissOnboardingIfOpen(page);
|
|
await page.getByRole('button', { name: localized('Import OpenAPI', 'Импорт OpenAPI') }).click();
|
|
|
|
let releasePreview;
|
|
const pendingPreview = new Promise((resolve) => {
|
|
releasePreview = resolve;
|
|
});
|
|
await page.route('**/imports/openapi/preview', async (route) => {
|
|
await pendingPreview;
|
|
try {
|
|
await route.fulfill({ status: 503, contentType: 'application/json', body: '{}' });
|
|
} catch (_error) {
|
|
// pagehide aborts the active request before the delayed route settles.
|
|
}
|
|
});
|
|
await page.locator('#openapi-import-file').setInputFiles({
|
|
name: 'resume.yaml',
|
|
mimeType: 'application/yaml',
|
|
buffer: Buffer.from('openapi: 3.0.3'),
|
|
});
|
|
await page.locator('#openapi-import-preview').click();
|
|
await expect(page.locator('#openapi-import-status')).toContainText(localized('Uploading and parsing', 'Загружаю и разбираю'));
|
|
|
|
await page.evaluate(() => window.dispatchEvent(new PageTransitionEvent('pagehide', { persisted: true })));
|
|
await expect(page.locator('#openapi-import-status')).toHaveText('');
|
|
await expect(page.locator('#openapi-import-cancel')).toBeHidden();
|
|
await page.evaluate(() => window.dispatchEvent(new PageTransitionEvent('pageshow', { persisted: true })));
|
|
await expect(page.locator('#openapi-import-retry')).toBeHidden();
|
|
await expect(page.locator('#openapi-import-file-name')).toContainText(localized('No file selected', 'Файл не выбран'));
|
|
await expect(page.locator('#openapi-import-status')).toContainText(localized('context changed', 'контекст изменился'));
|
|
releasePreview();
|
|
});
|
|
|
|
test('OpenAPI apply cancellation preserves the import job for an idempotent retry', async ({ page }) => {
|
|
await login(page);
|
|
await dismissOnboardingIfOpen(page);
|
|
await page.getByRole('button', { name: localized('Import OpenAPI', 'Импорт OpenAPI') }).click();
|
|
|
|
await page.route('**/imports/openapi/preview', async (route) => {
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
job_id: 'job_retry_same_authority',
|
|
preview: {
|
|
source: { servers: ['https://api.example.test'] },
|
|
findings: [],
|
|
groups: [{
|
|
title: 'retry',
|
|
operations: [{
|
|
key: 'get:/retry', method: 'GET', path: '/retry', suggested_name: 'preview_name',
|
|
suggested_display_name: 'Retry', input_fields: 0, output_fields: 0,
|
|
draft: { input_mapping: { rules: [] }, output_mapping: { rules: [] } }, findings: [],
|
|
}],
|
|
}],
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
|
|
const requests = [];
|
|
let releaseFirst;
|
|
const firstRequest = new Promise((resolve) => {
|
|
releaseFirst = resolve;
|
|
});
|
|
await page.route('**/imports/openapi/*/create', async (route) => {
|
|
requests.push({ url: route.request().url(), body: route.request().postData() });
|
|
if (requests.length === 1) await firstRequest;
|
|
if (requests.length === 3) {
|
|
await route.fulfill({
|
|
status: 409,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ error: { code: 'import_job.application_mismatch', message: 'conflict' } }),
|
|
});
|
|
return;
|
|
}
|
|
try {
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
created: [{
|
|
operation_key: 'get:/retry', name: 'renamed_after_conflict', operation_id: 'op_retry', version: 1,
|
|
}],
|
|
skipped: [], findings: [],
|
|
}),
|
|
});
|
|
} catch (_error) {
|
|
// The first request is deliberately aborted by the browser.
|
|
}
|
|
});
|
|
|
|
await page.locator('#openapi-import-file').setInputFiles({
|
|
name: 'retry.yaml', mimeType: 'application/yaml', buffer: Buffer.from('openapi: 3.0.3'),
|
|
});
|
|
await page.locator('#openapi-import-preview').click();
|
|
await expect(page.locator('#openapi-import-preview-panel')).toBeVisible();
|
|
await page.locator('#openapi-import-create').click();
|
|
await expect.poll(() => requests.length).toBe(1);
|
|
await page.locator('#openapi-import-cancel').click();
|
|
await expect(page.locator('#openapi-import-status')).toContainText(localized('may have completed', 'мог завершиться'));
|
|
await expect(page.locator('#openapi-import-retry')).toBeVisible();
|
|
await page.locator('[data-openapi-close]').last().click();
|
|
await page.reload();
|
|
await page.getByRole('button', { name: localized('Import OpenAPI', 'Импорт OpenAPI') }).click();
|
|
await expect(page.locator('#openapi-import-retry')).toBeVisible();
|
|
await page.locator('#openapi-import-retry').click();
|
|
await expect(page.locator('#openapi-import-result')).toContainText('GET /retry');
|
|
await expect.poll(() => requests.length).toBe(2);
|
|
// Let the cancelled first attempt answer after the retry. Its stale response
|
|
// must not overwrite or unlock the newer attempt's state.
|
|
releaseFirst();
|
|
await page.waitForTimeout(50);
|
|
await expect(page.locator('#openapi-import-result')).toContainText('GET /retry');
|
|
expect(requests[0].url).toContain('/imports/openapi/job_retry_same_authority/create');
|
|
expect(requests[1].url).toBe(requests[0].url);
|
|
expect(requests[1].body).toBe(requests[0].body);
|
|
|
|
// A received 4xx is a definitive non-commit outcome, unlike a transport
|
|
// failure. It must release the wizard instead of trapping it in replay mode.
|
|
await page.locator('#openapi-import-reset').click();
|
|
await page.locator('#openapi-import-file').setInputFiles({
|
|
name: 'definitive-4xx.yaml', mimeType: 'application/yaml', buffer: Buffer.from('openapi: 3.0.3'),
|
|
});
|
|
await page.locator('#openapi-import-preview').click();
|
|
await expect(page.locator('#openapi-import-preview-panel')).toBeVisible();
|
|
await page.locator('#openapi-import-create').click();
|
|
await expect.poll(() => requests.length).toBe(3);
|
|
await expect(page.locator('#openapi-import-file-select')).toBeEnabled();
|
|
await expect(page.locator('#openapi-import-reset')).toBeEnabled();
|
|
await expect(page.locator('#openapi-import-retry')).toBeVisible();
|
|
expect(await page.evaluate(() => sessionStorage.getItem('crank_openapi_apply_replay_v1'))).toBeNull();
|
|
});
|
|
|
|
test('OpenAPI apply preserves job authority after language or workspace changes', async ({ page }) => {
|
|
await login(page);
|
|
await dismissOnboardingIfOpen(page);
|
|
await page.getByRole('button', { name: localized('Import OpenAPI', 'Импорт OpenAPI') }).click();
|
|
|
|
await page.route('**/imports/openapi/preview', async (route) => {
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
job_id: 'job_active_apply',
|
|
preview: {
|
|
source: { servers: ['https://api.example.test'] },
|
|
findings: [],
|
|
groups: [{
|
|
title: 'active apply',
|
|
operations: [{
|
|
key: 'get:/active', method: 'GET', path: '/active', suggested_name: 'active',
|
|
suggested_display_name: 'Active', input_fields: 0, output_fields: 0,
|
|
draft: { input_mapping: { rules: [] }, output_mapping: { rules: [] } }, findings: [],
|
|
}],
|
|
}],
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
const releases = [];
|
|
const applyRequests = [];
|
|
await page.route('**/imports/openapi/*/create', async (route) => {
|
|
applyRequests.push({ url: route.request().url(), body: route.request().postData() });
|
|
await new Promise((resolve) => releases.push(resolve));
|
|
try {
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
created: [{ operation_key: 'get:/active', name: 'active', operation_id: 'op_active', version: 1 }],
|
|
skipped: [], findings: [],
|
|
}),
|
|
});
|
|
} catch (_error) {
|
|
// The invalidation aborts a request that must not update the modal afterwards.
|
|
}
|
|
});
|
|
|
|
async function beginApply(name) {
|
|
await page.locator('#openapi-import-file').setInputFiles({
|
|
name,
|
|
mimeType: 'application/yaml',
|
|
buffer: Buffer.from('openapi: 3.0.3'),
|
|
});
|
|
await page.locator('#openapi-import-preview').click();
|
|
await expect(page.locator('#openapi-import-preview-panel')).toBeVisible();
|
|
await page.locator('#openapi-import-server-custom').fill('https://api.example.test');
|
|
await page.locator('#openapi-import-create').click();
|
|
await expect(page.locator('#openapi-import-cancel')).toBeVisible();
|
|
await expect.poll(() => releases.length).toBeGreaterThan(0);
|
|
}
|
|
|
|
await beginApply('language-change.yaml');
|
|
// Exercise the actual UI language switcher path: it updates storage,
|
|
// re-renders translations and dispatches crank:langchange in one action.
|
|
await page.evaluate(() => window.setLang('en'));
|
|
await expect(page.locator('#openapi-import-status')).toContainText('may have completed');
|
|
await expect(page.locator('#openapi-import-retry')).toBeVisible();
|
|
await expect(page.locator('#openapi-import-file-select')).toBeDisabled();
|
|
releases.shift()();
|
|
await page.locator('#openapi-import-retry').click();
|
|
await expect.poll(() => releases.length).toBeGreaterThan(0);
|
|
releases.shift()();
|
|
await expect(page.locator('#openapi-import-result')).toContainText('GET /active');
|
|
expect(applyRequests[1].url).toBe(applyRequests[0].url);
|
|
expect(applyRequests[1].body).toBe(applyRequests[0].body);
|
|
|
|
await page.locator('#openapi-import-reset').click();
|
|
await beginApply('workspace-change.yaml');
|
|
const workspaceRequest = applyRequests.at(-1);
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new CustomEvent('crank:workspacechange', { detail: { id: 'workspace-after-apply' } }));
|
|
});
|
|
await expect(page.locator('#openapi-import-status')).toContainText('may have completed');
|
|
await expect(page.locator('#openapi-import-retry')).toBeVisible();
|
|
await expect(page.locator('#openapi-import-file-select')).toBeDisabled();
|
|
releases.shift()();
|
|
await page.locator('#openapi-import-retry').click();
|
|
await expect.poll(() => releases.length).toBeGreaterThan(0);
|
|
releases.shift()();
|
|
await expect(page.locator('#openapi-import-file-select')).toBeEnabled();
|
|
const workspaceReplay = applyRequests.at(-1);
|
|
expect(workspaceReplay.url).toBe(workspaceRequest.url);
|
|
expect(workspaceReplay.body).toBe(workspaceRequest.body);
|
|
});
|
|
|
|
test('OpenAPI upload only renders the latest selected file and clears reset or close races', async ({ page }) => {
|
|
await login(page);
|
|
await dismissOnboardingIfOpen(page);
|
|
await page.getByRole('button', { name: localized('Import OpenAPI', 'Импорт OpenAPI') }).click();
|
|
|
|
await page.route('**/imports/openapi/preview', async (route) => {
|
|
const isFirst = route.request().postData().includes('first.yaml');
|
|
await new Promise((resolve) => setTimeout(resolve, isFirst ? 180 : 10));
|
|
try {
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
job_id: isFirst ? 'job_first' : 'job_second',
|
|
preview: {
|
|
source: { servers: ['https://api.example.test'] },
|
|
findings: [],
|
|
groups: [{
|
|
title: isFirst ? 'first-only' : 'second-only',
|
|
operations: [{
|
|
key: isFirst ? 'get:/first' : 'get:/second',
|
|
method: 'GET',
|
|
path: isFirst ? '/first' : '/second',
|
|
suggested_name: isFirst ? 'first' : 'second',
|
|
suggested_display_name: isFirst ? 'First only' : 'Second only',
|
|
input_fields: 0,
|
|
output_fields: 0,
|
|
draft: { input_mapping: { rules: [] }, output_mapping: { rules: [] } },
|
|
findings: [],
|
|
}],
|
|
}],
|
|
},
|
|
}),
|
|
});
|
|
} catch (_error) {
|
|
// The browser intentionally aborts stale requests before this response settles.
|
|
}
|
|
});
|
|
|
|
await page.locator('#openapi-import-file').setInputFiles({
|
|
name: 'first.yaml',
|
|
mimeType: 'application/yaml',
|
|
buffer: Buffer.from('openapi: 3.0.3'),
|
|
});
|
|
await page.locator('#openapi-import-preview').click();
|
|
await expect(page.locator('#openapi-import-preview-panel')).toBeVisible();
|
|
await page.locator('#openapi-import-server-custom').fill('https://stale.example.test');
|
|
await page.locator('#openapi-import-file').setInputFiles({
|
|
name: 'second.yaml',
|
|
mimeType: 'application/yaml',
|
|
buffer: Buffer.from('openapi: 3.0.3'),
|
|
});
|
|
await expect(page.locator('#openapi-import-server-custom')).toHaveValue('');
|
|
await expect(page.locator('#openapi-import-server option')).toHaveCount(0);
|
|
await page.locator('#openapi-import-preview').click();
|
|
await expect(page.locator('.openapi-import-group')).toContainText('second-only');
|
|
await page.waitForTimeout(220);
|
|
await expect(page.locator('.openapi-import-group')).not.toContainText('first-only');
|
|
|
|
await page.locator('#openapi-import-file').setInputFiles({
|
|
name: 'third.yaml',
|
|
mimeType: 'application/yaml',
|
|
buffer: Buffer.from('openapi: 3.0.3'),
|
|
});
|
|
await page.locator('#openapi-import-preview').click();
|
|
await page.locator('#openapi-import-reset').click();
|
|
await page.waitForTimeout(220);
|
|
await expect(page.locator('#openapi-import-preview-panel')).toBeHidden();
|
|
await expect(page.locator('#openapi-import-file-name')).toContainText(localized('No file selected', 'Файл не выбран'));
|
|
|
|
await page.locator('#openapi-import-file').setInputFiles({
|
|
name: 'fourth.yaml',
|
|
mimeType: 'application/yaml',
|
|
buffer: Buffer.from('openapi: 3.0.3'),
|
|
});
|
|
await page.locator('#openapi-import-preview').click();
|
|
await page.keyboard.press('Escape');
|
|
await page.waitForTimeout(220);
|
|
await expect(page.locator('#openapi-import-modal')).toBeHidden();
|
|
});
|
|
|
|
test('OpenAPI upload ignores a stale failure and renders only correlation identifiers', async ({ page }) => {
|
|
await login(page);
|
|
await dismissOnboardingIfOpen(page);
|
|
await page.getByRole('button', { name: localized('Import OpenAPI', 'Импорт OpenAPI') }).click();
|
|
const sourceCanary = 'do-not-render-openapi-source-canary';
|
|
await page.locator('#openapi-import-file').setInputFiles({
|
|
name: 'safe.yaml',
|
|
mimeType: 'application/yaml',
|
|
buffer: Buffer.from(`openapi: 3.0.3\ninfo: { title: Safe, description: ${sourceCanary} }\npaths: { /health: { get: { responses: { '200': { description: OK } } } } }\n`),
|
|
});
|
|
let releaseStaleFailure;
|
|
const staleFailure = new Promise((resolve) => {
|
|
releaseStaleFailure = resolve;
|
|
});
|
|
await page.route('**/imports/openapi/preview', async (route) => {
|
|
await staleFailure;
|
|
await route.fulfill({
|
|
status: 400,
|
|
contentType: 'application/json',
|
|
headers: {
|
|
'x-request-id': 'req_ui_evidence_1',
|
|
'x-trace-id': '0123456789abcdef0123456789abcdef',
|
|
},
|
|
body: JSON.stringify({ error: { code: 'openapi_upload.invalid_document', message: 'Safe upload failure' } }),
|
|
});
|
|
});
|
|
await page.locator('#openapi-import-preview').click();
|
|
await expect(page.locator('#openapi-import-cancel')).toBeVisible();
|
|
await page.locator('#openapi-import-cancel').click({ force: true });
|
|
releaseStaleFailure();
|
|
await page.waitForTimeout(100);
|
|
await expect(page.locator('#openapi-import-status')).toContainText(localized('Request cancelled', 'Запрос отменён'));
|
|
await expect(page.locator('body')).not.toContainText(sourceCanary);
|
|
|
|
await page.unroute('**/imports/openapi/preview');
|
|
await page.route('**/imports/openapi/preview', async (route) => {
|
|
await route.fulfill({
|
|
status: 400,
|
|
contentType: 'application/json',
|
|
headers: {
|
|
'x-request-id': 'req_ui_evidence_2',
|
|
'x-trace-id': 'abcdef0123456789abcdef0123456789',
|
|
},
|
|
body: JSON.stringify({ error: { code: 'openapi_upload.invalid_document', message: 'Safe upload failure' } }),
|
|
});
|
|
});
|
|
await page.locator('#openapi-import-retry').click();
|
|
await expect(page.locator('#openapi-import-status')).toContainText(localized('not a valid OpenAPI document', 'не является корректным документом OpenAPI'));
|
|
await expect(page.locator('#openapi-import-status')).toBeFocused();
|
|
await expect(page.locator('#openapi-import-status')).toContainText('Request ID: req_ui_evidence_2');
|
|
await expect(page.locator('#openapi-import-status')).toContainText('Trace ID: abcdef0123456789abcdef0123456789');
|
|
|
|
await page.evaluate(() => window.setLang('en'));
|
|
await expect(page.locator('#openapi-import-title')).toHaveText('Import OpenAPI');
|
|
|
|
await page.unroute('**/imports/openapi/preview');
|
|
await page.route('**/imports/openapi/preview', async (route) => {
|
|
await route.fulfill({
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
job_id: 'job_ui_evidence',
|
|
preview: {
|
|
source: { servers: ['https://api.example.test'] },
|
|
findings: [],
|
|
groups: [{
|
|
title: 'evidence',
|
|
operations: [{
|
|
key: 'get:/health', method: 'GET', path: '/health', suggested_name: 'health',
|
|
suggested_display_name: 'Health', input_fields: 0, output_fields: 0,
|
|
draft: { input_mapping: { rules: [] }, output_mapping: { rules: [] } }, findings: [],
|
|
}],
|
|
}],
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
await page.locator('#openapi-import-retry').click();
|
|
await expect(page.locator('#openapi-import-preview-panel')).toBeVisible();
|
|
let createRequests = 0;
|
|
await page.route('**/imports/openapi/*/create', async (route) => {
|
|
createRequests += 1;
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
await route.fulfill({ contentType: 'application/json', body: JSON.stringify({ created: [], skipped: [], findings: [] }) });
|
|
});
|
|
await page.locator('#openapi-import-create').click();
|
|
await page.locator('#openapi-import-create').evaluate((button) => button.click());
|
|
await expect(page.locator('#openapi-import-status')).toContainText('Created: 0; skipped: 0.');
|
|
expect(createRequests).toBe(1);
|
|
});
|