Index: distribution-test/ui-tests/src/test/it/org/xwiki/it/ui/BlogCategoriesTest.java
===================================================================
--- distribution-test/ui-tests/src/test/it/org/xwiki/it/ui/BlogCategoriesTest.java	(revision 0)
+++ distribution-test/ui-tests/src/test/it/org/xwiki/it/ui/BlogCategoriesTest.java	(revision 0)
@@ -0,0 +1,165 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.it.ui;
+
+import junit.framework.Assert;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.xwiki.it.ui.elements.ManageCategoriesPage;
+import org.xwiki.it.ui.framework.AbstractAdminAuthenticatedTest;
+import org.xwiki.it.ui.framework.TestUtils;
+
+
+/**
+ * Test Blog categories. Tested features: add, rename, delete.
+ * 
+ * @version 
+ * @since 
+ */
+public class BlogCategoriesTest extends AbstractAdminAuthenticatedTest {
+    private ManageCategoriesPage categoriesPage;
+    
+    private static final String WORD_CATEGORY = "SomeCategory";
+    
+    private static final String SPACE_CATEGORY = "Another Category";
+
+    private static final String SPECIAL_CATEGORY = "The \"Do\"s & Don'ts";
+
+    @Before
+    public void setUp()
+    {
+        super.setUp();
+        
+        categoriesPage = new ManageCategoriesPage(getDriver());
+        
+        // clean up
+        TestUtils.deletePage("Blog", WORD_CATEGORY, getDriver());
+        TestUtils.deletePage("Blog", SPACE_CATEGORY, getDriver());
+        TestUtils.deletePage("Blog", SPECIAL_CATEGORY, getDriver());
+    } 
+
+    @Test
+    public void testCategoryAdd()
+    {
+        categoryAdd(WORD_CATEGORY);
+    }
+
+    @Test
+    public void testCategoryRename()
+    {
+        categoryAdd(WORD_CATEGORY);
+        categoryRename(WORD_CATEGORY, SPACE_CATEGORY);
+    }
+
+    @Test
+    public void testCategoryRemove()
+    {
+        categoryAdd(WORD_CATEGORY);
+        categoryRemove(WORD_CATEGORY);
+    }
+
+    @Test
+    public void testCategoryAddSpace()
+    {
+        categoryAdd(SPACE_CATEGORY);
+    }
+
+    @Test
+    public void testCategoryRenameSpace()
+    {
+        categoryAdd(SPACE_CATEGORY);
+        categoryRename(SPACE_CATEGORY, SPECIAL_CATEGORY);
+    }
+
+    @Test
+    public void testCategoryRemoveSpace()
+    {
+        categoryAdd(SPACE_CATEGORY);
+        categoryRemove(SPACE_CATEGORY);
+    }
+
+    @Test
+    public void testCategoryAddSpecial()
+    {
+        categoryAdd(SPECIAL_CATEGORY);
+    }
+
+    @Test
+    public void testCategoryRenameSpecial()
+    {
+        categoryAdd(SPECIAL_CATEGORY);
+        categoryRename(SPECIAL_CATEGORY, WORD_CATEGORY);
+    }
+
+    @Test
+    public void testCategoryRemoveSpecial()
+    {
+        categoryAdd(SPECIAL_CATEGORY);
+        categoryRemove(SPECIAL_CATEGORY);
+    }
+
+    /**
+     * Helper method that adds a new category and checks for success
+     * 
+     * @param name
+     */
+    private void categoryAdd(String name)
+    {
+        categoriesPage.gotoManageCategoriesPage();
+        Assert.assertFalse(categoriesPage.isCategoryPresent(name));
+        
+        categoriesPage.clickAddCategory();
+        categoriesPage.addCategory(name);
+        Assert.assertTrue(categoriesPage.isCategoryPresent(name));
+    }
+
+    /**
+     * Helper method that renames a category and checks for success
+     * 
+     * @param fromName  source name, must exist
+     * @param toName    target name, must not exist
+     */
+    private void categoryRename(String fromName, String toName)
+    {
+        categoriesPage.gotoManageCategoriesPage();
+        Assert.assertTrue(categoriesPage.isCategoryPresent(fromName));
+        Assert.assertFalse(categoriesPage.isCategoryPresent(toName));
+        
+        categoriesPage.renameCategory(fromName, toName);
+        Assert.assertFalse(categoriesPage.isCategoryPresent(fromName));
+        Assert.assertTrue(categoriesPage.isCategoryPresent(toName));
+    }
+
+    /**
+     * Helper method that removes a category and checks for success
+     * 
+     * @param name  category name, must exist
+     */
+    private void categoryRemove(String name)
+    {
+        categoriesPage.gotoManageCategoriesPage();
+        Assert.assertTrue(categoriesPage.isCategoryPresent(name));
+
+        categoriesPage.deleteCategory(name);
+        Assert.assertFalse(categoriesPage.isCategoryPresent(name));
+    }
+}
+

