четверг, 26 ноября 2009 г.

Модульное тестирование javascript кода при помощи JsTestDriver

Бочка мёда

Хочу порекомендовать JsTestDriver - великолепный фрэймворк для модульного тестирования (unit testing) javascript кода от ребят из Google. Он позволяет в считанные доли секунды протестировать javascript-код одновременно на любом (ну, почти любом) количестве любых браузеров с любых компьютеров и платформ. Впечатляет, не правда ли?

Идея JsTestDriver проста, как всё гениальное (картинка взята с оф. сайта):


Запускается небольшой веб-сервер, причем не обязательно его запускать на локальной машине. К нему, как и к обычному веб-серверу, используя определённый URL (например, http://localhost:4224/capture), коннектятся необходимые для тестирования браузеры, которые могут находится на любом компьютере (и, соответственно, платформе), с которого доступен сервер. Таким образом, можно будет одновременно тестировать код на Safari под Mac OS, Internet Explorer под Windows и, например, Firefox под Linux.

Далее происходит основная магия.
Браузеры (slaves в терминологии JsTestDriver) начинают периодически опрашивать сервер (при помощи AJAX) в ожидании новых задач (тестов). И, как только задание будет получено, браузеры загрузят необходимые скрипты, выполнят тестовые функции (test cases) и вернут на сервер результаты выполнения. Управление же сервером происходит при помощи клиентского приложения, которое легко интегрируется как с IDE разработчика, так и с системами автоматической сборки проектов (Ant, Team City, Phing и т.п.).

Однако, разработчику важна не столько вся эта кухня, сколько возможность иметь под рукой удобный инструмент для лёгкого и быстрого запуска модульных тестов и отслеживания результатов. В этом нам поможет JsTestDriver плагин для Eclipse (аналогичный плагин есть так же и для IntelliJ IDEA).



Не вдаваясь в подробности опишу лишь основные фичи этого плагина:
  • Минимум настроек: только номер порта для сервера и пути к файлам браузеров (Firefox, Chrome, Safari, Internet Explorer и Opera) на локальном компьютере
  • Запуск сервера и браузеров для тестирования прямо из IDE
  • Простейший конфигурационный файл для запуска тестов (прописывается только URL тестового сервера и список необходимых javascript-файлов)
  • Ручной запуск модульных тестов через меню Run
  • Автоматический запуск модульных тестов при каждом изменении javascript-файлов
  • Наглядное отображение результатов тестирования прямо в IDE (на каком браузере под какой платформой в каком тест-кейсе произошла ошибка)
В общем, установка и настройка JsTestDriver не вызывают никаких особых проблем (всё очень подробно описано на оф. сайте), а уж использование приносит только сплошное удовольствие. Правда, если быть честным, для этого пришлось немного (совсем чуть-чуть :) "поработать напильником"...

Капля дёгтя

Дело в том, что текущая версия (1.2 для сервера и 1.0.6 для плагина) немного недоработана (уверен, что это не надолго). В частности, если в каком либо тестируемом js-файле будет синтаксическая ошибка (или иная ошибка, возникающая при исполнении этого файла), то в результатах тестов это никак не отражается. Кроме того, наблюдается некоторая глючность Opera при исполнении тестов.

Лекарство

Для решения проблемы пришлось переписать один метод по загрузке скриптов и добавить "системный" тест-кейс, который будет отражать ошибки, произошедшие до начала выполнения пользовательских тест-кейсов (см. runner-fix.js). Ну и, в дополнение, мне показалось уместным добавить ещё две вещи:
  1. Обновление тестовой страницы после выполнения тестов, чтобы исключить возможность влияния предыдущего выполнения тестов на последующие
  2. assertFail( [expectedErrorMessage], caseWrapper ) - функция, которая позволяет протестировать случаи, когда должна происходить ошибка. Пример использования:
      function doSomething() {
          throw new Error( 'Some error' );
      }
      ...
    
      // Test case
      assertFail( 'Some error', function(){ doSomething() } );
      // true
      
Чтобы внедрить эти изменения в JsTestDriver нужно выполнить следующие шаги:
  1. Загружаем архив (содержит три файла: runner-fix.js, Runnerquirks.html, Runnerstrict.html)
  2. Находим JsTestDriver.jar из которого запускается сервер. Для плагина к Eclipse он находится где-то в папке configuration. В частности у меня, вот тут:
    c:\Program Files\Eclipse\Galileo\configuration\org.eclipse.osgi\bundles\929\1\.cp\lib\
  3. Открываем JsTestDriver.jar как обычный zip-архив
  4. Заходим в папку JsTestDriver.jar\com\google\jstestdriver\javascript\
  5. Копируем туда файлы из runner-fix.zip
  6. Запускаем JsTestDriver (вручную или через Eclipse плагин) и наслаждаемся :)
