// @vitest-environment jsdom
import { describe, expect, it } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import { XmlTree } from '../XmlTree';
const parseXml = (source: string): Element => {
const doc = new DOMParser().parseFromString(source, 'application/xml');
return doc.documentElement;
};
describe('XmlTree', () => {
it('renders the root tag name', () => {
render(')} />);
// Self-closing — no closing tag, so a single occurrence is expected.
expect(screen.getAllByText('feed')).toHaveLength(1);
});
it('renders attributes inline with the open tag', () => {
render(')} />);
expect(screen.getByText('id')).toBeInTheDocument();
expect(screen.getByText('42')).toBeInTheDocument();
expect(screen.getByText('lang')).toBeInTheDocument();
expect(screen.getByText('en')).toBeInTheDocument();
});
it('renders non-whitespace text content', () => {
render(Hello world')} />);
expect(screen.getByText('Hello world')).toBeInTheDocument();
});
it('filters whitespace-only text nodes between sibling elements', () => {
// Pretty-printed XML has whitespace text nodes between every sibling
// element. After filtering, only the two - elements survive —
// no stray text nodes containing the source's `\n ` indentation.
const { container } = render(
\n
- a
\n - b
\n')}
/>,
);
// Each - renders as open + close tag, so 2 items × 2 = 4
// occurrences of "item" text.
expect(screen.getAllByText('item')).toHaveLength(4);
// The leaf text values are present.
expect(screen.getByText('a')).toBeInTheDocument();
expect(screen.getByText('b')).toBeInTheDocument();
// The original source's whitespace indentation between
-
// siblings should not appear as a standalone text node. The exact
// pattern "\n " (newline + two spaces) was between siblings; if
// we'd rendered it, the textContent would contain that fragment
// outside of the element tags.
const allText = container.textContent ?? '';
expect(allText).not.toMatch(/<\/item>\s*\n\s+
- /);
});
it('renders CDATA content wrapped in markers', () => {
render(
html
]]>')} />,
);
expect(screen.getByText('')).toBeInTheDocument();
expect(screen.getByText('html
')).toBeInTheDocument();
});
it('recursively renders nested elements', () => {
render(
T')}
/>,
);
// Each non-self-closing element renders open + close, hence 2x.
expect(screen.getAllByText('feed')).toHaveLength(2);
expect(screen.getAllByText('entry')).toHaveLength(2);
expect(screen.getAllByText('title')).toHaveLength(2);
expect(screen.getByText('T')).toBeInTheDocument();
});
it('hides children via display:none on collapse (does not unmount)', () => {
// The whole point of using display:none rather than `{!collapsed
// && }` is that nested collapse state survives a parent
// collapse/expand cycle. Verify the mechanism directly at the DOM
// level: after collapsing, the inner element's tag name STILL
// appears in the rendered tree (it's in the DOM, just hidden).
const { container } = render(
v')} />,
);
// Both elements visible initially.
expect(container.textContent).toContain('inner');
// Collapse outer. There is one chevron button on outer (since
// outer has a renderable child); click it.
const chevronButtons = container.querySelectorAll('button');
fireEvent.click(chevronButtons[0]);
// Inner's tag name MUST still be in the DOM textContent — that's
// the unmount-vs-display-none assertion. If we used unmount, the
// string "inner" would have vanished entirely.
expect(container.textContent).toContain('inner');
// The children container should have display:none applied so the
// inner element is visually hidden.
const hiddenContainers = container.querySelectorAll(
'div[style*="display: none"]',
);
expect(hiddenContainers.length).toBeGreaterThan(0);
});
it('toggles the chevron icon aria-label between Collapse and Expand', () => {
const { container } = render(
')} />,
);
const button = container.querySelector('button');
if (!button) throw new Error('expected a chevron button');
expect(button.getAttribute('aria-label')).toBe('Collapse');
fireEvent.click(button);
expect(button.getAttribute('aria-label')).toBe('Expand');
fireEvent.click(button);
expect(button.getAttribute('aria-label')).toBe('Collapse');
});
it('renders a self-closing form when the element has no renderable children', () => {
const { container } = render(')} />);
// Self-closing should render as `` — i.e. there should be
// no separate closing tag span. Look for the inline " />" marker.
expect(container.textContent).toContain('/>');
// No closing-tag line — i.e. no `` rendered.
expect(container.textContent ?? '').not.toContain('');
});
it('renders a closing tag for elements with children', () => {
const { container } = render(
')} />,
);
// wrap should have `` closing tag visible.
expect(container.textContent ?? '').toContain('');
});
it('preserves namespace prefixes in element names', () => {
render(
',
)}
/>,
);
expect(screen.getByText('media:thumbnail')).toBeInTheDocument();
});
});