358 lines
14 KiB
JavaScript
358 lines
14 KiB
JavaScript
const {
|
|
chromium,
|
|
} = require('C:/Users/macbook/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
|
|
const fs = require('node:fs');
|
|
const assert = require('node:assert/strict');
|
|
const { execFileSync } = require('node:child_process');
|
|
const base = 'http://127.0.0.1:18092';
|
|
const dir = 'storage/app/private/investmoz-v2';
|
|
(async () => {
|
|
const browser = await chromium.launch({
|
|
headless: true,
|
|
executablePath:
|
|
'C:/Users/macbook/AppData/Local/ms-playwright/chromium-1243/chrome-win64/chrome.exe',
|
|
});
|
|
const errors = [],
|
|
blocked = [],
|
|
checks = [];
|
|
async function context(width) {
|
|
const ctx = await browser.newContext({
|
|
viewport: { width, height: width < 640 ? 844 : 1000 },
|
|
hasTouch: width < 640,
|
|
});
|
|
await ctx.route('**/*', (r) => {
|
|
if (r.request().url().startsWith(base)) return r.continue();
|
|
blocked.push(r.request().url());
|
|
return r.abort();
|
|
});
|
|
const page = await ctx.newPage();
|
|
page.on('pageerror', (e) => errors.push(e.message));
|
|
return page;
|
|
}
|
|
async function visit(page, path) {
|
|
const r = await page.goto(base + path);
|
|
assert.equal(
|
|
r.headers()['x-investmoz-qa-target'],
|
|
'investmoz_v2_20261008_test:55441',
|
|
);
|
|
assert.ok(r.status() < 400, path + ':' + r.status());
|
|
await page.locator('nav[aria-label="Áreas do portal"]').waitFor();
|
|
assert.equal(
|
|
await page.locator('nav[aria-label="Áreas do portal"]').count(),
|
|
1,
|
|
);
|
|
await page.waitForLoadState('networkidle');
|
|
}
|
|
async function login(page, role) {
|
|
await visit(page, '/login');
|
|
await page
|
|
.locator('input[type=email]')
|
|
.fill(`guidance-qa-${role}@example.invalid`);
|
|
await page.locator('input[type=password]').fill('QA-Guidance-2026!');
|
|
await page.locator('button[type=submit]').click();
|
|
await page.waitForURL((url) => !url.pathname.startsWith('/login'));
|
|
}
|
|
async function api(page, path, method = 'GET', data) {
|
|
return page.evaluate(
|
|
async ({ path, method, data }) => {
|
|
const token = decodeURIComponent(
|
|
document.cookie
|
|
.split('; ')
|
|
.find((v) => v.startsWith('XSRF-TOKEN='))
|
|
?.slice(11) ?? '',
|
|
);
|
|
const r = await fetch(path, {
|
|
method,
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'Content-Type': 'application/json',
|
|
'X-XSRF-TOKEN': token,
|
|
},
|
|
...(data && method !== 'GET'
|
|
? { body: JSON.stringify(data) }
|
|
: {}),
|
|
});
|
|
return { status: r.status, data: await r.json() };
|
|
},
|
|
{ path, method, data },
|
|
);
|
|
}
|
|
async function menus(page, management, touch) {
|
|
const nav = page.locator('nav[aria-label="Áreas do portal"]');
|
|
assert.equal(
|
|
await nav.getByRole('button', { name: /^Gestão/ }).count(),
|
|
management ? 1 : 0,
|
|
);
|
|
const groups = nav.locator(':scope > ul > li > button');
|
|
for (let i = 0; i < (await groups.count()); i++) {
|
|
const b = groups.nth(i);
|
|
await b.scrollIntoViewIfNeeded();
|
|
if (touch) await b.tap();
|
|
else {
|
|
await b.focus();
|
|
await page.keyboard.press('Enter');
|
|
}
|
|
assert.equal(await b.getAttribute('aria-expanded'), 'true');
|
|
const panel = nav.locator(
|
|
`[id="${await b.getAttribute('aria-controls')}"]`,
|
|
);
|
|
const box = await panel.boundingBox(),
|
|
width = page.viewportSize().width;
|
|
assert.ok(
|
|
box.x >= -1 && box.x + box.width <= width + 1,
|
|
JSON.stringify({ box, width }),
|
|
);
|
|
const nested = panel.getByRole('button');
|
|
if (await nested.count()) {
|
|
const n = nested.first();
|
|
await n.click();
|
|
assert.equal(await n.getAttribute('aria-expanded'), 'true');
|
|
await page.keyboard.press('Escape');
|
|
assert.equal(await n.getAttribute('aria-expanded'), 'false');
|
|
assert.equal(
|
|
await n.evaluate((el) => el === document.activeElement),
|
|
true,
|
|
);
|
|
}
|
|
await page.keyboard.press('Escape');
|
|
assert.equal(await b.getAttribute('aria-expanded'), 'false');
|
|
assert.equal(
|
|
await b.evaluate((el) => el === document.activeElement),
|
|
true,
|
|
);
|
|
}
|
|
const overflow = await page.evaluate(
|
|
() => document.documentElement.scrollWidth > window.innerWidth,
|
|
);
|
|
assert.equal(overflow, false, page.url());
|
|
}
|
|
try {
|
|
const caseId = JSON.parse(
|
|
fs.readFileSync(dir + '/desk-browser-evidence.json'),
|
|
).caseId;
|
|
for (const width of [1440, 390]) {
|
|
const page = await context(width);
|
|
await visit(page, '/');
|
|
await menus(page, false, width < 640);
|
|
await page.screenshot({
|
|
path: `${dir}/public-${width}.png`,
|
|
fullPage: true,
|
|
});
|
|
for (const [kind, title] of [
|
|
['project', 'V2 QA project'],
|
|
['cluster', 'V2 QA cluster'],
|
|
]) {
|
|
await visit(page, `/carteira?kind=${kind}`);
|
|
await page.getByLabel('Pesquisar', { exact: true }).fill(title);
|
|
await page
|
|
.getByRole('button', { name: 'Pesquisar', exact: true })
|
|
.click();
|
|
await page
|
|
.getByRole('link', { name: title, exact: true })
|
|
.click();
|
|
await page
|
|
.getByRole('heading', { name: title, exact: true })
|
|
.waitFor();
|
|
assert.ok(
|
|
await page
|
|
.locator('[data-current-group="true"]')
|
|
.textContent()
|
|
.then((t) => t.includes('Explorar')),
|
|
);
|
|
}
|
|
await visit(page, '/zonas');
|
|
await page.getByLabel('Pesquisar por nome ou código').fill('V2 QA');
|
|
await page
|
|
.getByRole('button', { name: 'Pesquisar', exact: true })
|
|
.click();
|
|
await page
|
|
.getByRole('link', {
|
|
name: 'V2 QA localização fictícia',
|
|
exact: true,
|
|
})
|
|
.click();
|
|
await page
|
|
.getByRole('heading', {
|
|
name: 'V2 QA localização fictícia',
|
|
exact: true,
|
|
})
|
|
.waitFor();
|
|
await login(page, 'investor');
|
|
await visit(page, '/investidor');
|
|
await menus(page, false, width < 640);
|
|
await page.screenshot({
|
|
path: `${dir}/investor-${width}.png`,
|
|
fullPage: true,
|
|
});
|
|
const draft = await api(
|
|
page,
|
|
'/api/v1/simulation-drafts/00000000-0000-4000-8000-000000000202',
|
|
);
|
|
assert.equal(draft.status, 200);
|
|
assert.equal(
|
|
draft.data.data.economic_snapshot.project_title,
|
|
'V2 QA rascunho anterior',
|
|
);
|
|
const restored = await api(
|
|
page,
|
|
'/balcao-virtual/dossiers',
|
|
'POST',
|
|
{
|
|
title: 'V2 QA continuidade do rascunho',
|
|
simulation_id: '00000000-0000-4000-8000-000000000202',
|
|
reuse_consent: true,
|
|
client_key: crypto.randomUUID(),
|
|
},
|
|
);
|
|
assert.equal(restored.status, 201);
|
|
const guidance = await api(
|
|
page,
|
|
`/balcao-virtual/dossiers/${restored.data.id}/orientacao`,
|
|
);
|
|
assert.equal(
|
|
guidance.data.facts.project_summary,
|
|
'Ideia anterior preservada QA',
|
|
);
|
|
assert.equal(
|
|
guidance.data.phases[1].evidence[0].state,
|
|
'investor_declared_not_verified',
|
|
);
|
|
assert.equal(guidance.data.phases[2].evidence[0].state, 'absent');
|
|
for (const path of [
|
|
'/como-investir',
|
|
'/simulador-fiscal',
|
|
`/balcao-virtual/dossiers/${caseId}`,
|
|
'/parcerias',
|
|
]) {
|
|
await visit(page, path);
|
|
await page
|
|
.getByRole('button', {
|
|
name: 'Acompanhar o meu projecto neste portal',
|
|
exact: true,
|
|
})
|
|
.click();
|
|
await page
|
|
.getByLabel('Contexto do projecto')
|
|
.selectOption(caseId);
|
|
await page
|
|
.getByText('Regras internas', { exact: false })
|
|
.waitFor();
|
|
await page
|
|
.getByRole('button', {
|
|
name: 'Acompanhar o meu projecto neste portal',
|
|
exact: true,
|
|
})
|
|
.click();
|
|
}
|
|
checks.push(
|
|
`Publicação QA, pesquisa e fichas; menus públicos e de investidor; rascunho e contexto entre módulos em ${width}px.`,
|
|
);
|
|
await page.context().close();
|
|
}
|
|
const owner = await context(1440);
|
|
await login(owner, 'investor');
|
|
const manager = await context(1440);
|
|
await login(manager, 'manager');
|
|
await visit(manager, '/management/crm/leads');
|
|
await menus(manager, true, false);
|
|
execFileSync(
|
|
'php',
|
|
['tools/investmoz-v2-qa-router.php', 'seed', caseId],
|
|
{ stdio: 'pipe' },
|
|
);
|
|
await visit(owner, `/balcao-virtual/dossiers/${caseId}?phase=location`);
|
|
await owner
|
|
.getByText('Documento associado, por conferir', { exact: false })
|
|
.first()
|
|
.waitFor();
|
|
const dossier = await api(
|
|
manager,
|
|
`/balcao-virtual/dossiers/${caseId}`,
|
|
);
|
|
const document = dossier.data.documents.find(
|
|
(d) => d.category === 'location_evidence',
|
|
);
|
|
assert.ok(document);
|
|
await visit(manager, `/balcao-virtual/dossiers/${caseId}`);
|
|
const review = manager.locator('form').filter({
|
|
has: manager.getByRole('heading', {
|
|
name: 'Conferência documental interna',
|
|
}),
|
|
});
|
|
await review
|
|
.getByLabel('Documento', { exact: true })
|
|
.selectOption(document.id);
|
|
await review.getByLabel('Localização', { exact: true }).check();
|
|
await review
|
|
.getByLabel('O que foi verificado')
|
|
.fill('Conferido apenas o acesso descrito na fixture V2 QA.');
|
|
await review
|
|
.getByLabel('Evidência utilizada')
|
|
.fill('Ficheiro sintético, versão e hash registados nesta QA.');
|
|
await review
|
|
.getByRole('button', {
|
|
name: 'Registar conferência com a minha autoria',
|
|
})
|
|
.click();
|
|
await manager.waitForLoadState('networkidle');
|
|
await visit(owner, `/balcao-virtual/dossiers/${caseId}?phase=location`);
|
|
await owner
|
|
.getByText('Verificador: Guidance QA manager', { exact: false })
|
|
.waitFor();
|
|
await owner
|
|
.getByText('Conferido internamente no âmbito descrito', {
|
|
exact: false,
|
|
})
|
|
.first()
|
|
.waitFor();
|
|
await owner.screenshot({
|
|
path: `${dir}/scoped-evidence.png`,
|
|
fullPage: true,
|
|
});
|
|
const other = await context(390);
|
|
await login(other, 'other');
|
|
assert.equal(
|
|
(await api(other, `/balcao-virtual/dossiers/${caseId}/orientacao`))
|
|
.status,
|
|
403,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await api(
|
|
other,
|
|
'/api/v1/simulation-drafts/00000000-0000-4000-8000-000000000202',
|
|
)
|
|
).status,
|
|
404,
|
|
);
|
|
assert.equal((await api(other, '/management/crm/leads')).status, 403);
|
|
await visit(other, '/investidor');
|
|
await menus(other, false, true);
|
|
checks.push(
|
|
'Conferência pela interface com autor/data/âmbito; gestão autorizada; isolamento de dossier e rascunho entre utilizadores.',
|
|
);
|
|
assert.deepEqual(errors, []);
|
|
fs.writeFileSync(
|
|
dir + '/navigation-browser-evidence.json',
|
|
JSON.stringify(
|
|
{
|
|
passed: true,
|
|
database: 'investmoz_v2_20261008_test',
|
|
port: 55441,
|
|
checks,
|
|
errors,
|
|
blockedExternalRequests: blocked,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
console.log(JSON.stringify({ passed: true, checks, errors }));
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
})().catch((e) => {
|
|
console.error(e);
|
|
process.exitCode = 1;
|
|
});
|