refactor(upgrade): use Bazel packages to avoid symlinks in the source (#29466)
Previously we had to share code between upgrade/dynamic and upgrade/static by symlinking the `src` folder, which allowed both packages to access the upgrade/common files. These symlinks are always problematic on Windows, where we had to run a script to re-link them, and restore them. This change uses Bazel packages to share the `upgrade/common` code, which avoids the need for symlinking the `src` folder. Also, the Windows specific scripts that fixup the symlinks have also been removed as there is no more need for them. PR Close #29466
This commit is contained in:

committed by
Jason Aden

parent
e5201f92fc
commit
9f54d76ef5
16
packages/upgrade/src/common/BUILD.bazel
Normal file
16
packages/upgrade/src/common/BUILD.bazel
Normal file
@ -0,0 +1,16 @@
|
||||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
package(default_visibility = [
|
||||
"//packages/upgrade:__subpackages__",
|
||||
"//tools/public_api_guard:__subpackages__",
|
||||
])
|
||||
|
||||
ts_library(
|
||||
name = "common",
|
||||
srcs = glob([
|
||||
"src/**/*.ts",
|
||||
]),
|
||||
deps = [
|
||||
"//packages/core",
|
||||
],
|
||||
)
|
23
packages/upgrade/src/common/test/BUILD.bazel
Normal file
23
packages/upgrade/src/common/test/BUILD.bazel
Normal file
@ -0,0 +1,23 @@
|
||||
load("//tools:defaults.bzl", "ts_library", "ts_web_test_suite")
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(["**/*.ts"]),
|
||||
deps = [
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
"//packages/upgrade/src/common",
|
||||
"//packages/upgrade/src/common/test/helpers",
|
||||
],
|
||||
)
|
||||
|
||||
ts_web_test_suite(
|
||||
name = "test",
|
||||
static_files = [
|
||||
"//:angularjs_scripts",
|
||||
],
|
||||
deps = [
|
||||
":test_lib",
|
||||
],
|
||||
)
|
37
packages/upgrade/src/common/test/component_info_spec.ts
Normal file
37
packages/upgrade/src/common/test/component_info_spec.ts
Normal file
@ -0,0 +1,37 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {PropertyBinding} from '../src/component_info';
|
||||
|
||||
{
|
||||
describe('PropertyBinding', () => {
|
||||
it('should process a simple binding', () => {
|
||||
const binding = new PropertyBinding('someBinding', 'someBinding');
|
||||
expect(binding.prop).toEqual('someBinding');
|
||||
expect(binding.attr).toEqual('someBinding');
|
||||
expect(binding.bracketAttr).toEqual('[someBinding]');
|
||||
expect(binding.bracketParenAttr).toEqual('[(someBinding)]');
|
||||
expect(binding.parenAttr).toEqual('(someBinding)');
|
||||
expect(binding.onAttr).toEqual('onSomeBinding');
|
||||
expect(binding.bindAttr).toEqual('bindSomeBinding');
|
||||
expect(binding.bindonAttr).toEqual('bindonSomeBinding');
|
||||
});
|
||||
|
||||
it('should process a two-part binding', () => {
|
||||
const binding = new PropertyBinding('someProp', 'someAttr');
|
||||
expect(binding.prop).toEqual('someProp');
|
||||
expect(binding.attr).toEqual('someAttr');
|
||||
expect(binding.bracketAttr).toEqual('[someAttr]');
|
||||
expect(binding.bracketParenAttr).toEqual('[(someAttr)]');
|
||||
expect(binding.parenAttr).toEqual('(someAttr)');
|
||||
expect(binding.onAttr).toEqual('onSomeAttr');
|
||||
expect(binding.bindAttr).toEqual('bindSomeAttr');
|
||||
expect(binding.bindonAttr).toEqual('bindonSomeAttr');
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,203 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {Compiler, Component, ComponentFactory, Injector, NgModule, TestabilityRegistry} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import * as angular from '../src/angular1';
|
||||
import {DowngradeComponentAdapter, groupNodesBySelector} from '../src/downgrade_component_adapter';
|
||||
|
||||
import {nodes, withEachNg1Version} from './helpers/common_test_helpers';
|
||||
|
||||
withEachNg1Version(() => {
|
||||
describe('DowngradeComponentAdapter', () => {
|
||||
describe('groupNodesBySelector', () => {
|
||||
it('should return an array of node collections for each selector', () => {
|
||||
const contentNodes = nodes(
|
||||
'<div class="x"><span>div-1 content</span></div>' +
|
||||
'<input type="number" name="myNum">' +
|
||||
'<input type="date" name="myDate">' +
|
||||
'<span>span content</span>' +
|
||||
'<div class="x"><span>div-2 content</span></div>');
|
||||
|
||||
const selectors = ['input[type=date]', 'span', '.x'];
|
||||
const projectableNodes = groupNodesBySelector(selectors, contentNodes);
|
||||
expect(projectableNodes[0]).toEqual(nodes('<input type="date" name="myDate">'));
|
||||
expect(projectableNodes[1]).toEqual(nodes('<span>span content</span>'));
|
||||
expect(projectableNodes[2])
|
||||
.toEqual(nodes(
|
||||
'<div class="x"><span>div-1 content</span></div>' +
|
||||
'<div class="x"><span>div-2 content</span></div>'));
|
||||
});
|
||||
|
||||
it('should collect up unmatched nodes for the wildcard selector', () => {
|
||||
const contentNodes = nodes(
|
||||
'<div class="x"><span>div-1 content</span></div>' +
|
||||
'<input type="number" name="myNum">' +
|
||||
'<input type="date" name="myDate">' +
|
||||
'<span>span content</span>' +
|
||||
'<div class="x"><span>div-2 content</span></div>');
|
||||
|
||||
const selectors = ['.x', '*', 'input[type=date]'];
|
||||
const projectableNodes = groupNodesBySelector(selectors, contentNodes);
|
||||
|
||||
expect(projectableNodes[0])
|
||||
.toEqual(nodes(
|
||||
'<div class="x"><span>div-1 content</span></div>' +
|
||||
'<div class="x"><span>div-2 content</span></div>'));
|
||||
expect(projectableNodes[1])
|
||||
.toEqual(nodes(
|
||||
'<input type="number" name="myNum">' +
|
||||
'<span>span content</span>'));
|
||||
expect(projectableNodes[2]).toEqual(nodes('<input type="date" name="myDate">'));
|
||||
});
|
||||
|
||||
it('should return an array of empty arrays if there are no nodes passed in', () => {
|
||||
const selectors = ['.x', '*', 'input[type=date]'];
|
||||
const projectableNodes = groupNodesBySelector(selectors, []);
|
||||
expect(projectableNodes).toEqual([[], [], []]);
|
||||
});
|
||||
|
||||
it('should return an empty array for each selector that does not match', () => {
|
||||
const contentNodes = nodes(
|
||||
'<div class="x"><span>div-1 content</span></div>' +
|
||||
'<input type="number" name="myNum">' +
|
||||
'<input type="date" name="myDate">' +
|
||||
'<span>span content</span>' +
|
||||
'<div class="x"><span>div-2 content</span></div>');
|
||||
|
||||
const projectableNodes = groupNodesBySelector([], contentNodes);
|
||||
expect(projectableNodes).toEqual([]);
|
||||
|
||||
const noMatchSelectorNodes = groupNodesBySelector(['.not-there'], contentNodes);
|
||||
expect(noMatchSelectorNodes).toEqual([[]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('testability', () => {
|
||||
|
||||
let adapter: DowngradeComponentAdapter;
|
||||
let content: string;
|
||||
let compiler: Compiler;
|
||||
let registry: TestabilityRegistry;
|
||||
let element: angular.IAugmentedJQuery;
|
||||
|
||||
class mockScope implements angular.IScope {
|
||||
private destroyListeners: (() => void)[] = [];
|
||||
|
||||
$new() { return this; }
|
||||
$watch(exp: angular.Ng1Expression, fn?: (a1?: any, a2?: any) => void) {
|
||||
return () => {};
|
||||
}
|
||||
$on(event: string, fn?: (event?: any, ...args: any[]) => void) {
|
||||
if (event === '$destroy' && fn) {
|
||||
this.destroyListeners.push(fn);
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
$destroy() {
|
||||
let listener: (() => void)|undefined;
|
||||
while ((listener = this.destroyListeners.shift())) listener();
|
||||
}
|
||||
$apply(exp?: angular.Ng1Expression) {
|
||||
return () => {};
|
||||
}
|
||||
$digest() {
|
||||
return () => {};
|
||||
}
|
||||
$evalAsync(exp: angular.Ng1Expression, locals?: any) {
|
||||
return () => {};
|
||||
}
|
||||
// TODO(issue/24571): remove '!'.
|
||||
$$childTail !: angular.IScope;
|
||||
// TODO(issue/24571): remove '!'.
|
||||
$$childHead !: angular.IScope;
|
||||
// TODO(issue/24571): remove '!'.
|
||||
$$nextSibling !: angular.IScope;
|
||||
[key: string]: any;
|
||||
$id = 'mockScope';
|
||||
// TODO(issue/24571): remove '!'.
|
||||
$parent !: angular.IScope;
|
||||
// TODO(issue/24571): remove '!'.
|
||||
$root !: angular.IScope;
|
||||
}
|
||||
|
||||
function getAdaptor(): DowngradeComponentAdapter {
|
||||
let attrs = undefined as any;
|
||||
let scope: angular.IScope; // mock
|
||||
let ngModel = undefined as any;
|
||||
let parentInjector: Injector; // testbed
|
||||
let $injector = undefined as any;
|
||||
let $compile = undefined as any;
|
||||
let $parse = undefined as any;
|
||||
let componentFactory: ComponentFactory<any>; // testbed
|
||||
let wrapCallback = (cb: any) => cb;
|
||||
|
||||
content = `
|
||||
<h1> new component </h1>
|
||||
<div> a great component </div>
|
||||
<comp></comp>
|
||||
`;
|
||||
element = angular.element(content);
|
||||
scope = new mockScope();
|
||||
|
||||
@Component({
|
||||
selector: 'comp',
|
||||
template: '',
|
||||
})
|
||||
class NewComponent {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
providers: [{provide: 'hello', useValue: 'component'}],
|
||||
declarations: [NewComponent],
|
||||
entryComponents: [NewComponent],
|
||||
})
|
||||
class NewModule {
|
||||
}
|
||||
|
||||
const modFactory = compiler.compileModuleSync(NewModule);
|
||||
const module = modFactory.create(TestBed);
|
||||
componentFactory = module.componentFactoryResolver.resolveComponentFactory(NewComponent) !;
|
||||
parentInjector = TestBed;
|
||||
|
||||
return new DowngradeComponentAdapter(
|
||||
element, attrs, scope, ngModel, parentInjector, $injector, $compile, $parse,
|
||||
componentFactory, wrapCallback);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
compiler = TestBed.get(Compiler);
|
||||
registry = TestBed.get(TestabilityRegistry);
|
||||
adapter = getAdaptor();
|
||||
});
|
||||
beforeEach(() => registry.unregisterAllApplications());
|
||||
afterEach(() => registry.unregisterAllApplications());
|
||||
|
||||
it('should add testabilities hook when creating components', () => {
|
||||
|
||||
let registry = TestBed.get(TestabilityRegistry);
|
||||
adapter.createComponent([]);
|
||||
expect(registry.getAllTestabilities().length).toEqual(1);
|
||||
|
||||
adapter = getAdaptor(); // get a new adaptor to creat a new component
|
||||
adapter.createComponent([]);
|
||||
expect(registry.getAllTestabilities().length).toEqual(2);
|
||||
});
|
||||
|
||||
it('should remove the testability hook when destroy a component', () => {
|
||||
const registry = TestBed.get(TestabilityRegistry);
|
||||
expect(registry.getAllTestabilities().length).toEqual(0);
|
||||
adapter.createComponent([]);
|
||||
expect(registry.getAllTestabilities().length).toEqual(1);
|
||||
adapter.registerCleanup();
|
||||
element.remove !();
|
||||
expect(registry.getAllTestabilities().length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injector} from '@angular/core';
|
||||
import * as angular from '../src/angular1';
|
||||
import {$INJECTOR, INJECTOR_KEY, UPGRADE_APP_TYPE_KEY} from '../src/constants';
|
||||
import {downgradeInjectable} from '../src/downgrade_injectable';
|
||||
import {UpgradeAppType} from '../src/util';
|
||||
|
||||
describe('downgradeInjectable', () => {
|
||||
const setupMockInjectors = (downgradedModule = '') => {
|
||||
const mockNg1Injector = jasmine.createSpyObj<angular.IInjectorService>(['get', 'has']);
|
||||
mockNg1Injector.get.and.callFake((key: string) => mockDependencies[key]);
|
||||
mockNg1Injector.has.and.callFake((key: string) => mockDependencies.hasOwnProperty(key));
|
||||
|
||||
const mockNg2Injector = jasmine.createSpyObj<Injector>(['get']);
|
||||
mockNg2Injector.get.and.returnValue('service value');
|
||||
|
||||
const mockDependencies: {[key: string]: any} = {
|
||||
[UPGRADE_APP_TYPE_KEY]: downgradedModule ? UpgradeAppType.Lite : UpgradeAppType.Static,
|
||||
[`${INJECTOR_KEY}${downgradedModule}`]: mockNg2Injector,
|
||||
};
|
||||
|
||||
return {mockNg1Injector, mockNg2Injector};
|
||||
};
|
||||
|
||||
it('should return an AngularJS annotated factory for the token', () => {
|
||||
const factory = downgradeInjectable('someToken');
|
||||
expect(factory).toEqual(jasmine.any(Function));
|
||||
expect((factory as any).$inject).toEqual([$INJECTOR]);
|
||||
|
||||
const {mockNg1Injector, mockNg2Injector} = setupMockInjectors();
|
||||
expect(factory(mockNg1Injector)).toEqual('service value');
|
||||
expect(mockNg2Injector.get).toHaveBeenCalledWith('someToken');
|
||||
});
|
||||
|
||||
it('should inject the specified module\'s injector when specifying a module name', () => {
|
||||
const factory = downgradeInjectable('someToken', 'someModule');
|
||||
expect(factory).toEqual(jasmine.any(Function));
|
||||
expect((factory as any).$inject).toEqual([$INJECTOR]);
|
||||
|
||||
const {mockNg1Injector, mockNg2Injector} = setupMockInjectors('someModule');
|
||||
expect(factory(mockNg1Injector)).toEqual('service value');
|
||||
expect(mockNg2Injector.get).toHaveBeenCalledWith('someToken');
|
||||
});
|
||||
});
|
14
packages/upgrade/src/common/test/helpers/BUILD.bazel
Normal file
14
packages/upgrade/src/common/test/helpers/BUILD.bazel
Normal file
@ -0,0 +1,14 @@
|
||||
load("//tools:defaults.bzl", "ts_library")
|
||||
|
||||
package(default_visibility = ["//packages/upgrade:__subpackages__"])
|
||||
|
||||
ts_library(
|
||||
name = "helpers",
|
||||
srcs = glob([
|
||||
"*.ts",
|
||||
]),
|
||||
deps = [
|
||||
"//packages/core/testing",
|
||||
"//packages/upgrade/src/common",
|
||||
],
|
||||
)
|
165
packages/upgrade/src/common/test/helpers/common_test_helpers.ts
Normal file
165
packages/upgrade/src/common/test/helpers/common_test_helpers.ts
Normal file
@ -0,0 +1,165 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {setAngularJSGlobal} from '../../src/angular1';
|
||||
|
||||
// Whether the upgrade tests should run against AngularJS minified or not. This can be
|
||||
// temporarily switched to "false" in order to make it easy to debug AngularJS locally.
|
||||
const TEST_MINIFIED = true;
|
||||
const ANGULARJS_FILENAME = TEST_MINIFIED ? 'angular.min.js' : 'angular.js';
|
||||
|
||||
const ng1Versions = [
|
||||
{
|
||||
label: '1.5',
|
||||
files: [`angular-1.5/${ANGULARJS_FILENAME}`, 'angular-mocks-1.5/angular-mocks.js'],
|
||||
},
|
||||
{
|
||||
label: '1.6',
|
||||
files: [`angular-1.6/${ANGULARJS_FILENAME}`, 'angular-mocks-1.6/angular-mocks.js'],
|
||||
},
|
||||
{
|
||||
label: '1.7',
|
||||
files: [`angular/${ANGULARJS_FILENAME}`, 'angular-mocks/angular-mocks.js'],
|
||||
},
|
||||
];
|
||||
|
||||
export function createWithEachNg1VersionFn(setNg1: typeof setAngularJSGlobal) {
|
||||
return (specSuite: () => void) => ng1Versions.forEach(({label, files}) => {
|
||||
describe(`[AngularJS v${label}]`, () => {
|
||||
// Problem:
|
||||
// As soon as `angular-mocks.js` is loaded, it runs `beforeEach` and `afterEach` to register
|
||||
// setup/tear down callbacks. Jasmine 2.9+ does not allow `beforeEach`/`afterEach` to be
|
||||
// nested inside a `beforeAll` call (only inside `describe`).
|
||||
// Hacky work-around:
|
||||
// Patch the affected jasmine methods while loading `angular-mocks.js` (inside `beforeAll`) to
|
||||
// capture the registered callbacks. Also, inside the `describe` call register a callback with
|
||||
// each affected method that runs all captured callbacks.
|
||||
// (Note: Currently, async callbacks are not supported, but that should be OK, since
|
||||
// `angular-mocks.js` does not use them.)
|
||||
const methodsToPatch = ['beforeAll', 'beforeEach', 'afterEach', 'afterAll'];
|
||||
const methodCallbacks = methodsToPatch.reduce<{[name: string]: any[]}>(
|
||||
(aggr, method) => ({...aggr, [method]: []}), {});
|
||||
const win = window as any;
|
||||
|
||||
function patchJasmineMethods(): () => void {
|
||||
const originalMethods: {[name: string]: any} = {};
|
||||
|
||||
methodsToPatch.forEach(method => {
|
||||
originalMethods[method] = win[method];
|
||||
win[method] = (cb: any) => methodCallbacks[method].push(cb);
|
||||
});
|
||||
|
||||
return () => methodsToPatch.forEach(method => win[method] = originalMethods[method]);
|
||||
}
|
||||
|
||||
function loadScript(scriptUrl: string, retry = 0): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.async = true;
|
||||
script.onerror = (retry > 0) ? () => {
|
||||
// Sometimes (especially on mobile browsers on SauceLabs) the script may fail to load
|
||||
// due to a temporary issue with the internet connection. To avoid flakes on CI when
|
||||
// this happens, we retry the download after some delay.
|
||||
const delay = 5000;
|
||||
win.console.warn(
|
||||
`\n[${new Date().toISOString()}] Retrying to load "${scriptUrl}" in ${delay}ms...`);
|
||||
|
||||
document.body.removeChild(script);
|
||||
setTimeout(() => loadScript(scriptUrl, --retry).then(resolve, reject), delay);
|
||||
} : () => {
|
||||
// Whenever the script failed loading, browsers will just pass an "ErrorEvent" which
|
||||
// does not contain useful information on most browsers we run tests against. In order
|
||||
// to avoid writing logic to convert the event into a readable error and since just
|
||||
// passing the event might cause people to spend unnecessary time debugging the
|
||||
// "ErrorEvent", we create a simple error that doesn't imply that there is a lot of
|
||||
// information within the "ErrorEvent".
|
||||
reject(`An error occurred while loading "${scriptUrl}".`);
|
||||
};
|
||||
script.onload = () => {
|
||||
document.body.removeChild(script);
|
||||
resolve();
|
||||
};
|
||||
script.src = `base/npm/node_modules/${scriptUrl}`;
|
||||
document.body.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
beforeAll(done => {
|
||||
const restoreJasmineMethods = patchJasmineMethods();
|
||||
const onSuccess = () => {
|
||||
restoreJasmineMethods();
|
||||
done();
|
||||
};
|
||||
const onError = (err: any) => {
|
||||
restoreJasmineMethods();
|
||||
done.fail(err);
|
||||
};
|
||||
|
||||
// Load AngularJS before running tests.
|
||||
files.reduce((prev, file) => prev.then(() => loadScript(file, 1)), Promise.resolve())
|
||||
.then(() => setNg1(win.angular))
|
||||
.then(onSuccess, onError);
|
||||
|
||||
// When Saucelabs is flaky, some browsers (esp. mobile) take some time to load and execute
|
||||
// the AngularJS scripts. Specifying a higher timeout here, reduces flaky-ness.
|
||||
}, 60000);
|
||||
|
||||
afterAll(() => {
|
||||
// `win.angular` will not be defined if loading the script in `berofeAll()` failed. In that
|
||||
// case, avoid causing another error in `afterAll()`, because the reporter only shows the
|
||||
// most recent error (thus hiding the original, possibly more informative, error message).
|
||||
if (win.angular) {
|
||||
// In these tests we are loading different versions of AngularJS on the same window.
|
||||
// AngularJS leaves an "expandoId" property on `document`, which can trick subsequent
|
||||
// `window.angular` instances into believing an app is already bootstrapped.
|
||||
win.angular.element.cleanData([document]);
|
||||
}
|
||||
|
||||
// Remove AngularJS to leave a clean state for subsequent tests.
|
||||
setNg1(undefined);
|
||||
delete win.angular;
|
||||
});
|
||||
|
||||
methodsToPatch.forEach(method => win[method](function() {
|
||||
// Run the captured callbacks. (Async callbacks not supported.)
|
||||
methodCallbacks[method].forEach(cb => cb.call(this));
|
||||
}));
|
||||
|
||||
specSuite();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function html(html: string): Element {
|
||||
// Don't return `body` itself, because using it as a `$rootElement` for ng1
|
||||
// will attach `$injector` to it and that will affect subsequent tests.
|
||||
const body = document.body;
|
||||
body.innerHTML = `<div>${html.trim()}</div>`;
|
||||
const div = document.body.firstChild as Element;
|
||||
|
||||
if (div.childNodes.length === 1 && div.firstChild instanceof HTMLElement) {
|
||||
return div.firstChild;
|
||||
}
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
export function multiTrim(text: string | null | undefined, allSpace = false): string {
|
||||
if (typeof text == 'string') {
|
||||
const repl = allSpace ? '' : ' ';
|
||||
return text.replace(/\n/g, '').replace(/\s+/g, repl).trim();
|
||||
}
|
||||
throw new Error('Argument can not be undefined.');
|
||||
}
|
||||
|
||||
export function nodes(html: string) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html.trim();
|
||||
return Array.prototype.slice.call(div.childNodes);
|
||||
}
|
||||
|
||||
export const withEachNg1Version = createWithEachNg1VersionFn(setAngularJSGlobal);
|
Reference in New Issue
Block a user