В последнем сообщении 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) содержит спец.символы, которые НЕЛЬЗЯ использовать в названиях каталогов и файлов операционной системы (например, " # * . < > ? : | ), то вам необходимо самим произвести замену этих символов на знак подчеркивания "_" в момент заполнения поля ввода "Изображение".
Комментариев нет:
Отправить комментарий