Skip to content
Open
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
41 changes: 38 additions & 3 deletions src/app-check/app-check.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { PLATFORM_ID } from '@angular/core';
import { PLATFORM_ID, PendingTasks } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { FirebaseApp, getApp, initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { APP_CHECK_ON_SERVER, AppCheck, CustomProvider, initializeAppCheck, provideAppCheck } from '@angular/fire/app-check';
import { FirebaseApp, deleteApp, getApp, initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { APP_CHECK_ON_SERVER, AppCheck, CustomProvider, initializeAppCheck, onTokenChanged, provideAppCheck } from '@angular/fire/app-check';
import { Auth, connectAuthEmulator, getAuth, provideAuth } from '@angular/fire/auth';
import { COMMON_CONFIG } from '../test-config';
import { rando } from '../utils';
Expand Down Expand Up @@ -109,4 +109,39 @@ describe('AppCheck', () => {
expect(appCheck.app).toEqual(TestBed.inject(FirebaseApp));
});

describe('zone wrapping', () => {
let appCheck: AppCheck;
let addTask: jasmine.Spy;
let cleanups: (() => void)[];

beforeEach(() => {
TestBed.configureTestingModule({
providers: [
provideFirebaseApp(() => initializeApp(COMMON_CONFIG, appName)),
provideTestAppCheck(),
],
});
appCheck = TestBed.inject(AppCheck);
addTask = spyOn(TestBed.inject(PendingTasks), 'add').and.callThrough();
cleanups = [];
});

afterEach(() => {
// Listeners re-enter this test's injector, so none may outlive the TestBed.
cleanups.forEach(cleanup => cleanup());
});

it('onTokenChanged does not hold the application unstable', () => {
const unsubscribe = TestBed.runInInjectionContext(() => onTokenChanged(appCheck, () => undefined));
cleanups.push(unsubscribe);
expect(addTask).not.toHaveBeenCalled();
});

it('a blocking call in the same context does register a task', async () => {
await TestBed.runInInjectionContext(() => deleteApp(initializeApp(COMMON_CONFIG, rando())));
expect(addTask).toHaveBeenCalled();
});

});

});
2 changes: 1 addition & 1 deletion src/app-check/firebase.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 26 additions & 1 deletion src/app/app.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PendingTasks } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { FirebaseApp, initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { FirebaseApp, deleteApp, initializeApp, onLog, provideFirebaseApp } from '@angular/fire/app';
import { COMMON_CONFIG } from '../test-config';
import { rando } from '../utils';

Expand Down Expand Up @@ -30,4 +31,28 @@ describe('FirebaseApp', () => {

});

describe('zone wrapping', () => {
let addTask: jasmine.Spy;

beforeEach(() => {
addTask = spyOn(TestBed.inject(PendingTasks), 'add').and.callThrough();
});

afterEach(() => {
// The handler onLog installs re-enters this test's injector, so it must not outlive the TestBed.
TestBed.runInInjectionContext(() => onLog(null));
});

it('onLog does not hold the application unstable', () => {
TestBed.runInInjectionContext(() => onLog(() => undefined));
expect(addTask).not.toHaveBeenCalled();
});

it('a blocking call in the same context does register a task', async () => {
await TestBed.runInInjectionContext(() => deleteApp(initializeApp(COMMON_CONFIG, rando())));
expect(addTask).toHaveBeenCalled();
});

});

});
2 changes: 1 addition & 1 deletion src/app/firebase.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

