Библиотека сайта rus-linux.net
Проект Selenium WebDriver
Автор:Simon Stewart
Перевод: Н.Ромоданов
16.6. Дистанционный драйвер и, в частности, драйвер Firefox
Дистанционный WebDriver изначально представлял собой хорошо известный механизм RPC. С тех пор он превратился в один из ключевых механизмов, которые мы используем с целью уменьшить расходы на поддержку WebDriver за счет предоставления единообразного интерфейса для привязки к коду языковых сборок. Даже, несмотря на то, что мы перенесли в драйвер столько логики, сколько смогли изъять из привязок к языкам, если бы нам потребовалось чтобы каждый драйвер взаимодействовал через уникальный протокол, у нас до сих пор было бы много кода, который повторялся бы во всех языковых привязках.
Дистанционный протокол WebDriver используется везде, где нам нужно было взаимодействовать с экземпляром браузера, работающим в другом процессе. При создании этого протокола нужно было учесть ряд проблем. Большинство из них были техническими, но, поскольку это открытый код, также рассматривался и социальный аспект.
Любой механизм RPC состоит из двух частей: транспортного механизма и механизма кодирования. Мы знали, что, несмотря на то, что мы реализовывали дистанционный протокол WebDriver, мы для тех языков, которые мы хотели использовать в качестве клиентских, должны были поддерживать обе части. Первая итерация проекта была разработана как часть драйвера Firefox.
Mozilla и, следовательно, Firefox всегда рассматривается их разработчиками как мультиплатформенное приложение. Чтобы помочь разработчикам, Mozilla по примеру COM от Microsoft создала фреймворк XPCOM (кросс-платформенный COM), который позволяет собирать компоненты и соединять их воедино. Интерфейс XPCOM описывается с помощью языка IDL и есть привязки для языков C и JavaScript и для других языков. Поскольку XPCOM используется для создания Firefox, и т. к. XPCOM является привязкой к языку Javascript, в расширениях Firefox можно использовать объекты XPCOM.
В обычной Win32 COM допускаются интерфейсы, к которым можно обращаться дистанционно. Были планы добавить к XPCOM такую же самую возможность и, чтобы способствовать этому, Дарин Фишер (Darin Fisher) добавил реализацию XPCOM ServerSocket. Хотя планы для D-XPCOM никогда не были осуществлены, все еще есть эта рудиментарная инфраструктура, похожая на аппендикс. Мы воспользовались этим для того, чтобы внутри пользовательского расширения Firefox, содержащего всю логику управления браузером Firefox, создать очень простой сервер. Первоначально используемый протокол был текстовый и строчно-ориентирована, причем для кодирования строк использовалась кодировка UTF-2. Каждый запрос или ответ начался с номера, указывающий, сколько должно быть получено символов новой строки прежде, чем можно было принять решение о том, что запрос или ответ был полностью отправлен. Важно отметить, что эта схема была проста для реализации на языке Javascript, поскольку в SeaMonkey (на то момент движок Javascript для Firefox) для внутреннего хранения строк Javascript использовались 16-битные целые числа без знака.
Хотя занятия с пользовательскими протоколами кодирования поверх низкоуровневых сокетов представляют собой интересный способ скоротать время, у них есть ряд недостатков. Для пользовательского протокола нет широко распространенных библиотек, так что для каждого языка, который мы хотели поддерживать, нужно было все реализовывать с самого нуля. Эта необходимость реализовывать большое количество кода менее приятна, чем щедрость разработчиков открытого кода, которые могут поучаствовать в разработке новых языковых привязок. Кроме того, хотя когда мы рассылали только текстовые данные, строчно-ориентированный протокол был замечателен, с ним возникли проблемы, когда нам потребовалось рассылать изображения (например, скриншоты).
Очень быстро стало совершенно очевидно, что на практике этот исходный механизм RPC совершенно не годится. К счастью, был хорошо известен транспортный механизм, который широко распространен, поддерживался почти на всех языках и который мог бы позволить нам делать все, что мы хотели: HTTP.
После того, как только мы решили в качестве транспортного механизма использовать HTTP, следующий выбор, который нужно было сделать, это решить, использовать ли механизм одного единого обращения (а-ля SOAP) или нескольких обращений (в стиле REST). В исходном протоколе Selenese использовался механизм единого обращения, в котором в строке запроса были закодированы команды и аргументы. Хотя этот подход работал хорошо, не «чувствовалось», что он правильный: мы предвидели, что нам для просмотра состояния сервера потребуется в браузере дистанционно подключаться к экземпляру WebDriver. Мы ограничились тем, что выбрали подход, который мы назвали «похожим на REST» («REST-ish»): множественные адреса URL, использующие операции HTTP, которые делают их его осмысленными, но нарушают ряд ограничений, требующихся для истинных систем вида RESTful, в частности связанные с определением состояния и кэширования, главным образом постольку, поскольку есть только одно определение состояния приложения с тем, что в нем был смысл.
Хотя HTTP позволяет легко поддерживать множество вариантов кодирования данных, использующих соглашение о типе содержимого, мы решили, что нам нужен канонический вариант, с которым могли бы работать все реализации дистанционного протокола WebDriver. Было несколько очевидных вариантов: HTML, XML или JSON. Мы быстро исключили XML: хотя это вполне разумный формат данных и почти в каждом языке есть библиотеки, которые его поддерживают, у меня сложилось впечатление, что сообществу, работающему с открытым кодом, с ним работать не нравится. Кроме того, что хотя возвращаемые данные должны иметь обобщенный «вид», исключительно просто добавлять дополнительные поля [3]. Хотя такие расширения можно смоделировать с помощью пространства имен XML, с этого начинается постепенное усложнение клиентского кода: то, чего я стремился избежать. Вариант использования XML был отвергнут. HTML был не очень хорошим вариантом, поскольку нам требовалось определять наши собственные форматы данных, и хотя для этого можно было использовать встроенные механизмы микроформатов, это выглядело как использование молотка для разбивания яиц.
В качестве окончательной возможности был рассмотрен JavaScript Object Notation (JSON). Браузеры могут преобразовывать строку в объект либо при помощи прямого обращения к функции eval, либо на более современных браузерах с помощью примитивов, созданных для безопасного преобразования объектов языка Javascript в строку и наоборот без побочных эффектов. С практической точки зрения, JSON является популярным форматом данных, причем библиотеки для его обработки доступны почти для каждого языка и он нравится всем начинающим разработчикам. Легкий выбор.
Поэтому во второй итерации дистанционного протокола WebDriver в качестве транспортного механизма был использован HTTP, а в качестве схемы кодирования, используемой по умолчанию, был выбран JSON в кодировке UTF-8. UTF-8 был выбрана в качестве кодировки, используемой по умолчанию с тем, чтобы клиенты могли легко писать на языках с ограниченной поддержкой Unicode, т. к. в UTF-8 есть обратная совместимость с ASCII. Команды отправлялись на сервер по URL, в котором определялось, какая команда посылается, а закодированные параметры команды указывались в массиве.
Например, вызов WebDriver.get("http://www.example.com") отображался в запрос POST в адресе URL, в котором закодирован идентификатор сессии, заканчивающийся на "/url", а массив параметров выглядит, например, как{[}'http://www.example.com'{]}. Возвращаемый результат имел несколько более сложную структуру и содержал информацию о месте, куда происходит возврат, и код ошибки. Это длилось недолго - до третьей итерации дистанционного протокола, в которой массив параметров, используемый в запросе, был заменен словарем именованных параметров. Преимущество такого подхода было в том, что запросы, используемые при отладке, были существенно проще, и клиент теперь не мог по ошибке неправильно указать порядок следования параметров, что сделало систему в целом более надежной. Естественно, было решено в тех случаях, где это было делать наиболее уместно, использовать обычные коды ошибок HTTP для индикации определенных возвращаемых значений и ответов; например, если пользователь пытается вызвать URL, к котором нечего не отображено, или когда мы хотим указать, что «ответ не содержит данные».
Дистанционный протокол WebDriver имеет два уровня обработки ошибок: один - для неправильных запросов и один - для неверных команд. Например, неправильный запрос ресурса, которого нет на сервере, или, возможно, запрос действия, которое непонятно ресурсу (например, отправка команды DELETE — УДАЛИТЬ ресурсу, используемому для работы с адресом URL текущей страницы). В подобных случаях отсылается обычный ответ HTTP 4xx. Для команд, которые не удалось выполнить, возвращаемый код ошибки устанавливается равным 500 («Internal Server Error» — «Внутренняя ошибка сервера»), а возвращаемые данные содержат более подробные свидения о том, что пошло не так.
Когда с сервера посылаются данные, содержащие ответ, они имеют вид объекта JSON:
Ключ | Описание |
sessionId | Скрытый регулятор, используемый сервером для того, чтобы определить, куда направлять команды конкретной сессии. |
status | Цифровой код состояния, подытоживающий результат работы команды. Ненулевое значение указывает, что команда не была выполнена. |
value | Значение - ответ JSON. |
Например, ответ может быть следующим:
{ sessionId: 'BD204170-1A52-49C2-A6F8-872D127E7AE8', status: 7, value: 'Unable to locate element with id: foo' }
Видно, что в ответе закодирован код состояния с ненулевым значения, указывающим, что произошло что-то ужасно непоправимое. Драйвер IE был первым, где использовались коды состояния, и это отразилось на значениях, используемых в сетевом протоколе. Поскольку все коды ошибок во всех драйверах согласованы между собой, можно код, обрабатывающий ошибки, использовать во всех драйверах, написанных на определенном языке, что упрощает работу разработчиков на клиентской стороне.
Сервер дистанционной обработки Remote WebDriver Server является простым сервлетом на языке Java, который работает как мультиплексор, выполняющий перенаправление всех команд, которые он получает, соответствующему экземпляру WebDriver. Это то, что мог бы написать аспирант второго года обучения. В драйвере Firefox также реализован протокол дистанционного доступа WebDriver, и существенно более интересна архитектура этого драйвера, так что давайте проследим за запросом, который поступает из вызова в языковой привязке в работающий в фоновом режиме драйвер, который затем возвращает пользователю ответ.
Предположим, что мы используем язык Java и что элементом «element» является экземпляр WebElement, тогда все начинается следующим образом:
element.getAttribute(&lquot;row&rquot;);
Если заглянуть внутрь, то в элементе есть идентификатор «id», который на серверной стороне используется для идентификации элемента, о котором идет речь. В рамках этого обсуждения мы будем считать, что он имеет значение «некоторое_скрытое_id». Оно кодируется в Java-объекте Command с помощью класса Map, хранящего (на этот раз именованные) параметрыid - для элемента ID и name - для имени запрашиваемого атрибута.
Беглый взгляд в таблицу указывает, что правильным адресом URL будет следующий:
/session/:sessionId/element/:id/attribute/:name
Любая часть адреса URL, которая начинается с двоеточия, считается переменной, для которой необходимо подставить значение. У нас уже заданы параметры id и name , а идентификатор сессии sessionId является еще одним скрытым регулятором, который используется при маршрутизации в случае, если сервер одновременно обрабатывает более одной сессии (что не может делать драйвер Firefox). Поэтому такой адрес URL обычно раскрывается, например, следующим образом:
http://localhost:7055/hub/session/XXX/element/some_opaque_id/attribute/row
В качестве отступления: протокол дистанционного доступа для WebDriver был первоначально разработан в то же самое время, когда в качестве чернового варианта RFC были предложены шаблоны URL Templates. Обе наши схемы - спецификация адресов URL и шаблоны URL Templates - позволяют в адресе URL использовать переменные (и, следовательно, вычислять значения). Хотя шаблоны URL Templates предлагались в то же время, нам, к сожалению, стало о них известно довольно поздно, и поэтому при описании сетевого протокола они не использовались.
Поскольку метод мы, который мы выполняем, идемпотентен [4], правильным методом HTTP, который следует использовать, будет метод GET. Мы все делегировали в библиотеку Java, которая может обрабатывать запросы HTTP (клиент Apache HTTP), направляемые в сервер.
Рис.16.4: Общая схема архитектуры драйвера Firefox
Драйвер Firefox реализован как расширение браузера Firefox; базовая архитектура драйвера показана на рис.16.4. Несколько необычно то, что имеется встроенный сервер HTTP. Хотя первоначально мы использовали тот сервер, который мы сделали самостоятельно, создание серверов HTTP в XPCOM не относилось к тем задачам, в которых мы компетентны, и когда появилась возможность, мы заменили его простейшим сервером HTTPD, написанным в рамках самого проекта Mozilla. Запросы поступают на сервер HTTPD и почти сразу же передаются объекту-диспетчеру dispatcher.
Диспетчер принимает запрос и перебирает известный ему список поддерживаемых адресов URL, пытаясь найти тот, который соответствует запросу. Это сравнение выполняется с учетом вставки значения переменной, которое выполнено на клиентской стороне. Как только будет найдено точное совпадение, в том числе и выполнение действия, будет сконструирован объект JSON, представляющий собой команду для запуска. В нашем случае это выглядит следующим образом:
{ 'name': 'getElementAttribute', 'sessionId': { 'value': 'XXX' }, 'parameters': { 'id': 'some_opaque_key', 'name': 'rows' } }
Затем это будет передано в виде строки JSON в пользовательскую компоненту XPCOM, написанную нами, которая называется командным процессором CommandProcessor. Ее код следующий:
var jsonResponseString = JSON.stringify(json); var callback = function(jsonResponseString) { var jsonResponse = JSON.parse(jsonResponseString); if (jsonResponse.status != ErrorCode.SUCCESS) { response.setStatus(Response.INTERNAL_ERROR); } response.setContentType('application/json'); response.setBody(jsonResponseString); response.commit(); }; // Dispatch the command. Components.classes['@googlecode.com/webdriver/command-processor;1']. getService(Components.interfaces.nsICommandProcessor). execute(jsonString, callback);
Здесь сравнительно много кода, но есть два ключевых момента. Во-первых, мы преобразовали объект, рассмотренный выше, в строку JSON. Во-вторых, мы передали в метод execute функцию обратного вызова, которая будет выполнена, когда будет отправлен ответ HTTP.
Метод execute котмандного процессора ищет «имя» чтобы определить, какую функцию нужно вызывать, что затем он и делает. Первый параметр передаваемый для этой функции, является объектом «respond» (так называется потому, что первоначально он просто использовался функцией для возврата ответа обратно к пользователю), в котором инкапсулированы не только те возможные значения, которые могут быть посланы, но также метод, который позволяет перенаправить ответ обратно пользователю, и механизмы, позволяющим узнать информацию о модели DOM. Вторым параметром является значение объекта parameters, который мы уже видели раньше (в данном случае, id и name). Преимущество этой схемы в том, что во всех функциях используется единообразный интерфейс, в котором отражена структура, используемая на клиентской стороне. Это значит, что на каждой стороне ментальные модели, используемые для рассмотрения кода, аналогичны. Ниже приведена реализация метода getAttribute, который вы уже видели в разделе 16.5:
FirefoxDriver.prototype.getElementAttribute = function(respond, parameters) { var element = Utils.getElementAt(parameters.id, respond.session.getDocument()); var attributeName = parameters.name; respond.value = webdriver.element.getAttribute(element, attributeName); respond.send(); };
Чтобы сделать ссылки на элемент согласованными, в первой строке просто выполняется поиск элемента, на который указывает скрытый идентификатор ID, находящийся в кэше. В драйвере Firefox, этот скрытый ID является просто UUID, а «кэш» является просто отображением. Метод getElementAt также проверяет, известен ли этот элемент и есть ли он в модели DOM. Если какая-либо из проверок оказалась неудачной, то ID удаляется из кэша (если это необходимо) и пользователю возвращается исключительное состояние.
Во второй строчке от конца используются атомы автоматизации браузера, рассмотренные ранее, которые на этот раз откомпилированы в виде монолитного скрипта и загружены как часть расширения.
В последней строке, вызывается метод send. Здесь делается простая проверка с тем, чтобы перед тем, как вызвать функцию обратного вызова, указанную в методе execute, удостовериться, что мы отправляем ответ только один раз. Ответ отправляется обратно пользователю в виде строки JSON, преобразуемой в объект, который выглядит следующим образом (при условии, что getAttribute возвращает «7», означающий, что элемент не найден):
{ 'value': '7', 'status': 0, 'sessionId': 'XXX' }
Затем клиент языка Java проверяет значение поля состояния. Если это значение не равно нулю, он, используя поле значения «value», которое помогает установить значение, посланное пользователю, преобразует цифровой код состояния в исключительное сотояние соответствующего типа. Если состояние равно нулю, то значение поля «value» возвращается пользователю.
Большая часть того, что происходит, понятно, но есть одна вещь, о которой проницательный читатель может спросить: почему диспетчер перед тем, как вызвать метод execute, преобразует объект, который у него есть, в строку?
Причина этого в том, что драйвер Firefox также поддерживает выполнение тестов, написанных на чистом языке Javascript. Обычно осуществлять такую поддержку чрезвычайно трудно: тесты выполняются в браузере в контексте песочницы безопасности Javascript, и, поэтому, не удается выполнять целый ряд действий, которые могли бы использоваться в тестах, например, переход между доменами или загрузка файлов на сервер. Однако расширение Firefox для WebDriver предоставляет возможность выхода из песочницы. Оно объявляет о своем присутствии при помощи добавления к элементу document свойства webdriver. Интерфейс Javascript API для WebDriver использует это свойство как индикатор того, что он может добавить к элементу document сериализованные объекты команд JSON как значение свойства command, включать событие webdriverCommand, а затем для того же самого элемента ожидать события webdriverResponse, которое сообщит о том, что свойство response было установлено.
При этом предполагается, что просмотр интернета с помощью копии Firefox с установленным расширением WebDriver — действительно плохая мысль, поскольку кому-то другому станет исключительно просто дистанционно управлять браузером.
За всем этим есть мессенджер DOM, ожидающий, когда webdriverCommand прочитает сериализованный объект JSON и вызовет в командном процессоре метод execute. На этот раз, обратный вызов будет просто установкой атрибута response для элемента document, а затем отработкой ожидаемого события webdriverResponse.
Продолжение статьи - Драйвер IE.