Files

215 lines
6.0 KiB
TypeScript

/**
* @license
* Copyright 2018 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { debugAssert, fail } from '../../src/util/assert';
import { DocumentLike, WindowLike } from '../../src/util/types';
/* eslint-disable no-restricted-globals */
/**
* `Window` fake that implements the event and storage API that is used by
* Firestore.
*/
export class FakeWindow implements WindowLike {
private readonly fakeStorageArea: Storage;
private readonly fakeIndexedDb: IDBFactory | null;
private storageListeners: EventListener[] = [];
constructor(
sharedFakeStorage: SharedFakeWebStorage,
fakeIndexedDb?: IDBFactory
) {
this.fakeStorageArea = sharedFakeStorage.getStorageArea(event => {
for (const listener of this.storageListeners) {
listener(event);
}
});
this.fakeIndexedDb =
fakeIndexedDb ||
(typeof window !== 'undefined' && window.indexedDB) ||
null;
}
get localStorage(): Storage {
return this.fakeStorageArea;
}
get indexedDB(): IDBFactory | null {
return this.fakeIndexedDb;
}
addEventListener(type: string, listener: EventListener): void {
switch (type) {
case 'storage':
this.storageListeners.push(listener);
break;
case 'pagehide':
case 'visibilitychange':
// The spec tests currently do not rely on `pagehide`/`visibilitychange`
// listeners.
break;
default:
fail(0xe53d, `MockWindow doesn't support events of type '${type}'`);
}
}
removeEventListener(type: string, listener: EventListener): void {
if (type === 'storage') {
const oldCount = this.storageListeners.length;
this.storageListeners = this.storageListeners.filter(
registeredListener => listener !== registeredListener
);
const newCount = this.storageListeners.length;
debugAssert(
newCount === oldCount - 1,
"Listener passed to 'removeEventListener' doesn't match any registered listener."
);
}
}
}
export function testWindow(
sharedWebStorage = new SharedFakeWebStorage()
): FakeWindow {
return new FakeWindow(sharedWebStorage);
}
/**
* `Document` fake that implements the `visibilitychange` API used by Firestore.
*/
export class FakeDocument implements DocumentLike {
private _visibilityState: DocumentVisibilityState = 'hidden';
private visibilityListener: EventListener | null = null;
get visibilityState(): DocumentVisibilityState {
return this._visibilityState;
}
addEventListener(type: string, listener: EventListener): void {
debugAssert(
type === 'visibilitychange',
"FakeDocument only supports events of type 'visibilitychange'"
);
this.visibilityListener = listener;
}
removeEventListener(type: string, listener: EventListener): void {
if (listener === this.visibilityListener) {
this.visibilityListener = null;
}
}
raiseVisibilityEvent(visibility: DocumentVisibilityState): void {
this._visibilityState = visibility;
if (this.visibilityListener) {
this.visibilityListener(new Event('visibilitychange'));
}
}
}
export function testDocument(): FakeDocument {
return new FakeDocument();
}
/**
* `WebStorage` mock that implements the WebStorage behavior for multiple
* clients. To get a client-specific storage area that implements the WebStorage
* API, invoke `getStorageArea(storageListener)`.
*/
export class SharedFakeWebStorage {
private readonly data = new Map<string, string>();
private readonly activeClients: Array<{
storageListener: EventListener;
storageArea: Storage;
}> = [];
getStorageArea(storageListener: EventListener): Storage {
const clientIndex = this.activeClients.length;
const self = this;
const storageArea: Storage = {
get length(): number {
return self.length;
},
getItem: (key: string) => this.getItem(key),
key: (index: number) => this.key(index),
clear: () => this.clear(),
removeItem: (key: string) => {
const oldValue = this.getItem(key);
this.removeItem(key);
this.raiseStorageEvent(clientIndex, key, oldValue, null);
},
setItem: (key: string, value: string) => {
const oldValue = this.getItem(key);
this.setItem(key, value);
this.raiseStorageEvent(clientIndex, key, oldValue, value);
}
};
this.activeClients[clientIndex] = { storageListener, storageArea };
return storageArea;
}
private clear(): void {
this.data.clear();
}
private getItem(key: string): string | null {
return this.data.has(key) ? this.data.get(key)! : null;
}
private key(index: number): string | null {
const key = Array.from(this.data.keys())[index];
return key !== undefined ? key : null;
}
private removeItem(key: string): void {
this.data.delete(key);
}
private setItem(key: string, data: string): void {
this.data.set(key, data);
}
private get length(): number {
return this.data.size;
}
private raiseStorageEvent(
sourceClientIndex: number,
key: string,
oldValue: string | null,
newValue: string | null
): void {
this.activeClients.forEach((client, index) => {
// WebStorage doesn't raise events for writes from the originating client.
if (sourceClientIndex === index) {
return;
}
client.storageListener({
key,
oldValue,
newValue,
storageArea: client.storageArea
} as any); /* eslint-disable-line @typescript-eslint/no-explicit-any*/ // Not mocking entire Event type.
});
}
}