/* * Copyright 2016 GoDataDriven B.V. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.divolte.server; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import io.divolte.server.ServerTestUtils.EventPayload; import io.divolte.server.config.BrowserSourceConfiguration; import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.support.ui.WebDriverWait; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.ParametersAreNonnullByDefault; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import static io.divolte.server.IncomingRequestProcessor.DUPLICATE_EVENT_KEY; import static io.divolte.server.SeleniumTestBase.TestPages.*; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; @ParametersAreNonnullByDefault public class SeleniumJavaScriptTest extends SeleniumTestBase { private static final Logger logger = LoggerFactory.getLogger(SeleniumJavaScriptTest.class); @Test public void shouldRegenerateIDsOnExplicitNavigation() throws Exception { doSetUp(); Preconditions.checkState(null != server); // do a sequence of explicit navigation by setting the browser location // and then check that all requests generated a unique pageview ID final Runnable[] actions = { () -> gotoPage(BASIC), () -> gotoPage(BASIC_COPY), () -> gotoPage(BASIC), }; final int numberOfUniquePageViewIDs = uniquePageViewIdsForSeriesOfActions(actions); assertEquals(actions.length, numberOfUniquePageViewIDs); } @Test public void shouldRegenerateIDsOnRefresh() throws Exception { doSetUp(); Preconditions.checkState(null != driver && null != server); // Navigate to the same page twice final Runnable[] actions = { () -> gotoPage(BASIC), driver.navigate()::refresh }; final int numberOfUniquePageViewIDs = uniquePageViewIdsForSeriesOfActions(actions); assertEquals(actions.length, numberOfUniquePageViewIDs); } @Test public void shouldRegenerateIDsOnBackNavigation() throws Exception { doSetUp(); Preconditions.checkState(null != driver && null != server); // Navigate to the same page twice final Runnable[] actions = { () -> gotoPage(BASIC), () -> gotoPage(BASIC_COPY), driver.navigate()::back, }; final int numberOfUniquePageViewIDs = uniquePageViewIdsForSeriesOfActions(actions); assertEquals(actions.length, numberOfUniquePageViewIDs); } @Test public void shouldRegenerateIDsOnForwardNavigation() throws Exception { doSetUp(); Preconditions.checkState(null != driver && null != server); // Navigate to the same page twice final Runnable[] actions = { () -> gotoPage(BASIC), () -> gotoPage(BASIC_COPY), driver.navigate()::back, driver.navigate()::forward, }; final int numberOfUniquePageViewIDs = uniquePageViewIdsForSeriesOfActions(actions); assertEquals(actions.length, numberOfUniquePageViewIDs); } @Test public void shouldGenerateIDsOnComplexSeriesOfEvents() throws Exception { doSetUp(); Preconditions.checkState(null != driver && null != server); // Navigate to the same page twice final Runnable[] actions = { () -> gotoPage(BASIC), () -> gotoPage(BASIC_COPY), () -> gotoPage(BASIC), () -> gotoPage(BASIC_COPY), () -> gotoPage(BASIC), driver.navigate()::back, driver.navigate()::back, () -> driver.findElement(By.id("custom")).click(), driver.navigate()::forward, driver.navigate()::refresh, driver.navigate()::back, () -> gotoPage(PAGE_VIEW_SUPPLIED), driver.navigate()::back }; // We expect one duplicate PV ID, because of the custom event final int numberOfUniquePageViewIDs = uniquePageViewIdsForSeriesOfActions(actions); assertEquals(actions.length - 1, numberOfUniquePageViewIDs); } private int uniquePageViewIdsForSeriesOfActions(final Runnable[] actions) { Preconditions.checkState(null != server); logger.info("Starting sequence of {} browser actions.", actions.length); return IntStream.range(0, actions.length) .mapToObj((index) -> { final Runnable action = actions[index]; logger.info("Issuing browser action #{}.", index); action.run(); logger.debug("Waiting for event from server."); final EventPayload payload = unchecked(server::waitForEvent); final DivolteEvent event = payload.event; final Optional<String> pageViewId = event.browserEventData.map(b -> b.pageViewId); logger.info("Browser action #{} yielded pageview/event: {}/{}", index, pageViewId, event.eventId); return pageViewId; }) .flatMap((pageViewId) -> pageViewId.map(Stream::of).orElse(null)) .collect(Collectors.toSet()).size(); } @FunctionalInterface private interface ExceptionSupplier<T> { T supply() throws Exception; } private static <T> T unchecked(final ExceptionSupplier<T> supplier) { try { return supplier.supply(); } catch (final RuntimeException e) { // Pass through as-is; throw e; } catch (final Exception e) { throw new RuntimeException(e); } } @Test public void shouldSignalWhenOpeningPage() throws Exception { doSetUp(); Preconditions.checkState(null != server); final String location = gotoPage(BASIC); final EventPayload payload = server.waitForEvent(); final DivolteEvent eventData = payload.event; final Boolean detectedDuplicate = payload.event.exchange.getAttachment(DUPLICATE_EVENT_KEY); assertFalse(eventData.corruptEvent); assertFalse(detectedDuplicate); assertFalse(Strings.isNullOrEmpty(eventData.partyId.value)); assertTrue(eventData.newPartyId); assertFalse(Strings.isNullOrEmpty(eventData.sessionId.value)); assertTrue(eventData.firstInSession); assertTrue(eventData.browserEventData.isPresent()); final DivolteEvent.BrowserEventData browserEventData = eventData.browserEventData.get(); assertFalse(Strings.isNullOrEmpty(browserEventData.pageViewId)); assertFalse(Strings.isNullOrEmpty(eventData.eventId)); assertTrue(eventData.eventType.isPresent()); assertEquals("pageView", eventData.eventType.get()); assertTrue(browserEventData.location.isPresent()); assertEquals(location, browserEventData.location.get()); /* * We don't really know anything about the clock on the executing browser, * but we'd expect it to be a reasonably accurate clock on the same planet. * So, if it is within +/- 12 hours of our clock, we think it's fine. */ final Instant now = Instant.now(); assertThat(eventData.clientTime, allOf(greaterThan(now.minus(12, ChronoUnit.HOURS)), lessThan(now.plus(12, ChronoUnit.HOURS)))); /* * Doing true assertions against the viewport and window size * is problematic on different devices, as the number do not * always make sense on SauceLabs. Also, sometimes the window * is partially outside of the screen view port or other strange * things. It gets additionally complicated on mobile devices. * * Hence, we just check whether these are integers greater than 50. */ assertTrue(browserEventData.viewportPixelWidth.isPresent()); assertThat(browserEventData.viewportPixelWidth.get(), greaterThan(50)); assertTrue(browserEventData.viewportPixelHeight.isPresent()); assertThat(browserEventData.viewportPixelHeight.get(), greaterThan(50)); assertTrue(browserEventData.screenPixelWidth.isPresent()); assertThat(browserEventData.screenPixelWidth.get(), greaterThan(50)); assertTrue(browserEventData.screenPixelHeight.isPresent()); assertThat(browserEventData.screenPixelHeight.get(), greaterThan(50)); } @Test public void shouldSendCustomEvent() throws Exception { doSetUp(); Preconditions.checkState(null != driver && null != server); gotoPage(BASIC); server.waitForEvent(); driver.findElement(By.id("custom")).click(); final EventPayload payload = server.waitForEvent(); final DivolteEvent eventData = payload.event; assertTrue(eventData.eventType.isPresent()); assertEquals("custom", eventData.eventType.get()); final Optional<String> customEventParameters = eventData.eventParametersProducer.get().map(Object::toString); assertTrue(customEventParameters.isPresent()); assertEquals("{\"a\":{}," + "\"b\":\"c\"," + "\"d\":{\"a\":[],\"b\":\"g\"}," + "\"e\":[\"1\",\"2\"]," + "\"f\":42," + "\"g\":53.2," + "\"h\":-37," + "\"i\":-7.83E-9," + "\"j\":true," + "\"k\":false," + "\"l\":null," + "\"m\":\"2015-06-13T15:49:33.002Z\"," + "\"n\":{}," + "\"o\":[{},{\"a\":\"b\"},{\"c\":\"d\"}]," + "\"p\":[null,null,{\"a\":\"b\"},\"custom\",null,{}]," + "\"q\":{}}", customEventParameters.get()); } @Test public void shouldSetAppropriateCookies() throws Exception { doSetUp(); Preconditions.checkState(null != driver && null != server); gotoPage(BASIC); server.waitForEvent(); final Optional<DivolteIdentifier> parsedPartyCookieOption = DivolteIdentifier.tryParse(driver.manage().getCookieNamed(BrowserSourceConfiguration.DEFAULT_BROWSER_SOURCE_CONFIGURATION.partyCookie).getValue()); assertTrue(parsedPartyCookieOption.isPresent()); assertThat( parsedPartyCookieOption.get(), isA(DivolteIdentifier.class)); final Optional<DivolteIdentifier> parsedSessionCookieOption = DivolteIdentifier.tryParse(driver.manage().getCookieNamed(BrowserSourceConfiguration.DEFAULT_BROWSER_SOURCE_CONFIGURATION.sessionCookie).getValue()); assertTrue(parsedSessionCookieOption.isPresent()); assertThat( parsedSessionCookieOption.get(), isA(DivolteIdentifier.class)); } @Test public void shouldPickupProvidedPageViewIdFromHash() throws Exception { doSetUp(); Preconditions.checkState(null != server); gotoPage(PAGE_VIEW_SUPPLIED); final EventPayload payload = server.waitForEvent(); final DivolteEvent eventData = payload.event; assertEquals(Optional.of("supercalifragilisticexpialidocious"), eventData.browserEventData.map(bed -> bed.pageViewId)); assertEquals("supercalifragilisticexpialidocious0", eventData.eventId); } @Test public void shouldSupportCustomJavascriptName() throws Exception { doSetUp("selenium-test-custom-javascript-name.conf"); Preconditions.checkState(null != server); gotoPage(CUSTOM_JAVASCRIPT_NAME); final EventPayload payload = server.waitForEvent(); final DivolteEvent eventData = payload.event; assertEquals(Optional.of("pageView"), eventData.eventType); } @Test public void shouldUseConfiguredEventSuffix() throws Exception { doSetUp("selenium-test-custom-event-suffix.conf"); Preconditions.checkState(null != server); gotoPage(BASIC); final EventPayload payload = server.waitForEvent(); final DivolteEvent eventData = payload.event; assertEquals(Optional.of("pageView"), eventData.eventType); } @Test public void shouldAdvanceToNextEventOnTimeout() throws Exception { doSetUp("selenium-test-slow-server.conf"); Preconditions.checkState(null != server && null != driver); gotoPage(BASIC); // No page-view events. // Emit two events. logger.info("Clicking link that will trigger 2 slow events"); driver.findElement(By.id("deliver2events")).click(); /* * The server has been configured to delay responses by 15 seconds. * The JavaScript should time them out quickly. * This means both events should arrive within the time here, * instead of waiting for the delayed responses. */ logger.info("Waiting for first event"); server.waitForEvent(); logger.info("First event received; waiting for second event."); server.waitForEvent(); logger.info("Second event received."); } @Test public void shouldFlushEventsBeforeClickOut() throws Exception { doSetUp(); Preconditions.checkState(null != server && null != driver); gotoPage(EVENT_COMMIT); assertEquals(Optional.of("pageView"), server.waitForEvent().event.eventType); // Record the current page URL, so that later we can check we navigated away. final String initialPageUrl = driver.getCurrentUrl(); // Locate the link to click on, and click it. logger.info("Clicking link that will trigger events"); driver.findElement(By.id("clickout")).click(); // At this point 10 events should arrive, after which the browser navigates // to the basic page. for (int i = 0; i < 10; ++i) { final DivolteEvent eventData = server.waitForEvent().event; assertEquals("Unexpected event type for event #" + i, Optional.of("clickOutEvent"), eventData.eventType); assertEquals("Event index parameter incorrect for event #" + i, Optional.of(i), eventData.eventParametersProducer.get().map(jsonNode -> jsonNode.get("i").asInt())); } logger.info("Triggered events have all arrived."); // Check that the browser did navigate to a new page. (It will also generate a page-view.) final WebDriverWait wait = new WebDriverWait(driver, 30); wait.until(driver -> !driver.getCurrentUrl().equals(initialPageUrl)); assertEquals(Optional.of("pageView"), server.waitForEvent().event.eventType); } @Test public void shouldInvokeFlushCallbackImmediatelyIfNoEventsPending() throws Exception { doSetUp(); Preconditions.checkState(null != server && null != driver); gotoPage(EVENT_COMMIT); assertEquals(Optional.of("pageView"), server.waitForEvent().event.eventType); // Hit the 'flush' link, and wait for the event. driver.findElement(By.id("flushdirect")).click(); // The event contains information about whether the callback was invoked immediately or not. final DivolteEvent event = server.waitForEvent().event; assertEquals(Optional.of("firstFlush"), event.eventType); assertEquals(Optional.of(true), event.eventParametersProducer.get().map(jsonNode -> jsonNode.get("callbackWasImmediate").asBoolean())); } @Test public void shouldTimeoutIfRequiredOnFlushBeforeClickOut() throws Exception { doSetUp(); Preconditions.checkState(null != server && null != driver); gotoPage(EVENT_COMMIT); assertEquals(Optional.of("pageView"), server.waitForEvent().event.eventType); // Record the current page URL, so that later we can check we navigated away. final String initialPageUrl = driver.getCurrentUrl(); // Locate the link to click on, and click it. logger.info("Clicking link that will trigger events"); driver.findElement(By.id("quickout")).click(); // At this point lots of events should arrive, but before we reach 100000 the // flush should have timed-out and navigated to the new page. int count = 0; DivolteEvent lastEventData; do { lastEventData = server.waitForEvent().event; if (!Optional.of("clickOutEvent").equals(lastEventData.eventType)) { break; } ++count; } while (count < 1000); assertThat(count, is(both(greaterThan(0)).and(lessThan(1000)))); logger.info("Triggered events have all arrived: {}", count); // Check that the browser did navigate to a new page. It will have generated a new page-view. final WebDriverWait wait = new WebDriverWait(driver, 10); wait.until(driver -> !driver.getCurrentUrl().equals(initialPageUrl)); assertEquals(Optional.of("pageView"), lastEventData.eventType); } }