Подробнее о том, как собственно устанавливать, настраивать и использовать сервер или плагин, или писать тест-кейсы, смотрите по ссылкам, приведенным ниже. Там же можно найти общую информацию о тестировании вообще и о модульном тестировании в частности.

На последок хочется отметить, что разработчики JsTestDriver надеятся, что javascript сообщество объединится в поддержку этого фрэймворка, как единого движка для запуска тестов (наподобие JUnit в Java). Что, совместно с использование различных расширений (например, для YUI Test или QUnit), сделает процесс модульного тестирования javascript максимально продуктивным.

Что ж, кажется JsTestDriver этого вполне достоин.


Ссылки по теме:

пятница, 31 июля 2009 г.

Тюнинг code completion в Spket IDE

Spket IDE — отличная среда для современной сложной JavaScript-разработки (подробнее по ссылкам ниже [^]), однако, по непонятным мне причинам, при работе на кастомном профайле (в настройках Эклипса Spket/JavaScript Profiles) в code completion отсутствуют некоторые свойства стандартных объектов (например, методы match, replace, search и split для String). Причем, добавить их через подключение js-файла в кастомный JavaScript-профайл тоже не получается.

Решить проблему можно следующим образом (на примере плагина под Эклипс):
  1. Выходим из Эклипса (если он запущен)
  2. Заходим в папку plugins Эклипса
  3. При помощи любого файлового менеджера (FAR, Total Commander) открываем архивный файл com.spket.js_Х.Х.Х.jar (где X.X.X — ваша версия плагина Spket IDE)
  4. Редактируем файл config/core.js, добавляя недостающие свойства
Синтаксис там очень простой:
...

class Number {
    static var MAX_VALUE:Number;
    static var MIN_VALUE:Number;
    ...
}

class String {
    var length:Number;

    static function fromCharCode(... chars):String;

    function String(string:String);
    function charAt(index:Number):String;
    ...

    function match(searchValue:RegExp_or_String):Array;
    function replace(searchValue:RegExp_or_String, replaceValue:String_or_Function):String;
    function search(searchValue:RegExp_or_String):Number;
    function split(separator:String, limit:Number):Array;
}

class RegExp {
    function RegExp(pattern:String, flags:String);
    ...
}

...
так что проблем особых возникнуть не должно. После перезаргузки Эклипса новые свойства (методы) станут доступны для code completion.


Ссылки по теме:

понедельник, 8 июня 2009 г.

ie-iframe-substrate 1.0.2

Новая версия "IFRAME-подложки" доступна для скачивания: ie-iframe-substrate.1.0.2.css (~1.3 KB).

Изменения:
  1. Исправлен баг в IE 5.5
Исходники: ie-iframe-substrate.1.0.2.src.ru.css (~4.5 KB)



пятница, 10 апреля 2009 г.

ie-iframe-substrate 1.0.1

Новая версия "IFRAME-подложки" доступна для скачивания: ie-iframe-substrate.1.0.1.css (~1.3 KB).

Изменения:
  1. Убран звук "клика" при вставке IFRAME-а
  2. Улучшено быстродействие для страниц, на которых нет SELECT-ов
  3. Исправлен баг
Протестировать можно тут: тест 1, тест 2, тест 3.

Исходники: ie-iframe-substrate.1.0.1.src.ru.css (~4.5 KB)



четверг, 26 марта 2009 г.

Универсальное решение для борьбы с «просвечивающими» SELECT-ами в Internet Explorer


UPD [08.06.2009] ie-iframe-substrate 1.0.2
UPD [10.04.2009] ie-iframe-substrate 1.0.1

Введение

Как известно, Internet Explorer версий 5.5 и 6.0 содержит ошибку (aka "z-index issue"), которая заключается в том, что элементы SELECT "пробивают" любые другие элементы (например, позиционированные DIV-ы), размещённые над ними.



Единственный элемент, с которым этого не происходит — IFRAME. Именно на использовании его в качестве "прокладки" и основываются все методы устранения описанной выше ошибки IE [^]. Как правило, в них применяется один из двух вариантов. Либо, IFRAME вставляется внутрь (первым потомком) необходимого элемента, делается прозрачным, и стиль z-index устанавливается в -1. Либо, IFRAME добавляется в DOM, позиционируется непосредственно под необходимым элементом и делается равным ему по размеру.

