Разделы портала

Онлайн-тренинги

.
Самодельные аддоны к браузерам на службе тестировщика
30.05.2018 14:13

Оригинальная публикация

Аддоны к браузерам вряд ли пригодятся в автоматизации тестирования 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:&nbsp&nbsp<input type="text" id="cursoruser" ></label><br>
        <label>cursor User Password:&nbsp&nbsp<input type="text" id="cursoruserpassword" ></label><br><br>
	<h3>Credentials for Admin and Net interfaces:</h3>
        <label>Admin User:&nbsp&nbsp<input type="text" id="adminuser" ></label><br>
        <label>Admin User Password:&nbsp&nbsp<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:&nbsp&nbsp<input type="text" id="cursoruser" ></label><br>
	<label>CURSOR User Password:&nbsp&nbsp<input type="text" id="cursoruserpassword" ></label><br><br>
	<h3>Credentials for admin and net interfaces:</h3>
	<label>Admin User:&nbsp&nbsp<input type="text" id="adminuser" ></label><br>
	<label>Admin User Password:&nbsp&nbsp<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/. При этом появится предупреждение о том, что расширение может причинить вред вашему компьютеру. Если вы доверяете себе, то предупреждение можно проигнорировать.

Обсудить в форуме