feat: add desktop repo (#1071)
This commit is contained in:
52
packages/desktop/test/TestIpcMessage.ts
Normal file
52
packages/desktop/test/TestIpcMessage.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
export interface TestIPCMessage {
|
||||
id: number
|
||||
type: MessageType
|
||||
args: any[]
|
||||
}
|
||||
|
||||
export interface TestIPCMessageResult {
|
||||
id: number
|
||||
resolve?: any
|
||||
reject?: any
|
||||
}
|
||||
|
||||
export interface AppTestMessage {
|
||||
type: AppMessageType
|
||||
}
|
||||
|
||||
export enum AppMessageType {
|
||||
Ready,
|
||||
WindowLoaded,
|
||||
SavedBackup,
|
||||
Log,
|
||||
}
|
||||
|
||||
export enum MessageType {
|
||||
WindowCount,
|
||||
StoreData,
|
||||
StoreSettingsLocation,
|
||||
StoreSet,
|
||||
SetLocalStorageValue,
|
||||
AppMenuItems,
|
||||
SpellCheckerManager,
|
||||
SpellCheckerLanguages,
|
||||
ClickLanguage,
|
||||
BackupsAreEnabled,
|
||||
ToggleBackupsEnabled,
|
||||
BackupsLocation,
|
||||
PerformBackup,
|
||||
ChangeBackupsLocation,
|
||||
CopyDecryptScript,
|
||||
MenuReloaded,
|
||||
UpdateState,
|
||||
CheckForUpdate,
|
||||
UpdateManagerNotifiedStateChange,
|
||||
Relaunch,
|
||||
DataArchive,
|
||||
GetJSON,
|
||||
DownloadFile,
|
||||
AutoUpdateEnabled,
|
||||
HasReloadedMenu,
|
||||
AppStateCall,
|
||||
SignOut,
|
||||
}
|
||||
123
packages/desktop/test/backupsManager.spec.ts
Normal file
123
packages/desktop/test/backupsManager.spec.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import anyTest, { TestFn } from 'ava'
|
||||
import { Driver, createDriver } from './driver'
|
||||
|
||||
const test = anyTest as TestFn<Driver>
|
||||
|
||||
const BackupsDirectoryName = 'Standard Notes Backups'
|
||||
|
||||
test.beforeEach(async (t) => {
|
||||
t.context = await createDriver()
|
||||
const backupsLocation = await t.context.backups.location()
|
||||
await fs.rmdir(backupsLocation, { recursive: true })
|
||||
await t.context.backups.copyDecryptScript(backupsLocation)
|
||||
})
|
||||
|
||||
test.afterEach.always(async (t) => {
|
||||
await t.context.stop()
|
||||
})
|
||||
|
||||
/**
|
||||
* Depending on the current system load, performing a backup
|
||||
* might take a while
|
||||
*/
|
||||
const timeoutDuration = 20 * 1000 /** 20s */
|
||||
|
||||
function wait(duration = 1000) {
|
||||
return new Promise((resolve) => setTimeout(resolve, duration))
|
||||
}
|
||||
|
||||
test('saves incoming data to the backups folder', async (t) => {
|
||||
const data = 'Sample Data'
|
||||
const fileName = await t.context.backups.save(data)
|
||||
const backupsLocation = await t.context.backups.location()
|
||||
const files = await fs.readdir(backupsLocation)
|
||||
t.true(files.includes(fileName))
|
||||
t.is(data, await fs.readFile(path.join(backupsLocation, fileName), 'utf8'))
|
||||
})
|
||||
|
||||
test('saves the decrypt script to the backups folder', async (t) => {
|
||||
const backupsLocation = await t.context.backups.location()
|
||||
await wait(300) /** Disk might be busy */
|
||||
const files = await fs.readdir(backupsLocation)
|
||||
t.true(files.includes('decrypt.html'))
|
||||
})
|
||||
|
||||
test('performs a backup', async (t) => {
|
||||
t.timeout(timeoutDuration)
|
||||
|
||||
await wait()
|
||||
await t.context.backups.perform()
|
||||
const backupsLocation = await t.context.backups.location()
|
||||
const files = await fs.readdir(backupsLocation)
|
||||
|
||||
t.true(files.length >= 1)
|
||||
})
|
||||
|
||||
test('changes backups folder location', async (t) => {
|
||||
t.timeout(timeoutDuration)
|
||||
await wait()
|
||||
await t.context.backups.perform()
|
||||
let newLocation = path.join(t.context.userDataPath, 'newLocation')
|
||||
await fs.mkdir(newLocation)
|
||||
const currentLocation = await t.context.backups.location()
|
||||
const fileNames = await fs.readdir(currentLocation)
|
||||
await t.context.backups.changeLocation(newLocation)
|
||||
newLocation = path.join(newLocation, BackupsDirectoryName)
|
||||
t.deepEqual(fileNames, await fs.readdir(newLocation))
|
||||
|
||||
/** Assert that the setting was saved */
|
||||
const data = await t.context.storage.dataOnDisk()
|
||||
t.is(data.backupsLocation, newLocation)
|
||||
|
||||
/** Perform backup and make sure there is one more file in the directory */
|
||||
await t.context.backups.perform()
|
||||
const newFileNames = await fs.readdir(newLocation)
|
||||
t.deepEqual(newFileNames.length, fileNames.length + 1)
|
||||
})
|
||||
|
||||
test('changes backups location to a child directory', async (t) => {
|
||||
t.timeout(timeoutDuration)
|
||||
|
||||
await wait()
|
||||
await t.context.backups.perform()
|
||||
const currentLocation = await t.context.backups.location()
|
||||
const backups = await fs.readdir(currentLocation)
|
||||
|
||||
t.is(backups.length, 2) /** 1 + decrypt script */
|
||||
|
||||
const newLocation = path.join(currentLocation, 'child_dir')
|
||||
await t.context.backups.changeLocation(newLocation)
|
||||
|
||||
t.deepEqual(await fs.readdir(path.join(newLocation, BackupsDirectoryName)), backups)
|
||||
})
|
||||
|
||||
test('changing backups location to the same directory should not do anything', async (t) => {
|
||||
t.timeout(timeoutDuration)
|
||||
await wait()
|
||||
await t.context.backups.perform()
|
||||
await t.context.backups.perform()
|
||||
const currentLocation = await t.context.backups.location()
|
||||
let totalFiles = (await fs.readdir(currentLocation)).length
|
||||
t.is(totalFiles, 3) /** 2 + decrypt script */
|
||||
await t.context.backups.changeLocation(currentLocation)
|
||||
totalFiles = (await fs.readdir(currentLocation)).length
|
||||
t.is(totalFiles, 3)
|
||||
})
|
||||
|
||||
test('backups are enabled by default', async (t) => {
|
||||
t.is(await t.context.backups.enabled(), true)
|
||||
})
|
||||
|
||||
test('does not save a backup when they are disabled', async (t) => {
|
||||
await t.context.backups.toggleEnabled()
|
||||
await t.context.windowLoaded
|
||||
/** Do not wait on this one as the backup shouldn't be triggered */
|
||||
t.context.backups.perform()
|
||||
await wait()
|
||||
const backupsLocation = await t.context.backups.location()
|
||||
const files = await fs.readdir(backupsLocation)
|
||||
t.deepEqual(files, ['decrypt.html'])
|
||||
})
|
||||
BIN
packages/desktop/test/data/zip-file.zip
Normal file
BIN
packages/desktop/test/data/zip-file.zip
Normal file
Binary file not shown.
203
packages/desktop/test/driver.ts
Normal file
203
packages/desktop/test/driver.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
/* 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
|
||||
}
|
||||
130
packages/desktop/test/extServer.spec.ts
Normal file
130
packages/desktop/test/extServer.spec.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import anyTest, { TestFn } from 'ava'
|
||||
import { promises as fs } from 'fs'
|
||||
import http from 'http'
|
||||
import { AddressInfo } from 'net'
|
||||
import path from 'path'
|
||||
import proxyquire from 'proxyquire'
|
||||
import { ensureDirectoryExists } from '../app/javascripts/Main/Utils/FileUtils'
|
||||
import { initializeStrings } from '../app/javascripts/Main/strings'
|
||||
import { createTmpDir } from './testUtils'
|
||||
import makeFakePaths from './fakePaths'
|
||||
|
||||
const test = anyTest as TestFn<{
|
||||
server: http.Server
|
||||
host: string
|
||||
}>
|
||||
|
||||
const tmpDir = createTmpDir(__filename)
|
||||
const FakePaths = makeFakePaths(tmpDir.path)
|
||||
|
||||
let server: http.Server
|
||||
|
||||
const { createExtensionsServer, normalizeFilePath } = proxyquire('../app/javascripts/Main/ExtensionsServer', {
|
||||
'./paths': {
|
||||
Paths: FakePaths,
|
||||
'@noCallThru': true,
|
||||
},
|
||||
electron: {
|
||||
app: {
|
||||
getPath() {
|
||||
return tmpDir.path
|
||||
},
|
||||
},
|
||||
},
|
||||
http: {
|
||||
createServer(...args: any) {
|
||||
server = http.createServer(...args)
|
||||
return server
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const extensionsDir = path.join(tmpDir.path, 'Extensions')
|
||||
|
||||
initializeStrings('en')
|
||||
|
||||
const log = console.log
|
||||
const error = console.error
|
||||
|
||||
test.before(async (t) => {
|
||||
/** Prevent the extensions server from outputting anything */
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
// console.log = () => {}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
// console.error = () => {}
|
||||
|
||||
await ensureDirectoryExists(extensionsDir)
|
||||
await new Promise((resolve) => {
|
||||
createExtensionsServer(resolve)
|
||||
t.context.server = server
|
||||
server.once('listening', () => {
|
||||
const { address, port } = server.address() as AddressInfo
|
||||
t.context.host = `http://${address}:${port}/`
|
||||
resolve(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test.after((t): Promise<any> => {
|
||||
/** Restore the console's functionality */
|
||||
console.log = log
|
||||
console.error = error
|
||||
|
||||
return Promise.all([tmpDir.clean(), new Promise((resolve) => t.context.server.close(resolve))])
|
||||
})
|
||||
|
||||
test('serves the files in the Extensions directory over HTTP', (t) => {
|
||||
const data = {
|
||||
name: 'Boxes',
|
||||
meter: {
|
||||
4: 4,
|
||||
},
|
||||
syncopation: true,
|
||||
instruments: ['Drums', 'Bass', 'Vocals', { name: 'Piano', type: 'Electric' }],
|
||||
}
|
||||
|
||||
return fs.writeFile(path.join(extensionsDir, 'file.json'), JSON.stringify(data)).then(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
let serverData = ''
|
||||
http.get(t.context.host + 'Extensions/file.json').on('response', (response) => {
|
||||
response
|
||||
.setEncoding('utf-8')
|
||||
.on('data', (chunk) => {
|
||||
serverData += chunk
|
||||
})
|
||||
.on('end', () => {
|
||||
t.deepEqual(data, JSON.parse(serverData))
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test('does not serve files outside the Extensions directory', async (t) => {
|
||||
await new Promise((resolve) => {
|
||||
http.get(t.context.host + 'Extensions/../../../package.json').on('response', (response) => {
|
||||
t.is(response.statusCode, 500)
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('returns a 404 for files that are not present', async (t) => {
|
||||
await new Promise((resolve) => {
|
||||
http.get(t.context.host + 'Extensions/nothing').on('response', (response) => {
|
||||
t.is(response.statusCode, 404)
|
||||
resolve(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
test('normalizes file paths to always point somewhere in the Extensions directory', (t) => {
|
||||
t.is(normalizeFilePath('/Extensions/test/yes', '127.0.0.1'), path.join(tmpDir.path, 'Extensions', 'test', 'yes'))
|
||||
t.is(
|
||||
normalizeFilePath('/Extensions/../../data/outside/the/extensions/directory'),
|
||||
path.join(tmpDir.path, 'Extensions', 'data', 'outside', 'the', 'extensions', 'directory'),
|
||||
)
|
||||
})
|
||||
29
packages/desktop/test/fakePaths.ts
Normal file
29
packages/desktop/test/fakePaths.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import path from 'path'
|
||||
|
||||
export default function makeFakePaths(tmpDir: string) {
|
||||
const Paths = {
|
||||
get userDataDir(): string {
|
||||
return tmpDir
|
||||
},
|
||||
get documentsDir(): string {
|
||||
return tmpDir
|
||||
},
|
||||
get tempDir(): string {
|
||||
return tmpDir
|
||||
},
|
||||
get extensionsDirRelative(): string {
|
||||
return 'Extensions'
|
||||
},
|
||||
get extensionsDir(): string {
|
||||
return path.join(Paths.userDataDir, 'Extensions')
|
||||
},
|
||||
get extensionsMappingJson(): string {
|
||||
return path.join(Paths.extensionsDir, 'mapping.json')
|
||||
},
|
||||
get windowPositionJson(): string {
|
||||
return path.join(Paths.userDataDir, 'window-position.json')
|
||||
},
|
||||
}
|
||||
|
||||
return Paths
|
||||
}
|
||||
111
packages/desktop/test/fileUtils.spec.ts
Normal file
111
packages/desktop/test/fileUtils.spec.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import test, { TestFn } from 'ava'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import {
|
||||
deleteDir,
|
||||
ensureDirectoryExists,
|
||||
extractNestedZip,
|
||||
FileDoesNotExist,
|
||||
moveDirContents,
|
||||
readJSONFile,
|
||||
writeJSONFile,
|
||||
} from '../app/javascripts/Main/Utils/FileUtils'
|
||||
|
||||
const dataPath = path.join(__dirname, 'data')
|
||||
const tmpPath = path.join(dataPath, 'tmp', path.basename(__filename))
|
||||
const zipFileDestination = path.join(tmpPath, 'zip-file-output')
|
||||
const root = path.join(tmpPath, 'tmp1')
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await ensureDirectoryExists(root)
|
||||
})
|
||||
|
||||
test.afterEach(async () => {
|
||||
await deleteDir(tmpPath)
|
||||
})
|
||||
|
||||
test('extracts a zip and unnests the folders by one level', async (t) => {
|
||||
await extractNestedZip(path.join(dataPath, 'zip-file.zip'), zipFileDestination)
|
||||
t.deepEqual(await fs.readdir(zipFileDestination), ['package.json', 'test-file.txt'])
|
||||
})
|
||||
|
||||
test('creates a directory even when parent directories are non-existent', async (t) => {
|
||||
await ensureDirectoryExists(path.join(root, 'tmp2', 'tmp3'))
|
||||
t.deepEqual(await fs.readdir(root), ['tmp2'])
|
||||
t.deepEqual(await fs.readdir(path.join(root, 'tmp2')), ['tmp3'])
|
||||
})
|
||||
|
||||
test('deletes a deeply-nesting directory', async (t) => {
|
||||
await ensureDirectoryExists(path.join(root, 'tmp2', 'tmp3'))
|
||||
await deleteDir(root)
|
||||
try {
|
||||
await fs.readdir(path.join(tmpPath, 'tmp1'))
|
||||
t.fail('Should not have been able to read')
|
||||
} catch (error: any) {
|
||||
if (error.code === FileDoesNotExist) {
|
||||
t.pass()
|
||||
} else {
|
||||
t.fail(error)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('moves the contents of one directory to the other', async (t) => {
|
||||
const fileNames = ['1.txt', '2.txt', '3.txt', 'nested/4.txt', 'nested/5.txt', 'nested/6.txt']
|
||||
|
||||
/** Create a temp directory and fill it with files */
|
||||
const dir = path.join(tmpPath, 'move_contents_src')
|
||||
await ensureDirectoryExists(dir)
|
||||
await ensureDirectoryExists(path.join(dir, 'nested'))
|
||||
await Promise.all(fileNames.map((fileName) => fs.writeFile(path.join(dir, fileName), fileName)))
|
||||
|
||||
/** Now move its contents */
|
||||
const dest = path.join(tmpPath, 'move_contents_dest')
|
||||
await moveDirContents(dir, dest)
|
||||
await Promise.all(
|
||||
fileNames.map(async (fileName) => {
|
||||
const contents = await fs.readFile(path.join(dest, fileName), 'utf8')
|
||||
t.is(contents, fileName)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test('moves the contents of one directory to a child directory', async (t) => {
|
||||
const srcFileNames = ['1.txt', '2.txt', '3.txt', 'nested/4.txt', 'nested/5.txt', 'nested/6.txt']
|
||||
const destFileNames = ['1.txt', '2.txt', '3.txt', '4.txt', '5.txt', '6.txt']
|
||||
|
||||
/** Create a temp directory and fill it with files */
|
||||
const dir = path.join(tmpPath, 'move_contents_src')
|
||||
await ensureDirectoryExists(dir)
|
||||
await ensureDirectoryExists(path.join(dir, 'nested'))
|
||||
await Promise.all(srcFileNames.map((fileName) => fs.writeFile(path.join(dir, fileName), fileName)))
|
||||
|
||||
/** Now move its contents */
|
||||
const dest = path.join(dir, 'nested')
|
||||
await moveDirContents(dir, dest)
|
||||
|
||||
/** Ensure everything is there */
|
||||
t.deepEqual((await fs.readdir(dest)).sort(), destFileNames.sort())
|
||||
await Promise.all(
|
||||
destFileNames.map(async (fileName, index) => {
|
||||
const contents = await fs.readFile(path.join(dest, fileName), 'utf8')
|
||||
t.is(contents, srcFileNames[index])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test('serializes and deserializes an object to the same values', async (t) => {
|
||||
const data = {
|
||||
meter: {
|
||||
4: 4,
|
||||
},
|
||||
chorus: {
|
||||
passengers: 2,
|
||||
destination: 'moon',
|
||||
activities: [{ type: 'play', environment: 'stars' }],
|
||||
},
|
||||
}
|
||||
const filePath = path.join(tmpPath, 'data.json')
|
||||
await writeJSONFile(filePath, data)
|
||||
t.deepEqual(data, await readJSONFile(filePath))
|
||||
})
|
||||
49
packages/desktop/test/menus.spec.ts
Normal file
49
packages/desktop/test/menus.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import anyTest, { TestFn } from 'ava'
|
||||
import { MenuItem } from 'electron'
|
||||
import { AppName } from '../app/javascripts/Main/strings'
|
||||
import { createDriver, Driver } from './driver'
|
||||
|
||||
const test = anyTest as TestFn<{
|
||||
driver: Driver
|
||||
menuItems: MenuItem[]
|
||||
}>
|
||||
|
||||
test.before(async (t) => {
|
||||
t.context.driver = await createDriver()
|
||||
})
|
||||
|
||||
test.after.always(async (t) => {
|
||||
await t.context.driver.stop()
|
||||
})
|
||||
|
||||
test.beforeEach(async (t) => {
|
||||
t.context.menuItems = await t.context.driver.appMenu.items()
|
||||
})
|
||||
|
||||
function findSpellCheckerLanguagesMenu(menuItems: MenuItem[]) {
|
||||
return menuItems.find((item) => {
|
||||
if (item.role?.toLowerCase() === 'editmenu') {
|
||||
return item?.submenu?.items?.find((item) => item.id === 'SpellcheckerLanguages')
|
||||
}
|
||||
})
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
test('shows the App menu on Mac', (t) => {
|
||||
t.is(t.context.menuItems[0].role.toLowerCase(), 'appmenu')
|
||||
t.is(t.context.menuItems[0].label, AppName)
|
||||
})
|
||||
|
||||
test('hides the spellchecking submenu on Mac', (t) => {
|
||||
t.falsy(findSpellCheckerLanguagesMenu(t.context.menuItems))
|
||||
})
|
||||
} else {
|
||||
test('hides the App menu on Windows/Linux', (t) => {
|
||||
t.is(t.context.menuItems[0].role.toLowerCase(), 'editmenu')
|
||||
})
|
||||
|
||||
test('shows the spellchecking submenu on Windows/Linux', (t) => {
|
||||
const menu = findSpellCheckerLanguagesMenu(t.context.menuItems)
|
||||
t.truthy(menu)
|
||||
t.true(menu!.submenu!.items!.length > 0)
|
||||
})
|
||||
}
|
||||
58
packages/desktop/test/networking.spec.ts
Normal file
58
packages/desktop/test/networking.spec.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import anyTest, { TestFn } from 'ava'
|
||||
import { promises as fs } from 'fs'
|
||||
import http from 'http'
|
||||
import { AddressInfo } from 'net'
|
||||
import path from 'path'
|
||||
import { createDriver, Driver } from './driver'
|
||||
import { createTmpDir } from './testUtils'
|
||||
|
||||
const test = anyTest as TestFn<Driver>
|
||||
|
||||
const tmpDir = createTmpDir(__filename)
|
||||
|
||||
const sampleData = {
|
||||
title: 'Diamond Dove',
|
||||
meter: {
|
||||
4: 4,
|
||||
},
|
||||
instruments: ['Piano', 'Chiptune'],
|
||||
}
|
||||
|
||||
let server: http.Server
|
||||
let serverAddress: string
|
||||
|
||||
test.before(
|
||||
(): Promise<any> =>
|
||||
Promise.all([
|
||||
tmpDir.make(),
|
||||
new Promise((resolve) => {
|
||||
server = http.createServer((_req, res) => {
|
||||
res.write(JSON.stringify(sampleData))
|
||||
res.end()
|
||||
})
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
const { address, port } = server.address() as AddressInfo
|
||||
serverAddress = `http://${address}:${port}`
|
||||
resolve(null)
|
||||
})
|
||||
}),
|
||||
]),
|
||||
)
|
||||
|
||||
test.after((): Promise<any> => Promise.all([tmpDir.clean(), new Promise((resolve) => server.close(resolve))]))
|
||||
|
||||
test.beforeEach(async (t) => {
|
||||
t.context = await createDriver()
|
||||
})
|
||||
test.afterEach((t) => t.context.stop())
|
||||
|
||||
test('downloads a JSON file', async (t) => {
|
||||
t.deepEqual(await t.context.net.getJSON(serverAddress), sampleData)
|
||||
})
|
||||
|
||||
test('downloads a folder to the specified location', async (t) => {
|
||||
const filePath = path.join(tmpDir.path, 'fileName.json')
|
||||
await t.context.net.downloadFile(serverAddress + '/file', filePath)
|
||||
const fileContents = await fs.readFile(filePath, 'utf8')
|
||||
t.is(JSON.stringify(sampleData), fileContents)
|
||||
})
|
||||
158
packages/desktop/test/packageManager.spec.ts
Normal file
158
packages/desktop/test/packageManager.spec.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import test from 'ava'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import proxyquire from 'proxyquire'
|
||||
import { ensureDirectoryExists, readJSONFile } from '../app/javascripts/Main/Utils/FileUtils'
|
||||
import { createTmpDir } from './testUtils'
|
||||
import { AppName } from '../app/javascripts/Main/strings'
|
||||
import makeFakePaths from './fakePaths'
|
||||
import { PackageManagerInterface } from '../app/javascripts/Main/Packages/PackageManagerInterface'
|
||||
|
||||
const tmpDir = createTmpDir(__filename)
|
||||
const FakePaths = makeFakePaths(tmpDir.path)
|
||||
|
||||
const contentDir = path.join(tmpDir.path, 'Extensions')
|
||||
let downloadFileCallCount = 0
|
||||
|
||||
const { initializePackageManager } = proxyquire('../app/javascripts/Main/Packages/PackageManager', {
|
||||
'./paths': {
|
||||
Paths: FakePaths,
|
||||
'@noCallThru': true,
|
||||
},
|
||||
'./networking': {
|
||||
/** Download a fake component file */
|
||||
async downloadFile(_src: string, dest: string) {
|
||||
downloadFileCallCount += 1
|
||||
if (!path.normalize(dest).startsWith(tmpDir.path)) {
|
||||
throw new Error(`Bad download destination: ${dest}`)
|
||||
}
|
||||
await ensureDirectoryExists(path.dirname(dest))
|
||||
await fs.copyFile(path.join(__dirname, 'data', 'zip-file.zip'), path.join(dest))
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const fakeWebContents = {
|
||||
send(_eventName: string, { error }) {
|
||||
if (error) throw error
|
||||
},
|
||||
}
|
||||
|
||||
const name = 'Fake Component'
|
||||
const identifier = 'fake.component'
|
||||
const uuid = 'fake-component'
|
||||
const version = '1.0.0'
|
||||
const modifiers = Array(20)
|
||||
.fill(0)
|
||||
.map((_, i) => String(i).padStart(2, '0'))
|
||||
|
||||
function fakeComponent({ deleted = false, modifier = '' } = {}) {
|
||||
return {
|
||||
uuid: uuid + modifier,
|
||||
deleted,
|
||||
content: {
|
||||
name: name + modifier,
|
||||
autoupdateDisabled: false,
|
||||
package_info: {
|
||||
version,
|
||||
identifier: identifier + modifier,
|
||||
download_url: 'https://standardnotes.com',
|
||||
url: 'https://standardnotes.com',
|
||||
latest_url: 'https://standardnotes.com',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let packageManager: PackageManagerInterface
|
||||
|
||||
const log = console.log
|
||||
const error = console.error
|
||||
|
||||
test.before(async function () {
|
||||
/** Silence the package manager's output. */
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
console.log = () => {}
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
console.error = () => {}
|
||||
await ensureDirectoryExists(contentDir)
|
||||
packageManager = await initializePackageManager(fakeWebContents)
|
||||
})
|
||||
|
||||
test.after.always(async function () {
|
||||
console.log = log
|
||||
console.error = error
|
||||
await tmpDir.clean()
|
||||
})
|
||||
|
||||
test.beforeEach(function () {
|
||||
downloadFileCallCount = 0
|
||||
})
|
||||
|
||||
test('installs multiple components', async (t) => {
|
||||
await packageManager.syncComponents(modifiers.map((modifier) => fakeComponent({ modifier })))
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
|
||||
const files = await fs.readdir(contentDir)
|
||||
t.is(files.length, 1 + modifiers.length)
|
||||
for (const modifier of modifiers) {
|
||||
t.true(files.includes(identifier + modifier))
|
||||
}
|
||||
t.true(files.includes('mapping.json'))
|
||||
const mappingContents = await fs.readFile(path.join(contentDir, 'mapping.json'), 'utf8')
|
||||
|
||||
t.deepEqual(
|
||||
JSON.parse(mappingContents),
|
||||
modifiers.reduce((acc, modifier) => {
|
||||
acc[uuid + modifier] = {
|
||||
location: path.join('Extensions', identifier + modifier),
|
||||
version,
|
||||
}
|
||||
return acc
|
||||
}, {}),
|
||||
)
|
||||
|
||||
const downloads = await fs.readdir(path.join(tmpDir.path, AppName, 'downloads'))
|
||||
t.is(downloads.length, modifiers.length)
|
||||
for (const modifier of modifiers) {
|
||||
t.true(downloads.includes(`${name + modifier}.zip`))
|
||||
}
|
||||
|
||||
for (const modifier of modifiers) {
|
||||
const componentFiles = await fs.readdir(path.join(contentDir, identifier + modifier))
|
||||
t.is(componentFiles.length, 2)
|
||||
}
|
||||
})
|
||||
|
||||
test('uninstalls multiple components', async (t) => {
|
||||
await packageManager.syncComponents(modifiers.map((modifier) => fakeComponent({ deleted: true, modifier })))
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
|
||||
const files = await fs.readdir(contentDir)
|
||||
t.deepEqual(files, ['mapping.json'])
|
||||
|
||||
t.deepEqual(await readJSONFile(path.join(contentDir, 'mapping.json')), {})
|
||||
})
|
||||
|
||||
test("doesn't download anything when two install/uninstall tasks are queued", async (t) => {
|
||||
await Promise.all([
|
||||
packageManager.syncComponents([fakeComponent({ deleted: false })]),
|
||||
packageManager.syncComponents([fakeComponent({ deleted: false })]),
|
||||
packageManager.syncComponents([fakeComponent({ deleted: true })]),
|
||||
])
|
||||
t.is(downloadFileCallCount, 1)
|
||||
})
|
||||
|
||||
test("Relies on download_url's version field to store the version number", async (t) => {
|
||||
await packageManager.syncComponents([fakeComponent()])
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
|
||||
const mappingFileVersion = JSON.parse(await fs.readFile(path.join(contentDir, 'mapping.json'), 'utf8'))[uuid].version
|
||||
|
||||
const packageJsonVersion = JSON.parse(
|
||||
await fs.readFile(path.join(contentDir, identifier, 'package.json'), 'utf-8'),
|
||||
).version
|
||||
|
||||
t.not(mappingFileVersion, packageJsonVersion)
|
||||
t.is(mappingFileVersion, version)
|
||||
})
|
||||
38
packages/desktop/test/spellcheckerManager.spec.ts
Normal file
38
packages/desktop/test/spellcheckerManager.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import anyTest, { TestFn } from 'ava'
|
||||
import { Driver, createDriver } from './driver'
|
||||
|
||||
const StoreKeys = {
|
||||
SelectedSpellCheckerLanguageCodes: 'selectedSpellCheckerLanguageCodes',
|
||||
}
|
||||
|
||||
const test = anyTest as TestFn<Driver>
|
||||
|
||||
test.before(async (t) => {
|
||||
t.context = await createDriver()
|
||||
})
|
||||
|
||||
test.after.always(async (t) => {
|
||||
await t.context.stop()
|
||||
})
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
test('does not create a manager on Mac', async (t) => {
|
||||
t.falsy(await t.context.spellchecker.manager())
|
||||
})
|
||||
} else {
|
||||
const language = 'cs'
|
||||
|
||||
test("adds a clicked language menu item to the store and session's languages", async (t) => {
|
||||
await t.context.appMenu.clickLanguage(language as any)
|
||||
const data = await t.context.storage.dataOnDisk()
|
||||
t.true(data[StoreKeys.SelectedSpellCheckerLanguageCodes].includes(language))
|
||||
t.true((await t.context.spellchecker.languages()).includes(language))
|
||||
})
|
||||
|
||||
test("removes a clicked language menu item to the store's and session's languages", async (t) => {
|
||||
await t.context.appMenu.clickLanguage(language as any)
|
||||
const data = await t.context.storage.dataOnDisk()
|
||||
t.false(data[StoreKeys.SelectedSpellCheckerLanguageCodes].includes(language))
|
||||
t.false((await t.context.spellchecker.languages()).includes(language))
|
||||
})
|
||||
}
|
||||
130
packages/desktop/test/storage.spec.ts
Normal file
130
packages/desktop/test/storage.spec.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import anyTest, { TestFn, ExecutionContext } from 'ava'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import proxyquire from 'proxyquire'
|
||||
import { timeout } from '../app/javascripts/Main/Utils/Utils'
|
||||
import { createDriver, Driver } from './driver'
|
||||
|
||||
const { serializeStoreData } = proxyquire('../app/javascripts/Main/Store', {
|
||||
'./backupsManager': {
|
||||
'@noCallThru': true,
|
||||
},
|
||||
'@electron': {
|
||||
'@noCallThru': true,
|
||||
},
|
||||
'@electron/remote': {
|
||||
'@noCallThru': true,
|
||||
},
|
||||
})
|
||||
|
||||
async function validateData(t: ExecutionContext<Driver>) {
|
||||
const data = await t.context.storage.dataOnDisk()
|
||||
|
||||
/**
|
||||
* There should always be 8 values in the store.
|
||||
* If one was added/removed intentionally, update this number
|
||||
*/
|
||||
const numberOfStoreKeys = 10
|
||||
t.is(Object.keys(data).length, numberOfStoreKeys)
|
||||
|
||||
t.is(typeof data.isMenuBarVisible, 'boolean')
|
||||
|
||||
t.is(typeof data.useSystemMenuBar, 'boolean')
|
||||
|
||||
t.is(typeof data.backupsDisabled, 'boolean')
|
||||
|
||||
t.is(typeof data.minimizeToTray, 'boolean')
|
||||
|
||||
t.is(typeof data.enableAutoUpdates, 'boolean')
|
||||
|
||||
t.is(typeof data.zoomFactor, 'number')
|
||||
t.true(data.zoomFactor > 0)
|
||||
|
||||
t.is(typeof data.extServerHost, 'string')
|
||||
/** Must not throw */
|
||||
const extServerHost = new URL(data.extServerHost)
|
||||
t.is(extServerHost.hostname, '127.0.0.1')
|
||||
t.is(extServerHost.protocol, 'http:')
|
||||
t.is(extServerHost.port, '45653')
|
||||
|
||||
t.is(typeof data.backupsLocation, 'string')
|
||||
|
||||
t.is(data.useNativeKeychain, null)
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
t.is(data.selectedSpellCheckerLanguageCodes, null)
|
||||
} else {
|
||||
t.true(Array.isArray(data.selectedSpellCheckerLanguageCodes))
|
||||
for (const language of data.selectedSpellCheckerLanguageCodes) {
|
||||
t.is(typeof language, 'string')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const test = anyTest as TestFn<Driver>
|
||||
|
||||
test.beforeEach(async (t) => {
|
||||
t.context = await createDriver()
|
||||
})
|
||||
test.afterEach.always((t) => {
|
||||
return t.context.stop()
|
||||
})
|
||||
|
||||
test('has valid data', async (t) => {
|
||||
await validateData(t)
|
||||
})
|
||||
|
||||
test('recreates a missing data file', async (t) => {
|
||||
const location = await t.context.storage.dataLocation()
|
||||
/** Delete the store's backing file */
|
||||
await fs.promises.unlink(location)
|
||||
await t.context.restart()
|
||||
await validateData(t)
|
||||
})
|
||||
|
||||
test('recovers from corrupted data', async (t) => {
|
||||
const location = await t.context.storage.dataLocation()
|
||||
/** Write bad data in the store's file */
|
||||
await fs.promises.writeFile(location, '\uFFFF'.repeat(300))
|
||||
await t.context.restart()
|
||||
await validateData(t)
|
||||
})
|
||||
|
||||
test('persists changes to disk after setting a value', async (t) => {
|
||||
const factor = 4.8
|
||||
await t.context.storage.setZoomFactor(factor)
|
||||
const diskData = await t.context.storage.dataOnDisk()
|
||||
t.is(diskData.zoomFactor, factor)
|
||||
})
|
||||
|
||||
test('serializes string sets to an array', (t) => {
|
||||
t.deepEqual(
|
||||
serializeStoreData({
|
||||
set: new Set(['value']),
|
||||
} as any),
|
||||
JSON.stringify({
|
||||
set: ['value'],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test('deletes local storage data after signing out', async (t) => {
|
||||
function readLocalStorageContents() {
|
||||
return fs.promises.readFile(path.join(t.context.userDataPath, 'Local Storage', 'leveldb', '000003.log'), {
|
||||
encoding: 'utf8',
|
||||
})
|
||||
}
|
||||
await t.context.windowLoaded
|
||||
await t.context.storage.setLocalStorageValue('foo', 'bar')
|
||||
let localStorageContents = await readLocalStorageContents()
|
||||
|
||||
t.is(localStorageContents.includes('foo'), true)
|
||||
t.is(localStorageContents.includes('bar'), true)
|
||||
|
||||
await timeout(1_000)
|
||||
await t.context.window.signOut()
|
||||
await timeout(1_000)
|
||||
localStorageContents = await readLocalStorageContents()
|
||||
t.is(localStorageContents.includes('foo'), false)
|
||||
t.is(localStorageContents.includes('bar'), false)
|
||||
})
|
||||
21
packages/desktop/test/testUtils.ts
Normal file
21
packages/desktop/test/testUtils.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import path from 'path'
|
||||
import { deleteDir, ensureDirectoryExists } from '../app/javascripts/Main/Utils/FileUtils'
|
||||
|
||||
export function createTmpDir(name: string): {
|
||||
path: string
|
||||
make(): Promise<string>
|
||||
clean(): Promise<void>
|
||||
} {
|
||||
const tmpDirPath = path.join(__dirname, 'data', 'tmp', path.basename(name))
|
||||
|
||||
return {
|
||||
path: tmpDirPath,
|
||||
async make() {
|
||||
await ensureDirectoryExists(tmpDirPath)
|
||||
return tmpDirPath
|
||||
},
|
||||
async clean() {
|
||||
await deleteDir(tmpDirPath)
|
||||
},
|
||||
}
|
||||
}
|
||||
21
packages/desktop/test/updates.spec.ts
Normal file
21
packages/desktop/test/updates.spec.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import anyTest, { TestFn } from 'ava'
|
||||
import { createDriver, Driver } from './driver'
|
||||
|
||||
const test = anyTest as TestFn<Driver>
|
||||
|
||||
test.beforeEach(async (t) => {
|
||||
t.context = await createDriver()
|
||||
})
|
||||
|
||||
test.afterEach.always(async (t) => {
|
||||
await t.context.stop()
|
||||
})
|
||||
|
||||
test('has auto-updates enabled by default', async (t) => {
|
||||
t.true(await t.context.updates.autoUpdateEnabled())
|
||||
})
|
||||
|
||||
test('reloads the menu after checking for an update', async (t) => {
|
||||
await t.context.updates.check()
|
||||
t.true(await t.context.appMenu.hasReloaded())
|
||||
})
|
||||
14
packages/desktop/test/utils.spec.ts
Normal file
14
packages/desktop/test/utils.spec.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import test from 'ava'
|
||||
import { lowercaseDriveLetter } from '../app/javascripts/Main/Utils/Utils'
|
||||
|
||||
test("lowerCaseDriverLetter converts the drive letter of a given file's path to lower case", (t) => {
|
||||
t.is(lowercaseDriveLetter('/C:/Lansing'), '/c:/Lansing')
|
||||
t.is(lowercaseDriveLetter('/c:/Bone Rage'), '/c:/Bone Rage')
|
||||
t.is(lowercaseDriveLetter('/C:/Give/Us/the/Gold'), '/c:/Give/Us/the/Gold')
|
||||
})
|
||||
|
||||
test('lowerCaseDriverLetter only changes a single drive letter', (t) => {
|
||||
t.is(lowercaseDriveLetter('C:/Hold Me In'), 'C:/Hold Me In')
|
||||
t.is(lowercaseDriveLetter('/Cd:/Egg Replacer'), '/Cd:/Egg Replacer')
|
||||
t.is(lowercaseDriveLetter('/C:radle of Rocks'), '/C:radle of Rocks')
|
||||
})
|
||||
16
packages/desktop/test/window.spec.ts
Normal file
16
packages/desktop/test/window.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import anyTest, { TestFn } from 'ava'
|
||||
import { createDriver, Driver } from './driver'
|
||||
|
||||
const test = anyTest as TestFn<Driver>
|
||||
|
||||
test.before(async (t) => {
|
||||
t.context = await createDriver()
|
||||
})
|
||||
|
||||
test.after.always((t) => {
|
||||
return t.context.stop()
|
||||
})
|
||||
|
||||
test('Only has one window', async (t) => {
|
||||
t.is(await t.context.windowCount(), 1)
|
||||
})
|
||||
Reference in New Issue
Block a user