bmc_hub/tests/js/email-sender-identity.test.cjs

219 lines
11 KiB
JavaScript
Raw Permalink Normal View History

const {test} = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const vm = require('node:vm');
const path = require('node:path');
const source = fs.readFileSync(path.join(__dirname, '../../app/emails/frontend/emails_v2.html'), 'utf8');
test('mail queues keep new and awaiting statuses separate', () => {
assert.match(source, /state\.filter === 'active'[\s\S]{0,120}status=new/);
assert.match(source, /emails\/queue-counts/);
assert.match(source, /active: Number\(stats\.active/);
assert.doesNotMatch(source, /\['new', 'awaiting_user_action'\]\.includes/);
});
test('inbox keeps the initial list passive and supports paged loading', () => {
assert.match(source, /pageSize: 100/);
assert.match(source, /emails\/count/);
assert.match(source, /Indlæs flere/);
assert.doesNotMatch(source, /else if \(emails\.length\) \{\s*await selectEmail/);
});
function setup() {
const host = {innerHTML: '', querySelectorAll: () => []};
const state = {selectedEmailId: 7, selectedEmail: {sender_name: '<Anne>', sender_email: 'anne@example.dk'},
senderIdentity: {ambiguous: true, candidates: [
{id: 1, name: 'Hus A', kind: 'customer', source: 'contact_email', contact_name: 'Anne'},
{id: 2, name: 'Hus B', kind: 'customer', source: 'contact_email', contact_name: 'Anne'}]}};
const context = vm.createContext({state, document: {getElementById: id => id === 'v2SenderIdentity' ? host : null},
clearTimeout, setTimeout, apiFetch: async () => ({}), selectEmail: async () => {}});
const start = source.indexOf(' function renderIdentityAssessment()');
const end = source.indexOf(' function renderClassificationDecision(', start);
const escStart = source.indexOf(' function escapeHtml(');
const escEnd = source.indexOf(' function formatEmailPlainText(', escStart);
vm.runInContext(source.slice(escStart, escEnd) + source.slice(start, end), context);
return {context, host, state};
}
test('ambiguous sender renders both choices, contact reason and escaped sender', () => {
const {context, host} = setup();
vm.runInContext('renderIdentityAssessment()', context);
assert.match(host.innerHTML, /Flere mulige tilknytninger/);
assert.match(host.innerHTML, /Hus A/);
assert.match(host.innerHTML, /Hus B/);
assert.match(host.innerHTML, /Mailadresse matcher kontakt/);
assert.match(host.innerHTML, /&lt;Anne&gt;/);
assert.match(host.innerHTML, /anne@example.dk/);
});
test('manual link applies only to this mail and does not learn a domain', async () => {
const {context} = setup();
let request;
context.apiFetch = async (url, options) => {request = {url, body: JSON.parse(options.body)};};
await vm.runInContext('linkSenderIdentity("customer", 42, {disabled: false})', context);
assert.equal(request.url, '/api/v1/emails/7/link');
assert.deepEqual(request.body, {customer_id: 42, remember_domain: false});
});
test('late sender result cannot overwrite the next selected mail', async () => {
const {context, state} = setup();
let resolve;
context.apiFetch = () => new Promise(done => {resolve = done;});
const pending = vm.runInContext('loadSenderIdentity()', context);
state.selectedEmailId = 8;
const newer = {candidates: [{id: 8}]};
state.senderIdentity = newer;
resolve({candidates: [{id: 7}]});
await pending;
assert.equal(state.senderIdentity, newer);
});
test('exact sole supplier contact is linked automatically without learning domain', async () => {
const {context, state} = setup();
const requests = [];
let refreshed;
context.apiFetch = async (url, options) => {
requests.push({url, options});
return {candidates: [{id: 6, kind: 'vendor', name: 'Globalconnect', source: 'contact_email'}]};
};
context.selectEmail = async id => {refreshed = id;};
await vm.runInContext('loadSenderIdentity()', context);
assert.equal(requests.length, 2);
assert.equal(requests[1].url, '/api/v1/emails/7/link');
assert.deepEqual(JSON.parse(requests[1].options.body), {supplier_id: 6, remember_domain: false});
assert.equal(refreshed, 7);
});
for (const [label, candidates, existing] of [
['domain alone', [{id: 6, kind: 'vendor', source: 'exact_domain'}], {}],
['multiple companies', [{id: 6, kind: 'vendor', source: 'contact_email'}, {id: 8, kind: 'customer', source: 'contact_email'}], {}],
['existing manual link', [{id: 6, kind: 'vendor', source: 'contact_email'}], {supplier_id: 8}],
]) {
test(`${label} never auto-links or overwrites identity`, async () => {
const {context, state} = setup();
Object.assign(state.selectedEmail, existing);
const calls = [];
context.apiFetch = async (url, options) => {calls.push({url, options}); return {candidates};};
await vm.runInContext('loadSenderIdentity()', context);
assert.equal(calls.length, 1);
assert.equal(calls[0].options, undefined);
});
}
test('ordinary Globalconnect mail keeps big buttons without invoice recognition claim', () => {
const {context} = setup();
const nodes = new Map();
context.document = {
getElementById(id) {
if (!nodes.has(id)) nodes.set(id, {innerHTML: '', addEventListener() {}});
return nodes.get(id);
},
querySelectorAll: () => [],
};
for (const name of ['setMailStatus', 'renderVendorSuggestion', 'renderDomainCustomerSuggestion',
'renderWorkflowPreview', 'loadWorkflowPreviewCurrent', 'loadSenderIdentity', 'patchReadState',
'archiveCurrent', 'markProcessedCurrent', 'reprocessCurrent', 'executeWorkflowsCurrent',
'autoRunWorkflowsCurrent', 'autoParseAndLinkCurrent', 'createCaseFromCurrent',
'extractVendorSuggestionCurrent', 'loadDomainCustomerSuggestionCurrent', 'openCaseLinkPicker',
'openSupplierCase', 'createSupplierCase', 'submitPurchaseMail', 'simplifySideSections']) context[name] = () => {};
context.formatDate = () => '';
context.sanitizeEmailHtml = () => '';
context.formatEmailPlainText = value => value;
const start = source.indexOf(' function renderDetail(email)');
const end = source.indexOf(' async function uploadEmailFiles(', start);
vm.runInContext(source.slice(start, end), context);
context.email = {id: 7, supplier_id: 6, supplier_name: 'Globalconnect',
sender_email: 'contact@globalconnect.dk', classification: 'general', body_text: 'Netværksopsætning'};
vm.runInContext('renderDetail(email)', context);
const html = nodes.get('v2QuickActions').innerHTML;
assert.match(html, /emails-v2-flow-choice flow-support/);
assert.match(html, /Leverandørfaktura/);
assert.match(html, /id="v2LinkCaseAction"/);
assert.match(html, /id="v2SupplierCaseAction"/);
assert.match(nodes.get('v2SideActions').innerHTML, /Hvem er sagen for/);
assert.doesNotMatch(html, /Internetfaktura genkendt|ANBEFALET/);
const invoiceButton = html.match(/<button[^>]*data-v2-quick-action="create_supplier_invoice"[^>]*>/)[0];
const archiveButton = html.match(/<button[^>]*data-v2-quick-action="archive_entity_mail"[^>]*>/)[0];
assert.doesNotMatch(invoiceButton, /disabled/);
assert.doesNotMatch(archiveButton, /disabled/);
context.email = {id: 8, sender_email: 'ukendt@example.dk', classification: 'general', body_text: 'Hej'};
vm.runInContext('renderDetail(email)', context);
const unknownHtml = nodes.get('v2QuickActions').innerHTML;
const unknownSupport = unknownHtml.match(/<button[^>]*data-v2-quick-action="create_customer_case"[^>]*>/)[0];
const unknownArchive = unknownHtml.match(/<button[^>]*data-v2-quick-action="archive_entity_mail"[^>]*>/)[0];
assert.match(unknownSupport, /disabled/);
assert.doesNotMatch(unknownArchive, /disabled/);
});
function setupSupplierForm() {
const {context, state} = setup();
const nodes = {
v2SupplierCaseSubmit: {disabled: false},
v2SupplierCaseStatus: {textContent: ''},
v2SupplierCaseTitle: {value: 'Teknisk dialog'},
v2CaseLinkPicker: {open: false, scrollIntoView() {}},
v2SagSearch: {focus() {this.focused = true;}},
};
context.document = {getElementById: id => nodes[id] || null};
context.setDetailStatus = () => {};
state.selectedEmail.supplier_id = 6;
state.supplierCaseCustomer = {id: 42, name: 'Kunden'};
state.senderIdentity = {candidates: [{id: 6, kind: 'vendor', source: 'contact_email', contact_id: 2232}]};
const start = source.indexOf(' function openCaseLinkPicker()');
const end = source.indexOf(' async function createCaseFromCurrent()', start);
vm.runInContext(source.slice(start, end), context);
return {context, state, nodes};
}
test('link case button opens existing picker and focuses search without writes', () => {
const {context, nodes} = setupSupplierForm();
vm.runInContext('openCaseLinkPicker()', context);
assert.equal(nodes.v2SagSearch.focused, true);
});
test('supplier case keeps supplier contact and chosen customer separate', async () => {
const {context} = setupSupplierForm();
let request;
context.apiFetch = async (url, options) => {
request = {url, body: JSON.parse(options.body)};
return {sag: {id: 99}};
};
await vm.runInContext('createSupplierCase({preventDefault() {}})', context);
assert.equal(request.url, '/api/v1/emails/7/create-sag');
assert.deepEqual(request.body, {titel: 'Teknisk dialog', case_type: 'leverandor', customer_id: 42, contact_id: 2232});
});
test('supplier case cannot be created before choosing a customer', async () => {
const {context, state} = setupSupplierForm();
state.supplierCaseCustomer = null;
context.apiFetch = () => {throw new Error('Must not write');};
await vm.runInContext('createSupplierCase({preventDefault() {}})', context);
});
test('repeated supplier submit is blocked while request is pending', async () => {
const {context} = setupSupplierForm();
let resolve, count = 0;
context.apiFetch = () => {count++; return new Promise(done => {resolve = done;});};
const first = vm.runInContext('createSupplierCase({preventDefault() {}})', context);
await vm.runInContext('createSupplierCase({preventDefault() {}})', context);
assert.equal(count, 1);
resolve({sag: {id: 99}});
await first;
});
test('delivery panel submits explicitly selected purchase and tracking', async () => {
const {context, state} = setup();
const nodes = {v2PurchaseSubmit: {disabled: false}, v2PurchaseStatus: {}, v2PurchaseTracking: {value: ' TRACK-123 '}};
context.document = {getElementById: id => nodes[id]};
state.selectedPurchase = {id: 10};
state.purchaseMailKind = 'freight_note';
context.setDetailStatus = () => {};
let request;
context.apiFetch = async (url, options) => {request = {url, body: JSON.parse(options.body)}; return {message: 'OK'};};
const start = source.indexOf(' function openPurchaseMail(');
const end = source.indexOf(' function openCaseLinkPicker()', start);
vm.runInContext(source.slice(start, end), context);
await vm.runInContext('submitPurchaseMail({preventDefault() {}})', context);
assert.equal(request.url, '/api/v1/emails/7/purchase-link');
assert.deepEqual(request.body, {purchase_id: 10, kind: 'freight_note', tracking_number: 'TRACK-123'});
});