Skip to content
This repository has been archived by the owner on May 29, 2023. It is now read-only.

Commit

Permalink
Merge e873246 into a71b642
Browse files Browse the repository at this point in the history
  • Loading branch information
waterplea committed Feb 16, 2021
2 parents a71b642 + e873246 commit 1eab761
Show file tree
Hide file tree
Showing 11 changed files with 275 additions and 35 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ will provide the same functionality on the server side as you have in browser. I
you will get type-safe mocks and you can at least be sure you will not have
`cannot read propery of null` or `undefined is not a function` errors in SSR.

> **IMPORTANT:** This library relies on **_Node.js_ v10** and above on your server side
## Mocks

Add following line to your `server.ts` to mock native classes used in other @ng-web-apis packages:
Expand All @@ -32,7 +34,7 @@ import '@ng-web-apis/universal/mocks';

## Tokens

- `WINDOW`no need to provide fallback, Angular Universal handles it
- `WINDOW`add `UNIVERSAL_WINDOW` to provide type-safe mock object, effectively mocking all `navigator` related entities
- `NAVIGATOR` — add `UNIVERSAL_NAVIGATOR` to provide type-safe mock object, effectively mocking all `navigator` related entities
- `NETWORK_INFORMATION` — no need to do anything
- `USER_AGENT` — see _special cases_ below
Expand Down
5 changes: 5 additions & 0 deletions projects/universal/src/classes/blob-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export class BlobMock implements Blob {
size = 0;
type = '';
slice = () => this;
}
27 changes: 11 additions & 16 deletions projects/universal/src/constants/universal-navigator.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import {FactoryProvider, Optional} from '@angular/core';
import {NAVIGATOR} from '@ng-web-apis/common';
import {SSR_USER_AGENT} from '../tokens/ssr-user-agent';
import {EVENT_TARGET} from '../utils/event-target';
import {
alwaysFalse,
alwaysRejected,
alwaysZero,
empty,
emptyArray,
emptyFunction,
emptyObject,
} from '../utils/functions';

const EVENT_TARGET: EventTarget = {
addEventListener: empty,
dispatchEvent: alwaysFalse,
removeEventListener: empty,
};

