- Introduced tests for drift operations UI and backend to ensure proper handling of ignore rules and UI elements. - Added tests for email identity API to validate email linking and identity retrieval without updates. - Implemented tests for email purchase link service to verify correct handling of purchase mail links and conflict scenarios. - Created tests for email reply automation to ensure proper routing and processing of replies based on classification. - Developed tests for email sender creation to validate suggestions and handling of existing contacts and companies. - Added tests for email sender identity to ensure correct matching of contacts and domains. - Implemented tests for work desk functionalities to validate task management, meeting registrations, and timer functionalities.
219 lines
11 KiB
JavaScript
219 lines
11 KiB
JavaScript
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, /<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(/<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'});
|
|
});
|