При кажущейся простоте, у первого варианта два серьёзных недостатка. Во-первых, изменяются "внутренности" элемента, что может привести к конфликтам при динамической обработке контента javascript-ом. Во-вторых, "прикрыть", например, элементы с overflow:auto или overflow:scroll весьма проблематично. Второй вариант не имеет данных недостатков, т.к. IFRAME ни коим образом не связан с элементом, который он "прикрывает". Однако, существует проблема изменения позиции или размера элемента, которым IFRAME постоянно должен соответствовать.

В описываемом ниже решении для борьбы с «просвечивающими» SELECT-ами используется второй вариант размещения IFRAME.

"IFRAME-подложка"

Теперь собственно о главном. Предлагаемое решение основано на применении javascript-expressions в CSS стилях. Используется оно следующим образом:
  1. Скачиваем последнюю версию ie-iframe-substrate.css (~1.3 KB)
  2. Подключаем его на страницу при помощи тэга LINK или в любой CSS-файл при помощи @import
  3. Прямо в этом же файле переписываем селекторы для элементов, которым необходима "подложка" (обязательно с * html).
    Например: * html div.some-layer, * html #someLayer {...}
Всё.

Протестировать можно вот на этих страницах: тест 1, тест 2, тест 3.
К сведению: протестированный для сравнения bgIframe (плагин для jQuery) часть приведённых выше тестов пройти не смог.

Некоторые ограничения и особенности:
  1. "Прикрываемый" элемент не должен использовать стиль behavior
  2. У каждого "прикрываемого" элемента создаётся и используется свойство "_i" (element._i)
  3. Для "IFRAME-подложки" используется CSS-класс "ie-substrate" (iframe.ie-substrate)
Как это работает (в версии 1.0.0):

/**
 * Селектор слоёв (позиционированных элементов),
 * которым нужно создать "IFRAME-подложку".
 */
* html div.layer { /* Скрываем от IE 7+ в "standards-compliant" режиме */
    behavior: expression( 
        /* Если документ загружен и распаршен */
        document.readyState=='complete' ? (
            /* и браузер не IE 7+ (в "quirks" режиме) */
            parseFloat(navigator.appVersion.split('MSIE')[1])<7
            /* и "IFRAME-подложка" ещё не создана */
            && this.iframe==null ? (
                /* создаём IFRAME и вставляем его прямо перед данным элементом (слоем) */
                this.iframe = this.parentNode.insertBefore(
                    document.createElement('iframe'), this
                ),
                /* устанавливаем свойство "src" (важно для HTTPS соединения) */
                this.iframe.src = 'javascript:false',
                /* устанавливаем свойство "id" (для функции обновления) */
                this.iframe.id = '_ie_sub' + new Date().valueOf(),
                /* устанавливаем свойство "layer" для ссылки на данный слой */
                this.iframe.layer = this,
                /* создаём функцию обновления (важно для IE 6.0) */
                this.iframe.func = new Function(
                    "var iframe = document.getElementById('" + this.iframe.id +
                    "'); iframe.allowTransparency=true; iframe.allowTransparency=false;"
                ),
                /* устанавливаем соответствующее имя класса */
                this.iframe.className = 'ie-substrate',
                /* сбрасываем ссылку на IFRAME и устанавливаем флажок */
                this.iframe = true,
                /* отключаем "behavior" для данного элемента */
                this.style.behavior = 'none'
            /* иначе - отключаем "behavior" для данного элемента */
            ) : this.style.behavior = 'none'
        /* иначе - ничего не делаем */
        ) : null
    );
}

/**
 * Стили для "IFRAME-подложки"
 */
