Project

General

Profile

Download (20 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.concurrent.TimeUnit;
17
import java.util.regex.Matcher;
18
import java.util.regex.Pattern;
19
import java.util.stream.Collectors;
20

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

    
35
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
36
import com.google.common.base.Function;
37

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

    
45
/**
46
 * FIXME only works with the cichorieae theme
47
 *
48
 * @author a.kohlbecker
49
 *
50
 */
51
public abstract class PortalPage {
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
    public static enum HealthChecks {
74
        NO_ERROR, NO_WARNING;
75
    }
76

    
77
    public EnumSet<HealthChecks> getAciveHealthChecks() {
78
        return aciveHealthChecks;
79
    }
80

    
81

    
82
    public void setAciveHealthChecks(EnumSet<HealthChecks> aciveHealthChecks) {
83
        this.aciveHealthChecks = aciveHealthChecks;
84
    }
85

    
86
    private EnumSet<HealthChecks> aciveHealthChecks = EnumSet.allOf(HealthChecks.class);
87

    
88

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

    
97
    private String initialDrupalPagePath;
98

    
99
    protected URL pageUrl;
100

    
101
    // ==== WebElements === //
102

    
103
    protected BaseElement mainContent = null;
104

    
105
    @FindBy(className="node")
106
    protected WebElement node;
107

    
108

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

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

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

    
121
    private Boolean isZenTheme;
122

    
123

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

    
136

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

    
150
        this.driver = driver;
151

    
152
        this.context = context;
153

    
154
        this.wait = new JUnitWebDriverWait(driver, WAIT_SECONDS);
155

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

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

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

    
171
        takeScreenShot();
172

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

    
176
        pageHealthChecks();
177
    }
178

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

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

    
215

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

    
223
        this.driver = driver;
224

    
225
        this.context = context;
226

    
227
        this.wait = new JUnitWebDriverWait(driver, 25);
228

    
229
        this.pageUrl = new URL(context.getSiteUri().toString());
230

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

    
234
        takeScreenShot();
235

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

    
240
        this.pageUrl = url;
241

    
242
        logger.info("loading " + pageUrl);
243

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

    
247
    }
248

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

    
256
        this.driver = driver;
257

    
258
        this.context = context;
259

    
260
        this.wait = new JUnitWebDriverWait(driver, 25);
261

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

    
266
        takeScreenShot();
267

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

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

    
275
        logger.info("loading " + pageUrl);
276

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

    
280
    }
281

    
282

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

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

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

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

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

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

    
347
    public List<String> getMessageItems(MessageType messageType){
348
        if(messages != null){
349
            for(WebElement m : messages){
350
                if(m.getAttribute("class").contains(messageType.name())){
351
                    List<WebElement> messageItems = m.findElements(By.cssSelector("ul.messages__list li"));
352
                    if(messageItems.size() == 0 && !m.getText().isEmpty()) {
353
                        // we have only one item which is not shown as list.
354
                        messageItems.add(m);
355

    
356
                    }
357
                    return messageItems.stream().map(mi -> mi.getText()).collect(Collectors.toList());
358
                }
359
            }
360
        }
361
        return new ArrayList<>();
362
    }
363

    
364
    /**
365
     * @return the warning messages from the Drupal message box
366
     */
367
    public List<String> getWarnings() {
368
        return getMessageItems(MessageType.warning);
369
    }
370

    
371
    /**
372
     * @return the error messages from the Drupal message box
373
     */
374
    public List<String> getErrors() {
375
        return getMessageItems(MessageType.error);
376
    }
377

    
378
    public String getAuthorInformationText() {
379

    
380
        WebElement authorInformation = null;
381

    
382
        try {
383
            authorInformation  = node.findElement(By.className("submitted"));
384
        } catch (NoSuchElementException e) {
385
            // IGNORE //
386
        }
387

    
388

    
389
        if(authorInformation != null){
390
            return authorInformation.getText();
391
        } else {
392
            return null;
393
        }
394
    }
395

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

    
406
        for(WebElement a : primaryTabLinks) {
407
            WebElement renderedLink = a;
408
            if(renderedLink.isDisplayed()){
409
                tabs.add(new LinkElement(renderedLink));
410
            }
411
        }
412

    
413
        return tabs;
414
    }
415

    
416
    /**
417
     * Provides access to the the CDM specific main content <code>div</code> element.
418
     *
419
     *
420
     * @return the main content div
421
     */
422
    public BaseElement getDataPortalContent() {
423
        if(mainContent == null) {
424
            if(isZenTheme()) {
425
                mainContent = new BaseElement(driver.findElement(By.id("content")));
426
            } else {
427
                // fallback to garland
428
                mainContent = new BaseElement(driver.findElement(By.id("squeeze")));
429
            }
430
        }
431
        return mainContent;
432
    }
433

    
434
    public ClassificationTreeBlock getClassificationTree() {
435
        return new ClassificationTreeBlock(classificationBrowserBlock);
436
    }
437

    
438
    public void hover(WebElement element) {
439
        Actions actions = new Actions(driver);
440
        actions.moveToElement(element, 1, 1).perform();
441
        logger.debug("hovering");
442
    }
443

    
444

    
445
    /**
446
     * @return the current URL string from the {@link WebDriver}
447
     */
448
    public URL getPageURL() {
449
        return pageUrl;
450
    }
451

    
452

    
453
    /**
454
     * @return the <code>scheme://domain:port</code> part of the initial url of this page.
455
     */
