Skip to content

Commit b46f235

Browse files
Add unit tests for HTML sanitization, link safety, page titles, and conditional report navigation
1 parent 97f84de commit b46f235

4 files changed

Lines changed: 96 additions & 0 deletions

File tree

‎tests/unit/html5-attributes.test.js‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,20 @@ describe('HTML5 attribute compliance (sources)', () => {
8282
expect(html).toContain('<td>');
8383
});
8484

85+
for (const attribute of OBSOLETE_ATTRIBUTES) {
86+
it(`strips the obsolete ${attribute} attribute from rendered markup`, () => {
87+
const html = renderTemplate(
88+
`<div id="subject" ${attribute}="legacy-value">content</div>`,
89+
{}
90+
);
91+
const subject = new JSDOM(html).window.document.querySelector('#subject');
92+
93+
expect(subject).not.toBeNull();
94+
expect(subject.hasAttribute(attribute)).toBe(false);
95+
expect(subject.textContent).toBe('content');
96+
});
97+
}
98+
8599
it('preserves valid ordered-list numbering', () => {
86100
const html = renderTemplate('<ol start="3"><li>Third</li></ol>', {});
87101
const list = new JSDOM(html).window.document.querySelector('ol');

‎tests/unit/page-titles.test.js‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ const PAGES = {
1616
'terms-of-service': () => import('../../app/terms-of-service/page.jsx')
1717
};
1818

19+
const CHANGED_PAGE_TITLES = {
20+
about: 'About - RefactorFirst',
21+
api: 'API - RefactorFirst',
22+
documentation: 'Documentation - RefactorFirst',
23+
examples: 'Example Reports - RefactorFirst',
24+
faq: 'FAQ - RefactorFirst',
25+
feedback: 'Feedback - RefactorFirst',
26+
'getting-started': 'Getting Started - RefactorFirst',
27+
'privacy-policy': 'Privacy Policy - RefactorFirst',
28+
'terms-of-service': 'Terms of Service - RefactorFirst'
29+
};
30+
1931
describe('static page titles (WCAG 2.4.2)', () => {
2032
it('every static page exports a descriptive, unique title', async () => {
2133
const titles = new Map();
@@ -33,4 +45,11 @@ describe('static page titles (WCAG 2.4.2)', () => {
3345
titles.set(title, route);
3446
}
3547
});
48+
49+
it('uses the expected page-specific title before the site name', async () => {
50+
for (const [route, expectedTitle] of Object.entries(CHANGED_PAGE_TITLES)) {
51+
const mod = await PAGES[route]();
52+
expect(mod.metadata.title).toBe(expectedTitle);
53+
}
54+
});
3655
});

‎tests/unit/renderer.test.js‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,29 @@ describe('Mustache Rendering', () => {
6767
expect(result).toContain('<p>safe</p>');
6868
})
6969

70+
const forbiddenElements = [
71+
['script', '<script>window.__xss = 1</script>'],
72+
['iframe', '<iframe src="https://evil.example"></iframe>'],
73+
['object', '<object data="https://evil.example/payload"></object>'],
74+
['embed', '<embed src="https://evil.example/payload">'],
75+
['meta', '<meta http-equiv="refresh" content="0;url=https://evil.example">'],
76+
['link', '<link rel="stylesheet" href="https://evil.example/styles.css">'],
77+
['form', '<form action="https://evil.example"><button>Submit</button></form>'],
78+
['input', '<input name="secret" value="credential">'],
79+
['select', '<select name="secret"><option>credential</option></select>'],
80+
['textarea', '<textarea name="secret">credential</textarea>']
81+
];
82+
83+
for (const [tag, markup] of forbiddenElements) {
84+
it(`strips forbidden <${tag}> elements from raw interpolations`, () => {
85+
const html = renderTemplate('<section>{{{content}}}</section>', { content: markup });
86+
const document = new JSDOM(html).window.document;
87+
88+
expect(document.querySelector(tag)).toBeNull();
89+
expect(document.querySelector('section')).not.toBeNull();
90+
});
91+
}
92+
7093
it('should handle empty data', () => {
7194
const template = '{{name}}';
7295
const result = renderTemplate(template, {});
@@ -128,6 +151,19 @@ describe('templating safety (repository-provided templates are untrusted)', () =
128151
expect(document.querySelector('div[target="_blank"]')?.hasAttribute('rel')).toBe(false);
129152
});
130153

154+
it('removes an explicit opener relationship from links that open a new context', () => {
155+
const html = renderTemplate(
156+
'<a href="https://example.com" target="_blank" rel="opener nofollow">New tab</a>',
157+
{}
158+
);
159+
const link = new JSDOM(html).window.document.querySelector('a');
160+
const relationships = link.getAttribute('rel').split(/\s+/);
161+
162+
expect(relationships).toContain('noopener');
163+
expect(relationships).toContain('noreferrer');
164+
expect(relationships).not.toContain('opener');
165+
});
166+
131167
it('keeps benign structure, style attributes and data attributes intact', () => {
132168
const html = renderTemplate(
133169
'<div id="popup-classGraph" style="width: 100%;"><span class="close-btn">&times;</span></div>',

‎tests/unit/report-template-wcag.test.js‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,31 @@ describe('report template WCAG 2.2 AA (rendered with fixture data)', () => {
115115

116116
expect(docWithoutCycleMap.querySelector('a[href="#CYCLEMAP"]')).toBeNull();
117117
});
118+
119+
it('omits optional navigation links when their report sections are absent', () => {
120+
const sparseFixture = structuredClone(fixture);
121+
sparseFixture.classRelationshipsToRemove.hasRelationships = false;
122+
sparseFixture.packageMap.hasEdges = false;
123+
sparseFixture.packageRelationshipsToRemove.hasRelationships = false;
124+
sparseFixture.hasDisharmonies = false;
125+
sparseFixture.disharmonies = [];
126+
sparseFixture.classCycles.hasCycles = false;
127+
128+
const sparseDoc = new JSDOM(renderTemplate(template, sparseFixture)).window.document;
129+
const optionalTargets = [
130+
'#CLASSEDGES',
131+
'#PACKAGEMAP',
132+
'#PACKAGEEDGES',
133+
'#DISHARMONIES',
134+
'#CYCLES',
135+
'#CYCLEMAP'
136+
];
137+
138+
expect(sparseDoc.querySelector('a[href="#CLASSMAP"]')).not.toBeNull();
139+
expect(sparseDoc.getElementById('CLASSMAP')).not.toBeNull();
140+
for (const target of optionalTargets) {
141+
expect(sparseDoc.querySelector(`a[href="${target}"]`)).toBeNull();
142+
expect(sparseDoc.getElementById(target.slice(1))).toBeNull();
143+
}
144+
});
118145
});

0 commit comments

Comments
 (0)