mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
O nome letters-only não carrega o runID, então o discoverOwnedRows do ownership não encontra o contato criado via formulário (create row/sheet) e a limpeza da fixture o deixava para trás — cada run vazava 2 contatos no banco de teste. Deletar explicitamente por nome exato no fim do teste.
291 lines
12 KiB
TypeScript
291 lines
12 KiB
TypeScript
import {
|
|
campaignPageChrome,
|
|
expect,
|
|
expectPostResponse,
|
|
test,
|
|
} from './fixtures/campaignE2EFixtures.js'
|
|
|
|
import type { CampaignE2EOwnership } from './fixtures/campaignE2EFixtures.js'
|
|
|
|
/**
|
|
* C139 — the campaign contacts entity page: staff CRUD on `/campanha/contatos`
|
|
* (desktop table + create row, mobile cards + FAB/sheet) and the staff gate.
|
|
*
|
|
* The UI name schema forbids digits (`contactNameSchema`), so contacts CREATED
|
|
* through the forms need a letters-only unique name: the fixture runID is a
|
|
* hex UUID, and hex → [a-p] is a bijection that keeps the full 128-bit
|
|
* uniqueness (API-created fixture contacts may keep digits).
|
|
*/
|
|
const lettersOnlyUnique = (runID: string): string =>
|
|
runID
|
|
.replaceAll('-', '')
|
|
.replace(/[0-9a-f]/g, (hex) => String.fromCharCode(97 + parseInt(hex, 16)))
|
|
|
|
const uiName = (fixtures: CampaignE2EOwnership, prefix: string): string =>
|
|
`${prefix} ${lettersOnlyUnique(fixtures.runID)}`
|
|
|
|
test.describe('Contatos — página da entidade Contact', () => {
|
|
test('staff opens the contacts page and the omnibox narrows the recorte', async ({
|
|
campaign,
|
|
page,
|
|
}) => {
|
|
const { fixtures } = campaign
|
|
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
|
name: fixtures.value('Coordenadora Contatos'),
|
|
})
|
|
const phone = fixtures.phone()
|
|
const first = await fixtures.payload.create({
|
|
collection: 'contact',
|
|
data: {
|
|
name: `Alpha ${fixtures.value('Busca')}`,
|
|
phones: [{ value: phone }],
|
|
state: 'BA',
|
|
city: 'Salvador',
|
|
},
|
|
depth: 0,
|
|
})
|
|
const second = await fixtures.payload.create({
|
|
collection: 'contact',
|
|
data: { name: `Beta ${fixtures.value('Busca')}`, state: 'BA', city: 'Salvador' },
|
|
depth: 0,
|
|
})
|
|
|
|
await campaign.login(page, coordinator.email!, coordinator.password)
|
|
await page.goto('/campanha/contatos')
|
|
|
|
await expect(page).toHaveURL(/\/campanha\/contatos$/)
|
|
await expect(campaignPageChrome(page, 'Contatos')).toBeVisible()
|
|
const firstRow = page.getByRole('row', { name: new RegExp(first.name as string) })
|
|
await expect(firstRow).toBeVisible()
|
|
await expect(page.getByRole('row', { name: new RegExp(second.name as string) })).toBeVisible()
|
|
await expect(page.getByText(/^\d+ contatos?$/)).toBeVisible()
|
|
|
|
// The row action links to WhatsApp with the primary phone (C139).
|
|
await expect(
|
|
firstRow.getByRole('link', { name: `Enviar WhatsApp para ${first.name}` }),
|
|
).toHaveAttribute('href', `https://wa.me/55${phone}`)
|
|
|
|
// The omnibox narrows the recorte (canonical URL, chips mounted).
|
|
const omnibox = page.getByRole('combobox', { name: 'Filtrar contatos' })
|
|
await omnibox.fill(first.name as string)
|
|
await omnibox.press('Enter')
|
|
await expect(page).toHaveURL(/q=/)
|
|
await expect(firstRow).toBeVisible()
|
|
await expect(page.getByRole('row', { name: new RegExp(second.name as string) })).toBeHidden()
|
|
})
|
|
|
|
test('the desktop create row keeps typed values on error and lands a valid contact', async ({
|
|
campaign,
|
|
page,
|
|
}) => {
|
|
const { fixtures } = campaign
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
const contactName = uiName(fixtures, 'Contato Criado')
|
|
const phone = fixtures.phone()
|
|
|
|
await campaign.login(page, coordinator.email!, coordinator.password)
|
|
await page.goto('/campanha/contatos')
|
|
|
|
await page.getByRole('button', { name: 'Novo contato' }).click()
|
|
const createRow = page.locator('[data-view="contact-create-row"]')
|
|
const nameInput = createRow.getByRole('textbox', { name: 'Nome do contato' })
|
|
await expect(nameInput).toBeVisible()
|
|
|
|
// Digits are not allowed by the name schema: the alert shows and the
|
|
// typed value survives the failed submit (C139 — no React form reset).
|
|
await nameInput.fill('Nome Invalido 123')
|
|
await createRow.getByRole('button', { name: 'Salvar' }).click()
|
|
await expect(createRow.getByRole('alert')).toContainText(
|
|
'Use apenas letras, no máximo um espaço ou hífen entre termos',
|
|
)
|
|
await expect(nameInput).toHaveValue('Nome Invalido 123')
|
|
|
|
// Valid create lands the row and the write reaches the database.
|
|
await nameInput.fill(contactName)
|
|
await createRow.getByRole('textbox', { name: 'Telefone do contato' }).fill(phone)
|
|
const persisted = expectPostResponse(page, '/campanha/contatos')
|
|
await createRow.getByRole('button', { name: 'Salvar' }).click()
|
|
await persisted
|
|
await expect(page.getByRole('row', { name: new RegExp(contactName) })).toBeVisible()
|
|
|
|
const stored = await fixtures.payload.find({
|
|
collection: 'contact',
|
|
where: { name: { equals: contactName } },
|
|
depth: 0,
|
|
pagination: false,
|
|
})
|
|
expect(stored.docs).toHaveLength(1)
|
|
expect(stored.docs[0]!.phones?.[0]?.value).toBe(phone)
|
|
|
|
// The row was created THROUGH the UI, so the ownership proxy never tracked
|
|
// it (and its letters-only name does not carry the runID, so discovery
|
|
// cannot find it) — delete it explicitly or every run leaks a contact.
|
|
await fixtures.payload.delete({
|
|
collection: 'contact',
|
|
where: { name: { equals: contactName } },
|
|
depth: 0,
|
|
})
|
|
})
|
|
|
|
test('a duplicate name shows the safe conflict message in the create row', async ({
|
|
campaign,
|
|
page,
|
|
}) => {
|
|
const { fixtures } = campaign
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
const existing = await fixtures.payload.create({
|
|
collection: 'contact',
|
|
data: { name: uiName(fixtures, 'Conflito Existente'), state: 'BA' },
|
|
depth: 0,
|
|
})
|
|
|
|
await campaign.login(page, coordinator.email!, coordinator.password)
|
|
await page.goto('/campanha/contatos')
|
|
|
|
await page.getByRole('button', { name: 'Novo contato' }).click()
|
|
const createRow = page.locator('[data-view="contact-create-row"]')
|
|
const nameInput = createRow.getByRole('textbox', { name: 'Nome do contato' })
|
|
await nameInput.fill(existing.name as string)
|
|
await createRow.getByRole('button', { name: 'Salvar' }).click()
|
|
await expect(createRow.getByRole('alert')).toContainText(
|
|
'Já existe um contato com este nome — confira a lista antes de salvar.',
|
|
)
|
|
await expect(nameInput).toHaveValue(existing.name as string)
|
|
})
|
|
|
|
test('leader cannot open the contacts page', async ({ campaign, page }) => {
|
|
const { fixtures } = campaign
|
|
const phone = fixtures.phone()
|
|
const leader = await fixtures.createCampaignUser('leader', {
|
|
name: fixtures.value('Liderança Contatos Sem Acesso'),
|
|
username: phone,
|
|
})
|
|
|
|
await campaign.login(page, phone, leader.password)
|
|
await page.goto('/campanha/contatos')
|
|
|
|
await expect(page).toHaveURL(/\/campanha\/meus-contatos/)
|
|
await expect(page.getByRole('heading', { name: 'Meus contatos' })).toBeVisible()
|
|
})
|
|
|
|
test('mobile: the FAB opens the create sheet and a valid save lands a card', async ({
|
|
campaign,
|
|
page,
|
|
}) => {
|
|
await page.setViewportSize({ width: 390, height: 844 })
|
|
const { fixtures } = campaign
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
const contactName = uiName(fixtures, 'Contato Sheet')
|
|
const phone = fixtures.phone()
|
|
|
|
await campaign.login(page, coordinator.email!, coordinator.password)
|
|
await page.goto('/campanha/contatos')
|
|
|
|
// The FAB is the mobile-only create entry point (the toolbar row is
|
|
// `md:hidden`); it floats above the bottom nav (C139 z-index fix).
|
|
await page.getByRole('button', { name: 'Novo contato' }).click()
|
|
const dialog = page.getByRole('dialog', { name: 'Novo contato' })
|
|
await expect(dialog).toBeVisible()
|
|
await dialog.getByRole('textbox', { name: 'Nome *' }).fill(contactName)
|
|
await dialog.getByRole('button', { name: 'Adicionar telefone' }).click()
|
|
await dialog.getByRole('textbox', { name: 'Celular 1' }).fill(phone)
|
|
|
|
const persisted = expectPostResponse(page, '/campanha/contatos')
|
|
await dialog.getByRole('button', { name: 'Salvar' }).click()
|
|
await persisted
|
|
await expect(dialog).toBeHidden()
|
|
|
|
const card = page.locator('[data-view="mobile-cards"] li').filter({ hasText: contactName })
|
|
await expect(card).toBeVisible()
|
|
await expect(card).toContainText('BA')
|
|
|
|
const stored = await fixtures.payload.find({
|
|
collection: 'contact',
|
|
where: { name: { equals: contactName } },
|
|
depth: 0,
|
|
pagination: false,
|
|
})
|
|
expect(stored.docs).toHaveLength(1)
|
|
expect(stored.docs[0]!.phones?.[0]?.value).toBe(phone)
|
|
await expect(
|
|
card.getByRole('link', { name: `Enviar WhatsApp para ${contactName}` }),
|
|
).toHaveAttribute('href', `https://wa.me/55${phone}`)
|
|
|
|
// UI-created (see the desktop create-row test): delete explicitly so the
|
|
// ownership discovery (runID in name) is not the only net.
|
|
await fixtures.payload.delete({
|
|
collection: 'contact',
|
|
where: { name: { equals: contactName } },
|
|
depth: 0,
|
|
})
|
|
})
|
|
|
|
test('mobile: the edit sheet keeps values on conflict and saves the ficha atomically', async ({
|
|
campaign,
|
|
page,
|
|
}) => {
|
|
await page.setViewportSize({ width: 390, height: 844 })
|
|
const { fixtures } = campaign
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
|
|
const targetName = uiName(fixtures, 'Ficha Editada')
|
|
const target = await fixtures.payload.create({
|
|
collection: 'contact',
|
|
data: {
|
|
name: targetName,
|
|
phones: [{ value: fixtures.phone() }],
|
|
state: 'BA',
|
|
city: 'Salvador',
|
|
},
|
|
depth: 0,
|
|
})
|
|
// The conflict target: a letters-only name that already exists.
|
|
const conflictName = uiName(fixtures, 'Ficha Conflito')
|
|
await fixtures.payload.create({
|
|
collection: 'contact',
|
|
data: { name: conflictName, state: 'BA' },
|
|
depth: 0,
|
|
})
|
|
|
|
await campaign.login(page, coordinator.email!, coordinator.password)
|
|
await page.goto('/campanha/contatos')
|
|
|
|
const omnibox = page.getByRole('combobox', { name: 'Filtrar contatos' })
|
|
await omnibox.fill(targetName)
|
|
await omnibox.press('Enter')
|
|
|
|
await page.getByRole('button', { name: `Editar contato ${targetName}` }).click()
|
|
const dialog = page.getByRole('dialog', { name: 'Editar contato' })
|
|
await expect(dialog).toBeVisible()
|
|
await expect(dialog.getByRole('textbox', { name: 'Nome *' })).toHaveValue(targetName)
|
|
|
|
// A conflict (the typed name already exists) keeps the sheet open with
|
|
// the typed value and a visible banner — no silent wipe (C139).
|
|
await dialog.getByRole('textbox', { name: 'Nome *' }).fill(conflictName)
|
|
await dialog.getByRole('button', { name: 'Salvar' }).click()
|
|
await expect(dialog.getByRole('alert')).toContainText(
|
|
'Já existe um contato com este nome — confira a lista antes de salvar.',
|
|
)
|
|
await expect(dialog.getByRole('textbox', { name: 'Nome *' })).toHaveValue(conflictName)
|
|
|
|
// Reverting the name and editing the city writes the whole ficha in one
|
|
// action: the card shows the new city and the database agrees.
|
|
await dialog.getByRole('textbox', { name: 'Nome *' }).fill(targetName)
|
|
await dialog.getByRole('textbox', { name: 'Cidade' }).fill('Feira de Santana')
|
|
const persisted = expectPostResponse(page, '/campanha/contatos')
|
|
await dialog.getByRole('button', { name: 'Salvar' }).click()
|
|
await persisted
|
|
await expect(dialog).toBeHidden()
|
|
|
|
const card = page.locator('[data-view="mobile-cards"] li').filter({ hasText: targetName })
|
|
await expect(card).toContainText('Feira de Santana · BA')
|
|
|
|
const stored = await fixtures.payload.findByID({
|
|
collection: 'contact',
|
|
id: target.id,
|
|
depth: 0,
|
|
})
|
|
expect(stored.city).toBe('Feira de Santana')
|
|
expect(stored.name).toBe(targetName)
|
|
})
|
|
})
|