diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..d38daab --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["es2015", "react"], + "plugins": ["transform-es2015-modules-commonjs","dynamic-import-webpack"] +} diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..c7b23e4 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +/coverage/* +/versions/* +/dist/* \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..d88710c --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "env": { + "browser": true, + "node": false, + "es6": true + }, + "parser": "babel-eslint", + "extends": ["eslint:recommended", "standard"], + "rules": { + "no-console": 0, + "no-unused-vars": ["error", { "args": "after-used" }], + "indent": ["error", 4, { "SwitchCase": 1 }], + "semi": ["error", "always"], + "padded-blocks": 0, + "no-trailing-spaces": [ "error", { "ignoreComments": true } ], + "valid-jsdoc": ["error", { + "requireParamDescription": false, + "requireReturnDescription": false + }], + "require-jsdoc": ["error", { + "require": { + "FunctionDeclaration": true, + "MethodDefinition": true, + "ClassDeclaration": true, + "ArrowFunctionExpression": false + } + }] + } +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index f3120fe..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Launch", - "type": "node", - "request": "launch", - "program": "${workspaceRoot}/./make_version.js", - "stopOnEntry": false, - "args": ["-v", "2.1.1"], - "cwd": "${workspaceRoot}", - "preLaunchTask": null, - "runtimeExecutable": null, - "runtimeArgs": [ - "--nolazy" - ], - "env": { - "NODE_ENV": "development" - }, - "console": "internalConsole", - "sourceMaps": false, - "outDir": null - }, - { - "name": "Attach", - "type": "node", - "request": "attach", - "port": 5858, - "address": "localhost", - "restart": false, - "sourceMaps": false, - "outDir": null, - "localRoot": "${workspaceRoot}", - "remoteRoot": null - }, - { - "name": "Attach to Process", - "type": "node", - "request": "attach", - "processId": "${command.PickProcess}", - "port": 5858, - "sourceMaps": false, - "outDir": null - } - ] -} \ No newline at end of file diff --git a/ApprovedOrigins.js b/ApprovedOrigins.js index ff5f0b8..5ebb831 100644 --- a/ApprovedOrigins.js +++ b/ApprovedOrigins.js @@ -1,40 +1,67 @@ -var APPROVED_ORIGINS_KEY = "wdc_approved_origins"; -var SEPARATOR = ","; -var Cookies = require('cookies-js'); +import * as Cookies from 'cookies-js'; -function _getApprovedOriginsValue() { - var result = Cookies.get(APPROVED_ORIGINS_KEY); - return result; -} +const SEPARATOR = ','; +export const APPROVED_ORIGINS_KEY = 'wdc_approved_origins'; +// alias of Cookies for testing ( and for future replacement? ) +export let CookiesLib = Cookies; + +/** + * @returns {*} + */ +function _getApprovedOriginsValue () { + let result = CookiesLib.get(APPROVED_ORIGINS_KEY); -function _saveApprovedOrigins(originArray) { - var newOriginString = originArray.join(SEPARATOR); - console.log("Saving approved origins '" + newOriginString + "'"); - - // We could potentially make this a longer term cookie instead of just for the current session - var result = Cookies.set(APPROVED_ORIGINS_KEY, newOriginString); - return result; + return result; } -// Adds an approved origins to the list already saved in a session cookie -function addApprovedOrigin(origin) { - if (origin) { - var origins = getApprovedOrigins(); - origins.push(origin); - _saveApprovedOrigins(origins); - } +/** + * + * @param {Array} originArray + * @returns {*} + */ +function _saveApprovedOrigins (originArray) { + + const APPROVED_ORIGINS = originArray.join(SEPARATOR); + + console.log(`Saving approved origins ${APPROVED_ORIGINS} `); + + // We could potentially make this a longer term cookie instead of just for the current session + let result = CookiesLib.set(APPROVED_ORIGINS_KEY, APPROVED_ORIGINS); + + return result; } -// Retrieves the origins which have already been approved by the user -function getApprovedOrigins() { - var originsString = _getApprovedOriginsValue(); - if (!originsString || 0 === originsString.length) { - return []; - } +/** + * Adds an approved origin to the list already saved in a session cookie + * @param {String} origin + * @returns {Object|Undefined} + */ +export function addApprovedOrigin (origin) { + + if (origin) { + let origins = getApprovedOrigins(); + + origins.push(origin); + + // pass along the output of the private function + return _saveApprovedOrigins(origins); + } - var origins = originsString.split(SEPARATOR); - return origins; } -module.exports.addApprovedOrigin = addApprovedOrigin; -module.exports.getApprovedOrigins = getApprovedOrigins; +/** + * Retrieves the origins which have already been approved by the user + * @returns {Array} + */ +export function getApprovedOrigins () { + + let originsString = _getApprovedOriginsValue(); + + if (!originsString || originsString.length === 0) { + return []; + } + + let origins = originsString.split(SEPARATOR); + + return origins; +} diff --git a/ApprovedOrigins.test.js b/ApprovedOrigins.test.js new file mode 100644 index 0000000..aec6e3c --- /dev/null +++ b/ApprovedOrigins.test.js @@ -0,0 +1,50 @@ +/* eslint-env node, mocha, jest */ +import * as ApprovedOrigins from './ApprovedOrigins'; + +// use if required for debugging +let consoleLog = console.log; // eslint-disable-line no-unused-vars +console.log = jest.fn(); + +describe('UNIT - ApprovedOrigins', () => { + + beforeEach(() => { + // Cookies Mock to make this a Unit Test instead of an integration test + // temp mock, will investigate on a better mocking for Cookies lib :( + // (tried with standard jest mock, ridiculously failing) + + ApprovedOrigins.CookiesLib.mokedCookies = {}; + + ApprovedOrigins.CookiesLib.get = jest.fn(() => { + return ApprovedOrigins.CookiesLib.mokedCookies[ApprovedOrigins.APPROVED_ORIGINS_KEY]; + }); + + ApprovedOrigins.CookiesLib.set = jest.fn((key, val) => { + ApprovedOrigins.CookiesLib.mokedCookies[key] = val; + + return ApprovedOrigins.CookiesLib; + }); + + }); + + it('getApprovedOrigins should get an empty array if no value is set', () => { + expect(ApprovedOrigins.getApprovedOrigins()).toEqual([]); + }); + + it('addApprovedOrigin should add an approved origin and ApprovedOrigins.getApprovedOrigins get it correctly', () => { + let origin = 'http://tableau.com'; + expect(ApprovedOrigins.addApprovedOrigin(origin)).toBe(ApprovedOrigins.CookiesLib); + + expect(ApprovedOrigins.getApprovedOrigins()).toEqual([origin]); + + }); + + it('addApprovedOrigin should add multiple values and get an array of the values', () => { + let origin1 = 'http://tableau.com'; + let origin2 = 'http://connectors.tableab.com'; + ApprovedOrigins.addApprovedOrigin(origin1); + ApprovedOrigins.addApprovedOrigin(origin2); + + expect(ApprovedOrigins.getApprovedOrigins()).toEqual([origin1, origin2]); + + }); +}); diff --git a/DevUtils/BuildNumber.js b/DevUtils/BuildNumber.js index d2980ce..d15b6d3 100644 --- a/DevUtils/BuildNumber.js +++ b/DevUtils/BuildNumber.js @@ -1,50 +1,81 @@ -var fs = require('fs'); -var path = require('path'); -const execSync = require('child_process').execSync; - -var WDC_LIB_PREFIX = "tableauwdc-"; - -function VersionNumber(versionString) { - var components = versionString.split("."); - if (components.length < 3) { - console.log() - throw "Invalid number of components. versionString was '" + versionString + "'"; - } - - this.major = parseInt(components[0]).toString(); - this.minor = parseInt(components[1]).toString(); - this.fix = parseInt(components[2]).toString(); -} +import { version as BUILD_NUMBER } from '../package.json'; +export let WDC_LIB_PREFIX = 'tableauwdc-'; -VersionNumber.prototype.toString = function() { - return this.major + "." + this.minor + "." + this.fix; -} +/** + * + */ +export class VersionNumber { + /** + * + * @param {String} versionString + */ + constructor (versionString) { + let components = versionString.split('.'); -VersionNumber.prototype.compare = function(other) { - var majorDiff = this.major - other.major; - var minorDiff = this.minor - other.minor; - var fixDiff = this.fix - other.fix; + if (components.length < 3) { + console.log(); + throw new Error(`Invalid number of components. versionString was ${versionString} `); + } - if (majorDiff != 0) return majorDiff; - if (minorDiff != 0) return minorDiff; - if (fixDiff != 0) return fixDiff; + this.major = parseInt(components[0]).toString(); + this.minor = parseInt(components[1]).toString(); + this.patch = parseInt(components[2]).toString(); + } - return 0; -} + /** + * @returns {String} + */ + toString () { + return `${this.major}.${this.minor}.${this.patch}`; + } + + /** + * + * @param {{ + * major: (String), + * minor: (String), + * patch: (String) + * }} version Input version to compare against this.version + * + * @returns {Number} + */ + compare ({ major, minor, patch } = {}) { + let majorDiff = this.major - major; + let minorDiff = this.minor - minor; + let patchDiff = this.patch - patch; + + if (majorDiff !== 0) { + return majorDiff; + } + + if (minorDiff !== 0) { + return minorDiff; + } -function getBuildNumber() { - // Grab the version number from the environment variable - var versionNumber = process.env.npm_package_config_versionNumber; - if (versionNumber) { - console.log("Found versionNumber in environment variable: '" + versionNumber + "'"); - } else { - versionNumber = process.argv.versionNumber; - console.log("Found versionNumber in argument: '" + versionNumber + "'"); - } - - return versionNumber; + if (patchDiff !== 0) { + return patchDiff; + } + + return 0; + } } -module.exports.VersionNumber = VersionNumber; -module.exports.WDC_LIB_PREFIX = WDC_LIB_PREFIX; -module.exports.getBuildNumber = getBuildNumber; \ No newline at end of file +/** + * + * @param {Object} config + * @returns {String} + */ +export function getBuildNumber ({ showLog = false } = {}) { + // Single source of truth for version is package json + // which is stored at the top as BUILD_NUMBER constant + + if (!BUILD_NUMBER) { + throw new Error(`Unable to retrieve version number from package.json`); + } + + if (showLog) { + console.log(`Found versionNumber in package.json: ${BUILD_NUMBER}`); + } + + return BUILD_NUMBER; +} diff --git a/Enums.js b/Enums.js index 40559eb..218ba99 100644 --- a/Enums.js +++ b/Enums.js @@ -1,99 +1,103 @@ /** This file lists all of the enums which should available for the WDC */ -var allEnums = { - phaseEnum : { - interactivePhase: "interactive", - authPhase: "auth", - gatherDataPhase: "gatherData" - }, +export const ENUMS_DICTIONARY = { + phaseEnum: { + interactivePhase: 'interactive', + authPhase: 'auth', + gatherDataPhase: 'gatherData' + }, - authPurposeEnum : { - ephemeral: "ephemeral", - enduring: "enduring" - }, + authPurposeEnum: { + ephemeral: 'ephemeral', + enduring: 'enduring' + }, - authTypeEnum : { - none: "none", - basic: "basic", - custom: "custom" - }, + authTypeEnum: { + none: 'none', + basic: 'basic', + custom: 'custom' + }, - dataTypeEnum : { - bool: "bool", - date: "date", - datetime: "datetime", - float: "float", - int: "int", - string: "string", - geometry: "geometry" - }, + dataTypeEnum: { + bool: 'bool', + date: 'date', + datetime: 'datetime', + float: 'float', + int: 'int', + string: 'string', + geometry: 'geometry' + }, - columnRoleEnum : { - dimension: "dimension", - measure: "measure" - }, + columnRoleEnum: { + dimension: 'dimension', + measure: 'measure' + }, - columnTypeEnum : { - continuous: "continuous", - discrete: "discrete" - }, + columnTypeEnum: { + continuous: 'continuous', + discrete: 'discrete' + }, - aggTypeEnum : { - sum: "sum", - avg: "avg", - median: "median", - count: "count", - countd: "count_dist" - }, + aggTypeEnum: { + sum: 'sum', + avg: 'avg', + median: 'median', + count: 'count', + countd: 'count_dist' + }, - geographicRoleEnum : { - area_code: "area_code", - cbsa_msa: "cbsa_msa", - city: "city", - congressional_district: "congressional_district", - country_region: "country_region", - county: "county", - state_province: "state_province", - zip_code_postcode: "zip_code_postcode", - latitude: "latitude", - longitude: "longitude" - }, + geographicRoleEnum: { + area_code: 'area_code', + cbsa_msa: 'cbsa_msa', + city: 'city', + congressional_district: 'congressional_district', + country_region: 'country_region', + county: 'county', + state_province: 'state_province', + zip_code_postcode: 'zip_code_postcode', + latitude: 'latitude', + longitude: 'longitude' + }, - unitsFormatEnum : { - thousands: "thousands", - millions: "millions", - billions_english: "billions_english", - billions_standard: "billions_standard" - }, + unitsFormatEnum: { + thousands: 'thousands', + millions: 'millions', + billions_english: 'billions_english', + billions_standard: 'billions_standard' + }, - numberFormatEnum : { - number: "number", - currency: "currency", - scientific: "scientific", - percentage: "percentage" - }, + numberFormatEnum: { + number: 'number', + currency: 'currency', + scientific: 'scientific', + percentage: 'percentage' + }, - localeEnum : { - america: "en-us", - brazil: "pt-br", - china: "zh-cn", - france: "fr-fr", - germany: "de-de", - japan: "ja-jp", - korea: "ko-kr", - spain: "es-es" - }, + localeEnum: { + america: 'en-us', + brazil: 'pt-br', + china: 'zh-cn', + france: 'fr-fr', + germany: 'de-de', + japan: 'ja-jp', + korea: 'ko-kr', + spain: 'es-es' + }, - joinEnum : { - inner: "inner", - left: "left" - } -} + joinEnum: { + inner: 'inner', + left: 'left' + } +}; -// Applies the enums as properties of the target object -function apply(target) { - for(var key in allEnums) { - target[key] = allEnums[key]; - } +/** + * Applies the enums as properties of the target object + * this is a mixin ( will overwrite existing properties) + * + * @param {Object} target + * @returns {Undefined} + */ +export function applyEnums (target) { + for (let key in ENUMS_DICTIONARY) { + target[key] = ENUMS_DICTIONARY[key]; + } } - -module.exports.apply = apply; diff --git a/Enums.test.js b/Enums.test.js new file mode 100644 index 0000000..bb27b09 --- /dev/null +++ b/Enums.test.js @@ -0,0 +1,20 @@ +/* eslint-env node, mocha, jest */ +import { ENUMS_DICTIONARY, applyEnums } from './Enums'; + +describe('UNIT - Enums', () => { + + it('applyEnums to copy properties from ENUMS_DICTIONARY target', () => { + + let target = { + joinEnum: { + inner: 'to be overwritten', + left: 'to be overwritten' + } + }; + + applyEnums(target); + + expect(target).toEqual(ENUMS_DICTIONARY); + + }); +}); diff --git a/NativeDispatcher.js b/NativeDispatcher.js index 6f65701..9c1594b 100644 --- a/NativeDispatcher.js +++ b/NativeDispatcher.js @@ -1,116 +1,206 @@ -/** @class Used for communicating between Tableau desktop/server and the WDC's -* Javascript. is predominantly a pass-through to the Qt WebBridge methods -* @param nativeApiRootObj {Object} - The root object where the native Api methods -* are available. For WebKit, this is window. -*/ -function NativeDispatcher (nativeApiRootObj) { - this.nativeApiRootObj = nativeApiRootObj; - this._initPublicInterface(); - this._initPrivateInterface(); -} - -NativeDispatcher.prototype._initPublicInterface = function() { - console.log("Initializing public interface for NativeDispatcher"); - this._submitCalled = false; - - var publicInterface = {}; - publicInterface.abortForAuth = this._abortForAuth.bind(this); - publicInterface.abortWithError = this._abortWithError.bind(this); - publicInterface.addCrossOriginException = this._addCrossOriginException.bind(this); - publicInterface.log = this._log.bind(this); - publicInterface.submit = this._submit.bind(this); - publicInterface.reportProgress = this._reportProgress.bind(this); - - this.publicInterface = publicInterface; -} - -NativeDispatcher.prototype._abortForAuth = function(msg) { - this.nativeApiRootObj.WDCBridge_Api_abortForAuth.api(msg); -} - -NativeDispatcher.prototype._abortWithError = function(msg) { - this.nativeApiRootObj.WDCBridge_Api_abortWithError.api(msg); -} - -NativeDispatcher.prototype._addCrossOriginException = function(destOriginList) { - this.nativeApiRootObj.WDCBridge_Api_addCrossOriginException.api(destOriginList); -} - -NativeDispatcher.prototype._log = function(msg) { - this.nativeApiRootObj.WDCBridge_Api_log.api(msg); -} - -NativeDispatcher.prototype._submit = function() { - if (this._submitCalled) { - console.log("submit called more than once"); - return; - } - - this._submitCalled = true; - this.nativeApiRootObj.WDCBridge_Api_submit.api(); -}; - -NativeDispatcher.prototype._initPrivateInterface = function() { - console.log("Initializing private interface for NativeDispatcher"); - - this._initCallbackCalled = false; - this._shutdownCallbackCalled = false; - - var privateInterface = {}; - privateInterface._initCallback = this._initCallback.bind(this); - privateInterface._shutdownCallback = this._shutdownCallback.bind(this); - privateInterface._schemaCallback = this._schemaCallback.bind(this); - privateInterface._tableDataCallback = this._tableDataCallback.bind(this); - privateInterface._dataDoneCallback = this._dataDoneCallback.bind(this); - - this.privateInterface = privateInterface; -} - -NativeDispatcher.prototype._initCallback = function() { - if (this._initCallbackCalled) { - console.log("initCallback called more than once"); - return; - } - - this._initCallbackCalled = true; - this.nativeApiRootObj.WDCBridge_Api_initCallback.api(); -} - -NativeDispatcher.prototype._shutdownCallback = function() { - if (this._shutdownCallbackCalled) { - console.log("shutdownCallback called more than once"); - return; - } - - this._shutdownCallbackCalled = true; - this.nativeApiRootObj.WDCBridge_Api_shutdownCallback.api(); -} - -NativeDispatcher.prototype._schemaCallback = function(schema, standardConnections) { - // Check to make sure we are using a version of desktop which has the WDCBridge_Api_schemaCallbackEx defined - if (!!this.nativeApiRootObj.WDCBridge_Api_schemaCallbackEx) { - // Providing standardConnections is optional but we can't pass undefined back because Qt will choke - this.nativeApiRootObj.WDCBridge_Api_schemaCallbackEx.api(schema, standardConnections || []); - } else { - this.nativeApiRootObj.WDCBridge_Api_schemaCallback.api(schema); - } -} - -NativeDispatcher.prototype._tableDataCallback = function(tableName, data) { - this.nativeApiRootObj.WDCBridge_Api_tableDataCallback.api(tableName, data); -} - -NativeDispatcher.prototype._reportProgress = function (progress) { - // Report progress was added in 2.1 so it may not be available if Tableau only knows 2.0 - if (!!this.nativeApiRootObj.WDCBridge_Api_reportProgress) { - this.nativeApiRootObj.WDCBridge_Api_reportProgress.api(progress); - } else { - console.log("reportProgress not available from this Tableau version"); - } -} -NativeDispatcher.prototype._dataDoneCallback = function() { - this.nativeApiRootObj.WDCBridge_Api_dataDoneCallback.api(); +/** + * Used for communicating between Tableau desktop/server and the WDC's + * Javascript. is predominantly a pass-through to the Qt WebBridge methods + */ +class NativeDispatcher { + + /** + * + * @param {Object} nativeApiRootObj - The root object where the native Api methods are available. + * For WebKit, 'this' is window. + * + */ + constructor (nativeApiRootObj) { + this.nativeApiRootObj = nativeApiRootObj; + this._initPublicInterface(); + this._initPrivateInterface(); + } + + /** + * @returns {Undefined} + */ + _initPublicInterface () { + + console.log('Initializing public interface for NativeDispatcher'); + + this._submitCalled = false; + + this.publicInterface = { + abortForAuth: this._abortForAuth.bind(this), + abortWithError: this._abortWithError.bind(this), + addCrossOriginException: this._addCrossOriginException.bind(this), + log: this._log.bind(this), + submit: this._submit.bind(this), + reportProgress: this._reportProgress.bind(this) + }; + + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableau.abortforauth + * + * @param {String} msg + * @returns {Undefined} + */ + _abortForAuth (msg) { + this.nativeApiRootObj.WDCBridge_Api_abortForAuth.api(msg); + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableau.abortwitherror + * + * @param {String} msg + * @returns {Undefined} + */ + _abortWithError (msg) { + this.nativeApiRootObj.WDCBridge_Api_abortWithError.api(msg); + } + + /** + * Missing documentation online, we need to add one + * + * @param {Array} destOriginList + * @returns {Undefined} + */ + _addCrossOriginException (destOriginList) { + this.nativeApiRootObj.WDCBridge_Api_addCrossOriginException.api(destOriginList); + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableau.log + * + * @param {String} msg + * @returns {Undefined} + */ + _log (msg) { + this.nativeApiRootObj.WDCBridge_Api_log.api(msg); + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableau.submit + * + * @returns {Undefined} + */ + _submit () { + + if (this._submitCalled) { + console.log('submit called more than once'); + return; + } + + this._submitCalled = true; + this.nativeApiRootObj.WDCBridge_Api_submit.api(); + } + + /** + * @returns {Undefined} + */ + _initPrivateInterface () { + console.log('Initializing private interface for NativeDispatcher'); + + this._initCallbackCalled = false; + this._shutdownCallbackCalled = false; + + this.privateInterface = { + _initCallback: this._initCallback.bind(this), + _shutdownCallback: this._shutdownCallback.bind(this), + _schemaCallback: this._schemaCallback.bind(this), + _tableDataCallback: this._tableDataCallback.bind(this), + _dataDoneCallback: this._dataDoneCallback.bind(this) + }; + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.initcallback + * + * @returns {Undefined} + */ + _initCallback () { + + if (this._initCallbackCalled) { + console.log('initCallback called more than once'); + return; + } + + this._initCallbackCalled = true; + this.nativeApiRootObj.WDCBridge_Api_initCallback.api(); + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.shutdowncallback + * + * @returns {Undefined} + */ + _shutdownCallback () { + + if (this._shutdownCallbackCalled) { + console.log('shutdownCallback called more than once'); + return; + } + + this._shutdownCallbackCalled = true; + this.nativeApiRootObj.WDCBridge_Api_shutdownCallback.api(); + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.schemacallback + * + * @param {Array} schema TableInfo @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableinfo-1 + * @param {Array} standardConnections StandardConnection @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.standardconnection + * @returns {Undefined} + */ + _schemaCallback (schema, standardConnections = []) { + + // Check to make sure we are using a version of desktop which has the WDCBridge_Api_schemaCallbackEx defined + let schemaCallbackExAvailable = !!this.nativeApiRootObj.WDCBridge_Api_schemaCallbackEx; + + if (schemaCallbackExAvailable) { + + // Providing standardConnections is optional but we can't pass undefined back because Qt will choke + this.nativeApiRootObj.WDCBridge_Api_schemaCallbackEx.api(schema, standardConnections); + + } else { + this.nativeApiRootObj.WDCBridge_Api_schemaCallback.api(schema); + } + } + + /** + * + * @param {String} tableName + * @param {*} data + * @returns {Undefined} + */ + _tableDataCallback (tableName, data) { + this.nativeApiRootObj.WDCBridge_Api_tableDataCallback.api(tableName, data); + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableau.reportProgress + * + * @param {String} progressMessage + * @returns {Undefined} + */ + _reportProgress (progressMessage) { + + // Report progress was added in 2.1 so it may not be available if Tableau only knows 2.0 + let reportProgressAvailable = !!this.nativeApiRootObj.WDCBridge_Api_reportProgress; + + if (reportProgressAvailable) { + + this.nativeApiRootObj.WDCBridge_Api_reportProgress.api(progressMessage); + + } else { + console.log('reportProgress not available from this Tableau version'); + } + } + + /** + * @returns {Undefined} + */ + _dataDoneCallback () { + this.nativeApiRootObj.WDCBridge_Api_dataDoneCallback.api(); + } } -module.exports = NativeDispatcher; +export default NativeDispatcher; diff --git a/NativeDispatcher.test.js b/NativeDispatcher.test.js new file mode 100644 index 0000000..d0f28cb --- /dev/null +++ b/NativeDispatcher.test.js @@ -0,0 +1,198 @@ +/* eslint-env node, mocha, jest */ +import NativeDispatcher from './NativeDispatcher'; + +let nativeApiRootObj; +let nativeDispatcher; + +// set for better visual hint on tests +const wdcBridgeApiPrefix = 'WDCBridge_Api'; + +let publicInterfaceMethodsNames = ['abortForAuth', 'abortWithError', 'addCrossOriginException', 'log', 'submit', 'reportProgress']; +let privateInterfaceMethodsNames = ['_initCallback', '_shutdownCallback', '_schemaCallback', '_tableDataCallback', '_dataDoneCallback']; + +// use if required for debugging +let consoleLog = console.log; // eslint-disable-line no-unused-vars +console.log = jest.fn(); + +beforeEach(() => { + + nativeApiRootObj = { + [`${wdcBridgeApiPrefix}_abortForAuth`]: { api: jest.fn() }, + [`${wdcBridgeApiPrefix}_abortWithError`]: { api: jest.fn() }, + [`${wdcBridgeApiPrefix}_addCrossOriginException`]: { api: jest.fn() }, + [`${wdcBridgeApiPrefix}_log`]: { api: jest.fn() }, + [`${wdcBridgeApiPrefix}_submit`]: { api: jest.fn() }, + [`${wdcBridgeApiPrefix}_initCallback`]: { api: jest.fn() }, + [`${wdcBridgeApiPrefix}_shutdownCallback`]: { api: jest.fn() }, + [`${wdcBridgeApiPrefix}_schemaCallbackEx`]: { api: jest.fn() }, + [`${wdcBridgeApiPrefix}_schemaCallback`]: { api: jest.fn() }, + [`${wdcBridgeApiPrefix}_tableDataCallback`]: { api: jest.fn() }, + [`${wdcBridgeApiPrefix}_reportProgress`]: { api: jest.fn() }, + [`${wdcBridgeApiPrefix}_dataDoneCallback`]: { api: jest.fn() } + }; + + nativeDispatcher = new NativeDispatcher(nativeApiRootObj); +}); + +describe('UNIT - NativeDispatcher', () => { + + it('NativeDispatcher should initPublicInterface and initPrivateInterface correctly upon instantiation', () => { + + expect(nativeDispatcher).toBeInstanceOf(NativeDispatcher); + + // all public interface methods were created + expect(Object.keys(nativeDispatcher.publicInterface)).toEqual(publicInterfaceMethodsNames); + + for (let methodName of publicInterfaceMethodsNames) { + expect(typeof nativeDispatcher.publicInterface[methodName]).toBe('function'); + } + + // all private interface methods were created + expect(Object.keys(nativeDispatcher.privateInterface)).toEqual(privateInterfaceMethodsNames); + + for (let methodName of privateInterfaceMethodsNames) { + expect(typeof nativeDispatcher.privateInterface[methodName]).toBe('function'); + } + + // initial state values were set + expect(nativeDispatcher._submitCalled).toBe(false); + expect(nativeDispatcher._initCallbackCalled).toBe(false); + expect(nativeDispatcher._shutdownCallbackCalled).toBe(false); + + }); + + it('_abortForAuth to call WDCBridge_Api_***.api with the correct arguments values', () => { + + nativeDispatcher._abortForAuth('01'); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_abortForAuth`].api).not.toBeCalledWith('011'); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_abortForAuth`].api).toBeCalledWith('01'); + }); + + it('_abortWithError to call WDCBridge_Api_***.api with the correct arguments values', () => { + + nativeDispatcher._abortWithError('02'); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_abortWithError`].api).not.toBeCalledWith('022'); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_abortWithError`].api).toBeCalledWith('02'); + }); + + it('_addCrossOriginException to call WDCBridge_Api_***.api with the correct arguments values', () => { + let list = [1, 2, 3]; + + nativeDispatcher._addCrossOriginException(list); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_addCrossOriginException`].api).not.toBeCalledWith([]); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_addCrossOriginException`].api).toBeCalledWith(list); + }); + + it('_log to call WDCBridge_Api_***.api with the correct arguments values', () => { + + nativeDispatcher._log(5); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_log`].api).not.toBeCalledWith(6); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_log`].api).toBeCalledWith(5); + }); + + it('_submit to call WDCBridge_Api_***.api just once', () => { + + expect(nativeDispatcher._submitCalled).toBe(false); + + nativeDispatcher._submit(); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_submit`].api).toBeCalled(); + + expect(nativeDispatcher._submitCalled).toBe(true); + + expect(nativeDispatcher._submit()).toBeUndefined(); + // check if message was logged + expect(console.log).toBeCalledWith('submit called more than once'); + // check WDCBridge_Api_submit wasn't called twice + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_submit`].api.mock.calls.length).toBe(1); + + }); + + it('_initCallback to call WDCBridge_Api_***.api just once', () => { + + expect(nativeDispatcher._initCallbackCalled).toBe(false); + + nativeDispatcher._initCallback(); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_initCallback`].api).toBeCalled(); + + expect(nativeDispatcher._initCallbackCalled).toBe(true); + + expect(nativeDispatcher._initCallback()).toBeUndefined(); + // check if message was logged + expect(console.log).toBeCalledWith('initCallback called more than once'); + // check WDCBridge_Api_initCallback wasn't called twice + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_initCallback`].api.mock.calls.length).toBe(1); + + }); + + it('_shutdownCallback to call WDCBridge_Api_***.api just once', () => { + + expect(nativeDispatcher._shutdownCallbackCalled).toBe(false); + + nativeDispatcher._shutdownCallback(); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_shutdownCallback`].api).toBeCalled(); + + expect(nativeDispatcher._shutdownCallbackCalled).toBe(true); + + expect(nativeDispatcher._shutdownCallback()).toBeUndefined(); + // check if message was logged + expect(console.log).toBeCalledWith('shutdownCallback called more than once'); + // check WDCBridge_Api_shutdownCallback wasn't called twice + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_shutdownCallback`].api.mock.calls.length).toBe(1); + + }); + + it('_schemaCallback to call WDCBridge_Api_schemaCallbackEx.api with the correct arguments values', () => { + let schema = { prop: 1 }; + let standardConnections = [2]; + + nativeDispatcher._schemaCallback(schema, standardConnections); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_schemaCallbackEx`].api).not.toBeCalledWith({}, []); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_schemaCallbackEx`].api).toBeCalledWith(schema, standardConnections); + }); + + it('_schemaCallback to fallback to WDCBridge_Api_schemaCallback with only schema if WDCBridge_Api_schemaCallbackEx is not defined', () => { + let schema = { prop: 1 }; + let standardConnections = [2]; + + nativeApiRootObj[`${wdcBridgeApiPrefix}_schemaCallbackEx`] = null; + + nativeDispatcher._schemaCallback(schema, standardConnections); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_schemaCallback`].api).not.toBeCalledWith({}, []); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_schemaCallback`].api).not.toBeCalledWith(schema, standardConnections); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_schemaCallback`].api).toBeCalledWith(schema); + }); + + it('_tableDataCallback to call WDCBridge_Api_***.api with the correct arguments values', () => { + let tableName = 'Sample'; + let data = [2]; + + nativeDispatcher._tableDataCallback(tableName, data); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_tableDataCallback`].api).not.toBeCalledWith('', []); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_tableDataCallback`].api).toBeCalledWith(tableName, data); + }); + + it('_reportProgress to call WDCBridge_Api_***.api if available', () => { + let progress = 'Sample progress string'; + + nativeDispatcher._reportProgress(progress); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_reportProgress`].api).not.toBeCalledWith('blabla'); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_reportProgress`].api).toBeCalledWith(progress); + }); + + it('_reportProgress to log message if WDCBridge_Api_reportProgress.api is not defined', () => { + let progress = 'Sample progress string'; + + nativeApiRootObj[`${wdcBridgeApiPrefix}_reportProgress`] = null; + + nativeDispatcher._reportProgress(progress); + // check if message was logged + expect(console.log).toBeCalledWith('reportProgress not available from this Tableau version'); + }); + + it('_dataDoneCallback to call WDCBridge_Api_***.api', () => { + + nativeDispatcher._dataDoneCallback(); + expect(nativeApiRootObj[`${wdcBridgeApiPrefix}_dataDoneCallback`].api).toBeCalled(); + + }); +}); diff --git a/Shared.js b/Shared.js index 93bca32..290b223 100644 --- a/Shared.js +++ b/Shared.js @@ -1,156 +1,242 @@ -var Table = require('./Table.js'); -var Enums = require('./Enums.js'); -/** @class This class represents the shared parts of the javascript +import { applyEnums } from './Enums'; +import Table from './Table'; + +/** +* This class represents the shared parts of the javascript * library which do not have any dependence on whether we are running in * the simulator, in Tableau, or anywhere else -* @param tableauApiObj {Object} - The already created tableau API object (usually window.tableau) -* @param privateApiObj {Object} - The already created private API object (usually window._tableau) -* @param globalObj {Object} - The global object to attach things to (usually window) */ -function Shared (tableauApiObj, privateApiObj, globalObj) { - this.privateApiObj = privateApiObj; - this.globalObj = globalObj; - this._hasAlreadyThrownErrorSoDontThrowAgain = false; - - this.changeTableauApiObj(tableauApiObj); -} - +class Shared { + + /** + * + * @param {Object} tableauApiObj - The already created tableau API object (usually window.tableau) + * @param {Object} privateApiObj - The already created private API object (usually window._tableau) + * @param {Object} globalObj - The global object to attach things to (usually window) + */ + constructor (tableauApiObj, privateApiObj, globalObj) { + this.privateApiObj = privateApiObj; + this.globalObj = globalObj; + this._hasAlreadyThrownErrorSoDontThrowAgain = false; + + this.changeTableauApiObj(tableauApiObj); + } -Shared.prototype.init = function() { - console.log("Initializing shared WDC"); - this.globalObj.onerror = this._errorHandler.bind(this); + /** + * @returns {Undefined} + */ + init () { + console.log('Initializing shared WDC'); - // Initialize the functions which will be invoked by the native code - this._initTriggerFunctions(); + this.globalObj.onerror = this._errorHandler.bind(this); - // Assign the deprecated functions which aren't availible in this version of the API - this._initDeprecatedFunctions(); -} + // Initialize the functions which will be invoked by the native code + this._initTriggerFunctions(); -Shared.prototype.changeTableauApiObj = function(tableauApiObj) { - this.tableauApiObj = tableauApiObj; + // Assign the deprecated functions which aren't availible in this version of the API + this._initDeprecatedFunctions(); + } - // Assign our make & register functions right away because a connector can use - // them immediately, even before bootstrapping has completed - this.tableauApiObj.makeConnector = this._makeConnector.bind(this); - this.tableauApiObj.registerConnector = this._registerConnector.bind(this); + /** + * + * @param {Object} tableauApiObj + * @returns {Undefined} + */ + changeTableauApiObj (tableauApiObj) { + this.tableauApiObj = tableauApiObj; - Enums.apply(this.tableauApiObj); -} + // Assign our make & register functions right away because a connector can use + // them immediately, even before bootstrapping has completed + this.tableauApiObj.makeConnector = this._makeConnector.bind(this); + this.tableauApiObj.registerConnector = this._registerConnector.bind(this); -Shared.prototype._errorHandler = function(message, file, line, column, errorObj) { - console.error(errorObj); // print error for debugging in the browser - if (this._hasAlreadyThrownErrorSoDontThrowAgain) { - return true; - } - - var msg = message; - if(errorObj) { - msg += " stack:" + errorObj.stack; - } else { - msg += " file: " + file; - msg += " line: " + line; - } - - if (this.tableauApiObj && this.tableauApiObj.abortWithError) { - this.tableauApiObj.abortWithError(msg); - } else { - throw msg; - } - - this._hasAlreadyThrownErrorSoDontThrowAgain = true; - return true; -} + applyEnums(this.tableauApiObj); + } -Shared.prototype._makeConnector = function() { - var defaultImpls = { - init: function(cb) { cb(); }, - shutdown: function(cb) { cb(); } - }; + /** + * + * @param {String} message + * @param {String} file + * @param {String} line + * @param {String} column + * @param {Object} errorObj + * @returns {Boolean} + */ + _errorHandler (message, file, line, column, errorObj) { + // print error for debugging in the browser + console.error(errorObj); + + if (this._hasAlreadyThrownErrorSoDontThrowAgain) { + console.log('Error already thrown'); + return true; + } + + let msg = message; + + if (errorObj) { + msg += ' stack:' + errorObj.stack; + } else { + msg += ' file: ' + file; + msg += ' line: ' + line; + } + + if (this.tableauApiObj && this.tableauApiObj.abortWithError) { + this.tableauApiObj.abortWithError(msg); + } else { + throw msg; + } + + this._hasAlreadyThrownErrorSoDontThrowAgain = true; + + return true; + } - return defaultImpls; -} + /** + * @returns {Object} + */ + _makeConnector () { -Shared.prototype._registerConnector = function (wdc) { + let defaultImpls = { + init: function (cb) { cb(); }, + shutdown: function (cb) { cb(); } + }; - // do some error checking on the wdc - var functionNames = ["init", "shutdown", "getSchema", "getData"]; - for (var ii = functionNames.length - 1; ii >= 0; ii--) { - if (typeof(wdc[functionNames[ii]]) !== "function") { - throw "The connector did not define the required function: " + functionNames[ii]; + return defaultImpls; } - }; - console.log("Connector registered"); - - this.globalObj._wdc = wdc; - this._wdc = wdc; -} + /** + * + * @param {Object} wdc + * @returns {Undefined} + */ + _registerConnector (wdc) { + // do some error checking on the wdc + const FUNCTION_NAMES = ['init', 'shutdown', 'getSchema', 'getData']; + + for (let ii = FUNCTION_NAMES.length - 1; ii >= 0; ii--) { + if (typeof (wdc[FUNCTION_NAMES[ii]]) !== 'function') { + throw new Error(`The connector did not define the required function: ${FUNCTION_NAMES[ii]}`); + } + } + + console.log('Connector registered'); + + this.globalObj._wdc = wdc; + this._wdc = wdc; + } -Shared.prototype._initTriggerFunctions = function() { - this.privateApiObj.triggerInitialization = this._triggerInitialization.bind(this); - this.privateApiObj.triggerSchemaGathering = this._triggerSchemaGathering.bind(this); - this.privateApiObj.triggerDataGathering = this._triggerDataGathering.bind(this); - this.privateApiObj.triggerShutdown = this._triggerShutdown.bind(this); -} + /** + * @returns {Undefined} + */ + _initTriggerFunctions () { + this.privateApiObj.triggerInitialization = this._triggerInitialization.bind(this); + this.privateApiObj.triggerSchemaGathering = this._triggerSchemaGathering.bind(this); + this.privateApiObj.triggerDataGathering = this._triggerDataGathering.bind(this); + this.privateApiObj.triggerShutdown = this._triggerShutdown.bind(this); + } -// Starts the WDC -Shared.prototype._triggerInitialization = function() { - this._wdc.init(this.privateApiObj._initCallback); -} + /** + * Starts the WDC + * @returns {Undefined} + */ + _triggerInitialization () { + this._wdc.init(this.privateApiObj._initCallback); + } -// Starts the schema gathering process -Shared.prototype._triggerSchemaGathering = function() { - this._wdc.getSchema(this.privateApiObj._schemaCallback); -} + /** + * Starts the schema gathering process + * @returns {Undefined} + */ + _triggerSchemaGathering () { + this._wdc.getSchema(this.privateApiObj._schemaCallback); + } -// Starts the data gathering process -Shared.prototype._triggerDataGathering = function(tablesAndIncrementValues) { - if (tablesAndIncrementValues.length != 1) { - throw ("Unexpected number of tables specified. Expected 1, actual " + tablesAndIncrementValues.length.toString()); - } - - var tableAndIncremntValue = tablesAndIncrementValues[0]; - var isJoinFiltered = !!tableAndIncremntValue.filterColumnId; - var table = new Table( - tableAndIncremntValue.tableInfo, - tableAndIncremntValue.incrementValue, - isJoinFiltered, - tableAndIncremntValue.filterColumnId || '', - tableAndIncremntValue.filterValues || [], - this.privateApiObj._tableDataCallback); - - this._wdc.getData(table, this.privateApiObj._dataDoneCallback); -} + /** + * Starts the data gathering process + * @param {Array} tablesAndIncrementValues + * @returns {undefined} + */ + _triggerDataGathering (tablesAndIncrementValues) { + + if (tablesAndIncrementValues.length !== 1) { + throw new Error(`Unexpected number of tables specified. Expected 1, actual ${tablesAndIncrementValues.length}`); + } + + let tableAndIncrementValue = tablesAndIncrementValues[0]; + let isJoinFiltered = !!tableAndIncrementValue.filterColumnId; + + let table = new Table( + tableAndIncrementValue.tableInfo, + tableAndIncrementValue.incrementValue, + isJoinFiltered, + tableAndIncrementValue.filterColumnId, + tableAndIncrementValue.filterValues, + this.privateApiObj._tableDataCallback + ); + + this._wdc.getData(table, this.privateApiObj._dataDoneCallback); + } -// Tells the WDC it's time to shut down -Shared.prototype._triggerShutdown = function() { - this._wdc.shutdown(this.privateApiObj._shutdownCallback); -} + /** + * Tells the WDC it's time to shut down + * @returns {Undefined} + */ + _triggerShutdown () { + this._wdc.shutdown(this.privateApiObj._shutdownCallback); + } -// Initializes a series of global callbacks which have been deprecated in version 2.0.0 -Shared.prototype._initDeprecatedFunctions = function() { - this.tableauApiObj.initCallback = this._initCallback.bind(this); - this.tableauApiObj.headersCallback = this._headersCallback.bind(this); - this.tableauApiObj.dataCallback = this._dataCallback.bind(this); - this.tableauApiObj.shutdownCallback = this._shutdownCallback.bind(this); -} + /** + * Initializes a series of global callbacks which have been deprecated in version 2.0.0 + * @returns {Undefined} + */ + _initDeprecatedFunctions () { + this.tableauApiObj.initCallback = this._initCallback.bind(this); + this.tableauApiObj.headersCallback = this._headersCallback.bind(this); + this.tableauApiObj.dataCallback = this._dataCallback.bind(this); + this.tableauApiObj.shutdownCallback = this._shutdownCallback.bind(this); + } -Shared.prototype._initCallback = function () { - this.tableauApiObj.abortWithError("tableau.initCallback has been deprecated in version 2.0.0. Please use the callback function passed to init"); -}; + /** + * @deprecated Since v2.0.0 + * + * @returns {Undefined} + */ + _initCallback () { + this.tableauApiObj.abortWithError('tableau.initCallback has been deprecated in version 2.0.0. Please use the callback function passed to init'); + } -Shared.prototype._headersCallback = function (fieldNames, types) { - this.tableauApiObj.abortWithError("tableau.headersCallback has been deprecated in version 2.0.0"); -}; + /** + * @deprecated Since v2.0.0 + * + * @param {Array} fieldNames + * @param {Array} types + * @returns {Undefined} + */ + _headersCallback (fieldNames, types) { // eslint-disable-line no-unused-vars + this.tableauApiObj.abortWithError('tableau.headersCallback has been deprecated in version 2.0.0'); + } -Shared.prototype._dataCallback = function (data, lastRecordToken, moreData) { - this.tableauApiObj.abortWithError("tableau.dataCallback has been deprecated in version 2.0.0"); -}; + /** + * @deprecated Since v2.0.0 + * + * @param {*} data + * @param {*} lastRecordToken + * @param {*} moreData + * @returns {Undefined} + */ + _dataCallback (data, lastRecordToken, moreData = null) { // eslint-disable-line no-unused-vars + this.tableauApiObj.abortWithError('tableau.dataCallback has been deprecated in version 2.0.0'); + } -Shared.prototype._shutdownCallback = function () { - this.tableauApiObj.abortWithError("tableau.shutdownCallback has been deprecated in version 2.0.0. Please use the callback function passed to shutdown"); -}; + /** + * @deprecated Since v2.0.0 + * + * @returns {Undefined} + */ + _shutdownCallback () { + this.tableauApiObj.abortWithError('tableau.shutdownCallback has been deprecated in version 2.0.0. Please use the callback function passed to shutdown'); + } +} -module.exports = Shared; +export default Shared; diff --git a/SimulatorDispatcher.js b/SimulatorDispatcher.js index bb482a3..e7b54a7 100644 --- a/SimulatorDispatcher.js +++ b/SimulatorDispatcher.js @@ -1,314 +1,484 @@ -var ApprovedOrigins = require('./ApprovedOrigins.js'); +/* globals require */ +import * as ApprovedOrigins from './ApprovedOrigins'; +import { getBuildNumber } from './DevUtils/BuildNumber'; + +import deStringsMap from './resources/Shim_lib_resources_de-DE.json'; +import enStringsMap from './resources/Shim_lib_resources_en-US.json'; +import esStringsMap from './resources/Shim_lib_resources_es-ES.json'; +import jaStringsMap from './resources/Shim_lib_resources_ja-JP.json'; +import frStringsMap from './resources/Shim_lib_resources_fr-FR.json'; +import koStringsMap from './resources/Shim_lib_resources_ko-KR.json'; +import ptStringsMap from './resources/Shim_lib_resources_pt-BR.json'; +import zhStringsMap from './resources/Shim_lib_resources_zh-CN.json'; // Required for IE & Edge which don't support endsWith -require('String.prototype.endsWith'); - -/** @class Used for communicating between the simulator and web data connector. It does -* this by passing messages between the WDC window and its parent window -* @param globalObj {Object} - the global object to find tableau interfaces as well -* as register events (usually window) -*/ -function SimulatorDispatcher (globalObj) { - this.globalObj = globalObj; - this._initMessageHandling(); - this._initPublicInterface(); - this._initPrivateInterface(); -} +// require('String.prototype.endsWith'); // now using babel-polyfill + +const BUILD_NUMBER = getBuildNumber(); + +/** + * Used for communicating between the simulator and web data connector. It does + * this by passing messages between the WDC window and its parent window + */ +class SimulatorDispatcher { + + /** + * + * @param {Object} globalObj - the global object to find tableau interfaces as well + * as register events (usually window) + */ + constructor (globalObj) { + this.globalObj = globalObj; + + this._initMessageHandling(); + this._initPublicInterface(); + this._initPrivateInterface(); + } -SimulatorDispatcher.prototype._initMessageHandling = function() { - console.log("Initializing message handling"); - this.globalObj.addEventListener('message', this._receiveMessage.bind(this), false); - this.globalObj.document.addEventListener("DOMContentLoaded", this._onDomContentLoaded.bind(this)); -} + /** + * @returns {Undefined} + */ + _initMessageHandling () { + console.log('Initializing message handling'); -SimulatorDispatcher.prototype._onDomContentLoaded = function() { - // Attempt to notify the simulator window that the WDC has loaded - if(this.globalObj.parent !== window) { - this.globalObj.parent.postMessage(this._buildMessagePayload('loaded'), '*'); - } - - if(this.globalObj.opener) { - try { // Wrap in try/catch for older versions of IE - this.globalObj.opener.postMessage(this._buildMessagePayload('loaded'), '*'); - } catch(e) { - console.warn('Some versions of IE may not accurately simulate the Web Data Connector. Please retry on a Webkit based browser'); + this.globalObj.addEventListener('message', this._receiveMessage.bind(this), false); + this.globalObj.document.addEventListener('DOMContentLoaded', this._onDomContentLoaded.bind(this)); } - } -} -SimulatorDispatcher.prototype._packagePropertyValues = function() { - var propValues = { - "connectionName": this.globalObj.tableau.connectionName, - "connectionData": this.globalObj.tableau.connectionData, - "password": this.globalObj.tableau.password, - "username": this.globalObj.tableau.username, - "usernameAlias": this.globalObj.tableau.usernameAlias, - "incrementalExtractColumn": this.globalObj.tableau.incrementalExtractColumn, - "versionNumber": this.globalObj.tableau.versionNumber, - "locale": this.globalObj.tableau.locale, - "authPurpose": this.globalObj.tableau.authPurpose, - "platformOS": this.globalObj.tableau.platformOS, - "platformVersion": this.globalObj.tableau.platformVersion, - "platformEdition": this.globalObj.tableau.platformEdition, - "platformBuildNumber": this.globalObj.tableau.platformBuildNumber - }; - - return propValues; -} + /** + * @returns {Undefined} + */ + _onDomContentLoaded () { + + // Attempt to notify the simulator window that the WDC has loaded + if (this.globalObj.parent !== window) { + this.globalObj.parent.postMessage(this._buildMessagePayload('loaded'), '*'); + } + + if (this.globalObj.opener) { + // Wrap in try/catch for older versions of IE + try { + this.globalObj.opener.postMessage(this._buildMessagePayload('loaded'), '*'); + } catch (e) { + console.warn('Some versions of IE may not accurately simulate the Web Data Connector. Please retry on a Webkit based browser'); + } + } -SimulatorDispatcher.prototype._applyPropertyValues = function(props) { - if (props) { - this.globalObj.tableau.connectionName = props.connectionName; - this.globalObj.tableau.connectionData = props.connectionData; - this.globalObj.tableau.password = props.password; - this.globalObj.tableau.username = props.username; - this.globalObj.tableau.usernameAlias = props.usernameAlias; - this.globalObj.tableau.incrementalExtractColumn = props.incrementalExtractColumn; - this.globalObj.tableau.locale = props.locale; - this.globalObj.tableau.language = props.locale; - this.globalObj.tableau.authPurpose = props.authPurpose; - this.globalObj.tableau.platformOS = props.platformOS; - this.globalObj.tableau.platformVersion = props.platformVersion; - this.globalObj.tableau.platformEdition = props.platformEdition; - this.globalObj.tableau.platformBuildNumber = props.platformBuildNumber; - } -} + } -SimulatorDispatcher.prototype._buildMessagePayload = function(msgName, msgData, props) { - var msgObj = {"msgName": msgName, "msgData": msgData, "props": props, "version": BUILD_NUMBER }; - return JSON.stringify(msgObj); -} + /** + * @returns {Object} + */ + _packagePropertyValues () { + let propValues = { + 'connectionName': this.globalObj.tableau.connectionName, + 'connectionData': this.globalObj.tableau.connectionData, + 'password': this.globalObj.tableau.password, + 'username': this.globalObj.tableau.username, + 'usernameAlias': this.globalObj.tableau.usernameAlias, + 'incrementalExtractColumn': this.globalObj.tableau.incrementalExtractColumn, + 'versionNumber': this.globalObj.tableau.versionNumber, + 'locale': this.globalObj.tableau.locale, + 'authPurpose': this.globalObj.tableau.authPurpose, + 'platformOS': this.globalObj.tableau.platformOS, + 'platformVersion': this.globalObj.tableau.platformVersion, + 'platformEdition': this.globalObj.tableau.platformEdition, + 'platformBuildNumber': this.globalObj.tableau.platformBuildNumber + }; + return propValues; + } -SimulatorDispatcher.prototype._sendMessage = function(msgName, msgData) { - var messagePayload = this._buildMessagePayload(msgName, msgData, this._packagePropertyValues()); - - // Check first to see if we have a messageHandler defined to post the message to - if (typeof this.globalObj.webkit != 'undefined' && - typeof this.globalObj.webkit.messageHandlers != 'undefined' && - typeof this.globalObj.webkit.messageHandlers.wdcHandler != 'undefined') { - this.globalObj.webkit.messageHandlers.wdcHandler.postMessage(messagePayload); - } else if (!this._sourceWindow) { - throw "Looks like the WDC is calling a tableau function before tableau.init() has been called." - } else { - // Make sure we only post this info back to the source origin the user approved in _getWebSecurityWarningConfirm - this._sourceWindow.postMessage(messagePayload, this._sourceOrigin); - } -} + /** + * + * @param {Object} props + * @returns {Undefined} + */ + _applyPropertyValues (props) { + if (props) { + this.globalObj.tableau.connectionName = props.connectionName; + this.globalObj.tableau.connectionData = props.connectionData; + this.globalObj.tableau.password = props.password; + this.globalObj.tableau.username = props.username; + this.globalObj.tableau.usernameAlias = props.usernameAlias; + this.globalObj.tableau.incrementalExtractColumn = props.incrementalExtractColumn; + this.globalObj.tableau.locale = props.locale; + this.globalObj.tableau.language = props.locale; + this.globalObj.tableau.authPurpose = props.authPurpose; + this.globalObj.tableau.platformOS = props.platformOS; + this.globalObj.tableau.platformVersion = props.platformVersion; + this.globalObj.tableau.platformEdition = props.platformEdition; + this.globalObj.tableau.platformBuildNumber = props.platformBuildNumber; + } + } -SimulatorDispatcher.prototype._getPayloadObj = function(payloadString) { - var payload = null; - try { - payload = JSON.parse(payloadString); - } catch(e) { - return null; - } + /** + * + * @param {String} msgName + * @param {String} msgData + * @param {String} props + * @returns {String} + */ + _buildMessagePayload (msgName, msgData, props) { - return payload; -} + let msgObj = { 'msgName': msgName, 'msgData': msgData, 'props': props, 'version': BUILD_NUMBER }; -SimulatorDispatcher.prototype._getWebSecurityWarningConfirm = function() { - // Due to cross-origin security issues over https, we may not be able to retrieve _sourceWindow. - // Use sourceOrigin instead. - var origin = this._sourceOrigin; - - var Uri = require('jsuri'); - var parsedOrigin = new Uri(origin); - var hostName = parsedOrigin.host(); - - var supportedHosts = ["localhost", "tableau.github.io"]; - if (supportedHosts.indexOf(hostName) >= 0) { - return true; - } - - // Whitelist Tableau domains - if (hostName && hostName.endsWith("online.tableau.com")) { - return true; - } - - var alreadyApprovedOrigins = ApprovedOrigins.getApprovedOrigins(); - if (alreadyApprovedOrigins.indexOf(origin) >= 0) { - // The user has already approved this origin, no need to ask again - console.log("Already approved the origin'" + origin + "', not asking again"); - return true; - } - - var localizedWarningTitle = this._getLocalizedString("webSecurityWarning"); - var completeWarningMsg = localizedWarningTitle + "\n\n" + hostName + "\n"; - var isConfirmed = confirm(completeWarningMsg); - - if (isConfirmed) { - // Set a session cookie to mark that we've approved this already - ApprovedOrigins.addApprovedOrigin(origin); - } - - return isConfirmed; -} + return JSON.stringify(msgObj); + } + + /** + * + * @param {*} msgName + * @param {*} msgData + * @returns {Undefined} + */ + _sendMessage (msgName, msgData) { + + let messagePayload = this._buildMessagePayload(msgName, msgData, this._packagePropertyValues()); + + let wdcHandler; + + try { + // try to use webkit.messageHandlers but don't fail since we have a fallback + wdcHandler = this.globalObj.webkit.messageHandlers.wdcHandler; -SimulatorDispatcher.prototype._getCurrentLocale = function() { - // Use current browser's locale to get a localized warning message - var currentBrowserLanguage = (navigator.language || navigator.userLanguage); - var locale = currentBrowserLanguage? currentBrowserLanguage.substring(0, 2): "en"; + } catch (e) { - var supportedLocales = ["de", "en", "es", "fr", "ja", "ko", "pt", "zh"]; - // Fall back to English for other unsupported lanaguages - if (supportedLocales.indexOf(locale) < 0) { - locale = 'en'; + // wdcHandler not found on webkit.messageHandlers to post messagePayload + // we might be on the simulator + + } + + // Check first to see if we have a wdcHandler defined on messageHandlers to post the message to + if (wdcHandler) { + + wdcHandler.postMessage(messagePayload); + + } else if (!this._sourceWindow) { + + throw new Error('Looks like the WDC is calling a tableau function before tableau.init() has been called.'); + + } else { + + // Make sure we only post this info back to the source origin the user approved in _getWebSecurityWarningConfirm + this._sourceWindow.postMessage(messagePayload, this._sourceOrigin); + + } } - return locale; -} + /** + * + * @param {String} payloadString + * @returns {Object|null} + */ + _getPayloadObj (payloadString) { + let payload; -SimulatorDispatcher.prototype._getLocalizedString = function(stringKey) { - var locale = this._getCurrentLocale(); - - // Use static require here, otherwise webpack would generate a much bigger JS file - var deStringsMap = require('json!./resources/Shim_lib_resources_de-DE.json'); - var enStringsMap = require('json!./resources/Shim_lib_resources_en-US.json'); - var esStringsMap = require('json!./resources/Shim_lib_resources_es-ES.json'); - var jaStringsMap = require('json!./resources/Shim_lib_resources_ja-JP.json'); - var frStringsMap = require('json!./resources/Shim_lib_resources_fr-FR.json'); - var koStringsMap = require('json!./resources/Shim_lib_resources_ko-KR.json'); - var ptStringsMap = require('json!./resources/Shim_lib_resources_pt-BR.json'); - var zhStringsMap = require('json!./resources/Shim_lib_resources_zh-CN.json'); - - var stringJsonMapByLocale = - { - "de": deStringsMap, - "en": enStringsMap, - "es": esStringsMap, - "fr": frStringsMap, - "ja": jaStringsMap, - "ko": koStringsMap, - "pt": ptStringsMap, - "zh": zhStringsMap - }; - - var localizedStringsJson = stringJsonMapByLocale[locale]; - return localizedStringsJson[stringKey]; -} + try { -SimulatorDispatcher.prototype._receiveMessage = function(evt) { - console.log("Received message!"); - - var wdc = this.globalObj._wdc; - if (!wdc) { - throw "No WDC registered. Did you forget to call tableau.registerConnector?"; - } - - var payloadObj = this._getPayloadObj(evt.data); - if(!payloadObj) return; // This message is not needed for WDC - - if (!this._sourceWindow) { - this._sourceWindow = evt.source; - this._sourceOrigin = evt.origin; - } - - var msgData = payloadObj.msgData; - this._applyPropertyValues(payloadObj.props); - - switch(payloadObj.msgName) { - case "init": - // Warn users about possible phinishing attacks - var confirmResult = this._getWebSecurityWarningConfirm(); - if (!confirmResult) { - window.close(); - } else { - this.globalObj.tableau.phase = msgData.phase; - this.globalObj._tableau.triggerInitialization(); - } - - break; - case "shutdown": - this.globalObj._tableau.triggerShutdown(); - break; - case "getSchema": - this.globalObj._tableau.triggerSchemaGathering(); - break; - case "getData": - this.globalObj._tableau.triggerDataGathering(msgData.tablesAndIncrementValues); - break; - } -}; - -/**** PUBLIC INTERFACE *****/ -SimulatorDispatcher.prototype._initPublicInterface = function() { - console.log("Initializing public interface"); - this._submitCalled = false; - - var publicInterface = {}; - publicInterface.abortForAuth = this._abortForAuth.bind(this); - publicInterface.abortWithError = this._abortWithError.bind(this); - publicInterface.addCrossOriginException = this._addCrossOriginException.bind(this); - publicInterface.log = this._log.bind(this); - publicInterface.reportProgress = this._reportProgress.bind(this); - publicInterface.submit = this._submit.bind(this); - - // Assign the public interface to this - this.publicInterface = publicInterface; -} + payload = JSON.parse(payloadString); -SimulatorDispatcher.prototype._abortForAuth = function(msg) { - this._sendMessage("abortForAuth", {"msg": msg}); -} + } catch (e) { -SimulatorDispatcher.prototype._abortWithError = function(msg) { - this._sendMessage("abortWithError", {"errorMsg": msg}); -} + return null; -SimulatorDispatcher.prototype._addCrossOriginException = function(destOriginList) { - // Don't bother passing this back to the simulator since there's nothing it can - // do. Just call back to the WDC indicating that it worked - console.log("Cross Origin Exception requested in the simulator. Pretending to work.") - setTimeout(function() { - this.globalObj._wdc.addCrossOriginExceptionCompleted(destOriginList); - }.bind(this), 0); -} + } -SimulatorDispatcher.prototype._log = function(msg) { - this._sendMessage("log", {"logMsg": msg}); -} + return payload; + } -SimulatorDispatcher.prototype._reportProgress = function(msg) { - this._sendMessage("reportProgress", {"progressMsg": msg}); -} + /** + * @returns {Boolean} + */ + _getWebSecurityWarningConfirm () { -SimulatorDispatcher.prototype._submit = function() { - this._sendMessage("submit"); -}; + // Due to cross-origin security issues over https, we may not be able to retrieve _sourceWindow. + // Use sourceOrigin instead. + let origin = this._sourceOrigin; + let Uri = require('jsuri'); // @todo review this + let parsedOrigin = new Uri(origin); + let hostName = parsedOrigin.host(); -/**** PRIVATE INTERFACE *****/ -SimulatorDispatcher.prototype._initPrivateInterface = function() { - console.log("Initializing private interface"); + const SUPPORTED_HOSTS = ['localhost', 'tableau.github.io']; - var privateInterface = {}; - privateInterface._initCallback = this._initCallback.bind(this); - privateInterface._shutdownCallback = this._shutdownCallback.bind(this); - privateInterface._schemaCallback = this._schemaCallback.bind(this); - privateInterface._tableDataCallback = this._tableDataCallback.bind(this); - privateInterface._dataDoneCallback = this._dataDoneCallback.bind(this); + if (SUPPORTED_HOSTS.indexOf(hostName) >= 0) { + return true; + } - // Assign the private interface to this - this.privateInterface = privateInterface; -} + // Whitelist Tableau domains + if (hostName && hostName.endsWith('online.tableau.com')) { + return true; + } -SimulatorDispatcher.prototype._initCallback = function() { - this._sendMessage("initCallback"); -} + let alreadyApprovedOrigins = ApprovedOrigins.getApprovedOrigins(); -SimulatorDispatcher.prototype._shutdownCallback = function() { - this._sendMessage("shutdownCallback"); -} + if (alreadyApprovedOrigins.indexOf(origin) >= 0) { + // The user has already approved this origin, no need to ask again + console.log(`Already approved the origin ${origin} , not asking again`); + return true; + } -SimulatorDispatcher.prototype._schemaCallback = function(schema, standardConnections) { - this._sendMessage("_schemaCallback", {"schema": schema, "standardConnections" : standardConnections || []}); -} + let localizedWarningTitle = this._getLocalizedString('webSecurityWarning'); + let completeWarningMsg = localizedWarningTitle + '\n\n' + hostName + '\n'; + let isConfirmed = confirm(completeWarningMsg); -SimulatorDispatcher.prototype._tableDataCallback = function(tableName, data) { - this._sendMessage("_tableDataCallback", { "tableName": tableName, "data": data }); -} + if (isConfirmed) { + // Set a session cookie to mark that we've approved this already + ApprovedOrigins.addApprovedOrigin(origin); + } + + return isConfirmed; + } + + /** + * @returns {String} + */ + _getCurrentLocale () { + // Use current browser's locale to get a localized warning message + let currentBrowserLanguage = (navigator.language || navigator.userLanguage); + let locale = currentBrowserLanguage ? currentBrowserLanguage.substring(0, 2) : 'en'; + let supportedLocales = ['de', 'en', 'es', 'fr', 'ja', 'ko', 'pt', 'zh']; // can we move this to package json as we did with connectors? + + // Fall back to English for other unsupported lanaguages + if (supportedLocales.indexOf(locale) < 0) { + locale = 'en'; + } + + return locale; + } + + /** + * + * @param {String} stringKey + * @returns {String} + */ + _getLocalizedString (stringKey) { + let locale = this._getCurrentLocale(); + + // Use static require here, otherwise webpack would generate a much bigger JS file + // ( the increment in size is irrelevant, specially when minimized, moving to the top and import [JAX]) + + // let deStringsMap = require('json-loader!./resources/Shim_lib_resources_de-DE.json'); + // let enStringsMap = require('json-loader!./resources/Shim_lib_resources_en-US.json'); + // let esStringsMap = require('json-loader!./resources/Shim_lib_resources_es-ES.json'); + // let jaStringsMap = require('json-loader!./resources/Shim_lib_resources_ja-JP.json'); + // let frStringsMap = require('json-loader!./resources/Shim_lib_resources_fr-FR.json'); + // let koStringsMap = require('json-loader!./resources/Shim_lib_resources_ko-KR.json'); + // let ptStringsMap = require('json-loader!./resources/Shim_lib_resources_pt-BR.json'); + // let zhStringsMap = require('json-loader!./resources/Shim_lib_resources_zh-CN.json'); + + let stringJsonMapByLocale = { + 'de': deStringsMap, + 'en': enStringsMap, + 'es': esStringsMap, + 'fr': frStringsMap, + 'ja': jaStringsMap, + 'ko': koStringsMap, + 'pt': ptStringsMap, + 'zh': zhStringsMap + }; + + let localizedStringsJson = stringJsonMapByLocale[locale]; + + return localizedStringsJson[stringKey]; + } + + /** + * + * @param {Object} evt + * @returns {Undefined} + */ + _receiveMessage (evt) { + console.log('Received message!'); + + let wdc = this.globalObj._wdc; + + if (!wdc) { + throw new Error('No WDC registered. Did you forget to call tableau.registerConnector?'); + } + + let payloadObj = this._getPayloadObj(evt.data); + + if (!payloadObj) { + return; // This message is not needed for WDC + } + + if (!this._sourceWindow) { + this._sourceWindow = evt.source; + this._sourceOrigin = evt.origin; + } + + let msgData = payloadObj.msgData; + this._applyPropertyValues(payloadObj.props); + + switch (payloadObj.msgName) { + case 'init': + // Warn users about possible phinishing attacks + if (!this._getWebSecurityWarningConfirm()) { + window.close(); + } else { + this.globalObj.tableau.phase = msgData.phase; + this.globalObj._tableau.triggerInitialization(); + } + break; + case 'shutdown': + this.globalObj._tableau.triggerShutdown(); + break; + case 'getSchema': + this.globalObj._tableau.triggerSchemaGathering(); + break; + case 'getData': + this.globalObj._tableau.triggerDataGathering(msgData.tablesAndIncrementValues); + break; + } + } + + /** + * PUBLIC INTERFACE + * @returns {Undefined} + */ + _initPublicInterface () { + console.log('Initializing public interface'); + + this._submitCalled = false; + + // Assign the public interface to this + this.publicInterface = { + abortForAuth: this._abortForAuth.bind(this), + abortWithError: this._abortWithError.bind(this), + addCrossOriginException: this._addCrossOriginException.bind(this), + log: this._log.bind(this), + reportProgress: this._reportProgress.bind(this), + submit: this._submit.bind(this) + }; + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableau.abortforauth + * + * @param {String} msg + * @returns {Undefined} + */ + _abortForAuth (msg) { + this._sendMessage('abortForAuth', { 'msg': msg }); + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableau.abortwitherror + * + * @param {String} msg + * @returns {Undefined} + */ + _abortWithError (msg) { + this._sendMessage('abortWithError', { 'errorMsg': msg }); + } -SimulatorDispatcher.prototype._dataDoneCallback = function() { - this._sendMessage("_dataDoneCallback"); + /** + * Missing documentation online, we need to add one + * + * @param {Array} destOriginList + * @returns {Undefined} + */ + _addCrossOriginException (destOriginList) { + // Don't bother passing this back to the simulator since there's nothing it can + // do. Just call back to the WDC indicating that it worked + console.log('Cross Origin Exception requested in the simulator. Pretending to work.'); + + // can we return a promise so we can listen for resolution instead of blindly wait? + setTimeout(function () { + this.globalObj._wdc.addCrossOriginExceptionCompleted(destOriginList); + }.bind(this), 0); + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableau.log + * + * @param {String} msg + * @returns {Undefined} + */ + _log (msg) { + this._sendMessage('log', { 'logMsg': msg }); + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableau.reportProgress + * + * @param {String} progressMessage + * @returns {Undefined} + */ + _reportProgress (progressMessage) { + this._sendMessage('reportProgress', { 'progressMsg': progressMessage }); + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableau.submit + * + * @returns {Undefined} + */ + _submit () { + this._sendMessage('submit'); + } + + /** + * PRIVATE INTERFACE + * @returns {Undefined} + */ + _initPrivateInterface () { + console.log('Initializing private interface'); + + // Assign the private interface to this + this.privateInterface = { + _initCallback: this._initCallback.bind(this), + _shutdownCallback: this._shutdownCallback.bind(this), + _schemaCallback: this._schemaCallback.bind(this), + _tableDataCallback: this._tableDataCallback.bind(this), + _dataDoneCallback: this._dataDoneCallback.bind(this) + }; + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.initcallback + * + * @returns {Undefined} + */ + _initCallback () { + this._sendMessage('initCallback'); + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.shutdowncallback + * + * @returns {Undefined} + */ + _shutdownCallback () { + this._sendMessage('shutdownCallback'); + } + + /** + * @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.schemacallback + * + * @param {Array} schema TableInfo @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.tableinfo-1 + * @param {Array} standardConnections StandardConnection @see http://tableau.github.io/webdataconnector/docs/api_ref.html#webdataconnectorapi.standardconnection + * @returns {Undefined} + */ + _schemaCallback (schema, standardConnections = []) { + this._sendMessage('_schemaCallback', { 'schema': schema, 'standardConnections': standardConnections }); + } + /** + * + * @param {String} tableName + * @param {*} data + * @returns {Undefined} + */ + _tableDataCallback (tableName, data) { + this._sendMessage('_tableDataCallback', { 'tableName': tableName, 'data': data }); + } + + /** + * @returns {Undefined} + */ + _dataDoneCallback () { + this._sendMessage('_dataDoneCallback'); + } } -module.exports = SimulatorDispatcher; +export default SimulatorDispatcher; diff --git a/Table.js b/Table.js index 43f79c2..f181446 100644 --- a/Table.js +++ b/Table.js @@ -1,55 +1,73 @@ /** -* @class Represents a single table which Tableau has requested -* @param tableInfo {Object} - Information about the table -* @param incrementValue {string=} - Incremental update value -*/ -function Table(tableInfo, incrementValue, isJoinFiltered, filterColumnId, filterValues, dataCallbackFn) { - /** @member {Object} Information about the table which has been requested. This is - guaranteed to be one of the tables the connector returned in the call to getSchema. */ - this.tableInfo = tableInfo; - - /** @member {string} Defines the incremental update value for this table. Empty string if - there is not an incremental update requested. */ - this.incrementValue = incrementValue || ""; - - /** @member {boolean} Whether or not this table is meant to be filtered using filterValues. */ - this.isJoinFiltered = isJoinFiltered; - - /** @member {string} If this table is filtered, this is the column where the filter values - * should be found. */ - this.filterColumnId = filterColumnId; - - /** @member {array} An array of strings which specifies the values we want to retrieve. For - * example, if an ID column was the filter column, this would be a collection of IDs to retrieve. */ - this.filterValues = filterValues; - - /** @private */ - this._dataCallbackFn = dataCallbackFn; - - // bind the public facing version of this function so it can be passed around - this.appendRows = this._appendRows.bind(this); -} + * + */ +class Table { + + /** + * Represents a single table which Tableau has requeste + * + * @param {Object} tableInfo Information about the table which has been requested. + * This is guaranteed to be one of the tables the connector returned in the call to getSchema. + * + * @param {string=} incrementValue Defines the incremental update value for this table. + * Empty string if there is not an incremental update requested. + * + * @param {Boolean=} isJoinFiltered Whether or not this table is meant to be filtered using filterValues. + * @param {String=} filterColumnId If this table is filtered, this is the column where the filter values should be found. + * @param {Array} filterValues An array of strings which specifies the values we want to retrieve. + * For example, if an ID column was the filter column, this would be a collection of IDs to retrieve. + * + * @param {Function} dataCallbackFn + */ + constructor (tableInfo, incrementValue = '', isJoinFiltered = false, filterColumnId = '', filterValues = [], dataCallbackFn) { + + this.tableInfo = tableInfo; + + this.incrementValue = incrementValue; + + this.isJoinFiltered = isJoinFiltered; + + this.filterColumnId = filterColumnId; + + this.filterValues = filterValues; + + this._dataCallbackFn = dataCallbackFn; // privacy by dangling underscore + + // bind the public facing version of this function so it can be passed around + this.appendRows = this._appendRows.bind(this); + } + + /** + * Appends the given rows to the set of data contained in this table + * + * @param {Array} data - Either an array of arrays or an array of objects which represent the individual rows of data to append to this table + * + * @returns {Boolean} + */ + _appendRows (data) { + // note: add boolean return for testing purpose + + // Do some quick validation that this data is the format we expect + // is this validation enough? shouldn't we throw an error? (Jax) + if (!data) { + console.warn('rows data is null or undefined'); + + return false; + } + + if (!Array.isArray(data)) { + // Log a warning because the data is not an array like we expected + // is this validation enough? shouldn't we throw an error? (Jax) + console.warn('Table.appendRows must take an array of arrays or array of objects'); + return false; + } + + // Call back with the rows for this table + this._dataCallbackFn(this.tableInfo.id, data); + + return true; + } -/** -* @method appends the given rows to the set of data contained in this table -* @param data {array} - Either an array of arrays or an array of objects which represent -* the individual rows of data to append to this table -*/ -Table.prototype._appendRows = function(data) { - // Do some quick validation that this data is the format we expect - if (!data) { - console.warn("rows data is null or undefined"); - return; - } - - if (!Array.isArray(data)) { - // Log a warning because the data is not an array like we expected - console.warn("Table.appendRows must take an array of arrays or array of objects"); - return; - } - - // Call back with the rows for this table - this._dataCallbackFn(this.tableInfo.id, data); } -module.exports = Table; +export default Table; diff --git a/Table.test.js b/Table.test.js new file mode 100644 index 0000000..6b77706 --- /dev/null +++ b/Table.test.js @@ -0,0 +1,76 @@ +/* eslint-env node, mocha, jest */ +import Table from './Table'; + +// use if required for debugging +let consoleLog = console.log; // eslint-disable-line no-unused-vars +let consoleWarn = console.warn; // eslint-disable-line no-unused-vars +console.log = jest.fn(); +console.warn = jest.fn(); + +describe('UNIT - Table', () => { + + it('Table should initialize with documented DEFAULTS', () => { + let table = new Table(); + + expect(table.tableInfo).toBeUndefined(); + + expect(table.incrementValue).toBe(''); + + expect(table.isJoinFiltered).toBe(false); + + expect(table.filterColumnId).toBe(''); + + expect(table.filterValues).toEqual([]); + + expect(table._dataCallbackFn).toBeUndefined(); + + }); + + it('Table should initialize with assigned initialization values', () => { + let tableInfo = {}; + let incrementValue = '3'; + let isJoinFiltered = true; + let filterColumnId = '5'; + let filterValues = [2, 3, 5, 7, 11, 13, 17, 19, 23]; + let dataCallbackFn = function dataCallbackFnTest () { + // this is a test, the param is passed by reference + }; + let table = new Table(tableInfo, incrementValue, isJoinFiltered, filterColumnId, filterValues, dataCallbackFn); + + expect(table.tableInfo).toBe(tableInfo); + + expect(table.incrementValue).toBe(incrementValue); + + expect(table.isJoinFiltered).toBe(isJoinFiltered); + + expect(table.filterColumnId).toBe(filterColumnId); + + expect(table.filterValues).toBe(filterValues); + + expect(table._dataCallbackFn).toBe(dataCallbackFn); + + }); + + it('_appendRows should return boolean "execution success"', () => { + let table = new Table({}, '', false, '', [], () => { }); + + expect(table._appendRows()).toBe(false); + + expect(table._appendRows({})).toBe(false); + + expect(table._appendRows([])).toBe(true); + + }); + + it('_appendRows should call _dataCallbackFn on input success"', () => { + let mockCallback = jest.fn(); + + let table = new Table({}, '', false, '', [], mockCallback); + + table._appendRows([]); + table._appendRows([]); + + expect(mockCallback.mock.calls.length).toBe(2); + + }); +}); diff --git a/Utilities.js b/Utilities.js index c4fc60b..6ae84ce 100644 --- a/Utilities.js +++ b/Utilities.js @@ -1,9 +1,17 @@ -function copyFunctions(src, dest) { - for(var key in src) { - if (typeof src[key] === 'function') { - dest[key] = src[key]; +/** + * WARNING - copies are BY REFERENCE + * + * @param {Object} src Source Object + * @param {Object} dest Target Object + * + * @returns {Undefined} + */ +export function copyFunctions (src, dest) { + + for (let key in src) { + if (typeof src[key] === 'function') { + dest[key] = src[key]; + } } - } -} -module.exports.copyFunctions = copyFunctions; +} diff --git a/Utilities.test.js b/Utilities.test.js new file mode 100644 index 0000000..f51dd1a --- /dev/null +++ b/Utilities.test.js @@ -0,0 +1,20 @@ +/* eslint-env node, mocha, jest */ +import { copyFunctions } from './Utilities'; + +describe('UNIT - Utilities', () => { + + it('copyFunctions to copy functions from source to target', () => { + let source = { + a: function a () { + + } + }; + + let target = {}; + + copyFunctions(source, target); + + expect(target.a).toBe(source.a); + + }); +}); diff --git a/index.js b/index.js index fb5636d..1f24cee 100644 --- a/index.js +++ b/index.js @@ -2,5 +2,5 @@ // This file will be exported as a bundled js file by webpack so it can be included // in a