import { test, expect } from '@playwright/test'; import { startTestServer } from './server.mjs'; let ctx; test.beforeAll(async () => { ctx = await startTestServer(); }); test.afterAll(async () => { if (ctx) await ctx.teardown(); }); async function loginViaApi() { const login = await ctx.apiRequest('POST', '/api/auth/login', { body: { username: 'admin', password: ctx.adminPw } }); expect(login.status).toBe(200); const setCookie = login.headers.get('set-cookie'); return setCookie ? setCookie.split(';')[0] : ''; } async function setCookieOnPage(page, cookie) { const match = cookie ? cookie.match(/maquina_pessoal_session=([^;]+)/) : null; if (!match) return; await page.context().addCookies([{ name: 'maquina_pessoal_session', value: match[1], domain: '127.0.0.1', path: '/', httpOnly: true, sameSite: 'Strict' }]); } test.describe('Campaign Pilot Browser Tests', () => { test.describe('Authentication', () => { test('login with seeded admin credentials', async ({ page }) => { await page.goto('/login.html'); await expect(page.locator('#form-login')).toBeVisible(); await page.fill('#login-username', 'admin'); await page.fill('#login-password', ctx.adminPw); await page.click('button[type="submit"]'); await page.waitForURL('**/', { timeout: 10000 }); await expect(page.locator('#usuario-logado')).toContainText('admin'); }); }); test.describe('Campaign Cards', () => { let cookie; test.beforeAll(async () => { cookie = await loginViaApi(); }); test('campaign tab shows cards with correct status/limits', async ({ page }) => { await setCookieOnPage(page, cookie); await page.goto('/'); await page.waitForLoadState('networkidle'); await page.click('button[data-tab="campanhas"]'); await page.waitForTimeout(1500); const cards = page.locator('.campaign-card'); const count = await cards.count(); expect(count).toBe(3); const cardText = await cards.first().textContent(); expect(cardText).toContain('G-Delivery'); expect(cardText).toContain('Semana 1'); expect(cardText).toContain('Limite: 10'); }); test('separate manual/automatic limits display', async ({ page }) => { await setCookieOnPage(page, cookie); await page.goto('/'); await page.waitForLoadState('networkidle'); await page.click('button[data-tab="campanhas"]'); await page.waitForTimeout(1500); const cardText = await page.locator('.campaign-card').first().textContent(); expect(cardText).toMatch(/Manual|manual/); expect(cardText).toMatch(/Automático|auto/i); }); }); test.describe('Campaign Queue Operations (via API)', () => { let cookie; test.beforeAll(async () => { cookie = await loginViaApi(); }); test('prepare creates queue items', async () => { const { data } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } }); const campaignId = data.campaigns[0].id; const response = await ctx.apiRequest('POST', `/api/campaigns/${campaignId}/prepare`, { headers: { cookie }, body: {} }); expect(response.status).toBe(200); expect(response.data.summary.previewed).toBeGreaterThanOrEqual(0); }); test('preview rows show server-rendered text', async () => { const { data: campaigns } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } }); const campaignId = campaigns.campaigns[0].id; await ctx.apiRequest('POST', `/api/campaigns/${campaignId}/prepare`, { headers: { cookie }, body: {} }); const { data: queue } = await ctx.apiRequest('GET', `/api/campaigns/${campaignId}/queue`, { headers: { cookie } }); expect(queue.queue.length).toBeGreaterThanOrEqual(1); expect(queue.queue[0].rendered_message).toBeTruthy(); expect(queue.queue[0].status).toBe('preview'); }); test('per-row removal works', async () => { const { data: campaigns } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } }); const campaignId = campaigns.campaigns[0].id; await ctx.apiRequest('POST', `/api/campaigns/${campaignId}/prepare`, { headers: { cookie }, body: {} }); const { data: before } = await ctx.apiRequest('GET', `/api/campaigns/${campaignId}/queue?status=preview`, { headers: { cookie } }); if (before.queue.length === 0) return; const firstId = before.queue[0].id; const removeResponse = await ctx.apiRequest('DELETE', `/api/campaigns/${campaignId}/queue/${firstId}`, { headers: { cookie } }); expect(removeResponse.status).toBe(200); const { data: after } = await ctx.apiRequest('GET', `/api/campaigns/${campaignId}/queue?status=preview`, { headers: { cookie } }); expect(after.queue.find((q) => q.id === firstId)).toBeUndefined(); }); test('approval flow works', async () => { const { data: campaigns } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } }); const campaign = campaigns.campaigns.find((c) => c.status === 'paused'); if (!campaign) return; let { data: queue } = await ctx.apiRequest('GET', `/api/campaigns/${campaign.id}/queue?status=preview`, { headers: { cookie } }); if (queue.queue.length === 0) { await ctx.apiRequest('POST', `/api/campaigns/${campaign.id}/prepare`, { headers: { cookie }, body: {} }); const { data: refetched } = await ctx.apiRequest('GET', `/api/campaigns/${campaign.id}/queue?status=preview`, { headers: { cookie } }); if (refetched.queue.length === 0) return; queue.queue = refetched.queue; } const ids = queue.queue.slice(0, 1).map((q) => q.id); const approveResponse = await ctx.apiRequest('POST', `/api/campaigns/${campaign.id}/approve`, { headers: { cookie }, body: { queueIds: ids, date: '2026-07-29' } }); expect(approveResponse.status).toBe(200); expect(approveResponse.data.scheduled).toBeGreaterThanOrEqual(1); const { data: updated } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } }); const updatedCampaign = updated.campaigns.find((c) => c.id === campaign.id); expect(['ready', 'running']).toContain(updatedCampaign.status); }); test('pause shows reason', async () => { const { data: campaigns } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } }); const campaign = campaigns.campaigns.find((c) => c.status === 'ready' || c.status === 'paused'); if (!campaign) return; const pauseResponse = await ctx.apiRequest('POST', `/api/campaigns/${campaign.id}/pause`, { headers: { cookie }, body: { reason: 'revisao manual do operador' } }); expect(pauseResponse.status).toBe(200); expect(pauseResponse.data.campaign.pause_reason).toBe('revisao manual do operador'); expect(pauseResponse.data.campaign.status).toBe('paused'); }); test('start succeeds on paused campaign with scheduled items', async () => { const { data: campaigns } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } }); const campaign = campaigns.campaigns.find((c) => c.status === 'paused' || c.status === 'ready'); if (!campaign) return; const startResponse = await ctx.apiRequest('POST', `/api/campaigns/${campaign.id}/start`, { headers: { cookie }, body: {} }); expect(startResponse.status).toBe(200); expect(startResponse.data.campaign.status).toBe('running'); }); }); test.describe('Stage Classification', () => { let cookie; test.beforeAll(async () => { cookie = await loginViaApi(); }); test('stage classification works via API', async () => { const leadId = Number(ctx.db.prepare( "SELECT id FROM leads WHERE empresa LIKE '%Restaurante%' LIMIT 1" ).get()?.id); if (!leadId) return; const stageResponse = await ctx.apiRequest('PUT', `/api/campaigns/leads/${leadId}/stage`, { headers: { cookie }, body: { stage: 'interessado_gti_vendas', note: 'demonstracao agendada' } }); expect(stageResponse.status).toBe(200); expect(stageResponse.data.state.stage).toBe('interessado_gti_vendas'); }); test('appointment/demo scheduling form works via API', async () => { const leadId = Number(ctx.db.prepare( "SELECT id FROM leads WHERE empresa LIKE '%Pizzaria%' LIMIT 1" ).get()?.id); if (!leadId) return; const appointmentResponse = await ctx.apiRequest('POST', `/api/campaigns/leads/${leadId}/appointments`, { headers: { cookie }, body: { kind: 'demonstracao', participantName: 'Maria Silva', companyName: 'Pizzaria Teste', preferredDay: '2026-08-05', preferredTime: '15:30' } }); expect(appointmentResponse.status).toBe(201); expect(appointmentResponse.data.appointment.kind).toBe('demonstracao'); }); test('pending replies section accessible via API', async () => { const { data } = await ctx.apiRequest('GET', '/api/campaigns/replies/pending', { headers: { cookie } }); expect(Array.isArray(data.pending)).toBe(true); }); test('metrics endpoint returns data', async () => { const { data: campaigns } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } }); if (campaigns.campaigns.length === 0) return; const campaignId = campaigns.campaigns[0].id; const { data } = await ctx.apiRequest('GET', `/api/campaigns/${campaignId}/metrics`, { headers: { cookie } }); expect(data.metrics).toBeDefined(); }); test('metrics supports segmento and variant filters', async () => { const { data: campaigns } = await ctx.apiRequest('GET', '/api/campaigns', { headers: { cookie } }); if (campaigns.campaigns.length === 0) return; const campaignId = campaigns.campaigns[0].id; const { data: filtered } = await ctx.apiRequest( 'GET', `/api/campaigns/${campaignId}/metrics?segmento=hamburguerias&variant=A`, { headers: { cookie } } ); expect(filtered.metrics).toBeDefined(); }); }); test.describe('UI Rendering', () => { let cookie; test.beforeAll(async () => { cookie = await loginViaApi(); }); test('pending replies section visible on campaigns page', async ({ page }) => { await setCookieOnPage(page, cookie); await page.goto('/'); await page.waitForLoadState('networkidle'); await page.click('button[data-tab="campanhas"]'); await page.waitForTimeout(1500); const repliesSection = page.locator('#pending-replies'); await expect(repliesSection).toBeVisible(); const content = await repliesSection.textContent(); expect(content.length).toBeGreaterThan(0); }); test('stage classification section renders', async ({ page }) => { await setCookieOnPage(page, cookie); await page.goto('/'); await page.waitForLoadState('networkidle'); await page.click('button[data-tab="campanhas"]'); await page.waitForTimeout(1500); const stageSection = page.locator('#stage-classification'); await expect(stageSection).toBeVisible(); }); test('appointment scheduling form renders', async ({ page }) => { await setCookieOnPage(page, cookie); await page.goto('/'); await page.waitForLoadState('networkidle'); await page.click('button[data-tab="campanhas"]'); await page.waitForTimeout(1500); const appointmentSection = page.locator('#appointment-form'); await expect(appointmentSection).toBeVisible(); }); test('metrics section renders with filters', async ({ page }) => { await setCookieOnPage(page, cookie); await page.goto('/'); await page.waitForLoadState('networkidle'); await page.click('button[data-tab="campanhas"]'); await page.waitForTimeout(1500); const metricsFilters = page.locator('#metrics-filters'); await expect(metricsFilters).toBeVisible(); await page.waitForTimeout(1000); const filterSelects = metricsFilters.locator('select'); const selectCount = await filterSelects.count(); expect(selectCount).toBeGreaterThanOrEqual(1); }); }); test.describe('Safety', () => { test('fake provider send count remains zero', async () => { expect(ctx.fakeSends.length).toBe(0); }); }); test.describe('Mobile Viewport', () => { let cookie; test.beforeAll(async () => { cookie = await loginViaApi(); }); test('mobile viewport renders campaign cards', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await setCookieOnPage(page, cookie); await page.goto('/'); await page.waitForLoadState('networkidle'); await page.click('button[data-tab="campanhas"]'); await page.waitForTimeout(1500); const cards = page.locator('.campaign-card'); const count = await cards.count(); expect(count).toBe(3); for (let i = 0; i < count; i++) { await expect(cards.nth(i)).toBeVisible(); } const queueSection = page.locator('#campaign-queue'); await expect(queueSection).toBeVisible(); }); test('mobile viewport shows full-width controls', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await setCookieOnPage(page, cookie); await page.goto('/'); await page.waitForLoadState('networkidle'); await page.click('button[data-tab="campanhas"]'); await page.waitForTimeout(1500); const section = page.locator('#tab-campanhas'); const box = await section.boundingBox(); expect(box.width).toBeGreaterThanOrEqual(350); }); }); });