Skip to content
Draft
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
4 changes: 3 additions & 1 deletion src/modules/shared/alert/alert.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { Alert } from './alert.interface';
@Injectable('AlertService')
export class AlertService {
private _currentAlert: Alert | undefined;
onAlertChanged?: (alert: Alert | undefined) => void;

get currentAlert(): Alert | undefined {
return this._currentAlert;
}

set currentAlert(value: Alert) {
set currentAlert(value: Alert | undefined) {
this._currentAlert = value;
this.onAlertChanged?.(value);
}

clearCurrentAlert(): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,8 +299,10 @@ export class ApiXbrowsersyncService implements ApiService {
}

// Render markdown and add link classes to service message
let message = serviceInfoResponse.message ? (marked.parse(serviceInfoResponse.message) as string) : '';
if (message) {
// DOMParser/DOMPurify require DOM — skip rich formatting in service worker context
let message = serviceInfoResponse.message ?? '';
if (message && typeof DOMParser !== 'undefined') {
message = marked.parse(message) as string;
const messageDom = new DOMParser().parseFromString(message, 'text/html');
messageDom.querySelectorAll('a').forEach((hyperlink) => {
hyperlink.className = 'new-tab';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,7 @@ describe('BookmarkHelperService', () => {
const bookmarks: any[] = [
{
title: 'Folder',
children: [
{ title: 'Child', url: 'https://example.com', invalidKey: 'removed' }
],
children: [{ title: 'Child', url: 'https://example.com', invalidKey: 'removed' }],
invalidKey: 'removed'
}
];
Expand Down Expand Up @@ -188,9 +186,7 @@ describe('BookmarkHelperService', () => {
{
id: 1,
title: 'Folder',
children: [
{ id: 2, title: 'Child', url: 'https://child.com' }
]
children: [{ id: 2, title: 'Child', url: 'https://child.com' }]
}
];

Expand All @@ -201,9 +197,7 @@ describe('BookmarkHelperService', () => {
});

test('findBookmarkById: Returns undefined when id not found', () => {
const bookmarks: Bookmark[] = [
{ id: 1, title: 'First', url: 'https://first.com' }
];
const bookmarks: Bookmark[] = [{ id: 1, title: 'First', url: 'https://first.com' }];

const result = bookmarkHelperSvc.findBookmarkById(999, bookmarks);

Expand Down Expand Up @@ -276,19 +270,15 @@ describe('BookmarkHelperService', () => {
});

test('getContainer: Returns undefined when container not found', () => {
const bookmarks: Bookmark[] = [
{ title: BookmarkContainer.Menu, children: [] }
];
const bookmarks: Bookmark[] = [{ title: BookmarkContainer.Menu, children: [] }];

const result = bookmarkHelperSvc.getContainer(BookmarkContainer.Other, bookmarks);

expect(result).toBeUndefined();
});

test('getContainer: Creates container when not found and createIfNotPresent is true', () => {
const bookmarks: Bookmark[] = [
{ title: BookmarkContainer.Menu, children: [] }
];
const bookmarks: Bookmark[] = [{ title: BookmarkContainer.Menu, children: [] }];

const result = bookmarkHelperSvc.getContainer(BookmarkContainer.Other, bookmarks, true);

Expand All @@ -310,9 +300,7 @@ describe('BookmarkHelperService', () => {
});

test('getNewBookmarkId: Considers taken ids', () => {
const bookmarks: Bookmark[] = [
{ id: 1, title: 'First', url: 'https://first.com' }
];
const bookmarks: Bookmark[] = [{ id: 1, title: 'First', url: 'https://first.com' }];

const result = bookmarkHelperSvc.getNewBookmarkId(bookmarks, [0, 10]);

Expand Down Expand Up @@ -343,9 +331,7 @@ describe('BookmarkHelperService', () => {
});

test('newBookmark: Generates id when bookmarks provided', () => {
const existingBookmarks: Bookmark[] = [
{ id: 5, title: 'Existing', url: 'https://existing.com' }
];
const existingBookmarks: Bookmark[] = [{ id: 5, title: 'Existing', url: 'https://existing.com' }];

const result = bookmarkHelperSvc.newBookmark('New', 'https://new.com', undefined, undefined, existingBookmarks);

Expand Down Expand Up @@ -396,9 +382,7 @@ describe('BookmarkHelperService', () => {
{
id: 1,
title: BookmarkContainer.Menu,
children: [
{ id: 10, title: 'Child', url: 'https://child.com' }
]
children: [{ id: 10, title: 'Child', url: 'https://child.com' }]
},
{ id: 2, title: BookmarkContainer.Other, children: [] }
];
Expand All @@ -423,9 +407,7 @@ describe('BookmarkHelperService', () => {
});

test('searchBookmarksByKeywords: Returns empty array when no matches', () => {
const bookmarks: Bookmark[] = [
{ id: 1, title: 'JavaScript Tutorial', url: 'https://js.com' }
];
const bookmarks: Bookmark[] = [{ id: 1, title: 'JavaScript Tutorial', url: 'https://js.com' }];

const results = bookmarkHelperSvc.searchBookmarksByKeywords(bookmarks, 'en', ['ruby']);

Expand All @@ -436,9 +418,7 @@ describe('BookmarkHelperService', () => {
const bookmarks: Bookmark[] = [
{
title: 'Dev Folder',
children: [
{ id: 1, title: 'JavaScript Tutorial', url: 'https://js.com', tags: ['javascript'] }
]
children: [{ id: 1, title: 'JavaScript Tutorial', url: 'https://js.com', tags: ['javascript'] }]
}
];

Expand Down Expand Up @@ -475,9 +455,7 @@ describe('BookmarkHelperService', () => {
const bookmarks: Bookmark[] = [
{
title: 'Folder',
children: [
{ id: 1, title: 'Deep', url: 'https://example.com/deep' }
]
children: [{ id: 1, title: 'Deep', url: 'https://example.com/deep' }]
}
];

Expand Down Expand Up @@ -521,19 +499,15 @@ describe('BookmarkHelperService', () => {
test('modifyBookmarkById: Throws BookmarkNotFoundError when bookmark not found', () => {
const bookmarks: Bookmark[] = [{ id: 1, title: 'Test', url: 'https://test.com' }];

expect(() =>
bookmarkHelperSvc.modifyBookmarkById(999, { title: 'Updated' }, bookmarks)
).toThrow();
expect(() => bookmarkHelperSvc.modifyBookmarkById(999, { title: 'Updated' }, bookmarks)).toThrow();
});

test('modifyBookmarkById: Updates bookmark metadata', async () => {
const bookmarks: Bookmark[] = [
{
id: 1,
title: BookmarkContainer.Menu,
children: [
{ id: 2, title: 'Old Title', url: 'https://old.com' }
]
children: [{ id: 2, title: 'Old Title', url: 'https://old.com' }]
}
];

Expand Down
25 changes: 8 additions & 17 deletions src/modules/shared/crypto/crypto.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { TextEncoder as NodeTextEncoder, TextDecoder as NodeTextDecoder } from 'util';
import '../../../test/mock-angular';
import { TextDecoder as NodeTextDecoder, TextEncoder as NodeTextEncoder } from 'util';
import { $q } from '../../../test/mock-services';
import { ArgumentError, InvalidCredentialsError } from '../errors/errors';
import { CryptoService } from './crypto.service';

(global as any).TextEncoder = (global as any).TextEncoder || NodeTextEncoder;
(global as any).TextDecoder = (global as any).TextDecoder || NodeTextDecoder;

import '../../../test/mock-angular';

jest.mock('lzutf8', () => {
const impl = {
compress: (data: string) => new (global as any).TextEncoder().encode(data),
Expand All @@ -24,10 +27,6 @@ jest.mock('base64-js', () => {
};
});

import { $q } from '../../../test/mock-services';
import { ArgumentError, InvalidCredentialsError } from '../errors/errors';
import { CryptoService } from './crypto.service';

describe('CryptoService', () => {
let cryptoSvc: CryptoService;
const mockLogSvc = { logWarning: jest.fn(), logInfo: jest.fn() } as any;
Expand Down Expand Up @@ -170,11 +169,7 @@ describe('CryptoService', () => {
const result = await cryptoSvc.encryptData('test data');

expect(mockImportKey).toBeCalled();
expect(mockEncrypt).toBeCalledWith(
expect.objectContaining({ name: 'AES-GCM' }),
'key',
expect.anything()
);
expect(mockEncrypt).toBeCalledWith(expect.objectContaining({ name: 'AES-GCM' }), 'key', expect.anything());
expect(typeof result).toBe('string');

if (originalCryptoDescriptor) {
Expand Down Expand Up @@ -216,11 +211,7 @@ describe('CryptoService', () => {
const result = await cryptoSvc.decryptData(encodedData);

expect(mockImportKey).toBeCalled();
expect(mockDecrypt).toBeCalledWith(
expect.objectContaining({ name: 'AES-GCM' }),
'key',
expect.any(ArrayBuffer)
);
expect(mockDecrypt).toBeCalledWith(expect.objectContaining({ name: 'AES-GCM' }), 'key', expect.any(ArrayBuffer));
expect(result).toBe('decrypted data');

if (originalCryptoDescriptor) {
Expand Down
6 changes: 3 additions & 3 deletions src/modules/shared/errors/errors.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {
BaseError,
ArgumentError,
BaseError,
BookmarkNotFoundError,
FailedRestoreDataError,
HttpRequestFailedError,
InvalidCredentialsError,
NetworkConnectionError,
SyncFailedError,
FailedRestoreDataError
SyncFailedError
} from './errors';

describe('Errors', () => {
Expand Down
5 changes: 4 additions & 1 deletion src/modules/shared/sync/sync.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,10 @@ describe('SyncService', () => {
const mockApiSvc = { getBookmarksLastUpdated: jest.fn().mockRejectedValue(new SyncNotFoundError()) };
mockUtilitySvc.getApiService.mockResolvedValue(mockApiSvc);
mockBookmarkHelperSvc.getCachedBookmarks.mockResolvedValue([]);
mockStoreSvc.get.mockResolvedValue({ lastUpdated: '2023-01-01', syncInfo: { id: 'test', password: 'pass', version: '1.5.0' } });
mockStoreSvc.get.mockResolvedValue({
lastUpdated: '2023-01-01',
syncInfo: { id: 'test', password: 'pass', version: '1.5.0' }
});

const result = await syncSvc.checkSyncExists();

Expand Down
23 changes: 14 additions & 9 deletions src/modules/shared/utility/utility.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import '../../../test/mock-angular';
import { $q } from '../../../test/mock-services';
import { StoreKey } from '../store/store.enum';
import { UtilityService } from './utility.service';

jest.mock('xregexp', () => {
return {
default: (pattern: string, flags: string) => new RegExp(pattern, flags + 'u'),
default: (pattern: string, flags: string) => new RegExp(pattern, `${flags}u`),
__esModule: true
};
});
Expand All @@ -11,18 +14,20 @@ jest.mock('detect-browser', () => ({
detect: () => ({ name: 'chrome', version: '100.0' })
}));

import { $q } from '../../../test/mock-services';
import { StoreKey } from '../store/store.enum';
import { StoreService } from '../store/store.service';
import { LogService } from '../log/log.service';
import { NetworkService } from '../network/network.service';
import { UtilityService } from './utility.service';

describe('UtilityService', () => {
let utilitySvc: UtilityService;
const mock$exceptionHandler = jest.fn();
const mock$http = { get: jest.fn() } as any;
const mock$injector = { get: jest.fn(), annotate: jest.fn(), has: jest.fn(), instantiate: jest.fn(), invoke: jest.fn(), loadNewModules: jest.fn(), modules: {}, strictDi: false } as any;
const mock$injector = {
get: jest.fn(),
annotate: jest.fn(),
has: jest.fn(),
instantiate: jest.fn(),
invoke: jest.fn(),
loadNewModules: jest.fn(),
modules: {},
strictDi: false
} as any;
const mock$location = { path: jest.fn() } as any;
const mock$rootScope = { $broadcast: jest.fn() } as any;
const mockLogSvc = { logInfo: jest.fn(), logWarning: jest.fn() } as any;
Expand Down
5 changes: 3 additions & 2 deletions src/modules/shared/utility/utility.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ export class UtilityService {
}

getUniqueishId(): string {
return window.crypto.getRandomValues(new Uint32Array(1))[0].toString(36);
// crypto is available globally in both window and service worker contexts
return crypto.getRandomValues(new Uint32Array(1))[0].toString(36);
}

@boundMethod
Expand All @@ -211,7 +212,7 @@ export class UtilityService {
}

isBraveBrowser(): boolean {
return !angular.isUndefined(window.navigator.brave);
return typeof navigator !== 'undefined' && !angular.isUndefined((navigator as any).brave);
}

@boundMethod
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,40 @@
import angular from 'angular';
import { NgModule } from 'angular-ts-decorators';
import { WebExtBackgroundModule } from '../../webext-background/webext-background.module';
/**
* Chromium background entry point for MV3 service worker.
* Replaces the AngularJS bootstrap with a manual DI container.
*/

import browser from 'webextension-polyfill';
import { WebExtV160UpgradeProviderService } from '../../shared/webext-upgrade/webext-v1.6.0-upgrade-provider.service';
import { setupAngularShim } from '../../webext-background/angular-shims';
import { createBackgroundContainer } from '../../webext-background/background-container';
import { ChromiumBookmarkService } from '../shared/chromium-bookmark/chromium-bookmark.service';
import { ChromiumPlatformService } from '../shared/chromium-platform/chromium-platform.service';

@NgModule({
id: 'ChromiumBackgroundModule',
imports: [WebExtBackgroundModule],
providers: [ChromiumBookmarkService, ChromiumPlatformService]
})
class ChromiumBackgroundModule {}
// Set up angular shim before any service code runs
setupAngularShim();

Comment on lines +13 to +15
Copy link

Copilot AI Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setupAngularShim() is called in the module body, but all imports (including services that may reference angular or import the real AngularJS package) are evaluated before this runs. If the intent is to rely on the shim in the MV3 background context, move shim setup to a side-effect import that executes before other modules, or ensure background dependencies do not import angular at all.

This issue also appears on line 27 of the same file.

Copilot uses AI. Check for mistakes.
// Mark this as the background context
// eslint-disable-next-line no-undef, no-restricted-globals
(self as any).__xbs_isBackground = true;

// Create the DI container with Chromium-specific services
const { backgroundSvc } = createBackgroundContainer({
BookmarkServiceClass: ChromiumBookmarkService,
PlatformServiceClass: ChromiumPlatformService,
UpgradeProviderServiceClass: WebExtV160UpgradeProviderService
});

// Register event handlers synchronously (required for MV3 service workers)
let startupInitiated = false;

browser.runtime.onInstalled.addListener((details) => {
if (startupInitiated) return;
startupInitiated = true;
backgroundSvc.onInstall(details.reason);
});

angular.element(document).ready(() => {
angular.bootstrap(document, [(ChromiumBackgroundModule as NgModule).module.name]);
browser.runtime.onStartup.addListener(() => {
if (startupInitiated) return;
startupInitiated = true;
backgroundSvc.init();
});
Loading
Loading