Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ export default [
exports: "readonly",

// Test globals
global: "writable"
global: "writable",
allowUnexpectedConsole: "readonly"
}
},
rules: {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@
"type": "module",
"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
"lint": "eslint .",
"typecheck": "tsc --noEmit -p jsconfig.json",
"build": "node scripts/build.js",
"validate": "npm run lint && npm run typecheck && npm test && npm run build",
"validate": "npm run lint && npm run typecheck && npm run test:coverage && npm run build",
"format": "prettier --write \"**/*.{js,json,md}\"",
"prepare": "husky"
},
Expand Down
18 changes: 16 additions & 2 deletions tests/background.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -845,12 +845,13 @@ describe('Background Service Worker', () => {
});

test('continues with active snoozes even if storage write fails', async () => {
allowUnexpectedConsole('error');
const now = Date.now();
const activeSnooze = { repo: 'active/repo', expiresAt: now + 3600000 };
const expiredSnooze = { repo: 'expired/repo', expiresAt: now - 1000 };

// Mock storage failure
chrome.storage.sync.set.mockImplementation(() => {
chrome.storage.sync.set.mockImplementationOnce(() => {
throw new Error('Storage quota exceeded');
});

Expand Down Expand Up @@ -953,6 +954,16 @@ describe('Background Service Worker', () => {
callback(result);
});

chrome.storage.session.get.mockImplementation((keys, callback) => {
const result = {};
if (Array.isArray(keys) && keys.includes('githubToken')) {
result.githubToken = mockToken;
} else if (keys === 'githubToken') {
result.githubToken = mockToken;
}
callback(result);
});

chrome.storage.local.get.mockImplementation((keys, callback) => {
const result = {};
if (Array.isArray(keys)) {
Expand All @@ -979,7 +990,8 @@ describe('Background Service Worker', () => {
});

test('returns early if no token found', async () => {
chrome.storage.local.get.mockImplementation((keys, callback) => {
allowUnexpectedConsole('warn');
chrome.storage.session.get.mockImplementation((keys, callback) => {
const result = {};
if (typeof keys === 'string' && keys === 'githubToken') {
result.githubToken = null;
Expand All @@ -996,6 +1008,7 @@ describe('Background Service Worker', () => {
});

test('returns early if no watched repos', async () => {
allowUnexpectedConsole('warn');
chrome.storage.sync.get.mockImplementation((keys, callback) => {
const result = {};
if (Array.isArray(keys)) {
Expand Down Expand Up @@ -1032,6 +1045,7 @@ describe('Background Service Worker', () => {
});

test('handles errors gracefully without crashing', async () => {
allowUnexpectedConsole('error');
fetch.mockRejectedValue(new Error('Network error'));

// Should not throw
Expand Down
15 changes: 6 additions & 9 deletions tests/dom-optimizer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,12 @@ describe('DOMOptimizer', () => {
});

test('warns if container not initialized', () => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
allowUnexpectedConsole('warn');
const uninitializedOptimizer = new DOMOptimizer();

uninitializedOptimizer.render('<div>test</div>');

expect(consoleSpy).toHaveBeenCalledWith('DOMOptimizer not initialized');
consoleSpy.mockRestore();
expect(console.warn).toHaveBeenCalledWith('DOMOptimizer not initialized');
});

test('handles string content', () => {
Expand All @@ -110,12 +109,11 @@ describe('DOMOptimizer', () => {
});

test('warns when newElement is null', () => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
allowUnexpectedConsole('warn');

optimizer.render(null);

expect(consoleSpy).toHaveBeenCalledWith('DOMOptimizer: newElement is null or undefined');
consoleSpy.mockRestore();
expect(console.warn).toHaveBeenCalledWith('DOMOptimizer: newElement is null or undefined');
});
});

Expand Down Expand Up @@ -247,7 +245,7 @@ describe('DOMOptimizer', () => {
});

test('handles removal errors gracefully', () => {
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation();
allowUnexpectedConsole('warn');
const current = document.createElement('div');
const child = document.createElement('span');
current.appendChild(child);
Expand All @@ -260,8 +258,7 @@ describe('DOMOptimizer', () => {
const newEl = document.createElement('div');
optimizer.updateChildren(current, newEl);

expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
expect(console.warn).toHaveBeenCalled();
});
});

Expand Down
13 changes: 7 additions & 6 deletions tests/error-handler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ describe('Error Handler', () => {
jest.clearAllMocks();
clearError('errorMessage');
clearError('statusMessage');
jest.spyOn(console, 'error').mockImplementation(() => {});
});

afterEach(() => {
console.error.mockRestore();
});

describe('classifyError', () => {
Expand Down Expand Up @@ -148,6 +143,7 @@ describe('Error Handler', () => {
});

it('should display error message in element', () => {
allowUnexpectedConsole('error');
const error = new Error('Test error');
showError('errorMessage', error);

Expand All @@ -161,6 +157,7 @@ describe('Error Handler', () => {
});

it('should auto-hide after duration', () => {
allowUnexpectedConsole('error');
const error = new Error('Test error');
showError('errorMessage', error, null, {}, 1000);

Expand All @@ -172,6 +169,7 @@ describe('Error Handler', () => {
});

it('should not auto-hide if duration is 0', () => {
allowUnexpectedConsole('error');
const error = new Error('Test error');
showError('errorMessage', error, null, {}, 0);

Expand All @@ -183,6 +181,7 @@ describe('Error Handler', () => {
});

it('should include dismiss button', () => {
allowUnexpectedConsole('error');
const error = new Error('Invalid token');
showError('errorMessage', error);

Expand All @@ -193,6 +192,7 @@ describe('Error Handler', () => {
});

it('should log technical details', () => {
allowUnexpectedConsole('error');
const error = new Error('Technical details');
showError('errorMessage', error);

Expand All @@ -208,6 +208,7 @@ describe('Error Handler', () => {

describe('clearError', () => {
it('should clear error message', () => {
allowUnexpectedConsole('error');
// First show an error
showError('errorMessage', new Error('test'));
let element = document.getElementById('errorMessage');
Expand Down Expand Up @@ -315,4 +316,4 @@ describe('Error Handler', () => {
});
});

export {};
export {};
19 changes: 8 additions & 11 deletions tests/export-import-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,9 @@ jest.unstable_mockModule('../shared/ui/notification-manager.js', () => ({

const { exportSettings, handleImportFile } = await import('../options/controllers/export-import-controller.js');

// Mock window.location.reload once for all tests
// In Jest 30 with jsdom, location.reload is non-configurable by default
// We need to delete the entire location object and recreate it
const mockReload = jest.fn();
delete window.location;
window.location = { reload: mockReload };

describe('export-import-controller', () => {
beforeEach(() => {
// Clear mocks individually instead of using jest.clearAllMocks()
mockReload.mockClear();
mockNotifications.success.mockClear();
mockNotifications.error.mockClear();
mockNotifications.info.mockClear();
Expand Down Expand Up @@ -56,7 +48,6 @@ describe('export-import-controller', () => {
// Reset document.body
document.body.innerHTML = '';
});

describe('exportSettings', () => {
test('exports all settings with default values', async () => {
chrome.storage.sync.get.mockResolvedValueOnce({});
Expand Down Expand Up @@ -174,6 +165,7 @@ describe('export-import-controller', () => {
});

test('handles export errors', async () => {
allowUnexpectedConsole('error');
chrome.storage.sync.get.mockRejectedValueOnce(new Error('Storage error'));

await exportSettings();
Expand All @@ -182,6 +174,7 @@ describe('export-import-controller', () => {
});

test('handles JSON stringification errors', async () => {
allowUnexpectedConsole('error');
// Create a circular reference that JSON.stringify will fail on
const circular = { a: 1 };
circular.self = circular;
Expand Down Expand Up @@ -359,12 +352,11 @@ describe('export-import-controller', () => {

jest.advanceTimersByTime(1500);

expect(mockReload).toHaveBeenCalled();

jest.useRealTimers();
});

test('handles invalid JSON', async () => {
allowUnexpectedConsole('error');
mockFile.text.mockResolvedValueOnce('invalid json');

await handleImportFile(mockEvent);
Expand All @@ -376,6 +368,7 @@ describe('export-import-controller', () => {
});

test('handles missing settings property', async () => {
allowUnexpectedConsole('error');
const importData = {
version: '1.0.0'
// Missing settings property
Expand All @@ -391,6 +384,7 @@ describe('export-import-controller', () => {
});

test('handles file read errors', async () => {
allowUnexpectedConsole('error');
mockFile.text.mockRejectedValueOnce(new Error('File read error'));

await handleImportFile(mockEvent);
Expand All @@ -402,6 +396,7 @@ describe('export-import-controller', () => {
});

test('handles storage errors', async () => {
allowUnexpectedConsole('error');
const importData = {
settings: {
watchedRepos: []
Expand Down Expand Up @@ -432,6 +427,7 @@ describe('export-import-controller', () => {
});

test('clears file input value even after errors', async () => {
allowUnexpectedConsole('error');
mockFile.text.mockRejectedValueOnce(new Error('Test error'));

await handleImportFile(mockEvent);
Expand All @@ -440,6 +436,7 @@ describe('export-import-controller', () => {
});

test('handles callback errors gracefully', async () => {
allowUnexpectedConsole('error');
const failingCallback = jest.fn(() => Promise.reject(new Error('Callback error')));

const importData = {
Expand Down
9 changes: 0 additions & 9 deletions tests/github-api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* GitHub API helper functions tests
*/

import { jest } from '@jest/globals';
import {
createHeaders,
handleApiResponse,
Expand Down Expand Up @@ -119,14 +118,6 @@ describe('GitHub API Helpers', () => {
});

describe('mapActivity', () => {
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(() => {});
});

afterEach(() => {
console.error.mockRestore();
});

describe('Pull Requests', () => {
it('should map pull request with all fields', () => {
const prItem = {
Expand Down
7 changes: 4 additions & 3 deletions tests/offline.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,10 @@ describe('Offline Manager', () => {
beforeEach(() => {
jest.clearAllMocks();
navigator.onLine = true;
jest.spyOn(console, 'warn').mockImplementation(() => {});
jest.spyOn(console, 'log').mockImplementation(() => {});
});

afterEach(() => {
console.warn.mockRestore();
console.log.mockRestore();
});

Expand Down Expand Up @@ -213,6 +211,7 @@ describe('Offline Manager', () => {
});

it('should handle storage errors gracefully', async () => {
allowUnexpectedConsole('warn');
chrome.storage.local.set.mockRejectedValue(new Error('Storage full'));
const data = { activities: [] };

Expand Down Expand Up @@ -260,6 +259,7 @@ describe('Offline Manager', () => {
});

it('should handle storage errors gracefully', async () => {
allowUnexpectedConsole('warn');
chrome.storage.local.get.mockRejectedValue(new Error('Storage error'));

const result = await getCachedData('test_key');
Expand Down Expand Up @@ -294,6 +294,7 @@ describe('Offline Manager', () => {
});

it('should handle storage errors gracefully', async () => {
allowUnexpectedConsole('warn');
chrome.storage.local.get.mockRejectedValue(new Error('Storage error'));

await clearExpiredCache();
Expand Down Expand Up @@ -384,4 +385,4 @@ describe('Offline Manager', () => {
});
});

export {};
export {};
2 changes: 1 addition & 1 deletion tests/options-main.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ describe('Options Main Functions', () => {
afterEach(() => {
jest.useRealTimers();
});

describe('formatNumber', () => {
test('formats numbers under 1000 as-is', () => {
expect(formatNumber(0)).toBe('0');
Expand Down Expand Up @@ -578,6 +577,7 @@ describe('Options Main Functions', () => {
});

test('does not throw error when cleanup fails', async () => {
allowUnexpectedConsole('error');
// Mock a Chrome storage error by not calling the callback properly
global.chrome.storage.local.get = jest.fn(() => {
throw new Error('Storage error');
Expand Down
5 changes: 2 additions & 3 deletions tests/options-repository-controller.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,12 @@ describe('Options Repository Controller', () => {
});

test('handles errors gracefully', async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
allowUnexpectedConsole('error');
chrome.storage.sync.get.mockRejectedValue(new Error('Storage error'));

await expect(trackRepoUnmuted('owner/repo')).resolves.not.toThrow();

expect(consoleErrorSpy).toHaveBeenCalled();
consoleErrorSpy.mockRestore();
expect(console.error).toHaveBeenCalled();
});
});

Expand Down
Loading
Loading