mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
156 lines
6.5 KiB
TypeScript
156 lines
6.5 KiB
TypeScript
import type { Page } from '@playwright/test'
|
|
|
|
import type { CampaignUser } from '../../src/payload-types.js'
|
|
import {
|
|
expect,
|
|
test,
|
|
waitForStreamSettled,
|
|
type CampaignE2EFixture,
|
|
} from './fixtures/campaignE2EFixtures.js'
|
|
|
|
const MOBILE_VIEWPORT = { width: 390, height: 844 }
|
|
const DESKTOP_VIEWPORT = { width: 1280, height: 900 }
|
|
|
|
const seedUpdates = async (
|
|
campaign: CampaignE2EFixture,
|
|
): Promise<CampaignUser & { password: string }> => {
|
|
const { fixtures } = campaign
|
|
const coordinator = await fixtures.createCampaignUser('coordinator')
|
|
const municipality = await fixtures.claimMunicipality()
|
|
// Explicitly increasing timestamps: rows created back-to-back can share the
|
|
// same Postgres `now()` (transaction-scoped), which makes the feed's
|
|
// `sort: -createdAt` unstable and the "#8 on top" assertion flaky.
|
|
const base = Date.now()
|
|
for (let i = 0; i < 8; i += 1) {
|
|
await fixtures.payload.create({
|
|
collection: 'municipalityUpdate',
|
|
data: {
|
|
municipality: municipality.id,
|
|
author: coordinator.id,
|
|
polarity: i % 2 === 0 ? 'boa' : 'ruim',
|
|
body: `Fato de campo C106 #${i + 1} em ${municipality.name}`,
|
|
createdAt: new Date(base + (i + 1) * 1000).toISOString(),
|
|
},
|
|
depth: 0,
|
|
})
|
|
}
|
|
return coordinator
|
|
}
|
|
|
|
const scrollFeedToBottom = (page: Page) =>
|
|
page.evaluate(() => {
|
|
document.querySelector('[data-slot="campaign-content-scroll"]')?.scrollTo(0, 1e5)
|
|
})
|
|
|
|
test.describe('C106 — atualizações mobile sem moldura', () => {
|
|
test.setTimeout(90_000)
|
|
|
|
test('mobile: strip sem label/moldura e sticky, criar no header, cards edge-to-edge', async ({
|
|
campaign,
|
|
page,
|
|
}) => {
|
|
const coordinator = await seedUpdates(campaign)
|
|
await page.setViewportSize(MOBILE_VIEWPORT)
|
|
await campaign.login(page, coordinator.email!, coordinator.password)
|
|
await page.goto(`${campaign.baseURL}/campanha/atualizacoes`)
|
|
await waitForStreamSettled(page, { timeout: 15_000 })
|
|
|
|
const strip = page.getByRole('search')
|
|
const feedList = page.locator('ul').filter({ hasText: 'Fato de campo C106 #8' }).first()
|
|
|
|
// Label visualmente oculta no mobile (o input carrega o accessible name).
|
|
await expect(strip.locator('label')).toBeHidden()
|
|
|
|
// Filtro sem moldura e sticky sob a top bar, separado por uma linha
|
|
// (campaignListOmniboxFormClassName — padrão B184 compartilhado).
|
|
await expect(strip).toHaveCSS('position', 'sticky')
|
|
await expect(strip).toHaveCSS('top', '0px')
|
|
await expect(strip).toHaveCSS('border-bottom-width', '1px')
|
|
// C88 — scoped to the filter strip: each update card may now carry its own
|
|
// combobox (the deliberation assignee select), so a page-wide locator is
|
|
// ambiguous.
|
|
await expect(strip.getByRole('combobox').locator('xpath=..')).toHaveCSS(
|
|
'border-top-width',
|
|
'0px',
|
|
)
|
|
|
|
// "Nova atualização" com texto some; o icon entra no header mobile.
|
|
await expect(strip.getByRole('button', { name: 'Nova atualização' })).toBeHidden()
|
|
const headerCreate = page.locator(
|
|
'[data-slot="campaign-mobile-top-bar"] button[aria-label="Nova atualização"]',
|
|
)
|
|
await expect(headerCreate).toBeVisible()
|
|
|
|
// Cards edge-to-edge, sem moldura, com a linha entre eles.
|
|
await expect(feedList).toHaveCSS('margin-left', '-16px')
|
|
const firstCard = feedList.locator('li').first()
|
|
await expect(firstCard).toHaveCSS('border-top-width', '0px')
|
|
await expect(firstCard).toHaveCSS('border-radius', '0px')
|
|
await expect(firstCard).toHaveCSS('border-bottom-width', '1px')
|
|
await expect(firstCard).toContainText('Fato de campo C106 #8')
|
|
|
|
// O filtro permanece visível o tempo todo: preso no topo durante a rolagem
|
|
// (o padrão B184 do form pina na borda do scroll container — a posição
|
|
// exata é a natural, o que importa é não rolar junto com os cards).
|
|
const pinnedY = (await strip.boundingBox())?.y
|
|
await scrollFeedToBottom(page)
|
|
await expect.poll(async () => (await strip.boundingBox())?.y).toBe(pinnedY)
|
|
const scrolled = await page.evaluate(
|
|
() => document.querySelector('[data-slot="campaign-content-scroll"]')?.scrollTop ?? 0,
|
|
)
|
|
expect(scrolled).toBeGreaterThan(100)
|
|
|
|
// O icon no header abre o mesmo bottom sheet de criação.
|
|
await headerCreate.click()
|
|
const dialog = page.getByRole('dialog')
|
|
await expect(dialog).toBeVisible()
|
|
await expect(dialog.getByText('Nova atualização', { exact: true })).toBeVisible()
|
|
await expect(dialog.getByRole('button', { name: 'Registrar atualização' })).toBeVisible()
|
|
})
|
|
|
|
test('mobile: empty state aponta para a ação de criar sem citar o botão', async ({
|
|
campaign,
|
|
page,
|
|
}) => {
|
|
const coordinator = await seedUpdates(campaign)
|
|
await page.setViewportSize(MOBILE_VIEWPORT)
|
|
await campaign.login(page, coordinator.email!, coordinator.password)
|
|
await page.goto(`${campaign.baseURL}/campanha/atualizacoes`)
|
|
await waitForStreamSettled(page, { timeout: 15_000 })
|
|
|
|
await page.getByRole('combobox', { name: 'Filtrar atualizações' }).fill('zzz')
|
|
await page.getByRole('combobox', { name: 'Filtrar atualizações' }).press('Enter')
|
|
|
|
await expect(page.getByText('Nenhuma atualização encontrada')).toBeVisible()
|
|
await expect(page.getByText('Ajuste o filtro ou registre um novo fato de campo.')).toBeVisible()
|
|
})
|
|
|
|
test('desktop: label, botão com texto e cards com moldura ficam como estão', async ({
|
|
campaign,
|
|
page,
|
|
}) => {
|
|
const coordinator = await seedUpdates(campaign)
|
|
await page.setViewportSize(DESKTOP_VIEWPORT)
|
|
await campaign.login(page, coordinator.email!, coordinator.password)
|
|
await page.goto(`${campaign.baseURL}/campanha/atualizacoes`)
|
|
await waitForStreamSettled(page, { timeout: 15_000 })
|
|
|
|
const strip = page.getByRole('search')
|
|
|
|
// Sem sticky e sem linha separadora no desktop.
|
|
await expect(strip).toHaveCSS('position', 'static')
|
|
await expect(strip).toHaveCSS('border-bottom-width', '0px')
|
|
|
|
// Label e botão com texto seguem visíveis.
|
|
await expect(strip.locator('label')).toBeVisible()
|
|
await expect(strip.getByRole('button', { name: 'Nova atualização' })).toBeVisible()
|
|
|
|
// Nenhum icon de criar nos headers.
|
|
await expect(page.locator('header button[aria-label="Nova atualização"]').first()).toBeHidden()
|
|
|
|
// Cards mantêm a moldura arredondada e o gap.
|
|
const firstCard = page.locator('ul li').filter({ hasText: 'Fato de campo C106 #8' }).first()
|
|
await expect(firstCard).toHaveCSS('border-top-width', '1px')
|
|
await expect(firstCard).toHaveCSS('border-radius', '12px')
|
|
})
|
|
})
|