diff --git a/components/Accordion.tsx b/components/Accordion.tsx index d6428ff0d..69073acf1 100644 --- a/components/Accordion.tsx +++ b/components/Accordion.tsx @@ -1,5 +1,12 @@ +/* eslint-disable linebreak-style */ import React, { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/ui/collapsible'; +import { cn } from '@/lib/utils'; interface AccordionItem { question: string; @@ -12,84 +19,122 @@ interface AccordionProps { } const Accordion: React.FC = ({ items }) => { - const [activeIndex, setActiveIndex] = useState(null); + const [openItems, setOpenItems] = useState>(new Set()); const router = useRouter(); - const handleToggle = (index: number) => { - setActiveIndex((prevIndex) => (prevIndex === index ? null : index)); + const handleToggle = (id: number) => { + setOpenItems((prev) => { + const newSet = new Set(prev); + if (newSet.has(id)) { + newSet.delete(id); + } else { + newSet.add(id); + } + return newSet; + }); }; useEffect(() => { const hash = router.asPath.split('#')[1]; if (hash) { const id = parseInt(hash, 10); - const index = items.findIndex((item) => item.id === id); - if (index !== -1) { - setActiveIndex(index); - - setTimeout(() => { - const element = document.getElementById(hash); - if (element) { - const navbarHeight = 150; - const offset = element.offsetTop - navbarHeight; - window.scrollTo({ top: offset, behavior: 'smooth' }); - } - }, 0); + const item = items.find((item) => item.id === id); + if (item) { + setOpenItems(new Set([id])); } } }, [items, router.asPath]); - const handleLinkClick = (id: number) => { - const index = items.findIndex((item) => item.id === id); - setActiveIndex(index); - - const newUrl = `#${id}`; - router.push(newUrl, undefined, { shallow: true }); - }; - return ( -
- {items.map((item, index) => ( -
+ {items.map((item) => ( + handleToggle(item.id)} + className='w-full' data-test={`accordion-item-${item.id}`} > -
- -
handleToggle(index)} +
+ - + -
-
- {activeIndex === index && ( - + + + - {item.answer} -
- )} -
+
+ {item.answer} +
+ +
+ ))} ); diff --git a/components/CarbonsAds.tsx b/components/CarbonsAds.tsx index 7cf46651c..e418b4ebb 100644 --- a/components/CarbonsAds.tsx +++ b/components/CarbonsAds.tsx @@ -74,46 +74,92 @@ CarbonAds.stylesheet = { #carbonads { padding: 0.5rem; margin: 2rem auto; - background-color: #f9f9f9; - border: 1px dashed; - border-color: #cacaca; + background-color: #ffffff; + border: 1px dashed #e5e7eb; box-sizing: border-box; border-radius: 10px; max-width: 300px; + transition: background-color 0.3s ease, color 0.3s ease; } - #carbonads > span { - width: 100%; - height: fit-content; - display: flex; - flex-direction: column; + #carbonads .carbon-wrap { + background-color: #f9f9f9 !important; } - .carbon-wrap { - flex: 1; - display: inline-flex; - flex-direction: row; + #carbonads .carbon-text, + #carbonads .carbon-poweredby { + background-color: inherit !important; + color: #374151 !important; } - .carbon-img { - margin: auto; + #carbonads .carbon-text { + font-size: 12px; + font-family: Inter, ui-sans-serif, system-ui; + line-height: 1.4; } - .carbon-img > img { - align-self: stretch; - margin-right: 0.75rem; + #carbonads .carbon-img { + text-align: center; + margin: 0 auto 8px; } - .carbon-text { - font-size: 12px; - font-family: Inter, ui-sans-serif, system-ui; - color: rgb(100 116 139); + #carbonads .carbon-img img { + display: inline-block; + margin: 0 auto; + filter: brightness(1); + border: 1px solid #e5e7eb !important; + background-color: #f9fafb !important; + border-radius: 4px; } - .carbon-poweredby { - font-size: 12px; + #carbonads .carbon-poweredby { + font-size: 11px !important; + text-align: center !important; + display: block; margin-top: 4px; - color: rgb(100 116 139); + color: #6b7280 !important; + } + + #carbonads a { + color: #2563eb !important; + text-decoration: none !important; + } + + #carbonads a:hover { + color: #1d4ed8 !important; + } + + /* Dark mode overrides */ + .dark #carbonads { + background-color: rgb(30 41 59) !important; + border-color: rgb(51 65 85) !important; + } + + .dark #carbonads .carbon-text, + .dark #carbonads .carbon-poweredby { + color: #f1f5f9 !important; + } + + .dark #carbonads .carbon-poweredby { + color: #94a3b8 !important; + } + + .dark #carbonads a { + color: #7dd3fc !important; + } + + .dark #carbonads .carbon-wrap { + background-color: #0f172a !important; + } + + .dark #carbonads a:hover { + color: #e2e8f0 !important; + } + + .dark #carbonads .carbon-img img { + filter: brightness(0.9) contrast(1.1); + border-color: rgb(51 65 85) !important; + background-color: rgb(15 23 42) !important; } @media (max-width: 1023px) { diff --git a/components/DocTable.tsx b/components/DocTable.tsx index 8b668261a..a8862797b 100644 --- a/components/DocTable.tsx +++ b/components/DocTable.tsx @@ -1,66 +1,87 @@ +/* eslint-disable linebreak-style */ import React from 'react'; import Link from 'next/link'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -const DocTable = ({ frontmatter }: any) => { +interface DocTableProps { + frontmatter: { + Specification: string; + Published: string; + authors: string[]; + Metaschema: string; + }; +} + +const DocTable = ({ frontmatter }: DocTableProps) => { return ( - <> -
-
-
Specification Details
-
- - - - - - - - - - - - - - - - - - - -
- Specification - + + + + Specification Details + + + +
+
+
+
+ Specification +
+
{frontmatter.Specification} -
- Published - {frontmatter.Published}
- Authors - + + + +
+
+
+ Published +
+
{frontmatter.Published}
+
+
+
+
+
+ Authors +
+
{frontmatter.authors.map((author: string, index: number) => ( {author} {index < frontmatter.authors.length - 1 ? ', ' : ''} ))} -
- Metaschema - + + + +
+
+
+ Metaschema +
+
{frontmatter.Metaschema} -
-
- + + + + + + ); }; diff --git a/cypress/components/Accordian.cy.tsx b/cypress/components/Accordian.cy.tsx deleted file mode 100644 index 5e54db05f..000000000 --- a/cypress/components/Accordian.cy.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import React from 'react'; -import Accordion from '~/components/Accordion'; -import mockNextRouter, { MockRouter } from '../plugins/mockNextRouterUtils'; - -interface AccordionItem { - question: string; - answer: string; - id: number; -} - -interface AccordionProps { - items: AccordionItem[]; -} - -const items: AccordionProps['items'] = [ - { - question: 'What is JSON Schema?', - answer: - 'JSON Schema is a vocabulary that allows you to annotate and validate JSON documents. It is used to define the structure of JSON data for documentation, validation, and interaction control.', - id: 1, - }, - { - question: 'What is JSON Schema used for?', - answer: - 'JSON Schema is used to define the structure of JSON data for documentation, validation, and interaction control.', - id: 2, - }, - { - question: 'What is JSON Schema validation?', - answer: - 'JSON Schema validation is the process of ensuring that a JSON document is correctly formatted and structured according to a JSON Schema.', - id: 3, - }, -]; - -describe('Accordion Component', () => { - let mockRouter: MockRouter; - beforeEach(() => { - mockRouter = mockNextRouter(); - cy.mount(); - }); - - // Render the Accordion component with items corrently - it('should render the Accordion Items correctly', () => { - // Check if all items are rendered - items.forEach((item) => { - cy.get(`[data-test="accordion-item-${item.id}"]`) - .should('exist') - .within(() => { - // Check if the question is rendered - cy.get(`[data-test=accordion-question-${item.id}]`).should( - 'have.text', - item.question, - ); - - // Now click on the question to see the answer - cy.get(`[data-test=accordion-question-${item.id}]`).click(); - cy.get('@routerPush').should('have.been.calledWith', `#${item.id}`); - cy.get(`[ data-test=accordion-answer-${item.id}]`).should( - 'have.text', - item.answer, - ); - }); - // Check if answer is expanded and visible - cy.get(`[data-test=accordion-item-${item.id}]`).should( - 'have.class', - 'max-h-96', - ); - }); - }); - - // Toggle Button of the Accordion items should work correctly - it('should handle the toggle correctly', () => { - /* Testing the behavior with the first item */ - - // Initially, all items should be collapsed - cy.get(`[data-test="accordion-item-${items[0].id}"]`).should( - 'have.class', - 'max-h-20', - ); - - // Click on the first item to expand it - cy.get(`[data-test="accordion-toggle-${items[0].id}"]`).click(); - - // Check if the first item is expanded - cy.get(`[data-test="accordion-item-${items[0].id}"]`).should( - 'have.class', - 'max-h-96', - ); - - // Click on the toggle button to collapse the first item - cy.get(`[data-test="accordion-toggle-${items[0].id}"]`).click(); - - // Check if the first item is collapsed - cy.get(`[data-test="accordion-item-${items[0].id}"]`).should( - 'have.class', - 'max-h-20', - ); - }); - - describe('Accordion scrolling behavior', () => { - /* Testing the behavior with the first item */ - - // Scroll to the Accordion item when the router asPath changes - it('should scroll when router asPath changes', () => { - // spy the scrollTo method - const scrollToSpy = cy.spy(window, 'scrollTo'); - const foundIndex = 0; - cy.get(`[data-test="accordion-item-${items[0].id}"]`).should( - 'have.class', - 'max-h-20', - ); - cy.get(`[data-test="accordion-question-${items[0].id}"]`).click(); - - // Simulate the behavior of findIndex - cy.stub(items, 'findIndex').callsFake((predicate) => { - console.log(predicate); - return predicate({ id: items[0].id }) ? foundIndex : -1; - }); - - // Simulate the router asPath change - mockRouter.asPath = `#${items[0].id}`; - cy.wrap(null).then(() => { - Cypress.$(window).trigger('hashchange'); - }); - - // Check if the accordion item is expanded - cy.get(`[data-test="accordion-item-${items[0].id}"]`).should( - 'have.class', - 'max-h-96', - ); - - // Check if scrollTo was called - cy.wrap(scrollToSpy).should('have.been.called'); - cy.get(`[data-test="accordion-answer-${items[0].id}"]`).should('exist'); - - // Check if scrollTo was called with the correct arguments - cy.wrap(scrollToSpy).should('have.been.calledWithMatch', { - top: Cypress.sinon.match.number, - behavior: 'smooth', - }); - }); - - it('should not scroll when findIndex is -1', () => { - /* Testing the behavior with the first item */ - - const foundIndex = 0; - cy.get(`[data-test="accordion-question-${items[0].id}"]`).click(); - - // Simulate the router asPath change to the first item - mockRouter.asPath = `#${items[0].id}`; - cy.wrap(null).then(() => { - Cypress.$(window).trigger('hashchange'); - }); - - // Using mockId as null to get findIndex return -1 - cy.stub(items, 'findIndex').callsFake((predicate) => { - console.log(predicate); - return predicate({ id: null }) ? foundIndex : -1; - }); - }); - - it('should not scroll when element is not found', () => { - /* Testing the behavior with the first item */ - - const foundIndex = 0; - // Stub the getElementById method - const getElementByIdStub = cy.stub(document, 'getElementById'); - - cy.get(`[data-test="accordion-question-${items[0].id}"]`).click(); - - // Simulate the router asPath change to the first item - mockRouter.asPath = `#${items[0].id}`; - cy.wrap(null).then(() => { - Cypress.$(window).trigger('hashchange'); - }); - - // Simulate the behavior of findIndex when element not found - cy.stub(items, 'findIndex').callsFake((predicate) => { - return predicate({ id: items[0].id }) ? foundIndex : -1; - }); - - // using mockId as -1 to simulate the behavior of getElementById when element not found - const mockId = -1; - const mockElement = document.createElement('div'); - getElementByIdStub.withArgs(mockId.toString()).returns(mockElement); - }); - }); -}); diff --git a/cypress/components/Accordion.cy.tsx b/cypress/components/Accordion.cy.tsx new file mode 100644 index 000000000..c25908b6f --- /dev/null +++ b/cypress/components/Accordion.cy.tsx @@ -0,0 +1,648 @@ +/* eslint-disable cypress/no-unnecessary-waiting */ +/* eslint-disable cypress/unsafe-to-chain-command */ +import React from 'react'; +import Accordion from '~/components/Accordion'; +import mockNextRouter, { MockRouter } from '../plugins/mockNextRouterUtils'; + +interface AccordionItem { + question: string; + answer: string; + id: number; +} + +interface AccordionProps { + items: AccordionItem[]; +} + +const items: AccordionProps['items'] = [ + { + question: 'What is JSON Schema?', + answer: + 'JSON Schema is a vocabulary that allows you to annotate and validate JSON documents. It is used to define the structure of JSON data for documentation, validation, and interaction control.', + id: 1, + }, + { + question: 'What is JSON Schema used for?', + answer: + 'JSON Schema is used to define the structure of JSON data for documentation, validation, and interaction control.', + id: 2, + }, + { + question: 'What is JSON Schema validation?', + answer: + 'JSON Schema validation is the process of ensuring that a JSON document is correctly formatted and structured according to a JSON Schema.', + id: 3, + }, +]; + +describe('Accordion Component', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + mockRouter = mockNextRouter(); + cy.mount(); + }); + + // Render the Accordion component with items correctly + it('should render the Accordion Items correctly', () => { + // Check if all items are rendered + items.forEach((item) => { + cy.get(`[data-test="accordion-item-${item.id}"]`) + .should('exist') + .within(() => { + // Check if the question is rendered + cy.get(`[data-test="accordion-question-${item.id}"]`).should( + 'have.text', + item.question, + ); + + // Initially, answer should not be visible + cy.get(`[data-test="accordion-answer-${item.id}"]`).should( + 'not.be.visible', + ); + + // Click on the question to see the answer + cy.get(`[data-test="accordion-question-${item.id}"]`).click(); + + // Answer should now be visible + cy.get(`[data-test="accordion-answer-${item.id}"]`).should( + 'be.visible', + ); + cy.get(`[data-test="accordion-answer-${item.id}"]`).should( + 'have.text', + item.answer, + ); + }); + }); + }); + + // Toggle functionality of the Accordion items should work correctly + it('should handle the toggle correctly', () => { + const firstItem = items[0]; + + // Initially, answer should not be visible + cy.get(`[data-test="accordion-answer-${firstItem.id}"]`).should( + 'not.be.visible', + ); + + // Click on the first item to expand it + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`).click(); + + // Check if the first item is expanded (answer should be visible) + cy.get(`[data-test="accordion-answer-${firstItem.id}"]`).should( + 'be.visible', + ); + + // Click on the toggle button to collapse the first item + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`).click(); + + // Check if the first item is collapsed (answer should not be visible) + cy.get(`[data-test="accordion-answer-${firstItem.id}"]`).should( + 'not.be.visible', + ); + }); + + // Test multiple items interaction + it('should handle multiple items being open simultaneously', () => { + // Open first item + cy.get(`[data-test="accordion-toggle-${items[0].id}"]`).click(); + cy.get(`[data-test="accordion-answer-${items[0].id}"]`).should( + 'be.visible', + ); + + // Open second item + cy.get(`[data-test="accordion-toggle-${items[1].id}"]`).click(); + cy.get(`[data-test="accordion-answer-${items[1].id}"]`).should( + 'be.visible', + ); + + // Both items should remain open + cy.get(`[data-test="accordion-answer-${items[0].id}"]`).should( + 'be.visible', + ); + cy.get(`[data-test="accordion-answer-${items[1].id}"]`).should( + 'be.visible', + ); + + // Close first item + cy.get(`[data-test="accordion-toggle-${items[0].id}"]`).click(); + cy.get(`[data-test="accordion-answer-${items[0].id}"]`).should( + 'not.be.visible', + ); + + // Second item should still be open + cy.get(`[data-test="accordion-answer-${items[1].id}"]`).should( + 'be.visible', + ); + }); + + // Test click behavior on already open items + it('should close item when clicking on already open question', () => { + const firstItem = items[0]; + + // Open the item first + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`).click(); + cy.get(`[data-test="accordion-answer-${firstItem.id}"]`).should( + 'be.visible', + ); + + // Click on the question link when it's already open + cy.get(`[data-test="accordion-question-${firstItem.id}"]`).click(); + + // Item should be closed + cy.get(`[data-test="accordion-answer-${firstItem.id}"]`).should( + 'not.be.visible', + ); + }); + + // Test hover effects on question titles + it('should apply hover effects on question titles', () => { + const firstItem = items[0]; + + // Hover over the question title + cy.get(`[data-test="accordion-question-${firstItem.id}"]`) + .trigger('mouseover') + .should('have.class', 'hover:text-lg'); + }); + + // Test visual states when accordion items are open + it('should show correct visual states when items are open', () => { + const firstItem = items[0]; + + // Click to open the item + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`).click(); + + // Check that the container has the correct background color class + cy.get(`[data-test="accordion-item-${firstItem.id}"]`) + .find('div') + .first() + .should('have.class', 'bg-[#e2e8f0]'); + + // Check that the question title has the active color class + cy.get(`[data-test="accordion-question-${firstItem.id}"]`).should( + 'have.class', + 'text-primary', + ); + + // Check that the circle icon shows the correct state + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`) + .find('span') + .should('have.text', '×'); + }); + + // Test visual states when accordion items are closed + it('should show correct visual states when items are closed', () => { + const firstItem = items[0]; + + // Ensure item is closed initially + cy.get(`[data-test="accordion-answer-${firstItem.id}"]`).should( + 'not.be.visible', + ); + + // Check that the circle icon shows the correct state for closed items + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`) + .find('span') + .should('have.text', '+'); + + // Check that the question title doesn't have active color class + cy.get(`[data-test="accordion-question-${firstItem.id}"]`).should( + 'not.have.class', + 'text-primary', + ); + }); + + // Test dark mode styling classes + it('should have dark mode styling classes', () => { + const firstItem = items[0]; + + // Check for dark mode border classes (always present) + cy.get(`[data-test="accordion-item-${firstItem.id}"]`) + .find('div') + .first() + .should('have.class', 'dark:border-[#bfdbfe]'); + + // Check for dark mode background classes (only when open) + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`).click(); + cy.get(`[data-test="accordion-item-${firstItem.id}"]`) + .find('div') + .first() + .should('have.class', 'dark:bg-[#0f172a]'); + + // Check for dark mode text classes on question + cy.get(`[data-test="accordion-question-${firstItem.id}"]`).should( + 'have.class', + 'dark:hover:text-[#bfdbfe]', + ); + + // Check for dark mode icon classes (only when open) + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`) + .find('div') + .should('have.class', 'dark:bg-[#bfdbfe]') + .and('have.class', 'dark:text-black') + .and('have.class', 'dark:border-[#bfdbfe]'); + }); + + // Test accessibility features + it('should have proper accessibility attributes', () => { + const firstItem = items[0]; + + // Check that collapsible trigger has proper ARIA attributes + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`).should( + 'have.attr', + 'data-state', + ); + + // Check that content has proper ARIA attributes + cy.get(`[data-test="accordion-answer-${firstItem.id}"]`).should( + 'have.attr', + 'data-state', + ); + + // Check that the trigger has button-like behavior + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`).should( + 'have.attr', + 'type', + 'button', + ); + }); + + // Test keyboard navigation + it('should support keyboard navigation', () => { + const firstItem = items[0]; + + // Click to focus and open the toggle + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`).click(); + cy.get(`[data-test="accordion-answer-${firstItem.id}"]`).should( + 'be.visible', + ); + + // Click again to close + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`).click(); + cy.get(`[data-test="accordion-answer-${firstItem.id}"]`).should( + 'not.be.visible', + ); + + // Test that the component responds to keyboard events through the collapsible + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`).click(); + cy.get(`[data-test="accordion-answer-${firstItem.id}"]`).should( + 'be.visible', + ); + }); + + // Test animation classes + it('should have proper animation classes', () => { + const firstItem = items[0]; + + // Check for animation classes on content + cy.get(`[data-test="accordion-answer-${firstItem.id}"]`) + .should('have.class', 'data-[state=closed]:animate-collapsible-up') + .and('have.class', 'data-[state=open]:animate-collapsible-down'); + }); + + // Test edge cases + it('should handle empty items array', () => { + cy.mount(); + + // Should render without errors + cy.get('[data-test^="accordion-item-"]').should('not.exist'); + }); + + it('should handle single item', () => { + const singleItem = [items[0]]; + cy.mount(); + + // Should render the single item correctly + cy.get(`[data-test="accordion-item-${singleItem[0].id}"]`).should('exist'); + cy.get(`[data-test="accordion-question-${singleItem[0].id}"]`).should( + 'have.text', + singleItem[0].question, + ); + }); + + // Test scroll behavior with element positioning + it('should scroll to correct position when opening item', () => { + const firstItem = items[0]; + const scrollToSpy = cy.spy(window, 'scrollTo'); + + // Mock getElementById to return a mock element + const mockElement = { + offsetTop: 500, + }; + cy.stub(document, 'getElementById').returns(mockElement as any); + + // Click on the question to open it and trigger scroll + cy.get(`[data-test="accordion-question-${firstItem.id}"]`).click(); + + // Wait for the setTimeout to execute + cy.wait(150); + + // Check if scrollTo was called with correct parameters + cy.wrap(scrollToSpy).should('have.been.calledWithMatch', { + top: 330, // 500 - 150 (navbar) - 20 (offset) + behavior: 'smooth', + }); + }); + + // Test scroll behavior when element is not found + it('should handle scroll gracefully when element is not found', () => { + const firstItem = items[0]; + const scrollToSpy = cy.spy(window, 'scrollTo'); + + // Mock getElementById to return null + cy.stub(document, 'getElementById').returns(null); + + // Click on the question to open it + cy.get(`[data-test="accordion-question-${firstItem.id}"]`).click(); + + // Wait for the setTimeout to execute + cy.wait(150); + + // scrollTo should not be called when element is not found + cy.wrap(scrollToSpy).should('not.have.been.called'); + }); + + // Test that clicking on question link prevents default behavior + it('should prevent default behavior when clicking on question link', () => { + const firstItem = items[0]; + + // Click on the question link to open it + cy.get(`[data-test="accordion-question-${firstItem.id}"]`).click(); + + // The item should be opened + cy.get(`[data-test="accordion-answer-${firstItem.id}"]`).should( + 'be.visible', + ); + }); + + // Test that the component handles malformed items gracefully + it('should handle malformed items gracefully', () => { + const malformedItems = [ + { + question: 'Test Question', + answer: 'Test Answer', + id: 'invalid-id', // string instead of number + }, + ]; + + // Should mount without errors + cy.mount(); + + // Should still render the item + cy.get('[data-test="accordion-item-invalid-id"]').should('exist'); + }); + + // Test that the component handles duplicate IDs gracefully + it('should handle duplicate IDs gracefully', () => { + const duplicateItems = [ + { + question: 'First Question', + answer: 'First Answer', + id: 1, + }, + { + question: 'Second Question', + answer: 'Second Answer', + id: 1, // duplicate ID + }, + ]; + + // Should mount without errors + cy.mount(); + + // Should render both items (React will handle the key conflict) + cy.get('[data-test="accordion-item-1"]').should('have.length', 2); + }); + + describe('Accordion scrolling behavior', () => { + it('should handle hash changes correctly', () => { + // Set up the router with a hash before mounting + mockRouter.asPath = `#${items[0].id}`; + + // Remount the component with the hash in the URL + cy.mount(); + + // Check if the accordion item is expanded due to the hash + cy.get(`[data-test="accordion-answer-${items[0].id}"]`).should( + 'be.visible', + ); + }); + + it('should handle non-existent hash gracefully', () => { + cy.mount(); + + // Simulate the router asPath change to a non-existent item + mockRouter.asPath = '#999'; + cy.wrap(null).then(() => { + Cypress.$(window).trigger('hashchange'); + }); + + // No items should be expanded + items.forEach((item) => { + cy.get(`[data-test="accordion-answer-${item.id}"]`).should( + 'not.be.visible', + ); + }); + }); + + it('should handle invalid hash format gracefully', () => { + cy.mount(); + + // Simulate the router asPath change to an invalid hash + mockRouter.asPath = '#invalid'; + cy.wrap(null).then(() => { + Cypress.$(window).trigger('hashchange'); + }); + + // No items should be expanded + items.forEach((item) => { + cy.get(`[data-test="accordion-answer-${item.id}"]`).should( + 'not.be.visible', + ); + }); + }); + + it('should handle hash with no fragment gracefully', () => { + cy.mount(); + + // Simulate the router asPath change with no hash fragment + mockRouter.asPath = '/some-path'; + cy.wrap(null).then(() => { + Cypress.$(window).trigger('hashchange'); + }); + + // No items should be expanded + items.forEach((item) => { + cy.get(`[data-test="accordion-answer-${item.id}"]`).should( + 'not.be.visible', + ); + }); + }); + + it('should handle useEffect when items prop changes', () => { + // Start with initial items + cy.mount(); + + // Create new items with different IDs + const newItems = [ + { + question: 'New Question 1', + answer: 'New Answer 1', + id: 10, + }, + { + question: 'New Question 2', + answer: 'New Answer 2', + id: 11, + }, + ]; + + // Set hash to match new item before remounting + mockRouter.asPath = '#10'; + + // Remount with new items + cy.mount(); + + // Wait a bit for useEffect to process + cy.wait(50); + + // New item should be expanded due to hash + cy.get('[data-test="accordion-answer-10"]').should('be.visible'); + cy.get('[data-test="accordion-answer-11"]').should('not.be.visible'); + }); + }); + + // Test component structure and CSS classes + it('should have proper component structure', () => { + const firstItem = items[0]; + + // Check main container + cy.get(`[data-test="accordion-item-${firstItem.id}"]`).should( + 'have.class', + 'w-full', + ); + + // Check collapsible trigger structure + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`) + .should('have.class', 'w-full') + .within(() => { + // Check question container + cy.get('div').first().should('have.class', 'flex-1'); + + // Check icon container - it's the second div with ml-4 flex-shrink-0 + cy.get('div').eq(1).should('have.class', 'ml-4 flex-shrink-0'); + }); + + // Check answer container + cy.get(`[data-test="accordion-answer-${firstItem.id}"]`).should( + 'have.class', + 'overflow-hidden', + ); + }); + + // Test transition effects + it('should have proper transition effects', () => { + const firstItem = items[0]; + + // Check for transition classes + cy.get(`[data-test="accordion-item-${firstItem.id}"]`) + .find('div') + .first() + .should('have.class', 'transition-colors'); + + cy.get(`[data-test="accordion-question-${firstItem.id}"]`).should( + 'have.class', + 'transition-all duration-200', + ); + + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`) + .find('div') + .should('have.class', 'transition-all duration-200'); + }); + + // Test with long content + it('should handle long content properly', () => { + const longContentItems = [ + { + question: + 'This is a very long question that might wrap to multiple lines and should be handled gracefully by the accordion component', + answer: + 'This is a very long answer that contains a lot of text and might also wrap to multiple lines. It should be displayed properly within the accordion content area without breaking the layout or causing any visual issues. The content should be readable and properly formatted.', + id: 100, + }, + ]; + + cy.mount(); + + // Should render without layout issues + cy.get('[data-test="accordion-item-100"]').should('exist'); + cy.get('[data-test="accordion-question-100"]').should( + 'have.text', + longContentItems[0].question, + ); + + // Open the item + cy.get('[data-test="accordion-toggle-100"]').click(); + cy.get('[data-test="accordion-answer-100"]').should('be.visible'); + cy.get('[data-test="accordion-answer-100"]').should( + 'have.text', + longContentItems[0].answer, + ); + }); + + // Test responsive design classes + it('should have proper responsive design classes', () => { + const firstItem = items[0]; + + // Check for responsive width classes + cy.get(`[data-test="accordion-item-${firstItem.id}"]`).should( + 'have.class', + 'w-full', + ); + + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`).should( + 'have.class', + 'w-full', + ); + + // Check for responsive padding and spacing - the p-4 is on the trigger div itself + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`).should( + 'have.class', + 'p-4', + ); + + // Open the item first to check the answer content div + cy.get(`[data-test="accordion-toggle-${firstItem.id}"]`).click(); + cy.get(`[data-test="accordion-answer-${firstItem.id}"]`) + .find('div') + .should('have.class', 'px-4 pb-4'); + }); + + // Test that all items have unique data-test attributes + it('should have unique data-test attributes for all elements', () => { + items.forEach((item) => { + // Check that each item has unique test attributes + cy.get(`[data-test="accordion-item-${item.id}"]`).should('exist'); + cy.get(`[data-test="accordion-toggle-${item.id}"]`).should('exist'); + cy.get(`[data-test="accordion-question-${item.id}"]`).should('exist'); + cy.get(`[data-test="accordion-answer-${item.id}"]`).should('exist'); + }); + + // Verify that we have the correct number of elements + cy.get('[data-test^="accordion-item-"]').should( + 'have.length', + items.length, + ); + cy.get('[data-test^="accordion-toggle-"]').should( + 'have.length', + items.length, + ); + cy.get('[data-test^="accordion-question-"]').should( + 'have.length', + items.length, + ); + cy.get('[data-test^="accordion-answer-"]').should( + 'have.length', + items.length, + ); + }); +}); diff --git a/cypress/components/DocTable.cy.tsx b/cypress/components/DocTable.cy.tsx new file mode 100644 index 000000000..f9e7b590e --- /dev/null +++ b/cypress/components/DocTable.cy.tsx @@ -0,0 +1,191 @@ +import React from 'react'; +import DocTable from '~/components/DocTable'; + +interface DocTableProps { + frontmatter: { + Specification: string; + Published: string; + authors: string[]; + Metaschema: string; + }; +} + +const mockFrontmatter: DocTableProps['frontmatter'] = { + Specification: 'https://json-schema.org/draft/2020-12/schema', + Published: '2021-12-01', + authors: ['John Doe', 'Jane Smith', 'Bob Johnson'], + Metaschema: 'https://json-schema.org/draft/2020-12/meta/core', +}; + +const mockFrontmatterSingleAuthor: DocTableProps['frontmatter'] = { + Specification: 'https://json-schema.org/draft/2019-09/schema', + Published: '2019-09-01', + authors: ['Single Author'], + Metaschema: 'https://json-schema.org/draft/2019-09/meta/core', +}; + +describe('DocTable Component', () => { + // Render the DocTable component with multiple authors + it('should render DocTable correctly with multiple authors', () => { + cy.mount(); + + // Check if the header is rendered correctly + cy.get('[data-slot="card-title"]').should( + 'have.text', + 'Specification Details', + ); + + // Check if all table rows are rendered + cy.get('[data-slot="card-content"]').within(() => { + // Check Specification row + cy.contains('Specification').should('exist'); + cy.get('a[href="https://json-schema.org/draft/2020-12/schema"]') + .should('exist') + .and('have.text', 'https://json-schema.org/draft/2020-12/schema'); + + // Check Published row + cy.contains('Published').should('exist'); + cy.contains('2021-12-01').should('exist'); + + // Check Authors row + cy.contains('Authors').should('exist'); + cy.contains('John Doe, Jane Smith, Bob Johnson').should('exist'); + + // Check Metaschema row + cy.contains('Metaschema').should('exist'); + cy.get('a[href="https://json-schema.org/draft/2020-12/meta/core"]') + .should('exist') + .and('have.text', 'https://json-schema.org/draft/2020-12/meta/core'); + }); + }); + + // Render the DocTable component with single author + it('should render DocTable correctly with single author', () => { + cy.mount(); + + // Check if the header is rendered correctly + cy.get('[data-slot="card-title"]').should( + 'have.text', + 'Specification Details', + ); + + // Check Authors row with single author + cy.contains('Authors').should('exist'); + cy.contains('Single Author').should('exist'); + + // Verify no comma is added for single author + cy.get('[data-slot="card-content"]').should( + 'not.contain', + 'Single Author,', + ); + }); + + // Test link functionality + it('should have working external links', () => { + cy.mount(); + + // Check that links have correct attributes + cy.get('a[href="https://json-schema.org/draft/2020-12/schema"]') + .should('have.attr', 'target', '_blank') + .and('have.attr', 'rel', 'noopener noreferrer'); + + cy.get('a[href="https://json-schema.org/draft/2020-12/meta/core"]') + .should('have.attr', 'target', '_blank') + .and('have.attr', 'rel', 'noopener noreferrer'); + }); + + // Test styling classes + it('should have correct styling classes', () => { + cy.mount(); + + // Check card styling + cy.get('[data-slot="card"]') + .should('have.class', 'w-full') + .and('have.class', 'overflow-hidden') + .and('have.class', 'border-2') + .and('have.class', 'border-primary') + .and('have.class', 'shadow-lg'); + + // Check header styling + cy.get('[data-slot="card-header"]') + .should('have.class', 'bg-primary') + .and('have.class', 'text-primary-foreground'); + + // Check title styling + cy.get('[data-slot="card-title"]') + .should('have.class', 'text-xl') + .and('have.class', 'font-semibold'); + + // Check content styling + cy.get('[data-slot="card-content"]').should('have.class', 'p-0'); + }); + + // Test label styling + it('should have correct label styling', () => { + cy.mount(); + + // Check that all labels have the correct styling + cy.get('[data-slot="card-content"]').within(() => { + cy.contains('Specification') + .should('have.class', 'font-semibold') + .and('have.class', 'text-base'); + cy.contains('Published') + .should('have.class', 'font-semibold') + .and('have.class', 'text-base'); + cy.contains('Authors') + .should('have.class', 'font-semibold') + .and('have.class', 'text-base'); + cy.contains('Metaschema') + .should('have.class', 'font-semibold') + .and('have.class', 'text-base'); + }); + }); + + // Test link styling + it('should have correct link styling', () => { + cy.mount(); + + // Check that links have the correct styling + cy.get('a[href="https://json-schema.org/draft/2020-12/schema"]') + .should('have.class', 'text-primary') + .and('have.class', 'hover:underline'); + + cy.get('a[href="https://json-schema.org/draft/2020-12/meta/core"]') + .should('have.class', 'text-primary') + .and('have.class', 'hover:underline'); + }); + + // Test responsive layout + it('should have correct responsive layout', () => { + cy.mount(); + + // Check that the layout uses flexbox with correct width distribution + cy.get('[data-slot="card-content"]').within(() => { + cy.get('.flex').should('have.length', 4); // 4 rows + + // Check that each row has the correct width distribution + cy.get('[class*="w-1/3"]').should('have.length', 4); // 4 label columns + cy.get('[class*="w-2/3"]').should('have.length', 4); // 4 value columns + }); + }); + + // Test border styling + it('should have correct border styling', () => { + cy.mount(); + + // Check that rows have border separators + cy.get('[data-slot="card-content"]').within(() => { + cy.get('.border-b').should('have.length', 3); // 3 rows with bottom borders + }); + }); + + // Test background styling + it('should have correct background styling', () => { + cy.mount(); + + // Check the background color classes + cy.get('[data-slot="card-content"]').within(() => { + cy.get('.bg-\\[\\#e2e8f0\\]').should('exist'); + }); + }); +});