mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
Split dashboard and leader contacts into dedicated routes, update nav and gates, and fix revalidation paths after the route move.
225 lines
9.1 KiB
TypeScript
225 lines
9.1 KiB
TypeScript
import type { Locator, Page } from '@playwright/test'
|
|
import {
|
|
loadMunicipalityGeometryModule,
|
|
loadMunicipalityZoneGeometryModule,
|
|
} from '../../src/lib/bahiaGeometries.js'
|
|
import type { BahiaMunicipalityFeature } from '../../src/lib/bahiaGeometriesTypes.js'
|
|
import {
|
|
getMunicipalityCatalogEntry,
|
|
municipalityCatalog,
|
|
} from '../../src/lib/municipalityCatalog.js'
|
|
import {
|
|
featureCentroid,
|
|
featureContainsPoint,
|
|
haversineKm,
|
|
resolveNearbyMunicipality,
|
|
type GeoPoint,
|
|
} from '../../src/lib/municipalityProximity.js'
|
|
import { featureBounds } from '../helpers/featureBounds.js'
|
|
|
|
import { expect, test } from './fixtures/campaignE2EFixtures.js'
|
|
|
|
/**
|
|
* B14 — the geo shortcut on the staff dashboard. What these tests protect is the
|
|
* contract the field depends on: a granted permission resolves the município the
|
|
* user is standing in (including a Salvador ZE when the zone mesh matches),
|
|
* a position outside the portfolio says so, and a refused permission leaves
|
|
* a card the user can act on instead of a spinner.
|
|
*/
|
|
|
|
const SALVADOR_CENTRE: GeoPoint = { lat: -12.973, lng: -38.5121 }
|
|
|
|
const salvadorZoneAtCentre = async () => {
|
|
const geometry = await loadMunicipalityGeometryModule()
|
|
const zoneGeometry = await loadMunicipalityZoneGeometryModule()
|
|
const accessible = municipalityCatalog
|
|
.filter((entry) => entry.city === 'Salvador')
|
|
.map((entry) => ({ slug: entry.slug, name: entry.name, ibgeCode: entry.ibgeCode }))
|
|
|
|
const resolution = resolveNearbyMunicipality({
|
|
point: SALVADOR_CENTRE,
|
|
geometry,
|
|
zoneGeometry,
|
|
accessible,
|
|
})
|
|
|
|
if (resolution.kind !== 'inScope' || resolution.match !== 'zoneContainment') {
|
|
throw new Error('Expected Salvador centre to resolve to an in-scope zone for the e2e contract.')
|
|
}
|
|
|
|
return resolution.municipality
|
|
}
|
|
|
|
/**
|
|
* The card mirrors its headline in an sr-only live region (so the outcome is
|
|
* announced), which means every headline matches twice. `.last()` is the visible
|
|
* copy — DOM order puts the live region first.
|
|
*/
|
|
const visibleText = (scope: Locator, text: string) => scope.getByText(text).last()
|
|
|
|
/** Assertions belong to the card, not to "somewhere on the dashboard". */
|
|
const geoCard = (page: Page) => page.locator('[data-slot="card"]').filter({ hasText: 'Onde estou' })
|
|
|
|
/**
|
|
* Coarse grid scan for a point that is really inside the polygon — a centroid can
|
|
* fall outside a concave município, and the fixture hands out an arbitrary one.
|
|
*/
|
|
const interiorPointOf = (feature: BahiaMunicipalityFeature): GeoPoint => {
|
|
const { west, east, south, north } = featureBounds(feature)
|
|
const steps = 24
|
|
|
|
for (let row = 1; row < steps; row += 1) {
|
|
for (let column = 1; column < steps; column += 1) {
|
|
const point = {
|
|
lng: west + ((east - west) * column) / steps,
|
|
lat: south + ((north - south) * row) / steps,
|
|
}
|
|
if (featureContainsPoint(feature, point)) return point
|
|
}
|
|
}
|
|
|
|
throw new Error(`No interior point found for ${feature.properties.name}`)
|
|
}
|
|
|
|
/**
|
|
* Farthest and nearest other município from a given one, by centroid. The fixture
|
|
* hands out an arbitrary município, so the two fallback positions have to be
|
|
* derived from it — a fixed city would be inside the 150 km suggestion radius on
|
|
* some runs and outside it on others.
|
|
*/
|
|
const otherMunicipalitiesByDistance = (
|
|
features: readonly BahiaMunicipalityFeature[],
|
|
origin: BahiaMunicipalityFeature,
|
|
): { nearest: BahiaMunicipalityFeature; farthest: BahiaMunicipalityFeature } => {
|
|
const from = featureCentroid(origin)
|
|
const ranked = features
|
|
.filter((feature) => feature.properties.codarea !== origin.properties.codarea)
|
|
.map((feature) => ({ feature, distanceKm: haversineKm(from, featureCentroid(feature)) }))
|
|
.sort((left, right) => left.distanceKm - right.distanceKm)
|
|
|
|
return { nearest: ranked[0]!.feature, farthest: ranked[ranked.length - 1]!.feature }
|
|
}
|
|
|
|
/** Whole-municipality unit (not a Salvador zone), so containment maps to one slug. */
|
|
const claimWholeMunicipality = async (
|
|
claim: () => Promise<{ id: number; name: string; slug: string }>,
|
|
) => {
|
|
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
const municipality = await claim()
|
|
if (getMunicipalityCatalogEntry(municipality.slug)?.kind === 'municipio') return municipality
|
|
}
|
|
throw new Error('Could not claim a whole-municipality unit for the geo shortcut test.')
|
|
}
|
|
|
|
/*
|
|
* Serial with a wider timeout: all three tests land on the staff dashboard (map +
|
|
* 435 municípios), and simultaneous cold hits on the dev server time out the
|
|
* login navigation before anything about this card is exercised. The advisor test
|
|
* renders that dashboard four times, which does not fit the default 30s in dev.
|
|
*/
|
|
test.describe.configure({ mode: 'serial', timeout: 150_000 })
|
|
|
|
test.describe('Município mais próximo', () => {
|
|
test('advisor in his own município opens it, and is told when he is outside the portfolio', async ({
|
|
campaign,
|
|
page,
|
|
}) => {
|
|
const { fixtures } = campaign
|
|
const advisor = await fixtures.createCampaignUser('advisor', {
|
|
name: fixtures.value('Assessor Geo'),
|
|
})
|
|
const password = advisor.password
|
|
const email = advisor.email!
|
|
const municipality = await claimWholeMunicipality(() => fixtures.claimMunicipality())
|
|
await campaign.payload.update({
|
|
collection: 'municipality',
|
|
id: municipality.id,
|
|
data: { advisors: [advisor.id] },
|
|
depth: 0,
|
|
})
|
|
fixtures.touchMunicipality(municipality.id)
|
|
|
|
const { features, getMunicipalityFeature } = await loadMunicipalityGeometryModule()
|
|
const portfolio = getMunicipalityFeature(
|
|
getMunicipalityCatalogEntry(municipality.slug)!.ibgeCode,
|
|
)!
|
|
const { nearest, farthest } = otherMunicipalitiesByDistance(features, portfolio)
|
|
const inside = interiorPointOf(portfolio)
|
|
|
|
await page.context().grantPermissions(['geolocation'])
|
|
await page.context().setGeolocation({ latitude: inside.lat, longitude: inside.lng })
|
|
|
|
await campaign.login(page, email, password)
|
|
await page.goto(`${campaign.baseURL}/campanha/quadro`)
|
|
|
|
const card = geoCard(page)
|
|
await expect(visibleText(card, `Você está em ${municipality.name}`)).toBeVisible()
|
|
await expect(card.getByRole('link', { name: `Abrir ${municipality.name}` })).toBeVisible()
|
|
// A device-grade fix carries no caveat.
|
|
await expect(card.getByText('Localização aproximada')).toHaveCount(0)
|
|
|
|
// Next door but off his portfolio: named where he is, offered what he can open.
|
|
const nextDoor = interiorPointOf(nearest)
|
|
await page.context().setGeolocation({ latitude: nextDoor.lat, longitude: nextDoor.lng })
|
|
await page.reload()
|
|
await expect(visibleText(card, 'fora da sua carteira')).toBeVisible()
|
|
await expect(card.getByText(`Mais próximo na sua carteira: ${municipality.name}`)).toBeVisible()
|
|
await expect(card.getByRole('link', { name: `Abrir ${municipality.name}` })).toBeVisible()
|
|
|
|
// Across the state: suggesting the portfolio would be noise, not a shortcut.
|
|
const farAway = interiorPointOf(farthest)
|
|
await page.context().setGeolocation({ latitude: farAway.lat, longitude: farAway.lng })
|
|
await page.reload()
|
|
await expect(visibleText(card, 'fora da sua carteira')).toBeVisible()
|
|
await expect(card.getByText('Nenhum município da sua carteira por perto.')).toBeVisible()
|
|
await expect(card.getByRole('link', { name: /^Abrir/ })).toHaveCount(0)
|
|
})
|
|
|
|
test('coordinator in Salvador opens the matching zone directly', async ({ campaign, page }) => {
|
|
const zone = await salvadorZoneAtCentre()
|
|
const { fixtures } = campaign
|
|
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
|
name: fixtures.value('Coordenadora Geo'),
|
|
})
|
|
const password = coordinator.password
|
|
const email = coordinator.email!
|
|
|
|
await page.context().grantPermissions(['geolocation'])
|
|
await page.context().setGeolocation({
|
|
latitude: SALVADOR_CENTRE.lat,
|
|
longitude: SALVADOR_CENTRE.lng,
|
|
})
|
|
|
|
await campaign.login(page, email, password)
|
|
await page.goto(`${campaign.baseURL}/campanha/quadro`)
|
|
|
|
const card = geoCard(page)
|
|
await expect(visibleText(card, zone.name)).toBeVisible()
|
|
await card.getByRole('link', { name: `Abrir ${zone.name}` }).click()
|
|
|
|
await expect(page).toHaveURL(
|
|
new RegExp(`/campanha/municipios/${zone.slug.replace(/-/g, '\\-')}$`),
|
|
)
|
|
})
|
|
|
|
test('a refused permission leaves an actionable card instead of a spinner', async ({
|
|
campaign,
|
|
page,
|
|
}) => {
|
|
const { fixtures } = campaign
|
|
const blockedUser = await fixtures.createCampaignUser('coordinator', {
|
|
name: fixtures.value('Coordenadora Bloqueada'),
|
|
})
|
|
const password = blockedUser.password
|
|
const email = blockedUser.email!
|
|
|
|
// No grantPermissions: Chromium refuses the position, the same as a user tapping "Block".
|
|
await campaign.login(page, email, password)
|
|
await page.goto(`${campaign.baseURL}/campanha/quadro`)
|
|
|
|
const card = geoCard(page)
|
|
await expect(visibleText(card, 'Localização bloqueada neste navegador')).toBeVisible()
|
|
await expect(card.getByRole('button', { name: 'Tentar de novo' })).toBeEnabled()
|
|
await expect(card.getByText('Localizando…')).toHaveCount(0)
|
|
})
|
|
})
|