среда, 29 октября 2014 г.

FileSaver - Сохранение произвольного текста из браузера в файл

/* FileSaver.js
 *  A saveAs() & saveTextAs() FileSaver implementation.
 *  2014-06-24
 *
 *  Modify by Brian Chen
 *  Author: Eli Grey, http://eligrey.com
 *  License: X11/MIT
 *    See https://github.com/eligrey/FileSaver.js/blob/master/LICENSE.md
 */

/*global self */
/*jslint bitwise: true, indent: 4, laxbreak: true, laxcomma: true, smarttabs: true, plusplus: true */

/*! @source http://purl.eligrey.com/github/FileSaver.js/blob/master/FileSaver.js */

var saveAs = saveAs
  // IE 10+ (native saveAs)
  || (typeof navigator !== "undefined" &&
      navigator.msSaveOrOpenBlob && navigator.msSaveOrOpenBlob.bind(navigator))
  // Everyone else
  || (function (view) {
      "use strict";
      // IE <10 is explicitly unsupported
      if (typeof navigator !== "undefined" &&
          /MSIE [1-9]\./.test(navigator.userAgent)) {
          return;
      }
      var
            doc = view.document
            // only get URL when necessary in case Blob.js hasn't overridden it yet
          , get_URL = function () {
              return view.URL || view.webkitURL || view;
          }
          , save_link = doc.createElementNS("http://www.w3.org/1999/xhtml", "a")
          , can_use_save_link = !view.externalHost && "download" in save_link
          , click = function (node) {
              var event = doc.createEvent("MouseEvents");
              event.initMouseEvent(
                  "click", true, false, view, 0, 0, 0, 0, 0
                  , false, false, false, false, 0, null
              );
              node.dispatchEvent(event);
          }
          , webkit_req_fs = view.webkitRequestFileSystem
          , req_fs = view.requestFileSystem || webkit_req_fs || view.mozRequestFileSystem
          , throw_outside = function (ex) {
              (view.setImmediate || view.setTimeout)(function () {
                  throw ex;
              }, 0);
          }
          , force_saveable_type = "application/octet-stream"
          , fs_min_size = 0
          , deletion_queue = []
          , process_deletion_queue = function () {
              var i = deletion_queue.length;
              while (i--) {
                  var file = deletion_queue[i];
                  if (typeof file === "string") { // file is an object URL
                      get_URL().revokeObjectURL(file);
                  } else { // file is a File
                      file.remove();
                  }
              }
              deletion_queue.length = 0; // clear queue
          }
          , dispatch = function (filesaver, event_types, event) {
              event_types = [].concat(event_types);
              var i = event_types.length;
              while (i--) {
                  var listener = filesaver["on" + event_types[i]];
                  if (typeof listener === "function") {
                      try {
                          listener.call(filesaver, event || filesaver);
                      } catch (ex) {
                          throw_outside(ex);
                      }
                  }
              }
          }
          , FileSaver = function (blob, name) {
              // First try a.download, then web filesystem, then object URLs
              var
                    filesaver = this
                  , type = blob.type
                  , blob_changed = false
                  , object_url
                  , target_view
                  , get_object_url = function () {
                      var object_url = get_URL().createObjectURL(blob);
                      deletion_queue.push(object_url);
                      return object_url;
                  }
                  , dispatch_all = function () {
                      dispatch(filesaver, "writestart progress write writeend".split(" "));
                  }
                  // on any filesys errors revert to saving with object URLs
                  , fs_error = function () {
                      // don't create more object URLs than needed
                      if (blob_changed || !object_url) {
                          object_url = get_object_url(blob);
                      }
                      if (target_view) {
                          target_view.location.href = object_url;
                      } else {
                          window.open(object_url, "_blank");
                      }
                      filesaver.readyState = filesaver.DONE;
                      dispatch_all();
                  }
                  , abortable = function (func) {
                      return function () {
                          if (filesaver.readyState !== filesaver.DONE) {
                              return func.apply(this, arguments);
                          }
                      };
                  }
                  , create_if_not_found = { create: true, exclusive: false }
                  , slice
              ;
              filesaver.readyState = filesaver.INIT;
              if (!name) {
                  name = "download";
              }
              if (can_use_save_link) {
                  object_url = get_object_url(blob);
                  save_link.href = object_url;
                  save_link.download = name;
                  click(save_link);
                  filesaver.readyState = filesaver.DONE;
                  dispatch_all();
                  return;
              }
              // Object and web filesystem URLs have a problem saving in Google Chrome when
              // viewed in a tab, so I force save with application/octet-stream
              // http://code.google.com/p/chromium/issues/detail?id=91158
              if (view.chrome && type && type !== force_saveable_type) {
                  slice = blob.slice || blob.webkitSlice;
                  blob = slice.call(blob, 0, blob.size, force_saveable_type);
                  blob_changed = true;
              }
              // Since I can't be sure that the guessed media type will trigger a download
              // in WebKit, I append .download to the filename.
              // https://bugs.webkit.org/show_bug.cgi?id=65440
              if (webkit_req_fs && name !== "download") {
                  name += ".download";
              }
              if (type === force_saveable_type || webkit_req_fs) {
                  target_view = view;
              }
              if (!req_fs) {
                  fs_error();
                  return;
              }
              fs_min_size += blob.size;
              req_fs(view.TEMPORARY, fs_min_size, abortable(function (fs) {
                  fs.root.getDirectory("saved", create_if_not_found, abortable(function (dir) {
                      var save = function () {
                          dir.getFile(name, create_if_not_found, abortable(function (file) {
                              file.createWriter(abortable(function (writer) {
                                  writer.onwriteend = function (event) {
                                      target_view.location.href = file.toURL();
                                      deletion_queue.push(file);
                                      filesaver.readyState = filesaver.DONE;
                                      dispatch(filesaver, "writeend", event);
                                  };
                                  writer.onerror = function () {
                                      var error = writer.error;
                                      if (error.code !== error.ABORT_ERR) {
                                          fs_error();
                                      }
                                  };
                                  "writestart progress write abort".split(" ").forEach(function (event) {
                                      writer["on" + event] = filesaver["on" + event];
                                  });
                                  writer.write(blob);
                                  filesaver.abort = function () {
                                      writer.abort();
                                      filesaver.readyState = filesaver.DONE;
                                  };
                                  filesaver.readyState = filesaver.WRITING;
                              }), fs_error);
                          }), fs_error);
                      };
                      dir.getFile(name, { create: false }, abortable(function (file) {
                          // delete file if it already exists
                          file.remove();
                          save();
                      }), abortable(function (ex) {
                          if (ex.code === ex.NOT_FOUND_ERR) {
                              save();
                          } else {
                              fs_error();
                          }
                      }));
                  }), fs_error);
              }), fs_error);
          }
          , FS_proto = FileSaver.prototype
          , saveAs = function (blob, name) {
              return new FileSaver(blob, name);
          }
      ;
      FS_proto.abort = function () {
          var filesaver = this;
          filesaver.readyState = filesaver.DONE;
          dispatch(filesaver, "abort");
      };
      FS_proto.readyState = FS_proto.INIT = 0;
      FS_proto.WRITING = 1;
      FS_proto.DONE = 2;

      FS_proto.error =
      FS_proto.onwritestart =
      FS_proto.onprogress =
      FS_proto.onwrite =
      FS_proto.onabort =
      FS_proto.onerror =
      FS_proto.onwriteend =
          null;

      view.addEventListener("unload", process_deletion_queue, false);
      saveAs.unload = function () {
          process_deletion_queue();
          view.removeEventListener("unload", process_deletion_queue, false);
      };
      return saveAs;
  }(
    typeof self !== "undefined" && self
 || typeof window !== "undefined" && window
 || this.content
));
// `self` is undefined in Firefox for Android content script context
// while `this` is nsIContentFrameMessageManager
// with an attribute `content` that corresponds to the window

