Isolated exec tests (#11531)
* Run exec tests in fresh contexts * Reevaluate modules in every context * Cache module code when running tests * Eliminate weakmap accesses as much as possible * Remove old multiline usage * Using bundled polyfill to significantly increase performance The individual requires for each file were the part that was sooooo slow. * Drop LRU cache size * Fixes * Fix test Co-authored-by: Huáng Jùnliàng <jlhwung@gmail.com>
This commit is contained in:
parent
3bff1ce35a
commit
a5bc48661b
@ -1,8 +1,8 @@
|
|||||||
var code = multiline([
|
var code = `
|
||||||
"function foo() {",
|
function foo() {
|
||||||
" var a = a ? a : a;",
|
var a = a ? a : a;
|
||||||
"}",
|
}
|
||||||
]);
|
`;
|
||||||
|
|
||||||
transform(code, {
|
transform(code, {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
var code = multiline([
|
var code = `
|
||||||
"(function() {",
|
(function() {
|
||||||
" var bar = 'lol';",
|
var bar = 'lol';
|
||||||
" function foo(b){",
|
function foo(b){
|
||||||
" b === bar;",
|
b === bar
|
||||||
" foo(b);",
|
foo(b);
|
||||||
" }",
|
}
|
||||||
"})();",
|
})();
|
||||||
]);
|
`;
|
||||||
|
|
||||||
transform(code, {
|
transform(code, {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
"jest": "^24.8.0",
|
"jest": "^24.8.0",
|
||||||
"jest-diff": "^24.8.0",
|
"jest-diff": "^24.8.0",
|
||||||
"lodash": "^4.17.19",
|
"lodash": "^4.17.19",
|
||||||
|
"quick-lru": "5.1.0",
|
||||||
"resolve": "^1.3.2",
|
"resolve": "^1.3.2",
|
||||||
"source-map": "^0.5.0"
|
"source-map": "^0.5.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,34 +14,100 @@ import fs from "fs";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import vm from "vm";
|
import vm from "vm";
|
||||||
import checkDuplicatedNodes from "babel-check-duplicated-nodes";
|
import checkDuplicatedNodes from "babel-check-duplicated-nodes";
|
||||||
|
import QuickLRU from "quick-lru";
|
||||||
|
|
||||||
import diff from "jest-diff";
|
import diff from "jest-diff";
|
||||||
|
|
||||||
const moduleCache = {};
|
const cachedScripts = new QuickLRU({ maxSize: 10 });
|
||||||
const testContext = vm.createContext({
|
const contextModuleCache = new WeakMap();
|
||||||
...helpers,
|
const sharedTestContext = createContext();
|
||||||
process: process,
|
|
||||||
transform: babel.transform,
|
|
||||||
setTimeout: setTimeout,
|
|
||||||
setImmediate: setImmediate,
|
|
||||||
expect,
|
|
||||||
});
|
|
||||||
testContext.global = testContext;
|
|
||||||
|
|
||||||
// Initialize the test context with the polyfill, and then freeze the global to prevent implicit
|
function createContext() {
|
||||||
// global creation in tests, which could cause things to bleed between tests.
|
const context = vm.createContext({
|
||||||
runModuleInTestContext("@babel/polyfill", __filename);
|
...helpers,
|
||||||
|
process: process,
|
||||||
|
transform: babel.transform,
|
||||||
|
setTimeout: setTimeout,
|
||||||
|
setImmediate: setImmediate,
|
||||||
|
expect,
|
||||||
|
});
|
||||||
|
context.global = context;
|
||||||
|
|
||||||
// Populate the "babelHelpers" global with Babel's helper utilities.
|
const moduleCache = Object.create(null);
|
||||||
runCodeInTestContext(buildExternalHelpers(), {
|
contextModuleCache.set(context, moduleCache);
|
||||||
filename: path.join(__dirname, "babel-helpers-in-memory.js"),
|
|
||||||
});
|
// Initialize the test context with the polyfill, and then freeze the global to prevent implicit
|
||||||
|
// global creation in tests, which could cause things to bleed between tests.
|
||||||
|
runModuleInTestContext(
|
||||||
|
"@babel/polyfill/dist/polyfill.min",
|
||||||
|
__filename,
|
||||||
|
context,
|
||||||
|
moduleCache,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Populate the "babelHelpers" global with Babel's helper utilities.
|
||||||
|
runCacheableScriptInTestContext(
|
||||||
|
path.join(__dirname, "babel-helpers-in-memory.js"),
|
||||||
|
buildExternalHelpers,
|
||||||
|
context,
|
||||||
|
moduleCache,
|
||||||
|
);
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runCacheableScriptInTestContext(
|
||||||
|
filename: string,
|
||||||
|
srcFn: () => string,
|
||||||
|
context: Context,
|
||||||
|
moduleCache: Object,
|
||||||
|
) {
|
||||||
|
let cached = cachedScripts.get(filename);
|
||||||
|
if (!cached) {
|
||||||
|
const code = `(function (exports, require, module, __filename, __dirname) {\n${srcFn()}\n});`;
|
||||||
|
cached = {
|
||||||
|
code,
|
||||||
|
cachedData: undefined,
|
||||||
|
};
|
||||||
|
cachedScripts.set(filename, cached);
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = new vm.Script(cached.code, {
|
||||||
|
filename,
|
||||||
|
displayErrors: true,
|
||||||
|
lineOffset: -1,
|
||||||
|
cachedData: cached.cachedData,
|
||||||
|
produceCachedData: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (script.cachedDataProduced) {
|
||||||
|
cached.cachedData = script.cachedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const module = {
|
||||||
|
id: filename,
|
||||||
|
exports: {},
|
||||||
|
};
|
||||||
|
const req = id => runModuleInTestContext(id, filename, context, moduleCache);
|
||||||
|
const dirname = path.dirname(filename);
|
||||||
|
|
||||||
|
script
|
||||||
|
.runInContext(context)
|
||||||
|
.call(module.exports, module.exports, req, module, filename, dirname);
|
||||||
|
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A basic implementation of CommonJS so we can execute `@babel/polyfill` inside our test context.
|
* A basic implementation of CommonJS so we can execute `@babel/polyfill` inside our test context.
|
||||||
* This allows us to run our unittests
|
* This allows us to run our unittests
|
||||||
*/
|
*/
|
||||||
function runModuleInTestContext(id: string, relativeFilename: string) {
|
function runModuleInTestContext(
|
||||||
|
id: string,
|
||||||
|
relativeFilename: string,
|
||||||
|
context: Context,
|
||||||
|
moduleCache: Object,
|
||||||
|
) {
|
||||||
const filename = resolve.sync(id, {
|
const filename = resolve.sync(id, {
|
||||||
basedir: path.dirname(relativeFilename),
|
basedir: path.dirname(relativeFilename),
|
||||||
});
|
});
|
||||||
@ -50,23 +116,17 @@ function runModuleInTestContext(id: string, relativeFilename: string) {
|
|||||||
// the context's global scope.
|
// the context's global scope.
|
||||||
if (filename === id) return require(id);
|
if (filename === id) return require(id);
|
||||||
|
|
||||||
|
// Modules can only evaluate once per context, so the moduleCache is a
|
||||||
|
// stronger cache guarantee than the LRU's Script cache.
|
||||||
if (moduleCache[filename]) return moduleCache[filename].exports;
|
if (moduleCache[filename]) return moduleCache[filename].exports;
|
||||||
|
|
||||||
const module = (moduleCache[filename] = {
|
const module = runCacheableScriptInTestContext(
|
||||||
id: filename,
|
|
||||||
exports: {},
|
|
||||||
});
|
|
||||||
const dirname = path.dirname(filename);
|
|
||||||
const req = id => runModuleInTestContext(id, filename);
|
|
||||||
|
|
||||||
const src = fs.readFileSync(filename, "utf8");
|
|
||||||
const code = `(function (exports, require, module, __filename, __dirname) {\n${src}\n});`;
|
|
||||||
|
|
||||||
vm.runInContext(code, testContext, {
|
|
||||||
filename,
|
filename,
|
||||||
displayErrors: true,
|
() => fs.readFileSync(filename, "utf8"),
|
||||||
lineOffset: -1,
|
context,
|
||||||
}).call(module.exports, module.exports, req, module, filename, dirname);
|
moduleCache,
|
||||||
|
);
|
||||||
|
moduleCache[filename] = module;
|
||||||
|
|
||||||
return module.exports;
|
return module.exports;
|
||||||
}
|
}
|
||||||
@ -76,10 +136,15 @@ function runModuleInTestContext(id: string, relativeFilename: string) {
|
|||||||
*
|
*
|
||||||
* Exposed for unit tests, not for use as an API.
|
* Exposed for unit tests, not for use as an API.
|
||||||
*/
|
*/
|
||||||
export function runCodeInTestContext(code: string, opts: { filename: string }) {
|
export function runCodeInTestContext(
|
||||||
|
code: string,
|
||||||
|
opts: { filename: string },
|
||||||
|
context = sharedTestContext,
|
||||||
|
) {
|
||||||
const filename = opts.filename;
|
const filename = opts.filename;
|
||||||
const dirname = path.dirname(filename);
|
const dirname = path.dirname(filename);
|
||||||
const req = id => runModuleInTestContext(id, filename);
|
const moduleCache = contextModuleCache.get(context);
|
||||||
|
const req = id => runModuleInTestContext(id, filename, context, moduleCache);
|
||||||
|
|
||||||
const module = {
|
const module = {
|
||||||
id: filename,
|
id: filename,
|
||||||
@ -94,7 +159,7 @@ export function runCodeInTestContext(code: string, opts: { filename: string }) {
|
|||||||
// Note: This isn't doing .call(module.exports, ...) because some of our tests currently
|
// Note: This isn't doing .call(module.exports, ...) because some of our tests currently
|
||||||
// rely on 'this === global'.
|
// rely on 'this === global'.
|
||||||
const src = `(function(exports, require, module, __filename, __dirname, opts) {\n${code}\n});`;
|
const src = `(function(exports, require, module, __filename, __dirname, opts) {\n${code}\n});`;
|
||||||
return vm.runInContext(src, testContext, {
|
return vm.runInContext(src, context, {
|
||||||
filename,
|
filename,
|
||||||
displayErrors: true,
|
displayErrors: true,
|
||||||
lineOffset: -1,
|
lineOffset: -1,
|
||||||
@ -183,13 +248,14 @@ function run(task) {
|
|||||||
let resultExec;
|
let resultExec;
|
||||||
|
|
||||||
if (execCode) {
|
if (execCode) {
|
||||||
|
const context = createContext();
|
||||||
const execOpts = getOpts(exec);
|
const execOpts = getOpts(exec);
|
||||||
result = babel.transform(execCode, execOpts);
|
result = babel.transform(execCode, execOpts);
|
||||||
checkDuplicatedNodes(babel, result.ast);
|
checkDuplicatedNodes(babel, result.ast);
|
||||||
execCode = result.code;
|
execCode = result.code;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
resultExec = runCodeInTestContext(execCode, execOpts);
|
resultExec = runCodeInTestContext(execCode, execOpts, context);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Pass empty location to include the whole file in the output.
|
// Pass empty location to include the whole file in the output.
|
||||||
err.message =
|
err.message =
|
||||||
|
|||||||
@ -1,27 +1,25 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
const NOSET = `NOSET${__filename}`;
|
|
||||||
const NOWRITE = `NOWRITE${__filename}`;
|
|
||||||
|
|
||||||
Object.defineProperty(Object.prototype, NOSET, {
|
Object.defineProperty(Object.prototype, 'NOSET', {
|
||||||
get(value) {
|
get(value) {
|
||||||
// noop
|
// noop
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(Object.prototype, NOWRITE, {
|
Object.defineProperty(Object.prototype, 'NOWRITE', {
|
||||||
writable: false,
|
writable: false,
|
||||||
value: 'abc',
|
value: 'abc',
|
||||||
});
|
});
|
||||||
|
|
||||||
const obj = { [NOSET]: 123 };
|
const obj = { 'NOSET': 123 };
|
||||||
// this won't work as expected if transformed as Object.assign (or equivalent)
|
// this won't work as expected if transformed as Object.assign (or equivalent)
|
||||||
// because those trigger object setters (spread don't)
|
// because those trigger object setters (spread don't)
|
||||||
expect(() => {
|
expect(() => {
|
||||||
const objSpread = { ...obj };
|
const objSpread = { ...obj };
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
|
|
||||||
const obj2 = { [NOWRITE]: 456 };
|
const obj2 = { 'NOWRITE': 456 };
|
||||||
// this throws `TypeError: Cannot assign to read only property 'NOWRITE'`
|
// this throws `TypeError: Cannot assign to read only property 'NOWRITE'`
|
||||||
// if transformed as Object.assign (or equivalent) because those use *assignment* for creating properties
|
// if transformed as Object.assign (or equivalent) because those use *assignment* for creating properties
|
||||||
// (spread defines them)
|
// (spread defines them)
|
||||||
expect(() => {
|
expect(() => {
|
||||||
|
|||||||
@ -1,27 +1,24 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
const NOSET = `NOSET${__filename}`;
|
Object.defineProperty(Object.prototype, 'NOSET', {
|
||||||
const NOWRITE = `NOWRITE${__filename}`;
|
|
||||||
|
|
||||||
Object.defineProperty(Object.prototype, NOSET, {
|
|
||||||
get(value) {
|
get(value) {
|
||||||
// noop
|
// noop
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(Object.prototype, NOWRITE, {
|
Object.defineProperty(Object.prototype, 'NOWRITE', {
|
||||||
writable: false,
|
writable: false,
|
||||||
value: 'abc',
|
value: 'abc',
|
||||||
});
|
});
|
||||||
|
|
||||||
const obj = { [NOSET]: 123 };
|
const obj = { 'NOSET': 123 };
|
||||||
// this won't work as expected if transformed as Object.assign (or equivalent)
|
// this won't work as expected if transformed as Object.assign (or equivalent)
|
||||||
// because those trigger object setters (spread don't)
|
// because those trigger object setters (spread don't)
|
||||||
expect(() => {
|
expect(() => {
|
||||||
const objSpread = { ...obj };
|
const objSpread = { ...obj };
|
||||||
}).toThrow();
|
}).toThrow();
|
||||||
|
|
||||||
const obj2 = { [NOWRITE]: 456 };
|
const obj2 = { 'NOWRITE': 456 };
|
||||||
// this throws `TypeError: Cannot assign to read only property 'NOWRITE'`
|
// this throws `TypeError: Cannot assign to read only property 'NOWRITE'`
|
||||||
// if transformed as Object.assign (or equivalent) because those use *assignment* for creating properties
|
// if transformed as Object.assign (or equivalent) because those use *assignment* for creating properties
|
||||||
// (spread defines them)
|
// (spread defines them)
|
||||||
expect(() => {
|
expect(() => {
|
||||||
|
|||||||
@ -1,31 +1,27 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
const NOSET = `NOSET${__filename}`;
|
Object.defineProperty(Object.prototype, 'NOSET', {
|
||||||
const NOWRITE = `NOWRITE${__filename}`;
|
get(value) {
|
||||||
|
|
||||||
Object.defineProperty(Object.prototype, NOSET, {
|
|
||||||
set(value) {
|
|
||||||
// noop
|
// noop
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(Object.prototype, NOWRITE, {
|
Object.defineProperty(Object.prototype, 'NOWRITE', {
|
||||||
writable: false,
|
writable: false,
|
||||||
value: 'abc',
|
value: 'abc',
|
||||||
});
|
});
|
||||||
|
|
||||||
const obj = { [NOSET]: 123 };
|
const obj = { NOSET: 123 };
|
||||||
// this wouldn't work as expected if transformed as Object.assign (or equivalent)
|
// this wouldn't work as expected if transformed as Object.assign (or equivalent)
|
||||||
// because those trigger object setters (spread don't)
|
// because those trigger object setters (spread don't)
|
||||||
const objSpread = { ...obj };
|
const objSpread = { ...obj };
|
||||||
|
expect(objSpread).toHaveProperty('NOSET', 123);
|
||||||
|
|
||||||
const obj2 = { NOSET: 123, [NOWRITE]: 456 };
|
const obj2 = { NOWRITE: 456 };
|
||||||
// this line would throw `TypeError: Cannot assign to read only property 'NOWRITE'`
|
// this line would throw `TypeError: Cannot assign to read only property 'NOWRITE'`
|
||||||
// if transformed as Object.assign (or equivalent) because those use *assignment* for creating properties
|
// if transformed as Object.assign (or equivalent) because those use *assignment* for creating properties
|
||||||
// (spread defines them)
|
// (spread defines them)
|
||||||
const obj2Spread = { ...obj2 };
|
const obj2Spread = { ...obj2 };
|
||||||
|
expect(obj2Spread).toHaveProperty('NOWRITE', 456);
|
||||||
expect(objSpread).toEqual(obj);
|
|
||||||
expect(obj2Spread).toEqual(obj2);
|
|
||||||
|
|
||||||
const KEY = Symbol('key');
|
const KEY = Symbol('key');
|
||||||
const obj3Spread = { ...{ get foo () { return 'bar' } }, [KEY]: 'symbol' };
|
const obj3Spread = { ...{ get foo () { return 'bar' } }, [KEY]: 'symbol' };
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
const code = multiline([
|
const code = `
|
||||||
"for (const {foo, ...bar} of { bar: [] }) {",
|
for (const {foo, ...bar} of { bar: [] }) {
|
||||||
"() => foo;",
|
() => foo;
|
||||||
"const [qux] = bar;",
|
const [qux] = bar;
|
||||||
"try {} catch (e) {",
|
try {} catch (e) {
|
||||||
"let quux = qux;",
|
let quux = qux;
|
||||||
"}",
|
}
|
||||||
"}"
|
}
|
||||||
]);
|
`;
|
||||||
|
|
||||||
let programPath;
|
let programPath;
|
||||||
let forOfPath;
|
let forOfPath;
|
||||||
|
|||||||
@ -1,26 +1,15 @@
|
|||||||
const oldReflect = this.Reflect;
|
// Pretend that `Reflect.construct` isn't supported.
|
||||||
const oldHTMLElement = this.HTMLElement;
|
global.Reflect = undefined;
|
||||||
|
|
||||||
try {
|
global.HTMLElement = function() {
|
||||||
// Pretend that `Reflect.construct` isn't supported.
|
// Here, `this.HTMLElement` is this function, not the original HTMLElement
|
||||||
this.Reflect = undefined;
|
// constructor. `this.constructor` should be this function too, but isn't.
|
||||||
|
constructor = this.constructor;
|
||||||
|
};
|
||||||
|
|
||||||
this.HTMLElement = function() {
|
var constructor;
|
||||||
// Here, `this.HTMLElement` is this function, not the original HTMLElement
|
|
||||||
// constructor. `this.constructor` should be this function too, but isn't.
|
|
||||||
constructor = this.constructor;
|
|
||||||
};
|
|
||||||
|
|
||||||
var constructor;
|
|
||||||
|
|
||||||
class CustomElement extends HTMLElement {};
|
|
||||||
new CustomElement();
|
|
||||||
|
|
||||||
expect(constructor).toBe(CustomElement);
|
|
||||||
} finally {
|
|
||||||
// Restore original env
|
|
||||||
this.Reflect = oldReflect;
|
|
||||||
this.HTMLElement = oldHTMLElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
class CustomElement extends HTMLElement {}
|
||||||
|
new CustomElement();
|
||||||
|
|
||||||
|
expect(constructor).toBe(CustomElement);
|
||||||
|
|||||||
@ -1,13 +1,9 @@
|
|||||||
var a = (() => [1, 2, 3])();
|
var a = (() => [1, 2, 3])();
|
||||||
|
|
||||||
// Simulate old environment
|
// Simulate old environment
|
||||||
let _Symbol = Symbol;
|
global.Symbol = void 0;
|
||||||
Symbol = void 0;
|
|
||||||
try {
|
|
||||||
var [first, ...rest] = a;
|
|
||||||
|
|
||||||
expect(first).toBe(1);
|
var [first, ...rest] = a;
|
||||||
expect(rest).toEqual([2, 3]);
|
|
||||||
} finally {
|
expect(first).toBe(1);
|
||||||
Symbol = _Symbol;
|
expect(rest).toEqual([2, 3]);
|
||||||
}
|
|
||||||
|
|||||||
@ -8,15 +8,8 @@ expect(
|
|||||||
() => [foo, bar] = {}
|
() => [foo, bar] = {}
|
||||||
).toThrow(/destructure non-iterable/);
|
).toThrow(/destructure non-iterable/);
|
||||||
|
|
||||||
// Simulate old browser
|
global.Symbol = void 0;
|
||||||
let _Symbol = Symbol;
|
|
||||||
Symbol = void 0;
|
|
||||||
try {
|
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
() => [foo, bar] = {}
|
() => [foo, bar] = {}
|
||||||
).toThrow(/destructure non-iterable/);
|
).toThrow(/destructure non-iterable/);
|
||||||
|
|
||||||
} finally {
|
|
||||||
Symbol = _Symbol;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,13 +3,13 @@ var actual = transform(
|
|||||||
Object.assign({}, opts, { filename: '/fake/path/mock.js' })
|
Object.assign({}, opts, { filename: '/fake/path/mock.js' })
|
||||||
).code;
|
).code;
|
||||||
|
|
||||||
var expected = multiline([
|
var expected = `
|
||||||
'var _jsxFileName = "/fake/path/mock.js";',
|
var _jsxFileName = "/fake/path/mock.js";
|
||||||
'var x = <sometag __source={{',
|
var x = <sometag __source={{
|
||||||
' fileName: _jsxFileName,',
|
fileName: _jsxFileName,
|
||||||
' lineNumber: 1,',
|
lineNumber: 1,
|
||||||
' columnNumber: 9',
|
columnNumber: 9
|
||||||
'}} />;',
|
}} />;
|
||||||
]);
|
`.trim();
|
||||||
|
|
||||||
expect(actual).toBe(expected);
|
expect(actual).toBe(expected);
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
var a = (() => [2, 3])();
|
var a = (() => [2, 3])();
|
||||||
|
|
||||||
// Simulate old environment
|
// Simulate old environment
|
||||||
let _Symbol = Symbol;
|
global.Symbol = void 0;
|
||||||
Symbol = void 0;
|
|
||||||
try {
|
expect([1, ...a]).toEqual([1, 2, 3]);
|
||||||
expect([1, ...a]).toEqual([1, 2, 3]);
|
|
||||||
} finally {
|
|
||||||
Symbol = _Symbol;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -5,6 +5,6 @@ expect(() => [...undefined]).toThrow(/spread non-iterable/);
|
|||||||
expect(() => [...o]).toThrow(/spread non-iterable/);
|
expect(() => [...o]).toThrow(/spread non-iterable/);
|
||||||
|
|
||||||
// Simulate old browser
|
// Simulate old browser
|
||||||
Symbol = void 0;
|
global.Symbol = void 0;
|
||||||
|
|
||||||
expect(() => [...o]).toThrow(/spread non-iterable/);
|
expect(() => [...o]).toThrow(/spread non-iterable/);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user