mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 09:34:46 +08:00
fix(onboarding): trocar de organização não configurada deixava a pessoa presa
Duas coisas, e a segunda é o defeito que a primeira expôs. 1. Dois seeds disputavam o slug `e2e-segunda-org`: o de funis cria a org SEM `onboarded_at`, e o de duas organizações, ao encontrá-la, devolvia o id sem olhar o estado. Na parte 2 do e2e o de funis roda antes, e a org B chegava sem onboarding — `app/app/layout.tsx:51` manda essa org para /onboarding, o shell sai da árvore e o seletor vai junto. Era o `element(s) not found` que derrubou a main. Slug próprio remove a disputa; o seed também garante a precondição que ele exige. 2. E o wizard não tinha saída. Medido no snapshot do run 33164258175: "Termos de Uso", "Política de Privacidade" e um "Continuar" desabilitado — nenhuma porta. Quem foi convidado para uma organização nova e trocou para ver perdia o caminho de volta. Agora o cabeçalho oferece a volta para quem tem outra organização, e navega de verdade (setActiveOrg revalida /app, não /onboarding). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AKUXpCeT28H7yPMfvEqJc6
This commit is contained in:
co-authored by
Claude Opus 5
parent
8b868bf3e4
commit
036db8e68b
@@ -250,6 +250,7 @@ jobs:
|
||||
agenda-painel-cabe-na-tela.spec.ts
|
||||
agenda-ver-na-agenda.spec.ts
|
||||
agenda-escopo-da-organizacao.spec.ts
|
||||
troca-de-organizacao-tem-volta.spec.ts
|
||||
agenda-caminho-ate-os-horarios.spec.ts
|
||||
agenda-google-volta-do-consentimento.spec.ts
|
||||
# Cada item aqui tem o motivo MEDIDO, não presumido — e o gate cobra que a
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTransition } from "react";
|
||||
|
||||
import { setActiveOrg } from "@/app/actions/shell/setActiveOrg";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { ArrowBendUpLeft, CaretDown } from "@/lib/ui/icons";
|
||||
|
||||
/**
|
||||
* A VOLTA — o caminho de saída do wizard para quem tem outra organização.
|
||||
*
|
||||
* ═══ O defeito que isto fecha ═══
|
||||
*
|
||||
* `app/app/layout.tsx` manda para `/onboarding` toda organização ativa sem
|
||||
* `onboarded_at`. Trocar para uma organização recém-criada — pelo seletor no
|
||||
* topo, um clique — levava a pessoa para o wizard dela **e tirava o seletor da
|
||||
* tela junto**: o layout de `/app` sai inteiro da árvore, e com ele o
|
||||
* `TenantSwitcher`.
|
||||
*
|
||||
* O que sobrava no wizard, medido no snapshot de uma falha do CI: "Termos de
|
||||
* Uso", "Política de Privacidade" e um "Continuar" desabilitado. Três controles,
|
||||
* nenhum deles uma saída. Quem chegou ali por engano — foi convidado para uma
|
||||
* organização nova, trocou para ver o que era — perdia o caminho de volta para
|
||||
* a organização onde estava trabalhando, e a única saída real era limpar cookie
|
||||
* ou adivinhar a URL de logout.
|
||||
*
|
||||
* É o invariante "nenhuma demanda sem próximo passo" pelo avesso: a tela pede
|
||||
* seis passos de configuração de quem talvez só quisesse espiar, e não oferece
|
||||
* a porta de trás. Configurar continua sendo o caminho principal — este botão
|
||||
* não o atropela, fica ao lado dele no cabeçalho.
|
||||
*
|
||||
* ⚠️ Navega EXPLICITAMENTE depois de trocar. `setActiveOrg` revalida `/app`, não
|
||||
* `/onboarding`, então o layout desta rota não re-renderiza sozinho: sem o
|
||||
* `replace`, o cookie mudaria e a pessoa continuaria olhando o wizard da
|
||||
* organização que ela acabou de deixar.
|
||||
*/
|
||||
export function OutrasOrganizacoes({
|
||||
outras,
|
||||
}: {
|
||||
outras: Array<{ id: string; nome: string }>;
|
||||
}) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const router = useRouter();
|
||||
|
||||
if (outras.length === 0) return null;
|
||||
|
||||
const trocar = (id: string) =>
|
||||
startTransition(async () => {
|
||||
const r = await setActiveOrg(id);
|
||||
if (r.ok) router.replace("/app/inbox");
|
||||
});
|
||||
|
||||
// Uma organização só: a escolha já está feita, e um menu de um item é
|
||||
// cerimônia. O rótulo diz PARA ONDE se vai, não "trocar" — quem está preso
|
||||
// aqui quer o nome do lugar de onde veio.
|
||||
if (outras.length === 1) {
|
||||
const unica = outras[0]!;
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
data-testid="sair-do-onboarding"
|
||||
disabled={isPending}
|
||||
onClick={() => trocar(unica.id)}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<ArrowBendUpLeft size={14} weight="bold" aria-hidden />
|
||||
<span className="max-w-[180px] truncate">Voltar para {unica.nome}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
data-testid="sair-do-onboarding"
|
||||
disabled={isPending}
|
||||
className="gap-1.5"
|
||||
>
|
||||
<ArrowBendUpLeft size={14} weight="bold" aria-hidden />
|
||||
<span>Ir para outra organização</span>
|
||||
<CaretDown size={12} aria-hidden />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="min-w-[220px]">
|
||||
{outras.map((o) => (
|
||||
<DropdownMenuItem
|
||||
key={o.id}
|
||||
data-testid={`sair-do-onboarding-item-${o.id}`}
|
||||
onClick={() => trocar(o.id)}
|
||||
>
|
||||
<span className="truncate">{o.nome}</span>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { redirect } from "next/navigation";
|
||||
import { requireAuth, resolveActiveOrg } from "@/lib/auth/server";
|
||||
import { loadOnboardingState } from "@/app/actions/onboarding/_shared";
|
||||
import { Stepper } from "./_components/Stepper";
|
||||
import { OutrasOrganizacoes } from "./_components/OutrasOrganizacoes";
|
||||
import { SkipToEnd } from "./_components/SkipToEnd";
|
||||
import { branding } from "@/lib/branding";
|
||||
import { passosVisiveis } from "@/lib/onboarding/passos";
|
||||
@@ -34,7 +35,20 @@ export default async function OnboardingLayout({ children }: { children: React.R
|
||||
<p className="text-xs uppercase tracking-wider text-muted-foreground">{branding().name}</p>
|
||||
<h1 className="text-lg font-semibold tracking-tight">{activeOrg.name}</h1>
|
||||
</div>
|
||||
{isDev ? <SkipToEnd /> : null}
|
||||
<div className="flex items-center gap-1">
|
||||
{/*
|
||||
A SAÍDA, para quem tem outra organização. Ver o cabeçalho de
|
||||
`OutrasOrganizacoes`: sem ela, trocar de organização pelo seletor
|
||||
do topo levava a um wizard sem porta de volta — o layout de `/app`
|
||||
sai da árvore e leva o `TenantSwitcher` junto.
|
||||
*/}
|
||||
<OutrasOrganizacoes
|
||||
outras={user.organizations
|
||||
.filter((o) => o.organization_id !== activeOrg.orgId)
|
||||
.map((o) => ({ id: o.organization_id, nome: o.organization_name }))}
|
||||
/>
|
||||
{isDev ? <SkipToEnd /> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto w-full max-w-3xl px-4 pb-2">
|
||||
<Stepper passos={passos} />
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 136 KiB |
@@ -42,7 +42,27 @@ const admin = createClient(credenciais.url, credenciais.serviceRole, {
|
||||
|
||||
const CREDS_PATH = path.join(process.cwd(), ".e2e-creds.json");
|
||||
|
||||
const ORG_B_SLUG = "e2e-segunda-org";
|
||||
/**
|
||||
* ⚠️ SLUG PRÓPRIO, e a troca dele é o conserto de um CI vermelho.
|
||||
*
|
||||
* Isto era `"e2e-segunda-org"` — o MESMO slug que `scripts/seed-e2e-funis.ts:50`
|
||||
* usa. Os dois seeds construíam estados diferentes na mesma linha de
|
||||
* `organizations`, e quem rodava primeiro vencia: o de funis cria a org **sem
|
||||
* `onboarded_at`** (ele só quer o funil homônimo), e este aqui, ao encontrar a
|
||||
* linha por slug, devolvia o id sem corrigir nada.
|
||||
*
|
||||
* Na parte 2 do e2e o `pipelines-gestao` roda antes do `agenda-escopo`, então a
|
||||
* org B chegava sem onboarding — e `app/app/layout.tsx` manda para `/onboarding`
|
||||
* toda org nesse estado. A troca de organização terminava num redirect, o shell
|
||||
* saía da árvore junto com o seletor, e a spec reprovava com `element(s) not
|
||||
* found` depois de alguns `unexpected value "disabled"`. Medido no run
|
||||
* 33164258175 e reproduzido aqui zerando o `onboarded_at` à mão.
|
||||
*
|
||||
* Dois seeds disputando uma linha é o defeito; corrigir o `onboarded_at` no
|
||||
* caminho de "já existe" trataria o sintoma e deixaria a disputa de pé, para o
|
||||
* próximo campo que divergisse. Slug próprio remove a disputa.
|
||||
*/
|
||||
const ORG_B_SLUG = "e2e-org-b";
|
||||
const ORG_B_NOME = "E2E Segunda Organização";
|
||||
|
||||
/** Um tipo por org, com nome que não existe na outra. É a asserção da spec. */
|
||||
@@ -63,7 +83,25 @@ async function orgB(): Promise<string> {
|
||||
.eq("slug", ORG_B_SLUG)
|
||||
.maybeSingle();
|
||||
if (error) throw new Error(`buscar org B: ${error.message}`);
|
||||
if (existente) return (existente as { id: string }).id;
|
||||
if (existente) {
|
||||
const id = (existente as { id: string }).id;
|
||||
/**
|
||||
* O SEED GARANTE A PRECONDIÇÃO QUE ELE PRÓPRIO EXIGE — mesmo reencontrando a
|
||||
* org de uma corrida anterior.
|
||||
*
|
||||
* Defesa em profundidade ao lado do slug próprio: o que quebrou o CI foi uma
|
||||
* org **sem `onboarded_at`**, e um seed que devolve o id sem olhar o estado
|
||||
* herda qualquer coisa que estivesse ali. `update` e não `upsert` porque a
|
||||
* linha existe; e só este campo, para não pisar no que outra spec configurou.
|
||||
*/
|
||||
const { error: fixErr } = await admin
|
||||
.from("organizations")
|
||||
.update({ onboarded_at: new Date().toISOString() } as never)
|
||||
.eq("id", id)
|
||||
.is("onboarded_at", null);
|
||||
if (fixErr) throw new Error(`garantir onboarding da org B: ${fixErr.message}`);
|
||||
return id;
|
||||
}
|
||||
|
||||
const { data, error: insErr } = await admin
|
||||
.from("organizations")
|
||||
|
||||
@@ -74,6 +74,17 @@ async function main(): Promise<void> {
|
||||
slug: SEGUNDA_SLUG,
|
||||
legal_name: "E2E Segunda Org LTDA",
|
||||
display_name: "E2E Segunda Org",
|
||||
// ⚠️ SEM `onboarded_at` DE PROPÓSITO — e agora isso é o fixture, não um
|
||||
// descuido.
|
||||
//
|
||||
// Esta org existe para o caso do funil homônimo, e ficou sem onboarding
|
||||
// por omissão. Enquanto ela dividia o slug com o seed de duas
|
||||
// organizações, essa omissão vazava para uma spec que não a pediu e
|
||||
// derrubava o CI (ver o cabeçalho de `seed-e2e-duas-organizacoes.ts`).
|
||||
// Com os slugs separados, ela passa a ser o único lugar do harness com
|
||||
// uma organização legitimamente NÃO configurada — que é exatamente o
|
||||
// estado de que `troca-de-organizacao-tem-volta.spec.ts` precisa para
|
||||
// provar que dá para sair do wizard.
|
||||
})
|
||||
.select("id")
|
||||
.single();
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
|
||||
/**
|
||||
* TROCAR DE ORGANIZAÇÃO TEM VOLTA — mesmo quando a organização de destino
|
||||
* ainda não foi configurada.
|
||||
*
|
||||
* ─── O defeito, e como ele apareceu ──────────────────────────────────────
|
||||
*
|
||||
* `app/app/layout.tsx` manda para `/onboarding` toda organização ativa sem
|
||||
* `onboarded_at`. Então trocar de organização pelo seletor do topo — um clique,
|
||||
* a ação mais banal do cabeçalho — podia levar ao wizard de seis passos da
|
||||
* organização nova **e tirar o seletor da tela junto**: o layout de `/app` sai
|
||||
* inteiro da árvore, e o `TenantSwitcher` mora nele.
|
||||
*
|
||||
* O que sobrava, medido no snapshot de uma falha do CI (run 33164258175):
|
||||
* "Termos de Uso", "Política de Privacidade" e um "Continuar" desabilitado.
|
||||
* Três controles, nenhuma saída. Quem foi convidado para uma organização nova e
|
||||
* trocou para ver o que era ficava sem caminho de volta — limpar cookie ou
|
||||
* adivinhar a URL de logout.
|
||||
*
|
||||
* O vermelho do CI era outro (dois seeds disputando o mesmo slug deixavam a org
|
||||
* B sem onboarding), e essa parte se conserta no harness. Esta spec prende o
|
||||
* que o vermelho EXPÔS, que é de produto e sobrevive ao conserto do seed.
|
||||
*
|
||||
* ─── Por que a organização do seed de funis ──────────────────────────────
|
||||
*
|
||||
* Depois da separação dos slugs, `e2e-segunda-org` é o único lugar do harness
|
||||
* com uma organização legitimamente **não configurada** — e é exatamente o
|
||||
* fixture de que este caso precisa. Está escrito lá, no `insert`, para ninguém
|
||||
* "consertar" a ausência do `onboarded_at` achando que é descuido.
|
||||
*/
|
||||
const RAIZ = path.resolve(__dirname, "../..");
|
||||
|
||||
interface Creds {
|
||||
password: string;
|
||||
users: Record<string, { email: string } | undefined>;
|
||||
funis?: { segunda_org_id: string };
|
||||
duas_orgs?: { org_a_id: string };
|
||||
}
|
||||
|
||||
function lerCreds(): Creds {
|
||||
const p = path.join(RAIZ, ".e2e-creds.json");
|
||||
if (!fs.existsSync(p)) throw new Error("`.e2e-creds.json` ausente — rode `scripts/seed-e2e-credentials.ts`");
|
||||
let c = JSON.parse(fs.readFileSync(p, "utf8")) as Creds;
|
||||
// A spec semeia a própria precondição: depender de `pipelines-gestao` ter
|
||||
// rodado antes seria depender da ORDEM, que é o defeito que esta suíte já
|
||||
// pagou mais de uma vez.
|
||||
if (!c.funis || !c.duas_orgs) {
|
||||
if (!c.duas_orgs) execFileSync("npx", ["tsx", "scripts/seed-e2e-duas-organizacoes.ts"], { stdio: "inherit" });
|
||||
if (!c.funis) execFileSync("npx", ["tsx", "scripts/seed-e2e-funis.ts"], { stdio: "inherit" });
|
||||
c = JSON.parse(fs.readFileSync(p, "utf8")) as Creds;
|
||||
}
|
||||
if (!c.funis?.segunda_org_id) throw new Error("o seed de funis não gravou `funis.segunda_org_id`");
|
||||
if (!c.duas_orgs?.org_a_id) throw new Error("o seed de duas orgs não gravou `duas_orgs.org_a_id`");
|
||||
return c;
|
||||
}
|
||||
|
||||
async function entrar(page: Page, creds: Creds) {
|
||||
const usuario = creds.users.manager;
|
||||
if (!usuario) throw new Error(".e2e-creds.json sem o usuário `manager`");
|
||||
await page.goto("/login");
|
||||
await page.getByLabel(/e-?mail/i).fill(usuario.email);
|
||||
await page.getByLabel(/senha/i).fill(creds.password);
|
||||
await page.getByRole("button", { name: /entrar/i }).click();
|
||||
await page.waitForURL(/\/app(\/|$)/, { timeout: 20_000 });
|
||||
}
|
||||
|
||||
test.describe.configure({ timeout: 150_000 });
|
||||
|
||||
test("trocar para uma organização não configurada leva ao wizard — e dá para voltar", async ({ page }) => {
|
||||
const creds = lerCreds();
|
||||
const semOnboarding = creds.funis!.segunda_org_id;
|
||||
const orgA = creds.duas_orgs!.org_a_id;
|
||||
|
||||
await entrar(page, creds);
|
||||
await page.goto("/app/inbox");
|
||||
|
||||
// Ancora na org A e guarda o nome dela — é para cá que a volta tem de trazer.
|
||||
const seletor = page.getByTestId("tenant-switcher");
|
||||
await expect(seletor).toBeVisible({ timeout: 20_000 });
|
||||
await seletor.click();
|
||||
await page.getByTestId(`tenant-switcher-item-${orgA}`).click();
|
||||
await expect(seletor).toBeEnabled({ timeout: 60_000 });
|
||||
const nomeDaOrgA = (await seletor.textContent())!.trim();
|
||||
expect(nomeDaOrgA.length, "o seletor não anuncia o nome da organização ativa").toBeGreaterThan(0);
|
||||
|
||||
// ── a troca que prendia ────────────────────────────────────────────────
|
||||
await seletor.click();
|
||||
await page.getByTestId(`tenant-switcher-item-${semOnboarding}`).click();
|
||||
|
||||
// O wizard é o destino CORRETO — a organização não está configurada mesmo. O
|
||||
// defeito nunca foi vir para cá; foi não ter como sair.
|
||||
await page.waitForURL(/\/onboarding/, { timeout: 60_000 });
|
||||
await expect(
|
||||
page.getByTestId("tenant-switcher"),
|
||||
"o seletor de organização sobreviveu ao redirect — se ele está aqui, este caso perdeu o objeto",
|
||||
).toHaveCount(0);
|
||||
|
||||
// ── A SAÍDA ────────────────────────────────────────────────────────────
|
||||
const saida = page.getByTestId("sair-do-onboarding");
|
||||
await expect(
|
||||
saida,
|
||||
"o wizard não oferece caminho de volta — quem trocou de organização por engano fica preso aqui",
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
|
||||
await saida.click();
|
||||
// Com mais de uma organização de destino a saída abre um menu; com uma só, o
|
||||
// clique já basta. Cobrimos o caminho que este ambiente produz.
|
||||
const item = page.getByTestId(`sair-do-onboarding-item-${orgA}`);
|
||||
if ((await item.count()) > 0) await item.click();
|
||||
|
||||
// ── e a volta CHEGA: o produto de novo, na organização de antes ────────
|
||||
await page.waitForURL(/\/app(\/|$)/, { timeout: 60_000 });
|
||||
const seletorDeVolta = page.getByTestId("tenant-switcher");
|
||||
await expect(seletorDeVolta, "voltei para o produto e o seletor não reapareceu").toBeVisible({
|
||||
timeout: 20_000,
|
||||
});
|
||||
await expect(seletorDeVolta).toBeEnabled({ timeout: 60_000 });
|
||||
await expect(
|
||||
seletorDeVolta,
|
||||
`a volta não trouxe para "${nomeDaOrgA}" — trocou de lugar, não desfez a troca`,
|
||||
).toContainText(nomeDaOrgA, { timeout: 20_000 });
|
||||
|
||||
await page.screenshot({ path: "evidence/onboarding/troca-de-org-tem-volta.png" });
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* O WIZARD TEM PORTA DE SAÍDA — para quem tem outra organização, e só.
|
||||
*
|
||||
* ─── O defeito que esta cerca fecha ──────────────────────────────────────
|
||||
*
|
||||
* `app/app/layout.tsx` manda para `/onboarding` toda organização ativa sem
|
||||
* `onboarded_at`, e o layout de `/app` sai da árvore junto com o
|
||||
* `TenantSwitcher`. Quem trocou de organização pelo seletor do topo caía num
|
||||
* wizard de seis passos com três controles na tela — "Termos de Uso",
|
||||
* "Política de Privacidade" e um "Continuar" desabilitado — e nenhuma saída.
|
||||
* Medido no snapshot de uma falha do CI, não deduzido.
|
||||
*
|
||||
* ─── O que este arquivo prova, e o que fica para a spec em tela ──────────
|
||||
*
|
||||
* Aqui: as três formas do controle (nenhuma outra organização, uma, várias) e o
|
||||
* fato de que ele NAVEGA depois de trocar. A navegação é o detalhe que mais
|
||||
* facilmente se perde numa refatoração e falha em silêncio: `setActiveOrg`
|
||||
* revalida `/app`, não `/onboarding`, então sem o `replace` o cookie muda e a
|
||||
* pessoa continua olhando o wizard da organização que acabou de deixar — a tela
|
||||
* fica idêntica, e o clique parece não ter feito nada.
|
||||
*
|
||||
* A jornada inteira (trocar, cair no wizard, voltar) é da
|
||||
* `tests/e2e/troca-de-organizacao-tem-volta.spec.ts`, que dirige o browser.
|
||||
*/
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi, beforeEach } from "vitest";
|
||||
|
||||
const setActiveOrg = vi.fn(async () => ({ ok: true }));
|
||||
const replace = vi.fn();
|
||||
|
||||
vi.mock("@/app/actions/shell/setActiveOrg", () => ({
|
||||
setActiveOrg: (...args: unknown[]) => setActiveOrg(...(args as [])),
|
||||
}));
|
||||
vi.mock("next/navigation", () => ({ useRouter: () => ({ replace }) }));
|
||||
|
||||
import { OutrasOrganizacoes } from "@/app/onboarding/_components/OutrasOrganizacoes";
|
||||
|
||||
afterEach(cleanup);
|
||||
beforeEach(() => {
|
||||
setActiveOrg.mockClear();
|
||||
replace.mockClear();
|
||||
});
|
||||
|
||||
describe("a saída do wizard", () => {
|
||||
it("não existe para quem só tem esta organização", () => {
|
||||
// Quem instalou o sistema e está configurando a própria empresa não tem
|
||||
// para onde voltar — oferecer o controle prometeria uma ação vazia, que é
|
||||
// o anti-pattern de controle decorativo desta base.
|
||||
render(<OutrasOrganizacoes outras={[]} />);
|
||||
expect(screen.queryByTestId("sair-do-onboarding")).toBeNull();
|
||||
});
|
||||
|
||||
it("com uma organização, o rótulo diz PARA ONDE se vai", () => {
|
||||
// "Trocar de organização" faria a pessoa abrir um menu para descobrir a
|
||||
// única resposta. Quem está preso aqui quer o nome do lugar de onde veio.
|
||||
render(<OutrasOrganizacoes outras={[{ id: "o1", nome: "Clínica Vida" }]} />);
|
||||
expect(screen.getByTestId("sair-do-onboarding")).toHaveTextContent("Voltar para Clínica Vida");
|
||||
});
|
||||
|
||||
it("clicar troca a organização E NAVEGA — as duas coisas", async () => {
|
||||
// ⚠️ A navegação é metade do conserto. `setActiveOrg` revalida `/app`, não
|
||||
// `/onboarding`: sem o `replace`, o cookie muda e o wizard continua na tela,
|
||||
// idêntico. O clique pareceria não ter feito nada.
|
||||
render(<OutrasOrganizacoes outras={[{ id: "o1", nome: "Clínica Vida" }]} />);
|
||||
fireEvent.click(screen.getByTestId("sair-do-onboarding"));
|
||||
|
||||
await waitFor(() => expect(setActiveOrg).toHaveBeenCalledWith("o1"));
|
||||
await waitFor(() => expect(replace).toHaveBeenCalledWith("/app/inbox"));
|
||||
});
|
||||
|
||||
it("com várias, vira menu — e cada organização é um destino", async () => {
|
||||
// ⚠️ `userEvent` e não `fireEvent.click`: o menu do Radix abre no PONTEIRO,
|
||||
// não no clique sintético. Com `fireEvent` o gatilho fica `data-state=
|
||||
// "closed"` e o caso reprovaria por causa da ferramenta, acusando o produto.
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<OutrasOrganizacoes
|
||||
outras={[
|
||||
{ id: "o1", nome: "Clínica Vida" },
|
||||
{ id: "o2", nome: "Studio Norte" },
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
const gatilho = screen.getByTestId("sair-do-onboarding");
|
||||
expect(gatilho).toHaveTextContent("Ir para outra organização");
|
||||
await user.click(gatilho);
|
||||
// O menu do Radix monta ao abrir; os dois destinos têm testid próprio para a
|
||||
// spec em tela poder escolher um deles sem depender da ordem.
|
||||
expect(await screen.findByTestId("sair-do-onboarding-item-o1")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("sair-do-onboarding-item-o2")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user