Merge changes I3a17b387,I872bc926,I40692a07,I66a3cb81,I8fb9e8dc, ...

* changes:
  Get rid of some global variables - Part 4
  Get rid of some global variables - Part 3
  Get rid of some global variables - Part 2
  Get rid of global GrEtagDecorator
  Get rid of global GrDomHooksManager and GrDomHook
  Get rid of some global variables - Part 1
  Get rid of global GrChangeActionsInterface
  Get rid of global GrDiffBuilderBinary
  Get rid of global GrDiffBuilderUnified
  Get rid of global GrDiffBuilderImage
  Get rid of global GrDiffBuilderSideBySide
  Get rid of global GrDiffBuilder
  Get rid of global GrDiffGroup
  Get rid of global GrDiffLine
  Get rid of global GrAttributeHelper
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
index 7de6e61..d6da71a 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list/gr-change-list.js
@@ -30,7 +30,7 @@
 import {LegacyElementMixin} from '@polymer/polymer/lib/legacy/legacy-element-mixin.js';
 import {PolymerElement} from '@polymer/polymer/polymer-element.js';
 import {htmlTemplate} from './gr-change-list_html.js';
-import {flags} from '../../../services/flags';
+import {appContext} from '../../../services/app-context.js';
 import {BaseUrlBehavior} from '../../../behaviors/base-url-behavior/base-url-behavior.js';
 import {ChangeTableBehavior} from '../../../behaviors/gr-change-table-behavior/gr-change-table-behavior.js';
 import {URLEncodingBehavior} from '../../../behaviors/gr-url-encoding-behavior/gr-url-encoding-behavior.js';
@@ -152,6 +152,11 @@
     };
   }
 
+  constructor() {
+    super();
+    this.flagsService = appContext.flagsService;
+  }
+
   /** @override */
   created() {
     super.created();
@@ -204,7 +209,7 @@
     this.changeTableColumns = this.columnNames;
     this.showNumber = false;
     this.visibleChangeTableColumns = this.getEnabledColumns(this.columnNames,
-        config, flags.enabledExperiments);
+        config, this.flagsService.enabledExperiments);
 
     if (account) {
       this.showNumber = !!(preferences &&
@@ -213,7 +218,7 @@
           preferences.change_table.length > 0) {
         const prefColumns = this.getVisibleColumns(preferences.change_table);
         this.visibleChangeTableColumns = this.getEnabledColumns(prefColumns,
-            config, flags.enabledExperiments);
+            config, this.flagsService.enabledExperiments);
       }
     }
   }
diff --git a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
index 059aa71..4de3128 100644
--- a/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
+++ b/polygerrit-ui/app/elements/change/gr-change-metadata/gr-change-metadata_html.js
@@ -23,17 +23,11 @@
     <style include="shared-styles">
       :host {
         display: table;
+        --account-max-length: 20ch;
       }
       gr-change-requirements {
         --requirements-horizontal-padding: var(--metadata-horizontal-padding);
       }
-      gr-account-link {
-        max-width: 20ch;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        vertical-align: top;
-        white-space: nowrap;
-      }
       gr-editable-label {
         max-width: 9em;
       }
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.js b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
index 0c3bebb..9028782 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.js
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.js
@@ -71,7 +71,6 @@
       },
       comments: {
         type: Object,
-        observer: '_commentsChanged',
       },
       config: Object,
       hideAutomated: {
@@ -266,17 +265,6 @@
     return expanded;
   }
 
-  /**
-   * If there is no value set on the message object as to whether _expanded
-   * should be true or not, then _expanded is set to true if there are
-   * inline comments (otherwise false).
-   */
-  _commentsChanged(value) {
-    if (this.message && this.message.expanded === undefined) {
-      this.set('message.expanded', Object.keys(value || {}).length > 0);
-    }
-  }
-
   _handleClick(e) {
     if (this.message.expanded) { return; }
     e.stopPropagation();
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
index 8864056..2e195c1 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message_test.html
@@ -61,6 +61,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'Uploaded patch set 1.',
         _revision_number: 1,
+        expanded: false,
       };
 
       element.addEventListener('reply', e => {
@@ -85,6 +86,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'Uploaded patch set 1.',
         _revision_number: 1,
+        expanded: false,
       };
 
       flushAsynchronousOperations();
@@ -102,6 +104,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'Uploaded patch set 1.',
         _revision_number: 1,
+        expanded: false,
       };
 
       element.addEventListener('change-message-deleted', e => {
@@ -118,6 +121,7 @@
       element.message = {
         tag: 'autogenerated:gerrit:test',
         updated: '2016-01-12 20:24:49.448000000',
+        expanded: false,
       };
 
       assert.isTrue(element.isAutomated);
@@ -133,6 +137,7 @@
         tag: 'autogenerated:gerrit:test',
         updated: '2016-01-12 20:24:49.448000000',
         reviewer: {},
+        expanded: false,
       };
 
       assert.isTrue(element.isAutomated);
@@ -148,6 +153,7 @@
         type: 'REVIEWER_UPDATE',
         updated: '2016-01-12 20:24:49.448000000',
         reviewer: {},
+        expanded: false,
       };
 
       assert.isTrue(element.isAutomated);