if (typeof module !== "undefined" && module !== null) {
    module.exports = saveAs;
} else if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) {
    define([], function () {
        return saveAs;
    });
}

String.prototype.endsWithAny = function () {
    var strArray = Array.prototype.slice.call(arguments),
        $this = this.toLowerCase().toString();
    for (var i = 0; i < strArray.length; i++) {
        if ($this.indexOf(strArray[i], $this.length - strArray[i].length) !== -1) return true;
    }
    return false;
};

var saveTextAs = saveTextAs
|| (function (textContent, fileName, charset) {
    fileName = fileName || 'download.txt';
    charset = charset || 'utf-8';
    textContent = (textContent || '').replace(/\r?\n/g, "\r\n");
    if (saveAs && Blob) {
        var blob = new Blob([textContent], { type: "text/plain;charset=" + charset });
        saveAs(blob, fileName);
        return true;
    } else {//IE9-
        var saveTxtWindow = window.frames.saveTxtWindow;
        if (!saveTxtWindow) {
            saveTxtWindow = document.createElement('iframe');
            saveTxtWindow.id = 'saveTxtWindow';
            saveTxtWindow.style.display = 'none';
            document.body.insertBefore(saveTxtWindow, null);
            saveTxtWindow = window.frames.saveTxtWindow;
            if (!saveTxtWindow) {
                saveTxtWindow = window.open('', '_temp', 'width=100,height=100');
                if (!saveTxtWindow) {
                    window.alert('Sorry, download file could not be created.');
                    return false;
                }
            }
        }

        var doc = saveTxtWindow.document;
        doc.open('text/plain', 'replace');
        doc.charset = charset;
        if (fileName.endsWithAny('.htm', '.html')) {
            doc.close();
            doc.body.innerHTML = '\r\n' + textContent + '\r\n';
        } else {
            if (!fileName.endsWithAny('.txt')) fileName += '.txt';
            doc.write(textContent);
            doc.close();
        }

        var retValue = doc.execCommand('SaveAs', null, fileName);
        saveTxtWindow.close();
        return retValue;
    }
})

Решение проблемы, если при скачивании файла Internet Explorer не передает заголовок Referer

Внимание!!! Решение не подойдет из-за того, что браузер блокирует программный клик по гиперссылкам.

Стандартное решение.

setTimeout(function () {
    var href = self.$element.attr("href");
    var target = self.$element.attr("target");
    if (href && href.indexOf("#") !== 0) {
        if (target == "_blank") {
            window.open(href, target);
        } else {
            var a = document.createElement("a");

            if (!a.click) {
                window.location = href;
                return;
            }

            a.setAttribute("href", url);
            a.style.display = "none";
            document.body.appendChild(a);
            a.click();
        }
    }
}, 100);

Решение для разных браузеров.

// use browser sniffing to determine if IE or Opera (ugly, but required)
var isOpera, isIE = false;
if (typeof(window.opera) != 'undefined') {isOpera = true;}
if (!isOpera && navigator.userAgent.indexOf('Internet Explorer')) {isIE = true;}

// define for all browsers
function goto(url){
  location.href = url;
}

// re-define for IE
if(isIE){
  function goto(url){
    var referLink = document.createElement('a');
    referLink.href = url;
    document.body.appendChild(referLink);
    referLink.click();
  }
}

Вызов скачивания файла в браузере через JavaScript посредством создания iFrame

