Idea: Detect angular pages automatically #3859
Description
Preface: This idea got uglier while researching the implementation details. I think #3858 is probably a better idea, but I'm making this issue anyway just to write everything down
We could replace browser.ignoreSynchronization
with browser.synchronizationMode
which would be either 'ALWAYS'
, 'NEVER'
, or 'DETECT'
(I'm open to suggestions for different names). ALWAYS
would be equivalent to browser.ignoreSynchronization = true
, NEVER
would be equivalent to browser.ignoreSynchronization = false
, and DETECT
would work as follows:
First, we replace testForAngular
with the following logic:
function testForAngular(callback: (data: {ver?: number}) => void) {
// Pretend we add `undefined` checks throughout this function
function testForAngular_inner(callback: (data: {ver?: number}) => void) {
if (looksLikeNg1Obj(window.angular) || systemJsHas('angular')) {
callback({ver: 1});
} else if (looksLikeModernNgObj(window.ng || window.angular) || hasTestabilityFunctions(window) || systemJsHas('@angular')) {
callback({ver: 2});
} else {
waitForSystemJS(callback);
}
}
function systemJsHas(packageName: string): boolean {
let sysJs = window.SystemJS || window.System;
// Pretend this for loop syntax works
for (let name in (sysJs.map or _.invert(sysJs.map) or sysJs.defined or sysJs._loader.modules or sysJs._loader.importPromises)) {
if ((new RegExp('(\/|^)' + packageName + '(\/|$)').test(packageName)) {
return true;
}
}
return false;
}
var importDone: {[url: string]: boolean} = {};
function waitForSystemJS(callback: (data: {ver?: number}) => void) {
let sysJs = window.SystemJS || window.System;
let waitForImport = (promise: Promise<any>, url: string) => {
importDone[url] = false;
let onComplete = () => {
importDone[url] = true;
// If all promises are resolved, call testForAngular_inner() again
for (let resolved of importResolved) {
if (!resolved) {
return;
}
}
testForAngular_inner(callback);
};
promise.then(onComplete, onComplete);
}
let newPromises = false;
for (let url in sysJs._loader.importPromises) {
if (sysJs._loader.importPromises[url] && (importDone[url] === undefined)) {
importResolved[url] = false;
waitForImport(sysJs._loader.importPromises[url], url);
}
}
if (!newPromises) {
callback({}); // Give up, no angular
}
}
testForAngular_inner(callback);
}
(back when I came up with this idea, I didn't realize what an issue SystemJS
was going to be)
Now, in browser.get
, we use testForAngular
, and based on the result of that we decide if we want to do synchronization. Some pitfalls:
- If navigation happened via something other than
browser.get
(e.g. someone clicks on a link), we wouldn't bootstrap and end up using an outdated synchronization setting from a previous page. However, Bootstrapping should (mostly) work with Browser-initiated navigation (e.g. clicking on links) #3857 should address that. SystemJS.defined
andSystemJS._loader
are not public APIs- No support for other package managers
- Eventually there will be native package managers, but maybe they'll block js execution like
<script>
tags do, or maybe there will be something analogous toDOMContentLoaded
for them. - This could still fail if someone is accessing angular indirectly through another package which has angular hard-coded
Alternative ideas
- Feature request: Allow users to specify which URLs to sync against #3858
- In the examples I've seen, zone.js always seems to be loaded directly via a
<script>
tag. If we seewindow.Zone
, we could wait a few seconds for angular to show up. - We could make a package like
@protractor/testability-flag
and ask users to include it client side in a<script>
tag.