package com.faforever.client.main;
import com.faforever.client.cast.CastsController;
import com.faforever.client.chat.ChatController;
import com.faforever.client.chat.ChatService;
import com.faforever.client.chat.UserInfoWindowController;
import com.faforever.client.connectivity.ConnectivityService;
import com.faforever.client.connectivity.ConnectivityState;
import com.faforever.client.fx.WindowController;
import com.faforever.client.game.GameService;
import com.faforever.client.game.GamesController;
import com.faforever.client.hub.CommunityHubController;
import com.faforever.client.i18n.I18n;
import com.faforever.client.leaderboard.LeaderboardController;
import com.faforever.client.login.LoginController;
import com.faforever.client.map.MapVaultController;
import com.faforever.client.mod.ModVaultController;
import com.faforever.client.net.ConnectionState;
import com.faforever.client.news.NewsController;
import com.faforever.client.notification.NotificationService;
import com.faforever.client.notification.PersistentNotificationsController;
import com.faforever.client.notification.TransientNotification;
import com.faforever.client.notification.TransientNotificationsController;
import com.faforever.client.patch.GameUpdateService;
import com.faforever.client.player.PlayerInfoBeanBuilder;
import com.faforever.client.player.PlayerService;
import com.faforever.client.preferences.ForgedAlliancePrefs;
import com.faforever.client.preferences.NotificationsPrefs;
import com.faforever.client.preferences.Preferences;
import com.faforever.client.preferences.PreferencesService;
import com.faforever.client.preferences.ui.SettingsController;
import com.faforever.client.preferences.ToastPosition;
import com.faforever.client.preferences.WindowPrefs;
import com.faforever.client.rankedmatch.MatchmakerMessage;
import com.faforever.client.remote.FafService;
import com.faforever.client.remote.domain.RatingRange;
import com.faforever.client.replay.ReplayVaultController;
import com.faforever.client.task.TaskService;
import com.faforever.client.test.AbstractPlainJavaFxTest;
import com.faforever.client.theme.ThemeService;
import com.faforever.client.update.ClientUpdateService;
import com.faforever.client.user.UserService;
import com.google.common.eventbus.EventBus;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.geometry.Rectangle2D;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.stage.Screen;
import javafx.stage.Window;
import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.springframework.context.ApplicationContext;
import org.testfx.util.WaitForAsyncUtils;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import static java.util.Collections.singletonList;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNot.not;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyVararg;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class MainControllerTest extends AbstractPlainJavaFxTest {
@Mock
private PersistentNotificationsController persistentNotificationsController;
@Mock
private PreferencesService preferencesService;
@Mock
private LeaderboardController leaderboardController;
@Mock
private ConnectivityService connectivityService;
@Mock
private GameUpdateService gameUpdateService;
@Mock
private PlayerService playerService;
@Mock
private CommunityHubController communityHubController;
@Mock
private MapVaultController mapMapVaultController;
@Mock
private GamesController gamesController;
@Mock
private NewsController newsController;
@Mock
private CastsController castsController;
@Mock
private ModVaultController modVaultController;
@Mock
private ReplayVaultController replayVaultController;
@Mock
private ChatController chatController;
@Mock
private SettingsController settingsController;
@Mock
private UserInfoWindowController userInfoWindowController;
@Mock
private Preferences preferences;
@Mock
private ApplicationContext applicationContext;
@Mock
private I18n i18n;
@Mock
private WindowPrefs mainWindowPrefs;
@Mock
private FafService fafService;
@Mock
private UserService userService;
@Mock
private NotificationService notificationService;
@Mock
private TaskService taskService;
@Mock
private ForgedAlliancePrefs forgedAlliancePrefs;
@Mock
private ClientUpdateService clientUpdateService;
@Mock
private GameService gameService;
@Mock
private UserMenuController userMenuController;
@Mock
private TransientNotificationsController transientNotificationsController;
@Mock
private NotificationsPrefs notificationPrefs;
@Mock
private LoginController loginController;
@Mock
private ChatService chatService;
@Mock
private ThreadPoolExecutor threadPoolExecutor;
@Mock
private WindowController windowController;
@Mock
private ThemeService themeService;
@Mock
private EventBus eventBus;
private MainController instance;
private CountDownLatch mainControllerInitializedLatch;
private SimpleObjectProperty<ConnectionState> connectionStateProperty;
private SimpleObjectProperty<ConnectivityState> connectivityStateProperty;
private BooleanProperty loggedInProperty;
private BooleanProperty gameRunningProperty;
@Before
public void setUp() throws Exception {
instance = loadController("main.fxml");
instance.stage = getStage();
instance.i18n = i18n;
instance.applicationContext = applicationContext;
instance.playerService = playerService;
instance.preferencesService = preferencesService;
instance.connectivityService = connectivityService;
instance.gameUpdateService = gameUpdateService;
instance.fafService = fafService;
instance.userService = userService;
instance.replayVaultController = replayVaultController;
instance.leaderboardController = leaderboardController;
instance.modVaultController = modVaultController;
instance.mapMapVaultController = mapMapVaultController;
instance.gamesController = gamesController;
instance.castsController = castsController;
instance.newsController = newsController;
instance.communityHubController = communityHubController;
instance.settingsController = settingsController;
instance.chatController = chatController;
instance.persistentNotificationsController = persistentNotificationsController;
instance.notificationService = notificationService;
instance.taskService = taskService;
instance.clientUpdateService = clientUpdateService;
instance.gameService = gameService;
instance.userMenuController = userMenuController;
instance.transientNotificationsController = transientNotificationsController;
instance.loginController = loginController;
instance.chatService = chatService;
instance.threadPoolExecutor = threadPoolExecutor;
instance.windowController = windowController;
instance.themeService = themeService;
instance.eventBus = eventBus;
instance.ratingBeta = 250;
connectionStateProperty = new SimpleObjectProperty<>();
connectivityStateProperty = new SimpleObjectProperty<>(ConnectivityState.UNKNOWN);
ObjectProperty<ConnectionState> chatConnectionStateProperty = new SimpleObjectProperty<>();
loggedInProperty = new SimpleBooleanProperty();
gameRunningProperty = new SimpleBooleanProperty();
IntegerProperty chatUnreadMessagesCountProperty = new SimpleIntegerProperty();
when(chatController.getRoot()).thenReturn(new Pane());
when(persistentNotificationsController.getRoot()).thenReturn(new Pane());
when(leaderboardController.getRoot()).thenReturn(new Pane());
when(castsController.getRoot()).thenReturn(new Pane());
when(userMenuController.getRoot()).thenReturn(new Pane());
when(newsController.getRoot()).thenReturn(new Pane());
when(communityHubController.getRoot()).thenReturn(new Pane());
when(userMenuController.getRoot()).thenReturn(new Pane());
when(transientNotificationsController.getRoot()).thenReturn(new Pane());
when(taskService.getActiveTasks()).thenReturn(FXCollections.emptyObservableList());
when(preferencesService.getPreferences()).thenReturn(preferences);
when(applicationContext.getBean(UserInfoWindowController.class)).thenReturn(userInfoWindowController);
when(applicationContext.getBean(WindowController.class)).thenReturn(windowController);
when(preferences.getMainWindow()).thenReturn(mainWindowPrefs);
when(mainWindowPrefs.getLastChildViews()).thenReturn(FXCollections.observableHashMap());
when(preferences.getForgedAlliance()).thenReturn(forgedAlliancePrefs);
when(forgedAlliancePrefs.portProperty()).thenReturn(new SimpleIntegerProperty());
when(preferences.getNotification()).thenReturn(notificationPrefs);
when(notificationPrefs.toastPositionProperty()).thenReturn(new SimpleObjectProperty<>(ToastPosition.BOTTOM_RIGHT));
when(notificationPrefs.getToastPosition()).thenReturn(ToastPosition.BOTTOM_RIGHT);
when(fafService.connectionStateProperty()).thenReturn(connectionStateProperty);
when(connectivityService.checkConnectivity()).thenReturn(CompletableFuture.completedFuture(null));
when(connectivityService.connectivityStateProperty()).thenReturn(connectivityStateProperty);
when(chatService.connectionStateProperty()).thenReturn(chatConnectionStateProperty);
when(chatService.unreadMessagesCount()).thenReturn(chatUnreadMessagesCountProperty);
when(userService.loggedInProperty()).thenReturn(loggedInProperty);
when(gameService.gameRunningProperty()).thenReturn(gameRunningProperty);
doAnswer(invocation -> getThemeFile(invocation.getArgumentAt(0, String.class))).when(themeService).getThemeFile(any());
instance.postConstruct();
verify(userService).loggedInProperty();
mainControllerInitializedLatch = new CountDownLatch(1);
// As the login check is executed AFTER the main controller has been switched to logged in state, we hook to it
doAnswer(invocation -> {
mainControllerInitializedLatch.countDown();
return null;
}).when(clientUpdateService).checkForUpdateInBackground();
}
@Test
@SuppressWarnings("unchecked")
public void testDisplay() throws Exception {
attachToRoot();
fakeLogin();
when(communityHubController.getRoot()).thenReturn(new Pane());
WaitForAsyncUtils.waitForAsyncFx(1000, () -> instance.display());
when(mainWindowPrefs.getLastView()).thenReturn(instance.newsButton.getId());
verify(gameUpdateService).checkForUpdateInBackground();
assertTrue(getStage().isShowing());
}
/**
* Attaches the instance's root to the test's root. This is necessary since some components only work properly if they
* are attached to a window.
*/
private void attachToRoot() {
WaitForAsyncUtils.waitForAsyncFx(5000, () -> getRoot().getChildren().add(instance.mainRoot));
WaitForAsyncUtils.waitForFxEvents();
}
private void fakeLogin() throws InterruptedException {
loggedInProperty.set(true);
assertTrue(mainControllerInitializedLatch.await(3000, TimeUnit.SECONDS));
}
@Test
public void testOnFaConnectedDoesntThrowUp() throws Exception {
String disconnectedText = "foobar";
instance.fafConnectionButton.setText(disconnectedText);
CompletableFuture<String> textFuture = new CompletableFuture<>();
instance.fafConnectionButton.textProperty().addListener((observable, oldValue, newValue) -> {
textFuture.complete(newValue);
});
connectionStateProperty.set(ConnectionState.CONNECTED);
assertThat(textFuture.get(3, TimeUnit.SECONDS), not(disconnectedText));
}
@Test
public void testOnFaConnecting() throws Exception {
String disconnectedText = "foobar";
instance.fafConnectionButton.setText(disconnectedText);
CompletableFuture<String> textFuture = new CompletableFuture<>();
instance.fafConnectionButton.textProperty().addListener((observable, oldValue, newValue) -> {
textFuture.complete(newValue);
});
connectionStateProperty.set(ConnectionState.CONNECTING);
assertThat(textFuture.get(3, TimeUnit.SECONDS), not(disconnectedText));
}
@Test
public void testOnFafDisconnected() throws Exception {
String disconnectedText = "foobar";
instance.fafConnectionButton.setText(disconnectedText);
CompletableFuture<String> textFuture = new CompletableFuture<>();
instance.fafConnectionButton.textProperty().addListener((observable, oldValue, newValue) -> {
textFuture.complete(newValue);
});
connectionStateProperty.set(ConnectionState.DISCONNECTED);
assertThat(textFuture.get(3, TimeUnit.SECONDS), not(disconnectedText));
}
@Test
@Ignore("Not yet implemented")
public void testOnPortCheckHelpClicked() throws Exception {
instance.onPortCheckHelpClicked();
}
@Test
@Ignore("Not yet implemented")
public void testOnChangePortClicked() throws Exception {
instance.onChangePortClicked();
}
@Test
public void testOnPortCheckRetryClicked() throws Exception {
instance.onPortCheckRetryClicked();
verify(connectivityService).checkConnectivity();
}
@Test
public void testOnFafReconnectClicked() throws Exception {
instance.onFafReconnectClicked();
verify(fafService).reconnect();
}
@Test
public void testOnIrcReconnectClicked() throws Exception {
instance.onChatReconnectClicked();
verify(chatService).reconnect();
}
@Test
public void testOnNotificationsButtonClicked() throws Exception {
attachToRoot();
WaitForAsyncUtils.waitForAsyncFx(1000, instance::onNotificationsButtonClicked);
assertThat(instance.persistentNotificationsPopup.isShowing(), is(true));
}
@Test
public void testOnGamePortCheckFailed() throws Exception {
testChangeConnectivity(ConnectivityState.PUBLIC, ConnectivityState.UNKNOWN);
}
private void testChangeConnectivity(ConnectivityState initialState, ConnectivityState newState) throws Exception {
connectivityStateProperty.setValue(initialState);
String unknown = "Unknown";
WaitForAsyncUtils.waitForAsyncFx(1000, () -> instance.portCheckStatusButton.setText(unknown));
CompletableFuture<String> textFuture = new CompletableFuture<>();
instance.portCheckStatusButton.textProperty().addListener((observable, oldValue, newValue) -> {
textFuture.complete(newValue);
});
connectivityStateProperty.setValue(newState);
assertThat(textFuture.get(1, TimeUnit.SECONDS), not(unknown));
}
@Test
public void testOnGamePortCheckResultProxy() throws Exception {
testChangeConnectivity(ConnectivityState.UNKNOWN, ConnectivityState.STUN);
}
@Test
public void testOnGamePortCheckResultUnreachable() throws Exception {
testChangeConnectivity(ConnectivityState.UNKNOWN, ConnectivityState.BLOCKED);
}
@Test
public void testOnGamePortCheckResultReachable() throws Exception {
testChangeConnectivity(ConnectivityState.UNKNOWN, ConnectivityState.PUBLIC);
}
@Test
@Ignore("Not yet implemented")
public void testOnSupportItemSelected() throws Exception {
instance.onSupportItemSelected();
}
@Test
public void testOnSettingsItemSelected() throws Exception {
attachToRoot();
Pane root = new Pane();
when(settingsController.getRoot()).thenReturn(root);
WaitForAsyncUtils.waitForAsyncFx(1000, instance::onSettingsItemSelected);
verify(windowController).configure(
any(), eq(root), eq(true), eq(WindowController.WindowButtonType.CLOSE)
);
}
@Test
@Ignore("Needs UI for testing")
public void testOnChoseGameDirectory() throws Exception {
}
@Test
public void testGetRoot() throws Exception {
assertThat(instance.getRoot(), CoreMatchers.is(instance.mainRoot));
assertThat(instance.getRoot().getParent(), CoreMatchers.is(nullValue()));
}
@Test
public void testOnCommunitySelected() throws Exception {
attachToRoot();
when(communityHubController.getRoot()).thenReturn(new Pane());
WaitForAsyncUtils.waitForAsyncFx(1000, instance.newsButton::fire);
}
@Test
public void testOnVaultSelected() throws Exception {
attachToRoot();
when(modVaultController.getRoot()).thenReturn(new Pane());
WaitForAsyncUtils.waitForAsyncFx(1000, instance.vaultButton::fire);
}
@Test
public void testOnLeaderboardSelected() throws Exception {
attachToRoot();
when(leaderboardController.getRoot()).thenReturn(new Pane());
WaitForAsyncUtils.waitForAsyncFx(1000, instance.leaderboardButton::fire);
}
@Test
public void testOnPlaySelected() throws Exception {
attachToRoot();
when(gamesController.getRoot()).thenReturn(new Pane());
WaitForAsyncUtils.waitForAsyncFx(1000, instance.playButton::fire);
}
@Test
public void testOnChatSelected() throws Exception {
WaitForAsyncUtils.waitForAsyncFx(1000, instance.chatButton::fire);
}
@Test
@Ignore("CommunityHub is not yet available")
public void testOnCommunityHubSelected() throws Exception {
when(communityHubController.getRoot()).thenReturn(new Pane());
WaitForAsyncUtils.waitForAsyncFx(1000, () -> instance.newsButton.getItems().get(0).fire());
}
@Test
public void testOnNewsSelected() throws Exception {
WaitForAsyncUtils.waitForAsyncFx(1000, instance.newsButton::fire);
}
@Test
public void testOnPlayCustomSelected() throws Exception {
attachToRoot();
when(gamesController.getRoot()).thenReturn(new Pane());
WaitForAsyncUtils.waitForAsyncFx(1000, () -> instance.playButton.getItems().get(0).fire());
}
@Test
@Ignore("Not yet implemented")
public void testOnPlayRanked1v1Selected() throws Exception {
WaitForAsyncUtils.waitForAsyncFx(1000, () -> instance.playButton.getItems().get(1).fire());
}
@Test
public void testOnModsSelected() throws Exception {
attachToRoot();
when(modVaultController.getRoot()).thenReturn(new Pane());
WaitForAsyncUtils.waitForAsyncFx(1000, () -> instance.vaultButton.getItems().get(0).fire());
}
@Test
public void testOnReplaysSelected() throws Exception {
attachToRoot();
when(replayVaultController.getRoot()).thenReturn(new Pane());
WaitForAsyncUtils.waitForAsyncFx(1000, () -> instance.vaultButton.getItems().get(2).fire());
}
@Test
public void testOnLeaderboardRanked1v1Selected() throws Exception {
attachToRoot();
when(leaderboardController.getRoot()).thenReturn(new Pane());
WaitForAsyncUtils.waitForAsyncFx(1000, () -> instance.leaderboardButton.getItems().get(0).fire());
}
@Test
public void testOnMatchMakerMessageDisplaysNotification80Quality() {
prepareTestMatchmakerMessageTest(100);
verify(notificationService).addNotification(any(TransientNotification.class));
}
private void prepareTestMatchmakerMessageTest(float deviation) {
@SuppressWarnings("unchecked")
ArgumentCaptor<Consumer<MatchmakerMessage>> matchmakerMessageCaptor = ArgumentCaptor.forClass(Consumer.class);
when(notificationPrefs.isRanked1v1ToastEnabled()).thenReturn(true);
when(playerService.getCurrentPlayer()).thenReturn(
PlayerInfoBeanBuilder.create("JUnit").leaderboardRatingMean(1500).leaderboardRatingDeviation(deviation).get()
);
verify(gameService).addOnRankedMatchNotificationListener(matchmakerMessageCaptor.capture());
MatchmakerMessage matchmakerMessage = new MatchmakerMessage();
matchmakerMessage.setQueues(singletonList(new MatchmakerMessage.MatchmakerQueue("ladder1v1",
singletonList(new RatingRange(1500, 1510)), singletonList(new RatingRange(1500, 1510)))));
matchmakerMessageCaptor.getValue().accept(matchmakerMessage);
}
@Test
public void testOnMatchMakerMessageDisplaysNotification75Quality() {
prepareTestMatchmakerMessageTest(101);
verify(notificationService).addNotification(any(TransientNotification.class));
}
@Test
public void testOnMatchMakerMessageDoesNotDisplaysNotificationLessThan75Quality() {
prepareTestMatchmakerMessageTest(201);
verify(notificationService, never()).addNotification(any(TransientNotification.class));
}
@Test
public void testOnMatchMakerMessageDoesNotDisplaysNotificationWhenGameIsRunning() {
gameRunningProperty.set(true);
prepareTestMatchmakerMessageTest(100);
verify(notificationService, never()).addNotification(any(TransientNotification.class));
}
@Test
public void testOnMatchMakerMessageDisplaysNotificationNullQueues() {
@SuppressWarnings("unchecked")
ArgumentCaptor<Consumer<MatchmakerMessage>> matchmakerMessageCaptor = ArgumentCaptor.forClass(Consumer.class);
verify(gameService).addOnRankedMatchNotificationListener(matchmakerMessageCaptor.capture());
MatchmakerMessage matchmakerMessage = new MatchmakerMessage();
matchmakerMessage.setQueues(null);
matchmakerMessageCaptor.getValue().accept(matchmakerMessage);
verify(notificationService, never()).addNotification(any(TransientNotification.class));
}
@Test
public void testOnMatchMakerMessageDisplaysNotificationWithQueuesButDisabled() {
@SuppressWarnings("unchecked")
ArgumentCaptor<Consumer<MatchmakerMessage>> matchmakerMessageCaptor = ArgumentCaptor.forClass(Consumer.class);
when(notificationPrefs.isRanked1v1ToastEnabled()).thenReturn(false);
when(playerService.getCurrentPlayer()).thenReturn(
PlayerInfoBeanBuilder.create("JUnit").leaderboardRatingMean(1500).leaderboardRatingDeviation(100).get()
);
verify(gameService).addOnRankedMatchNotificationListener(matchmakerMessageCaptor.capture());
MatchmakerMessage matchmakerMessage = new MatchmakerMessage();
matchmakerMessage.setQueues(singletonList(new MatchmakerMessage.MatchmakerQueue("ladder1v1",
singletonList(new RatingRange(1500, 1510)), singletonList(new RatingRange(1500, 1510)))));
matchmakerMessageCaptor.getValue().accept(matchmakerMessage);
verify(notificationService, never()).addNotification(any(TransientNotification.class));
}
@Test
public void testWindowOutsideScreensGetsCentered() throws Exception {
Rectangle2D visualBounds = Screen.getPrimary().getBounds();
when(mainWindowPrefs.getY()).thenReturn(visualBounds.getMaxY() + 1);
when(mainWindowPrefs.getX()).thenReturn(visualBounds.getMaxX() + 1);
doAnswer(invocation -> {
getRoot().getChildren().setAll(invocation.getArgumentAt(1, Region.class));
return null;
}).when(windowController).configure(any(), any(), anyBoolean(), anyVararg());
CountDownLatch latch = new CountDownLatch(1);
Platform.runLater(() -> {
instance.display();
latch.countDown();
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
verify(windowController).configure(any(), any(), anyBoolean(), anyVararg());
Window window = instance.getRoot().getScene().getWindow();
Rectangle2D bounds = new Rectangle2D(window.getX(), window.getY(), window.getWidth(), window.getHeight());
assertTrue(Screen.getPrimary().getBounds().contains(bounds));
}
}