Files
dns-autoresolver/web/src/pages/DomainsPage.test.tsx
T
vasyansk 58a57ff4d0 docs: custom records and import-as-sync invariants
Document the two Task 1-9 features in CLAUDE.md invariants and README
features. Also closes two review gaps: a Go test proving importResponse
marshals created/removed as [] (never null) on a no-op import, and a
frontend test-race fix in DomainsPage.test.tsx where the import button
was clicked before its disabled state cleared.
2026-08-19 18:45:52 +07:00

155 lines
5.8 KiB
TypeScript

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 { DomainsPage } from "./DomainsPage"
import { AuthProvider } from "@/auth/AuthContext"
import { api } from "@/api/client"
import { vi, beforeEach, test, expect } from "vitest"
import type { Account, Domain, Template } from "@/api/types"
const PROJECT_ID = "p1"
const accounts: Account[] = [
{ id: "acc1", provider: "selectel", comment: "Main" },
{ id: "acc2", provider: "cloudflare", comment: "Backup" },
]
const templates: Template[] = [
{ id: "t1", name: "Standard", records: [], version: 1 },
{ id: "t2", name: "Minimal", records: [], version: 1 },
]
const domains: Domain[] = [
{ id: "d1", providerAccountId: "acc1", zoneName: "example.com.", zoneId: "z1", templateId: null, lastCheckStatus: "drift" },
{ id: "d2", providerAccountId: "acc2", zoneName: "test.org.", zoneId: "z2", templateId: "t1", lastCheckStatus: "in_sync" },
]
function renderPage() {
const qc = new QueryClient()
return render(
<QueryClientProvider client={qc}>
<AuthProvider>
<MemoryRouter initialEntries={["/domains"]}>
<Routes>
<Route path="/domains" element={<DomainsPage />} />
<Route path="/domains/:id" element={<div>diff page</div>} />
</Routes>
</MemoryRouter>
</AuthProvider>
</QueryClientProvider>,
)
}
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, "listDomains").mockResolvedValue(domains)
vi.spyOn(api, "listAccounts").mockResolvedValue(accounts)
vi.spyOn(api, "listTemplates").mockResolvedValue(templates)
})
test("отрисовывает домены и ссылку на diff-страницу", async () => {
renderPage()
expect(await screen.findByText("example.com.")).toBeInTheDocument()
expect(screen.getByText("test.org.")).toBeInTheDocument()
const links = screen.getAllByRole("link", { name: /diff/i })
expect(links.length).toBe(2)
expect(links[0]).toHaveAttribute("href", "/domains/d1")
expect(links[1]).toHaveAttribute("href", "/domains/d2")
})
test("кнопка импорта вызывает api.importZones с выбранной учёткой", async () => {
const importSpy = vi.spyOn(api, "importZones").mockResolvedValue({ created: [], removed: [] })
const user = userEvent.setup()
renderPage()
await screen.findByText("example.com.")
await user.click(screen.getByRole("combobox", { name: /учётн/i }))
await user.click(await screen.findByRole("option", { name: /cloudflare/i }))
await user.click(screen.getByRole("button", { name: /импортировать зоны/i }))
await waitFor(() => expect(importSpy).toHaveBeenCalledWith(PROJECT_ID, "acc2"))
})
test("после импорта показывает, сколько зон создано и сколько удалено", async () => {
vi.spyOn(api, "importZones").mockResolvedValue({
created: [
{ id: "d1", providerAccountId: "a1", zoneName: "new.example.com.", zoneId: "z1" },
],
removed: [
{ id: "d2", providerAccountId: "a1", zoneName: "gone.example.com.", zoneId: "z9" },
],
})
const user = userEvent.setup()
renderPage()
await screen.findByText("example.com.")
await user.click(await screen.findByRole("button", { name: /импортировать зоны/i }))
expect(await screen.findByText(/создано 1, удалено 1/i)).toBeInTheDocument()
})
test("привязка шаблона в строке домена вызывает api.setDomainTemplate", async () => {
const setTemplateSpy = vi.spyOn(api, "setDomainTemplate").mockResolvedValue(domains[0])
const user = userEvent.setup()
renderPage()
await screen.findByText("example.com.")
await user.click(screen.getByRole("combobox", { name: /example\.com\./i }))
await user.click(await screen.findByRole("option", { name: /^standard$/i }))
await waitFor(() => expect(setTemplateSpy).toHaveBeenCalledWith(PROJECT_ID, "d1", "t1"))
})
test("ошибка привязки шаблона отображается пользователю", async () => {
vi.spyOn(api, "setDomainTemplate").mockRejectedValue(new Error("Не удалось привязать шаблон"))
const user = userEvent.setup()
renderPage()
await screen.findByText("example.com.")
await user.click(screen.getByRole("combobox", { name: /example\.com\./i }))
await user.click(await screen.findByRole("option", { name: /^standard$/i }))
expect(await screen.findByRole("alert")).toHaveTextContent("Не удалось привязать шаблон")
})
test("пустое состояние при отсутствии доменов", async () => {
vi.spyOn(api, "listDomains").mockResolvedValue([])
renderPage()
expect(await screen.findByText(/доменов пока нет/i)).toBeInTheDocument()
})
test("drift-badge отражает lastCheckStatus каждого домена", async () => {
renderPage()
await screen.findByText("example.com.")
expect(screen.getByText("in sync")).toBeInTheDocument()
})
test("домен без templateId показывает «без шаблона» вместо lastCheckStatus", async () => {
renderPage()
await screen.findByText("example.com.")
expect(screen.getByText("без шаблона")).toBeInTheDocument()
expect(screen.queryByText("drift")).not.toBeInTheDocument()
})
test("кнопки удаления домена нет", async () => {
renderPage()
await screen.findByText("example.com.")
expect(screen.queryByRole("button", { name: /удалить/i })).not.toBeInTheDocument()
})