1
0

glyphs.test.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. /**
  2. * Glyph fallback / Unicode-support detection.
  3. *
  4. * Pinned because the matrix is small and the consequence of regression
  5. * is highly visible: shimmer-worker output on Windows mojibakes when
  6. * UTF-8 glyphs are written via `fs.writeSync` (see #168). The detection
  7. * + ASCII fallback is the contract that prevents this.
  8. */
  9. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  10. import {
  11. supportsUnicode,
  12. getGlyphs,
  13. UNICODE_GLYPHS,
  14. ASCII_GLYPHS,
  15. _resetGlyphsCache,
  16. } from '../src/ui/glyphs';
  17. function withEnv(patch: Record<string, string | undefined>, fn: () => void): void {
  18. const saved: Record<string, string | undefined> = {};
  19. const savedPlatform = process.platform;
  20. for (const key of Object.keys(patch)) {
  21. saved[key] = process.env[key];
  22. if (patch[key] === undefined) delete process.env[key];
  23. else process.env[key] = patch[key];
  24. }
  25. _resetGlyphsCache();
  26. try {
  27. fn();
  28. } finally {
  29. for (const key of Object.keys(saved)) {
  30. if (saved[key] === undefined) delete process.env[key];
  31. else process.env[key] = saved[key];
  32. }
  33. Object.defineProperty(process, 'platform', { value: savedPlatform });
  34. _resetGlyphsCache();
  35. }
  36. }
  37. function setPlatform(value: NodeJS.Platform): void {
  38. Object.defineProperty(process, 'platform', { value });
  39. }
  40. describe('supportsUnicode', () => {
  41. let originalPlatform: NodeJS.Platform;
  42. beforeEach(() => {
  43. originalPlatform = process.platform;
  44. _resetGlyphsCache();
  45. });
  46. afterEach(() => {
  47. Object.defineProperty(process, 'platform', { value: originalPlatform });
  48. _resetGlyphsCache();
  49. });
  50. it('returns false on Windows by default (mojibake-prone consoles)', () => {
  51. withEnv({ CODEGRAPH_ASCII: undefined, CODEGRAPH_UNICODE: undefined, TERM: undefined }, () => {
  52. setPlatform('win32');
  53. expect(supportsUnicode()).toBe(false);
  54. });
  55. });
  56. it('returns true on macOS by default', () => {
  57. withEnv({ CODEGRAPH_ASCII: undefined, CODEGRAPH_UNICODE: undefined, TERM: undefined }, () => {
  58. setPlatform('darwin');
  59. expect(supportsUnicode()).toBe(true);
  60. });
  61. });
  62. it('returns true on Linux by default', () => {
  63. withEnv({ CODEGRAPH_ASCII: undefined, CODEGRAPH_UNICODE: undefined, TERM: undefined }, () => {
  64. setPlatform('linux');
  65. expect(supportsUnicode()).toBe(true);
  66. });
  67. });
  68. it('returns false on Linux kernel console (TERM=linux)', () => {
  69. withEnv({ CODEGRAPH_ASCII: undefined, CODEGRAPH_UNICODE: undefined, TERM: 'linux' }, () => {
  70. setPlatform('linux');
  71. expect(supportsUnicode()).toBe(false);
  72. });
  73. });
  74. it('respects CODEGRAPH_UNICODE=1 on Windows (opt-in escape hatch)', () => {
  75. withEnv({ CODEGRAPH_UNICODE: '1', CODEGRAPH_ASCII: undefined }, () => {
  76. setPlatform('win32');
  77. expect(supportsUnicode()).toBe(true);
  78. });
  79. });
  80. it('respects CODEGRAPH_ASCII=1 on macOS (opt-out escape hatch)', () => {
  81. withEnv({ CODEGRAPH_ASCII: '1', CODEGRAPH_UNICODE: undefined }, () => {
  82. setPlatform('darwin');
  83. expect(supportsUnicode()).toBe(false);
  84. });
  85. });
  86. it('CODEGRAPH_ASCII takes precedence over CODEGRAPH_UNICODE', () => {
  87. withEnv({ CODEGRAPH_ASCII: '1', CODEGRAPH_UNICODE: '1' }, () => {
  88. setPlatform('darwin');
  89. expect(supportsUnicode()).toBe(false);
  90. });
  91. });
  92. });
  93. describe('getGlyphs', () => {
  94. let originalPlatform: NodeJS.Platform;
  95. beforeEach(() => {
  96. originalPlatform = process.platform;
  97. _resetGlyphsCache();
  98. });
  99. afterEach(() => {
  100. Object.defineProperty(process, 'platform', { value: originalPlatform });
  101. _resetGlyphsCache();
  102. });
  103. it('returns ASCII glyphs on Windows', () => {
  104. withEnv({ CODEGRAPH_ASCII: undefined, CODEGRAPH_UNICODE: undefined }, () => {
  105. setPlatform('win32');
  106. const g = getGlyphs();
  107. expect(g).toBe(ASCII_GLYPHS);
  108. expect(g.ok).toBe('[OK]');
  109. expect(g.rail).toBe('|');
  110. expect(g.phaseDone).toBe('*');
  111. expect(g.dash).toBe('-');
  112. });
  113. });
  114. it('returns Unicode glyphs on macOS', () => {
  115. withEnv({ CODEGRAPH_ASCII: undefined, CODEGRAPH_UNICODE: undefined }, () => {
  116. setPlatform('darwin');
  117. const g = getGlyphs();
  118. expect(g).toBe(UNICODE_GLYPHS);
  119. expect(g.ok).toBe('✓');
  120. expect(g.rail).toBe('│');
  121. expect(g.phaseDone).toBe('◆');
  122. expect(g.dash).toBe('—');
  123. });
  124. });
  125. it('caches the result so repeated calls return the same object', () => {
  126. withEnv({ CODEGRAPH_ASCII: undefined, CODEGRAPH_UNICODE: undefined }, () => {
  127. setPlatform('darwin');
  128. expect(getGlyphs()).toBe(getGlyphs());
  129. });
  130. });
  131. });
  132. describe('Glyph sets', () => {
  133. it('ASCII and Unicode sets cover the same keys', () => {
  134. expect(Object.keys(ASCII_GLYPHS).sort()).toEqual(Object.keys(UNICODE_GLYPHS).sort());
  135. });
  136. it('ASCII glyphs are all 7-bit ASCII', () => {
  137. for (const [key, value] of Object.entries(ASCII_GLYPHS)) {
  138. const flat = Array.isArray(value) ? value.join('') : value;
  139. for (let i = 0; i < flat.length; i++) {
  140. const codepoint = flat.charCodeAt(i);
  141. expect(codepoint, `ASCII_GLYPHS.${key} contains non-ASCII char U+${codepoint.toString(16).toUpperCase().padStart(4, '0')}`).toBeLessThan(128);
  142. }
  143. }
  144. });
  145. it('ASCII spinner has the same frame count as the Unicode spinner', () => {
  146. expect(ASCII_GLYPHS.spinner.length).toBe(UNICODE_GLYPHS.spinner.length);
  147. });
  148. });