ui: polish websocket wizard test runs

This commit is contained in:
a.tolmachev
2026-05-02 20:30:30 +00:00
parent fa2a98765d
commit 420e9416e5
5 changed files with 284 additions and 8 deletions
+69
View File
@@ -213,6 +213,74 @@ function buildRestWindowOperationPayload(name) {
};
}
function buildWebsocketWindowOperationPayload(name, overrides = {}) {
const streaming = Object.assign({
mode: 'window',
transport_behavior: 'server_stream',
window_duration_ms: 1000,
upstream_timeout_ms: 1000,
max_items: 3,
max_bytes: 16384,
aggregation_mode: 'summary_plus_samples',
items_path: '$.items',
done_path: '$.done',
redacted_paths: [],
truncate_item_fields: false,
drop_duplicates: false,
tool_family: {},
}, overrides.streaming || {});
const websocketOptions = Object.assign({
heartbeat_interval_ms: 250,
reconnect_max_attempts: 1,
reconnect_backoff_ms: 10,
}, overrides.websocketOptions || {});
return {
name,
display_name: 'Playwright WebSocket Window',
category: 'streaming',
protocol: 'websocket',
target: {
kind: 'websocket',
url: `ws://127.0.0.1:${STREAM_FIXTURE_PORT}/events`,
subprotocols: [],
subscribe_message_template: null,
unsubscribe_message_template: null,
static_headers: {},
},
input_schema: schemaObject({
topic: schemaString(false),
}),
output_schema: schemaObject({}),
input_mapping: {
rules: [
{
source: '$.mcp.topic',
target: '$.request.body.topic',
required: false,
default_value: 'telemetry',
},
],
},
output_mapping: { rules: [] },
execution_config: {
timeout_ms: 1000,
headers: {},
protocol_options: {
websocket: websocketOptions,
},
streaming,
},
tool_description: {
title: 'Playwright WebSocket Window',
description: 'Collects a bounded WebSocket event window from the local fixture.',
tags: ['playwright', 'streaming', 'websocket', 'window'],
examples: [{ input: {} }],
},
};
}
const SOAP_TEST_WSDL = `<?xml version="1.0"?>
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
@@ -558,6 +626,7 @@ module.exports = {
buildRestAsyncJobOperationPayload,
buildRestWindowOperationPayload,
buildSoapOperationPayload,
buildWebsocketWindowOperationPayload,
createAgent,
createOperation,
createPlatformApiKey,
+122
View File
@@ -2,6 +2,7 @@ const { test, expect } = require('@playwright/test');
const {
SOAP_TEST_WSDL,
buildSoapOperationPayload,
buildWebsocketWindowOperationPayload,
createOperation,
getCurrentWorkspace,
login,
@@ -96,3 +97,124 @@ test('soap wizard uploads wsdl, applies discovered binding and runs test', async
await expect(page.locator('#wizard-test-response-preview')).toHaveValue(/lead_soap_123/);
await expect(page.locator('#wizard-test-errors')).toHaveValue('[]');
});
test('websocket wizard reports bounded window success and reconnect exhaustion clearly', async ({ page }) => {
await login(page);
const workspace = await getCurrentWorkspace(page);
const successOperation = await createOperation(
page,
workspace.id,
buildWebsocketWindowOperationPayload(uniqueName('playwright_websocket_success')),
);
const failureOperation = await createOperation(
page,
workspace.id,
buildWebsocketWindowOperationPayload(uniqueName('playwright_websocket_failure'), {
streaming: {
max_items: 5,
},
websocketOptions: {
reconnect_max_attempts: 0,
reconnect_backoff_ms: 0,
},
}),
);
await page.goto(`/wizard/?mode=edit&operationId=${encodeURIComponent(successOperation.operation_id)}`);
await page.locator('#btn-continue').click();
await page.locator('#upstream-new-trigger').click();
await page.locator('#new-upstream-name').fill('fixture-websocket');
await page.locator('#new-upstream-url').fill('ws://127.0.0.1:3310');
await page.locator('#btn-continue').click();
await expect(page.locator('#step-panel-3-websocket .step-panel-title')).toContainText(/WebSocket/i);
await page.locator('#websocket-path').fill('/events');
await page.locator('#websocket-subprotocols').fill('');
await page.locator('#websocket-subscribe-template').fill('');
await page.locator('#websocket-unsubscribe-template').fill('');
await page.locator('#btn-continue').click();
await page.locator('#btn-continue').click();
await page.locator('#wizard-test-input').fill('{}');
await page.evaluate(() => {
const original = window.CrankApi.runOperationTest;
window.__wizardRunTestOriginal = original;
window.CrankApi.runOperationTest = async () => ({
ok: true,
mode: 'window',
request_preview: {
body: { topic: 'telemetry' },
},
response_preview: {
items: [
{ seq: 1, value: 101 },
{ seq: 2, value: 102 },
{ seq: 3, value: 103 },
],
window_complete: true,
truncated: false,
has_more: false,
},
errors: [],
window: {
window_complete: true,
truncated: false,
has_more: false,
cursor: null,
},
});
});
await page.locator('#wizard-run-test').click();
await expect(page.locator('#wizard-live-status-title')).toContainText(localized('WebSocket window test completed', 'WebSocket оконный тест завершен'));
await expect(page.locator('#wizard-live-status-text')).toContainText(localized('bounded test window', 'тестового окна'));
await expect(page.locator('#wizard-test-response-preview')).toHaveValue(/"seq": 1/);
await expect(page.locator('#wizard-test-request-preview')).toHaveValue(/"topic": "telemetry"/);
await expect(page.locator('#wizard-test-errors')).toHaveValue('[]');
await page.evaluate(() => {
if (window.__wizardRunTestOriginal) {
window.CrankApi.runOperationTest = window.__wizardRunTestOriginal;
delete window.__wizardRunTestOriginal;
}
});
await page.goto(`/wizard/?mode=edit&operationId=${encodeURIComponent(failureOperation.operation_id)}`);
await page.locator('#btn-continue').click();
await page.locator('#upstream-new-trigger').click();
await page.locator('#new-upstream-name').fill('fixture-websocket');
await page.locator('#new-upstream-url').fill('ws://127.0.0.1:3310');
await page.locator('#btn-continue').click();
await page.locator('#websocket-path').fill('/events');
await page.locator('#websocket-subprotocols').fill('');
await page.locator('#websocket-subscribe-template').fill('');
await page.locator('#websocket-unsubscribe-template').fill('');
await page.locator('#btn-continue').click();
await page.locator('#btn-continue').click();
await page.locator('#wizard-test-input').fill('{}');
await page.evaluate(() => {
const original = window.CrankApi.runOperationTest;
window.__wizardRunTestOriginal = original;
window.CrankApi.runOperationTest = async () => ({
ok: false,
mode: 'window',
request_preview: {
body: { topic: 'telemetry' },
},
response_preview: null,
errors: [
{
code: 'runtime_websocket_error',
message: 'websocket reconnect policy exhausted',
},
],
window: null,
});
});
await page.locator('#wizard-run-test').click();
await expect(page.locator('#wizard-live-status-title')).toContainText(localized('WebSocket test returned errors', 'WebSocket тест завершился с ошибками'));
await expect(page.locator('#wizard-live-status-text')).toContainText(localized('reconnect policy was exhausted', 'reconnect policy была исчерпана'));
await expect(page.locator('#wizard-test-errors')).toHaveValue(/runtime_websocket_error/);
await page.evaluate(() => {
if (window.__wizardRunTestOriginal) {
window.CrankApi.runOperationTest = window.__wizardRunTestOriginal;
delete window.__wizardRunTestOriginal;
}
});
});