function iframeFileDownload (url, data) {
    var iframe = $('#download_file_iframe')
        , iframeDoc
        , iframeHTML ='<html><head></head><body><form method="GET" action="' + url + '">'
        , key;
    for (key in data) {
        if (data.hasOwnProperty(key)) {
            iframeHTML += '<input type="hidden" name="' + key + '" value="' + data[key] + '" />';
        }
    }
    iframeHTML += '</form></body></html>';
    if (iframe.length === 0) {
        iframe = $('<iframe id="download_file_iframe" style="display: none" src="about:blank"></iframe>');
        $('body').append(iframe);
    }
    iframeDoc = iframe[0].contentWindow || iframe[0].contentDocument;
    if (iframeDoc.document) {
        iframeDoc = iframeDoc.document;
    }
    iframeDoc.open();
    iframeDoc.write(iframeHTML);
    $(iframeDoc).find('form').submit();
}

// Как использовать
$('#someid').on('click', function() {
    iframeFileDownload('/download.action', {'param1': 1, 'param2': 2});
});

Скачивание файлов посредством AJAX-запроса не приводит к вызову окна с предложением сохранить файл.
Вы может использовать XMLHttpRequest для получения бинарного файла, но вы не сможете сохранить его на компьютер пользователя. Использование Flash снимает это ограничение, но не всегда удобно использовать Flash.
Поэтому в помощь нам прийдет iframe. iframe может существовать внутри DOM страницы, но в большинстве случаев он воспринимается браузером как отдельно окно. Динамически вставляя скрытый iframe в DOM и устанавливая его "src" на скачиваемый файл мы вызываем скачивание файла так, как-будто оно было вызвано в главном окне. Такой способ позволяет нам обойти проблему сбоя во время скачивания файла, приводящего к перезагрузке страницы.
Как любое обычное скачивание файла в основном окне, скачивание файла через iframe никогда не приведет к перезагрузке страницы, если скачивание было завершено успешно. Поэтому для определения успешного скачивания сервер должен установить в браузер cookie. Таким образом ответ сервера будет примерно таким:
Content-Disposition: attachment; filename=Report0.pdf
Set-Cookie: fileDownload=true; path=/
До тех пор пока мы не можем напрямую сказать был ли файл скачан успешно, мы должны проверять наличие cookie, которое должен был установить сервер.
Как только скачивание через iframe началось, то мы начинаем циклически проверять наличие cookie.
Если содержимое iframe во время скачивания файла изменится, то это будет означать, что при скачивании произошла ошибка.
А, если cookie было записано, то мы прекращаем проверку и удаляем iframe, потому что диалоговое окно с предолжением сохранить файл уже появилось на экране.

вторник, 28 октября 2014 г.

Как применить фильтры и эффекты к изображению при помощи javascript и canvas?

Элемент <canvas> (холст) полностью поддерживается IE, начиная с 9 версии. Canvas предназначен для создания и обработки различной растровой графики при помощи JavaScript. Кроме этого его можно использовать для работы с анимацииями и даже обработки видео в реальном времени.

Рассмотрим работу с canvas в контексте применения различных эффектов к изображению. В этом случае задача сводится к тому, чтобы поместить изображение на canvas и преобразовать его пиксели.

// создаем или находим изображение
var img = document.getElementById('img');

img.onload = function() {
  // создаем или находим canvas
  var canvas = document.getElementById('canvas');
  // получаем его 2D контекст
  var context = canvas.getContext('2d');
  // помещаем изображение в контекст
  context.drawImage(img, 0, 0);
  // получаем объект, описывающий внутреннее состояние области контекста
  var imageData = context.getImageData(0, 0, 300, 300);
  // фильтруем
  imageDataFiltered = sepia(imageData);
  // кладем результат фильтрации обратно в canvas
  context.putImageData(imageDataFiltered, 0, 0);
}

img.src = 'img/girl.png';

Фильтр представляет из себя набор математических опаераций над значениями красного, зеленого и синего канала каждой из точек изображения. Вот простейший эффект сепия.

var sepia = function (imageData) {
  // получаем одномерный массив, описывающий все пиксели изображения
  var pixels = imageData.data;
  // циклически преобразуем массив, изменяя значения красного, зеленого и синего каналов
  for (var i = 0; i < pixels.length; i += 4) {
    var r = pixels[i];
    var g = pixels[i + 1];
    var b = pixels[i + 2];
    pixels[i]     = (r * 0.393)+(g * 0.769)+(b * 0.189); // red
    pixels[i + 1] = (r * 0.349)+(g * 0.686)+(b * 0.168); // green
    pixels[i + 2] = (r * 0.272)+(g * 0.534)+(b * 0.131); // blue
  }
  return imageData;
};

pixels – это одномерный массив, в котором последовательно представлены значения красного, зеленого, синего канала, а также канала прозрачности для каждого пикселя изображения. Графически его можно представить так:


В итоге получится следующее преобразование картинки:


Достоинства Canvas:
- поддерживается IE 9 и практически всеми мобильными браузерами;
- множество готовых решений, библиотек, плагино;
- фильтры могут быть настолько сложными и нестандартными, насколько у вас хватит фантазии и насколько вы разбираетесь в - цифровой обработке изображений.

