@prenta/admin 1.14.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/__tests__/lib/api.test.d.ts +2 -0
  3. package/dist/__tests__/lib/api.test.d.ts.map +1 -0
  4. package/dist/__tests__/lib/api.test.js +58 -0
  5. package/dist/__tests__/lib/api.test.js.map +1 -0
  6. package/dist/__tests__/lib/seo-service-autopilot.test.d.ts +2 -0
  7. package/dist/__tests__/lib/seo-service-autopilot.test.d.ts.map +1 -0
  8. package/dist/__tests__/lib/seo-service-autopilot.test.js +171 -0
  9. package/dist/__tests__/lib/seo-service-autopilot.test.js.map +1 -0
  10. package/dist/__tests__/lib/seo-service-geo.test.d.ts +2 -0
  11. package/dist/__tests__/lib/seo-service-geo.test.d.ts.map +1 -0
  12. package/dist/__tests__/lib/seo-service-geo.test.js +103 -0
  13. package/dist/__tests__/lib/seo-service-geo.test.js.map +1 -0
  14. package/dist/__tests__/lib/seo-service-proposals.test.js +28 -0
  15. package/dist/__tests__/lib/seo-service-proposals.test.js.map +1 -1
  16. package/dist/__tests__/router/seo-tab-for-path.test.js +4 -0
  17. package/dist/__tests__/router/seo-tab-for-path.test.js.map +1 -1
  18. package/dist/__tests__/views/redirects-pane.render.test.js +30 -12
  19. package/dist/__tests__/views/redirects-pane.render.test.js.map +1 -1
  20. package/dist/__tests__/views/seo-ai-visibility.render.test.d.ts +2 -0
  21. package/dist/__tests__/views/seo-ai-visibility.render.test.d.ts.map +1 -0
  22. package/dist/__tests__/views/seo-ai-visibility.render.test.js +244 -0
  23. package/dist/__tests__/views/seo-ai-visibility.render.test.js.map +1 -0
  24. package/dist/__tests__/views/seo-autopilot-applied.render.test.d.ts +2 -0
  25. package/dist/__tests__/views/seo-autopilot-applied.render.test.d.ts.map +1 -0
  26. package/dist/__tests__/views/seo-autopilot-applied.render.test.js +330 -0
  27. package/dist/__tests__/views/seo-autopilot-applied.render.test.js.map +1 -0
  28. package/dist/__tests__/views/seo-autopilot-card.render.test.d.ts +2 -0
  29. package/dist/__tests__/views/seo-autopilot-card.render.test.d.ts.map +1 -0
  30. package/dist/__tests__/views/seo-autopilot-card.render.test.js +512 -0
  31. package/dist/__tests__/views/seo-autopilot-card.render.test.js.map +1 -0
  32. package/dist/__tests__/views/seo-geo-settings-card.render.test.d.ts +2 -0
  33. package/dist/__tests__/views/seo-geo-settings-card.render.test.d.ts.map +1 -0
  34. package/dist/__tests__/views/seo-geo-settings-card.render.test.js +227 -0
  35. package/dist/__tests__/views/seo-geo-settings-card.render.test.js.map +1 -0
  36. package/dist/__tests__/views/seo-proposals-tab.render.test.js +139 -0
  37. package/dist/__tests__/views/seo-proposals-tab.render.test.js.map +1 -1
  38. package/dist/__tests__/views/seo-settings.render.test.js +53 -4
  39. package/dist/__tests__/views/seo-settings.render.test.js.map +1 -1
  40. package/dist/__tests__/views/seo-technical-tab.render.test.js +13 -0
  41. package/dist/__tests__/views/seo-technical-tab.render.test.js.map +1 -1
  42. package/dist/lib/api.d.ts +13 -2
  43. package/dist/lib/api.d.ts.map +1 -1
  44. package/dist/lib/api.js +1 -2
  45. package/dist/lib/api.js.map +1 -1
  46. package/dist/lib/seo-service.d.ts +300 -2
  47. package/dist/lib/seo-service.d.ts.map +1 -1
  48. package/dist/lib/seo-service.js +172 -5
  49. package/dist/lib/seo-service.js.map +1 -1
  50. package/dist/prenta-admin.css +1 -1
  51. package/dist/views/SEO.d.ts +5 -1
  52. package/dist/views/SEO.d.ts.map +1 -1
  53. package/dist/views/SEO.js +12 -7
  54. package/dist/views/SEO.js.map +1 -1
  55. package/dist/views/seo/AiVisibilityTab.d.ts +4 -0
  56. package/dist/views/seo/AiVisibilityTab.d.ts.map +1 -0
  57. package/dist/views/seo/AiVisibilityTab.js +165 -0
  58. package/dist/views/seo/AiVisibilityTab.js.map +1 -0
  59. package/dist/views/seo/AutopilotAppliedSection.d.ts +10 -0
  60. package/dist/views/seo/AutopilotAppliedSection.d.ts.map +1 -0
  61. package/dist/views/seo/AutopilotAppliedSection.js +295 -0
  62. package/dist/views/seo/AutopilotAppliedSection.js.map +1 -0
  63. package/dist/views/seo/ProposalsTab.d.ts.map +1 -1
  64. package/dist/views/seo/ProposalsTab.js +53 -17
  65. package/dist/views/seo/ProposalsTab.js.map +1 -1
  66. package/dist/views/seo/RedirectsTab.d.ts +3 -1
  67. package/dist/views/seo/RedirectsTab.d.ts.map +1 -1
  68. package/dist/views/seo/RedirectsTab.js +14 -19
  69. package/dist/views/seo/RedirectsTab.js.map +1 -1
  70. package/dist/views/seo/TechnicalTab.d.ts +3 -1
  71. package/dist/views/seo/TechnicalTab.d.ts.map +1 -1
  72. package/dist/views/seo/TechnicalTab.js +4 -4
  73. package/dist/views/seo/TechnicalTab.js.map +1 -1
  74. package/dist/views/seo/relative-age.d.ts +7 -0
  75. package/dist/views/seo/relative-age.d.ts.map +1 -0
  76. package/dist/views/seo/relative-age.js +20 -0
  77. package/dist/views/seo/relative-age.js.map +1 -0
  78. package/dist/views/settings/SeoAutopilotCard.d.ts +30 -0
  79. package/dist/views/settings/SeoAutopilotCard.d.ts.map +1 -0
  80. package/dist/views/settings/SeoAutopilotCard.js +375 -0
  81. package/dist/views/settings/SeoAutopilotCard.js.map +1 -0
  82. package/dist/views/settings/SeoGeoSettingsCard.d.ts +12 -0
  83. package/dist/views/settings/SeoGeoSettingsCard.d.ts.map +1 -0
  84. package/dist/views/settings/SeoGeoSettingsCard.js +178 -0
  85. package/dist/views/settings/SeoGeoSettingsCard.js.map +1 -0
  86. package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
  87. package/dist/views/settings/SeoSettingsTab.js +3 -1
  88. package/dist/views/settings/SeoSettingsTab.js.map +1 -1
  89. package/dist/views/settings/components.d.ts +3 -1
  90. package/dist/views/settings/components.d.ts.map +1 -1
  91. package/dist/views/settings/components.js +2 -2
  92. package/dist/views/settings/components.js.map +1 -1
  93. package/package.json +3 -3
  94. package/src/__tests__/lib/api.test.ts +76 -0
  95. package/src/__tests__/lib/seo-service-autopilot.test.ts +196 -0
  96. package/src/__tests__/lib/seo-service-geo.test.ts +118 -0
  97. package/src/__tests__/lib/seo-service-proposals.test.ts +29 -0
  98. package/src/__tests__/router/seo-tab-for-path.test.ts +5 -0
  99. package/src/__tests__/views/redirects-pane.render.test.tsx +36 -16
  100. package/src/__tests__/views/seo-ai-visibility.render.test.tsx +279 -0
  101. package/src/__tests__/views/seo-autopilot-applied.render.test.tsx +382 -0
  102. package/src/__tests__/views/seo-autopilot-card.render.test.tsx +610 -0
  103. package/src/__tests__/views/seo-geo-settings-card.render.test.tsx +253 -0
  104. package/src/__tests__/views/seo-proposals-tab.render.test.tsx +161 -0
  105. package/src/__tests__/views/seo-settings.render.test.tsx +68 -7
  106. package/src/__tests__/views/seo-technical-tab.render.test.tsx +15 -0
  107. package/src/lib/api.ts +19 -3
  108. package/src/lib/seo-service.ts +439 -6
  109. package/src/views/SEO.tsx +16 -7
  110. package/src/views/seo/AiVisibilityTab.tsx +616 -0
  111. package/src/views/seo/AutopilotAppliedSection.tsx +597 -0
  112. package/src/views/seo/ProposalsTab.tsx +92 -12
  113. package/src/views/seo/RedirectsTab.tsx +39 -60
  114. package/src/views/seo/TechnicalTab.tsx +27 -8
  115. package/src/views/seo/relative-age.ts +15 -0
  116. package/src/views/settings/SeoAutopilotCard.tsx +826 -0
  117. package/src/views/settings/SeoGeoSettingsCard.tsx +310 -0
  118. package/src/views/settings/SeoSettingsTab.tsx +4 -0
  119. package/src/views/settings/components.tsx +10 -1
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=seo-ai-visibility.render.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-ai-visibility.render.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/views/seo-ai-visibility.render.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,244 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
4
+ function report(over = {}) {
5
+ return {
6
+ enabled: true,
7
+ window: { from: '2026-08-14', to: '2026-09-10', days: 28 },
8
+ firstSeenAt: '2026-08-01',
9
+ crawl: {
10
+ // Whole-window total from the server — includes the capped "other"
11
+ // bucket, so it is deliberately larger than the sum of `bots[].hits`.
12
+ total: { hits: 21, prevHits: 12 },
13
+ series: [
14
+ { date: '2026-09-09', hits: { GPTBot: 3, Googlebot: 5 } },
15
+ { date: '2026-09-10', hits: { GPTBot: 1 } },
16
+ ],
17
+ bots: [
18
+ {
19
+ bot: 'Googlebot',
20
+ category: 'search',
21
+ hits: 5,
22
+ prevHits: 4,
23
+ lastSeenAt: '2026-09-09',
24
+ policy: 'allowed',
25
+ },
26
+ {
27
+ bot: 'GPTBot',
28
+ category: 'training',
29
+ hits: 4,
30
+ prevHits: 8,
31
+ lastSeenAt: '2026-09-10',
32
+ policy: 'blocked_still_crawling',
33
+ },
34
+ {
35
+ bot: 'ClaudeBot',
36
+ category: 'training',
37
+ hits: 0,
38
+ prevHits: null,
39
+ lastSeenAt: null,
40
+ policy: 'blocked',
41
+ },
42
+ ],
43
+ topPaths: [
44
+ {
45
+ path: '/pricing',
46
+ hits: 6,
47
+ bots: ['GPTBot', 'Googlebot'],
48
+ entityId: 'doc-1',
49
+ entityTitle: 'Pricing',
50
+ collection: 'pages',
51
+ },
52
+ {
53
+ path: '/blog/launch',
54
+ hits: 4,
55
+ bots: ['GPTBot'],
56
+ entityId: 'doc-2',
57
+ entityTitle: 'Launch post',
58
+ collection: 'blog',
59
+ },
60
+ { path: '/legacy', hits: 3, bots: ['Googlebot'] },
61
+ ],
62
+ otherHits: 12,
63
+ },
64
+ referrals: {
65
+ total: { visits: 9, prevVisits: 3 },
66
+ engines: [
67
+ { engine: 'ChatGPT', visits: 7, prevVisits: 3 },
68
+ { engine: 'Perplexity', visits: 2, prevVisits: null },
69
+ ],
70
+ topPaths: [
71
+ {
72
+ path: '/pricing',
73
+ visits: 8,
74
+ engines: ['ChatGPT', 'Perplexity'],
75
+ entityId: 'doc-1',
76
+ entityTitle: 'Pricing',
77
+ collection: 'pages',
78
+ },
79
+ { path: '/blog/x', visits: 1, engines: ['ChatGPT'] },
80
+ ],
81
+ otherVisits: 4,
82
+ },
83
+ ...over,
84
+ };
85
+ }
86
+ const EMPTY_WINDOW = {
87
+ crawl: { total: { hits: 0, prevHits: null }, series: [], bots: [], topPaths: [], otherHits: 0 },
88
+ referrals: { total: { visits: 0, prevVisits: null }, engines: [], topPaths: [], otherVisits: 0 },
89
+ };
90
+ const fetchSeoGeo = vi.fn(async (_days) => report());
91
+ vi.mock('../../lib/seo-service.js', () => ({ fetchSeoGeo: (d) => fetchSeoGeo(d) }));
92
+ // recharts needs layout; stub the chart so the table assertions drive the test.
93
+ vi.mock('recharts', () => ({
94
+ ResponsiveContainer: ({ children }) => (_jsx("div", { "data-testid": "chart", children: children })),
95
+ BarChart: ({ children }) => _jsx("div", { children: children }),
96
+ Bar: () => null,
97
+ XAxis: () => null,
98
+ YAxis: () => null,
99
+ Tooltip: () => null,
100
+ Legend: () => null,
101
+ CartesianGrid: () => null,
102
+ }));
103
+ const { AiVisibilityTab } = await import('../../views/seo/AiVisibilityTab.js');
104
+ beforeEach(() => {
105
+ fetchSeoGeo.mockReset();
106
+ fetchSeoGeo.mockImplementation(async () => report());
107
+ });
108
+ describe('AiVisibilityTab', () => {
109
+ it('renders bots with policy text, deltas, and top paths with editor links gated on entityId', async () => {
110
+ const onNavigate = vi.fn();
111
+ render(_jsx(AiVisibilityTab, { onNavigate: onNavigate }));
112
+ await screen.findByRole('table', { name: /crawlers/i });
113
+ expect(fetchSeoGeo).toHaveBeenCalledWith(28);
114
+ const crawlers = screen.getByRole('table', { name: /crawlers/i });
115
+ expect(within(crawlers).getByText('Blocked — still crawling')).toBeTruthy();
116
+ expect(within(crawlers).getByText('Allowed')).toBeTruthy();
117
+ expect(within(crawlers).getByText('Blocked')).toBeTruthy();
118
+ expect(within(crawlers).getAllByText('AI training', { selector: 'td' })).toHaveLength(2);
119
+ expect(within(crawlers).getByText('Search', { selector: 'td' })).toBeTruthy();
120
+ // null prevHits (ClaudeBot) and null lastSeenAt both render as an em dash.
121
+ expect(within(crawlers).getAllByText('—').length).toBeGreaterThanOrEqual(2);
122
+ // +25% (5 vs 4) and -50% (4 vs 8).
123
+ expect(within(crawlers).getByText('+25%')).toBeTruthy();
124
+ expect(within(crawlers).getByText('-50%')).toBeTruthy();
125
+ // Headline crawl number comes from `crawl.total`, not from summing bots.
126
+ const crawlSummary = screen.getByRole('group', { name: /crawl summary/i });
127
+ expect(within(crawlSummary).getByText('21')).toBeTruthy();
128
+ expect(within(crawlSummary).getByText('+75%')).toBeTruthy();
129
+ expect(screen.getByTestId('chart')).toBeTruthy();
130
+ const cited = screen.getByRole('table', { name: /top cited pages/i });
131
+ fireEvent.click(within(cited).getByRole('button', { name: /open pricing in editor/i }));
132
+ expect(onNavigate).toHaveBeenCalledWith('/pages/doc-1/edit');
133
+ expect(within(cited).queryByRole('button', { name: /open \/blog\/x/i })).toBeNull();
134
+ expect(within(cited).getByText('/blog/x')).toBeTruthy();
135
+ // Post-type collections route through the post editor, not the pages one.
136
+ const crawled = screen.getByRole('table', { name: /top crawled pages/i });
137
+ fireEvent.click(within(crawled).getByRole('button', { name: /open launch post in editor/i }));
138
+ expect(onNavigate).toHaveBeenLastCalledWith('/posts/blog/doc-2/edit');
139
+ expect(within(crawled).queryByRole('button', { name: /open \/legacy/i })).toBeNull();
140
+ expect(screen.getByText(/12 hits on paths beyond the daily cap/i)).toBeTruthy();
141
+ expect(screen.getByText(/4 visits on paths beyond the daily cap/i)).toBeTruthy();
142
+ expect(screen.getByText(/self-reported by your middleware/i)).toBeTruthy();
143
+ });
144
+ it('renders paths as plain text when there is no onNavigate, even with an entityId', async () => {
145
+ render(_jsx(AiVisibilityTab, {}));
146
+ const cited = await screen.findByRole('table', { name: /top cited pages/i });
147
+ expect(within(cited).queryByRole('button')).toBeNull();
148
+ expect(within(cited).getByText('/pricing')).toBeTruthy();
149
+ });
150
+ it('switching the window refetches', async () => {
151
+ render(_jsx(AiVisibilityTab, {}));
152
+ await screen.findByRole('table', { name: /crawlers/i });
153
+ const group = screen.getByRole('group', { name: /^window$/i });
154
+ const seven = within(group).getByRole('button', { name: /7 days/i });
155
+ expect(seven.getAttribute('aria-pressed')).toBe('false');
156
+ expect(within(group)
157
+ .getByRole('button', { name: /28 days/i })
158
+ .getAttribute('aria-pressed')).toBe('true');
159
+ fireEvent.click(seven);
160
+ await waitFor(() => expect(fetchSeoGeo).toHaveBeenLastCalledWith(7));
161
+ expect(seven.getAttribute('aria-pressed')).toBe('true');
162
+ });
163
+ it('keeps the clicked window button focused and enabled while the refetch is in flight', async () => {
164
+ render(_jsx(AiVisibilityTab, {}));
165
+ await screen.findByRole('table', { name: /crawlers/i });
166
+ // Hold the second request open so we can inspect the loading state.
167
+ let release;
168
+ fetchSeoGeo.mockImplementationOnce(() => new Promise((resolve) => {
169
+ release = () => resolve(report());
170
+ }));
171
+ const seven = screen.getByRole('button', { name: /7 days/i });
172
+ seven.focus();
173
+ fireEvent.click(seven);
174
+ await waitFor(() => expect(fetchSeoGeo).toHaveBeenLastCalledWith(7));
175
+ expect(seven.disabled).toBe(false);
176
+ expect(document.activeElement).toBe(seven);
177
+ release?.();
178
+ await screen.findByRole('table', { name: /crawlers/i });
179
+ });
180
+ it('exposes the null-baseline dash to assistive tech, not only via title', async () => {
181
+ render(_jsx(AiVisibilityTab, {}));
182
+ const crawlers = await screen.findByRole('table', { name: /crawlers/i });
183
+ expect(within(crawlers).getAllByText('No full prior window yet').length).toBeGreaterThan(0);
184
+ expect(screen.getByText(/first seen/i)).toBeTruthy();
185
+ expect(screen.queryByText(/collecting since/i)).toBeNull();
186
+ });
187
+ it('shows the disabled state with a settings button (disabledReason: settings)', async () => {
188
+ fetchSeoGeo.mockResolvedValue(report({ enabled: false, disabledReason: 'settings', firstSeenAt: null, ...EMPTY_WINDOW }));
189
+ const onNavigate = vi.fn();
190
+ render(_jsx(AiVisibilityTab, { onNavigate: onNavigate }));
191
+ await screen.findByText(/measurement is off/i);
192
+ // The header (window switch + Refresh) stays available in every state.
193
+ expect(screen.getByRole('button', { name: /refresh/i })).toBeTruthy();
194
+ fireEvent.click(screen.getByRole('button', { name: /open seo settings/i }));
195
+ expect(onNavigate).toHaveBeenCalledWith('/settings?tab=seo');
196
+ });
197
+ it('falls back to the settings copy when enabled:false carries no disabledReason (older core)', async () => {
198
+ fetchSeoGeo.mockResolvedValue(report({ enabled: false, firstSeenAt: null, ...EMPTY_WINDOW }));
199
+ render(_jsx(AiVisibilityTab, { onNavigate: vi.fn() }));
200
+ await screen.findByText(/measurement is off/i);
201
+ expect(screen.getByRole('button', { name: /open seo settings/i })).toBeTruthy();
202
+ });
203
+ it('shows the not-migrated state without a settings action (disabledReason: unavailable)', async () => {
204
+ fetchSeoGeo.mockResolvedValue(report({ enabled: false, disabledReason: 'unavailable', firstSeenAt: null, ...EMPTY_WINDOW }));
205
+ render(_jsx(AiVisibilityTab, { onNavigate: vi.fn() }));
206
+ await screen.findByText(/geo tables not migrated/i);
207
+ expect(screen.getByText(/prisma migrate deploy/i)).toBeTruthy();
208
+ expect(screen.getByText(/0018_geo_daily/i)).toBeTruthy();
209
+ expect(screen.queryByText(/measurement is off/i)).toBeNull();
210
+ expect(screen.queryByRole('button', { name: /open seo settings/i })).toBeNull();
211
+ expect(screen.getByRole('button', { name: /refresh/i })).toBeTruthy();
212
+ });
213
+ it('shows the not-reporting state with a middleware-snippet action when nothing has ever arrived', async () => {
214
+ fetchSeoGeo.mockResolvedValue(report({ firstSeenAt: null, ...EMPTY_WINDOW }));
215
+ const onNavigate = vi.fn();
216
+ render(_jsx(AiVisibilityTab, { onNavigate: onNavigate }));
217
+ await screen.findByText(/site middleware is not reporting yet/i);
218
+ expect(screen.queryByRole('table')).toBeNull();
219
+ fireEvent.click(screen.getByRole('button', { name: /show middleware snippet/i }));
220
+ expect(onNavigate).toHaveBeenCalledWith('/settings?tab=seo');
221
+ });
222
+ it('omits the middleware-snippet action when there is no navigator', async () => {
223
+ fetchSeoGeo.mockResolvedValue(report({ firstSeenAt: null, ...EMPTY_WINDOW }));
224
+ render(_jsx(AiVisibilityTab, {}));
225
+ await screen.findByText(/site middleware is not reporting yet/i);
226
+ expect(screen.queryByRole('button', { name: /show middleware snippet/i })).toBeNull();
227
+ });
228
+ it('shows an empty state when reporting but the window is empty', async () => {
229
+ fetchSeoGeo.mockResolvedValue(report(EMPTY_WINDOW));
230
+ render(_jsx(AiVisibilityTab, {}));
231
+ await screen.findByText(/no ai crawler or referral activity in this window/i);
232
+ expect(screen.queryByRole('table')).toBeNull();
233
+ });
234
+ it('shows an error state with retry', async () => {
235
+ fetchSeoGeo.mockRejectedValueOnce(new Error('boom'));
236
+ render(_jsx(AiVisibilityTab, {}));
237
+ const alert = await screen.findByRole('alert');
238
+ expect(within(alert).getByText('boom')).toBeTruthy();
239
+ fireEvent.click(screen.getByRole('button', { name: /retry/i }));
240
+ await screen.findByRole('table', { name: /crawlers/i });
241
+ expect(fetchSeoGeo).toHaveBeenCalledTimes(2);
242
+ });
243
+ });
244
+ //# sourceMappingURL=seo-ai-visibility.render.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-ai-visibility.render.test.js","sourceRoot":"","sources":["../../../src/__tests__/views/seo-ai-visibility.render.test.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=seo-autopilot-applied.render.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seo-autopilot-applied.render.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/views/seo-autopilot-applied.render.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,330 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // @vitest-environment happy-dom
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
5
+ /**
6
+ * SEO → Proposals "Applied by autopilot" ledger (spec §4 / §6). Pins: rows
7
+ * render entity → change → gate → outcome, Revert is admin-only and confirmed,
8
+ * a 409 conflict re-prompts with the field list and a forced retry, and one
9
+ * mutation is allowed in flight at a time.
10
+ */
11
+ function row(over = {}) {
12
+ return {
13
+ id: 'la-1',
14
+ kind: 'issue-fix',
15
+ issueType: 'missing-meta-title',
16
+ entityId: 'doc-1',
17
+ collection: 'pages',
18
+ entityTitle: 'Pricing',
19
+ entityUrl: '/pricing',
20
+ path: '/pricing',
21
+ before: { metaTitle: null },
22
+ after: { metaTitle: 'Pricing for growing teams' },
23
+ gate: { source: 'ai', brandAlignment: 92, confidence: null, checks: { brand: true } },
24
+ status: 'applied',
25
+ outcome: 'held',
26
+ outcomeMetrics: { priorClicks: 120, afterClicks: 130 },
27
+ appliedAt: new Date(Date.now() - 2 * 3600_000).toISOString(),
28
+ revertedAt: null,
29
+ revertReason: null,
30
+ ...over,
31
+ };
32
+ }
33
+ function list(over = {}) {
34
+ return { docs: [row()], total: 1, page: 1, pageSize: 20, canRevert: true, ...over };
35
+ }
36
+ const fetchAutopilotApplied = vi.fn(async (_params) => list());
37
+ const revertAutopilotChange = vi.fn(async (_id, _force) => ({ ok: true }));
38
+ vi.mock('../../lib/seo-service.js', () => ({
39
+ fetchAutopilotApplied: (params) => fetchAutopilotApplied(params),
40
+ revertAutopilotChange: (id, force) => revertAutopilotChange(id, force),
41
+ }));
42
+ vi.mock('sonner', () => ({
43
+ toast: { success: vi.fn(), error: vi.fn(), info: vi.fn(), loading: vi.fn() },
44
+ }));
45
+ const { AutopilotAppliedSection } = await import('../../views/seo/AutopilotAppliedSection.js');
46
+ const { toast } = await import('sonner');
47
+ function deferred() {
48
+ let resolve;
49
+ const promise = new Promise((res) => {
50
+ resolve = res;
51
+ });
52
+ return { promise, resolve };
53
+ }
54
+ async function flushMicrotasks() {
55
+ await new Promise((r) => setTimeout(r, 0));
56
+ }
57
+ beforeEach(() => {
58
+ fetchAutopilotApplied.mockClear();
59
+ fetchAutopilotApplied.mockImplementation(async () => list());
60
+ revertAutopilotChange.mockClear();
61
+ revertAutopilotChange.mockImplementation(async () => ({ ok: true }));
62
+ vi.mocked(toast.success).mockClear();
63
+ vi.mocked(toast.error).mockClear();
64
+ });
65
+ describe('AutopilotAppliedSection', () => {
66
+ it('lists applied rows with before → after, brand score, kind, status, and outcome badges', async () => {
67
+ render(_jsx(AutopilotAppliedSection, {}));
68
+ const table = await screen.findByRole('table', { name: 'Changes applied by autopilot' });
69
+ expect(within(table).getByText('Pricing')).toBeTruthy();
70
+ expect(within(table).getByText('Missing meta title')).toBeTruthy();
71
+ expect(within(table).getByText('Issue fix')).toBeTruthy();
72
+ expect(within(table).getByText('Applied')).toBeTruthy();
73
+ expect(within(table).getByText('Pricing for growing teams')).toBeTruthy();
74
+ expect(within(table).getByLabelText('Brand voice score 92 out of 100')).toBeTruthy();
75
+ expect(within(table).getByText('Held')).toBeTruthy();
76
+ });
77
+ it('renders the applied time as a <time> with an absolute timestamp for assistive tech', async () => {
78
+ const appliedAt = new Date(Date.now() - 2 * 3600_000).toISOString();
79
+ fetchAutopilotApplied.mockImplementationOnce(async () => list({ docs: [row({ appliedAt })] }));
80
+ render(_jsx(AutopilotAppliedSection, {}));
81
+ await screen.findByRole('table', { name: 'Changes applied by autopilot' });
82
+ const time = document.querySelector('time');
83
+ expect(time).toBeTruthy();
84
+ expect(time?.getAttribute('dateTime')).toBe(appliedAt);
85
+ expect(time?.textContent).toContain('2h ago');
86
+ expect(time?.textContent).toContain(new Date(appliedAt).toLocaleString());
87
+ });
88
+ it('shows a redirect row as source → destination and hides pending / skipped outcomes', async () => {
89
+ fetchAutopilotApplied.mockImplementationOnce(async () => list({
90
+ docs: [
91
+ row({
92
+ id: 'la-r',
93
+ kind: 'redirect',
94
+ issueType: 'redirect',
95
+ entityId: null,
96
+ collection: null,
97
+ entityTitle: null,
98
+ entityUrl: '/old-page',
99
+ path: null,
100
+ before: null,
101
+ after: { source: '/old-page', destination: '/new-page', suggestionId: 's1' },
102
+ gate: { source: 'ai-404-recovery', brandAlignment: null, confidence: 0.91, checks: {} },
103
+ outcome: 'skipped',
104
+ }),
105
+ ],
106
+ }));
107
+ render(_jsx(AutopilotAppliedSection, {}));
108
+ const table = await screen.findByRole('table', { name: 'Changes applied by autopilot' });
109
+ expect(within(table).getByText('Redirect')).toBeTruthy();
110
+ expect(within(table).getByText('404 recovery')).toBeTruthy();
111
+ // Title and the source side of the diff both read the redirect's source path.
112
+ expect(within(table).getAllByText('/old-page')).toHaveLength(2);
113
+ expect(within(table).getByText('/new-page')).toBeTruthy();
114
+ expect(within(table).getByText('91%')).toBeTruthy();
115
+ expect(within(table).queryByText('Not watched')).toBeNull();
116
+ expect(within(table).queryByText('Watching')).toBeNull();
117
+ // Redirects have no document behind them, so the title is never an editor link.
118
+ expect(within(table).queryByRole('button', { name: /in editor/ })).toBeNull();
119
+ });
120
+ it('opens the document editor through onNavigate from the entity title', async () => {
121
+ const onNavigate = vi.fn();
122
+ render(_jsx(AutopilotAppliedSection, { onNavigate: onNavigate }));
123
+ fireEvent.click(await screen.findByRole('button', { name: 'Open Pricing in editor' }));
124
+ expect(onNavigate).toHaveBeenCalledWith('/pages/doc-1/edit');
125
+ expect(screen.queryByRole('link')).toBeNull();
126
+ });
127
+ it('renders the entity title as plain text when there is no navigator', async () => {
128
+ render(_jsx(AutopilotAppliedSection, {}));
129
+ const table = await screen.findByRole('table', { name: 'Changes applied by autopilot' });
130
+ expect(within(table).queryByRole('button', { name: /in editor/ })).toBeNull();
131
+ const title = within(table).getByText('Pricing');
132
+ expect(title.tagName).toBe('SPAN');
133
+ expect(title.closest('button')).toBeNull();
134
+ });
135
+ it('hides Revert for non-admins and for rows that are no longer applied', async () => {
136
+ fetchAutopilotApplied.mockImplementationOnce(async () => list({
137
+ canRevert: true,
138
+ docs: [
139
+ row(),
140
+ row({ id: 'la-2', entityTitle: 'About', status: 'reverted', outcome: 'skipped' }),
141
+ row({ id: 'la-3', entityTitle: 'Blog', status: 'verify_failed', outcome: 'skipped' }),
142
+ ],
143
+ total: 3,
144
+ }));
145
+ render(_jsx(AutopilotAppliedSection, {}));
146
+ const table = await screen.findByRole('table', { name: 'Changes applied by autopilot' });
147
+ expect(screen.getByRole('button', { name: 'Revert change to Pricing' })).toBeTruthy();
148
+ expect(screen.queryByRole('button', { name: 'Revert change to About' })).toBeNull();
149
+ expect(screen.queryByRole('button', { name: 'Revert change to Blog' })).toBeNull();
150
+ expect(within(table).getByText('Reverted')).toBeTruthy();
151
+ expect(within(table).getByText('Verify failed')).toBeTruthy();
152
+ });
153
+ it('hides Revert entirely when the caller cannot revert', async () => {
154
+ fetchAutopilotApplied.mockImplementationOnce(async () => list({ canRevert: false }));
155
+ render(_jsx(AutopilotAppliedSection, {}));
156
+ await screen.findByRole('table', { name: 'Changes applied by autopilot' });
157
+ expect(screen.queryByRole('button', { name: /revert/i })).toBeNull();
158
+ });
159
+ it('Revert asks for confirmation, posts, toasts, announces, refetches, and notifies the parent', async () => {
160
+ const onChanged = vi.fn();
161
+ render(_jsx(AutopilotAppliedSection, { onChanged: onChanged }));
162
+ await screen.findByRole('table', { name: 'Changes applied by autopilot' });
163
+ // The live region exists before anything happens so AT picks up the later text change.
164
+ const status = screen.getByRole('status');
165
+ expect(status.getAttribute('aria-live')).toBe('polite');
166
+ expect(status.textContent).toBe('');
167
+ fireEvent.click(screen.getByRole('button', { name: 'Revert change to Pricing' }));
168
+ expect(revertAutopilotChange).not.toHaveBeenCalled();
169
+ fireEvent.click(await screen.findByRole('button', { name: 'Revert change' }));
170
+ await waitFor(() => expect(revertAutopilotChange).toHaveBeenCalledWith('la-1', false));
171
+ await waitFor(() => expect(toast.success).toHaveBeenCalledWith('Change reverted.'));
172
+ await waitFor(() => expect(screen.getByRole('status').textContent).toContain('Reverted the change to “Pricing”'));
173
+ // Same element, text toggled — not a freshly mounted live region.
174
+ expect(screen.getByRole('status')).toBe(status);
175
+ await waitFor(() => expect(fetchAutopilotApplied).toHaveBeenCalledTimes(2));
176
+ // The table stays on screen during the silent refetch — no loading flash.
177
+ expect(screen.getByRole('table', { name: 'Changes applied by autopilot' })).toBeTruthy();
178
+ // The server reopened the issue; the parent must refetch its inbox.
179
+ expect(onChanged).toHaveBeenCalledTimes(1);
180
+ });
181
+ it('does not notify the parent when the revert conflicts or fails', async () => {
182
+ const onChanged = vi.fn();
183
+ revertAutopilotChange
184
+ .mockResolvedValueOnce({
185
+ ok: false,
186
+ error: 'Fields changed since autopilot applied them: metaTitle',
187
+ conflictFields: ['metaTitle'],
188
+ })
189
+ .mockResolvedValueOnce({ ok: false, error: 'Autopilot change not found' });
190
+ render(_jsx(AutopilotAppliedSection, { onChanged: onChanged }));
191
+ fireEvent.click(await screen.findByRole('button', { name: 'Revert change to Pricing' }));
192
+ fireEvent.click(await screen.findByRole('button', { name: 'Revert change' }));
193
+ const dialog = await screen.findByRole('dialog', { name: 'Someone edited this since' });
194
+ expect(onChanged).not.toHaveBeenCalled();
195
+ fireEvent.click(within(dialog).getByRole('button', { name: 'Revert anyway' }));
196
+ await waitFor(() => expect(toast.error).toHaveBeenCalledWith('Autopilot change not found'));
197
+ expect(revertAutopilotChange).toHaveBeenCalledTimes(2);
198
+ expect(onChanged).not.toHaveBeenCalled();
199
+ expect(screen.getByRole('status').textContent).toBe('');
200
+ });
201
+ it('a 409 re-prompts with the conflicting fields and Revert anyway forces the revert', async () => {
202
+ revertAutopilotChange
203
+ .mockResolvedValueOnce({
204
+ ok: false,
205
+ error: 'Fields changed since autopilot applied them: metaTitle',
206
+ conflictFields: ['metaTitle'],
207
+ })
208
+ .mockResolvedValueOnce({ ok: true });
209
+ render(_jsx(AutopilotAppliedSection, {}));
210
+ fireEvent.click(await screen.findByRole('button', { name: 'Revert change to Pricing' }));
211
+ fireEvent.click(await screen.findByRole('button', { name: 'Revert change' }));
212
+ const dialog = await screen.findByRole('dialog', { name: 'Someone edited this since' });
213
+ expect(dialog.textContent).toContain('metaTitle');
214
+ expect(toast.error).not.toHaveBeenCalled();
215
+ fireEvent.click(within(dialog).getByRole('button', { name: 'Revert anyway' }));
216
+ await waitFor(() => expect(revertAutopilotChange).toHaveBeenCalledWith('la-1', true));
217
+ await waitFor(() => expect(toast.success).toHaveBeenCalledWith('Change reverted.'));
218
+ });
219
+ it('surfaces non-conflict errors as a toast and leaves the row in place', async () => {
220
+ revertAutopilotChange.mockResolvedValueOnce({ ok: false, error: 'Autopilot change not found' });
221
+ render(_jsx(AutopilotAppliedSection, {}));
222
+ fireEvent.click(await screen.findByRole('button', { name: 'Revert change to Pricing' }));
223
+ fireEvent.click(await screen.findByRole('button', { name: 'Revert change' }));
224
+ await waitFor(() => expect(toast.error).toHaveBeenCalledWith('Autopilot change not found'));
225
+ expect(screen.queryByRole('dialog')).toBeNull();
226
+ expect(screen.getByRole('button', { name: 'Revert change to Pricing' })).toHaveProperty('disabled', false);
227
+ expect(fetchAutopilotApplied).toHaveBeenCalledTimes(1);
228
+ });
229
+ it('issues a single revert when confirm fires twice in the same tick and locks other rows', async () => {
230
+ fetchAutopilotApplied.mockImplementation(async () => list({ docs: [row(), row({ id: 'la-2', entityTitle: 'About' })], total: 2 }));
231
+ const inflight = deferred();
232
+ revertAutopilotChange.mockImplementationOnce(() => inflight.promise);
233
+ render(_jsx(AutopilotAppliedSection, {}));
234
+ fireEvent.click(await screen.findByRole('button', { name: 'Revert change to Pricing' }));
235
+ const confirm = await screen.findByRole('button', { name: 'Revert change' });
236
+ // Both clicks land before React commits the `mutation` state, so only the
237
+ // synchronous ref guard can stop the second `doRevert`.
238
+ act(() => {
239
+ confirm.click();
240
+ confirm.click();
241
+ });
242
+ await flushMicrotasks();
243
+ expect(revertAutopilotChange).toHaveBeenCalledTimes(1);
244
+ const about = screen.getByRole('button', { name: 'Revert change to About' });
245
+ expect(about).toHaveProperty('disabled', true);
246
+ inflight.resolve({ ok: true });
247
+ await waitFor(() => expect(about).toHaveProperty('disabled', false));
248
+ expect(revertAutopilotChange).toHaveBeenCalledTimes(1);
249
+ });
250
+ it('stays silent after unmount when the revert settles', async () => {
251
+ const inflight = deferred();
252
+ revertAutopilotChange.mockImplementationOnce(() => inflight.promise);
253
+ const { unmount } = render(_jsx(AutopilotAppliedSection, {}));
254
+ fireEvent.click(await screen.findByRole('button', { name: 'Revert change to Pricing' }));
255
+ fireEvent.click(await screen.findByRole('button', { name: 'Revert change' }));
256
+ await waitFor(() => expect(revertAutopilotChange).toHaveBeenCalledTimes(1));
257
+ unmount();
258
+ inflight.resolve({ ok: true });
259
+ await flushMicrotasks();
260
+ expect(toast.success).not.toHaveBeenCalled();
261
+ expect(fetchAutopilotApplied).toHaveBeenCalledTimes(1);
262
+ });
263
+ it('filters by status and outcome and resets to page 1', async () => {
264
+ render(_jsx(AutopilotAppliedSection, {}));
265
+ await screen.findByRole('table', { name: 'Changes applied by autopilot' });
266
+ fireEvent.change(screen.getByLabelText('Filter by status'), { target: { value: 'reverted' } });
267
+ await waitFor(() => expect(fetchAutopilotApplied).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, status: 'reverted', outcome: undefined })));
268
+ fireEvent.change(screen.getByLabelText('Filter by outcome'), { target: { value: 'regressed' } });
269
+ await waitFor(() => expect(fetchAutopilotApplied).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, status: 'reverted', outcome: 'regressed' })));
270
+ });
271
+ it('paginates through the ledger', async () => {
272
+ fetchAutopilotApplied.mockImplementation(async (params) => {
273
+ const p = params;
274
+ return list({
275
+ docs: [row({ id: `la-${p.page}`, entityTitle: `Page ${p.page}` })],
276
+ total: 45,
277
+ page: p.page,
278
+ });
279
+ });
280
+ render(_jsx(AutopilotAppliedSection, {}));
281
+ await screen.findByText('Page 1');
282
+ expect(screen.getByText('1–20 of 45')).toBeTruthy();
283
+ expect(screen.getByRole('button', { name: 'Previous' })).toHaveProperty('disabled', true);
284
+ fireEvent.click(screen.getByRole('button', { name: 'Next' }));
285
+ await screen.findByText('Page 2');
286
+ expect(fetchAutopilotApplied).toHaveBeenLastCalledWith(expect.objectContaining({ page: 2 }));
287
+ expect(screen.getByText('21–40 of 45')).toBeTruthy();
288
+ });
289
+ it('snaps back to the last page when a refetch lands on an empty page', async () => {
290
+ let total = 21;
291
+ fetchAutopilotApplied.mockImplementation(async (params) => {
292
+ const p = params;
293
+ const docs = (p.page - 1) * 20 < total
294
+ ? [row({ id: `la-${p.page}`, entityTitle: `Page ${p.page}` })]
295
+ : [];
296
+ return list({ docs, total, page: p.page });
297
+ });
298
+ const { rerender } = render(_jsx(AutopilotAppliedSection, { refreshKey: 0 }));
299
+ await screen.findByText('Page 1');
300
+ fireEvent.click(screen.getByRole('button', { name: 'Next' }));
301
+ await screen.findByText('Page 2');
302
+ // The row on page 2 disappears (e.g. filtered out after a revert elsewhere).
303
+ total = 20;
304
+ rerender(_jsx(AutopilotAppliedSection, { refreshKey: 1 }));
305
+ await screen.findByText('Page 1');
306
+ expect(fetchAutopilotApplied).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1 }));
307
+ expect(screen.queryByText('Autopilot has not applied anything yet')).toBeNull();
308
+ });
309
+ it('refetches when refreshKey changes', async () => {
310
+ const { rerender } = render(_jsx(AutopilotAppliedSection, { refreshKey: 0 }));
311
+ await screen.findByRole('table', { name: 'Changes applied by autopilot' });
312
+ rerender(_jsx(AutopilotAppliedSection, { refreshKey: 1 }));
313
+ await waitFor(() => expect(fetchAutopilotApplied).toHaveBeenCalledTimes(2));
314
+ });
315
+ it('shows the empty state with a settings shortcut and an error state with Retry', async () => {
316
+ const onNavigate = vi.fn();
317
+ fetchAutopilotApplied.mockImplementationOnce(async () => list({ docs: [], total: 0 }));
318
+ render(_jsx(AutopilotAppliedSection, { onNavigate: onNavigate }));
319
+ await screen.findByText('Autopilot has not applied anything yet');
320
+ fireEvent.click(screen.getByRole('button', { name: 'Open autopilot settings' }));
321
+ expect(onNavigate).toHaveBeenCalledWith('/settings?tab=seo');
322
+ fetchAutopilotApplied.mockRejectedValueOnce(new Error('boom'));
323
+ render(_jsx(AutopilotAppliedSection, {}));
324
+ const alert = await screen.findByRole('alert');
325
+ expect(alert.textContent).toContain('boom');
326
+ fireEvent.click(within(alert).getByRole('button', { name: 'Retry' }));
327
+ await waitFor(() => expect(fetchAutopilotApplied).toHaveBeenCalledTimes(3));
328
+ });
329
+ });
330
+ //# sourceMappingURL=seo-autopilot-applied.render.test.js.map