455 lines
17 KiB
JavaScript
455 lines
17 KiB
JavaScript
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 { renderToStaticMarkup } from 'react-dom/server';
|
|
import ts from 'typescript';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
function load(path, mocks) {
|
|
const output = ts.transpileModule(
|
|
fs.readFileSync(new URL(path, import.meta.url), 'utf8'),
|
|
{
|
|
compilerOptions: {
|
|
module: ts.ModuleKind.CommonJS,
|
|
target: ts.ScriptTarget.ES2022,
|
|
jsx: ts.JsxEmit.ReactJSX,
|
|
},
|
|
},
|
|
).outputText;
|
|
const module = { exports: {} };
|
|
compileFunction(output, ['require', 'module', 'exports'])(
|
|
(name) => mocks[name] ?? require(name),
|
|
module,
|
|
module.exports,
|
|
);
|
|
return module.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)];
|
|
}
|
|
await test('preferences submit their own version and chosen criteria, with explicit opt-in and an independent cancellation action', () => {
|
|
let form, sent;
|
|
const Component = load(
|
|
'../../resources/js/components/opportunity-alert-preferences.tsx',
|
|
{
|
|
'@inertiajs/react': {
|
|
useForm(initial) {
|
|
form ??= {
|
|
data: initial,
|
|
errors: {},
|
|
processing: false,
|
|
setData(key, value) {
|
|
this.data = { ...this.data, [key]: value };
|
|
},
|
|
transform(fn) {
|
|
this.apply = fn;
|
|
},
|
|
put(url, options) {
|
|
sent = {
|
|
url,
|
|
options,
|
|
data: this.apply(this.data),
|
|
};
|
|
},
|
|
};
|
|
return form;
|
|
},
|
|
},
|
|
'@/components/opportunity-search-filters': {
|
|
modalityLabels: {
|
|
investment_project: 'Projecto de investimento',
|
|
},
|
|
},
|
|
'@/pages/investment/shared': { card: '', button: '', input: '' },
|
|
},
|
|
);
|
|
const preferences = {
|
|
version: 7,
|
|
enabled: true,
|
|
frequency: 'daily',
|
|
channel: 'in_app',
|
|
next_run_at: null,
|
|
criteria: {
|
|
sector: 'qa',
|
|
province_id: 'province',
|
|
category: 'investment_project',
|
|
},
|
|
};
|
|
const render = () =>
|
|
Component({
|
|
preferences,
|
|
options: {
|
|
frequencies: { daily: 'Daily' },
|
|
sectors: [{ slug: 'qa', name: 'Synthetic sector' }],
|
|
provinces: [{ id: 'province', name: 'Synthetic province' }],
|
|
categories: ['investment_project'],
|
|
},
|
|
});
|
|
let tree = render();
|
|
assert.equal(form.data.consent, false);
|
|
assert.ok(
|
|
renderToStaticMarkup(tree).includes('Não são enviados emails ou SMS'),
|
|
);
|
|
const criteria = nodes(tree).find(
|
|
(node) => node.type === 'select' && node.props.value === 'qa',
|
|
);
|
|
criteria.props.onChange({ target: { value: '' } });
|
|
tree = render();
|
|
assert.equal(form.data.criteria.province_id, 'province');
|
|
nodes(tree)
|
|
.find(
|
|
(node) =>
|
|
node.type === 'input' && node.props.name === 'alert-consent',
|
|
)
|
|
.props.onChange({ target: { checked: true } });
|
|
tree = render();
|
|
nodes(tree)
|
|
.find((node) => node.type === 'form')
|
|
.props.onSubmit({ preventDefault() {} });
|
|
assert.equal(sent.url, '/investidor/alert-preferences');
|
|
assert.equal(sent.data.version, 7);
|
|
assert.equal(sent.data.channel, 'in_app');
|
|
assert.equal(sent.data.enabled, true);
|
|
assert.equal(sent.data.consent, true);
|
|
assert.equal(sent.data.criteria.sector, '');
|
|
nodes(tree)
|
|
.find(
|
|
(node) =>
|
|
node.type === 'button' &&
|
|
node.props.children === 'Cancelar estes alertas',
|
|
)
|
|
.props.onClick();
|
|
assert.equal(sent.data.enabled, false);
|
|
assert.equal(sent.data.consent, false);
|
|
assert.equal(preferences.enabled, true);
|
|
const unavailable = renderToStaticMarkup(
|
|
Component({
|
|
preferences: { ...preferences, criteria_available: false },
|
|
options: {
|
|
frequencies: { daily: 'Daily' },
|
|
sectors: [],
|
|
provinces: [],
|
|
categories: [],
|
|
},
|
|
}),
|
|
);
|
|
assert.ok(
|
|
unavailable.includes(
|
|
'O acompanhamento das selecções indisponíveis fica suspenso',
|
|
),
|
|
);
|
|
assert.ok(unavailable.includes('Província anterior indisponível'));
|
|
assert.ok(unavailable.includes('Cancelar estes alertas'));
|
|
assert.equal(form.data.criteria.province_id, 'province');
|
|
});
|
|
|
|
await test('opportunity notifications preserve the legacy manifestation action and link to the public fiche', () => {
|
|
const Page = load('../../resources/js/pages/investment/notifications.tsx', {
|
|
'@inertiajs/react': { Link: 'a', router: { post() {} } },
|
|
'./shared': { Frame: 'main', card: '', button: '' },
|
|
'@/components/opportunity-alert-preferences': {
|
|
__esModule: true,
|
|
default: () => null,
|
|
},
|
|
});
|
|
const html = renderToStaticMarkup(
|
|
Page({
|
|
unread: 2,
|
|
notifications: {
|
|
data: [
|
|
{
|
|
id: 'alert',
|
|
opportunity_id: 12,
|
|
submission_id: null,
|
|
kind: 'opportunity_match',
|
|
payload: {
|
|
title: 'Synthetic public opportunity',
|
|
slug: 'synthetic-fiche',
|
|
source: 'Synthetic catalogue',
|
|
preference_version: 7,
|
|
},
|
|
created_at: '2026-11-02T12:00:00Z',
|
|
read_at: null,
|
|
},
|
|
{
|
|
id: 'legacy',
|
|
submission_id: 'synthetic-submission',
|
|
kind: 'received',
|
|
payload: { reference: 'SYNTHETIC-REF' },
|
|
created_at: '2026-11-02T12:00:00Z',
|
|
read_at: null,
|
|
},
|
|
],
|
|
prev_page_url: null,
|
|
next_page_url: null,
|
|
},
|
|
}),
|
|
);
|
|
assert.ok(html.includes('/oportunidades/synthetic-fiche'));
|
|
assert.ok(html.includes('/investidor/manifestacoes/synthetic-submission'));
|
|
assert.ok(html.includes('versão 7'));
|
|
assert.ok(html.includes('Manifestação recebida'));
|
|
assert.ok(!html.includes('/manifestacoes/null'));
|
|
});
|
|
|
|
await test('tender alerts require an explicit source choice and show only their applicable form controls', () => {
|
|
let form, sent;
|
|
const Component = load(
|
|
'../../resources/js/components/opportunity-alert-preferences.tsx',
|
|
{
|
|
'@inertiajs/react': {
|
|
useForm(initial) {
|
|
form ??= {
|
|
data: initial,
|
|
errors: {},
|
|
processing: false,
|
|
setData(key, value) {
|
|
this.data = { ...this.data, [key]: value };
|
|
},
|
|
transform(fn) {
|
|
this.apply = fn;
|
|
},
|
|
put(url) {
|
|
sent = { url, data: this.apply(this.data) };
|
|
},
|
|
};
|
|
return form;
|
|
},
|
|
},
|
|
'@/components/opportunity-search-filters': {
|
|
modalityLabels: {
|
|
investment_project: 'Projecto de investimento',
|
|
},
|
|
},
|
|
'@/pages/investment/shared': { card: '', button: '', input: '' },
|
|
},
|
|
);
|
|
const preferences = {
|
|
version: 3,
|
|
enabled: true,
|
|
frequency: 'daily',
|
|
channel: 'in_app',
|
|
next_run_at: null,
|
|
criteria: { category: 'investment_project' },
|
|
};
|
|
const options = {
|
|
frequencies: { daily: 'Daily' },
|
|
sectors: [],
|
|
provinces: [],
|
|
categories: ['investment_project'],
|
|
tendersAvailable: true,
|
|
tenderRegimes: { private: 'Concurso privado' },
|
|
};
|
|
const render = (currentOptions = options) =>
|
|
Component({ preferences, options: currentOptions });
|
|
let tree = render();
|
|
assert.deepEqual(form.data.sources, ['opportunities']);
|
|
assert.equal(
|
|
nodes(tree).find((node) => node.props?.name === 'source-tenders').props
|
|
.checked,
|
|
false,
|
|
);
|
|
form.setData('consent', true);
|
|
nodes(tree)
|
|
.find((node) => node.props?.name === 'source-tenders')
|
|
.props.onChange({ target: { checked: true } });
|
|
assert.equal(form.data.consent, false);
|
|
tree = render();
|
|
nodes(tree)
|
|
.find((node) => node.props?.name === 'source-opportunities')
|
|
.props.onChange({ target: { checked: false } });
|
|
tree = render();
|
|
const html = renderToStaticMarkup(tree);
|
|
assert.ok(html.includes('Regime do concurso'));
|
|
assert.ok(!html.includes('Todas as modalidades'));
|
|
assert.ok(html.includes('Concurso privado'));
|
|
form.setData('consent', true);
|
|
nodes(tree)
|
|
.find((node) => node.type === 'form')
|
|
.props.onSubmit({ preventDefault() {} });
|
|
assert.deepEqual(sent.data.sources, ['tenders']);
|
|
tree = render({ ...options, tendersAvailable: false });
|
|
const unavailable = nodes(tree).find(
|
|
(node) => node.props?.name === 'source-tenders',
|
|
);
|
|
assert.equal(
|
|
unavailable.props.disabled,
|
|
false,
|
|
'An unavailable selected source must remain removable.',
|
|
);
|
|
unavailable.props.onChange({ target: { checked: false } });
|
|
assert.deepEqual(form.data.sources, []);
|
|
});
|
|
|
|
await test('published tender acts retain their identity and use the tender action rather than a manifestation URL', () => {
|
|
const Page = load('../../resources/js/pages/investment/notifications.tsx', {
|
|
'@inertiajs/react': { Link: 'a', router: { post() {} } },
|
|
'./shared': { Frame: 'main', card: '', button: '' },
|
|
'@/components/opportunity-alert-preferences': {
|
|
__esModule: true,
|
|
default: () => null,
|
|
},
|
|
});
|
|
const html = renderToStaticMarkup(
|
|
Page({
|
|
unread: 1,
|
|
alertOptions: { tenderActs: { extension: 'Prorrogação' } },
|
|
notifications: {
|
|
data: [
|
|
{
|
|
id: 'synthetic-alert',
|
|
submission_id: null,
|
|
tender_version_id: 'synthetic-version',
|
|
kind: 'tender_match',
|
|
payload: {
|
|
title: 'Synthetic public tender',
|
|
tender_id: 'synthetic-tender',
|
|
act: 'extension',
|
|
publication_version: 4,
|
|
source: 'Synthetic catalogue',
|
|
preference_version: 3,
|
|
},
|
|
created_at: '2026-11-02T12:00:00Z',
|
|
read_at: null,
|
|
},
|
|
],
|
|
prev_page_url: null,
|
|
next_page_url: null,
|
|
},
|
|
}),
|
|
);
|
|
assert.ok(html.includes('/concursos/synthetic-tender'));
|
|
assert.ok(html.includes('Consultar concurso'));
|
|
assert.ok(html.includes('Prorrogação'));
|
|
assert.ok(html.includes('versão 4'));
|
|
assert.ok(html.includes('não apresenta uma proposta'));
|
|
assert.ok(!html.includes('/manifestacoes/null'));
|
|
});
|
|
|
|
await test('funding preferences require opt-in and expose scope and window choices only for that source', () => {
|
|
let form;
|
|
const Component = load(
|
|
'../../resources/js/components/opportunity-alert-preferences.tsx',
|
|
{
|
|
'@inertiajs/react': {
|
|
useForm(initial) {
|
|
form ??= {
|
|
data: initial,
|
|
processing: false,
|
|
errors: {},
|
|
setData(key, value) {
|
|
this.data = { ...this.data, [key]: value };
|
|
},
|
|
};
|
|
return form;
|
|
},
|
|
},
|
|
'@/components/opportunity-search-filters': { modalityLabels: {} },
|
|
'@/pages/investment/shared': { card: '', button: '', input: '' },
|
|
},
|
|
);
|
|
const preferences = {
|
|
version: 1,
|
|
enabled: true,
|
|
frequency: 'daily',
|
|
criteria: {},
|
|
channel: 'in_app',
|
|
next_run_at: null,
|
|
};
|
|
const options = {
|
|
frequencies: { daily: 'Daily' },
|
|
sectors: [],
|
|
provinces: [],
|
|
categories: [],
|
|
fundingAvailable: true,
|
|
};
|
|
let tree = Component({ preferences, options });
|
|
assert.deepEqual(form.data.sources, ['opportunities']);
|
|
assert.equal(
|
|
renderToStaticMarkup(tree).includes('Estado de uma janela'),
|
|
false,
|
|
);
|
|
form.setData('consent', true);
|
|
nodes(tree)
|
|
.find((node) => node.props?.name === 'source-funding')
|
|
.props.onChange({ target: { checked: true } });
|
|
assert.equal(form.data.consent, false);
|
|
tree = Component({ preferences, options });
|
|
assert.ok(
|
|
renderToStaticMarkup(tree).includes(
|
|
'Incluir e assinalar informação por confirmar',
|
|
),
|
|
);
|
|
assert.ok(renderToStaticMarkup(tree).includes('Estado de uma janela'));
|
|
assert.equal(form.data.criteria.funding_certainty, 'documented');
|
|
const unavailable = nodes(
|
|
Component({
|
|
preferences,
|
|
options: { ...options, fundingAvailable: false },
|
|
}),
|
|
).find((node) => node.props?.name === 'source-funding');
|
|
assert.equal(unavailable.props.disabled, false);
|
|
unavailable.props.onChange({ target: { checked: false } });
|
|
assert.deepEqual(form.data.sources, ['opportunities']);
|
|
});
|
|
|
|
await test('funding notice links to its programme and qualifies historical window states', () => {
|
|
const Page = load('../../resources/js/pages/investment/notifications.tsx', {
|
|
'@inertiajs/react': { Link: 'a', router: { post() {} } },
|
|
'./shared': { Frame: 'main', card: '', button: '' },
|
|
'@/components/opportunity-alert-preferences': {
|
|
__esModule: true,
|
|
default: () => null,
|
|
},
|
|
});
|
|
const html = renderToStaticMarkup(
|
|
Page({
|
|
unread: 1,
|
|
notifications: {
|
|
data: [
|
|
{
|
|
id: 'qa',
|
|
submission_id: null,
|
|
kind: 'funding_match',
|
|
payload: {
|
|
title: 'Synthetic fund',
|
|
programme_id: 'qa-fund',
|
|
source_url:
|
|
'https://example.invalid/funding-source',
|
|
source_reference: 'Synthetic source edition',
|
|
source_checked_on: '2026-11-02',
|
|
publication_version: 2,
|
|
scope_explanation: {
|
|
sector: 'Âmbito por confirmar',
|
|
},
|
|
window_states: [
|
|
{
|
|
title: 'Synthetic historical window',
|
|
state: 'closed',
|
|
end_on: '2025-10-20',
|
|
},
|
|
],
|
|
},
|
|
created_at: '2026-11-02T12:00:00Z',
|
|
read_at: null,
|
|
},
|
|
],
|
|
prev_page_url: null,
|
|
next_page_url: null,
|
|
},
|
|
}),
|
|
);
|
|
assert.ok(html.includes('/fundos/qa-fund'));
|
|
assert.ok(html.includes('https://example.invalid/funding-source'));
|
|
assert.ok(html.includes('Synthetic source edition'));
|
|
assert.ok(html.includes('não confirma uma candidatura aberta'));
|
|
assert.ok(html.includes('estado na data do aviso'));
|
|
assert.ok(html.includes('encerrada'));
|
|
assert.ok(html.includes('Âmbito por confirmar'));
|
|
assert.ok(!html.includes('/manifestacoes/null'));
|
|
});
|