/** * Tests for components/subscription-manager.js — renderSubscriptionCard. * * The card is built by template-string interpolation, so the assertions * focus on data correctness (escaping, branching) rather than CSS class * spelling. Covers active vs paused, optional folder/notes, and the * data-subscription-id round-trip used by the action buttons. * * Kept in its own file (instead of extending subscription-manager.test.js) * so it can land alongside / independently of PR #4297 without merge * conflicts on a freshly-created test file. */ // Load xss-protection.js first so window.escapeHtml is defined: the card // renderer escapes user fields via the global helper (the class no longer // ships its own escapeHtml). In production, base.html loads xss-protection.js // before subscription-manager.js with matching `defer` ordering. import '@js/security/xss-protection.js'; import '@js/components/subscription-manager.js'; let manager; beforeAll(() => { // The module wires up its singleton inside a DOMContentLoaded listener; // dispatch it manually if happy-dom already fired before the dynamic // import settled. if (!window.subscriptionManager) { document.dispatchEvent(new Event('DOMContentLoaded')); } manager = window.subscriptionManager; }); function makeSubscription(overrides = {}) { // A baseline subscription that exercises the always-present fields. // Use a far-future next_refresh so the computed timeUntil is stable // (e.g. "365d" rather than something time-sensitive). const oneYearMs = 365 * 24 * 60 * 60 * 1000; return { id: 'sub-abc-123', query_or_topic: 'AI safety', refresh_interval_minutes: 60, next_refresh: new Date(Date.now() + oneYearMs).toISOString(), status: 'active', ...overrides, }; } describe('subscriptionManager.renderSubscriptionCard', () => { it('includes core fields (id, query, interval) in the output', () => { const html = manager.renderSubscriptionCard(makeSubscription()); expect(html).toContain('data-subscription-id="sub-abc-123"'); expect(html).toContain('AI safety'); expect(html).toContain('Every 60 min'); }); it('shows a Pause button + pause icon when status is active', () => { const html = manager.renderSubscriptionCard( makeSubscription({ status: 'active' }) ); expect(html).toContain('title="Pause"'); expect(html).toContain('bi-pause'); expect(html).not.toContain('title="Resume"'); expect(html).not.toContain('bi-play'); }); it('shows a Resume button + play icon when status is not active (paused)', () => { const html = manager.renderSubscriptionCard( makeSubscription({ status: 'paused' }) ); expect(html).toContain('title="Resume"'); expect(html).toContain('bi-play'); expect(html).not.toContain('title="Pause"'); }); it('omits the folder span when subscription has no folder', () => { const html = manager.renderSubscriptionCard( makeSubscription({ folder: undefined }) ); expect(html).not.toContain('bi-folder'); }); it('renders the folder span when a folder is set', () => { const html = manager.renderSubscriptionCard( makeSubscription({ folder: 'Research' }) ); expect(html).toContain('bi-folder'); expect(html).toContain('Research'); }); it('omits the notes paragraph when no notes are provided', () => { const html = manager.renderSubscriptionCard( makeSubscription({ notes: undefined }) ); // The notes branch is the only

in the card template. expect(html).not.toContain('

{ const html = manager.renderSubscriptionCard( makeSubscription({ notes: 'Important topic' }) ); expect(html).toContain('Important topic'); expect(html).toContain('

{ const html = manager.renderSubscriptionCard( makeSubscription({ query_or_topic: '', folder: '', notes: '', }) ); // Local escapeHtml uses div+textContent+innerHTML, which escapes // `<` to `<`. The literal `'); expect(html).not.toContain(''); // The escaped form should be present. expect(html).toContain('<script>'); }); it('embeds the formatTimeUntil output near "Next:"', () => { // Sanity check that the formatTimeUntil method is being called and // its output is interpolated into the card. The exact number depends // on system time, so just assert that a unit suffix follows "Next:". const html = manager.renderSubscriptionCard(makeSubscription()); expect(html).toMatch(/Next:\s*\d+[mhd]/); }); });