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( /]*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; })();