iframe.ie-substrate {
    position: absolute;
    /* Делаем IFRAME прозрачным */
    filter: Mask();

    /* Отслеживаем позицию и размеры "прикрываемого" слоя */
    behavior: expression( 
        /* Если IFRAME в DOM-е (т.е. не удален) */
        this.parentNode ? (
           /* Если "прикрываемый" слой в DOM-е (т.е. не удален) */
            this.layer.parentNode ? (
                /* и IFRAME вставлен в DOM-е перед "прикрываемым" слоем */
                this.nextSibling==this.layer ? (
                    /* перехватываем свойства "прикрываемого" слоя */
                    this.style.pixelWidth = this.layer.offsetWidth,
                    this.style.pixelHeight = this.layer.offsetHeight,
                    this.style.pixelLeft = this.layer.offsetLeft,
                    this.style.pixelTop = this.layer.offsetTop,
                    this.offsetTop>0 ? ( /* только если IFRAME виден */
                        this.style.zIndex = this.layer.currentStyle.zIndex,
                        this.style.visibility = this.layer.currentStyle.visibility,
                        this.style.display = this.layer.currentStyle.display
                    ) : null
                /* иначе */
                ) : (
                    /* вставляем IFRAME перед "прикрываемым" слоем в DOM-е */
                    this.layer.parentNode.insertBefore(this, this.layer)
                )
            /* иначе */
            ) : (
                /* удаляем IFRAME */
                this.removeNode(true)
            )
        /* иначе - ничего не делаем */
        ) : null
    );

    /* Обновляем IFRAME-ы периодически, чтобы решить проблему в IE 6.0
     * (SELECT-ы всё ещё могут перекрывать слои после скроллинга). */
    refresh: e\xpression( /* Скрываем от IE 5.5 */
        /* Если IFRAME виден */
        this.offsetTop>0 ? (
            /* включить обновление */
            this.timer ? null : this.timer = window.setInterval(this.func, 300)
        /* иначе */
        ) : (
            /* выключить обновление */
            this.timer ? this.timer = window.clearInterval(this.timer) : null
        )
    );
}

[Скачать этот код в виде файла: ie-iframe-substrate.1.0.0.src.ru.css]

Достоинства:
  1. Простота и прозрачность в использовании
  2. Корректно работает с любыми элементами как в "standards-compliant", так и в "quirks" режимах, а также при любых динамических манипуляциях с "прикрытыми" элементами
  3. Не требует внесения изменений в ваш CSS- или javascript-код
  4. Небольшой объём библиотечного файла (всего 1 KB без комментариев)
  5. Нет привязки к каким-либо фрэймворкам
  6. Не оказывает никакого влияния на другие браузеры (в том числе IE 7+)
  7. Легко отключается не оставляя следов (например, при отказе от поддержки IE 6 или от использования SELECT-ов)
Недостатки:
  1. Снижение быстродействия при увеличении количества "прикрытых" элементов
  2. Утечка памяти в IE 5.5 при большом количестве "прикрытых" элементов и при интенсивных манипуляциях с DOM-ом
  3. Некорректное позиционирование "IFRAME-подложки", если "прикрываемый" элемент имеет относительное позиционирование и расположен внутри элемента TD со значением стиля vertical-align отличным от top
Насколько эти недостатки критичны — нужно рассматривать для каждого конкретного проекта отдельно.


Ссылки по теме:

пятница, 20 февраля 2009 г.

Классическое наследование в JavaScript

Данная статья адресована, в первую очередь, программистам, пишущим на JavaScript и имеющим, как минимум, базовые знания в области объектов и наследования через прототипы. Поэтому, я не буду излагать азы (которые, при желании, можно найти по ссылкам в конце статьи [^]). Хочу просто показать, как можно реализовать классическое (классовое) наследование, используя стандартные возможности языка.

Поставим перед собой несколько целей:
  1. Наследование должно быть полноценным, т.е. конструкция object instanceof Class
    должна работать [^]
  2. Реализовать простую возможность вызова конструктора [^] или любого метода
    базового класса [^]
  3. Простота в использовании, стройность и лёгкость кода [^]
  4. По возможности, облегчить задачу современным IDE (в частности Spket) для корректного функционирования code complete по созданным объектам [^]
Итак, создадим базовый класс:

// Пишем конструктор базового класса
/**
 * Некий абстрактный человек, имеющий имя и
 * умеющий выполнять некоторые повседневные действия
 * @constructor Person
 * @param name {String} Имя человека
 */
var Person = function (name) {
    // Инициализируем public свойство name
    this.name = name;
    // Локальные переменные определённые внутри конструктора
    // можно использовать, при необходимости, как private члены класса
    // но только через специальные public методы (getter и/или setter),
    // определённые здесь же - в конструкторе
    //
    // var somePrivateVariable = {};
    // this._getSomePrivateVariable = function() {
    //     return somePrivateVariable;
    // }
}
// Описываем члены базового класса внутри wrapper-функции
// и сохраняем ссылку на неё в public static свойство $class
// для дальнейшего использования
// ПРИМЕЧАНИЕ:
//    Естественно, wrapper-функция может быть и анонимной.
//    В данном примере имя (совпадающее с именем класса) используется
//    только для того, чтобы получить более дружественный code complete в IDE