Property changes on: distribution-test/ui-tests/src/test/it/org/xwiki/it/ui/BlogCategoriesTest.java
___________________________________________________________________
Added: svn:keywords
   + Author Id Revision HeadURL
Added: svn:eol-style
   + native

Index: distribution-test/ui-tests/src/test/it/org/xwiki/it/ui/elements/BasePage.java
===================================================================
--- distribution-test/ui-tests/src/test/it/org/xwiki/it/ui/elements/BasePage.java	(revision 27744)
+++ distribution-test/ui-tests/src/test/it/org/xwiki/it/ui/elements/BasePage.java	(working copy)
@@ -21,6 +21,7 @@
 
 import org.openqa.selenium.By;
 import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.NotFoundException;
 import org.openqa.selenium.RenderedWebElement;
 import org.openqa.selenium.WebDriver;
 import org.openqa.selenium.WebElement;
@@ -32,7 +33,6 @@
 import org.openqa.selenium.support.ui.ExpectedCondition;
 import org.openqa.selenium.support.ui.Wait;
 import org.openqa.selenium.support.ui.WebDriverWait;
-import org.xwiki.it.ui.framework.TestUtils;
 
 /**
  * Represents the common actions possible on all Pages.
@@ -86,6 +86,56 @@
         });
     }
 
+    public void waitUntilElementDisappears(final By locator)
+    {
+        waitUntilElementDisappears(locator, 10);
+    }
+
+    /**
+     * Waits until the given element is either hidden or deleted.
+     * 
+     * @param locator
+     * @param timeout
+     */
+    public void waitUntilElementDisappears(final By locator, int timeout)
+    {
+        Wait<WebDriver> wait = new WebDriverWait(driver, timeout);
+        wait.until(new ExpectedCondition<Boolean>()
+        {
+            public Boolean apply(WebDriver driver)
+            {
+                try {
+                    RenderedWebElement element = (RenderedWebElement) driver.findElement(locator);
+                    return Boolean.valueOf(!element.isDisplayed());
+                } catch (NotFoundException e) {
+                    return Boolean.TRUE;
+                }
+            }
+        });
+    }
+
+    /**
+     * Shows hidden elements, as if they would be shown on hover.
+     * 
+     * Currently implemented using JavaScript. Will throw a {@link RuntimeException} if the web driver
+     * does not support JavaScript or JavaScript is disabled.
+     * 
+     * @param locator  locator used to find the element, in case multiple elements are found, the first is used
+     */
+    public void makeElementVisible(By locator)
+    {
+        // RenderedWebElement.hover() don't seem to work, workarounded using JavaScript call
+        if (!(getDriver() instanceof JavascriptExecutor)) {
+            throw new RuntimeException("Currently used web driver (" + getDriver().getClass()
+                + ") does not support JavaScript execution");
+        }
+        JavascriptExecutor js = (JavascriptExecutor) getDriver();
+        if (!js.isJavascriptEnabled()) {
+            throw new RuntimeException("JavaScript is disabled");
+        }
+        js.executeScript("arguments[0].style.visibility='visible'", getDriver().findElement(locator));
+    }
+
     /**
      * Logs in the Admin user (move to the home page if the current page has no log in link).
      */
