@prenta/admin 1.13.0 → 1.15.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 (100) hide show
  1. package/CHANGELOG.md +31 -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-proposals.test.d.ts +2 -0
  11. package/dist/__tests__/lib/seo-service-proposals.test.d.ts.map +1 -0
  12. package/dist/__tests__/lib/seo-service-proposals.test.js +99 -0
  13. package/dist/__tests__/lib/seo-service-proposals.test.js.map +1 -0
  14. package/dist/__tests__/router/seo-tab-for-path.test.js +1 -0
  15. package/dist/__tests__/router/seo-tab-for-path.test.js.map +1 -1
  16. package/dist/__tests__/views/redirects-pane.render.test.js +30 -12
  17. package/dist/__tests__/views/redirects-pane.render.test.js.map +1 -1
  18. package/dist/__tests__/views/seo-autopilot-applied.render.test.d.ts +2 -0
  19. package/dist/__tests__/views/seo-autopilot-applied.render.test.d.ts.map +1 -0
  20. package/dist/__tests__/views/seo-autopilot-applied.render.test.js +330 -0
  21. package/dist/__tests__/views/seo-autopilot-applied.render.test.js.map +1 -0
  22. package/dist/__tests__/views/seo-autopilot-card.render.test.d.ts +2 -0
  23. package/dist/__tests__/views/seo-autopilot-card.render.test.d.ts.map +1 -0
  24. package/dist/__tests__/views/seo-autopilot-card.render.test.js +512 -0
  25. package/dist/__tests__/views/seo-autopilot-card.render.test.js.map +1 -0
  26. package/dist/__tests__/views/seo-proposals-tab.render.test.d.ts +2 -0
  27. package/dist/__tests__/views/seo-proposals-tab.render.test.d.ts.map +1 -0
  28. package/dist/__tests__/views/seo-proposals-tab.render.test.js +431 -0
  29. package/dist/__tests__/views/seo-proposals-tab.render.test.js.map +1 -0
  30. package/dist/__tests__/views/seo-settings.render.test.js +41 -4
  31. package/dist/__tests__/views/seo-settings.render.test.js.map +1 -1
  32. package/dist/components/seo/ChangePreview.d.ts +6 -0
  33. package/dist/components/seo/ChangePreview.d.ts.map +1 -0
  34. package/dist/components/seo/ChangePreview.js +18 -0
  35. package/dist/components/seo/ChangePreview.js.map +1 -0
  36. package/dist/components/seo/SeoIssueFixPanel.d.ts.map +1 -1
  37. package/dist/components/seo/SeoIssueFixPanel.js +1 -14
  38. package/dist/components/seo/SeoIssueFixPanel.js.map +1 -1
  39. package/dist/lib/api.d.ts +13 -2
  40. package/dist/lib/api.d.ts.map +1 -1
  41. package/dist/lib/api.js +1 -2
  42. package/dist/lib/api.js.map +1 -1
  43. package/dist/lib/seo-service.d.ts +293 -2
  44. package/dist/lib/seo-service.d.ts.map +1 -1
  45. package/dist/lib/seo-service.js +170 -0
  46. package/dist/lib/seo-service.js.map +1 -1
  47. package/dist/prenta-admin.css +1 -1
  48. package/dist/views/SEO.d.ts +5 -1
  49. package/dist/views/SEO.d.ts.map +1 -1
  50. package/dist/views/SEO.js +28 -9
  51. package/dist/views/SEO.js.map +1 -1
  52. package/dist/views/seo/AutopilotAppliedSection.d.ts +10 -0
  53. package/dist/views/seo/AutopilotAppliedSection.d.ts.map +1 -0
  54. package/dist/views/seo/AutopilotAppliedSection.js +295 -0
  55. package/dist/views/seo/AutopilotAppliedSection.js.map +1 -0
  56. package/dist/views/seo/ProposalsTab.d.ts +7 -0
  57. package/dist/views/seo/ProposalsTab.d.ts.map +1 -0
  58. package/dist/views/seo/ProposalsTab.js +334 -0
  59. package/dist/views/seo/ProposalsTab.js.map +1 -0
  60. package/dist/views/seo/RedirectsTab.d.ts +3 -1
  61. package/dist/views/seo/RedirectsTab.d.ts.map +1 -1
  62. package/dist/views/seo/RedirectsTab.js +14 -19
  63. package/dist/views/seo/RedirectsTab.js.map +1 -1
  64. package/dist/views/seo/relative-age.d.ts +7 -0
  65. package/dist/views/seo/relative-age.d.ts.map +1 -0
  66. package/dist/views/seo/relative-age.js +20 -0
  67. package/dist/views/seo/relative-age.js.map +1 -0
  68. package/dist/views/settings/SeoAutopilotCard.d.ts +30 -0
  69. package/dist/views/settings/SeoAutopilotCard.d.ts.map +1 -0
  70. package/dist/views/settings/SeoAutopilotCard.js +375 -0
  71. package/dist/views/settings/SeoAutopilotCard.js.map +1 -0
  72. package/dist/views/settings/SeoSettingsTab.d.ts.map +1 -1
  73. package/dist/views/settings/SeoSettingsTab.js +2 -1
  74. package/dist/views/settings/SeoSettingsTab.js.map +1 -1
  75. package/dist/views/settings/components.d.ts +3 -1
  76. package/dist/views/settings/components.d.ts.map +1 -1
  77. package/dist/views/settings/components.js +2 -2
  78. package/dist/views/settings/components.js.map +1 -1
  79. package/package.json +3 -3
  80. package/src/__tests__/lib/api.test.ts +76 -0
  81. package/src/__tests__/lib/seo-service-autopilot.test.ts +196 -0
  82. package/src/__tests__/lib/seo-service-proposals.test.ts +108 -0
  83. package/src/__tests__/router/seo-tab-for-path.test.ts +1 -0
  84. package/src/__tests__/views/redirects-pane.render.test.tsx +36 -16
  85. package/src/__tests__/views/seo-autopilot-applied.render.test.tsx +382 -0
  86. package/src/__tests__/views/seo-autopilot-card.render.test.tsx +610 -0
  87. package/src/__tests__/views/seo-proposals-tab.render.test.tsx +531 -0
  88. package/src/__tests__/views/seo-settings.render.test.tsx +47 -7
  89. package/src/components/seo/ChangePreview.tsx +47 -0
  90. package/src/components/seo/SeoIssueFixPanel.tsx +1 -44
  91. package/src/lib/api.ts +19 -3
  92. package/src/lib/seo-service.ts +455 -2
  93. package/src/views/SEO.tsx +41 -8
  94. package/src/views/seo/AutopilotAppliedSection.tsx +597 -0
  95. package/src/views/seo/ProposalsTab.tsx +761 -0
  96. package/src/views/seo/RedirectsTab.tsx +39 -60
  97. package/src/views/seo/relative-age.ts +15 -0
  98. package/src/views/settings/SeoAutopilotCard.tsx +826 -0
  99. package/src/views/settings/SeoSettingsTab.tsx +2 -0
  100. package/src/views/settings/components.tsx +10 -1
