diff --git a/__tests__/Glossary.test.tsx b/__tests__/Glossary.test.tsx deleted file mode 100644 index 691f2a73e..000000000 --- a/__tests__/Glossary.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { render, fireEvent, screen } from '@testing-library/react'; -import React from 'react'; - -import { Glossary } from '../components/Glossary'; - -test('should output a glossary item if the term exists', () => { - const term = 'acme'; - const definition = 'This is a definition'; - const { container } = render(acme); - - const trigger = container.querySelector('.GlossaryItem-trigger'); - expect(trigger).toHaveTextContent(term); - if (trigger) { - fireEvent.mouseEnter(trigger); - } - const tooltipContent = screen.getByText(definition, { exact: false }); - expect(tooltipContent).toHaveTextContent(`${term} - ${definition}`); -}); - -test('should be case insensitive', () => { - const term = 'aCme'; - const definition = 'This is a definition'; - const { container } = render(acme); - - const trigger = container.querySelector('.GlossaryItem-trigger'); - expect(trigger).toHaveTextContent('acme'); - if (trigger) { - fireEvent.mouseEnter(trigger); - } - const tooltipContent = screen.getByText(definition, { exact: false }); - expect(tooltipContent).toHaveTextContent(`${term} - ${definition}`); -}); - -test('should output the term if the definition does not exist', () => { - const term = 'something'; - const { container } = render({term}); - - expect(container.querySelector('.GlossaryItem-trigger')).not.toBeInTheDocument(); - expect(container.querySelector('span')).toHaveTextContent(term); -}); diff --git a/__tests__/__snapshots__/compilers.test.ts.snap b/__tests__/__snapshots__/compilers.test.ts.snap deleted file mode 100644 index b8522bf29..000000000 --- a/__tests__/__snapshots__/compilers.test.ts.snap +++ /dev/null @@ -1,6 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`ReadMe Flavored Blocks > Embed 1`] = ` -"[Embedded meta links.](https://nyti.me/s/gzoa2xb2v3 "@embed") -" -`; diff --git a/__tests__/__snapshots__/index.test.js.snap b/__tests__/__snapshots__/index.test.js.snap deleted file mode 100644 index b04a9ad03..000000000 --- a/__tests__/__snapshots__/index.test.js.snap +++ /dev/null @@ -1,348 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`anchor target: should allow _blank if using HTML 1`] = `"

test

"`; - -exports[`anchor target: should allow download if using HTML 1`] = `"

test

"`; - -exports[`anchor target: should default to _self 1`] = `"

test

"`; - -exports[`anchors 1`] = ` -"

link
-xss
-doc
-ref
-blog
-changelog
-page

" -`; - -exports[`anchors with baseUrl 1`] = ` -"<p><a href=\\"doc:slug\\">doc</a><br> -<a href=\\"ref:slug\\">ref</a><br> -<a href=\\"blog:slug\\">blog</a><br> -<a href=\\"changelog:slug\\">changelog</a><br> -<a href=\\"page:slug\\">page</a></p>" -`; - -exports[`anchors with baseUrl and special characters in url hash 1`] = `"

ref

"`; - -exports[`check list items 1`] = ` -"" -`; - -exports[`code samples > should parse indented code on the first line 1`] = ` -{ - "children": [ - { - "children": [ - { - "position": { - "end": { - "column": 23, - "line": 1, - "offset": 22, - }, - "start": { - "column": 5, - "line": 1, - "offset": 4, - }, - }, - "type": "text", - "value": "const code = true;", - }, - ], - "position": { - "end": { - "column": 23, - "line": 1, - "offset": 22, - }, - "start": { - "column": 5, - "line": 1, - "offset": 4, - }, - }, - "type": "paragraph", - }, - ], - "position": { - "end": { - "column": 23, - "line": 1, - "offset": 22, - }, - "start": { - "column": 1, - "line": 1, - "offset": 0, - }, - }, - "type": "root", -} -`; - -exports[`emojis 1`] = ` -"

\\":joy:\\"
-
-:unknown-emoji:

" -`; - -exports[`export multiple Markdown renderers > allows complex compact headings 1`] = `undefined`; - -exports[`export multiple Markdown renderers > renders HTML 1`] = `undefined`; - -exports[`export multiple Markdown renderers > renders MD 1`] = ` -"# Hello World -" -`; - -exports[`export multiple Markdown renderers > renders custom React components 1`] = `[Function]`; - -exports[`export multiple Markdown renderers > renders hAST 1`] = `undefined`; - -exports[`export multiple Markdown renderers > renders mdAST 1`] = ` -{ - "children": [ - { - "children": [ - { - "position": { - "end": { - "column": 14, - "line": 1, - "offset": 13, - }, - "start": { - "column": 3, - "line": 1, - "offset": 2, - }, - }, - "type": "text", - "value": "Hello World", - }, - ], - "depth": 1, - "position": { - "end": { - "column": 14, - "line": 1, - "offset": 13, - }, - "start": { - "column": 1, - "line": 1, - "offset": 0, - }, - }, - "type": "heading", - }, - { - "children": [ - { - "position": { - "end": { - "column": 34, - "line": 7, - "offset": 184, - }, - "start": { - "column": 3, - "line": 3, - "offset": 17, - }, - }, - "type": "text", - "value": "| Col. A | Col. B | Col. C | -|:-------:|:-------:|:-------:| -| Cell A1 | Cell B1 | Cell C1 | -| Cell A2 | Cell B2 | Cell C2 | -| Cell A3 | Cell B3 | Cell C3 |", - }, - ], - "position": { - "end": { - "column": 34, - "line": 7, - "offset": 184, - }, - "start": { - "column": 3, - "line": 3, - "offset": 17, - }, - }, - "type": "paragraph", - }, - { - "children": [ - { - "children": [ - { - "position": { - "end": { - "column": 15, - "line": 9, - "offset": 200, - }, - "start": { - "column": 4, - "line": 9, - "offset": 189, - }, - }, - "type": "text", - "value": "Embed Title", - }, - ], - "position": { - "end": { - "column": 67, - "line": 9, - "offset": 252, - }, - "start": { - "column": 3, - "line": 9, - "offset": 188, - }, - }, - "title": "@embed", - "type": "link", - "url": "https://jsfiddle.net/rafegoldberg/5VA5j/", - }, - ], - "position": { - "end": { - "column": 67, - "line": 9, - "offset": 252, - }, - "start": { - "column": 3, - "line": 9, - "offset": 188, - }, - }, - "type": "paragraph", - }, - { - "children": [ - { - "children": [ - { - "position": { - "end": { - "column": 12, - "line": 11, - "offset": 265, - }, - "start": { - "column": 5, - "line": 11, - "offset": 258, - }, - }, - "type": "text", - "value": "❗️ UhOh", - }, - ], - "position": { - "end": { - "column": 12, - "line": 11, - "offset": 265, - }, - "start": { - "column": 5, - "line": 11, - "offset": 258, - }, - }, - "type": "paragraph", - }, - { - "children": [ - { - "position": { - "end": { - "column": 61, - "line": 13, - "offset": 330, - }, - "start": { - "column": 5, - "line": 13, - "offset": 274, - }, - }, - "type": "text", - "value": "Lorem ipsum dolor sit amet consectetur adipisicing elit.", - }, - ], - "position": { - "end": { - "column": 61, - "line": 13, - "offset": 330, - }, - "start": { - "column": 5, - "line": 13, - "offset": 274, - }, - }, - "type": "paragraph", - }, - ], - "position": { - "end": { - "column": 61, - "line": 13, - "offset": 330, - }, - "start": { - "column": 3, - "line": 11, - "offset": 256, - }, - }, - "type": "blockquote", - }, - ], - "position": { - "end": { - "column": 3, - "line": 16, - "offset": 335, - }, - "start": { - "column": 1, - "line": 1, - "offset": 0, - }, - }, - "type": "root", -} -`; - -exports[`export multiple Markdown renderers > renders plain markdown as React 1`] = `undefined`; - -exports[`heading 1`] = `"

Example Header

"`; - -exports[`image 1`] = `"Image"`; - -exports[`list items 1`] = ` -"" -`; - -exports[`prefix anchors with "section-" > should add a section- prefix to heading anchors 1`] = `undefined`; - -exports[`tables 1`] = `"
TablesAreCool
col 3 isright-aligned$1600
col 2 iscentered$12
zebra stripesare neat$1
"`; diff --git a/__tests__/__snapshots__/link-parsers.test.js.snap b/__tests__/__snapshots__/link-parsers.test.js.snap deleted file mode 100644 index a29f2b323..000000000 --- a/__tests__/__snapshots__/link-parsers.test.js.snap +++ /dev/null @@ -1,178 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`a bare autoLinked url 1`] = ` -Object { - "children": Array [ - Object { - "children": Array [ - Object { - "children": Array [ - Object { - "type": "text", - "value": "http://www.googl.com", - }, - ], - "title": null, - "type": "link", - "url": "http://www.googl.com", - }, - ], - "type": "paragraph", - }, - ], - "type": "root", -} -`; - -exports[`a bare autoLinked url with no protocol 1`] = ` -Object { - "children": Array [ - Object { - "children": Array [ - Object { - "children": Array [ - Object { - "type": "text", - "value": "www.google.com", - }, - ], - "title": null, - "type": "link", - "url": "http://www.google.com", - }, - ], - "type": "paragraph", - }, - ], - "type": "root", -} -`; - -exports[`a bracketed autoLinked url 1`] = ` -Object { - "children": Array [ - Object { - "children": Array [ - Object { - "children": Array [ - Object { - "type": "text", - "value": "http://www.google.com", - }, - ], - "title": null, - "type": "link", - "url": "http://www.google.com", - }, - ], - "type": "paragraph", - }, - ], - "type": "root", -} -`; - -exports[`a link ref 1`] = ` -Object { - "children": Array [ - Object { - "children": Array [ - Object { - "children": Array [ - Object { - "type": "text", - "value": "link", - }, - ], - "identifier": "link", - "label": "link", - "referenceType": "shortcut", - "type": "linkReference", - }, - ], - "type": "paragraph", - }, - ], - "type": "root", -} -`; - -exports[`a link ref with reference 1`] = ` -Object { - "children": Array [ - Object { - "children": Array [ - Object { - "children": Array [ - Object { - "type": "text", - "value": "link", - }, - ], - "identifier": "link", - "label": "link", - "referenceType": "shortcut", - "type": "linkReference", - }, - ], - "type": "paragraph", - }, - Object { - "identifier": "link", - "label": "link", - "title": null, - "type": "definition", - "url": "www.example.com", - }, - ], - "type": "root", -} -`; - -exports[`a link with label 1`] = ` -Object { - "children": Array [ - Object { - "children": Array [ - Object { - "children": Array [ - Object { - "type": "text", - "value": "link", - }, - ], - "title": null, - "type": "link", - "url": "http://www.foo.com", - }, - ], - "type": "paragraph", - }, - ], - "type": "root", -} -`; - -exports[`a link with no url 1`] = ` -Object { - "children": Array [ - Object { - "children": Array [ - Object { - "children": Array [ - Object { - "type": "text", - "value": "link", - }, - ], - "title": null, - "type": "link", - "url": "", - }, - ], - "type": "paragraph", - }, - ], - "type": "root", -} -`; diff --git a/__tests__/compilers.test.ts b/__tests__/compilers.test.ts deleted file mode 100644 index 3861b22c0..000000000 --- a/__tests__/compilers.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { Element } from 'hast'; - -import { mdast, mdx, mdxish } from '../index'; - -describe('ReadMe Flavored Blocks', () => { - it('Embed', () => { - const txt = '[Embedded meta links.](https://nyti.me/s/gzoa2xb2v3 "@embed")'; - const ast = mdast(txt); - const out = mdx(ast); - expect(out).toMatchSnapshot(); - }); - - it('Emojis', () => { - expect(mdx(mdast(':smiley:'))).toMatchInlineSnapshot(` - ":smiley: - " - `); - }); -}); - -describe('mdxish ReadMe Flavored Blocks', () => { - it('Embed', () => { - const txt = '[Embedded meta links.](https://nyti.me/s/gzoa2xb2v3 "@embed")'; - const hast = mdxish(txt); - const embed = hast.children[0] as Element; - - expect(embed.type).toBe('element'); - expect(embed.tagName).toBe('embed'); - expect(embed.properties.url).toBe('https://nyti.me/s/gzoa2xb2v3'); - expect(embed.properties.title).toBe('Embedded meta links.'); - }); - - it('Emojis', () => { - const hast = mdxish(':smiley:'); - const paragraph = hast.children[0] as Element; - - expect(paragraph.type).toBe('element'); - expect(paragraph.tagName).toBe('p'); - // gemojiTransformer converts :smiley: to πŸ˜ƒ - const textNode = paragraph.children[0]; - expect(textNode.type).toBe('text'); - expect('value' in textNode && textNode.value).toBe('πŸ˜ƒ'); - }); -}); diff --git a/__tests__/compilers/gemoji.test.ts b/__tests__/compilers/gemoji.test.ts index d4765e2df..0d1e1f797 100644 --- a/__tests__/compilers/gemoji.test.ts +++ b/__tests__/compilers/gemoji.test.ts @@ -9,6 +9,13 @@ describe('gemoji compiler', () => { expect(mdx(mdast(markdown)).trimEnd()).toStrictEqual(markdown); }); + it('should compile :smiley: back to a shortcode', () => { + expect(mdx(mdast(':smiley:'))).toMatchInlineSnapshot(` + ":smiley: + " + `); + }); + it('should compile owlmoji back to a shortcode', () => { const markdown = ':owlbert:'; @@ -23,6 +30,17 @@ describe('gemoji compiler', () => { }); describe('mdxish gemoji compiler', () => { + it('should convert :smiley: to emoji character', () => { + const hast = mdxish(':smiley:'); + const paragraph = hast.children[0] as Element; + + expect(paragraph.type).toBe('element'); + expect(paragraph.tagName).toBe('p'); + const textNode = paragraph.children[0]; + expect(textNode.type).toBe('text'); + expect('value' in textNode && textNode.value).toBe('πŸ˜ƒ'); + }); + it('should convert gemojis to emoji nodes', () => { const markdown = 'This is a gemoji :joy:.'; diff --git a/__tests__/components/Accordion.test.tsx b/__tests__/components/Accordion.test.tsx new file mode 100644 index 000000000..7537d8921 --- /dev/null +++ b/__tests__/components/Accordion.test.tsx @@ -0,0 +1,101 @@ +import '@testing-library/jest-dom'; +import { render } from '@testing-library/react'; +import React from 'react'; + +import Accordion from '../../components/Accordion'; +import { mdxish, renderMdxish } from '../../lib'; +import { execute } from '../helpers'; + +describe('Accordion', () => { + describe('mdxish', () => { + describe('given a basic Accordion', () => { + const md = ` +Content +`; + const mod = renderMdxish(mdxish(md)); + + it('should render a details element with Accordion class', () => { + const { container } = render(); + expect(container.querySelector('details.Accordion')).toBeInTheDocument(); + }); + + it('should render a summary with the title', () => { + const { container } = render(); + const summary = container.querySelector('summary.Accordion-title'); + expect(summary).toBeInTheDocument(); + expect(summary).toHaveTextContent('Title'); + }); + + it('should render children in Accordion-content', () => { + const { container } = render(); + const content = container.querySelector('.Accordion-content'); + expect(content).toBeInTheDocument(); + expect(content).toHaveTextContent('Content'); + }); + }); + + describe('given an Accordion with icon props', () => { + const md = ` +Settings content +`; + const mod = renderMdxish(mdxish(md)); + + it('should render an icon element', () => { + const { container } = render(); + expect(container.querySelector('i.Accordion-icon')).toBeInTheDocument(); + }); + + it('should apply the icon color style', () => { + const { container } = render(); + const icon = container.querySelector('i.Accordion-icon'); + expect(icon).toHaveStyle({ color: '#FF0000' }); + }); + }); + }); + + describe('mdx', () => { + it('renders an accordion', () => { + const md = 'Content'; + const Content = execute(md); + const { container } = render(); + + expect(container.querySelector('details.Accordion')).toBeInTheDocument(); + expect(container.querySelector('summary.Accordion-title')).toHaveTextContent('Title'); + expect(container.querySelector('.Accordion-content')).toHaveTextContent('Content'); + }); + + it('renders an accordion with icon props', () => { + const md = 'Settings content'; + const Content = execute(md); + const { container } = render(); + + expect(container.querySelector('i.Accordion-icon')).toBeInTheDocument(); + expect(container.querySelector('i.Accordion-icon')).toHaveStyle({ color: '#FF0000' }); + }); + }); + + describe('render', () => { + it('renders a details element with Accordion class', () => { + const { container } = render(Content); + expect(container.querySelector('details.Accordion')).toBeInTheDocument(); + }); + + it('renders a summary with the title', () => { + const { container } = render(Content); + const summary = container.querySelector('summary.Accordion-title'); + expect(summary).toHaveTextContent('Title'); + }); + + it('renders children in Accordion-content', () => { + const { container } = render(Content); + expect(container.querySelector('.Accordion-content')).toHaveTextContent('Content'); + }); + + it('renders an icon when icon prop is provided', () => { + const { container } = render(Content); + const icon = container.querySelector('i.Accordion-icon'); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveStyle({ color: '#FF0000' }); + }); + }); +}); diff --git a/__tests__/components/Anchor.test.tsx b/__tests__/components/Anchor.test.tsx index c5c715b83..2e4953d25 100644 --- a/__tests__/components/Anchor.test.tsx +++ b/__tests__/components/Anchor.test.tsx @@ -1,66 +1,105 @@ +import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; import React from 'react'; import Anchor from '../../components/Anchor'; +import { mdxish, renderMdxish } from '../../lib'; +import { execute } from '../helpers'; describe('Anchor', () => { - it('renders a basic anchor', () => { - render(Click me); + describe('mdxish', () => { + it('renders a markdown link', () => { + const md = '[Example](https://example.com)'; + const mod = renderMdxish(mdxish(md)); + const { container } = render(); - expect(screen.getByRole('link')).toMatchInlineSnapshot(` - - Click me - - `); + const link = container.querySelector('a'); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', 'https://example.com'); + expect(link).toHaveTextContent('Example'); + }); + + it('renders an autolink', () => { + const md = ''; + const mod = renderMdxish(mdxish(md)); + const { container } = render(); + + const link = container.querySelector('a'); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', 'https://example.com'); + }); }); - it('unwraps nested anchor elements', () => { - // Simulates what happens when GFM autolinks URL-like text inside an Anchor - const { container } = render( - - https://example.com - , - ); + describe('mdx', () => { + it('renders a markdown link', () => { + const md = '[Example](https://example.com)'; + const Content = execute(md); + const { container } = render(); - // Should only have one tag, not nested - const anchors = container.querySelectorAll('a'); - expect(anchors).toHaveLength(1); - expect(anchors[0]).toMatchInlineSnapshot(` - - https://example.com - - `); + const link = container.querySelector('a'); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', 'https://example.com'); + expect(link).toHaveTextContent('Example'); + }); }); - it('preserves non-anchor children', () => { - render( - - Bold and italic - , - ); + describe('render', () => { + it('renders a basic anchor', () => { + render(Click me); + + expect(screen.getByRole('link')).toMatchInlineSnapshot(` + + Click me + + `); + }); + + it('unwraps nested anchor elements', () => { + const { container } = render( + + https://example.com + , + ); + + const anchors = container.querySelectorAll('a'); + expect(anchors).toHaveLength(1); + expect(anchors[0]).toMatchInlineSnapshot(` + + https://example.com + + `); + }); + + it('preserves non-anchor children', () => { + render( + + Bold and italic + , + ); - expect(screen.getByRole('link')).toMatchInlineSnapshot(` - - - Bold - - and - - italic - - - `); + expect(screen.getByRole('link')).toMatchInlineSnapshot(` + + + Bold + + and + + italic + + + `); + }); }); }); diff --git a/__tests__/components/Callout.test.tsx b/__tests__/components/Callout.test.tsx index 2b894d7dc..9a2b031e8 100644 --- a/__tests__/components/Callout.test.tsx +++ b/__tests__/components/Callout.test.tsx @@ -1,30 +1,101 @@ +import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/react'; import React from 'react'; import Callout from '../../components/Callout'; +import { mdxish, renderMdxish } from '../../lib'; +import { execute } from '../helpers'; describe('Callout', () => { - it('render _all_ its children', () => { - render( - -

Title

-

First Paragraph

-

Second Paragraph

-
, - ); - - expect(screen.getByText('Second Paragraph')).toBeVisible(); + describe('mdxish', () => { + it('renders a callout with emoji and title', () => { + const md = `> ❗️ Error Callout +> +> Something went wrong.`; + const mod = renderMdxish(mdxish(md)); + const { container } = render(); + + expect(container.querySelector('.callout')).toBeInTheDocument(); + expect(container.textContent).toContain('Error Callout'); + expect(container.textContent).toContain('Something went wrong.'); + }); + + it('renders a callout with no title', () => { + const md = `> 🚧 +> +> Callout content`; + const mod = renderMdxish(mdxish(md)); + const { container } = render(); + + expect(container.querySelector('.callout-heading.empty')).toBeInTheDocument(); + expect(container.textContent).toContain('Callout content'); + }); + + it('renders a regular blockquote without emoji', () => { + const md = '> Hello world'; + const mod = renderMdxish(mdxish(md)); + const { container } = render(); + + expect(container.querySelector('blockquote')).toBeInTheDocument(); + expect(container.querySelector('.callout')).not.toBeInTheDocument(); + expect(container.textContent).toContain('Hello world'); + }); + }); + + describe('mdx', () => { + it('renders a callout with emoji and title', () => { + const md = '> \u2757\uFE0F Error Callout\n>\n> Something went wrong.'; + const Content = execute(md); + const { container } = render(); + + expect(container.querySelector('.callout')).toBeInTheDocument(); + expect(container.textContent).toContain('Error Callout'); + expect(container.textContent).toContain('Something went wrong.'); + }); + + it('renders a callout with no title', () => { + const md = '> \uD83D\uDEA7\n>\n> Callout with no title.'; + const Content = execute(md); + const { container } = render(); + + expect(container.querySelector('.callout-heading.empty')).toBeInTheDocument(); + expect(container.innerHTML).toMatchInlineSnapshot( + `"
🚧

Callout with no title.

"`, + ); + }); + + it('renders an error callout', () => { + const md = '> \u2757\uFE0F Error Callout\n>\n> Lorem ipsum dolor.'; + const Content = execute(md); + const { container } = render(); + + expect(container.innerHTML).toMatchSnapshot(); + }); }); - it("doesn't render all its children if it's **empty**", () => { - render( - -

Title

-

First Paragraph

-

Second Paragraph

-
, - ); + describe('render', () => { + it('render _all_ its children', () => { + render( + +

Title

+

First Paragraph

+

Second Paragraph

+
, + ); + + expect(screen.getByText('Second Paragraph')).toBeVisible(); + }); + + it("doesn't render all its children if it's **empty**", () => { + render( + +

Title

+

First Paragraph

+

Second Paragraph

+
, + ); - expect(screen.queryByText('Title')).toBeNull(); + expect(screen.queryByText('Title')).toBeNull(); + }); }); }); diff --git a/__tests__/components/Cards.test.tsx b/__tests__/components/Cards.test.tsx new file mode 100644 index 000000000..31be0b776 --- /dev/null +++ b/__tests__/components/Cards.test.tsx @@ -0,0 +1,184 @@ +import type { Element } from 'hast'; + +import '@testing-library/jest-dom'; +import { render } from '@testing-library/react'; +import React from 'react'; + +import CardsGrid, { Card } from '../../components/Cards'; +import { mdxish, renderMdxish } from '../../lib'; +import { execute } from '../helpers'; + +describe('Cards', () => { + describe('mdxish', () => { + describe('given a Cards with Card children', () => { + const md = ` + + First content + Second content + +`; + const mod = renderMdxish(mdxish(md)); + + it('should render a CardsGrid wrapper', () => { + const { container } = render(); + expect(container.querySelector('.CardsGrid')).toBeInTheDocument(); + }); + + it('should render Card children', () => { + const { container } = render(); + const cards = container.querySelectorAll('.Card'); + expect(cards).toHaveLength(2); + }); + + it('should render card titles', () => { + const { container } = render(); + const titles = container.querySelectorAll('.Card-title'); + expect(titles).toHaveLength(2); + expect(titles[0]).toHaveTextContent('First'); + expect(titles[1]).toHaveTextContent('Second'); + }); + }); + + describe('given a Card with href', () => { + const md = ` + + Linked + +`; + const mod = renderMdxish(mdxish(md)); + + it('should render as an anchor element', () => { + const { container } = render(); + const card = container.querySelector('a.Card'); + expect(card).toBeInTheDocument(); + expect(card).toHaveAttribute('href', 'https://example.com'); + }); + + it('should render the arrow icon', () => { + const { container } = render(); + expect(container.querySelector('.Card-arrow')).toBeInTheDocument(); + }); + }); + + describe('given a Card without href', () => { + const md = ` + + Static + +`; + const mod = renderMdxish(mdxish(md)); + + it('should render as a div element', () => { + const { container } = render(); + const card = container.querySelector('div.Card'); + expect(card).toBeInTheDocument(); + }); + }); + + describe('given a Card with icon and badge props', () => { + const md = ` + + Featured content + +`; + const mod = renderMdxish(mdxish(md)); + + it('should render an icon element', () => { + const { container } = render(); + expect(container.querySelector('.Card-icon')).toBeInTheDocument(); + }); + + it('should render a badge element', () => { + const { container } = render(); + const badge = container.querySelector('.Card-badge'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveTextContent('New'); + }); + + it('should pass props through the HAST tree', () => { + const tree = mdxish(md); + const cardsNode = tree.children[0] as Element; + const cardChild = cardsNode.children.find( + (child): child is Element => child.type === 'element' && child.tagName === 'Card', + ); + expect(cardChild?.properties?.icon).toBe('fa-star'); + expect(cardChild?.properties?.badge).toBe('New'); + }); + }); + }); + + describe('mdx', () => { + it('renders Cards with Card children', () => { + const md = 'ContentMore'; + const Content = execute(md); + const { container } = render(); + + expect(container.querySelector('.CardsGrid')).toBeInTheDocument(); + const cards = container.querySelectorAll('.Card'); + expect(cards).toHaveLength(2); + }); + + it('renders Card as an anchor when href is provided', () => { + const md = 'Linked'; + const Content = execute(md); + const { container } = render(); + + const card = container.querySelector('a.Card'); + expect(card).toBeInTheDocument(); + expect(card).toHaveAttribute('href', 'https://example.com'); + }); + }); + + describe('render', () => { + it('renders a CardsGrid wrapper', () => { + const { container } = render( + + Content + , + ); + expect(container.querySelector('.CardsGrid')).toBeInTheDocument(); + }); + + it('renders Card children with titles', () => { + const { container } = render( + + First + Second + , + ); + const cards = container.querySelectorAll('.Card'); + expect(cards).toHaveLength(2); + expect(container.querySelectorAll('.Card-title')[0]).toHaveTextContent('First'); + }); + + it('renders Card as an anchor when href is provided', () => { + const { container } = render( + + Linked + , + ); + const card = container.querySelector('a.Card'); + expect(card).toHaveAttribute('href', 'https://example.com'); + expect(container.querySelector('.Card-arrow')).toBeInTheDocument(); + }); + + it('renders Card as a div when no href', () => { + const { container } = render( + + Static + , + ); + expect(container.querySelector('div.Card')).toBeInTheDocument(); + }); + + it('renders icon and badge props', () => { + const { container } = render( + + Content + , + ); + expect(container.querySelector('.Card-icon')).toBeInTheDocument(); + expect(container.querySelector('.Card-badge')).toHaveTextContent('New'); + }); + }); +}); diff --git a/__tests__/components/Code.test.tsx b/__tests__/components/Code.test.tsx index d581bd9d2..31a2d7648 100644 --- a/__tests__/components/Code.test.tsx +++ b/__tests__/components/Code.test.tsx @@ -1,47 +1,59 @@ -import { fireEvent, render, screen } from '@testing-library/react'; -import copy from 'copy-to-clipboard'; +import '@testing-library/jest-dom'; +import { render } from '@testing-library/react'; import React from 'react'; -import { vi } from 'vitest'; - import Code from '../../components/Code'; - - -const codeProps = { - copyButtons: true, -}; - -vi.mock('@readme/syntax-highlighter', () => ({ - default: code => { - return {code.replace(/<<.*?>>/, 'VARIABLE_SUBSTITUTED')}; - }, - canonical: lang => lang, -})); - -describe.skip('Code', () => { - it.skip('copies the variable interpolated code', () => { - const props = { - children: ['console.log("<>");'], - }; - - const { container } = render(); - - expect(container).toHaveTextContent(/VARIABLE_SUBSTITUTED/); - fireEvent.click(screen.getByRole('button')); - - expect(copy).toHaveBeenCalledWith(expect.stringMatching(/VARIABLE_SUBSTITUTED/)); +import { mdxish, renderMdxish } from '../../lib'; +import { execute } from '../helpers'; + +describe('Code', () => { + describe('mdxish', () => { + it('renders a fenced code block', () => { + const md = `\`\`\`js +const x = 1; +\`\`\``; + const mod = renderMdxish(mdxish(md)); + const { container } = render(); + + expect(container.querySelector('code')).toBeInTheDocument(); + expect(container.textContent).toContain('const x = 1;'); + }); + + it('renders inline code', () => { + const md = 'Use `console.log()` to debug'; + const mod = renderMdxish(mdxish(md)); + const { container } = render(); + + expect(container.querySelector('code')).toBeInTheDocument(); + expect(container.textContent).toContain('console.log()'); + }); }); - it.skip('does not nest the button inside the code block', () => { - render({'console.log("hi");'}); - const btn = screen.getByRole('button'); + describe('mdx', () => { + it('renders a fenced code block', () => { + const md = '```js\nconst x = 1;\n```'; + const Content = execute(md); + const { container } = render(); - expect(btn.parentNode?.nodeName.toLowerCase()).not.toBe('code'); + expect(container.querySelector('code')).toBeInTheDocument(); + expect(container.textContent).toContain('const x = 1;'); + }); }); - it.skip('allows undefined children?!', () => { - const { container } = render(); - - expect(container).toHaveTextContent(''); + describe('render', () => { + it('renders a code element', () => { + const { container } = render({'console.log("hi");'}); + expect(container.querySelector('code.rdmd-code')).toBeInTheDocument(); + }); + + it('renders children as code content', () => { + const { container } = render({'console.log("hi");'}); + expect(container).toHaveTextContent('console.log("hi");'); + }); + + it('handles undefined children', () => { + const { container } = render(); + expect(container).toHaveTextContent(''); + }); }); }); diff --git a/__tests__/components/CodeTabs.test.tsx b/__tests__/components/CodeTabs.test.tsx index eb2d420e5..630025438 100644 --- a/__tests__/components/CodeTabs.test.tsx +++ b/__tests__/components/CodeTabs.test.tsx @@ -1,11 +1,47 @@ +import '@testing-library/jest-dom'; import { render } from '@testing-library/react'; import React from 'react'; +import Code from '../../components/Code'; +import CodeTabs from '../../components/CodeTabs'; +import { mdxish, renderMdxish } from '../../lib'; import { execute } from '../helpers'; describe('CodeTabs', () => { - it.skip('render _all_ its children', () => { - const md = ` + describe('mdxish', () => { + it('combines consecutive code blocks into CodeTabs', () => { + const md = `\`\`\`js +const a = 1; +\`\`\` +\`\`\`py +a = 1 +\`\`\``; + const mod = renderMdxish(mdxish(md)); + const { container } = render(); + + expect(container.querySelector('.CodeTabs')).toBeInTheDocument(); + expect(container.textContent).toContain('const a = 1;'); + expect(container.textContent).toContain('a = 1'); + }); + + it('renders toolbar buttons with language names', () => { + const md = `\`\`\`javascript +code1 +\`\`\` +\`\`\`python +code2 +\`\`\``; + const mod = renderMdxish(mdxish(md)); + const { container } = render(); + + const buttons = container.querySelectorAll('button'); + expect(buttons).toHaveLength(2); + }); + }); + + describe('mdx', () => { + it('render _all_ its children', () => { + const md = ` \`\`\` assert('theme', 'dark'); \`\`\` @@ -13,10 +49,43 @@ assert('theme', 'dark'); assert('theme', 'light'); \`\`\` `; - const Component = execute(md); - const { container } = render(); + const Component = execute(md); + const { container } = render(); + + expect(container).toHaveTextContent("assert('theme', 'dark')"); + expect(container).toHaveTextContent("assert('theme', 'light')"); + }); + }); + + describe('render', () => { + it('renders the CodeTabs wrapper', () => { + const { container } = render( + +
{'console.log("hello");'}
+
, + ); + expect(container.querySelector('.CodeTabs')).toBeInTheDocument(); + }); + + it('renders toolbar buttons for each tab', () => { + const { container } = render( + +
{'const a = 1;'}
+
{'a = 1'}
+
, + ); + const buttons = container.querySelectorAll('.CodeTabs-toolbar button'); + expect(buttons).toHaveLength(2); + }); - expect(container).toHaveTextContent("assert('theme', 'dark')"); - expect(container).toHaveTextContent("assert('theme', 'light')"); + it('renders code content in CodeTabs-inner', () => { + const { container } = render( + +
{'const a = 1;'}
+
, + ); + expect(container.querySelector('.CodeTabs-inner')).toBeInTheDocument(); + expect(container).toHaveTextContent('const a = 1;'); + }); }); }); diff --git a/__tests__/components/Columns.test.tsx b/__tests__/components/Columns.test.tsx new file mode 100644 index 000000000..a88d7e82c --- /dev/null +++ b/__tests__/components/Columns.test.tsx @@ -0,0 +1,127 @@ +import type { Element } from 'hast'; + +import '@testing-library/jest-dom'; +import { render } from '@testing-library/react'; +import React from 'react'; + +import Columns, { Column } from '../../components/Columns'; +import { mdxish, renderMdxish } from '../../lib'; +import { execute } from '../helpers'; + +describe('Columns', () => { + describe('mdxish', () => { + describe('given Columns with two Column children', () => { + const md = ` + + Col 1 + Col 2 + +`; + const mod = renderMdxish(mdxish(md)); + + it('should render a Columns wrapper with grid style', () => { + const { container } = render(); + const columns = container.querySelector('.Columns'); + expect(columns).toBeInTheDocument(); + expect(columns).toHaveStyle({ gridTemplateColumns: 'repeat(2, auto)' }); + }); + + it('should render Column children', () => { + const { container } = render(); + const cols = container.querySelectorAll('.Column'); + expect(cols).toHaveLength(2); + expect(cols[0]).toHaveTextContent('Col 1'); + expect(cols[1]).toHaveTextContent('Col 2'); + }); + }); + + describe('given Columns with three children', () => { + const md = ` + + A + B + C + +`; + const mod = renderMdxish(mdxish(md)); + + it('should set grid columns based on child count', () => { + const { container } = render(); + const columns = container.querySelector('.Columns'); + expect(columns).toHaveStyle({ gridTemplateColumns: 'repeat(3, auto)' }); + }); + }); + + describe('given Columns with the HAST tree', () => { + const md = ` + + Col 1 + Col 2 + +`; + + it('should produce Column children in the tree', () => { + const tree = mdxish(md); + const columnsNode = tree.children[0] as Element; + expect(columnsNode.tagName).toBe('Columns'); + + const columnChildren = columnsNode.children.filter( + (child): child is Element => child.type === 'element' && child.tagName === 'Column', + ); + expect(columnChildren).toHaveLength(2); + }); + }); + }); + + describe('mdx', () => { + it('renders Columns with Column children', () => { + const md = 'Col 1Col 2'; + const Content = execute(md); + const { container } = render(); + + expect(container.querySelector('.Columns')).toBeInTheDocument(); + const cols = container.querySelectorAll('.Column'); + expect(cols).toHaveLength(2); + expect(cols[0]).toHaveTextContent('Col 1'); + expect(cols[1]).toHaveTextContent('Col 2'); + }); + }); + + describe('render', () => { + it('renders a Columns wrapper with grid style', () => { + const { container } = render( + + A + B + , + ); + const columns = container.querySelector('.Columns'); + expect(columns).toBeInTheDocument(); + expect(columns).toHaveStyle({ gridTemplateColumns: 'repeat(2, auto)' }); + }); + + it('renders Column children', () => { + const { container } = render( + + Col 1 + Col 2 + , + ); + const cols = container.querySelectorAll('.Column'); + expect(cols).toHaveLength(2); + expect(cols[0]).toHaveTextContent('Col 1'); + expect(cols[1]).toHaveTextContent('Col 2'); + }); + + it('adjusts grid columns based on child count', () => { + const { container } = render( + + A + B + C + , + ); + expect(container.querySelector('.Columns')).toHaveStyle({ gridTemplateColumns: 'repeat(3, auto)' }); + }); + }); +}); diff --git a/__tests__/components/Embed.test.tsx b/__tests__/components/Embed.test.tsx new file mode 100644 index 000000000..17437fcf3 --- /dev/null +++ b/__tests__/components/Embed.test.tsx @@ -0,0 +1,173 @@ +import type { Element } from 'hast'; + +import '@testing-library/jest-dom'; +import { render } from '@testing-library/react'; +import React from 'react'; + +import Embed from '../../components/Embed'; +import { mdast, mdx, mdxish, renderMdxish } from '../../lib'; +import { execute } from '../helpers'; + +describe('Embed', () => { + describe('mdxish', () => { + describe('given an Embed in link mode', () => { + const md = ` + +`; + const mod = renderMdxish(mdxish(md)); + + it('should render an embed wrapper', () => { + const { container } = render(); + expect(container.querySelector('.embed')).toBeInTheDocument(); + }); + + it('should render an embed-link anchor', () => { + const { container } = render(); + const link = container.querySelector('a.embed-link'); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', 'https://example.com'); + }); + + it('should render the embed title', () => { + const { container } = render(); + const title = container.querySelector('.embed-title'); + expect(title).toBeInTheDocument(); + expect(title).toHaveTextContent('Example'); + }); + }); + + describe('given an Embed in iframe mode', () => { + const md = ` + +`; + const mod = renderMdxish(mdxish(md)); + + it('should render an iframe element', () => { + const { container } = render(); + const iframe = container.querySelector('iframe'); + expect(iframe).toBeInTheDocument(); + expect(iframe).toHaveAttribute('src', 'https://example.com'); + expect(iframe).toHaveAttribute('title', 'Example'); + }); + }); + + describe('given an @embed link', () => { + it('should convert an @embed link to an embed HAST node', () => { + const txt = '[Embedded meta links.](https://nyti.me/s/gzoa2xb2v3 "@embed")'; + const hast = mdxish(txt); + const embed = hast.children[0] as Element; + + expect(embed.type).toBe('element'); + expect(embed.tagName).toBe('embed'); + expect(embed.properties.url).toBe('https://nyti.me/s/gzoa2xb2v3'); + expect(embed.properties.title).toBe('Embedded meta links.'); + }); + }); + + describe('given the HAST tree', () => { + const md = ` + +`; + + it('should pass props through the HAST tree', () => { + const tree = mdxish(md); + const node = tree.children[0] as Element; + expect(node.tagName).toBe('embed'); + expect(node.properties?.url).toBe('https://example.com'); + expect(node.properties?.title).toBe('Example'); + }); + }); + }); + + describe('mdx', () => { + it('renders an embed in link mode', () => { + const md = ''; + const Content = execute(md); + const { container } = render(); + + expect(container.querySelector('.embed')).toBeInTheDocument(); + const link = container.querySelector('a.embed-link'); + expect(link).toHaveAttribute('href', 'https://example.com'); + }); + + it('renders an embed in iframe mode', () => { + const md = ''; + const Content = execute(md); + const { container } = render(); + + const iframe = container.querySelector('iframe'); + expect(iframe).toBeInTheDocument(); + expect(iframe).toHaveAttribute('src', 'https://example.com'); + }); + + it('renders an embed with html content', () => { + const md = ``; + const Content = execute(md); + const { container } = render(); + + expect(container.innerHTML).toMatchSnapshot(); + }); + + it('renders an embed with meta content', () => { + const md = ``; + const Content = execute(md); + const { container } = render(); + + expect(container.innerHTML).toMatchSnapshot(); + }); + + it('renders an @embed link as an embed', () => { + const txt = '[Embedded meta links.](https://nyti.me/s/gzoa2xb2v3 "@embed")'; + const ast = mdast(txt); + const out = mdx(ast); + expect(out).toMatchSnapshot(); + }); + + it('renders an rdmd embed link', () => { + const md = '[rdmd](https://www.nytimes.com/2020/05/03/us/politics/george-w-bush-coronavirus-unity.html "@embed")'; + const Content = execute(md); + const { container } = render(); + + expect(container.innerHTML).toMatchSnapshot(); + }); + }); + + describe('render', () => { + it('renders in link mode with title', () => { + const { container } = render(); + expect(container.querySelector('.embed')).toBeInTheDocument(); + const link = container.querySelector('a.embed-link'); + expect(link).toHaveAttribute('href', 'https://example.com'); + expect(container.querySelector('.embed-title')).toHaveTextContent('Example'); + }); + + it('renders in iframe mode', () => { + const { container } = render(); + const iframe = container.querySelector('iframe'); + expect(iframe).toHaveAttribute('src', 'https://example.com'); + expect(iframe).toHaveAttribute('title', 'Example'); + }); + + it('renders html content when html prop is provided', () => { + const { container } = render(); + expect(container.querySelector('.embed-media')).toBeInTheDocument(); + }); + + it('derives provider from url', () => { + const { container } = render(); + expect(container.querySelector('.embed-provider')).toHaveTextContent('example.com'); + }); + }); +}); diff --git a/__tests__/components/Glossary.test.tsx b/__tests__/components/Glossary.test.tsx index f190f6539..0c1c35f53 100644 --- a/__tests__/components/Glossary.test.tsx +++ b/__tests__/components/Glossary.test.tsx @@ -1,14 +1,74 @@ -import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { fireEvent, render, screen } from '@testing-library/react'; import React from 'react'; +import { Glossary } from '../../components/Glossary'; +import { mdxish, renderMdxish } from '../../lib'; import { execute } from '../helpers'; describe('Glossary', () => { - it('renders a glossary item', () => { - const md = 'parliament'; - const Content = execute(md); - render(); + describe('mdxish', () => { + it('renders a glossary item without errors', () => { + const md = 'The term exogenous should render.'; + const mod = renderMdxish(mdxish(md)); + render(); - expect(screen.getByText('parliament')).toBeVisible(); + expect(screen.getByText('exogenous')).toBeVisible(); + }); + }); + + describe('mdx', () => { + it('renders a glossary item', () => { + const md = 'parliament'; + const Content = execute(md); + render(); + + expect(screen.getByText('parliament')).toBeVisible(); + }); + }); + + describe('render', () => { + it('renders the term when found in glossary', () => { + const terms = [{ term: 'API', definition: 'Application Programming Interface' }]; + render(API); + expect(screen.getByText('API')).toBeInTheDocument(); + }); + + it('renders a tooltip trigger when term is found', () => { + const terms = [{ term: 'API', definition: 'Application Programming Interface' }]; + const { container } = render(API); + expect(container.querySelector('.GlossaryItem-trigger')).toBeInTheDocument(); + }); + + it('renders a plain span when term is not found', () => { + const terms = [{ term: 'API', definition: 'Application Programming Interface' }]; + const { container } = render(unknown); + expect(container.querySelector('.GlossaryItem-trigger')).not.toBeInTheDocument(); + expect(screen.getByText('unknown')).toBeInTheDocument(); + }); + + it('shows tooltip with definition on hover', () => { + const terms = [{ term: 'acme', definition: 'This is a definition' }]; + const { container } = render(acme); + + const trigger = container.querySelector('.GlossaryItem-trigger'); + expect(trigger).toHaveTextContent('acme'); + fireEvent.mouseEnter(trigger!); + expect(screen.getByText('This is a definition', { exact: false })).toHaveTextContent( + 'acme - This is a definition', + ); + }); + + it('matches terms case-insensitively and shows tooltip', () => { + const terms = [{ term: 'aCme', definition: 'This is a definition' }]; + const { container } = render(acme); + + const trigger = container.querySelector('.GlossaryItem-trigger'); + expect(trigger).toHaveTextContent('acme'); + fireEvent.mouseEnter(trigger!); + expect(screen.getByText('This is a definition', { exact: false })).toHaveTextContent( + 'aCme - This is a definition', + ); + }); }); }); diff --git a/__tests__/components/HTMLBlock.test.tsx b/__tests__/components/HTMLBlock.test.tsx index b080762e8..5a982d448 100644 --- a/__tests__/components/HTMLBlock.test.tsx +++ b/__tests__/components/HTMLBlock.test.tsx @@ -1,3 +1,4 @@ +import '@testing-library/jest-dom'; import { render, screen, cleanup } from '@testing-library/react'; import React from 'react'; import { renderToStaticMarkup, renderToString } from 'react-dom/server'; @@ -5,60 +6,86 @@ import { renderToStaticMarkup, renderToString } from 'react-dom/server'; import { vi } from 'vitest'; import HTMLBlock from '../../components/HTMLBlock'; +import { mdxish, renderMdxish } from '../../lib'; import { execute } from '../helpers'; describe('HTML Block', () => { - beforeEach(() => { - global.mockFn = vi.fn(); - }); + describe('mdxish', () => { + it('renders an HTML block from markdown', () => { + const md = `
+

Hello from HTML

+
`; + const mod = renderMdxish(mdxish(md)); + const { container } = render(); - afterEach(() => { - cleanup(); - vi.restoreAllMocks(); - }); + expect(container.querySelector('.custom-block')).toBeInTheDocument(); + expect(container.textContent).toContain('Hello from HTML'); + }); - it('runs user scripts in compat mode', () => { - render({''}); - expect(global.mockFn).toHaveBeenCalledTimes(1); - }); + it('renders a simple HTML element', () => { + const md = '
'; + const mod = renderMdxish(mdxish(md)); + const { container } = render(); - it("doesn't run user scripts by default", () => { - render({''}); - expect(global.mockFn).toHaveBeenCalledTimes(0); + expect(container.querySelector('hr')).toBeInTheDocument(); + }); }); - it("doesn't render user scripts by default", () => { - render({''}); - expect(screen.queryByText('mockFn()')).not.toBeInTheDocument(); + describe('mdx', () => { + it('renders the html in a `
` tag if safeMode={true}', () => {
+      const md = '{`