Files
crank/apps/ui/tests/e2e/wizard.spec.js
T
github-ops 0af60b1693
CI / Rust Checks (push) Failing after 2m6s
CI / UI Checks (push) Has been skipped
CI / Frontend E2E (push) Has been skipped
CI / Deployment Manifests (push) Has been skipped
Deploy / deploy (push) Successful in 2m26s
Remove enterprise leftovers from community
2026-06-20 12:04:46 +00:00

587 lines
21 KiB
JavaScript

const { test, expect } = require('@playwright/test');
const { getCurrentWorkspace, login, localized } = require('./helpers');
test('wizard loads and protocol selection updates flow', async ({ page }) => {
await login(page);
await page.goto('/wizard/');
await expect(page.locator('[data-step-counter]').first()).toContainText(localized('Step', 'Шаг'));
await expect(page.locator('.step-pane:visible')).toHaveCount(1);
await expect(page.locator('#step-panel-1')).toBeVisible();
await expect(page.locator('#step-panel-1 .step-panel-title')).toContainText(localized('Choose a protocol', 'Выберите протокол'));
for (const selector of ['.btn-back', '.btn-save-draft']) {
const box = await page.locator(selector).boundingBox();
const viewport = page.viewportSize();
expect(box).not.toBeNull();
expect(box.y).toBeGreaterThanOrEqual(0);
expect(box.y + box.height).toBeLessThanOrEqual(viewport.height);
}
await page.locator('[data-testid="wizard-protocol-rest"]').click();
await page.locator('#btn-continue').click();
await expect(page.locator('#step-panel-2 .step-panel-title')).toContainText(localized('Select upstream', 'Выберите upstream'));
});
test('community wizard exposes REST-only protocol flow', async ({ page }) => {
await login(page);
await page.goto('/wizard/');
await expect(page.locator('[data-testid="wizard-protocol-rest"]')).toBeVisible();
await page.locator('[data-testid="wizard-protocol-rest"]').click();
await page.locator('#btn-continue').click();
await page.locator('#btn-continue').click();
await page.locator('#btn-continue').click();
await page.locator('#btn-continue').click();
await expect(page.locator('#wizard-streaming-config-card')).toBeHidden();
await expect(page.locator('#wizard-streaming-capability-note')).toHaveCount(0);
});
test('REST method callout clears when switching to methods without request body', async ({ page }) => {
await login(page);
const workspace = await getCurrentWorkspace(page);
await page.route(
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/upstreams`,
async (route) => {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
items: [
{
id: 'upstream_e2e_rest_methods',
workspace_id: workspace.id,
name: 'E2E REST target',
base_url: 'https://api.example.test',
static_headers: {},
auth_profile_id: null,
created_at: '2026-06-19T00:00:00Z',
updated_at: '2026-06-19T00:00:00Z',
},
],
}),
});
},
);
await page.goto('/wizard/');
await expect(page.locator('[data-step-counter]').first()).toContainText(localized('Step', 'Шаг'));
await page.locator('[data-testid="wizard-protocol-rest"]').click();
await page.locator('#btn-continue').click();
await expect(page.locator('#step-panel-2')).toBeVisible();
await page.locator('#upstream-combobox-trigger').click();
await page.getByText('E2E REST target').click();
await page.locator('#btn-continue').click();
await expect(page.locator('#step-panel-3-rest')).toBeVisible();
const callout = page.locator('#method-callout-rest');
await page.locator('.method-card[data-method="POST"]').click();
await expect(callout).toBeVisible();
await expect(callout).toContainText(/POST|Выбран POST/);
await page.locator('.method-card[data-method="PATCH"]').click();
await expect(callout).toBeVisible();
await expect(callout).toContainText(/PATCH|Выбран PATCH/);
await page.locator('.method-card[data-method="DELETE"]').click();
await expect(callout).toBeHidden();
await expect(callout).toHaveText('');
await page.locator('.method-card[data-method="GET"]').click();
await expect(callout).toBeHidden();
await expect(callout).toHaveText('');
});
test('wizard loads reusable upstreams from backend', async ({ page }) => {
await login(page);
const workspace = await getCurrentWorkspace(page);
await page.route(
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/upstreams`,
async (route) => {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
items: [
{
id: 'upstream_frankfurter',
workspace_id: workspace.id,
name: 'Frankfurter',
base_url: 'https://api.frankfurter.app',
static_headers: { Accept: 'application/json' },
auth_profile_id: null,
created_at: '2026-06-19T00:00:00Z',
updated_at: '2026-06-19T00:00:00Z',
},
],
}),
});
},
);
await page.goto('/wizard/');
await page.locator('[data-testid="wizard-protocol-rest"]').click();
await page.locator('.btn-continue').click();
await expect(page.locator('#step-panel-2')).toBeVisible();
await page.locator('#upstream-combobox-trigger').click();
await expect(page.locator('#upstream-dropdown-list')).toContainText('Frankfurter');
await expect(page.locator('#upstream-dropdown-list')).toContainText('https://api.frankfurter.app');
await expect(page.locator('#upstream-dropdown-list')).not.toContainText('Open Meteo');
});
test('wizard edit mode hydrates fields from operation version snapshot', async ({ page }) => {
await login(page);
const workspace = await getCurrentWorkspace(page);
const operationId = 'frankfurter_monthly_rates';
const version = 7;
await page.route(
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/operations/${operationId}`,
async (route) => {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
id: operationId,
workspace_id: workspace.id,
name: operationId,
display_name: 'Frankfurter monthly rates',
category: 'finance',
protocol: 'rest',
security_level: 'standard',
status: 'draft',
current_draft_version: version,
latest_published_version: null,
created_at: '2026-06-19T00:00:00Z',
updated_at: '2026-06-19T00:00:00Z',
published_at: null,
draft_version_ref: { version, status: 'draft' },
published_version_ref: null,
agent_refs: [],
}),
});
},
);
await page.route(
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/operations/${operationId}/versions/${version}`,
async (route) => {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
operation_id: operationId,
workspace_id: workspace.id,
version,
status: 'draft',
change_note: null,
created_at: '2026-06-19T00:00:00Z',
created_by: null,
snapshot: {
id: operationId,
name: operationId,
display_name: 'Frankfurter monthly rates',
category: 'finance',
protocol: 'rest',
security_level: 'standard',
status: 'draft',
version,
target: {
kind: 'rest',
base_url: 'https://api.frankfurter.app',
method: 'GET',
path_template: '/2024-01..2024-03',
static_headers: {
Accept: 'application/json',
'X-Demo': 'frankfurter',
},
},
input_schema: {
type: 'object',
description: null,
required: false,
nullable: false,
default_value: null,
fields: {
from: {
type: 'string',
description: 'Base currency',
required: true,
nullable: false,
default_value: null,
fields: {},
items: null,
enum_values: [],
variants: [],
},
to: {
type: 'string',
description: 'Quote currency',
required: false,
nullable: false,
default_value: 'EUR',
fields: {},
items: null,
enum_values: [],
variants: [],
},
},
items: null,
enum_values: [],
variants: [],
},
output_schema: {
type: 'object',
description: null,
required: false,
nullable: false,
default_value: null,
fields: {
rates: {
type: 'object',
description: 'Rates by date',
required: true,
nullable: false,
default_value: null,
fields: {},
items: null,
enum_values: [],
variants: [],
},
},
items: null,
enum_values: [],
variants: [],
},
input_mapping: {
rules: [
{ source: '$.mcp.from', target: '$.request.query.from' },
],
},
output_mapping: {
rules: [
{ source: '$.response.body.rates', target: '$.output.rates' },
],
},
execution_config: {
timeout_ms: 9000,
retry_policy: { max_attempts: 2 },
auth_profile_ref: null,
headers: {},
protocol_options: null,
streaming: null,
},
tool_description: {
title: 'Получить историю курсов за месяц',
description: 'Возвращает историю курсов валют Frankfurter за заданный период.',
tags: [],
examples: [
{
input: {
from: 'USD',
to: 'EUR',
},
},
],
},
wizard_state: {
input_sample: {
from: 'USD',
to: 'EUR',
},
output_sample: {
rates: {
'2024-01': {
EUR: 0.91,
},
},
},
test_input: {
from: 'GBP',
to: 'USD',
},
},
samples: [],
generated_draft: null,
config_export: null,
},
}),
});
},
);
await page.goto(`/wizard/?mode=edit&operationId=${operationId}`);
await expect(page.locator('#tool-name')).toHaveValue(operationId);
await expect(page.locator('#tool-display-name')).toHaveValue('Frankfurter monthly rates');
await expect(page.locator('#new-upstream-url')).toHaveValue('https://api.frankfurter.app');
await expect(page.locator('#new-upstream-static-headers')).toHaveValue(/"Accept": "application\/json"/);
await expect(page.locator('#new-upstream-static-headers')).toHaveValue(/"X-Demo": "frankfurter"/);
await expect(page.locator('#endpoint-path')).toHaveValue('/2024-01..2024-03');
await expect(page.locator('.method-card.active')).toHaveAttribute('data-method', 'GET');
await expect(page.locator('#tool-title')).toHaveValue('Получить историю курсов за месяц');
await expect(page.locator('#tool-description')).toHaveValue('Возвращает историю курсов валют Frankfurter за заданный период.');
await expect(page.locator('#tool-input-schema')).toHaveValue(/"from"/);
await expect(page.locator('#tool-input-schema')).toHaveValue(/"to"/);
await expect(page.locator('#tool-input-schema')).not.toHaveValue(/first_name/);
await expect(page.locator('#tool-output-schema')).toHaveValue(/"rates"/);
await expect(page.locator('#tool-input-mapping')).toHaveValue(/query\.from/);
await expect(page.locator('#tool-output-mapping')).toHaveValue(/rates: \$\.response\.body\.rates/);
await expect(page.locator('#tool-exec-config')).toHaveValue(/9000/);
await expect(page.locator('#tool-exec-config')).toHaveValue(/max_attempts: 2/);
await expect(page.locator('#wizard-input-sample')).toHaveValue(/"from": "USD"/);
await expect(page.locator('#wizard-input-sample')).toHaveValue(/"to": "EUR"/);
await expect(page.locator('#wizard-input-sample')).not.toHaveValue(/Ada/);
await expect(page.locator('#wizard-output-sample')).toHaveValue(/"rates":/);
await expect(page.locator('#wizard-output-sample')).toHaveValue(/0\.91/);
await expect(page.locator('#wizard-test-input')).toHaveValue(/"from": "GBP"/);
await expect(page.locator('#wizard-test-input')).toHaveValue(/"to": "USD"/);
await expect(page.locator('#wizard-test-input')).not.toHaveValue(/Ada/);
});
test('wizard edit mode preserves explicit request mapping targets on save', async ({ page }) => {
await login(page);
const workspace = await getCurrentWorkspace(page);
const operationId = 'frankfurter_latest_rate';
const version = 3;
let updatePayload = null;
await page.route(
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/operations/${operationId}`,
async (route) => {
if (route.request().method() === 'PATCH') {
updatePayload = route.request().postDataJSON();
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
operation_id: operationId,
workspace_id: workspace.id,
version,
status: 'draft',
updated_at: '2026-06-19T00:00:00Z',
}),
});
return;
}
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
id: operationId,
workspace_id: workspace.id,
name: operationId,
display_name: 'Frankfurter latest rate',
category: 'finance',
protocol: 'rest',
security_level: 'standard',
status: 'draft',
current_draft_version: version,
latest_published_version: null,
created_at: '2026-06-19T00:00:00Z',
updated_at: '2026-06-19T00:00:00Z',
published_at: null,
draft_version_ref: { version, status: 'draft' },
published_version_ref: null,
agent_refs: [],
}),
});
},
);
await page.route(
`/api/admin/workspaces/${encodeURIComponent(workspace.id)}/operations/${operationId}/versions/${version}`,
async (route) => {
await route.fulfill({
contentType: 'application/json',
body: JSON.stringify({
operation_id: operationId,
workspace_id: workspace.id,
version,
status: 'draft',
change_note: null,
created_at: '2026-06-19T00:00:00Z',
created_by: null,
snapshot: {
id: operationId,
name: operationId,
display_name: 'Frankfurter latest rate',
category: 'finance',
protocol: 'rest',
security_level: 'standard',
status: 'draft',
version,
target: {
kind: 'rest',
base_url: 'https://api.frankfurter.app',
method: 'GET',
path_template: '/latest',
static_headers: {
Accept: 'application/json',
},
},
input_schema: {
type: 'object',
description: null,
required: false,
nullable: false,
default_value: null,
fields: {
base: {
type: 'string',
description: null,
required: true,
nullable: false,
default_value: null,
fields: {},
items: null,
enum_values: [],
variants: [],
},
symbols: {
type: 'string',
description: 'Quote symbols',
required: false,
nullable: false,
default_value: 'EUR',
fields: {},
items: null,
enum_values: [],
variants: [],
},
},
items: null,
enum_values: [],
variants: [],
},
output_schema: {
type: 'object',
description: null,
required: false,
nullable: false,
default_value: null,
fields: {},
items: null,
enum_values: [],
variants: [],
},
input_mapping: {
rules: [
{ source: '$.mcp.base', target: '$.request.query.base', required: true },
{ source: '$.mcp.symbols', target: '$.request.query.symbols', required: true },
{ source: '$.mcp.date', target: '$.request.path.date', required: true },
{
source: '$.mcp.group',
target: '$.request.query.group',
required: false,
default_value: 'month',
},
],
},
output_mapping: {
rules: [
{ source: '$.response.body.base', target: '$.output.base', required: true },
],
},
execution_config: {
timeout_ms: 9000,
retry_policy: null,
auth_profile_ref: null,
headers: {},
protocol_options: null,
streaming: null,
},
tool_description: {
title: 'Получить последний курс',
description: 'Возвращает последний курс.',
tags: ['finance'],
examples: [{ input: { base: 'USD', symbols: 'EUR' } }],
},
wizard_state: {
input_sample: {
base: 'USD',
symbols: 'EUR',
},
output_sample: {
base: 'USD',
rates: {
EUR: 0.91,
},
},
test_input: {
base: 'CHF',
symbols: 'JPY',
},
},
samples: [],
generated_draft: null,
config_export: null,
},
}),
});
},
);
await page.goto(`/wizard/?mode=edit&operationId=${operationId}`);
await expect(page.locator('#tool-input-mapping')).toHaveValue(/query\.base/);
await expect(page.locator('#tool-input-mapping')).toHaveValue(/path\.date/);
await page.locator('.btn-save-draft').click();
await expect.poll(() => updatePayload).not.toBeNull();
expect(updatePayload.display_name).toBe('Frankfurter latest rate');
expect(updatePayload.target).toEqual({
kind: 'rest',
base_url: 'https://api.frankfurter.app',
method: 'GET',
path_template: '/latest',
static_headers: {
Accept: 'application/json',
},
});
expect(updatePayload.input_schema.fields).toHaveProperty('base');
expect(updatePayload.input_schema.fields).toHaveProperty('symbols');
expect(updatePayload.output_mapping.rules).toEqual([
{ source: '$.response.body.base', target: '$.output.base', required: true },
]);
expect(updatePayload.execution_config).toEqual({
timeout_ms: 9000,
retry_policy: null,
auth_profile_ref: null,
headers: {},
protocol_options: null,
streaming: null,
});
expect(updatePayload.tool_description).toEqual({
title: 'Получить последний курс',
description: 'Возвращает последний курс.',
tags: ['finance'],
examples: [{ input: { base: 'USD', symbols: 'EUR' } }],
});
expect(updatePayload.wizard_state).toEqual({
input_sample: {
base: 'USD',
symbols: 'EUR',
},
output_sample: {
base: 'USD',
rates: {
EUR: 0.91,
},
},
test_input: {
base: 'CHF',
symbols: 'JPY',
},
});
expect(updatePayload.input_mapping.rules).toEqual([
{ source: '$.mcp.base', target: '$.request.query.base', required: true },
{ source: '$.mcp.symbols', target: '$.request.query.symbols', required: true },
{ source: '$.mcp.date', target: '$.request.path.date', required: true },
{
source: '$.mcp.group',
target: '$.request.query.group',
required: false,
default_value: 'month',
},
]);
});