diff --git a/packages/e2e/src/helpers/games.ts b/packages/e2e/src/helpers/games.ts index f2d9c2f08c..0c241fd12a 100644 --- a/packages/e2e/src/helpers/games.ts +++ b/packages/e2e/src/helpers/games.ts @@ -10,6 +10,8 @@ import { Timeouts } from "./timeouts"; // VORTEX_E2E=1 disables automatic discovery, so all games go through the // "Game not discovered" dialog and have their path set via a dialog.showOpenDialog stub. +// A logged-in app may still list the game under "Games detected": that section comes +// from the user's store libraries via the Nexus API, and doesn't give Vortex a path. export type ManagedGameId = "stardewvalley"; export interface ManagedGame { @@ -27,7 +29,6 @@ export async function manageGame( await test.step(`Manage game: ${gameId}`, async () => { const navbar = new NavBar(vortexWindow); - const gamesPage = new GamesPage(vortexWindow); // First interaction after a cold app launch — allow the nav to finish // mounting rather than racing the default 5s UI budget. @@ -36,29 +37,11 @@ export async function manageGame( await stubOpenDialog(electronApp, fakeGame.gamePath); - // The supported games list is windowed, so the target game's tile isn't in - // the DOM until the list is filtered down to it. Search by name first. - await expect(gamesPage.searchInput).toBeVisible(); - await gamesPage.searchInput.fill(gameName); - - const tile = gamesPage.gameTileInSection(gamesPage.supportedSection, gameName); - await expect(tile).toBeVisible({ timeout: Timeouts.NETWORK }); - await tile.scrollIntoViewIfNeeded(); - await tile.hover(); - - const manualAddButton = gamesPage.manualAddButton(gameName); - await expect(manualAddButton).toBeVisible(); - await manualAddButton.click(); - - await expect(gamesPage.notDiscoveredDialog).toBeVisible(); - await expect(gamesPage.notDiscoveredDialog).toContainText( - "hasn't been automatically discovered", - ); - await gamesPage.continueButton.click(); - await expect(gamesPage.notDiscoveredDialog).toBeHidden(); + const gamesPage = new GamesPage(vortexWindow); + await addGameManually(gamesPage, gameName); try { - await expect(navbar.modsLink).toBeVisible({ timeout: Timeouts.NETWORK }); + await openAddedGame(gamesPage, navbar, gameName); } catch (e) { throw new Error( `${gameName} did not become the active game. On screen: ${await describeScreen(vortexWindow)}`, @@ -69,3 +52,47 @@ export async function manageGame( return fakeGame; } + +/** Adds a game from "All supported games", answering the "Game not discovered" dialog. */ +async function addGameManually(gamesPage: GamesPage, gameName: string): Promise { + // The supported games list is windowed, so the target game's tile isn't in + // the DOM until the list is filtered down to it. Search by name first. + await expect(gamesPage.searchInput).toBeVisible(); + await gamesPage.searchInput.fill(gameName); + + const tile = gamesPage.gameTileInSection(gamesPage.supportedSection, gameName); + await expect(tile).toBeVisible({ timeout: Timeouts.NETWORK }); + // hover scrolls the tile into view itself, and unlike scrollIntoViewIfNeeded it + // retries if the list re-renders the tile in the meantime + await tile.hover(); + + const manualAddButton = gamesPage.manualAddButton(gameName); + await expect(manualAddButton).toBeVisible(); + await manualAddButton.click(); + + await expect(gamesPage.notDiscoveredDialog).toBeVisible(); + await expect(gamesPage.notDiscoveredDialog).toContainText("hasn't been automatically discovered"); + await gamesPage.continueButton.click(); + await expect(gamesPage.notDiscoveredDialog).toBeHidden(); +} + +/** + * Adding a game switches to its new profile, but the spine follows the open page, + * so the user is left on the Games page in the Home workspace. The game's tile + * moves to "Added games", and its Open goes to the Mods page. + */ +async function openAddedGame( + gamesPage: GamesPage, + navbar: NavBar, + gameName: string, +): Promise { + const addedTile = gamesPage.gameTileInSection(gamesPage.addedSection, gameName); + await expect(addedTile).toBeVisible({ timeout: Timeouts.GAME_ACTIVATION }); + await addedTile.hover(); + + const openButton = gamesPage.openButton(gameName); + await expect(openButton).toBeVisible(); + await openButton.click(); + + await expect(navbar.modsLink).toBeVisible({ timeout: Timeouts.GAME_ACTIVATION }); +} diff --git a/packages/e2e/src/helpers/timeouts.ts b/packages/e2e/src/helpers/timeouts.ts index 8a5ab09c20..114214974a 100644 --- a/packages/e2e/src/helpers/timeouts.ts +++ b/packages/e2e/src/helpers/timeouts.ts @@ -22,6 +22,11 @@ export const Timeouts = { * are throttled, so SMAPI alone takes ~30s locally — past a single round-trip. */ MOD_INSTALL: min(2) * scalingFactor, + /** + * Making a game the active one: the profile switch, the staging folder set up + * on first manage, and the game's workspace loading afterwards. + */ + GAME_ACTIVATION: sec(30) * scalingFactor, /** Bounded wait for a client-rendered modal to appear (or confirm it won't). */ MODAL: sec(10) * scalingFactor, /** Cold-start and worker fixture setup. */ diff --git a/packages/e2e/src/selectors/games.ts b/packages/e2e/src/selectors/games.ts index d915caae75..38a31d24dc 100644 --- a/packages/e2e/src/selectors/games.ts +++ b/packages/e2e/src/selectors/games.ts @@ -2,7 +2,10 @@ import type { Locator, Page } from "@playwright/test"; export class GamesPage { readonly page: Page; - /** "Games detected": installed games Vortex found but the user hasn't added. */ + /** + * "Games detected", or "New games detected" when it has games the user hasn't + * seen: installed games Vortex found but the user hasn't added. + */ readonly detectedSection: Locator; /** "Added games": the games the user is managing. */ readonly addedSection: Locator; @@ -19,7 +22,7 @@ export class GamesPage { constructor(page: Page) { this.page = page; - this.detectedSection = this.section("Games detected"); + this.detectedSection = this.section(/^(New games|Games) detected$/); this.addedSection = this.section("Added games"); this.supportedSection = this.section("All supported games"); this.notDiscoveredDialog = page @@ -40,15 +43,33 @@ export class GamesPage { /** * The primary action on a tile in "All supported games". It's revealed on - * hover, so hover the tile before clicking. + * hover, so hover the tile before clicking. Scoped to that section because a + * detected game also has a tile under "Games detected", whose action is + * "Add game" instead. */ manualAddButton(gameName: string): Locator { - return this.gameTile(gameName).getByRole("button", { name: "Manual add", exact: true }); + return this.gameTileInSection(this.supportedSection, gameName).getByRole("button", { + name: "Manual add", + exact: true, + }); + } + + /** + * The primary action on a tile in "Added games". Adding a game activates it + * but leaves the user on the Games page; Open is what takes them into the + * game's workspace. + */ + openButton(gameName: string): Locator { + return this.gameTileInSection(this.addedSection, gameName).getByRole("button", { + name: "Open", + exact: true, + }); } // A section is a
whose header carries its title; the sections have - // no accessible name, so anchor on the exact header text. - private section(title: string): Locator { + // no accessible name, so anchor on the header text. The title may also be + // given as a pattern for a section whose title varies. + private section(title: string | RegExp): Locator { return this.page .locator("section") .filter({ has: this.page.getByText(title, { exact: true }) });