Skip to content

Commit

Permalink
[Test/e2e] Add a mechanism to stub binaries (#3485)
Browse files Browse the repository at this point in the history
* Ability to stub legendary commands and skip some code during e2e tests

* Simplify legendary stubbing in e2e tests

* Add command stubbing for gogdl and nile. Refactor.

* Reset all stubs after each test

* Remove unneeded comment

* Allow stubbing binaries commands with a promise too

* remove old test
  • Loading branch information
arielj authored Dec 14, 2024
1 parent 767b589 commit 0f8f5e7
Show file tree
Hide file tree
Showing 15 changed files with 306 additions and 18 deletions.
10 changes: 2 additions & 8 deletions e2e/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,15 @@ import { electronTest } from './helpers'

declare const window: { api: typeof import('../src/backend/api').default }

electronTest('renders the first page', async (app) => {
const page = await app.firstWindow()
electronTest('renders the first page', async (app, page) => {
await expect(page).toHaveTitle('Heroic Games Launcher')
})

electronTest('gets heroic, legendary, and gog versions', async (app) => {
const page = await app.firstWindow()

electronTest('gets heroic, legendary, and gog versions', async (app, page) => {
await test.step('get heroic version', async () => {
const heroicVersion = await page.evaluate(async () =>
window.api.getHeroicVersion()
)
console.log('Heroic Version: ', heroicVersion)
// check that heroic version is newer or equal to 2.6.3
expect(compareVersions(heroicVersion, '2.6.3')).toBeGreaterThanOrEqual(0)
})
Expand All @@ -26,7 +22,6 @@ electronTest('gets heroic, legendary, and gog versions', async (app) => {
window.api.getLegendaryVersion()
)
legendaryVersion = legendaryVersion.trim().split(' ')[0]
console.log('Legendary Version: ', legendaryVersion)
expect(compareVersions(legendaryVersion, '0.20.32')).toBeGreaterThanOrEqual(
0
)
Expand All @@ -36,7 +31,6 @@ electronTest('gets heroic, legendary, and gog versions', async (app) => {
const gogdlVersion = await page.evaluate(async () =>
window.api.getGogdlVersion()
)
console.log('Gogdl Version: ', gogdlVersion)
expect(compareVersions(gogdlVersion, '0.7.1')).toBeGreaterThanOrEqual(0)
})
})
56 changes: 50 additions & 6 deletions e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { join } from 'path'
import {
test,
_electron as electron,
type ElectronApplication
ElectronApplication,
Page
} from '@playwright/test'

const main_js = join(__dirname, '../build/main/main.js')
Expand All @@ -14,15 +15,58 @@ const main_js = join(__dirname, '../build/main/main.js')
*/
function electronTest(
name: string,
func: (app: ElectronApplication) => void | Promise<void>
func: (app: ElectronApplication, page: Page) => void | Promise<void>
) {
test(name, async () => {
const app = await electron.launch({
const electronApp = await electron.launch({
args: [main_js]
})
await func(app)
await app.close()

// uncomment these lines to print electron's output
// electronApp
// .process()!
// .stdout?.on('data', (data) => console.log(`stdout: ${data}`))
// electronApp
// .process()!
// .stderr?.on('data', (error) => console.log(`stderr: ${error}`))

const page = await electronApp.firstWindow()

await func(electronApp, page)

await resetAllStubs(electronApp)

await electronApp.close()
})
}

export { electronTest }
async function resetAllStubs(app: ElectronApplication) {
await resetLegendaryCommandStub(app)
await resetGogdlCommandStub(app)
await resetNileCommandStub(app)
}

async function resetLegendaryCommandStub(app: ElectronApplication) {
await app.evaluate(({ ipcMain }) => {
ipcMain.emit('resetLegendaryCommandStub')
})
}

async function resetGogdlCommandStub(app: ElectronApplication) {
await app.evaluate(({ ipcMain }) => {
ipcMain.emit('resetGogdlCommandStub')
})
}

async function resetNileCommandStub(app: ElectronApplication) {
await app.evaluate(({ ipcMain }) => {
ipcMain.emit('resetNileCommandStub')
})
}

export {
electronTest,
resetLegendaryCommandStub,
resetGogdlCommandStub,
resetNileCommandStub
}
65 changes: 65 additions & 0 deletions e2e/settings.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { expect, test } from '@playwright/test'
import { electronTest } from './helpers'

electronTest('Settings', async (app, page) => {
// stub `legendary --version`
await app.evaluate(({ ipcMain }) => {
ipcMain.emit('setLegendaryCommandStub', [
{
commandParts: ['--version'],
stdout: 'legendary version "1.2.3", codename "Some Name"'
}
])
})

// stub `gogdl --version`
await app.evaluate(({ ipcMain }) => {
ipcMain.emit('setGogdlCommandStub', [
{
commandParts: ['--version'],
stdout: '2.3.4'
}
])
})

// stub `nile --version`
await app.evaluate(({ ipcMain }) => {
ipcMain.emit('setNileCommandStub', [
{
commandParts: ['--version'],
stdout: '1.1.1 JoJo'
}
])
})

await test.step('shows the Advanced settings', async () => {
await page.getByTestId('settings').click()
page.getByText('Global Settings')
await page.getByText('Advanced').click()
})

await test.step('shows alternative binaries inputs', async () => {
await expect(
page.getByLabel('Choose an Alternative Legendary Binary')
).toBeVisible()
await expect(
page.getByLabel('Choose an Alternative GOGDL Binary to use')
).toBeVisible()
await expect(
page.getByLabel('Choose an Alternative Nile Binary')
).toBeVisible()
})

await test.step('shows the binaries versions from the binaries', async () => {
await expect(
page.getByText('Legendary Version: 1.2.3 Some Name')
).toBeVisible()
await expect(page.getByText('GOGDL Version: 2.3.4')).toBeVisible()
await expect(page.getByText('Nile Version: 1.1.1 JoJo')).toBeVisible()
})

await test.step('shows the default experimental features', async () => {
await expect(page.getByLabel('New design')).not.toBeChecked()
await expect(page.getByLabel('Help component')).not.toBeChecked()
})
})
1 change: 1 addition & 0 deletions src/backend/anticheat/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { runOnceWhenOnline } from '../online_monitor'
import { axiosClient } from 'backend/utils'

async function downloadAntiCheatData() {
if (process.env.CI === 'e2e') return
if (isWindows) return

runOnceWhenOnline(async () => {
Expand Down
2 changes: 1 addition & 1 deletion src/backend/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ if (!gotTheLock) {
handleProtocol([request.url])
return new Response('Operation initiated.', { status: 201 })
})
if (!app.isDefaultProtocolClient('heroic')) {
if (process.env.CI !== 'e2e' && !app.isDefaultProtocolClient('heroic')) {
if (app.setAsDefaultProtocolClient('heroic')) {
logInfo('Registered protocol with OS.', LogPrefix.Backend)
} else {
Expand Down
5 changes: 5 additions & 0 deletions src/backend/online_monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ const ping = async (url: string, signal: AbortSignal) => {
}

const pingSites = () => {
if (process.env.CI === 'e2e') {
setStatus('online')
return
}

logInfo(`Pinging external endpoints`, LogPrefix.Connection)
abortController = new AbortController()

Expand Down
41 changes: 41 additions & 0 deletions src/backend/storeManagers/gog/e2eMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ipcMain } from 'electron'
import { RunnerCommandStub } from 'common/types'

/*
* Multiple parts of a command can be set for the stub to be able to stub
* similar commands
*
* The first stub for which all commandParts are included in the executed
* command will be selected. The stubs should be declared from more
* precise to less precise to avoid unreachable stubs.
*
* We can stub a Promise<ExecResult> as a response, or stub stdout/stderr
* values as an alternative to make the stubbing easier
*/
const defaultStubs: RunnerCommandStub[] = [
{
commandParts: ['--version'],
stdout: '0.7.1'
}
]

let currentStubs = [...defaultStubs]

export const runGogdlCommandStub = async (command: string[]) => {
const stub = currentStubs.find((stub) =>
stub.commandParts.every((part) => command.includes(part))
)

if (stub?.response) return stub.response

return Promise.resolve({
stdout: stub?.stdout || '',
stderr: stub?.stderr || ''
})
}

// Add listeners to be called from e2e tests to stub the gogdl command calls
if (process.env.CI === 'e2e') {
ipcMain.on('setGogdlCommandStub', (stubs) => (currentStubs = [...stubs]))
ipcMain.on('resetGogdlCommandStub', () => (currentStubs = [...defaultStubs]))
}
5 changes: 5 additions & 0 deletions src/backend/storeManagers/gog/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { unzipSync } from 'node:zlib'
import { readdirSync, rmSync, writeFileSync } from 'node:fs'
import { checkForRedistUpdates } from './redist'
import { runGogdlCommandStub } from './e2eMock'

const library: Map<string, GameInfo> = new Map()
const installedGames: Map<string, InstalledInfo> = new Map()
Expand Down Expand Up @@ -1319,6 +1320,10 @@ export async function runRunnerCommand(
commandParts: string[],
options?: CallRunnerOptions
): Promise<ExecResult> {
if (process.env.CI === 'e2e') {
return runGogdlCommandStub(commandParts)
}

const { dir, bin } = getGOGdlBin()
const authConfig = join(app.getPath('userData'), 'gog_store', 'auth.json')

Expand Down
48 changes: 48 additions & 0 deletions src/backend/storeManagers/legendary/e2eMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ipcMain } from 'electron'
import { RunnerCommandStub } from 'common/types'
import { LegendaryCommand } from './commands'

/*
* Multiple parts of a command can be set for the stub to be able to stub
* similar commands
*
* The first stub for which all commandParts are included in the executed
* command will be selected. The stubs should be declared from more
* precise to less precise to avoid unreachable stubs.
*
* We can stub a Promise<ExecResult> as a response, or stub stdout/stderr
* values as an alternative to make the stubbing easier
*/
const defaultStubs: RunnerCommandStub[] = [
{
commandParts: ['--version'],
response: Promise.resolve({
stdout: 'legendary version "0.20.33", codename "Undue Alarm"',
stderr: ''
})
}
]

let currentStubs = [...defaultStubs]

export const runLegendaryCommandStub = async (command: LegendaryCommand) => {
const stub = currentStubs.find((stub) =>
stub.commandParts.every((part) => command[part])
)

if (stub?.response) return stub.response

return Promise.resolve({
stdout: stub?.stdout || '',
stderr: stub?.stderr || ''
})
}

// Add listeners to be called from e2e tests to stub the legendary command calls
if (process.env.CI === 'e2e') {
ipcMain.on('setLegendaryCommandStub', (stubs) => (currentStubs = [...stubs]))
ipcMain.on(
'resetLegendaryCommandStub',
() => (currentStubs = [...defaultStubs])
)
}
5 changes: 5 additions & 0 deletions src/backend/storeManagers/legendary/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import { Path } from 'backend/schemas'
import shlex from 'shlex'
import thirdParty from './thirdParty'
import { Entries } from 'type-fest'
import { runLegendaryCommandStub } from './e2eMock'

const allGames: Set<string> = new Set()
let installedGames: Map<string, InstalledJsonMetadata> = new Map()
Expand Down Expand Up @@ -679,6 +680,10 @@ export async function runRunnerCommand(
command: LegendaryCommand,
options?: CallRunnerOptions
): Promise<ExecResult> {
if (process.env.CI === 'e2e') {
return runLegendaryCommandStub(command)
}

const { dir, bin } = getLegendaryBin()

// Set LEGENDARY_CONFIG_PATH to a custom, Heroic-specific location so user-made
Expand Down
41 changes: 41 additions & 0 deletions src/backend/storeManagers/nile/e2eMock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { ipcMain } from 'electron'
import { RunnerCommandStub } from 'common/types'

/*
* Multiple parts of a command can be set for the stub to be able to stub
* similar commands
*
* The first stub for which all commandParts are included in the executed
* command will be selected. The stubs should be declared from more
* precise to less precise to avoid unreachable stubs.
*
* We can stub a Promise<ExecResult> as a response, or stub stdout/stderr
* values as an alternative to make the stubbing easier
*/
const defaultStubs: RunnerCommandStub[] = [
{
commandParts: ['--version'],
stdout: '1.0.0 Jonathan Joestar'
}
]

let currentStubs = [...defaultStubs]

export const runNileCommandStub = async (command: string[]) => {
const stub = currentStubs.find((stub) =>
stub.commandParts.every((part) => command.includes(part))
)

if (stub?.response) return stub.response

return Promise.resolve({
stdout: stub?.stdout || '',
stderr: stub?.stderr || ''
})
}

// Add listeners to be called from e2e tests to stub the nile command calls
if (process.env.CI === 'e2e') {
ipcMain.on('setNileCommandStub', (stubs) => (currentStubs = [...stubs]))
ipcMain.on('resetNileCommandStub', () => (currentStubs = [...defaultStubs]))
}
5 changes: 5 additions & 0 deletions src/backend/storeManagers/nile/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { dirname, join } from 'path'
import { app } from 'electron'
import { copySync } from 'fs-extra'
import { NileUser } from './user'
import { runNileCommandStub } from './e2eMock'

const installedGames: Map<string, NileInstallMetadataInfo> = new Map()
const library: Map<string, GameInfo> = new Map()
Expand Down Expand Up @@ -466,6 +467,10 @@ export async function runRunnerCommand(
commandParts: string[],
options?: CallRunnerOptions
): Promise<ExecResult> {
if (process.env.CI === 'e2e') {
return runNileCommandStub(commandParts)
}

const { dir, bin } = getNileBin()

// Set NILE_CONFIG_PATH to a custom, Heroic-specific location so user-made
Expand Down
Loading

0 comments on commit 0f8f5e7

Please sign in to comment.