@@ -1 +1 @@
1
- {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../../src/views/settings/components.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../../../src/views/settings/components.tsx"],"names":[],"mappings":"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"}
@@ -15,9 +15,9 @@ import { adminPortalContainer } from '../../lib/portal-container.js';
15
15
  * Security, SEO, AI, and beyond. Match the Appearance tab exactly: `p-6`,
16
16
  * `text-lg` medium title, `text-sm` muted description.
17
17
  */
18
- export function SettingsCard({ title, description, actions, children, className, headingLevel: Heading = 'h3', }) {
18
+ export function SettingsCard({ title, description, actions, children, className, headingLevel: Heading = 'h3', busy = false, }) {
19
19
  const headingId = useId();
20
- return (_jsxs(Card, { padding: "none", "aria-labelledby": headingId, role: "region", className: className, children: [_jsxs(CardHeader, { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx(Heading, { id: headingId, className: "text-[length:var(--fs-card)] font-semibold text-[var(--txt)]", children: title }), description && _jsx(CardDescription, { children: description })] }), actions && _jsx("div", { className: "flex shrink-0 items-center gap-2", children: actions })] }), _jsx(CardContent, { className: "text-[var(--txt)]", children: children })] }));
20
+ return (_jsxs(Card, { padding: "none", "aria-labelledby": headingId, "aria-busy": busy || undefined, role: "region", className: className, children: [_jsxs(CardHeader, { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsx(Heading, { id: headingId, className: "text-[length:var(--fs-card)] font-semibold text-[var(--txt)]", children: title }), description && _jsx(CardDescription, { children: description })] }), actions && _jsx("div", { className: "flex shrink-0 items-center gap-2", children: actions })] }), _jsx(CardContent, { className: "text-[var(--txt)]", children: children })] }));
21
21
  }
