teqo/tests/e2e/campaignSavedFilters.e2e.spec.ts
Francisco Solla f0dff330bd feat(C102): sem hamburger/sheet de sidebar no mobile staff — navegação pela barra + Mais
No mobile (<768px) staff perde o botão de abrir sidebar na top bar
(CampaignMobileTopBar role-gated via useCampaignPageChromeRole +
isStaffCampaignRole; leader mantém) e o sheet de navegação deixa de
montar (CampaignSidebar early-return quando isMobile && staff).
Filtros salvos de Municípios (B18) migram para o drawer Mais
(MunicipalityNavSavedFilters variant 'overflow', mesma store,
apagar/desfazer/sucessão de foco; drawer ganha overflow-y-auto).

Correção de bug pré-existente (D4): o Toaster vivia dentro do wrapper
[contain:layout_paint] do painel da AI, que vira containing block para
position:fixed e stacking context próprio — toasts com ação (ex.
Desfazer) ficavam inalcançáveis sob os portais z-50 de drawer/sheet.
Movido para o root layout (campaign).

Testes: unit top bar (staff sem trigger / leader com trigger) + e2e
(staff sem trigger/dialog, leader com sheet, B18 no drawer completo).
2026-08-09 19:13:43 -03:00

164 lines
7.4 KiB
TypeScript

