import { test, expect } from '@playwright/test'; import { adminPath } from './helpers.mjs'; import { ensureDataSource, ensureMailshot, cleanupByNames } from './helpers.mjs'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const invoicingHtml = fs.readFileSync(path.resolve(__dirname, '../../../samples/invoicing.html'), 'utf8'); const setJoditHtml = async (page, html) => { await page.evaluate((nextHtml) => { const host = /** @type {any} */ (document.getElementById('ms-template-jodit')); const instances = (window.Jodit && window.Jodit.instances) ? window.Jodit.instances : null; let jodit = host && host.jodit ? host.jodit : null; if (!jodit && instances) { jodit = instances['ms-template-jodit'] || instances.ms_template_jodit || null; } if (!jodit && instances && typeof instances === 'object') { const vals = Array.isArray(instances) ? instances : Object.values(instances); jodit = vals.find((x) => x && x.container && x.container.isConnected) || null; } if (!jodit) { throw new Error('Jodit instance not available'); } if (typeof jodit.setEditorValue === 'function') { jodit.setEditorValue(nextHtml); } else { jodit.value = nextHtml; } if (typeof jodit.synchronizeValues === 'function') { jodit.synchronizeValues(); } }, html); }; test('regression: mailshot message modal saves and closes on first click', async ({ page }) => { const longBody = Array.from({ length: 120 }, (_, i) => `

Line ${i + 1}

`).join(''); const html = `
One click save marker
${longBody}
Tail marker
`; await page.goto(adminPath('feca-mailshots-mailshots')); await expect(page.locator('h1', { hasText: 'Mailshots' })).toBeVisible(); await page.locator('#ms-open-new').click(); await expect(page.locator('#ms-editor-modal')).toBeVisible(); await page.locator('#ms-edit-message').click(); await expect(page.locator('#ms-template-modal')).toBeVisible(); await setJoditHtml(page, html); await page.locator('#ms-template-save').click(); await expect(page.locator('#ms-template-modal')).toBeHidden(); await expect(page.locator('#ms_message')).toHaveValue(/regression-one-click/); await expect(page.locator('#ms_message')).toHaveValue(/tail-marker/); await expect(page.locator('#ms-message-snippet')).toContainText('One click save marker'); }); test('regression: message editor single-save with invoicing template (scroll case)', async ({ page }) => { await page.goto(adminPath('feca-mailshots-mailshots')); await expect(page.locator('h1', { hasText: 'Mailshots' })).toBeVisible(); await page.locator('#ms-open-new').click(); await expect(page.locator('#ms-editor-modal')).toBeVisible(); await page.locator('#ms-edit-message').click(); await expect(page.locator('#ms-template-modal')).toBeVisible(); await setJoditHtml(page, invoicingHtml); await page.evaluate(() => { const host = /** @type {HTMLTextAreaElement|null} */ (document.getElementById('ms-template-jodit')); if (host) { host.scrollTop = host.scrollHeight; host.focus(); } const modal = document.getElementById('ms-template-modal'); if (!modal) { return; } const areas = modal.querySelectorAll('textarea'); areas.forEach((ta) => { const el = /** @type {HTMLTextAreaElement} */ (ta); el.scrollTop = el.scrollHeight; el.focus(); }); }); await page.locator('#ms-template-save').click(); await expect(page.locator('#ms-template-modal')).toBeHidden(); await expect(page.locator('#ms_message')).toHaveValue(/invoice-header/); await expect(page.locator('#ms_message')).toHaveValue(/Registered Charity No\. 293020/); }); test('regression: message editor manual source edit saves on first click', async ({ page }) => { await page.goto(adminPath('feca-mailshots-mailshots')); await expect(page.locator('h1', { hasText: 'Mailshots' })).toBeVisible(); await page.locator('#ms-open-new').click(); await expect(page.locator('#ms-editor-modal')).toBeVisible(); await page.locator('#ms-edit-message').click(); await expect(page.locator('#ms-template-modal')).toBeVisible(); await setJoditHtml(page, invoicingHtml); await page.evaluate(() => { const modal = document.getElementById('ms-template-modal'); if (!modal) { return; } const sourceAreas = Array.from(modal.querySelectorAll('textarea')) .filter((el) => el.offsetParent !== null); const source = sourceAreas[sourceAreas.length - 1] || null; if (!source) { return; } source.scrollTop = source.scrollHeight; source.focus(); source.setSelectionRange(source.value.length, source.value.length); source.value = `${source.value}\n`; source.dispatchEvent(new Event('input', { bubbles: true })); source.dispatchEvent(new Event('change', { bubbles: true })); }); await page.locator('#ms-template-save').click(); await expect(page.locator('#ms-template-modal')).toBeHidden(); await expect(page.locator('#ms_message')).toHaveValue(/manual-edit-marker/); await expect(page.locator('#ms_message')).toHaveValue(/invoice-header/); }); test('regression: mailshot validation failure keeps entered values in modal', async ({ page }) => { await page.goto(adminPath('feca-mailshots-mailshots')); await expect(page.locator('h1', { hasText: 'Mailshots' })).toBeVisible(); await page.locator('#ms-open-new').click(); await expect(page.locator('#ms-editor-modal')).toBeVisible(); await page.locator('#ms_purpose').fill('validation draft mailshot'); await page.locator('#ms_subject').fill(''); await page.locator('#ms-edit-message').click(); await expect(page.locator('#ms-template-modal')).toBeVisible(); await setJoditHtml(page, '

Draft body should survive validation failure

'); await page.locator('#ms-template-save').click(); await expect(page.locator('#ms-template-modal')).toBeHidden(); await page.locator('#mailshot-editor button[type="submit"]').click(); await expect(page.locator('#ms-editor-modal')).toBeVisible(); await expect(page.locator('#mailshot-editor')).toContainText('Please fix the following:'); await expect(page.locator('#mailshot-editor')).toContainText('Subject is required.'); await expect(page.locator('.wrap')).not.toContainText('Mailshot action failed.'); await expect(page.locator('#ms_purpose')).toHaveValue('validation draft mailshot'); await expect(page.locator('#ms_message')).toHaveValue(/Draft body should survive validation failure/); }); test('regression: editing existing mailshot allows empty message for pdf-only workflow', async ({ page, request }) => { const uniq = `${Date.now()}_${Math.floor(Math.random() * 100000)}`; const dsName = `e2e_ms_edit_ds_${uniq}`; const purpose = `e2e_mailshot_edit_${uniq}`; const editedPurpose = `${purpose}_edited`; let mailshotId = ''; try { await ensureDataSource(request, dsName, 'contacts'); const saved = await ensureMailshot(request, { Purpose: purpose, DataSource: dsName, CC: '', BCC: '', Subject: 'Original subject', Message: '

Original message

', PDFAttachment: '', AttachmentNames: '[]', PDFFilenameDerivedFrom: '', RecipientEmailField: 'contacts.contact_email_1', ReplyTo: '' }); mailshotId = String(saved.id || ''); await page.goto(`${adminPath('feca-mailshots-mailshots')}&edit_id=${encodeURIComponent(mailshotId)}`); await expect(page.locator('#ms-editor-modal')).toBeVisible(); await page.locator('#ms_purpose').fill(editedPurpose); await page.locator('#ms_subject').fill('Edited subject'); await page.locator('#ms_message').fill(''); await page.locator('#mailshot-editor button[type="submit"]').click(); await expect(page.locator('#ms-editor-modal')).toBeHidden(); await expect(page.getByRole('cell', { name: editedPurpose })).toBeVisible(); } finally { try { await cleanupByNames(request, { dataSourceNames: [dsName], mailshotPurposes: [purpose, editedPurpose] }); } catch { // best effort } } }); test('regression: attachments new button opens modal editor', async ({ page }) => { await page.goto(adminPath('feca-mailshots-attachments')); await expect(page.locator('h1', { hasText: 'Attachments' })).toBeVisible(); await page.locator('#att-open-new').click(); await expect(page.locator('#att-editor-modal')).toBeVisible(); await expect(page.locator('#att-editor-form')).toBeVisible(); await expect(page.locator('#att-editor-id')).toHaveValue(''); }); test('regression: attachments upload populates filename and failed save preserves data', async ({ page }) => { await page.goto(adminPath('feca-mailshots-attachments')); await expect(page.locator('h1', { hasText: 'Attachments' })).toBeVisible(); await page.locator('#att-open-new').click(); await expect(page.locator('#att-editor-modal')).toBeVisible(); await page.locator('#att_name').fill('draft_attachment'); await page.setInputFiles('#att_file_upload', { name: 'sample-attachment.txt', mimeType: 'text/plain', buffer: Buffer.from('attachment regression sample', 'utf8') }); await expect(page.locator('#att_file_name')).toHaveValue('sample-attachment.txt'); await page.locator('#att_name').fill(''); await page.getByRole('button', { name: 'Create Attachment' }).click(); await expect(page.locator('#att-editor-modal')).toBeVisible(); await expect(page.locator('#att-editor-form')).toContainText('Please fix the following:'); await expect(page.locator('#att-editor-form')).toContainText('name is required.'); await expect(page.locator('#att_file_name')).toHaveValue('sample-attachment.txt'); await expect(page.locator('#att_base64')).not.toHaveValue(''); }); test('regression: pdf assets new button opens modal editor', async ({ page }) => { await page.goto(adminPath('feca-mailshots-pdf-assets')); await expect(page.locator('h1', { hasText: 'PDF Assets' })).toBeVisible(); await page.locator('#pdf-open-new').click(); await expect(page.locator('#pdf-editor-modal')).toBeVisible(); await expect(page.locator('#pdf-editor-form')).toBeVisible(); await expect(page.locator('#pdf-editor-id')).toHaveValue(''); }); test('regression: pdf assets failed save keeps modal values and shows inline errors', async ({ page }) => { await page.goto(adminPath('feca-mailshots-pdf-assets')); await expect(page.locator('h1', { hasText: 'PDF Assets' })).toBeVisible(); await page.locator('#pdf-open-new').click(); await expect(page.locator('#pdf-editor-modal')).toBeVisible(); await page.locator('#pdf_name').fill('draft_pdf_asset'); await page.locator('#pdf_width').fill(''); await page.locator('#pdf_height').fill(''); await page.locator('#pdf_file_name').fill(''); await page.setInputFiles('#pdf_file_upload', { name: 'sample-asset.png', mimeType: 'image/png', buffer: Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00]) }); await expect(page.locator('#pdf_file_name')).toHaveValue('sample-asset.png'); await page.locator('#pdf_file_name').fill(''); await page.getByRole('button', { name: 'Create PDF Asset' }).click(); await expect(page.locator('#pdf-editor-modal')).toBeVisible(); await expect(page.locator('#pdf-editor-form')).toContainText('Please fix the following:'); await expect(page.locator('#pdf-editor-form')).toContainText('file_name is required.'); await expect(page.locator('#pdf-editor-form')).toContainText('width_mm and height_mm must be > 0.'); await expect(page.locator('#pdf_name')).toHaveValue('draft_pdf_asset'); await expect(page.locator('#pdf_base64')).not.toHaveValue(''); });