|
5 | 5 | generateNotFoundSVG, |
6 | 6 | generateRateLimitSVG, |
7 | 7 | generateHeatmapSVG, |
| 8 | + generatePulseSVG, |
8 | 9 | particleCount, |
9 | 10 | escapeXML, |
10 | 11 | getSizeScale, |
@@ -1778,3 +1779,111 @@ describe('buildTowerPaths', () => { |
1778 | 1779 | expect(paths.top).toBe('M0 -9 L7.2 -4.5 L0 0 L-7.2 -4.5 Z'); |
1779 | 1780 | }); |
1780 | 1781 | }); |
| 1782 | + |
| 1783 | +// ── generatePulseSVG accessibility ───────────────────────────────────────── |
| 1784 | + |
| 1785 | +describe('generatePulseSVG accessibility', () => { |
| 1786 | + const mockStats: StreakStats = { |
| 1787 | + currentStreak: 3, |
| 1788 | + longestStreak: 7, |
| 1789 | + totalContributions: 50, |
| 1790 | + todayDate: '2024-06-12', |
| 1791 | + }; |
| 1792 | + |
| 1793 | + // Build 5 weeks × 6 days = 30 days of deterministic contribution data |
| 1794 | + const weeks: ContributionCalendar['weeks'] = []; |
| 1795 | + for (let w = 0; w < 5; w++) { |
| 1796 | + const contributionDays = []; |
| 1797 | + for (let d = 0; d < 6; d++) { |
| 1798 | + const idx = w * 6 + d; |
| 1799 | + contributionDays.push({ |
| 1800 | + contributionCount: idx % 5, |
| 1801 | + date: `2024-05-${String(idx + 1).padStart(2, '0')}`, |
| 1802 | + }); |
| 1803 | + } |
| 1804 | + weeks.push({ contributionDays } as ContributionCalendar['weeks'][number]); |
| 1805 | + } |
| 1806 | + const mockCalendar = { weeks } as ContributionCalendar; |
| 1807 | + |
| 1808 | + const baseParams: BadgeParams = { |
| 1809 | + user: 'octocat', |
| 1810 | + } as unknown as BadgeParams; |
| 1811 | + |
| 1812 | + it('includes role="img" on the root svg', () => { |
| 1813 | + const svg = generatePulseSVG(mockStats, baseParams, mockCalendar); |
| 1814 | + expect(svg).toContain('role="img"'); |
| 1815 | + }); |
| 1816 | + |
| 1817 | + it('includes aria-labelledby referencing cp-title-<safeId>', () => { |
| 1818 | + const svg = generatePulseSVG(mockStats, baseParams, mockCalendar); |
| 1819 | + expect(svg).toContain('aria-labelledby="cp-title-octocat"'); |
| 1820 | + }); |
| 1821 | + |
| 1822 | + it('includes aria-describedby referencing cp-desc-<safeId>', () => { |
| 1823 | + const svg = generatePulseSVG(mockStats, baseParams, mockCalendar); |
| 1824 | + expect(svg).toContain('aria-describedby="cp-desc-octocat"'); |
| 1825 | + }); |
| 1826 | + |
| 1827 | + it('includes <title> with the correct id attribute', () => { |
| 1828 | + const svg = generatePulseSVG(mockStats, baseParams, mockCalendar); |
| 1829 | + expect(svg).toContain('<title id="cp-title-octocat">Heartbeat Sparkline for octocat</title>'); |
| 1830 | + }); |
| 1831 | + |
| 1832 | + it('includes <desc> with the correct id attribute and pulseTotal in its text', () => { |
| 1833 | + const svg = generatePulseSVG(mockStats, baseParams, mockCalendar); |
| 1834 | + expect(svg).toContain('<desc id="cp-desc-octocat">'); |
| 1835 | + expect(svg).toContain('showing commit activity over the last 30 days'); |
| 1836 | + expect(svg).toMatch(/total commits: \d+/); |
| 1837 | + }); |
| 1838 | + |
| 1839 | + it('sanitizes special characters in username for the safeId', () => { |
| 1840 | + const svg = generatePulseSVG( |
| 1841 | + mockStats, |
| 1842 | + { ...baseParams, user: 'user.name+test' } as unknown as BadgeParams, |
| 1843 | + mockCalendar |
| 1844 | + ); |
| 1845 | + expect(svg).toContain('aria-labelledby="cp-title-user_name_test"'); |
| 1846 | + expect(svg).toContain('aria-describedby="cp-desc-user_name_test"'); |
| 1847 | + expect(svg).toContain('id="cp-title-user_name_test"'); |
| 1848 | + expect(svg).toContain('id="cp-desc-user_name_test"'); |
| 1849 | + }); |
| 1850 | + |
| 1851 | + it('produces valid SVG markup (no parsererror)', () => { |
| 1852 | + const svg = generatePulseSVG(mockStats, baseParams, mockCalendar); |
| 1853 | + assertValidSVG(svg); |
| 1854 | + }); |
| 1855 | + |
| 1856 | + describe('auto-theme variant', () => { |
| 1857 | + const autoParams: BadgeParams = { |
| 1858 | + user: 'octocat', |
| 1859 | + autoTheme: true, |
| 1860 | + } as unknown as BadgeParams; |
| 1861 | + |
| 1862 | + it('includes aria-labelledby in auto-theme output', () => { |
| 1863 | + const svg = generatePulseSVG(mockStats, autoParams, mockCalendar); |
| 1864 | + expect(svg).toContain('aria-labelledby="cp-title-octocat"'); |
| 1865 | + }); |
| 1866 | + |
| 1867 | + it('includes aria-describedby in auto-theme output', () => { |
| 1868 | + const svg = generatePulseSVG(mockStats, autoParams, mockCalendar); |
| 1869 | + expect(svg).toContain('aria-describedby="cp-desc-octocat"'); |
| 1870 | + }); |
| 1871 | + |
| 1872 | + it('includes id-bearing <title> in auto-theme output', () => { |
| 1873 | + const svg = generatePulseSVG(mockStats, autoParams, mockCalendar); |
| 1874 | + expect(svg).toContain('<title id="cp-title-octocat">Heartbeat Sparkline for octocat</title>'); |
| 1875 | + }); |
| 1876 | + |
| 1877 | + it('includes id-bearing <desc> with pulseTotal in auto-theme output', () => { |
| 1878 | + const svg = generatePulseSVG(mockStats, autoParams, mockCalendar); |
| 1879 | + expect(svg).toContain('<desc id="cp-desc-octocat">'); |
| 1880 | + expect(svg).toContain('showing commit activity over the last 30 days'); |
| 1881 | + expect(svg).toMatch(/total commits: \d+/); |
| 1882 | + }); |
| 1883 | + |
| 1884 | + it('produces valid SVG markup in auto-theme mode (no parsererror)', () => { |
| 1885 | + const svg = generatePulseSVG(mockStats, autoParams, mockCalendar); |
| 1886 | + assertValidSVG(svg); |
| 1887 | + }); |
| 1888 | + }); |
| 1889 | +}); |
0 commit comments