chromium/base/test/android/javatests/src/org/chromium/base/test/transit/Station.java

// Copyright 2023 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.base.test.transit;

import androidx.test.espresso.Espresso;

import org.chromium.base.test.transit.Transition.TransitionOptions;
import org.chromium.base.test.transit.Transition.Trigger;

import java.util.ArrayList;
import java.util.List;

/**
 * A major {@link ConditionalState}, a "screen" the app can be in. Only one can be active at a time.
 *
 * <p>A transit-layer class should be derived from it and instantiated.
 *
 * <p>As a {@link ConditionalState}, it has a defined lifecycle and must declare {@link Elements}
 * that determine its enter and exit {@link Condition}s.
 *
 * <p>Transitions should be done with {@link Trip#travelSync(Station, Station, Trigger)}. The
 * transit-layer derived class should expose screen-specific methods for the test-layer to use.
 */
public abstract class Station extends ConditionalState {
    private static final String TAG = "Transit";
    private static int sLastStationId;

    private final int mId;
    // All facilities that have ever been entered. Exited ones remain so that the history is can
    // be queried.
    private final List<Facility<?>> mFacilities = new ArrayList<>();
    private final String mName;

    protected Station() {
        mId = sLastStationId++;
        TrafficControl.notifyCreatedStation(this);
        mName = String.format("<S%d: %s>", mId, getClass().getSimpleName());
    }

    protected List<Facility<?>> getFacilitiesWithPhase(@Phase int phase) {
        List<Facility<?>> facilities = new ArrayList<>();
        for (Facility<?> facility : mFacilities) {
            if (facility.getPhase() == phase) {
                facilities.add(facility);
            }
        }
        return facilities;
    }

    private void registerFacility(Facility<?> facility) {
        facility.setHostStation(this);
        mFacilities.add(facility);
    }

    private void assertHasFacilities(List<Facility<?>> facilities) {
        for (var facility : facilities) {
            assert mFacilities.contains(facility) : this + " does not have " + facility;
        }
    }

    @Override
    public String getName() {
        return mName;
    }

    @Override
    public String toString() {
        return mName;
    }

    /**
     * @return the self-incrementing id for logging purposes.
     */
    public int getId() {
        return mId;
    }

    /**
     * Starts a transition from this origin {@link Station} to another destination {@link Station}.
     * Runs the transition |trigger|, and blocks until the destination {@link Station} is considered
     * ACTIVE (enter Conditions are fulfilled), the origin {@link Station} is considered FINISHED
     * (exit Conditions are fulfilled), and the {@link Trip}'s transition conditions are fulfilled.
     *
     * @param destination the {@link Facility} to arrive at.
     * @param trigger the trigger to start the transition (e.g. clicking a view).
     * @return the destination {@link Station}, now ACTIVE.
     * @param <T> the type of the destination {@link Station}.
     */
    public final <T extends Station> T travelToSync(T destination, Trigger trigger) {
        Trip trip = new Trip(this, destination, TransitionOptions.DEFAULT, trigger);
        trip.transitionSync();
        return destination;
    }

    /** Version of #travelToSync() with extra TransitionOptions. */
    public final <T extends Station> T travelToSync(
            T destination, TransitionOptions options, Trigger trigger) {
        Trip trip = new Trip(this, destination, options, trigger);
        trip.transitionSync();
        return destination;
    }

    /**
     * Starts a transition into the {@link Facility}, runs the transition |trigger| and blocks until
     * the facility is considered ACTIVE (enter Conditions are fulfilled).
     *
     * @param facility the {@link Facility} to enter.
     * @param trigger the trigger to start the transition (e.g. clicking a view).
     * @return the {@link Facility} entered, now ACTIVE.
     * @param <F> the type of {@link Facility} entered.
     */
    public <F extends Facility<?>> F enterFacilitySync(F facility, Trigger trigger) {
        return enterFacilitySync(facility, TransitionOptions.DEFAULT, trigger);
    }

    /** Version of {@link #enterFacilitySync(F, Trigger)} with extra TransitionOptions. */
    public <F extends Facility<?>> F enterFacilitySync(
            F facility, TransitionOptions options, Trigger trigger) {
        registerFacility(facility);
        FacilityCheckIn checkIn = new FacilityCheckIn(facility, options, trigger);
        checkIn.transitionSync();
        return facility;
    }

    /**
     * Starts a transition out of the {@link Facility}, runs the transition |trigger| and blocks
     * until the facility is considered FINISHED (exit Conditions are fulfilled).
     *
     * @param facility the {@link Facility} to exit.
     * @param trigger the trigger to start the transition (e.g. clicking a view).
     */
    public void exitFacilitySync(Facility<?> facility, Trigger trigger) {
        exitFacilitySync(facility, TransitionOptions.DEFAULT, trigger);
    }