const PLUGIN = new (class extends Array<MimeType> implements Plugin {
readonly description = '';
readonly filename = '';
Expand Down Expand Up @@ -71,10 +66,10 @@ export function navigatorFactory(userAgent: string | null): NavigatorLike {

confirmSiteSpecificTrackingException: alwaysFalse,
confirmWebWideTrackingException: alwaysFalse,
removeSiteSpecificTrackingException: empty,
removeWebWideTrackingException: empty,
storeSiteSpecificTrackingException: empty,
storeWebWideTrackingException: empty,
removeSiteSpecificTrackingException: emptyFunction,
removeWebWideTrackingException: emptyFunction,
storeSiteSpecificTrackingException: emptyFunction,
storeWebWideTrackingException: emptyFunction,

msSaveBlob: alwaysFalse,
msSaveOrOpenBlob: alwaysFalse,
Expand Down Expand Up @@ -108,8 +103,8 @@ export function navigatorFactory(userAgent: string | null): NavigatorLike {
doNotTrack: null,
gamepadInputEmulation: 'keyboard',
geolocation: {
clearWatch: empty,
getCurrentPosition: empty,
clearWatch: emptyFunction,
getCurrentPosition: emptyFunction,
watchPosition: alwaysZero,
},
maxTouchPoints: 0,
Expand Down Expand Up @@ -139,14 +134,14 @@ export function navigatorFactory(userAgent: string | null): NavigatorLike {
getRegistration: alwaysRejected,
getRegistrations: alwaysRejected,
register: alwaysRejected,
startMessages: empty,
startMessages: emptyFunction,
},
webdriver: false,
getGamepads: emptyArray,
getUserMedia: empty,
getUserMedia: emptyFunction,
getVRDisplays: alwaysRejected,
javaEnabled: alwaysFalse,
msLaunchUri: empty,
msLaunchUri: emptyFunction,
requestMediaKeySystemAccess: alwaysRejected,
vibrate: alwaysFalse,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {UNIVERSAL_PERFORMANCE} from './universal-performance';
import {UNIVERSAL_SESSION_STORAGE} from './universal-session-storage';
import {UNIVERSAL_SPEECH_SYNTHESIS} from './universal-speech-synthesis';
import {UNIVERSAL_USER_AGENT} from './universal-user-agent';
import {UNIVERSAL_WINDOW} from './universal-window';

export const UNIVERSAL_PROVIDERS: Provider[] = [
UNIVERSAL_ANIMATION_FRAME,
Expand All @@ -17,6 +18,7 @@ export const UNIVERSAL_PROVIDERS: Provider[] = [
UNIVERSAL_PERFORMANCE,
UNIVERSAL_SPEECH_SYNTHESIS,
UNIVERSAL_USER_AGENT,
UNIVERSAL_WINDOW,
];

/** @deprecated use {@link UNIVERSAL_PROVIDERS} */
Expand Down
16 changes: 8 additions & 8 deletions projects/universal/src/constants/universal-speech-synthesis.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import {ValueProvider} from '@angular/core';
import {SPEECH_SYNTHESIS} from '@ng-web-apis/common';
import {alwaysFalse, empty, emptyArray} from '../utils/functions';
import {alwaysFalse, emptyArray, emptyFunction} from '../utils/functions';

const MOCK: SpeechSynthesis = {
paused: false,
pending: false,
speaking: false,
onvoiceschanged: empty,
addEventListener: empty,
removeEventListener: empty,
onvoiceschanged: emptyFunction,
addEventListener: emptyFunction,
removeEventListener: emptyFunction,
dispatchEvent: alwaysFalse,
cancel: empty,
pause: empty,
resume: empty,
speak: empty,
cancel: emptyFunction,
pause: emptyFunction,
resume: emptyFunction,
speak: emptyFunction,
getVoices: emptyArray,
};

Expand Down
218 changes: 218 additions & 0 deletions projects/universal/src/constants/universal-window.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import {DOCUMENT} from '@angular/common';
import {FactoryProvider} from '@angular/core';
import {
LOCAL_STORAGE,
LOCATION,
NAVIGATOR,
PERFORMANCE,
SESSION_STORAGE,
SPEECH_SYNTHESIS,
WINDOW,
} from '@ng-web-apis/common';
import {BlobMock} from '../classes/blob-mock';
import {EVENT_TARGET} from '../utils/event-target';
import {
alwaysFalse,
alwaysNull,
alwaysRejected,
alwaysZero,
emptyFunction,
identity,
} from '../utils/functions';

const COMPUTED_STYLES: Partial<CSSStyleDeclaration> = {
getPropertyPriority: () => '',
getPropertyValue: () => '',
item: () => '',
removeProperty: () => '',
setProperty: emptyFunction,
};
const COMPUTED_STYLES_HANDLER: ProxyHandler<CSSStyleDeclaration> = {
get: (obj, key: any) => (key in obj ? obj[key] : null),
};
const COMPUTED_STYLES_PROXY = new Proxy<CSSStyleDeclaration>(
COMPUTED_STYLES as any,
COMPUTED_STYLES_HANDLER,
);
const CSS_RULES = new (class extends Array<CSSRule> implements CSSRuleList {
item = () => null;
})();
const BAR_PROP: BarProp = {
visible: false,
};
const DB_REQUEST: IDBOpenDBRequest = {
...EVENT_TARGET,
onblocked: null,
onerror: null,
onsuccess: null,
onupgradeneeded: null,
error: null,
readyState: 'pending',
result: null as any, // Cannot be accessed for 'pending' state anyway
source: null as any, // null for open requests
transaction: null,
};
const SELF = ['frames', 'parent', 'self', 'top', 'window'];
const WINDOW_HANDLER: ProxyHandler<Window> = {
get: (windowRef, key: keyof Window) => {
if (SELF.includes(key)) {
return windowRef;
}

return key.startsWith('on') ? null : windowRef[key];
},
};

export function windowFactory(
document: Document,
navigator: Navigator,
localStorage: Storage,
location: Location,
performance: Performance,
sessionStorage: Storage,
speechSynthesis: SpeechSynthesis,
): Window {
const windowMock: Window = {
...EVENT_TARGET,
document,
localStorage,
location,
navigator,
performance,
sessionStorage,
speechSynthesis,
URL,
URLSearchParams,
setTimeout,
setInterval,
clearTimeout,
clearInterval,
console,
Blob: BlobMock,
alert: emptyFunction,
clientInformation: navigator,
// TODO: Candidate for token
matchMedia: () => ({
...EVENT_TARGET,
matches: false,
media: '',
onchange: null,
addListener: emptyFunction,
removeListener: emptyFunction,
}),
// TODO: Candidate for token
caches: {
delete: () => Promise.resolve(false),
has: () => Promise.resolve(false),
keys: () => Promise.resolve([]),
match: alwaysRejected,
open: alwaysRejected,
},
// TODO: Candidate for token
indexedDB: {
cmp: alwaysZero,
open: () => DB_REQUEST,
deleteDatabase: () => DB_REQUEST,
},
customElements: {
define: emptyFunction,
get: emptyFunction,
upgrade: emptyFunction,
whenDefined: alwaysRejected,
},
styleMedia: {
type: '',
matchMedium: alwaysFalse,
},
crypto: {
subtle: undefined as any, // Insecure context
getRandomValues: identity,
},
history: {
length: 0,
scrollRestoration: 'auto',
state: {},
back: emptyFunction,
forward: emptyFunction,
go: emptyFunction,
pushState: emptyFunction,
replaceState: emptyFunction,
},
closed: false,
defaultStatus: '',
devicePixelRatio: 1,
doNotTrack: '',
frameElement: null as any, // TODO: bug in TypeScript
innerHeight: 0,
innerWidth: 0,
isSecureContext: false,
length: 0,
name: '',
offscreenBuffering: false,
opener: {},
origin: '',
orientation: '',
outerHeight: 0,
outerWidth: 0,
pageXOffset: 0,
pageYOffset: 0,
screenLeft: 0,
screenTop: 0,
screenX: 0,
screenY: 0,
scrollX: 0,
scrollY: 0,
status: '',
blur: emptyFunction,
cancelAnimationFrame: emptyFunction,
captureEvents: emptyFunction,
close: emptyFunction,
confirm: alwaysFalse,
departFocus: emptyFunction,
focus: emptyFunction,
moveBy: emptyFunction,
moveTo: emptyFunction,
open: alwaysNull,
postMessage: emptyFunction,
print: emptyFunction,
prompt: alwaysNull,
releaseEvents: emptyFunction,
requestAnimationFrame: alwaysZero,
resizeBy: emptyFunction,
resizeTo: emptyFunction,
scroll: emptyFunction,
scrollBy: emptyFunction,
scrollTo: emptyFunction,
stop: emptyFunction,
atob: identity,
btoa: identity,
fetch: alwaysRejected,
createImageBitmap: alwaysRejected,
queueMicrotask: emptyFunction,
locationbar: BAR_PROP,
menubar: BAR_PROP,
personalbar: BAR_PROP,
scrollbars: BAR_PROP,
statusbar: BAR_PROP,
toolbar: BAR_PROP,
getComputedStyle: () => COMPUTED_STYLES_PROXY,
getMatchedCSSRules: () => CSS_RULES,
getSelection: () => null as any, // TODO: old TypeScript issue
} as any;

return new Proxy(windowMock, WINDOW_HANDLER);
}

export const UNIVERSAL_WINDOW: FactoryProvider = {
provide: WINDOW,
deps: [
DOCUMENT,
LOCAL_STORAGE,
LOCATION,
NAVIGATOR,
PERFORMANCE,
SESSION_STORAGE,
SPEECH_SYNTHESIS,
],
useFactory: windowFactory,
};
4 changes: 3 additions & 1 deletion projects/universal/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ export * from './constants/universal-navigator';
export * from './constants/universal-performance';
export * from './constants/universal-session-storage';
export * from './constants/universal-speech-synthesis';
export * from './constants/universal-tokens';
export * from './constants/universal-user-agent';
export * from './constants/universal-window';

export * from './constants/universal-providers';

// Utils
export * from './utils/provide-location';
Expand Down
7 changes: 7 additions & 0 deletions projects/universal/src/utils/event-target.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {alwaysFalse, emptyFunction} from './functions';

export const EVENT_TARGET: EventTarget = {
addEventListener: emptyFunction,
dispatchEvent: alwaysFalse,
removeEventListener: emptyFunction,
};
8 changes: 6 additions & 2 deletions projects/universal/src/utils/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export function identity<T>(v: T): T {
return v;
}

export function empty() {}
export function emptyFunction() {}

export function emptyArray(): any[] {
return [];
Expand All @@ -16,10 +16,14 @@ export function alwaysFalse(): boolean {
return false;
}

export function alwaysNull(): null {
return null;
}

export function alwaysZero(): number {
return 0;
}

export function alwaysRejected(): Promise<any> {
return Promise.reject().catch(empty);
return Promise.reject().catch(emptyFunction);
}
Loading

0 comments on commit 1eab761

Please sign in to comment.