teqo-backup/tests/e2e/campaignTerritoriesHttp.e2e.spec.ts

148 lines
7.2 KiB
TypeScript

import { assertLeaderRedirect, expect, rendered, test } from './fixtures/campaignHttpTest.js'
/**
* OPS35 — `campaignTerritories` migrated to the browserless HTTP mode: same
* server, same database, real HTTP, no browser. The family's assertions were
* server-rendered content + URL contracts + a route-level redirect, so the
* equivalence is direct: what the browser spec read from the DOM is read from
* the rendered HTML; what it navigated by click is requested by URL.
*
* Two render realities shaped the assertions (verified against the real
* markup): React's streaming inserts `<!-- -->` text-node separators, so text
* assertions run over `rendered()` (comments stripped — they are transport,
* not content); and the Cobertura column label lives in the RSC flight
* payload (the column picker menu is a client component whose props serialize
* even when closed), so "hidden by default" is pinned by the absence of its
* sortable control, which `resolveVisibleColumns` never renders
* (CampaignTable.tsx:168). CSS rung visibility at a given viewport is a
* browser concern (OPS35 keeps viewports in the browser families) — the
* server contract asserted here is which columns are rendered for a given
* state.
*/
test.describe('Territórios de Identidade (HTTP)', () => {
test('staff sorts, filters and opens the municipality queue for a territory', async ({
campaign,
campaignRequest,
}) => {
const { fixtures } = campaign
const coordinator = await fixtures.createCampaignUser('coordinator', {
name: fixtures.value('Coordenadora Territórios'),
})
const request = await campaignRequest(coordinator, coordinator.password)
// The sidebar way in — the same link the browser spec clicks.
const list = await request.get('/campanha/territorios')
expect(list.status()).toBe(200)
const listHTML = rendered(await list.text())
expect(listHTML).toContain('Territórios de Identidade')
expect(listHTML).toContain('href="/campanha/territorios"')
// B175 — the count rides the name ("Irecê (N)"): the row link leads with
// the territory name and carries the municipality count.
expect(listHTML).toMatch(/Irecê <span[^>]*>\(\d+\)/)
// The controls the browser spec exercised by click are anchors: pin their
// hrefs so a broken affordance fails here instead of passing silently.
expect(listHTML).toContain('href="/campanha/municipios?region=Irec%C3%AA"')
expect(listHTML).toContain('href="/campanha/territorios?sort=votes2022')
// Sorting is a URL contract: the server renders the summary in the caption.
const sorted = await request.get('/campanha/territorios?sort=votes2022')
expect(sorted.status()).toBe(200)
expect(rendered(await sorted.text())).toContain('Ordenado por 2022 (maior primeiro)')
// Filtering is the same contract: the footer reports the recorte.
const filtered = await request.get('/campanha/territorios?region=Irecê')
expect(filtered.status()).toBe(200)
expect(rendered(await filtered.text())).toContain('1 território encontrado')
// The row opens the municipality queue for the territory.
const queue = await request.get('/campanha/municipios?region=Irecê')
expect(queue.status()).toBe(200)
expect(rendered(await queue.text())).toContain('Irecê')
})
test('parent territory rows expose hash anchor ids for deep links', async ({
campaign,
campaignRequest,
}) => {
const { fixtures } = campaign
const coordinator = await fixtures.createCampaignUser('coordinator', {
name: fixtures.value('Coordenadora Âncoras'),
})
const request = await campaignRequest(coordinator, coordinator.password)
const response = await request.get('/campanha/territorios')
expect(response.status()).toBe(200)
const html = await response.text()
expect(html).toContain('id="ti-irece"')
expect(html).toContain('id="ti-velho-chico"')
})
test('the read-only network columns render in the server HTML; Cobertura stays hidden by default', async ({
campaign,
campaignRequest,
}) => {
const { fixtures } = campaign
const coordinator = await fixtures.createCampaignUser('coordinator', {
name: fixtures.value('Coordenadora Rede Larga'),
})
const request = await campaignRequest(coordinator, coordinator.password)
const response = await request.get('/campanha/territorios')
expect(response.status()).toBe(200)
const html = await response.text()
// B175 — the sortable rungs (P0 + Captura + network + Classe + Assessoria)
// announce "Ordenar por <label> (…)" via aria-label. No `(` anchor: the
// active sort renders "Ordenar por X, inverter para …", the inactive one
// "Ordenar por X (…)" — both must match (the assertion is about the
// column existing, not about which sort is active).
for (const header of ['2022', 'Captura', '2026', 'Classe', 'Assessoria']) {
expect(html, `sortable header ${header}`).toContain(`aria-label="Ordenar por ${header}`)
}
// The read-only network headers are plain text — match exactly (never by
// substring: "Assessor" is a prefix of "Assessores"/"Assessoria"). Their
// panel-width rung classes are server-rendered too, so the "wide panel
// shows them" contract keeps a server-side pin (CSS visibility at a
// viewport stays a browser concern).
const networkRungs: Array<[string, string]> = [
['Assessor', 'hidden @min-[60rem]/territory-list:table-cell'],
['Liderança', 'hidden @min-[66rem]/territory-list:table-cell'],
['Dobradinha', 'hidden @min-[72rem]/territory-list:table-cell'],
]
for (const [header, rung] of networkRungs) {
expect(html, `network header ${header}`).toContain(`${header}<`)
expect(html, `network rung ${header}`).toContain(rung)
}
// Cobertura is P3 AND hidden in the picker: the hidden column is never
// rendered (its sortable control is absent), even though its label exists
// in the RSC flight payload as the picker menu item.
expect(html).not.toContain('Ordenar por Cobertura')
})
test('leader cannot open the territories page', async ({ campaign, campaignRequest }) => {
const { fixtures } = campaign
const phone = fixtures.phone()
const leader = await fixtures.createCampaignUser('leader', {
name: fixtures.value('Liderança Territórios'),
username: phone,
})
// Log in by username (phone), the way the browser spec did — the REST
// login accepts email or username and both branches are exercised.
const request = await campaignRequest({ username: phone }, leader.password)
// The `noLeader` gate throws `redirect()` deep in the page, after the
// layout has started streaming — so the document usually answers 200
// carrying Next's route-redirect meta tag instead of a 3xx. When the
// redirect beats the stream it is a real 3xx (dev 307 / prod 308). Both
// are the server contract the browser follows — assert the redirect
// target, never pin the transport.
await assertLeaderRedirect(request, '/campanha/territorios')
// The leader home renders no way in.
const home = await request.get('/campanha/meus-contatos')
expect(home.status()).toBe(200)
expect(await home.text()).not.toContain('href="/campanha/territorios"')
})
})