22
22
  /**
23
23
  * A sub-section inside a {@link SettingsCard}: a top divider plus a
@@ -1 +1 @@
1
- {"version":3,"file":"components.js","sourceRoot":"","sources":["../../../src/views/settings/components.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ,OAAO,KAAK,MAAM,MAAM,wBAAwB,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AAClE,OAAO,EAAE,KAAK,EAAkB,MAAM,OAAO,CAAA;AAC7C,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAA;AAC5F,OAAO,EAAE,MAAM,EAAE,MAAM,+BAA+B,CAAA;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAA;AAGpE;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,EAC3B,KAAK,EACL,WAAW,EACX,OAAO,EACP,QAAQ,EACR,SAAS,EACT,YAAY,EAAE,OAAO,GAAG,IAAI,GAU7B;IACC,MAAM,SAAS,GAAG,KAAK,EAAE,CAAA;IACzB,OAAO,CACL,MAAC,IAAI,IAAC,OAAO,EAAC,MAAM,qBAAkB,SAAS,EAAE,IAAI,EAAC,QAAQ,EAAC,SAAS,EAAE,SAAS,aACjF,MAAC,UAAU,IAAC,SAAS,EAAC,wCAAwC,aAC5D,eAAK,SAAS,EAAC,SAAS,aACtB,KAAC,OAAO,IACN,EAAE,EAAE,SAAS,EACb,SAAS,EAAC,8DAA8D,YAEvE,KAAK,GACE,EACT,WAAW,IAAI,KAAC,eAAe,cAAE,WAAW,GAAmB,IAC5D,EACL,OAAO,IAAI,cAAK,SAAS,EAAC,kCAAkC,YAAE,OAAO,GAAO,IAClE,EACb,KAAC,WAAW,IAAC,SAAS,EAAC,mBAAmB,YAAE,QAAQ,GAAe,IAC9D,CACR,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,EACjC,KAAK,EACL,WAAW,EACX,QAAQ,EACR,SAAS,GAMV;IACC,OAAO,CACL,eAAK,SAAS,EAAE,+BAA+B,SAAS,IAAI,EAAE,EAAE,aAC9D,cAAK,SAAS,EAAC,uCAAuC,YAAE,KAAK,GAAO,EACnE,WAAW,IAAI,YAAG,SAAS,EAAC,sCAAsC,YAAE,WAAW,GAAK,EACpF,QAAQ,CAAC,CAAC,CAAC,cAAK,SAAS,EAAC,MAAM,YAAE,QAAQ,GAAO,CAAC,CAAC,CAAC,IAAI,IACrD,CACP,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,EACjC,KAAK,EACL,WAAW,EACX,OAAO,EACP,OAAO,EACP,OAAO,EACP,KAAK,GAAG,QAAQ,EAChB,SAAS,GAaV;IACC,MAAM,UAAU,GAAG,6CAA6C,CAAA;IAChE,OAAO,CACL,eACE,SAAS,EAAE,8BAA8B,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,aAAa,IAAI,SAAS,IAAI,EAAE,EAAE,aAEjH,eAAK,SAAS,EAAC,iCAAiC,aAC7C,OAAO,EACR,eAAK,SAAS,EAAC,SAAS,aACrB,OAAO,CAAC,CAAC,CAAC,CACT,gBAAO,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,YAC3C,KAAK,GACA,CACT,CAAC,CAAC,CAAC,CACF,eAAM,SAAS,EAAE,UAAU,YAAG,KAAK,GAAQ,CAC5C,EACA,WAAW,IAAI,YAAG,SAAS,EAAC,sCAAsC,YAAE,WAAW,GAAK,IACjF,IACF,EACN,cAAK,SAAS,EAAC,kCAAkC,YAAE,OAAO,GAAO,IAC7D,CACP,CAAA;AACH,CAAC;AAED,MAAM,YAAY,GAAqD;IACrE,MAAM,EAAE,gBAAgB;IACxB,WAAW,EAAE,qBAAqB;IAClC,QAAQ,EAAE,UAAU;IACpB,MAAM,EAAE,gBAAgB;IACxB,QAAQ,EAAE,UAAU;CACrB,CAAA;AAED,8DAA8D;AAC9D,MAAM,UAAU,mBAAmB,CAAC,EAAE,IAAI,EAAqC;IAC7E,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,UAAU,IAAI,IAAI,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAA;IACvE,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAA;IAC7B,OAAO,CACL,gBACE,SAAS,EAAC,qHAAqH,EAC/H,KAAK,EAAE,IAAI,CAAC,MAAM,aAEjB,MAAM,IAAI,KAAC,IAAI,IAAC,IAAI,EAAE,EAAE,iBAAc,MAAM,GAAG,EAC/C,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,IACrB,CACR,CAAA;AACH,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,iBAAiB,CAAC,EAChC,KAAK,EACL,WAAW,EACX,OAAO,EACP,QAAQ,EACR,QAAQ,GAAG,KAAK,EAChB,KAAK,GAQN;IACC,OAAO,CACL,eAAK,SAAS,EAAC,wCAAwC,aACrD,eAAK,SAAS,EAAC,QAAQ,aACrB,eAAK,SAAS,EAAC,yBAAyB,aACtC,eAAM,SAAS,EAAC,uCAAuC,YAAE,KAAK,GAAQ,EACrE,KAAK,IACF,EACN,YAAG,SAAS,EAAC,sCAAsC,YAAE,WAAW,GAAK,IACjE,EACN,cAAK,SAAS,EAAC,iBAAiB,YAC9B,KAAC,MAAM,IACL,OAAO,EAAE,OAAO,EAChB,eAAe,EAAE,QAAQ,EACzB,QAAQ,EAAE,QAAQ,gBACN,KAAK,GACjB,GACE,IACF,CACP,CAAA;AACH,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,yBAAyB,CAAC,EAAE,MAAM,EAAiC;IACjF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IACpC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAA;IAC3D,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAA;IAC/D,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAA;IACjC,OAAO,CACL,eACE,IAAI,EAAC,OAAO,eACF,QAAQ,EAClB,SAAS,EAAE,wDACT,OAAO;YACL,CAAC,CAAC,0DAA0D;YAC5D,CAAC,CAAC,8CACN,EAAE,aAEF,KAAC,aAAa,IAAC,IAAI,EAAE,EAAE,EAAE,SAAS,EAAC,iBAAiB,iBAAa,MAAM,GAAG,EAC1E,cAAK,SAAS,EAAC,WAAW,YACvB,CAAC,GAAG,MAAM,EAAE,GAAG,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,CAC1C,sBAAgC,KAAK,CAAC,OAAO,IAArC,GAAG,KAAK,CAAC,KAAK,IAAI,CAAC,EAAE,CAAqB,CACnD,CAAC,GACE,IACF,CACP,CAAA;AACH,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,eAAe,CAAC,EAC9B,KAAK,EACL,OAAO,EACP,SAAS,EACT,SAAS,EACT,MAAM,EACN,OAAO,GAQR;IACC,MAAM,MAAM,GAAG,SAAS,KAAK,QAAQ,CAAA;IACrC,OAAO,CACL,eAAK,SAAS,EAAC,qHAAqH,aACjI,SAAS,KAAK,SAAS,IAAI,CAAC,KAAK,IAAI,CACpC,gBAAM,SAAS,EAAC,gDAAgD,eAAW,QAAQ,aACjF,KAAC,KAAK,IAAC,IAAI,EAAE,EAAE,iBAAc,MAAM,GAAG,cACjC,CACR,EACA,KAAK,IAAI,CAAC,MAAM,IAAI,CACnB,eAAM,SAAS,EAAC,+BAA+B,eAAW,QAAQ,gCAE3D,CACR,EACD,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,CAAC,KAAK,IAAI,MAAM,EAC1B,SAAS,EAAC,8IAA8I,sBAGjJ,EACT,kBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,CAAC,OAAO,IAAI,CAAC,KAAK,IAAI,SAAS,IAAI,MAAM,EACnD,SAAS,EAAC,sQAAsQ,aAE/Q,MAAM,IAAI,KAAC,OAAO,IAAC,IAAI,EAAE,EAAE,EAAE,SAAS,EAAC,cAAc,iBAAa,MAAM,GAAG,EAC3E,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc,IAC7B,IACL,CACP,CAAA;AACH,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,6BAA6B,CAAC,EAC5C,IAAI,EACJ,KAAK,EACL,WAAW,EACX,YAAY,GAAG,UAAU,EACzB,SAAS,EACT,QAAQ,GAQT;IACC,OAAO,CACL,KAAC,MAAM,CAAC,IAAI,IAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,QAAQ,EAAE,YAC5D,MAAC,MAAM,CAAC,MAAM,IAAC,SAAS,EAAE,oBAAoB,EAAE,aAC9C,KAAC,MAAM,CAAC,OAAO,IAAC,SAAS,EAAC,gCAAgC,GAAG,EAC7D,MAAC,MAAM,CAAC,OAAO,IAAC,SAAS,EAAC,qIAAqI,aAC7J,eAAK,SAAS,EAAC,8BAA8B,aAC3C,eAAM,SAAS,EAAC,kFAAkF,YAChG,KAAC,aAAa,IAAC,IAAI,EAAE,EAAE,iBAAc,MAAM,GAAG,GACzC,EACP,KAAC,MAAM,CAAC,KAAK,IAAC,SAAS,EAAC,qCAAqC,YAAE,KAAK,GAAgB,IAChF,EACN,KAAC,MAAM,CAAC,WAAW,IAAC,SAAS,EAAC,oCAAoC,YAC/D,WAAW,GACO,EACrB,eAAK,SAAS,EAAC,qCAAqC,aAClD,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,QAAQ,EACjB,SAAS,EAAC,uGAAuG,uBAG1G,EACT,iBACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,SAAS,EAClB,SAAS,EAAC,2HAA2H,YAEpI,YAAY,GACN,IACL,IACS,IACH,GACJ,CACf,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"components.js","sourceRoot":"","sources":["../../../src/views/settings/components.tsx"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prenta/admin",
3
- "version": "1.13.0",
3
+ "version": "1.15.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -99,7 +99,7 @@
99
99
  "typescript": "^5.7.0",
100
100
  "vitest": "^4.1.0",
101
101
  "@prenta/component-blocks": "0.3.1",
102
- "@prenta/core": "5.5.0",
102
+ "@prenta/core": "5.7.0",
103
103
  "@prenta/plugin-ai": "1.0.0",
104
104
  "@prenta/plugin-blocks": "0.2.0",
105
105
  "@prenta/plugin-commerce": "0.2.1",
@@ -107,7 +107,7 @@
107
107
  "@prenta/plugin-media": "0.2.0",
108
108
  "@prenta/plugin-navigation": "0.3.0",
109
109
  "@prenta/plugin-seo": "0.1.2",
110
- "@prenta/sections-react": "0.17.34"
110
+ "@prenta/sections-react": "0.17.36"
111
111
  },
112
112
  "scripts": {
113
113
  "build": "tsc --project tsconfig.json && npx @tailwindcss/cli -i src/styles/build-input.css -o dist/prenta-admin.css --minify",
@@ -0,0 +1,76 @@
1
+ // @vitest-environment node
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import { cmsApi, getApiBase, setApiBase } from '../../lib/api.js'
5
+
6
+ function jsonResponse(status: number, body: unknown): Response {
7
+ return new Response(JSON.stringify(body), {
8
+ status,
9
+ headers: { 'Content-Type': 'application/json' },
10
+ })
11
+ }
12
+
13
+ const originalBase = getApiBase()
14
+
15
+ afterEach(() => {
16
+ vi.unstubAllGlobals()
17
+ setApiBase(originalBase)
18
+ })
19
+
20
+ describe('cmsApi', () => {
21
+ it('unwraps a 2xx { data } envelope once and reports the status', async () => {
22
+ const fetchMock = vi.fn(async () => jsonResponse(200, { data: { docs: [], total: 0 } }))
23
+ vi.stubGlobal('fetch', fetchMock)
24
+ await expect(cmsApi('/seo/autopilot/applied?page=1')).resolves.toEqual({
25
+ data: { docs: [], total: 0 },
26
+ status: 200,
27
+ })
28
+ expect(fetchMock).toHaveBeenCalledTimes(1)
29
+ const [url, init] = fetchMock.mock.calls[0] as unknown as [string, RequestInit]
30
+ expect(url).toBe('/api/cms/seo/autopilot/applied?page=1')
31
+ expect(init.credentials).toBe('include')
32
+ })
33
+
34
+ it('surfaces a non-2xx JSON body as error, code, errorBody, and status', async () => {
35
+ const body = {
36
+ error: 'metaTitle was edited since autopilot changed it',
37
+ code: 'conflict',
38
+ fields: ['metaTitle'],
39
+ }
40
+ vi.stubGlobal(
41
+ 'fetch',
42
+ vi.fn(async () => jsonResponse(409, body)),
43
+ )
44
+ await expect(cmsApi('/seo/autopilot/applied/la-1/revert', { method: 'POST' })).resolves.toEqual(
45
+ {
46
+ error: 'metaTitle was edited since autopilot changed it',
47
+ code: 'conflict',
48
+ errorBody: body,
49
+ status: 409,
50
+ },
51
+ )
52
+ })
53
+
54
+ it('falls back to a generic message and drops a non-string code on a bare non-2xx', async () => {
55
+ vi.stubGlobal(
56
+ 'fetch',
57
+ vi.fn(async () => new Response('not json', { status: 502 })),
58
+ )
59
+ await expect(cmsApi('/anything')).resolves.toEqual({
60
+ error: 'Request failed (502)',
61
+ code: undefined,
62
+ errorBody: {},
63
+ status: 502,
64
+ })
65
+ })
66
+
67
+ it('reports a network failure as status 0 without an errorBody', async () => {
68
+ vi.stubGlobal(
69
+ 'fetch',
70
+ vi.fn(async () => {
71
+ throw new Error('offline')
72
+ }),
73
+ )
74
+ await expect(cmsApi('/anything')).resolves.toEqual({ error: 'offline', status: 0 })
75
+ })
76
+ })
@@ -0,0 +1,196 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ /**
4
+ * Wire-level tests for the SEO Autopilot service wrappers. The render tests
5
+ * replace this module wholesale (`vi.mock('../../lib/seo-service.js')`), so
6
+ * URL construction, envelope unwrapping, and the 409 → `conflictFields`
7
+ * mapping are pinned here against a mocked `cmsApi`.
8
+ */
9
+ const cmsApi = vi.fn()
10
+ vi.mock('../../lib/api.js', () => ({ cmsApi: (...args: unknown[]) => cmsApi(...args) }))
11
+
12
+ const svc = await import('../../lib/seo-service.js')
13
+
14
+ const SETTINGS = {
15
+ enabled: true,
16
+ enabledById: 'u1',
17
+ pausedAt: null,
18
+ pausedReason: null,
19
+ types: { 'missing-meta-title': true },
20
+ pausedTypes: {},
21
+ minBrandAlignment: 85,
22
+ redirectMinConfidence: 85,
23
+ maxPerRun: 5,
24
+ maxPerDay: 20,
25
+ verifyFailuresPerRun: 2,
26
+ outcomeWatch: { enabled: true, days: 14, dropRatio: 0.4, minPriorClicks: 50 },
27
+ lastRunAt: null,
28
+ lastRun: null,
29
+ governance: { issueFixApprovalRequired: false, redirectApprovalRequired: false },
30
+ }
31
+
32
+ beforeEach(() => {
33
+ // Block body on purpose: `mockReset()` returns the mock, and vitest would
34
+ // run a returned function as after-test cleanup.
35
+ cmsApi.mockReset()
36
+ })
37
+
38
+ describe('autopilot settings wrappers', () => {
39
+ it('fetchAutopilotSettings GETs /seo/autopilot/settings and returns the body', async () => {
40
+ cmsApi.mockResolvedValueOnce({ status: 200, data: SETTINGS })
41
+ await expect(svc.fetchAutopilotSettings()).resolves.toEqual({ settings: SETTINGS })
42
+ expect(cmsApi).toHaveBeenLastCalledWith('/seo/autopilot/settings')
43
+ })
44
+
45
+ it('fetchAutopilotSettings surfaces a transport error and rejects a malformed body', async () => {
46
+ cmsApi.mockResolvedValueOnce({ status: 500, error: 'boom' })
47
+ await expect(svc.fetchAutopilotSettings()).resolves.toEqual({ settings: null, error: 'boom' })
48
+ cmsApi.mockResolvedValueOnce({ status: 200, data: { enabled: 'yes' } })
49
+ await expect(svc.fetchAutopilotSettings()).resolves.toEqual({
50
+ settings: null,
51
+ error: 'Unexpected autopilot settings response',
52
+ })
53
+ })
54
+
55
+ it('updateAutopilotSettings PUTs the sparse patch verbatim and adopts the echoed policy', async () => {
56
+ cmsApi.mockResolvedValueOnce({ status: 200, data: { ...SETTINGS, maxPerRun: 9 } })
57
+ await expect(
58
+ svc.updateAutopilotSettings({ maxPerRun: 9, outcomeWatch: { days: 7 } }),
59
+ ).resolves.toEqual({ ok: true, settings: { ...SETTINGS, maxPerRun: 9 } })
60
+ expect(cmsApi).toHaveBeenLastCalledWith('/seo/autopilot/settings', {
61
+ method: 'PUT',
62
+ body: JSON.stringify({ maxPerRun: 9, outcomeWatch: { days: 7 } }),
63
+ })
64
+ cmsApi.mockResolvedValueOnce({ status: 400, error: 'maxPerRun must be an integer' })
65
+ await expect(svc.updateAutopilotSettings({ maxPerRun: 99 })).resolves.toEqual({
66
+ ok: false,
67
+ error: 'maxPerRun must be an integer',
68
+ })
69
+ })
70
+
71
+ it('runAutopilotNow POSTs an empty object and returns the run summary', async () => {
72
+ const summary = {
73
+ startedAt: '2026-09-07T04:00:00Z',
74
+ durationMs: 10,
75
+ applied: 1,
76
+ verifyFailed: 0,
77
+ reverted: 0,
78
+ gated: {},
79
+ applyOutcomes: {},
80
+ generated: { generated: 1, skipped: 0, failed: 0, haltedReason: null },
81
+ halted: false,
82
+ haltedReason: null,
83
+ }
84
+ cmsApi.mockResolvedValueOnce({ status: 200, data: summary })
85
+ await expect(svc.runAutopilotNow()).resolves.toEqual({ ok: true, summary })
86
+ expect(cmsApi).toHaveBeenLastCalledWith('/seo/autopilot/run', { method: 'POST', body: '{}' })
87
+ cmsApi.mockResolvedValueOnce({ status: 403, error: 'Admin only' })
88
+ await expect(svc.runAutopilotNow()).resolves.toEqual({ ok: false, error: 'Admin only' })
89
+ })
90
+ })
91
+
92
+ describe('fetchAutopilotApplied', () => {
93
+ const list = { docs: [], total: 0, page: 2, pageSize: 20, canRevert: true }
94
+
95
+ it('encodes page, pageSize, and the optional filters in that order', async () => {
96
+ cmsApi.mockResolvedValueOnce({ status: 200, data: list })
97
+ await expect(
98
+ svc.fetchAutopilotApplied({ page: 2, pageSize: 20, status: 'applied' }),
99
+ ).resolves.toEqual(list)
100
+ expect(cmsApi).toHaveBeenLastCalledWith(
101
+ '/seo/autopilot/applied?page=2&pageSize=20&status=applied',
102
+ )
103
+ cmsApi.mockResolvedValueOnce({ status: 200, data: list })
104
+ await svc.fetchAutopilotApplied({ page: 1, pageSize: 20, outcome: 'regressed' })
105
+ expect(cmsApi).toHaveBeenLastCalledWith(
106
+ '/seo/autopilot/applied?page=1&pageSize=20&outcome=regressed',
107
+ )
108
+ })
109
+
110
+ it('always sends page and pageSize even when no filter is set', async () => {
111
+ cmsApi.mockResolvedValueOnce({ status: 200, data: list })
112
+ await svc.fetchAutopilotApplied({ page: 1, pageSize: 20 })
113
+ expect(cmsApi).toHaveBeenLastCalledWith('/seo/autopilot/applied?page=1&pageSize=20')
114
+ })
115
+
116
+ it('throws on a transport error and falls back to an empty page on a missing body', async () => {
117
+ cmsApi.mockResolvedValueOnce({ status: 501, error: 'Run prisma migrate deploy' })
118
+ await expect(svc.fetchAutopilotApplied({ page: 1, pageSize: 20 })).rejects.toThrow(
119
+ 'Run prisma migrate deploy',
120
+ )
121
+ cmsApi.mockResolvedValueOnce({ status: 200 })
122
+ await expect(svc.fetchAutopilotApplied({ page: 3, pageSize: 10 })).resolves.toEqual({
123
+ docs: [],
124
+ total: 0,
125
+ page: 3,
126
+ pageSize: 10,
127
+ canRevert: false,
128
+ })
129
+ })
130
+ })
131
+
132
+ describe('revertAutopilotChange', () => {
133
+ it('POSTs { force } to the encoded revert path and resolves ok', async () => {
134
+ cmsApi.mockResolvedValueOnce({ status: 200, data: { reverted: true } })
135
+ await expect(svc.revertAutopilotChange('la/1')).resolves.toEqual({ ok: true })
136
+ expect(cmsApi).toHaveBeenLastCalledWith('/seo/autopilot/applied/la%2F1/revert', {
137
+ method: 'POST',
138
+ body: JSON.stringify({ force: false }),
139
+ })
140
+ cmsApi.mockResolvedValueOnce({ status: 200, data: { reverted: true } })
141
+ await svc.revertAutopilotChange('la-1', true)
142
+ expect(cmsApi).toHaveBeenLastCalledWith('/seo/autopilot/applied/la-1/revert', {
143
+ method: 'POST',
144
+ body: JSON.stringify({ force: true }),
145
+ })
146
+ })
147
+
148
+ it('maps a 409 conflict body to conflictFields', async () => {
149
+ const error = 'metaTitle was edited since autopilot changed it'
150
+ cmsApi.mockResolvedValueOnce({
151
+ ok: false,
152
+ error,
153
+ code: 'conflict',
154
+ status: 409,
155
+ errorBody: { error, code: 'conflict', fields: ['metaTitle'] },
156
+ })
157
+ await expect(svc.revertAutopilotChange('la-1')).resolves.toEqual({
158
+ ok: false,
159
+ error,
160
+ conflictFields: ['metaTitle'],
161
+ })
162
+ })
163
+
164
+ it('does not report conflictFields when the error is not a conflict', async () => {
165
+ cmsApi.mockResolvedValueOnce({
166
+ ok: false,
167
+ error: 'Not found',
168
+ code: undefined,
169
+ status: 404,
170
+ errorBody: { error: 'Not found', fields: ['metaTitle'] },
171
+ })
172
+ const res = await svc.revertAutopilotChange('la-1')
173
+ expect(res).toEqual({ ok: false, error: 'Not found' })
174
+ expect(res).not.toHaveProperty('conflictFields')
175
+ })
176
+
177
+ it('filters non-string entries out of a conflict field list and tolerates a missing list', async () => {
178
+ cmsApi.mockResolvedValueOnce({
179
+ error: 'conflict',
180
+ code: 'conflict',
181
+ status: 409,
182
+ errorBody: { fields: ['metaTitle', 42, null, { name: 'x' }, 'metaDescription'] },
183
+ })
184
+ await expect(svc.revertAutopilotChange('la-1')).resolves.toEqual({
185
+ ok: false,
186
+ error: 'conflict',
187
+ conflictFields: ['metaTitle', 'metaDescription'],
188
+ })
189
+ cmsApi.mockResolvedValueOnce({ error: 'conflict', code: 'conflict', status: 409 })
190
+ await expect(svc.revertAutopilotChange('la-1')).resolves.toEqual({
191
+ ok: false,
192
+ error: 'conflict',
193
+ conflictFields: [],
194
+ })
195
+ })
196
+ })
@@ -0,0 +1,108 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ const cmsApi = vi.fn()
4
+ vi.mock('../../lib/api.js', () => ({ cmsApi: (...args: unknown[]) => cmsApi(...args) }))
5
+
6
+ const svc = await import('../../lib/seo-service.js')
7
+
8
+ beforeEach(() => {
9
+ // Block body on purpose: `mockReset()` returns the mock, and vitest would
10
+ // run a returned function as after-test cleanup.
11
+ cmsApi.mockReset()
12
+ })
13
+
14
+ describe('proposal service helpers', () => {
15
+ it('fetchSeoProposals GETs /seo/proposals and throws on error', async () => {
16
+ cmsApi.mockResolvedValueOnce({ status: 500, error: 'boom' })
17
+ await expect(svc.fetchSeoProposals()).rejects.toThrow('boom')
18
+ cmsApi.mockResolvedValueOnce({
19
+ status: 200,
20
+ data: {
21
+ proposals: [],
22
+ counts: { issueFixes: 0, redirects: 0, stale: 0, eligibleForGeneration: 0 },
23
+ governance: { issueFixBulk: false, redirectBulk: false },
24
+ },
25
+ })
26
+ await expect(svc.fetchSeoProposals()).resolves.toMatchObject({ proposals: [] })
27
+ expect(cmsApi).toHaveBeenLastCalledWith('/seo/proposals')
28
+ })
29
+
30
+ it('fetchSeoProposals fills in autopilot when an older core omits it', async () => {
31
+ cmsApi.mockResolvedValueOnce({
32
+ status: 200,
33
+ data: {
34
+ proposals: [],
35
+ counts: { issueFixes: 0, redirects: 0, stale: 0, eligibleForGeneration: 0 },
36
+ governance: { issueFixBulk: true, redirectBulk: false },
37
+ },
38
+ })
39
+ await expect(svc.fetchSeoProposals()).resolves.toEqual({
40
+ proposals: [],
41
+ counts: { issueFixes: 0, redirects: 0, stale: 0, eligibleForGeneration: 0 },
42
+ governance: { issueFixBulk: true, redirectBulk: false },
43
+ autopilot: { enabled: false, pausedReason: null, lastRunAt: null, appliedLast7d: 0 },
44
+ })
45
+ // A server that does send it wins over the fallback.
46
+ const autopilot = { enabled: true, pausedReason: null, lastRunAt: null, appliedLast7d: 3 }
47
+ cmsApi.mockResolvedValueOnce({
48
+ status: 200,
49
+ data: {
50
+ proposals: [],
51
+ counts: { issueFixes: 0, redirects: 0, stale: 0, eligibleForGeneration: 0 },
52
+ governance: { issueFixBulk: true, redirectBulk: false },
53
+ autopilot,
54
+ },
55
+ })
56
+ await expect(svc.fetchSeoProposals()).resolves.toMatchObject({ autopilot })
57
+ })
58
+
59
+ it('generateSeoProposals POSTs confirm: true and returns the result or error', async () => {
60
+ cmsApi.mockResolvedValueOnce({
61
+ status: 200,
62
+ data: { generated: 3, skipped: 1, failed: 0, remaining: 2, haltedReason: null },
63
+ })
64
+ await expect(svc.generateSeoProposals()).resolves.toEqual({
65
+ result: { generated: 3, skipped: 1, failed: 0, remaining: 2, haltedReason: null },
66
+ })
67
+ expect(cmsApi).toHaveBeenCalledWith('/seo/proposals/generate', {
68
+ method: 'POST',
69
+ body: JSON.stringify({ confirm: true }),
70
+ })
71
+ cmsApi.mockResolvedValueOnce({ status: 429, error: 'AI rate limit reached.' })
72
+ await expect(svc.generateSeoProposals()).resolves.toEqual({ error: 'AI rate limit reached.' })
73
+ })
74
+
75
+ it('bulkApplySeoProposals flags governance_blocked distinctly', async () => {
76
+ cmsApi.mockResolvedValueOnce({
77
+ status: 403,
78
+ error: 'Enable "Allow bulk apply"',
79
+ code: 'governance_blocked',
80
+ })
81
+ await expect(
82
+ svc.bulkApplySeoProposals([{ kind: 'issue-fix', id: 'i1', fingerprint: 'f' }]),
83
+ ).resolves.toEqual({ error: 'Enable "Allow bulk apply"', governanceBlocked: true })
84
+ cmsApi.mockResolvedValueOnce({
85
+ status: 200,
86
+ data: {
87
+ results: [{ kind: 'issue-fix', id: 'i1', status: 'applied' }],
88
+ applied: 1,
89
+ failed: 0,
90
+ pending: 0,
91
+ },
92
+ })
93
+ await expect(svc.bulkApplySeoProposals([{ kind: 'issue-fix', id: 'i1' }])).resolves.toEqual({
94
+ result: {
95
+ results: [{ kind: 'issue-fix', id: 'i1', status: 'applied' }],
96
+ applied: 1,
97
+ failed: 0,
98
+ pending: 0,
99
+ },
100
+ })
101
+ })
102
+
103
+ it('dismissSeoIssueFix POSTs to the encoded issue path', async () => {
104
+ cmsApi.mockResolvedValueOnce({ status: 200, data: { dismissed: true } })
105
+ await expect(svc.dismissSeoIssueFix('a/b')).resolves.toEqual({})
106
+ expect(cmsApi).toHaveBeenCalledWith('/seo/issues/a%2Fb/dismiss-fix', { method: 'POST' })
107
+ })
108
+ })
@@ -18,6 +18,7 @@ describe('seoTabForPath', () => {
18
18
  expect(seoTabForPath('/seo/links')).toBe('technical')
19
19
  expect(seoTabForPath('/seo/redirects')).toBe('redirects')
20
20
  expect(seoTabForPath('/seo/audit')).toBe('audit')
21
+ expect(seoTabForPath('/seo/proposals')).toBe('proposals')
21
22
  })
