Project

General

Profile

Download (17.9 KB) Statistics
| Branch: | Tag: | Revision:
1
package eu.etaxonomy.dataportal.pages;
2

    
3
import static org.junit.Assert.assertFalse;
4
import static org.junit.Assert.assertTrue;
5

    
6
import java.io.File;
7
import java.io.IOException;
8
import java.lang.reflect.Constructor;
9
import java.net.MalformedURLException;
10
import java.net.URL;
11
import java.util.ArrayList;
12
import java.util.List;
13
import java.util.concurrent.TimeUnit;
14
import java.util.regex.Matcher;
15
import java.util.regex.Pattern;
16
import java.util.stream.Collectors;
17

    
18
import org.apache.commons.io.FileUtils;
19
import org.apache.log4j.Logger;
20
import org.openqa.selenium.By;
21
import org.openqa.selenium.NoSuchElementException;
22
import org.openqa.selenium.OutputType;
23
import org.openqa.selenium.TakesScreenshot;
24
import org.openqa.selenium.WebDriver;
25
import org.openqa.selenium.WebElement;
26
import org.openqa.selenium.interactions.Actions;
27
import org.openqa.selenium.support.CacheLookup;
28
import org.openqa.selenium.support.FindBy;
29
import org.openqa.selenium.support.PageFactory;
30
import org.openqa.selenium.support.ui.WebDriverWait;
31

    
32
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
33
import com.google.common.base.Function;
34

    
35
import eu.etaxonomy.dataportal.DataPortalContext;
36
import eu.etaxonomy.dataportal.elements.BaseElement;
37
import eu.etaxonomy.dataportal.elements.ClassificationTreeBlock;
38
import eu.etaxonomy.dataportal.elements.LinkElement;
39
import eu.etaxonomy.dataportal.selenium.JUnitWebDriverWait;
40
import eu.etaxonomy.dataportal.selenium.UrlLoaded;
41

    
42
/**
43
 * FIXME only works with the cichorieae theme
44
 *
45
 * @author a.kohlbecker
46
 *
47
 */
