|
| 1 | +import type { CheckRunRow, CheckRunsView } from "@/lib/checks/contract"; |
| 2 | +import { stubIntersectionObserver, stubResizeObserver } from "@/tests/observers"; |
| 3 | +import { render, screen, within } from "@testing-library/react"; |
| 4 | +import { describe, expect, it, vi } from "vitest"; |
| 5 | +import { CheckRunsTable } from "./CheckRunsTable"; |
| 6 | + |
| 7 | +const now = new Date("2026-07-24T14:45:00.000Z"); |
| 8 | + |
| 9 | +function marketRow(overrides: Partial<CheckRunRow> = {}): CheckRunRow { |
| 10 | + return { |
| 11 | + attemptCount: 1, |
| 12 | + attempts: [ |
| 13 | + { |
| 14 | + costCents: 0.35, |
| 15 | + degradedToCountry: false, |
| 16 | + detail: null, |
| 17 | + durationMs: 1_900, |
| 18 | + outcome: "ok", |
| 19 | + provider: "dataforseo", |
| 20 | + providerLabel: "DataForSEO", |
| 21 | + }, |
| 22 | + ], |
| 23 | + checkedAt: "2026-07-24T13:45:00.000Z", |
| 24 | + costCents: 0.35, |
| 25 | + degradedToCountry: false, |
| 26 | + device: "desktop", |
| 27 | + durationMs: 1_900, |
| 28 | + error: null, |
| 29 | + estimatedCostCents: null, |
| 30 | + finishedAt: "2026-07-24T13:45:01.900Z", |
| 31 | + id: "run", |
| 32 | + keyword: "ai meeting notes", |
| 33 | + keywordId: "kw", |
| 34 | + keywordPublicId: "kw", |
| 35 | + languageLabel: "English", |
| 36 | + location: "San Francisco, CA, US", |
| 37 | + position: 4, |
| 38 | + previousPosition: 6, |
| 39 | + provider: "dataforseo", |
| 40 | + providerLabel: "DataForSEO", |
| 41 | + requestedDepth: 20, |
| 42 | + startedAt: "2026-07-24T13:45:00.000Z", |
| 43 | + status: "completed", |
| 44 | + trigger: "scheduled", |
| 45 | + viaFallback: false, |
| 46 | + ...overrides, |
| 47 | + }; |
| 48 | +} |
| 49 | + |
| 50 | +function viewFor(rows: CheckRunRow[]): CheckRunsView { |
| 51 | + return { |
| 52 | + counts: { |
| 53 | + completed: rows.length, |
| 54 | + deferred: 0, |
| 55 | + failed: 0, |
| 56 | + running: 0, |
| 57 | + runs: rows.length, |
| 58 | + viaFallback: 0, |
| 59 | + }, |
| 60 | + deferredGroups: [], |
| 61 | + nextCursor: null, |
| 62 | + providerHealth: [], |
| 63 | + rows, |
| 64 | + spendCents: 0, |
| 65 | + }; |
| 66 | +} |
| 67 | + |
| 68 | +function tableProps(view: CheckRunsView) { |
| 69 | + return { |
| 70 | + expandedRunIds: new Set<string>(), |
| 71 | + filter: "all" as const, |
| 72 | + keywordHref: (id: string) => `/app/rank-tracker/${id}`, |
| 73 | + now, |
| 74 | + onLoadMore: vi.fn(), |
| 75 | + onToggleRun: vi.fn(), |
| 76 | + view, |
| 77 | + }; |
| 78 | +} |
| 79 | + |
| 80 | +describe("CheckRunsTable", () => { |
| 81 | + it("renders distinct Location, Language, and Device columns for rows with the same keyword text", () => { |
| 82 | + stubResizeObserver(); |
| 83 | + stubIntersectionObserver(); |
| 84 | + |
| 85 | + const rows = [ |
| 86 | + marketRow({ |
| 87 | + id: "run_sf_desktop", |
| 88 | + keywordPublicId: "kw_sf_desktop", |
| 89 | + location: "San Francisco, CA, US", |
| 90 | + languageLabel: "English", |
| 91 | + device: "desktop", |
| 92 | + }), |
| 93 | + marketRow({ |
| 94 | + id: "run_lon_mobile", |
| 95 | + keywordPublicId: "kw_lon_mobile", |
| 96 | + location: "London, UK", |
| 97 | + languageLabel: "English", |
| 98 | + device: "mobile", |
| 99 | + }), |
| 100 | + ]; |
| 101 | + |
| 102 | + render(<CheckRunsTable {...tableProps(viewFor(rows))} />); |
| 103 | + |
| 104 | + const table = screen.getByRole("table", { name: "Check runs" }); |
| 105 | + expect(table).toHaveClass("min-w-[900px]"); |
| 106 | + const headerCells = within(table).getAllByRole("columnheader"); |
| 107 | + expect(headerCells.map((cell) => cell.textContent)).toEqual([ |
| 108 | + "Status", |
| 109 | + "Keyword", |
| 110 | + "Location", |
| 111 | + "Language", |
| 112 | + "Device", |
| 113 | + "Result", |
| 114 | + "Provider", |
| 115 | + "Depth", |
| 116 | + "Cost", |
| 117 | + "When", |
| 118 | + "", |
| 119 | + ]); |
| 120 | + |
| 121 | + expect(screen.getByText("San Francisco, CA, US")).toBeInTheDocument(); |
| 122 | + expect(screen.getByText("London, UK")).toBeInTheDocument(); |
| 123 | + expect(screen.getByText("Desktop")).toBeInTheDocument(); |
| 124 | + expect(screen.getByText("Mobile")).toBeInTheDocument(); |
| 125 | + }); |
| 126 | + |
| 127 | + it("renders a dash for a missing language label", () => { |
| 128 | + stubResizeObserver(); |
| 129 | + stubIntersectionObserver(); |
| 130 | + |
| 131 | + const rows = [ |
| 132 | + marketRow({ |
| 133 | + id: "run_no_lang", |
| 134 | + keywordPublicId: "kw_no_lang", |
| 135 | + languageLabel: null, |
| 136 | + }), |
| 137 | + ]; |
| 138 | + |
| 139 | + render(<CheckRunsTable {...tableProps(viewFor(rows))} />); |
| 140 | + |
| 141 | + const table = screen.getByRole("table", { name: "Check runs" }); |
| 142 | + const bodyRows = within(table).getAllByRole("row").slice(1); |
| 143 | + expect(bodyRows).toHaveLength(1); |
| 144 | + expect(within(bodyRows[0]).getByText("-")).toBeInTheDocument(); |
| 145 | + }); |
| 146 | + |
| 147 | + it("renders two rows with identical keyword text but different markets as visually distinguishable", () => { |
| 148 | + stubResizeObserver(); |
| 149 | + stubIntersectionObserver(); |
| 150 | + |
| 151 | + const rows = [ |
| 152 | + marketRow({ |
| 153 | + id: "run_sf", |
| 154 | + keywordPublicId: "kw_sf", |
| 155 | + location: "San Francisco, CA, US", |
| 156 | + device: "desktop", |
| 157 | + }), |
| 158 | + marketRow({ |
| 159 | + id: "run_lon", |
| 160 | + keywordPublicId: "kw_lon", |
| 161 | + location: "London, UK", |
| 162 | + device: "mobile", |
| 163 | + }), |
| 164 | + ]; |
| 165 | + |
| 166 | + render(<CheckRunsTable {...tableProps(viewFor(rows))} />); |
| 167 | + |
| 168 | + const table = screen.getByRole("table", { name: "Check runs" }); |
| 169 | + const bodyRows = within(table).getAllByRole("row").slice(1); |
| 170 | + expect(bodyRows).toHaveLength(2); |
| 171 | + |
| 172 | + const firstMarket = within(bodyRows[0]).getByText("San Francisco, CA, US"); |
| 173 | + const secondMarket = within(bodyRows[1]).getByText("London, UK"); |
| 174 | + expect(firstMarket).not.toEqual(secondMarket); |
| 175 | + |
| 176 | + const keywordLinks = within(table).getAllByRole("link", { name: "ai meeting notes" }); |
| 177 | + expect(keywordLinks).toHaveLength(2); |
| 178 | + }); |
| 179 | +}); |
0 commit comments