    /** Version of {@link #exitFacilitySync(Facility, Trigger)} with extra TransitionOptions. */
    public void exitFacilitySync(Facility<?> facility, TransitionOptions options, Trigger trigger) {
        exitFacilitiesSync(List.of(facility), options, trigger);
    }

    /**
     * Starts a transition out of multiple {@link Facility}s, runs the transition |trigger| and
     * blocks until the facilities are considered FINISHED (exit Conditions are fulfilled).
     *
     * @param facilities the {@link Facility}s to exit.
     * @param trigger the trigger to start the transition (e.g. clicking a view).
     */
    public void exitFacilitiesSync(List<Facility<?>> facilities, Trigger trigger) {
        exitFacilitiesSync(facilities, TransitionOptions.DEFAULT, trigger);
    }

    /** Version of {@link #exitFacilitiesSync(List, Trigger)} with extra TransitionOptions. */
    public void exitFacilitiesSync(
            List<Facility<?>> facilities, TransitionOptions options, Trigger trigger) {
        assertHasFacilities(facilities);
        FacilityCheckOut checkOut = new FacilityCheckOut(facilities, options, trigger);
        checkOut.transitionSync();
    }

    /**
     * Starts a transition out of a {@link Facility} and into another {@link Facility}.
     *
     * <p>Runs the transition |trigger| and blocks until |facilityToExit| is considered FINISHED
     * (exit Conditions are fulfilled) and |facilityToEnter| is considered ACTIVE (enter Conditions
     * are fulfilled).
     *
     * @param facilityToExit the {@link Facility} to exit.
     * @param facilityToEnter the {@link Facility} to enter.
     * @param trigger the trigger to start the transition (e.g. clicking a view).
     * @return the {@link Facility} entered, now ACTIVE.
     * @param <F> the type of {@link Facility} entered.
     */
    public <F extends Facility<?>> F swapFacilitySync(
            Facility<?> facilityToExit, F facilityToEnter, Trigger trigger) {
        return swapFacilitySync(
                List.of(facilityToExit), facilityToEnter, TransitionOptions.DEFAULT, trigger);
    }

    /** Version of {@link #swapFacilitySync(Facility, F, Trigger)} with extra TransitionOptions. */
    public <F extends Facility<?>> F swapFacilitySync(
            Facility<?> facilityToExit,
            F facilityToEnter,
            TransitionOptions options,
            Trigger trigger) {
        return swapFacilitySync(List.of(facilityToExit), facilityToEnter, options, trigger);
    }

    /**
     * Starts a transition out of 1+ {@link Facility}s and into another {@link Facility}.
     *
     * <p>Runs the transition |trigger| and blocks until all |facilitiesToExit| are considered
     * FINISHED (exit Conditions are fulfilled) and |facilityToEnter| is considered ACTIVE (enter
     * Conditions are fulfilled).
     *
     * @param facilitiesToExit the {@link Facility}s to exit.
     * @param facilityToEnter the {@link Facility} to enter.
     * @param trigger the trigger to start the transition (e.g. clicking a view).
     * @return the {@link Facility} entered, now ACTIVE.
     * @param <F> the type of {@link Facility} entered.
     */
    public <F extends Facility<?>> F swapFacilitySync(
            List<Facility<?>> facilitiesToExit, F facilityToEnter, Trigger trigger) {
        return swapFacilitySync(
                facilitiesToExit, facilityToEnter, TransitionOptions.DEFAULT, trigger);
    }

    /** Version of {@link #swapFacilitySync(List, F, Trigger)} with extra TransitionOptions. */
    public <F extends Facility<?>> F swapFacilitySync(
            List<Facility<?>> facilitiesToExit,
            F facilityToEnter,
            TransitionOptions options,
            Trigger trigger) {
        assertHasFacilities(facilitiesToExit);
        registerFacility(facilityToEnter);
        FacilitySwap swap = new FacilitySwap(facilitiesToExit, facilityToEnter, options, trigger);
        swap.transitionSync();
        return facilityToEnter;
    }

    /**
     * Add a Facility which will be entered together with this Station. Both will become ACTIVE in
     * the same Trip.
     */
    public <F extends Facility<?>> F addInitialFacility(F facility) {
        assertInPhase(Phase.NEW);
        registerFacility(facility);
        return facility;
    }

    /**
     * Press back expecting to get to the given destination.
     *
     * <p>Left vague because back behavior is too case-by-case to determine in the Transit Layer.
     */
    public <T extends Station> T pressBack(T destination) {
        return travelToSync(destination, Espresso::pressBack);
    }
}