48
public abstract class PortalPage {
49

    
50
    /**
51
     *
52
     */
53
    public static final int WAIT_SECONDS = 25;
54

    
55
    public static final Logger logger = Logger.getLogger(PortalPage.class);
56

    
57
    protected final static String DRUPAL_PAGE_QUERY = "q=";
58

    
59
    private final static Pattern DRUPAL_PAGE_QUERY_PATTERN = Pattern.compile("q=([^&]*)");
60

    
61
    public enum MessageType {status, warning, error} // needs to be lowercase
62

    
63
    protected WebDriver driver;
64

    
65
    protected DataPortalContext context;
66

    
67
    protected final JUnitWebDriverWait wait;
68

    
69
    public WebDriverWait getWait() {
70
        return wait;
71
    }
72

    
73

    
74
    /**
75
     * Implementations of this method will supply the relative
76
     * path to the Drupal page. This path will usually have the form
77
     * <code>cdm_dataportal/{nodetype}</code>. For example the taxon pages all
78
     * have the page base <code>cdm_dataportal/taxon</code>
79
     */
80
    protected abstract String getDrupalPageBase();
81

    
82
    private String initialDrupalPagePath;
83

    
84
    protected URL pageUrl;
85

    
86
    // ==== WebElements === //
87

    
88
    protected BaseElement mainContent = null;
89

    
90
    @FindBy(className="node")
91
    protected WebElement node;
92

    
93

    
94
    @FindBy(id="block-cdm-dataportal-2")
95
    @CacheLookup
96
    protected WebElement searchBlockElement;
97

    
98
    @FindBy(id="block-cdm-taxontree-cdm-tree")
99
    @CacheLookup
100
    protected WebElement classificationBrowserBlock;
101

    
102
    @FindBy(className="messages")
103
    @CacheLookup
104
    protected List<WebElement> messages;
105

    
106
    private Boolean isZenTheme;
107

    
108

    
109
    public boolean isZenTheme() {
110
        if(isZenTheme == null) {
111
            try {
112
                WebElement bodyElement = driver.findElement(By.tagName("body"));
113
                isZenTheme = bodyElement.getAttribute("class").contains("zen_dataportal");
114
            } catch (ElementNotFoundException e) {
115
                // IGNORE
116
            }
117
        }
118
        return isZenTheme.booleanValue();
119
    }
120

    
121
    /**
122
     * Creates a new PortaPage. Implementations of this class will provide the base path of the page by
123
     * implementing the method {@link #getDrupalPageBase()}. The constructor argument <code>pagePathSuffix</code>
124
     * specifies the specific page to navigate to. For example:
125
     * <ol>
126
     * <li>{@link #getDrupalPageBase()} returns <code>/cdm_dataportal/taxon</code></li>
127
     * <li><code>pagePathSuffix</code> gives <code>7fe8a8b6-b0ba-4869-90b3-177b76c1753f</code></li>
128
     * </ol>
129
     * Both are combined to form the URL pathelement <code>/cdm_dataportal/taxon/7fe8a8b6-b0ba-4869-90b3-177b76c1753f</code>
130
     *
131
     */
132
    public PortalPage(WebDriver driver, DataPortalContext context, String pagePathSuffix) throws MalformedURLException {
133

    
134
        this.driver = driver;
135

    
136
        this.context = context;
137

    
138
        this.wait = new JUnitWebDriverWait(driver, WAIT_SECONDS);
139

    
140
        this.initialDrupalPagePath = getDrupalPageBase() + (pagePathSuffix != null ? "/" + pagePathSuffix: "");
141

    
142
        this.pageUrl = new URL(context.getBaseUri().toString() + "?" + DRUPAL_PAGE_QUERY + initialDrupalPagePath);
143

    
144
        // tell browser to navigate to the page
145
        logger.info("loading " + pageUrl);
146
        driver.get(pageUrl.toString());
147
        logger.info("loading done");
148

    
149
        takeScreenShot();
150

    
151

    
152
        // This call sets the WebElement fields.
153
        PageFactory.initElements(driver, this);
154

    
155
        pageHealthChecks();
156

    
157

    
158

    
159
    }
160

    
161
    /**
162
     *
163
     */
164
    protected void pageHealthChecks() {
165

    
166
        try {
167
            String ignore_error = "Expecting web service to return pager objects but received an array";
168
            List<String> errors = getErrors().stream().filter(str -> str.startsWith(ignore_error)).collect(Collectors.toList());
169
            assertTrue("The page must not show an error box", errors.size() == 0);
170
        } catch (NoSuchElementException e) {
171
            //IGNORE since this is expected!
172
        }
173
        assertFalse("The default footnote list key PAGE_GLOBAL must not occur in the page.", driver.getPageSource().contains("member-of-footnotes-PAGE_GLOBAL"));
174

    
175
    }
176

    
177

    
178
    /**
179
     * Creates a new PortaPage at given URL location. An Exception is thrown if
180
     * this URL is not matching the expected URL for the specific page type.
181
     *
182
     */
183
    public PortalPage(WebDriver driver, DataPortalContext context, URL url) throws Exception {
184

    
185
        this.driver = driver;
186

    
187
        this.context = context;
188

    
189
        this.wait = new JUnitWebDriverWait(driver, 25);
190

    
191
        this.pageUrl = new URL(context.getBaseUri().toString());
192

    
193
        // tell browser to navigate to the given URL
194
        driver.get(url.toString());
195

    
196
        takeScreenShot();
197

    
198
        if(!isOnPage()){
199
            throw new Exception("Not on the expected portal page ( current: " + driver.getCurrentUrl() + ", expected: " +  pageUrl + " )");
200
        }
201

    
202
        this.pageUrl = url;
203

    
204
        logger.info("loading " + pageUrl);
205

    
206
        // This call sets the WebElement fields.
207
        PageFactory.initElements(driver, this);
208

    
209
    }
210

    
211
    /**
212
     * Creates a new PortaPage at the WebDrivers current URL location. An Exception is thrown if
213
     * driver.getCurrentUrl() is not matching the expected URL for the specific page type.
214
     *
215
     */
216
    public PortalPage(WebDriver driver, DataPortalContext context) throws Exception {
217

    
218
        this.driver = driver;
219

    
220
        this.context = context;
221

    
222
        this.wait = new JUnitWebDriverWait(driver, 25);
223

    
224
        // preliminary set the pageUrl to the base path of this page, this is used in the next setp to check if the
225
        // driver.getCurrentUrl() is a sub path of the base path
226
        this.pageUrl = new URL(context.getBaseUri().toString());
227

    
228
        takeScreenShot();
229

    
230
        if(!isOnPage()){
231
            throw new Exception("Not on the expected portal page ( current: " + driver.getCurrentUrl() + ", expected: " +  pageUrl + " )");
232
        }
233

    
234
        // now set the real URL
235
        this.pageUrl = new URL(driver.getCurrentUrl());
236

    
237
        logger.info("loading " + pageUrl);
238

    
239
        // This call sets the WebElement fields.
240
        PageFactory.initElements(driver, this);
241

    
242
    }
243

    
244

    
245
    protected boolean isOnPage() {
246
        return driver.getCurrentUrl().startsWith(pageUrl.toString());
247
    }
248

    
249
    /**
250
     * navigate and reload the page if not yet there
251
     */
252
    public void get() {
253
        if(!driver.getCurrentUrl().equals(pageUrl.toString())){
254
            driver.get(pageUrl.toString());
255
            wait.until(new UrlLoaded(pageUrl.toString()));
256
            // take screenshot of new page
257
            takeScreenShot();
258
            PageFactory.initElements(driver, this);
259
        }
260
    }
261

    
262
    /**
263
     * go back in history
264
     */
265
    public void back() {
266
        driver.navigate().back();
267
    }
268

    
269
    public String getDrupalPagePath() {
270
        URL currentURL;
271
        try {
272
            currentURL = new URL(driver.getCurrentUrl());
273
            if(currentURL.getQuery() != null && currentURL.getQuery().contains(DRUPAL_PAGE_QUERY)){
274
                Matcher m = DRUPAL_PAGE_QUERY_PATTERN.matcher(currentURL.getQuery());
275
                m.matches();
276
                return m.group(1);
277
            } else {
278
                String uriBasePath = context.getBaseUri().getPath();
279
                if(!uriBasePath.endsWith("/")){
280
                    uriBasePath += "/";
281
                }
282
                return currentURL.getPath().replaceFirst("^"+ uriBasePath, "");
283
            }
284
        } catch (MalformedURLException e) {
285
            throw new RuntimeException(e);
286
        }
287
    }
288

    
289
    /**
290
     * Returns the the page path with which the page has initially been loaded.
291
     * Due to redirects the actual page path which can be retrieved by
292
     * {@link #getDrupalPagePath()} might be different
293
     *
294
     */
295
    public String getInitialDrupalPagePath() {
296
        return initialDrupalPagePath;
297
    }
298

    
299
    /**
300
     *
301
     * @return the page title
302
     * @deprecated use {@link WebDriver#getTitle()}
303
     */
304
    @Deprecated
305
    public String getTitle() {
306
        return driver.getTitle();
307
    }
308

    
309
    public List<String> getMessageItems(MessageType messageType){
310
        if(messages != null){
311
            for(WebElement m : messages){
312
                if(m.getAttribute("class").contains(messageType.name())){
313
                    List<WebElement> messageItems = m.findElements(By.cssSelector("ul.messages__list li"));
314
                    if(messageItems.size() == 0 && !m.getText().isEmpty()) {
315
                        // we have only one item which is not shown as list.
316
                        messageItems.add(m);
317

    
318
                    }
319
                    return messageItems.stream().map(mi -> mi.getText()).collect(Collectors.toList());
320
                }
321
            }
322
        }
323
        return new ArrayList<>();
324
    }
325

    
326
    /**
327
     * @return the warning messages from the Drupal message box
328
     */
329
    public List<String> getWarnings() {
330
        return getMessageItems(MessageType.warning);
331
    }
332

    
333
    /**
334
     * @return the error messages from the Drupal message box
335
     */
336
    public List<String> getErrors() {
337
        return getMessageItems(MessageType.error);
338
    }
339

    
340
    public String getAuthorInformationText() {
341

    
342
        WebElement authorInformation = null;
343

    
344
        try {
345
            authorInformation  = node.findElement(By.className("submitted"));
346
        } catch (NoSuchElementException e) {
347
            // IGNORE //
348
        }
349

    
350

    
351
        if(authorInformation != null){
352
            return authorInformation.getText();
353
        } else {
354
            return null;
355
        }
356
    }
357

    
358
    public List<LinkElement> getPrimaryTabs(){
359
        List<LinkElement> tabs = new ArrayList<LinkElement>();
360
        List<WebElement> primaryTabLinks = null;
361
        if(isZenTheme()) {
362
            primaryTabLinks = driver.findElements(By.cssSelector(".tabs-primary li a"));
363
        } else {
364
            // try the old garland theme
365
            primaryTabLinks = driver.findElements(By.cssSelector("#tabs-wrapper ul.primary li a"));
366
        }
367

    
368
        for(WebElement a : primaryTabLinks) {
369
            WebElement renderedLink = a;
370
            if(renderedLink.isDisplayed()){
371
                tabs.add(new LinkElement(renderedLink));
372
            }
373
        }
374

    
375
        return tabs;
376
    }
377

    
378
    /**
379
     * Provides access to the the CDM specific main content <code>div</code> element.
380
     *
381
     *
382
     * @return the main content div
383
     */
384
    public BaseElement getDataPortalContent() {
385
        if(mainContent == null) {
386
            if(isZenTheme()) {
387
                mainContent = new BaseElement(driver.findElement(By.id("content")));
388
            } else {
389
                // fallback to garland
390
                mainContent = new BaseElement(driver.findElement(By.id("squeeze")));
391
            }
392
        }
393
        return mainContent;
394
    }
395

    
396
    public ClassificationTreeBlock getClassificationTree() {
397
        return new ClassificationTreeBlock(classificationBrowserBlock);
398
    }
399

    
400
    public void hover(WebElement element) {
401
        Actions actions = new Actions(driver);
402
        actions.moveToElement(element, 1, 1).perform();
403
        logger.debug("hovering");
404
    }
405

    
406

    
407
    /**
408
     * @return the current URL string from the {@link WebDriver}
409
     */
410
    public URL getPageURL() {
411
        return pageUrl;
412
    }
413

    
414

    
415
    /**
416
     * @return the <code>scheme://domain:port</code> part of the initial url of this page.
417
     */
418
    public String getInitialUrlBase() {
419
        return pageUrl.getProtocol() + "://" + pageUrl.getHost() + pageUrl.getPort();
420
    }
421

    
422
    @Override
423
    public boolean equals(Object obj) {
424
        if (PortalPage.class.isAssignableFrom(obj.getClass())) {
425
            PortalPage page = (PortalPage) obj;
426
            return this.getPageURL().toString().equals(page.getPageURL().toString());
427

    
428
        } else {
429
            return false;
430
        }
431
    }
432

    
433

    
434
    /**
435
     * @param isTrue see {@link org.openqa.selenium.support.ui.FluentWait#until(Function)}
436
     * @param pageType the return type
437
     * @param duration may be null, if this in null <code>waitUnit</code> will be ignored.
438
     * @param waitUnit may be null, is ignored if <code>duration</code> is null defaults to {@link TimeUnit#SECONDS}
439

    
440
     */
441
    public <T extends PortalPage> T clickLink(BaseElement element, Function<? super WebDriver, Boolean> isTrue, Class<T> pageType, Long duration, TimeUnit waitUnit) {
442

    
443
        if( pageType!= null && pageType.getClass().equals(PortalPage.class) ) {
444
            throw new RuntimeException("Parameter pageType must be a subclass of PortalPage");
445
        }
446
        String targetWindow = null;
447
        List<String> targets = element.getLinkTargets(driver);
448
        if(targets.size() > 0){
449
            targetWindow = targets.get(0);
450
        }
451

    
452
        if(logger.isInfoEnabled() || logger.isDebugEnabled()){
453
            logger.info("clickLink() on " + element.toStringWithLinks());
454
        }
455
        element.getElement().click();
456
        if(targetWindow != null){
457
            driver.switchTo().window(targetWindow);
458
        }
459

    
460
        try {
461
            if(duration != null){
462
                if(waitUnit == null){
463
                    waitUnit = TimeUnit.SECONDS;
464
                }
465
                wait.withTimeout(duration, waitUnit).until(isTrue);
466
            } else {
467
                wait.until(isTrue);
468
            }
469
        } catch (AssertionError timeout){
470
            logger.info("clickLink timed out. Current WindowHandles:" + driver.getWindowHandles());
471
            throw timeout;
472
        }
473

    
474

    
475
        Constructor<T> constructor;
476
        T pageInstance;
477
            if(pageType != null) {
478
            try {
479
                constructor = pageType.getConstructor(WebDriver.class, DataPortalContext.class);
480
                pageInstance = constructor.newInstance(driver, context);
481
            } catch (Exception e) {
482
                throw new RuntimeException(e);
483
            }
484
            // take screenshot of new page
485
            takeScreenShot();
486
            return pageInstance;
487
        } else {
488
            return null;
489
        }
490
    }
491

    
492

    
493
    /**
494
     * @param isTrue see {@link org.openqa.selenium.support.ui.FluentWait#until(Function)}
495
     * @param type the return type
496

    
497
     */
498
    public <T extends PortalPage> T clickLink(BaseElement element, Function<? super WebDriver, Boolean> isTrue, Class<T> type) {
499
        return clickLink(element, isTrue, type, null, null);
500
    }
501

    
502

    
503
    /**
504
     * replaces all underscores '_' by hyphens '-'
505
     */
506
    protected String normalizeClassAttribute(String featureName) {
507
        return featureName.replace('_', '-');
508
    }
509

    
510
    public File takeScreenShot(){
511

    
512

    
513
        logger.info("Screenshot ...");
514
        File destFile = fileForTestMethod(new File("screenshots"));
515
        if(logger.isDebugEnabled()){
516
            logger.debug("Screenshot destFile" + destFile.getPath().toString());
517
        }
518
        File scrFile = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
519
        try {
520
            FileUtils.copyFile(scrFile, destFile);
521
            logger.info("Screenshot taken and saved as " + destFile.getAbsolutePath());
522
            return destFile;
523
        } catch (IOException e) {
524
            logger.error("could not copy sceenshot to " + destFile.getAbsolutePath(), e);
525
        }
526

    
527
        return null;
528

    
529
    }
530

    
531
    /**
532
     * Finds the test class and method in the stack trace which is using the
533
     * PortalPage and returns a File object consisting of:
534
     * {@code $targetFolder/$className/$methodName }
535
     *
536
     *
537
     * If no test class is found it will fall back to using
538
     * "noTest" as folder name and a timestamp as filename.
539
     */
540
    private File fileForTestMethod(File targetFolder){
541

    
542
        StackTraceElement[] trace = Thread.currentThread().getStackTrace();
543
        for (StackTraceElement stackTraceElement : trace) {
544
            // according to the convention all test class names should end with "Test"
545
            if(logger.isTraceEnabled()){
546
                logger.trace("fileForTestMethod() - " + stackTraceElement.toString());
547
            }
548
            if(stackTraceElement.getClassName().endsWith("Test")){
549
                return uniqueIndexedFile(
550
                            targetFolder.getAbsolutePath() + File.separator + stackTraceElement.getClassName(),
551
                            stackTraceElement.getMethodName(),
552
                            "png");
553

    
554
            }
555
        }
556
        return uniqueIndexedFile(
557
                targetFolder.getAbsolutePath() + File.separator + "noTest",
558
                Long.toString(System.currentTimeMillis()),
559
                "png");
560
    }
561

    
562

    
563
    private File uniqueIndexedFile(String folder, String fileName, String suffix){
564
        File file;
565
        int i = 0;
566
        while(true){
567
            file = new File(folder + File.separator + fileName + "_" + Integer.toString(i++) + "." + suffix);
568
            if(!file.exists()){
569
                return file;
570
            }
571
        }
572
    }
573

    
574

    
575
}
(4-4/9)