diff --git a/packages/ckeditor5-utils/src/dom/global.ts b/packages/ckeditor5-utils/src/dom/global.ts index 55147ec4ba8..20f0dd849b5 100644 --- a/packages/ckeditor5-utils/src/dom/global.ts +++ b/packages/ckeditor5-utils/src/dom/global.ts @@ -23,4 +23,21 @@ * * console.log( global.window.innerWidth ); */ -export default { window, document }; + +let global: { window: Window & typeof globalThis; document: Document }; + +// In some environments window and document API might not be available. +try { + global = { window, document }; +} catch ( e ) { + // It's not possible to mock a window object to simulate lack of a window object without writing extremely convoluted code. + /* istanbul ignore next */ + + // Let's cast it to not change module's API. + // We only handle this so loading editor in environments without window and document doesn't fail. + // For better DX we shouldn't introduce mixed types and require developers to check the type manually. + // This module should not be used on purpose in any environment outside browser. + global = { window: {} as any, document: {} as any }; +} + +export default global; diff --git a/packages/ckeditor5-utils/src/env.ts b/packages/ckeditor5-utils/src/env.ts index 0c4d0951f7e..afa49262ba7 100644 --- a/packages/ckeditor5-utils/src/env.ts +++ b/packages/ckeditor5-utils/src/env.ts @@ -9,7 +9,22 @@ * @module utils/env */ -const userAgent = navigator.userAgent.toLowerCase(); +/** + * Safely returns `userAgent` from browser's navigator API in a lower case. + * If navigator API is not available it will return an empty string. + * + * @returns {String} + */ +export function getUserAgent( ): string { + // In some environments navigator API might not be available. + try { + return navigator.userAgent.toLowerCase(); + } catch ( e ) { + return ''; + } +} + +const userAgent = getUserAgent(); /** * A namespace containing environment and browser information. diff --git a/packages/ckeditor5-utils/src/translation-service.ts b/packages/ckeditor5-utils/src/translation-service.ts index b8d93ef1d01..4e08971e316 100644 --- a/packages/ckeditor5-utils/src/translation-service.ts +++ b/packages/ckeditor5-utils/src/translation-service.ts @@ -3,7 +3,6 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ -/* globals window */ /* eslint-disable no-var */ /** @@ -11,6 +10,7 @@ */ import CKEditorError from './ckeditorerror'; +import global from './dom/global'; declare global { var CKEDITOR_TRANSLATIONS: { @@ -22,8 +22,8 @@ declare global { } /* istanbul ignore else */ -if ( !window.CKEDITOR_TRANSLATIONS ) { - window.CKEDITOR_TRANSLATIONS = {}; +if ( !global.window.CKEDITOR_TRANSLATIONS ) { + global.window.CKEDITOR_TRANSLATIONS = {}; } /** @@ -78,15 +78,15 @@ if ( !window.CKEDITOR_TRANSLATIONS ) { * the one below: * * function addTranslations( language, translations, getPluralForm ) { - * if ( !window.CKEDITOR_TRANSLATIONS ) { - * window.CKEDITOR_TRANSLATIONS = {}; + * if ( !global.window.CKEDITOR_TRANSLATIONS ) { + * global.window.CKEDITOR_TRANSLATIONS = {}; * } - * if ( !window.CKEDITOR_TRANSLATIONS[ language ] ) { - * window.CKEDITOR_TRANSLATIONS[ language ] = {}; + * if ( !global.window.CKEDITOR_TRANSLATIONS[ language ] ) { + * global.window.CKEDITOR_TRANSLATIONS[ language ] = {}; * } * - * const languageTranslations = window.CKEDITOR_TRANSLATIONS[ language ]; + * const languageTranslations = global.window.CKEDITOR_TRANSLATIONS[ language ]; * * languageTranslations.dictionary = languageTranslations.dictionary || {}; * languageTranslations.getPluralForm = getPluralForm || languageTranslations.getPluralForm; @@ -106,11 +106,11 @@ export function add( translations: { readonly [ messageId: string ]: string | readonly string[] }, getPluralForm?: ( n: number ) => number ): void { - if ( !window.CKEDITOR_TRANSLATIONS[ language ] ) { - window.CKEDITOR_TRANSLATIONS[ language ] = {} as any; + if ( !global.window.CKEDITOR_TRANSLATIONS[ language ] ) { + global.window.CKEDITOR_TRANSLATIONS[ language ] = {} as any; } - const languageTranslations = window.CKEDITOR_TRANSLATIONS[ language ]; + const languageTranslations = global.window.CKEDITOR_TRANSLATIONS[ language ]; languageTranslations.dictionary = languageTranslations.dictionary || {}; languageTranslations.getPluralForm = getPluralForm || languageTranslations.getPluralForm; @@ -166,7 +166,7 @@ export function _translate( language: string, message: Message, quantity: number if ( numberOfLanguages === 1 ) { // Override the language to the only supported one. // This can't be done in the `Locale` class, because the translations comes after the `Locale` class initialization. - language = Object.keys( window.CKEDITOR_TRANSLATIONS )[ 0 ]; + language = Object.keys( global.window.CKEDITOR_TRANSLATIONS )[ 0 ]; } const messageId = message.id || message.string; @@ -180,8 +180,8 @@ export function _translate( language: string, message: Message, quantity: number return message.string; } - const dictionary = window.CKEDITOR_TRANSLATIONS[ language ].dictionary; - const getPluralForm = window.CKEDITOR_TRANSLATIONS[ language ].getPluralForm || ( n => n === 1 ? 0 : 1 ); + const dictionary = global.window.CKEDITOR_TRANSLATIONS[ language ].dictionary; + const getPluralForm = global.window.CKEDITOR_TRANSLATIONS[ language ].getPluralForm || ( n => n === 1 ? 0 : 1 ); const translation = dictionary[ messageId ]; if ( typeof translation === 'string' ) { @@ -200,19 +200,19 @@ export function _translate( language: string, message: Message, quantity: number * @protected */ export function _clear(): void { - window.CKEDITOR_TRANSLATIONS = {}; + global.window.CKEDITOR_TRANSLATIONS = {}; } // Checks whether the dictionary exists and translation in that dictionary exists. function hasTranslation( language: string, messageId: string ): boolean { return ( - !!window.CKEDITOR_TRANSLATIONS[ language ] && - !!window.CKEDITOR_TRANSLATIONS[ language ].dictionary[ messageId ] + !!global.window.CKEDITOR_TRANSLATIONS[ language ] && + !!global.window.CKEDITOR_TRANSLATIONS[ language ].dictionary[ messageId ] ); } function getNumberOfLanguages(): number { - return Object.keys( window.CKEDITOR_TRANSLATIONS ).length; + return Object.keys( global.window.CKEDITOR_TRANSLATIONS ).length; } /** diff --git a/packages/ckeditor5-utils/tests/env.js b/packages/ckeditor5-utils/tests/env.js index eb6c83cd0e1..0d4c9271a0f 100644 --- a/packages/ckeditor5-utils/tests/env.js +++ b/packages/ckeditor5-utils/tests/env.js @@ -4,7 +4,7 @@ */ import env, { - isMac, isWindows, isGecko, isSafari, isiOS, isAndroid, isRegExpUnicodePropertySupported, isBlink + isMac, isWindows, isGecko, isSafari, isiOS, isAndroid, isRegExpUnicodePropertySupported, isBlink, getUserAgent } from '../src/env'; import global from '../src/dom/global'; @@ -286,4 +286,26 @@ describe( 'Env', () => { } } ); } ); + + describe( 'getUserAgent()', () => { + it( 'should return user agent in lower case', () => { + sinon.stub( global.window.navigator, 'userAgent' ).value( 'CKBrowser' ); + + expect( getUserAgent() ).to.equal( 'ckbrowser' ); + } ); + + it( 'should return empty string if navigator API is unavailable', () => { + sinon.stub( global.window, 'navigator' ).value( undefined ); + + expect( getUserAgent() ).to.equal( '' ); + } ); + + it( 'should not throw an error if navigator API is unavailable', () => { + sinon.stub( global.window, 'navigator' ).value( undefined ); + + expect( () => { + getUserAgent(); + } ).to.not.throw(); + } ); + } ); } );