/**
 * @class Person
 */
Person.$class = function Person() {
    // Локальные переменные определённые в этой области
    // будут что-то вроде private static членов класса
    var $className = 'Person';
    // Описываем public свойства класса
    /**
    * Имя человека
    * @type {String}
    */
    this.name = '';
    // Описываем public методы класса
    /**
    * Возвращает список ежедневных действий человека
    * @returns {String}
    */
    this.performEverydayActions = function () {
        return '- Спит, ест, развлекается [as a ' + $className + ']';
    }
    /**
    * Возвращает имя человека
    * @returns {String}
    */
    this.getName = function () {
        return this.name;
    }
    this.toString = function () {
        return this.name;
    }
}
// "Компилируем" базовый класс, создавая прототип из wrapper-функции
Person.prototype = new Person.$class();

Далее, создадим производный класс:

// Пишем конструктор производного класса
/**
 * Некий абстрактный работник, имеющий имя (как и любой человек) и
 * умеющий выполнять какую-либо работу
 * @constructor Employee
 * @extends Person
 * @param name {String} Имя работника
 * @param job {String} Функция работника (или работа, которую он умеет выполнять)
 */
var Employee = function (name, job) {
    // Вызываем конструктор базового класса Person
    // (через ссылку $super, которую установим ниже),
    // чтобы проинициализировать имя при создании
    // объекта производного класса (Employee)
    arguments.callee.$super.call(this, name);
    // Инициализируем public свойство job
    this.job = job;
}
// Сохраняем ссылку на базовый класс для дальнейшего использования
Employee.$super = Person;
// Описываем члены производного класса внутри wrapper-функции
// и сохраняем ссылку на неё для дальнейшего использования
/**
 * @class Employee
 * @extends Person
 * @param $super {Object} Прототип базового класса
 */
Employee.$class = function Employee($super) {
    // Внутрь wrapper-функция производного класса будем передавать
    // в качестве агрумента ссылку на прототип базового класса.
    // Это существенно облегчит обращение к методам базового класса (см. ниже)

    var $className = 'Employee';

    // Описываем public свойства класса
    /**
     * Функция работника
     * @type {String}
     */
    this.job = '';
    
    // Описываем public методы класса
    /**
     * Возвращает функцию работника
     * @returns {String}
     */
    this.getJob = function () {
        return this.job;
    }
    // Переопределяем метод базового класса
    /**
     * Возвращает список ежедневных действий человека, включая его рабочие функции
     * @returns {String}
     */
    this.performEverydayActions = function () {
        // Вызываем метод базового класса
        return $super.performEverydayActions.call(this) +
                '\n- ' + this.job + ' [as an ' + $className + ']';
    }
}
// Устанавливаем наследование производного класса
// от базового класса через прототипы
Employee.$class.prototype = Employee.$super.prototype;
// "Компилируем" класс, создавая прототип из wrapper-функции
// и передавая в качестве аргумента прототип базового класса
Employee.prototype = new Employee.$class(Employee.$super.prototype);

Теперь, создадим класс производный от производного (именно на этом шаге многие попытки реализовать классовое наследование терпели неудачу):

// Пишем конструктор ещё одного производного класса
/**
 * Программист, который может писать код
 * @constructor Developer
 * @extends Employee
 * @param {String} name Имя
 */
var Developer = function (name) {
    // Вызываем конструктор базового класса (Employee)
    arguments.callee.$super.call(this, name, 'Пишет код');
}

// Т.к. класс Developer не имеет собственных членов, то wrapper - пустая функция 
/**
 * @class Developer
 * @extends Employee
 */
Developer.$class = function () {};
// Сохраняем ссылку на базовый класс
Developer.$super = Employee;
// Устанавливаем наследование
Developer.$class.prototype = Developer.$super.prototype;
// "Компилируем" класс
Developer.prototype = new Developer.$class(Developer.$super.prototype);

Ещё немного усложним - создадим третий производный класс:

// Пишем конструктор последнего производного класса
/**
 * Ведущий инженер-программист, который ещё и командой разработчиков управляет
 * @constructor LeadDeveloper
 * @extends Developer
 * @param {String} name Имя
 * @param {Developer[]} team Команда программистов
 */
var LeadDeveloper = function(name, team) {
    // Вызываем конструктор базового класса (Developer)
    arguments.callee.$super.call(this, name);
    // Инициализируем public свойство team
    this.team = team;
}
// Сохраняем ссылку на базовый класс
LeadDeveloper.$super = Developer;
// Описываем члены класса
/**
 * @class LeadDeveloper
 * @extends Developer
 * @param $super {Object} Прототип базового класса
 */
