// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.test.transit.page;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static org.chromium.base.test.transit.ViewSpec.viewSpec;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.test.transit.Condition;
import org.chromium.base.test.transit.ConditionStatus;
import org.chromium.base.test.transit.ConditionStatusWithResult;
import org.chromium.base.test.transit.ConditionWithResult;
import org.chromium.base.test.transit.Elements;
import org.chromium.base.test.transit.UiThreadCondition;
import org.chromium.base.test.transit.ViewElement;
import org.chromium.base.test.transit.ViewSpec;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.test.util.Coordinates;
import org.chromium.content_public.browser.test.util.JavaScriptUtils;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
/** The screen that shows a loaded webpage with the omnibox and the toolbar. */
public class WebPageStation extends PageStation {
public static final ViewSpec URL_BAR = viewSpec(withId(R.id.url_bar));
protected Supplier<WebContents> mWebContentsSupplier;
private boolean mIgnoreUrlBar;
protected <T extends WebPageStation> WebPageStation(Builder<T> builder) {
super(builder);
}
protected <T extends WebPageStation> WebPageStation(WebStationBuilder<T> builder) {
super(builder);
mIgnoreUrlBar = builder.mIgnoreUrlBar;
}
public static class WebStationBuilder<T extends WebPageStation> extends PageStation.Builder<T> {
private boolean mIgnoreUrlBar;
public WebStationBuilder(Function<PageStation.Builder<T>, T> factoryMethod) {
super(factoryMethod);
}
/**
* Set whether URL is a required element for this webpage station. This is used for pages
* that doesn't show the URL bar (e.g. fullscreen page, or pages that scrolled off the
* browser controls).
*/
public WebStationBuilder<T> ignoreUrlBar(boolean ignoreUrlBar) {
mIgnoreUrlBar = ignoreUrlBar;
return this;
}
}
public static WebStationBuilder<WebPageStation> newBuilder() {
return new WebStationBuilder<>(WebPageStation::new);
}
@Override
public void declareElements(Elements.Builder elements) {
super.declareElements(elements);
mWebContentsSupplier =
elements.declareEnterCondition(
new WebContentsPresentCondition(mPageLoadedSupplier));
elements.declareEnterCondition(new FrameInfoUpdatedCondition(mWebContentsSupplier));
if (!mIgnoreUrlBar) {
// TODO(crbug.com/41497463): This should be shared, not unscoped, but the toolbar exists
// in the tab switcher and it is not completely occluded.
elements.declareView(URL_BAR, ViewElement.unscopedOption());
}
}
/** Opens the web page app menu by pressing the toolbar "..." button */
public RegularWebPageAppMenuFacility openRegularTabAppMenu() {
assert !mIncognito;
return enterFacilitySync(new RegularWebPageAppMenuFacility(), MENU_BUTTON::click);
}
/** Opens the web page app menu by pressing the toolbar "..." button */
public IncognitoWebPageAppMenuFacility openIncognitoTabAppMenu() {
assert mIncognito;
return enterFacilitySync(new IncognitoWebPageAppMenuFacility(), MENU_BUTTON::click);
}
private static class WebContentsPresentCondition extends ConditionWithResult<WebContents> {
private final Supplier<Tab> mLoadedTabSupplier;
public WebContentsPresentCondition(Supplier<Tab> loadedTabSupplier) {
super(/* isRunOnUiThread= */ false);
mLoadedTabSupplier = dependOnSupplier(loadedTabSupplier, "LoadedTab");
}
@Override
protected ConditionStatusWithResult<WebContents> resolveWithSuppliers() {
return whether(hasValue()).withResult(get());
}
@Override
public String buildDescription() {
return "WebContents present";
}
@Override
public WebContents get() {
// Do not return a WebContents that has been destroyed, so always get it from the
// Tab instead of letting ConditionWithResult return its |mResult|.
Tab loadedTab = mLoadedTabSupplier.get();
if (loadedTab == null) {
return null;
}
return loadedTab.getWebContents();
}
@Override
public boolean hasValue() {
return get() != null;
}
}
private static class FrameInfoUpdatedCondition extends UiThreadCondition {
Supplier<WebContents> mWebContentsSupplier;
public FrameInfoUpdatedCondition(Supplier<WebContents> webContentsSupplier) {
mWebContentsSupplier = dependOnSupplier(webContentsSupplier, "WebContents");
}
@Override
protected ConditionStatus checkWithSuppliers() {
Coordinates coordinates = Coordinates.createFor(mWebContentsSupplier.get());
return whether(
coordinates.frameInfoUpdated(),
"frameInfoUpdated %b, pageScaleFactor: %.2f",
coordinates.frameInfoUpdated(),
coordinates.getPageScaleFactor());
}
@Override
public String buildDescription() {
return "WebContents frame info updated";
}
}
// Condition checks whether web page reaches the bottom by checking viewport position and scroll
// elements heights.
protected static class ScrollToBottomCondition extends Condition {
Supplier<WebContents> mWebContentsSupplier;
public ScrollToBottomCondition(Supplier<WebContents> webContentsSupplier) {
super(/* isRunOnUiThread= */ false);
mWebContentsSupplier = webContentsSupplier;
}
@Override
protected ConditionStatus checkWithSuppliers() throws Exception {
if (isPageScrolledToBottom(mWebContentsSupplier.get())) {
return fulfilled();
}
return notFulfilled("Not scrolled to the bottom yet.");
}
@Override
public String buildDescription() {
return "Page scrolled to the bottom.";
}
private static boolean isPageScrolledToBottom(WebContents wc) {
String code =
"window.visualViewport.pageTop + window.visualViewport.height "
+ ">= document.scrollingElement.scrollHeight - 1";
try {
return Boolean.parseBoolean(
JavaScriptUtils.executeJavaScriptAndWaitForResult(wc, code));
} catch (TimeoutException e) {
throw new RuntimeException(e);
}
}
}
}