Skip to content

Commit 04f4980

Browse files
authored
Merge pull request #355 from Eniola3321/Browser
feat: Browser Compatibility
2 parents f60e1da + 70c6b7b commit 04f4980

2 files changed

Lines changed: 144 additions & 3 deletions

File tree

frontend/compat.js

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
var ua = navigator.userAgent;
1212

1313
var browser = (function () {
14+
var isBrave = !!(navigator.brave && typeof navigator.brave.isBrave === 'function');
15+
if (isBrave) return { name: 'Brave', version: (ua.match(/Chrome\/([\d.]+)/) || [])[1] };
1416
if (/Edg\//.test(ua)) return { name: 'Edge', version: (ua.match(/Edg\/([\d.]+)/) || [])[1] };
1517
if (/OPR\//.test(ua)) return { name: 'Opera', version: (ua.match(/OPR\/([\d.]+)/) || [])[1] };
1618
if (/Chrome\//.test(ua)) return { name: 'Chrome', version: (ua.match(/Chrome\/([\d.]+)/) || [])[1] };
@@ -21,12 +23,14 @@
2123
})();
2224

2325
// Minimum supported versions
24-
var MIN_VERSIONS = { Chrome: 90, Firefox: 88, Safari: 14, Edge: 90, Opera: 76 };
26+
var MIN_VERSIONS = { Chrome: 90, Firefox: 88, Safari: 14, Edge: 90, Opera: 76, Brave: 90 };
2527

2628
var majorVersion = parseInt((browser.version || '0').split('.')[0], 10);
2729
var isSupported = browser.name !== 'IE' &&
2830
(!(browser.name in MIN_VERSIONS) || majorVersion >= MIN_VERSIONS[browser.name]);
2931
var isIE = browser.name === 'IE';
32+
var isSafari = browser.name === 'Safari';
33+
var isMobileSafari = /iPhone|iPad|iPod/.test(ua);
3034

3135
// ---------------------------------------------------------------------------
3236
// Feature detection
@@ -50,6 +54,9 @@
5054
clipboard: !!(navigator.clipboard && navigator.clipboard.writeText),
5155
notifications: typeof Notification !== 'undefined',
5256
serviceWorker: 'serviceWorker' in navigator,
57+
abortController: typeof AbortController !== 'undefined',
58+
promiseAllSettled:typeof Promise.allSettled !== 'undefined',
59+
stringReplaceAll: typeof String.prototype.replaceAll !== 'undefined',
5360
};
5461

5562
// ---------------------------------------------------------------------------
@@ -120,6 +127,40 @@
120127
window.cancelAnimationFrame = clearTimeout;
121128
}
122129

130+
// Promise.allSettled
131+
if (!features.promiseAllSettled) {
132+
Promise.allSettled = function (promises) {
133+
return Promise.all(promises.map(function (p) {
134+
return Promise.resolve(p).then(
135+
function (val) { return { status: 'fulfilled', value: val }; },
136+
function (err) { return { status: 'rejected', reason: err }; }
137+
);
138+
}));
139+
};
140+
}
141+
142+
// AbortController (basic polyfill)
143+
if (!features.abortController) {
144+
function AbortSignal() { this.aborted = false; this.onabort = null; }
145+
function AbortController() { this.signal = new AbortSignal(); }
146+
AbortController.prototype.abort = function () {
147+
this.signal.aborted = true;
148+
if (typeof this.signal.onabort === 'function') this.signal.onabort();
149+
};
150+
window.AbortController = AbortController;
151+
window.AbortSignal = AbortSignal;
152+
}
153+
154+
// String.prototype.replaceAll
155+
if (!features.stringReplaceAll) {
156+
String.prototype.replaceAll = function (search, replacement) {
157+
if (search instanceof RegExp && !search.global) {
158+
throw new TypeError('replaceAll must be called with a global RegExp');
159+
}
160+
return this.split(search).join(replacement);
161+
};
162+
}
163+
123164
// ---------------------------------------------------------------------------
124165
// CSS fallbacks for browsers without custom properties
125166
// ---------------------------------------------------------------------------
@@ -190,14 +231,43 @@
190231
);
191232
}
192233

234+
if (!features.localStorage) {
235+
showWarning(
236+
'LocalStorage is disabled or not supported. This might be due to Private Browsing mode. ' +
237+
'Settings and session data will not be saved.',
238+
'compat-storage-warning'
239+
);
240+
}
241+
193242
// ---------------------------------------------------------------------------
194243
// Browser-specific fixes
195244
// ---------------------------------------------------------------------------
196245

