@@ -0,0 +1,104 @@
|
||||
import { App } from '@inertiajs/react';
|
||||
import type { Page } from '@inertiajs/core';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { expect, test } from 'vitest';
|
||||
import { AiPlanSettings } from '@/components/ai-plan-settings';
|
||||
import { AiPreparationPreview } from '@/components/ai-preparation-preview';
|
||||
import type { AiPreparation } from '@/components/ai-preparation-preview';
|
||||
|
||||
const page = {
|
||||
component: 'company/parameters',
|
||||
url: '/administracao/parametrizacao',
|
||||
version: 'ai-plan',
|
||||
props: { errors: {} },
|
||||
clearHistory: false,
|
||||
encryptHistory: true,
|
||||
} satisfies Page;
|
||||
|
||||
test.each([true, false])(
|
||||
'AI preparation settings clearly save a plan rather than activate a provider (%s)',
|
||||
(ready) => {
|
||||
function Screen() {
|
||||
return (
|
||||
<AiPlanSettings
|
||||
ready={ready}
|
||||
version={3}
|
||||
domains={{
|
||||
procurement: 'Compras',
|
||||
people: 'Pessoas e contratações',
|
||||
}}
|
||||
scopes={{ process_state: 'Estado do processo' }}
|
||||
plan={{
|
||||
state: 'draft',
|
||||
provider: 'none',
|
||||
model: '',
|
||||
domains: [],
|
||||
data_scopes: [],
|
||||
monthly_budget_usd: '0.00',
|
||||
monthly_requests: 0,
|
||||
max_input_bytes: 4500,
|
||||
max_output_tokens: 1000,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const html = renderToString(
|
||||
<App initialPage={page} initialComponent={Screen} />,
|
||||
);
|
||||
expect(html.includes('<form')).toBe(ready);
|
||||
expect(html).toContain('não envia dados nem activa chamadas externas');
|
||||
if (ready) {
|
||||
expect(html).toContain('Guardar plano para pré-visualização');
|
||||
expect(html).toContain('ainda não existe contador mensal');
|
||||
expect(html).toContain('Orçamento mensal planeado (USD)');
|
||||
}
|
||||
expect(html).not.toContain('type="password"');
|
||||
},
|
||||
);
|
||||
|
||||
test.each([
|
||||
'not_configured',
|
||||
'outside_scope',
|
||||
'no_permitted_facts',
|
||||
'input_exceeds_limit',
|
||||
'draft',
|
||||
] as const)(
|
||||
'AI preview handles %s without exposing an external execution action',
|
||||
(status) => {
|
||||
const data: AiPreparation = {
|
||||
status,
|
||||
external_execution_enabled: false,
|
||||
usage_checked: false,
|
||||
policy_revision: 2,
|
||||
provider: 'openai',
|
||||
model: 'gpt-5.6-terra',
|
||||
max_input_bytes: 4500,
|
||||
max_output_tokens: 1000,
|
||||
input_bytes: 123,
|
||||
facts:
|
||||
status === 'draft'
|
||||
? [
|
||||
{
|
||||
id: 'request',
|
||||
text: 'Pedido em aprovação.',
|
||||
source: {
|
||||
href: '#report-request',
|
||||
label: 'Identificação do pedido',
|
||||
pointer: 'requisition.status',
|
||||
},
|
||||
},
|
||||
]
|
||||
: [],
|
||||
};
|
||||
const html = renderToString(<AiPreparationPreview data={data} />);
|
||||
if (status === 'not_configured') expect(html).toBe('');
|
||||
else {
|
||||
expect(html).toContain('O plano não autoriza execução externa');
|
||||
expect(html).not.toContain('<form');
|
||||
expect(html).not.toContain('<button');
|
||||
if (status === 'draft')
|
||||
expect(html).toContain('href="#report-request"');
|
||||
else expect(html).not.toContain('Pedido em aprovação.');
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,72 @@
|
||||
import { App } from '@inertiajs/react';
|
||||
import type { Page } from '@inertiajs/core';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { expect, test } from 'vitest';
|
||||
import Entry from '@/pages/candidate/entry';
|
||||
import Profile from '@/pages/candidate/profile';
|
||||
|
||||
const page = {
|
||||
component: 'candidate/entry',
|
||||
url: '/candidaturas/1',
|
||||
version: 'candidate',
|
||||
props: { errors: {} },
|
||||
clearHistory: false,
|
||||
encryptHistory: true,
|
||||
} satisfies Page;
|
||||
test.each([true, false])(
|
||||
'registration availability (%s) preserves existing login and does not promise submission',
|
||||
(registration_open) => {
|
||||
const initialPage = {
|
||||
...page,
|
||||
props: {
|
||||
...page.props,
|
||||
company: { id: 1, name: 'Empresa fictícia' },
|
||||
portal: {
|
||||
version: 2,
|
||||
registration_open,
|
||||
notice: 'Aviso fictício da empresa.',
|
||||
},
|
||||
},
|
||||
};
|
||||
const html = renderToString(
|
||||
<App initialPage={initialPage} initialComponent={Entry} />,
|
||||
);
|
||||
expect(html).toContain('Entrar na minha área');
|
||||
expect(html.includes('Criar a minha conta')).toBe(registration_open);
|
||||
expect(html).toContain('Criar um perfil não constitui candidatura');
|
||||
expect(html).not.toContain('/dashboard');
|
||||
},
|
||||
);
|
||||
test('candidate profile renders geography and retained version without internal evaluation controls', () => {
|
||||
const initialPage = {
|
||||
...page,
|
||||
component: 'candidate/profile',
|
||||
props: {
|
||||
...page.props,
|
||||
companyId: 1,
|
||||
email: 'test@example.invalid',
|
||||
profile: {
|
||||
version: 3,
|
||||
data: {
|
||||
name: 'Pessoa fictícia',
|
||||
phone: '',
|
||||
summary: 'Perfil fictício.',
|
||||
availability: {
|
||||
province: 'Província fictícia',
|
||||
district: 'Distrito fictício',
|
||||
locality: 'Localidade fictícia',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const html = renderToString(
|
||||
<App initialPage={initialPage} initialComponent={Profile} />,
|
||||
);
|
||||
expect(html).toContain('value="Província fictícia"');
|
||||
expect(html).toContain('value="Distrito fictício"');
|
||||
expect(html).toContain('value="Localidade fictícia"');
|
||||
expect(html).toContain('Guardar nova versão do perfil');
|
||||
expect(html).not.toContain('Aprovar');
|
||||
expect(html).not.toContain('/pessoas');
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
import { App } from '@inertiajs/react';
|
||||
import type { Page } from '@inertiajs/core';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { expect, test } from 'vitest';
|
||||
import Opportunity from '@/pages/candidate/opportunity';
|
||||
import Receipt from '@/pages/candidate/receipt';
|
||||
import Access from '@/pages/candidate/access';
|
||||
|
||||
const page = {
|
||||
component: 'candidate/opportunity',
|
||||
url: '/candidaturas/1/oportunidades/1',
|
||||
version: 'submission',
|
||||
props: { errors: {} },
|
||||
clearHistory: false,
|
||||
encryptHistory: true,
|
||||
} satisfies Page;
|
||||
const definition = {
|
||||
fields: [
|
||||
{
|
||||
key: 'full_name',
|
||||
label: 'Nome completo',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'availability_location',
|
||||
label: 'Disponibilidade',
|
||||
type: 'location',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
documents: [],
|
||||
};
|
||||
test.each([true, false])(
|
||||
'actual submission form reuses geographic profile and requires email confirmation (%s)',
|
||||
(emailVerified) => {
|
||||
const initialPage = {
|
||||
...page,
|
||||
props: {
|
||||
...page.props,
|
||||
companyId: 1,
|
||||
email: 'candidate@example.invalid',
|
||||
emailVerified,
|
||||
opportunity: {
|
||||
id: 1,
|
||||
title: 'Oportunidade fictícia',
|
||||
description: 'Descrição pública',
|
||||
closes_on: '2026-11-10',
|
||||
publication_version: 1,
|
||||
form: { version: 2, definition },
|
||||
},
|
||||
profile: {
|
||||
version: 3,
|
||||
data: {
|
||||
name: 'Nome do perfil',
|
||||
summary: 'Perfil fictício',
|
||||
availability: {
|
||||
province: 'Província do perfil',
|
||||
district: 'Distrito do perfil',
|
||||
locality: 'Localidade do perfil',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const html = renderToString(
|
||||
<App initialPage={initialPage} initialComponent={Opportunity} />,
|
||||
);
|
||||
expect(html).toContain('value="Nome do perfil"');
|
||||
expect(html).toContain('value="Distrito do perfil"');
|
||||
expect(html).toContain('Submeter candidatura e obter comprovativo');
|
||||
expect(html.includes('disabled=""')).toBe(!emailVerified);
|
||||
},
|
||||
);
|
||||
test('receipt displays user labels and geographic values without exposing internal field keys or evaluation', () => {
|
||||
const initialPage = {
|
||||
...page,
|
||||
component: 'candidate/receipt',
|
||||
props: {
|
||||
...page.props,
|
||||
companyId: 1,
|
||||
receipt: {
|
||||
reference: 'CAND-TEST',
|
||||
received_at: '2026-10-30',
|
||||
snapshot: {
|
||||
opportunity: {
|
||||
title: 'Oportunidade',
|
||||
form: { definition },
|
||||
},
|
||||
profile: { version: 3 },
|
||||
answers: {
|
||||
availability_location: {
|
||||
province: 'Província fictícia',
|
||||
district: 'Distrito fictício',
|
||||
locality: 'Localidade fictícia',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const html = renderToString(
|
||||
<App initialPage={initialPage} initialComponent={Receipt} />,
|
||||
);
|
||||
expect(html).toContain('Disponibilidade');
|
||||
expect(html).toContain('Distrito fictício');
|
||||
expect(html).not.toContain('availability_location');
|
||||
expect(html).not.toContain('Avaliação interna');
|
||||
});
|
||||
test('recovery never claims delivery when the mail transport is absent', () => {
|
||||
const initialPage = {
|
||||
...page,
|
||||
component: 'candidate/access',
|
||||
props: {
|
||||
...page.props,
|
||||
companyId: 1,
|
||||
purpose: 'recuperar',
|
||||
emailDeliveryAvailable: false,
|
||||
accessStatus: null,
|
||||
},
|
||||
};
|
||||
const html = renderToString(
|
||||
<App initialPage={initialPage} initialComponent={Access} />,
|
||||
);
|
||||
expect(html).toContain('O envio de email ainda não está configurado');
|
||||
expect(html).toContain('disabled=""');
|
||||
});
|
||||
@@ -0,0 +1,96 @@
|
||||
import { App } from '@inertiajs/react';
|
||||
import type { Page } from '@inertiajs/core';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { expect, test } from 'vitest';
|
||||
import Submissions from '@/pages/people/submissions';
|
||||
import Receipt from '@/pages/candidate/receipt';
|
||||
|
||||
const page = {
|
||||
component: 'people/submissions',
|
||||
url: '/pessoas/1/recebidas',
|
||||
version: 'triage',
|
||||
props: { errors: {} },
|
||||
clearHistory: false,
|
||||
encryptHistory: true,
|
||||
} satisfies Page;
|
||||
const row = {
|
||||
id: 7,
|
||||
reference: 'CAND-TEST',
|
||||
received_at: '2026-10-30',
|
||||
decision: null,
|
||||
snapshot: {
|
||||
opportunity: {
|
||||
form: {
|
||||
definition: {
|
||||
fields: [{ key: 'full_name', label: 'Nome completo' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
profile: {
|
||||
data: { name: 'Pessoa fictícia', summary: 'Perfil fictício.' },
|
||||
},
|
||||
answers: { full_name: 'Pessoa fictícia' },
|
||||
},
|
||||
};
|
||||
test.each([true, false])(
|
||||
'internal triage editing follows assignment (%s) and keeps private and public messages separate',
|
||||
(canTriage) => {
|
||||
const initialPage = {
|
||||
...page,
|
||||
props: {
|
||||
...page.props,
|
||||
process: { id: 1, title: 'Processo fictício', version: 2 },
|
||||
publication: null,
|
||||
publicUrl: '/candidaturas/1/oportunidades/1',
|
||||
canPrepare: canTriage,
|
||||
canTriage,
|
||||
submissions: {
|
||||
data: [row],
|
||||
prev_page_url: null,
|
||||
next_page_url: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
const html = renderToString(
|
||||
<App initialPage={initialPage} initialComponent={Submissions} />,
|
||||
);
|
||||
expect(html).toContain('Pessoa fictícia');
|
||||
expect(html.includes('Registar decisão de triagem')).toBe(canTriage);
|
||||
expect(html.includes('Motivo interno (reservado)')).toBe(canTriage);
|
||||
expect(html.includes('Comunicação destinada ao candidato')).toBe(
|
||||
canTriage,
|
||||
);
|
||||
expect(html.includes('Guardar publicação')).toBe(canTriage);
|
||||
},
|
||||
);
|
||||
test('candidate receives a triage status that does not claim hiring or inclusion in a database', () => {
|
||||
const initialPage = {
|
||||
...page,
|
||||
component: 'candidate/receipt',
|
||||
props: {
|
||||
...page.props,
|
||||
companyId: 1,
|
||||
receipt: {
|
||||
reference: 'CAND-TEST',
|
||||
received_at: '2026-10-30',
|
||||
snapshot: {
|
||||
opportunity: { title: 'Processo' },
|
||||
profile: { version: 1 },
|
||||
answers: {},
|
||||
},
|
||||
},
|
||||
decision: {
|
||||
outcome: 'accepted_for_analysis',
|
||||
message: 'Comunicação aprovada para o titular.',
|
||||
decided_at: '2026-10-30',
|
||||
},
|
||||
},
|
||||
};
|
||||
const html = renderToString(
|
||||
<App initialPage={initialPage} initialComponent={Receipt} />,
|
||||
);
|
||||
expect(html).toContain('Aceite para análise');
|
||||
expect(html).toContain('Comunicação aprovada para o titular.');
|
||||
expect(html).not.toContain('Motivo interno');
|
||||
expect(html).toContain('não significa aceitação');
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { expect, test } from 'vitest';
|
||||
import { EvidenceBrief } from '@/components/evidence-brief';
|
||||
import { App } from '@inertiajs/react';
|
||||
import type { Page } from '@inertiajs/core';
|
||||
import ReviewReport from '@/pages/purchases/review-report';
|
||||
|
||||
test('real report renders source targets and preserves printable report sections', () => {
|
||||
const report = {
|
||||
generated_at: '2026-10-30T12:00:00Z',
|
||||
generated_by: 'Pessoa fictícia',
|
||||
company: 'Empresa fictícia',
|
||||
requisition: {
|
||||
id: 42,
|
||||
number: 'REQ-42',
|
||||
status: 'approved',
|
||||
justification: 'Necessidade fictícia',
|
||||
department: 'Operações',
|
||||
requester: 'Requisitante fictício',
|
||||
},
|
||||
comparison: {
|
||||
ready: false,
|
||||
top_count: 0,
|
||||
rows: [],
|
||||
policy: {
|
||||
revision: 0,
|
||||
configured: false,
|
||||
minimum_proposals: 3,
|
||||
minimum_evaluators: 2,
|
||||
weights: {
|
||||
price: 40,
|
||||
quality: 20,
|
||||
delivery: 20,
|
||||
history: 10,
|
||||
payment: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
recommendation: null,
|
||||
admission: null,
|
||||
qualification: null,
|
||||
opening: null,
|
||||
award: null,
|
||||
order: null,
|
||||
fulfilment: {
|
||||
receipts: [],
|
||||
finance_visible: false,
|
||||
invoices: null,
|
||||
payments: null,
|
||||
},
|
||||
};
|
||||
const page = {
|
||||
component: 'purchases/review-report',
|
||||
url: '/requisicoes/42/relatorio',
|
||||
version: 'brief',
|
||||
props: {
|
||||
errors: {},
|
||||
report,
|
||||
brief: {
|
||||
method: 'procurement_evidence_brief_v1',
|
||||
generated_at: report.generated_at,
|
||||
basis_sha256: 'a'.repeat(64),
|
||||
finance_visible: false,
|
||||
claims: [],
|
||||
},
|
||||
},
|
||||
clearHistory: false,
|
||||
encryptHistory: true,
|
||||
} satisfies Page;
|
||||
const html = renderToString(
|
||||
<App initialPage={page} initialComponent={ReviewReport} />,
|
||||
);
|
||||
for (const section of [
|
||||
'request',
|
||||
'comparison',
|
||||
'qualification',
|
||||
'records',
|
||||
'fulfilment',
|
||||
])
|
||||
expect(html).toContain(`id="report-${section}"`);
|
||||
expect(html).toContain('Síntese com fontes');
|
||||
expect(html).toContain('Imprimir esta consulta');
|
||||
expect(html).toContain('Necessidade fictícia');
|
||||
});
|
||||
|
||||
test.each([true, false])(
|
||||
'evidence brief exposes provenance and the correct financial visibility (%s)',
|
||||
(finance_visible) => {
|
||||
const html = renderToString(
|
||||
<EvidenceBrief
|
||||
data={{
|
||||
method: 'procurement_evidence_brief_v1',
|
||||
generated_at: '2026-10-30T12:00:00Z',
|
||||
basis_sha256: 'a'.repeat(64),
|
||||
finance_visible,
|
||||
claims: [
|
||||
{
|
||||
id: 'request',
|
||||
text: 'A requisição encontra-se aprovada. <script>alert(1)</script>',
|
||||
basis_sha256: 'b'.repeat(64),
|
||||
source: {
|
||||
label: 'Identificação do pedido',
|
||||
href: '#report-request',
|
||||
pointer: 'requisition.status',
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain('href="#report-request"');
|
||||
expect(html).toContain('sem modelo de IA activo');
|
||||
expect(html).toContain('não é uma assinatura ou aprovação');
|
||||
expect(html).not.toContain('<script>');
|
||||
expect(html).toContain('<script>');
|
||||
expect(html.includes('O âmbito financeiro está reservado')).toBe(
|
||||
!finance_visible,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
import { App } from '@inertiajs/react';
|
||||
import type { Page } from '@inertiajs/core';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { expect, test } from 'vitest';
|
||||
import { IntelligenceSettings } from '@/components/intelligence-settings';
|
||||
|
||||
test.each([true, false])(
|
||||
'intelligence settings expose local domain choices only when installed (%s)',
|
||||
(ready) => {
|
||||
const page = {
|
||||
component: 'company/parameters',
|
||||
url: '/administracao/parametrizacao',
|
||||
version: 'policy',
|
||||
props: { errors: {} },
|
||||
clearHistory: false,
|
||||
encryptHistory: true,
|
||||
} satisfies Page;
|
||||
function Screen() {
|
||||
return (
|
||||
<IntelligenceSettings
|
||||
ready={ready}
|
||||
version={2}
|
||||
policy={{
|
||||
revision: 1,
|
||||
configured: true,
|
||||
procurement_brief: false,
|
||||
people_brief: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const html = renderToString(
|
||||
<App initialPage={page} initialComponent={Screen} />,
|
||||
);
|
||||
expect(html).toContain('externo de documentos');
|
||||
expect(html.includes('<form')).toBe(ready);
|
||||
if (ready) {
|
||||
expect(html).toContain('Síntese com fontes em Compras');
|
||||
expect(html).toContain('Síntese com fontes em Pessoas');
|
||||
} else expect(html).toContain('Instalação da configuração');
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,307 @@
|
||||
import { App } from '@inertiajs/react';
|
||||
import type { Page } from '@inertiajs/core';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { expect, test } from 'vitest';
|
||||
import PeopleIndex from '@/pages/people';
|
||||
import PeopleShow from '@/pages/people/show';
|
||||
import PeopleAccess from '@/pages/people/access';
|
||||
import { PeopleClarifications } from '@/components/people-clarifications';
|
||||
import type { ClarificationData } from '@/components/people-clarifications';
|
||||
import { PeopleOperationalInsights } from '@/components/people-operational-insights';
|
||||
|
||||
const base = {
|
||||
component: 'people/index',
|
||||
url: '/pessoas',
|
||||
version: 'people-regression',
|
||||
props: { errors: {} },
|
||||
clearHistory: false,
|
||||
encryptHistory: false,
|
||||
} satisfies Page;
|
||||
|
||||
test('operational reading explains its scope and missing deadlines without suggesting candidate decisions', () => {
|
||||
function Screen() {
|
||||
return (
|
||||
<PeopleOperationalInsights
|
||||
data={{
|
||||
method: 'operational_facts_v1',
|
||||
generated_at: '2026-10-30T12:00:00Z',
|
||||
coverage: { processes: 2, tasks: 3, approvals: 1 },
|
||||
overdue: {
|
||||
items: 2,
|
||||
processes: 1,
|
||||
maximum_days: 7,
|
||||
oldest: [
|
||||
{
|
||||
id: 'clarification-7',
|
||||
reference: 'PES-42',
|
||||
label: 'Prazo vencido',
|
||||
due_on: '2026-10-23',
|
||||
days_overdue: 7,
|
||||
href: '/pessoas/42#clarification-7',
|
||||
},
|
||||
],
|
||||
},
|
||||
task_actions: { evaluate: 3 },
|
||||
tasks_without_deadline: 3,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const html = renderToString(
|
||||
<App initialPage={base} initialComponent={Screen} />,
|
||||
);
|
||||
const text = html.replace(/<!--.*?-->/g, '');
|
||||
expect(text).toContain('2 processos autorizados');
|
||||
expect(text).toContain('2 prazos vencidos em');
|
||||
expect(text).toContain('7 dias de calendário');
|
||||
expect(html).toContain('/pessoas/42#clarification-7');
|
||||
expect(html).toContain('esta ausência não indica incumprimento');
|
||||
expect(html).toContain('sem previsão por machine learning');
|
||||
expect(html).not.toContain('<form');
|
||||
});
|
||||
|
||||
test('both hiring paths are available in the real creation form', () => {
|
||||
const page = {
|
||||
...base,
|
||||
props: {
|
||||
...base.props,
|
||||
ready: true,
|
||||
kinds: ['service', 'staff'],
|
||||
canPrepare: ['service', 'staff'],
|
||||
processes: {
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Processo fictício',
|
||||
kind: 'service',
|
||||
status: 'contract_draft',
|
||||
closes_on: '2026-10-30',
|
||||
},
|
||||
],
|
||||
links: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const html = renderToString(
|
||||
<App initialPage={page} initialComponent={PeopleIndex} />,
|
||||
);
|
||||
expect(html).toContain('Prestador individual');
|
||||
expect(html).toContain('Trabalhador para o quadro');
|
||||
expect(html).toContain('Critérios de avaliação');
|
||||
expect(html).toContain('Minuta contratual preparada');
|
||||
});
|
||||
|
||||
test.each(['service', 'staff'])(
|
||||
'the %s hiring report and evaluation forms render without object children',
|
||||
(kind) => {
|
||||
const process = {
|
||||
id: 1,
|
||||
title: 'Processo fictício',
|
||||
purpose: 'Âmbito fictício',
|
||||
kind,
|
||||
status: 'assessment',
|
||||
version: 4,
|
||||
closes_on: '2026-10-30',
|
||||
rules: {
|
||||
minimum_reviews: 1,
|
||||
criteria: [{ label: 'Competências', weight: 100 }],
|
||||
},
|
||||
selection: null,
|
||||
selected_application_id: null,
|
||||
contract: null,
|
||||
};
|
||||
const page = {
|
||||
...base,
|
||||
component: 'people/show',
|
||||
props: {
|
||||
...base.props,
|
||||
process,
|
||||
brief: {
|
||||
method: 'people_evidence_brief_v1',
|
||||
generated_at: '2026-10-30T12:00:00Z',
|
||||
basis_sha256: 'a'.repeat(64),
|
||||
finance_visible: false,
|
||||
claims: [
|
||||
{
|
||||
id: 'candidates',
|
||||
text: 'Matriz actual, sem selecção automática.',
|
||||
basis_sha256: 'b'.repeat(64),
|
||||
source: {
|
||||
href: '#people-report-candidates',
|
||||
label: 'Candidaturas e avaliação',
|
||||
pointer: 'candidates',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
candidates: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Pessoa fictícia',
|
||||
reference: 'CAND-1',
|
||||
summary: 'Resumo profissional fictício',
|
||||
received_on: '2026-10-30',
|
||||
evaluation_count: 0,
|
||||
score_minor: null,
|
||||
reviews: [],
|
||||
},
|
||||
],
|
||||
canPrepare: true,
|
||||
canReview: true,
|
||||
history: [],
|
||||
},
|
||||
};
|
||||
const html = renderToString(
|
||||
<App initialPage={page} initialComponent={PeopleShow} />,
|
||||
);
|
||||
expect(html).toContain('Registar avaliação');
|
||||
expect(html).toContain('Propor selecção');
|
||||
expect(html).toContain('Pessoa fictícia');
|
||||
expect(html).toContain('Síntese com fontes');
|
||||
expect(html).toContain('href="#people-report-candidates"');
|
||||
expect(html).toContain('id="people-report-candidates"');
|
||||
expect(html).toContain('id="people-report-scope"');
|
||||
},
|
||||
);
|
||||
|
||||
test('team configuration renders without requiring or displaying candidate content', () => {
|
||||
const page = {
|
||||
...base,
|
||||
component: 'people/access',
|
||||
props: {
|
||||
...base.props,
|
||||
process: { id: 1, kind: 'staff', status: 'assessment' },
|
||||
version: 3,
|
||||
scopes: { view: 'Consultar processo', approve: 'Decidir selecção' },
|
||||
users: [{ id: 1, name: 'Responsável fictício' }],
|
||||
assignments: [
|
||||
{
|
||||
user_id: 1,
|
||||
scopes: ['view'],
|
||||
expires_at: '2030-01-01 12:00:00',
|
||||
},
|
||||
],
|
||||
canView: false,
|
||||
},
|
||||
};
|
||||
const html = renderToString(
|
||||
<App initialPage={page} initialComponent={PeopleAccess} />,
|
||||
);
|
||||
expect(html).toContain('Responsável fictício');
|
||||
expect(html).not.toContain('href="/pessoas/1"');
|
||||
expect(html).toContain('Guardar atribuição');
|
||||
});
|
||||
|
||||
test('personal tasks and approvals render as separate queues linked to original processes', () => {
|
||||
const item = {
|
||||
id: '1:evaluate:99',
|
||||
process_id: 1,
|
||||
reference: 'PES-1',
|
||||
title: 'Vaga fictícia',
|
||||
kind: 'staff',
|
||||
status: 'assessment',
|
||||
action: 'evaluate',
|
||||
label: 'Avaliar candidatura #99',
|
||||
due_on: null,
|
||||
href: '/pessoas/1?candidatura=99#people-evaluate',
|
||||
};
|
||||
const page = {
|
||||
...base,
|
||||
props: {
|
||||
...base.props,
|
||||
ready: true,
|
||||
kinds: ['staff'],
|
||||
canPrepare: [],
|
||||
processes: { data: [], links: [] },
|
||||
company: { name: 'Entidade fictícia' },
|
||||
workdesk: {
|
||||
updated_at: '2026-10-30T12:00:00Z',
|
||||
counts: {
|
||||
processes: 1,
|
||||
active: 1,
|
||||
contract_drafts: 0,
|
||||
tasks: 1,
|
||||
approvals: 0,
|
||||
overdue: 0,
|
||||
},
|
||||
tasks: [item],
|
||||
approvals: [],
|
||||
alerts: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const html = renderToString(
|
||||
<App initialPage={page} initialComponent={PeopleIndex} />,
|
||||
);
|
||||
expect(html).toContain('As minhas tarefas');
|
||||
expect(html).toContain('Sem decisões formais pendentes');
|
||||
expect(html).toContain('href="/pessoas/1?candidatura=99#people-evaluate"');
|
||||
expect(html).toContain('Entidade fictícia');
|
||||
expect(html).toContain('Actualizar');
|
||||
});
|
||||
|
||||
test('assigned clarification response renders without requiring broader interview access', () => {
|
||||
const data: ClarificationData = {
|
||||
ready: true,
|
||||
sources: [],
|
||||
users: [],
|
||||
items: [
|
||||
{
|
||||
id: 7,
|
||||
application_id: 1,
|
||||
version: 1,
|
||||
status: 'open',
|
||||
due_on: '2026-10-30',
|
||||
outcome: null,
|
||||
question: 'Confirmar o comprovativo documental.',
|
||||
canRespond: true,
|
||||
canReview: false,
|
||||
canReassign: false,
|
||||
responses: [],
|
||||
decisions: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
function Screen() {
|
||||
return <PeopleClarifications processId={1} data={data} />;
|
||||
}
|
||||
const html = renderToString(
|
||||
<App initialPage={base} initialComponent={Screen} />,
|
||||
);
|
||||
expect(html).toContain('Confirmar o comprovativo documental.');
|
||||
expect(html).toContain('Registar resposta');
|
||||
expect(html).not.toContain('Registar conferência');
|
||||
expect(html).toContain('id="clarification-7"');
|
||||
});
|
||||
|
||||
test('restricted clarification metadata distinguishes unproven information without exposing evidence', () => {
|
||||
const data: ClarificationData = {
|
||||
ready: true,
|
||||
sources: [],
|
||||
users: [],
|
||||
preservedDecision: true,
|
||||
items: [
|
||||
{
|
||||
id: 7,
|
||||
application_id: 1,
|
||||
version: 3,
|
||||
status: 'resolved',
|
||||
due_on: '2026-10-30',
|
||||
outcome: 'insufficient',
|
||||
canRespond: false,
|
||||
canReview: false,
|
||||
canReassign: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
function Screen() {
|
||||
return <PeopleClarifications processId={1} data={data} />;
|
||||
}
|
||||
const html = renderToString(
|
||||
<App initialPage={base} initialComponent={Screen} />,
|
||||
);
|
||||
expect(html).toContain('Informação permanece por comprovar');
|
||||
expect(html).toContain('Conteúdo reservado');
|
||||
expect(html).toContain('conserva a base registada na decisão');
|
||||
expect(html).not.toContain('<form');
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
import { App } from '@inertiajs/react';
|
||||
import type { Page } from '@inertiajs/core';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { expect, test } from 'vitest';
|
||||
import Registry from '@/pages/people/registry';
|
||||
import PeopleShow from '@/pages/people/show';
|
||||
import { PeopleRegistryChoice } from '@/components/people-registry-choice';
|
||||
|
||||
const page = {
|
||||
component: 'people/registry',
|
||||
url: '/pessoas/cadastro',
|
||||
version: 'registry',
|
||||
props: { errors: {} },
|
||||
clearHistory: false,
|
||||
encryptHistory: true,
|
||||
} satisfies Page;
|
||||
const person = {
|
||||
id: 7,
|
||||
name: 'Pessoa do cadastro',
|
||||
reference: 'CAD-7',
|
||||
profile_summary: 'Perfil profissional reutilizável.',
|
||||
active: true,
|
||||
version: 2,
|
||||
canEdit: false,
|
||||
};
|
||||
|
||||
test.each([true, false])(
|
||||
'registry respects directory editing authority (%s) without linking to restricted processes',
|
||||
(canCreate) => {
|
||||
const initialPage = {
|
||||
...page,
|
||||
props: {
|
||||
...page.props,
|
||||
ready: true,
|
||||
canCreate,
|
||||
records: {
|
||||
data: [person],
|
||||
total: 1,
|
||||
prev_page_url: null,
|
||||
next_page_url: null,
|
||||
},
|
||||
filters: {},
|
||||
},
|
||||
};
|
||||
const html = renderToString(
|
||||
<App initialPage={initialPage} initialComponent={Registry} />,
|
||||
);
|
||||
expect(html).toContain('Pessoa do cadastro');
|
||||
expect(html).toContain('Perfil profissional reutilizável.');
|
||||
expect(html.includes('Registar pessoa</button>')).toBe(canCreate);
|
||||
expect(html).not.toContain('Guardar nova versão');
|
||||
expect(html).not.toContain('/pessoas/7');
|
||||
},
|
||||
);
|
||||
|
||||
test('process registry selection identifies source version without repeating an entire registration form', () => {
|
||||
const html = renderToString(
|
||||
<PeopleRegistryChoice
|
||||
processId={1}
|
||||
selected="7"
|
||||
data={{
|
||||
ready: true,
|
||||
canCreate: true,
|
||||
search: 'CAD',
|
||||
items: [person],
|
||||
}}
|
||||
choose={() => {}}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain('CAD-7');
|
||||
expect(html.replace(/<!--.*?-->/g, '')).toContain('v2');
|
||||
expect(html).toContain('Pesquisar cadastro');
|
||||
expect(html).toContain('A candidatura conserva a versão');
|
||||
expect(html).not.toContain('<form');
|
||||
});
|
||||
|
||||
test('real reception form offers reuse and saving the first profile in the same flow', () => {
|
||||
const initialPage = {
|
||||
...page,
|
||||
component: 'people/show',
|
||||
url: '/pessoas/1',
|
||||
props: {
|
||||
...page.props,
|
||||
process: {
|
||||
id: 1,
|
||||
title: 'Recepção fictícia',
|
||||
purpose: 'Âmbito fictício',
|
||||
kind: 'service',
|
||||
status: 'open',
|
||||
version: 2,
|
||||
closes_on: '2026-10-30',
|
||||
rules: {
|
||||
minimum_reviews: 1,
|
||||
criteria: [{ label: 'Competências', weight: 100 }],
|
||||
},
|
||||
selection: null,
|
||||
selected_application_id: null,
|
||||
contract: null,
|
||||
},
|
||||
candidates: [],
|
||||
canPrepare: true,
|
||||
canReview: false,
|
||||
history: [],
|
||||
registry: {
|
||||
ready: true,
|
||||
canCreate: true,
|
||||
search: 'CAD',
|
||||
items: [person],
|
||||
},
|
||||
},
|
||||
};
|
||||
const html = renderToString(
|
||||
<App initialPage={initialPage} initialComponent={PeopleShow} />,
|
||||
);
|
||||
expect(html).toContain('Reutilizar pessoa do cadastro');
|
||||
expect(html).toContain('Guardar também no meu cadastro para reutilizar');
|
||||
expect(html).toContain('CAD-7');
|
||||
expect(html).toContain('Referência documental da candidatura');
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { App } from '@inertiajs/react';
|
||||
import type { Page } from '@inertiajs/core';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import type { ReactNode } from 'react';
|
||||
import { expect, test } from 'vitest';
|
||||
import PreviewLayout from '@/layouts/preview-layout';
|
||||
import Preview from '@/pages/preview';
|
||||
import { previewPaths } from '@/lib/preview-navigation';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
|
||||
const page = {
|
||||
component: 'preview/index',
|
||||
url: '/proposta/principal',
|
||||
version: 'regression',
|
||||
props: {
|
||||
errors: {},
|
||||
name: 'SGC',
|
||||
canRequest: false,
|
||||
auth: { user: null },
|
||||
organizationAccess: {},
|
||||
payablesAvailable: false,
|
||||
sidebarOpen: true,
|
||||
initialScreen: 'overview',
|
||||
},
|
||||
clearHistory: false,
|
||||
encryptHistory: false,
|
||||
} satisfies Page;
|
||||
|
||||
test('Inertia default preview layout renders the child page instead of its shared props', () => {
|
||||
function Screen() {
|
||||
return <main>Conteúdo da página de demonstração</main>;
|
||||
}
|
||||
const html = renderToString(
|
||||
<App
|
||||
initialPage={page}
|
||||
initialComponent={Screen}
|
||||
defaultLayout={() => PreviewLayout}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain('Conteúdo da página de demonstração');
|
||||
expect(html).toContain('Dados fictícios');
|
||||
expect(html).toContain('sgc-main');
|
||||
});
|
||||
|
||||
test('the former default layout reproduces the reported props object crash', () => {
|
||||
const formerLayout = (props: ReactNode) => (
|
||||
<PreviewLayout>{props}</PreviewLayout>
|
||||
);
|
||||
function Screen() {
|
||||
return <main>Conteúdo</main>;
|
||||
}
|
||||
expect(() =>
|
||||
renderToString(
|
||||
<App
|
||||
initialPage={page}
|
||||
initialComponent={Screen}
|
||||
defaultLayout={() => formerLayout}
|
||||
/>,
|
||||
),
|
||||
).toThrow(/Objects are not valid as a React child.*initialScreen/);
|
||||
});
|
||||
|
||||
test.each(Object.entries(previewPaths))(
|
||||
'preview screen %s renders through the real Inertia adapter',
|
||||
(screen, url) => {
|
||||
const html = renderToString(
|
||||
<TooltipProvider>
|
||||
<App
|
||||
initialPage={{
|
||||
...page,
|
||||
url,
|
||||
props: { ...page.props, initialScreen: screen },
|
||||
}}
|
||||
initialComponent={Preview}
|
||||
defaultLayout={() => PreviewLayout}
|
||||
/>
|
||||
</TooltipProvider>,
|
||||
);
|
||||
expect(html).toContain('Dados fictícios');
|
||||
expect(html).toContain('sgc-main');
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,62 @@
|
||||
import { App } from '@inertiajs/react';
|
||||
import type { Page } from '@inertiajs/core';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { expect, test } from 'vitest';
|
||||
import { ProcurementOperationalInsights } from '@/components/procurement-operational-insights';
|
||||
import type { ProcurementInsights } from '@/components/procurement-operational-insights';
|
||||
|
||||
const base = {
|
||||
component: 'reports/procurement',
|
||||
url: '/relatorios',
|
||||
version: 'insights',
|
||||
props: { errors: {} },
|
||||
clearHistory: false,
|
||||
encryptHistory: true,
|
||||
} satisfies Page;
|
||||
|
||||
test.each([0, 2])(
|
||||
'procurement reading renders scope %i and source links with honest limitations',
|
||||
(processes) => {
|
||||
const data: ProcurementInsights = {
|
||||
method: 'procurement_facts_v1',
|
||||
generated_at: '2026-10-30T12:00:00Z',
|
||||
processes,
|
||||
pending: {
|
||||
count: 1,
|
||||
unknown_submission_age: 0,
|
||||
oldest: [
|
||||
{
|
||||
id: 42,
|
||||
number: 'REQ-42',
|
||||
submitted_at: '2026-10-23T12:00:00Z',
|
||||
days_since_submission: 7,
|
||||
href: '/requisicoes/42/processo',
|
||||
},
|
||||
],
|
||||
},
|
||||
unapproved_past_needed_date: 1,
|
||||
approved: {
|
||||
count: 1,
|
||||
without_registered_quotes: 0,
|
||||
without_visible_order: 0,
|
||||
},
|
||||
};
|
||||
function Screen() {
|
||||
return <ProcurementOperationalInsights data={data} />;
|
||||
}
|
||||
const html = renderToString(
|
||||
<App initialPage={base} initialComponent={Screen} />,
|
||||
).replace(/<!--.*?-->/g, '');
|
||||
expect(html).toContain(`${processes} requisições autorizadas`);
|
||||
if (processes === 0) {
|
||||
expect(html).toContain('Sem requisições neste âmbito');
|
||||
expect(html).not.toContain('REQ-42');
|
||||
} else {
|
||||
expect(html).toContain('/requisicoes/42/processo');
|
||||
expect(html).toContain('7 dias de calendário');
|
||||
expect(html).toContain('não comprova atraso de entrega');
|
||||
expect(html).toContain('não significam propostas válidas');
|
||||
}
|
||||
expect(html).not.toContain('<form');
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,40 @@
|
||||
import { App } from '@inertiajs/react';
|
||||
import type { Page } from '@inertiajs/core';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { expect, test } from 'vitest';
|
||||
import RequisitionForm from '@/pages/requisitions/form';
|
||||
import Catalogue from '@/pages/stock/catalogue';
|
||||
|
||||
const page = {
|
||||
component: 'requisitions/form', url: '/requisicoes/1/editar', version: 'catalogue',
|
||||
props: { errors: {} }, clearHistory: false, encryptHistory: true,
|
||||
} satisfies Page;
|
||||
|
||||
test('saved article version remains selected when the published source has changed', () => {
|
||||
const initialPage = { ...page, props: { ...page.props, costCenters: [],
|
||||
requisition: { id: 1, cost_center_id: 1, priority: 'normal', needed_by: '2026-11-10', justification: 'Pedido', items: [{
|
||||
id: 4, catalogue_entry_id: 7, catalogue_snapshot: { version: 1, code: 'ART-7' },
|
||||
description: 'Nome original', category: 'Material', purchase_type: 'consumable', quantity: '2', unit: 'cx', unit_price_minor: 1000, tax_rate: '16',
|
||||
}] },
|
||||
catalogue: { ready: true, search: '', more: false, items: [{ id: 7, version: 2, stock_item_id: 3, code: 'ART-7', description: 'Nome actualizado', unit: 'cx', category: 'Material', purchase_type: 'consumable' }] },
|
||||
} };
|
||||
const html = renderToString(<App initialPage={initialPage} initialComponent={RequisitionForm} />);
|
||||
expect(html).toMatch(/<option value="7:1" selected="">/);
|
||||
expect(html).toContain('<option value="7:2">');
|
||||
expect(html).toContain('value="Nome original"');
|
||||
expect(html).toMatch(/readonly=""/i);
|
||||
expect(html).toContain('Preenchimento livre');
|
||||
});
|
||||
|
||||
test.each([true, false])('catalogue editing follows authority (%s)', (canManage) => {
|
||||
const initialPage = { ...page, component: 'stock/catalogue', props: { ...page.props,
|
||||
ready: true, canManage, filters: {}, records: { total: 1, prev_page_url: null, next_page_url: null, data: [{
|
||||
id: 3, code: 'ART-7', name: 'Artigo existente', unit: 'cx', stock_basis: 'a'.repeat(64), version: 1,
|
||||
category: 'Material', purchase_type: 'consumable', published: true, source_current: true,
|
||||
}] },
|
||||
} };
|
||||
const html = renderToString(<App initialPage={initialPage} initialComponent={Catalogue} />);
|
||||
expect(html).toContain('Artigo existente');
|
||||
expect(html.includes('Guardar versão do catálogo')).toBe(canManage);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { App } from '@inertiajs/react';
|
||||
import type { Page } from '@inertiajs/core';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { expect, test } from 'vitest';
|
||||
import SubmissionForms from '@/pages/company/submission-forms';
|
||||
|
||||
const page = {
|
||||
component: 'company/submission-forms',
|
||||
url: '/administracao/candidaturas/formularios',
|
||||
version: 'forms',
|
||||
props: { errors: {} },
|
||||
clearHistory: false,
|
||||
encryptHistory: true,
|
||||
} satisfies Page;
|
||||
|
||||
test.each([true, false])(
|
||||
'form editor preserves revision and respects write authority (%s)',
|
||||
(canManage) => {
|
||||
const definition = {
|
||||
available: false,
|
||||
fields: [
|
||||
{
|
||||
key: 'availability_location',
|
||||
label: 'Local de disponibilidade',
|
||||
type: 'location',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: 'full_name',
|
||||
label: 'Nome completo',
|
||||
type: 'text',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
documents: [
|
||||
{
|
||||
key: 'cv',
|
||||
label: 'CV',
|
||||
formats: ['pdf'],
|
||||
max_kb: 2048,
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
const initialPage = {
|
||||
...page,
|
||||
props: {
|
||||
...page.props,
|
||||
ready: true,
|
||||
canManage,
|
||||
externalPortalAvailable: false,
|
||||
forms: [
|
||||
{
|
||||
audience: 'staff',
|
||||
modality: 'competition',
|
||||
version: 2,
|
||||
definition,
|
||||
history: [
|
||||
{
|
||||
id: 1,
|
||||
version: 1,
|
||||
created_by: 4,
|
||||
created_at: '2026-10-30',
|
||||
reason: 'Definição anterior preservada',
|
||||
definition,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
const html = renderToString(
|
||||
<App
|
||||
initialPage={initialPage}
|
||||
initialComponent={SubmissionForms}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain('Recrutamento');
|
||||
expect(html).toContain('Nome completo');
|
||||
expect(html).toContain('Definição anterior preservada');
|
||||
expect(html).toContain('Ainda não abre candidaturas externas');
|
||||
expect(html.includes('Guardar nova versão')).toBe(canManage);
|
||||
expect(html).toContain('placeholder="Província"');
|
||||
expect(html).toContain('placeholder="Distrito"');
|
||||
expect(html).toContain('placeholder="Localidade"');
|
||||
expect(html.includes('<fieldset disabled="" class="space-y-5"')).toBe(
|
||||
!canManage,
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user