Files
Investidor-V2/tools/legacy-content-browser-qa.cjs
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

580 lines
21 KiB
JavaScript

/** Real Chromium render/click acceptance on the existing, isolated QA server. */
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const {
chromium,
} = require('C:/Users/macbook/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
const ROOT = path.resolve(__dirname, '..');
const PRIVATE = path.join(
ROOT,
'storage/app/private/investmoz-v2/portal-audit',
);
const BASE = 'http://127.0.0.1:18097';
const TARGET = 'desk_restore_20261009_174726_test:55441';
const receiptPath = path.join(PRIVATE, 'legacy-content-browser-qa.json');
const actors = JSON.parse(
fs.readFileSync(
path.join(PRIVATE, 'profiles-v1-actors-private.json'),
'utf8',
),
);
assert.equal(
actors.promoter.id,
21,
'Use the designated synthetic Promotor QA21',
);
const previous = fs.existsSync(receiptPath)
? JSON.parse(fs.readFileSync(receiptPath, 'utf8'))
: {};
if (previous.started_at) {
const archive = path.join(
PRIVATE,
'legacy-browser-attempt-' +
previous.started_at.replace(/[^0-9TZ-]/g, '-') +
'.json',
);
if (!fs.existsSync(archive)) fs.copyFileSync(receiptPath, archive);
}
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
const screenshotDir = path.join(PRIVATE, 'legacy-browser-' + stamp);
fs.mkdirSync(screenshotDir, { recursive: true });
const proof = {
started_at: new Date().toISOString(),
base: BASE,
target: TARGET,
browser: 'installed Chromium 1243, isolated headless contexts',
native_control: 'pipe unavailable; two probes',
interface_observed: true,
active_writes: 0,
checks: {},
events: [],
screenshots: [],
dossier_id: previous.dossier_id || null,
continuation_id: previous.continuation_id || null,
title: previous.title || 'QA visual — ideia de agronegócio ' + stamp,
last_step: 'initialization',
};
let browser;
function save() {
fs.writeFileSync(receiptPath, JSON.stringify(proof, null, 2));
}
function step(name) {
proof.last_step = name;
save();
console.log(JSON.stringify({ step: name }));
}
function totp(secret) {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let bits = 0,
value = 0;
const bytes = [];
for (const char of secret.toUpperCase().replace(/=+$/, '')) {
const digit = alphabet.indexOf(char);
assert(digit >= 0);
value = (value << 5) | digit;
bits += 5;
if (bits >= 8) {
bytes.push((value >>> (bits - 8)) & 255);
bits -= 8;
}
}
const counter = Buffer.alloc(8);
counter.writeBigUInt64BE(BigInt(Math.floor(Date.now() / 30000)));
const digest = crypto
.createHmac('sha1', Buffer.from(bytes))
.update(counter)
.digest();
const offset = digest[19] & 15;
return String(
(digest.readUInt32BE(offset) & 0x7fffffff) % 1000000,
).padStart(6, '0');
}
async function context(viewport = { width: 1440, height: 1000 }) {
const ctx = await browser.newContext({
viewport,
locale: 'pt-MZ',
reducedMotion: 'reduce',
});
ctx.qaConfirmed = false;
await ctx.route('**/*', async (route) => {
const request = route.request(),
url = new URL(request.url());
if (url.origin !== BASE) {
proof.events.push({ external_request_blocked: url.origin });
return route.abort();
}
if (!['GET', 'HEAD'].includes(request.method()))
assert(ctx.qaConfirmed, 'Confirm actual QA before any write');
await route.continue();
});
ctx.on('response', (response) => {
const request = response.request(),
url = new URL(response.url());
if (url.origin === BASE && !url.pathname.startsWith('/build-qa-')) {
const headers = response.headers();
proof.events.push({
method: request.method(),
path: url.pathname,
status: response.status(),
target: headers['x-investmoz-qa-target'] || null,
redirect_path: headers.location
? new URL(headers.location, BASE).pathname
: null,
});
}
});
return ctx;
}
async function open(page, route) {
const response = await page.goto(BASE + route, {
waitUntil: 'networkidle',
});
assert.equal(response.headers()['x-investmoz-qa-target'], TARGET);
page.context().qaConfirmed = true;
return response;
}
async function screenshot(page, name, fullPage = true) {
const file = path.join(screenshotDir, name + '.png');
await page.screenshot({ path: file, fullPage });
proof.screenshots.push({
name,
path: path.relative(ROOT, file).replaceAll('\\', '/'),
});
}
async function login(page, role) {
const account = actors[role];
if (!page.url().startsWith(BASE + '/login')) await open(page, '/login');
await page.locator('#email').fill(account.email);
await page.locator('#password').fill(account.password);
await page.locator('[data-test="login-button"]').click();
await page.waitForURL((url) => url.pathname === '/two-factor-challenge', {
timeout: 15000,
});
assert(account.totp_secret, 'Normal MFA is required; never bypass it');
await page.locator('input[name="code"]').fill(totp(account.totp_secret));
await page.getByRole('button', { name: 'Continue', exact: true }).click();
await page.waitForURL(
(url) => !['/login', '/two-factor-challenge'].includes(url.pathname),
{ timeout: 15000 },
);
await page.waitForLoadState('networkidle');
const cookies = await page.context().cookies(BASE);
proof.checks['mfa_' + role] = {
actor_id: account.id,
final_path: new URL(page.url()).pathname,
cookies: cookies.map(
({ name, domain, path, secure, sameSite, httpOnly }) => ({
name,
domain,
path,
secure,
sameSite,
httpOnly,
}),
),
};
}
async function logout(page) {
await open(page, '/dashboard');
const triggers = page.locator('button[aria-haspopup="menu"]');
const count = await triggers.count();
for (let i = count - 1; i >= 0; i--) {
await triggers.nth(i).click();
const action = page.locator('[data-test="logout-button"]');
if (await action.isVisible()) {
await action.click();
await page.waitForURL(BASE + '/');
return;
}
await page.keyboard.press('Escape');
}
throw new Error('Visible logout action missing on dashboard');
}
async function assertDossier(page, id) {
await page.waitForURL(BASE + '/investimentos/' + id);
assert.equal(new URL(page.url()).pathname, '/investimentos/' + id);
await page
.getByRole('heading', { name: proof.title, exact: true })
.waitFor();
assert.equal(
await page.getByRole('heading', { name: /^Fase \d+ ·/ }).count(),
5,
);
assert.equal(await page.locator('article[id^="S"]').count(), 19);
assert.equal(
await page
.getByRole('region', { name: 'Fontes da ideia' })
.locator('li')
.count(),
1,
);
await page
.getByRole('heading', { name: 'Próxima acção', exact: true })
.waitFor();
}
(async () => {
browser = await chromium.launch({
headless: true,
executablePath:
'C:/Users/macbook/AppData/Local/ms-playwright/chromium-1243/chrome-win64/chrome.exe',
});
step('public desktop rendering');
const anonymous = await context(),
page = await anonymous.newPage();
page.setDefaultTimeout(15000);
const first = await open(page, '/explorar/agronegocio');
assert.equal(first.status(), 200);
assert.equal(await page.title(), 'Explorar agronegócio - InvesteMoz QA');
proof.checks.qa_browser_title = true;
await page
.getByRole('heading', { name: 'Investir no agronegócio', exact: true })
.waitFor();
await page.locator('main img').waitFor();
await page.evaluate(() => document.fonts.ready);
proof.checks.desktop = await page.locator('main').evaluate((main) => ({
background: getComputedStyle(main).backgroundColor,
text: getComputedStyle(main).color,
font: getComputedStyle(main).fontFamily,
headingFont: getComputedStyle(main.querySelector('h1')).fontFamily,
notoSansLoaded: document.fonts.check('16px "Noto Sans"'),
notoSerifLoaded: document.fonts.check('600 36px "Noto Serif"'),
photoLoaded:
main.querySelector('img').complete &&
main.querySelector('img').naturalWidth === 900,
horizontalOverflow: document.documentElement.scrollWidth > innerWidth,
}));
assert(proof.checks.desktop.photoLoaded);
assert(!proof.checks.desktop.horizontalOverflow);
await screenshot(page, 'public-landing-desktop');
step('visible search hit and miss');
await page.getByRole('searchbox').fill('corredores');
await page.getByRole('button', { name: 'Pesquisar', exact: true }).click();
await page.waitForURL(/q=corredores/);
await page
.getByRole('link', { name: 'Ler o artigo', exact: true })
.waitFor();
await page.getByRole('searchbox').fill('zzzz-no-content-qa');
await page.getByRole('button', { name: 'Pesquisar', exact: true }).click();
await page
.getByText('Nenhum conteúdo corresponde à pesquisa.', { exact: true })
.waitFor();
assert.equal(
await page
.getByRole('link', { name: 'Ler o artigo', exact: true })
.count(),
0,
);
await screenshot(page, 'public-search-no-result');
await page.getByRole('searchbox').fill('');
await page.getByRole('button', { name: 'Pesquisar', exact: true }).click();
await page
.getByRole('link', { name: 'Ler o artigo', exact: true })
.waitFor();
proof.checks.search_hit_miss_clear = true;
step('keyboard article navigation and focus');
const read = page.getByRole('link', { name: 'Ler o artigo', exact: true });
await read.focus();
await page.keyboard.press('Tab');
await page.keyboard.press('Shift+Tab');
proof.checks.focus = await read.evaluate((el) => ({
focused: el === document.activeElement,
outlineStyle: getComputedStyle(el).outlineStyle,
outlineWidth: getComputedStyle(el).outlineWidth,
}));
await page.keyboard.press('Enter');
await page.waitForURL(BASE + '/agronegocio');
const article = page.getByRole('region', { name: 'Artigo educativo' });
await article.waitFor();
assert((await article.innerText()).length > 500);
await screenshot(page, 'public-article-desktop');
proof.checks.public_article = {
characters_rendered: (await article.innerText()).length,
source_visible: true,
};
step('consumer links through clicks');
proof.checks.consumer_links = [];
for (const name of [
'Consultar os corredores cadastrados',
'Consultar localizações e mapas disponíveis',
'Exemplar na biblioteca',
]) {
const consumer = page.getByRole('link', { name, exact: true });
const destination = new URL(await consumer.getAttribute('href'), BASE);
await consumer.click();
await page.waitForURL(destination.href);
await page.waitForLoadState('networkidle');
proof.checks.consumer_links.push({
name,
path: new URL(page.url()).pathname,
rendered_heading:
(await page.locator('h1').allTextContents()).filter(Boolean)
.length > 0,
});
await page.goBack({ waitUntil: 'networkidle' });
await page.waitForURL(BASE + '/agronegocio');
await article.waitFor();
}
const opportunity = page
.locator('section[aria-labelledby="opportunities-title"] a')
.first();
if (await opportunity.count()) {
const destination = new URL(
await opportunity.getAttribute('href'),
BASE,
);
await opportunity.click();
await page.waitForURL(destination.href);
await page.waitForLoadState('networkidle');
assert(new URL(page.url()).pathname.startsWith('/oportunidades/'));
proof.checks.published_opportunity_clicked = new URL(
page.url(),
).pathname;
await page.goBack({ waitUntil: 'networkidle' });
await article.waitFor();
}
step('mobile public rendering');
const mobileCtx = await context({ width: 390, height: 844 }),
mobile = await mobileCtx.newPage();
await open(mobile, '/explorar/agronegocio');
await mobile
.getByRole('heading', { name: 'Investir no agronegócio', exact: true })
.waitFor();
proof.checks.mobile = await mobile.evaluate(() => ({
width: innerWidth,
contentWidth: document.documentElement.scrollWidth,
photoLoaded: !!document.querySelector('main img')?.naturalWidth,
}));
assert(proof.checks.mobile.contentWidth <= proof.checks.mobile.width);
await screenshot(mobile, 'public-landing-mobile');
await mobile
.getByRole('link', { name: 'Ler o artigo', exact: true })
.click();
await mobile.getByRole('region', { name: 'Artigo educativo' }).waitFor();
assert(
await mobile.evaluate(
() => document.documentElement.scrollWidth <= innerWidth,
),
);
await screenshot(mobile, 'public-article-mobile');
await mobileCtx.close();
step('public CTA, normal Promotor QA21 login and MFA');
await page
.getByRole('link', {
name: 'Iniciar ou continuar uma ideia',
exact: true,
})
.click();
await page.waitForURL(BASE + '/login');
await login(page, 'promoter');
assert.equal(
new URL(page.url()).pathname,
'/investimentos/a-partir-de/agronegocio',
);
await page
.getByRole('heading', { name: 'Os meus investimentos', exact: true })
.waitFor();
step('create and save dossier using UI');
const createdThisRun = !proof.dossier_id;
if (createdThisRun) {
await page.getByLabel('Título', { exact: true }).fill(proof.title);
await page
.getByLabel(
'Autorizo reutilizar o meu perfil neste dossier privado.',
)
.check();
await page
.getByRole('button', {
name: 'Criar e guardar dossier',
exact: true,
})
.click();
await page.waitForURL(/\/investimentos\/[0-9a-f-]{36}$/);
proof.dossier_id = new URL(page.url()).pathname.split('/').pop();
save();
} else
await page
.getByRole('link', { name: proof.title, exact: true })
.click();
await assertDossier(page, proof.dossier_id);
await screenshot(page, 'private-dossier-created', false);
proof.checks.created_from_ui = true;
proof.checks.created_this_run = createdThisRun;
if (!createdThisRun) {
// Creation may be an earlier observed action, not another insert on every rerun.
const files = fs
.readdirSync(PRIVATE)
.filter(
(name) =>
name.startsWith('legacy-browser-attempt-') &&
name.endsWith('.json'),
);
const creation = files
.map((name) => ({
name,
receipt: JSON.parse(
fs.readFileSync(path.join(PRIVATE, name), 'utf8'),
),
}))
.find(
({ receipt }) =>
receipt.dossier_id === proof.dossier_id &&
receipt.events?.some(
(event) =>
event.method === 'POST' &&
event.path === '/investimentos' &&
event.status === 302,
),
);
assert(
creation,
'Existing dossier must have retained evidence of UI creation',
);
proof.creation_evidence = {
receipt: creation.name,
started_at: creation.receipt.started_at,
action: 'POST /investimentos through UI; subsequent retries recover this same dossier',
whole_attempt_result: creation.receipt.result,
};
}
step('source return and same dossier context');
await page
.getByRole('region', { name: 'Fontes da ideia' })
.locator('a')
.first()
.click();
await article.waitFor();
await page
.getByRole('link', { name: 'Continuar: ' + proof.title, exact: true })
.click();
await assertDossier(page, proof.dossier_id);
proof.checks.private_return_context = true;
step('visible logout and new browser session');
await logout(page);
await anonymous.close();
const freshCtx = await context(),
fresh = await freshCtx.newPage();
await open(fresh, '/investimentos/' + proof.dossier_id);
assert.equal(new URL(fresh.url()).pathname, '/login');
await login(fresh, 'promoter');
await assertDossier(fresh, proof.dossier_id);
await fresh.reload({ waitUntil: 'networkidle' });
await assertDossier(fresh, proof.dossier_id);
await screenshot(fresh, 'private-dossier-new-session', false);
proof.checks.logout_new_session_reload = true;
step('continue existing dossier and repeat without duplicate');
await open(fresh, '/investimentos/a-partir-de/agronegocio');
const card = fresh.getByRole('listitem').filter({
has: fresh.getByRole('link', { name: proof.title, exact: true }),
});
await card
.getByRole('button', {
name: 'Continuar neste dossier com o artigo',
exact: true,
})
.click();
await assertDossier(fresh, proof.dossier_id);
proof.checks.existing_dossier_replay_single_visible_reference = true;
step('anonymous context does not expose private dossier');
const publicCtx = await context(),
publicPage = await publicCtx.newPage();
await open(publicPage, '/agronegocio?context=' + proof.dossier_id);
await publicPage
.getByRole('region', { name: 'Artigo educativo' })
.waitFor();
assert.equal(
await publicPage.getByText(proof.title, { exact: false }).count(),
0,
);
await publicPage
.getByRole('link', {
name: 'Iniciar ou continuar uma ideia',
exact: true,
})
.waitFor();
proof.checks.anonymous_private_context_hidden = true;
await publicCtx.close();
step('other synthetic account denied and public source retained');
const otherCtx = await context(),
other = await otherCtx.newPage();
await open(other, '/login');
await login(other, 'outsider');
const denied = await open(other, '/investimentos/' + proof.dossier_id);
assert.equal(denied.status(), 403);
await open(other, '/agronegocio?context=' + proof.dossier_id);
await other.getByRole('region', { name: 'Artigo educativo' }).waitFor();
assert.equal(
await other.getByText(proof.title, { exact: false }).count(),
0,
);
proof.checks.other_account_denied = {
actor_id: actors.outsider.id,
dossier_status: 403,
public_article_visible: true,
};
await otherCtx.close();
await logout(fresh);
await freshCtx.close();
proof.result = 'passed';
proof.completed_at = new Date().toISOString();
save();
console.log(
JSON.stringify({
result: proof.result,
actor_id: 21,
dossier_id: proof.dossier_id,
interface_observed: true,
checks: Object.keys(proof.checks),
screenshots: proof.screenshots.length,
active_writes: 0,
}),
);
})()
.catch(async (error) => {
let message = error.message;
for (const account of Object.values(actors)) {
for (const key of ['password', 'totp_secret', 'secret']) {
if (typeof account[key] === 'string' && account[key])
message = message.replaceAll(account[key], '[REDACTED]');
}
}
message = message.replace(/\b\d{6}\b/g, '[MFA OMITIDO]');
proof.result = 'failed';
proof.failure = {
step: proof.last_step,
type: error.name,
message: message.slice(0, 2500),
pages: [],
};
if (browser)
for (const ctx of browser.contexts())
for (const page of ctx.pages()) {
proof.failure.pages.push({
path: new URL(page.url()).pathname,
headings: await page
.locator('h1')
.allTextContents()
.catch(() => []),
error_count: await page
.getByRole('alert')
.count()
.catch(() => 0),
});
}
save();
console.error(
JSON.stringify({
step: proof.failure.step,
type: proof.failure.type,
message: 'Sanitized diagnostic saved privately',
}),
);
process.exitCode = 1;
})
.finally(async () => {
if (browser) await browser.close();
});