@@ -162,6 +168,7 @@
       element.message = {
         tag: 'something',
         updated: '2016-01-12 20:24:49.448000000',
+        expanded: false,
       };
 
       assert.isFalse(element.isAutomated);
@@ -175,6 +182,7 @@
     test('reply button hidden unless logged in', () => {
       const message = {
         message: 'Uploaded patch set 1.',
+        expanded: false,
       };
       assert.isFalse(element._computeShowReplyButton(message, false));
       assert.isTrue(element._computeShowReplyButton(message, true));
@@ -183,6 +191,7 @@
     test('_computeShowOnBehalfOf', () => {
       const message = {
         message: '...',
+        expanded: false,
       };
       assert.isNotOk(element._computeShowOnBehalfOf(message));
       message.author = {_account_id: 1115495};
@@ -218,6 +227,7 @@
         updated: '2016-01-12 20:24:49.448000000',
         reviewer: {},
         id: '47c43261_55aa2c41',
+        expanded: false,
       };
       flushAsynchronousOperations();
       const stub = sinon.stub();
@@ -367,6 +377,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'Uploaded patch set 1.',
         _revision_number: 1,
+        expanded: false,
       };
 
       flushAsynchronousOperations();
@@ -402,6 +413,7 @@
         date: '2016-01-12 20:24:49.448000000',
         message: 'Uploaded patch set 1.',
         _revision_number: 1,
+        expanded: false,
       };
 
       flushAsynchronousOperations();
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
index 79b832d..ca8ab02 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.js
@@ -38,6 +38,16 @@
 };
 
 /**
+ * The content of the enum is also used in the UI for the button text.
+ *
+ * @enum {string}
+ */
+const ExpandAllState = {
+  EXPAND_ALL: 'Expand All',
+  COLLAPSE_ALL: 'Collapse All',
+};
+
+/**
  * @extends Polymer.Element
  */
 class GrMessagesList extends mixinBehaviors( [
@@ -68,14 +78,20 @@
       },
       labels: Object,
 
-      _expanded: {
-        type: Boolean,
-        value: false,
-        observer: '_expandedChanged',
-      },
-
-      _expandCollapseTitle: {
+      /**
+       * Keeps track of the state of the "Expand All" toggle button. Note that
+       * you can individually expand/collapse some messages without affecting
+       * the toggle button's state.
+       *
+       * @type {ExpandAllState}
+       */
+      _expandAllState: {
         type: String,
+        value: ExpandAllState.EXPAND_ALL,
+      },
+      _expandAllTitle: {
+        type: String,
+        computed: '_computeExpandAllTitle(_expandAllState)',
       },
 
       _hideAutomated: {
@@ -181,13 +197,18 @@
         mDate = null;
       }
     }
+    result.forEach(m => {
+      if (m.expanded === undefined) {
+        m.expanded = false;
+      }
+    });
     return result;
   }
 
-  _expandedChanged(exp) {
+  _updateExpandedStateOfAllMessages(expanded) {
     if (this._processedMessages) {
       for (let i = 0; i < this._processedMessages.length; i++) {
-        this._processedMessages[i].expanded = exp;
+        this._processedMessages[i].expanded = expanded;
       }
     }
     // _visibleMessages is a subarray of _processedMessages
@@ -199,14 +220,18 @@
         this.notifyPath(`_visibleMessages.${i}.expanded`);
       }
     }
+  }
 
