/** * Фикстурный тест manage-project.js — против локального HTTP-сервера с Element UI фикстурой. * * Почему 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`. */ const { test } = require('node:test'); const assert = require('node:assert'); const { execFile } = require('node:child_process'); const http = require('node:http'); const fs = require('node:fs'); const path = require('node:path'); const SCRIPT = path.resolve(__dirname, 'manage-project.js'); const FIXTURE_PATH = path.resolve(__dirname, 'fixtures', 'rt-form-element-ui.html'); /** Запустить 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; } // Default: serve fixture HTML res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(html); }); server.listen(0, '127.0.0.1', () => resolve(server)); }); } /** Спавнить manage-project.js, подать JSON на stdin, вернуть {code, stdout, stderr}. */ function runScript(input, extraEnv) { return new Promise((resolve, reject) => { const child = execFile( 'node', [SCRIPT], { timeout: 90_000, env: { ...process.env, ...extraEnv } }, (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(), }); }, ); child.stdin.write(JSON.stringify(input)); child.stdin.end(); }); } // --------------------------------------------------------------------------- // 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}`); 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(); } }); // --------------------------------------------------------------------------- // 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}`); 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(); } }); // --------------------------------------------------------------------------- // 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}`); });