import { test, expect } from '@playwright/test'; import { adminPath, apiPost, cleanupByNames, ensureDataSource } from './helpers.mjs'; test('data sources: builder metadata is lazy and browser runtime is clean', async ({ page }) => { const runtimeErrors = []; const metadataRequests = []; page.on('pageerror', error => runtimeErrors.push(error.message)); page.on('console', message => { if (message.type() === 'error') { runtimeErrors.push(message.text()); } }); page.on('request', request => { const url = request.url(); if (url.includes('op=schemas') || url.includes('op=source_fields')) { metadataRequests.push(url); } }); await page.goto(adminPath('feca-mailshot-data-sources')); await expect(page.getByRole('heading', { name: 'Mailshot Data Sources' })).toBeVisible(); expect(metadataRequests).toHaveLength(0); await page.getByRole('button', { name: 'New Data Source' }).click(); await page.locator('#ds-build-dsl-open').click(); await expect(page.locator('#ds-builder-modal')).toBeVisible(); await expect.poll(() => metadataRequests.filter(url => url.includes('op=schemas')).length).toBe(1); await expect(page.locator('#ds-builder-schema option')).not.toHaveCount(1); await page.locator('.ds-source-built[value="contacts"]').check(); await page.locator('#ds-builder-add-row').click(); const row = page.locator('.feca-builder-row').first(); await row.locator('select').first().selectOption('compare'); await row.locator('select').nth(1).selectOption('contacts'); await expect.poll(() => metadataRequests.filter(url => url.includes('op=source_fields')).length).toBe(1); await expect(row.locator('select').nth(2).locator('option')).not.toHaveCount(1); expect(runtimeErrors).toEqual([]); }); test('data sources: create + preview + validation failure path', async ({ page, request }) => { const uniq = `${Date.now()}_${Math.floor(Math.random() * 100000)}`; const dsName = `e2e_ds_${uniq}`; try { await page.goto(adminPath('feca-mailshot-data-sources')); await expect(page.getByRole('heading', { name: 'Mailshot Data Sources' })).toBeVisible(); await page.getByRole('button', { name: 'New Data Source' }).click(); await page.locator('#ds_name').fill(dsName); await page.locator('#ds_dsl').fill('contacts and accounts'); await page.getByRole('button', { name: /^Create|Save$/ }).first().click(); await expect(page.getByRole('cell', { name: dsName })).toBeVisible(); const preview = await apiPost(request, 'feca_mailshots_data_sources_api', 'preview', { dsl_text: 'contacts and accounts', limit: '10' }); expect(preview._httpStatus).toBe(200); expect(JSON.stringify(preview)).not.toContain('Duplicate column name'); const invalid = await apiPost(request, 'feca_mailshots_data_sources_api', 'validate', { dsl_text: 'unknownsource' }); expect(invalid._httpStatus).toBe(200); expect(Array.isArray(invalid.errors)).toBeTruthy(); expect(invalid.errors.join(' | ')).toContain('Unknown source'); } finally { try { await cleanupByNames(request, { dataSourceNames: [dsName] }); } catch { // best effort } } }); test('data sources: duplicate row action creates editable copy', async ({ page, request }) => { const uniq = `${Date.now()}_${Math.floor(Math.random() * 100000)}`; const dsName = `e2e_ds_duplicate_${uniq}`; const copyName = `Copy of ${dsName}`; try { await ensureDataSource(request, dsName, 'contacts and accounts'); await page.goto(adminPath('feca-mailshot-data-sources')); await expect(page.getByRole('heading', { name: 'Mailshot Data Sources' })).toBeVisible(); const row = page.locator('tr', { has: page.getByRole('cell', { name: dsName }) }).first(); await expect(row).toBeVisible(); await row.getByRole('button', { name: 'Duplicate' }).click(); const copyRow = page.locator('tr', { has: page.getByRole('cell', { name: copyName }) }).first(); await expect(copyRow).toBeVisible(); await copyRow.getByRole('link', { name: 'Edit' }).click(); await expect(page.locator('#ds-editor-modal')).toBeVisible(); await expect(page.locator('#ds_name')).toHaveValue(copyName); await expect(page.locator('#ds_dsl')).toHaveValue('contacts and accounts'); } finally { try { await cleanupByNames(request, { dataSourceNames: [dsName, copyName] }); } catch { // best effort } } }); test('data sources: DSL builder round-trip preserves complex representable DSL', async ({ page, request }) => { const uniq = `${Date.now()}_${Math.floor(Math.random() * 100000)}`; const dsName = `e2e_ds_roundtrip_${uniq}`; const dsl = "contacts and accounts where fen1-contact and not (primary-contact) and contacts.account_id = accounts.id and contacts.last_name contains 'smith' and accounts.id in (123) and accounts.name starts-with 'St' and accounts.name ends-with 'Ltd' and contacts.phone_1 blank and contacts.account_id != accounts.id"; try { await ensureDataSource(request, dsName, dsl); await page.goto(adminPath('feca-mailshot-data-sources')); await expect(page.getByRole('heading', { name: 'Mailshot Data Sources' })).toBeVisible(); const row = page.locator('tr', { has: page.getByRole('cell', { name: dsName }) }).first(); await expect(row).toBeVisible(); await row.getByRole('link', { name: 'Edit' }).click(); await expect(page.locator('#ds-editor-modal')).toBeVisible(); await expect(page.locator('#ds_dsl')).toHaveValue(dsl); await page.locator('#ds-build-dsl-open').click(); await expect(page.locator('#ds-builder-modal')).toBeVisible(); await expect(page.locator('#ds-builder-output')).toHaveValue(dsl); await page.locator('#ds-builder-apply').click(); await expect(page.locator('#ds_dsl')).toHaveValue(dsl); await page.locator('#ds-build-dsl-open').click(); await expect(page.locator('#ds-builder-output')).toHaveValue(dsl); } finally { try { await cleanupByNames(request, { dataSourceNames: [dsName] }); } catch { // best effort } } }); test('data sources: DSL builder round-trip preserves FEN built-in filters', async ({ page, request }) => { const uniq = `${Date.now()}_${Math.floor(Math.random() * 100000)}`; const dsName = `e2e_ds_fen_roundtrip_${uniq}`; const dsl = 'invoices and ads and advertisers where invoice-ids(10, 11) and issue(202605) and selected'; try { await ensureDataSource(request, dsName, dsl); await page.goto(adminPath('feca-mailshot-data-sources')); await expect(page.getByRole('heading', { name: 'Mailshot Data Sources' })).toBeVisible(); const row = page.locator('tr', { has: page.getByRole('cell', { name: dsName }) }).first(); await expect(row).toBeVisible(); await row.getByRole('link', { name: 'Edit' }).click(); await expect(page.locator('#ds-editor-modal')).toBeVisible(); await expect(page.locator('#ds_dsl')).toHaveValue(dsl); await page.locator('#ds-build-dsl-open').click(); await expect(page.locator('#ds-builder-modal')).toBeVisible(); await expect(page.locator('#ds-builder-output')).toHaveValue(dsl); await page.locator('#ds-builder-apply').click(); await expect(page.locator('#ds_dsl')).toHaveValue(dsl); } finally { try { await cleanupByNames(request, { dataSourceNames: [dsName] }); } catch { // best effort } } }); test('data sources: DSL builder round-trip preserves account type filters', async ({ page, request }) => { const uniq = `${Date.now()}_${Math.floor(Math.random() * 100000)}`; const dsName = `e2e_ds_account_type_roundtrip_${uniq}`; const dsl = 'accounts where member-account and not (affiliate-account)'; try { await ensureDataSource(request, dsName, dsl); await page.goto(adminPath('feca-mailshot-data-sources')); await expect(page.getByRole('heading', { name: 'Mailshot Data Sources' })).toBeVisible(); const row = page.locator('tr', { has: page.getByRole('cell', { name: dsName }) }).first(); await expect(row).toBeVisible(); await row.getByRole('link', { name: 'Edit' }).click(); await expect(page.locator('#ds-editor-modal')).toBeVisible(); await expect(page.locator('#ds_dsl')).toHaveValue(dsl); await page.locator('#ds-build-dsl-open').click(); await expect(page.locator('#ds-builder-modal')).toBeVisible(); await expect(page.locator('#ds-builder-output')).toHaveValue(dsl); await page.locator('#ds-builder-apply').click(); await expect(page.locator('#ds_dsl')).toHaveValue(dsl); } finally { try { await cleanupByNames(request, { dataSourceNames: [dsName] }); } catch { // best effort } } });