refactor: dedupe art-tray thumbnail template; test real escaping path
This commit is contained in:
@@ -10,13 +10,17 @@ export class ArtTray extends HTMLElement {
|
||||
}
|
||||
connectedCallback() { this.render(); }
|
||||
|
||||
render() {
|
||||
const results = searchArt(this.manifest, this._query);
|
||||
const grid = results.map((a) => `
|
||||
renderThumb(a) {
|
||||
return `
|
||||
<button class="thumb" data-art-id="${escapeHtml(a.id)}" title="${escapeHtml(a.label)}">
|
||||
<img src="${escapeHtml(a.src)}" alt="${escapeHtml(a.label)}" loading="lazy" />
|
||||
<span class="thumb-cap">${escapeHtml(a.label)}</span>
|
||||
</button>`).join('');
|
||||
</button>`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const results = searchArt(this.manifest, this._query);
|
||||
const grid = results.map((a) => this.renderThumb(a)).join('');
|
||||
|
||||
this.innerHTML = `
|
||||
<div class="art-tray">
|
||||
@@ -33,11 +37,7 @@ export class ArtTray extends HTMLElement {
|
||||
renderResults() {
|
||||
const results = searchArt(this.manifest, this._query);
|
||||
const container = this.querySelector('[data-role=art-results]');
|
||||
container.innerHTML = results.map((a) => `
|
||||
<button class="thumb" data-art-id="${escapeHtml(a.id)}" title="${escapeHtml(a.label)}">
|
||||
<img src="${escapeHtml(a.src)}" alt="${escapeHtml(a.label)}" loading="lazy" />
|
||||
<span class="thumb-cap">${escapeHtml(a.label)}</span>
|
||||
</button>`).join('') || '<div class="art-empty">No matches</div>';
|
||||
container.innerHTML = results.map((a) => this.renderThumb(a)).join('') || '<div class="art-empty">No matches</div>';
|
||||
this.bindThumbs();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,10 +34,14 @@ describe('<art-tray>', () => {
|
||||
expect(detail).toEqual({ src: '/art/b.png', label: 'Neon street' });
|
||||
});
|
||||
|
||||
it('escapes a query containing a quote (input does not break)', () => {
|
||||
const input = el.querySelector('[data-role=art-query]');
|
||||
input.value = 'rain " <x>';
|
||||
input.dispatchEvent(new Event('input'));
|
||||
expect(el.querySelector('[data-role=art-query]').value).toBe('rain " <x>');
|
||||
it('escapes malicious manifest label/src in rendered thumbnails', () => {
|
||||
document.body.innerHTML = '';
|
||||
const bad = document.createElement('art-tray');
|
||||
bad.manifest = [{ id: 'x', src: '/art/x.png', label: 'evil" onerror="boom', tags: ['x'] }];
|
||||
document.body.appendChild(bad);
|
||||
const img = bad.querySelector('[data-art-id="x"] img');
|
||||
expect(img).not.toBe(null);
|
||||
expect(img.hasAttribute('onerror')).toBe(false); // attribute not injected
|
||||
expect(img.getAttribute('alt')).toBe('evil" onerror="boom'); // literal value, escaped then parsed back
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user