// @vitest-environment jsdom
import type { ReactElement } from 'react';
import { describe, expect, it } from 'vitest';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';
import { xmlRenderer } from '../xml';
import type { RenderCtx } from '../types';
const baseCtx: RenderCtx = {
contentType: 'application/xml',
url: 'https://example.com/feed.xml',
};
const renderXml = (view: 'preview' | 'raw', body: string) =>
render(xmlRenderer.render({ view, body, ctx: baseCtx }) as ReactElement);
const VALID_XML =
'Demo';
describe('xmlRenderer', () => {
it('declares both preview and raw views with preview as default', () => {
expect(xmlRenderer.views).toEqual(['preview', 'raw']);
expect(xmlRenderer.defaultView).toBe('preview');
});
it('supports override (XML bodies are strings — the editor works on them)', () => {
expect(xmlRenderer.supportsOverride).toBe(true);
});
describe('preview view', () => {
it('renders the parsed XML tree (tag names visible)', () => {
renderXml('preview', VALID_XML);
// Each non-self-closing element renders both an open and a close
// tag with the same name — two occurrences each is expected.
expect(screen.getAllByText('feed').length).toBeGreaterThan(0);
expect(screen.getAllByText('title').length).toBeGreaterThan(0);
expect(screen.getByText('Demo')).toBeInTheDocument();
});
});
describe('raw view', () => {
it('renders the XML source as text and does not render a tree', () => {
renderXml('raw', VALID_XML);
// The full source should be present as a CodeBlock text node.
expect(screen.getByText(VALID_XML)).toBeInTheDocument();
// The tag-name span (used by the tree, with the standalone text
// "feed" inside a styled span) should not appear when raw — the
// raw source contains "feed" only inside the larger XML literal.
const standaloneFeed = screen
.queryAllByText('feed')
.filter((el) => el.tagName.toLowerCase() === 'span');
expect(standaloneFeed).toHaveLength(0);
});
});
describe('malformed XML', () => {
it('falls back to source + warning when parser produces a parsererror element', () => {
// Forces the Chrome-style failure where DOMParser returns a
// Document whose documentElement IS the parsererror element.
const MALFORMED = '';
renderXml('preview', MALFORMED);
expect(screen.getByText(MALFORMED)).toBeInTheDocument();
expect(
screen.getByText(/Failed to parse as XML, showing as raw text/),
).toBeInTheDocument();
});
it('also flags Firefox-style nested parsererror (namespace-aware detection)', () => {
// Build a Document by hand that mimics Firefox's failure shape:
// a valid documentElement that nests a in the
// Firefox XML-parser-error namespace. The hasParseError helper
// must detect this via getElementsByTagNameNS, not just by
// checking documentElement.nodeName.
//
// We can't easily force the Firefox parser path inside jsdom,
// but we can verify the detector via a hand-crafted body that
// includes the FF namespace as XML — when DOMParser parses it,
// the resulting document will contain a parsererror in that NS.
const FIREFOX_LIKE =
'broken';
renderXml('preview', FIREFOX_LIKE);
expect(
screen.getByText(/Failed to parse as XML, showing as raw text/),
).toBeInTheDocument();
});
});
describe('content-type acceptance', () => {
it('renders an application/xhtml+xml body as a tree', () => {
const XHTML =
'Hi
';
renderXml('preview', XHTML);
// Each element renders open + close, so 2x per tag.
expect(screen.getAllByText('html').length).toBeGreaterThan(0);
expect(screen.getAllByText('body').length).toBeGreaterThan(0);
expect(screen.getAllByText('p').length).toBeGreaterThan(0);
expect(screen.getByText('Hi')).toBeInTheDocument();
});
});
});