diff --git a/packages-exp/auth-exp/src/platform_browser/persistence/indexed_db.test.ts b/packages-exp/auth-exp/src/platform_browser/persistence/indexed_db.test.ts index 93ecbc89086..24d13b88d49 100644 --- a/packages-exp/auth-exp/src/platform_browser/persistence/indexed_db.test.ts +++ b/packages-exp/auth-exp/src/platform_browser/persistence/indexed_db.test.ts @@ -34,6 +34,7 @@ import { Receiver } from '../messagechannel/receiver'; import { Sender } from '../messagechannel/sender'; import * as workerUtil from '../util/worker'; import { + _deleteObject, indexedDBLocalPersistence, _clearDatabase, _openDatabase, @@ -135,6 +136,18 @@ describe('platform_browser/persistence/indexed_db', () => { expect(callback).to.have.been.calledWith(newValue); }); + it('should trigger the listener when the key is removed', async () => { + await _putObject(db, key, newValue); + await waitUntilPoll(clock); + callback.resetHistory(); + + await _deleteObject(db, key); + + await waitUntilPoll(clock); + + expect(callback).to.have.been.calledOnceWith(null); + }); + it('should not trigger the listener when a different key changes', async () => { await _putObject(db, 'other-key', newValue); diff --git a/packages-exp/auth-exp/src/platform_browser/persistence/indexed_db.ts b/packages-exp/auth-exp/src/platform_browser/persistence/indexed_db.ts index 3a060b58e0d..2f4cf31b16d 100644 --- a/packages-exp/auth-exp/src/platform_browser/persistence/indexed_db.ts +++ b/packages-exp/auth-exp/src/platform_browser/persistence/indexed_db.ts @@ -152,7 +152,7 @@ async function getObject( return data === undefined ? null : data.value; } -function deleteObject(db: IDBDatabase, key: string): Promise { +export function _deleteObject(db: IDBDatabase, key: string): Promise { const request = getObjectStore(db, true).delete(key); return new DBPromise(request).toPromise(); } @@ -317,7 +317,7 @@ class IndexedDBLocalPersistence implements InternalPersistence { } const db = await _openDatabase(); await _putObject(db, STORAGE_AVAILABLE_KEY, '1'); - await deleteObject(db, STORAGE_AVAILABLE_KEY); + await _deleteObject(db, STORAGE_AVAILABLE_KEY); return true; } catch {} return false; @@ -350,7 +350,7 @@ class IndexedDBLocalPersistence implements InternalPersistence { async _remove(key: string): Promise { return this._withPendingWrite(async () => { - await this._withRetries((db: IDBDatabase) => deleteObject(db, key)); + await this._withRetries((db: IDBDatabase) => _deleteObject(db, key)); delete this.localCache[key]; return this.notifyServiceWorker(key); }); @@ -373,12 +373,21 @@ class IndexedDBLocalPersistence implements InternalPersistence { } const keys = []; + const keysInResult = new Set(); for (const { fbase_key: key, value } of result) { + keysInResult.add(key); if (JSON.stringify(this.localCache[key]) !== JSON.stringify(value)) { this.notifyListeners(key, value as PersistenceValue); keys.push(key); } } + for (const localKey of Object.keys(this.localCache)) { + if (!keysInResult.has(localKey)) { + // Deleted + this.notifyListeners(localKey, null); + keys.push(localKey); + } + } return keys; } diff --git a/packages-exp/auth-exp/src/platform_browser/persistence/local_storage.ts b/packages-exp/auth-exp/src/platform_browser/persistence/local_storage.ts index 4e47cfe6c81..cadbe0aa478 100644 --- a/packages-exp/auth-exp/src/platform_browser/persistence/local_storage.ts +++ b/packages-exp/auth-exp/src/platform_browser/persistence/local_storage.ts @@ -28,6 +28,7 @@ import { import { PersistenceInternal as InternalPersistence, PersistenceType, + PersistenceValue, StorageEventListener } from '../../core/persistence'; import { BrowserPersistenceClass } from './browser'; @@ -239,6 +240,24 @@ class BrowserLocalPersistence this.stopPolling(); } } + + // Update local cache on base operations: + + async _set(key: string, value: PersistenceValue): Promise { + await super._set(key, value); + this.localCache[key] = JSON.stringify(value); + } + + async _get(key: string): Promise { + const value = await super._get(key); + this.localCache[key] = JSON.stringify(value); + return value; + } + + async _remove(key: string): Promise { + await super._remove(key); + delete this.localCache[key]; + } } /** diff --git a/packages-exp/auth-exp/test/integration/webdriver/persistence.test.ts b/packages-exp/auth-exp/test/integration/webdriver/persistence.test.ts index a8944a7cf12..7f9b97129f3 100644 --- a/packages-exp/auth-exp/test/integration/webdriver/persistence.test.ts +++ b/packages-exp/auth-exp/test/integration/webdriver/persistence.test.ts @@ -20,11 +20,13 @@ import { UserCredential } from '@firebase/auth-exp'; import { expect } from 'chai'; import { createAnonAccount } from '../../helpers/integration/emulator_rest_helpers'; import { API_KEY } from '../../helpers/integration/settings'; +import { START_FUNCTION } from './util/auth_driver'; import { AnonFunction, CoreFunction, PersistenceFunction } from './util/functions'; +import { JsLoadCondition } from './util/js_load_condition'; import { browserDescribe } from './util/test_runner'; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -417,4 +419,73 @@ browserDescribe('WebDriver persistence test', driver => { } }); }); + + context('persistence sync across windows and tabs', () => { + it('sync current user across windows with indexedDB', async () => { + const cred: UserCredential = await driver.call( + AnonFunction.SIGN_IN_ANONYMOUSLY + ); + const uid = cred.user.uid; + await driver.webDriver.executeScript('window.open(".");'); + await driver.selectPopupWindow(); + await driver.webDriver.wait(new JsLoadCondition(START_FUNCTION)); + await driver.injectConfigAndInitAuth(); + await driver.waitForAuthInit(); + const userInPopup = await driver.getUserSnapshot(); + expect(userInPopup).not.to.be.null; + expect(userInPopup.uid).to.equal(uid); + + await driver.call(CoreFunction.SIGN_OUT); + expect(await driver.getUserSnapshot()).to.be.null; + await driver.selectMainWindow({ noWait: true }); + await driver.pause(500); + expect(await driver.getUserSnapshot()).to.be.null; + + const cred2: UserCredential = await driver.call( + AnonFunction.SIGN_IN_ANONYMOUSLY + ); + const uid2 = cred2.user.uid; + + await driver.selectPopupWindow(); + await driver.pause(500); + expect(await driver.getUserSnapshot()).to.contain({ uid: uid2 }); + }); + + it('sync current user across windows with localStorage', async () => { + await driver.webDriver.navigate().refresh(); + // Simulate browsers that do not support indexedDB. + await driver.webDriver.executeScript('delete window.indexedDB'); + await driver.injectConfigAndInitAuth(); + await driver.waitForAuthInit(); + const cred: UserCredential = await driver.call( + AnonFunction.SIGN_IN_ANONYMOUSLY + ); + const uid = cred.user.uid; + await driver.webDriver.executeScript('window.open(".");'); + await driver.selectPopupWindow(); + await driver.webDriver.wait(new JsLoadCondition(START_FUNCTION)); + // Simulate browsers that do not support indexedDB. + await driver.webDriver.executeScript('delete window.indexedDB'); + await driver.injectConfigAndInitAuth(); + await driver.waitForAuthInit(); + const userInPopup = await driver.getUserSnapshot(); + expect(userInPopup).not.to.be.null; + expect(userInPopup.uid).to.equal(uid); + + await driver.call(CoreFunction.SIGN_OUT); + expect(await driver.getUserSnapshot()).to.be.null; + await driver.selectMainWindow({ noWait: true }); + await driver.pause(500); + expect(await driver.getUserSnapshot()).to.be.null; + + const cred2: UserCredential = await driver.call( + AnonFunction.SIGN_IN_ANONYMOUSLY + ); + const uid2 = cred2.user.uid; + + await driver.selectPopupWindow(); + await driver.pause(500); + expect(await driver.getUserSnapshot()).to.contain({ uid: uid2 }); + }); + }); }); diff --git a/packages-exp/auth-exp/test/integration/webdriver/static/index.js b/packages-exp/auth-exp/test/integration/webdriver/static/index.js index 91ad74edb8a..ec69288acc2 100644 --- a/packages-exp/auth-exp/test/integration/webdriver/static/index.js +++ b/packages-exp/auth-exp/test/integration/webdriver/static/index.js @@ -49,20 +49,21 @@ window.startLegacySDK = async persistence => { // TODO: Find some way to make the tests work without Internet. appScript.src = 'https://www.gstatic.com/firebasejs/8.3.0/firebase-app.js'; appScript.onerror = reject; - document.head.appendChild(appScript); - - const authScript = document.createElement('script'); - authScript.src = - 'https://www.gstatic.com/firebasejs/8.3.0/firebase-auth.js'; - authScript.onerror = reject; - authScript.onload = function () { - firebase.initializeApp(firebaseConfig); - const legacyAuth = firebase.auth(); - legacyAuth.useEmulator(emulatorUrl); - legacyAuth.setPersistence(persistence.toLowerCase()); - window.legacyAuth = legacyAuth; - resolve(); + appScript.onload = () => { + const authScript = document.createElement('script'); + authScript.src = + 'https://www.gstatic.com/firebasejs/8.3.0/firebase-auth.js'; + authScript.onerror = reject; + authScript.onload = () => { + firebase.initializeApp(firebaseConfig); + const legacyAuth = firebase.auth(); + legacyAuth.useEmulator(emulatorUrl); + legacyAuth.setPersistence(persistence.toLowerCase()); + window.legacyAuth = legacyAuth; + resolve(); + }; + document.head.appendChild(authScript); }; - document.head.appendChild(authScript); + document.head.appendChild(appScript); }); }; diff --git a/packages-exp/auth-exp/test/integration/webdriver/static/persistence.js b/packages-exp/auth-exp/test/integration/webdriver/static/persistence.js index 9f185b4c387..d2bb4fb89b5 100644 --- a/packages-exp/auth-exp/test/integration/webdriver/static/persistence.js +++ b/packages-exp/auth-exp/test/integration/webdriver/static/persistence.js @@ -134,7 +134,7 @@ function dbPromise(dbRequest) { reject(dbRequest.error); }); dbRequest.addEventListener('blocked', () => { - reject(dbRequest.error || 'blocked'); + reject('blocked'); }); }); } diff --git a/packages-exp/auth-exp/test/integration/webdriver/util/auth_driver.ts b/packages-exp/auth-exp/test/integration/webdriver/util/auth_driver.ts index 8e890f5a92f..e1d38976217 100644 --- a/packages-exp/auth-exp/test/integration/webdriver/util/auth_driver.ts +++ b/packages-exp/auth-exp/test/integration/webdriver/util/auth_driver.ts @@ -28,7 +28,7 @@ import { CoreFunction } from './functions'; import { JsLoadCondition } from './js_load_condition'; import { authTestServer } from './test_server'; -const START_FUNCTION = 'startAuth'; +export const START_FUNCTION = 'startAuth'; const START_LEGACY_SDK_FUNCTION = 'startLegacySDK'; const PASSED_ARGS = '...Array.prototype.slice.call(arguments, 0, -1)'; @@ -200,14 +200,16 @@ export class AuthDriver { .window(handles.find(h => h !== currentWindowHandle)!); } - async selectMainWindow(): Promise { - const condition = new Condition( - 'Waiting for popup to close', - async driver => { - return (await driver.getAllWindowHandles()).length === 1; - } - ); - await this.webDriver.wait(condition); + async selectMainWindow(options: { noWait?: boolean } = {}): Promise { + if (!options.noWait) { + const condition = new Condition( + 'Waiting for popup to close', + async driver => { + return (await driver.getAllWindowHandles()).length === 1; + } + ); + await this.webDriver.wait(condition); + } const handles = await this.webDriver.getAllWindowHandles(); return this.webDriver.switchTo().window(handles[0]); } @@ -218,6 +220,16 @@ export class AuthDriver { return this.selectMainWindow(); } + async closeExtraWindows(): Promise { + const handles = await this.webDriver.getAllWindowHandles(); + await this.webDriver.switchTo().window(handles[handles.length - 1]); + while (handles.length > 1) { + await this.webDriver.close(); + handles.pop(); + await this.webDriver.switchTo().window(handles[handles.length - 1]); + } + } + isCompatLayer(): boolean { return process.env.COMPAT_LAYER === 'true'; } diff --git a/packages-exp/auth-exp/test/integration/webdriver/util/test_runner.ts b/packages-exp/auth-exp/test/integration/webdriver/util/test_runner.ts index ed688a3ed4d..e84b248acc4 100644 --- a/packages-exp/auth-exp/test/integration/webdriver/util/test_runner.ts +++ b/packages-exp/auth-exp/test/integration/webdriver/util/test_runner.ts @@ -69,6 +69,7 @@ setTimeout(() => { // It's assumed that the tests will start with a clean slate (i.e. // no storage). beforeEach(async () => { + await DRIVER.closeExtraWindows(); await DRIVER.reset(); await DRIVER.injectConfigAndInitAuth(); });