From 3b00cf6d5adcc557802418aa39954d351f5d310a Mon Sep 17 00:00:00 2001 From: zhangjialiang Date: Wed, 13 Aug 2025 14:49:54 +0800 Subject: [PATCH 01/13] * [task#147913,doing,1h] Add buried point method. --- module/admin/control.php | 6 ++++++ module/admin/model.php | 23 +++++++++++++++++++++++ module/install/js/common.ui.js | 10 ++++++++++ 3 files changed, 39 insertions(+) create mode 100644 module/install/js/common.ui.js diff --git a/module/admin/control.php b/module/admin/control.php index ef62c11580..5e5b296dc3 100755 --- a/module/admin/control.php +++ b/module/admin/control.php @@ -719,4 +719,10 @@ class admin extends control $this->view->company = $company->name; $this->display(); } + + public function installEvent() + { + $data = form::data($this->config->admin->form->installEvent)->get(); + $this->admin->sentInstallEvent($data); + } } diff --git a/module/admin/model.php b/module/admin/model.php index 7bb1999af4..19cced8ef4 100755 --- a/module/admin/model.php +++ b/module/admin/model.php @@ -466,4 +466,27 @@ class adminModel extends model $this->lang->switcherMenu = $output; } + + public function sentInstallEvent($data) + { + $apiRoot = $this->config->admin->register->apiRoot; + $apiURL = $apiRoot . "/zentaoevent-install.json"; + + $sn = $this->config->global->sn; + if(empty($sn)) $sn = $this->loadModel('setting')->setSN(); + + $httpData['sn'] = $sn; + $httpData['location'] = $data->location; + $httpData['fingerprint'] = $data->fingerprint; + $httpData['edition'] = $this->config->edition; + $httpData['version'] = $this->config->version; + $httpData['phpVersion'] = phpversion(); + $httpData['os'] = PHP_OS . ' ' . php_uname('m'); + $httpData['is_docker'] = file_exists('/.dockerenv'); + $httpData['installType'] = ''; + $httpData['ip'] = helper::getRemoteIP(); + + $this->loadModel('common'); + common::http($apiURL, $httpData); + } } diff --git a/module/install/js/common.ui.js b/module/install/js/common.ui.js new file mode 100644 index 0000000000..3c4cfc3571 --- /dev/null +++ b/module/install/js/common.ui.js @@ -0,0 +1,10 @@ +window.ajaxInstallEvent = function(location = '') +{ + $.getLib(config.webRoot + 'js/fingerprint/fingerprint.js', {root: false}, async function() + { + const agent = typeof(FingerprintJS) !== 'undefined' ? await FingerprintJS.load() : ''; + let fingerprint = agent ? await agent.get() : ''; + fingerprint = fingerprint ? fingerprint.visitorId : ''; + $.ajax({url: $.createLink('admin', 'installEvent'), type: "post", data: {fingerprint, location}, timeout: 2000}); + }); +} From 669f883b54801ff979604132ecb85d6a9f84f13d Mon Sep 17 00:00:00 2001 From: zhangjialiang Date: Wed, 13 Aug 2025 15:30:58 +0800 Subject: [PATCH 02/13] * [task#147913,doing,0.1h] Format code: add comments, add unit test. --- module/admin/control.php | 9 +++++++- module/admin/model.php | 20 ++++++++++++---- .../admin/test/lib/admin.unittest.class.php | 15 ++++++++++++ module/admin/test/model/sendInstallEvent.php | 23 +++++++++++++++++++ 4 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 module/admin/test/model/sendInstallEvent.php diff --git a/module/admin/control.php b/module/admin/control.php index 5e5b296dc3..5f835f68bd 100755 --- a/module/admin/control.php +++ b/module/admin/control.php @@ -720,9 +720,16 @@ class admin extends control $this->display(); } + /** + * 安装过程的事件埋点。 + * Event burial points during installation process. + * + * @access public + * @return void + */ public function installEvent() { $data = form::data($this->config->admin->form->installEvent)->get(); - $this->admin->sentInstallEvent($data); + $this->admin->sendInstallEvent($data); } } diff --git a/module/admin/model.php b/module/admin/model.php index 19cced8ef4..b6191bf648 100755 --- a/module/admin/model.php +++ b/module/admin/model.php @@ -467,10 +467,18 @@ class adminModel extends model $this->lang->switcherMenu = $output; } - public function sentInstallEvent($data) + /** + * 发送安装过程埋点。 + * Sending installation process buried points. + * + * @param object $data + * @access public + * @return bool + */ + public function sendInstallEvent($data) { - $apiRoot = $this->config->admin->register->apiRoot; - $apiURL = $apiRoot . "/zentaoevent-install.json"; + $apiRoot = $this->config->admin->register->apiRoot; + $apiURL = $apiRoot . "/zentaoevent-install.json"; $sn = $this->config->global->sn; if(empty($sn)) $sn = $this->loadModel('setting')->setSN(); @@ -486,7 +494,9 @@ class adminModel extends model $httpData['installType'] = ''; $httpData['ip'] = helper::getRemoteIP(); - $this->loadModel('common'); - common::http($apiURL, $httpData); + $response = common::http($apiURL, $httpData); + $response = json_decode($response); + if($response && $response->code == 200) return true; + return false; } } diff --git a/module/admin/test/lib/admin.unittest.class.php b/module/admin/test/lib/admin.unittest.class.php index 4e4828675a..2b6c911cb9 100644 --- a/module/admin/test/lib/admin.unittest.class.php +++ b/module/admin/test/lib/admin.unittest.class.php @@ -80,4 +80,19 @@ class adminTest } return $link; } + + /** + * 测试发送安装过程埋点。 + * Test send install event. + * + * @param object $data + * @access public + * @return string + */ + public function sendInstallEventTest($data): string + { + $objects = $this->objectModel->sendInstallEvent($data); + if(empty($objects)) return 'Fail'; + return 'Success'; + } } diff --git a/module/admin/test/model/sendInstallEvent.php b/module/admin/test/model/sendInstallEvent.php new file mode 100644 index 0000000000..4888415b37 --- /dev/null +++ b/module/admin/test/model/sendInstallEvent.php @@ -0,0 +1,23 @@ +#!/usr/bin/env php +sendInstallEvent(); +timeout=0 +cid=1 + +- 发送安装过程埋点 @Success + +*/ + +include dirname(__FILE__, 5) . '/test/lib/init.php'; +include dirname(__FILE__, 2) . '/lib/admin.unittest.class.php'; + +zenData('user')->gen(5); +su('admin'); + +$admin = new adminTest(); +$data = new stdClass(); +$data->fingerprint = 'ba237c362b6aad54d17e7613eac23ec1'; +$data->location = 'join-community'; +r($admin->sendInstallEvent($data)) && p() && e('Success'); //测试发送安装过程埋点是否成功 From 714291ae7d02dade364c8986e83c539119f19496 Mon Sep 17 00:00:00 2001 From: zhangjialiang Date: Wed, 13 Aug 2025 16:03:22 +0800 Subject: [PATCH 03/13] * [task#147913,doing,2h] Successfully installed buried points. --- config/zentaopms.php | 1 + module/admin/config/form.php | 12 ++++++++---- module/admin/control.php | 5 +++++ module/admin/js/register.ui.js | 32 ++++++++++++++++++++++++++++++- module/admin/ui/register.html.php | 8 +++++++- module/install/js/index.ui.js | 2 ++ module/install/js/step6.ui.js | 2 ++ 7 files changed, 56 insertions(+), 6 deletions(-) diff --git a/config/zentaopms.php b/config/zentaopms.php index 296f9dfc57..0b4fa09f22 100644 --- a/config/zentaopms.php +++ b/config/zentaopms.php @@ -227,6 +227,7 @@ $config->openMethods[] = 'admin.register'; $config->openMethods[] = 'admin.getcaptcha'; $config->openMethods[] = 'admin.sendcode'; $config->openMethods[] = 'admin.planmodal'; +$config->openMethods[] = 'admin.installevent'; /* 登录用户可以使用的方法。The methods that can be used by the logged users. */ $config->logonMethods = array(); diff --git a/module/admin/config/form.php b/module/admin/config/form.php index 775170a85d..7d856d0e91 100644 --- a/module/admin/config/form.php +++ b/module/admin/config/form.php @@ -25,11 +25,15 @@ $config->admin->form->log['days'] = array('type' => 'int', 'required' => true, ' $config->admin->form->resetpwdsetting = array(); $config->admin->form->resetpwdsetting['resetPWDByMail'] = array('type' => 'int', 'required' => false, 'default' => 0); -$config->admin->form->register['mobile'] = array('type' => 'string', 'required' => true); -$config->admin->form->register['code'] = array('type' => 'string', 'required' => true); -$config->admin->form->register['agreeUX'] = array('type' => 'string', 'required' => false); +$config->admin->form->register['mobile'] = array('type' => 'string', 'required' => true); +$config->admin->form->register['code'] = array('type' => 'string', 'required' => true); +$config->admin->form->register['agreeUX'] = array('type' => 'string', 'required' => false); +$config->admin->form->register['fingerprint'] = array('type' => 'string', 'required' => true); $config->admin->form->giftPackage['nickname'] = array('type' => 'string', 'required' => true); $config->admin->form->giftPackage['position'] = array('type' => 'string', 'required' => true); $config->admin->form->giftPackage['company'] = array('type' => 'string', 'required' => true); -$config->admin->form->giftPackage['solvedProblems'] = array('type' => 'array', 'required' => true); \ No newline at end of file +$config->admin->form->giftPackage['solvedProblems'] = array('type' => 'array', 'required' => true); + +$config->admin->form->installEvent['location'] = array('type' => 'string', 'required' => true); +$config->admin->form->installEvent['fingerprint'] = array('type' => 'string', 'required' => true); \ No newline at end of file diff --git a/module/admin/control.php b/module/admin/control.php index 5f835f68bd..252d5f484f 100755 --- a/module/admin/control.php +++ b/module/admin/control.php @@ -579,6 +579,11 @@ class admin extends control $this->loadModel('setting')->setItem('system.admin.register.agreeUX', $agreeUX); $this->config->admin->agreeUX = $agreeUX; + $eventData = new stdClass(); + $eventData->fingerprint = $data->fingerprint; + $eventData->location = 'join-community'; + $this->admin->sendInstallEvent($eventData); + $callBack = $this->loadModel('user')->isLogon() ? 'loadToRegister()' : 'loadToIndex()'; return $this->send(array('result' => 'success', 'message' => $this->lang->admin->community->joinSuccess, 'callback' => $callBack)); diff --git a/module/admin/js/register.ui.js b/module/admin/js/register.ui.js index 28fb2d5f10..1fc0cdfae9 100644 --- a/module/admin/js/register.ui.js +++ b/module/admin/js/register.ui.js @@ -91,4 +91,34 @@ window.loadToIndex = function() setTimeout(function() { location.href = $.createLink('index', 'index'); }, 2000); -} \ No newline at end of file +} + +window.getFingerprint = function() +{ + $.getLib(config.webRoot + 'js/fingerprint/fingerprint.js', {root: false}, async function(fingerprint) + { + const agent = typeof(FingerprintJS) !== 'undefined' ? await FingerprintJS.load() : ''; + fingerprint = agent ? await agent.get() : ''; + fingerprint = fingerprint ? fingerprint.visitorId : ''; + $('.form-fingerprint').val(fingerprint); + }); +} + +window.skip = function() +{ + ajaxInstallEvent('click-skip-join-community'); + setTimeout(function() { + location.href = $.createLink('index', 'index'); + }, 1000); +} + +window.ajaxInstallEvent = function(location = '') +{ + $.getLib(config.webRoot + 'js/fingerprint/fingerprint.js', {root: false}, async function() + { + const agent = typeof(FingerprintJS) !== 'undefined' ? await FingerprintJS.load() : ''; + let fingerprint = agent ? await agent.get() : ''; + fingerprint = fingerprint ? fingerprint.visitorId : ''; + $.ajax({url: $.createLink('admin', 'installEvent'), type: "post", data: {fingerprint, location}, timeout: 2000}); + }); +} diff --git a/module/admin/ui/register.html.php b/module/admin/ui/register.html.php index d63b757df1..2554f1fdf8 100644 --- a/module/admin/ui/register.html.php +++ b/module/admin/ui/register.html.php @@ -23,7 +23,7 @@ if(strpos($_SERVER['REQUEST_URI'], '_single=1') !== false) ( setClass('btn capitalize skip-btn font-normal'), $lang->admin->community->skip, - set::href(createLink('index')), + on::click()->call('skip') ) ); } @@ -240,6 +240,12 @@ else $lang->admin->community->uxPlanStatusTitle ) ) + ), + formHidden( + 'fingerprint', + '', + setClass('form-fingerprint'), + on::init()->call('getFingerprint') ) ), $skip, diff --git a/module/install/js/index.ui.js b/module/install/js/index.ui.js index f43c18e3fa..fd834f44ae 100644 --- a/module/install/js/index.ui.js +++ b/module/install/js/index.ui.js @@ -4,3 +4,5 @@ window.switchLang = function(lang) }; $.ajax({url: $.createLink('misc', 'ajaxSendEvent', "step=start"), timeout: 2000}); + +ajaxInstallEvent('start-install'); diff --git a/module/install/js/step6.ui.js b/module/install/js/step6.ui.js index d29b083458..0a5b043471 100644 --- a/module/install/js/step6.ui.js +++ b/module/install/js/step6.ui.js @@ -1 +1,3 @@ $.ajax({url: sendEventLink, timeout: 2000}); + +ajaxInstallEvent('finish-install'); From 38ab811a11c29781917886881ad0f6b463d23bcc Mon Sep 17 00:00:00 2001 From: zhangjialiang Date: Wed, 13 Aug 2025 16:14:48 +0800 Subject: [PATCH 04/13] * [task#147913,doing,0.1h] The 'install. php' entry does not load 'common', here we add "loadModel('common')". --- module/admin/model.php | 1 + 1 file changed, 1 insertion(+) diff --git a/module/admin/model.php b/module/admin/model.php index b6191bf648..ce7bcdc849 100755 --- a/module/admin/model.php +++ b/module/admin/model.php @@ -494,6 +494,7 @@ class adminModel extends model $httpData['installType'] = ''; $httpData['ip'] = helper::getRemoteIP(); + $this->loadModel('common'); $response = common::http($apiURL, $httpData); $response = json_decode($response); if($response && $response->code == 200) return true; From 8684148d3ce16503ec1eaa4e4ce79086813e446c Mon Sep 17 00:00:00 2001 From: zhangjialiang Date: Wed, 13 Aug 2025 16:17:45 +0800 Subject: [PATCH 05/13] * [task#147913,doing,0.1h] Format code. --- module/admin/test/lib/admin.unittest.class.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/admin/test/lib/admin.unittest.class.php b/module/admin/test/lib/admin.unittest.class.php index 2b6c911cb9..317e34d78b 100644 --- a/module/admin/test/lib/admin.unittest.class.php +++ b/module/admin/test/lib/admin.unittest.class.php @@ -92,7 +92,7 @@ class adminTest public function sendInstallEventTest($data): string { $objects = $this->objectModel->sendInstallEvent($data); - if(empty($objects)) return 'Fail'; + if(!$objects) return 'Fail'; return 'Success'; } } From cae1b9a12dd7e8583448cd92e0baeb9ccdf05899 Mon Sep 17 00:00:00 2001 From: zhangjialiang Date: Wed, 13 Aug 2025 16:31:51 +0800 Subject: [PATCH 06/13] * [task#147913,doing,1h] Generate SN before installation. --- module/setting/model.php | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/module/setting/model.php b/module/setting/model.php index a7aab1927e..5757260a4f 100644 --- a/module/setting/model.php +++ b/module/setting/model.php @@ -320,19 +320,36 @@ class settingModel extends model * Set the sn of current zentaopms. * * @access public - * @return void + * @return string */ public function setSN() { - $sn = $this->getItem('owner=system&module=common§ion=global&key=sn'); - if($this->snNeededUpdate($sn)) + $file = dirname(dirname(dirname(__FILE__))) . '/tmp/sn'; + $fileSn = file_get_contents($file); + if($fileSn) { - $sn = $this->computeSN(); - $this->setItem('system.common.global.sn', $sn); + $sn = $fileSn; + } + else + { + if(isset($this->config->global->sn)) + { + $sn = $this->getItem('owner=system&module=common§ion=global&key=sn'); + if($this->snNeededUpdate($sn)) + { + $sn = $this->computeSN(); + $this->setItem('system.common.global.sn', $sn); + } + } + else + { + $sn = $this->computeSN(); + } } - if(!isset($this->config->global)) $this->config->global = new stdclass(); $this->config->global->sn = $sn; + file_put_contents($file, $sn); + return $sn; } /** From 441c6f34a560223e7be50781e608c636118866da Mon Sep 17 00:00:00 2001 From: zhangjialiang Date: Wed, 13 Aug 2025 16:37:28 +0800 Subject: [PATCH 07/13] * [task#147913,doing,0.1h] Format code. --- .../test/model/{sendInstallEvent.php => sendinstallevent.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename module/admin/test/model/{sendInstallEvent.php => sendinstallevent.php} (100%) diff --git a/module/admin/test/model/sendInstallEvent.php b/module/admin/test/model/sendinstallevent.php similarity index 100% rename from module/admin/test/model/sendInstallEvent.php rename to module/admin/test/model/sendinstallevent.php From 8ab5b1684dded4fc518b84df480fd82202e416f8 Mon Sep 17 00:00:00 2001 From: zhangjialiang Date: Thu, 14 Aug 2025 08:41:38 +0800 Subject: [PATCH 08/13] * [task#147913,doing,0.1h] Change variable name. --- module/admin/model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/admin/model.php b/module/admin/model.php index ce7bcdc849..00a82a130e 100755 --- a/module/admin/model.php +++ b/module/admin/model.php @@ -490,7 +490,7 @@ class adminModel extends model $httpData['version'] = $this->config->version; $httpData['phpVersion'] = phpversion(); $httpData['os'] = PHP_OS . ' ' . php_uname('m'); - $httpData['is_docker'] = file_exists('/.dockerenv'); + $httpData['isDocker'] = file_exists('/.dockerenv'); $httpData['installType'] = ''; $httpData['ip'] = helper::getRemoteIP(); From d2b4d667a0cf1a8defe68d5de516a801d012e9c8 Mon Sep 17 00:00:00 2001 From: zhangjialiang Date: Thu, 14 Aug 2025 17:00:20 +0800 Subject: [PATCH 09/13] * [task#147913,doing,2.5h] Transfer buried points to Misc module, SN cache uses cookies. --- config/filter.php | 7 ++++ config/zentaopms.php | 2 +- module/admin/config/form.php | 5 +-- module/admin/control.php | 15 +------- module/admin/js/register.ui.js | 2 +- module/admin/model.php | 34 ------------------- .../admin/test/lib/admin.unittest.class.php | 15 -------- module/admin/test/model/sendinstallevent.php | 23 ------------- module/install/control.php | 4 +++ module/install/js/common.ui.js | 2 +- module/misc/config.php | 1 + module/misc/control.php | 15 ++++++++ module/misc/model.php | 32 +++++++++++++++++ module/misc/test/model/sendinstallevent.php | 21 ++++++++++++ module/setting/model.php | 27 ++++++++------- 15 files changed, 99 insertions(+), 106 deletions(-) delete mode 100644 module/admin/test/model/sendinstallevent.php create mode 100644 module/misc/test/model/sendinstallevent.php diff --git a/config/filter.php b/config/filter.php index 9571c43b3d..4b184a8553 100755 --- a/config/filter.php +++ b/config/filter.php @@ -80,6 +80,7 @@ $filter->kanban = new stdclass(); $filter->group = new stdclass(); $filter->convert = new stdclass(); $filter->admin = new stdclass(); +$filter->install = new stdclass(); $filter->index->index = new stdclass(); $filter->block->default = new stdclass(); @@ -112,6 +113,7 @@ $filter->api->edit = new stdClass(); $filter->mail->ztcloud = new stdclass(); $filter->mail->batchdelete = new stdclass(); $filter->misc->checkupdate = new stdclass(); +$filter->misc->installevent = new stdclass(); $filter->file->download = new stdclass(); $filter->product->browse = new stdclass(); $filter->product->all = new stdclass(); @@ -210,6 +212,7 @@ $filter->admin->register = new stdclass(); $filter->admin->getcaptcha = new stdclass(); $filter->admin->sendcode = new stdclass(); $filter->admin->giftpackage = new stdclass(); +$filter->install->step5 = new stdclass(); $filter->index->index->get['open'] = 'reg::base64'; @@ -453,6 +456,10 @@ $filter->mail->batchdelete->get['idList'] = 'reg::idList'; $filter->misc->checkupdate->get['browser'] = 'code'; $filter->misc->checkupdate->get['note'] = 'reg::base64'; +$filter->misc->installevent->cookie['sn'] = 'string'; + +$filter->install->step5->cookie['sn'] = 'string'; + $filter->sso->getbindusers->get['hash'] = 'reg::md5'; $filter->sso->gettodolist->get['hash'] = 'reg::md5'; $filter->sso->getuserpairs->get['hash'] = 'reg::md5'; diff --git a/config/zentaopms.php b/config/zentaopms.php index 0b4fa09f22..7b1d22fc84 100644 --- a/config/zentaopms.php +++ b/config/zentaopms.php @@ -210,6 +210,7 @@ $config->openMethods[] = 'api.getsessionid'; $config->openMethods[] = 'misc.captcha'; $config->openMethods[] = 'misc.checkupdate'; $config->openMethods[] = 'misc.ajaxsendevent'; +$config->openMethods[] = 'misc.installevent'; $config->openMethods[] = 'sso.bind'; $config->openMethods[] = 'sso.feishuauthen'; $config->openMethods[] = 'sso.feishulogin'; @@ -227,7 +228,6 @@ $config->openMethods[] = 'admin.register'; $config->openMethods[] = 'admin.getcaptcha'; $config->openMethods[] = 'admin.sendcode'; $config->openMethods[] = 'admin.planmodal'; -$config->openMethods[] = 'admin.installevent'; /* 登录用户可以使用的方法。The methods that can be used by the logged users. */ $config->logonMethods = array(); diff --git a/module/admin/config/form.php b/module/admin/config/form.php index 7d856d0e91..062e399533 100644 --- a/module/admin/config/form.php +++ b/module/admin/config/form.php @@ -33,7 +33,4 @@ $config->admin->form->register['fingerprint'] = array('type' => 'string', 'requi $config->admin->form->giftPackage['nickname'] = array('type' => 'string', 'required' => true); $config->admin->form->giftPackage['position'] = array('type' => 'string', 'required' => true); $config->admin->form->giftPackage['company'] = array('type' => 'string', 'required' => true); -$config->admin->form->giftPackage['solvedProblems'] = array('type' => 'array', 'required' => true); - -$config->admin->form->installEvent['location'] = array('type' => 'string', 'required' => true); -$config->admin->form->installEvent['fingerprint'] = array('type' => 'string', 'required' => true); \ No newline at end of file +$config->admin->form->giftPackage['solvedProblems'] = array('type' => 'array', 'required' => true); \ No newline at end of file diff --git a/module/admin/control.php b/module/admin/control.php index 252d5f484f..3eac85a59c 100755 --- a/module/admin/control.php +++ b/module/admin/control.php @@ -582,7 +582,7 @@ class admin extends control $eventData = new stdClass(); $eventData->fingerprint = $data->fingerprint; $eventData->location = 'join-community'; - $this->admin->sendInstallEvent($eventData); + $this->loadModel('misc')->sendInstallEvent($eventData); $callBack = $this->loadModel('user')->isLogon() ? 'loadToRegister()' : 'loadToIndex()'; @@ -724,17 +724,4 @@ class admin extends control $this->view->company = $company->name; $this->display(); } - - /** - * 安装过程的事件埋点。 - * Event burial points during installation process. - * - * @access public - * @return void - */ - public function installEvent() - { - $data = form::data($this->config->admin->form->installEvent)->get(); - $this->admin->sendInstallEvent($data); - } } diff --git a/module/admin/js/register.ui.js b/module/admin/js/register.ui.js index 1fc0cdfae9..7945a4a706 100644 --- a/module/admin/js/register.ui.js +++ b/module/admin/js/register.ui.js @@ -119,6 +119,6 @@ window.ajaxInstallEvent = function(location = '') const agent = typeof(FingerprintJS) !== 'undefined' ? await FingerprintJS.load() : ''; let fingerprint = agent ? await agent.get() : ''; fingerprint = fingerprint ? fingerprint.visitorId : ''; - $.ajax({url: $.createLink('admin', 'installEvent'), type: "post", data: {fingerprint, location}, timeout: 2000}); + $.ajax({url: $.createLink('misc', 'installEvent'), type: "post", data: {fingerprint, location}, timeout: 2000}); }); } diff --git a/module/admin/model.php b/module/admin/model.php index 00a82a130e..7bb1999af4 100755 --- a/module/admin/model.php +++ b/module/admin/model.php @@ -466,38 +466,4 @@ class adminModel extends model $this->lang->switcherMenu = $output; } - - /** - * 发送安装过程埋点。 - * Sending installation process buried points. - * - * @param object $data - * @access public - * @return bool - */ - public function sendInstallEvent($data) - { - $apiRoot = $this->config->admin->register->apiRoot; - $apiURL = $apiRoot . "/zentaoevent-install.json"; - - $sn = $this->config->global->sn; - if(empty($sn)) $sn = $this->loadModel('setting')->setSN(); - - $httpData['sn'] = $sn; - $httpData['location'] = $data->location; - $httpData['fingerprint'] = $data->fingerprint; - $httpData['edition'] = $this->config->edition; - $httpData['version'] = $this->config->version; - $httpData['phpVersion'] = phpversion(); - $httpData['os'] = PHP_OS . ' ' . php_uname('m'); - $httpData['isDocker'] = file_exists('/.dockerenv'); - $httpData['installType'] = ''; - $httpData['ip'] = helper::getRemoteIP(); - - $this->loadModel('common'); - $response = common::http($apiURL, $httpData); - $response = json_decode($response); - if($response && $response->code == 200) return true; - return false; - } } diff --git a/module/admin/test/lib/admin.unittest.class.php b/module/admin/test/lib/admin.unittest.class.php index 317e34d78b..4e4828675a 100644 --- a/module/admin/test/lib/admin.unittest.class.php +++ b/module/admin/test/lib/admin.unittest.class.php @@ -80,19 +80,4 @@ class adminTest } return $link; } - - /** - * 测试发送安装过程埋点。 - * Test send install event. - * - * @param object $data - * @access public - * @return string - */ - public function sendInstallEventTest($data): string - { - $objects = $this->objectModel->sendInstallEvent($data); - if(!$objects) return 'Fail'; - return 'Success'; - } } diff --git a/module/admin/test/model/sendinstallevent.php b/module/admin/test/model/sendinstallevent.php deleted file mode 100644 index 4888415b37..0000000000 --- a/module/admin/test/model/sendinstallevent.php +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env php -sendInstallEvent(); -timeout=0 -cid=1 - -- 发送安装过程埋点 @Success - -*/ - -include dirname(__FILE__, 5) . '/test/lib/init.php'; -include dirname(__FILE__, 2) . '/lib/admin.unittest.class.php'; - -zenData('user')->gen(5); -su('admin'); - -$admin = new adminTest(); -$data = new stdClass(); -$data->fingerprint = 'ba237c362b6aad54d17e7613eac23ec1'; -$data->location = 'join-community'; -r($admin->sendInstallEvent($data)) && p() && e('Success'); //测试发送安装过程埋点是否成功 diff --git a/module/install/control.php b/module/install/control.php index 4419a8fb5c..7ad5219553 100644 --- a/module/install/control.php +++ b/module/install/control.php @@ -388,6 +388,10 @@ class install extends control $this->install->enableCache(); + /* 保存sn到数据库。*/ + /* Save SN to database. */ + $this->loadModel('setting')->setSN(); + return $this->send(array('result' => 'success', 'load' => inlink('step6'))); } diff --git a/module/install/js/common.ui.js b/module/install/js/common.ui.js index 3c4cfc3571..22633cb4f5 100644 --- a/module/install/js/common.ui.js +++ b/module/install/js/common.ui.js @@ -5,6 +5,6 @@ window.ajaxInstallEvent = function(location = '') const agent = typeof(FingerprintJS) !== 'undefined' ? await FingerprintJS.load() : ''; let fingerprint = agent ? await agent.get() : ''; fingerprint = fingerprint ? fingerprint.visitorId : ''; - $.ajax({url: $.createLink('admin', 'installEvent'), type: "post", data: {fingerprint, location}, timeout: 2000}); + $.ajax({url: $.createLink('misc', 'installEvent'), type: "post", data: {fingerprint, location}, timeout: 2000}); }); } diff --git a/module/misc/config.php b/module/misc/config.php index 455ed36153..b06585fd2a 100644 --- a/module/misc/config.php +++ b/module/misc/config.php @@ -3,4 +3,5 @@ $config->misc->api = 'https://api.zentao.net'; $config->misc->sendEventAPI = 'https://www.zentao.net/misc-event-install.html'; $config->misc->enApi = 'https://api.zentao.pm'; $config->misc->qucheng = 'https://www.qucheng.com'; +$config->misc->zentaonet = 'https://www.zentao.net'; $config->misc->featureLimit = '2024-06-14'; diff --git a/module/misc/control.php b/module/misc/control.php index 9879e8ecfd..34249c91c8 100644 --- a/module/misc/control.php +++ b/module/misc/control.php @@ -348,4 +348,19 @@ class misc extends control $api = $this->config->misc->sendEventAPI . '?action=' . $step . '&version=' . $this->config->version; commonModel::http($api, null, array(), array(), 'data', 'GET', $timeout = 2); } + + /** + * 安装过程的事件埋点。 + * Event burial points during installation process. + * + * @access public + * @return void + */ + public function installEvent() + { + $data = new stdClass(); + $data->location = $this->post->location; + $data->fingerprint = $this->post->fingerprint; + $this->misc->sendInstallEvent($data); + } } diff --git a/module/misc/model.php b/module/misc/model.php index c53949147c..679c1d0540 100644 --- a/module/misc/model.php +++ b/module/misc/model.php @@ -346,4 +346,36 @@ class miscModel extends model $this->loadModel('setting')->updateItem('system.misc.statistics', json_encode(array('date' => date('Y-m-d'), 'data' => $data))); return $data; } + + /** + * 发送安装过程埋点。 + * Sending installation process buried points. + * + * @param object $data + * @access public + * @return bool + */ + public function sendInstallEvent($data) + { + $apiRoot = $this->config->misc->zentaonet; + $apiURL = $apiRoot . "/zentaoevent-install.json"; + $sn = $this->loadModel('setting')->setSN(); + + $httpData['sn'] = $sn; + $httpData['location'] = $data->location; + $httpData['fingerprint'] = $data->fingerprint; + $httpData['edition'] = $this->config->edition; + $httpData['version'] = $this->config->version; + $httpData['phpVersion'] = phpversion(); + $httpData['os'] = PHP_OS . ' ' . php_uname('m'); + $httpData['isDocker'] = file_exists('/.dockerenv'); + $httpData['installType'] = ''; + $httpData['ip'] = helper::getRemoteIP(); + + $this->loadModel('common'); + $response = common::http($apiURL, $httpData); + $response = json_decode($response); + if($response && $response->result == 'success') return true; + return false; + } } diff --git a/module/misc/test/model/sendinstallevent.php b/module/misc/test/model/sendinstallevent.php new file mode 100644 index 0000000000..399a3644a7 --- /dev/null +++ b/module/misc/test/model/sendinstallevent.php @@ -0,0 +1,21 @@ +#!/usr/bin/env php +sendInstallEvent(); +timeout=0 +cid=1 + +- 发送安装过程埋点 @Success + +*/ + +global $tester; +$miscModel = $tester->loadModel('misc'); + +$data = new stdClass(); +$data->fingerprint = 'ba237c362b6aad54d17e7613eac23ec1'; +$data->location = 'join-community'; +r($miscModel->sendInstallEvent($data)) && p() && e('Success'); //测试发送安装过程埋点是否成功 diff --git a/module/setting/model.php b/module/setting/model.php index 5757260a4f..976b945996 100644 --- a/module/setting/model.php +++ b/module/setting/model.php @@ -324,31 +324,32 @@ class settingModel extends model */ public function setSN() { - $file = dirname(dirname(dirname(__FILE__))) . '/tmp/sn'; - $fileSn = file_get_contents($file); - if($fileSn) + $sn = $_COOKIE['sn']; + if(!isset($this->config->installed) || !$this->config->installed) { - $sn = $fileSn; + if(!$sn) $sn = ''; + if($this->snNeededUpdate($sn)) + { + $sn = $this->computeSN(); + setCookie('sn', $sn); + } + } else { - if(isset($this->config->global->sn)) + if(!$sn) { $sn = $this->getItem('owner=system&module=common§ion=global&key=sn'); if($this->snNeededUpdate($sn)) { $sn = $this->computeSN(); - $this->setItem('system.common.global.sn', $sn); } } - else - { - $sn = $this->computeSN(); - } + $this->setItem('system.common.global.sn', $sn); + if(!isset($this->config->global)) $this->config->global = new stdclass(); + $this->config->global->sn = $sn; + setCookie('sn', $sn); } - if(!isset($this->config->global)) $this->config->global = new stdclass(); - $this->config->global->sn = $sn; - file_put_contents($file, $sn); return $sn; } From e405752c056bd22f00fbb359170df39eee16f906 Mon Sep 17 00:00:00 2001 From: zhangjialiang Date: Thu, 14 Aug 2025 17:14:15 +0800 Subject: [PATCH 10/13] * [task#147913,doing,0.1h] Format code. --- module/admin/js/register.ui.js | 2 +- module/install/js/index.ui.js | 2 +- module/install/js/step6.ui.js | 2 +- module/setting/model.php | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/module/admin/js/register.ui.js b/module/admin/js/register.ui.js index 7945a4a706..e90017fe9e 100644 --- a/module/admin/js/register.ui.js +++ b/module/admin/js/register.ui.js @@ -106,7 +106,7 @@ window.getFingerprint = function() window.skip = function() { - ajaxInstallEvent('click-skip-join-community'); + ajaxInstallEvent('click-skip'); setTimeout(function() { location.href = $.createLink('index', 'index'); }, 1000); diff --git a/module/install/js/index.ui.js b/module/install/js/index.ui.js index fd834f44ae..bc7753b1f3 100644 --- a/module/install/js/index.ui.js +++ b/module/install/js/index.ui.js @@ -5,4 +5,4 @@ window.switchLang = function(lang) $.ajax({url: $.createLink('misc', 'ajaxSendEvent', "step=start"), timeout: 2000}); -ajaxInstallEvent('start-install'); +ajaxInstallEvent('start'); diff --git a/module/install/js/step6.ui.js b/module/install/js/step6.ui.js index 0a5b043471..690f76b959 100644 --- a/module/install/js/step6.ui.js +++ b/module/install/js/step6.ui.js @@ -1,3 +1,3 @@ $.ajax({url: sendEventLink, timeout: 2000}); -ajaxInstallEvent('finish-install'); +ajaxInstallEvent('finish'); diff --git a/module/setting/model.php b/module/setting/model.php index 976b945996..3ae18a01e8 100644 --- a/module/setting/model.php +++ b/module/setting/model.php @@ -333,7 +333,6 @@ class settingModel extends model $sn = $this->computeSN(); setCookie('sn', $sn); } - } else { From 214032dbea350d2701ba2a80817c61677147b4b2 Mon Sep 17 00:00:00 2001 From: zhangjialiang Date: Wed, 20 Aug 2025 09:22:14 +0800 Subject: [PATCH 11/13] * [task#147913,doing,0.2h] Format code. --- module/misc/model.php | 11 +++++++++-- module/setting/model.php | 6 +++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/module/misc/model.php b/module/misc/model.php index 679c1d0540..07b0dd3edb 100644 --- a/module/misc/model.php +++ b/module/misc/model.php @@ -357,10 +357,17 @@ class miscModel extends model */ public function sendInstallEvent($data) { + if(!$data->location || !$data->fingerprint) return false; $apiRoot = $this->config->misc->zentaonet; $apiURL = $apiRoot . "/zentaoevent-install.json"; - $sn = $this->loadModel('setting')->setSN(); - + if(empty($this->config->global->sn)) + { + $sn = $this->loadModel('setting')->setSN(); + } + else + { + $sn = $this->config->global->sn; + } $httpData['sn'] = $sn; $httpData['location'] = $data->location; $httpData['fingerprint'] = $data->fingerprint; diff --git a/module/setting/model.php b/module/setting/model.php index 3ae18a01e8..d104e59af7 100644 --- a/module/setting/model.php +++ b/module/setting/model.php @@ -324,14 +324,14 @@ class settingModel extends model */ public function setSN() { - $sn = $_COOKIE['sn']; + $sn = $this->cookie->sn; if(!isset($this->config->installed) || !$this->config->installed) { if(!$sn) $sn = ''; if($this->snNeededUpdate($sn)) { $sn = $this->computeSN(); - setCookie('sn', $sn); + helper::setcookie('sn', $sn, 0); } } else @@ -347,7 +347,7 @@ class settingModel extends model $this->setItem('system.common.global.sn', $sn); if(!isset($this->config->global)) $this->config->global = new stdclass(); $this->config->global->sn = $sn; - setCookie('sn', $sn); + helper::setcookie('sn', $sn, 0); } return $sn; } From f6641d2121b31f6ec4a94dbd394c46e576ee4a6c Mon Sep 17 00:00:00 2001 From: zhangjialiang Date: Wed, 20 Aug 2025 09:48:58 +0800 Subject: [PATCH 12/13] * [task#147913,doing,0.3h] Supplementary Unit Testing. --- module/misc/test/model/sendinstallevent.php | 46 +++++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/module/misc/test/model/sendinstallevent.php b/module/misc/test/model/sendinstallevent.php index 399a3644a7..f2637208f2 100644 --- a/module/misc/test/model/sendinstallevent.php +++ b/module/misc/test/model/sendinstallevent.php @@ -8,14 +8,52 @@ title=测试 miscModel->sendInstallEvent(); timeout=0 cid=1 -- 发送安装过程埋点 @Success +- 测试发送安装过程埋点是否成功 @true +- 测试location = '' @false +- 测试fingerprint = '' @false +- 测试fingerprint = '' 并且 location = '' @false +- 测试$data中没有属性 @false */ global $tester; $miscModel = $tester->loadModel('misc'); +$fingerprintList = array(); +$locationList = array(); + +$fingerprintList[0] = 'ba237c362b6aad54d17e7613eac23ec1'; +$locationList[0] = 'start'; + +$fingerprintList[1] = '572e8c0ccbce6f171980fd98b2b298b1'; +$locationList[1] = ''; + +$fingerprintList[2] = ''; +$locationList[2] = 'join-community'; + +$fingerprintList[3] = ''; +$locationList[3] = ''; + + $data = new stdClass(); -$data->fingerprint = 'ba237c362b6aad54d17e7613eac23ec1'; -$data->location = 'join-community'; -r($miscModel->sendInstallEvent($data)) && p() && e('Success'); //测试发送安装过程埋点是否成功 +$data->fingerprint = $fingerprintList[0]; +$data->location = $locationList[0]; +r($miscModel->sendInstallEvent($data)) && p() && e('true'); // 测试发送安装过程埋点是否成功 + +$data = new stdClass(); +$data->fingerprint = $fingerprintList[1]; +$data->location = $locationList[1]; +r($miscModel->sendInstallEvent($data)) && p() && e('false'); // 测试location = '' + +$data = new stdClass(); +$data->fingerprint = $fingerprintList[2]; +$data->location = $locationList[2]; +r($miscModel->sendInstallEvent($data)) && p() && e('false'); // 测试fingerprint = '' + +$data = new stdClass(); +$data->fingerprint = $fingerprintList[3]; +$data->location = $locationList[3]; +r($miscModel->sendInstallEvent($data)) && p() && e('false'); // 测试fingerprint = '' 并且 location = '' + +$data = new stdClass(); +r($miscModel->sendInstallEvent($data)) && p() && e('false'); // 测试$data中没有属性 \ No newline at end of file From bea52a6d789b57e240e2f634386a0b960f61c4a2 Mon Sep 17 00:00:00 2001 From: zhangjialiang Date: Wed, 20 Aug 2025 16:42:10 +0800 Subject: [PATCH 13/13] + [task#147913,doing,0.1h] Add fingerprint.js. --- www/js/fingerprint/Gruntfile.js | 31 + www/js/fingerprint/fingerprint.js | 12 + www/js/fingerprint/index.html | 0 www/js/fingerprint/package.json | 10 + www/js/fingerprint/specs/fingerprint_spec.js | 124 + www/js/fingerprint/specs/index.html | 0 www/js/fingerprint/specs/lib/index.html | 0 .../specs/lib/jasmine-1.3.1/index.html | 0 .../specs/lib/jasmine-1.3.1/jasmine-html.js | 681 +++++ .../specs/lib/jasmine-1.3.1/jasmine.css | 82 + .../specs/lib/jasmine-1.3.1/jasmine.js | 2600 +++++++++++++++++ .../lib/phantom-jasmine/console-runner.js | 104 + .../specs/lib/phantom-jasmine/index.html | 0 .../phantom-jasmine/run_jasmine_test.coffee | 46 + www/js/fingerprint/specs/run.sh | 1 + www/js/fingerprint/specs/test_runner.html | 22 + 16 files changed, 3713 insertions(+) create mode 100644 www/js/fingerprint/Gruntfile.js create mode 100644 www/js/fingerprint/fingerprint.js create mode 100644 www/js/fingerprint/index.html create mode 100644 www/js/fingerprint/package.json create mode 100644 www/js/fingerprint/specs/fingerprint_spec.js create mode 100644 www/js/fingerprint/specs/index.html create mode 100644 www/js/fingerprint/specs/lib/index.html create mode 100644 www/js/fingerprint/specs/lib/jasmine-1.3.1/index.html create mode 100644 www/js/fingerprint/specs/lib/jasmine-1.3.1/jasmine-html.js create mode 100644 www/js/fingerprint/specs/lib/jasmine-1.3.1/jasmine.css create mode 100644 www/js/fingerprint/specs/lib/jasmine-1.3.1/jasmine.js create mode 100644 www/js/fingerprint/specs/lib/phantom-jasmine/console-runner.js create mode 100644 www/js/fingerprint/specs/lib/phantom-jasmine/index.html create mode 100644 www/js/fingerprint/specs/lib/phantom-jasmine/run_jasmine_test.coffee create mode 100644 www/js/fingerprint/specs/run.sh create mode 100644 www/js/fingerprint/specs/test_runner.html diff --git a/www/js/fingerprint/Gruntfile.js b/www/js/fingerprint/Gruntfile.js new file mode 100644 index 0000000000..d9352c8301 --- /dev/null +++ b/www/js/fingerprint/Gruntfile.js @@ -0,0 +1,31 @@ +module.exports = function(grunt) { + + // Project configuration. + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + uglify: { + options: { + banner: '/*! <%= pkg.name %> <%= grunt.template.today("yyyy-mm-dd") %> - v<%= pkg.version %> */\n' + }, + build: { + src: 'fingerprint.js', + dest: 'build/fingerprint.min.js' + } + }, + jshint: { + file: ['Gruntfile.js', 'fingerprint.js', 'specs/**/*_spec.js'], + options: { + eqnull: true, + '-W086': true //W086: Expected a 'break' statement before 'case'. + } + } + }); + + // Load the plugin that provides the "uglify" task. + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + + // Default task(s). + grunt.registerTask('default', ['jshint', 'uglify']); + +}; \ No newline at end of file diff --git a/www/js/fingerprint/fingerprint.js b/www/js/fingerprint/fingerprint.js new file mode 100644 index 0000000000..11d0ffb419 --- /dev/null +++ b/www/js/fingerprint/fingerprint.js @@ -0,0 +1,12 @@ +/** + * FingerprintJS v4.6.0 - Copyright (c) FingerprintJS, Inc, 2025 (https://fingerprint.com) + * + * Licensed under Business Source License 1.1 https://mariadb.com/bsl11/ + * Licensor: FingerprintJS, Inc. + * Licensed Work: FingerprintJS browser fingerprinting library + * Additional Use Grant: None + * Change Date: Four years from first release for the specific version. + * Change License: MIT, text at https://opensource.org/license/mit/ with the following copyright notice: + * Copyright 2015-present FingerprintJS, Inc. + */ +var FingerprintJS=function(n){"use strict";var e=function(){return e=Object.assign||function(n){for(var e,t=1,r=arguments.length;t0&&o[o.length-1])||6!==c[0]&&2!==c[0])){a=0;continue}if(3===c[0]&&(!o||c[1]>o[0]&&c[1]=i+o?(i=c,[4,new Promise((function(n){var e=new MessageChannel;e.port1.onmessage=function(){return n()},e.port2.postMessage(null)}))]):[3,3]):[3,4];case 2:r.sent(),r.label=3;case 3:return++a,[3,1];case 4:return[2,t]}}))}))}function l(n){return n.then(void 0,(function(){})),n}function s(n){return parseInt(n)}function d(n){return parseFloat(n)}function f(n,e){return"number"==typeof n&&isNaN(n)?e:n}function m(n){return n.reduce((function(n,e){return n+(e?1:0)}),0)}function v(n,e){if(void 0===e&&(e=1),Math.abs(e)>=1)return Math.round(n/e)*e;var t=1/e;return Math.round(n*t)/t}function h(n,e){var t=n[0]>>>16,r=65535&n[0],o=n[1]>>>16,i=65535&n[1],a=e[0]>>>16,c=65535&e[0],u=e[1]>>>16,l=0,s=0,d=0,f=0;d+=(f+=i+(65535&e[1]))>>>16,f&=65535,s+=(d+=o+u)>>>16,d&=65535,l+=(s+=r+c)>>>16,s&=65535,l+=t+a,l&=65535,n[0]=l<<16|s,n[1]=d<<16|f}function p(n,e){var t=n[0]>>>16,r=65535&n[0],o=n[1]>>>16,i=65535&n[1],a=e[0]>>>16,c=65535&e[0],u=e[1]>>>16,l=65535&e[1],s=0,d=0,f=0,m=0;f+=(m+=i*l)>>>16,m&=65535,d+=(f+=o*l)>>>16,f&=65535,d+=(f+=i*u)>>>16,f&=65535,s+=(d+=r*l)>>>16,d&=65535,s+=(d+=o*u)>>>16,d&=65535,s+=(d+=i*c)>>>16,d&=65535,s+=t*l+r*u+o*c+i*a,s&=65535,n[0]=s<<16|d,n[1]=f<<16|m}function b(n,e){var t=n[0];32===(e%=64)?(n[0]=n[1],n[1]=t):e<32?(n[0]=t<>>32-e,n[1]=n[1]<>>32-e):(e-=32,n[0]=n[1]<>>32-e,n[1]=t<>>32-e)}function y(n,e){0!==(e%=64)&&(e<32?(n[0]=n[1]>>>32-e,n[1]=n[1]<>>1];g(n,e),p(n,w),e[1]=n[0]>>>1,g(n,e),p(n,L),e[1]=n[0]>>>1,g(n,e)}var V=[2277735313,289559509],S=[1291169091,658871167],W=[0,5],x=[0,1390208809],Z=[0,944331445];function M(n,e){var t=function(n){for(var e=new Uint8Array(n.length),t=0;t127)return(new TextEncoder).encode(n);e[t]=r}return e}(n);e=e||0;var r,o=[0,t.length],i=o[1]%16,a=o[1]-i,c=[0,e],u=[0,e],l=[0,0],s=[0,0];for(r=0;r>>0).toString(16)).slice(-8)+("00000000"+(c[1]>>>0).toString(16)).slice(-8)+("00000000"+(u[0]>>>0).toString(16)).slice(-8)+("00000000"+(u[1]>>>0).toString(16)).slice(-8)}function R(n){return"function"!=typeof n}function F(n,e,o,i){var a=Object.keys(n).filter((function(n){return!function(n,e){for(var t=0,r=n.length;t=4}function I(){var n=window,e=navigator;return m(["msWriteProfilerMark"in n,"MSStream"in n,"msLaunchUri"in e,"msSaveBlob"in e])>=3&&!G()}function Y(){var n=window,e=navigator;return m(["webkitPersistentStorage"in e,"webkitTemporaryStorage"in e,0===e.vendor.indexOf("Google"),"webkitResolveLocalFileSystemURL"in n,"BatteryManager"in n,"webkitMediaStream"in n,"webkitSpeechGrammar"in n])>=5}function C(){var n=window;return m(["ApplePayError"in n,"CSSPrimitiveValue"in n,"Counter"in n,0===navigator.vendor.indexOf("Apple"),"RGBColor"in n,"WebKitMediaKeys"in n])>=4}function j(){var n=window,e=n.HTMLElement,t=n.Document;return m(["safari"in n,!("ongestureend"in n),!("TouchEvent"in n),!("orientation"in n),e&&!("autocapitalize"in e.prototype),t&&"pointerLockElement"in t.prototype])>=4}function X(){var n,e=window;return n=e.print,/^function\s.*?\{\s*\[native code]\s*}$/.test(String(n))&&"[object WebPageNamespace]"===String(e.browser)}function P(){var n,e,t=window;return m(["buildID"in navigator,"MozAppearance"in(null!==(e=null===(n=document.documentElement)||void 0===n?void 0:n.style)&&void 0!==e?e:{}),"onmozfullscreenchange"in t,"mozInnerScreenX"in t,"CSSMozDocumentRule"in t,"CanvasCaptureMediaStream"in t])>=4}function E(){var n=window,e=navigator,t=n.CSS,r=n.HTMLButtonElement;return m([!("getStorageUpdates"in e),r&&"popover"in r.prototype,"CSSCounterStyleRule"in n,t.supports("font-size-adjust: ex-height 0.5"),t.supports("text-transform: full-width")])>=4}function H(){var n=document;return n.fullscreenElement||n.msFullscreenElement||n.mozFullScreenElement||n.webkitFullscreenElement||null}function A(){var n=Y(),e=P(),t=window,r=navigator,o="connection";return n?m([!("SharedWorker"in t),r[o]&&"ontypechange"in r[o],!("sinkId"in new Audio)])>=2:!!e&&m(["onorientationchange"in t,"orientation"in t,/android/i.test(r.appVersion)])>=2}function N(){var n=navigator,e=window,t=Audio.prototype,r=e.visualViewport;return m(["srLatency"in t,"srChannelCount"in t,"devicePosture"in n,r&&"segments"in r,"getTextInformation"in Image.prototype])>=3}function J(){var n=window,e=n.OfflineAudioContext||n.webkitOfflineAudioContext;if(!e)return-2;if(C()&&!j()&&!function(){var n=window;return m(["DOMRectList"in n,"RTCPeerConnectionIceEvent"in n,"SVGGeometryElement"in n,"ontransitioncancel"in n])>=3}())return-1;var t=new e(1,5e3,44100),r=t.createOscillator();r.type="triangle",r.frequency.value=1e4;var o=t.createDynamicsCompressor();o.threshold.value=-50,o.knee.value=40,o.ratio.value=12,o.attack.value=0,o.release.value=.25,r.connect(o),o.connect(t.destination),r.start(0);var i=function(n){var e=3,t=500,r=500,o=5e3,i=function(){};return[new Promise((function(c,u){var s=!1,d=0,f=0;n.oncomplete=function(n){return c(n.renderedBuffer)};var m=function(){setTimeout((function(){return u(T("timeout"))}),Math.min(r,f+o-Date.now()))},v=function(){try{var r=n.startRendering();switch(a(r)&&l(r),n.state){case"running":f=Date.now(),s&&m();break;case"suspended":document.hidden||d++,s&&d>=e?u(T("suspended")):setTimeout(v,t)}}catch(o){u(o)}};v(),i=function(){s||(s=!0,f>0&&m())}})),i]}(t),c=i[0],u=i[1],s=l(c.then((function(n){return function(n){for(var e=0,t=0;t.6*t.length}))).sort(),[2,i]}var c}))}))},fontPreferences:function(){return function(n,e){void 0===e&&(e=4e3);return D((function(t,r){var i=r.document,a=i.body,c=a.style;c.width="".concat(e,"px"),c.webkitTextSizeAdjust=c.textSizeAdjust="none",Y()?a.style.zoom="".concat(1/r.devicePixelRatio):C()&&(a.style.zoom="reset");var u=i.createElement("div");return u.textContent=o([],Array(e/20<<0),!0).map((function(){return"word"})).join(" "),a.appendChild(u),n(i,a)}),'')}((function(n,e){for(var t={},r={},o=0,i=Object.keys(vn);o=3)?-4:J();var n,e},screenFrame:function(){var n=this;if(C()&&E()&&X())return function(){return Promise.resolve(void 0)};var e=nn();return function(){return t(n,void 0,void 0,(function(){var n,t;return r(this,(function(r){switch(r.label){case 0:return[4,e()];case 1:return n=r.sent(),[2,[(t=function(n){return null===n?null:v(n,10)})(n[0]),t(n[1]),t(n[2]),t(n[3])]]}}))}))}},canvas:function(){return U(C()&&E()&&X())},osCpu:function(){return navigator.oscpu},languages:function(){var n,e=navigator,t=[],r=e.language||e.userLanguage||e.browserLanguage||e.systemLanguage;if(void 0!==r&&t.push([r]),Array.isArray(e.languages))Y()&&m([!("MediaSettingsRange"in(n=window)),"RTCEncodedAudioFrame"in n,""+n.Intl=="[object Intl]",""+n.Reflect=="[object Reflect]"])>=3||t.push(e.languages);else if("string"==typeof e.languages){var o=e.languages;o&&t.push(o.split(","))}return t},colorDepth:function(){return window.screen.colorDepth},deviceMemory:function(){return f(d(navigator.deviceMemory),void 0)},screenResolution:function(){if(!(C()&&E()&&X()))return K()},hardwareConcurrency:function(){return f(s(navigator.hardwareConcurrency),void 0)},timezone:function(){var n,e=null===(n=window.Intl)||void 0===n?void 0:n.DateTimeFormat;if(e){var t=(new e).resolvedOptions().timeZone;if(t)return t}var r,o=(r=(new Date).getFullYear(),-Math.max(d(new Date(r,0,1).getTimezoneOffset()),d(new Date(r,6,1).getTimezoneOffset())));return"UTC".concat(o>=0?"+":"").concat(o)},sessionStorage:function(){try{return!!window.sessionStorage}catch(n){return!0}},localStorage:function(){try{return!!window.localStorage}catch(n){return!0}},indexedDB:function(){if(!G()&&!I())try{return!!window.indexedDB}catch(n){return!0}},openDatabase:function(){return!!window.openDatabase},cpuClass:function(){return navigator.cpuClass},platform:function(){var n=navigator.platform;return"MacIntel"===n&&C()&&!j()?function(){if("iPad"===navigator.platform)return!0;var n=screen,e=n.width/n.height;return m(["MediaSource"in window,!!Element.prototype.webkitRequestFullscreen,e>.65&&e<1.53])>=2}()?"iPad":"iPhone":n},plugins:function(){var n=navigator.plugins;if(n){for(var e=[],t=0;t 0; + }, + toBeA: function(expected) { + return typeof this.actual === expected; + } + }); + }); + describe("new Fingerprint", function(){ + it("Creates a new instance of Fingerprint", function(){ + expect(new Fingerprint()).not.toBeNull(); + }); + + it("Accepts a custom hashing function as argument", function(){ + var hasher = function(){return 31;}; + expect(new Fingerprint(hasher)).not.toBeNull(); + }); + + it("Accepts a custom hasing function as options argument", function(){ + var hasher = function(){return 31;}; + expect(new Fingerprint({hasher: hasher})).not.toBeNull(); + }); + }); + + describe("#get", function(){ + it("Calculates fingerprint with built-in hashing if no custom hashing is given", function(){ + var fingerprint = new Fingerprint(); + spyOn(fingerprint, 'murmurhash3_32_gc'); + fingerprint.get(); + expect(fingerprint.murmurhash3_32_gc).toHaveBeenCalled(); + }); + + it("Calculates fingerprint with custom hashing if it is given as an argument", function(){ + var hasher = function(){return 'abcdef';}; + var fingerprint = new Fingerprint(hasher); + expect(fingerprint.get()).toEqual('abcdef'); + }); + + it("Calculates fingerprint with custom hashing if it is given as an options argument", function(){ + var hasher = function(){return 'abcdef';}; + var fingerprint = new Fingerprint({hasher: hasher}); + expect(fingerprint.get()).toEqual('abcdef'); + }); + + it('Calculates fingerprint with canvas fingerprinting if it is said to do so', function(){ + var fp = new Fingerprint({canvas: true}); + spyOn(fp, 'getCanvasFingerprint'); + fp.get(); + expect(fp.getCanvasFingerprint).toHaveBeenCalled(); + }); + + + it('Does not try to use canvas fingerprinting when not told to(version 1)', function(){ + var fp = new Fingerprint({canvas: false}); + spyOn(fp, 'getCanvasFingerprint'); + fp.get(); + expect(fp.getCanvasFingerprint).not.toHaveBeenCalled(); + }); + + it('Does not try to use canvas fingerprinting when not told to(version 2)', function(){ + var fp = new Fingerprint(); + spyOn(fp, 'getCanvasFingerprint'); + fp.get(); + expect(fp.getCanvasFingerprint).not.toHaveBeenCalled(); + }); + + it('Calculates fingerprint with ActiveX fingerprinting if it is said to do so', function(){ + var fp = new Fingerprint({ie_activex: true}); + spyOn(fp, 'isIE').andReturn(true); + spyOn(fp, 'getIEPluginsString'); + fp.get(); + expect(fp.getIEPluginsString).toHaveBeenCalled(); + }); + + + it('Does not try to use ActiveX fingerprinting when not told to(version 1)', function(){ + var fp = new Fingerprint({ie_activex: false}); + spyOn(fp, 'getIEPluginsString'); + fp.get(); + expect(fp.getIEPluginsString).not.toHaveBeenCalled(); + }); + + it('Does not try to use ActiveX fingerprinting when not told to(version 2)', function(){ + var fp = new Fingerprint(); + spyOn(fp, 'getIEPluginsString'); + fp.get(); + expect(fp.getIEPluginsString).not.toHaveBeenCalled(); + }); + + it('Calculates fingerprint accessing screen resolution if it is said to do so', function(){ + var fp = new Fingerprint({screen_resolution: true}); + spyOn(fp, 'getScreenResolution'); + fp.get(); + expect(fp.getScreenResolution).toHaveBeenCalled(); + }); + + it('Does not try to use screen resolution when not told to', function(){ + var fp = new Fingerprint(); + spyOn(fp, 'getScreenResolution'); + fp.get(); + expect(fp.getScreenResolution).not.toHaveBeenCalled(); + }); + + it("Returns a number as a fingerprint value when used with a built-in hashing function", function(){ + var fingerprint = new Fingerprint(); + expect(fingerprint.get()).toBeA('number'); + }); + + it('Returns a string from getCanvasFingerprint function', function(){ + var fp = new Fingerprint({canvas: true}); + expect(fp.getCanvasFingerprint()).toBeA('string'); + }); + + it('Does not fail when document.body is null', function(){ + var body = document.body.parentNode.removeChild(document.body); + var fingerprint = new Fingerprint(); + expect(fingerprint.get()).toBeA('number'); + document.body = body; + }); + }); +}); \ No newline at end of file diff --git a/www/js/fingerprint/specs/index.html b/www/js/fingerprint/specs/index.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/www/js/fingerprint/specs/lib/index.html b/www/js/fingerprint/specs/lib/index.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/www/js/fingerprint/specs/lib/jasmine-1.3.1/index.html b/www/js/fingerprint/specs/lib/jasmine-1.3.1/index.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/www/js/fingerprint/specs/lib/jasmine-1.3.1/jasmine-html.js b/www/js/fingerprint/specs/lib/jasmine-1.3.1/jasmine-html.js new file mode 100644 index 0000000000..543d56963e --- /dev/null +++ b/www/js/fingerprint/specs/lib/jasmine-1.3.1/jasmine-html.js @@ -0,0 +1,681 @@ +jasmine.HtmlReporterHelpers = {}; + +jasmine.HtmlReporterHelpers.createDom = function(type, attrs, childrenVarArgs) { + var el = document.createElement(type); + + for (var i = 2; i < arguments.length; i++) { + var child = arguments[i]; + + if (typeof child === 'string') { + el.appendChild(document.createTextNode(child)); + } else { + if (child) { + el.appendChild(child); + } + } + } + + for (var attr in attrs) { + if (attr == "className") { + el[attr] = attrs[attr]; + } else { + el.setAttribute(attr, attrs[attr]); + } + } + + return el; +}; + +jasmine.HtmlReporterHelpers.getSpecStatus = function(child) { + var results = child.results(); + var status = results.passed() ? 'passed' : 'failed'; + if (results.skipped) { + status = 'skipped'; + } + + return status; +}; + +jasmine.HtmlReporterHelpers.appendToSummary = function(child, childElement) { + var parentDiv = this.dom.summary; + var parentSuite = (typeof child.parentSuite == 'undefined') ? 'suite' : 'parentSuite'; + var parent = child[parentSuite]; + + if (parent) { + if (typeof this.views.suites[parent.id] == 'undefined') { + this.views.suites[parent.id] = new jasmine.HtmlReporter.SuiteView(parent, this.dom, this.views); + } + parentDiv = this.views.suites[parent.id].element; + } + + parentDiv.appendChild(childElement); +}; + + +jasmine.HtmlReporterHelpers.addHelpers = function(ctor) { + for(var fn in jasmine.HtmlReporterHelpers) { + ctor.prototype[fn] = jasmine.HtmlReporterHelpers[fn]; + } +}; + +jasmine.HtmlReporter = function(_doc) { + var self = this; + var doc = _doc || window.document; + + var reporterView; + + var dom = {}; + + // Jasmine Reporter Public Interface + self.logRunningSpecs = false; + + self.reportRunnerStarting = function(runner) { + var specs = runner.specs() || []; + + if (specs.length == 0) { + return; + } + + createReporterDom(runner.env.versionString()); + doc.body.appendChild(dom.reporter); + setExceptionHandling(); + + reporterView = new jasmine.HtmlReporter.ReporterView(dom); + reporterView.addSpecs(specs, self.specFilter); + }; + + self.reportRunnerResults = function(runner) { + reporterView && reporterView.complete(); + }; + + self.reportSuiteResults = function(suite) { + reporterView.suiteComplete(suite); + }; + + self.reportSpecStarting = function(spec) { + if (self.logRunningSpecs) { + self.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); + } + }; + + self.reportSpecResults = function(spec) { + reporterView.specComplete(spec); + }; + + self.log = function() { + var console = jasmine.getGlobal().console; + if (console && console.log) { + if (console.log.apply) { + console.log.apply(console, arguments); + } else { + console.log(arguments); // ie fix: console.log.apply doesn't exist on ie + } + } + }; + + self.specFilter = function(spec) { + if (!focusedSpecName()) { + return true; + } + + return spec.getFullName().indexOf(focusedSpecName()) === 0; + }; + + return self; + + function focusedSpecName() { + var specName; + + (function memoizeFocusedSpec() { + if (specName) { + return; + } + + var paramMap = []; + var params = jasmine.HtmlReporter.parameters(doc); + + for (var i = 0; i < params.length; i++) { + var p = params[i].split('='); + paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); + } + + specName = paramMap.spec; + })(); + + return specName; + } + + function createReporterDom(version) { + dom.reporter = self.createDom('div', { id: 'HTMLReporter', className: 'jasmine_reporter' }, + dom.banner = self.createDom('div', { className: 'banner' }, + self.createDom('span', { className: 'title' }, "Jasmine "), + self.createDom('span', { className: 'version' }, version)), + + dom.symbolSummary = self.createDom('ul', {className: 'symbolSummary'}), + dom.alert = self.createDom('div', {className: 'alert'}, + self.createDom('span', { className: 'exceptions' }, + self.createDom('label', { className: 'label', 'for': 'no_try_catch' }, 'No try/catch'), + self.createDom('input', { id: 'no_try_catch', type: 'checkbox' }))), + dom.results = self.createDom('div', {className: 'results'}, + dom.summary = self.createDom('div', { className: 'summary' }), + dom.details = self.createDom('div', { id: 'details' })) + ); + } + + function noTryCatch() { + return window.location.search.match(/catch=false/); + } + + function searchWithCatch() { + var params = jasmine.HtmlReporter.parameters(window.document); + var removed = false; + var i = 0; + + while (!removed && i < params.length) { + if (params[i].match(/catch=/)) { + params.splice(i, 1); + removed = true; + } + i++; + } + if (jasmine.CATCH_EXCEPTIONS) { + params.push("catch=false"); + } + + return params.join("&"); + } + + function setExceptionHandling() { + var chxCatch = document.getElementById('no_try_catch'); + + if (noTryCatch()) { + chxCatch.setAttribute('checked', true); + jasmine.CATCH_EXCEPTIONS = false; + } + chxCatch.onclick = function() { + window.location.search = searchWithCatch(); + }; + } +}; +jasmine.HtmlReporter.parameters = function(doc) { + var paramStr = doc.location.search.substring(1); + var params = []; + + if (paramStr.length > 0) { + params = paramStr.split('&'); + } + return params; +} +jasmine.HtmlReporter.sectionLink = function(sectionName) { + var link = '?'; + var params = []; + + if (sectionName) { + params.push('spec=' + encodeURIComponent(sectionName)); + } + if (!jasmine.CATCH_EXCEPTIONS) { + params.push("catch=false"); + } + if (params.length > 0) { + link += params.join("&"); + } + + return link; +}; +jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter); +jasmine.HtmlReporter.ReporterView = function(dom) { + this.startedAt = new Date(); + this.runningSpecCount = 0; + this.completeSpecCount = 0; + this.passedCount = 0; + this.failedCount = 0; + this.skippedCount = 0; + + this.createResultsMenu = function() { + this.resultsMenu = this.createDom('span', {className: 'resultsMenu bar'}, + this.summaryMenuItem = this.createDom('a', {className: 'summaryMenuItem', href: "#"}, '0 specs'), + ' | ', + this.detailsMenuItem = this.createDom('a', {className: 'detailsMenuItem', href: "#"}, '0 failing')); + + this.summaryMenuItem.onclick = function() { + dom.reporter.className = dom.reporter.className.replace(/ showDetails/g, ''); + }; + + this.detailsMenuItem.onclick = function() { + showDetails(); + }; + }; + + this.addSpecs = function(specs, specFilter) { + this.totalSpecCount = specs.length; + + this.views = { + specs: {}, + suites: {} + }; + + for (var i = 0; i < specs.length; i++) { + var spec = specs[i]; + this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom, this.views); + if (specFilter(spec)) { + this.runningSpecCount++; + } + } + }; + + this.specComplete = function(spec) { + this.completeSpecCount++; + + if (isUndefined(this.views.specs[spec.id])) { + this.views.specs[spec.id] = new jasmine.HtmlReporter.SpecView(spec, dom); + } + + var specView = this.views.specs[spec.id]; + + switch (specView.status()) { + case 'passed': + this.passedCount++; + break; + + case 'failed': + this.failedCount++; + break; + + case 'skipped': + this.skippedCount++; + break; + } + + specView.refresh(); + this.refresh(); + }; + + this.suiteComplete = function(suite) { + var suiteView = this.views.suites[suite.id]; + if (isUndefined(suiteView)) { + return; + } + suiteView.refresh(); + }; + + this.refresh = function() { + + if (isUndefined(this.resultsMenu)) { + this.createResultsMenu(); + } + + // currently running UI + if (isUndefined(this.runningAlert)) { + this.runningAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "runningAlert bar" }); + dom.alert.appendChild(this.runningAlert); + } + this.runningAlert.innerHTML = "Running " + this.completeSpecCount + " of " + specPluralizedFor(this.totalSpecCount); + + // skipped specs UI + if (isUndefined(this.skippedAlert)) { + this.skippedAlert = this.createDom('a', { href: jasmine.HtmlReporter.sectionLink(), className: "skippedAlert bar" }); + } + + this.skippedAlert.innerHTML = "Skipping " + this.skippedCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all"; + + if (this.skippedCount === 1 && isDefined(dom.alert)) { + dom.alert.appendChild(this.skippedAlert); + } + + // passing specs UI + if (isUndefined(this.passedAlert)) { + this.passedAlert = this.createDom('span', { href: jasmine.HtmlReporter.sectionLink(), className: "passingAlert bar" }); + } + this.passedAlert.innerHTML = "Passing " + specPluralizedFor(this.passedCount); + + // failing specs UI + if (isUndefined(this.failedAlert)) { + this.failedAlert = this.createDom('span', {href: "?", className: "failingAlert bar"}); + } + this.failedAlert.innerHTML = "Failing " + specPluralizedFor(this.failedCount); + + if (this.failedCount === 1 && isDefined(dom.alert)) { + dom.alert.appendChild(this.failedAlert); + dom.alert.appendChild(this.resultsMenu); + } + + // summary info + this.summaryMenuItem.innerHTML = "" + specPluralizedFor(this.runningSpecCount); + this.detailsMenuItem.innerHTML = "" + this.failedCount + " failing"; + }; + + this.complete = function() { + dom.alert.removeChild(this.runningAlert); + + this.skippedAlert.innerHTML = "Ran " + this.runningSpecCount + " of " + specPluralizedFor(this.totalSpecCount) + " - run all"; + + if (this.failedCount === 0) { + dom.alert.appendChild(this.createDom('span', {className: 'passingAlert bar'}, "Passing " + specPluralizedFor(this.passedCount))); + } else { + showDetails(); + } + + dom.banner.appendChild(this.createDom('span', {className: 'duration'}, "finished in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s")); + }; + + return this; + + function showDetails() { + if (dom.reporter.className.search(/showDetails/) === -1) { + dom.reporter.className += " showDetails"; + } + } + + function isUndefined(obj) { + return typeof obj === 'undefined'; + } + + function isDefined(obj) { + return !isUndefined(obj); + } + + function specPluralizedFor(count) { + var str = count + " spec"; + if (count > 1) { + str += "s" + } + return str; + } + +}; + +jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.ReporterView); + + +jasmine.HtmlReporter.SpecView = function(spec, dom, views) { + this.spec = spec; + this.dom = dom; + this.views = views; + + this.symbol = this.createDom('li', { className: 'pending' }); + this.dom.symbolSummary.appendChild(this.symbol); + + this.summary = this.createDom('div', { className: 'specSummary' }, + this.createDom('a', { + className: 'description', + href: jasmine.HtmlReporter.sectionLink(this.spec.getFullName()), + title: this.spec.getFullName() + }, this.spec.description) + ); + + this.detail = this.createDom('div', { className: 'specDetail' }, + this.createDom('a', { + className: 'description', + href: '?spec=' + encodeURIComponent(this.spec.getFullName()), + title: this.spec.getFullName() + }, this.spec.getFullName()) + ); +}; + +jasmine.HtmlReporter.SpecView.prototype.status = function() { + return this.getSpecStatus(this.spec); +}; + +jasmine.HtmlReporter.SpecView.prototype.refresh = function() { + this.symbol.className = this.status(); + + switch (this.status()) { + case 'skipped': + break; + + case 'passed': + this.appendSummaryToSuiteDiv(); + break; + + case 'failed': + this.appendSummaryToSuiteDiv(); + this.appendFailureDetail(); + break; + } +}; + +jasmine.HtmlReporter.SpecView.prototype.appendSummaryToSuiteDiv = function() { + this.summary.className += ' ' + this.status(); + this.appendToSummary(this.spec, this.summary); +}; + +jasmine.HtmlReporter.SpecView.prototype.appendFailureDetail = function() { + this.detail.className += ' ' + this.status(); + + var resultItems = this.spec.results().getItems(); + var messagesDiv = this.createDom('div', { className: 'messages' }); + + for (var i = 0; i < resultItems.length; i++) { + var result = resultItems[i]; + + if (result.type == 'log') { + messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); + } else if (result.type == 'expect' && result.passed && !result.passed()) { + messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); + + if (result.trace.stack) { + messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); + } + } + } + + if (messagesDiv.childNodes.length > 0) { + this.detail.appendChild(messagesDiv); + this.dom.details.appendChild(this.detail); + } +}; + +jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SpecView);jasmine.HtmlReporter.SuiteView = function(suite, dom, views) { + this.suite = suite; + this.dom = dom; + this.views = views; + + this.element = this.createDom('div', { className: 'suite' }, + this.createDom('a', { className: 'description', href: jasmine.HtmlReporter.sectionLink(this.suite.getFullName()) }, this.suite.description) + ); + + this.appendToSummary(this.suite, this.element); +}; + +jasmine.HtmlReporter.SuiteView.prototype.status = function() { + return this.getSpecStatus(this.suite); +}; + +jasmine.HtmlReporter.SuiteView.prototype.refresh = function() { + this.element.className += " " + this.status(); +}; + +jasmine.HtmlReporterHelpers.addHelpers(jasmine.HtmlReporter.SuiteView); + +/* @deprecated Use jasmine.HtmlReporter instead + */ +jasmine.TrivialReporter = function(doc) { + this.document = doc || document; + this.suiteDivs = {}; + this.logRunningSpecs = false; +}; + +jasmine.TrivialReporter.prototype.createDom = function(type, attrs, childrenVarArgs) { + var el = document.createElement(type); + + for (var i = 2; i < arguments.length; i++) { + var child = arguments[i]; + + if (typeof child === 'string') { + el.appendChild(document.createTextNode(child)); + } else { + if (child) { el.appendChild(child); } + } + } + + for (var attr in attrs) { + if (attr == "className") { + el[attr] = attrs[attr]; + } else { + el.setAttribute(attr, attrs[attr]); + } + } + + return el; +}; + +jasmine.TrivialReporter.prototype.reportRunnerStarting = function(runner) { + var showPassed, showSkipped; + + this.outerDiv = this.createDom('div', { id: 'TrivialReporter', className: 'jasmine_reporter' }, + this.createDom('div', { className: 'banner' }, + this.createDom('div', { className: 'logo' }, + this.createDom('span', { className: 'title' }, "Jasmine"), + this.createDom('span', { className: 'version' }, runner.env.versionString())), + this.createDom('div', { className: 'options' }, + "Show ", + showPassed = this.createDom('input', { id: "__jasmine_TrivialReporter_showPassed__", type: 'checkbox' }), + this.createDom('label', { "for": "__jasmine_TrivialReporter_showPassed__" }, " passed "), + showSkipped = this.createDom('input', { id: "__jasmine_TrivialReporter_showSkipped__", type: 'checkbox' }), + this.createDom('label', { "for": "__jasmine_TrivialReporter_showSkipped__" }, " skipped") + ) + ), + + this.runnerDiv = this.createDom('div', { className: 'runner running' }, + this.createDom('a', { className: 'run_spec', href: '?' }, "run all"), + this.runnerMessageSpan = this.createDom('span', {}, "Running..."), + this.finishedAtSpan = this.createDom('span', { className: 'finished-at' }, "")) + ); + + this.document.body.appendChild(this.outerDiv); + + var suites = runner.suites(); + for (var i = 0; i < suites.length; i++) { + var suite = suites[i]; + var suiteDiv = this.createDom('div', { className: 'suite' }, + this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, "run"), + this.createDom('a', { className: 'description', href: '?spec=' + encodeURIComponent(suite.getFullName()) }, suite.description)); + this.suiteDivs[suite.id] = suiteDiv; + var parentDiv = this.outerDiv; + if (suite.parentSuite) { + parentDiv = this.suiteDivs[suite.parentSuite.id]; + } + parentDiv.appendChild(suiteDiv); + } + + this.startedAt = new Date(); + + var self = this; + showPassed.onclick = function(evt) { + if (showPassed.checked) { + self.outerDiv.className += ' show-passed'; + } else { + self.outerDiv.className = self.outerDiv.className.replace(/ show-passed/, ''); + } + }; + + showSkipped.onclick = function(evt) { + if (showSkipped.checked) { + self.outerDiv.className += ' show-skipped'; + } else { + self.outerDiv.className = self.outerDiv.className.replace(/ show-skipped/, ''); + } + }; +}; + +jasmine.TrivialReporter.prototype.reportRunnerResults = function(runner) { + var results = runner.results(); + var className = (results.failedCount > 0) ? "runner failed" : "runner passed"; + this.runnerDiv.setAttribute("class", className); + //do it twice for IE + this.runnerDiv.setAttribute("className", className); + var specs = runner.specs(); + var specCount = 0; + for (var i = 0; i < specs.length; i++) { + if (this.specFilter(specs[i])) { + specCount++; + } + } + var message = "" + specCount + " spec" + (specCount == 1 ? "" : "s" ) + ", " + results.failedCount + " failure" + ((results.failedCount == 1) ? "" : "s"); + message += " in " + ((new Date().getTime() - this.startedAt.getTime()) / 1000) + "s"; + this.runnerMessageSpan.replaceChild(this.createDom('a', { className: 'description', href: '?'}, message), this.runnerMessageSpan.firstChild); + + this.finishedAtSpan.appendChild(document.createTextNode("Finished at " + new Date().toString())); +}; + +jasmine.TrivialReporter.prototype.reportSuiteResults = function(suite) { + var results = suite.results(); + var status = results.passed() ? 'passed' : 'failed'; + if (results.totalCount === 0) { // todo: change this to check results.skipped + status = 'skipped'; + } + this.suiteDivs[suite.id].className += " " + status; +}; + +jasmine.TrivialReporter.prototype.reportSpecStarting = function(spec) { + if (this.logRunningSpecs) { + this.log('>> Jasmine Running ' + spec.suite.description + ' ' + spec.description + '...'); + } +}; + +jasmine.TrivialReporter.prototype.reportSpecResults = function(spec) { + var results = spec.results(); + var status = results.passed() ? 'passed' : 'failed'; + if (results.skipped) { + status = 'skipped'; + } + var specDiv = this.createDom('div', { className: 'spec ' + status }, + this.createDom('a', { className: 'run_spec', href: '?spec=' + encodeURIComponent(spec.getFullName()) }, "run"), + this.createDom('a', { + className: 'description', + href: '?spec=' + encodeURIComponent(spec.getFullName()), + title: spec.getFullName() + }, spec.description)); + + + var resultItems = results.getItems(); + var messagesDiv = this.createDom('div', { className: 'messages' }); + for (var i = 0; i < resultItems.length; i++) { + var result = resultItems[i]; + + if (result.type == 'log') { + messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage log'}, result.toString())); + } else if (result.type == 'expect' && result.passed && !result.passed()) { + messagesDiv.appendChild(this.createDom('div', {className: 'resultMessage fail'}, result.message)); + + if (result.trace.stack) { + messagesDiv.appendChild(this.createDom('div', {className: 'stackTrace'}, result.trace.stack)); + } + } + } + + if (messagesDiv.childNodes.length > 0) { + specDiv.appendChild(messagesDiv); + } + + this.suiteDivs[spec.suite.id].appendChild(specDiv); +}; + +jasmine.TrivialReporter.prototype.log = function() { + var console = jasmine.getGlobal().console; + if (console && console.log) { + if (console.log.apply) { + console.log.apply(console, arguments); + } else { + console.log(arguments); // ie fix: console.log.apply doesn't exist on ie + } + } +}; + +jasmine.TrivialReporter.prototype.getLocation = function() { + return this.document.location; +}; + +jasmine.TrivialReporter.prototype.specFilter = function(spec) { + var paramMap = {}; + var params = this.getLocation().search.substring(1).split('&'); + for (var i = 0; i < params.length; i++) { + var p = params[i].split('='); + paramMap[decodeURIComponent(p[0])] = decodeURIComponent(p[1]); + } + + if (!paramMap.spec) { + return true; + } + return spec.getFullName().indexOf(paramMap.spec) === 0; +}; diff --git a/www/js/fingerprint/specs/lib/jasmine-1.3.1/jasmine.css b/www/js/fingerprint/specs/lib/jasmine-1.3.1/jasmine.css new file mode 100644 index 0000000000..8c008dc722 --- /dev/null +++ b/www/js/fingerprint/specs/lib/jasmine-1.3.1/jasmine.css @@ -0,0 +1,82 @@ +body { background-color: #eeeeee; padding: 0; margin: 5px; overflow-y: scroll; } + +#HTMLReporter { font-size: 11px; font-family: Monaco, "Lucida Console", monospace; line-height: 14px; color: #333333; } +#HTMLReporter a { text-decoration: none; } +#HTMLReporter a:hover { text-decoration: underline; } +#HTMLReporter p, #HTMLReporter h1, #HTMLReporter h2, #HTMLReporter h3, #HTMLReporter h4, #HTMLReporter h5, #HTMLReporter h6 { margin: 0; line-height: 14px; } +#HTMLReporter .banner, #HTMLReporter .symbolSummary, #HTMLReporter .summary, #HTMLReporter .resultMessage, #HTMLReporter .specDetail .description, #HTMLReporter .alert .bar, #HTMLReporter .stackTrace { padding-left: 9px; padding-right: 9px; } +#HTMLReporter #jasmine_content { position: fixed; right: 100%; } +#HTMLReporter .version { color: #aaaaaa; } +#HTMLReporter .banner { margin-top: 14px; } +#HTMLReporter .duration { color: #aaaaaa; float: right; } +#HTMLReporter .symbolSummary { overflow: hidden; *zoom: 1; margin: 14px 0; } +#HTMLReporter .symbolSummary li { display: block; float: left; height: 7px; width: 14px; margin-bottom: 7px; font-size: 16px; } +#HTMLReporter .symbolSummary li.passed { font-size: 14px; } +#HTMLReporter .symbolSummary li.passed:before { color: #5e7d00; content: "\02022"; } +#HTMLReporter .symbolSummary li.failed { line-height: 9px; } +#HTMLReporter .symbolSummary li.failed:before { color: #b03911; content: "x"; font-weight: bold; margin-left: -1px; } +#HTMLReporter .symbolSummary li.skipped { font-size: 14px; } +#HTMLReporter .symbolSummary li.skipped:before { color: #bababa; content: "\02022"; } +#HTMLReporter .symbolSummary li.pending { line-height: 11px; } +#HTMLReporter .symbolSummary li.pending:before { color: #aaaaaa; content: "-"; } +#HTMLReporter .exceptions { color: #fff; float: right; margin-top: 5px; margin-right: 5px; } +#HTMLReporter .bar { line-height: 28px; font-size: 14px; display: block; color: #eee; } +#HTMLReporter .runningAlert { background-color: #666666; } +#HTMLReporter .skippedAlert { background-color: #aaaaaa; } +#HTMLReporter .skippedAlert:first-child { background-color: #333333; } +#HTMLReporter .skippedAlert:hover { text-decoration: none; color: white; text-decoration: underline; } +#HTMLReporter .passingAlert { background-color: #a6b779; } +#HTMLReporter .passingAlert:first-child { background-color: #5e7d00; } +#HTMLReporter .failingAlert { background-color: #cf867e; } +#HTMLReporter .failingAlert:first-child { background-color: #b03911; } +#HTMLReporter .results { margin-top: 14px; } +#HTMLReporter #details { display: none; } +#HTMLReporter .resultsMenu, #HTMLReporter .resultsMenu a { background-color: #fff; color: #333333; } +#HTMLReporter.showDetails .summaryMenuItem { font-weight: normal; text-decoration: inherit; } +#HTMLReporter.showDetails .summaryMenuItem:hover { text-decoration: underline; } +#HTMLReporter.showDetails .detailsMenuItem { font-weight: bold; text-decoration: underline; } +#HTMLReporter.showDetails .summary { display: none; } +#HTMLReporter.showDetails #details { display: block; } +#HTMLReporter .summaryMenuItem { font-weight: bold; text-decoration: underline; } +#HTMLReporter .summary { margin-top: 14px; } +#HTMLReporter .summary .suite .suite, #HTMLReporter .summary .specSummary { margin-left: 14px; } +#HTMLReporter .summary .specSummary.passed a { color: #5e7d00; } +#HTMLReporter .summary .specSummary.failed a { color: #b03911; } +#HTMLReporter .description + .suite { margin-top: 0; } +#HTMLReporter .suite { margin-top: 14px; } +#HTMLReporter .suite a { color: #333333; } +#HTMLReporter #details .specDetail { margin-bottom: 28px; } +#HTMLReporter #details .specDetail .description { display: block; color: white; background-color: #b03911; } +#HTMLReporter .resultMessage { padding-top: 14px; color: #333333; } +#HTMLReporter .resultMessage span.result { display: block; } +#HTMLReporter .stackTrace { margin: 5px 0 0 0; max-height: 224px; overflow: auto; line-height: 18px; color: #666666; border: 1px solid #ddd; background: white; white-space: pre; } + +#TrivialReporter { padding: 8px 13px; position: absolute; top: 0; bottom: 0; left: 0; right: 0; overflow-y: scroll; background-color: white; font-family: "Helvetica Neue Light", "Lucida Grande", "Calibri", "Arial", sans-serif; /*.resultMessage {*/ /*white-space: pre;*/ /*}*/ } +#TrivialReporter a:visited, #TrivialReporter a { color: #303; } +#TrivialReporter a:hover, #TrivialReporter a:active { color: blue; } +#TrivialReporter .run_spec { float: right; padding-right: 5px; font-size: .8em; text-decoration: none; } +#TrivialReporter .banner { color: #303; background-color: #fef; padding: 5px; } +#TrivialReporter .logo { float: left; font-size: 1.1em; padding-left: 5px; } +#TrivialReporter .logo .version { font-size: .6em; padding-left: 1em; } +#TrivialReporter .runner.running { background-color: yellow; } +#TrivialReporter .options { text-align: right; font-size: .8em; } +#TrivialReporter .suite { border: 1px outset gray; margin: 5px 0; padding-left: 1em; } +#TrivialReporter .suite .suite { margin: 5px; } +#TrivialReporter .suite.passed { background-color: #dfd; } +#TrivialReporter .suite.failed { background-color: #fdd; } +#TrivialReporter .spec { margin: 5px; padding-left: 1em; clear: both; } +#TrivialReporter .spec.failed, #TrivialReporter .spec.passed, #TrivialReporter .spec.skipped { padding-bottom: 5px; border: 1px solid gray; } +#TrivialReporter .spec.failed { background-color: #fbb; border-color: red; } +#TrivialReporter .spec.passed { background-color: #bfb; border-color: green; } +#TrivialReporter .spec.skipped { background-color: #bbb; } +#TrivialReporter .messages { border-left: 1px dashed gray; padding-left: 1em; padding-right: 1em; } +#TrivialReporter .passed { background-color: #cfc; display: none; } +#TrivialReporter .failed { background-color: #fbb; } +#TrivialReporter .skipped { color: #777; background-color: #eee; display: none; } +#TrivialReporter .resultMessage span.result { display: block; line-height: 2em; color: black; } +#TrivialReporter .resultMessage .mismatch { color: black; } +#TrivialReporter .stackTrace { white-space: pre; font-size: .8em; margin-left: 10px; max-height: 5em; overflow: auto; border: 1px inset red; padding: 1em; background: #eef; } +#TrivialReporter .finished-at { padding-left: 1em; font-size: .6em; } +#TrivialReporter.show-passed .passed, #TrivialReporter.show-skipped .skipped { display: block; } +#TrivialReporter #jasmine_content { position: fixed; right: 100%; } +#TrivialReporter .runner { border: 1px solid gray; display: block; margin: 5px 0; padding: 2px 0 2px 10px; } diff --git a/www/js/fingerprint/specs/lib/jasmine-1.3.1/jasmine.js b/www/js/fingerprint/specs/lib/jasmine-1.3.1/jasmine.js new file mode 100644 index 0000000000..6b3459b913 --- /dev/null +++ b/www/js/fingerprint/specs/lib/jasmine-1.3.1/jasmine.js @@ -0,0 +1,2600 @@ +var isCommonJS = typeof window == "undefined" && typeof exports == "object"; + +/** + * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework. + * + * @namespace + */ +var jasmine = {}; +if (isCommonJS) exports.jasmine = jasmine; +/** + * @private + */ +jasmine.unimplementedMethod_ = function() { + throw new Error("unimplemented method"); +}; + +/** + * Use jasmine.undefined instead of undefined, since undefined is just + * a plain old variable and may be redefined by somebody else. + * + * @private + */ +jasmine.undefined = jasmine.___undefined___; + +/** + * Show diagnostic messages in the console if set to true + * + */ +jasmine.VERBOSE = false; + +/** + * Default interval in milliseconds for event loop yields (e.g. to allow network activity or to refresh the screen with the HTML-based runner). Small values here may result in slow test running. Zero means no updates until all tests have completed. + * + */ +jasmine.DEFAULT_UPDATE_INTERVAL = 250; + +/** + * Maximum levels of nesting that will be included when an object is pretty-printed + */ +jasmine.MAX_PRETTY_PRINT_DEPTH = 40; + +/** + * Default timeout interval in milliseconds for waitsFor() blocks. + */ +jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000; + +/** + * By default exceptions thrown in the context of a test are caught by jasmine so that it can run the remaining tests in the suite. + * Set to false to let the exception bubble up in the browser. + * + */ +jasmine.CATCH_EXCEPTIONS = true; + +jasmine.getGlobal = function() { + function getGlobal() { + return this; + } + + return getGlobal(); +}; + +/** + * Allows for bound functions to be compared. Internal use only. + * + * @ignore + * @private + * @param base {Object} bound 'this' for the function + * @param name {Function} function to find + */ +jasmine.bindOriginal_ = function(base, name) { + var original = base[name]; + if (original.apply) { + return function() { + return original.apply(base, arguments); + }; + } else { + // IE support + return jasmine.getGlobal()[name]; + } +}; + +jasmine.setTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'setTimeout'); +jasmine.clearTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearTimeout'); +jasmine.setInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'setInterval'); +jasmine.clearInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearInterval'); + +jasmine.MessageResult = function(values) { + this.type = 'log'; + this.values = values; + this.trace = new Error(); // todo: test better +}; + +jasmine.MessageResult.prototype.toString = function() { + var text = ""; + for (var i = 0; i < this.values.length; i++) { + if (i > 0) text += " "; + if (jasmine.isString_(this.values[i])) { + text += this.values[i]; + } else { + text += jasmine.pp(this.values[i]); + } + } + return text; +}; + +jasmine.ExpectationResult = function(params) { + this.type = 'expect'; + this.matcherName = params.matcherName; + this.passed_ = params.passed; + this.expected = params.expected; + this.actual = params.actual; + this.message = this.passed_ ? 'Passed.' : params.message; + + var trace = (params.trace || new Error(this.message)); + this.trace = this.passed_ ? '' : trace; +}; + +jasmine.ExpectationResult.prototype.toString = function () { + return this.message; +}; + +jasmine.ExpectationResult.prototype.passed = function () { + return this.passed_; +}; + +/** + * Getter for the Jasmine environment. Ensures one gets created + */ +jasmine.getEnv = function() { + var env = jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env(); + return env; +}; + +/** + * @ignore + * @private + * @param value + * @returns {Boolean} + */ +jasmine.isArray_ = function(value) { + return jasmine.isA_("Array", value); +}; + +/** + * @ignore + * @private + * @param value + * @returns {Boolean} + */ +jasmine.isString_ = function(value) { + return jasmine.isA_("String", value); +}; + +/** + * @ignore + * @private + * @param value + * @returns {Boolean} + */ +jasmine.isNumber_ = function(value) { + return jasmine.isA_("Number", value); +}; + +/** + * @ignore + * @private + * @param {String} typeName + * @param value + * @returns {Boolean} + */ +jasmine.isA_ = function(typeName, value) { + return Object.prototype.toString.apply(value) === '[object ' + typeName + ']'; +}; + +/** + * Pretty printer for expecations. Takes any object and turns it into a human-readable string. + * + * @param value {Object} an object to be outputted + * @returns {String} + */ +jasmine.pp = function(value) { + var stringPrettyPrinter = new jasmine.StringPrettyPrinter(); + stringPrettyPrinter.format(value); + return stringPrettyPrinter.string; +}; + +/** + * Returns true if the object is a DOM Node. + * + * @param {Object} obj object to check + * @returns {Boolean} + */ +jasmine.isDomNode = function(obj) { + return obj.nodeType > 0; +}; + +/** + * Returns a matchable 'generic' object of the class type. For use in expecations of type when values don't matter. + * + * @example + * // don't care about which function is passed in, as long as it's a function + * expect(mySpy).toHaveBeenCalledWith(jasmine.any(Function)); + * + * @param {Class} clazz + * @returns matchable object of the type clazz + */ +jasmine.any = function(clazz) { + return new jasmine.Matchers.Any(clazz); +}; + +/** + * Returns a matchable subset of a JSON object. For use in expectations when you don't care about all of the + * attributes on the object. + * + * @example + * // don't care about any other attributes than foo. + * expect(mySpy).toHaveBeenCalledWith(jasmine.objectContaining({foo: "bar"}); + * + * @param sample {Object} sample + * @returns matchable object for the sample + */ +jasmine.objectContaining = function (sample) { + return new jasmine.Matchers.ObjectContaining(sample); +}; + +/** + * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks. + * + * Spies should be created in test setup, before expectations. They can then be checked, using the standard Jasmine + * expectation syntax. Spies can be checked if they were called or not and what the calling params were. + * + * A Spy has the following fields: wasCalled, callCount, mostRecentCall, and argsForCall (see docs). + * + * Spies are torn down at the end of every spec. + * + * Note: Do not call new jasmine.Spy() directly - a spy must be created using spyOn, jasmine.createSpy or jasmine.createSpyObj. + * + * @example + * // a stub + * var myStub = jasmine.createSpy('myStub'); // can be used anywhere + * + * // spy example + * var foo = { + * not: function(bool) { return !bool; } + * } + * + * // actual foo.not will not be called, execution stops + * spyOn(foo, 'not'); + + // foo.not spied upon, execution will continue to implementation + * spyOn(foo, 'not').andCallThrough(); + * + * // fake example + * var foo = { + * not: function(bool) { return !bool; } + * } + * + * // foo.not(val) will return val + * spyOn(foo, 'not').andCallFake(function(value) {return value;}); + * + * // mock example + * foo.not(7 == 7); + * expect(foo.not).toHaveBeenCalled(); + * expect(foo.not).toHaveBeenCalledWith(true); + * + * @constructor + * @see spyOn, jasmine.createSpy, jasmine.createSpyObj + * @param {String} name + */ +jasmine.Spy = function(name) { + /** + * The name of the spy, if provided. + */ + this.identity = name || 'unknown'; + /** + * Is this Object a spy? + */ + this.isSpy = true; + /** + * The actual function this spy stubs. + */ + this.plan = function() { + }; + /** + * Tracking of the most recent call to the spy. + * @example + * var mySpy = jasmine.createSpy('foo'); + * mySpy(1, 2); + * mySpy.mostRecentCall.args = [1, 2]; + */ + this.mostRecentCall = {}; + + /** + * Holds arguments for each call to the spy, indexed by call count + * @example + * var mySpy = jasmine.createSpy('foo'); + * mySpy(1, 2); + * mySpy(7, 8); + * mySpy.mostRecentCall.args = [7, 8]; + * mySpy.argsForCall[0] = [1, 2]; + * mySpy.argsForCall[1] = [7, 8]; + */ + this.argsForCall = []; + this.calls = []; +}; + +/** + * Tells a spy to call through to the actual implemenatation. + * + * @example + * var foo = { + * bar: function() { // do some stuff } + * } + * + * // defining a spy on an existing property: foo.bar + * spyOn(foo, 'bar').andCallThrough(); + */ +jasmine.Spy.prototype.andCallThrough = function() { + this.plan = this.originalValue; + return this; +}; + +/** + * For setting the return value of a spy. + * + * @example + * // defining a spy from scratch: foo() returns 'baz' + * var foo = jasmine.createSpy('spy on foo').andReturn('baz'); + * + * // defining a spy on an existing property: foo.bar() returns 'baz' + * spyOn(foo, 'bar').andReturn('baz'); + * + * @param {Object} value + */ +jasmine.Spy.prototype.andReturn = function(value) { + this.plan = function() { + return value; + }; + return this; +}; + +/** + * For throwing an exception when a spy is called. + * + * @example + * // defining a spy from scratch: foo() throws an exception w/ message 'ouch' + * var foo = jasmine.createSpy('spy on foo').andThrow('baz'); + * + * // defining a spy on an existing property: foo.bar() throws an exception w/ message 'ouch' + * spyOn(foo, 'bar').andThrow('baz'); + * + * @param {String} exceptionMsg + */ +jasmine.Spy.prototype.andThrow = function(exceptionMsg) { + this.plan = function() { + throw exceptionMsg; + }; + return this; +}; + +/** + * Calls an alternate implementation when a spy is called. + * + * @example + * var baz = function() { + * // do some stuff, return something + * } + * // defining a spy from scratch: foo() calls the function baz + * var foo = jasmine.createSpy('spy on foo').andCall(baz); + * + * // defining a spy on an existing property: foo.bar() calls an anonymnous function + * spyOn(foo, 'bar').andCall(function() { return 'baz';} ); + * + * @param {Function} fakeFunc + */ +jasmine.Spy.prototype.andCallFake = function(fakeFunc) { + this.plan = fakeFunc; + return this; +}; + +/** + * Resets all of a spy's the tracking variables so that it can be used again. + * + * @example + * spyOn(foo, 'bar'); + * + * foo.bar(); + * + * expect(foo.bar.callCount).toEqual(1); + * + * foo.bar.reset(); + * + * expect(foo.bar.callCount).toEqual(0); + */ +jasmine.Spy.prototype.reset = function() { + this.wasCalled = false; + this.callCount = 0; + this.argsForCall = []; + this.calls = []; + this.mostRecentCall = {}; +}; + +jasmine.createSpy = function(name) { + + var spyObj = function() { + spyObj.wasCalled = true; + spyObj.callCount++; + var args = jasmine.util.argsToArray(arguments); + spyObj.mostRecentCall.object = this; + spyObj.mostRecentCall.args = args; + spyObj.argsForCall.push(args); + spyObj.calls.push({object: this, args: args}); + return spyObj.plan.apply(this, arguments); + }; + + var spy = new jasmine.Spy(name); + + for (var prop in spy) { + spyObj[prop] = spy[prop]; + } + + spyObj.reset(); + + return spyObj; +}; + +/** + * Determines whether an object is a spy. + * + * @param {jasmine.Spy|Object} putativeSpy + * @returns {Boolean} + */ +jasmine.isSpy = function(putativeSpy) { + return putativeSpy && putativeSpy.isSpy; +}; + +/** + * Creates a more complicated spy: an Object that has every property a function that is a spy. Used for stubbing something + * large in one call. + * + * @param {String} baseName name of spy class + * @param {Array} methodNames array of names of methods to make spies + */ +jasmine.createSpyObj = function(baseName, methodNames) { + if (!jasmine.isArray_(methodNames) || methodNames.length === 0) { + throw new Error('createSpyObj requires a non-empty array of method names to create spies for'); + } + var obj = {}; + for (var i = 0; i < methodNames.length; i++) { + obj[methodNames[i]] = jasmine.createSpy(baseName + '.' + methodNames[i]); + } + return obj; +}; + +/** + * All parameters are pretty-printed and concatenated together, then written to the current spec's output. + * + * Be careful not to leave calls to jasmine.log in production code. + */ +jasmine.log = function() { + var spec = jasmine.getEnv().currentSpec; + spec.log.apply(spec, arguments); +}; + +/** + * Function that installs a spy on an existing object's method name. Used within a Spec to create a spy. + * + * @example + * // spy example + * var foo = { + * not: function(bool) { return !bool; } + * } + * spyOn(foo, 'not'); // actual foo.not will not be called, execution stops + * + * @see jasmine.createSpy + * @param obj + * @param methodName + * @return {jasmine.Spy} a Jasmine spy that can be chained with all spy methods + */ +var spyOn = function(obj, methodName) { + return jasmine.getEnv().currentSpec.spyOn(obj, methodName); +}; +if (isCommonJS) exports.spyOn = spyOn; + +/** + * Creates a Jasmine spec that will be added to the current suite. + * + * // TODO: pending tests + * + * @example + * it('should be true', function() { + * expect(true).toEqual(true); + * }); + * + * @param {String} desc description of this specification + * @param {Function} func defines the preconditions and expectations of the spec + */ +var it = function(desc, func) { + return jasmine.getEnv().it(desc, func); +}; +if (isCommonJS) exports.it = it; + +/** + * Creates a disabled Jasmine spec. + * + * A convenience method that allows existing specs to be disabled temporarily during development. + * + * @param {String} desc description of this specification + * @param {Function} func defines the preconditions and expectations of the spec + */ +var xit = function(desc, func) { + return jasmine.getEnv().xit(desc, func); +}; +if (isCommonJS) exports.xit = xit; + +/** + * Starts a chain for a Jasmine expectation. + * + * It is passed an Object that is the actual value and should chain to one of the many + * jasmine.Matchers functions. + * + * @param {Object} actual Actual value to test against and expected value + * @return {jasmine.Matchers} + */ +var expect = function(actual) { + return jasmine.getEnv().currentSpec.expect(actual); +}; +if (isCommonJS) exports.expect = expect; + +/** + * Defines part of a jasmine spec. Used in cominbination with waits or waitsFor in asynchrnous specs. + * + * @param {Function} func Function that defines part of a jasmine spec. + */ +var runs = function(func) { + jasmine.getEnv().currentSpec.runs(func); +}; +if (isCommonJS) exports.runs = runs; + +/** + * Waits a fixed time period before moving to the next block. + * + * @deprecated Use waitsFor() instead + * @param {Number} timeout milliseconds to wait + */ +var waits = function(timeout) { + jasmine.getEnv().currentSpec.waits(timeout); +}; +if (isCommonJS) exports.waits = waits; + +/** + * Waits for the latchFunction to return true before proceeding to the next block. + * + * @param {Function} latchFunction + * @param {String} optional_timeoutMessage + * @param {Number} optional_timeout + */ +var waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { + jasmine.getEnv().currentSpec.waitsFor.apply(jasmine.getEnv().currentSpec, arguments); +}; +if (isCommonJS) exports.waitsFor = waitsFor; + +/** + * A function that is called before each spec in a suite. + * + * Used for spec setup, including validating assumptions. + * + * @param {Function} beforeEachFunction + */ +var beforeEach = function(beforeEachFunction) { + jasmine.getEnv().beforeEach(beforeEachFunction); +}; +if (isCommonJS) exports.beforeEach = beforeEach; + +/** + * A function that is called after each spec in a suite. + * + * Used for restoring any state that is hijacked during spec execution. + * + * @param {Function} afterEachFunction + */ +var afterEach = function(afterEachFunction) { + jasmine.getEnv().afterEach(afterEachFunction); +}; +if (isCommonJS) exports.afterEach = afterEach; + +/** + * Defines a suite of specifications. + * + * Stores the description and all defined specs in the Jasmine environment as one suite of specs. Variables declared + * are accessible by calls to beforeEach, it, and afterEach. Describe blocks can be nested, allowing for specialization + * of setup in some tests. + * + * @example + * // TODO: a simple suite + * + * // TODO: a simple suite with a nested describe block + * + * @param {String} description A string, usually the class under test. + * @param {Function} specDefinitions function that defines several specs. + */ +var describe = function(description, specDefinitions) { + return jasmine.getEnv().describe(description, specDefinitions); +}; +if (isCommonJS) exports.describe = describe; + +/** + * Disables a suite of specifications. Used to disable some suites in a file, or files, temporarily during development. + * + * @param {String} description A string, usually the class under test. + * @param {Function} specDefinitions function that defines several specs. + */ +var xdescribe = function(description, specDefinitions) { + return jasmine.getEnv().xdescribe(description, specDefinitions); +}; +if (isCommonJS) exports.xdescribe = xdescribe; + + +// Provide the XMLHttpRequest class for IE 5.x-6.x: +jasmine.XmlHttpRequest = (typeof XMLHttpRequest == "undefined") ? function() { + function tryIt(f) { + try { + return f(); + } catch(e) { + } + return null; + } + + var xhr = tryIt(function() { + return new ActiveXObject("Msxml2.XMLHTTP.6.0"); + }) || + tryIt(function() { + return new ActiveXObject("Msxml2.XMLHTTP.3.0"); + }) || + tryIt(function() { + return new ActiveXObject("Msxml2.XMLHTTP"); + }) || + tryIt(function() { + return new ActiveXObject("Microsoft.XMLHTTP"); + }); + + if (!xhr) throw new Error("This browser does not support XMLHttpRequest."); + + return xhr; +} : XMLHttpRequest; +/** + * @namespace + */ +jasmine.util = {}; + +/** + * Declare that a child class inherit it's prototype from the parent class. + * + * @private + * @param {Function} childClass + * @param {Function} parentClass + */ +jasmine.util.inherit = function(childClass, parentClass) { + /** + * @private + */ + var subclass = function() { + }; + subclass.prototype = parentClass.prototype; + childClass.prototype = new subclass(); +}; + +jasmine.util.formatException = function(e) { + var lineNumber; + if (e.line) { + lineNumber = e.line; + } + else if (e.lineNumber) { + lineNumber = e.lineNumber; + } + + var file; + + if (e.sourceURL) { + file = e.sourceURL; + } + else if (e.fileName) { + file = e.fileName; + } + + var message = (e.name && e.message) ? (e.name + ': ' + e.message) : e.toString(); + + if (file && lineNumber) { + message += ' in ' + file + ' (line ' + lineNumber + ')'; + } + + return message; +}; + +jasmine.util.htmlEscape = function(str) { + if (!str) return str; + return str.replace(/&/g, '&') + .replace(//g, '>'); +}; + +jasmine.util.argsToArray = function(args) { + var arrayOfArgs = []; + for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]); + return arrayOfArgs; +}; + +jasmine.util.extend = function(destination, source) { + for (var property in source) destination[property] = source[property]; + return destination; +}; + +/** + * Environment for Jasmine + * + * @constructor + */ +jasmine.Env = function() { + this.currentSpec = null; + this.currentSuite = null; + this.currentRunner_ = new jasmine.Runner(this); + + this.reporter = new jasmine.MultiReporter(); + + this.updateInterval = jasmine.DEFAULT_UPDATE_INTERVAL; + this.defaultTimeoutInterval = jasmine.DEFAULT_TIMEOUT_INTERVAL; + this.lastUpdate = 0; + this.specFilter = function() { + return true; + }; + + this.nextSpecId_ = 0; + this.nextSuiteId_ = 0; + this.equalityTesters_ = []; + + // wrap matchers + this.matchersClass = function() { + jasmine.Matchers.apply(this, arguments); + }; + jasmine.util.inherit(this.matchersClass, jasmine.Matchers); + + jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass); +}; + + +jasmine.Env.prototype.setTimeout = jasmine.setTimeout; +jasmine.Env.prototype.clearTimeout = jasmine.clearTimeout; +jasmine.Env.prototype.setInterval = jasmine.setInterval; +jasmine.Env.prototype.clearInterval = jasmine.clearInterval; + +/** + * @returns an object containing jasmine version build info, if set. + */ +jasmine.Env.prototype.version = function () { + if (jasmine.version_) { + return jasmine.version_; + } else { + throw new Error('Version not set'); + } +}; + +/** + * @returns string containing jasmine version build info, if set. + */ +jasmine.Env.prototype.versionString = function() { + if (!jasmine.version_) { + return "version unknown"; + } + + var version = this.version(); + var versionString = version.major + "." + version.minor + "." + version.build; + if (version.release_candidate) { + versionString += ".rc" + version.release_candidate; + } + versionString += " revision " + version.revision; + return versionString; +}; + +/** + * @returns a sequential integer starting at 0 + */ +jasmine.Env.prototype.nextSpecId = function () { + return this.nextSpecId_++; +}; + +/** + * @returns a sequential integer starting at 0 + */ +jasmine.Env.prototype.nextSuiteId = function () { + return this.nextSuiteId_++; +}; + +/** + * Register a reporter to receive status updates from Jasmine. + * @param {jasmine.Reporter} reporter An object which will receive status updates. + */ +jasmine.Env.prototype.addReporter = function(reporter) { + this.reporter.addReporter(reporter); +}; + +jasmine.Env.prototype.execute = function() { + this.currentRunner_.execute(); +}; + +jasmine.Env.prototype.describe = function(description, specDefinitions) { + var suite = new jasmine.Suite(this, description, specDefinitions, this.currentSuite); + + var parentSuite = this.currentSuite; + if (parentSuite) { + parentSuite.add(suite); + } else { + this.currentRunner_.add(suite); + } + + this.currentSuite = suite; + + var declarationError = null; + try { + specDefinitions.call(suite); + } catch(e) { + declarationError = e; + } + + if (declarationError) { + this.it("encountered a declaration exception", function() { + throw declarationError; + }); + } + + this.currentSuite = parentSuite; + + return suite; +}; + +jasmine.Env.prototype.beforeEach = function(beforeEachFunction) { + if (this.currentSuite) { + this.currentSuite.beforeEach(beforeEachFunction); + } else { + this.currentRunner_.beforeEach(beforeEachFunction); + } +}; + +jasmine.Env.prototype.currentRunner = function () { + return this.currentRunner_; +}; + +jasmine.Env.prototype.afterEach = function(afterEachFunction) { + if (this.currentSuite) { + this.currentSuite.afterEach(afterEachFunction); + } else { + this.currentRunner_.afterEach(afterEachFunction); + } + +}; + +jasmine.Env.prototype.xdescribe = function(desc, specDefinitions) { + return { + execute: function() { + } + }; +}; + +jasmine.Env.prototype.it = function(description, func) { + var spec = new jasmine.Spec(this, this.currentSuite, description); + this.currentSuite.add(spec); + this.currentSpec = spec; + + if (func) { + spec.runs(func); + } + + return spec; +}; + +jasmine.Env.prototype.xit = function(desc, func) { + return { + id: this.nextSpecId(), + runs: function() { + } + }; +}; + +jasmine.Env.prototype.compareRegExps_ = function(a, b, mismatchKeys, mismatchValues) { + if (a.source != b.source) + mismatchValues.push("expected pattern /" + b.source + "/ is not equal to the pattern /" + a.source + "/"); + + if (a.ignoreCase != b.ignoreCase) + mismatchValues.push("expected modifier i was" + (b.ignoreCase ? " " : " not ") + "set and does not equal the origin modifier"); + + if (a.global != b.global) + mismatchValues.push("expected modifier g was" + (b.global ? " " : " not ") + "set and does not equal the origin modifier"); + + if (a.multiline != b.multiline) + mismatchValues.push("expected modifier m was" + (b.multiline ? " " : " not ") + "set and does not equal the origin modifier"); + + if (a.sticky != b.sticky) + mismatchValues.push("expected modifier y was" + (b.sticky ? " " : " not ") + "set and does not equal the origin modifier"); + + return (mismatchValues.length === 0); +}; + +jasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) { + if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) { + return true; + } + + a.__Jasmine_been_here_before__ = b; + b.__Jasmine_been_here_before__ = a; + + var hasKey = function(obj, keyName) { + return obj !== null && obj[keyName] !== jasmine.undefined; + }; + + for (var property in b) { + if (!hasKey(a, property) && hasKey(b, property)) { + mismatchKeys.push("expected has key '" + property + "', but missing from actual."); + } + } + for (property in a) { + if (!hasKey(b, property) && hasKey(a, property)) { + mismatchKeys.push("expected missing key '" + property + "', but present in actual."); + } + } + for (property in b) { + if (property == '__Jasmine_been_here_before__') continue; + if (!this.equals_(a[property], b[property], mismatchKeys, mismatchValues)) { + mismatchValues.push("'" + property + "' was '" + (b[property] ? jasmine.util.htmlEscape(b[property].toString()) : b[property]) + "' in expected, but was '" + (a[property] ? jasmine.util.htmlEscape(a[property].toString()) : a[property]) + "' in actual."); + } + } + + if (jasmine.isArray_(a) && jasmine.isArray_(b) && a.length != b.length) { + mismatchValues.push("arrays were not the same length"); + } + + delete a.__Jasmine_been_here_before__; + delete b.__Jasmine_been_here_before__; + return (mismatchKeys.length === 0 && mismatchValues.length === 0); +}; + +jasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) { + mismatchKeys = mismatchKeys || []; + mismatchValues = mismatchValues || []; + + for (var i = 0; i < this.equalityTesters_.length; i++) { + var equalityTester = this.equalityTesters_[i]; + var result = equalityTester(a, b, this, mismatchKeys, mismatchValues); + if (result !== jasmine.undefined) return result; + } + + if (a === b) return true; + + if (a === jasmine.undefined || a === null || b === jasmine.undefined || b === null) { + return (a == jasmine.undefined && b == jasmine.undefined); + } + + if (jasmine.isDomNode(a) && jasmine.isDomNode(b)) { + return a === b; + } + + if (a instanceof Date && b instanceof Date) { + return a.getTime() == b.getTime(); + } + + if (a.jasmineMatches) { + return a.jasmineMatches(b); + } + + if (b.jasmineMatches) { + return b.jasmineMatches(a); + } + + if (a instanceof jasmine.Matchers.ObjectContaining) { + return a.matches(b); + } + + if (b instanceof jasmine.Matchers.ObjectContaining) { + return b.matches(a); + } + + if (jasmine.isString_(a) && jasmine.isString_(b)) { + return (a == b); + } + + if (jasmine.isNumber_(a) && jasmine.isNumber_(b)) { + return (a == b); + } + + if (a instanceof RegExp && b instanceof RegExp) { + return this.compareRegExps_(a, b, mismatchKeys, mismatchValues); + } + + if (typeof a === "object" && typeof b === "object") { + return this.compareObjects_(a, b, mismatchKeys, mismatchValues); + } + + //Straight check + return (a === b); +}; + +jasmine.Env.prototype.contains_ = function(haystack, needle) { + if (jasmine.isArray_(haystack)) { + for (var i = 0; i < haystack.length; i++) { + if (this.equals_(haystack[i], needle)) return true; + } + return false; + } + return haystack.indexOf(needle) >= 0; +}; + +jasmine.Env.prototype.addEqualityTester = function(equalityTester) { + this.equalityTesters_.push(equalityTester); +}; +/** No-op base class for Jasmine reporters. + * + * @constructor + */ +jasmine.Reporter = function() { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportRunnerStarting = function(runner) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportRunnerResults = function(runner) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportSuiteResults = function(suite) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportSpecStarting = function(spec) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.reportSpecResults = function(spec) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.Reporter.prototype.log = function(str) { +}; + +/** + * Blocks are functions with executable code that make up a spec. + * + * @constructor + * @param {jasmine.Env} env + * @param {Function} func + * @param {jasmine.Spec} spec + */ +jasmine.Block = function(env, func, spec) { + this.env = env; + this.func = func; + this.spec = spec; +}; + +jasmine.Block.prototype.execute = function(onComplete) { + if (!jasmine.CATCH_EXCEPTIONS) { + this.func.apply(this.spec); + } + else { + try { + this.func.apply(this.spec); + } catch (e) { + this.spec.fail(e); + } + } + onComplete(); +}; +/** JavaScript API reporter. + * + * @constructor + */ +jasmine.JsApiReporter = function() { + this.started = false; + this.finished = false; + this.suites_ = []; + this.results_ = {}; +}; + +jasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) { + this.started = true; + var suites = runner.topLevelSuites(); + for (var i = 0; i < suites.length; i++) { + var suite = suites[i]; + this.suites_.push(this.summarize_(suite)); + } +}; + +jasmine.JsApiReporter.prototype.suites = function() { + return this.suites_; +}; + +jasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) { + var isSuite = suiteOrSpec instanceof jasmine.Suite; + var summary = { + id: suiteOrSpec.id, + name: suiteOrSpec.description, + type: isSuite ? 'suite' : 'spec', + children: [] + }; + + if (isSuite) { + var children = suiteOrSpec.children(); + for (var i = 0; i < children.length; i++) { + summary.children.push(this.summarize_(children[i])); + } + } + return summary; +}; + +jasmine.JsApiReporter.prototype.results = function() { + return this.results_; +}; + +jasmine.JsApiReporter.prototype.resultsForSpec = function(specId) { + return this.results_[specId]; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) { + this.finished = true; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) { +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.reportSpecResults = function(spec) { + this.results_[spec.id] = { + messages: spec.results().getItems(), + result: spec.results().failedCount > 0 ? "failed" : "passed" + }; +}; + +//noinspection JSUnusedLocalSymbols +jasmine.JsApiReporter.prototype.log = function(str) { +}; + +jasmine.JsApiReporter.prototype.resultsForSpecs = function(specIds){ + var results = {}; + for (var i = 0; i < specIds.length; i++) { + var specId = specIds[i]; + results[specId] = this.summarizeResult_(this.results_[specId]); + } + return results; +}; + +jasmine.JsApiReporter.prototype.summarizeResult_ = function(result){ + var summaryMessages = []; + var messagesLength = result.messages.length; + for (var messageIndex = 0; messageIndex < messagesLength; messageIndex++) { + var resultMessage = result.messages[messageIndex]; + summaryMessages.push({ + text: resultMessage.type == 'log' ? resultMessage.toString() : jasmine.undefined, + passed: resultMessage.passed ? resultMessage.passed() : true, + type: resultMessage.type, + message: resultMessage.message, + trace: { + stack: resultMessage.passed && !resultMessage.passed() ? resultMessage.trace.stack : jasmine.undefined + } + }); + } + + return { + result : result.result, + messages : summaryMessages + }; +}; + +/** + * @constructor + * @param {jasmine.Env} env + * @param actual + * @param {jasmine.Spec} spec + */ +jasmine.Matchers = function(env, actual, spec, opt_isNot) { + this.env = env; + this.actual = actual; + this.spec = spec; + this.isNot = opt_isNot || false; + this.reportWasCalled_ = false; +}; + +// todo: @deprecated as of Jasmine 0.11, remove soon [xw] +jasmine.Matchers.pp = function(str) { + throw new Error("jasmine.Matchers.pp() is no longer supported, please use jasmine.pp() instead!"); +}; + +// todo: @deprecated Deprecated as of Jasmine 0.10. Rewrite your custom matchers to return true or false. [xw] +jasmine.Matchers.prototype.report = function(result, failing_message, details) { + throw new Error("As of jasmine 0.11, custom matchers must be implemented differently -- please see jasmine docs"); +}; + +jasmine.Matchers.wrapInto_ = function(prototype, matchersClass) { + for (var methodName in prototype) { + if (methodName == 'report') continue; + var orig = prototype[methodName]; + matchersClass.prototype[methodName] = jasmine.Matchers.matcherFn_(methodName, orig); + } +}; + +jasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) { + return function() { + var matcherArgs = jasmine.util.argsToArray(arguments); + var result = matcherFunction.apply(this, arguments); + + if (this.isNot) { + result = !result; + } + + if (this.reportWasCalled_) return result; + + var message; + if (!result) { + if (this.message) { + message = this.message.apply(this, arguments); + if (jasmine.isArray_(message)) { + message = message[this.isNot ? 1 : 0]; + } + } else { + var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); }); + message = "Expected " + jasmine.pp(this.actual) + (this.isNot ? " not " : " ") + englishyPredicate; + if (matcherArgs.length > 0) { + for (var i = 0; i < matcherArgs.length; i++) { + if (i > 0) message += ","; + message += " " + jasmine.pp(matcherArgs[i]); + } + } + message += "."; + } + } + var expectationResult = new jasmine.ExpectationResult({ + matcherName: matcherName, + passed: result, + expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0], + actual: this.actual, + message: message + }); + this.spec.addMatcherResult(expectationResult); + return jasmine.undefined; + }; +}; + + + + +/** + * toBe: compares the actual to the expected using === + * @param expected + */ +jasmine.Matchers.prototype.toBe = function(expected) { + return this.actual === expected; +}; + +/** + * toNotBe: compares the actual to the expected using !== + * @param expected + * @deprecated as of 1.0. Use not.toBe() instead. + */ +jasmine.Matchers.prototype.toNotBe = function(expected) { + return this.actual !== expected; +}; + +/** + * toEqual: compares the actual to the expected using common sense equality. Handles Objects, Arrays, etc. + * + * @param expected + */ +jasmine.Matchers.prototype.toEqual = function(expected) { + return this.env.equals_(this.actual, expected); +}; + +/** + * toNotEqual: compares the actual to the expected using the ! of jasmine.Matchers.toEqual + * @param expected + * @deprecated as of 1.0. Use not.toEqual() instead. + */ +jasmine.Matchers.prototype.toNotEqual = function(expected) { + return !this.env.equals_(this.actual, expected); +}; + +/** + * Matcher that compares the actual to the expected using a regular expression. Constructs a RegExp, so takes + * a pattern or a String. + * + * @param expected + */ +jasmine.Matchers.prototype.toMatch = function(expected) { + return new RegExp(expected).test(this.actual); +}; + +/** + * Matcher that compares the actual to the expected using the boolean inverse of jasmine.Matchers.toMatch + * @param expected + * @deprecated as of 1.0. Use not.toMatch() instead. + */ +jasmine.Matchers.prototype.toNotMatch = function(expected) { + return !(new RegExp(expected).test(this.actual)); +}; + +/** + * Matcher that compares the actual to jasmine.undefined. + */ +jasmine.Matchers.prototype.toBeDefined = function() { + return (this.actual !== jasmine.undefined); +}; + +/** + * Matcher that compares the actual to jasmine.undefined. + */ +jasmine.Matchers.prototype.toBeUndefined = function() { + return (this.actual === jasmine.undefined); +}; + +/** + * Matcher that compares the actual to null. + */ +jasmine.Matchers.prototype.toBeNull = function() { + return (this.actual === null); +}; + +/** + * Matcher that compares the actual to NaN. + */ +jasmine.Matchers.prototype.toBeNaN = function() { + this.message = function() { + return [ "Expected " + jasmine.pp(this.actual) + " to be NaN." ]; + }; + + return (this.actual !== this.actual); +}; + +/** + * Matcher that boolean not-nots the actual. + */ +jasmine.Matchers.prototype.toBeTruthy = function() { + return !!this.actual; +}; + + +/** + * Matcher that boolean nots the actual. + */ +jasmine.Matchers.prototype.toBeFalsy = function() { + return !this.actual; +}; + + +/** + * Matcher that checks to see if the actual, a Jasmine spy, was called. + */ +jasmine.Matchers.prototype.toHaveBeenCalled = function() { + if (arguments.length > 0) { + throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith'); + } + + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + + this.message = function() { + return [ + "Expected spy " + this.actual.identity + " to have been called.", + "Expected spy " + this.actual.identity + " not to have been called." + ]; + }; + + return this.actual.wasCalled; +}; + +/** @deprecated Use expect(xxx).toHaveBeenCalled() instead */ +jasmine.Matchers.prototype.wasCalled = jasmine.Matchers.prototype.toHaveBeenCalled; + +/** + * Matcher that checks to see if the actual, a Jasmine spy, was not called. + * + * @deprecated Use expect(xxx).not.toHaveBeenCalled() instead + */ +jasmine.Matchers.prototype.wasNotCalled = function() { + if (arguments.length > 0) { + throw new Error('wasNotCalled does not take arguments'); + } + + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + + this.message = function() { + return [ + "Expected spy " + this.actual.identity + " to not have been called.", + "Expected spy " + this.actual.identity + " to have been called." + ]; + }; + + return !this.actual.wasCalled; +}; + +/** + * Matcher that checks to see if the actual, a Jasmine spy, was called with a set of parameters. + * + * @example + * + */ +jasmine.Matchers.prototype.toHaveBeenCalledWith = function() { + var expectedArgs = jasmine.util.argsToArray(arguments); + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + this.message = function() { + var invertedMessage = "Expected spy " + this.actual.identity + " not to have been called with " + jasmine.pp(expectedArgs) + " but it was."; + var positiveMessage = ""; + if (this.actual.callCount === 0) { + positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but it was never called."; + } else { + positiveMessage = "Expected spy " + this.actual.identity + " to have been called with " + jasmine.pp(expectedArgs) + " but actual calls were " + jasmine.pp(this.actual.argsForCall).replace(/^\[ | \]$/g, '') + } + return [positiveMessage, invertedMessage]; + }; + + return this.env.contains_(this.actual.argsForCall, expectedArgs); +}; + +/** @deprecated Use expect(xxx).toHaveBeenCalledWith() instead */ +jasmine.Matchers.prototype.wasCalledWith = jasmine.Matchers.prototype.toHaveBeenCalledWith; + +/** @deprecated Use expect(xxx).not.toHaveBeenCalledWith() instead */ +jasmine.Matchers.prototype.wasNotCalledWith = function() { + var expectedArgs = jasmine.util.argsToArray(arguments); + if (!jasmine.isSpy(this.actual)) { + throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.'); + } + + this.message = function() { + return [ + "Expected spy not to have been called with " + jasmine.pp(expectedArgs) + " but it was", + "Expected spy to have been called with " + jasmine.pp(expectedArgs) + " but it was" + ]; + }; + + return !this.env.contains_(this.actual.argsForCall, expectedArgs); +}; + +/** + * Matcher that checks that the expected item is an element in the actual Array. + * + * @param {Object} expected + */ +jasmine.Matchers.prototype.toContain = function(expected) { + return this.env.contains_(this.actual, expected); +}; + +/** + * Matcher that checks that the expected item is NOT an element in the actual Array. + * + * @param {Object} expected + * @deprecated as of 1.0. Use not.toContain() instead. + */ +jasmine.Matchers.prototype.toNotContain = function(expected) { + return !this.env.contains_(this.actual, expected); +}; + +jasmine.Matchers.prototype.toBeLessThan = function(expected) { + return this.actual < expected; +}; + +jasmine.Matchers.prototype.toBeGreaterThan = function(expected) { + return this.actual > expected; +}; + +/** + * Matcher that checks that the expected item is equal to the actual item + * up to a given level of decimal precision (default 2). + * + * @param {Number} expected + * @param {Number} precision, as number of decimal places + */ +jasmine.Matchers.prototype.toBeCloseTo = function(expected, precision) { + if (!(precision === 0)) { + precision = precision || 2; + } + return Math.abs(expected - this.actual) < (Math.pow(10, -precision) / 2); +}; + +/** + * Matcher that checks that the expected exception was thrown by the actual. + * + * @param {String} [expected] + */ +jasmine.Matchers.prototype.toThrow = function(expected) { + var result = false; + var exception; + if (typeof this.actual != 'function') { + throw new Error('Actual is not a function'); + } + try { + this.actual(); + } catch (e) { + exception = e; + } + if (exception) { + result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected)); + } + + var not = this.isNot ? "not " : ""; + + this.message = function() { + if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) { + return ["Expected function " + not + "to throw", expected ? expected.message || expected : "an exception", ", but it threw", exception.message || exception].join(' '); + } else { + return "Expected function to throw an exception."; + } + }; + + return result; +}; + +jasmine.Matchers.Any = function(expectedClass) { + this.expectedClass = expectedClass; +}; + +jasmine.Matchers.Any.prototype.jasmineMatches = function(other) { + if (this.expectedClass == String) { + return typeof other == 'string' || other instanceof String; + } + + if (this.expectedClass == Number) { + return typeof other == 'number' || other instanceof Number; + } + + if (this.expectedClass == Function) { + return typeof other == 'function' || other instanceof Function; + } + + if (this.expectedClass == Object) { + return typeof other == 'object'; + } + + return other instanceof this.expectedClass; +}; + +jasmine.Matchers.Any.prototype.jasmineToString = function() { + return ''; +}; + +jasmine.Matchers.ObjectContaining = function (sample) { + this.sample = sample; +}; + +jasmine.Matchers.ObjectContaining.prototype.jasmineMatches = function(other, mismatchKeys, mismatchValues) { + mismatchKeys = mismatchKeys || []; + mismatchValues = mismatchValues || []; + + var env = jasmine.getEnv(); + + var hasKey = function(obj, keyName) { + return obj != null && obj[keyName] !== jasmine.undefined; + }; + + for (var property in this.sample) { + if (!hasKey(other, property) && hasKey(this.sample, property)) { + mismatchKeys.push("expected has key '" + property + "', but missing from actual."); + } + else if (!env.equals_(this.sample[property], other[property], mismatchKeys, mismatchValues)) { + mismatchValues.push("'" + property + "' was '" + (other[property] ? jasmine.util.htmlEscape(other[property].toString()) : other[property]) + "' in expected, but was '" + (this.sample[property] ? jasmine.util.htmlEscape(this.sample[property].toString()) : this.sample[property]) + "' in actual."); + } + } + + return (mismatchKeys.length === 0 && mismatchValues.length === 0); +}; + +jasmine.Matchers.ObjectContaining.prototype.jasmineToString = function () { + return ""; +}; +// Mock setTimeout, clearTimeout +// Contributed by Pivotal Computer Systems, www.pivotalsf.com + +jasmine.FakeTimer = function() { + this.reset(); + + var self = this; + self.setTimeout = function(funcToCall, millis) { + self.timeoutsMade++; + self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false); + return self.timeoutsMade; + }; + + self.setInterval = function(funcToCall, millis) { + self.timeoutsMade++; + self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true); + return self.timeoutsMade; + }; + + self.clearTimeout = function(timeoutKey) { + self.scheduledFunctions[timeoutKey] = jasmine.undefined; + }; + + self.clearInterval = function(timeoutKey) { + self.scheduledFunctions[timeoutKey] = jasmine.undefined; + }; + +}; + +jasmine.FakeTimer.prototype.reset = function() { + this.timeoutsMade = 0; + this.scheduledFunctions = {}; + this.nowMillis = 0; +}; + +jasmine.FakeTimer.prototype.tick = function(millis) { + var oldMillis = this.nowMillis; + var newMillis = oldMillis + millis; + this.runFunctionsWithinRange(oldMillis, newMillis); + this.nowMillis = newMillis; +}; + +jasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) { + var scheduledFunc; + var funcsToRun = []; + for (var timeoutKey in this.scheduledFunctions) { + scheduledFunc = this.scheduledFunctions[timeoutKey]; + if (scheduledFunc != jasmine.undefined && + scheduledFunc.runAtMillis >= oldMillis && + scheduledFunc.runAtMillis <= nowMillis) { + funcsToRun.push(scheduledFunc); + this.scheduledFunctions[timeoutKey] = jasmine.undefined; + } + } + + if (funcsToRun.length > 0) { + funcsToRun.sort(function(a, b) { + return a.runAtMillis - b.runAtMillis; + }); + for (var i = 0; i < funcsToRun.length; ++i) { + try { + var funcToRun = funcsToRun[i]; + this.nowMillis = funcToRun.runAtMillis; + funcToRun.funcToCall(); + if (funcToRun.recurring) { + this.scheduleFunction(funcToRun.timeoutKey, + funcToRun.funcToCall, + funcToRun.millis, + true); + } + } catch(e) { + } + } + this.runFunctionsWithinRange(oldMillis, nowMillis); + } +}; + +jasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) { + this.scheduledFunctions[timeoutKey] = { + runAtMillis: this.nowMillis + millis, + funcToCall: funcToCall, + recurring: recurring, + timeoutKey: timeoutKey, + millis: millis + }; +}; + +/** + * @namespace + */ +jasmine.Clock = { + defaultFakeTimer: new jasmine.FakeTimer(), + + reset: function() { + jasmine.Clock.assertInstalled(); + jasmine.Clock.defaultFakeTimer.reset(); + }, + + tick: function(millis) { + jasmine.Clock.assertInstalled(); + jasmine.Clock.defaultFakeTimer.tick(millis); + }, + + runFunctionsWithinRange: function(oldMillis, nowMillis) { + jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis); + }, + + scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) { + jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring); + }, + + useMock: function() { + if (!jasmine.Clock.isInstalled()) { + var spec = jasmine.getEnv().currentSpec; + spec.after(jasmine.Clock.uninstallMock); + + jasmine.Clock.installMock(); + } + }, + + installMock: function() { + jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer; + }, + + uninstallMock: function() { + jasmine.Clock.assertInstalled(); + jasmine.Clock.installed = jasmine.Clock.real; + }, + + real: { + setTimeout: jasmine.getGlobal().setTimeout, + clearTimeout: jasmine.getGlobal().clearTimeout, + setInterval: jasmine.getGlobal().setInterval, + clearInterval: jasmine.getGlobal().clearInterval + }, + + assertInstalled: function() { + if (!jasmine.Clock.isInstalled()) { + throw new Error("Mock clock is not installed, use jasmine.Clock.useMock()"); + } + }, + + isInstalled: function() { + return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer; + }, + + installed: null +}; +jasmine.Clock.installed = jasmine.Clock.real; + +//else for IE support +jasmine.getGlobal().setTimeout = function(funcToCall, millis) { + if (jasmine.Clock.installed.setTimeout.apply) { + return jasmine.Clock.installed.setTimeout.apply(this, arguments); + } else { + return jasmine.Clock.installed.setTimeout(funcToCall, millis); + } +}; + +jasmine.getGlobal().setInterval = function(funcToCall, millis) { + if (jasmine.Clock.installed.setInterval.apply) { + return jasmine.Clock.installed.setInterval.apply(this, arguments); + } else { + return jasmine.Clock.installed.setInterval(funcToCall, millis); + } +}; + +jasmine.getGlobal().clearTimeout = function(timeoutKey) { + if (jasmine.Clock.installed.clearTimeout.apply) { + return jasmine.Clock.installed.clearTimeout.apply(this, arguments); + } else { + return jasmine.Clock.installed.clearTimeout(timeoutKey); + } +}; + +jasmine.getGlobal().clearInterval = function(timeoutKey) { + if (jasmine.Clock.installed.clearTimeout.apply) { + return jasmine.Clock.installed.clearInterval.apply(this, arguments); + } else { + return jasmine.Clock.installed.clearInterval(timeoutKey); + } +}; + +/** + * @constructor + */ +jasmine.MultiReporter = function() { + this.subReporters_ = []; +}; +jasmine.util.inherit(jasmine.MultiReporter, jasmine.Reporter); + +jasmine.MultiReporter.prototype.addReporter = function(reporter) { + this.subReporters_.push(reporter); +}; + +(function() { + var functionNames = [ + "reportRunnerStarting", + "reportRunnerResults", + "reportSuiteResults", + "reportSpecStarting", + "reportSpecResults", + "log" + ]; + for (var i = 0; i < functionNames.length; i++) { + var functionName = functionNames[i]; + jasmine.MultiReporter.prototype[functionName] = (function(functionName) { + return function() { + for (var j = 0; j < this.subReporters_.length; j++) { + var subReporter = this.subReporters_[j]; + if (subReporter[functionName]) { + subReporter[functionName].apply(subReporter, arguments); + } + } + }; + })(functionName); + } +})(); +/** + * Holds results for a set of Jasmine spec. Allows for the results array to hold another jasmine.NestedResults + * + * @constructor + */ +jasmine.NestedResults = function() { + /** + * The total count of results + */ + this.totalCount = 0; + /** + * Number of passed results + */ + this.passedCount = 0; + /** + * Number of failed results + */ + this.failedCount = 0; + /** + * Was this suite/spec skipped? + */ + this.skipped = false; + /** + * @ignore + */ + this.items_ = []; +}; + +/** + * Roll up the result counts. + * + * @param result + */ +jasmine.NestedResults.prototype.rollupCounts = function(result) { + this.totalCount += result.totalCount; + this.passedCount += result.passedCount; + this.failedCount += result.failedCount; +}; + +/** + * Adds a log message. + * @param values Array of message parts which will be concatenated later. + */ +jasmine.NestedResults.prototype.log = function(values) { + this.items_.push(new jasmine.MessageResult(values)); +}; + +/** + * Getter for the results: message & results. + */ +jasmine.NestedResults.prototype.getItems = function() { + return this.items_; +}; + +/** + * Adds a result, tracking counts (total, passed, & failed) + * @param {jasmine.ExpectationResult|jasmine.NestedResults} result + */ +jasmine.NestedResults.prototype.addResult = function(result) { + if (result.type != 'log') { + if (result.items_) { + this.rollupCounts(result); + } else { + this.totalCount++; + if (result.passed()) { + this.passedCount++; + } else { + this.failedCount++; + } + } + } + this.items_.push(result); +}; + +/** + * @returns {Boolean} True if everything below passed + */ +jasmine.NestedResults.prototype.passed = function() { + return this.passedCount === this.totalCount; +}; +/** + * Base class for pretty printing for expectation results. + */ +jasmine.PrettyPrinter = function() { + this.ppNestLevel_ = 0; +}; + +/** + * Formats a value in a nice, human-readable string. + * + * @param value + */ +jasmine.PrettyPrinter.prototype.format = function(value) { + this.ppNestLevel_++; + try { + if (value === jasmine.undefined) { + this.emitScalar('undefined'); + } else if (value === null) { + this.emitScalar('null'); + } else if (value === jasmine.getGlobal()) { + this.emitScalar(''); + } else if (value.jasmineToString) { + this.emitScalar(value.jasmineToString()); + } else if (typeof value === 'string') { + this.emitString(value); + } else if (jasmine.isSpy(value)) { + this.emitScalar("spy on " + value.identity); + } else if (value instanceof RegExp) { + this.emitScalar(value.toString()); + } else if (typeof value === 'function') { + this.emitScalar('Function'); + } else if (typeof value.nodeType === 'number') { + this.emitScalar('HTMLNode'); + } else if (value instanceof Date) { + this.emitScalar('Date(' + value + ')'); + } else if (value.__Jasmine_been_here_before__) { + this.emitScalar(''); + } else if (jasmine.isArray_(value) || typeof value == 'object') { + value.__Jasmine_been_here_before__ = true; + if (jasmine.isArray_(value)) { + this.emitArray(value); + } else { + this.emitObject(value); + } + delete value.__Jasmine_been_here_before__; + } else { + this.emitScalar(value.toString()); + } + } finally { + this.ppNestLevel_--; + } +}; + +jasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) { + for (var property in obj) { + if (!obj.hasOwnProperty(property)) continue; + if (property == '__Jasmine_been_here_before__') continue; + fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) !== jasmine.undefined && + obj.__lookupGetter__(property) !== null) : false); + } +}; + +jasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_; +jasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_; +jasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_; +jasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_; + +jasmine.StringPrettyPrinter = function() { + jasmine.PrettyPrinter.call(this); + + this.string = ''; +}; +jasmine.util.inherit(jasmine.StringPrettyPrinter, jasmine.PrettyPrinter); + +jasmine.StringPrettyPrinter.prototype.emitScalar = function(value) { + this.append(value); +}; + +jasmine.StringPrettyPrinter.prototype.emitString = function(value) { + this.append("'" + value + "'"); +}; + +jasmine.StringPrettyPrinter.prototype.emitArray = function(array) { + if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) { + this.append("Array"); + return; + } + + this.append('[ '); + for (var i = 0; i < array.length; i++) { + if (i > 0) { + this.append(', '); + } + this.format(array[i]); + } + this.append(' ]'); +}; + +jasmine.StringPrettyPrinter.prototype.emitObject = function(obj) { + if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) { + this.append("Object"); + return; + } + + var self = this; + this.append('{ '); + var first = true; + + this.iterateObject(obj, function(property, isGetter) { + if (first) { + first = false; + } else { + self.append(', '); + } + + self.append(property); + self.append(' : '); + if (isGetter) { + self.append(''); + } else { + self.format(obj[property]); + } + }); + + this.append(' }'); +}; + +jasmine.StringPrettyPrinter.prototype.append = function(value) { + this.string += value; +}; +jasmine.Queue = function(env) { + this.env = env; + + // parallel to blocks. each true value in this array means the block will + // get executed even if we abort + this.ensured = []; + this.blocks = []; + this.running = false; + this.index = 0; + this.offset = 0; + this.abort = false; +}; + +jasmine.Queue.prototype.addBefore = function(block, ensure) { + if (ensure === jasmine.undefined) { + ensure = false; + } + + this.blocks.unshift(block); + this.ensured.unshift(ensure); +}; + +jasmine.Queue.prototype.add = function(block, ensure) { + if (ensure === jasmine.undefined) { + ensure = false; + } + + this.blocks.push(block); + this.ensured.push(ensure); +}; + +jasmine.Queue.prototype.insertNext = function(block, ensure) { + if (ensure === jasmine.undefined) { + ensure = false; + } + + this.ensured.splice((this.index + this.offset + 1), 0, ensure); + this.blocks.splice((this.index + this.offset + 1), 0, block); + this.offset++; +}; + +jasmine.Queue.prototype.start = function(onComplete) { + this.running = true; + this.onComplete = onComplete; + this.next_(); +}; + +jasmine.Queue.prototype.isRunning = function() { + return this.running; +}; + +jasmine.Queue.LOOP_DONT_RECURSE = true; + +jasmine.Queue.prototype.next_ = function() { + var self = this; + var goAgain = true; + + while (goAgain) { + goAgain = false; + + if (self.index < self.blocks.length && !(this.abort && !this.ensured[self.index])) { + var calledSynchronously = true; + var completedSynchronously = false; + + var onComplete = function () { + if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) { + completedSynchronously = true; + return; + } + + if (self.blocks[self.index].abort) { + self.abort = true; + } + + self.offset = 0; + self.index++; + + var now = new Date().getTime(); + if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) { + self.env.lastUpdate = now; + self.env.setTimeout(function() { + self.next_(); + }, 0); + } else { + if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) { + goAgain = true; + } else { + self.next_(); + } + } + }; + self.blocks[self.index].execute(onComplete); + + calledSynchronously = false; + if (completedSynchronously) { + onComplete(); + } + + } else { + self.running = false; + if (self.onComplete) { + self.onComplete(); + } + } + } +}; + +jasmine.Queue.prototype.results = function() { + var results = new jasmine.NestedResults(); + for (var i = 0; i < this.blocks.length; i++) { + if (this.blocks[i].results) { + results.addResult(this.blocks[i].results()); + } + } + return results; +}; + + +/** + * Runner + * + * @constructor + * @param {jasmine.Env} env + */ +jasmine.Runner = function(env) { + var self = this; + self.env = env; + self.queue = new jasmine.Queue(env); + self.before_ = []; + self.after_ = []; + self.suites_ = []; +}; + +jasmine.Runner.prototype.execute = function() { + var self = this; + if (self.env.reporter.reportRunnerStarting) { + self.env.reporter.reportRunnerStarting(this); + } + self.queue.start(function () { + self.finishCallback(); + }); +}; + +jasmine.Runner.prototype.beforeEach = function(beforeEachFunction) { + beforeEachFunction.typeName = 'beforeEach'; + this.before_.splice(0,0,beforeEachFunction); +}; + +jasmine.Runner.prototype.afterEach = function(afterEachFunction) { + afterEachFunction.typeName = 'afterEach'; + this.after_.splice(0,0,afterEachFunction); +}; + + +jasmine.Runner.prototype.finishCallback = function() { + this.env.reporter.reportRunnerResults(this); +}; + +jasmine.Runner.prototype.addSuite = function(suite) { + this.suites_.push(suite); +}; + +jasmine.Runner.prototype.add = function(block) { + if (block instanceof jasmine.Suite) { + this.addSuite(block); + } + this.queue.add(block); +}; + +jasmine.Runner.prototype.specs = function () { + var suites = this.suites(); + var specs = []; + for (var i = 0; i < suites.length; i++) { + specs = specs.concat(suites[i].specs()); + } + return specs; +}; + +jasmine.Runner.prototype.suites = function() { + return this.suites_; +}; + +jasmine.Runner.prototype.topLevelSuites = function() { + var topLevelSuites = []; + for (var i = 0; i < this.suites_.length; i++) { + if (!this.suites_[i].parentSuite) { + topLevelSuites.push(this.suites_[i]); + } + } + return topLevelSuites; +}; + +jasmine.Runner.prototype.results = function() { + return this.queue.results(); +}; +/** + * Internal representation of a Jasmine specification, or test. + * + * @constructor + * @param {jasmine.Env} env + * @param {jasmine.Suite} suite + * @param {String} description + */ +jasmine.Spec = function(env, suite, description) { + if (!env) { + throw new Error('jasmine.Env() required'); + } + if (!suite) { + throw new Error('jasmine.Suite() required'); + } + var spec = this; + spec.id = env.nextSpecId ? env.nextSpecId() : null; + spec.env = env; + spec.suite = suite; + spec.description = description; + spec.queue = new jasmine.Queue(env); + + spec.afterCallbacks = []; + spec.spies_ = []; + + spec.results_ = new jasmine.NestedResults(); + spec.results_.description = description; + spec.matchersClass = null; +}; + +jasmine.Spec.prototype.getFullName = function() { + return this.suite.getFullName() + ' ' + this.description + '.'; +}; + + +jasmine.Spec.prototype.results = function() { + return this.results_; +}; + +/** + * All parameters are pretty-printed and concatenated together, then written to the spec's output. + * + * Be careful not to leave calls to jasmine.log in production code. + */ +jasmine.Spec.prototype.log = function() { + return this.results_.log(arguments); +}; + +jasmine.Spec.prototype.runs = function (func) { + var block = new jasmine.Block(this.env, func, this); + this.addToQueue(block); + return this; +}; + +jasmine.Spec.prototype.addToQueue = function (block) { + if (this.queue.isRunning()) { + this.queue.insertNext(block); + } else { + this.queue.add(block); + } +}; + +/** + * @param {jasmine.ExpectationResult} result + */ +jasmine.Spec.prototype.addMatcherResult = function(result) { + this.results_.addResult(result); +}; + +jasmine.Spec.prototype.expect = function(actual) { + var positive = new (this.getMatchersClass_())(this.env, actual, this); + positive.not = new (this.getMatchersClass_())(this.env, actual, this, true); + return positive; +}; + +/** + * Waits a fixed time period before moving to the next block. + * + * @deprecated Use waitsFor() instead + * @param {Number} timeout milliseconds to wait + */ +jasmine.Spec.prototype.waits = function(timeout) { + var waitsFunc = new jasmine.WaitsBlock(this.env, timeout, this); + this.addToQueue(waitsFunc); + return this; +}; + +/** + * Waits for the latchFunction to return true before proceeding to the next block. + * + * @param {Function} latchFunction + * @param {String} optional_timeoutMessage + * @param {Number} optional_timeout + */ +jasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) { + var latchFunction_ = null; + var optional_timeoutMessage_ = null; + var optional_timeout_ = null; + + for (var i = 0; i < arguments.length; i++) { + var arg = arguments[i]; + switch (typeof arg) { + case 'function': + latchFunction_ = arg; + break; + case 'string': + optional_timeoutMessage_ = arg; + break; + case 'number': + optional_timeout_ = arg; + break; + } + } + + var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this); + this.addToQueue(waitsForFunc); + return this; +}; + +jasmine.Spec.prototype.fail = function (e) { + var expectationResult = new jasmine.ExpectationResult({ + passed: false, + message: e ? jasmine.util.formatException(e) : 'Exception', + trace: { stack: e.stack } + }); + this.results_.addResult(expectationResult); +}; + +jasmine.Spec.prototype.getMatchersClass_ = function() { + return this.matchersClass || this.env.matchersClass; +}; + +jasmine.Spec.prototype.addMatchers = function(matchersPrototype) { + var parent = this.getMatchersClass_(); + var newMatchersClass = function() { + parent.apply(this, arguments); + }; + jasmine.util.inherit(newMatchersClass, parent); + jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass); + this.matchersClass = newMatchersClass; +}; + +jasmine.Spec.prototype.finishCallback = function() { + this.env.reporter.reportSpecResults(this); +}; + +jasmine.Spec.prototype.finish = function(onComplete) { + this.removeAllSpies(); + this.finishCallback(); + if (onComplete) { + onComplete(); + } +}; + +jasmine.Spec.prototype.after = function(doAfter) { + if (this.queue.isRunning()) { + this.queue.add(new jasmine.Block(this.env, doAfter, this), true); + } else { + this.afterCallbacks.unshift(doAfter); + } +}; + +jasmine.Spec.prototype.execute = function(onComplete) { + var spec = this; + if (!spec.env.specFilter(spec)) { + spec.results_.skipped = true; + spec.finish(onComplete); + return; + } + + this.env.reporter.reportSpecStarting(this); + + spec.env.currentSpec = spec; + + spec.addBeforesAndAftersToQueue(); + + spec.queue.start(function () { + spec.finish(onComplete); + }); +}; + +jasmine.Spec.prototype.addBeforesAndAftersToQueue = function() { + var runner = this.env.currentRunner(); + var i; + + for (var suite = this.suite; suite; suite = suite.parentSuite) { + for (i = 0; i < suite.before_.length; i++) { + this.queue.addBefore(new jasmine.Block(this.env, suite.before_[i], this)); + } + } + for (i = 0; i < runner.before_.length; i++) { + this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this)); + } + for (i = 0; i < this.afterCallbacks.length; i++) { + this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this), true); + } + for (suite = this.suite; suite; suite = suite.parentSuite) { + for (i = 0; i < suite.after_.length; i++) { + this.queue.add(new jasmine.Block(this.env, suite.after_[i], this), true); + } + } + for (i = 0; i < runner.after_.length; i++) { + this.queue.add(new jasmine.Block(this.env, runner.after_[i], this), true); + } +}; + +jasmine.Spec.prototype.explodes = function() { + throw 'explodes function should not have been called'; +}; + +jasmine.Spec.prototype.spyOn = function(obj, methodName, ignoreMethodDoesntExist) { + if (obj == jasmine.undefined) { + throw "spyOn could not find an object to spy upon for " + methodName + "()"; + } + + if (!ignoreMethodDoesntExist && obj[methodName] === jasmine.undefined) { + throw methodName + '() method does not exist'; + } + + if (!ignoreMethodDoesntExist && obj[methodName] && obj[methodName].isSpy) { + throw new Error(methodName + ' has already been spied upon'); + } + + var spyObj = jasmine.createSpy(methodName); + + this.spies_.push(spyObj); + spyObj.baseObj = obj; + spyObj.methodName = methodName; + spyObj.originalValue = obj[methodName]; + + obj[methodName] = spyObj; + + return spyObj; +}; + +jasmine.Spec.prototype.removeAllSpies = function() { + for (var i = 0; i < this.spies_.length; i++) { + var spy = this.spies_[i]; + spy.baseObj[spy.methodName] = spy.originalValue; + } + this.spies_ = []; +}; + +/** + * Internal representation of a Jasmine suite. + * + * @constructor + * @param {jasmine.Env} env + * @param {String} description + * @param {Function} specDefinitions + * @param {jasmine.Suite} parentSuite + */ +jasmine.Suite = function(env, description, specDefinitions, parentSuite) { + var self = this; + self.id = env.nextSuiteId ? env.nextSuiteId() : null; + self.description = description; + self.queue = new jasmine.Queue(env); + self.parentSuite = parentSuite; + self.env = env; + self.before_ = []; + self.after_ = []; + self.children_ = []; + self.suites_ = []; + self.specs_ = []; +}; + +jasmine.Suite.prototype.getFullName = function() { + var fullName = this.description; + for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) { + fullName = parentSuite.description + ' ' + fullName; + } + return fullName; +}; + +jasmine.Suite.prototype.finish = function(onComplete) { + this.env.reporter.reportSuiteResults(this); + this.finished = true; + if (typeof(onComplete) == 'function') { + onComplete(); + } +}; + +jasmine.Suite.prototype.beforeEach = function(beforeEachFunction) { + beforeEachFunction.typeName = 'beforeEach'; + this.before_.unshift(beforeEachFunction); +}; + +jasmine.Suite.prototype.afterEach = function(afterEachFunction) { + afterEachFunction.typeName = 'afterEach'; + this.after_.unshift(afterEachFunction); +}; + +jasmine.Suite.prototype.results = function() { + return this.queue.results(); +}; + +jasmine.Suite.prototype.add = function(suiteOrSpec) { + this.children_.push(suiteOrSpec); + if (suiteOrSpec instanceof jasmine.Suite) { + this.suites_.push(suiteOrSpec); + this.env.currentRunner().addSuite(suiteOrSpec); + } else { + this.specs_.push(suiteOrSpec); + } + this.queue.add(suiteOrSpec); +}; + +jasmine.Suite.prototype.specs = function() { + return this.specs_; +}; + +jasmine.Suite.prototype.suites = function() { + return this.suites_; +}; + +jasmine.Suite.prototype.children = function() { + return this.children_; +}; + +jasmine.Suite.prototype.execute = function(onComplete) { + var self = this; + this.queue.start(function () { + self.finish(onComplete); + }); +}; +jasmine.WaitsBlock = function(env, timeout, spec) { + this.timeout = timeout; + jasmine.Block.call(this, env, null, spec); +}; + +jasmine.util.inherit(jasmine.WaitsBlock, jasmine.Block); + +jasmine.WaitsBlock.prototype.execute = function (onComplete) { + if (jasmine.VERBOSE) { + this.env.reporter.log('>> Jasmine waiting for ' + this.timeout + ' ms...'); + } + this.env.setTimeout(function () { + onComplete(); + }, this.timeout); +}; +/** + * A block which waits for some condition to become true, with timeout. + * + * @constructor + * @extends jasmine.Block + * @param {jasmine.Env} env The Jasmine environment. + * @param {Number} timeout The maximum time in milliseconds to wait for the condition to become true. + * @param {Function} latchFunction A function which returns true when the desired condition has been met. + * @param {String} message The message to display if the desired condition hasn't been met within the given time period. + * @param {jasmine.Spec} spec The Jasmine spec. + */ +jasmine.WaitsForBlock = function(env, timeout, latchFunction, message, spec) { + this.timeout = timeout || env.defaultTimeoutInterval; + this.latchFunction = latchFunction; + this.message = message; + this.totalTimeSpentWaitingForLatch = 0; + jasmine.Block.call(this, env, null, spec); +}; +jasmine.util.inherit(jasmine.WaitsForBlock, jasmine.Block); + +jasmine.WaitsForBlock.TIMEOUT_INCREMENT = 10; + +jasmine.WaitsForBlock.prototype.execute = function(onComplete) { + if (jasmine.VERBOSE) { + this.env.reporter.log('>> Jasmine waiting for ' + (this.message || 'something to happen')); + } + var latchFunctionResult; + try { + latchFunctionResult = this.latchFunction.apply(this.spec); + } catch (e) { + this.spec.fail(e); + onComplete(); + return; + } + + if (latchFunctionResult) { + onComplete(); + } else if (this.totalTimeSpentWaitingForLatch >= this.timeout) { + var message = 'timed out after ' + this.timeout + ' msec waiting for ' + (this.message || 'something to happen'); + this.spec.fail({ + name: 'timeout', + message: message + }); + + this.abort = true; + onComplete(); + } else { + this.totalTimeSpentWaitingForLatch += jasmine.WaitsForBlock.TIMEOUT_INCREMENT; + var self = this; + this.env.setTimeout(function() { + self.execute(onComplete); + }, jasmine.WaitsForBlock.TIMEOUT_INCREMENT); + } +}; + +jasmine.version_= { + "major": 1, + "minor": 3, + "build": 1, + "revision": 1354556913 +}; diff --git a/www/js/fingerprint/specs/lib/phantom-jasmine/console-runner.js b/www/js/fingerprint/specs/lib/phantom-jasmine/console-runner.js new file mode 100644 index 0000000000..e197595940 --- /dev/null +++ b/www/js/fingerprint/specs/lib/phantom-jasmine/console-runner.js @@ -0,0 +1,104 @@ +/** + Jasmine Reporter that outputs test results to the browser console. + Useful for running in a headless environment such as PhantomJs, ZombieJs etc. + + Usage: + // From your html file that loads jasmine: + jasmine.getEnv().addReporter(new jasmine.ConsoleReporter()); + jasmine.getEnv().execute(); +*/ + +(function(jasmine, console) { + if (!jasmine) { + throw "jasmine library isn't loaded!"; + } + + var ANSI = {} + ANSI.color_map = { + "green" : 32, + "red" : 31 + } + + ANSI.colorize_text = function(text, color) { + var color_code = this.color_map[color]; + return "\033[" + color_code + "m" + text + "\033[0m"; + } + + var ConsoleReporter = function() { + if (!console || !console.log) { throw "console isn't present!"; } + this.status = this.statuses.stopped; + }; + + var proto = ConsoleReporter.prototype; + proto.statuses = { + stopped : "stopped", + running : "running", + fail : "fail", + success : "success" + }; + + proto.reportRunnerStarting = function(runner) { + this.status = this.statuses.running; + this.start_time = (new Date()).getTime(); + this.executed_specs = 0; + this.passed_specs = 0; + this.log("Starting..."); + }; + + proto.reportRunnerResults = function(runner) { + var failed = this.executed_specs - this.passed_specs; + var spec_str = this.executed_specs + (this.executed_specs === 1 ? " spec, " : " specs, "); + var fail_str = failed + (failed === 1 ? " failure in " : " failures in "); + var color = (failed > 0)? "red" : "green"; + var dur = (new Date()).getTime() - this.start_time; + + this.log(""); + this.log("Finished"); + this.log("-----------------"); + this.log(spec_str + fail_str + (dur/1000) + "s.", color); + + this.status = (failed > 0)? this.statuses.fail : this.statuses.success; + + /* Print something that signals that testing is over so that headless browsers + like PhantomJs know when to terminate. */ + this.log(""); + this.log("ConsoleReporter finished"); + }; + + + proto.reportSpecStarting = function(spec) { + this.executed_specs++; + }; + + proto.reportSpecResults = function(spec) { + if (spec.results().passed()) { + this.passed_specs++; + return; + } + + var resultText = spec.suite.description + " : " + spec.description; + this.log(resultText, "red"); + + var items = spec.results().getItems() + for (var i = 0; i < items.length; i++) { + var trace = items[i].trace.stack || items[i].trace; + this.log(trace, "red"); + } + }; + + proto.reportSuiteResults = function(suite) { + if (!suite.parentSuite) { return; } + var results = suite.results(); + var failed = results.totalCount - results.passedCount; + var color = (failed > 0)? "red" : "green"; + this.log(suite.getFullName() + ": " + results.passedCount + " of " + results.totalCount + " passed.", color); + }; + + proto.log = function(str, color) { + var text = (color != undefined)? ANSI.colorize_text(str, color) : str; + console.log(text) + }; + + jasmine.ConsoleReporter = ConsoleReporter; +})(jasmine, console); + diff --git a/www/js/fingerprint/specs/lib/phantom-jasmine/index.html b/www/js/fingerprint/specs/lib/phantom-jasmine/index.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/www/js/fingerprint/specs/lib/phantom-jasmine/run_jasmine_test.coffee b/www/js/fingerprint/specs/lib/phantom-jasmine/run_jasmine_test.coffee new file mode 100644 index 0000000000..3c276aeabc --- /dev/null +++ b/www/js/fingerprint/specs/lib/phantom-jasmine/run_jasmine_test.coffee @@ -0,0 +1,46 @@ +#!/usr/local/bin/phantomjs + +# Runs a Jasmine Suite from an html page +# @page is a PhantomJs page object +# @exit_func is the function to call in order to exit the script + +class PhantomJasmineRunner + constructor: (@page, @exit_func = phantom.exit) -> + @tries = 0 + @max_tries = 10 + + get_status: -> @page.evaluate(-> console_reporter.status) + + terminate: -> + switch @get_status() + when "success" then @exit_func 0 + when "fail" then @exit_func 1 + else @exit_func 2 + +# Script Begin +if phantom.args.length == 0 + console.log "Need a url as the argument" + phantom.exit 1 + +page = new WebPage() + +runner = new PhantomJasmineRunner(page) + +# Don't supress console output +page.onConsoleMessage = (msg) -> + console.log msg + + # Terminate when the reporter singals that testing is over. + # We cannot use a callback function for this (because page.evaluate is sandboxed), + # so we have to *observe* the website. + if msg == "ConsoleReporter finished" + runner.terminate() + +address = phantom.args[0] + +page.open address, (status) -> + if status != "success" + console.log "can't load the address!" + phantom.exit 1 + + # Now we wait until onConsoleMessage reads the termination signal from the log. diff --git a/www/js/fingerprint/specs/run.sh b/www/js/fingerprint/specs/run.sh new file mode 100644 index 0000000000..247acac1bb --- /dev/null +++ b/www/js/fingerprint/specs/run.sh @@ -0,0 +1 @@ +phantomjs lib/phantom-jasmine/run_jasmine_test.coffee test_runner.html diff --git a/www/js/fingerprint/specs/test_runner.html b/www/js/fingerprint/specs/test_runner.html new file mode 100644 index 0000000000..a05ae60b6e --- /dev/null +++ b/www/js/fingerprint/specs/test_runner.html @@ -0,0 +1,22 @@ + + + + Jasmine Test Runner + + + + + + + + + + + + +