This commit is contained in:
2026-11-13 09:54:01 +02:00
33 changed files with 3442 additions and 314 deletions
+111
View File
@@ -0,0 +1,111 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\SectorResourceReferences;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia;
use Tests\TestCase;
final class EnergyReferenceTest extends TestCase
{
use DatabaseTransactions;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
$identity = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $identity->name);
$this->assertSame(55441, (int) $identity->port);
config(['investor-path.enabled' => true, 'investor-path.private_preparation' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]);
Http::preventStrayRequests();
}
private function choice(): array
{
return ['area_id' => 'ENE-07', 'group_id' => 'EN-SOLAR', 'location_label' => 'Niassa', 'source_sha256' => SectorResourceReferences::SOURCES['energia-especifica']];
}
public function test_energy_source_preserves_named_places_solar_discrepancies_and_project_states_without_activating_rules(): void
{
$service = app(SectorResourceReferences::class);
$raw = $service->data('energia-especifica');
$view = $service->view('energia-especifica');
$this->assertCount(7, $view['groups']);
$this->assertCount(11, $view['areas']);
$this->assertCount(10, $view['projects']);
$this->assertCount(24, $view['sources']);
$this->assertCount(4, $view['risks']);
$this->assertCount(6, $raw['original_tables']);
$this->assertArrayNotHasKey('original_tables', $view);
foreach ($view['areas'] as $area) {
$this->assertNull($area['coordinates']);
$this->assertFalse($area['promotion_ready']);
}
$solar = implode(' ', $view['projects']);
$this->assertStringContainsString('47,3 milhões EUR', $solar);
$this->assertStringContainsString('56 milhões USD', $solar);
$this->assertStringContainsString('15 MWac', $solar);
$this->assertStringContainsString('após fecho financeiro', $solar);
$upstream = $service->selection('energia-especifica', ['area_id' => 'ENE-01', 'location_label' => 'Buzi', 'source_sha256' => $view['source_sha256']]);
$this->assertSame('Buzi', $upstream['location_label']);
$this->assertArrayNotHasKey('province_id', $upstream);
$unknown = $service->selection('energia-especifica', ['area_id' => 'ENE-09', 'source_sha256' => $view['source_sha256']]);
$this->assertNull($unknown['location_label']);
$this->assertSame('source_area_reference', $unknown['territory_precision']);
file_put_contents(base_path('outputs/energy-reference-20261111/public-view.json'), json_encode($view, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
$this->get('/sectores/energia-especifica')->assertOk()->assertInertia(fn (AssertableInertia $page) => $page->component('portal/services')->has('sectorEntry.resource_reference.groups', 7));
}
public function test_energy_new_dossier_and_existing_mining_dossier_keep_both_sources_and_recover_swot_evidence(): void
{
$this->actingAs(User::factory()->create());
$before = DB::table('opportunities')->count();
$selection = $this->choice();
$start = $this->getJson('/investimentos/a-partir-do-setor/energia-especifica?'.http_build_query(['resource_area' => $selection['area_id'], 'resource_location' => $selection['location_label'], 'resource_group' => $selection['group_id'], 'resource_source' => $selection['source_sha256']]))->assertOk();
$this->assertSame('EN-SOLAR', $start->json('resourceSource.group_id'));
file_put_contents(base_path('outputs/energy-reference-20261111/start-page.json'), json_encode($start->json(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
$body = ['title' => 'SYNTHETIC energy dossier', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true, 'sector_slug' => 'energia-especifica', 'sector_reference' => $selection];
$id = $this->postJson('/investimentos', $body)->assertCreated()->json('id');
$this->assertSame($id, $this->postJson('/investimentos', $body)->assertCreated()->json('id'));
$one = $this->getJson('/investimentos/'.$id)->assertOk()->json();
$this->assertCount(1, $one['sector_references']);
$this->assertSame('Niassa', $one['sector_references'][0]['location_label']);
$mining = ['area_id' => 'MIN-02', 'location_label' => 'Cabo Delgado', 'group_id' => 'MINERAL-04', 'source_sha256' => SectorResourceReferences::SOURCES['mineracao']];
$this->postJson('/investimentos/'.$id.'/sectores', ['version' => $one['case']['version'], 'sector_slug' => 'mineracao', 'sector_reference' => $mining])->assertOk();
$two = $this->getJson('/investimentos/'.$id)->assertOk()->json();
$this->assertCount(2, $two['sector_references']);
$this->assertCount(2, $two['sector_contexts']);
$this->assertSame($one['sector_references'][0], $two['sector_references'][0]);
$this->assertSame('SYNTHETIC energy dossier', $two['case']['title']);
$version = $two['case']['version'];
$this->postJson('/investimentos/'.$id.'/sectores', ['version' => $version - 1, 'sector_slug' => 'energia-especifica', 'sector_reference' => $selection])->assertOk();
$this->assertSame($version, $this->getJson('/investimentos/'.$id)->assertOk()->json('case.version'));
$workspace = $this->getJson('/investimentos/'.$id.'/preparacao')->assertOk();
$this->assertStringContainsString('Solar, baterias', $workspace->getContent());
$this->assertStringContainsString('Grafite', $workspace->getContent());
$this->assertSame($before, DB::table('opportunities')->count());
file_put_contents(base_path('outputs/energy-reference-20261111/recovered-dossier.json'), json_encode($two, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
$this->actingAs(User::factory()->create());
$this->postJson('/investimentos/'.$id.'/sectores', ['version' => $version, 'sector_slug' => 'energia-especifica', 'sector_reference' => $selection])->assertForbidden();
}
public function test_energy_reference_rejects_wrong_sector_location_group_and_changed_creation_intent(): void
{
$this->actingAs(User::factory()->create());
$body = ['title' => 'SYNTHETIC guarded energy dossier', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true, 'sector_slug' => 'energia-especifica', 'sector_reference' => $this->choice()];
$this->postJson('/investimentos', $body)->assertCreated();
$this->postJson('/investimentos', [...$body, 'sector_reference' => [...$this->choice(), 'location_label' => 'Cabo Delgado']])->assertStatus(409);
foreach ([['location_label' => 'Gaza'], ['group_id' => 'EN-WIND'], ['source_sha256' => str_repeat('a', 64)]] as $bad) {
$count = DB::table('desk_cases')->count();
$this->postJson('/investimentos', [...$body, 'client_key' => (string) Str::uuid(), 'sector_reference' => [...$this->choice(), ...$bad]])->assertStatus(isset($bad['source_sha256']) ? 409 : 422);
$this->assertSame($count, DB::table('desk_cases')->count());
}
$this->postJson('/investimentos', [...$body, 'client_key' => (string) Str::uuid(), 'sector_slug' => 'turismo'])->assertStatus(422);
}
}
+119
View File
@@ -0,0 +1,119 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Services\SectorInvestment;
use App\Services\SectorResourceReferences;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia;
use Tests\TestCase;
final class MiningReferenceTest extends TestCase
{
use DatabaseTransactions;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
$db = DB::selectOne('select current_database() as name,inet_server_port() as port');
$this->assertSame('simulator_fiscal_delta_20261021_test', $db->name);
$this->assertSame(55441, (int) $db->port);
config(['investor-path.enabled' => true, 'investor-path.private_preparation' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]);
Http::preventStrayRequests();
}
private function choice(string $area = 'MIN-02', ?string $province = 'Cabo Delgado'): array
{
return ['area_id' => $area, 'location_label' => $province, 'source_sha256' => SectorResourceReferences::SOURCES['mineracao']];
}
public function test_complete_reference_has_source_counts_distinct_metrics_and_no_invented_coordinates(): void
{
$service = app(SectorResourceReferences::class);
$data = $service->data('mineracao');
$this->assertCount(11, $data['minerals']);
$this->assertCount(10, $data['areas']);
$this->assertCount(12, $data['indicators']);
$this->assertCount(6, $data['projects']);
$this->assertCount(13, $data['sources']);
foreach ([...$data['minerals'], ...$data['areas']] as $row) {
$this->assertNull($row['coordinates']);
}
$view = $service->view('mineracao');
$this->assertArrayNotHasKey('original_paragraphs', $view);
$this->assertArrayNotHasKey('source_fields', $view['areas'][0]);
$this->assertCount(6, $view['risks']);
file_put_contents(base_path('outputs/mining-reference-20261111/public-view.json'), json_encode($view, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
$this->get('/sectores/mineracao')->assertOk()->assertInertia(fn (AssertableInertia $page) => $page->component('portal/services')->has('sectorEntry.resource_reference.groups', 11));
$this->assertStringNotContainsString('Próximos passos', json_encode($view['risks']));
$selection = $service->selection('mineracao', $this->choice('MIN-06', 'Maputo'));
$this->assertSame('province_or_city_unspecified', $selection['territory_precision']);
$this->assertFalse($selection['promotion_ready']);
$this->assertNull($selection['coordinates']);
}
public function test_native_sector_start_save_retry_recovery_and_swot_keep_same_reference_without_creating_opportunities(): void
{
$actor = User::factory()->create();
$this->actingAs($actor);
$before = DB::table('opportunities')->count();
$choice = [...$this->choice(), 'group_id' => 'MINERAL-04'];
$start = $this->getJson('/investimentos/a-partir-do-setor/mineracao?'.http_build_query(['resource_area' => $choice['area_id'], 'resource_location' => $choice['location_label'], 'resource_group' => $choice['group_id'], 'resource_source' => $choice['source_sha256']]))->assertOk();
$this->assertSame('Grafite', $start->json('resourceSource.group_name'));
file_put_contents(base_path('outputs/mining-reference-20261111/start-page.json'), json_encode($start->json(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
$body = ['title' => 'SYNTHETIC mining value-chain preparation', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true, 'sector_slug' => 'mineracao', 'sector_reference' => $choice];
$id = $this->postJson('/investimentos', $body)->assertCreated()->json('id');
$again = $this->postJson('/investimentos', $body)->assertCreated();
$this->assertSame($id, $again->json('id'));
$view = $this->getJson('/investimentos/'.$id)->assertOk()->json();
$this->assertCount(1, $view['sector_references']);
$this->assertSame('Cabo Delgado', $view['sector_references'][0]['location_label']);
$this->assertSame('MINERAL-04', $view['sector_references'][0]['group_id']);
$this->assertSame(SectorResourceReferences::SOURCES['mineracao'], $view['sector_references'][0]['source_sha256']);
$this->assertStringContainsString('Beneficiamento', $view['sector_references'][0]['value_chain']);
$this->assertCount(1, $view['sector_contexts']);
file_put_contents(base_path('outputs/mining-reference-20261111/recovered-dossier.json'), json_encode($view, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
$workspace = $this->getJson('/investimentos/'.$id.'/preparacao')->assertOk();
$this->assertStringContainsString('sector-resource:', $workspace->getContent());
$this->assertSame($before, DB::table('opportunities')->count());
$this->postJson('/investimentos', [...$body, 'sector_reference' => $this->choice('MIN-01', 'Tete')])->assertStatus(409);
$this->actingAs(User::factory()->create());
$this->getJson('/investimentos/'.$id)->assertForbidden();
}
public function test_existing_dossier_appends_distinct_area_and_preserves_manual_fields_and_previous_reference(): void
{
$actor = User::factory()->create();
$this->actingAs($actor);
$id = $this->postJson('/investimentos', ['title' => 'SYNTHETIC existing mining dossier', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id');
$service = app(SectorInvestment::class);
$version = $this->getJson('/investimentos/'.$id)->assertOk()->json('case.version');
$service->attach($actor, $id, ['version' => $version, 'sector_slug' => 'mineracao', 'sector_reference' => $this->choice()]);
$one = $this->getJson('/investimentos/'.$id)->assertOk()->json();
$service->attach($actor, $id, ['version' => $one['case']['version'], 'sector_slug' => 'mineracao', 'sector_reference' => $this->choice('MIN-01', 'Tete')]);
$two = $this->getJson('/investimentos/'.$id)->assertOk()->json();
$this->assertCount(2, $two['sector_references']);
$this->assertSame($one['sector_references'][0], $two['sector_references'][0]);
$this->assertSame('SYNTHETIC existing mining dossier', $two['case']['title']);
$this->assertCount(1, $two['sector_contexts']);
$service->attach($actor, $id, ['version' => $version, 'sector_slug' => 'mineracao', 'sector_reference' => $this->choice()]);
$this->assertSame($two['case']['version'], $this->getJson('/investimentos/'.$id)->json('case.version'));
DB::table('desk_cases')->where('id', $id)->update(['closed_at' => now()]);
$this->postJson('/investimentos/'.$id.'/sectores', ['version' => $two['case']['version'], 'sector_slug' => 'mineracao', 'sector_reference' => $this->choice('MIN-05', 'Cabo Delgado')])->assertStatus(409);
}
public function test_mismatched_source_province_and_mineral_are_refused_before_dossier_creation(): void
{
$this->actingAs(User::factory()->create());
$count = DB::table('desk_cases')->count();
foreach ([['source_sha256' => str_repeat('0', 64)], ['location_label' => 'Gaza'], ['group_id' => 'MINERAL-01']] as $bad) {
$response = $this->postJson('/investimentos', ['title' => 'SYNTHETIC invalid mineral', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true, 'sector_slug' => 'mineracao', 'sector_reference' => [...$this->choice(), ...$bad]]);
$this->assertContains($response->status(), [409, 422]);
}$this->assertSame($count, DB::table('desk_cases')->count());
}
}
+4 -7
View File
@@ -84,14 +84,11 @@ test("actual institution view keeps physical address separate from product cover
const html = renderToStaticMarkup(
React.createElement(InstitutionView, { value: details }),
);
assert.match(html, /Sede: SYNTHETIC Maputo office/);
assert.match(html, /Nacional, segundo a fonte/);
assert.match(html, /Condições: Por confirmar/);
assert.match(html, /Disponibilidade: Por confirmar/);
assert.match(html, /Sede: SYNTHETIC address/);
assert.match(html, /SYNTHETIC QA investment/);
assert.match(html, /https:\/\/example.invalid\/contact/);
assert.match(html, /https:\/\/example\.invalid\/source/);
assert.match(html, /Consultar informação da entidade/);
assert.match(html, /não é elegibilidade confirmada/);
assert.doesNotMatch(html,/Por confirmar|não é elegibilidade|SHA-256/);
});
test("adding a physical location does not fill product financing coverage", () => {
const { InstitutionFields } = load("components/funding-institution.tsx");
@@ -60,16 +60,26 @@ function load(file) {
}
: name === './shared'
? { windowLabels: {} }
: name === '@/lib/portal-api' ? {portalRequest:()=>{throw Error('Read-only rendering must not send requests');}} : name === '@/components/funding-classification'
? load('components/funding-classification.tsx')
: name === '@/components/funding-institution'
? load('components/funding-institution.tsx')
: name === '@/components/funding-dossier-choice'
? { FundingDossierChoice: () => null }
: name ===
'@/components/funding-criteria-assessment'
? load('components/funding-criteria-assessment.tsx')
: require(name),
: name === '@/lib/portal-api'
? {
portalRequest: () => {
throw Error(
'Read-only rendering must not send requests',
);
},
}
: name === '@/components/funding-classification'
? load('components/funding-classification.tsx')
: name === '@/components/funding-institution'
? load('components/funding-institution.tsx')
: name === '@/components/funding-dossier-choice'
? { FundingDossierChoice: () => null }
: name ===
'@/components/funding-criteria-assessment'
? load(
'components/funding-criteria-assessment.tsx',
)
: require(name),
module,
module.exports,
);
@@ -146,8 +156,143 @@ test('actual public cards and detail lead with description/products and keep imp
new RegExp(raw.title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
);
}
const out=new URL('../../outputs/funding-public-editorial-20261111/',import.meta.url);fs.mkdirSync(out,{recursive:true});const name=p.products.length?'bank-with-products':'investment-manager';fs.writeFileSync(new URL(name+'-index.html',out),index);fs.writeFileSync(new URL(name+'-detail.html',out),detail);fs.writeFileSync(new URL('verification.json',out),JSON.stringify({publishedSourceRecords:270,descriptionsPreserved:true,productsNotFabricated:true,actualPublicComponents:true,metadataRemovedFromPresentation:true,privateEditorialFieldsUnchanged:true,activeDataWrites:0,visualBrowserReview:false},null,2));
const out = new URL(
'../../outputs/funding-public-editorial-20261111/',
import.meta.url,
);
fs.mkdirSync(out, { recursive: true });
const name = p.products.length
? 'bank-with-products'
: 'investment-manager';
fs.writeFileSync(new URL(name + '-index.html', out), index);
fs.writeFileSync(new URL(name + '-detail.html', out), detail);
fs.writeFileSync(
new URL('verification.json', out),
JSON.stringify(
{
publishedSourceRecords: 270,
descriptionsPreserved: true,
productsNotFabricated: true,
actualPublicComponents: true,
metadataRemovedFromPresentation: true,
privateEditorialFieldsUnchanged: true,
activeDataWrites: 0,
visualBrowserReview: false,
},
null,
2,
),
);
assert.match(detail, /Descrição/);
if (p.products.length) assert.match(detail, /Soluções PME/);
}
});
test('Quorus and Moza use one shared clean profile in both list and detail', () => {
const Index = load('pages/funding/index.tsx').default,
Show = load('pages/funding/show.tsx').default;
const out = new URL(
'../../outputs/funding-uniform-menu-20261111/',
import.meta.url,
);
fs.mkdirSync(out, { recursive: true });
for (const title of ['Quorus', 'Moza Banco']) {
const raw = records.find((p) => p.title === title);
assert.ok(raw, title);
const p = { ...raw, windows: [], documents: [] },
item = {
id: 'synthetic',
version_id: 'synthetic-version',
version: 1,
payload: p,
};
const index = renderToStaticMarkup(
React.createElement(Index, {
items: { data: [item], links: [] },
filters: {},
kinds: {},
options: { territories: [], sectors: [] },
management: false,
}),
);
const detail = renderToStaticMarkup(
React.createElement(Show, {
record: item,
authenticated: false,
saved: null,
}),
);
for (const html of [
index.match(/<article class="card">([\s\S]*?)<\/article>/)?.[0] ??
index,
detail,
]) {
assert.doesNotMatch(
html,
/Por confirmar|por confirmar|correspondência confirmada|Instituição identificada|Entidade responsável|Sectores:|Cobertura|garantia de financiamento|Não encontrado|ficha|Sede ou contacto indicado|Informação institucional/,
);
assert.equal((html.match(/Produtos e serviços/g) || []).length, 1);
assert.equal((html.match(/Descrição/g) || []).length, 1);
}
if (title === 'Quorus') {
assert.equal(
(detail.match(/Entidade de corretagem de seguros/g) || [])
.length,
1,
);
assert.doesNotMatch(
detail,
/<h3[^>]*>Contactos|<h3[^>]*>Localização/,
);
} else {
assert.equal((detail.match(/21 342 000/g) || []).length, 1);
assert.equal(
(detail.match(/Rua dos Desportistas/g) || []).length,
1,
);
assert.equal(
(
detail.match(
/Tesouraria, investimento, construção e equipamento/g,
) || []
).length,
1,
);
assert.doesNotMatch(detail, /500\.000|30\.000\.000|Prime|spread/);
}
fs.writeFileSync(
new URL(title.replaceAll(' ', '-') + '-index.html', out),
index,
);
fs.writeFileSync(
new URL(title.replaceAll(' ', '-') + '-detail.html', out),
detail,
);
}
});
test('contact and location deduplication preserves different branches and original records', () => {
const moza = records.find((p) => p.title === 'Moza Banco'),
original = JSON.stringify(moza);
assert.equal(
presentation
.publicInstitutionContacts(moza.contacts)
.filter((c) => c.kind === 'phone').length,
1,
);
assert.equal(
presentation.publicInstitutionLocations(moza.locations).length,
1,
);
assert.equal(
presentation.publicInstitutionLocations([
...moza.locations,
{
...moza.locations[0],
role: 'branch',
address: 'Rua dos Desportistas, número 714',
},
]).length,
2,
);
assert.equal(JSON.stringify(moza), original);
});
+4 -4
View File
@@ -417,7 +417,7 @@ void test('installed concours has its own primary group and preserves public and
authenticated,
procurement: true,
});
const group = groups.find((g) => g.id === 'procurement-area');
const group = groups.find((g) => g.id === 'discovery').children.find(g=>g.id==='procurement-area');
assert.equal(group.label, 'Concursos e anúncios');
assert.equal(
destinations(groups).filter((h) => h === '/concursos').length,
@@ -427,15 +427,15 @@ void test('installed concours has its own primary group and preserves public and
JSON.stringify(groups.find((g) => g.id === 'discovery')).includes(
'/concursos',
),
false,
true,
);
assert.equal(
currentNavigationGroup(groups, '/concursos?q=obras'),
'procurement-area',
'discovery',
);
assert.equal(
currentNavigationGroup(groups, '/concursos/fixture'),
'procurement-area',
'discovery',
);
assert.equal(
destinations(group.children).includes('/concursos/acompanhar'),
@@ -0,0 +1,194 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { createRequire } from 'node:module';
import { compileFunction } from 'node:vm';
import ts from 'typescript';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import * as navigation from '../../resources/js/lib/portal-navigation.ts';
import { searchTerritoryHref } from '../../resources/js/lib/search-territory.ts';
const require = createRequire(import.meta.url);
let state, mobile;
const primitive =
(tag) =>
({ children, asChild, className, ...props }) =>
asChild
? React.cloneElement(children, { className })
: React.createElement(
tag,
{
className,
...Object.fromEntries(
Object.entries(props).filter(
([k]) =>
k.startsWith('aria-') ||
k.startsWith('data-'),
),
),
},
children,
);
const source = fs.readFileSync(
new URL(
'../../resources/js/components/portal-grouped-menu.tsx',
import.meta.url,
),
'utf8',
),
module = { exports: {} };
const output = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
jsx: ts.JsxEmit.ReactJSX,
},
}).outputText;
compileFunction(output, ['require', 'module', 'exports'])(
(name) =>
name === '@/components/portal-product-identity'
? {}
: name === '@inertiajs/react'
? {
usePage: () => state,
Link: ({ children, href, className, ...props }) =>
React.createElement(
'a',
{
href,
className,
...Object.fromEntries(
Object.entries(props).filter(
([k]) =>
k.startsWith('aria-') ||
k.startsWith('data-'),
),
),
},
children,
),
router: {},
}
: name === 'lucide-react'
? new Proxy({}, { get: () => () => null })
: name === '@/hooks/use-mobile'
? { useIsMobile: () => mobile }
: name === '@/components/navigation-context'
? { useNavigationContext: () => ({ view: null }) }
: name === '@/components/tempo-brand'
? { useTempoUi: () => ({ t: (x) => x, language: 'pt' }) }
: name === '@/components/tempo-header-tools'
? { TempoLanguageSelector: () => null }
: name === '@/components/ui/dropdown-menu'
? new Proxy(
{},
{
get: (_, key) =>
primitive(
String(key).includes('Trigger')
? 'button'
: 'div',
),
},
)
: name === '@/lib/portal-navigation'
? navigation
: name === '@/lib/search-territory'
? { searchTerritoryHref }
: name === '@/routes'
? { logout: () => '/logout' }
: require(name),
module,
module.exports,
);
test('actual header has exactly the requested six primary entries on desktop and mobile for public and signed-in users', () => {
const out = new URL(
'../../outputs/funding-uniform-menu-20261111/',
import.meta.url,
);
fs.mkdirSync(out, { recursive: true });
for (const authenticated of [false, true])
for (mobile of [false, true]) {
state = {
url: '/',
props: {
auth: {
user: authenticated
? {
name: 'SYNTHETIC QA',
email: 'qa@example.invalid',
}
: null,
canManagePortal: false,
},
procurementEnabled: true,
fundingEnabled: true,
},
};
const html = renderToStaticMarkup(
React.createElement(module.exports.PortalGroupedMenu),
);
const nav = html.match(
/<nav[^>]*aria-label="Áreas do portal"[\s\S]*?<\/nav>/,
)?.[0];
assert.ok(nav);
const labels = [
...nav.matchAll(
/<(?:a|button)[^>]*class="tempo-nav-trigger"[^>]*>([\s\S]*?)<\/(?:a|button)>/g,
),
].map((m) => m[1].replace(/<[^>]*>/g, '').trim());
assert.deepEqual(labels, [
'Início',
'Descoberta',
'Realização',
'Suporte',
'Sobre nós',
'Mais',
]);
for (const href of [
'/concursos',
'/fundos',
'/documentos',
'/legislacao',
'/sobre',
'/contacto',
'/acesso-gestao',
])
assert.ok(html.includes('href="' + href + '"'), href);
assert.ok(html.includes('step=S08'));
assert.equal(html.includes('/management/'), false);
fs.writeFileSync(
new URL(
'header-' + authenticated + '-' + mobile + '.html',
out,
),
html,
);
}
fs.writeFileSync(
new URL('verification.json', out),
JSON.stringify(
{
synthetic: true,
sharedInstitutionProfile: true,
suppliedExamples: ['Quorus', 'Moza Banco'],
sourceRowsPreserved: true,
primaryEntries: [
'Início',
'Descoberta',
'Realização',
'Suporte',
'Sobre nós',
'Mais',
],
actualHeaderRendered: true,
publicAndSignedIn: true,
mobileAndDesktopBranches: true,
visualBrowserReview: false,
activeDataWrites: 0,
},
null,
2,
),
);
});
@@ -0,0 +1,119 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { compileFunction } from 'node:vm';
import { createRequire } from 'node:module';
import { renderToStaticMarkup } from 'react-dom/server';
import ts from 'typescript';
const require = createRequire(import.meta.url);
const root = new URL('../../', import.meta.url);
const source = JSON.parse(fs.readFileSync(new URL('database/data/mining-reference-20261111.json', root)));
// Public data follows exactly the service projection, while native QA verifies the route itself.
const begin = source.risks.findIndex(x => x.startsWith('Riscos e pontos'));
const end = source.risks.findIndex(x => x.startsWith('Próximos passos'));
const data = { ...source, sector_slug:'mineracao', title:'Mineração e cadeias de valor', introduction:'Explore recursos minerais por localização.', group_label:'Grupo mineral', resource_heading:'Recursos e localização', resource_notice:'Conservar o âmbito territorial da fonte.', background:[], risks: source.risks.slice(begin + 1, end) };
data.groups=source.minerals.map(({province_names,...group})=>({...group,location_labels:province_names}));
data.indicators=source.indicators.map(({mineral,value,source})=>({value,source,label:mineral}));
delete data.original_paragraphs;
data.areas = data.areas.map(({source_fields: _sourceFields,province_names,mineral_ids, ...area}) => ({...area,location_labels:province_names,group_ids:mineral_ids}));
function load(file, mocks) {
const code = ts.transpileModule(fs.readFileSync(new URL(file, root), 'utf8'), {
compilerOptions: {module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX},
}).outputText;
const mod = {exports:{}};
compileFunction(code,['require','module','exports'])(key => mocks[key] ?? require(key),mod,mod.exports);
return mod.exports.default;
}
function nodes(tree) {
if (!tree || typeof tree !== 'object') return [];
if (Array.isArray(tree)) return tree.flatMap(nodes);
return [tree,...nodes(tree.props?.children)];
}
const Link = ({children, ...props}) => require('react').createElement('a',props,children);
function mount(url = '/sectores/mineracao', content = data) {
const state = []; let cursor = 0;
const Component = load('resources/js/components/sector-resource-reference.tsx', {
react:{useState(initial) {const n=cursor++; if (!(n in state)) state[n]=initial; return [state[n],value=>state[n]=value];}},
'@inertiajs/react':{Link,usePage:()=>({url})},
});
return () => {cursor=0;return Component({data:content});};
}
test('mineral and province filters keep value-chain selection in the existing investment route',()=>{
const render=mount(); let tree=render();
nodes(tree).find(n=>n.props?.['aria-label']==='Localização sectorial').props.onChange({target:{value:'Cabo Delgado'}});
tree=render(); nodes(tree).find(n=>n.props?.['aria-label']==='Grupo mineral').props.onChange({target:{value:'MINERAL-04'}});
tree=render();
const links=nodes(tree).filter(n=>n.type===Link);
assert.equal(links.length,1);
const url=new URL(links[0].props.href,'https://qa.invalid');
assert.equal(url.pathname,'/investimentos/a-partir-do-setor/mineracao');
assert.equal(url.searchParams.get('resource_area'),'MIN-02');
assert.equal(url.searchParams.get('resource_location'),'Cabo Delgado');
assert.equal(url.searchParams.get('resource_group'),'MINERAL-04');
assert.equal(url.searchParams.get('resource_source'),source.source_sha256);
const html=renderToStaticMarkup(tree);
assert.match(html,/Beneficiamento/);
if (process.env.INVESTMOZ_MINING_QA_ARTIFACTS === '1') {
const view=JSON.parse(fs.readFileSync(new URL('outputs/mining-reference-20261111/public-view.json',root)));
for (const key of ['source_sha256','reference_date','groups','areas','projects','indicators','sources','risks']) assert.deepEqual(view[key],data[key]);
fs.writeFileSync(new URL('outputs/mining-reference-20261111/Cabo-Delgado-Grafite.html',root),html);
}
});
test('return link filters and source uncertainty survive without invented map pins or publishing instructions',()=>{
const render=mount('/sectores/mineracao?resource_area=MIN-06&resource_location=Maputo');
const html=renderToStaticMarkup(render());
assert.match(html,/Maputo — província\/cidade a precisar/);
assert.match(html,/Minerais industriais/);
assert.doesNotMatch(html,/Próximos passos|documento gerador|seis requisitos|latitude|longitude/);
const clear=nodes(render()).find(n=>n.type==='button');clear.props.onClick();
assert.ok(nodes(render()).filter(n=>n.type===Link).length>=1);
});
test('selected mining context reaches both new and existing dossier actions',()=>{
let initial; const posted=[];
const selection={sector_slug:'mineracao',area_id:'MIN-02',group_id:'MINERAL-04',location_label:'Cabo Delgado',source_sha256:source.source_sha256,title:data.areas[1].title,value_chain:data.areas[1].value_chain};
const Component=load('resources/js/pages/investor-path/index.tsx',{
react:{useState:fn=>[typeof fn==='function'?fn():fn,()=>{}]},
'@/lib/operation-key':{createOperationKey:()=> 'SYNTHETIC-key'},
'@/lib/company-comparison':{}, '@/lib/investment-guides':{},
'@inertiajs/react':{Head:()=>null,Link,router:{post:(...args)=>posted.push(args)},useForm:data=>{initial=data;return {data,errors:{},processing:false,setData:()=>{},post:url=>posted.push([url,data])};}},
});
const tree=Component({contexts:[{id:'SYNTHETIC-case',version:3,title:'Synthetic dossier',updated_at:'2026-11-11'}],sectorSource:{slug:'mineracao',name:'Mineração',href:'/sectores/mineracao'},resourceSource:selection});
assert.deepEqual(initial.sector_reference,{area_id:selection.area_id,group_id:selection.group_id,source_sha256:selection.source_sha256,location_label:selection.location_label});
const button=nodes(tree).find(n=>n.type==='button' && n.props.onClick);
button.props.onClick();
assert.equal(posted[0][0],'/investimentos/SYNTHETIC-case/sectores');
assert.deepEqual(posted[0][1].sector_reference,initial.sector_reference);
nodes(tree).find(n=>n.type==='form').props.onSubmit({preventDefault(){}});
assert.equal(posted[1][0],'/investimentos');
assert.deepEqual(posted[1][1].sector_reference,initial.sector_reference);
});
const energySource=JSON.parse(fs.readFileSync(new URL('database/data/energy-reference-20261111.json',root)));
const energyData={...energySource,sector_slug:'energia-especifica',title:'Energia e cadeias de valor',introduction:'Prepare o investimento energético.',group_label:'Segmento energético',resource_heading:'Segmentos e âmbito territorial',resource_notice:'Conservar bacias,corredores e localidades.'};
test('energy solar selection preserves Niassa, batteries and the correct sector route',()=>{
const render=mount('/sectores/energia-especifica',energyData);let tree=render();
nodes(tree).find(n=>n.props?.['aria-label']==='Localização sectorial').props.onChange({target:{value:'Niassa'}});
tree=render();nodes(tree).find(n=>n.props?.['aria-label']==='Segmento energético').props.onChange({target:{value:'EN-SOLAR'}});
tree=render();const links=nodes(tree).filter(n=>n.type===Link);assert.equal(links.length,1);
const url=new URL(links[0].props.href,'https://qa.invalid');
assert.equal(url.pathname,'/investimentos/a-partir-do-setor/energia-especifica');
assert.equal(url.searchParams.get('resource_area'),'ENE-07');
assert.equal(url.searchParams.get('resource_location'),'Niassa');
assert.equal(url.searchParams.get('resource_group'),'EN-SOLAR');
const html=renderToStaticMarkup(tree);
assert.match(html,/baterias|armazenamento/);assert.match(html,/47,3 milhões EUR/);assert.match(html,/56 milhões USD/);
assert.doesNotMatch(html,/Produção e recursos por período/);
if(process.env.INVESTMOZ_MINING_QA_ARTIFACTS==='1'){
const view=JSON.parse(fs.readFileSync(new URL('outputs/energy-reference-20261111/public-view.json',root)));
for(const key of ['groups','areas','projects','sources','risks','background'])assert.deepEqual(view[key],energyData[key]);
fs.writeFileSync(new URL('outputs/energy-reference-20261111/Niassa-Solar.html',root),html);
}
});
test('energy studies without a named location stay selectable without assigning national coverage',()=>{
const tree=mount('/sectores/energia-especifica?resource_area=ENE-09',energyData)();
const links=nodes(tree).filter(n=>n.type===Link);assert.equal(links.length,1);
const url=new URL(links[0].props.href,'https://qa.invalid');
assert.equal(url.searchParams.get('resource_location'),null);
assert.equal(url.searchParams.get('resource_area'),'ENE-09');
const html=renderToStaticMarkup(tree);assert.match(html,/Sem implantação concreta/);assert.doesNotMatch(html,/latitude|longitude/);
});