LeadDeveloper.$class = function LeadDeveloper($super) {

    var $className = 'LeadDeveloper';

    // Описываем public свойства класса
    /**
     * Команда программистов, которой руководит данный Lead
     * @type {Developer[]}
     */
    this.team = [];
    
    // Описываем public методы класса
    /**
     * Возвращает подчиненных данного Lead-а
     * @returns {Developer[]}
     */
    this.getTeam = function () {
        return this.team;
    }
    // Переопределяем метод базового класса
    /**
     * Возвращает список ежедневных действий человека, включая его рабочие функции
     * @returns {String}
     */
    this.performEverydayActions = function () {
        // Вызываем метод базового класса
        return $super.performEverydayActions.call(this) +
                '\n- Управляет командой: [as a ' +
                $className + ']\n    ' + this.team.join('\n    ');
    }
}
// Устанавливаем наследование
LeadDeveloper.$class.prototype = LeadDeveloper.$super.prototype;
// "Компилируем" класс
LeadDeveloper.prototype = new LeadDeveloper.$class(LeadDeveloper.$super.prototype);

Теперь проверим, что получилось:

var ivanov = new Person('Иванов Василий');
alert(ivanov.getName() + ':\n' + ivanov.performEverydayActions());
// Иванов Василий:
// - Спит, ест, развлекается [as a Person]

var petrova = new Employee('Петрова Мария','Доит коров');
alert(petrova.getName() + ':\n' + petrova.performEverydayActions());
// Петрова Мария:
// - Спит, ест, развлекается [as a Person]
// - Доит коров [as an Employee]

var sidorov = new Developer('Сидоров Алексей');
var pupkin = new Developer('Пупкин Иван');
var shishkin = new Developer('Шишкин Пётр');
alert(sidorov.getName() + ':\n' + sidorov.performEverydayActions());
// Сидоров Алексей:
// - Спит, ест, развлекается [as a Person]
// - Пишет код [as an Employee]

var myshkin = new LeadDeveloper('Мышкин Фёдор', [sidorov, pupkin, shishkin]);
alert(myshkin.getName() + ':\n' + myshkin.performEverydayActions());
// Мышкин Фёдор:
// - Спит, ест, развлекается [as a Person]
// - Пишет код [as an Employee]
// - Управляет командой: [as a LeadDeveloper]
//     Сидоров Алексей
//     Пупкин Иван
//     Шишкин Пётр 

alert(myshkin instanceof Person);
// true 
alert(myshkin instanceof Employee);
// true 
alert(myshkin instanceof Developer);
// true 
alert(myshkin instanceof LeadDeveloper);
// true 

[Весь предыдущий код одним файлом: classes-start.js]



Всё отлично работает. Code complete в Spket без проблем выдаёт список свойств и методов объекта myshkin, причем по каждому из них указан класс, который их содержит:


Осталось сделать красивую "обёртку":

Function.prototype.$extends = function ($super) { 
    this.$super = $super;
    return this;
}
Function.prototype.$class = function ($class) {
    if ($class == null) {
        $class = function () {};
    }
    this.$class = $class;
    if (this.$super != null) this.$class.prototype = this.$super.prototype;
    this.prototype = new $class(this.$super!=null ? this.$super.prototype : null);
    return this;
}

И "упаковать" (для примера, только два последних класса, чтобы не повторять опять гору кода):

var Developer = function (name) {

    arguments.callee.$super.call(this, name, 'Пишет код');

}.$extends( Employee ).$class();


var LeadDeveloper = function(name, team) {

    arguments.callee.$super.call(this, name);
    this.team = team;

}.$extends( Developer ).$class( function ($super) {

    var $className = 'LeadDeveloper';

    this.team = [];
    
    this.getTeam = function () {
        return this.team;
    }
    this.performEverydayActions = function () {
        return $super.performEverydayActions.call(this) +
                '\n- Управляет командой: [as a ' +
                $className + ']\n    ' + this.team.join('\n    ');
    }

});

[Финальная реализация одним файлом: classes-final.js]

К сожалению, данная конструкция уже не по зубам IDE и code complete в финальной варианте всё-таки работать не будет. Однако, есть некоторые хитрости, которые позволят извлечь максимум пользы из всего вышеизложенного на различных фазах девелопмента.

Но об этом как-нибудь в другой раз...


Ссылки по теме: