В последнем сообщении 2012 года хочу описать решение по созданию и использованию миниатюр для страниц инфопанелей.
В последнее время занимаюсь повышением usability текущего проекта.
Структура отчетности проекта - это набор информационных панелей. По одной для каждого отчета. Каждый отчет может содержать несколько страниц с различным представлением информации - плоская таблица, таблица среза, диаграмма и т.д.
Для удобства пользователей и снижения нагрузки каждая инфопанель содержит заглавную страницу, на которой расположены ссылки перехода на прочие страницы инфопанели их плюс описание.
И однажды я подумал, что неплохо бы "разбавить" текст ссылки перехода какой-нибудь графикой. Сначала это были просто одинаковые иконки, затем я стал подбирать иконки "по смыслу", и наконец пришел с мысли использовать сжатые скриншоты этих самых страниц (thumbnails).
Например, так теперь выглядит страница-содержание для одного из отчетов:
Итак, как достичь подобного.
Ничего особо сложного тут нет – используется любимый мною Selenium, который и создает скриншоты в момент выполнения регулярного регрессионного тестирования.
Ниже java-код для Selenium-теста, модифицированный для создания скриншотов.
package ru.servplus.tests; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.util.Properties; import org.junit.After; import org.junit.Before; import org.junit.Test; import com.thoughtworks.selenium.DefaultSelenium; import com.thoughtworks.selenium.SeleneseTestBase; import com.thoughtworks.selenium.Selenium; import java.awt.Image; import java.awt.image.BufferedImage; import javax.imageio.ImageIO; public class CheckAllDashboardPages extends SeleneseTestBase { private Selenium selenium; private Properties prop; private String serverHost; private int serverPort; private String browserType; private String browserUrl; private String nqUser; private String nqPassword; private int windowWidth; private int windowHeight; private String doScreenshots; private String screenshotsPath; private int thumbnailWidth; private int thumbnailHeight; @Before public void setUp() throws Exception { prop = new Properties(); prop.load(new FileInputStream("selenium.properties")); serverHost = prop.getProperty("serverHost"); serverPort = Integer.valueOf(prop.getProperty("serverPort")); browserType = prop.getProperty("browserType"); browserUrl = prop.getProperty("browserUrl"); nqUser = prop.getProperty("nqUser"); nqPassword = prop.getProperty("nqPassword"); windowWidth = Integer.valueOf(prop.getProperty("windowWidth")); windowHeight = Integer.valueOf(prop.getProperty("windowHeight")); doScreenshots = prop.getProperty("doScreenshots"); screenshotsPath = prop.getProperty("screenshotsPath"); thumbnailWidth = Integer.valueOf(prop.getProperty("thumbnailWidth")); thumbnailHeight = Integer.valueOf(prop.getProperty("thumbnailHeight")); selenium = new DefaultSelenium( serverHost, serverPort, browserType, browserUrl); selenium.start(); } @After public void tearDown() { selenium.stop(); } public String encodePath (String s) { StringBuilder str = new StringBuilder(); int i = s.length(); for (int j = 0; j < i; j++) { char c = s.charAt(j); switch (c) { case 34: // '"' case 35: // '#' case 37: // '%' case 42: // '*' case 46: // '.' case 58: // ':' case 60: // '<' case 62: // '>' case 63: // '?' case 124: // '|' str.append("_"); break; case 92: // '\\' if (++j == i) { return null; } c = s.charAt(j); switch (c) { case 42: // '*' case 47: // '/' case 63: // '?' case 92: // '\\' case 126: // '~' str.append("_"); break; default: return null; } default: str.append(c); break; } } return str.toString(); } public String getDashboardPath (int i) { StringBuilder script = new StringBuilder(); script.append("var div1 = window.document.getElementById('idCatalogItemsAccordion');"); script.append("var div2 = div1.getElementsByTagName('DIV')[0];"); script.append("var div3 = div2.getElementsByTagName('DIV')[1];"); script.append("var tab1 = div3.getElementsByTagName('TABLE')[0];"); script.append("var tbd1 = tab1.getElementsByTagName('TBODY')[0];"); script.append("var tr1 = tbd1.childNodes[" + i + "];"); script.append("var td1 = tr1.getElementsByTagName('TD')[0];"); script.append("var div4 = td1.getElementsByTagName('DIV')[0];"); script.append("var trows = div4.getElementsByTagName('TR');"); script.append("var tr2 = trows[trows.length-3];"); script.append("var td2 = tr2.getElementsByTagName('TD')[0];"); script.append("var a1 = td2.getElementsByTagName('A')[0];"); script.append("a1.listItem.itemInfo.path.toString();"); String path = selenium.getEval(script.toString()); return path; } @Test public void test() { int allErrorCount = 0; try { //входим в BI под админ. учетной записью (имеет смысл создать отдельную учетку-reader) //также крайне важно задать язык сессии, так как по англ. заголовкам элементов будет вестись поиск в тесте selenium.open("/analytics/saw.dll?catalog&nqUser=" + nqUser + "&nqPassword=" + nqPassword + "&lang=en"); //перейдем в режим "Расширенного поиска": будем искать все инфопанели в shared-папках selenium.open("/analytics/saw.dll?catalog&action=searchpanel&type=all#{\"action\":\"search\",\"location\":\"/shared\",\"mask\":\"*\",\"type\":\"dashboard\",\"favorite\":null}"); //распахнем окно на заданные ширину-высоту, либо сделаем окно полноэкранным if (windowWidth > 0 && windowHeight > 0) { selenium.getEval("window.resizeTo(" + String.valueOf(windowWidth) + ", " + String.valueOf(windowHeight) + "); window.moveTo(0,0);"); } else { selenium.windowMaximize(); } int dashboardCount = 0; //крайне важно дождаться появления указанных XPath-конструкцией элементов (они догружаются ajax'ом после того как загрузилась сама страница) selenium.waitForCondition("selenium.getXpathCount(\"//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr\") > 0", "30000"); //считываем кол-во строк, возвращенных страницей поиска инфопанелей dashboardCount = selenium.getXpathCount("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr").intValue(); //System.out.println("Dashboard count: "+dashboardCount); //определяем массив инфопанелей (путь + название) и массив страниц каждой инфопанели String[] dashboardPaths = new String[dashboardCount]; String[] dashboardPathsEncoded = new String[dashboardCount]; String[][] dashboardPages = new String[dashboardCount][]; //заполняем значениями эти массивы (считываем путь и название каждой инфопанели с помощью XPath-конструкций) for(int i=0; i<dashboardCount; i++){ //обработаем ситуацию, когда у инфопанели задано "Описание" //dashboardDescrRowsCount = selenium.getXpathCount("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr[" + (i+1) + "]/td/div/table/tbody/tr").intValue(); //dashboardPaths[i] = selenium.getText("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr[" + (i+1) + "]/td/div/table/tbody/tr[" + (dashboardDescrRowsCount-1) + "]/td/a") + // "/" + selenium.getText("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr[" + (i+1) + "]/td/div/table/tbody/tr[1]/td[2]/span[1]"); //dashboardPaths[i] = dashboardPaths[i].replace("/Shared Folders/", "/shared/").replace("/Dashboards/", "/_portal/"); dashboardPaths[i] = getDashboardPath(i); dashboardPathsEncoded[i] = dashboardPaths[i].replace("\\", "\\\\"); dashboardPathsEncoded[i] = dashboardPathsEncoded[i].replace("\"", "\\\""); //System.out.println("Dashboard "+i + " = " + dashboardPaths[i]); //System.out.println("Dashboard "+i + " = " + dashboardPathsEncoded[i]); } //далее будем считывать значения названий страниц каждой инфопанели int dashboardPageCount = 0; for(int i=0; i<dashboardCount; i++){ //не получилось реализовать считывание страниц внутри одного и того же окна браузера //возможно, из-за хитрого параметра "type" //сейчас реализовано таким образом, что список страниц каждой инфопанели открывается в новом окне //открываем в новом окне страницу со списком содержимого инфопанели selenium.openWindow("/analytics/saw.dll?catalog&action=searchpanel&type=all#{\"location\":\"" + dashboardPathsEncoded[i] + "\"}", dashboardPaths[i]); selenium.waitForPopUp(dashboardPaths[i], "30000"); //перемещаем фокус в новое окно selenium.selectWindow(dashboardPaths[i]); //на всякий случай ставим задержку - даем странице успеть отрисоваться try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //убеждаемся, что на странице появились данные о страницах инфопанели selenium.waitForCondition("selenium.getXpathCount(\"//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr\") > 0", "10000"); //считываем кол-во страниц текущей инфопанели dashboardPageCount = selenium.getXpathCount("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr").intValue(); //в цикле считываем наименования страниц инфопанели с помощью Xpath-конструкции String[] dashboardPageNames = new String[dashboardPageCount]; for(int j=0; j<dashboardPageCount; j++){ dashboardPageNames[j] = selenium.getText("//div[@id='idCatalogItemsAccordion']/div/div[2]/table/tbody/tr[" + (j+1) + "]/td/div/table/tbody/tr[1]/td[2]/span[1]"); //System.out.println(dashboardPageNames[j]); } //закрываем вспомогательное окно selenium.close(); //перемещаем фокус в главное окно браузера selenium.selectWindow("null"); dashboardPages[i] = dashboardPageNames; } int errorCount = 0; String errorText = null; String dashboardPage = null; String dashboardPageEncoded = null; FileInputStream fis = null; FileOutputStream fos = null; BufferedImage sourceImage = null; Image thumbnail = null; BufferedImage bufferedThumbnail = null; //в цикле по всем инфопанелям for(int i=0; i<dashboardCount; i++){ //в цикле по всем страницам текущей инфопанели for(int j=0; j<dashboardPages[i].length; j++){ dashboardPage = dashboardPages[i][j].replace("/", "\\/"); dashboardPageEncoded = dashboardPage.replace("\\", "\\\\"); dashboardPageEncoded = dashboardPageEncoded.replace("\"", "\\\""); //открываем страницу инфопанели текущей итерации selenium.open("/analytics/saw.dll?Dashboard&PortalPath="+dashboardPathsEncoded[i]+"&page="+dashboardPageEncoded); //selenium.windowMaximize(); //даем странице время отрисоваться try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } //убеждаемся что все "часики" отработали, на всякий случай помещаем в try-блок - если за 30 сек. отчет не отработает try { selenium.waitForCondition("selenium.getXpathCount(\"//div[contains(text(),'Searching... To cancel, click')]\") == 0", "30000"); } catch (Exception e) { e.printStackTrace(); } //считываем кол-во ошибок (Xpath-проверка по наличию элементов с текстом 'Error Details') errorCount = selenium.getXpathCount("//a[contains(text(),'Error Details')]").intValue(); allErrorCount = allErrorCount + errorCount; //Выводим информацию о проблемной странице инфопанели в System.out if (errorCount > 0) { System.out.println("DASHBOARD: " + dashboardPaths[i]); System.out.println("DASHBOARD PAGE: " + dashboardPages[i][j]); System.out.println("ERRORS COUNT: " + errorCount); for(int k=0; k<errorCount; k++){ errorText = selenium.getText("xpath=(//a[contains(text(),'Error Details')]/parent::div/div/div[2]/div)[" + (k+1) + "]"); System.out.println("error["+k+"]: " + errorText); } System.out.println("========================================================================================================"); } if (doScreenshots.equalsIgnoreCase("YES")) { String srcFullPath = dashboardPaths[i].substring(1); //отбрасываем первый слеш / StringBuilder destFullPath = new StringBuilder(); srcFullPath = srcFullPath.replace("\\/", ";;;"); //для корректной разбивки строки полного адреса на массив заменим вхождения \/ на псевдо-разделитель ;;; String[] srcPaths = srcFullPath.split("/"); //разбиваем строку полного адреса на массив вложенных каталогов //в цикле по каждому каталогу полного пути //преобразуем имена каталогов for (int k=0; k<srcPaths.length; k++) { destFullPath.append(encodePath(srcPaths[k].replace(";;;", "\\/")) + File.separator); } //добавим в преобразованный полный путь название файла скриншота destFullPath.append(encodePath(dashboardPage)); //создадим сам файл скринота File f = new File(screenshotsPath + destFullPath.toString()); //для создаваемого файла может еще не существовать директории, создадим ее if (!f.getParentFile().exists()) f.getParentFile().mkdirs(); //System.out.println(f.getAbsolutePath()); //запишем в файл поток байтов с картинкой selenium.captureEntirePageScreenshot(f.getAbsolutePath(),""); //откроем поток на чтение файла со скриншотом fis = new FileInputStream(f.getAbsolutePath()); //откроем поток на запись файла с миниатюрой скриншота, также зададим расширение этому файлу fos = new FileOutputStream(f.getAbsolutePath() + ".png"); //считаем скриншот sourceImage = ImageIO.read(fis); //изменим размер скриншота на заданные в properties-файле //если ширина или высота указана -1, то сжатие будет происходить пропорционально исходным размерам скриншота thumbnail = sourceImage.getScaledInstance(thumbnailWidth, thumbnailHeight, Image.SCALE_SMOOTH); bufferedThumbnail = new BufferedImage(thumbnail.getWidth(null), thumbnail.getHeight(null), BufferedImage.TYPE_INT_RGB); bufferedThumbnail.getGraphics().drawImage(thumbnail, 0, 0, null); //запишем ужатую картинку в файл ImageIO.write(bufferedThumbnail, "png", fos); //закроем потоки fis.close(); fos.close(); } } } } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } finally { try { //делаем logout текущей BI-сессии selenium.click("//span[@id='logout']/span/span"); //закрываем главное окно браузера selenium.close(); } catch (Exception e) { e.printStackTrace(); } } //если в ходе теста были обнаружены проблемные страница инфопанелей - тест считаем проваленным assertTrue("See System.out for error explanation", (allErrorCount == 0) ); } }
На всякий случай выкладываю архив проекта в Eclipse.
Обратите внимание, что входные параметры теста вынесены в файл selenium.properties
serverHost=localhost serverPort=4444 browserType=*firefox browserUrl=http://localhost:7001 nqUser=weblogic nqPassword=weblogic_1 windowWidth=1300 windowHeight=900 doScreenshots=YES screenshotsPath=C:/Middleware/analyticsRes/ thumbnailWidth=300 thumbnailHeight=-1
Не буду повторяться относительно создания и настройки Selenium-теста - смотрите прошлое сообщение. Буду описывать нововведения.
Новые свойства в properties-файле:
windowWidth - задает ширину окна браузера в момент снятия скриншота (для моего 24 дюймого монитора это актуально);
windowHeight - задает высоту окна браузера в момент снятия скриншота;
doScreenshots - признак того, нужно ли делать скриншоты;
screenshotsPath - абсолютный путь до каталога, куда складируются скриншоты;
thumbnailWidth - ширина ужатой миниатюры;
thumbnailHeight - высота ужатой миниатюры.
Важно знать, что
- если windowWidth или windowHeight не указаны, то окно браузера будет распахнуто максимально;
- если значение thumbnailWidth (или thumbnailHeight) равно -1, то оно будет вычислять ПРОПОРЦИОНАЛЬНО исходным размерам скриншота относительно заданного thumbnailHeight (или thumbnailWidth);
- screenshotsPath должно содержать полный путь до каталога, где будут храниться скриншоты, включая последний разделитель.
Очевидно, что недостаточно просто организовать снятие, ужатие и сохранение скриншотов страниц инфопанелей куда-то на диск.
Желательно обеспечить несложное последующее использование полученных миниатюр в интерфейсе OBIEE.
Я использую созданные миниатюры следующим образом:
- для элемента инфопанели "Ссылка или изображение" указываю путь до желаемой страницы информационной панели;
- копирую строку с адресом этой страницы;
- вставляю строку адреса в поле ввода "Изображение";
- добавляю в конец строки "Изображение" текст ".png";
- добавляю в начало строки "Изображение" текст "/analyticsRes".
Ну и само собой, чтобы это работало, необходимо иметь задеплоенное в Weblogic приложение analyticsRes.
- "заготовку" для него можно взять тут - с:\Middleware\instances\instance1\bifoundation\OracleBIPresentationServicesComponent\coreapplication_obips1\analyticsRes ;
- скопируйте папку-заготовку куда-нибудь ПОБЛИЖЕ к корню диска (это важно, так как следует минимизировать суммарный путь до файлов миниатюр, например, у меня это - c:\Middleware\analyticsRes);
- в момент деплоя КРАЙНЕ важно указать, что приложение analyticsRes будет доступно в том же каталоге, где оно расположено, то есть в c:\Middleware\analyticsRes (иначе оно будет скопировано в STAGE-область WLS, нам это не нужно - мы настраиваем Selenium-тест на выгрузку миниатюр в исходную папку).
P.S. Есть одна проблема, который не удалось победить. Если в полный адрес до вашей страницы инфопанели (адрес, который вы видите в CatalogManager или в каталожном браузере OBIEE) содержит спец.символы, которые НЕЛЬЗЯ использовать в названиях каталогов и файлов операционной системы (например, " # * . < > ? : | ), то вам необходимо самим произвести замену этих символов на знак подчеркивания "_" в момент заполнения поля ввода "Изображение".
Комментариев нет:
Отправить комментарий