111 lines
9.1 KiB
JavaScript
111 lines
9.1 KiB
JavaScript
// Synthetic QA server only; never writes to the application database.
|
|
import fs from 'node:fs';
|
|
const target = await (await fetch('http://127.0.0.1:9224/json/new?about:blank', { method: 'PUT' })).json();
|
|
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
|
await new Promise(resolve => ws.addEventListener('open', resolve, { once: true }));
|
|
let sequence = 0;
|
|
const pending = new Map();
|
|
const errors = [];
|
|
const requests = [];
|
|
const responses = [];
|
|
const responseIds = new Set();
|
|
const responseBodies = [];
|
|
ws.onmessage = event => {
|
|
const message = JSON.parse(event.data);
|
|
if (message.method === 'Network.requestWillBeSent' && message.params.request.url.includes('/management/opportunities') && message.params.request.postData) {
|
|
const request = message.params.request; let data = {};
|
|
try { data = JSON.parse(request.postData); } catch {}
|
|
const keys = ['province_id','district_id','admin_post_id','locality_id','territory_version','reference'];
|
|
requests.push({ method: request.method, path: new URL(request.url).pathname, payload: Object.fromEntries(keys.filter(k => k in data).map(k => [k, data[k]])) });
|
|
}
|
|
if (message.method === 'Network.responseReceived' && message.params.response.url.includes('/management/opportunities')) {
|
|
responses.push({ path: new URL(message.params.response.url).pathname, status: message.params.response.status });
|
|
responseIds.add(message.params.requestId);
|
|
}
|
|
if (message.method === 'Network.loadingFinished' && responseIds.has(message.params.requestId)) {
|
|
send('Network.getResponseBody', { requestId: message.params.requestId }).then(result => {
|
|
try { const body = JSON.parse(result.body); responseBodies.push({component: body.component, errors: body.props?.errors}); } catch {}
|
|
}).catch(() => {});
|
|
}
|
|
if (message.method === 'Runtime.exceptionThrown') errors.push(message.params.exceptionDetails.text);
|
|
if (message.id) { const callback = pending.get(message.id); pending.delete(message.id); message.error ? callback.reject(message.error) : callback.resolve(message.result); }
|
|
};
|
|
const send = (method, params = {}) => new Promise((resolve, reject) => { const id = ++sequence; pending.set(id, { resolve, reject }); ws.send(JSON.stringify({ id, method, params })); });
|
|
const evaluate = async expression => { const result = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true }); if (result.exceptionDetails) throw Error(JSON.stringify(result.exceptionDetails)); return result.result?.value; };
|
|
const wait = async expression => { for (let n = 0; n < 100; n++) { if (await evaluate(expression)) return; await new Promise(resolve => setTimeout(resolve, 100)); } throw Error('Timed out: ' + expression); };
|
|
const select = async (index, value) => evaluate(`(()=>{const element=document.querySelectorAll('select')[${index}];Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype,'value').set.call(element,${JSON.stringify(value)});element.dispatchEvent(new Event('change',{bubbles:true}));})()`);
|
|
try {
|
|
await send('Runtime.enable'); await send('Page.enable'); await send('Network.enable');
|
|
await send('Page.navigate', { url: 'http://127.0.0.1:18027/login' });
|
|
await wait(`document.readyState==='complete' && (location.pathname!='/login' || !!document.querySelector('input[type="password"]'))`);
|
|
if (await evaluate(`location.pathname==='/login'`)) {
|
|
for (const [selector, text] of [['input[type="email"]', 'mne-browser@example.test'], ['input[type="password"]', 'SyntheticMneOnly-2026!']]) { await evaluate(`document.querySelector(${JSON.stringify(selector)}).focus()`); await send('Input.insertText', { text }); }
|
|
await evaluate(`document.querySelector('button[type="submit"]').click()`); await wait(`!location.pathname.startsWith('/login')`);
|
|
}
|
|
const token = 'geo-browser-' + Date.now();
|
|
const observations = [];
|
|
await send('Page.navigate', { url: 'http://127.0.0.1:18027/management/opportunities/create' });
|
|
await wait(`document.querySelectorAll('select').length===7`);
|
|
const fill = async (label, value) => {
|
|
await evaluate(`(()=>{const label=[...document.querySelectorAll('label')].find(l=>l.textContent.startsWith(${JSON.stringify(label)}));const input=label.querySelector('input');input.focus();input.select();})()`);
|
|
if (value === '') {
|
|
await send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 });
|
|
await send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Backspace', code: 'Backspace', windowsVirtualKeyCode: 8 });
|
|
} else await send('Input.insertText', { text: value });
|
|
};
|
|
await fill('Referência', token); await fill('Título', token); await fill('Slug', token);
|
|
await select(4, await evaluate(`document.querySelectorAll('select')[4].options[1].value`));
|
|
for (let level = 0; level < 4; level++) {
|
|
await wait(`!document.querySelectorAll('select')[${level}].disabled && document.querySelectorAll('select')[${level}].options.length>1`);
|
|
await select(level, `10000000-0000-4000-8000-${String(level+2).padStart(12, '0')}`);
|
|
}
|
|
const save = () => evaluate(`[...document.querySelectorAll('button')].find(b=>b.textContent==='Guardar').click()`);
|
|
const reopen = async () => {
|
|
await wait(`location.pathname==='/management/opportunities' && document.body.innerText.includes(${JSON.stringify(token)})`);
|
|
await evaluate(`[...document.querySelectorAll('article')].find(a=>a.textContent.includes(${JSON.stringify(token)})).querySelector('a').click()`);
|
|
await wait(`location.pathname.endsWith('/edit') && document.querySelectorAll('select').length===7`);
|
|
};
|
|
await save(); await reopen();
|
|
await wait(`document.querySelectorAll('select')[3].value==='10000000-0000-4000-8000-000000000005'`);
|
|
observations.push('create_save_reopen_full_path');
|
|
// Server validation must retain the chain and expose the failing field.
|
|
await fill('Referência', ''); await save();
|
|
await wait(`document.querySelector('[role="alert"]')!==null`);
|
|
if (!(await evaluate(`document.querySelectorAll('select')[3].value==='10000000-0000-4000-8000-000000000005'`))) throw Error('Validation lost selection');
|
|
await fill('Referência', token);
|
|
observations.push('validation_preserves_selection');
|
|
await select(2, '');
|
|
await wait(`document.querySelectorAll('select')[3].disabled && document.querySelectorAll('select')[3].value===''`);
|
|
await save(); await reopen();
|
|
await wait(`document.querySelectorAll('select')[1].value==='10000000-0000-4000-8000-000000000003' && document.querySelectorAll('select')[2].value===''`);
|
|
observations.push('edit_save_reopen_and_descendant_reset');
|
|
await send('Network.setBlockedURLs', { urls: ['*://127.0.0.1:18027/management/territory-options?type=province*'] });
|
|
await evaluate(`document.querySelector('input[aria-label="Pesquisar Província"]').focus()`);
|
|
await send('Input.insertText', { text: 'Synthetic' });
|
|
await wait(`document.body.innerText.includes('Tentar novamente')`);
|
|
await send('Network.setBlockedURLs', { urls: [] });
|
|
await evaluate(`[...document.querySelectorAll('button')].find(b=>b.textContent==='Tentar novamente').click()`);
|
|
await wait(`!document.body.innerText.includes('Tentar novamente') && document.querySelectorAll('select')[0].options.length>1 && !document.querySelectorAll('select')[0].disabled`);
|
|
observations.push('network_failure_and_retry');
|
|
await evaluate(`document.querySelector('input[aria-label="Pesquisar Distrito"]').focus()`);
|
|
await send('Input.insertText', { text: 'no-such-synthetic-district' });
|
|
await wait(`document.body.innerText.includes('Sem opções para esta pesquisa.')`);
|
|
observations.push('empty_search');
|
|
await select(0, '');
|
|
await select(0, '10000000-0000-4000-8000-000000000002');
|
|
await select(0, '');
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
if (!(await evaluate(`[...document.querySelectorAll('select')].slice(1,4).every(s=>s.disabled && s.value==='' && s.options.length===1)`))) throw Error('Stale children after rapid parent changes');
|
|
observations.push('rapid_parent_change_cancels_and_clears_children');
|
|
const widths = [];
|
|
for (const width of [390, 1440]) { await send('Emulation.setDeviceMetricsOverride', { width, height: 900, deviceScaleFactor: 1, mobile: width < 600 }); widths.push({ width, noOverflow: await evaluate('document.documentElement.scrollWidth<=innerWidth+1') }); }
|
|
if (errors.length || widths.some(w => !w.noOverflow)) throw Error('Browser validation failed');
|
|
const report = { date: new Date().toISOString(), observations, requests, responses, widths, javascript_errors: errors.length, scope: 'Synthetic isolated QA' };
|
|
fs.writeFileSync('docs/investmoz/gis/opportunity-dropdown-browser.json', JSON.stringify(report, null, 2) + '\n');
|
|
console.log(JSON.stringify(report));
|
|
} catch (error) {
|
|
console.log(JSON.stringify({ requests, responses, responseBodies }));
|
|
console.log(await evaluate(`({path:location.pathname,body:document.body.innerText.slice(0,800)})`));
|
|
throw error;
|
|
} finally { ws.close(); await fetch(`http://127.0.0.1:9224/json/close/${target.id}`); }
|