Files
standardnotes-app-web/packages/desktop/test/driver.ts
2022-06-07 11:52:15 -05:00

204 lines
6.4 KiB
TypeScript

/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ChildProcess, spawn } from 'child_process'
import electronPath, { MenuItem } from 'electron'
import path from 'path'
import { deleteDir, ensureDirectoryExists, readJSONFile } from '../app/javascripts/Main/Utils/fileUtils'
import { Language } from '../app/javascripts/Main/spellcheckerManager'
import { StoreKeys } from '../app/javascripts/Main/store'
import { UpdateState } from '../app/javascripts/Main/updateManager'
import { CommandLineArgs } from '../app/javascripts/Shared/CommandLineArgs'
import { AppMessageType, AppTestMessage, MessageType, TestIPCMessage, TestIPCMessageResult } from './TestIpcMessage'
function spawnAppprocess(userDataPath: string) {
const p = spawn(
electronPath as any,
[path.join(__dirname, '..'), CommandLineArgs.Testing, CommandLineArgs.UserDataPath, userDataPath],
{
stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
},
)
return p
}
class Driver {
private appProcess: ChildProcess
private calls: Array<{
resolve: (...args: any) => void
reject: (...args: any) => void
} | null> = []
private awaitedOnMessages: Array<{
type: AppMessageType
resolve: (...args: any) => void
}> = []
appReady: Promise<unknown>
windowLoaded: Promise<unknown>
constructor(readonly userDataPath: string) {
this.appProcess = spawnAppprocess(userDataPath)
this.appProcess.on('message', this.receive)
this.appReady = this.waitOn(AppMessageType.Ready)
this.windowLoaded = this.waitOn(AppMessageType.WindowLoaded)
}
private receive = (message: TestIPCMessageResult | AppTestMessage) => {
if ('type' in message) {
if (message.type === AppMessageType.Log) {
console.log(message)
}
this.awaitedOnMessages = this.awaitedOnMessages.filter(({ type, resolve }) => {
if (type === message.type) {
resolve()
return false
}
return true
})
}
if ('id' in message) {
const call = this.calls[message.id]!
this.calls[message.id] = null
if (message.reject) {
call.reject(message.reject)
} else {
call.resolve(message.resolve)
}
}
}
private waitOn = (messageType: AppMessageType) => {
return new Promise((resolve) => {
this.awaitedOnMessages.push({
type: messageType,
resolve,
})
})
}
private send = (type: MessageType, ...args: any): Promise<any> => {
const id = this.calls.length
const message: TestIPCMessage = {
id,
type,
args,
}
this.appProcess.send(message)
return new Promise((resolve, reject) => {
this.calls.push({ resolve, reject })
})
}
windowCount = (): Promise<number> => this.send(MessageType.WindowCount)
appStateCall = (methodName: string, ...args: any): Promise<any> =>
this.send(MessageType.AppStateCall, methodName, ...args)
readonly window = {
signOut: (): Promise<void> => this.send(MessageType.SignOut),
}
readonly storage = {
dataOnDisk: async (): Promise<{ [key in StoreKeys]: any }> => {
const location = await this.send(MessageType.StoreSettingsLocation)
return readJSONFile(location)
},
dataLocation: (): Promise<string> => this.send(MessageType.StoreSettingsLocation),
setZoomFactor: (factor: number) => this.send(MessageType.StoreSet, 'zoomFactor', factor),
setLocalStorageValue: (key: string, value: string): Promise<void> =>
this.send(MessageType.SetLocalStorageValue, key, value),
}
readonly appMenu = {
items: (): Promise<MenuItem[]> => this.send(MessageType.AppMenuItems),
clickLanguage: (language: Language) => this.send(MessageType.ClickLanguage, language),
hasReloaded: () => this.send(MessageType.HasReloadedMenu),
}
readonly spellchecker = {
manager: () => this.send(MessageType.SpellCheckerManager),
languages: () => this.send(MessageType.SpellCheckerLanguages),
}
readonly backups = {
enabled: (): Promise<boolean> => this.send(MessageType.BackupsAreEnabled),
toggleEnabled: (): Promise<boolean> => this.send(MessageType.ToggleBackupsEnabled),
location: (): Promise<string> => this.send(MessageType.BackupsLocation),
copyDecryptScript: async (location: string) => {
await this.send(MessageType.CopyDecryptScript, location)
},
changeLocation: (location: string) => this.send(MessageType.ChangeBackupsLocation, location),
save: (data: any) => this.send(MessageType.DataArchive, data),
perform: async () => {
await this.windowLoaded
await this.send(MessageType.PerformBackup)
await this.waitOn(AppMessageType.SavedBackup)
},
}
readonly updates = {
state: (): Promise<UpdateState> => this.send(MessageType.UpdateState),
autoUpdateEnabled: (): Promise<boolean> => this.send(MessageType.AutoUpdateEnabled),
check: () => this.send(MessageType.CheckForUpdate),
}
readonly net = {
getJSON: (url: string) => this.send(MessageType.GetJSON, url),
downloadFile: (url: string, filePath: string) => this.send(MessageType.DownloadFile, url, filePath),
}
stop = async () => {
this.appProcess.kill()
/** Give the process a little time before cleaning up */
await new Promise((resolve) => setTimeout(resolve, 150))
/**
* Windows can throw EPERM or EBUSY errors when we try to delete the
* user data directory too quickly.
*/
const maxTries = 5
for (let i = 0; i < maxTries; i++) {
try {
await deleteDir(this.userDataPath)
return
} catch (error: any) {
if (error.code === 'EPERM' || error.code === 'EBUSY') {
await new Promise((resolve) => setTimeout(resolve, 300))
} else {
throw error
}
}
}
throw new Error(`Couldn't delete user data directory after ${maxTries} tries`)
}
restart = async () => {
this.appProcess.kill()
this.appProcess = spawnAppprocess(this.userDataPath)
this.appProcess.on('message', this.receive)
this.appReady = this.waitOn(AppMessageType.Ready)
this.windowLoaded = this.waitOn(AppMessageType.WindowLoaded)
await this.appReady
}
}
export type { Driver }
export async function createDriver() {
const userDataPath = path.join(
__dirname,
'data',
'tmp',
`userData-${Date.now()}-${Math.round(Math.random() * 10000)}`,
)
await ensureDirectoryExists(userDataPath)
const driver = new Driver(userDataPath)
await driver.appReady
return driver
}