Merge branch 'main' of https://git.tempo.co.mz/nala/Investidor-V2 into front
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
Reference in New Issue
Block a user