Недостатки Canvas:
- Нельзя обработать картинки с других доменов (включая поддомены) из-за ограничений безопасности браузера. Это довольно легко решается проксированием или переводом в base64, но как бы то ни было, создает дополнительные проблемы. Хотя проксирование не всегда не всегда нужно, поскольку сервер может отдавать заголовки (CORS), которые разрешают конкретному сайту (домену) или всем использовать свои ресурсы. Тогда проблем с другим доменом не будет. Ведь мы можем хранить наши изображения в CDN и проксирование будет не эффективным. Так же конвертация в base64 и обратно – не быстрая операция. И при конвертации base64 -> image такому изображение ставится текущий домен в качестве origin. Это так же может помешать смешать два изображения с разных доменов.
Для того, чтобы не было проблем с быстродействием, можно выносить обработку изображения в Web Worker'ы.
Об этом почитать можно тут http://blogs.msdn.com/b/eternalcoding/archive/2012/09/20/using-web-workers-to-improve-performance-of-image-manipulation.aspx
Если обработка не требует соседних пикселей (как серпия, например), то можно даже распараллелить обработку по нескольким worker'ам.
- Если мы говорим про сложные фильтры, то это медленная, последовательная, блокирующая операция. Десктопный браузер при этом подтормаживает, а мобильный браузер серьезно тупит.

вторник, 21 октября 2014 г.

Архитектура тестов с использованием Require JS, Mocha JS, Chai, Sinon

Тесты расположены в одном общем файле.

Структура расположения файлов и папок.

css/mocha.css
js/lib/require/require.js
js/lib/require/text.js
js/lib/jquery/jquery.js
js/lib/mocha/mocha.js
js/lib/chai/chai.js
js/lib/sinon/sinon.js
js/lib/sinon/sinon-chai.js
js/index.js
index.html

Код файла index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <link href="css/mocha.css?v=1.0.0" rel="stylesheet" type="text/css" />
    <script data-main="js/index.js?v=1.0.0" src="js/lib/require/require.js?v=1.0.0" type="text/javascript"></script>
    <title>Тесты</title>
</head>
<body>
    <div id="mocha"></div>
</body>
</html>

Код файла index.js

require.config({
      paths: {
          jquery: 'lib/jquery/jquery'
        , mocha: 'lib/mocha/mocha'
        , chai: 'lib/chai/chai'
        , sinon: 'lib/sinon/sinon'
        , sinonChai: 'lib/sinon/sinon-chai'
        , text: 'lib/require/text'
      }
    , shim: {
          mocha: {exports: 'mocha'}
        , sinon: {exports: 'sinon'}
        , sinonChai: {deps: ['chai', 'sinon']}
      }
    , urlArgs: '_=' + Math.random()
});

require(
    [
          'jquery'
        , 'mocha'
        , 'chai'
        , 'sinon'
        , 'sinonChai'
    ]
    , function(
          $
        , mocha
        , chai
        , sinon
        , sinonChai
    ) {

        // Тестируемые функции

        function sayOK(){
            return 'OK';
        }

        function hello(name, callback) {
            callback('Hello ' + name);
        }

        // Тесты

        $.ajaxSetup({cache: false});

        mocha.setup('bdd');

        var assert = chai.assert
            , expect = chai.expect
            , should = chai.should(); // Note that should has to be executed

        chai.use(sinonChai);

        describe('Say OK Tests', function() {
                                   
            describe('Function sayOK()', function() {
                                         
                it('should work with assert', function() {
                    assert.equal(sayOK(), 'OK');
                });

                it('should work with expect', function() {
                    expect(sayOK()).to.equal('OK');
                })

                it('should work with should', function() {
                    sayOK().should.equal('OK');
                });

            });
         
        });

        describe('Say Hello Tests with Expect', function () {
            it('should call callback with correct greeting', function () {
                var callback = sinon.spy();
                hello('foo', callback);
                expect(callback).to.have.been.calledWith('Hello foo');
            });
        });
       
        describe('Say Hello Tests with Should', function () {
            it('should call callback with correct greeting', function () {
                var callback = sinon.spy();
                hello('foo', callback);
                callback.should.have.been.calledWith('Hello foo');
            });
        });

        mocha.run();
   
    }
);

Тесты расположены в отдельных файлах.

Структура расположения файлов и папок.

css/mocha.css
js/lib/require/require.js
js/lib/require/text.js
js/lib/jquery/jquery.js
js/lib/mocha/mocha.js
js/lib/chai/chai.js
js/lib/sinon/sinon.js
js/lib/sinon/sinon-chai.js
js/spec/sample/sample.js
js/index.js
js/tests.js
index.html

Код файла index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <link href="css/mocha.css?v=1.0.0" rel="stylesheet" type="text/css" />
    <script data-main="js/index.js?v=1.0.0" src="js/lib/require/require.js?v=1.0.0" type="text/javascript"></script>
    <title>Тесты</title>
</head>
<body>
    <div id="mocha"></div>
</body>
</html>

Код файла index.js

require.config({
      paths: {
          jquery: 'lib/jquery/jquery'
        , mocha: 'lib/mocha/mocha'
        , chai: 'lib/chai/chai'
        , sinon: 'lib/sinon/sinon'
        , sinonChai: 'lib/sinon/sinon-chai'
        , tests: 'tests'
        , text: 'lib/require/text'
      }
    , shim: {
          mocha: {exports: 'mocha'}
        , sinon: {exports: 'sinon'}
        , sinonChai: {deps: ['chai', 'sinon']}
        , tests: {deps: ['mocha', 'chai', 'sinon', 'sinonChai']}
      }
    , urlArgs: '_=' + Math.random()
});

require(
    [
          'jquery'
        , 'mocha'
        , 'chai'
        , 'sinon'
        , 'sinonChai'
        , 'tests'
    ]
    , function(
          $
        , mocha
        , chai
        , sinon
        , sinonChai
        , tests
    ) {

        $.ajaxSetup({cache: false});

        mocha.setup('bdd');

        chai.use(sinonChai);
       
        tests();

        mocha.run();
   
    }
);

Код файла tests.js

require.config({
      paths: {
          sampleTest: 'spec/sample/sample'
      }
});