108 changes: 105 additions & 3 deletions src/database/database.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
import { PendingTasks } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { FirebaseApp, getApp, initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { Database, connectDatabaseEmulator, getDatabase, provideDatabase } from '@angular/fire/database';
import { COMMON_CONFIG } from '../test-config';
import {
DataSnapshot,
Database,
ListenEvent,
Query,
Unsubscribe,
auditTrail,
connectDatabaseEmulator,
enableLogging,
fromRef,
getDatabase,
list,
off,
onChildAdded,
onChildChanged,
onChildMoved,
onChildRemoved,
onValue,
provideDatabase,
ref,
stateChanges,
} from '@angular/fire/database';
import { COMMON_CONFIG, databaseEmulatorPort } from '../test-config';
import { rando } from '../utils';

describe('Database', () => {
Expand All @@ -19,7 +41,7 @@ describe('Database', () => {
provideFirebaseApp(() => initializeApp(COMMON_CONFIG, appName)),
provideDatabase(() => {
providedDatabase = getDatabase(getApp(appName));
connectDatabaseEmulator(providedDatabase, 'localhost', 9002);
connectDatabaseEmulator(providedDatabase, 'localhost', databaseEmulatorPort);
return providedDatabase;
}),
],
Expand All @@ -34,6 +56,86 @@ describe('Database', () => {
expect(database.app).toEqual(app);
});

describe('zone wrapping', () => {
let addTask: jasmine.Spy;
let cleanups: (() => void)[];

beforeEach(() => {
addTask = spyOn(TestBed.inject(PendingTasks), 'add').and.callThrough();
cleanups = [];
});

afterEach(() => {
// Listeners re-enter this test's injector, so none may outlive the TestBed.
cleanups.forEach(cleanup => cleanup());
});

type ChildListener = (query: Query, callback: (snapshot: DataSnapshot) => void) => Unsubscribe;
const childListeners: [string, ChildListener][] = [
['onChildAdded', onChildAdded],
['onChildChanged', onChildChanged],
['onChildMoved', onChildMoved],
['onChildRemoved', onChildRemoved],
];
childListeners.forEach(([name, listen]) => {
it(`${name} does not hold the application unstable`, () => {
const unsubscribe = TestBed.runInInjectionContext(() => listen(ref(database, rando()), () => undefined));
cleanups.push(unsubscribe);
expect(addTask).not.toHaveBeenCalled();
});
});

it('off does not hold the application unstable', () => {
TestBed.runInInjectionContext(() => off(ref(database, rando()), 'value', () => undefined));
expect(addTask).not.toHaveBeenCalled();
});

it('enableLogging does not hold the application unstable', () => {
TestBed.runInInjectionContext(() => enableLogging(() => undefined));
cleanups.push(() => TestBed.runInInjectionContext(() => enableLogging(false)));
expect(addTask).not.toHaveBeenCalled();
});

[ListenEvent.added, ListenEvent.changed, ListenEvent.moved, ListenEvent.removed].forEach(event => {
it(`fromRef on ${event} does not hold the application unstable`, () => {
const subscription = TestBed.runInInjectionContext(() => fromRef(ref(database, rando()), event)).subscribe();
cleanups.push(() => subscription.unsubscribe());
expect(addTask).not.toHaveBeenCalled();
});
});

it('fromRef on the value event registers a pending task', () => {
const subscription = TestBed.runInInjectionContext(() => fromRef(ref(database, rando()), ListenEvent.value)).subscribe();
cleanups.push(() => subscription.unsubscribe());
expect(addTask).toHaveBeenCalledTimes(1);
});

it('stateChanges does not hold the application unstable', () => {
const subscription = TestBed.runInInjectionContext(() => stateChanges(ref(database, rando()))).subscribe();
cleanups.push(() => subscription.unsubscribe());
expect(addTask).not.toHaveBeenCalled();
});

it('auditTrail does not hold the application unstable', () => {
const subscription = TestBed.runInInjectionContext(() => auditTrail(ref(database, rando()))).subscribe();
cleanups.push(() => subscription.unsubscribe());
expect(addTask).not.toHaveBeenCalled();
});

it('onValue in the same context does register a task', () => {
const unsubscribe = TestBed.runInInjectionContext(() => onValue(ref(database, rando()), () => undefined));
cleanups.push(unsubscribe);
expect(addTask).toHaveBeenCalledTimes(1);
});

it('list in the same context does register a task', () => {
const subscription = TestBed.runInInjectionContext(() => list(ref(database, rando()))).subscribe();
cleanups.push(() => subscription.unsubscribe());
expect(addTask).toHaveBeenCalledTimes(1);
});

});

});

});
12 changes: 6 additions & 6 deletions src/database/firebase.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions src/database/overrides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { LogLevel, ɵzoneWrap } from '@angular/fire';
import { ListenEvent, fromRef as _fromRef } from 'rxfire/database';

const fromRefUntilFirst = ɵzoneWrap(_fromRef, true, LogLevel.WARN);
const fromRefWithoutWaiting = ɵzoneWrap(_fromRef, false, LogLevel.WARN);

// Only the value event fires on attach whatever the node holds. A child event may never fire.
export const fromRef: typeof _fromRef = (ref, event) =>
event === ListenEvent.value ? fromRefUntilFirst(ref, event) : fromRefWithoutWaiting(ref, event);
10 changes: 6 additions & 4 deletions src/database/rxfire.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/remote-config/firebase.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 9 additions & 1 deletion src/remote-config/remote-config.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PendingTasks } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { FirebaseApp, getApp, initializeApp, provideFirebaseApp } from '@angular/fire/app';
import { RemoteConfig, getRemoteConfig, provideRemoteConfig } from '@angular/fire/remote-config';
import { RemoteConfig, getRemoteConfig, isSupported, provideRemoteConfig } from '@angular/fire/remote-config';
import { COMMON_CONFIG } from '../test-config';
import { rando } from '../utils';

Expand Down Expand Up @@ -33,6 +34,13 @@ describe('RemoteConfig', () => {
expect(remoteConfig.app).toEqual(app);
});

