Learn Page and Content Pipeline Update #20

Merged
an.di merged 20 commits from adiallo/feature/LearnPage into main 2025-10-01 03:15:23 +00:00
8 changed files with 228 additions and 77 deletions
Showing only changes of commit b15f913a14 - Show all commits

No files matched your search

+5 -1
View File
@@ -14,10 +14,14 @@ export default function ContentBanner({ post }) {
};
const getBannerImageMd = (post) => {
// Use banner.horizontal when provided; fallback to default banner asset
// Use banner.horizontal when provided; fallback to horizontal thumbnail
if (post.frontmatter?.banner?.horizontal) {
return `/content/blog/${post.frontmatter.banner.horizontal}`;
}
// Fallback to horizontal thumbnail, then default banner
if (post.frontmatter?.thumbnail?.horizontal) {
return `/content/blog/${post.frontmatter.thumbnail.horizontal}`;
}
return getAssetPath("assets/Content_Banner_2.svg");
};
@@ -8,6 +8,8 @@ related:
thumbnail:
vertical: "resolving-active-conflicts-vertical.svg"
horizontal: "resolving-active-conflicts-horizontal.svg"
banner:
horizontal: "resolving-active-conflicts-banner.svg"
background:
color: "#E2EFFF"
---
@@ -0,0 +1,9 @@
<svg width="1024" height="358" viewBox="0 0 1024 358" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="1024" height="358.4" fill="url(#pattern0_18996_22618)"/>
<defs>
<pattern id="pattern0_18996_22618" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_18996_22618" transform="scale(0.000260417 0.000744048)"/>
</pattern>
<image id="image0_18996_22618" width="3840" height="1344" preserveAspectRatio="none" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 82 KiB