22
23
 
23
24
  it('returns null for non-SEO paths so they fall through', () => {
@@ -36,9 +36,8 @@ const fetchRedirectRules = vi.fn(async () => RULES)
36
36
  const createRedirectRule = vi.fn(async (_p: unknown) => ({}))
37
37
  const updateRedirectRule = vi.fn(async (_id: string, _p: unknown) => ({}))
38
38
  const deleteRedirectRule = vi.fn(async (_id: string) => ({}))
39
- const generate404RecoverySuggestions = vi.fn(
40
- async (): Promise<{ error?: string; autoApplied?: number; governanceBlocked?: string }> => ({}),
41
- )
39
+ const generate404RecoverySuggestions = vi.fn(async (): Promise<{ error?: string }> => ({}))
40
+ const saveRedirectRecoverySettings = vi.fn(async (_p: unknown): Promise<{ error?: string }> => ({}))
42
41
 
43
42
  vi.mock('../../lib/seo-service.js', () => ({
44
43
  fetchRedirectOverview: vi.fn(async () => ({
@@ -63,9 +62,9 @@ vi.mock('../../lib/seo-service.js', () => ({
63
62
  fetchRedirectRecoverySettings: vi.fn(async () => ({
64
63
  aiRerank: false,
65
64
  autoApply: false,
66
- autoApplyMinConfidence: 0.85,
65
+ autoApplyMinConfidence: 85,
67
66
  })),
68
- saveRedirectRecoverySettings: vi.fn(async () => ({})),
67
+ saveRedirectRecoverySettings: (p: unknown) => saveRedirectRecoverySettings(p),
69
68
  }))
70
69
 
71
70
  vi.mock('sonner', () => ({
@@ -79,6 +78,8 @@ beforeEach(() => {
79
78
  createRedirectRule.mockClear()
80
79
  updateRedirectRule.mockClear()
81
80
  deleteRedirectRule.mockClear()
81
+ generate404RecoverySuggestions.mockClear()
82
+ saveRedirectRecoverySettings.mockClear()
82
83
  })
83
84
 
84
85
  describe('Redirects inline editor pane', () => {
@@ -168,23 +169,42 @@ describe('Redirects inline editor pane', () => {
168
169
  })
169
170
  })
170
171
 
171
- describe('AI 404 recovery governance feedback', () => {
172
- it('explains why auto-apply was skipped when governance blocked it', async () => {
173
- generate404RecoverySuggestions.mockResolvedValueOnce({
174
- autoApplied: 0,
175
- governanceBlocked:
176
- 'The "404 Recovery suggestions" feature requires human approval before AI results are applied.',
177
- })
172
+ describe('404 recovery settings', () => {
173
+ it('points auto-apply at SEO Autopilot instead of exposing its own controls', async () => {
174
+ const onNavigate = vi.fn()
175
+ render(<RedirectsTab onNavigate={onNavigate} />)
176
+ await screen.findByRole('button', { name: '/old-path' })
177
+
178
+ // Task 8 moved the auto-apply policy into the autopilot; the card keeps
179
+ // AI re-ranking only and links to the Autopilot settings card.
180
+ expect(screen.getByLabelText('AI re-ranking')).toBeTruthy()
181
+ expect(screen.queryByLabelText(/Auto-apply high-confidence redirects/)).toBeNull()
182
+ expect(screen.queryByLabelText(/Auto-apply minimum confidence/)).toBeNull()
183
+ fireEvent.click(screen.getByRole('button', { name: 'SEO Autopilot' }))
184
+ expect(onNavigate).toHaveBeenCalledWith('/settings?tab=seo')
185
+ expect(screen.queryByRole('link')).toBeNull()
186
+ })
187
+
188
+ it('saves only the AI re-ranking flag', async () => {
189
+ render(<RedirectsTab />)
190
+ await screen.findByRole('button', { name: '/old-path' })
191
+ fireEvent.click(screen.getByLabelText('AI re-ranking'))
192
+ fireEvent.click(screen.getByRole('button', { name: 'Save settings' }))
193
+ await waitFor(() =>
194
+ expect(saveRedirectRecoverySettings).toHaveBeenCalledWith({ aiRerank: true }),
195
+ )
196
+ })
197
+
198
+ it('reports generation success without the retired auto-apply / governance messages', async () => {
199
+ generate404RecoverySuggestions.mockResolvedValueOnce({})
178
200
  const { toast } = await import('sonner')
179
201
 
180
202
  render(<RedirectsTab />)
181
203
  fireEvent.click(await screen.findByRole('button', { name: /Generate suggestions/ }))
182
204
 
183
- // Generation succeeded — suggestions exist — but the operator is told the
184
- // governance reason instead of silently seeing nothing auto-applied.
185
205
  await waitFor(() =>
186
- expect(toast.info).toHaveBeenCalledWith(expect.stringContaining('requires human approval')),
206
+ expect(toast.success).toHaveBeenCalledWith('Generated 404 recovery suggestions.'),
187
207
  )
188
- expect(toast.success).toHaveBeenCalledWith('Generated 404 recovery suggestions.')
208
+ expect(toast.info).not.toHaveBeenCalled()
189
209
  })
190
210
  })