it('isSupported does not hold the application unstable', async () => {
const addTask = spyOn(TestBed.inject(PendingTasks), 'add').and.callThrough();
const supported = TestBed.runInInjectionContext(isSupported);
expect(addTask).not.toHaveBeenCalled();
expect(typeof await supported).toBe('boolean');
});

});

});
30 changes: 27 additions & 3 deletions tools/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,14 @@ ${exportedZoneWrappedFns}
setUserId: { logLevel: LogLevel.VERBOSE },
setUserProperties: { logLevel: LogLevel.VERBOSE },
}),
reexport('app', 'firebase', 'firebase/app', tsKeys<typeof import('firebase/app')>()),
reexport('app', 'firebase', 'firebase/app', tsKeys<typeof import('firebase/app')>(), {
// The callback runs only when an SDK logs: `blockUntilFirst: false` otherwise `ApplicationRef.isStable` may never become `true`.
onLog: { blockUntilFirst: false, logLevel: LogLevel.WARN },
}),
reexport('app-check', 'firebase', 'firebase/app-check', tsKeys<typeof import('firebase/app-check')>(), {
getLimitedUseToken: { logLevel: LogLevel.VERBOSE },
// The callback runs only once a token is obtained, which a first visit may never request: `blockUntilFirst: false` otherwise `ApplicationRef.isStable` may never become `true`.
onTokenChanged: { blockUntilFirst: false, logLevel: LogLevel.WARN },
}),
reexport('auth', 'rxfire', 'rxfire/auth', tsKeys<typeof import('rxfire/auth')>()),
reexport('auth', 'firebase', 'firebase/auth', tsKeys<typeof import('firebase/auth')>(), {
Expand Down Expand Up @@ -218,15 +223,32 @@ ${exportedZoneWrappedFns}
verifyBeforeUpdateEmail: { logLevel: LogLevel.VERBOSE },
verifyPasswordResetCode: { logLevel: LogLevel.VERBOSE },
}),
reexport('database', 'rxfire', 'rxfire/database', tsKeys<typeof import('rxfire/database')>()),
reexport('database', 'rxfire', 'rxfire/database', tsKeys<typeof import('rxfire/database')>(), {
// These emit only on child events, which an empty node never produces: `blockUntilFirst: false` otherwise `ApplicationRef.isStable` may never become `true`.
auditTrail: { blockUntilFirst: false, logLevel: LogLevel.WARN },
stateChanges: { blockUntilFirst: false, logLevel: LogLevel.WARN },

// Hand-written in `src/database/overrides.ts`, which determines `blockUntilFirst` per event.
fromRef: { override: true },
}),
reexport('database', 'firebase', 'firebase/database', tsKeys<typeof import('firebase/database')>(), {
child: { logLevel: LogLevel.VERBOSE },
// The logger callback runs only when a live connection logs: `blockUntilFirst: false` otherwise `ApplicationRef.isStable` may never become `true`.
enableLogging: { blockUntilFirst: false, logLevel: LogLevel.WARN },

endAt: { logLevel: LogLevel.VERBOSE },
endBefore: { logLevel: LogLevel.VERBOSE },
equalTo: { logLevel: LogLevel.VERBOSE },
increment: { logLevel: LogLevel.VERBOSE },
limitToFirst: { logLevel: LogLevel.VERBOSE },
limitToLast: { logLevel: LogLevel.VERBOSE },
// The detach call never runs the callback it is given, and a child listener's callback first runs on a later change, or never on an empty node: `blockUntilFirst: false` otherwise `ApplicationRef.isStable` may never become `true`.
off: { blockUntilFirst: false, logLevel: LogLevel.WARN },
onChildAdded: { blockUntilFirst: false, logLevel: LogLevel.WARN },
onChildChanged: { blockUntilFirst: false, logLevel: LogLevel.WARN },
onChildMoved: { blockUntilFirst: false, logLevel: LogLevel.WARN },
onChildRemoved: { blockUntilFirst: false, logLevel: LogLevel.WARN },

orderByChild: { logLevel: LogLevel.VERBOSE },
orderByKey: { logLevel: LogLevel.VERBOSE },
orderByPriority: { logLevel: LogLevel.VERBOSE },
Expand Down Expand Up @@ -278,7 +300,9 @@ ${exportedZoneWrappedFns}
getBoolean: { exportName: 'getBooleanChanges' },
getAll: { exportName: 'getAllChanges' },
}),
reexport('remote-config', 'firebase', 'firebase/remote-config', tsKeys<typeof import('firebase/remote-config')>()),
reexport('remote-config', 'firebase', 'firebase/remote-config', tsKeys<typeof import('firebase/remote-config')>(), {
isSupported: { blockUntilFirst: false },
}),
reexport('storage', 'rxfire', 'rxfire/storage', tsKeys<typeof import('rxfire/storage')>(), {
getDownloadURL: null,
getMetadata: null,
Expand Down
Loading