UNPKG

@dcloudio/uni-debugger

Version:

uni-app debugger

1,433 lines (1,297 loc) 42.2 kB
// Copyright 2016 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /** * @fileoverview using private properties isn't a Closure violation in tests. * @suppress {accessControls} */ /* eslint-disable no-console */ /** @type {!{logToStderr: function(), navigateSecondaryWindow: function(string), notifyDone: function()}|undefined} */ self.testRunner; /** * Only tests in /LayoutTests/http/tests/devtools/startup/ need to call * this method because these tests need certain activities to be exercised * in the inspected page prior to the DevTools session. * @param {string} path * @return {!Promise<undefined>} */ TestRunner.setupStartupTest = function(path) { const absoluteURL = TestRunner.url(path); self.testRunner.navigateSecondaryWindow(absoluteURL); return new Promise(f => TestRunner._startupTestSetupFinished = () => { TestRunner._initializeTargetForStartupTest(); delete TestRunner._startupTestSetupFinished; f(); }); }; TestRunner._executeTestScript = function() { const testScriptURL = /** @type {string} */ (Runtime.queryParam('test')); fetch(testScriptURL) .then(data => data.text()) .then(testScript => { if (TestRunner._isDebugTest()) { TestRunner.addResult = console.log; TestRunner.completeTest = () => console.log('Test completed'); // Auto-start unit tests if (!self.testRunner) eval(`(function test(){${testScript}})()\n//# sourceURL=${testScriptURL}`); else self.eval(`function test(){${testScript}}\n//# sourceURL=${testScriptURL}`); return; } // Convert the test script into an expression (if needed) testScript = testScript.trimRight(); if (testScript.endsWith(';')) testScript = testScript.slice(0, testScript.length - 1); (async function() { try { await eval(testScript + `\n//# sourceURL=${testScriptURL}`); } catch (err) { TestRunner.addResult('TEST ENDED EARLY DUE TO UNCAUGHT ERROR:'); TestRunner.addResult(err && err.stack || err); TestRunner.addResult('=== DO NOT COMMIT THIS INTO -expected.txt ==='); TestRunner.completeTest(); } })(); }) .catch(error => { TestRunner.addResult(`Unable to execute test script because of error: ${error}`); TestRunner.completeTest(); }); }; /** @type {!Array<string>} */ TestRunner._results = []; TestRunner.completeTest = function() { TestRunner.flushResults(); self.testRunner.notifyDone(); }; /** * @suppressGlobalPropertiesCheck */ TestRunner.flushResults = function() { Array.prototype.forEach.call(document.documentElement.childNodes, x => x.remove()); const outputElement = document.createElement('div'); // Support for svg - add to document, not body, check for style. if (outputElement.style) { outputElement.style.whiteSpace = 'pre'; outputElement.style.height = '10px'; outputElement.style.overflow = 'hidden'; } document.documentElement.appendChild(outputElement); for (let i = 0; i < TestRunner._results.length; i++) { outputElement.appendChild(document.createTextNode(TestRunner._results[i])); outputElement.appendChild(document.createElement('br')); } TestRunner._results = []; }; /** * @param {*} text */ TestRunner.addResult = function(text) { TestRunner._results.push(String(text)); }; /** * @param {!Array<string>} textArray */ TestRunner.addResults = function(textArray) { if (!textArray) return; for (let i = 0, size = textArray.length; i < size; ++i) TestRunner.addResult(textArray[i]); }; /** * @param {!Array<function()>} tests */ TestRunner.runTests = function(tests) { nextTest(); function nextTest() { const test = tests.shift(); if (!test) { TestRunner.completeTest(); return; } TestRunner.addResult('\ntest: ' + test.name); let testPromise = test(); if (!(testPromise instanceof Promise)) testPromise = Promise.resolve(); testPromise.then(nextTest); } }; /** * @param {!Object} receiver * @param {string} methodName * @param {!Function} override * @param {boolean=} opt_sticky */ TestRunner.addSniffer = function(receiver, methodName, override, opt_sticky) { override = TestRunner.safeWrap(override); const original = receiver[methodName]; if (typeof original !== 'function') throw new Error('Cannot find method to override: ' + methodName); receiver[methodName] = function(var_args) { let result; try { result = original.apply(this, arguments); } finally { if (!opt_sticky) receiver[methodName] = original; } // In case of exception the override won't be called. try { Array.prototype.push.call(arguments, result); override.apply(this, arguments); } catch (e) { throw new Error('Exception in overriden method \'' + methodName + '\': ' + e); } return result; }; }; /** * @param {!Object} receiver * @param {string} methodName * @return {!Promise<*>} */ TestRunner.addSnifferPromise = function(receiver, methodName) { return new Promise(function(resolve, reject) { const original = receiver[methodName]; if (typeof original !== 'function') { reject('Cannot find method to override: ' + methodName); return; } receiver[methodName] = function(var_args) { let result; try { result = original.apply(this, arguments); } finally { receiver[methodName] = original; } // In case of exception the override won't be called. try { Array.prototype.push.call(arguments, result); resolve.apply(this, arguments); } catch (e) { reject('Exception in overridden method \'' + methodName + '\': ' + e); TestRunner.completeTest(); } return result; }; }); }; /** @type {function():void} */ TestRunner._resolveOnFinishInits; /** * @param {string} module * @return {!Promise<undefined>} */ TestRunner.loadModule = async function(module) { const promise = new Promise(resolve => TestRunner._resolveOnFinishInits = resolve); await self.runtime.loadModulePromise(module); if (!TestRunner._pendingInits) return; return promise; }; /** * @param {string} panel * @return {!Promise.<?UI.Panel>} */ TestRunner.showPanel = function(panel) { return UI.viewManager.showView(panel); }; /** * @param {string} key * @param {boolean=} ctrlKey * @param {boolean=} altKey * @param {boolean=} shiftKey * @param {boolean=} metaKey * @return {!KeyboardEvent} */ TestRunner.createKeyEvent = function(key, ctrlKey, altKey, shiftKey, metaKey) { return new KeyboardEvent('keydown', { key: key, bubbles: true, cancelable: true, ctrlKey: !!ctrlKey, altKey: !!altKey, shiftKey: !!shiftKey, metaKey: !!metaKey }); }; /** * @param {!Function|undefined} func * @param {!Function=} onexception * @return {!Function} */ TestRunner.safeWrap = function(func, onexception) { /** * @this {*} */ function result() { if (!func) return; const wrapThis = this; try { return func.apply(wrapThis, arguments); } catch (e) { TestRunner.addResult('Exception while running: ' + func + '\n' + (e.stack || e)); if (onexception) TestRunner.safeWrap(onexception)(); else TestRunner.completeTest(); } } return result; }; /** * @param {!Node} node * @return {string} */ TestRunner.textContentWithLineBreaks = function(node) { function padding(currentNode) { let result = 0; while (currentNode && currentNode !== node) { if (currentNode.nodeName === 'OL' && !(currentNode.classList && currentNode.classList.contains('object-properties-section'))) ++result; currentNode = currentNode.parentNode; } return Array(result * 4 + 1).join(' '); } let buffer = ''; let currentNode = node; let ignoreFirst = false; while (currentNode.traverseNextNode(node)) { currentNode = currentNode.traverseNextNode(node); if (currentNode.nodeType === Node.TEXT_NODE) { buffer += currentNode.nodeValue; } else if (currentNode.nodeName === 'LI' || currentNode.nodeName === 'TR') { if (!ignoreFirst) buffer += '\n' + padding(currentNode); else ignoreFirst = false; } else if (currentNode.nodeName === 'STYLE') { currentNode = currentNode.traverseNextNode(node); continue; } else if (currentNode.classList && currentNode.classList.contains('object-properties-section')) { ignoreFirst = true; } } return buffer; }; /** * @param {!Node} node * @return {string} */ TestRunner.textContentWithoutStyles = function(node) { let buffer = ''; let currentNode = node; while (currentNode.traverseNextNode(node)) { currentNode = currentNode.traverseNextNode(node); if (currentNode.nodeType === Node.TEXT_NODE) buffer += currentNode.nodeValue; else if (currentNode.nodeName === 'STYLE') currentNode = currentNode.traverseNextNode(node); } return buffer; }; /** * @param {!SDK.Target} target */ TestRunner._setupTestHelpers = function(target) { TestRunner.CSSAgent = target.cssAgent(); TestRunner.DeviceOrientationAgent = target.deviceOrientationAgent(); TestRunner.DOMAgent = target.domAgent(); TestRunner.DOMDebuggerAgent = target.domdebuggerAgent(); TestRunner.DebuggerAgent = target.debuggerAgent(); TestRunner.EmulationAgent = target.emulationAgent(); TestRunner.HeapProfilerAgent = target.heapProfilerAgent(); TestRunner.InspectorAgent = target.inspectorAgent(); TestRunner.NetworkAgent = target.networkAgent(); TestRunner.OverlayAgent = target.overlayAgent(); TestRunner.PageAgent = target.pageAgent(); TestRunner.ProfilerAgent = target.profilerAgent(); TestRunner.RuntimeAgent = target.runtimeAgent(); TestRunner.TargetAgent = target.targetAgent(); TestRunner.networkManager = target.model(SDK.NetworkManager); TestRunner.securityOriginManager = target.model(SDK.SecurityOriginManager); TestRunner.resourceTreeModel = target.model(SDK.ResourceTreeModel); TestRunner.debuggerModel = target.model(SDK.DebuggerModel); TestRunner.runtimeModel = target.model(SDK.RuntimeModel); TestRunner.domModel = target.model(SDK.DOMModel); TestRunner.domDebuggerModel = target.model(SDK.DOMDebuggerModel); TestRunner.cssModel = target.model(SDK.CSSModel); TestRunner.cpuProfilerModel = target.model(SDK.CPUProfilerModel); TestRunner.overlayModel = target.model(SDK.OverlayModel); TestRunner.serviceWorkerManager = target.model(SDK.ServiceWorkerManager); TestRunner.tracingManager = target.model(SDK.TracingManager); TestRunner.mainTarget = target; }; /** * @param {string} code * @return {!Promise<*>} */ TestRunner.evaluateInPageRemoteObject = async function(code) { const response = await TestRunner._evaluateInPage(code); return TestRunner.runtimeModel.createRemoteObject(response.result); }; /** * @param {string} code * @param {function(*, !Protocol.Runtime.ExceptionDetails=):void} callback */ TestRunner.evaluateInPage = async function(code, callback) { const response = await TestRunner._evaluateInPage(code); TestRunner.safeWrap(callback)(response.result.value, response.exceptionDetails); }; /** @type {number} */ TestRunner._evaluateInPageCounter = 0; /** * @param {string} code * @return {!Promise<{response: !SDK.RemoteObject, * exceptionDetails: (!Protocol.Runtime.ExceptionDetails|undefined)}>} */ TestRunner._evaluateInPage = async function(code) { const lines = new Error().stack.split('at '); // Handles cases where the function is safe wrapped const testScriptURL = /** @type {string} */ (Runtime.queryParam('test')); const functionLine = lines.reduce((acc, line) => line.includes(testScriptURL) ? line : acc, lines[lines.length - 2]); const components = functionLine.trim().split('/'); const source = components[components.length - 1].slice(0, -1).split(':'); const fileName = source[0]; const sourceURL = `test://evaluations/${TestRunner._evaluateInPageCounter++}/` + fileName; const lineOffset = parseInt(source[1], 10); code = '\n'.repeat(lineOffset - 1) + code; if (code.indexOf('sourceURL=') === -1) code += `//# sourceURL=${sourceURL}`; const response = await TestRunner.RuntimeAgent.invoke_evaluate({expression: code, objectGroup: 'console'}); const error = response[Protocol.Error]; if (error) { TestRunner.addResult('Error: ' + error); TestRunner.completeTest(); return; } return response; }; /** * Doesn't append sourceURL to snippets evaluated in inspected page * to avoid churning test expectations * @param {string} code * @return {!Promise<*>} */ TestRunner.evaluateInPageAnonymously = async function(code) { const response = await TestRunner.RuntimeAgent.invoke_evaluate({expression: code, objectGroup: 'console'}); if (!response[Protocol.Error]) return response.result.value; TestRunner.addResult( 'Error: ' + (response.exceptionDetails && response.exceptionDetails.text || 'exception from evaluateInPageAnonymously.')); TestRunner.completeTest(); }; /** * @param {string} code * @return {!Promise<*>} */ TestRunner.evaluateInPagePromise = function(code) { return new Promise(success => TestRunner.evaluateInPage(code, success)); }; /** * @param {string} code * @return {!Promise<*>} */ TestRunner.evaluateInPageAsync = async function(code) { const response = await TestRunner.RuntimeAgent.invoke_evaluate( {expression: code, objectGroup: 'console', includeCommandLineAPI: false, awaitPromise: true}); const error = response[Protocol.Error]; if (!error && !response.exceptionDetails) return response.result.value; TestRunner.addResult( 'Error: ' + (error || response.exceptionDetails && response.exceptionDetails.text || 'exception while evaluation in page.')); TestRunner.completeTest(); }; /** * @param {string} name * @param {!Array<*>} args * @return {!Promise<*>} */ TestRunner.callFunctionInPageAsync = function(name, args) { args = args || []; return TestRunner.evaluateInPageAsync(name + '(' + args.map(a => JSON.stringify(a)).join(',') + ')'); }; /** * @param {string} code */ TestRunner.evaluateInPageWithTimeout = function(code) { // FIXME: we need a better way of waiting for chromium events to happen TestRunner.evaluateInPageAnonymously('setTimeout(unescape(\'' + escape(code) + '\'), 1)'); }; /** * @param {function():*} func * @param {function(*):void} callback */ TestRunner.evaluateFunctionInOverlay = function(func, callback) { const expression = 'internals.evaluateInInspectorOverlay("(" + ' + func + ' + ")()")'; const mainContext = TestRunner.runtimeModel.executionContexts()[0]; mainContext .evaluate( { expression: expression, objectGroup: '', includeCommandLineAPI: false, silent: false, returnByValue: true, generatePreview: false }, /* userGesture */ false, /* awaitPromise*/ false) .then(result => void callback(result.object.value)); }; /** * @param {boolean} passCondition * @param {string} failureText */ TestRunner.check = function(passCondition, failureText) { if (!passCondition) TestRunner.addResult('FAIL: ' + failureText); }; /** * @param {!Function} callback */ TestRunner.deprecatedRunAfterPendingDispatches = function(callback) { const targets = SDK.targetManager.targets(); const promises = targets.map(target => new Promise(resolve => target._deprecatedRunAfterPendingDispatches(resolve))); Promise.all(promises).then(TestRunner.safeWrap(callback)); }; /** * This ensures a base tag is set so all DOM references * are relative to the test file and not the inspected page * (i.e. http/tests/devtools/resources/inspected-page.html). * @param {string} html * @return {!Promise<*>} */ TestRunner.loadHTML = function(html) { if (!html.includes('<base')) { // <!DOCTYPE...> tag needs to be first const doctypeRegex = /(<!DOCTYPE.*?>)/i; const baseTag = `<base href="${TestRunner.url()}">`; if (html.match(doctypeRegex)) html = html.replace(doctypeRegex, '$1' + baseTag); else html = baseTag + html; } html = html.replace(/'/g, '\\\'').replace(/\n/g, '\\n'); return TestRunner.evaluateInPageAnonymously(`document.write(\`${html}\`);document.close();`); }; /** * @param {string} path * @return {!Promise<*>} */ TestRunner.addScriptTag = function(path) { return TestRunner.evaluateInPageAsync(` (function(){ let script = document.createElement('script'); script.src = '${path}'; document.head.append(script); return new Promise(f => script.onload = f); })(); `); }; /** * @param {string} path * @return {!Promise<*>} */ TestRunner.addStylesheetTag = function(path) { return TestRunner.evaluateInPageAsync(` (function(){ let link = document.createElement('link'); link.rel = 'stylesheet'; link.type = 'text/css'; link.href = '${path}'; link.onload = onload; document.head.append(link); let resolve; let promise = new Promise(r => resolve = r); function onload() { // TODO(chenwilliam): It shouldn't be necessary to force // style recalc here but some tests rely on it. window.getComputedStyle(document.body).color; resolve(); } return promise; })(); `); }; /** * @param {string} path * @return {!Promise<*>} */ TestRunner.addHTMLImport = function(path) { return TestRunner.evaluateInPageAsync(` (function(){ let link = document.createElement('link'); link.rel = 'import'; link.href = '${path}'; let promise = new Promise(r => link.onload = r); document.body.append(link); return promise; })(); `); }; /** * @param {string} path * @param {!Object|undefined} options * @return {!Promise<*>} */ TestRunner.addIframe = function(path, options = {}) { options.id = options.id || ''; options.name = options.name || ''; return TestRunner.evaluateInPageAsync(` (function(){ let iframe = document.createElement('iframe'); iframe.src = '${path}'; iframe.id = '${options.id}'; iframe.name = '${options.name}'; document.body.appendChild(iframe); return new Promise(f => iframe.onload = f); })(); `); }; /** @type {number} */ TestRunner._pendingInits = 0; /** * The old test framework executed certain snippets in the inspected page * context as part of loading a test helper file. * * This is deprecated because: * 1) it makes the testing API less intuitive (need to read the various *TestRunner.js * files to know which helper functions are available in the inspected page). * 2) it complicates the test framework's module loading process. * * In most cases, this is used to set up inspected page functions (e.g. makeSimpleXHR) * which should become a *TestRunner method (e.g. NetworkTestRunner.makeSimpleXHR) * that calls evaluateInPageAnonymously(...). * @param {string} code */ TestRunner.deprecatedInitAsync = async function(code) { TestRunner._pendingInits++; await TestRunner.RuntimeAgent.invoke_evaluate({expression: code, objectGroup: 'console'}); TestRunner._pendingInits--; if (!TestRunner._pendingInits) TestRunner._resolveOnFinishInits(); }; /** * @param {string} title */ TestRunner.markStep = function(title) { TestRunner.addResult('\nRunning: ' + title); }; TestRunner.startDumpingProtocolMessages = function() { // TODO(chenwilliam): stop abusing Closure interface which is why // we need to opt out of type checking here const untypedConnection = /** @type {*} */ (Protocol.InspectorBackend.Connection); untypedConnection.prototype._dumpProtocolMessage = self.testRunner.logToStderr.bind(self.testRunner); Protocol.InspectorBackend.Options.dumpInspectorProtocolMessages = 1; }; /** * @param {string} url * @param {string} content * @param {!SDK.ResourceTreeFrame} frame */ TestRunner.addScriptForFrame = function(url, content, frame) { content += '\n//# sourceURL=' + url; const executionContext = TestRunner.runtimeModel.executionContexts().find(context => context.frameId === frame.id); TestRunner.RuntimeAgent.evaluate(content, 'console', false, false, executionContext.id); }; TestRunner.formatters = {}; /** * @param {*} value * @return {string} */ TestRunner.formatters.formatAsTypeName = function(value) { return '<' + typeof value + '>'; }; /** * @param {*} value * @return {string} */ TestRunner.formatters.formatAsTypeNameOrNull = function(value) { if (value === null) return 'null'; return TestRunner.formatters.formatAsTypeName(value); }; /** * @param {*} value * @return {string|!Date} */ TestRunner.formatters.formatAsRecentTime = function(value) { if (typeof value !== 'object' || !(value instanceof Date)) return TestRunner.formatters.formatAsTypeName(value); const delta = Date.now() - value; return 0 <= delta && delta < 30 * 60 * 1000 ? '<plausible>' : value; }; /** * @param {string} value * @return {string} */ TestRunner.formatters.formatAsURL = function(value) { if (!value) return value; const lastIndex = value.lastIndexOf('devtools/'); if (lastIndex < 0) return value; return '.../' + value.substr(lastIndex); }; /** * @param {string} value * @return {string} */ TestRunner.formatters.formatAsDescription = function(value) { if (!value) return value; return '"' + value.replace(/^function [gs]et /, 'function ') + '"'; }; /** * @typedef {!Object<string, string>} */ TestRunner.CustomFormatters; /** * @param {!Object} object * @param {!TestRunner.CustomFormatters=} customFormatters * @param {string=} prefix * @param {string=} firstLinePrefix */ TestRunner.addObject = function(object, customFormatters, prefix, firstLinePrefix) { prefix = prefix || ''; firstLinePrefix = firstLinePrefix || prefix; TestRunner.addResult(firstLinePrefix + '{'); const propertyNames = Object.keys(object); propertyNames.sort(); for (let i = 0; i < propertyNames.length; ++i) { const prop = propertyNames[i]; if (!object.hasOwnProperty(prop)) continue; const prefixWithName = ' ' + prefix + prop + ' : '; const propValue = object[prop]; if (customFormatters && customFormatters[prop]) { const formatterName = customFormatters[prop]; if (formatterName !== 'skip') { const formatter = TestRunner.formatters[formatterName]; TestRunner.addResult(prefixWithName + formatter(propValue)); } } else { TestRunner.dump(propValue, customFormatters, ' ' + prefix, prefixWithName); } } TestRunner.addResult(prefix + '}'); }; /** * @param {!Array} array * @param {!TestRunner.CustomFormatters=} customFormatters * @param {string=} prefix * @param {string=} firstLinePrefix */ TestRunner.addArray = function(array, customFormatters, prefix, firstLinePrefix) { prefix = prefix || ''; firstLinePrefix = firstLinePrefix || prefix; TestRunner.addResult(firstLinePrefix + '['); for (let i = 0; i < array.length; ++i) TestRunner.dump(array[i], customFormatters, prefix + ' '); TestRunner.addResult(prefix + ']'); }; /** * @param {!Node} node */ TestRunner.dumpDeepInnerHTML = function(node) { /** * @param {string} prefix * @param {!Node} node */ function innerHTML(prefix, node) { const openTag = []; if (node.nodeType === Node.TEXT_NODE) { if (!node.parentElement || node.parentElement.nodeName !== 'STYLE') TestRunner.addResult(node.nodeValue); return; } openTag.push('<' + node.nodeName); const attrs = node.attributes; for (let i = 0; attrs && i < attrs.length; ++i) openTag.push(attrs[i].name + '=' + attrs[i].value); openTag.push('>'); TestRunner.addResult(prefix + openTag.join(' ')); for (let child = node.firstChild; child; child = child.nextSibling) innerHTML(prefix + ' ', child); if (node.shadowRoot) innerHTML(prefix + ' ', node.shadowRoot); TestRunner.addResult(prefix + '</' + node.nodeName + '>'); } innerHTML('', node); }; /** * @param {!Node} node * @return {string} */ TestRunner.deepTextContent = function(node) { if (!node) return ''; if (node.nodeType === Node.TEXT_NODE && node.nodeValue) return !node.parentElement || node.parentElement.nodeName !== 'STYLE' ? node.nodeValue : ''; let res = ''; const children = node.childNodes; for (let i = 0; i < children.length; ++i) res += TestRunner.deepTextContent(children[i]); if (node.shadowRoot) res += TestRunner.deepTextContent(node.shadowRoot); return res; }; /** * @param {*} value * @param {!TestRunner.CustomFormatters=} customFormatters * @param {string=} prefix * @param {string=} prefixWithName */ TestRunner.dump = function(value, customFormatters, prefix, prefixWithName) { prefixWithName = prefixWithName || prefix; if (prefixWithName && prefixWithName.length > 80) { TestRunner.addResult(prefixWithName + 'was skipped due to prefix length limit'); return; } if (value === null) TestRunner.addResult(prefixWithName + 'null'); else if (value && value.constructor && value.constructor.name === 'Array') TestRunner.addArray(/** @type {!Array} */ (value), customFormatters, prefix, prefixWithName); else if (typeof value === 'object') TestRunner.addObject(/** @type {!Object} */ (value), customFormatters, prefix, prefixWithName); else if (typeof value === 'string') TestRunner.addResult(prefixWithName + '"' + value + '"'); else TestRunner.addResult(prefixWithName + value); }; /** * @param {!UI.TreeElement} treeElement */ TestRunner.dumpObjectPropertyTreeElement = function(treeElement) { const expandedSubstring = treeElement.expanded ? '[expanded]' : '[collapsed]'; TestRunner.addResult(expandedSubstring + ' ' + treeElement.listItemElement.deepTextContent()); for (let i = 0; i < treeElement.childCount(); ++i) { const property = treeElement.childAt(i).property; const key = property.name; const value = property.value._description; TestRunner.addResult(' ' + key + ': ' + value); } }; /** * @param {symbol} event * @param {!Common.Object} obj * @param {function(?):boolean=} condition * @return {!Promise} */ TestRunner.waitForEvent = function(event, obj, condition) { condition = condition || function() { return true; }; return new Promise(resolve => { obj.addEventListener(event, onEventFired); /** * @param {!Common.Event} event */ function onEventFired(event) { if (!condition(event.data)) return; obj.removeEventListener(event, onEventFired); resolve(event.data); } }); }; /** * @param {function(!SDK.Target):boolean} filter * @return {!Promise<!SDK.Target>} */ TestRunner.waitForTarget = function(filter) { filter = filter || (target => true); for (const target of SDK.targetManager.targets()) { if (filter(target)) return Promise.resolve(target); } return new Promise(fulfill => { const observer = /** @type {!SDK.TargetManager.Observer} */ ({ targetAdded: function(target) { if (filter(target)) { SDK.targetManager.unobserveTargets(observer); fulfill(target); } }, targetRemoved: function() {}, }); SDK.targetManager.observeTargets(observer); }); }; /** * @param {!SDK.RuntimeModel} runtimeModel * @return {!Promise} */ TestRunner.waitForExecutionContext = function(runtimeModel) { if (runtimeModel.executionContexts().length) return Promise.resolve(runtimeModel.executionContexts()[0]); return runtimeModel.once(SDK.RuntimeModel.Events.ExecutionContextCreated); }; /** * @param {!SDK.ExecutionContext} context * @return {!Promise} */ TestRunner.waitForExecutionContextDestroyed = function(context) { const runtimeModel = context.runtimeModel; if (runtimeModel.executionContexts().indexOf(context) === -1) return Promise.resolve(); return TestRunner.waitForEvent( SDK.RuntimeModel.Events.ExecutionContextDestroyed, runtimeModel, destroyedContext => destroyedContext === context); }; /** * @param {number} a * @param {number} b * @param {string=} message */ TestRunner.assertGreaterOrEqual = function(a, b, message) { if (a < b) TestRunner.addResult('FAILED: ' + (message ? message + ': ' : '') + a + ' < ' + b); }; /** * @param {string} url * @param {function():void} callback */ TestRunner.navigate = function(url, callback) { TestRunner._pageLoadedCallback = TestRunner.safeWrap(callback); TestRunner.resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.Load, TestRunner._pageNavigated); // Note: injected <base> means that url is relative to test // and not the inspected page TestRunner.evaluateInPageAnonymously('window.location.replace(\'' + url + '\')'); }; /** * @return {!Promise} */ TestRunner.navigatePromise = function(url) { return new Promise(fulfill => TestRunner.navigate(url, fulfill)); }; TestRunner._pageNavigated = function() { TestRunner.resourceTreeModel.removeEventListener(SDK.ResourceTreeModel.Events.Load, TestRunner._pageNavigated); TestRunner._handlePageLoaded(); }; /** * @param {function():void} callback */ TestRunner.hardReloadPage = function(callback) { TestRunner._innerReloadPage(true, undefined, callback); }; /** * @param {function():void} callback */ TestRunner.reloadPage = function(callback) { TestRunner._innerReloadPage(false, undefined, callback); }; /** * @param {(string|undefined)} injectedScript * @param {function():void} callback */ TestRunner.reloadPageWithInjectedScript = function(injectedScript, callback) { TestRunner._innerReloadPage(false, injectedScript, callback); }; /** * @return {!Promise} */ TestRunner.reloadPagePromise = function() { return new Promise(fulfill => TestRunner.reloadPage(fulfill)); }; /** * @param {boolean} hardReload * @param {(string|undefined)} injectedScript * @param {function():void} callback */ TestRunner._innerReloadPage = function(hardReload, injectedScript, callback) { TestRunner._pageLoadedCallback = TestRunner.safeWrap(callback); TestRunner.resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.Load, TestRunner.pageLoaded); TestRunner.resourceTreeModel.reloadPage(hardReload, injectedScript); }; TestRunner.pageLoaded = function() { TestRunner.resourceTreeModel.removeEventListener(SDK.ResourceTreeModel.Events.Load, TestRunner.pageLoaded); TestRunner.addResult('Page reloaded.'); TestRunner._handlePageLoaded(); }; TestRunner._handlePageLoaded = async function() { await TestRunner.waitForExecutionContext(/** @type {!SDK.RuntimeModel} */ (TestRunner.runtimeModel)); if (TestRunner._pageLoadedCallback) { const callback = TestRunner._pageLoadedCallback; delete TestRunner._pageLoadedCallback; callback(); } }; /** * @param {function():void} callback */ TestRunner.waitForPageLoad = function(callback) { TestRunner.resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.Load, onLoaded); function onLoaded() { TestRunner.resourceTreeModel.removeEventListener(SDK.ResourceTreeModel.Events.Load, onLoaded); callback(); } }; /** * @param {function():void} callback */ TestRunner.runWhenPageLoads = function(callback) { const oldCallback = TestRunner._pageLoadedCallback; function chainedCallback() { if (oldCallback) oldCallback(); callback(); } TestRunner._pageLoadedCallback = TestRunner.safeWrap(chainedCallback); }; /** * @param {!Array<function(function():void)>} testSuite */ TestRunner.runTestSuite = function(testSuite) { const testSuiteTests = testSuite.slice(); function runner() { if (!testSuiteTests.length) { TestRunner.completeTest(); return; } const nextTest = testSuiteTests.shift(); TestRunner.addResult(''); TestRunner.addResult( 'Running: ' + /function\s([^(]*)/.exec(nextTest)[1]); TestRunner.safeWrap(nextTest)(runner); } runner(); }; /** * @param {*} expected * @param {*} found * @param {string} message */ TestRunner.assertEquals = function(expected, found, message) { if (expected === found) return; let error; if (message) error = 'Failure (' + message + '):'; else error = 'Failure:'; throw new Error(error + ' expected <' + expected + '> found <' + found + '>'); }; /** * @param {*} found * @param {string} message */ TestRunner.assertTrue = function(found, message) { TestRunner.assertEquals(true, !!found, message); }; /** * @param {!Object} receiver * @param {string} methodName * @param {!Function} override * @param {boolean=} opt_sticky * @return {!Function} */ TestRunner.override = function(receiver, methodName, override, opt_sticky) { override = TestRunner.safeWrap(override); const original = receiver[methodName]; if (typeof original !== 'function') throw new Error('Cannot find method to override: ' + methodName); receiver[methodName] = function(var_args) { try { return override.apply(this, arguments); } catch (e) { throw new Error('Exception in overriden method \'' + methodName + '\': ' + e); } finally { if (!opt_sticky) receiver[methodName] = original; } }; return original; }; /** * @param {string} text * @return {string} */ TestRunner.clearSpecificInfoFromStackFrames = function(text) { let buffer = text.replace(/\(file:\/\/\/(?:[^)]+\)|[\w\/:-]+)/g, '(...)'); buffer = buffer.replace(/\(http:\/\/(?:[^)]+\)|[\w\/:-]+)/g, '(...)'); buffer = buffer.replace(/\(test:\/\/(?:[^)]+\)|[\w\/:-]+)/g, '(...)'); buffer = buffer.replace(/\(<anonymous>:[^)]+\)/g, '(...)'); buffer = buffer.replace(/VM\d+/g, 'VM'); return buffer.replace(/\s*at[^()]+\(native\)/g, ''); }; TestRunner.hideInspectorView = function() { UI.inspectorView.element.setAttribute('style', 'display:none !important'); }; /** * @return {?SDK.ResourceTreeFrame} */ TestRunner.mainFrame = function() { return TestRunner.resourceTreeModel.mainFrame; }; TestRunner.StringOutputStream = class { /** * @param {function(string):void} callback */ constructor(callback) { this._callback = callback; this._buffer = ''; } /** * @param {string} fileName * @return {!Promise<boolean>} */ async open(fileName) { return true; } /** * @param {string} chunk */ async write(chunk) { this._buffer += chunk; } async close() { this._callback(this._buffer); } }; /** * @template V */ TestRunner.MockSetting = class { /** * @param {V} value */ constructor(value) { this._value = value; } /** * @return {V} */ get() { return this._value; } /** * @param {V} value */ set(value) { this._value = value; } }; /** * @return {!Array<!Runtime.Module>} */ TestRunner.loadedModules = function() { return self.runtime._modules.filter(module => module._loadedForTest) .filter(module => module.name().indexOf('test_runner') === -1); }; /** * @param {!Array<!Runtime.Module>} relativeTo * @return {!Array<!Runtime.Module>} */ TestRunner.dumpLoadedModules = function(relativeTo) { const previous = new Set(relativeTo || []); function moduleSorter(left, right) { return String.naturalOrderComparator(left._descriptor.name, right._descriptor.name); } TestRunner.addResult('Loaded modules:'); const loadedModules = TestRunner.loadedModules().sort(moduleSorter); for (const module of loadedModules) { if (previous.has(module)) continue; TestRunner.addResult(' ' + module._descriptor.name); } return loadedModules; }; /** * @param {!SDK.Target} target * @return {boolean} */ TestRunner.isDedicatedWorker = function(target) { return target && !target.hasBrowserCapability() && target.hasJSCapability() && target.hasLogCapability(); }; /** * @param {!SDK.Target} target * @return {boolean} */ TestRunner.isServiceWorker = function(target) { return target && !target.hasBrowserCapability() && !target.hasJSCapability() && target.hasNetworkCapability() && target.hasTargetCapability(); }; /** * @param {!SDK.Target} target * @return {string} */ TestRunner.describeTargetType = function(target) { if (TestRunner.isDedicatedWorker(target)) return 'worker'; if (TestRunner.isServiceWorker(target)) return 'service-worker'; if (!target.parentTarget()) return 'page'; return 'frame'; }; /** * @param {string} urlSuffix * @param {!Workspace.projectTypes=} projectType * @return {!Promise} */ TestRunner.waitForUISourceCode = function(urlSuffix, projectType) { /** * @param {!Workspace.UISourceCode} uiSourceCode * @return {boolean} */ function matches(uiSourceCode) { if (projectType && uiSourceCode.project().type() !== projectType) return false; if (!projectType && uiSourceCode.project().type() === Workspace.projectTypes.Service) return false; if (urlSuffix && !uiSourceCode.url().endsWith(urlSuffix)) return false; return true; } for (const uiSourceCode of Workspace.workspace.uiSourceCodes()) { if (urlSuffix && matches(uiSourceCode)) return Promise.resolve(uiSourceCode); } return TestRunner.waitForEvent(Workspace.Workspace.Events.UISourceCodeAdded, Workspace.workspace, matches); }; /** * @param {!Function} callback */ TestRunner.waitForUISourceCodeRemoved = function(callback) { Workspace.workspace.once(Workspace.Workspace.Events.UISourceCodeRemoved).then(callback); }; /** * @param {string=} url * @return {string} */ TestRunner.url = function(url = '') { const testScriptURL = /** @type {string} */ (Runtime.queryParam('test')); // This handles relative (e.g. "../file"), root (e.g. "/resource"), // absolute (e.g. "http://", "data:") and empty (e.g. "") paths return new URL(url, testScriptURL + '/../').href; }; /** * @param {string} str * @param {string} mimeType * @return {!Promise.<undefined>} * @suppressGlobalPropertiesCheck */ TestRunner.dumpSyntaxHighlight = function(str, mimeType) { const node = document.createElement('span'); node.textContent = str; const javascriptSyntaxHighlighter = new UI.SyntaxHighlighter(mimeType, false); return javascriptSyntaxHighlighter.syntaxHighlightNode(node).then(dumpSyntax); function dumpSyntax() { const node_parts = []; for (let i = 0; i < node.childNodes.length; i++) { if (node.childNodes[i].getAttribute) node_parts.push(node.childNodes[i].getAttribute('class')); else node_parts.push('*'); } TestRunner.addResult(str + ': ' + node_parts.join(', ')); } }; /** * @param {string} messageType */ TestRunner._consoleOutputHook = function(messageType) { TestRunner.addResult(messageType + ': ' + Array.prototype.slice.call(arguments, 1)); }; /** * This monkey patches console functions in DevTools context so the console * messages are shown in the right places, instead of having all of the console * messages printed at the top of the test expectation file (default behavior). */ TestRunner._printDevToolsConsole = function() { if (TestRunner._isDebugTest()) return; console.log = TestRunner._consoleOutputHook.bind(TestRunner, 'log'); console.error = TestRunner._consoleOutputHook.bind(TestRunner, 'error'); console.info = TestRunner._consoleOutputHook.bind(TestRunner, 'info'); }; /** * @param {string} querySelector */ TestRunner.dumpInspectedPageElementText = async function(querySelector) { const value = await TestRunner.evaluateInPageAsync(`document.querySelector('${querySelector}').innerText`); TestRunner.addResult(value); }; /** @type {boolean} */ TestRunner._startedTest = false; /** * @implements {SDK.TargetManager.Observer} */ TestRunner._TestObserver = class { /** * @param {!SDK.Target} target * @override */ targetAdded(target) { if (TestRunner._startedTest) return; TestRunner._startedTest = true; TestRunner._setupTestHelpers(target); if (TestRunner._isStartupTest()) return; TestRunner .loadHTML(` <head> <base href="${TestRunner.url()}"> </head> <body> </body> `).then(() => TestRunner._executeTestScript()); } /** * @param {!SDK.Target} target * @override */ targetRemoved(target) { } }; /** * @return {boolean} */ TestRunner._isDebugTest = function() { return !self.testRunner || !!Runtime.queryParam('debugFrontend'); }; /** * @return {boolean} */ TestRunner._isStartupTest = function() { return Runtime.queryParam('test').includes('/startup/'); }; (async function() { /** * @param {string|!Event} message * @param {string} source * @param {number} lineno * @param {number} colno * @param {!Error} error */ function completeTestOnError(message, source, lineno, colno, error) { TestRunner.addResult('TEST ENDED IN ERROR: ' + error.stack); TestRunner.completeTest(); } self['onerror'] = completeTestOnError; TestRunner._printDevToolsConsole(); SDK.targetManager.observeTargets(new TestRunner._TestObserver()); if (!TestRunner._isStartupTest()) return; /** * Startup test initialization: * 1. Wait for DevTools app UI to load * 2. Execute test script, the first line will be TestRunner.setupStartupTest(...) which: * A. Navigate secondary window * B. After preconditions occur, secondary window calls testRunner.inspectSecondaryWindow() * 3. Backend executes TestRunner._startupTestSetupFinished() which calls _initializeTarget() */ TestRunner._initializeTargetForStartupTest = TestRunner.override(Main.Main._instanceForTest, '_initializeTarget', () => undefined) .bind(Main.Main._instanceForTest); await TestRunner.addSnifferPromise(Main.Main._instanceForTest, '_showAppUI'); TestRunner._executeTestScript(); })();