import { expect, test } from './fixtures/campaignE2EFixtures.js'
/**
* B18 — a named recorte of `/campanha/municipios` becomes a sidebar shortcut.
*
* Own file for the same reason B17 took one: `campaignMunicipalities.e2e.spec.ts`
* carries deterministic failures at one worker, ledgered as P1, and a red run
* there must not be read as a regression here.
*/
/** 416 whole municipalities — deep enough that `?page=2` is a real page. */
const SAVED_RECORTE = '/campanha/municipios?priority=alta'
const SAVED_NAME = 'Recorte do teste'
const RENAMED = 'Recorte renomeado'
/**
* A route transition on this list costs a full RSC round-trip, and in dev the
* suite's two workers share one server — which overruns the 5 s default of
* `expect` and produces a flake nobody reproduces at one worker. Same budget,
* for the same reason, as `campaignColumnPicker.e2e.spec.ts`.
*/
const NAVIGATION = { timeout: 20_000 }
test.describe('Filtros salvos de Municípios', () => {
test('saves the current recorte, restores it from the sidebar and deletes it', async ({
campaign,
page,
}) => {
test.slow()
const { fixtures } = campaign
const coordinator = await fixtures.createCampaignUser('coordinator', {
name: fixtures.value('Coordenadora Filtros'),
})
const password = coordinator.password
const email = coordinator.email!
await campaign.login(page, email, password)
// The bare list has no recorte to name.
await page.goto('/campanha/municipios')
await expect(page.getByRole('button', { name: 'Salvar filtro', exact: true })).toHaveCount(0)
await page.goto(SAVED_RECORTE)
await page.getByRole('button', { name: 'Salvar filtro', exact: true }).click()
const nameInput = page.getByLabel('Nome do filtro')
// The suggested name is the active-filters summary the bar already shows.
await expect(nameInput).not.toHaveValue('')
await nameInput.fill(SAVED_NAME)
await page.getByRole('button', { name: 'Salvar', exact: true }).click()
const shortcut = page.getByRole('link', { name: SAVED_NAME })
await expect(shortcut).toBeVisible()
await expect(shortcut).toHaveAttribute('aria-current', 'page')
// A page is a position inside the recorte, not part of it.
await page.goto(`${SAVED_RECORTE}&page=2`, NAVIGATION)
await expect(shortcut).toHaveAttribute('aria-current', 'page')
// Another recorte is not this one, and the shortcut brings it back. Use the
// sidebar link — a bare `goto` here races an in-flight RSC navigation and
// flakes as net::ERR_ABORTED under prod load (measured in CI 2026-07-30).
await page.getByRole('link', { name: 'Municípios', exact: true }).click()
await expect(page).toHaveURL('/campanha/municipios', NAVIGATION)
await expect(shortcut).not.toHaveAttribute('aria-current', 'page')
await shortcut.click()
await expect(page).toHaveURL(SAVED_RECORTE, NAVIGATION)
await expect(shortcut).toHaveAttribute('aria-current', 'page', NAVIGATION)
// Saving a recorte already bookmarked renames it instead of adding a twin.
await page.getByRole('button', { name: `Renomear o filtro salvo ${SAVED_NAME}` }).click()
await expect(page.getByLabel('Nome do filtro')).toHaveValue(SAVED_NAME)
await page.getByLabel('Nome do filtro').fill(RENAMED)
await page.getByRole('button', { name: 'Atualizar nome' }).click()
await expect(page.getByRole('link', { name: RENAMED })).toBeVisible()
await expect(shortcut).toHaveCount(0)
const remove = page.getByRole('button', { name: `Apagar o filtro salvo ${RENAMED}` })
await remove.click()
await expect(page.getByRole('link', { name: RENAMED })).toHaveCount(0)
// Emptying the group unmounts the sub-list, so focus lands on the nav link
// rather than falling to <body>.
await expect(page.getByRole('link', { name: 'Municípios', exact: true })).toBeFocused()
// Deleting is undoable, which is why it never asks for confirmation.
await page.getByRole('button', { name: 'Desfazer' }).click()
await expect(page.getByRole('link', { name: RENAMED })).toBeVisible()
})
test('survives a reload with shortcuts always visible', async ({ campaign, page }) => {
// Three loads of a heavy list route, against a dev server shared with the
// other worker — the login redirect alone can outrun the 30 s default.
test.slow()
const { fixtures } = campaign
const coordinator = await fixtures.createCampaignUser('coordinator', {
name: fixtures.value('Coordenadora Sidebar'),
})
const password = coordinator.password
const email = coordinator.email!
await campaign.login(page, email, password)
await page.goto(SAVED_RECORTE)
await page.getByRole('button', { name: 'Salvar filtro', exact: true }).click()
await page.getByLabel('Nome do filtro').fill(SAVED_NAME)
await page.getByRole('button', { name: 'Salvar', exact: true }).click()
const shortcut = page.getByRole('link', { name: SAVED_NAME })
await expect(shortcut).toBeVisible()
await page.reload()
await expect(shortcut).toBeVisible(NAVIGATION)
await expect(page.getByRole('list', { name: 'Filtros salvos de Municípios' })).toBeVisible()
await expect(page.getByRole('button', { name: /filtros salvos de Municípios/i })).toHaveCount(0)
})
test.describe('C102 — saved filters live in the "Mais" drawer on mobile', () => {
test.use({ viewport: { width: 390, height: 844 } })
test('saves, navigates from and deletes inside the drawer', async ({ campaign, page }) => {
test.slow()
const { fixtures } = campaign
const coordinator = await fixtures.createCampaignUser('coordinator', {
name: fixtures.value('Coordenadora Drawer Mobile'),
})
const password = coordinator.password
const email = coordinator.email!
await campaign.login(page, email, password)
await page.goto(SAVED_RECORTE)
await page.getByRole('button', { name: 'Salvar filtro', exact: true }).click()
await page.getByLabel('Nome do filtro').fill(SAVED_NAME)
await page.getByRole('button', { name: 'Salvar', exact: true }).click()
// The mobile staff top bar has no hamburger (C102) — the drawer is the home.
const topBar = page.locator('[data-slot="campaign-mobile-top-bar"]')
await expect(topBar.locator('[data-slot="sidebar-trigger"]')).toHaveCount(0)
const bottomNav = page.locator('[aria-label="Navegação principal"]')
await bottomNav.getByRole('button', { name: 'Mais' }).click()
const drawer = page.locator('[data-slot="drawer-content"]')
await expect(drawer).toBeVisible()
await expect(drawer.getByText('Filtros salvos de Municípios')).toBeVisible()
const shortcut = drawer.getByRole('link', { name: SAVED_NAME })
await expect(shortcut).toBeVisible()
await expect(shortcut).toHaveAttribute('aria-current', 'page')
// Tapping the row restores the recorte and closes the drawer.
await shortcut.click()
await expect(page).toHaveURL(SAVED_RECORTE, NAVIGATION)
await expect(drawer).toBeHidden()
// Delete and undo keep working in the drawer.
await bottomNav.getByRole('button', { name: 'Mais' }).click()
await drawer.getByRole('button', { name: `Apagar o filtro salvo ${SAVED_NAME}` }).click()
await expect(drawer.getByRole('link', { name: SAVED_NAME })).toHaveCount(0)
await page.getByRole('button', { name: 'Desfazer' }).click()
await expect(drawer.getByRole('link', { name: SAVED_NAME })).toBeVisible()
})
})
})