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: '', 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, /<Anne>/); 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(/]*data-v2-quick-action="create_supplier_invoice"[^>]*>/)[0]; const archiveButton = html.match(/]*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(/]*data-v2-quick-action="create_customer_case"[^>]*>/)[0]; const unknownArchive = unknownHtml.match(/]*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'}); });