Project

General

Profile

Download (20.7 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.EnumSet;
13
import java.util.List;
14
import java.util.Map;
15
import java.util.Map.Entry;
16
import java.util.Optional;
17
import java.util.concurrent.TimeUnit;
18
import java.util.regex.Matcher;
19
import java.util.regex.Pattern;
20
import java.util.stream.Collectors;
21

    
22
import org.apache.commons.io.FileUtils;
23
import org.apache.logging.log4j.LogManager;
24
import org.apache.logging.log4j.Logger;
25
import org.openqa.selenium.By;
26
import org.openqa.selenium.NoSuchElementException;
27
import org.openqa.selenium.OutputType;
28
import org.openqa.selenium.TakesScreenshot;
29
import org.openqa.selenium.WebDriver;
30
import org.openqa.selenium.WebElement;
31
import org.openqa.selenium.interactions.Actions;
32
import org.openqa.selenium.support.CacheLookup;
33
import org.openqa.selenium.support.FindBy;
34
import org.openqa.selenium.support.PageFactory;
35
import org.openqa.selenium.support.ui.WebDriverWait;
36

    
37
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
38
import com.google.common.base.Function;
39

    
40
import eu.etaxonomy.dataportal.DataPortalContext;
41
import eu.etaxonomy.dataportal.elements.BaseElement;
42
import eu.etaxonomy.dataportal.elements.ClassificationTreeBlock;
43
import eu.etaxonomy.dataportal.elements.LinkElement;
44
import eu.etaxonomy.dataportal.selenium.JUnitWebDriverWait;
45
import eu.etaxonomy.dataportal.selenium.UrlLoaded;
46

    
47
/**
48
 * FIXME only works with the cichorieae theme
49
 *
50
 * @author a.kohlbecker
51
 *
52
 */
53
public abstract class PortalPage {
54

    
55
    public static final int WAIT_SECONDS = 25;
56

    
57
    private static final Logger logger = LogManager.getLogger();
58

    
59
    protected final static String DRUPAL_PAGE_QUERY = "q=";
60

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

    
63
    public enum MessageType {status, warning, error} // needs to be lowercase
64

    
65
    protected WebDriver driver;
66

    
67
    protected DataPortalContext context;
68

    
69
    protected final JUnitWebDriverWait wait;
70

    
71
    public WebDriverWait getWait() {
72
        return wait;
73
    }
74

    
75
    public static enum HealthChecks {
76
        NO_ERROR, NO_WARNING;
77
    }
78

    
79
    public EnumSet<HealthChecks> getAciveHealthChecks() {
80
        return activeHealthChecks;
81
    }
82

    
83

    
84
    public void setAciveHealthChecks(EnumSet<HealthChecks> activeHealthChecks) {
85
        this.activeHealthChecks = activeHealthChecks;
86
    }
87

    
88
    private EnumSet<HealthChecks> activeHealthChecks = EnumSet.allOf(HealthChecks.class);
89

    
90

    
91
    /**
92
     * Implementations of this method will supply the relative
93
     * path to the Drupal page. This path will usually have the form
94
     * <code>cdm_dataportal/{nodetype}</code>. For example the taxon pages all
95
     * have the page base <code>cdm_dataportal/taxon</code>
96
     */
97
    protected abstract String getDrupalPageBase();
98

    
99
    private String initialDrupalPagePath;
100

    
101
    protected URL pageUrl;
102

    
103
    // ==== WebElements === //
104

    
105
    protected BaseElement mainContent = null;
106

    
107
    @FindBy(className="node")
108
    protected WebElement node;
109

    
110

    
111
    @FindBy(id="block-cdm-dataportal-2")
112
    @CacheLookup
113
    protected WebElement searchBlockElement;
114

    
115
    @FindBy(id="block-cdm-taxontree-cdm-tree")
116
    @CacheLookup
117
    protected WebElement classificationBrowserBlock;
118

    
119
    @FindBy(className="messages")
120
    @CacheLookup
121
    protected List<WebElement> messages;
122

    
123
    private Boolean isZenTheme;
124

    
125

    
126
    public boolean isZenTheme() {
127
        if(isZenTheme == null) {
128
            try {
129
                WebElement bodyElement = driver.findElement(By.tagName("body"));
130
                isZenTheme = bodyElement.getAttribute("class").contains("zen_dataportal");
131
            } catch (ElementNotFoundException e) {
132
                // IGNORE
133
            }
134
        }
135
        return isZenTheme.booleanValue();
136
    }
137

    
138

    
139
    /**
140
     * Creates a new PortaPage. Implementations of this class will provide the base path of the page by
141
     * implementing the method {@link #getDrupalPageBase()}. The constructor argument <code>pagePathSuffix</code>
142
     * specifies the specific page to navigate to. For example:
143
     * <ol>
144
     * <li>{@link #getDrupalPageBase()} returns <code>/cdm_dataportal/taxon</code></li>
145
     * <li><code>pagePathSuffix</code> gives <code>7fe8a8b6-b0ba-4869-90b3-177b76c1753f</code></li>
146
     * </ol>
147
     * Both are combined to form the URL path element <code>/cdm_dataportal/taxon/7fe8a8b6-b0ba-4869-90b3-177b76c1753f</code>
148
     *
149
     */
150
    public PortalPage(WebDriver driver, DataPortalContext context, String pagePathSuffix, Map<String, String> queryParameters) throws MalformedURLException {
151

    
152
        this.driver = driver;
153

    
154
        this.context = context;
155

    
156
        this.wait = new JUnitWebDriverWait(driver, WAIT_SECONDS);
157

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

    
160
        StringBuilder queryStringB = new StringBuilder();
161
        if(queryParameters != null && !queryParameters.isEmpty()) {
162
            for(Entry<String, String> entry : queryParameters.entrySet()) {
163
                queryStringB.append("&").append(entry.getKey()).append("=").append(entry.getValue());
164
            }
165
        }
166
        this.pageUrl = new URL(context.getSiteUri().toString() + "?" + DRUPAL_PAGE_QUERY + initialDrupalPagePath + queryStringB.toString());
167

    
168
        // tell browser to navigate to the page
169
        logger.info("loading " + pageUrl);
170
        driver.get(pageUrl.toString());
171
        logger.info("loading done");
172

    
173
        takeScreenShot();
174

    
175
        // This call sets the WebElement fields.
176
        PageFactory.initElements(driver, this);
177

    
178
        pageHealthChecks();
179
    }
180

    
181
    /**
182
     * Creates a new PortaPage. Implementations of this class will provide the base path of the page by
183
     * implementing the method {@link #getDrupalPageBase()}. The constructor argument <code>pagePathSuffix</code>
184
     * specifies the specific page to navigate to. For example:
185
     * <ol>
186
     * <li>{@link #getDrupalPageBase()} returns <code>/cdm_dataportal/taxon</code></li>
187
     * <li><code>pagePathSuffix</code> gives <code>7fe8a8b6-b0ba-4869-90b3-177b76c1753f</code></li>
188
     * </ol>
189
     * Both are combined to form the URL path element <code>/cdm_dataportal/taxon/7fe8a8b6-b0ba-4869-90b3-177b76c1753f</code>
190
     *
191
     */
192
    public PortalPage(WebDriver driver, DataPortalContext context, String pagePathSuffix) throws MalformedURLException {
193
        this(driver, context, pagePathSuffix, null);
194
    }
195

    
196
    /**
197
     *
198
     */
199
    protected void pageHealthChecks() {
200
        try {
201
            if(getAciveHealthChecks().contains(HealthChecks.NO_ERROR)) {
202
                String ignore_error = null;
203
                List<String> errors = getErrors().stream().filter(str -> ignore_error == null || !str.startsWith(ignore_error)).collect(Collectors.toList());
204
                assertTrue("The page must not show an error box: (" + String.join(" | ", errors) + ")", errors.size() == 0);
205
            }
206
            if(getAciveHealthChecks().contains(HealthChecks.NO_WARNING)) {
207
                String ignore_warning = null;
208
                List<String> warnings = getWarnings().stream().filter(str -> ignore_warning == null || !str.startsWith(ignore_warning)).collect(Collectors.toList());
209
                assertTrue("The page must not show an warning box: (" + String.join(" | ", warnings) + ")", warnings.size() == 0);
210
            }
211
        } catch (NoSuchElementException e) {
212
            //IGNORE since this is expected!
213
        }
214
        assertFalse("The default footnote list key PAGE_GLOBAL must not occur in the page.", driver.getPageSource().contains("member-of-footnotes-PAGE_GLOBAL"));
215
    }
216

    
217

    
218
    /**
219
     * Creates a new PortaPage at given URL location. An Exception is thrown if
220
     * this URL is not matching the expected URL for the specific page type.
221
     *
222
     */
223
    public PortalPage(WebDriver driver, DataPortalContext context, URL url) throws Exception {
224

    
225
        this.driver = driver;
226

    
227
        this.context = context;
228

    
229
        this.wait = new JUnitWebDriverWait(driver, 25);
230

    
231
        this.pageUrl = new URL(context.getSiteUri().toString());
232

    
233
        // tell browser to navigate to the given URL
234
        driver.get(url.toString());
235

    
236
        takeScreenShot();
237

    
238
        if(!isOnPage()){
239
            throw new Exception("Not on the expected portal page ( current: " + driver.getCurrentUrl() + ", expected: " +  pageUrl + " )");
240
        }
241

    
242
        this.pageUrl = url;
243

    
244
        logger.info("loading " + pageUrl);
245

    
246
        // This call sets the WebElement fields.
247
        PageFactory.initElements(driver, this);
248

    
249
    }
250

    
251
    /**
252
     * Creates a new PortaPage at the WebDrivers current URL location. An Exception is thrown if
253
     * driver.getCurrentUrl() is not matching the expected URL for the specific page type.
254
     *
255
     */
256
    public PortalPage(WebDriver driver, DataPortalContext context) throws Exception {
257

    
258
        this.driver = driver;
259

    
260
        this.context = context;
261

    
262
        this.wait = new JUnitWebDriverWait(driver, 25);
263

    
264
        // preliminary set the pageUrl to the base path of this page, this is used in the next setp to check if the
265
        // driver.getCurrentUrl() is a sub path of the base path
266
        this.pageUrl = new URL(context.getSiteUri().toString());
267

    
268
        takeScreenShot();
269

    
270
        if(!isOnPage()){
271
            throw new Exception("Not on the expected portal page ( current: " + driver.getCurrentUrl() + ", expected: " +  pageUrl + " )");
272
        }
273

    
274
        // now set the real URL
275
        this.pageUrl = new URL(driver.getCurrentUrl());
276

    
277
        logger.info("loading " + pageUrl);
278

    
279
        // This call sets the WebElement fields.
280
        PageFactory.initElements(driver, this);
281

    
282
    }
283

    
284

    
285
    protected boolean isOnPage() {
286
        return driver.getCurrentUrl().startsWith(pageUrl.toString());
287
    }
288

    
289
    /**
290
     * navigate and reload the page if not yet there
291
     */
292
    public void get() {
293
        if(!driver.getCurrentUrl().equals(pageUrl.toString())){
294
            driver.get(pageUrl.toString());
295
            wait.until(new UrlLoaded(pageUrl.toString()));
296
            // take screenshot of new page
297
            takeScreenShot();
298
            PageFactory.initElements(driver, this);
299
        }
300
    }
301

    
302
    /**
303
     * go back in history
304
     */
305
    public void back() {
306
        driver.navigate().back();
307
    }
308

    
309
    public String getDrupalPagePath() {
310
        URL currentURL;
311
        try {
312
            currentURL = new URL(driver.getCurrentUrl());
313
            if(currentURL.getQuery() != null && currentURL.getQuery().contains(DRUPAL_PAGE_QUERY)){
314
                Matcher m = DRUPAL_PAGE_QUERY_PATTERN.matcher(currentURL.getQuery());
315
                m.matches();
316
                return m.group(1);
317
            } else {
318
                String uriBasePath = context.getSiteUri().getPath();
319
                if(!uriBasePath.endsWith("/")){
320
                    uriBasePath += "/";
321
                }
322
                return currentURL.getPath().replaceFirst("^"+ uriBasePath, "");
323
            }
324
        } catch (MalformedURLException e) {
325
            throw new RuntimeException(e);
326
        }
327
    }
328

    
329
    /**
330
     * Returns the the page path with which the page has initially been loaded.
331
     * Due to redirects the actual page path which can be retrieved by
332
     * {@link #getDrupalPagePath()} might be different
333
     *
334
     */
335
    public String getInitialDrupalPagePath() {
336
        return initialDrupalPagePath;
337
    }
338

    
339
    /**
340
     *
341
     * @return the page title
342
     * @deprecated use {@link WebDriver#getTitle()}
343
     */
344
    @Deprecated
345
    public String getTitle() {
346
        return driver.getTitle();
347
    }
348

    
349
    public List<String> getMessageItems(MessageType messageType){
350
        if(messages != null){
351
            for(WebElement m : messages){
352
                if(m.getAttribute("class").contains(messageType.name())){
353
                    // need to check for two optional layouts
354
                    // 1. multiple messages in a list
355
                    List<WebElement> messageItems = m.findElements(By.cssSelector("ul.messages__list li"));
356
                    if(messageItems.size() == 0 && !m.getText().isEmpty()) {
357
                        // 2. one message only
358
                        // we have only one item which is not shown as list.
359
                        messageItems.add(m);
360

    
361
                    }
362
                    return messageItems.stream().map(mi -> mi.getText()).collect(Collectors.toList());
363
                }
364
            }
365
        }
366
        return new ArrayList<>();
367
    }
368

    
369
    /**
370
     * @return the warning messages from the Drupal message box
371
     */
372
    public List<String> getWarnings() {
373
        return getMessageItems(MessageType.warning);
374
    }
375

    
376
    /**
377
     * @return the error messages from the Drupal message box
378
     */
379
    public List<String> getErrors() {
380
        return getMessageItems(MessageType.error);
381
    }
382

    
383
    public String getAuthorInformationText() {
384

    
385
        WebElement authorInformation = null;
386

    
387
        try {
388
            authorInformation  = node.findElement(By.className("submitted"));
389
        } catch (NoSuchElementException e) {
390
            // IGNORE //
391
        }
392

    
393

    
394
        if(authorInformation != null){
395
            return authorInformation.getText();
396
        } else {
397
            return null;
398
        }
399
    }
400

    
401
    public List<LinkElement> getPrimaryTabs(){
402
        List<LinkElement> tabs = new ArrayList<LinkElement>();
403
        List<WebElement> primaryTabLinks = null;
404
        if(isZenTheme()) {
405
            primaryTabLinks = driver.findElements(By.cssSelector(".tabs-primary li a"));
406
        } else {
407
            // try the old garland theme
408
            primaryTabLinks = driver.findElements(By.cssSelector("#tabs-wrapper ul.primary li a"));
409
        }
410

    
411
        for(WebElement a : primaryTabLinks) {
412
            WebElement renderedLink = a;
413
            if(renderedLink.isDisplayed()){
414
                tabs.add(new LinkElement(renderedLink));
415
            }
416
        }
417

    
418
        return tabs;
419
    }
420

    
421
    /**
422
     * @return the active primary tab
423
     */
424
    public Optional<LinkElement> getActivePrimaryTab() {
425
        List<LinkElement> primaryTabs = getPrimaryTabs();
426
        return primaryTabs.stream()
427
                .filter(we -> we.getClassAttributes().stream().anyMatch(ca -> ca.equals("active")))
428
                .findFirst();
429

    
430
    }
431

    
432
    /**
433
     * Provides access to the the CDM specific main content <code>div</code> element.
434
     *
435
     *
436
     * @return the main content div
437
     */
438
    public BaseElement getDataPortalContent() {
439
        if(mainContent == null) {
440
            if(isZenTheme()) {
441
                mainContent = new BaseElement(driver.findElement(By.id("content")));
442
            } else {
443
                // fallback to garland
444
                mainContent = new BaseElement(driver.findElement(By.id("squeeze")));
445
            }
446
        }
447
        return mainContent;
448
    }
449

    
450
    public ClassificationTreeBlock getClassificationTree() {
451
        return new ClassificationTreeBlock(classificationBrowserBlock);
452
    }
453

    
454
    public void hover(WebElement element) {
455
        Actions actions = new Actions(driver);
456
        actions.moveToElement(element, 1, 1).perform();
457
        logger.debug("hovering");
458
    }
459

    
460

    
461
    /**
462
     * @return the current URL string from the {@link WebDriver}
463
     */
464
    public URL getPageURL() {
465
        return pageUrl;
466
    }
467

    
468

    
469
    /**
470
     * @return the <code>scheme://domain:port</code> part of the initial url of this page.
471
     */
472
    public String getInitialUrlBase() {
473
        return pageUrl.getProtocol() + "://" + pageUrl.getHost() + pageUrl.getPort();
474
    }
475

    
476
    @Override
477
    public boolean equals(Object obj) {
478
        if (PortalPage.class.isAssignableFrom(obj.getClass())) {
479
            PortalPage page = (PortalPage) obj;
480
            return this.getPageURL().toString().equals(page.getPageURL().toString());
481

    
482
        } else {
483
            return false;
484
        }
485
    }
486

    
487

    
488
    /**
489
     * @param isTrue see {@link org.openqa.selenium.support.ui.FluentWait#until(Function)}
490
     * @param pageType the return type
491
     * @param duration may be null, if this in null <code>waitUnit</code> will be ignored.
492
     * @param waitUnit may be null, is ignored if <code>duration</code> is null defaults to {@link TimeUnit#SECONDS}
493

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

    
497
        if( pageType!= null && pageType.getClass().equals(PortalPage.class) ) {
498
            throw new RuntimeException("Parameter pageType must be a subclass of PortalPage");
499
        }
500
        String targetWindow = null;
501
        List<String> targets = element.getLinkTargets(driver);
502
        if(targets.size() > 0){
503
            targetWindow = targets.get(0);
504
        }
505

    
506
        if(logger.isInfoEnabled() || logger.isDebugEnabled()){
507
            logger.info("clickLink() on " + element.toStringWithLinks());
508
        }
509
        element.getElement().click();
510
        if(targetWindow != null){
511
            driver.switchTo().window(targetWindow);
512
        }
513

    
514
        try {
515
            if(duration != null){
516
                if(waitUnit == null){
517
                    waitUnit = TimeUnit.SECONDS;
518
                }
519
                wait.withTimeout(duration, waitUnit).until(isTrue);
520
            } else {
521
                wait.until(isTrue);
522
            }
523
        } catch (AssertionError timeout){
524
            logger.info("clickLink timed out. Current WindowHandles:" + driver.getWindowHandles());
525
            throw timeout;
526
        }
527

    
528

    
529
        Constructor<T> constructor;
530
        T pageInstance;
531
            if(pageType != null) {
532
            try {
533
                constructor = pageType.getConstructor(WebDriver.class, DataPortalContext.class);
534
                pageInstance = constructor.newInstance(driver, context);
535
            } catch (Exception e) {
536
                throw new RuntimeException(e);
537
            }
538
            // take screenshot of new page
539
            takeScreenShot();
540
            return pageInstance;
541
        } else {
542
            return null;
543
        }
544
    }
545

    
546

    
547
    /**
548
     * @param isTrue see {@link org.openqa.selenium.support.ui.FluentWait#until(Function)}
549
     * @param type the return type
550

    
551
     */
552
    public <T extends PortalPage> T clickLink(BaseElement element, Function<? super WebDriver, Boolean> isTrue, Class<T> type) {
553
        return clickLink(element, isTrue, type, null, null);
554
    }
555

    
556

    
557
    /**
558
     * replaces all underscores '_' by hyphens '-'
559
     */
560
    protected String normalizeClassAttribute(String featureName) {
561
        return featureName.replace('_', '-');
562
    }
563

    
564
    public File takeScreenShot(){
565

    
566

    
567
        logger.info("Screenshot ...");
568
        File destFile = fileForTestMethod(new File("screenshots"));
569
        if(logger.isDebugEnabled()){
570
            logger.debug("Screenshot destFile" + destFile.getPath().toString());
571
        }
572
        File scrFile = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
573
        try {
574
            FileUtils.copyFile(scrFile, destFile);
575
            logger.info("Screenshot taken and saved as " + destFile.getAbsolutePath());
576
            return destFile;
577
        } catch (IOException e) {
578
            logger.error("could not copy sceenshot to " + destFile.getAbsolutePath(), e);
579
        }
580

    
581
        return null;
582

    
583
    }
584

    
585
    /**
586
     * Finds the test class and method in the stack trace which is using the
587
     * PortalPage and returns a File object consisting of:
588
     * {@code $targetFolder/$className/$methodName }
589
     *
590
     *
591
     * If no test class is found it will fall back to using
592
     * "noTest" as folder name and a timestamp as filename.
593
     */
594
    private File fileForTestMethod(File targetFolder){
595

    
596
        StackTraceElement[] trace = Thread.currentThread().getStackTrace();
597
        for (StackTraceElement stackTraceElement : trace) {
598
            // according to the convention all test class names should end with "Test"
599
            if(logger.isTraceEnabled()){
600
                logger.trace("fileForTestMethod() - " + stackTraceElement.toString());
601
            }
602
            if(stackTraceElement.getClassName().endsWith("Test")){
603
                return uniqueIndexedFile(
604
                            targetFolder.getAbsolutePath() + File.separator + stackTraceElement.getClassName(),
605
                            stackTraceElement.getMethodName(),
606
                            "png");
607

    
608
            }
609
        }
610
        return uniqueIndexedFile(
611
                targetFolder.getAbsolutePath() + File.separator + "noTest",
612
                Long.toString(System.currentTimeMillis()),
613
                "png");
614
    }
615

    
616

    
617
    private File uniqueIndexedFile(String folder, String fileName, String suffix){
618
        File file;
619
        int i = 0;
620
        while(true){
621
            file = new File(folder + File.separator + fileName + "_" + Integer.toString(i++) + "." + suffix);
622
            if(!file.exists()){
623
                return file;
624
            }
625
        }
626
    }
627

    
628

    
629
}
(4-4/9)