-    if (this._expanded) {
-      this._expandCollapseTitle = this.createTitle(
+  _computeExpandAllTitle(_expandAllState) {
+    if (_expandAllState === ExpandAllState.COLLAPSED_ALL) {
+      return this.createTitle(
           this.Shortcut.COLLAPSE_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
-    } else {
-      this._expandCollapseTitle = this.createTitle(
+    }
+    if (_expandAllState === ExpandAllState.EXPAND_ALL) {
+      return this.createTitle(
           this.Shortcut.EXPAND_ALL_MESSAGES, this.ShortcutSection.ACTIONS);
     }
+    return '';
   }
 
   _highlightEl(el) {
@@ -227,12 +252,15 @@
    * @param {boolean} expand
    */
   handleExpandCollapse(expand) {
-    this._expanded = expand;
+    this._expandAllState = expand ? ExpandAllState.COLLAPSE_ALL
+      : ExpandAllState.EXPAND_ALL;
+    this._updateExpandedStateOfAllMessages(expand);
   }
 
   _handleExpandCollapseTap(e) {
     e.preventDefault();
-    this.handleExpandCollapse(!this._expanded);
+    this.handleExpandCollapse(
+        this._expandAllState === ExpandAllState.EXPAND_ALL);
   }
 
   _handleAnchorClick(e) {
@@ -249,10 +277,6 @@
     return false;
   }
 
-  _computeExpandCollapseMessage(expanded) {
-    return expanded ? 'Collapse all' : 'Expand all';
-  }
-
   /**
    * Computes message author's file comments for change's message.
    * Method uses this.messages to find next message and relies on messages
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
index 3e9f7b5..1a24234 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_html.js
@@ -71,8 +71,8 @@
           <paper-toggle-button id="automatedMessageToggle" checked="{{_hideAutomated}}"></paper-toggle-button>Only comments
           <span class="transparent separator"></span>
         </span>
-        <gr-button id="collapse-messages" link="" title="[[_expandCollapseTitle]]" on-click="_handleExpandCollapseTap">
-          [[_computeExpandCollapseMessage(_expanded)]]
+        <gr-button id="collapse-messages" link="" title="[[_expandAllTitle]]" on-click="_handleExpandCollapseTap">
+          [[_expandAllState]]
         </gr-button>
       </div>
     <span id="messageControlsContainer" hidden\$="[[_computeShowHideTextHidden(_visibleMessages, _processedMessages, _hideAutomated, _visibleMessages.length)]]">
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
index a5cc939..ccbe67c 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list_test.html
@@ -300,28 +300,21 @@
     });
 
     test('expand/collapse from external keypress', () => {
-      MockInteractions.tap(element.shadowRoot
-          .querySelector('#collapse-messages'));
-      let allMessageEls = getMessages();
-      for (const message of allMessageEls) {
-        assert.isTrue(message._expanded);
-      }
+      // Start with one expanded message. -> not all collapsed
+      element.scrollToMessage(messages[1].id);
+      assert.isFalse([...getMessages()].filter(m => m._expanded).length == 0);
 
-      // Expand/collapse all text also changes.
-      assert.equal(element.shadowRoot
-          .querySelector('#collapse-messages').textContent.trim(),
-      'Collapse all');
+      // Press 'z' -> all collapsed
+      element.handleExpandCollapse(false);
+      assert.isTrue([...getMessages()].filter(m => m._expanded).length == 0);
 
-      MockInteractions.tap(element.shadowRoot
-          .querySelector('#collapse-messages'));
-      allMessageEls = getMessages();
-      for (const message of allMessageEls) {
-        assert.isFalse(message._expanded);
-      }
-      // Expand/collapse all text also changes.
-      assert.equal(element.shadowRoot
-          .querySelector('#collapse-messages').textContent.trim(),
-      'Expand all');
+      // Press 'x' -> all expanded
+      element.handleExpandCollapse(true);
+      assert.isTrue([...getMessages()].filter(m => !m._expanded).length == 0);
+
+      // Press 'z' -> all collapsed
+      element.handleExpandCollapse(false);
+      assert.isTrue([...getMessages()].filter(m => m._expanded).length == 0);
     });
 
     test('hide messages does not appear when no automated messages', () => {
diff --git a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
index 1c5331d6..c8b4ff5 100644
--- a/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
+++ b/polygerrit-ui/app/elements/core/gr-reporting/gr-reporting.js
@@ -17,7 +17,7 @@
 import '../../../scripts/bundled-polymer.js';
 
 import {Polymer} from '@polymer/polymer/lib/legacy/polymer-fn.js';
-import {flags} from '../../../services/flags';
+import {appContext} from '../../../services/app-context.js';
 
 // Latency reporting constants.
 const TIMING = {
@@ -279,7 +279,7 @@
       eventInfo.inBackgroundTab = isInBackgroundTab;
     }
 
-    const enabledExperiments = flags.enabledExperiments;
+    const enabledExperiments = appContext.flagsService.enabledExperiments;
     if (enabledExperiments.length) {
       eventInfo.enabledExperiments = JSON.stringify(enabledExperiments);
     }
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
index 7b8535d..ae363e5 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff-builder/gr-diff-builder-element.js
@@ -218,6 +218,7 @@
   }
 
   getContentByLineEl(lineEl) {
+    if (!lineEl) return;
     const root = dom(lineEl.parentElement);
     const side = this.getSideByLineEl(lineEl);
     const line = lineEl.getAttribute('data-value');
diff --git a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
index 5a2fdfe..0597994 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
+++ b/polygerrit-ui/app/elements/diff/gr-diff/gr-diff.js
@@ -633,6 +633,9 @@
    */
   _createComment(lineEl, lineNum, side, range) {
     const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
+    if (!contentText) {
+      return;
+    }
     const contentEl = contentText.parentElement;
     side = side ||
         this._getCommentSideByLineAndContent(lineEl, contentEl);
@@ -852,6 +855,7 @@
   }
 
   _handleRenderContent() {
+    this._unobserveIncrementalNodes();
     this._incrementalNodeObserver = dom(this).observeNodes(info => {
       const addedThreadEls = info.addedNodes.filter(isThreadEl);
       // Removed nodes do not need to be handled because all this code does is
@@ -866,6 +870,9 @@
         const lineEl = this.$.diffBuilder.getLineElByNumber(
             lineNumString, commentSide);
         const contentText = this.$.diffBuilder.getContentByLineEl(lineEl);
+        if (!contentText) {
+          continue;
+        }
         const contentEl = contentText.parentElement;
         const threadGroupEl = this._getOrCreateThreadGroup(
             contentEl, commentSide);
diff --git a/polygerrit-ui/app/elements/gr-app-init.js b/polygerrit-ui/app/elements/gr-app-init.js
index d1851e8..7caec6d 100644
--- a/polygerrit-ui/app/elements/gr-app-init.js
+++ b/polygerrit-ui/app/elements/gr-app-init.js
@@ -14,6 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import {initAppContext} from '../services/app-context-init.js';
 
 if (!window.Polymer) {
   window.Polymer = {
@@ -21,4 +22,6 @@
     lazyRegister: true,
   };
 }
-window.Gerrit = window.Gerrit || {};
\ No newline at end of file
+window.Gerrit = window.Gerrit || {};
+
+initAppContext();
\ No newline at end of file
diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js
index e5616db..ce6d613 100644
--- a/polygerrit-ui/app/elements/gr-app.js
+++ b/polygerrit-ui/app/elements/gr-app.js
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-/* TODO(taoalpha): Remove once all legacyUndefinedCheck removed. */
+
 /*
   FIXME(polymer-modulizer): the above comments were extracted
   from HTML and may be out of place here. Review them and
@@ -23,6 +23,16 @@
 import './gr-app-init.js';
 import './font-roboto-local-loader.js';
 import '../scripts/bundled-polymer.js';
+
+/**
+ * setCancelSyntheticClickEvents is set to true by
+ * default which will cancel synthetic click events
+ * on older touch device.
+ * See https://github.com/Polymer/polymer/issues/5289
+ */
+import {setCancelSyntheticClickEvents} from '@polymer/polymer/lib/utils/settings.js';
+setCancelSyntheticClickEvents(false);
+
 import 'polymer-resin/standalone/polymer-resin.js';
 import {initGlobalVariables} from './gr-app-global-var-init.js';
 // Initialize global variables before any other imports
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
index 5eb9ad5..422d004 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_html.js
@@ -20,6 +20,14 @@
     <style include="shared-styles">
       :host {
         display: inline-block;
+        /* Setting this really high, so all the following rules don't change
+           anything, only if --account-max-length is actually set to something
+           smaller like 20ch. */
+        max-width: var(--account-max-length, 500px);
+        overflow: hidden;
+        text-overflow: ellipsis;
+        vertical-align: top;
+        white-space: nowrap;
       }
       a {
         color: var(--primary-text-color);
diff --git a/polygerrit-ui/app/rules.bzl b/polygerrit-ui/app/rules.bzl
index bae9ba2..9303f2b 100644
--- a/polygerrit-ui/app/rules.bzl
+++ b/polygerrit-ui/app/rules.bzl
@@ -96,15 +96,17 @@
     )
 
 def _wct_test(name, srcs, split_index, split_count):
-    """Private macro to define single WCT suite for a
-    portion of test files with split_index. The actual split happens in test/tests.js file
+    """Macro to define single WCT suite
 
-        Args:
-            name: name of generated sh_test"
-            srcs: source files
-            split_index: index WCT suite. Must be less than split_count
-            split_count: total number of WCT suites
-        """
+    Defines a private macro for a portion of test files with split_index.
+    The actual split happens in test/tests.js file
+
+    Args:
+        name: name of generated sh_test
+        srcs: source files
+        split_index: index WCT suite. Must be less than split_count
+        split_count: total number of WCT suites
+    """
     str_index = str(split_index)
     config_json = struct(splitIndex = split_index, splitCount = split_count).to_json()
     native.sh_test(
@@ -127,20 +129,27 @@
 
 def wct_suite(name, srcs, split_count):
     """Define test suites for WCT tests.
+
     All tests files are splited to split_count WCT suites
 
-        Args:
-            name: rule name. The macro create a test suite rule with the name name+"_test"
-            srcs: source files
-            split: number of sh_test (i.e. WCT suites)
-        """
+    Args:
+        name: rule name. The macro create a test suite rule with the name name+"_test"
+        srcs: source files
+        split_count: number of sh_test (i.e. WCT suites)
+    """
     tests = []
     for i in range(split_count):
         test_name = "wct_test_" + str(i)
         _wct_test(test_name, srcs, i, split_count)
-        tests += [test_name]
+        tests.append(test_name)
 
     native.test_suite(
         name = name + "_test",
         tests = tests,
+        # Setup tags for suite as well.
+        # This excludes tests from the wildcard expansion (//...)
+        tags = [
+            "local",
+            "manual",
+        ],
     )
diff --git a/polygerrit-ui/app/services/README.md b/polygerrit-ui/app/services/README.md
index 33b1ee9..b88532b 100644
--- a/polygerrit-ui/app/services/README.md
+++ b/polygerrit-ui/app/services/README.md
@@ -9,10 +9,34 @@
 Regarding all stateful should be considered as services or not, it's still TBD. Will update as soon
 as it's finalized.
 
-## Future plans
+## How to access service
 
-To make services much easier to use, we may need to adopt a DI (dependency injection) system instead of exporting singleton
-from the services directly. And it will help in mocking services in tests as well.
+We use AppContext to access instance of service. It helps in mocking service in tests as well.
+We prefer setting instance of service in constructor and then accessing it from variable. We also
+allow access straight from appContext especially in static methods.
+
+```
+import {appContext} from '../../../services/app-context.js';
+
+class T {
+  constructor() {
+    super();
+    this.flagsService = appContext.flagsService;
+  }
+
+  action1() {
+    if (this.flagsService.isEnabled('test)) {
+      // do something
+    }
+  }
+}
+
+staticMethod() {
+  if (appContext.flagsService.isEnabled('test)) {
+    // do something
+  }
+}
+```
 
 ## What services we have
 
@@ -21,10 +45,10 @@
 'flags' is a service to provide easy access to all enabled experiments.
 
 ```
-import {flags} from "./flags.js";
+import {appContext} from '../../../services/app-context.js';
 
 // check if an experiment is enabled or not
-if (flags.isEnabled('test')) {
+if (appContext.flagsService.isEnabled('test')) {
   // do something
 }
 ```
\ No newline at end of file
diff --git a/polygerrit-ui/app/services/app-context-init.js b/polygerrit-ui/app/services/app-context-init.js
new file mode 100644
index 0000000..1c32eee
--- /dev/null
+++ b/polygerrit-ui/app/services/app-context-init.js
@@ -0,0 +1,48 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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 {appContext} from './app-context.js';
+import {FlagsService} from './flags.js';
+
+const initializedServices = new Map();
+
+function getService(serviceName, serviceInit) {
+  if (!initializedServices[serviceName]) {
+    initializedServices[serviceName] = serviceInit();
+  }
+  return initializedServices[serviceName];
+}
+
+/**
+ * The AppContext lazy initializator for all services
+ */
+export function initAppContext() {
+  const registeredServices = {};
+  function addService(serviceName, serviceCreator) {
+    if (registeredServices[serviceName]) {
+      throw new Error(`Service ${serviceName} already registered.`);
+    }
+    registeredServices[serviceName] = {
+      get() {
+        return getService(serviceName, serviceCreator);
+      },
+    };
+  }
+
+  addService('flagsService', () => new FlagsService());
+
+  Object.defineProperties(appContext, registeredServices);
+}
diff --git a/polygerrit-ui/app/services/app-context-init_test.html b/polygerrit-ui/app/services/app-context-init_test.html
new file mode 100644
index 0000000..8ee78ba
--- /dev/null
+++ b/polygerrit-ui/app/services/app-context-init_test.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<!--
+@license
+Copyright (C) 2020 The Android Open Source Project
+
+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.
+-->
+
+<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
+<script src="/node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
+<script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
+<script src="/components/wct-browser-legacy/browser.js"></script>
+
+<script type="module">
+  import '../test/common-test-setup.js';
+  import {appContext} from './app-context.js';
+  import {initAppContext} from './app-context-init.js';
+  suite('app context initializer tests', () => {
+    setup(() => {
+      initAppContext();
+    });
+
+    test('all services initialized and are singletons', () => {
+      Object.keys(appContext).forEach(serviceName => {
+        const service = appContext[serviceName];
+        assert.isNotNull(service);
+        const service2 = appContext[serviceName];
+        assert.strictEqual(service, service2);
+      });
+    });
+  });
+</script>
\ No newline at end of file
diff --git a/polygerrit-ui/app/services/app-context.js b/polygerrit-ui/app/services/app-context.js
new file mode 100644
index 0000000..e10ced5
--- /dev/null
+++ b/polygerrit-ui/app/services/app-context.js
@@ -0,0 +1,26 @@
+/**
+ * @license
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+/**
+ * The AppContext holds immortal singleton instances of services. It's a
+ * convenient way to provide singletons that can be swapped out for testing.
+ *
+ * AppContext is initialized in ./app-context-init.js
+ */
+export const appContext = {
+  flagsService: null,
+};
\ No newline at end of file
diff --git a/polygerrit-ui/app/services/flags.js b/polygerrit-ui/app/services/flags.js
index 737bb71..8f04f4a 100644
--- a/polygerrit-ui/app/services/flags.js
+++ b/polygerrit-ui/app/services/flags.js
@@ -20,7 +20,7 @@
  *
  * Provides all related methods / properties regarding on feature flags.
  */
-class Flags {
+export class FlagsService {
   constructor() {
     // stores all enabled experiments
     this._experiments = new Set();
@@ -46,6 +46,3 @@
     return [...this._experiments];
   }
 }
-
-// Export a single instance of Flags to be used across components.
-export const flags = new Flags();
\ No newline at end of file
diff --git a/polygerrit-ui/app/services/flags_test.html b/polygerrit-ui/app/services/flags_test.html
index 853ff05..ff94494 100644
--- a/polygerrit-ui/app/services/flags_test.html
+++ b/polygerrit-ui/app/services/flags_test.html
@@ -27,12 +27,9 @@
 
 <script type="module">
   import '../test/common-test-setup.js';
-  import {flags} from './flags.js';
-  import {flags as flags2} from './flags.js';
+  import {FlagsService} from './flags.js';
   suite('flags tests', () => {
-    test('singlton', () => {
-      assert.equal(flags, flags2);
-    });
+    const flags = new FlagsService();
 
     test('isEnabled', () => {
       assert.equal(flags.isEnabled('a'), true);
diff --git a/polygerrit-ui/app/test/common-test-setup.js b/polygerrit-ui/app/test/common-test-setup.js
index 54a417f..ca6cfc2 100644
--- a/polygerrit-ui/app/test/common-test-setup.js
+++ b/polygerrit-ui/app/test/common-test-setup.js
@@ -21,7 +21,7 @@
 import './test-router.js';
 import moment from 'moment/src/moment.js';
 import {SafeTypes} from '../behaviors/safe-types-behavior/safe-types-behavior.js';
-
+import {initAppContext} from '../services/app-context-init.js';
 self.moment = moment;
 security.polymer_resin.install({
   allowedIdentifierPrefixes: [''],
@@ -78,6 +78,7 @@
   if (Gerrit._testOnly_resetPlugins) {
     Gerrit._testOnly_resetPlugins();
   }
+  initAppContext();
 });
 
 if (window.stub) {