246+
// Safari: 100vh height fix for mobile
247+
if (isMobileSafari) {
248+
var setVH = function () {
249+
var vh = window.innerHeight * 0.01;
250+
document.documentElement.style.setProperty('--vh', vh + 'px');
251+
};
252+
window.addEventListener('resize', setVH);
253+
setVH();
254+
}
255+
256+
// Chrome / Edge / Brave: custom scrollbar styling if supported
257+
var isChromium = /Chrome/.test(browser.name) || /Edge/.test(browser.name) || /Brave/.test(browser.name);
258+
if (isChromium) {
259+
var scrollStyle = document.createElement('style');
260+
scrollStyle.textContent = [
261+
'::-webkit-scrollbar{width:8px;height:8px}',
262+
'::-webkit-scrollbar-track{background:#1a1a2e}',
263+
'::-webkit-scrollbar-thumb{background:#4b4b7c;border-radius:4px}',
264+
'::-webkit-scrollbar-thumb:hover{background:#6366f1}',
265+
].join('');
266+
document.head.appendChild(scrollStyle);
267+
}
268+
197269
// Safari: passive touch-event listeners to avoid scroll-blocking warnings
198270
// and fix 300ms tap delay on older iOS Safari (< 13).
199-
var isSafari = browser.name === 'Safari';
200-
var isMobileSafari = /iPhone|iPad|iPod/.test(ua);
201271
if (isSafari || isMobileSafari) {
202272
// Ensure touch-action is set so iOS Safari doesn't delay click events
203273
var safariStyle = document.createElement('style');
@@ -275,9 +345,14 @@
275345
fixes: {
276346
safariTouchAction: isSafari || isMobileSafari,
277347
iosTouchMove: isMobileSafari,
348+
safariVHFix: isMobileSafari,
349+
chromiumScrollbars: isChromium,
278350
firefoxFocusVisible: firefoxMajor < 85,
279351
safariSmoothScroll: safariMajor < 15,
280352
dialogPolyfill: typeof HTMLDialogElement === 'undefined',
353+
abortController: !features.abortController,
354+
promiseAllSettled: !features.promiseAllSettled,
355+
stringReplaceAll: !features.stringReplaceAll,
281356
},
282357
};
283358

frontend/tests/compat.test.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ describe('Browser detection', () => {
2222

2323
afterEach(() => {
2424
Object.defineProperty(navigator, 'userAgent', { value: originalUA, configurable: true });
25+
// Reset navigator.brave
26+
if (navigator.brave) {
27+
delete navigator.brave;
28+
}
2529
});
2630

2731
test('detects Chrome', () => {
@@ -34,6 +38,20 @@ describe('Browser detection', () => {
3438
expect(parseInt(window.StellarCompat.browser.version)).toBeGreaterThanOrEqual(90);
3539
});
3640

41+
test('detects Brave', () => {
42+
Object.defineProperty(navigator, 'userAgent', {
43+
value: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36',
44+
configurable: true,
45+
});
46+
// Stub navigator.brave
47+
Object.defineProperty(navigator, 'brave', {
48+
value: { isBrave: () => Promise.resolve(true) },
49+
configurable: true,
50+
});
51+
loadCompat();
52+
expect(window.StellarCompat.browser.name).toBe('Brave');
53+
});
54+
3755
test('detects Firefox', () => {
3856
Object.defineProperty(navigator, 'userAgent', {
3957
value: 'Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/109.0',
@@ -113,6 +131,22 @@ describe('Polyfills', () => {
113131
loadCompat();
114132
expect(typeof window.requestAnimationFrame).toBe('function');
115133
});
134+
135+
test('Promise.allSettled is defined after load', () => {
136+
loadCompat();
137+
expect(typeof Promise.allSettled).toBe('function');
138+
});
139+
140+
test('AbortController is defined after load', () => {
141+
loadCompat();
142+
expect(typeof window.AbortController).toBe('function');
143+
expect(typeof window.AbortSignal).toBe('function');
144+
});
145+
146+
test('String.prototype.replaceAll is defined after load', () => {
147+
loadCompat();
148+
expect(typeof String.prototype.replaceAll).toBe('function');
149+
});
116150
});
117151

118152
describe('Browser-specific fixes', () => {
@@ -136,6 +170,16 @@ describe('Browser-specific fixes', () => {
136170
expect(typeof window.StellarCompat.fixes.safariSmoothScroll).toBe('boolean');
137171
});
138172

173+
test('fixes.safariVHFix is a boolean', () => {
174+
loadCompat();
175+
expect(typeof window.StellarCompat.fixes.safariVHFix).toBe('boolean');
176+
});
177+
178+
test('fixes.chromiumScrollbars is a boolean', () => {
179+
loadCompat();
180+
expect(typeof window.StellarCompat.fixes.chromiumScrollbars).toBe('boolean');
181+
});
182+
139183
test('fixes.dialogPolyfill is a boolean', () => {
140184
loadCompat();
141185
expect(typeof window.StellarCompat.fixes.dialogPolyfill).toBe('boolean');
@@ -215,4 +259,26 @@ describe('Compatibility warnings', () => {
215259
btn.click();
216260
expect(document.getElementById('compat-ie-warning')).toBeNull();
217261
});
262+
263+
test('shows warning when localStorage is disabled', () => {
264+
// Mock localStorage to fail completely
265+
const originalLS = window.localStorage;
266+
Object.defineProperty(window, 'localStorage', {
267+
value: {
268+
setItem: () => { throw new Error('SecurityError'); },
269+
removeItem: () => {},
270+
getItem: () => null
271+
},
272+
configurable: true
273+
});
274+
275+
loadCompat();
276+
expect(document.getElementById('compat-storage-warning')).not.toBeNull();
277+
278+
// Restore
279+
Object.defineProperty(window, 'localStorage', {
280+
value: originalLS,
281+
configurable: true
282+
});
283+
});
218284
});

0 commit comments

Comments
 (0)