define(
    [
          'sampleTest'
    ]
    , function(
          sampleTest
    ) {
   
        return function () {
       
            sampleTest();

        };
   
    }
);

Кода файла sample.js

require.config({
      paths: {
          jquery: 'lib/jquery/jquery'
        , chai: 'lib/chai/chai'
        , sinon: 'lib/sinon/sinon'
      }
    , shim: {
          sinon: {exports: 'sinon'}
      }
});

define(
    [
          'jquery'
        , 'chai'
        , 'sinon'
    ]
    , function(
          $
        , chai
        , sinon
    ) {
   
        return function () {
       
            // Тестируемые функции

            function sayOK(){
                return 'OK';
            }

            function hello(name, callback) {
                callback('Hello ' + name);
            }

            // Тесты
               
            var assert = chai.assert
                , expect = chai.expect
                , should = chai.should(); // Note that should has to be executed
           
            describe('Say OK Tests', function() {
                                       
                describe('Function sayOK()', function() {
                                             
                    it('should work with assert', function() {
                        assert.equal(sayOK(), 'OK');
                    });

                    it('should work with expect', function() {
                        expect(sayOK()).to.equal('OK');
                    })

                    it('should work with should', function() {
                        sayOK().should.equal('OK');
                    });

                });

            });

            describe('Say Hello Tests with Expect', function () {
                it('should call callback with correct greeting', function () {
                    var callback = sinon.spy();
                    hello('foo', callback);
                    expect(callback).to.have.been.calledWith('Hello foo');
                });
            });
           
            describe('Say Hello Tests with Should', function () {
                it('should call callback with correct greeting', function () {
                    var callback = sinon.spy();
                    hello('foo', callback);
                    callback.should.have.been.calledWith('Hello foo');
                });
            });
       
        };
   
    }
);

понедельник, 20 октября 2014 г.

Mocha with Chai and Sinon in Browser

Тестирование JavaScript-кода через Mocha JS вместе с Chai и Sinon в браузере.



Представим, что ваш проект имеет следующую структуру расположения папок и файлов:

/css
/js/jquery.js
/js/index.js
/index.html

Файл jquery.js вы можете скачать с сайта jquery.com

Код файла index.js

function sayOK(){
    return 'OK';
}

function hello(name, callback) {
    callback('hello ' + name);
}

Код файла index.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/index.js"></script>
<script type="text/javascript">
$(document).ready(function(){
    alert(sayOK());
    hello('Boris', alert);
});
</script>
<title>My Page</title>
</head>
<body>
<h1>My Page</h1>
</body>
</html>

При открытии файла index.html в браузере будет выполнен код функции sayOK() и выведено сообщение "ОК", после чего будет выполнена функция hello и выведено сообщение "Hello Boris".

Для функций sayOK() и hello() нам необходимо будет написать тесты, которые будут выполняться через Mocha JS в браузере.

Для тестирования JavaScript-кода через Mocha JS в браузере необходимо скачать и файл "mocha.js" с кодом библиотеки Mocha JS, который расположен в GitHub по адресу

https://raw.githubusercontent.com/visionmedia/mocha/master/mocha.js

и файл "mocha.css" со стилями оформления для вывода результатов тестирования на страницу в браузере, который расположен в GitHub по адресу

https://raw.githubusercontent.com/visionmedia/mocha/master/mocha.css

Файл mocha.js скопируйте в папку js. А файл mocha.css скопируйте в папку css.

После этого структура расположения папок и файлов в проекте будет выглядеть так:

/css/mocha.css
/js/mocha.js
/js/jquery.js
/js/index.js
/index.html

Далее в папке js создадим файл tests.js, к котором напишем тесты для функции sayOK() с применением Mocha JS.

Код файла tests.js

// Код функции запуска тестов

function runTests() {

    mocha.setup('bdd');  // Устанавливаем тип тестов BDD
 
    // Определяем функцию assert, которую будем использовать для тестов ниже.

    function assert(expr, msg) {
        if (!expr) throw new Error(msg || 'failed');
    }

    // Создаем набор тестов, использующих Mocha JS
 
    describe("Say OK Tests", function() {
 
      describe("Function must return OK", function() {
        it("returns OK", function() {
          var result = sayOK();
          assert(result === 'OK');
        });
      });
 
      describe("Function return OKI", function() {
        it("returns OK", function() {
          var result = sayOK() + 'I';
          assert(result === 'OKI');
        });
      });
 
      describe("Function return Not a Number", function() {
        it("returns NaN", function() {
          var result = sayOK();
          assert(isNaN(result));
        });
      });
   
    });
 
    var runner = mocha.run(); // Запускаем наш набор тестов

}

$(document).ready(function(){
    runTests(); // Запускаем наши тесты после загрузки страницы
});

Теперь структура нашего проекта будет выглядеть так:

/css/mocha.css
/js/mocha.js
/js/jquery.js
/js/index.js
/js/tests.js
/index.html

Далее создадим HTML-файл testrunner.html, который мы будем открывать в браузере для запуска тестов нашего кода.

Код файла testrunner.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link href="css/mocha.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/mocha.js"></script>
<script type="text/javascript" src="js/index.js"></script>
<script type="text/javascript" src="js/tests.js"></script>
<title>Test Runner</title>
</head>
<body>
    <div id="mocha"></div>
</body>
</html>

В файле testrunner.html производится загрузка файла библиотеки Mocha JS, файла index.js с кодом функции sayOK(), которую мы будем тестировать и набор тестов для этой функции в файле tests.js, которые автоматически запускаются после загрузки файла testrunner.html в браузере.

Результаты выполнения тестов помещаются в специальный блок <div id="mocha"></div> внутри тэга <body>.

