fix(ui): isMacOS always returned false on Macs + plugin-logic tests (+49)

Coverage work found a 3rd real bug: isMacOS() compared os.name against the
legacy 'Mac OS' string, but ua-parser-js v2 reports 'macOS' — so it was dead,
and Mac users saw "Ctrl + k" instead of "⌘ + k" in the editor toolbar, search,
and settings shortcut hints. Now accepts both 'macOS' and 'Mac OS'.

Suites (via subagent, verified): via-servers (10 — power/popularity server
selection), bad-words (9), syntaxHighlight tokenize (14), plugins/utils
getEmoticonSearchStr (5), imageCompression formatFileSize/isCompressible (5),
user-agent (6, now asserting the fixed behavior).

Full suite now 501 tests, all passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 14:58:06 -04:00
parent 24662fa994
commit 30d0331174
7 changed files with 465 additions and 1 deletions
+38
View File
@@ -0,0 +1,38 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { formatFileSize, isCompressible } from './imageCompression';
test('formatFileSize formats bytes below 1KB as B', () => {
assert.equal(formatFileSize(0), '0 B');
assert.equal(formatFileSize(1), '1 B');
assert.equal(formatFileSize(512), '512 B');
assert.equal(formatFileSize(1023), '1023 B');
});
test('formatFileSize formats values in the KB range', () => {
assert.equal(formatFileSize(1024), '1.0 KB');
assert.equal(formatFileSize(1536), '1.5 KB');
// 1024 * 1024 - 1 is still under the MB boundary
assert.equal(formatFileSize(1024 * 1024 - 1), '1024.0 KB');
});
test('formatFileSize formats values in the MB range', () => {
assert.equal(formatFileSize(1024 * 1024), '1.0 MB');
assert.equal(formatFileSize(1024 * 1024 * 1.5), '1.5 MB');
assert.equal(formatFileSize(1024 * 1024 * 10), '10.0 MB');
});
test('isCompressible accepts raster image types', () => {
assert.equal(isCompressible({ type: 'image/png' } as Blob), true);
assert.equal(isCompressible({ type: 'image/jpeg' } as Blob), true);
assert.equal(isCompressible({ type: 'image/gif' } as Blob), true);
assert.equal(isCompressible({ type: 'image/webp' } as Blob), true);
});
test('isCompressible rejects svg, empty type, and non-images', () => {
assert.equal(isCompressible({ type: 'image/svg+xml' } as Blob), false);
assert.equal(isCompressible({ type: '' } as Blob), false);
assert.equal(isCompressible({ type: 'application/pdf' } as Blob), false);
assert.equal(isCompressible({ type: 'text/plain' } as Blob), false);
assert.equal(isCompressible({ type: 'video/mp4' } as Blob), false);
});
+117
View File
@@ -0,0 +1,117 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { tokenize, tokenStyle, SyntaxToken } from './syntaxHighlight';
const find = (tokens: SyntaxToken[], text: string) => tokens.find((t) => t.text === text);
test('tokenize falls back to a single plain token for unsupported languages', () => {
assert.deepEqual(tokenize('plain text here', 'unknownlang'), [
{ text: 'plain text here', type: 'plain' },
]);
// empty lang is also unsupported
assert.deepEqual(tokenize('hello', ''), [{ text: 'hello', type: 'plain' }]);
});
test('tokenize returns an empty array for empty input', () => {
assert.deepEqual(tokenize('', 'js'), []);
});
test('tokenize strips a leading "language-" prefix', () => {
const tokens = tokenize('const x', 'language-javascript');
assert.equal(find(tokens, 'const')?.type, 'kw');
});
test('tokenize classifies keywords, numbers, plain words and punctuation', () => {
const tokens = tokenize('const x = 5;', 'js');
assert.deepEqual(tokens, [
{ text: 'const', type: 'kw' },
{ text: ' ', type: 'plain' },
{ text: 'x', type: 'plain' },
{ text: ' = ', type: 'plain' },
{ text: '5', type: 'num' },
{ text: ';', type: 'plain' },
]);
});
test('tokenize detects function-call identifiers by a following paren', () => {
const tokens = tokenize('foo(1)', 'js');
assert.equal(find(tokens, 'foo')?.type, 'fn');
assert.equal(find(tokens, '1')?.type, 'num');
});
test('tokenize handles line comments up to the newline', () => {
const tokens = tokenize('// hi\ncode', 'js');
assert.deepEqual(tokens[0], { text: '// hi', type: 'cmt' });
// the newline and the rest are plain
assert.equal(find(tokens, 'code')?.type, 'plain');
});
test('tokenize handles block comments including the closing delimiter', () => {
const tokens = tokenize('/* block */x', 'js');
assert.deepEqual(tokens[0], { text: '/* block */', type: 'cmt' });
assert.equal(find(tokens, 'x')?.type, 'plain');
});
test('tokenize captures string literals including escaped quotes', () => {
assert.deepEqual(tokenize('"str"', 'js'), [{ text: '"str"', type: 'str' }]);
assert.deepEqual(tokenize("'a\\'b'", 'js'), [{ text: "'a\\'b'", type: 'str' }]);
});
test('tokenize treats # as a comment only in python', () => {
// python: # after a space starts a comment
const py = tokenize('a # not comment', 'python');
assert.equal(
py.some((t) => t.type === 'cmt' && t.text === '# not comment'),
true,
);
// js: # is not a comment marker
const js = tokenize('a # b', 'js');
assert.equal(
js.some((t) => t.type === 'cmt'),
false,
);
});
test('tokenize uses the python keyword set for python', () => {
const tokens = tokenize('def foo():', 'python');
assert.equal(find(tokens, 'def')?.type, 'kw');
assert.equal(find(tokens, 'foo')?.type, 'fn');
});
test('tokenize uses the rust keyword set for rust', () => {
const tokens = tokenize('fn main() {}', 'rust');
assert.equal(find(tokens, 'fn')?.type, 'kw');
assert.equal(find(tokens, 'main')?.type, 'fn');
});
test('tokenize keeps a plain word that is a keyword in another language plain', () => {
// "def" is a python keyword but not a js keyword
const tokens = tokenize('def x', 'js');
assert.equal(find(tokens, 'def')?.type, 'plain');
});
test('tokenize re-concatenates to the original source', () => {
const samples: Array<[string, string]> = [
['const x = foo(42); // done', 'js'],
['def add(a, b):\n return a + b # sum', 'python'],
['let mut v = vec![1, 2, 3];', 'rust'],
];
for (const [code, lang] of samples) {
assert.equal(
tokenize(code, lang)
.map((t) => t.text)
.join(''),
code,
`roundtrip for ${lang}`,
);
}
});
test('tokenStyle returns distinct styles per token kind', () => {
assert.deepEqual(tokenStyle('kw'), { color: 'var(--prism-keyword)' });
assert.deepEqual(tokenStyle('str'), { color: 'var(--prism-selector)' });
assert.deepEqual(tokenStyle('num'), { color: 'var(--prism-boolean)' });
assert.deepEqual(tokenStyle('cmt'), { color: 'var(--prism-comment)', fontStyle: 'italic' });
assert.deepEqual(tokenStyle('fn'), { color: 'var(--prism-atrule)' });
assert.deepEqual(tokenStyle('plain'), {});
});
+67
View File
@@ -0,0 +1,67 @@
import { test, afterEach } from 'node:test';
import assert from 'node:assert/strict';
import { isMacOS, mobileOrTablet } from './user-agent';
// `ua()` reads `window.navigator.userAgent` lazily on every call, so we can swap
// the global mock per test case and restore it afterwards.
const UA = {
mac: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36',
windows:
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36',
linux:
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36',
iphone:
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
ipad: 'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
android:
'Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Mobile Safari/537.36',
};
const g = globalThis as unknown as { window?: unknown };
const originalWindow = g.window;
const setUserAgent = (userAgent: string) => {
g.window = { navigator: { userAgent } };
};
afterEach(() => {
g.window = originalWindow;
});
test('isMacOS returns true for a Mac user agent (handles both macOS and legacy Mac OS)', () => {
setUserAgent(UA.mac);
assert.equal(isMacOS(), true);
});
test('isMacOS returns false for non-Mac user agents', () => {
setUserAgent(UA.windows);
assert.equal(isMacOS(), false);
setUserAgent(UA.linux);
assert.equal(isMacOS(), false);
setUserAgent(UA.android);
assert.equal(isMacOS(), false);
});
test('mobileOrTablet is true for an iPhone', () => {
setUserAgent(UA.iphone);
assert.equal(mobileOrTablet(), true);
});
test('mobileOrTablet is true for an iPad', () => {
setUserAgent(UA.ipad);
assert.equal(mobileOrTablet(), true);
});
test('mobileOrTablet is true for an Android device', () => {
setUserAgent(UA.android);
assert.equal(mobileOrTablet(), true);
});
test('mobileOrTablet is false for desktop user agents', () => {
setUserAgent(UA.mac);
assert.equal(mobileOrTablet(), false);
setUserAgent(UA.windows);
assert.equal(mobileOrTablet(), false);
setUserAgent(UA.linux);
assert.equal(mobileOrTablet(), false);
});
+6 -1
View File
@@ -2,7 +2,12 @@ import { UAParser } from 'ua-parser-js';
export const ua = () => UAParser(window.navigator.userAgent);
export const isMacOS = () => ua().os.name === 'Mac OS';
// ua-parser-js reports macOS as 'macOS' (v2+); older versions used 'Mac OS'.
// Accept both so the ⌘-vs-Ctrl shortcut hints render correctly on real Macs.
export const isMacOS = () => {
const name = ua().os.name;
return name === 'macOS' || name === 'Mac OS';
};
export const mobileOrTablet = (): boolean => {
const userAgent = ua();