Tracks the app that was already in place (capture, treatment, outreach, WhatsApp integration, server, frontend, tests, docs) so future feature work can branch and use isolated worktrees.
389 lines
14 KiB
JavaScript
389 lines
14 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|