+33 -1
View File
@@ -8,6 +8,13 @@ const mockBlogPost = {
"This is a sample article description that explains what the article covers.",
author: "Sample Author",
date: "2025-01-15",
thumbnail: {
horizontal: "resolving-active-conflicts-horizontal.svg",
vertical: "resolving-active-conflicts-vertical.svg",
},
banner: {
horizontal: "resolving-active-conflicts-banner.svg",
},
},
htmlContent:
"<p>This is the main content of the sample article.</p><p>It has multiple paragraphs.</p>",
@@ -20,7 +27,7 @@ export default {
docs: {
description: {
component:
"The ContentBanner component displays the header information for blog articles, including title, description, author, and date. Note: page background colors are applied at the blog page level using a hex color from frontmatter (background.color), not inside this component. Thumbnail images should be uploaded via the content pipeline to public/content/blog/ and referenced in frontmatter (thumbnail.horizontal / thumbnail.vertical).",
"The ContentBanner component displays the header information for blog articles, including title, description, author, and date.\n\nImages: sm uses thumbnail.horizontal; md+ uses banner.horizontal when provided, otherwise falls back to thumbnail.horizontal; final fallback is assets/Content_Banner_2.svg.\n\nNote: page background colors are applied at the blog page level using a hex color from frontmatter (background.color), not inside this component. Thumbnail and banner images should be uploaded via the content pipeline to public/content/blog/ and referenced in frontmatter.",
},
},
},
@@ -38,6 +45,31 @@ export const Default = {
},
};
export const NoBannerFallbackToThumbnail = {
args: {
post: {
...mockBlogPost,
frontmatter: {
...mockBlogPost.frontmatter,
banner: undefined,
},
},
},
};
export const NoImagesFallbackToDefault = {
args: {
post: {
...mockBlogPost,
frontmatter: {
...mockBlogPost.frontmatter,
thumbnail: undefined,
banner: undefined,
},
},
},
};
export const LongTitle = {
args: {
post: {
+64 -19
View File
@@ -30,6 +30,12 @@ const mockPost = {
description: "This is a test article description",
author: "Test Author",
date: "2025-04-15",
thumbnail: {
horizontal: "test-article-horizontal.svg",
},
banner: {
horizontal: "test-article-banner.svg",
},
},
};
@@ -39,7 +45,7 @@ describe("ContentBanner", () => {
// Check that the banner container exists - it's the first div with the specific classes
const banner = document.querySelector(
"div[class*='pt-[var(--measures-spacing-016)]']",
"div[class*='pt-[var(--measures-spacing-016)]']"
);
expect(banner).toBeInTheDocument();
expect(banner).toHaveClass(
@@ -54,7 +60,7 @@ describe("ContentBanner", () => {
"xl:h-[504px]",
"relative",
"w-full",
"sm:overflow-hidden",
"sm:overflow-hidden"
);
});
@@ -63,7 +69,7 @@ describe("ContentBanner", () => {
// Check for background div with correct styling
const backgroundDiv = document.querySelector(
"div[style*='background-image']",
"div[style*='background-image']"
);
expect(backgroundDiv).toBeInTheDocument();
expect(backgroundDiv).toHaveClass(
@@ -73,16 +79,16 @@ describe("ContentBanner", () => {
"h-full",
"bg-cover",
"bg-no-repeat",
"aspect-[320/225.5]",
"aspect-[320/225.5]"
);
});
it("shows different background image at md breakpoint and above", () => {
it("shows banner image at md breakpoint and above", () => {
render(<ContentBanner post={mockPost} />);
// Check for the md+ background div
// Check for the md+ background div with banner image
const mdBackgroundDiv = document.querySelector(
"div[style*='Content_Banner_2.svg']",
"div[style*='test-article-banner.svg']"
);
expect(mdBackgroundDiv).toBeInTheDocument();
expect(mdBackgroundDiv).toHaveClass("hidden", "md:block");
@@ -98,7 +104,7 @@ describe("ContentBanner", () => {
render(<ContentBanner post={mockPost} />);
expect(
screen.getByText("This is a test article description"),
screen.getByText("This is a test article description")
).toBeInTheDocument();
});
@@ -114,7 +120,7 @@ describe("ContentBanner", () => {
// Check the content container div
const contentContainer = document.querySelector(
"div[class*='relative z-10']",
"div[class*='relative z-10']"
);
expect(contentContainer).toBeInTheDocument();
expect(contentContainer).toHaveClass(
@@ -122,7 +128,7 @@ describe("ContentBanner", () => {
"z-10",
"h-full",
"flex",
"flex-col",
"flex-col"
);
});
@@ -135,7 +141,7 @@ describe("ContentBanner", () => {
"font-medium",
"text-[18px]",
"leading-[120%]",
"text-[var(--color-content-inverse-brand-royal)]",
"text-[var(--color-content-inverse-brand-royal)]"
);
const description = screen.getByText("This is a test article description");
@@ -144,7 +150,7 @@ describe("ContentBanner", () => {
"font-normal",
"text-[12px]",
"leading-[16px]",
"text-[var(--color-content-inverse-brand-royal)]",
"text-[var(--color-content-inverse-brand-royal)]"
);
});
@@ -157,7 +163,7 @@ describe("ContentBanner", () => {
"font-normal",
"text-[10px]",
"leading-[14px]",
"text-[var(--color-content-inverse-brand-royal)]",
"text-[var(--color-content-inverse-brand-royal)]"
);
const date = screen.getByText("April 2025");
@@ -166,7 +172,7 @@ describe("ContentBanner", () => {
"font-normal",
"text-[10px]",
"leading-[14px]",
"text-[var(--color-content-inverse-brand-royal)]",
"text-[var(--color-content-inverse-brand-royal)]"
);
});
@@ -175,7 +181,7 @@ describe("ContentBanner", () => {
// Check the ContentContainer spacing
const contentContainer = document.querySelector(
"div[class*='relative z-20']",
"div[class*='relative z-20']"
);
expect(contentContainer).toHaveClass("gap-[var(--measures-spacing-012)]");
});
@@ -184,13 +190,13 @@ describe("ContentBanner", () => {
render(<ContentBanner post={mockPost} />);
const outerContainer = document.querySelector(
"div[class*='pt-[var(--measures-spacing-016)]']",
"div[class*='pt-[var(--measures-spacing-016)]']"
);
expect(outerContainer).toHaveClass(
"pt-[var(--measures-spacing-016)]",
"md:pt-[var(--measures-spacing-008)]",
"lg:pt-[50px]",
"xl:pt-[112px]",
"xl:pt-[112px]"
);
});
@@ -208,6 +214,45 @@ describe("ContentBanner", () => {
expect(screen.getByText("Incomplete Post")).toBeInTheDocument();
});
it("falls back to thumbnail.horizontal when banner.horizontal is missing", () => {
const postWithoutBanner = {
...mockPost,
frontmatter: {
...mockPost.frontmatter,
banner: undefined,
},
};
render(<ContentBanner post={postWithoutBanner} />);
// Should use thumbnail.horizontal for md+ breakpoint
const mdBackgroundDiv = document.querySelector(
"div[style*='test-article-horizontal.svg']"
);
expect(mdBackgroundDiv).toBeInTheDocument();
expect(mdBackgroundDiv).toHaveClass("hidden", "md:block");
});
it("falls back to default banner when no images are provided", () => {
const postWithoutImages = {
...mockPost,
frontmatter: {
...mockPost.frontmatter,
thumbnail: undefined,
banner: undefined,
},
};
render(<ContentBanner post={postWithoutImages} />);
// Should use default banner for md+ breakpoint
const mdBackgroundDiv = document.querySelector(
"div[style*='Content_Banner_2.svg']"
);
expect(mdBackgroundDiv).toBeInTheDocument();
expect(mdBackgroundDiv).toHaveClass("hidden", "md:block");
});
it("applies responsive text sizing", () => {
render(<ContentBanner post={mockPost} />);
@@ -216,7 +261,7 @@ describe("ContentBanner", () => {
"sm:text-[24px]",
"md:text-[32px]",
"lg:text-[44px]",
"xl:text-[64px]",
"xl:text-[64px]"
);
const description = screen.getByText("This is a test article description");
@@ -224,7 +269,7 @@ describe("ContentBanner", () => {
"sm:text-[14px]",
"md:text-[14px]",
"lg:text-[18px]",
"xl:text-[24px]",
"xl:text-[24px]"
);
});
+41 -12
View File
@@ -28,21 +28,24 @@ const mockPost = {
"This is a test description for the blog post that should be long enough to test truncation.",
author: "Test Author",
date: "2025-04-15",
backgroundImages: ["/test-image-1.jpg", "/test-image-2.jpg"],
thumbnail: {
vertical: "test-post-vertical.svg",
horizontal: "test-post-horizontal.svg",
},
},
};
describe("ContentThumbnailTemplate", () => {
describe("Vertical Variant", () => {
it("should render vertical variant with correct dimensions", () => {
it("should render vertical variant with responsive dimensions", () => {
render(<ContentThumbnailTemplate post={mockPost} />);
const container = screen.getByRole("link");
expect(container).toBeInTheDocument();
// Check that the component has the correct classes for dimensions
// Check that the component has the correct classes for responsive dimensions
const thumbnailDiv = container.querySelector("div");
expect(thumbnailDiv).toHaveClass("w-[260px]", "h-[390px]");
expect(thumbnailDiv).toHaveClass("w-full", "aspect-[2/3]");
});
it("should display post title and description", () => {
@@ -50,7 +53,7 @@ describe("ContentThumbnailTemplate", () => {
expect(screen.getByText("Test Blog Post Title")).toBeInTheDocument();
expect(
screen.getByText(/This is a test description/),
screen.getByText(/This is a test description/)
).toBeInTheDocument();
});
@@ -63,7 +66,7 @@ describe("ContentThumbnailTemplate", () => {
});
describe("Horizontal Variant", () => {
it("should render horizontal variant", () => {
it("should render horizontal variant with responsive sizing", () => {
render(<ContentThumbnailTemplate post={mockPost} variant="horizontal" />);
const container = screen.getByRole("link");
@@ -71,7 +74,11 @@ describe("ContentThumbnailTemplate", () => {
// Check that the component has the correct classes for horizontal layout
const thumbnailDiv = container.querySelector("div");
expect(thumbnailDiv).toHaveClass("h-[225.5px]");
expect(thumbnailDiv).toHaveClass(
"min-w-[320px]",
"max-w-[800px]",
"h-[225.5px]"
);
});
it("should display post information in horizontal layout", () => {
@@ -79,7 +86,7 @@ describe("ContentThumbnailTemplate", () => {
expect(screen.getByText("Test Blog Post Title")).toBeInTheDocument();
expect(
screen.getByText(/This is a test description/),
screen.getByText(/This is a test description/)
).toBeInTheDocument();
expect(screen.getByText("Test Author")).toBeInTheDocument();
});
@@ -88,7 +95,7 @@ describe("ContentThumbnailTemplate", () => {
describe("Props and Customization", () => {
it("should apply custom className", () => {
render(
<ContentThumbnailTemplate post={mockPost} className="custom-class" />,
<ContentThumbnailTemplate post={mockPost} className="custom-class" />
);
const container = screen.getByRole("link");
@@ -117,12 +124,12 @@ describe("ContentThumbnailTemplate", () => {
expect(screen.getByText("Test Blog Post Title")).toBeInTheDocument();
});
it("should handle posts without background images", () => {
it("should handle posts without thumbnail images", () => {
const postWithoutImages = {
...mockPost,
frontmatter: {
...mockPost.frontmatter,
backgroundImages: undefined,
thumbnail: undefined,
},
};
@@ -131,6 +138,28 @@ describe("ContentThumbnailTemplate", () => {
// Should still render without errors
expect(screen.getByText("Test Blog Post Title")).toBeInTheDocument();
});
it("should use article-specific thumbnail images when provided", () => {
render(<ContentThumbnailTemplate post={mockPost} />);
// Check that the background image uses the article-specific thumbnail
const backgroundImg = document.querySelector(
"img[alt*='Background for']"
);
expect(backgroundImg).toBeInTheDocument();
expect(backgroundImg.src).toContain("test-post-vertical.svg");
});
it("should use article-specific horizontal images for horizontal variant", () => {
render(<ContentThumbnailTemplate post={mockPost} variant="horizontal" />);
// Check that the background image uses the article-specific horizontal thumbnail
const backgroundImg = document.querySelector(
"img[alt*='Background for']"
);
expect(backgroundImg).toBeInTheDocument();
expect(backgroundImg.src).toContain("test-post-horizontal.svg");
});
});
describe("Default Behavior", () => {
@@ -138,7 +167,7 @@ describe("ContentThumbnailTemplate", () => {
render(<ContentThumbnailTemplate post={mockPost} />);
const thumbnailDiv = screen.getByRole("link").querySelector("div");
expect(thumbnailDiv).toHaveClass("w-[260px]", "h-[390px]");
expect(thumbnailDiv).toHaveClass("w-full", "aspect-[2/3]");
});
it("should show metadata by default", () => {
+34 -43
View File
@@ -8,58 +8,55 @@ import Header, {
} from "../../app/components/Header.js";
describe("Header", () => {
const mockOnToggle = vi.fn();
beforeEach(() => {
mockOnToggle.mockClear();
// Clear any existing rendered content
document.body.innerHTML = "";
});
describe("Accessibility and Landmarks", () => {
test("renders header with correct structure and accessibility attributes", () => {
const { container } = render(<Header onToggle={mockOnToggle} />);
const { container } = render(<Header />);
// Check main header structure - use container to scope the search
const header = container.querySelector(
'[role="banner"][aria-label="Main navigation header"]',
'[role="banner"][aria-label="Main navigation header"]'
);
expect(header).toBeInTheDocument();
expect(header).toHaveAttribute("aria-label", "Main navigation header");
// Check navigation - use container to scope the search
const nav = container.querySelector(
'[role="navigation"][aria-label="Main navigation"]',
'[role="navigation"][aria-label="Main navigation"]'
);
expect(nav).toBeInTheDocument();
expect(nav).toHaveAttribute("aria-label", "Main navigation");
});
test("renders all navigation items with proper accessibility", () => {
render(<Header onToggle={mockOnToggle} />);
render(<Header />);
// Check all navigation items have proper aria-labels - use menuitem role since they're in a menubar
expect(
screen.getAllByRole("menuitem", { name: "Navigate to Use cases page" })
.length,
.length
).toBeGreaterThan(0);
expect(
screen.getAllByRole("menuitem", { name: "Navigate to Learn page" })
.length,
.length
).toBeGreaterThan(0);
expect(
screen.getAllByRole("menuitem", { name: "Navigate to About page" })
.length,
.length
).toBeGreaterThan(0);
});
});
describe("Schema Markup", () => {
test("renders schema markup for site navigation", () => {
render(<Header onToggle={mockOnToggle} />);
render(<Header />);
const script = document.querySelector(
'script[type="application/ld+json"]',
'script[type="application/ld+json"]'
);
expect(script).toBeInTheDocument();
@@ -126,14 +123,14 @@ describe("Header", () => {
describe("Logo Configuration", () => {
test("renders correct number of logo variants", () => {
render(<Header onToggle={mockOnToggle} />);
render(<Header />);
const logoWrappers = screen.getAllByTestId("logo-wrapper");
expect(logoWrappers).toHaveLength(logoConfig.length);
});
test("logo wrappers include expected breakpoint classes", () => {
render(<Header onToggle={mockOnToggle} />);
render(<Header />);
const logoWrappers = screen.getAllByTestId("logo-wrapper");
@@ -156,7 +153,7 @@ describe("Header", () => {
describe("Navigation Structure", () => {
test("renders all breakpoint-specific navigation containers", () => {
render(<Header onToggle={mockOnToggle} />);
render(<Header />);
expect(screen.getByTestId("nav-xs")).toBeInTheDocument();
expect(screen.getByTestId("nav-sm")).toBeInTheDocument();
@@ -166,7 +163,7 @@ describe("Header", () => {
});
test("navigation containers use expected breakpoint classes", () => {
render(<Header onToggle={mockOnToggle} />);
render(<Header />);
// XSmall navigation
const navXs = screen.getByTestId("nav-xs");
@@ -190,7 +187,7 @@ describe("Header", () => {
});
test("renders navigation items with correct text and links", () => {
render(<Header onToggle={mockOnToggle} />);
render(<Header />);
// Check navigation items
expect(screen.getAllByText("Use cases").length).toBeGreaterThan(0);
@@ -199,7 +196,7 @@ describe("Header", () => {
});
test("renders multiple instances of navigation items for responsive design", () => {
render(<Header onToggle={mockOnToggle} />);
render(<Header />);
// Should have multiple instances of navigation items for different breakpoints
const useCasesLinks = screen.getAllByText("Use cases");
@@ -214,7 +211,7 @@ describe("Header", () => {
describe("Authentication Structure", () => {
test("renders all breakpoint-specific auth containers", () => {
render(<Header onToggle={mockOnToggle} />);
render(<Header />);
expect(screen.getByTestId("auth-xs")).toBeInTheDocument();
expect(screen.getByTestId("auth-sm")).toBeInTheDocument();
@@ -224,7 +221,7 @@ describe("Header", () => {
});
test("auth containers use expected breakpoint classes", () => {
render(<Header onToggle={mockOnToggle} />);
render(<Header />);
// XSmall auth
const authXs = screen.getByTestId("auth-xs");
@@ -248,7 +245,7 @@ describe("Header", () => {
});
test("renders login button with correct accessibility", () => {
render(<Header onToggle={mockOnToggle} />);
render(<Header />);
const loginLinks = screen.getAllByRole("menuitem", {
name: "Log in to your account",
@@ -258,7 +255,7 @@ describe("Header", () => {
});
test("renders multiple login buttons for responsive design", () => {
render(<Header onToggle={mockOnToggle} />);
render(<Header />);
// Should have multiple login buttons for different breakpoints
const loginButtons = screen.getAllByText("Log in");
@@ -266,7 +263,7 @@ describe("Header", () => {
});
test("renders create rule button with avatar decoration", () => {
render(<Header onToggle={mockOnToggle} />);
render(<Header />);
const createRuleButtons = screen.getAllByRole("button", {
name: "Create a new rule with avatar decoration",
@@ -276,7 +273,7 @@ describe("Header", () => {
});
test("renders multiple create rule buttons for responsive design", () => {
render(<Header onToggle={mockOnToggle} />);
render(<Header />);
// Should have multiple create rule buttons for different breakpoints
const createRuleButtons = screen.getAllByText("Create rule");
@@ -286,7 +283,7 @@ describe("Header", () => {
describe("Avatar Images", () => {
test("renders avatar images with correct attributes", () => {
render(<Header onToggle={mockOnToggle} />);
render(<Header />);
const avatars = screen.getAllByRole("img");
expect(avatars.length).toBeGreaterThan(0);
@@ -296,45 +293,39 @@ describe("Header", () => {
(img) =>
img.alt === "Avatar 1" ||
img.alt === "Avatar 2" ||
img.alt === "Avatar 3",
img.alt === "Avatar 3"
);
expect(avatarImages.length).toBeGreaterThan(0);
});
});
describe("User Interactions", () => {
test("calls onToggle when navigation items are clicked", async () => {
const user = userEvent.setup();
render(<Header onToggle={mockOnToggle} />);
describe("Sticky Header Behavior", () => {
test("applies sticky positioning classes", () => {
const { container } = render(<Header />);
// Find and click a navigation item - use menuitem role since they're in a menubar
const useCasesLinks = screen.getAllByRole("menuitem", {
name: "Navigate to Use cases page",
});
expect(useCasesLinks.length).toBeGreaterThan(0);
const useCasesLink = useCasesLinks[0];
await user.click(useCasesLink);
expect(mockOnToggle).toHaveBeenCalledTimes(1);
const header = container.querySelector(
'[role="banner"][aria-label="Main navigation header"]'
);
expect(header).toHaveClass("sticky", "top-0", "z-50");
});
});
describe("CSS Classes and Styling", () => {
test("has correct CSS classes for styling", () => {
const { container } = render(<Header onToggle={mockOnToggle} />);
const { container } = render(<Header />);
const header = container.querySelector(
'[role="banner"][aria-label="Main navigation header"]',
'[role="banner"][aria-label="Main navigation header"]'
);
expect(header).toHaveClass("bg-[var(--color-surface-default-primary)]");
expect(header).toHaveClass("w-full");
expect(header).toHaveClass("border-b");
expect(header).toHaveClass(
"border-[var(--border-color-default-tertiary)]",
"border-[var(--border-color-default-tertiary)]"
);
const nav = container.querySelector(
'[role="navigation"][aria-label="Main navigation"]',
'[role="navigation"][aria-label="Main navigation"]'
);
expect(nav).toHaveClass("flex");
expect(nav).toHaveClass("items-center");
+40 -1
View File
@@ -15,6 +15,16 @@ describe("Blog Post Validation", () => {
author: "Test Author",
date: "2025-04-15",
related: ["post-1", "post-2"],
thumbnail: {
vertical: "test-vertical.svg",
horizontal: "test-horizontal.svg",
},
banner: {
horizontal: "test-banner.svg",
},
background: {
color: "#F4F3F1",
},
};
const result = validateBlogPost(validPost);
@@ -84,11 +94,14 @@ describe("Blog Post Validation", () => {
description: "Test description",
author: "Test Author",
date: "2025-04-15",
// Missing related
// Missing related, thumbnail, banner, background
};
const sanitized = sanitizeBlogPost(post);
expect(sanitized.related).toEqual([]);
expect(sanitized.thumbnail).toBeNull();
expect(sanitized.banner).toBeNull();
expect(sanitized.background).toBeNull();
});
it("should preserve existing optional fields", () => {
@@ -98,10 +111,30 @@ describe("Blog Post Validation", () => {
author: "Test Author",
date: "2025-04-15",
related: ["custom-post"],
thumbnail: {
vertical: "custom-vertical.svg",
horizontal: "custom-horizontal.svg",
},
banner: {
horizontal: "custom-banner.svg",
},
background: {
color: "#E2EFFF",
},
};
const sanitized = sanitizeBlogPost(post);
expect(sanitized.related).toEqual(["custom-post"]);
expect(sanitized.thumbnail).toEqual({
vertical: "custom-vertical.svg",
horizontal: "custom-horizontal.svg",
});
expect(sanitized.banner).toEqual({
horizontal: "custom-banner.svg",
});
expect(sanitized.background).toEqual({
color: "#E2EFFF",
});
});
});
@@ -112,6 +145,9 @@ describe("Blog Post Validation", () => {
expect(BLOG_POST_SCHEMA).toHaveProperty("author");
expect(BLOG_POST_SCHEMA).toHaveProperty("date");
expect(BLOG_POST_SCHEMA).toHaveProperty("related");
expect(BLOG_POST_SCHEMA).toHaveProperty("thumbnail");
expect(BLOG_POST_SCHEMA).toHaveProperty("banner");
expect(BLOG_POST_SCHEMA).toHaveProperty("background");
});
it("should have correct required field configuration", () => {
@@ -120,6 +156,9 @@ describe("Blog Post Validation", () => {
expect(BLOG_POST_SCHEMA.author.required).toBe(true);
expect(BLOG_POST_SCHEMA.date.required).toBe(true);
expect(BLOG_POST_SCHEMA.related.required).toBe(false);
expect(BLOG_POST_SCHEMA.thumbnail.required).toBe(false);
expect(BLOG_POST_SCHEMA.banner.required).toBe(false);
expect(BLOG_POST_SCHEMA.background.required).toBe(false);
});
});
});