Полная структура проекта в итоге будет выглядеть так:

/css/mocha.css
/js/mocha.js
/js/jquery.js
/js/index.js
/js/tests.js
/index.html
/testrunner.html

Теперь, если вместо определенной нами в файле tests.js функции

    function assert(expr, msg) {
        if (!expr) throw new Error(msg || 'failed');
    }

мы захотим использовать для тестирования JavaScript-кода библиотеку Chai, то скопируем в папку js файл chai.js, который можно взять по адресу

chaijs.com/chai.js

В результате структура нашего проекта станет выглядеть так:

/css/mocha.css
/js/mocha.js
/js/chai.js
/js/jquery.js
/js/index.js
/js/tests.js
/index.html
/testrunner.html

В HTML-файле для запуска тестов testrunner.html добавим ссылку на файл chai.js

Итоговый код файла testrunner.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link href="css/mocha.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/mocha.js"></script>
<script type="text/javascript" src="js/chai.js"></script>
<script type="text/javascript" src="js/index.js"></script>
<script type="text/javascript" src="js/tests.js"></script>
<title>Test Runner</title>
</head>
<body>
    <div id="mocha"></div>
</body>
</html>

В файле tests.js удалим код функции assert() и заменим старые тесты на тесты, использующие код билиотеки Chai.

Итоговый код файла tests.js

// Код функции запуска тестов

function runTests() {
 
    mocha.setup('bdd'); // Устанавливаем тип тестов BDD    
 
    // Создаем набор функций для тестирования с использованием библиотеки Chai.

    var assert = chai.assert,
          expect = chai.expect,
          should = chai.should(); // Обратите внимание, что should должна быть иметь скобки ()

    // Создаем набор тестов, использующих Mocha JS вместе с Chai

    describe('Say OK Tests', function() {
                             
      describe('Function sayOK()', function() {
                                   
        it('should work with assert', function() {
            assert.equal(sayOK(), 'OK');
        });
 
        it('should work with expect', function() {
            expect(sayOK()).to.equal('OK');
        })
 
        it('should work with should', function() {
            sayOK().should.equal('OK');
        });
     
      });
   
    });
 
    var runner = mocha.run();
 
}

$(document).ready(function(){
    runTests();
});

Таким образом для тестирования JavaScript-кода в браузере можно использовать Mocha JS вместе с Chai.

Если вам потребуется использовать библиотеку Sinon, то вам необходимо будет скачать файл "sinon.js" с кодом библиотеки Sinon, который расположен в GitHub по адресу

http://sinonjs.org/releases/sinon-1.10.3.js

и плагин для библиотеки Chai, который расположен по адресу

https://raw.githubusercontent.com/domenic/sinon-chai/master/lib/sinon-chai.js

Файл sinon-1.10.3.js переименуем в sinon.js. Теперь скопируйте в папку js файлы sinon.js и sinon-chai.js.

В файле testrunner.html после строчки

<script type="text/javascript" src="js/chai.js"></script>

вставьте эти строки

<script type="text/javascript" src="js/sinon-chai.js"></script>
<script type="text/javascript" src="js/sinon.js"></script>

Итоговый код файла testrunner.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link href="css/mocha.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/mocha.js"></script>
<script type="text/javascript" src="js/chai.js"></script>
<script type="text/javascript" src="js/sinon-chai.js"></script>
<script type="text/javascript" src="js/sinon.js"></script>
<script type="text/javascript" src="js/index.js"></script>
<script type="text/javascript" src="js/tests.js"></script>
<title>Test Runner</title>
</head>
<body>
    <div id="mocha"></div>
</body>

</html>

В файл tests.js добавим следующие тесты функции hello(), использующие библиотеку Sinon

    describe('Say Hello Tests with Expect', function () {
        it('should call callback with correct greeting', function () {
            var callback = sinon.spy();
            hello('foo', callback);
            expect(callback).to.have.been.calledWith('Hello foo');
        });
    });
 
    describe('Say Hello Tests with Should', function () {
        it('should call callback with correct greeting', function () {
            var callback = sinon.spy();
            hello('foo', callback);
            callback.should.have.been.calledWith('Hello foo');
        });
    });

Итоговый вид файла tests.js

function runTests() {

    mocha.setup('bdd');

    var assert = chai.assert,
          expect = chai.expect,
          should = chai.should(); // Note that should has to be executed

    describe('Say OK Tests', function() {
                             
      describe('Function sayOK()', function() {
                                     
        it('should work with assert', function() {
            assert.equal(sayOK(), 'OK');
        });

        it('should work with expect', function() {
            expect(sayOK()).to.equal('OK');
        })

        it('should work with should', function() {
            sayOK().should.equal('OK');
        });

      });
   
    });

    describe('Say Hello Tests with Expect', function () {
        it('should call callback with correct greeting', function () {
            var callback = sinon.spy();
            hello('foo', callback);
            expect(callback).to.have.been.calledWith('Hello foo');
        });
    });
 
    describe('Say Hello Tests with Should', function () {
        it('should call callback with correct greeting', function () {
            var callback = sinon.spy();
            hello('foo', callback);
            callback.should.have.been.calledWith('Hello foo');
        });
    });
 
    var runner = mocha.run();

}

$(document).ready(function(){
    runTests();
});

В конечном итоге содержимое всего проекта будет выглядеть следующим образом.

Структуру расположения папок и файлов

/css/mocha.css
/js/mocha.js
/js/chai.js
/js/sinon.js
/js/sinon-chai.js
/js/jquery.js
/js/index.js
/js/tests.js
/index.html
/testrunner.html

