2026-05-19 12:14:11 +03:00
|
|
|
|
/**
|
2026-05-19 16:40:44 +03:00
|
|
|
|
* Фикстурный тест manage-project.js — против локального HTTP-сервера с Element UI фикстурой.
|
2026-05-19 12:14:11 +03:00
|
|
|
|
*
|
2026-05-19 16:40:44 +03:00
|
|
|
|
* Почему HTTP, не file://: manage-project.js перехватывает ответ page.waitForResponse()
|
|
|
|
|
|
* с URL endsWith('/admin/visit/rt-project-save'). Браузер не шлёт network-запросы при
|
|
|
|
|
|
* file://-origin fetch из-за CORS/same-origin ограничений в Chromium.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Runner: встроенный node:test (Node 18+). Запуск: `node --test manage-project.test.js`.
|
2026-05-19 12:14:11 +03:00
|
|
|
|
*/
|
|
|
|
|
|
const { test } = require('node:test');
|
|
|
|
|
|
const assert = require('node:assert');
|
|
|
|
|
|
const { execFile } = require('node:child_process');
|
2026-05-19 16:40:44 +03:00
|
|
|
|
const http = require('node:http');
|
|
|
|
|
|
const fs = require('node:fs');
|
2026-05-19 12:14:11 +03:00
|
|
|
|
const path = require('node:path');
|
|
|
|
|
|
|
|
|
|
|
|
const SCRIPT = path.resolve(__dirname, 'manage-project.js');
|
2026-05-19 16:40:44 +03:00
|
|
|
|
const FIXTURE_PATH = path.resolve(__dirname, 'fixtures', 'rt-form-element-ui.html');
|
2026-05-19 12:14:11 +03:00
|
|
|
|
|
2026-05-19 16:40:44 +03:00
|
|
|
|
/** Запустить ephemeral HTTP-сервер, отдающий фикстуру и обрабатывающий mock-эндпоинты. */
|
|
|
|
|
|
function startFixtureServer() {
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
const html = fs.readFileSync(FIXTURE_PATH, 'utf8');
|
|
|
|
|
|
const server = http.createServer((req, res) => {
|
|
|
|
|
|
// Mock rt-project-save — Playwright перехватывает реальный сетевой запрос
|
|
|
|
|
|
if (req.url && req.url.includes('rt-project-save') && req.method === 'POST') {
|
|
|
|
|
|
// Consume request body (important — don't hang connection)
|
|
|
|
|
|
let body = '';
|
|
|
|
|
|
req.on('data', (c) => { body += c; });
|
|
|
|
|
|
req.on('end', () => {
|
|
|
|
|
|
const payload = JSON.stringify({ status: 'OK', message: '', result: null, id: '99001' });
|
|
|
|
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
|
|
|
|
res.end(payload);
|
|
|
|
|
|
});
|
|
|
|
|
|
return;
|
2026-05-19 12:14:11 +03:00
|
|
|
|
}
|
2026-05-19 16:40:44 +03:00
|
|
|
|
// Default: serve fixture HTML
|
|
|
|
|
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
|
|
|
|
res.end(html);
|
2026-05-19 12:14:11 +03:00
|
|
|
|
});
|
2026-05-19 16:40:44 +03:00
|
|
|
|
server.listen(0, '127.0.0.1', () => resolve(server));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Спавнить manage-project.js, подать JSON на stdin, вернуть {code, stdout, stderr}. */
|
2026-06-22 08:49:19 +03:00
|
|
|
|
function runScript(input, extraEnv) {
|
2026-05-19 16:40:44 +03:00
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
|
const child = execFile(
|
|
|
|
|
|
'node',
|
|
|
|
|
|
[SCRIPT],
|
2026-06-22 08:49:19 +03:00
|
|
|
|
{ timeout: 90_000, env: { ...process.env, ...extraEnv } },
|
2026-05-19 16:40:44 +03:00
|
|
|
|
(err, stdout, stderr) => {
|
|
|
|
|
|
if (err && err.killed) return reject(new Error('Process killed / timed out'));
|
|
|
|
|
|
// err.code — exit code; treat as expected (tests assert on code)
|
|
|
|
|
|
resolve({
|
|
|
|
|
|
code: err ? err.code : 0,
|
|
|
|
|
|
stdout: stdout.toString(),
|
|
|
|
|
|
stderr: stderr.toString(),
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
2026-05-19 12:14:11 +03:00
|
|
|
|
child.stdin.write(JSON.stringify(input));
|
|
|
|
|
|
child.stdin.end();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-19 16:40:44 +03:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Test 1 — createProject через Element UI фикстуру → external_id из mock-response
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
test('createProject fills Element UI form and returns external_id from intercept response', async () => {
|
|
|
|
|
|
const server = await startFixtureServer();
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { port } = server.address();
|
|
|
|
|
|
const url = `http://127.0.0.1:${port}`;
|
|
|
|
|
|
|
|
|
|
|
|
const result = await runScript({
|
|
|
|
|
|
operation: 'create',
|
|
|
|
|
|
url,
|
|
|
|
|
|
skipLogin: true,
|
|
|
|
|
|
dto: {
|
|
|
|
|
|
tag: '_lidpotok',
|
|
|
|
|
|
name: 'example.com',
|
|
|
|
|
|
platforms: ['B1'],
|
|
|
|
|
|
signal_type: 'site',
|
|
|
|
|
|
limit: 5,
|
|
|
|
|
|
workdays: [1, 2, 3, 4, 5],
|
|
|
|
|
|
domains: ['example.com'],
|
|
|
|
|
|
region_mode: 'include',
|
|
|
|
|
|
regions: [],
|
|
|
|
|
|
active: true,
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
assert.strictEqual(result.code, 0, `Expected exit 0, got ${result.code}. stderr: ${result.stderr}`);
|
2026-05-19 12:14:11 +03:00
|
|
|
|
|
2026-05-19 16:40:44 +03:00
|
|
|
|
let out;
|
|
|
|
|
|
try {
|
|
|
|
|
|
out = JSON.parse(result.stdout);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
assert.fail(`stdout is not valid JSON: ${result.stdout}\nstderr: ${result.stderr}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
assert.strictEqual(out.external_id, '99001', `expected external_id "99001", got ${JSON.stringify(out)}`);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
server.close();
|
|
|
|
|
|
}
|
2026-05-19 12:14:11 +03:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-19 16:40:44 +03:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Test 2 — listProjects в skipLogin-режиме возвращает массив projects
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
test('listProjects returns array (skipLogin mode, fixture page)', async () => {
|
|
|
|
|
|
const server = await startFixtureServer();
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { port } = server.address();
|
|
|
|
|
|
const url = `http://127.0.0.1:${port}`;
|
|
|
|
|
|
|
|
|
|
|
|
const result = await runScript({
|
|
|
|
|
|
operation: 'list',
|
|
|
|
|
|
url,
|
|
|
|
|
|
skipLogin: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// listOp в skipLogin-режиме не навигирует на /admin/visit/rt — просто открывает url.
|
|
|
|
|
|
// Фикстура не содержит Vuex и таблицы с проектами → возвращает {projects: []}.
|
|
|
|
|
|
assert.strictEqual(result.code, 0, `Expected exit 0. stderr: ${result.stderr}`);
|
2026-05-19 12:14:11 +03:00
|
|
|
|
|
2026-05-19 16:40:44 +03:00
|
|
|
|
let out;
|
|
|
|
|
|
try {
|
|
|
|
|
|
out = JSON.parse(result.stdout);
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
assert.fail(`stdout is not valid JSON: ${result.stdout}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
assert.ok(Array.isArray(out.projects), `expected projects array, got: ${JSON.stringify(out)}`);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
server.close();
|
|
|
|
|
|
}
|
2026-05-19 12:14:11 +03:00
|
|
|
|
});
|
2026-06-22 08:49:19 +03:00
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Test 3 — отказ запуска браузера → чистый exit 4 + JSON {error}, не опасный exit 1
|
|
|
|
|
|
// FN-SESSION (приёмка 22.06.2026): launch вне try/catch уводил отказ запуска в
|
|
|
|
|
|
// unhandled-rejection exit 1 — двойник «login rejected». Фикс: launch в try → exit 4.
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
test('browser launch failure → exit 4 + JSON {error} (not unhandled-rejection exit 1)', async () => {
|
|
|
|
|
|
const result = await runScript(
|
|
|
|
|
|
{ operation: 'list', url: 'http://127.0.0.1:1/', skipLogin: true },
|
|
|
|
|
|
{ PLAYWRIGHT_BROWSERS_PATH: path.resolve(__dirname, '__nonexistent_browsers__') },
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
assert.strictEqual(result.code, 4, `Expected exit 4, got ${result.code}. stderr: ${result.stderr}`);
|
|
|
|
|
|
|
|
|
|
|
|
let parsed;
|
|
|
|
|
|
try {
|
|
|
|
|
|
parsed = JSON.parse(result.stderr.trim());
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
assert.fail(`stderr не валидный JSON (сырой стек?): ${result.stderr}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
assert.ok(typeof parsed.error === 'string' && parsed.error.length > 0, `expected {error}, got ${result.stderr}`);
|
|
|
|
|
|
});
|