484 lines
20 KiB
JavaScript
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;
|
|
})();
|