Index: distribution-test/ui-tests/src/test/it/org/xwiki/it/ui/elements/ManageCategoriesPage.java
===================================================================
--- distribution-test/ui-tests/src/test/it/org/xwiki/it/ui/elements/ManageCategoriesPage.java	(revision 0)
+++ distribution-test/ui-tests/src/test/it/org/xwiki/it/ui/elements/ManageCategoriesPage.java	(revision 0)
@@ -0,0 +1,173 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.it.ui.elements;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.xwiki.it.ui.framework.TestUtils;
+
+
+/**
+ * Represents blog category management page.
+ * 
+ * @version 
+ * @since 
+ */
+public class ManageCategoriesPage extends BasePage {
+    @FindBy(xpath = "//li/span[@class='blog-add-category-label']/a")
+    private WebElement addCategoryLink;
+
+    @FindBy(xpath = "//form[@class='category-add-form']//input[@type='submit']")
+    private WebElement addButton;
+
+    @FindBy(xpath = "//form[@class='category-add-form']//a[text()='Cancel']")
+    private WebElement cancelLink;
+
+    @FindBy(xpath = "//input[@class='category-name-input']")
+    private WebElement nameInput;
+
+    @FindBy(xpath = "//form[@class='category-rename-form']//input[@type='submit']")
+    private WebElement renameButton;
+
+
+    /**
+     * Create new ManageCategoriesPage
+     * 
+     * @param driver
+     */
+    public ManageCategoriesPage(WebDriver driver)
+    {
+        super(driver);
+    }
+
+    public void gotoManageCategoriesPage()
+    {
+        TestUtils.gotoPage("Blog", "ManageCategories", "view", getDriver());
+    }
+
+    /**
+     * Click the "Add a Category" link and wait until the form shows up
+     */
+    public void clickAddCategory()
+    {
+        this.addCategoryLink.click();
+        waitUntilElementIsVisible(By.className("category-add-form"));
+    }
+    
+    /**
+     * Click cancel on the category add form
+     */
+    public void cancelAddingCategory()
+    {
+        this.cancelLink.click();
+    }
+
+    /**
+     * Add a new top-level category with the given name, {@link #clickAddCategory()} should be called first
+     * 
+     * @param name
+     */
+    public void addCategory(String name)
+    {
+        this.nameInput.sendKeys(name);
+        this.addButton.submit();
+
+        waitUntilElementIsVisible(categoryLocator(name));
+    }
+
+    /**
+     * Delete a category by name.
+     * 
+     * @param name  category name, must exist
+     */
+    public void deleteCategory(String name)
+    {
+        // click delete button
+        By deletePath = By.xpath("//a[@class='tool delete' and contains(@href, '"
+            + TestUtils.escapeURL(name) + "')]/img");
+        hoverCategoryItem(name);
+        waitUntilElementIsVisible(deletePath);
+        getDriver().findElement(deletePath).click();
+        
+        // answer yes in confirmation dialog
+        waitUntilElementIsVisible(By.xpath("//div[@class='xdialog-screen']"));
+        getDriver().findElement(By.xpath("//div[@class='xdialog-modal-container']//input[@value='Yes']")).click();
+        waitUntilElementDisappears(categoryLocator(name));
+    }
+
+    /**
+     * Rename an existing category.
+     * 
+     * @param fromName  source category name, must exist
+     * @param toName    target category name, must not exist
+     */
+    public void renameCategory(String fromName, String toName)
+    {
+        // show the rename form
+        By renamePath = By.xpath("//a[@class='tool rename' and contains(@href, '"
+            + TestUtils.escapeURL(fromName) + "')]/img");
+        hoverCategoryItem(fromName);
+        waitUntilElementIsVisible(renamePath);
+        getDriver().findElement(renamePath).click();
+        waitUntilElementIsVisible(By.className("category-rename-form"));
+        
+        // rename and wait for result
+        this.nameInput.sendKeys(toName);
+        this.renameButton.submit();
+        waitUntilElementIsVisible(categoryLocator(toName));
+    }
+    
+    /**
+     * Check if the given category is present in the categories tree
+     * 
+     * @param name  category name
+     */
+    public boolean isCategoryPresent(String name)
+    {
+        return !getDriver().findElements(categoryLocator(name)).isEmpty();
+    }
+    
+    /**
+     * Since the toolbox (with rename, add subcategory and delete buttons) is hidden by default,
+     * we need to hover it before any interaction, otherwise Selenium will complain.
+     * 
+     * @param name  category name, must exist
+     */
+    private void hoverCategoryItem(String name)
+    {
+        By locator = By.xpath("//a[@class='tool delete' and contains(@href, '"
+            + TestUtils.escapeURL(name) + "')]/ancestor::span[@class='blog-category-tools']");
+        makeElementVisible(locator);
+    }
+    
+    /**
+     * Return a xpath locator for the given category. Not guaranteed to find exactly one occurrence
+     * 
+     * @param name  category name
+     */
+    private By categoryLocator(String name)
+    {
+        return By.xpath("//span[@class='blog-category']//a[contains(@href, '"
+            + TestUtils.escapeURL(name) + "')]");
+    }
+}
+

Property changes on: distribution-test/ui-tests/src/test/it/org/xwiki/it/ui/elements/ManageCategoriesPage.java
___________________________________________________________________
Added: svn:keywords
   + Author Id Revision HeadURL
Added: svn:eol-style
   + native

Index: distribution-test/ui-tests/src/test/it/org/xwiki/it/ui/framework/TestUtils.java
===================================================================
--- distribution-test/ui-tests/src/test/it/org/xwiki/it/ui/framework/TestUtils.java	(revision 27744)
+++ distribution-test/ui-tests/src/test/it/org/xwiki/it/ui/framework/TestUtils.java	(working copy)
@@ -1,5 +1,8 @@
 package org.xwiki.it.ui.framework;
 
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
 import org.openqa.selenium.WebDriver;
 import org.openqa.selenium.firefox.FirefoxDriver;
 
@@ -76,4 +79,19 @@
     {
         TestUtils.gotoPage(space, page, "delete", "confirm=1", driver);
     }
+    
+    /**
+     * URL-escapes given string. Uses %20 for spaces (instead of +) to match the behavior of XWiki.getURL
+     * 
+     * @param s
+     */
+    public static String escapeURL(String s)
+    {
+        try {
+            return URLEncoder.encode(s, "UTF-8").replace("+", "%20");
+        } catch (UnsupportedEncodingException e) {
+            // should not happen
+            throw new RuntimeException(e);
+        }
+    }
 }
