mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
refactor(tests): B194 — extract shared SSE mock helper and migrate legacy chat specs to v7
Extract sollinhaChatStreamBody + mockSollinhaChat into campaignE2EFixtures.ts (canonical v7 wire format), reuse across all 6 chat e2e specs and migrate the two legacy specs (transcribe, openingChips) from the format that fails the v7 client schema. Closes #556
This commit is contained in:
parent
f8ead7c4b7
commit
30dac18872
9 changed files with 169 additions and 119 deletions
1
docs/changelog/2026-08-24-b194.md
Normal file
1
docs/changelog/2026-08-24-b194.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
**B194 — E2E do chat: extrair helper compartilhado do mock SSE (formato v7) e migrar specs legados (2026-08-24):** helper `sollinhaChatStreamBody(delta)` + `mockSollinhaChat(page, delta)` em `tests/e2e/fixtures/campaignE2EFixtures.ts` (ao lado de `checkRadixWhenHydrated`/`expectPostResponse`) centraliza o corpo SSE v7 canônico (start/text-start/text-delta/text-end/finish, id `t1`, finishReason `stop`); os 6 specs de chat (`campaignSollinhaContext`, `campaignAiChatResize`, `campaignAiLinks`, `campaignAiChatFollowUps` — este com mock dinâmico renomeado para `mockSollinhaChatDynamic` que reusa o body, `campaignAiTranscribe`, `campaignAiChatOpeningChips`) passam a usar o helper, migrando os 2 specs legados do formato `{"type":"text"}` (que falhava o schema v7 do cliente) para v7; eliminada a duplicação do corpo SSE entre specs, sem mudar comportamento observado nem timing.
|
||||
83
docs/plans/b194-e2e-chat-mock-sse-helper-impl.md
Normal file
83
docs/plans/b194-e2e-chat-mock-sse-helper-impl.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Impl: B194 — E2E do chat: extrair helper compartilhado do mock SSE (formato v7) e migrar spec legado
|
||||
|
||||
Status: rascunho
|
||||
Atualizado em: 2026-08-24
|
||||
Issue: #556
|
||||
Intenção: docs/plans/b194-e2e-chat-mock-sse-helper.md
|
||||
Appetite restante: herdado
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** Corpo SSE do mock de `/campanha/api/ai-chat` (formato wire v7) centralizado num helper compartilhado em `tests/e2e/fixtures/campaignE2EFixtures.ts`, eliminando a duplicação de 3 specs v7 e migrando o spec legado `campaignAiTranscribe` para o formato v7 canônico.
|
||||
- **O que NÃO negociar:** Formato v7 canônico (chunks `data: {"type":"..."}` separados por `\n\n`, array exato `[start, text-start, text-delta(delta), text-end, finish]`, id `"t1"`, finishReason `"stop"`); helper parametrizado por `delta`; não alterar comportamento observado nem timing dos specs.
|
||||
- **O que reavaliar:** Se o segundo spec legado (`campaignAiChatOpeningChips`) e o spec dinâmico (`campaignAiChatFollowUps`) entram no escopo — ver GAP DE ESCOPO abaixo.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Schema v7 route.ts + client] --> B[sollinhaChatStreamBody delta]
|
||||
B --> C[mockSollinhaChat page delta]
|
||||
C --> D[context/resize/links specs]
|
||||
C --> E[transcribe legado -> v7]
|
||||
B --> F[gatedMockAiChat reusa body]
|
||||
B --> G[followUps dinamico reusa body]
|
||||
C --> H[openingChips legado -> v7 PENDENTE GATE]
|
||||
```
|
||||
|
||||
**Opções consideradas:** A (copiar streamBody em cada spec, status quo) | B (helper só do body SSE, mocks locais chamam) | C (helper completo body+rota)
|
||||
**Recomendação:** B+C combinados — `sollinhaChatStreamBody(delta)` para o corpo, `mockSollinhaChat(page, delta)` para a rota fixa, e reuso do body dentro dos mocks dinâmicos/gated.
|
||||
**Rejeitadas:** A (duplicação é o débito em questão).
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`sollinhaChatStreamBody`** (tests/e2e/fixtures/campaignE2EFixtures.ts): nova função que recebe `delta: string` e retorna a string SSE v7 completa — array de chunks `data: {...}` separados por `\n\n` com `[start, text-start, text-delta(delta), text-end, finish]`, id `"t1"`, finishReason `"stop"`. Compartilhada por todos os specs.
|
||||
- **`mockSollinhaChat`** (tests/e2e/fixtures/campaignE2EFixtures.ts): `async (page: Page, delta: string): Promise<void>` que registra rota fixa em `/campanha/api/ai-chat` respondendo com `sollinhaChatStreamBody(delta)` (content-type `text/event-stream`).
|
||||
- **`campaignSollinhaContext.e2e.spec.ts`:** `mockAiChat(page, reply=DEFAULT_REPLY)` passa a chamar `mockSollinhaChat(page, reply)`; `gatedMockAiChat` reusa `sollinhaChatStreamBody(DEFAULT_REPLY)` no lugar do `streamBody` local (remover `streamBody` local). GATE do `gatedMockAiChat` mantido.
|
||||
- **`campaignAiChatResize.e2e.spec.ts`:** remover `mockAiChat`/`streamBody` local; usar `mockSollinhaChat(page, 'Resposta mockada da Sollinha.')`.
|
||||
- **`campaignAiChatLinks.e2e.spec.ts`:** remover `mockAiChat`/`streamBody` local; usar `mockSollinhaChat(page, ASSISTANT_LINKS)`.
|
||||
- **`campaignAiChatFollowUps.e2e.spec.ts`:** manter a lógica dinâmica (`lastUserText` → FIRST/SECOND/THIRD_RESPONSE) mas reusar `sollinhaChatStreamBody(response)` dentro do handler existente, eliminando o corpo duplicado.
|
||||
- **`campaignAiTranscribe.e2e.spec.ts`:** remover o mock legado (`data: {"type":"text","text":...}`) e usar `mockSollinhaChat(page, 'Resposta mockada da Sollinha.')`. Spec não asserta resposta do assistente, então a migração é estritamente melhor sem mudar comportamento.
|
||||
- **Migration:** sem migration (apenas testes)
|
||||
- **Access / Consent:** N/A (testes e2e)
|
||||
- **UI:** N/A
|
||||
|
||||
### Dados → forma
|
||||
|
||||
- N/A
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Extração do helper** — adicionar `sollinhaChatStreamBody` + `mockSollinhaChat` em `campaignE2EFixtures.ts`
|
||||
2. **Refactor dos specs v7** — context/resize/links/followUps usam o helper
|
||||
3. **Migração do legado** — `campaignAiTranscribe` (e `openingChips` se aprovado no GATE) para v7
|
||||
4. **Gates** — `pnpm gate:fast`; push via `pnpm push`
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- Comportamento do chat em si (B187/B188 já cobrem).
|
||||
- Rodar e2e em dev mode.
|
||||
- Alterar o schema v7 da rota real ou do cliente.
|
||||
- Criar novos asserts sobre a resposta do assistente nos specs legados (mantém-se apenas bolha de usuário/chips).
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Race de timing no `gatedMockAiChat`:** o GATE (primeira req responde, demais travam até release) deve ser preservado — reusa apenas o `sollinhaChatStreamBody`, não a lógica de espera. Mitigação: testar o spec de context após o refactor.
|
||||
- **followUps dinâmico quebra:** reuso do body dentro do handler deve manter a seleção por `lastUserText`. Mitigação: rodar o spec followUps isolado.
|
||||
- **Legado não asserta resposta:** migrar para v7 não muda o observável, mas confirma que o mock é aceito pelo `useChat` v7 (falha silenciosa do formato antigo some). Mitigação: specs continuam verdes.
|
||||
|
||||
## Decisões de engenharia (caro de reverter)
|
||||
|
||||
- Opções: A (manter duplicação) | B (helper só do body) | C (helper body+rota, reuso nos dinâmicos/gated); Recomendação: C; Rejeitadas: A (débito origem), B incompleto.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [ ] Aceite de produto da intenção ainda coberto
|
||||
- [ ] Invariantes AGENTS/engineering-standards
|
||||
- [ ] Testes de domínio previstos (unit/int) onde access/write paths mudam
|
||||
|
||||
## GAP DE ESCOPO A RESOLVER NO GATE (humano)
|
||||
|
||||
- O issue cita 1 spec legado (`campaignAiTranscribe`). Na verdade há 2 (também `campaignAiChatOpeningChips`) e 4 v7 (não 3: inclui `campaignAiChatFollowUps`, que é dinâmico).
|
||||
- Recomendação: migrar `openingChips` também e refatorar `followUps` para reusar o corpo. Custo trivial, deixa consistente.
|
||||
- Decisão: PENDENTE — humano confirma no GATE se abrange `openingChips` + `followUps` ou só o literal do issue.
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test, waitForRouterSettled } from './fixtures/campaignE2EFixtures.js'
|
||||
import {
|
||||
expect,
|
||||
sollinhaChatStreamBody,
|
||||
test,
|
||||
waitForRouterSettled,
|
||||
} from './fixtures/campaignE2EFixtures.js'
|
||||
|
||||
/**
|
||||
* Mock the AI endpoint with SDK v7 UI-message-stream SSE chunks (start /
|
||||
|
|
@ -41,8 +46,8 @@ const lastUserText = (body: unknown): string | null => {
|
|||
return null
|
||||
}
|
||||
|
||||
const mockAiChat = (page: Page) =>
|
||||
page.route('**/campanha/api/ai-chat', async (route) => {
|
||||
const mockSollinhaChatDynamic = async (page: Page): Promise<void> => {
|
||||
await page.route('**/campanha/api/ai-chat', async (route) => {
|
||||
const raw = route.request().postData() ?? ''
|
||||
const userText = lastUserText(JSON.parse(raw || '{}'))
|
||||
const response = userText?.includes('Ilhéus')
|
||||
|
|
@ -53,15 +58,10 @@ const mockAiChat = (page: Page) =>
|
|||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/event-stream' },
|
||||
body: [
|
||||
'data: {"type":"start"}\n\n',
|
||||
'data: {"type":"text-start","id":"t1"}\n\n',
|
||||
`data: {"type":"text-delta","id":"t1","delta":${JSON.stringify(response)}}\n\n`,
|
||||
'data: {"type":"text-end","id":"t1"}\n\n',
|
||||
'data: {"type":"finish","finishReason":"stop"}\n\n',
|
||||
].join(''),
|
||||
body: sollinhaChatStreamBody(response),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const chipButton = (page: Page, text: string) =>
|
||||
page.getByRole('button', { name: text, exact: true })
|
||||
|
|
@ -97,7 +97,7 @@ const askViaInput = async (page: Page, text: string, expected: string) => {
|
|||
test.describe('B192 — follow-ups sugeridos após cada resposta do Sollinha', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.slow()
|
||||
await mockAiChat(page)
|
||||
await mockSollinhaChatDynamic(page)
|
||||
})
|
||||
|
||||
test('resposta com bloco: bolha sem o bloco, chips acima do input, tocar envia e o slot é substituído', async ({
|
||||
|
|
|
|||
|
|
@ -1,20 +1,11 @@
|
|||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test, waitForRouterSettled } from './fixtures/campaignE2EFixtures.js'
|
||||
|
||||
/**
|
||||
* Mock the AI endpoint with a minimal SSE stream so sending a chip neither
|
||||
* needs a DeepSeek API key nor hits the real rate limiter — the user message is
|
||||
* still recorded client-side, which is all these assertions need.
|
||||
*/
|
||||
const mockAiChat = (page: Page) =>
|
||||
page.route('**/campanha/api/ai-chat', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/event-stream' },
|
||||
body: 'data: {"type":"text","text":"Resposta mockada da Sollinha."}\n\n',
|
||||
})
|
||||
})
|
||||
import {
|
||||
expect,
|
||||
mockSollinhaChat,
|
||||
test,
|
||||
waitForRouterSettled,
|
||||
} from './fixtures/campaignE2EFixtures.js'
|
||||
|
||||
const STAFF_DESKTOP_CHIPS = [
|
||||
'Quem foi o deputado mais votado em Feira de Santana?',
|
||||
|
|
@ -51,7 +42,7 @@ const pickChip = async (page: Page, text: string) => {
|
|||
test.describe('B191 — ações rápidas de abertura no chat Sollinha (chips de pergunta)', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.slow()
|
||||
await mockAiChat(page)
|
||||
await mockSollinhaChat(page, 'Resposta mockada da Sollinha.')
|
||||
})
|
||||
|
||||
test('staff no desktop vê 4 chips; tocar envia a pergunta e os chips somem', async ({
|
||||
|
|
|
|||
|
|
@ -1,29 +1,11 @@
|
|||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test, waitForRouterSettled } from './fixtures/campaignE2EFixtures.js'
|
||||
|
||||
/**
|
||||
* Mock the AI endpoint with a minimal SSE stream so sending a message neither
|
||||
* needs a DeepSeek API key nor hits the real rate limiter — the user message is
|
||||
* still recorded client-side, which is all these assertions need. The chunks
|
||||
* follow the SDK v7 UI-message-stream wire format (start / text-start /
|
||||
* text-delta / text-end / finish): a bare legacy `{"type":"text"}` chunk fails
|
||||
* the client schema and leaves the chat stuck in an error status.
|
||||
*/
|
||||
const mockAiChat = (page: Page) =>
|
||||
page.route('**/campanha/api/ai-chat', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/event-stream' },
|
||||
body: [
|
||||
'data: {"type":"start"}\n\n',
|
||||
'data: {"type":"text-start","id":"t1"}\n\n',
|
||||
'data: {"type":"text-delta","id":"t1","delta":"Resposta mockada da Sollinha."}\n\n',
|
||||
'data: {"type":"text-end","id":"t1"}\n\n',
|
||||
'data: {"type":"finish","finishReason":"stop"}\n\n',
|
||||
].join(''),
|
||||
})
|
||||
})
|
||||
import {
|
||||
expect,
|
||||
mockSollinhaChat,
|
||||
test,
|
||||
waitForRouterSettled,
|
||||
} from './fixtures/campaignE2EFixtures.js'
|
||||
|
||||
const MESSAGE = 'Mensagem que migra entre as superficies'
|
||||
|
||||
|
|
@ -55,7 +37,7 @@ const chatPanelWidthAt = (page: Page, index: number) =>
|
|||
test.describe('B167 — chat Sollinha migra entre painel e drawer ao redimensionar', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.slow()
|
||||
await mockAiChat(page)
|
||||
await mockSollinhaChat(page, 'Resposta mockada da Sollinha.')
|
||||
await page.setViewportSize({ width: 1280, height: 800 })
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test, waitForRouterSettled } from './fixtures/campaignE2EFixtures.js'
|
||||
import {
|
||||
expect,
|
||||
mockSollinhaChat,
|
||||
test,
|
||||
waitForRouterSettled,
|
||||
} from './fixtures/campaignE2EFixtures.js'
|
||||
|
||||
/**
|
||||
* B187 — links in the Sollinha's markdown answers must look like links: brand
|
||||
|
|
@ -14,24 +19,6 @@ import { expect, test, waitForRouterSettled } from './fixtures/campaignE2EFixtur
|
|||
const ASSISTANT_LINKS =
|
||||
'Confira [o portal da saúde](https://www.saude.ba.gov.br/) e [o município de Ilhéus](/campanha/municipios/ilheus).'
|
||||
|
||||
const mockAiChat = (page: Page) =>
|
||||
page.route('**/campanha/api/ai-chat', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/event-stream' },
|
||||
// SDK v7 UI-message-stream wire format (start / text-start / text-delta /
|
||||
// text-end / finish) — a bare legacy `{"type":"text"}` chunk fails the
|
||||
// client schema and the chat would never settle.
|
||||
body: [
|
||||
'data: {"type":"start"}\n\n',
|
||||
'data: {"type":"text-start","id":"t1"}\n\n',
|
||||
`data: {"type":"text-delta","id":"t1","delta":${JSON.stringify(ASSISTANT_LINKS)}}\n\n`,
|
||||
'data: {"type":"text-end","id":"t1"}\n\n',
|
||||
'data: {"type":"finish","finishReason":"stop"}\n\n',
|
||||
].join(''),
|
||||
})
|
||||
})
|
||||
|
||||
const openChatAndSend = async (page: Page) => {
|
||||
await expect(page.getByText('Olá! Eu sou o Sollinha')).toBeVisible({ timeout: 20_000 })
|
||||
// OPS42 — dev-only settle before interacting (see `waitForRouterSettled`).
|
||||
|
|
@ -52,7 +39,7 @@ const openChatAndSend = async (page: Page) => {
|
|||
test.describe('B187 — links com aparência de link nas respostas do Sollinha', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.slow()
|
||||
await mockAiChat(page)
|
||||
await mockSollinhaChat(page, ASSISTANT_LINKS)
|
||||
await page.setViewportSize({ width: 1280, height: 800 })
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test, waitForRouterSettled } from './fixtures/campaignE2EFixtures.js'
|
||||
import {
|
||||
expect,
|
||||
mockSollinhaChat,
|
||||
test,
|
||||
waitForRouterSettled,
|
||||
} from './fixtures/campaignE2EFixtures.js'
|
||||
|
||||
/**
|
||||
* B173 — voice transcription for the Sollinha chat.
|
||||
|
|
@ -47,15 +52,6 @@ const mockTranscribe = (page: Page, text: string) =>
|
|||
})
|
||||
})
|
||||
|
||||
const mockAiChat = (page: Page) =>
|
||||
page.route('**/campanha/api/ai-chat', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/event-stream' },
|
||||
body: 'data: {"type":"text","text":"Resposta mockada da Sollinha."}\n\n',
|
||||
})
|
||||
})
|
||||
|
||||
const openChat = async (page: Page) => {
|
||||
await expect(page.getByText('Olá! Eu sou o Sollinha')).toBeVisible({ timeout: 20_000 })
|
||||
// OPS42 — dev-only settle before interacting (see `waitForRouterSettled`).
|
||||
|
|
@ -80,7 +76,7 @@ test.describe('B173 — consulta por voz no chat da Sollinha', () => {
|
|||
await page.goto('/campanha')
|
||||
|
||||
await mockTranscribe(page, 'Quantos votos tivemos em Ilhéus?')
|
||||
await mockAiChat(page)
|
||||
await mockSollinhaChat(page, 'Resposta mockada da Sollinha.')
|
||||
await openChat(page)
|
||||
|
||||
const input = page.getByRole('textbox', { name: 'Pergunte para o Sollinha...' })
|
||||
|
|
@ -118,7 +114,7 @@ test.describe('B173 — consulta por voz no chat da Sollinha', () => {
|
|||
})
|
||||
await campaign.login(page, user.email!, user.password)
|
||||
await page.goto('/campanha')
|
||||
await mockAiChat(page)
|
||||
await mockSollinhaChat(page, 'Resposta mockada da Sollinha.')
|
||||
await openChat(page)
|
||||
|
||||
await page.getByRole('button', { name: 'Falar pergunta (voz)' }).click()
|
||||
|
|
|
|||
|
|
@ -1,41 +1,18 @@
|
|||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { expect, test, waitForRouterSettled } from './fixtures/campaignE2EFixtures.js'
|
||||
import {
|
||||
expect,
|
||||
mockSollinhaChat,
|
||||
sollinhaChatStreamBody,
|
||||
test,
|
||||
waitForRouterSettled,
|
||||
} from './fixtures/campaignE2EFixtures.js'
|
||||
|
||||
const SESSION_KEY = 'teqo:campaign:sollinha-chat-session'
|
||||
|
||||
const DEFAULT_REPLY = 'Resposta mockada com [Municípios](/campanha/municipios).'
|
||||
const EXTERNAL_REPLY = 'Resposta mockada com [portal da saúde](https://www.saude.ba.gov.br/).'
|
||||
|
||||
/**
|
||||
* The SSE body the mocks reply with. Chunks follow the SDK v7 UI-message-stream
|
||||
* wire format (start / text-start / text-delta / text-end / finish) — a bare
|
||||
* legacy `{"type":"text"}` chunk fails the client schema and the chat would
|
||||
* never settle.
|
||||
*/
|
||||
const streamBody = (reply: string) =>
|
||||
[
|
||||
'data: {"type":"start"}\n\n',
|
||||
'data: {"type":"text-start","id":"t1"}\n\n',
|
||||
`data: {"type":"text-delta","id":"t1","delta":${JSON.stringify(reply)}}\n\n`,
|
||||
'data: {"type":"text-end","id":"t1"}\n\n',
|
||||
'data: {"type":"finish","finishReason":"stop"}\n\n',
|
||||
].join('')
|
||||
|
||||
/**
|
||||
* Mock the AI endpoint with a minimal SSE stream so sending a message neither
|
||||
* needs a DeepSeek API key nor hits the real rate limiter. The reply carries a
|
||||
* markdown link so the "navigate without reload" path has a real anchor.
|
||||
*/
|
||||
const mockAiChat = (page: Page, reply: string = DEFAULT_REPLY) =>
|
||||
page.route('**/campanha/api/ai-chat', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/event-stream' },
|
||||
body: streamBody(reply),
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Mock whose FIRST request replies immediately and every later request hangs
|
||||
* until the test calls `release()` — a deterministic window in which the chat
|
||||
|
|
@ -56,7 +33,7 @@ const gatedMockAiChat = (page: Page) => {
|
|||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/event-stream' },
|
||||
body: streamBody(DEFAULT_REPLY),
|
||||
body: sollinhaChatStreamBody(DEFAULT_REPLY),
|
||||
})
|
||||
} catch {
|
||||
// An early failure may end the test before `release()` — fulfilling on a
|
||||
|
|
@ -113,7 +90,7 @@ const storedSession = (page: Page) =>
|
|||
test.describe('B188 — contexto da conversa persiste na sessão da janela/tab', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.slow()
|
||||
await mockAiChat(page)
|
||||
await mockSollinhaChat(page, DEFAULT_REPLY)
|
||||
})
|
||||
|
||||
test('reload na mesma aba restaura a conversa', async ({ page, campaign }) => {
|
||||
|
|
@ -234,7 +211,7 @@ test.describe('B188 — contexto da conversa persiste na sessão da janela/tab',
|
|||
test.describe('B198 — link de resposta fecha o drawer mobile ao navegar no mesmo tab', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.slow()
|
||||
await mockAiChat(page)
|
||||
await mockSollinhaChat(page, DEFAULT_REPLY)
|
||||
})
|
||||
|
||||
test('link interno: drawer fecha no toque e a navegação segue no mesmo tab', async ({
|
||||
|
|
@ -272,7 +249,7 @@ test.describe('B198 — link de resposta fecha o drawer mobile ao navegar no mes
|
|||
|
||||
test('link externo: abre em nova aba e o drawer permanece aberto', async ({ page, campaign }) => {
|
||||
// Last-registered route wins (LIFO) — overrides the beforeEach mock.
|
||||
await mockAiChat(page, EXTERNAL_REPLY)
|
||||
await mockSollinhaChat(page, EXTERNAL_REPLY)
|
||||
// The popup would hit the real site; fulfill it so CI stays deterministic.
|
||||
await page
|
||||
.context()
|
||||
|
|
@ -306,7 +283,7 @@ test.describe('B198 — link de resposta fecha o drawer mobile ao navegar no mes
|
|||
test.describe('B199 — fechar o drawer durante streaming persiste fechado no reload', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.slow()
|
||||
await mockAiChat(page)
|
||||
await mockSollinhaChat(page, DEFAULT_REPLY)
|
||||
})
|
||||
|
||||
test('fechar mid-stream grava open:false e o reload não reabre o drawer', async ({
|
||||
|
|
|
|||
|
|
@ -554,6 +554,39 @@ export const checkRadixWhenHydrated = async (page: Page, label: string) => {
|
|||
}).toPass({ timeout: 15_000 })
|
||||
}
|
||||
|
||||
/**
|
||||
* The SSE body the Sollinha chat mocks reply with. Chunks follow the SDK v7
|
||||
* UI-message-stream wire format (start / text-start / text-delta / text-end /
|
||||
* finish) — a bare legacy `{"type":"text"}` chunk fails the client schema and
|
||||
* the chat would never settle. Shared by every chat e2e mock so the v7 shape
|
||||
* lives in exactly one place.
|
||||
*/
|
||||
export const sollinhaChatStreamBody = (delta: string): string =>
|
||||
[
|
||||
'data: {"type":"start"}\n\n',
|
||||
'data: {"type":"text-start","id":"t1"}\n\n',
|
||||
`data: {"type":"text-delta","id":"t1","delta":${JSON.stringify(delta)}}\n\n`,
|
||||
'data: {"type":"text-end","id":"t1"}\n\n',
|
||||
'data: {"type":"finish","finishReason":"stop"}\n\n',
|
||||
].join('')
|
||||
|
||||
/**
|
||||
* Mock `/campanha/api/ai-chat` with a minimal v7 SSE stream so sending a
|
||||
* message neither needs a DeepSeek API key nor hits the real rate limiter. The
|
||||
* reply carries a markdown link so the "navigate without reload" path has a
|
||||
* real anchor. Mirrors the precedent helpers (`checkRadixWhenHydrated`,
|
||||
* `expectPostResponse`) that already live in this fixture module.
|
||||
*/
|
||||
export const mockSollinhaChat = async (page: Page, delta: string): Promise<void> => {
|
||||
await page.route('**/campanha/api/ai-chat', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/event-stream' },
|
||||
body: sollinhaChatStreamBody(delta),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for dynamic campaign pages to commit their RSC stream by polling
|
||||
* until the browser has no transient hidden `#S:*` shell copies left.
|
||||
|
|
|
|||
Loading…
Reference in a new issue