296 lines
11 KiB
JavaScript
296 lines
11 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);
|
|
if (message.error) callback.reject(message.error);
|
|
else 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}`);
|
|
}
|