refactor(C110): simplify — helpers de dominância/clamp, seed e2e generalizado, CSS sem regras mortas
This commit is contained in:
parent
b23a323f83
commit
3b81431d39
9 changed files with 190 additions and 242 deletions
|
|
@ -77,3 +77,9 @@ flowchart LR
|
|||
- [ ] Aceite de produto da intenção ainda coberto (grid segue o dedo; adjacente revelado com chevron + label + quadro; eventos assíncronos no preview; snap-back < 48 px; commit ≥ 48 px com transição suave; reduced-motion instantâneo; desktop, tap C91 e long-press C15 intactos; vista de dia sem create pós-arrasto)
|
||||
- [ ] Invariantes AGENTS/engineering-standards (sem migration/sem prod/sem Consent novo; copy pt-BR / identificadores em inglês; hook dono do gesto com contrato unit-testado; sem twin de componentes)
|
||||
- [ ] Testes de domínio: unit do helper adjacente + label parity; unit do hook (contrato novo); e2e mobile (snap-back, preview mid-drag com eventos, commit) + existentes verdes; gate:fast e gate:push verdes
|
||||
|
||||
## Débitos deferidos (pós-/simplify; nenhum com score ≥4 — nada virou Issue)
|
||||
|
||||
- **Dots do mês ancorados no content box, não no frame do grid** (o painel posiciona os dots contra o bloco cheio do preview — chevron/label inclusos — e o frame é um skeleton independente). Polimento visual dentro da aproximação aceita pelo aceite; gatilho de revisita: se o mês ganhar mais uso no mobile ou a crítica de produto apontar o desalinhamento.
|
||||
- **Gradiente de colunas duplicado no CSS** (base do frame + camada do mês) — custom property traria mais indireção que as ~5 linhas; descartado.
|
||||
- **Chevron segue o canvas do gate (aponta para o lado revelado)** — se a leitura "direção da mudança" do aceite prevalecer na prática, é flip de 1 linha (ChevronLeft/Right) no `ActivityAgendaSwipePreview`.
|
||||
|
|
|
|||
|
|
@ -95,11 +95,6 @@
|
|||
padding: 0.625rem;
|
||||
}
|
||||
|
||||
.activity-agenda-swipe-preview--next .activity-agenda-swipe-preview-content {
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.activity-agenda-swipe-preview--prev .activity-agenda-swipe-preview-content {
|
||||
align-items: flex-end;
|
||||
text-align: right;
|
||||
|
|
@ -135,8 +130,6 @@
|
|||
var(--border) calc(14.285% - 1px),
|
||||
var(--border) 14.285%
|
||||
);
|
||||
background-size: 100% 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.activity-agenda-swipe-frame[data-view='day'] {
|
||||
|
|
@ -189,21 +182,22 @@
|
|||
accept's async path): bars by time proportion for day/week, dots per date
|
||||
for month, rows for the list. Approximate on purpose — the commit renders
|
||||
the real grid. */
|
||||
.activity-agenda-swipe-event {
|
||||
.activity-agenda-swipe-event,
|
||||
.activity-agenda-swipe-dot {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 62%;
|
||||
height: 0.3125rem;
|
||||
border-radius: 0.1875rem;
|
||||
background: color-mix(in oklab, var(--primary) 72%, transparent);
|
||||
}
|
||||
|
||||
.activity-agenda-swipe-event {
|
||||
left: 0;
|
||||
width: 62%;
|
||||
height: 0.3125rem;
|
||||
}
|
||||
|
||||
.activity-agenda-swipe-dot {
|
||||
position: absolute;
|
||||
width: 9%;
|
||||
height: 0.375rem;
|
||||
border-radius: 0.1875rem;
|
||||
background: color-mix(in oklab, var(--primary) 72%, transparent);
|
||||
}
|
||||
|
||||
.activity-agenda-swipe-list-row {
|
||||
|
|
|
|||
|
|
@ -403,7 +403,6 @@ export const ActivityAgenda = ({
|
|||
const handleSwipePreviewEnd = useCallback(() => {
|
||||
swipePreviewKeyRef.current = null
|
||||
setSwipePreview(null)
|
||||
setSwipePreviewEvents([])
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -596,6 +595,8 @@ export const ActivityAgenda = ({
|
|||
headingLevel={2}
|
||||
/>
|
||||
|
||||
{/* `visibleRange &&` narrows the type for the preview's `view` prop
|
||||
(the memo already guarantees it is non-null). */}
|
||||
{swipePreview && visibleRange && adjacentPreviewRange ? (
|
||||
<ActivityAgendaSwipePreview
|
||||
view={visibleRange.view}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import type { ActivityAgendaEvent } from '@/utilities/activityViewModels'
|
|||
*/
|
||||
const SLOT_START_MINUTES = 7 * 60
|
||||
const SLOT_END_MINUTES = 22 * 60
|
||||
const SLOT_SPAN_MINUTES = SLOT_END_MINUTES - SLOT_START_MINUTES
|
||||
const MAX_PREVIEW_EVENTS = 6
|
||||
|
||||
const weekInitials = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'] as const
|
||||
|
|
@ -38,11 +39,8 @@ const timedTopPercent = (startAt: string): number => {
|
|||
const match = /T(\d{2}):(\d{2})/.exec(civil)
|
||||
if (!match) return 0
|
||||
const minutes = Number(match[1]) * 60 + Number(match[2])
|
||||
const clamped = Math.min(
|
||||
Math.max(minutes - SLOT_START_MINUTES, 0),
|
||||
SLOT_END_MINUTES - SLOT_START_MINUTES,
|
||||
)
|
||||
return (clamped / (SLOT_END_MINUTES - SLOT_START_MINUTES)) * 100
|
||||
const clamped = Math.min(Math.max(minutes - SLOT_START_MINUTES, 0), SLOT_SPAN_MINUTES)
|
||||
return (clamped / SLOT_SPAN_MINUTES) * 100
|
||||
}
|
||||
|
||||
const monthCellPosition = (
|
||||
|
|
@ -86,7 +84,8 @@ export const ActivityAgendaSwipePreview = ({
|
|||
|
||||
{/* C110 — the abstract grid frame; the list view has no columns to
|
||||
suggest, so it degrades to chevron + label + event rows (the
|
||||
accepted degradation of the intention). */}
|
||||
accepted degradation of the intention). The day view keeps just
|
||||
the time bands — a single-day header would mislabel the columns. */}
|
||||
{view !== 'list' ? (
|
||||
<div className="activity-agenda-swipe-frame" data-view={view}>
|
||||
{view === 'week' ? (
|
||||
|
|
|
|||
|
|
@ -74,6 +74,17 @@ const AGENDA_SWIPE_EXIT_MS = 180
|
|||
|
||||
type AgendaSwipeDirection = 'next' | 'prev'
|
||||
|
||||
/** C110 — the grid's displacement is clamped to the claimed direction: it
|
||||
* never crosses its starting point, and the release threshold reads this
|
||||
* clamped position (the finger may have reversed past the origin — the grid
|
||||
* did not move). */
|
||||
const clampDisplacement = (dx: number, direction: AgendaSwipeDirection): number =>
|
||||
direction === 'next' ? Math.min(dx, 0) : Math.max(dx, 0)
|
||||
|
||||
/** Swipe-vs-scroll discriminator: the vertical component dominates the drag. */
|
||||
const isVerticalDrag = (dx: number, dy: number): boolean =>
|
||||
Math.abs(dx) < Math.abs(dy) * AGENDA_SWIPE_DOMINANCE
|
||||
|
||||
type AgendaSwipeGesture = {
|
||||
pointerId: number
|
||||
startX: number
|
||||
|
|
@ -150,10 +161,10 @@ export const useAgendaSwipeNavigation = ({
|
|||
/** C110 — the live transform: follows the finger, clamped to the claimed
|
||||
* direction (the grid never crosses its starting point). */
|
||||
const applyTransform = useCallback(
|
||||
(dx: number, direction: AgendaSwipeDirection | null) => {
|
||||
(dx: number, direction: AgendaSwipeDirection) => {
|
||||
const container = containerRef.current
|
||||
if (!container) return
|
||||
const clamped = direction === 'next' ? Math.min(dx, 0) : Math.max(dx, 0)
|
||||
const clamped = clampDisplacement(dx, direction)
|
||||
container.style.transform = clamped === 0 ? '' : `translateX(${clamped}px)`
|
||||
},
|
||||
[containerRef],
|
||||
|
|
@ -247,10 +258,7 @@ export const useAgendaSwipeNavigation = ({
|
|||
const dx = touch.clientX - gesture.startX
|
||||
const dy = touch.clientY - gesture.startY
|
||||
if (gesture.direction === null) {
|
||||
if (
|
||||
Math.abs(dx) > AGENDA_SWIPE_CLAIM_PX &&
|
||||
Math.abs(dx) > Math.abs(dy) * AGENDA_SWIPE_DOMINANCE
|
||||
) {
|
||||
if (Math.abs(dx) > AGENDA_SWIPE_CLAIM_PX && !isVerticalDrag(dx, dy)) {
|
||||
// Clearly horizontal: keep the browser from claiming the gesture
|
||||
// for panning (which would pointercancel the stream before the
|
||||
// threshold) AND suppress the dateClick a sub-threshold flick would
|
||||
|
|
@ -268,6 +276,8 @@ export const useAgendaSwipeNavigation = ({
|
|||
// preventDefault because its commit ended the gesture within a move or
|
||||
// two; the release decision keeps the gesture alive through many.)
|
||||
event.preventDefault()
|
||||
gesture.lastDx = dx
|
||||
gesture.lastDy = dy
|
||||
applyTransform(dx, gesture.direction)
|
||||
},
|
||||
[applyTransform, claimGesture],
|
||||
|
|
@ -289,14 +299,14 @@ export const useAgendaSwipeNavigation = ({
|
|||
gesture.lastDy = dy
|
||||
|
||||
if (gesture.direction === null) {
|
||||
if (Math.abs(dx) < Math.abs(dy) * AGENDA_SWIPE_DOMINANCE) {
|
||||
if (isVerticalDrag(dx, dy)) {
|
||||
// Vertical drag (scrolling the timeline, rescheduling): never claim.
|
||||
gestureRef.current = null
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (Math.abs(dx) < Math.abs(dy) * AGENDA_SWIPE_DOMINANCE) {
|
||||
if (isVerticalDrag(dx, dy)) {
|
||||
// C110 — a claimed gesture turned vertical (scroll takeover): abandon,
|
||||
// snap back, close the preview. The browser is scrolling now — the
|
||||
// grid must not stay displaced.
|
||||
|
|
@ -329,20 +339,16 @@ export const useAgendaSwipeNavigation = ({
|
|||
if (gesture.direction !== null) {
|
||||
const dx = gesture.lastDx
|
||||
const dy = gesture.lastDy
|
||||
const displaced = gesture.direction === 'next' ? Math.min(dx, 0) : Math.max(dx, 0)
|
||||
if (
|
||||
Math.abs(displaced) >= AGENDA_SWIPE_THRESHOLD_PX &&
|
||||
Math.abs(dx) >= Math.abs(dy) * AGENDA_SWIPE_DOMINANCE
|
||||
) {
|
||||
settleTransform()
|
||||
onSwipePreviewEndRef.current?.()
|
||||
const displaced = clampDisplacement(dx, gesture.direction)
|
||||
if (Math.abs(displaced) >= AGENDA_SWIPE_THRESHOLD_PX && !isVerticalDrag(dx, dy)) {
|
||||
endWithoutCommit()
|
||||
onSwipeRef.current(gesture.direction)
|
||||
return
|
||||
}
|
||||
endWithoutCommit()
|
||||
}
|
||||
},
|
||||
[endWithoutCommit, settleTransform],
|
||||
[endWithoutCommit],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -111,35 +111,34 @@ export const activityAgendaAdjacentPeriod = (
|
|||
range: { start: string; end: string; anchorDate: string },
|
||||
direction: 'next' | 'prev',
|
||||
): { start: string; end: string; anchorDate: string } | null => {
|
||||
const shiftDays = direction === 'next' ? 1 : -1
|
||||
const shiftMonths = direction === 'next' ? 1 : -1
|
||||
const shift = direction === 'next' ? 1 : -1
|
||||
|
||||
const shiftRangeByDays = (days: number) => {
|
||||
const start = new Date(range.start)
|
||||
const end = new Date(range.end)
|
||||
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return null
|
||||
const shiftedStart = new Date(start.getTime() + days * 86_400_000).toISOString()
|
||||
const shiftedStart = new Date(start.getTime() + days * 86_400_000)
|
||||
return {
|
||||
start: shiftedStart,
|
||||
start: shiftedStart.toISOString(),
|
||||
end: new Date(end.getTime() + days * 86_400_000).toISOString(),
|
||||
anchorDate: formatBahiaCivilDate(new Date(start.getTime() + days * 86_400_000)),
|
||||
anchorDate: formatBahiaCivilDate(shiftedStart),
|
||||
}
|
||||
}
|
||||
|
||||
if (view === 'day' || view === 'week') {
|
||||
return shiftRangeByDays(shiftDays * (view === 'day' ? 1 : 7))
|
||||
return shiftRangeByDays(shift * (view === 'day' ? 1 : 7))
|
||||
}
|
||||
|
||||
const anchor = civilDateAnchor(range.anchorDate)
|
||||
const anchor = civilDateParts(range.anchorDate)
|
||||
if (!anchor || anchor.day !== 1) return null
|
||||
|
||||
const shiftedAnchorParts = shiftCivilMonth(anchor.year, anchor.month, shiftMonths)
|
||||
const shiftedAnchorParts = shiftCivilMonth(anchor.year, anchor.month, shift)
|
||||
const shiftedAnchor = `${shiftedAnchorParts.year}-${pad(shiftedAnchorParts.month)}-01`
|
||||
if (view === 'list') {
|
||||
const nextMonth = shiftCivilMonth(shiftedAnchorParts.year, shiftedAnchorParts.month, 1)
|
||||
return {
|
||||
start: civilInstantAtBahia(shiftedAnchor),
|
||||
end: civilInstantAtBahia(`${nextMonth.year}-${pad(nextMonth.month)}-01`),
|
||||
start: allDayStartInstant(shiftedAnchor),
|
||||
end: allDayStartInstant(`${nextMonth.year}-${pad(nextMonth.month)}-01`),
|
||||
anchorDate: shiftedAnchor,
|
||||
}
|
||||
}
|
||||
|
|
@ -148,42 +147,14 @@ export const activityAgendaAdjacentPeriod = (
|
|||
// and spans at most 6 weeks.
|
||||
const start = subtractBahiaCivilDays(shiftedAnchor, mondayOffsetOf(shiftedAnchor))
|
||||
return {
|
||||
start: civilInstantAtBahia(start),
|
||||
end: civilInstantAtBahia(subtractBahiaCivilDays(start, -42)),
|
||||
start: allDayStartInstant(start),
|
||||
end: allDayStartInstant(subtractBahiaCivilDays(start, -42)),
|
||||
anchorDate: shiftedAnchor,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Midnight of a Bahia civil date as a UTC instant. Bahia is fixed at UTC-3
|
||||
* (no DST since 2019), so the explicit `-03:00` offset is stable — the same
|
||||
* convention FullCalendar's own range strings use.
|
||||
*/
|
||||
const civilInstantAtBahia = (civilDate: string): string =>
|
||||
new Date(`${civilDate}T00:00:00-03:00`).toISOString()
|
||||
|
||||
const pad = (value: number): string => String(value).padStart(2, '0')
|
||||
|
||||
/** Full civil-date anchor (`aaaa-mm-dd`) with day/month/year — the runtime
|
||||
* passes `formatBahiaCivilDate(getDate())`, so the year is always present. */
|
||||
const civilDateAnchor = (
|
||||
civilDate: string,
|
||||
): { year: number; month: number; day: number } | null => {
|
||||
const [year, month, day] = civilDate.split('-').map(Number)
|
||||
if (
|
||||
!Number.isInteger(year) ||
|
||||
!Number.isInteger(month) ||
|
||||
!Number.isInteger(day) ||
|
||||
month < 1 ||
|
||||
month > 12 ||
|
||||
day < 1 ||
|
||||
day > 31
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return { year, month, day }
|
||||
}
|
||||
|
||||
const shiftCivilMonth = (
|
||||
year: number,
|
||||
month: number,
|
||||
|
|
@ -200,7 +171,7 @@ const mondayOffsetOf = (civilDate: string): number => {
|
|||
return (weekday + 6) % 7
|
||||
}
|
||||
|
||||
const civilDateParts = (civilDate: string): { day: number; month: number } | null => {
|
||||
const civilDateParts = (civilDate: string): { year: number; month: number; day: number } | null => {
|
||||
const [year, month, day] = civilDate.split('-').map(Number)
|
||||
if (
|
||||
!Number.isInteger(year) ||
|
||||
|
|
@ -213,7 +184,7 @@ const civilDateParts = (civilDate: string): { day: number; month: number } | nul
|
|||
) {
|
||||
return null
|
||||
}
|
||||
return { day, month }
|
||||
return { year, month, day }
|
||||
}
|
||||
|
||||
const civilDateLabel = (civilDate: string): string | null => {
|
||||
|
|
|
|||
|
|
@ -43,48 +43,39 @@ const openMonthView = async (page: Page) => {
|
|||
* `/campanha`. Paced, it stays a drag the agenda consumes via its
|
||||
* non-passive touchmove preventDefault, while still finishing far below
|
||||
* FullCalendar's 650ms long-press (which would start a reschedule).
|
||||
*
|
||||
* C110 — `inspectAfterMove` pauses the gesture mid-drag (after the 12px
|
||||
* claim) so a test can observe the live preview before the release decides.
|
||||
*/
|
||||
const dispatchTouchDrag = async (
|
||||
cdp: CDPSession,
|
||||
box: { x: number; y: number; width: number; height: number },
|
||||
fromRatio: number,
|
||||
toRatio: number,
|
||||
{
|
||||
moveDelayMs = 25,
|
||||
inspectAfterMove,
|
||||
}: { moveDelayMs?: number; inspectAfterMove?: () => Promise<void> } = {},
|
||||
) => {
|
||||
const y = Math.round(box.y + box.height * 0.5)
|
||||
const x0 = Math.round(box.x + box.width * fromRatio)
|
||||
const x1 = Math.round(box.x + box.width * toRatio)
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: x0, y }] })
|
||||
for (let i = 1; i <= 5; i += 1) {
|
||||
const x = Math.round(x0 + ((x1 - x0) * i) / 5)
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x, y }] })
|
||||
await new Promise((resolve) => setTimeout(resolve, moveDelayMs))
|
||||
if (i === 2) await inspectAfterMove?.()
|
||||
}
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] })
|
||||
}
|
||||
|
||||
const touchSwipe = async (
|
||||
cdp: CDPSession,
|
||||
box: { x: number; y: number; width: number; height: number },
|
||||
fromRatio: number,
|
||||
toRatio: number,
|
||||
) => {
|
||||
const y = Math.round(box.y + box.height * 0.5)
|
||||
const x0 = Math.round(box.x + box.width * fromRatio)
|
||||
const x1 = Math.round(box.x + box.width * toRatio)
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: x0, y }] })
|
||||
for (let i = 1; i <= 5; i += 1) {
|
||||
const x = Math.round(x0 + ((x1 - x0) * i) / 5)
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x, y }] })
|
||||
await new Promise((resolve) => setTimeout(resolve, 25))
|
||||
}
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] })
|
||||
}
|
||||
|
||||
/**
|
||||
* C110 — like `touchSwipe`, but pauses mid-gesture (after the 12px claim) for
|
||||
* `inspect` to observe the live preview before the release decides.
|
||||
*/
|
||||
const touchDragWithInspection = async (
|
||||
cdp: CDPSession,
|
||||
box: { x: number; y: number; width: number; height: number },
|
||||
fromRatio: number,
|
||||
toRatio: number,
|
||||
inspect: () => Promise<void>,
|
||||
) => {
|
||||
const y = Math.round(box.y + box.height * 0.5)
|
||||
const x0 = Math.round(box.x + box.width * fromRatio)
|
||||
const x1 = Math.round(box.x + box.width * toRatio)
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: x0, y }] })
|
||||
for (let i = 1; i <= 5; i += 1) {
|
||||
const x = Math.round(x0 + ((x1 - x0) * i) / 5)
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchMove', touchPoints: [{ x, y }] })
|
||||
await new Promise((resolve) => setTimeout(resolve, 40))
|
||||
if (i === 2) await inspect()
|
||||
}
|
||||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] })
|
||||
}
|
||||
) => dispatchTouchDrag(cdp, box, fromRatio, toRatio)
|
||||
|
||||
const agendaSwipePreview = (page: Page) => page.locator('.activity-agenda-swipe-preview')
|
||||
|
||||
|
|
@ -93,26 +84,43 @@ const tapAt = async (cdp: CDPSession, x: number, y: number) => {
|
|||
await cdp.send('Input.dispatchTouchEvent', { type: 'touchEnd', touchPoints: [] })
|
||||
}
|
||||
|
||||
const seedTodayActivity = async (campaign: CampaignE2EFixture, page: Page): Promise<string> => {
|
||||
/**
|
||||
* Seeds agenda activities and logs the coordinator in. `rows` maps a civil
|
||||
* date + hour to an activity title; `fixtures.value` keeps titles/slugs
|
||||
* unique per test run — literal titles would collide on the unique slug
|
||||
* between consecutive tests of this spec.
|
||||
*/
|
||||
const seedAgendaActivities = async (
|
||||
campaign: CampaignE2EFixture,
|
||||
page: Page,
|
||||
rows: Array<[civilDate: string, hour: string, title: string]>,
|
||||
): Promise<void> => {
|
||||
const { fixtures } = campaign
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator')
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
const civilDate = formatBahiaCivilDate(new Date())
|
||||
const startAt = parseBahiaDateTimeInput(`${civilDate}T10:00`)
|
||||
if (!startAt) throw new Error('Falha ao montar horário da fixture de agenda.')
|
||||
|
||||
await fixtures.payload.create({
|
||||
collection: 'activity',
|
||||
data: hookFilledCreateData<'activity'>({
|
||||
title: fixtures.value('Compromisso mobile nativo'),
|
||||
tags: ['Comício'],
|
||||
status: 'confirmado',
|
||||
startAt,
|
||||
municipality: municipality.id,
|
||||
}),
|
||||
depth: 0,
|
||||
})
|
||||
for (const [civilDate, hour, rawTitle] of rows) {
|
||||
const startAt = parseBahiaDateTimeInput(`${civilDate}T${hour}`)
|
||||
if (!startAt) throw new Error('Falha ao montar horário da fixture de agenda.')
|
||||
await fixtures.payload.create({
|
||||
collection: 'activity',
|
||||
data: hookFilledCreateData<'activity'>({
|
||||
title: fixtures.value(rawTitle),
|
||||
tags: ['Comício'],
|
||||
status: 'confirmado',
|
||||
startAt,
|
||||
municipality: municipality.id,
|
||||
}),
|
||||
depth: 0,
|
||||
})
|
||||
}
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
}
|
||||
|
||||
const seedTodayActivity = async (campaign: CampaignE2EFixture, page: Page): Promise<string> => {
|
||||
const civilDate = formatBahiaCivilDate(new Date())
|
||||
await seedAgendaActivities(campaign, page, [[civilDate, '10:00', 'Compromisso mobile nativo']])
|
||||
return civilDate
|
||||
}
|
||||
|
||||
|
|
@ -381,32 +389,12 @@ test.describe('C110 — feedback visual do arrasto (reveal do adjacente + commit
|
|||
campaign: CampaignE2EFixture,
|
||||
page: Page,
|
||||
): Promise<{ today: string; tomorrow: string }> => {
|
||||
const { fixtures } = campaign
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator')
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
const today = formatBahiaCivilDate(new Date())
|
||||
const tomorrow = civilDatePlusDays(today, 1)
|
||||
|
||||
for (const [civilDate, hour, title] of [
|
||||
await seedAgendaActivities(campaign, page, [
|
||||
[today, '10:00', 'Compromisso mobile hoje'],
|
||||
[tomorrow, '11:00', 'Compromisso mobile amanhã'],
|
||||
] as const) {
|
||||
const startAt = parseBahiaDateTimeInput(`${civilDate}T${hour}`)
|
||||
if (!startAt) throw new Error('Falha ao montar horário da fixture de agenda.')
|
||||
await fixtures.payload.create({
|
||||
collection: 'activity',
|
||||
data: hookFilledCreateData<'activity'>({
|
||||
title,
|
||||
tags: ['Comício'],
|
||||
status: 'confirmado',
|
||||
startAt,
|
||||
municipality: municipality.id,
|
||||
}),
|
||||
depth: 0,
|
||||
})
|
||||
}
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
])
|
||||
return { today, tomorrow }
|
||||
}
|
||||
|
||||
|
|
@ -429,22 +417,26 @@ test.describe('C110 — feedback visual do arrasto (reveal do adjacente + commit
|
|||
// A meio do gesto (após o claim de 12px): o grid acompanhou o dedo e o
|
||||
// painel do período adjacente está revelado na borda — chevron, rótulo do
|
||||
// período seguinte e os eventos do adjacente entrando conforme carregam.
|
||||
await touchDragWithInspection(cdp, agendaBox, 0.8, 0.3, async () => {
|
||||
const preview = agendaSwipePreview(page)
|
||||
await expect(preview).toBeVisible()
|
||||
await expect(preview.locator('svg.activity-agenda-swipe-chevron')).toBeVisible()
|
||||
await expect(preview.locator('.activity-agenda-swipe-label')).toHaveText(dayLabelFor(tomorrow))
|
||||
await expect
|
||||
.poll(
|
||||
() => preview.locator('.activity-agenda-swipe-event').count(),
|
||||
{ timeout: 15_000 },
|
||||
await dispatchTouchDrag(cdp, agendaBox, 0.8, 0.3, {
|
||||
moveDelayMs: 40,
|
||||
inspectAfterMove: async () => {
|
||||
const preview = agendaSwipePreview(page)
|
||||
await expect(preview).toBeVisible()
|
||||
await expect(preview.locator('svg.activity-agenda-swipe-chevron')).toBeVisible()
|
||||
await expect(preview.locator('.activity-agenda-swipe-label')).toHaveText(
|
||||
dayLabelFor(tomorrow),
|
||||
)
|
||||
.toBeGreaterThan(0)
|
||||
// O grid deslocado: o transform vivo acompanhou o dedo.
|
||||
const transform = await page.evaluate(
|
||||
() => getComputedStyle(document.querySelector('.activity-agenda')!).transform,
|
||||
)
|
||||
expect(transform).not.toBe('none')
|
||||
await expect
|
||||
.poll(() => preview.locator('.activity-agenda-swipe-event').count(), {
|
||||
timeout: 15_000,
|
||||
})
|
||||
.toBeGreaterThan(0)
|
||||
// O grid deslocado: o transform vivo acompanhou o dedo.
|
||||
const transform = await page.evaluate(
|
||||
() => getComputedStyle(document.querySelector('.activity-agenda')!).transform,
|
||||
)
|
||||
expect(transform).not.toBe('none')
|
||||
},
|
||||
})
|
||||
|
||||
// Soltou acima do limiar: o período adjacente assume a tela e o preview some.
|
||||
|
|
@ -461,7 +453,7 @@ test.describe('C110 — feedback visual do arrasto (reveal do adjacente + commit
|
|||
campaign,
|
||||
page,
|
||||
}) => {
|
||||
const { today } = await seedTodayAndTomorrowActivities(campaign, page)
|
||||
const { today, tomorrow } = await seedTodayAndTomorrowActivities(campaign, page)
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 844 })
|
||||
await page.goto(`${campaign.baseURL}/campanha/agenda`)
|
||||
|
|
@ -475,12 +467,15 @@ test.describe('C110 — feedback visual do arrasto (reveal do adjacente + commit
|
|||
|
||||
// Arrasto de ~47px (0.9 → 0.78 em 390px): cruza o claim de 12px — o
|
||||
// preview abre — mas o soltar fica abaixo dos 48px do limiar.
|
||||
await touchDragWithInspection(cdp, agendaBox, 0.9, 0.78, async () => {
|
||||
const preview = agendaSwipePreview(page)
|
||||
await expect(preview).toBeVisible()
|
||||
await expect(preview.locator('.activity-agenda-swipe-label')).toHaveText(
|
||||
dayLabelFor(civilDatePlusDays(today, 1)),
|
||||
)
|
||||
await dispatchTouchDrag(cdp, agendaBox, 0.9, 0.78, {
|
||||
moveDelayMs: 40,
|
||||
inspectAfterMove: async () => {
|
||||
const preview = agendaSwipePreview(page)
|
||||
await expect(preview).toBeVisible()
|
||||
await expect(preview.locator('.activity-agenda-swipe-label')).toHaveText(
|
||||
dayLabelFor(tomorrow),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
// Snap-back: o título não muda e o grid voltou ao repouso.
|
||||
|
|
|
|||
|
|
@ -132,6 +132,15 @@ describe('activityAgendaAdjacentPeriod', () => {
|
|||
end: '2026-08-11T03:00:00.000Z',
|
||||
anchorDate: '2026-08-10',
|
||||
})
|
||||
// paridade: o label do preview (range adjacente) == título pós-commit
|
||||
expect(
|
||||
activityAgendaPeriodLabel(
|
||||
'day',
|
||||
'2026-08-10T03:00:00.000Z',
|
||||
'2026-08-11T03:00:00.000Z',
|
||||
'2026-08-10',
|
||||
),
|
||||
).toBe('10 Agosto')
|
||||
expect(
|
||||
activityAgendaAdjacentPeriod(
|
||||
'day',
|
||||
|
|
@ -165,6 +174,15 @@ describe('activityAgendaAdjacentPeriod', () => {
|
|||
end: '2026-08-17T03:00:00.000Z',
|
||||
anchorDate: '2026-08-10',
|
||||
})
|
||||
// paridade: o label do preview == título pós-commit
|
||||
expect(
|
||||
activityAgendaPeriodLabel(
|
||||
'week',
|
||||
'2026-08-10T03:00:00.000Z',
|
||||
'2026-08-17T03:00:00.000Z',
|
||||
'2026-08-10',
|
||||
),
|
||||
).toBe('10–16 Agosto')
|
||||
})
|
||||
|
||||
it('cruza o ano na semana e o dia (28 Dezembro → 4 Janeiro)', () => {
|
||||
|
|
@ -245,24 +263,26 @@ describe('activityAgendaAdjacentPeriod', () => {
|
|||
})
|
||||
|
||||
it('desloca a lista em um mês inteiro (range do mês adjacente)', () => {
|
||||
expect(
|
||||
activityAgendaAdjacentPeriod(
|
||||
'list',
|
||||
{
|
||||
start: '2026-08-01T00:00:00-03:00',
|
||||
end: '2026-09-01T00:00:00-03:00',
|
||||
anchorDate: '2026-08-01',
|
||||
},
|
||||
'next',
|
||||
),
|
||||
).toEqual({
|
||||
const next = activityAgendaAdjacentPeriod(
|
||||
'list',
|
||||
{
|
||||
start: '2026-08-01T00:00:00-03:00',
|
||||
end: '2026-09-01T00:00:00-03:00',
|
||||
anchorDate: '2026-08-01',
|
||||
},
|
||||
'next',
|
||||
)
|
||||
expect(next).toEqual({
|
||||
start: '2026-09-01T03:00:00.000Z',
|
||||
end: '2026-10-01T03:00:00.000Z',
|
||||
anchorDate: '2026-09-01',
|
||||
})
|
||||
expect(activityAgendaPeriodLabel('list', next!.start, next!.end, next!.anchorDate)).toBe(
|
||||
'Agenda',
|
||||
)
|
||||
})
|
||||
|
||||
it('retorna null quando o anchor do mês não é o 1º ou o intervalo é ilegível', () => {
|
||||
it('retorna null quando o anchor do mês não é o 1º, é malformado, ou o intervalo é ilegível', () => {
|
||||
expect(
|
||||
activityAgendaAdjacentPeriod(
|
||||
'month',
|
||||
|
|
@ -274,6 +294,17 @@ describe('activityAgendaAdjacentPeriod', () => {
|
|||
'next',
|
||||
),
|
||||
).toBeNull()
|
||||
expect(
|
||||
activityAgendaAdjacentPeriod(
|
||||
'month',
|
||||
{
|
||||
start: '2026-08-01T00:00:00-03:00',
|
||||
end: '2026-09-01T00:00:00-03:00',
|
||||
anchorDate: 'não-é-data',
|
||||
},
|
||||
'next',
|
||||
),
|
||||
).toBeNull()
|
||||
expect(
|
||||
activityAgendaAdjacentPeriod(
|
||||
'day',
|
||||
|
|
@ -286,60 +317,6 @@ describe('activityAgendaAdjacentPeriod', () => {
|
|||
),
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('label do preview == título pós-commit (paridade dia/semana/mês/lista)', () => {
|
||||
const day = activityAgendaAdjacentPeriod(
|
||||
'day',
|
||||
{
|
||||
start: '2026-08-09T00:00:00-03:00',
|
||||
end: '2026-08-10T00:00:00-03:00',
|
||||
anchorDate: '2026-08-09',
|
||||
},
|
||||
'next',
|
||||
)
|
||||
expect(activityAgendaPeriodLabel('day', day!.start, day!.end, day!.anchorDate)).toBe(
|
||||
'10 Agosto',
|
||||
)
|
||||
|
||||
const week = activityAgendaAdjacentPeriod(
|
||||
'week',
|
||||
{
|
||||
start: '2026-07-27T00:00:00-03:00',
|
||||
end: '2026-08-03T00:00:00-03:00',
|
||||
anchorDate: '2026-07-27',
|
||||
},
|
||||
'next',
|
||||
)
|
||||
expect(activityAgendaPeriodLabel('week', week!.start, week!.end, week!.anchorDate)).toBe(
|
||||
'3–9 Agosto',
|
||||
)
|
||||
|
||||
const month = activityAgendaAdjacentPeriod(
|
||||
'month',
|
||||
{
|
||||
start: '2026-07-26T00:00:00-03:00',
|
||||
end: '2026-09-01T00:00:00-03:00',
|
||||
anchorDate: '2026-08-01',
|
||||
},
|
||||
'next',
|
||||
)
|
||||
expect(activityAgendaPeriodLabel('month', month!.start, month!.end, month!.anchorDate)).toBe(
|
||||
'Setembro',
|
||||
)
|
||||
|
||||
const list = activityAgendaAdjacentPeriod(
|
||||
'list',
|
||||
{
|
||||
start: '2026-08-01T00:00:00-03:00',
|
||||
end: '2026-09-01T00:00:00-03:00',
|
||||
anchorDate: '2026-08-01',
|
||||
},
|
||||
'next',
|
||||
)
|
||||
expect(activityAgendaPeriodLabel('list', list!.start, list!.end, list!.anchorDate)).toBe(
|
||||
'Agenda',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('activityAgendaViewFromFcId', () => {
|
||||
|
|
|
|||
|
|
@ -51,8 +51,7 @@ const Harness = ({
|
|||
if (onReady) onReady(suppressDateClickRef)
|
||||
return createElement('div', { ref: containerRef, 'data-testid': 'agenda' })
|
||||
}
|
||||
const agendaOf = (container: HTMLElement): HTMLElement =>
|
||||
container.firstElementChild as HTMLElement
|
||||
const agendaOf = (container: HTMLElement): HTMLElement => container.firstElementChild as HTMLElement
|
||||
|
||||
/** Dispatches a cancellable touchmove and returns the event for assertion. */
|
||||
const touchMoveAt = (agenda: HTMLElement, x: number, y: number): Event => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue