Добрый день.
Аддоны к браузерам вряд ли пригодятся в автоматизации тестирования web-систем, но при ручном тестировании они могут оказаться полезны. К примеру, можно заполнять элементы на выбранной странице, исходя из своих условий и входных данных. Ниже рассмотрено создание такого аддона для Firefox и Chrome без претензий на красоту кода.
Задача: разработать аддон для Firefox и расширение для Chrome со следующей функциональностью:
1. В тулбаре появляется кнопка (иконка).
2. При нажатии на эту кнопку анализируется URL активной страницы (вкладки). Если URL – один из заранее заданных URLs, то при нажатии на кнопку тулбара скрипт берет пару “пользователь-пароль” из опций в зависимости от URL и заполняет поля ввода логина и пароля на странице. Далее скрипт нажимает кнопку логина.
Начнем с Firefox.
Как вы, возможно, знаете, “старые” аддоны прекратят свою работу в Firefox 57, поэтому сейчас самое время перевести свои аддоны на механизм WebExtensions. Одно из основных достоинств WebExtensions – кросс-браузерность. Аддоны, написанные для Firefox, с небольшими изменениями можно запускать в Chrome. Вместе с тем, порог входа в разработку аддонов стал, на мой взгляд, несколько выше. Если раньше всю функциональность аддона можно было реализовать в одном js-скрипте и json-конфиге, то сейчас нужно создавать отдельные сущности для управления опциями аддона, размещением кнопки на тулбаре и обработкой клика на кнопку, действиями на странице плюс организовать взаимодействие между этими сущностями.
Аддон представляет собой структуру папок и файлов:
customlogin\content customlogin\content\login.js customlogin\icons customlogin\icons\button-1.png customlogin\index.js customlogin\manifest.json customlogin\options customlogin\options\options.html customlogin\options\options.js
Манифест (manifest.json):
{ "manifest_version": 2, "name": "Custom Login", "version": "3.0", "description": "Login to Cursor, Admin and Net interfaces", "icons": { "48": "icons/button-1.png" }, "permissions": [ "activeTab", "storage", "tabs" ], "browser_action": { "default_icon": "icons/button-1.png", "default_title": "Custom Login" }, "options_ui": { "page": "/options/options.html" }, "background": { "scripts": ["index.js"] } }
Html-страница для опций аддона (options.html):
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <form> <h3>Credentials for Cursor interface:</h3> <label>cursor User:   <input type="text" id="cursoruser" ></label><br> <label>cursor User Password:    <input type="text" id="cursoruserpassword" ></label><br><br> <h3>Credentials for Admin and Net interfaces:</h3> <label>Admin User:    <input type="text" id="adminuser" ></label><br> <label>Admin User Password:    <input type="text" id="adminuserpassword" ></label><br><br> <button type="submit">Save Changes</button> </form> <script src="options.js"></script> </body> </html>
Скрипт options.js:
function saveOptions(e) { e.preventDefault(); browser.storage.local.set({ cursoruser: document.querySelector("#cursoruser").value, cursoruserpassword: document.querySelector("#cursoruserpassword").value, adminuser: document.querySelector("#adminuser").value, adminuserpassword: document.querySelector("#adminuserpassword").value }); } function restoreOptions() { function setCurrentChoice(result) { document.querySelector("#cursoruser").value = result.cursoruser || "CURSORUser"; document.querySelector("#cursoruserpassword").value = result.cursoruserpassword || "CURSORUserPassword"; document.querySelector("#adminuser").value = result.adminuser || "AdminUser"; document.querySelector("#adminuserpassword").value = result.adminuserpassword || "AdminUserPassword"; } function onError(error) { console.log(`Error: ${error}`); } var gettingCURSORUser = browser.storage.local.get("cursoruser"); gettingCURSORUser.then(setCurrentChoice, onError); var gettingCURSORUserPassword = browser.storage.local.get("cursoruserpassword"); gettingCURSORUserPassword.then(setCurrentChoice, onError); var gettingAdminUser = browser.storage.local.get("adminuser"); gettingAdminUser.then(setCurrentChoice, onError); var gettingAdminUserPassword = browser.storage.local.get("adminuserpassword"); gettingAdminUserPassword.then(setCurrentChoice, onError); } document.addEventListener("DOMContentLoaded", restoreOptions); document.querySelector("form").addEventListener("submit", saveOptions);
Скрипт index.js (так называемый background script, ответственный за обработку клика кнопки на тулбаре, анализ URL, получение пары “пользователь-логин” из опций и передаче этих значений в content script):
function onError(e) { console.error(e); } function handleClick(storedSettings) { function handleURL(tabs) { for (let tab of tabs) { URL = tab.url; } var CURSORRegexp = /https.+common.+\/cursor/; var AdminRegexp = /https.+common.+\/admin/; var NetRegexp = /https.+common.+\/net/; var CURSORResult = CURSORRegexp.exec(URL); var AdminResult = AdminRegexp.exec(URL); var NetResult = NetRegexp.exec(URL); if (CURSORResult) { loginUserName = cursorUser; loginPassword = cursorUserPwd; } else if (AdminResult || NetResult) { loginUserName = adminUser; loginPassword = adminUserPwd; } if (CURSORResult || AdminResult || NetResult) { browser.tabs.executeScript({ file: "content/login.js" }).then(messageContent).catch(onError) } function messageContent() { var gettingActiveTab = browser.tabs.query({active: true, currentWindow: true}); gettingActiveTab.then((tabs) => { browser.tabs.sendMessage(tabs[0].id, {loginUserName: loginUserName, loginPassword: loginPassword}); }); } } // handleURL var loginUserName, loginPassword; const cursorUser = storedSettings.cursoruser; const cursorUserPwd = storedSettings.cursoruserpassword; const adminUser = storedSettings.adminuser; const adminUserPwd = storedSettings.adminuserpassword; var querying = browser.tabs.query({currentWindow: true, active: true}); querying.then(handleURL, onError); } // handleClick browser.browserAction.onClicked.addListener(() => { const gettingStoredSettings = browser.storage.local.get(); gettingStoredSettings.then(handleClick, onError); });
Скрипт login.js (так называемый content script, ответственный за действия на странице):
function justDoTheJob(request, sender, sendResponse) { var doc = window.content.document; doc.getElementById("btnLogin").disabled = false; doc.getElementById("loginUserName").value = request.loginUserName; doc.getElementById("loginPassword").value = request.loginPassword; setTimeout(function() { doc.getElementById("btnLogin").click(); }, 1000) } browser.runtime.onMessage.addListener(justDoTheJob);
Update (23.03.2018): Firefox 59 приготовил сюрприз: код, успешно работавший c WebExtension API в Firefox 57, вдруг перестал работать в Firefox 59. В сторону: эх, люблю Open Source… В итоге, content script нужно переписать:
function justDoTheJob(request, sender, sendResponse) { // var doc = window.content.document; // Это уже не нужно. Точнее, это не работает. Нужно использовать предопределенный объект document. document.getElementById("btnLogin").disabled = false; document.getElementById("loginUserName").value = request.loginUserName; document.getElementById("loginPassword").value = request.loginPassword; setTimeout(function() { document.getElementById("btnLogin").click(); }, 1000) }
После создания этой структуры файлов нужно собрать плагин и подписать его ключом разработчика. Для сборки плагина понадобится утилита web-ext (npm install –global web-ext) и команда web-ext build. Для подписи плагина нужно предварительно зарегистрироваться в качестве разработчика на mozilla.org и получить свой ключ (имя пользователя) и секрет. Подписать аддон можно так:
web-ext sign --api-key user:<USER_NAME> --api-secret <SECRET>
Результатом будет подписанный xpi-файл.
После этого можно ставить аддон на выбранные машины. Для этого надо открыть xpi-файл в Firefox через File -> Open File и нажать Add. После этого на тулбаре появится кнопка. Далее, нужно зайти в Tools -> Add-ons -> Extensions, выбрать плагин, нажать Options и задать пары “пользователь-пароль”.
Расширение для Chrome:
Структура папок и файлов такая же как и для аддона Firefox. Файл manifest.json идентичен манифесту для Firefox, рассмотренному выше.
Html-страница для опций плагина (options.html):
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> </head> <body> <h3>Credentials for cursor interface:</h3> <label>CURSOR User:   <input type="text" id="cursoruser" ></label><br> <label>CURSOR User Password:    <input type="text" id="cursoruserpassword" ></label><br><br> <h3>Credentials for admin and net interfaces:</h3> <label>Admin User:    <input type="text" id="adminuser" ></label><br> <label>Admin User Password:    <input type="text" id="adminuserpassword" ></label><br><br> <div id="status"></div> <button id="save">Save</button> <script src="options.js"></script> </body> </html>
Скрипт options.js:
// Saves options to chrome.storage function save_options() { var cursoruser = document.getElementById('cursoruser').value; var cursoruserpassword = document.getElementById('cursoruserpassword').value; var adminuser = document.getElementById('adminuser').value; var adminuserpassword = document.getElementById('adminuserpassword').value; chrome.storage.sync.set({ cursoruser: cursoruser, cursoruserpassword: cursoruserpassword, adminuser: adminuser, adminuserpassword: adminuserpassword }, function() { // Update status to let user know options were saved. var status = document.getElementById('status'); status.textContent = 'Options saved.'; setTimeout(function() { status.textContent = ''; }, 750); }); } // Restores options using the preferences stored in chrome.storage. function restore_options() { chrome.storage.sync.get({ cursoruser: 'CURSORUser', cursoruserpassword: 'CURSORUserPassword', adminuser: 'AdminUser', adminuserpassword: 'AdminUserPassword' }, function(items) { document.getElementById('cursoruser').value = items.cursoruser; document.getElementById('cursoruserpassword').value = items.cursoruserpassword; document.getElementById('adminuser').value = items.adminuser; document.getElementById('adminuserpassword').value = items.adminuserpassword; }); } document.addEventListener('DOMContentLoaded', restore_options); document.getElementById('save').addEventListener('click', save_options);
Скрипт index.js (так называемый background script, ответственный за обработку клика кнопки на тулбаре, анализ URL, получение пары “пользователь-логин” из опций и передаче этих значений в content script). Если в Firefox в нем были задействованы Promises, то в Chrome нужно использовать callbacks:
chrome.browserAction.onClicked.addListener(handleClick); function handleClick() { var cursoruser = "default"; var cursoruserpassword = ""; var adminuser = ""; var adminuserpassword = ""; chrome.storage.sync.get('cursoruser', function (result) { cursoruser = result.cursoruser; }); chrome.storage.sync.get('cursoruserpassword', function (result) { cursoruserpassword = result.cursoruserpassword; }); chrome.storage.sync.get('adminuser', function (result) { adminuser = result.adminuser; }); chrome.storage.sync.get('adminuserpassword', function (result) { adminuserpassword = result.adminuserpassword; }); chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) { var tab = tabs[0]; URL = tab.url; var CURSORRegexp = /https.+common.+\/cursor/; var AdminRegexp = /https.+common.+\/admin/; var NetRegexp = /https.+common.+\/net/; var CURSORResult = CURSORRegexp.exec(URL); var AdminResult = AdminRegexp.exec(URL); var NetResult = NetRegexp.exec(URL); if (CURSORResult) { loginUserName = cursoruser; loginPassword = cursoruserpassword; } else if (AdminResult || NetResult) { loginUserName = adminuser; loginPassword = adminuserpassword; } if (CURSORResult || AdminResult || NetResult) { chrome.tabs.executeScript({ file: "content/login.js" }, messageContent); function messageContent() { chrome.tabs.sendMessage(tab.id, {loginUserName: loginUserName, loginPassword: loginPassword}); } } }); // chrome.tabs.query({ active: true, currentWindow: true }, function(tabs) { } // handleClick
Скрипт login.js:
function justDoTheJob(request, sender, sendResponse) { document.getElementById("btnLogin").disabled = false; document.getElementById("loginUserName").value = request.loginUserName; document.getElementById("loginPassword").value = request.loginPassword; setTimeout(function() { document.getElementById("btnLogin").click(); }, 1000) } chrome.runtime.onMessage.addListener(justDoTheJob);
Здесь подробно описано, как запаковать расширение в Chrome. Опубликовать расширение в Chrome Web Store – дело довольно муторное, но его можно загружать напрямую в браузере, перетащив *.crx на страницу chrome://extensions/. При этом появится предупреждение о том, что расширение может причинить вред вашему компьютеру. Если вы доверяете себе, то предупреждение можно проигнорировать.
Всего доброго.
Что такое качество программного обеспечения и как его улучшить: теория и практика, задачи и решения, подводные камни и обходные пути.
Pingback : Самодельные аддоны к браузерам на службе тестировщика — Блоги экспертов | September 22, 2017
[…] Источник […]
Pingback : Сентябрьская лента: лучшее за месяц — Блоги экспертов | October 2, 2017
[…] аддоны к браузерам на службе […]