--- a/services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm
+++ b/services/sync/tps/extensions/tps/resource/auth/fxaccounts.jsm
@@ -10,34 +10,33 @@ this.EXPORTED_SYMBOLS = [
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
Cu.import("resource://gre/modules/FxAccounts.jsm");
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
Cu.import("resource://gre/modules/FxAccountsConfig.jsm");
-Cu.import("resource://services-common/async.js");
Cu.import("resource://services-sync/main.js");
Cu.import("resource://tps/logger.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services", "resource://gre/modules/Services.jsm");
Cu.importGlobalProperties(["fetch"]);
/**
* Helper object for Firefox Accounts authentication
*/
var Authentication = {
/**
* Check if an user has been logged in
*/
- get isLoggedIn() {
- return !!this.getSignedInUser();
+ async isLoggedIn() {
+ return !!(await this.getSignedInUser());
},
_getRestmailUsername(user) {
const restmailSuffix = "@restmail.net";
if (user.toLowerCase().endsWith(restmailSuffix)) {
return user.slice(0, -restmailSuffix.length);
}
return null;
@@ -136,95 +135,76 @@ var Authentication = {
return true;
},
/**
* Wrapper to retrieve the currently signed in user
*
* @returns Information about the currently signed in user
*/
- getSignedInUser: function getSignedInUser() {
- let cb = Async.makeSpinningCallback();
-
- fxAccounts.getSignedInUser().then(user => {
- cb(null, user);
- }, error => {
- cb(error);
- })
-
+ async getSignedInUser() {
try {
- return cb.wait();
+ return (await fxAccounts.getSignedInUser());
} catch (error) {
Logger.logError("getSignedInUser() failed with: " + JSON.stringify(error));
throw error;
}
},
/**
* Wrapper to synchronize the login of a user
*
* @param account
* Account information of the user to login
* @param account.username
* The username for the account (utf8)
* @param account.password
* The user's password
*/
- signIn: function signIn(account) {
- let cb = Async.makeSpinningCallback();
-
+ async signIn(account) {
Logger.AssertTrue(account.username, "Username has been found");
Logger.AssertTrue(account.password, "Password has been found");
Logger.logInfo("Login user: " + account.username);
- // Required here since we don't go through the real login page
- Async.promiseSpinningly(FxAccountsConfig.ensureConfigured());
+ try {
+ // Required here since we don't go through the real login page
+ await FxAccountsConfig.ensureConfigured();
- let client = new FxAccountsClient();
- client.signIn(account.username, account.password, true).then(credentials => {
- return fxAccounts.setSignedInUser(credentials);
- }).then(() => {
- return this._completeVerification(account.username)
- }).then(() => {
- cb(null, true);
- }, error => {
- cb(error, false);
- });
-
- try {
- cb.wait();
+ let client = new FxAccountsClient();
+ let credentials = await client.signIn(account.username, account.password, true)
+ await fxAccounts.setSignedInUser(credentials);
+ await this._completeVerification(account.username)
if (Weave.Status.login !== Weave.LOGIN_SUCCEEDED) {
Logger.logInfo("Logging into Weave.");
- Async.promiseSpinningly(Weave.Service.login());
+ await Weave.Service.login();
Logger.AssertEqual(Weave.Status.login, Weave.LOGIN_SUCCEEDED,
"Weave logged in");
}
-
return true;
} catch (error) {
throw new Error("signIn() failed with: " + error.message);
}
},
/**
* Sign out of Firefox Accounts. It also clears out the device ID, if we find one.
*/
- signOut() {
- if (Authentication.isLoggedIn) {
- let user = Authentication.getSignedInUser();
+ async signOut() {
+ if (await Authentication.isLoggedIn()) {
+ let user = await Authentication.getSignedInUser();
if (!user) {
throw new Error("Failed to get signed in user!");
}
let fxc = new FxAccountsClient();
let { sessionToken, deviceId } = user;
if (deviceId) {
Logger.logInfo("Destroying device " + deviceId);
- Async.promiseSpinningly(fxAccounts.deleteDeviceRegistration(sessionToken, deviceId));
- Async.promiseSpinningly(fxAccounts.signOut(true));
+ await fxAccounts.deleteDeviceRegistration(sessionToken, deviceId);
+ await fxAccounts.signOut(true);
} else {
Logger.logError("No device found.");
- Async.promiseSpinningly(fxc.signOut(sessionToken, { service: "sync" }));
+ await fxc.signOut(sessionToken, { service: "sync" });
}
}
}
};
--- a/services/sync/tps/extensions/tps/resource/tps.jsm
+++ b/services/sync/tps/extensions/tps/resource/tps.jsm
@@ -17,16 +17,17 @@ var module = this;
Cu.import("resource://gre/modules/Log.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
Cu.import("resource://gre/modules/PromiseUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://services-common/async.js");
Cu.import("resource://services-sync/constants.js");
Cu.import("resource://services-sync/main.js");
Cu.import("resource://services-sync/util.js");
Cu.import("resource://services-sync/telemetry.js");
Cu.import("resource://services-sync/bookmark_validator.js");
Cu.import("resource://services-sync/engines/passwords.js");
Cu.import("resource://services-sync/engines/forms.js");
@@ -195,17 +196,21 @@ var TPS = {
this._syncActive = false;
this.delayAutoSync();
// If this is the first sync error, retry...
if (this._syncErrors === 0) {
Logger.logInfo("Sync error; retrying...");
this._syncErrors++;
- Utils.nextTick(this.RunNextTestAction, this);
+ Utils.nextTick(() => {
+ this.RunNextTestAction().catch(err => {
+ this.DumpError("RunNextTestActionFailed", err);
+ });
+ });
} else {
this._triggeredSync = false;
this.DumpError("Sync error; aborting test");
return;
}
break;
@@ -272,17 +277,19 @@ var TPS = {
// the FinishAsyncOperation without a StartAsyncOperation is fine, and works
// as if a StartAsyncOperation had been called.
if (this._operations_pending) {
this._operations_pending--;
}
if (!this.operations_pending) {
this._currentAction++;
Utils.nextTick(function() {
- this.RunNextTestAction();
+ this.RunNextTestAction().catch(err => {
+ this.DumpError("RunNextTestActionFailed", err);
+ });
}, this);
}
},
quit: function TPS__quit() {
this._requestedQuit = true;
this.goQuitApplication();
},
@@ -362,46 +369,46 @@ var TPS = {
break;
default:
Logger.AssertTrue(false, "invalid action: " + action);
}
}
Logger.logPass("executing action " + action.toUpperCase() + " on pref");
},
- HandleForms(data, action) {
+ async HandleForms(data, action) {
this.shouldValidateForms = true;
for (let datum of data) {
Logger.logInfo("executing action " + action.toUpperCase() +
" on form entry " + JSON.stringify(datum));
let formdata = new FormData(datum, this._usSinceEpoch);
switch (action) {
case ACTION_ADD:
- Async.promiseSpinningly(formdata.Create());
+ await formdata.Create();
break;
case ACTION_DELETE:
- Async.promiseSpinningly(formdata.Remove());
+ await formdata.Remove();
break;
case ACTION_VERIFY:
- Logger.AssertTrue(Async.promiseSpinningly(formdata.Find()),
+ Logger.AssertTrue(await formdata.Find(),
"form data not found");
break;
case ACTION_VERIFY_NOT:
- Logger.AssertTrue(!Async.promiseSpinningly(formdata.Find()),
+ Logger.AssertTrue(!await formdata.Find(),
"form data found, but it shouldn't be present");
break;
default:
Logger.AssertTrue(false, "invalid action: " + action);
}
}
Logger.logPass("executing action " + action.toUpperCase() +
" on formdata");
},
- HandleHistory(entries, action) {
+ async HandleHistory(entries, action) {
try {
for (let entry of entries) {
Logger.logInfo("executing action " + action.toUpperCase() +
" on history entry " + JSON.stringify(entry));
switch (action) {
case ACTION_ADD:
HistoryEntry.Add(entry, this._usSinceEpoch);
break;
@@ -579,41 +586,41 @@ var TPS = {
}, 2000, this, "postmozmilltest");
}
},
MozmillSetTestListener: function TPS__MozmillSetTestListener(obj) {
Logger.logInfo("mozmill setTest: " + obj.name);
},
- Cleanup() {
+ async Cleanup() {
try {
- this.WipeServer();
+ await this.WipeServer();
} catch (ex) {
Logger.logError("Failed to wipe server: " + Log.exceptionStr(ex));
}
try {
- if (Authentication.isLoggedIn) {
+ if (await Authentication.isLoggedIn()) {
// signout and wait for Sync to completely reset itself.
Logger.logInfo("signing out");
- let waiter = this.createEventWaiter("weave:service:start-over:finish");
- Authentication.signOut();
- waiter();
+ let waiter = this.promiseObserver("weave:service:start-over:finish");
+ await Authentication.signOut();
+ await waiter;
Logger.logInfo("signout complete");
}
- Authentication.deleteEmail(this.config.fx_account.username);
+ await Authentication.deleteEmail(this.config.fx_account.username);
} catch (e) {
Logger.logError("Failed to sign out: " + Log.exceptionStr(e));
}
},
/**
* Use Sync's bookmark validation code to see if we've corrupted the tree.
*/
- ValidateBookmarks() {
+ async ValidateBookmarks() {
let getServerBookmarkState = async () => {
let bookmarkEngine = Weave.Service.engineManager.get("bookmarks");
let collection = bookmarkEngine.itemSource();
let collectionKey = bookmarkEngine.service.collectionKeys.keyForCollection(bookmarkEngine.name);
collection.full = true;
let items = [];
let resp = await collection.get();
@@ -623,25 +630,25 @@ var TPS = {
record.decrypt(collectionKey);
items.push(record.cleartext);
}
return items;
};
let serverRecordDumpStr;
try {
Logger.logInfo("About to perform bookmark validation");
- let clientTree = Async.promiseSpinningly(PlacesUtils.promiseBookmarksTree("", {
+ let clientTree = await (PlacesUtils.promiseBookmarksTree("", {
includeItemIds: true
}));
- let serverRecords = Async.promiseSpinningly(getServerBookmarkState());
+ let serverRecords = await getServerBookmarkState();
// We can't wait until catch to stringify this, since at that point it will have cycles.
serverRecordDumpStr = JSON.stringify(serverRecords);
let validator = new BookmarkValidator();
- let {problemData} = Async.promiseSpinningly(validator.compareServerWithClient(serverRecords, clientTree));
+ let {problemData} = await validator.compareServerWithClient(serverRecords, clientTree);
for (let {name, count} of problemData.getSummary()) {
// Exclude mobile showing up on the server hackily so that we don't
// report it every time, see bug 1273234 and 1274394 for more information.
if (name === "serverUnexpected" && problemData.serverUnexpected.indexOf("mobile") >= 0) {
--count;
}
if (count) {
@@ -658,25 +665,25 @@ var TPS = {
if (serverRecordDumpStr) {
Logger.logInfo("Server bookmark records:\n" + serverRecordDumpStr + "\n");
}
this.DumpError("Bookmark validation failed", e);
}
Logger.logInfo("Bookmark validation finished");
},
- ValidateCollection(engineName, ValidatorType) {
+ async ValidateCollection(engineName, ValidatorType) {
let serverRecordDumpStr;
let clientRecordDumpStr;
try {
Logger.logInfo(`About to perform validation for "${engineName}"`);
let engine = Weave.Service.engineManager.get(engineName);
let validator = new ValidatorType(engine);
- let serverRecords = Async.promiseSpinningly(validator.getServerItems(engine));
- let clientRecords = Async.promiseSpinningly(validator.getClientItems());
+ let serverRecords = await validator.getServerItems(engine);
+ let clientRecords = await validator.getClientItems();
try {
// This substantially improves the logs for addons while not making a
// substantial difference for the other two
clientRecordDumpStr = JSON.stringify(clientRecords.map(r => {
let res = validator.normalizeClientItem(r);
delete res.original; // Try and prevent cyclic references
return res;
}));
@@ -685,17 +692,17 @@ var TPS = {
clientRecordDumpStr = "<Cyclic value>";
}
try {
serverRecordDumpStr = JSON.stringify(serverRecords);
} catch (e) {
// as above
serverRecordDumpStr = "<Cyclic value>";
}
- let { problemData } = Async.promiseSpinningly(validator.compareClientWithServer(clientRecords, serverRecords));
+ let { problemData } = await validator.compareClientWithServer(clientRecords, serverRecords);
for (let { name, count } of problemData.getSummary()) {
if (count) {
Logger.logInfo(`Validation problem: "${name}": ${JSON.stringify(problemData[name])}`);
}
Logger.AssertEqual(count, 0, `Validation error for "${engineName}" of type "${name}"`);
}
} catch (e) {
// Dump the client records if possible
@@ -718,32 +725,31 @@ var TPS = {
ValidateForms() {
return this.ValidateCollection("forms", FormValidator);
},
ValidateAddons() {
return this.ValidateCollection("addons", AddonValidator);
},
- RunNextTestAction() {
+ async RunNextTestAction() {
try {
- if (this._currentAction >=
- this._phaselist[this._currentPhase].length) {
+ if (this._currentAction >= this._phaselist[this._currentPhase].length) {
// Run necessary validations and then finish up
if (this.shouldValidateBookmarks) {
- this.ValidateBookmarks();
+ await this.ValidateBookmarks();
}
if (this.shouldValidatePasswords) {
- this.ValidatePasswords();
+ await this.ValidatePasswords();
}
if (this.shouldValidateForms) {
- this.ValidateForms();
+ await this.ValidateForms();
}
if (this.shouldValidateAddons) {
- this.ValidateAddons();
+ await this.ValidateAddons();
}
// Force this early so that we run the validation and detect missing pings
// *before* we start shutting down, since if we do it after, the python
// code won't notice the failure.
SyncTelemetry.shutdown();
// we're all done
Logger.logInfo("test phase " + this._currentPhase + ": " +
(this._errors ? "FAIL" : "PASS"));
@@ -757,17 +763,17 @@ var TPS = {
else {
this.DumpError("seconds-since-epoch not set");
return;
}
let phase = this._phaselist[this._currentPhase];
let action = phase[this._currentAction];
Logger.logInfo("starting action: " + action[0].name);
- action[0].apply(this, action.slice(1));
+ await action[0].apply(this, action.slice(1));
// if we're in an async operation, don't continue on to the next action
if (this._operations_pending)
return;
this._currentAction++;
} catch (e) {
if (Async.isShutdownException(e)) {
@@ -776,29 +782,29 @@ var TPS = {
} else {
this.DumpError("Sync aborted due to shutdown, but we didn't request it");
}
} else {
this.DumpError("RunNextTestAction failed", e);
}
return;
}
- this.RunNextTestAction();
+ await this.RunNextTestAction();
},
_getFileRelativeToSourceRoot(testFileURL, relativePath) {
let file = fileProtocolHandler.getFileFromURLSpec(testFileURL);
let root = file // <root>/services/sync/tests/tps/test_foo.js
.parent // <root>/services/sync/tests/tps
.parent // <root>/services/sync/tests
.parent // <root>/services/sync
.parent // <root>/services
.parent // <root>
;
- root.appendRelativePath(relativePath);
+ root.appendRelativePath(OS.Path.normalize(relativePath));
return root;
},
// Attempt to load the sync_ping_schema.json and initialize `this.pingValidator`
// based on the source of the tps file. Assumes that it's at "../unit/sync_ping_schema.json"
// relative to the directory the tps test file (testFile) is contained in.
_tryLoadPingSchema(testFile) {
try {
@@ -848,17 +854,17 @@ var TPS = {
* String URI of the file to open.
* @param phase
* String name of the phase to run.
* @param logpath
* String path of the log file to write to.
* @param options
* Object defining addition run-time options.
*/
- RunTestPhase(file, phase, logpath, options) {
+ async RunTestPhase(file, phase, logpath, options) {
try {
let settings = options || {};
Logger.init(logpath);
Logger.logInfo("Sync version: " + WEAVE_VERSION);
Logger.logInfo("Firefox buildid: " + Services.appinfo.appBuildID);
Logger.logInfo("Firefox version: " + Services.appinfo.version);
Logger.logInfo("Firefox source revision: " + (AppConstants.SOURCE_REVISION_URL || "unknown"));
@@ -876,28 +882,29 @@ var TPS = {
}
// We only want to do this if we modified the bookmarks this phase.
this.shouldValidateBookmarks = false;
// Always give Sync an extra tick to initialize. If we waited for the
// service:ready event, this is required to ensure all handlers have
// executed.
- Utils.nextTick(this._executeTestPhase.bind(this, file, phase, settings));
+ await Async.promiseYield();
+ await this._executeTestPhase(file, phase, settings);
} catch (e) {
this.DumpError("RunTestPhase failed", e);
}
},
/**
* Executes a single test phase.
*
* This is called by RunTestPhase() after the environment is validated.
*/
- _executeTestPhase: function _executeTestPhase(file, phase, settings) {
+ async _executeTestPhase(file, phase, settings) {
try {
this.config = JSON.parse(prefs.getCharPref("tps.config"));
// parse the test file
Services.scriptloader.loadSubScript(file, this);
this._currentPhase = phase;
// cleanup phases are in the format `cleanup-${profileName}`.
if (this._currentPhase.startsWith("cleanup-")) {
let profileToClean = this._currentPhase.slice("cleanup-".length);
@@ -937,19 +944,18 @@ var TPS = {
Logger.logInfo("setting client.name to " + this.phases[this._currentPhase]);
Weave.Svc.Prefs.set("client.name", this.phases[this._currentPhase]);
this._interceptSyncTelemetry();
// start processing the test actions
this._currentAction = 0;
- this._windowsUpDeferred.promise.then(() => {
- this.RunNextTestAction();
- });
+ await this._windowsUpDeferred.promise;
+ await this.RunNextTestAction();
} catch (e) {
this.DumpError("_executeTestPhase failed", e);
}
},
/**
* Override sync telemetry functions so that we can detect errors generating
* the sync ping, and count how many pings we report.
@@ -1048,123 +1054,119 @@ var TPS = {
Cu.import("resource://mozmill/modules/frame.js", frame);
frame.events.addListener("setTest", this.MozmillSetTestListener.bind(this));
frame.events.addListener("endTest", this.MozmillEndTestListener.bind(this));
this.StartAsyncOperation();
frame.runTestFile(mozmillfile.path, null);
},
/**
- * Return an object that when called, will block until the named event
- * is observed. This is similar to waitForEvent, although is typically safer
- * if you need to do some other work that may make the event fire.
+ * Returns a promise that resolves when a specific observer notification is
+ * resolved. This is similar to the various waitFor* functions, although is
+ * typically safer if you need to do some other work that may make the event
+ * fire.
*
* eg:
* doSomething(); // causes the event to be fired.
- * waitForEvent("something");
+ * await promiseObserver("something");
* is risky as the call to doSomething may trigger the event before the
- * waitForEvent call is made. Contrast with:
+ * promiseObserver call is made. Contrast with:
*
- * let waiter = createEventWaiter("something"); // does *not* block.
+ * let waiter = promiseObserver("something");
* doSomething(); // causes the event to be fired.
- * waiter(); // will return as soon as the event fires, even if it fires
- * // before this function is called.
+ * await waiter; // will return as soon as the event fires, even if it fires
+ * // before this function is called.
*
* @param aEventName
* String event to wait for.
*/
- createEventWaiter(aEventName) {
- Logger.logInfo("Setting up wait for " + aEventName + "...");
- let cb = Async.makeSpinningCallback();
- Svc.Obs.add(aEventName, cb);
- return function() {
- try {
- cb.wait();
- } finally {
- Svc.Obs.remove(aEventName, cb);
- Logger.logInfo(aEventName + " observed!");
+ promiseObserver(aEventName) {
+ return new Promise(resolve => {
+ Logger.logInfo("Setting up wait for " + aEventName + "...");
+ let handler = () => {
+ Logger.logInfo("Observed " + aEventName);
+ Svc.Obs.remove(aEventName, handler);
+ resolve();
}
- }
+ Svc.Obs.add(aEventName, handler);
+ });
},
/**
- * Synchronously wait for the named event to be observed.
+ * Wait for the named event to be observed.
*
- * When the event is observed, the function will wait an extra tick before
- * returning.
- *
- * Note that in general, you should probably use createEventWaiter unless you
+ * Note that in general, you should probably use promiseObserver unless you
* are 100% sure that the event being waited on can only be sent after this
* call adds the listener.
*
* @param aEventName
* String event to wait for.
*/
- waitForEvent: function waitForEvent(aEventName) {
- this.createEventWaiter(aEventName)();
+ async waitForEvent(aEventName) {
+ await this.promiseObserver(aEventName);
},
/**
* Waits for Sync to logged in before returning
*/
- waitForSetupComplete: function waitForSetup() {
+ async waitForSetupComplete() {
if (!this._setupComplete) {
- this.waitForEvent("weave:service:setup-complete");
+ await this.waitForEvent("weave:service:setup-complete");
}
},
/**
* Waits for Sync to be finished before returning
*/
- waitForSyncFinished: function TPS__waitForSyncFinished() {
+ async waitForSyncFinished() {
if (this._syncActive) {
- this.waitForEvent("weave:service:resyncs-finished");
+ await this.waitForEvent("weave:service:resyncs-finished");
}
},
/**
* Waits for Sync to start tracking before returning.
*/
- waitForTracking: function waitForTracking() {
+ async waitForTracking() {
if (!this._isTracking) {
- this.waitForEvent("weave:engine:start-tracking");
+ await this.waitForEvent("weave:engine:start-tracking");
}
},
/**
* Login on the server
*/
- Login: function Login(force) {
- if (Authentication.isLoggedIn && !force) {
+ async Login(force) {
+ if ((await Authentication.isLoggedIn()) && !force) {
return;
}
// This might come during Authentication.signIn
this._triggeredSync = true;
Logger.logInfo("Setting client credentials and login.");
- Authentication.signIn(this.config.fx_account);
- this.waitForSetupComplete();
+ await Authentication.signIn(this.config.fx_account);
+ await this.waitForSetupComplete();
Logger.AssertEqual(Weave.Status.service, Weave.STATUS_OK, "Weave status OK");
- this.waitForTracking();
+ await this.waitForTracking();
// We might get an initial sync at login time - let that complete.
- this.waitForSyncFinished();
+ await this.waitForSyncFinished();
},
/**
* Triggers a sync operation
*
* @param {String} [wipeAction]
* Type of wipe to perform (resetClient, wipeClient, wipeRemote)
*
*/
- Sync: function TPS__Sync(wipeAction) {
+ async Sync(wipeAction) {
if (this._syncActive) {
Logger.logInfo("WARNING: Sync currently active! Waiting, before triggering another");
- this.waitForSyncFinished();
+ await this.waitForSyncFinished();
}
Logger.logInfo("Executing Sync" + (wipeAction ? ": " + wipeAction : ""));
// Force a wipe action if requested. In case of an initial sync the pref
// will be overwritten by Sync itself (see bug 992198), so ensure that we
// also handle it via the "weave:service:setup-complete" notification.
if (wipeAction) {
this._syncWipeAction = wipeAction;
@@ -1173,34 +1175,34 @@ var TPS = {
Weave.Svc.Prefs.reset("firstSync");
}
this.Login(false);
++this._syncCount;
this._triggeredSync = true;
this.StartAsyncOperation();
- Async.promiseSpinningly(Weave.Service.sync());
+ await Weave.Service.sync();
Logger.logInfo("Sync is complete");
},
- WipeServer: function TPS__WipeServer() {
+ async WipeServer() {
Logger.logInfo("Wiping data from server.");
- this.Login(false);
- Async.promiseSpinningly(Weave.Service.login());
- Async.promiseSpinningly(Weave.Service.wipeServer());
+ await this.Login(false);
+ await Weave.Service.login();
+ await Weave.Service.wipeServer();
},
/**
* Action which ensures changes are being tracked before returning.
*/
- EnsureTracking: function EnsureTracking() {
- this.Login(false);
- this.waitForTracking();
+ async EnsureTracking() {
+ await this.Login(false);
+ await this.waitForTracking();
}
};
var Addons = {
install: function Addons__install(addons) {
TPS.HandleAddons(addons, ACTION_ADD);
},
setEnabled: function Addons__setEnabled(addons, state) {