Файл index.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/index.js"></script>
<script type="text/javascript">
$(document).ready(function(){
    alert(sayOK());
    hello('Boris', alert);
});
</script>
<title>My Page</title>
</head>
<body>
<h1>My Page</h1>
</body>
</html>

Файл testrunner.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link href="css/mocha.css" rel="stylesheet" type="text/css" />
<script type="text/javascript" src="js/jquery.js"></script>
<script type="text/javascript" src="js/mocha.js"></script>
<script type="text/javascript" src="js/chai.js"></script>
<script type="text/javascript" src="js/sinon-chai.js"></script>
<script type="text/javascript" src="js/sinon.js"></script>
<script type="text/javascript" src="js/index.js"></script>
<script type="text/javascript" src="js/tests.js"></script>
<title>Test Runner</title>
</head>
<body>
    <div id="mocha"></div>
</body>
</html>

Файл index.js

function sayOK(){
    return 'OK';
}

function hello(name, callback) {
    callback('Hello ' + name);
}

Файл tests.js

function runTests() {

    mocha.setup('bdd');

    var assert = chai.assert,
          expect = chai.expect,
          should = chai.should(); // Note that should has to be executed

    describe('Say OK Tests', function() {
                             
      describe('Function sayOK()', function() {
                                     
        it('should work with assert', function() {
            assert.equal(sayOK(), 'OK');
        });

        it('should work with expect', function() {
            expect(sayOK()).to.equal('OK');
        })

        it('should work with should', function() {
            sayOK().should.equal('OK');
        });

      });
   
    });

    describe('Say Hello Tests with Expect', function () {
        it('should call callback with correct greeting', function () {
            var callback = sinon.spy();
            hello('foo', callback);
            expect(callback).to.have.been.calledWith('Hello foo');
        });
    });
 
    describe('Say Hello Tests with Should', function () {
        it('should call callback with correct greeting', function () {
            var callback = sinon.spy();
            hello('foo', callback);
            callback.should.have.been.calledWith('Hello foo');
        });
    });
 
    var runner = mocha.run();

}

$(document).ready(function(){
    runTests();
});

среда, 15 октября 2014 г.

Debug


Events



BEM


XHR Long polling amd Post to hidden iFrame


Работа над проектом

Работа над проектом
- Сбор требований и составление ТЗ
- Проектирование макета и дизайн
- Верстка
- Программирование
- Тестирование
- Релиз-деплой
- Следующая итерация

Всё начинается с таска
- Bugzilla, GitHub, JIRA, Mantis, Redmine, …
- Позволяют отслеживать статус выполнения задачи и затраченное не неё время
- Получать оповещения об изменениях
- Составлять план ведения работ и релизов

Проектирование макета
- Начинайте с эскиза
- Используйте сетки
- Разбивайте всё на отдельные слои
- Учитывайте разные длины слов в разных языках. Например: Скачать, Завантажити, Download, Indir
- Не злоупотребляйте с кастомными шрифтами

Верстка
Заводите отдельные таски для верстки и программирования
- Требуйте реальные тексты для рыбы
- Используйте сервера приложения с моками
- Среда разработки должна быть доступна в виртуальных машинах
- Автоматизируйте процесс сборки html, css и js файлов: grunt, bash, make-файлы, …
- Используйте готовые сетки: anygrid, bootstrap, …
- Используйте динамические сниппеты (emmet, шаблоны в редакторе)
- Выделяйте общие блоки
- Делайте блоки максимально независимыми

Программирование
- Разворачивайте на виртуальной машине систему аналогичную продакшин
- Процесс «разворачивания» приложения должен быть максимально автоматизирован и документирован
- Данные из хранилища должны быть легко заменяемы на моки
- Используйте готовые фреймворки
- Выделяйте общие компоненты в независимые модули
- Покрывайте тестами основные страницы и компоненты
- Создавайте API с автогенерируемой документацией
- Версионируйте API и до последнего поддерживайте обратную совместимость
- Создавайте рабочее окружение удобное для всех членов команды разработки
- Именуем ветки в соответствии с номерами тасков
- Много коммитим в форк / ветку, после завершения сквошим
- Финальный коммит берем из "Commit message"
- Автоматически собираем ченжлог со списком тасков-коммитов перед релизом

Тестирование
- Тестирование должно проходить на отдельном инстансе приложения, доступному по отдельному URL
- Тестовый сервер должен быть полностью аналогичен продакшн
- Приложение развернутое на тестовом сервере должно вспоследствие "as is" с точностью до байта переноситься в продакшн

Релиз-деплой
- Автоматизировать можно как угодно: grunt, bash, make-файлы,  мы используем deb-пакеты
- Собираем автоматически пулл-реквесты через Teamcity
- Travis CI, Jenkins, GitHub Web-hooks, …
- Изменения должны разворачиваться в продакшине максимально атомарно

Резюме
- Принимайте участие в обсуждении ТЗ, дизайна и технических моментов
- Бейте задачу на подзадачи и создавайте дерево тасков
- Старайтесь держать чистой, но полной историю изменений
- Севера разработки должны быть легко поднимаемы и требовать минимальной настройки
- Упрощайте процесс сборки и релиза до максимума

Методы регулярных выражений

- экземпляры RegExp:
        /regexp/.exec('строка')
                        null или массив ['всё совпадение', $1, $2, ...]
        /regexp/.test('строка')
                        false или true
                   
- экземпляры String:
        'str'.match(/regexp/)
        'str'.match('\\w{1,3}')
                        - эквивалент /regexp/.exec, если нет флага g;
                        - массив всех совпадений по строке, если есть флаг g
(внутренние группировки игнорируются)
                   
        'str'.search(/regexp/)
        'str'.search('\\w{1,3}')
                 позиция первого совпадения или -1

