@actuate-media/cms-admin 0.32.0 → 0.34.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.
- package/dist/__tests__/views/ai-settings.render.test.d.ts +2 -0
- package/dist/__tests__/views/ai-settings.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/ai-settings.render.test.js +182 -0
- package/dist/__tests__/views/ai-settings.render.test.js.map +1 -0
- package/dist/__tests__/views/seo-settings.render.test.d.ts +2 -0
- package/dist/__tests__/views/seo-settings.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/seo-settings.render.test.js +152 -0
- package/dist/__tests__/views/seo-settings.render.test.js.map +1 -0
- package/dist/__tests__/views/settings-save-footer.render.test.js +16 -9
- package/dist/__tests__/views/settings-save-footer.render.test.js.map +1 -1
- package/dist/actuate-admin.css +1 -1
- package/dist/components/MediaPickerModal.d.ts +15 -1
- package/dist/components/MediaPickerModal.d.ts.map +1 -1
- package/dist/components/MediaPickerModal.js +17 -1
- package/dist/components/MediaPickerModal.js.map +1 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +3 -47
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/settings/AISettingsTab.d.ts +6 -0
- package/dist/views/settings/AISettingsTab.d.ts.map +1 -0
- package/dist/views/settings/AISettingsTab.js +138 -0
- package/dist/views/settings/AISettingsTab.js.map +1 -0
- package/dist/views/settings/AiFeaturesCard.d.ts +12 -0
- package/dist/views/settings/AiFeaturesCard.d.ts.map +1 -0
- package/dist/views/settings/AiFeaturesCard.js +14 -0
- package/dist/views/settings/AiFeaturesCard.js.map +1 -0
- package/dist/views/settings/CapabilityBadges.d.ts +5 -0
- package/dist/views/settings/CapabilityBadges.d.ts.map +1 -0
- package/dist/views/settings/CapabilityBadges.js +26 -0
- package/dist/views/settings/CapabilityBadges.js.map +1 -0
- package/dist/views/settings/ModelSelector.d.ts +12 -0
- package/dist/views/settings/ModelSelector.d.ts.map +1 -0
- package/dist/views/settings/ModelSelector.js +29 -0
- package/dist/views/settings/ModelSelector.js.map +1 -0
- package/dist/views/settings/MonthlyUsageCard.d.ts +14 -0
- package/dist/views/settings/MonthlyUsageCard.d.ts.map +1 -0
- package/dist/views/settings/MonthlyUsageCard.js +30 -0
- package/dist/views/settings/MonthlyUsageCard.js.map +1 -0
- package/dist/views/settings/ProviderConnectionCard.d.ts +13 -0
- package/dist/views/settings/ProviderConnectionCard.d.ts.map +1 -0
- package/dist/views/settings/ProviderConnectionCard.js +48 -0
- package/dist/views/settings/ProviderConnectionCard.js.map +1 -0
- package/dist/views/settings/SeoSettingsTab.d.ts +7 -0
- package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -0
- package/dist/views/settings/SeoSettingsTab.js +126 -0
- package/dist/views/settings/SeoSettingsTab.js.map +1 -0
- package/dist/views/settings/useAiSettings.d.ts +143 -0
- package/dist/views/settings/useAiSettings.d.ts.map +1 -0
- package/dist/views/settings/useAiSettings.js +278 -0
- package/dist/views/settings/useAiSettings.js.map +1 -0
- package/dist/views/settings/useSeoSettings.d.ts +79 -0
- package/dist/views/settings/useSeoSettings.d.ts.map +1 -0
- package/dist/views/settings/useSeoSettings.js +278 -0
- package/dist/views/settings/useSeoSettings.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/views/ai-settings.render.test.tsx +225 -0
- package/src/__tests__/views/seo-settings.render.test.tsx +219 -0
- package/src/__tests__/views/settings-save-footer.render.test.tsx +16 -9
- package/src/components/MediaPickerModal.tsx +39 -2
- package/src/views/Settings.tsx +4 -141
- package/src/views/settings/AISettingsTab.tsx +411 -0
- package/src/views/settings/AiFeaturesCard.tsx +49 -0
- package/src/views/settings/CapabilityBadges.tsx +66 -0
- package/src/views/settings/ModelSelector.tsx +115 -0
- package/src/views/settings/MonthlyUsageCard.tsx +118 -0
- package/src/views/settings/ProviderConnectionCard.tsx +245 -0
- package/src/views/settings/SeoSettingsTab.tsx +658 -0
- package/src/views/settings/useAiSettings.ts +448 -0
- package/src/views/settings/useSeoSettings.ts +374 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useSeoSettings.js","sourceRoot":"","sources":["../../../src/views/settings/useSeoSettings.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actuate-media/cms-admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/actuate-media/actuatecms.git",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"tailwindcss": "^4.0.0",
|
|
89
89
|
"typescript": "^5.7.0",
|
|
90
90
|
"vitest": "^4.1.0",
|
|
91
|
-
"@actuate-media/cms-core": "0.
|
|
91
|
+
"@actuate-media/cms-core": "0.43.0",
|
|
92
92
|
"@actuate-media/component-blocks": "0.2.0"
|
|
93
93
|
},
|
|
94
94
|
"scripts": {
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Settings > AI reads three backends: /ai/settings (defaults + masked
|
|
7
|
+
* providers), /ai/models (catalog), /ai/usage/summary. Provider connection
|
|
8
|
+
* actions hit /ai/providers/*. Keys are never returned — only a masked last4.
|
|
9
|
+
*/
|
|
10
|
+
const SETTINGS_RESPONSE = {
|
|
11
|
+
settings: {
|
|
12
|
+
defaultProviderId: 'p1',
|
|
13
|
+
defaultModelId: 'anthropic:claude-sonnet-4-latest',
|
|
14
|
+
features: [
|
|
15
|
+
{
|
|
16
|
+
key: 'ai.features.contentWriting',
|
|
17
|
+
label: 'Content writing suggestions',
|
|
18
|
+
description: 'Draft, rewrite, summarize content.',
|
|
19
|
+
enabled: true,
|
|
20
|
+
category: 'content',
|
|
21
|
+
requiredCapabilities: ['text'],
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
key: 'ai.features.redirectChainFlattening',
|
|
25
|
+
label: 'Redirect chain flattening',
|
|
26
|
+
description: 'Suggest single-hop redirects.',
|
|
27
|
+
enabled: false,
|
|
28
|
+
category: 'seo',
|
|
29
|
+
requiredCapabilities: ['text'],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
budget: { monthlyTokenLimit: 1000000 },
|
|
33
|
+
},
|
|
34
|
+
providers: [
|
|
35
|
+
{
|
|
36
|
+
id: 'p1',
|
|
37
|
+
provider: 'anthropic',
|
|
38
|
+
displayName: 'Anthropic (Claude)',
|
|
39
|
+
enabled: true,
|
|
40
|
+
status: 'connected',
|
|
41
|
+
apiKeyLast4: 'f12a',
|
|
42
|
+
apiKeySource: 'database',
|
|
43
|
+
keyManagedExternally: false,
|
|
44
|
+
hasKey: true,
|
|
45
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
46
|
+
updatedAt: '2026-01-01T00:00:00.000Z',
|
|
47
|
+
updatedBy: 'u1',
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
defaultModel: null,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const MODELS = [
|
|
54
|
+
{
|
|
55
|
+
id: 'anthropic:claude-sonnet-4-latest',
|
|
56
|
+
provider: 'anthropic',
|
|
57
|
+
providerModelId: 'claude-sonnet-4-latest',
|
|
58
|
+
displayName: 'Claude Sonnet (balanced)',
|
|
59
|
+
status: 'available',
|
|
60
|
+
capabilities: { text: true, vision: true, toolCalling: true },
|
|
61
|
+
contextWindow: 200000,
|
|
62
|
+
inputModalities: ['text', 'image'],
|
|
63
|
+
outputModalities: ['text'],
|
|
64
|
+
pricing: { inputPerMillion: 3, outputPerMillion: 15 },
|
|
65
|
+
lastSyncedAt: '2026-01-01T00:00:00.000Z',
|
|
66
|
+
source: 'config',
|
|
67
|
+
},
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
const USAGE = {
|
|
71
|
+
totalTokens: 412000,
|
|
72
|
+
promptTokens: 0,
|
|
73
|
+
completionTokens: 0,
|
|
74
|
+
monthlyTokenLimit: 1000000,
|
|
75
|
+
periodStart: '2026-06-01T00:00:00.000Z',
|
|
76
|
+
periodEnd: '2026-07-01T00:00:00.000Z',
|
|
77
|
+
runCount: 5,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Mutable overrides so individual tests can tweak the loaded shape.
|
|
81
|
+
let settingsResponse: typeof SETTINGS_RESPONSE = SETTINGS_RESPONSE
|
|
82
|
+
|
|
83
|
+
const cmsApi = vi.fn(async (endpoint: string, options?: { method?: string }) => {
|
|
84
|
+
const method = options?.method ?? 'GET'
|
|
85
|
+
if (endpoint === '/ai/settings' && method === 'GET')
|
|
86
|
+
return { data: settingsResponse, status: 200 }
|
|
87
|
+
if (endpoint === '/ai/settings' && method === 'PUT')
|
|
88
|
+
return { data: settingsResponse, status: 200 }
|
|
89
|
+
if (endpoint === '/ai/models' && method === 'GET')
|
|
90
|
+
return { data: { models: MODELS }, status: 200 }
|
|
91
|
+
if (endpoint === '/ai/usage/summary') return { data: USAGE, status: 200 }
|
|
92
|
+
if (endpoint === '/ai/providers/p1/test' && method === 'POST') {
|
|
93
|
+
return {
|
|
94
|
+
data: {
|
|
95
|
+
provider: settingsResponse.providers[0],
|
|
96
|
+
result: { ok: true, status: 'connected', message: 'Connection successful.' },
|
|
97
|
+
},
|
|
98
|
+
status: 200,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (endpoint === '/ai/providers/p1/sync-models' && method === 'POST') {
|
|
102
|
+
return { data: { models: MODELS, fromFallback: false, warning: null }, status: 200 }
|
|
103
|
+
}
|
|
104
|
+
return { data: {}, status: 200 }
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
vi.mock('../../lib/api.js', () => ({
|
|
108
|
+
cmsApi: (e: string, o?: unknown) => cmsApi(e, o as any),
|
|
109
|
+
ensureCsrfToken: vi.fn(async () => {}),
|
|
110
|
+
}))
|
|
111
|
+
|
|
112
|
+
vi.mock('sonner', () => ({
|
|
113
|
+
toast: { success: vi.fn(), error: vi.fn(), warning: vi.fn(), loading: vi.fn() },
|
|
114
|
+
}))
|
|
115
|
+
|
|
116
|
+
const { AISettingsTab } = await import('../../views/settings/AISettingsTab.js')
|
|
117
|
+
|
|
118
|
+
function deepClone<T>(v: T): T {
|
|
119
|
+
return JSON.parse(JSON.stringify(v))
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
beforeEach(() => {
|
|
123
|
+
cmsApi.mockClear()
|
|
124
|
+
settingsResponse = deepClone(SETTINGS_RESPONSE)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('AISettingsTab', () => {
|
|
128
|
+
it('loads the default provider, model, and capability badges', async () => {
|
|
129
|
+
render(<AISettingsTab role="ADMIN" />)
|
|
130
|
+
expect(await screen.findByText('AI Assistant')).toBeTruthy()
|
|
131
|
+
|
|
132
|
+
const assistant = screen.getByRole('region', { name: 'AI Assistant' })
|
|
133
|
+
expect((within(assistant).getByLabelText('Default model') as HTMLSelectElement).value).toBe(
|
|
134
|
+
'anthropic:claude-sonnet-4-latest',
|
|
135
|
+
)
|
|
136
|
+
// Capability badges render as text (not color-only).
|
|
137
|
+
expect(within(assistant).getByText('Vision')).toBeTruthy()
|
|
138
|
+
expect(within(assistant).getByText('200,000 token context')).toBeTruthy()
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('shows real monthly usage (never a fabricated number)', async () => {
|
|
142
|
+
render(<AISettingsTab role="ADMIN" />)
|
|
143
|
+
expect(await screen.findByText('412K tokens used')).toBeTruthy()
|
|
144
|
+
expect(screen.getByRole('progressbar').getAttribute('aria-valuenow')).toBe('41')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('refreshes the model catalog via the provider sync endpoint', async () => {
|
|
148
|
+
render(<AISettingsTab role="ADMIN" />)
|
|
149
|
+
const assistant = await screen.findByRole('region', { name: 'AI Assistant' })
|
|
150
|
+
fireEvent.click(within(assistant).getByRole('button', { name: 'Refresh models' }))
|
|
151
|
+
await waitFor(() =>
|
|
152
|
+
expect(cmsApi).toHaveBeenCalledWith(
|
|
153
|
+
'/ai/providers/p1/sync-models',
|
|
154
|
+
expect.objectContaining({ method: 'POST' }),
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('tests a provider connection', async () => {
|
|
160
|
+
render(<AISettingsTab role="ADMIN" />)
|
|
161
|
+
await screen.findByText('AI Assistant')
|
|
162
|
+
fireEvent.click(screen.getByRole('button', { name: 'Test connection' }))
|
|
163
|
+
await waitFor(() =>
|
|
164
|
+
expect(cmsApi).toHaveBeenCalledWith(
|
|
165
|
+
'/ai/providers/p1/test',
|
|
166
|
+
expect.objectContaining({ method: 'POST' }),
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('toggles a feature and saves it to /ai/settings', async () => {
|
|
172
|
+
render(<AISettingsTab role="ADMIN" />)
|
|
173
|
+
await screen.findByText('AI Features')
|
|
174
|
+
fireEvent.click(screen.getByRole('switch', { name: 'Content writing suggestions' }))
|
|
175
|
+
|
|
176
|
+
const save = screen.getByRole('button', { name: 'Save Changes' }) as HTMLButtonElement
|
|
177
|
+
expect(save.disabled).toBe(false)
|
|
178
|
+
fireEvent.click(save)
|
|
179
|
+
|
|
180
|
+
await waitFor(() =>
|
|
181
|
+
expect(cmsApi).toHaveBeenCalledWith(
|
|
182
|
+
'/ai/settings',
|
|
183
|
+
expect.objectContaining({ method: 'PUT' }),
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
const putCall = cmsApi.mock.calls.find(
|
|
187
|
+
([e, o]) => e === '/ai/settings' && (o as { method?: string })?.method === 'PUT',
|
|
188
|
+
)
|
|
189
|
+
const body = JSON.parse((putCall![1] as { body: string }).body)
|
|
190
|
+
const cw = body.features.find((f: { key: string }) => f.key === 'ai.features.contentWriting')
|
|
191
|
+
expect(cw.enabled).toBe(false)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('is read-only for non-admins', async () => {
|
|
195
|
+
render(<AISettingsTab role="EDITOR" />)
|
|
196
|
+
expect(await screen.findByText(/read-only access to AI settings/i)).toBeTruthy()
|
|
197
|
+
const assistant = screen.getByRole('region', { name: 'AI Assistant' })
|
|
198
|
+
expect((within(assistant).getByLabelText('Default model') as HTMLSelectElement).disabled).toBe(
|
|
199
|
+
true,
|
|
200
|
+
)
|
|
201
|
+
expect(
|
|
202
|
+
(screen.getByRole('switch', { name: 'Content writing suggestions' }) as HTMLButtonElement)
|
|
203
|
+
.disabled,
|
|
204
|
+
).toBe(true)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('renders an env-managed key as read-only', async () => {
|
|
208
|
+
settingsResponse = deepClone(SETTINGS_RESPONSE)
|
|
209
|
+
settingsResponse.providers[0]!.keyManagedExternally = true
|
|
210
|
+
settingsResponse.providers[0]!.apiKeySource = 'environment'
|
|
211
|
+
render(<AISettingsTab role="ADMIN" />)
|
|
212
|
+
expect(await screen.findByText('Managed by environment')).toBeTruthy()
|
|
213
|
+
expect(screen.getByText(/set via an environment variable/i)).toBeTruthy()
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('warns when the configured default model is no longer available', async () => {
|
|
217
|
+
settingsResponse = deepClone(SETTINGS_RESPONSE)
|
|
218
|
+
settingsResponse.settings.defaultModelId = 'anthropic:ghost-model'
|
|
219
|
+
render(<AISettingsTab role="ADMIN" />)
|
|
220
|
+
await screen.findByText('AI Assistant')
|
|
221
|
+
expect(
|
|
222
|
+
await screen.findByText(/selected model is no longer offered by the provider/i),
|
|
223
|
+
).toBeTruthy()
|
|
224
|
+
})
|
|
225
|
+
})
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react'
|
|
4
|
+
|
|
5
|
+
// The SEO defaults tab reads four shared backends: /seo/config (meta defaults +
|
|
6
|
+
// verification), /seo/crawl-settings (sitemap toggles), /seo/sitemap/status
|
|
7
|
+
// (read-only summary), and /seo/index-status (environment).
|
|
8
|
+
const cmsApi = vi.fn(async (endpoint: string, options?: { method?: string }) => {
|
|
9
|
+
const method = options?.method ?? 'GET'
|
|
10
|
+
if (endpoint === '/seo/config' && method === 'GET') {
|
|
11
|
+
return {
|
|
12
|
+
data: {
|
|
13
|
+
static: { site: null },
|
|
14
|
+
overrides: { site: {} },
|
|
15
|
+
effective: {
|
|
16
|
+
site: {
|
|
17
|
+
siteName: 'Acme',
|
|
18
|
+
siteUrl: 'https://acme.test',
|
|
19
|
+
titleTemplate: '%page% — %site%',
|
|
20
|
+
defaultMetaDescription: 'Seed description.',
|
|
21
|
+
verification: { googleSearchConsole: '', bingWebmasterTools: '' },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
collections: [],
|
|
25
|
+
},
|
|
26
|
+
status: 200,
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (endpoint === '/seo/crawl-settings' && method === 'GET') {
|
|
30
|
+
return {
|
|
31
|
+
data: {
|
|
32
|
+
autoRegenerateSitemapOnPublish: true,
|
|
33
|
+
pingSearchEnginesOnPublish: true,
|
|
34
|
+
sitemapEnabled: true,
|
|
35
|
+
},
|
|
36
|
+
status: 200,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (endpoint === '/seo/sitemap/status' && method === 'GET') {
|
|
40
|
+
return {
|
|
41
|
+
data: {
|
|
42
|
+
sitemapUrl: 'https://acme.test/api/cms/sitemap.xml',
|
|
43
|
+
lastGeneratedAt: '2026-01-02T03:04:05.000Z',
|
|
44
|
+
pagesIncluded: 12,
|
|
45
|
+
pagesExcluded: 1,
|
|
46
|
+
warnings: [],
|
|
47
|
+
},
|
|
48
|
+
status: 200,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (endpoint === '/seo/index-status' && method === 'GET') {
|
|
52
|
+
return { data: { environment: 'preview' }, status: 200 }
|
|
53
|
+
}
|
|
54
|
+
return { data: {}, status: 200 }
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
vi.mock('../../lib/api.js', () => ({
|
|
58
|
+
cmsApi: (e: string, o?: unknown) => cmsApi(e, o as any),
|
|
59
|
+
ensureCsrfToken: vi.fn(async () => {}),
|
|
60
|
+
}))
|
|
61
|
+
|
|
62
|
+
// Stub the Media Center picker: when open, expose a button that selects a known
|
|
63
|
+
// asset (id + url), exercising the asset-id storage path without the real
|
|
64
|
+
// MediaBrowser/network stack.
|
|
65
|
+
vi.mock('../../components/MediaPickerModal.js', () => ({
|
|
66
|
+
MediaPickerModal: ({
|
|
67
|
+
open,
|
|
68
|
+
onSelect,
|
|
69
|
+
onSelectItem,
|
|
70
|
+
}: {
|
|
71
|
+
open: boolean
|
|
72
|
+
onSelect: (url: string) => void
|
|
73
|
+
onSelectItem?: (item: { id: string }) => void
|
|
74
|
+
}) =>
|
|
75
|
+
open ? (
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
onClick={() => {
|
|
79
|
+
onSelect('https://cdn.test/og.png')
|
|
80
|
+
onSelectItem?.({ id: 'asset-123' })
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
pick-asset
|
|
84
|
+
</button>
|
|
85
|
+
) : null,
|
|
86
|
+
}))
|
|
87
|
+
|
|
88
|
+
const { SeoSettingsTab } = await import('../../views/settings/SeoSettingsTab.js')
|
|
89
|
+
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
cmsApi.mockClear()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('SeoSettingsTab', () => {
|
|
95
|
+
it('renders the workspace callout and loads meta defaults from /seo/config', async () => {
|
|
96
|
+
render(<SeoSettingsTab role="ADMIN" />)
|
|
97
|
+
|
|
98
|
+
expect(await screen.findByText('Looking for the full SEO workspace?')).toBeTruthy()
|
|
99
|
+
expect((screen.getByLabelText('Title template') as HTMLInputElement).value).toBe(
|
|
100
|
+
'%page% — %site%',
|
|
101
|
+
)
|
|
102
|
+
expect((screen.getByLabelText('Default meta description') as HTMLTextAreaElement).value).toBe(
|
|
103
|
+
'Seed description.',
|
|
104
|
+
)
|
|
105
|
+
// Clean form → Save disabled.
|
|
106
|
+
expect(
|
|
107
|
+
(screen.getByRole('button', { name: 'Save Changes' }) as HTMLButtonElement).disabled,
|
|
108
|
+
).toBe(true)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('routes "Open SEO" to /seo', async () => {
|
|
112
|
+
const onNavigate = vi.fn()
|
|
113
|
+
render(<SeoSettingsTab role="ADMIN" onNavigate={onNavigate} />)
|
|
114
|
+
await screen.findByText('Looking for the full SEO workspace?')
|
|
115
|
+
fireEvent.click(screen.getByRole('button', { name: /Open SEO/ }))
|
|
116
|
+
expect(onNavigate).toHaveBeenCalledWith('/seo')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('rejects an unsupported title-template token and blocks save', async () => {
|
|
120
|
+
render(<SeoSettingsTab role="ADMIN" />)
|
|
121
|
+
const tpl = (await screen.findByLabelText('Title template')) as HTMLInputElement
|
|
122
|
+
fireEvent.change(tpl, { target: { value: '%page% %bogus%' } })
|
|
123
|
+
|
|
124
|
+
expect((await screen.findAllByText(/Unsupported token/i)).length).toBeGreaterThan(0)
|
|
125
|
+
expect(
|
|
126
|
+
(screen.getByRole('button', { name: 'Save Changes' }) as HTMLButtonElement).disabled,
|
|
127
|
+
).toBe(true)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('shows a character count and warns when the description is too long', async () => {
|
|
131
|
+
render(<SeoSettingsTab role="ADMIN" />)
|
|
132
|
+
const desc = (await screen.findByLabelText('Default meta description')) as HTMLTextAreaElement
|
|
133
|
+
fireEvent.change(desc, { target: { value: 'x'.repeat(200) } })
|
|
134
|
+
|
|
135
|
+
expect(screen.getByText('200/160')).toBeTruthy()
|
|
136
|
+
expect((await screen.findAllByText(/truncate/i)).length).toBeGreaterThan(0)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('persists meta defaults + verification to the shared /seo/config', async () => {
|
|
140
|
+
render(<SeoSettingsTab role="ADMIN" />)
|
|
141
|
+
const google = (await screen.findByLabelText('Google Search Console')) as HTMLInputElement
|
|
142
|
+
fireEvent.change(google, { target: { value: 'goog-token' } })
|
|
143
|
+
|
|
144
|
+
const save = screen.getByRole('button', { name: 'Save Changes' }) as HTMLButtonElement
|
|
145
|
+
expect(save.disabled).toBe(false)
|
|
146
|
+
fireEvent.click(save)
|
|
147
|
+
|
|
148
|
+
await waitFor(() =>
|
|
149
|
+
expect(cmsApi).toHaveBeenCalledWith(
|
|
150
|
+
'/seo/config',
|
|
151
|
+
expect.objectContaining({ method: 'PUT' }),
|
|
152
|
+
),
|
|
153
|
+
)
|
|
154
|
+
const putCall = cmsApi.mock.calls.find(
|
|
155
|
+
([e, o]) => e === '/seo/config' && (o as { method?: string })?.method === 'PUT',
|
|
156
|
+
)
|
|
157
|
+
const body = JSON.parse((putCall![1] as { body: string }).body)
|
|
158
|
+
expect(body.site.verification.googleSearchConsole).toBe('goog-token')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('stores the selected social image asset id from the Media Center picker', async () => {
|
|
162
|
+
render(<SeoSettingsTab role="ADMIN" />)
|
|
163
|
+
await screen.findByLabelText('Title template')
|
|
164
|
+
|
|
165
|
+
fireEvent.click(screen.getByRole('button', { name: 'Choose image' }))
|
|
166
|
+
fireEvent.click(screen.getByRole('button', { name: 'pick-asset' }))
|
|
167
|
+
|
|
168
|
+
const save = screen.getByRole('button', { name: 'Save Changes' }) as HTMLButtonElement
|
|
169
|
+
expect(save.disabled).toBe(false)
|
|
170
|
+
fireEvent.click(save)
|
|
171
|
+
|
|
172
|
+
await waitFor(() =>
|
|
173
|
+
expect(cmsApi).toHaveBeenCalledWith(
|
|
174
|
+
'/seo/config',
|
|
175
|
+
expect.objectContaining({ method: 'PUT' }),
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
const putCall = cmsApi.mock.calls.find(
|
|
179
|
+
([e, o]) => e === '/seo/config' && (o as { method?: string })?.method === 'PUT',
|
|
180
|
+
)
|
|
181
|
+
const body = JSON.parse((putCall![1] as { body: string }).body)
|
|
182
|
+
expect(body.site.defaultSocialImageAssetId).toBe('asset-123')
|
|
183
|
+
expect(body.site.defaultOgImage).toBe('https://cdn.test/og.png')
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('persists sitemap toggles to the shared /seo/crawl-settings', async () => {
|
|
187
|
+
render(<SeoSettingsTab role="ADMIN" />)
|
|
188
|
+
await screen.findByLabelText('Title template')
|
|
189
|
+
fireEvent.click(screen.getByRole('switch', { name: 'Ping search engines on publish' }))
|
|
190
|
+
|
|
191
|
+
fireEvent.click(screen.getByRole('button', { name: 'Save Changes' }))
|
|
192
|
+
|
|
193
|
+
await waitFor(() =>
|
|
194
|
+
expect(cmsApi).toHaveBeenCalledWith(
|
|
195
|
+
'/seo/crawl-settings',
|
|
196
|
+
expect.objectContaining({ method: 'PUT' }),
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('shows the read-only sitemap summary', async () => {
|
|
202
|
+
render(<SeoSettingsTab role="ADMIN" />)
|
|
203
|
+
await screen.findByLabelText('Title template')
|
|
204
|
+
expect(screen.getByText('Included URLs')).toBeTruthy()
|
|
205
|
+
expect(screen.getByText('12')).toBeTruthy()
|
|
206
|
+
expect(screen.getByRole('link', { name: /View sitemap/ })).toBeTruthy()
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('is read-only for non-admins', async () => {
|
|
210
|
+
render(<SeoSettingsTab role="EDITOR" />)
|
|
211
|
+
expect(await screen.findByText(/read-only access to SEO defaults/i)).toBeTruthy()
|
|
212
|
+
// Scope to the Meta Defaults region: the advanced SEOConfigPanel disclosure
|
|
213
|
+
// also mounts and can briefly produce overlapping label matches.
|
|
214
|
+
const metaCard = screen.getByRole('region', { name: 'Meta Defaults' })
|
|
215
|
+
expect((within(metaCard).getByLabelText('Title template') as HTMLInputElement).disabled).toBe(
|
|
216
|
+
true,
|
|
217
|
+
)
|
|
218
|
+
})
|
|
219
|
+
})
|
|
@@ -3,11 +3,16 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
3
3
|
import { render, screen, waitFor } from '@testing-library/react'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Regression: the shared "Save Changes" footer
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Regression: the shared no-op "Save Changes" footer (driven by the settings
|
|
7
|
+
* global `handleSave`) must only render on tabs that actually persist through
|
|
8
|
+
* it (currently just Layout). On self-managed tabs like SEO it used to render a
|
|
9
|
+
* SECOND footer whose click toasted "Settings saved successfully!" while
|
|
10
|
+
* persisting none of the tab's edits — misleading, no-op UI.
|
|
11
|
+
*
|
|
12
|
+
* The SEO tab now owns a real `SettingsSaveBar` (also labelled "Save Changes")
|
|
13
|
+
* that persists through `/seo/config`, so the contract is: exactly ONE
|
|
14
|
+
* "Save Changes" button on the SEO tab — the tab's own — never the global one
|
|
15
|
+
* stacked on top of it.
|
|
11
16
|
*/
|
|
12
17
|
const SEO_CONFIG = {
|
|
13
18
|
static: { site: null },
|
|
@@ -44,15 +49,17 @@ beforeEach(() => {
|
|
|
44
49
|
})
|
|
45
50
|
|
|
46
51
|
describe('Settings shared save footer', () => {
|
|
47
|
-
it(
|
|
52
|
+
it("renders only the SEO tab's own save bar, not the global footer on top", async () => {
|
|
48
53
|
render(<Settings config={LAYOUT_CONFIG} session={SESSION} initialTab="seo" />)
|
|
49
54
|
|
|
50
|
-
// The
|
|
55
|
+
// The Advanced disclosure still mounts the per-collection SEO panel, which
|
|
56
|
+
// owns its own persistence ("Save SEO defaults").
|
|
51
57
|
expect(
|
|
52
58
|
(await screen.findAllByRole('button', { name: 'Save SEO defaults' })).length,
|
|
53
59
|
).toBeGreaterThan(0)
|
|
54
|
-
// The
|
|
55
|
-
|
|
60
|
+
// The tab's own SettingsSaveBar is the ONLY "Save Changes" button — the
|
|
61
|
+
// no-op global footer must not be stacked on top of it.
|
|
62
|
+
expect(screen.getAllByRole('button', { name: 'Save Changes' })).toHaveLength(1)
|
|
56
63
|
})
|
|
57
64
|
|
|
58
65
|
it('renders the global "Save Changes" footer on the Layout tab', async () => {
|
|
@@ -16,14 +16,35 @@ interface MediaItem {
|
|
|
16
16
|
height?: number
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
/** Minimal selected-asset shape exposed to callers that need the asset id. */
|
|
20
|
+
export interface SelectedMediaItem {
|
|
21
|
+
id: string
|
|
22
|
+
storageKey: string
|
|
23
|
+
filename: string
|
|
24
|
+
mimeType: string
|
|
25
|
+
width?: number
|
|
26
|
+
height?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
export interface MediaPickerModalProps {
|
|
20
30
|
open: boolean
|
|
21
31
|
onClose: () => void
|
|
22
32
|
onSelect: (url: string, alt?: string) => void
|
|
33
|
+
/**
|
|
34
|
+
* Optional richer callback fired alongside `onSelect` with the full media
|
|
35
|
+
* item, so callers that need to store the asset id (not just the URL) can.
|
|
36
|
+
*/
|
|
37
|
+
onSelectItem?: (item: SelectedMediaItem) => void
|
|
23
38
|
accept?: string
|
|
24
39
|
}
|
|
25
40
|
|
|
26
|
-
export function MediaPickerModal({
|
|
41
|
+
export function MediaPickerModal({
|
|
42
|
+
open,
|
|
43
|
+
onClose,
|
|
44
|
+
onSelect,
|
|
45
|
+
onSelectItem,
|
|
46
|
+
accept,
|
|
47
|
+
}: MediaPickerModalProps) {
|
|
27
48
|
const [tab, setTab] = useState<'library' | 'upload'>('library')
|
|
28
49
|
const [search, setSearch] = useState('')
|
|
29
50
|
const [uploading, setUploading] = useState(false)
|
|
@@ -52,9 +73,17 @@ export function MediaPickerModal({ open, onClose, onSelect, accept }: MediaPicke
|
|
|
52
73
|
if (res.error) {
|
|
53
74
|
toast.error(res.error)
|
|
54
75
|
} else if (res.data) {
|
|
55
|
-
const mediaItem = res.data as
|
|
76
|
+
const mediaItem = res.data as MediaItem
|
|
56
77
|
const url = mediaItem.storageKey ?? ''
|
|
57
78
|
toast.success(`Uploaded ${file.name}`)
|
|
79
|
+
onSelectItem?.({
|
|
80
|
+
id: mediaItem.id,
|
|
81
|
+
storageKey: mediaItem.storageKey,
|
|
82
|
+
filename: mediaItem.filename ?? file.name,
|
|
83
|
+
mimeType: mediaItem.mimeType ?? file.type,
|
|
84
|
+
width: mediaItem.width,
|
|
85
|
+
height: mediaItem.height,
|
|
86
|
+
})
|
|
58
87
|
onSelect(url, file.name)
|
|
59
88
|
onClose()
|
|
60
89
|
}
|
|
@@ -64,6 +93,14 @@ export function MediaPickerModal({ open, onClose, onSelect, accept }: MediaPicke
|
|
|
64
93
|
}
|
|
65
94
|
|
|
66
95
|
function handleSelectItem(item: MediaItem) {
|
|
96
|
+
onSelectItem?.({
|
|
97
|
+
id: item.id,
|
|
98
|
+
storageKey: item.storageKey,
|
|
99
|
+
filename: item.filename,
|
|
100
|
+
mimeType: item.mimeType,
|
|
101
|
+
width: item.width,
|
|
102
|
+
height: item.height,
|
|
103
|
+
})
|
|
67
104
|
onSelect(item.storageKey, item.filename)
|
|
68
105
|
onClose()
|
|
69
106
|
}
|