import { render, screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { MemoryRouter, Routes, Route } from "react-router-dom" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { DomainDiffPage } from "./DomainDiffPage" import { AuthProvider } from "@/auth/AuthContext" import { api } from "@/api/client" import { vi, beforeEach, test, expect } from "vitest" import type { Domain } from "@/api/types" const PROJECT_ID = "p1" const domainWithTemplate: Domain = { id: "d1", providerAccountId: "acc1", zoneName: "example.com.", zoneId: "z1", templateId: "t1", lastCheckStatus: "drift", } function renderPage(qc: QueryClient = new QueryClient()) { return render( } /> , ) } beforeEach(() => { vi.restoreAllMocks() vi.spyOn(api.auth, "me").mockResolvedValue({ user: { id: "u1", email: "a@b.com" }, project: { id: PROJECT_ID, name: "Default" }, }) vi.spyOn(api, "domainHistory").mockResolvedValue([]) vi.spyOn(api, "listDomains").mockResolvedValue([domainWithTemplate]) }) test("apply sends applyPrunes=false by default, true only after opting in", async () => { vi.spyOn(api, "checkDomain").mockResolvedValue({ updates: [{ kind: "update", type: "A", name: "a.", desired: ["1"], actual: ["2"], readOnly: false }], prunes: [{ kind: "delete", type: "A", name: "b.", actual: ["3"], readOnly: false }], readOnly: [], inSyncCount: 0, }) const applySpy = vi.spyOn(api, "applyDomain").mockResolvedValue({ updates: [], prunes: [], readOnly: [], inSyncCount: 0 }) const zoneRecordsSpy = vi.spyOn(api, "zoneRecords") const user = userEvent.setup() renderPage() const applyBtn = await screen.findByRole("button", { name: /apply/i }) await user.click(applyBtn) await waitFor(() => expect(applySpy).toHaveBeenCalled()) expect(applySpy.mock.calls[0]).toEqual([PROJECT_ID, "d1", { applyUpdates: true, applyPrunes: false }]) // включить prune и применить снова const pruneToggle = screen.getByRole("checkbox", { name: /prune|удал/i }) await user.click(pruneToggle) await user.click(screen.getByRole("button", { name: /apply/i })) await waitFor(() => expect(applySpy).toHaveBeenCalledTimes(2)) expect(applySpy.mock.calls[1]).toEqual([PROJECT_ID, "d1", { applyUpdates: true, applyPrunes: true }]) // домен с шаблоном: записи зоны не нужны для диффа — запрос не должен уходить к провайдеру expect(zoneRecordsSpy).not.toHaveBeenCalled() }) test("пока список доменов грузится — показан общий лоадер, а не баннер об отсутствии шаблона", async () => { let resolveListDomains: (domains: Domain[]) => void vi.spyOn(api, "listDomains").mockReturnValue( new Promise((resolve) => { resolveListDomains = resolve }), ) const checkSpy = vi.spyOn(api, "checkDomain").mockResolvedValue({ updates: [], prunes: [], readOnly: [], inSyncCount: 0, }) const zoneRecordsSpy = vi.spyOn(api, "zoneRecords") renderPage() expect(await screen.findByText(/загрузка/i)).toBeInTheDocument() expect(screen.queryByText(/шаблон не привязан/i)).not.toBeInTheDocument() expect(checkSpy).not.toHaveBeenCalled() expect(zoneRecordsSpy).not.toHaveBeenCalled() resolveListDomains!([domainWithTemplate]) expect(await screen.findByRole("button", { name: /apply/i })).toBeInTheDocument() }) test("домен без шаблона показывает записи зоны и не вызывает check", async () => { vi.spyOn(api, "listDomains").mockResolvedValue([ { id: "d1", providerAccountId: "acc1", zoneName: "example.com.", zoneId: "z1", templateId: null }, ]) const checkSpy = vi.spyOn(api, "checkDomain") vi.spyOn(api, "zoneRecords").mockResolvedValue([ { type: "A", name: "example.com.", ttl: 3600, values: ["1.2.3.4"] }, ]) renderPage() expect(await screen.findByText(/шаблон не привязан/i)).toBeInTheDocument() expect(screen.getByRole("button", { name: /создать шаблон из этой зоны/i })).toBeInTheDocument() expect(await screen.findByText("example.com.")).toBeInTheDocument() expect(screen.getByText("1.2.3.4")).toBeInTheDocument() expect(screen.queryByText(/вычисляю дифф/i)).not.toBeInTheDocument() expect(checkSpy).not.toHaveBeenCalled() }) test("ошибка загрузки списка доменов показывает баннер ошибки и не уходит в ветку без шаблона", async () => { vi.spyOn(api, "listDomains").mockRejectedValue(new Error("network down")) const checkSpy = vi.spyOn(api, "checkDomain") const zoneRecordsSpy = vi.spyOn(api, "zoneRecords") // retry:false — иначе react-query переретраит listDomains с экспоненциальной // задержкой и findByText не успевает дождаться финального isError. renderPage(new QueryClient({ defaultOptions: { queries: { retry: false } } })) expect(await screen.findByText(/не удалось загрузить список доменов/i)).toBeInTheDocument() expect(screen.getByText("network down")).toBeInTheDocument() expect(screen.queryByText(/шаблон не привязан/i)).not.toBeInTheDocument() expect(checkSpy).not.toHaveBeenCalled() expect(zoneRecordsSpy).not.toHaveBeenCalled() }) test("создание шаблона из зоны вызывает templateFromZone", async () => { vi.spyOn(api, "listDomains").mockResolvedValue([ { id: "d1", providerAccountId: "acc1", zoneName: "example.com.", zoneId: "z1", templateId: null }, ]) vi.spyOn(api, "zoneRecords").mockResolvedValue([]) const templateFromZoneSpy = vi.spyOn(api, "templateFromZone").mockResolvedValue({ id: "t9", name: "example.com. snapshot", records: [], version: 1, }) const user = userEvent.setup() renderPage() const createBtn = await screen.findByRole("button", { name: /создать шаблон из этой зоны/i }) await user.click(createBtn) await waitFor(() => expect(templateFromZoneSpy).toHaveBeenCalledWith(PROJECT_ID, "d1")) })