- экземпляры String:
'str'.replace(/old/, 'new');
   
В строке замены поддерживаются следующие спецсимволы:
   $$   вставляет значок доллара "$"
    $&   подстрока, совпавшая с регэкспом
    $`   подстрока до $&
    $'   подстрока после $&
    $1, $2, $3 и т.д.: cтрока, совпавшая с соответствующей
скобочной группировкой
   
'str'.replace(/(r)(e)gexp/g,
    function(matched, $1, $2, offset, sourceString) {
        // чем заменить matched на этом шаге?
        return 'замена';
});

Устройство HTTP

<схема>://<логин>:<пароль>@<хост>:<порт>/<URL‐путь>?<параметры>#<якорь>

Структура протокола

<Метод> <URI> HTTP/1.1
<Заголовки>
    Referer: http://www.yandex.ru/
</Заголовки>
<Тело сообщения>
    param=value&a=1&b=2&c=3
</Тело сообщения>

Коды состояния HTTP
- (1xx) Информационные ответ
- (2xx) Ответы успеха
- (3xx) Ответы перенаправления
- (4xx) Ошибки клиента
- (5xx) Ошибки сервера

Заголовки HTTP
- General Headers
- Request Headers
- Response Headers
- Entity Headers

HTTP/1.1 200 OK!
Date: Mon, 17 Sep 2012 13:05:11 GMT!
Transfer-Encoding: chunked!
Connection: keep-alive!
Pragma: no-cache!
Cache-Control: no-cache, no-store, max-age=0,
must-revalidate!
Server: nginx!
Vary: X-Real-SSL-Protocol!
Content-Type: text/html; charset=UTF-8!
Expires: Mon, 17 Sep 2012 13:05:11 GMT!
Content-Encoding: gzip!

Про тестирование

Unit-тесты – тесты, проверяющие корректность работы отдельных модулей программы.

Плюсы unit-тестов
- Можно запустить сразу после внесения изменений в код – позволяют найти дефект сразу после его создания.
- Могут служить документацией к коду.
- Упрощают процесс рефакторинга.

Минусы unit-тестов
- Их надо писать.
- Их надо уметь писать.
- Их надо поддерживать.

Но даже если все компоненты по отдельности работают правильно, то это ещё ничего не значит.

Интеграционные тесты – тесты,  проверяющие корректность взаимодействия отдельных модулей друг с другом.

Плюсы интеграционных тестов
- Находят баги, которые не могут быть обнаружены unit-тестами.
- Запускаются после сборки проекта и позволяют быстро обнаружить проблемы взаимодействия.

Минусы интеграционных тестов
- Все минусы unit-тестов

Приёмочные тесты – тесты, проверяющие работоспособность системы целиком. В реальном окружении, с реальными данными, на реальных сценариях.

Плюсы приёмочных тестов
- Находят баги, которые не могут быть обнаружены unit- и интеграционными тестами.
- Позволяют оценить работоспособность продукта целиком.
- На этом уровне с продуктом могут ознакомиться будущие пользователи.

Минусы приёмочных тестов
- Самые высокоуровневые – сложнее локализовывать проблему
- Занимают больше времени
- Обнаруживают проблемы с некоторой задержкой

Функциональное тестирование – проверка работы кода/продукта на соответствие требованиям. Проверка логики работы.

Конфигурационное тестирование на клиенте –  проверка работоспособности на различных конфигурациях. Для веб-сайтов – в разных браузерах.
Конфигурационное тестирование сервер-сайда – проверка работоспособности в окружении, максимально идентичном продакшену (железка, OS, утилиты, библиотеки, конфиги, версии).

Нагрузочное тестирование – проверка работоспособности под нагрузкой (одновременная обработка большого потока запросов).

Тестирование производительности – проверка скорости работы системы.
Причём:
- Необходимо измерить длительность полного цикла «запрос-ответ». Оценить общее время, обратить внимание на отдельные этапы.
- То же самое – под нагрузкой
- В пользовательских условиях (сетевые условия).

Тестирование безопасности.

Тестирование юзабилити – тестирование удобства использования.

Тестирование стабильности – тестирование стабильности работы под нагрузкой, длительное время.

До кучи:
– Volume тестирование
– Stress/Recovery тестирование
– Spike тестирование
– Localization тестирование
– Compatibility тестирование
– и т. д. и т. п.

Способы тестирования
Ручное тестирование – выполнение тестов вручную или с помощью скриптов. Ручной анализ результатов.

Плюсы ручного подхода
- Более информативно – замечаются дефекты рядом

Минусы ручного подхода
- Долго
- Дорого

Автоматическое тестирование – выполнение с помощью скриптов или инструментов. Оценка результатов проводится автоматически.

Плюсы неручного подхода
- Удобно и легко

Минусы неручного подхода
- Тесты нужно писать и поддерживать
- Тесты выполняются «в лоб»
- Сами тесты/скрипты/инструменты могут содержать баги и порождать ложные результаты

Инструменты

Функциональное, приемочное тестирование:
- Selenium
- TestComplete

Функциональное, unit тестирование
- подбирается под используемый язык

Нагрузочное тестирование/тестирование производительности:
- Яндекс.Танк
- Jmeter:

Как сымитировать плохую сеть:
- Fiddler
- Charles
- Утилита tc: man tc

среда, 1 октября 2014 г.

Как остановить браузер перед переходом на другую страницу через JavaScript

Для того, чтобы Chrome или другой браузер не очищал Network debugger необходимо в консоли браузера выполнить следующий код:

window.addEventListener("beforeunload", function() { debugger; }, false);

Это код поставит Chrome на паузу перед загрузкой новой страницы и перенесет вас на точку остановки в debugger.