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

484 lines
20 KiB
JavaScript

const {
start,
BASE: _BASE,
DIR,
assert,
fs,
path,
sanitized,
} = require('./portal-visual-qa-lib.cjs');
const mode = process.argv[2] || 'categories';
void (async () => {
const q = await start('next-workflow-' + mode + '-chromium-20261010');
q.proof.cases = q.prior.cases || {};
let page, ctx, props;
async function actor(role) {
if (ctx) await ctx.close();
({ page, ctx } = await q.context(role));
await q.login(page, role);
}
async function open(id, edit = false) {
const r = await q.open(
page,
'/minhas-oportunidades/' + id + (edit ? '/edit' : '/view'),
);
assert.equal(r.status(), 200, 'Authorized workflow page');
const html = await r.text();
const a = html.match(/data-page="([^"]+)"/);
const raw =
a && a[1].startsWith('{')
? a[1]
.replace(/"/g, '"')
.replace(/&/g, '&')
.replace(/'/g, "'")
: html.match(
/<script[^>]*type="application\/json"[^>]*>([\s\S]*?)<\/script>/,
)?.[1];
props = JSON.parse(raw).props;
await page.getByRole('heading').first().waitFor();
return props;
}
const names = {
request_revision: 'Preparar alteração material',
submit: 'Submeter para validação',
start_review: 'Iniciar validação',
review_gate: 'Guardar revisão especializada',
approve: 'Aprovar versão',
suspend: 'Suspender',
reject: 'Rejeitar com fundamento',
appeal: 'Apresentar recurso',
appeal_reopen: 'Reabrir para correção',
appeal_uphold: 'Manter rejeição após recurso',
};
async function transition(id, action, options = {}) {
await open(id);
const item = q.proof.cases[id];
q.phase('Category ' + item.category + ' #' + id + ' ' + action);
const before = props.workflowState;
const box = page.getByRole('region', {
name: 'Workflow interno',
exact: true,
});
assert(
props.profileActions.includes(action),
'Permitted action ' + action,
);
await box
.getByLabel('Fundamentação', { exact: true })
.fill(
'Ensaio visual sintético QA de ' +
action +
': preservar versão, fundamentação e responsabilidades independentes.',
);
if (options.gate) {
await box.getByLabel(/^Revisão/).selectOption(options.gate);
await box.getByLabel(/^Decisão/).selectOption('approved');
const resource = props.canonicalOptions.resources.documents[0];
const title = resource.payload?.title || resource.file?.file_name;
assert(title, 'Resource evidence has a visible title');
await box.getByLabel(title, { exact: true }).check();
}
if (action === 'publish')
await box.getByLabel(/Confirmei a pré-visualização/).check();
const button =
action === 'publish'
? props.values.canonical.visibility === 'public'
? 'Publicar no catálogo público'
: 'Disponibilizar com acesso restrito'
: names[action];
assert(button);
const navigation = options.expected
? null
: page.waitForResponse(
(r) =>
r.request().method() === 'GET' &&
new URL(r.url()).pathname ===
'/minhas-oportunidades/' + id + '/view' &&
r.status() === 200,
);
navigation?.catch(() => {});
const pending = page.waitForResponse(
(r) =>
r.request().method() === 'POST' &&
new URL(r.url()).pathname ===
'/minhas-oportunidades/' + id + '/workflow',
);
await box.getByRole('button', { name: button, exact: true }).click();
const r = await pending;
const body = await r.json();
item.steps.push({
action,
actor: props.auth.user.id,
before,
status: r.status(),
gate: options.gate,
errors: r.status() !== 200 ? body.errors : undefined,
message: r.status() !== 200 ? body.message : undefined,
});
q.save();
if (options.expected) {
assert.equal(r.status(), options.expected);
assert(
body.message,
'Expected rejection must explain the dependency',
);
await page
.getByRole('status')
.filter({ hasText: body.message })
.first()
.waitFor();
await q.shot(page, id + '-' + action + '-blocked', false);
return;
}
assert.equal(r.status(), 200, 'Transition ' + action);
await navigation;
await page.waitForLoadState('networkidle');
await open(id);
item.steps.at(-1).after = props.workflowState;
const status = page.getByRole('status', {
name: 'Estado da oportunidade',
exact: true,
});
assert(await status.isVisible());
item.steps.at(-1).accessible_state = await status.ariaSnapshot();
q.save();
await q.shot(page, id + '-' + action + '-' + item.steps.length, false);
}
try {
if (mode === 'categories' || mode === 'modular') {
const source =
mode === 'modular'
? {
rows: JSON.parse(
fs.readFileSync(
path.join(
DIR,
'opportunity-modular-http-qa.json',
),
'utf8',
),
).cases,
}
: JSON.parse(
fs.readFileSync(
path.join(
DIR,
'next-contract-inspection-qa.json',
),
'utf8',
),
);
const ids =
mode === 'modular' ? [42, 44, 46, 48] : [31, 32, 33, 34, 35];
const todo = ids.filter(
(id) =>
!q.proof.cases[id]?.material_saved ||
!q.proof.cases[id].steps.some(
(s) => s.action === 'submit' && s.status === 200,
),
);
if (todo.length) {
await actor('promoter');
for (const id of todo) {
let p = await open(id);
const item = (q.proof.cases[id] ||= {
category: source.rows.find((x) => x.id === id).category,
steps: [],
});
assert.equal(p.auth.user.id, 21);
item.original_publication ??= p.profileContext.publication;
item.original_title ??= p.values.canonical.official_title;
q.save();
if (!item.revision_requested) {
await transition(id, 'request_revision');
item.revision_requested = true;
q.save();
}
p = await open(id, !item.material_saved);
if (!item.material_saved) {
assert.equal(p.workflowState, 'needs_information');
await page
.locator('[id="canonical.demand_statement"]')
.fill(
p.values.canonical.demand_statement +
' Alteração substantiva sintética QA da necessidade declarada; exige nova revisão interna.',
);
await page
.getByLabel('Motivo da alteração', { exact: true })
.fill(
'Alteração substantiva sintética QA após aprovação, sem alterar silenciosamente a ficha disponibilizada.',
);
const wait = page.waitForResponse(
(r) =>
r.request().method() === 'PUT' &&
new URL(r.url()).pathname ===
'/minhas-oportunidades/' + id + '/draft',
);
await page
.getByRole('button', {
name: 'Guardar rascunho',
exact: true,
})
.click();
assert.equal((await wait).status(), 200);
await page.waitForLoadState('networkidle');
p = await open(id);
assert(!p.profileContext.approval);
assert.equal(
p.values.canonical.official_title,
item.original_title,
);
item.material_saved = true;
item.public_projection_preserved_before_approval = true;
q.save();
}
p = await open(id);
if (p.workflowState === 'needs_information')
await transition(id, 'submit');
}
}
await actor('validator');
for (const id of ids) {
let p = await open(id);
if (p.workflowState === 'submitted')
await transition(id, 'start_review');
p = await open(id);
if (p.workflowState === 'in_review') {
for (const gate of p.canonicalOptions.gates) {
if (
!q.proof.cases[id].steps.some(
(s) =>
s.action === 'review_gate' &&
s.gate === gate &&
s.status === 200,
)
)
await transition(id, 'review_gate', { gate });
}
await transition(id, 'approve');
}
}
await actor('publisher');
for (const id of ids) {
let p = await open(id);
if (p.workflowState === 'approved') {
await transition(
id,
'publish',
id === 32 ? { expected: 422 } : {},
);
if (id === 32) {
q.proof.cases[
id
].publication_blocked_external_dependency = true;
continue;
}
}
p = await open(id);
assert.equal(p.workflowState, 'published');
assert.notEqual(
p.profileContext.approval.actor_id,
p.profileContext.publication.actor_id,
);
assert.equal(
p.profileContext.approval.hash,
p.profileContext.publication.hash,
);
q.proof.cases[id].publication_scope = p.publicationState.scope;
q.proof.cases[id].final_state = p.workflowState;
}
q.proof.checks[
mode === 'modular'
? 'four_qa_modular_categories_material_revision_review_restricted_release'
: 'five_categories_material_revision_review_publication_applicability'
] = true;
} else if (mode === 'suspension') {
const id = 31;
q.proof.cases[id] ||= { category: 'real_estate', steps: [] };
const item = q.proof.cases[id];
let p;
if (!item.reopened) {
await actor('publisher');
p = await open(id);
if (!item.suspended) {
assert.equal(p.workflowState, 'published');
item.previous_publication = p.profileContext.publication;
await transition(id, 'suspend');
item.suspended = true;
q.save();
}
p = await open(id);
assert.equal(p.workflowState, 'suspended');
assert(!p.publicationState.public_catalog);
await actor('promoter');
p = await open(id);
assert.equal(p.workflowState, 'suspended');
await transition(id, 'request_revision');
item.reopened = true;
q.save();
}
await actor('promoter');
p = await open(id);
if (p.workflowState === 'needs_information')
await transition(id, 'submit');
await actor('validator');
p = await open(id);
if (p.workflowState === 'submitted')
await transition(id, 'start_review');
if (props.workflowState === 'in_review') {
for (const gate of props.canonicalOptions.gates)
if (
!item.steps.some(
(s) =>
s.action === 'review_gate' &&
s.gate === gate &&
s.status === 200,
)
)
await transition(id, 'review_gate', { gate });
await transition(id, 'approve');
}
await actor('publisher');
p = await open(id);
if (p.workflowState === 'approved') await transition(id, 'publish');
assert.equal(props.workflowState, 'published');
assert(
props.profileContext.publication_history.some(
(x) => x.hash === item.previous_publication.hash,
),
);
q.proof.checks.suspension_recovery_reapproval_republication_history = true;
} else if (mode === 'appeal') {
const id = 34;
q.proof.cases[id] ||= {
category: 'business_partnership',
steps: [],
};
const item = q.proof.cases[id];
let p;
if (!item.appeal_reopened) {
await actor('promoter');
p = await open(id);
if (!item.reopened) {
await transition(id, 'request_revision');
item.reopened = true;
q.save();
}
if (props.workflowState === 'needs_information')
await transition(id, 'submit');
await actor('validator');
p = await open(id);
if (!item.rejected) {
await transition(id, 'reject');
item.rejected = true;
q.save();
}
p = await open(id);
assert.equal(p.workflowState, 'rejected');
await actor('promoter');
p = await open(id);
if (!item.appealed) {
await transition(id, 'appeal');
item.appealed = true;
q.save();
}
assert(props.profileContext.appeal.pending);
await actor('validator');
p = await open(id);
assert.equal(p.profileContext.rejection.actor_id, 24);
assert(p.profileContext.appeal.pending);
assert(!p.profileActions.includes('appeal_reopen'));
q.proof.checks.rejector_has_no_appeal_decision_action = true;
await actor('bootstrap');
p = await open(id);
await transition(id, 'appeal_reopen');
item.appeal_reopened = true;
q.save();
assert.equal(props.workflowState, 'needs_information');
assert.equal(props.profileContext.appeal.pending, false);
assert.notEqual(
props.profileContext.rejection.actor_id,
props.profileContext.appeal.decision.actor_id,
);
q.proof.checks.independent_appeal_reopened_with_history = true;
}
await actor('promoter');
p = await open(id);
if (p.workflowState === 'needs_information')
await transition(id, 'submit');
await actor('validator');
p = await open(id);
if (p.workflowState === 'submitted')
await transition(id, 'start_review');
if (props.workflowState === 'in_review') {
for (const gate of props.canonicalOptions.gates)
if (
!item.steps.some(
(s) =>
s.action === 'review_gate' &&
s.gate === gate &&
s.status === 200,
)
)
await transition(id, 'review_gate', { gate });
await transition(id, 'approve');
}
await actor('publisher');
p = await open(id);
if (p.workflowState === 'approved') await transition(id, 'publish');
assert.equal(props.workflowState, 'published');
assert.equal(
props.profileContext.approval.hash,
props.profileContext.publication.hash,
);
assert.notEqual(
props.profileContext.approval.actor_id,
props.profileContext.publication.actor_id,
);
assert(props.profileContext.rejection.reason);
assert.equal(props.profileContext.appeal.pending, false);
q.proof.checks.appeal_followed_by_review_and_republication = true;
}
q.proof.result = 'passed';
} catch (e) {
q.proof.result = 'failed';
q.proof.error = sanitized(e);
if (
page &&
!['/login', '/two-factor-challenge'].includes(
new URL(page.url()).pathname,
)
) {
q.proof.failure = {
route: new URL(page.url()).pathname,
alerts: await page
.getByRole('alert')
.allTextContents()
.catch(() => []),
labels: await page
.locator('label')
.allTextContents()
.catch(() => []),
buttons: await page
.getByRole('button')
.allTextContents()
.catch(() => []),
};
await q.shot(page, 'failure', false).catch(() => {});
}
} finally {
q.proof.completed_at = new Date().toISOString();
q.save();
await q.browser.close();
}
console.log(
JSON.stringify({
result: q.proof.result,
phase: q.proof.phase,
checks: q.proof.checks,
error: q.proof.error,
}),
);
if (q.proof.result === 'failed') process.exitCode = 1;
})();