456
    public String getInitialUrlBase() {
457
        return pageUrl.getProtocol() + "://" + pageUrl.getHost() + pageUrl.getPort();
458
    }
459

    
460
    @Override
461
    public boolean equals(Object obj) {
462
        if (PortalPage.class.isAssignableFrom(obj.getClass())) {
463
            PortalPage page = (PortalPage) obj;
464
            return this.getPageURL().toString().equals(page.getPageURL().toString());
465

    
466
        } else {
467
            return false;
468
        }
469
    }
470

    
471

    
472
    /**
473
     * @param isTrue see {@link org.openqa.selenium.support.ui.FluentWait#until(Function)}
474
     * @param pageType the return type
475
     * @param duration may be null, if this in null <code>waitUnit</code> will be ignored.
476
     * @param waitUnit may be null, is ignored if <code>duration</code> is null defaults to {@link TimeUnit#SECONDS}
477

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

    
481
        if( pageType!= null && pageType.getClass().equals(PortalPage.class) ) {
482
            throw new RuntimeException("Parameter pageType must be a subclass of PortalPage");
483
        }
484
        String targetWindow = null;
485
        List<String> targets = element.getLinkTargets(driver);
486
        if(targets.size() > 0){
487
            targetWindow = targets.get(0);
488
        }
489

    
490
        if(logger.isInfoEnabled() || logger.isDebugEnabled()){
491
            logger.info("clickLink() on " + element.toStringWithLinks());
492
        }
493
        element.getElement().click();
494
        if(targetWindow != null){
495
            driver.switchTo().window(targetWindow);
496
        }
497

    
498
        try {
499
            if(duration != null){
500
                if(waitUnit == null){
501
                    waitUnit = TimeUnit.SECONDS;
502
                }
503
                wait.withTimeout(duration, waitUnit).until(isTrue);
504
            } else {
505
                wait.until(isTrue);
506
            }
507
        } catch (AssertionError timeout){
508
            logger.info("clickLink timed out. Current WindowHandles:" + driver.getWindowHandles());
509
            throw timeout;
510
        }
511

    
512

    
513
        Constructor<T> constructor;
514
        T pageInstance;
515
            if(pageType != null) {
516
            try {
517
                constructor = pageType.getConstructor(WebDriver.class, DataPortalContext.class);
518
                pageInstance = constructor.newInstance(driver, context);
519
            } catch (Exception e) {
520
                throw new RuntimeException(e);
521
            }
522
            // take screenshot of new page
523
            takeScreenShot();
524
            return pageInstance;
525
        } else {
526
            return null;
527
        }
528
    }
529

    
530

    
531
    /**
532
     * @param isTrue see {@link org.openqa.selenium.support.ui.FluentWait#until(Function)}
533
     * @param type the return type
534

    
535
     */
536
    public <T extends PortalPage> T clickLink(BaseElement element, Function<? super WebDriver, Boolean> isTrue, Class<T> type) {
537
        return clickLink(element, isTrue, type, null, null);
538
    }
539

    
540

    
541
    /**
542
     * replaces all underscores '_' by hyphens '-'
543
     */
544
    protected String normalizeClassAttribute(String featureName) {
545
        return featureName.replace('_', '-');
546
    }
547

    
548
    public File takeScreenShot(){
549

    
550

    
551
        logger.info("Screenshot ...");
552
        File destFile = fileForTestMethod(new File("screenshots"));
553
        if(logger.isDebugEnabled()){
554
            logger.debug("Screenshot destFile" + destFile.getPath().toString());
555
        }
556
        File scrFile = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
557
        try {
558
            FileUtils.copyFile(scrFile, destFile);
559
            logger.info("Screenshot taken and saved as " + destFile.getAbsolutePath());
560
            return destFile;
561
        } catch (IOException e) {
562
            logger.error("could not copy sceenshot to " + destFile.getAbsolutePath(), e);
563
        }
564

    
565
        return null;
566

    
567
    }
568

    
569
    /**
570
     * Finds the test class and method in the stack trace which is using the
571
     * PortalPage and returns a File object consisting of:
572
     * {@code $targetFolder/$className/$methodName }
573
     *
574
     *
575
     * If no test class is found it will fall back to using
576
     * "noTest" as folder name and a timestamp as filename.
577
     */
578
    private File fileForTestMethod(File targetFolder){
579

    
580
        StackTraceElement[] trace = Thread.currentThread().getStackTrace();
581
        for (StackTraceElement stackTraceElement : trace) {
582
            // according to the convention all test class names should end with "Test"
583
            if(logger.isTraceEnabled()){
584
                logger.trace("fileForTestMethod() - " + stackTraceElement.toString());
585
            }
586
            if(stackTraceElement.getClassName().endsWith("Test")){
587
                return uniqueIndexedFile(
588
                            targetFolder.getAbsolutePath() + File.separator + stackTraceElement.getClassName(),
589
                            stackTraceElement.getMethodName(),
590
                            "png");
591

    
592
            }
593
        }
594
        return uniqueIndexedFile(
595
                targetFolder.getAbsolutePath() + File.separator + "noTest",
596
                Long.toString(System.currentTimeMillis()),
597
                "png");
598
    }
599

    
600

    
601
    private File uniqueIndexedFile(String folder, String fileName, String suffix){
602
        File file;
603
        int i = 0;
604
        while(true){
605
            file = new File(folder + File.separator + fileName + "_" + Integer.toString(i++) + "." + suffix);
606
            if(!file.exists()){
607
                return file;
608
            }
609
        }
610
    }
611

    
612

    
613
}
(4-4/9)