| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // test_custom_bindings.js |
| // mini-framework for ExtensionApiTest browser tests |
| |
| const environmentSpecificBindings = |
| require('test_environment_specific_bindings'); |
| const GetExtensionAPIDefinitionsForTest = |
| requireNative('apiDefinitions').GetExtensionAPIDefinitionsForTest; |
| const GetAPIFeatures = requireNative('test_features').GetAPIFeatures; |
| const userGestures = requireNative('user_gestures'); |
| |
| const GetModuleSystem = requireNative('v8_context').GetModuleSystem; |
| |
| function handleException(message, error) { |
| bindingUtil.handleException(message || 'Unknown error', error); |
| } |
| |
| apiBridge.registerCustomHook(function(api) { |
| const kFailureException = 'chrome.test.failure'; |
| |
| const chromeTest = api.compiledApi; |
| const apiFunctions = api.apiFunctions; |
| |
| chromeTest.tests = chromeTest.tests || []; |
| |
| let currentTest = null; |
| let lastTest = null; |
| let testsFailed = 0; |
| let testCount = 1; |
| let pendingCallbacks = 0; |
| let pendingPromiseRejections = 0; |
| |
| function safeFunctionApply(func, args) { |
| try { |
| if (func) |
| return $Function.apply(func, undefined, args); |
| } catch (e) { |
| if (e === kFailureException) |
| throw e; |
| handleException(e.message, e); |
| } |
| } |
| |
| function runNextTest() { |
| // There may have been callbacks or promise rejections which were |
| // interrupted by failure exceptions. |
| pendingCallbacks = 0; |
| pendingPromiseRejections = 0; |
| |
| lastTest = currentTest; |
| currentTest = $Array.shift(chromeTest.tests); |
| |
| if (!currentTest) { |
| allTestsDone(); |
| return; |
| } |
| |
| try { |
| chromeTest.log(`( RUN ) ${testName(currentTest)}`); |
| bindingUtil.setExceptionHandler(function(message, e) { |
| if (e !== kFailureException) |
| chromeTest.fail(`uncaught exception: ${message}`); |
| }); |
| const result = $Function.call(currentTest); |
| if (result instanceof Promise) { |
| result.catch(e => handleException(e.message, e)); |
| } |
| } catch (e) { |
| handleException(e.message, e); |
| } |
| } |
| |
| // Helper function to get around the fact that function names in javascript |
| // are read-only, and you can't assign one to anonymous functions. |
| function testName(test) { |
| return test ? (test.name || test.generatedName) : '(no test)'; |
| } |
| |
| function testDone() { |
| environmentSpecificBindings.testDone(runNextTest); |
| } |
| |
| function allTestsDone() { |
| if (testsFailed == 0) { |
| chromeTest.notifyPass(); |
| } else { |
| chromeTest.notifyFail(`Failed ${testsFailed} of ${testCount} tests`); |
| } |
| } |
| |
| // Helper function for boolean asserts. Compares |test| to |expected|. |
| function assertBool(test, expected, message) { |
| if (test !== expected) { |
| if (typeof test == 'string') { |
| if (message) |
| message = `${test}\n${message}`; |
| else |
| message = test; |
| } |
| chromeTest.fail(message); |
| } |
| } |
| |
| apiFunctions.setHandleRequest('callbackAdded', function() { |
| pendingCallbacks++; |
| |
| let called = null; |
| return function() { |
| if (called != null) { |
| const redundantPrefixLength = 'Error\n'.length; |
| chromeTest.fail( |
| 'Callback has already been run. ' + |
| 'First call:\n' + |
| $String.slice(called, redundantPrefixLength) + '\n' + |
| 'Second call:\n' + |
| $String.slice(new Error().stack, redundantPrefixLength)); |
| } |
| called = new Error().stack; |
| |
| pendingCallbacks--; |
| if (pendingCallbacks == 0) { |
| chromeTest.succeed(); |
| } |
| }; |
| }); |
| |
| apiFunctions.setHandleRequest('fail', function failHandler(message) { |
| chromeTest.log(`( FAILED ) ${testName(currentTest)}`); |
| |
| let stack = {}; |
| // NOTE(devlin): captureStackTrace() populates a stack property of the |
| // passed-in object with the stack trace. The second parameter (failHandler) |
| // represents a function to serve as a relative point, and is removed from |
| // the trace (so that everything doesn't include failHandler in the trace |
| // itself). This (and other APIs) are documented here: |
| // https://github.com/v8/v8/wiki/Stack%20Trace%20API. If we wanted to be |
| // really fancy, there may be more sophisticated ways of doing this. |
| Error.captureStackTrace(stack, failHandler); |
| |
| if (!message) |
| message = 'FAIL (no message)'; |
| |
| message += '\n' + stack.stack; |
| console.log(`[FAIL] ${testName(currentTest)}: ${message}`); |
| testsFailed++; |
| testDone(); |
| |
| // Interrupt the rest of the test. |
| throw kFailureException; |
| }); |
| |
| apiFunctions.setHandleRequest('succeed', function() { |
| chromeTest.assertEq( |
| 0, pendingPromiseRejections, |
| 'Test had pending promise rejections. This is likely the result of ' + |
| 'not waiting for the promise returned by `assertPromiseRejects()` to ' + |
| 'resolve. Instead, use `await assertPromiseRejects(...)` or ' + |
| '`assertPromiseRejects(...).then(...).`.'); |
| console.log(`[SUCCESS] ${testName(currentTest)}`); |
| chromeTest.log('( SUCCESS )'); |
| testDone(); |
| }); |
| |
| apiFunctions.setHandleRequest('getModuleSystem', function(context) { |
| return GetModuleSystem(context); |
| }); |
| |
| apiFunctions.setHandleRequest('assertTrue', function(test, message) { |
| assertBool(test, true, message); |
| }); |
| |
| apiFunctions.setHandleRequest('assertFalse', function(test, message) { |
| assertBool(test, false, message); |
| }); |
| |
| apiFunctions.setHandleRequest('checkDeepEq', function(expected, actual) { |
| if ((expected === null) != (actual === null)) |
| return false; |
| |
| if (expected === actual) |
| return true; |
| |
| if (typeof expected !== typeof actual) |
| return false; |
| if ($Array.isArray(expected) !== $Array.isArray(actual)) |
| return false; |
| |
| // Handle the ArrayBuffer cases. Bail out in case of type mismatch, to |
| // prevent the ArrayBuffer from being treated as an empty enumerable below. |
| if ((actual instanceof ArrayBuffer) !== (expected instanceof ArrayBuffer)) |
| return false; |
| if ((actual instanceof ArrayBuffer) && (expected instanceof ArrayBuffer)) { |
| if (actual.byteLength != expected.byteLength) |
| return false; |
| let actualView = new Uint8Array(actual); |
| let expectedView = new Uint8Array(expected); |
| for (let i = 0; i < actualView.length; ++i) { |
| if (actualView[i] != expectedView[i]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| for (let p in actual) { |
| if ($Object.hasOwnProperty(actual, p) && |
| !$Object.hasOwnProperty(expected, p)) { |
| return false; |
| } |
| } |
| for (let p in expected) { |
| if ($Object.hasOwnProperty(expected, p) && |
| !$Object.hasOwnProperty(actual, p)) { |
| return false; |
| } |
| } |
| |
| for (let p in expected) { |
| let eq = true; |
| switch (typeof expected[p]) { |
| case 'object': |
| eq = chromeTest.checkDeepEq(expected[p], actual[p]); |
| break; |
| case 'function': |
| eq = (typeof actual[p] != 'undefined' && |
| expected[p].toString() == actual[p].toString()); |
| break; |
| default: |
| eq = expected[p] == actual[p] && |
| typeof expected[p] == typeof actual[p]; |
| break; |
| } |
| if (!eq) |
| return false; |
| } |
| return true; |
| }); |
| |
| apiFunctions.setHandleRequest('assertEq', |
| function(expected, actual, message) { |
| let errorMsg = 'API Test Error in ' + testName(currentTest); |
| if (message) |
| errorMsg += ': ' + message; |
| if (typeof(expected) == 'object') { |
| if (!chromeTest.checkDeepEq(expected, actual)) { |
| errorMsg += '\nActual: ' + $JSON.stringify(actual) + |
| '\nExpected: ' + $JSON.stringify(expected); |
| chromeTest.fail(errorMsg); |
| } |
| return; |
| } |
| if (expected != actual) { |
| chromeTest.fail( |
| `${errorMsg}\nActual: ${actual}\nExpected: ${expected}`); |
| } |
| if (typeof expected != typeof actual) { |
| chromeTest.fail(`${errorMsg} (type mismatch)\nActual Type: ${ |
| typeof actual}\nExpected Type:${typeof expected}`); |
| } |
| }); |
| |
| apiFunctions.setHandleRequest('assertNe', |
| function(expected, actual, message) { |
| // Easy case: different types are always inequal. |
| if (typeof expected != typeof actual) |
| return; |
| |
| let errorMsg = 'API Test Error in ' + testName(currentTest); |
| if (message) |
| errorMsg += ': ' + message; |
| |
| if (typeof expected == 'object') { |
| if (chromeTest.checkDeepEq(expected, actual)) { |
| errorMsg += '\nExpected inequal values, but both are ' + |
| $JSON.stringify(expected); |
| chromeTest.fail(errorMsg); |
| } |
| return; |
| } |
| |
| if (expected == actual) { |
| errorMsg += '\nExpected inequal values, but both are ' + expected; |
| chromeTest.fail(errorMsg); |
| } |
| }); |
| |
| apiFunctions.setHandleRequest('assertNoLastError', function() { |
| if (chrome.runtime.lastError != undefined) { |
| chromeTest.fail('lastError.message == ' + |
| chrome.runtime.lastError.message); |
| } |
| }); |
| |
| apiFunctions.setHandleRequest('assertLastError', function(expectedError) { |
| chromeTest.assertEq(typeof expectedError, 'string'); |
| chromeTest.assertTrue( |
| chrome.runtime.lastError != undefined, |
| `No lastError, but expected ${expectedError}`); |
| chromeTest.assertEq(expectedError, chrome.runtime.lastError.message); |
| }); |
| |
| apiFunctions.setHandleRequest('assertThrows', |
| function(fn, self, args, message) { |
| chromeTest.assertTrue(typeof fn == 'function'); |
| try { |
| fn.apply(self, args); |
| chromeTest.fail('Did not throw error: ' + fn); |
| } catch (e) { |
| if (e != kFailureException && message !== undefined) { |
| if (message instanceof RegExp) { |
| chromeTest.assertTrue(message.test(e.message), |
| e.message + ' should match ' + message) |
| } else { |
| chromeTest.assertEq(message, e.message); |
| } |
| } |
| } |
| }); |
| |
| apiFunctions.setHandleRequest('loadScript', function(scriptUrl) { |
| // Note: Importing scripts is different depending on if this script is |
| // executing in a Service Worker context. |
| const inServiceWorker = 'ServiceWorkerGlobalScope' in self; |
| |
| function createError(exception) { |
| const errorStr = `Unable to load script: "${scriptUrl}"`; |
| if (inServiceWorker) { |
| return new Error(errorStr, {cause: exception}); |
| } |
| return new Error(errorStr); |
| } |
| |
| if (inServiceWorker) { |
| try { |
| importScripts(scriptUrl); |
| } catch (e) { |
| return Promise.reject(createError(e)); |
| } |
| return Promise.resolve(); |
| } |
| const script = document.createElement('script'); |
| const onScriptLoad = new Promise((resolve, reject) => { |
| script.onload = resolve; |
| function onError() { |
| reject(createError()); |
| } |
| script.onerror = onError; |
| }); |
| script.src = scriptUrl; |
| document.body.appendChild(script); |
| return onScriptLoad; |
| }); |
| |
| apiFunctions.setHandleRequest('assertPromiseRejects', |
| function(promise, expectedMessage) { |
| pendingPromiseRejections++; |
| return promise.then( |
| () => { |
| pendingPromiseRejections--; |
| chromeTest.assertTrue(pendingPromiseRejections >= 0, |
| 'Negative pending promise rejection count!'); |
| chromeTest.fail( |
| 'Promise did not reject. Expected error: ' + expectedMessage); |
| }, |
| (e) => { |
| pendingPromiseRejections--; |
| chromeTest.assertTrue(pendingPromiseRejections >= 0, |
| 'Negative pending promise rejection count!'); |
| if (expectedMessage instanceof RegExp) { |
| chromeTest.assertTrue( |
| expectedMessage.test(e.toString()), |
| `'${e.message}' should match '${expectedMessage}'`); |
| } else { |
| chromeTest.assertEq('string', typeof expectedMessage); |
| chromeTest.assertEq(expectedMessage, e.toString()); |
| } |
| }); |
| }); |
| |
| // Wrapper for generating test functions, that takes care of calling |
| // assertNoLastError() and (optionally) succeed() for you. |
| apiFunctions.setHandleRequest('callback', function(func, expectedError) { |
| if (func) { |
| chromeTest.assertEq(typeof func, 'function'); |
| } |
| const callbackCompleted = chromeTest.callbackAdded(); |
| |
| return function() { |
| if (expectedError == null) { |
| chromeTest.assertNoLastError(); |
| } else { |
| chromeTest.assertLastError(expectedError); |
| } |
| |
| let result; |
| if (func) { |
| result = safeFunctionApply(func, arguments); |
| } |
| |
| callbackCompleted(); |
| return result; |
| }; |
| }); |
| |
| apiFunctions.setHandleRequest('listenOnce', function(event, func) { |
| const callbackCompleted = chromeTest.callbackAdded(); |
| const listener = function() { |
| event.removeListener(listener); |
| safeFunctionApply(func, arguments); |
| callbackCompleted(); |
| }; |
| event.addListener(listener); |
| }); |
| |
| apiFunctions.setHandleRequest('listenForever', function(event, func) { |
| const callbackCompleted = chromeTest.callbackAdded(); |
| |
| const listener = function() { |
| safeFunctionApply(func, arguments); |
| }; |
| |
| const done = function() { |
| event.removeListener(listener); |
| callbackCompleted(); |
| }; |
| |
| event.addListener(listener); |
| return done; |
| }); |
| |
| apiFunctions.setHandleRequest('callbackPass', function(func) { |
| return chromeTest.callback(func); |
| }); |
| |
| apiFunctions.setHandleRequest('callbackFail', function(expectedError, func) { |
| return chromeTest.callback(func, expectedError); |
| }); |
| |
| apiFunctions.setHandleRequest('runTests', function(tests) { |
| chromeTest.tests = tests; |
| testCount = chromeTest.tests.length; |
| runNextTest(); |
| }); |
| |
| apiFunctions.setHandleRequest('getApiDefinitions', function() { |
| return GetExtensionAPIDefinitionsForTest(); |
| }); |
| |
| apiFunctions.setHandleRequest('getApiFeatures', function() { |
| return GetAPIFeatures(); |
| }); |
| |
| apiFunctions.setHandleRequest('isProcessingUserGesture', function() { |
| return userGestures.IsProcessingUserGesture(); |
| }); |
| |
| apiFunctions.setHandleRequest('runWithUserGesture', function(callback) { |
| chromeTest.assertEq(typeof(callback), 'function'); |
| return userGestures.RunWithUserGesture(callback); |
| }); |
| |
| apiFunctions.setHandleRequest('setExceptionHandler', function(callback) { |
| chromeTest.assertEq(typeof(callback), 'function'); |
| bindingUtil.setExceptionHandler(callback); |
| }); |
| |
| environmentSpecificBindings.registerHooks(api); |
| }); |