diff --git a/src/ambient.d.ts b/src/ambient.d.ts index 9df6ac97ff..0926c4abe3 100644 --- a/src/ambient.d.ts +++ b/src/ambient.d.ts @@ -13,6 +13,7 @@ import { OutputEntry, PMOperationOptions, RunResult, + RunnableVersion, SelectedLocalVersion, TestRequest, Version, @@ -72,6 +73,10 @@ declare global { type: 'toggle-monaco-option', listener: (path: string) => void, ): void; + addEventListener( + type: 'electron-types-changed', + listener: (types: string, version: string) => void, + ): void; addModules( { dir, packageManager }: PMOperationOptions, ...names: Array @@ -87,6 +92,7 @@ declare global { ): Promise; fetchVersions(): Promise; getAvailableThemes(): Promise>; + getElectronTypes(ver: RunnableVersion): Promise; getIsPackageManagerInstalled( packageManager: IPackageManager, ignoreCache?: boolean, @@ -128,6 +134,8 @@ declare global { showWindow(): void; taskDone(result: RunResult): void; themePath: string; + uncacheTypes(ver: RunnableVersion): Promise; + unwatchElectronTypes(): Promise; }; } } diff --git a/src/constants.ts b/src/constants.ts index 8a7b17bf6f..4652758562 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,2 +1,4 @@ export const SENTRY_DSN = 'https://966a5b01ac8d4941b81e4ebd0ab4c991@sentry.io/1882540'; + +export const ELECTRON_DTS = 'electron.d.ts'; diff --git a/src/interfaces.ts b/src/interfaces.ts index a3de5365b7..64c8b7ff3a 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -154,6 +154,7 @@ export type FiddleEvent = | 'before-quit' | 'bisect-task' | 'clear-console' + | 'electron-types-changed' | 'execute-monaco-command' | 'load-example' | 'load-gist' diff --git a/src/ipc-events.ts b/src/ipc-events.ts index ef0640f291..4b26f02c36 100644 --- a/src/ipc-events.ts +++ b/src/ipc-events.ts @@ -59,6 +59,10 @@ export enum IpcEvents { GET_PROJECT_NAME = 'GET_PROJECT_NAME', GET_USERNAME = 'GET_USERNAME', PATH_EXISTS = 'PATH_EXISTS', + ELECTRON_TYPES_CHANGED = 'ELECTRON_TYPES_CHANGED', + UNCACHE_TYPES = 'UNCACHE_TYPES', + GET_ELECTRON_TYPES = 'GET_ELECTRON_TYPES', + UNWATCH_ELECTRON_TYPES = 'UNWATCH_ELECTRON_TYPES', GET_NODE_TYPES = 'GET_NODE_TYPES', } @@ -97,6 +101,8 @@ export const ipcMainEvents = [ IpcEvents.GET_PROJECT_NAME, IpcEvents.GET_USERNAME, IpcEvents.PATH_EXISTS, + IpcEvents.GET_ELECTRON_TYPES, + IpcEvents.UNWATCH_ELECTRON_TYPES, IpcEvents.GET_NODE_TYPES, ]; diff --git a/src/main/electron-types.ts b/src/main/electron-types.ts index 2874291b64..e098c51535 100644 --- a/src/main/electron-types.ts +++ b/src/main/electron-types.ts @@ -2,23 +2,84 @@ import * as path from 'node:path'; import { ElectronVersions } from '@electron/fiddle-core'; import fetch from 'cross-fetch'; -import { IpcMainEvent, app } from 'electron'; +import { BrowserWindow, IpcMainEvent, app } from 'electron'; import * as fs from 'fs-extra'; +import watch from 'node-watch'; import packageJson from 'package-json'; import readdir from 'recursive-readdir'; import semver from 'semver'; import { ipcMainManager } from './ipc'; -import { NodeTypes } from '../interfaces'; +import { ELECTRON_DTS } from '../constants'; +import { NodeTypes, RunnableVersion, VersionSource } from '../interfaces'; import { IpcEvents } from '../ipc-events'; let electronTypes: ElectronTypes; export class ElectronTypes { + private localPaths: Map; + private watchers: Map; + constructor( private readonly knownVersions: ElectronVersions, + private readonly electronCacheDir: string, private readonly nodeCacheDir: string, - ) {} + ) { + this.localPaths = new Map(); + this.watchers = new Map(); + } + + private getWindowsForLocalPath(localPath: string): BrowserWindow[] { + return Array.from(this.localPaths.entries()) + .filter(([, path]) => path === localPath) + .map(([window]) => window); + } + + public async getElectronTypes( + window: BrowserWindow, + ver: RunnableVersion, + ): Promise { + const { localPath: dir, source, version } = ver; + let content: string | undefined; + + // If it's a local development version, pull Electron types from out directory. + if (dir) { + const file = path.join(dir, 'gen/electron/tsc/typings', ELECTRON_DTS); + content = this.getTypesFromFile(file); + try { + this.unwatch(window); + this.localPaths.set(window, dir); + + // If no watcher for that path yet, create it + if (!this.watchers.has(dir)) { + const watcher = watch(file, () => { + // Watcher should notify all windows watching that path + const windows = this.getWindowsForLocalPath(dir); + for (const window of windows) { + ipcMainManager.send( + IpcEvents.ELECTRON_TYPES_CHANGED, + [fs.readFileSync(file, 'utf8'), version], + window.webContents, + ); + } + }); + this.watchers.set(dir, watcher); + } + window.once('close', () => this.unwatch(window)); + } catch (err) { + console.debug(`Unable to watch "${file}" for changes: ${err}`); + } + } + + // If it's a published version, pull from cached file. + else if (source === VersionSource.remote) { + const file = this.getCacheFile(version); + await this.ensureElectronVersionIsCachedAt(version, file); + content = this.getTypesFromFile(file); + } + + return content; + } public async getNodeTypes( version: string, @@ -41,6 +102,11 @@ export class ElectronTypes { } } + public uncache(ver: RunnableVersion) { + if (ver.source === VersionSource.remote) + fs.removeSync(this.getCacheFile(ver.version)); + } + private async getTypesFromDir(dir: string): Promise { const types: NodeTypes = {}; @@ -60,10 +126,42 @@ export class ElectronTypes { return types; } + private getTypesFromFile(file: string): string | undefined { + try { + return fs.readFileSync(file, 'utf8'); + } catch (err) { + console.debug(`Unable to read types from "${file}": ${err.message}`); + return undefined; + } + } + + private getCacheFile(version: string) { + return path.join(this.electronCacheDir, version, ELECTRON_DTS); + } + private getCacheDir(version: string) { return path.join(this.nodeCacheDir, version); } + public unwatch(window: BrowserWindow) { + const localPath = this.localPaths.get(window); + + if (localPath) { + this.localPaths.delete(window); + + const windows = this.getWindowsForLocalPath(localPath); + + // If it's the last window watching that path, close the watcher + if (!windows.length) { + const watcher = this.watchers.get(localPath); + if (watcher) { + watcher.close(); + } + this.watchers.delete(localPath); + } + } + } + /** * This function ensures that the Node.js version for a given version of * Electron is downloaded and cached. It can be the case that DefinitelyTyped @@ -123,6 +221,21 @@ export class ElectronTypes { return downloadVersion; } + + private async ensureElectronVersionIsCachedAt(version: string, file: string) { + if (fs.existsSync(file)) return; + + const name = version.includes('nightly') ? 'electron-nightly' : 'electron'; + const url = `https://unpkg.com/${name}@${version}/${ELECTRON_DTS}`; + try { + const response = await fetch(url); + const text = await response.text(); + if (text.includes('Cannot find package')) throw new Error(text); + fs.outputFileSync(file, text); + } catch (err) { + console.warn(`Error saving "${url}" to "${file}": ${err}`); + } + } } export async function setupTypes(knownVersions: ElectronVersions) { @@ -130,13 +243,35 @@ export async function setupTypes(knownVersions: ElectronVersions) { electronTypes = new ElectronTypes( knownVersions, + path.join(userDataPath, 'electron-typedef'), path.join(userDataPath, 'nodejs-typedef'), ); + ipcMainManager.handle( + IpcEvents.GET_ELECTRON_TYPES, + (event: IpcMainEvent, ver: RunnableVersion) => { + return electronTypes.getElectronTypes( + BrowserWindow.fromWebContents(event.sender)!, + ver, + ); + }, + ); ipcMainManager.handle( IpcEvents.GET_NODE_TYPES, (_: IpcMainEvent, version: string) => { return electronTypes.getNodeTypes(version); }, ); + ipcMainManager.handle( + IpcEvents.UNCACHE_TYPES, + (_: IpcMainEvent, ver: RunnableVersion) => { + electronTypes.uncache(ver); + }, + ); + ipcMainManager.handle( + IpcEvents.UNWATCH_ELECTRON_TYPES, + (event: IpcMainEvent) => { + electronTypes.unwatch(BrowserWindow.fromWebContents(event.sender)!); + }, + ); } diff --git a/src/preload/preload.ts b/src/preload/preload.ts index 2a8cbf958a..80f9ec39ea 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -9,6 +9,7 @@ const channelMapping: Record = { 'before-quit': IpcEvents.BEFORE_QUIT, 'bisect-task': IpcEvents.TASK_BISECT, 'clear-console': IpcEvents.CLEAR_CONSOLE, + 'electron-types-changed': IpcEvents.ELECTRON_TYPES_CHANGED, 'execute-monaco-command': IpcEvents.MONACO_EXECUTE_COMMAND, 'load-example': IpcEvents.LOAD_ELECTRON_EXAMPLE_REQUEST, 'load-gist': IpcEvents.LOAD_GIST_REQUEST, @@ -79,6 +80,10 @@ export async function setupFiddleGlobal() { fetchVersions() { return ipcRenderer.invoke(IpcEvents.FETCH_VERSIONS); }, + getElectronTypes(ver) { + // Destructure ver into a copy, as the object sometimes can't be cloned + return ipcRenderer.invoke(IpcEvents.GET_ELECTRON_TYPES, { ...ver }); + }, getLatestStable() { return ipcRenderer.sendSync(IpcEvents.GET_LATEST_STABLE); }, @@ -181,6 +186,13 @@ export async function setupFiddleGlobal() { ipcRenderer.send(IpcEvents.TASK_DONE, result); }, themePath: await ipcRenderer.sendSync(IpcEvents.GET_THEME_PATH), + async uncacheTypes(ver) { + // Destructure ver into a copy, as the object sometimes can't be cloned + await ipcRenderer.invoke(IpcEvents.UNCACHE_TYPES, { ...ver }); + }, + async unwatchElectronTypes() { + await ipcRenderer.invoke(IpcEvents.UNWATCH_ELECTRON_TYPES); + }, }; } diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index 94b1e452fc..a27276c3f0 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -1,8 +1,5 @@ -import * as path from 'node:path'; - import { autorun, reaction, when } from 'mobx'; -import { USER_DATA_PATH } from './constants'; import { ElectronTypes } from './electron-types'; import { FileManager } from './file-manager'; import { RemoteLoader } from './remote-loader'; @@ -42,10 +39,7 @@ export class App { this.taskRunner = new TaskRunner(this); - this.electronTypes = new ElectronTypes( - window.ElectronFiddle.monaco, - path.join(USER_DATA_PATH, 'electron-typedef'), - ); + this.electronTypes = new ElectronTypes(window.ElectronFiddle.monaco); } private confirmReplaceUnsaved(): Promise { diff --git a/src/renderer/constants.ts b/src/renderer/constants.ts index 0090c3f9cd..fb954a9e28 100644 --- a/src/renderer/constants.ts +++ b/src/renderer/constants.ts @@ -1,6 +1,6 @@ import * as path from 'node:path'; -export const USER_DATA_PATH = window.ElectronFiddle.appPaths.userData; +const USER_DATA_PATH = window.ElectronFiddle.appPaths.userData; export const ELECTRON_DOWNLOAD_PATH = path.join(USER_DATA_PATH, 'electron-bin'); export const ELECTRON_INSTALL_PATH = path.join( ELECTRON_DOWNLOAD_PATH, diff --git a/src/renderer/electron-types.ts b/src/renderer/electron-types.ts index ab18ff6034..763928240c 100644 --- a/src/renderer/electron-types.ts +++ b/src/renderer/electron-types.ts @@ -1,13 +1,8 @@ -import * as path from 'node:path'; - -import * as fs from 'fs-extra'; import * as MonacoType from 'monaco-editor'; -import watch from 'node-watch'; +import { ELECTRON_DTS } from '../constants'; import { NodeTypes, RunnableVersion, VersionSource } from '../interfaces'; -const ELECTRON_DTS = 'electron.d.ts'; - /** * Keeps monaco informed of the current Electron version's .d.ts * @@ -18,41 +13,38 @@ const ELECTRON_DTS = 'electron.d.ts'; export class ElectronTypes { private disposables: MonacoType.IDisposable[] = []; private electronTypesDisposable: MonacoType.IDisposable | undefined; - private watcher: fs.FSWatcher | undefined; - constructor( - private readonly monaco: typeof MonacoType, - private readonly electronCacheDir: string, - ) {} + constructor(private readonly monaco: typeof MonacoType) { + window.ElectronFiddle.addEventListener( + 'electron-types-changed', + (types, version) => { + this.electronTypesDisposable?.dispose(); + this.electronTypesDisposable = undefined; + + this.setElectronTypes(types, version); + }, + ); + } public async setVersion(ver?: RunnableVersion): Promise { this.clear(); if (!ver) return; - await this.setElectronTypes(ver); + this.setElectronTypes( + await window.ElectronFiddle.getElectronTypes(ver), + ver.version, + ); await this.setNodeTypes(ver.version); } - private async setElectronTypes(ver: RunnableVersion): Promise { - const { localPath: dir, source, version } = ver; - - // If it's a local development version, pull Electron types from out directory. - if (dir) { - const file = path.join(dir, 'gen/electron/tsc/typings', ELECTRON_DTS); - this.setTypesFromFile(file, version); - try { - this.watcher = watch(file, () => this.setTypesFromFile(file, version)); - } catch (err) { - console.debug(`Unable to watch "${file}" for changes: ${err}`); - } - } - - // If it's a published version, pull from cached file. - if (source === VersionSource.remote) { - const file = this.getCacheFile(version); - await this.ensureElectronVersionIsCachedAt(version, file); - this.setTypesFromFile(file, version); + private setElectronTypes(types: string | undefined, version: string): void { + if (types) { + console.log(`Updating Monaco with "${ELECTRON_DTS}@${version}"`); + this.electronTypesDisposable = + this.monaco.languages.typescript.javascriptDefaults.addExtraLib(types); + } else { + console.log(`No types found for "${ELECTRON_DTS}@${version}"`); } } @@ -72,71 +64,28 @@ export class ElectronTypes { this.disposables.push(lib); } } else { - console.log(`No types found for Node.js in Electron ${ver}`); + console.log(`No Node.js types found for Electron ${ver}`); } } - public uncache(ver: RunnableVersion) { - if (ver.source === VersionSource.remote) - fs.removeSync(this.getCacheFile(ver.version)); - } - - private setTypesFromFile(file: string, version: string) { - // Dispose of any previous Electron types so there's only ever one - this.electronTypesDisposable?.dispose(); - this.electronTypesDisposable = undefined; - - try { - console.log(`Updating Monaco with "${ELECTRON_DTS}@${version}"`); - this.electronTypesDisposable = - this.monaco.languages.typescript.javascriptDefaults.addExtraLib( - fs.readFileSync(file, 'utf8'), - ); - } catch (err) { - console.debug(`Unable to read types from "${file}": ${err.message}`); + public async uncache(ver: RunnableVersion) { + if (ver.source === VersionSource.remote) { + await window.ElectronFiddle.uncacheTypes(ver); } } - private getCacheFile(version: string) { - return path.join(this.electronCacheDir, version, ELECTRON_DTS); - } - - private clear() { + private async clear() { this.dispose(); - this.unwatch(); + await window.ElectronFiddle.unwatchElectronTypes(); } private dispose() { this.electronTypesDisposable?.dispose(); this.electronTypesDisposable = undefined; - if (this.disposables.length > 0) { - for (const disposable of this.disposables) { - disposable.dispose(); - } - this.disposables = []; - } - } - - private unwatch() { - if (this.watcher) { - this.watcher.close(); - delete this.watcher; - } - } - - private async ensureElectronVersionIsCachedAt(version: string, file: string) { - if (fs.existsSync(file)) return; - - const name = version.includes('nightly') ? 'electron-nightly' : 'electron'; - const url = `https://unpkg.com/${name}@${version}/${ELECTRON_DTS}`; - try { - const response = await window.fetch(url); - const text = await response.text(); - if (text.includes('Cannot find package')) throw new Error(text); - fs.outputFileSync(file, text); - } catch (err) { - console.warn(`Error saving "${url}" to "${file}": ${err}`); + for (const disposable of this.disposables) { + disposable.dispose(); } + this.disposables = []; } } diff --git a/src/renderer/state.ts b/src/renderer/state.ts index bbfedcd24d..5ddd0b684b 100644 --- a/src/renderer/state.ts +++ b/src/renderer/state.ts @@ -815,11 +815,7 @@ export class AppState { ) { await this.installer.remove(version); if (this.installer.state(version) === InstallState.missing) { - const typeDefsCleaner = async () => { - window.ElectronFiddle.app.electronTypes.uncache(ver); - }; - - await typeDefsCleaner(); + await window.ElectronFiddle.app.electronTypes.uncache(ver); this.broadcastVersionStates([ver]); } diff --git a/tests/main/electron-types-spec.ts b/tests/main/electron-types-spec.ts index ff22cbdd52..70d227e4d7 100644 --- a/tests/main/electron-types-spec.ts +++ b/tests/main/electron-types-spec.ts @@ -1,7 +1,8 @@ import * as path from 'node:path'; -import { ReleaseInfo } from '@electron/fiddle-core'; +import { ElectronVersions, ReleaseInfo } from '@electron/fiddle-core'; import { fetch } from 'cross-fetch'; +import type { BrowserWindow } from 'electron'; import * as fs from 'fs-extra'; import { mocked } from 'jest-mock'; import * as tmp from 'tmp'; @@ -11,10 +12,14 @@ import { RunnableVersion, VersionSource, } from '../../src/interfaces'; +import { IpcEvents } from '../../src/ipc-events'; import { ElectronTypes } from '../../src/main/electron-types'; +import { ipcMainManager } from '../../src/main/ipc'; import { ElectronVersionsMock } from '../mocks/fiddle-core'; -import { NodeTypesMock } from '../mocks/mocks'; +import { BrowserWindowMock, NodeTypesMock } from '../mocks/mocks'; +import { waitFor } from '../utils'; +jest.mock('../../src/main/ipc'); jest.mock('cross-fetch'); jest.unmock('fs-extra'); @@ -23,11 +28,15 @@ const { Response } = jest.requireActual('cross-fetch'); describe('ElectronTypes', () => { const version = '10.11.12'; const nodeVersion = '16.2.0'; + let cacheFile: string; + let localFile: string; + let localVersion: RunnableVersion; let remoteVersion: RunnableVersion; let tmpdir: tmp.DirResult; let electronTypes: ElectronTypes; let nodeTypesData: NodeTypesMock[]; let electronVersions: ElectronVersionsMock; + let browserWindow: BrowserWindow; beforeEach(() => { tmpdir = tmp.dirSync({ @@ -35,6 +44,7 @@ describe('ElectronTypes', () => { unsafeCleanup: true, }); + const electronCacheDir = path.join(tmpdir.name, 'electron-cache'); const nodeCacheDir = path.join(tmpdir.name, 'node-cache'); const localDir = path.join(tmpdir.name, 'local'); @@ -46,18 +56,163 @@ describe('ElectronTypes', () => { state: InstallState.installed, source: VersionSource.remote, } as const; + cacheFile = path.join( + electronCacheDir, + remoteVersion.version, + 'electron.d.ts', + ); + + localVersion = { + version, + localPath: localDir, + state: InstallState.installed, + source: VersionSource.local, + } as const; + localFile = path.join(localDir, 'gen/electron/tsc/typings/electron.d.ts'); electronVersions = new ElectronVersionsMock(); - electronTypes = new ElectronTypes(electronVersions as any, nodeCacheDir); + electronTypes = new ElectronTypes( + electronVersions as unknown as ElectronVersions, + electronCacheDir, + nodeCacheDir, + ); nodeTypesData = require('../fixtures/node-types.json'); mocked(electronVersions.getReleaseInfo).mockReturnValue({ node: nodeVersion, } as ReleaseInfo); + + browserWindow = new BrowserWindowMock() as unknown as BrowserWindow; }); afterEach(async () => { + electronTypes.unwatch(browserWindow); tmpdir.removeCallback(); + mocked(fetch).mockReset(); + }); + + describe('getElectronTypes({ source: local })', () => { + const missingLocalVersion = { + version, + localPath: '/dev/null', + state: InstallState.installed, + source: VersionSource.local, + } as const; + + function saveTypesFile(content: string) { + fs.outputFileSync(localFile, content); + return content; + } + + it('watches for the types file to be updated', async () => { + const oldTypes = saveTypesFile('some types'); + await expect( + electronTypes.getElectronTypes(browserWindow, localVersion), + ).resolves.toBe(oldTypes); + + const newTypes = saveTypesFile('some changed types'); + expect(newTypes).not.toEqual(oldTypes); + await waitFor(() => mocked(ipcMainManager).send.mock.calls.length > 0); + expect(ipcMainManager.send).toHaveBeenCalledWith( + IpcEvents.ELECTRON_TYPES_CHANGED, + [newTypes, localVersion.version], + browserWindow.webContents, + ); + }); + + it('stops watching old types files when the version changes', async () => { + // set to version A + let types = saveTypesFile('some types'); + await expect( + electronTypes.getElectronTypes(browserWindow, localVersion), + ).resolves.toBe(types); + + // now switch to version B + await electronTypes.getElectronTypes(browserWindow, missingLocalVersion); + + // test that updating the now-unobserved version A triggers no actions + types = saveTypesFile('some changed types'); + try { + await waitFor(() => mocked(ipcMainManager).send.mock.calls.length > 0); + } catch (err) { + expect(err).toMatch(/timed out/i); + } + expect(ipcMainManager.send).not.toHaveBeenCalled(); + }); + + it('does not crash if the types file is missing', async () => { + await expect( + electronTypes.getElectronTypes(browserWindow, missingLocalVersion), + ).resolves.toBe(undefined); + }); + }); + + describe('getElectronTypes({ source: remote })', () => { + it('fetches types', async () => { + const version = { ...remoteVersion, version: '15.0.0-nightly.20210628' }; + const types = 'here are the types'; + mocked(fetch).mockImplementation( + () => + new Response(types, { + status: 200, + statusText: 'OK', + }), + ); + + await expect( + electronTypes.getElectronTypes(browserWindow, version), + ).resolves.toEqual(types); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith( + expect.stringMatching('electron-nightly'), + ); + }); + + it('caches fetched types', async () => { + expect(fs.existsSync(cacheFile)).toBe(false); + + // setup: fetch and cache a .d.ts that we did not have + const types = 'here are the types'; + mocked(fetch).mockImplementation( + () => + new Response(types, { + status: 200, + statusText: 'OK', + }), + ); + await expect( + electronTypes.getElectronTypes(browserWindow, remoteVersion), + ).resolves.toEqual(types); + expect(fetch).toHaveBeenCalledTimes(1); + expect(fs.existsSync(cacheFile)).toBe(true); + + // test re-using the same version does not trigger another fetch + // (i.e. the types are cached locally) + await electronTypes.getElectronTypes(browserWindow, remoteVersion); + expect(fetch).toHaveBeenCalledTimes(1); + }); + + it('does not crash if fetch() rejects', async () => { + mocked(fetch).mockRejectedValue(new Error('💩')); + await expect( + electronTypes.getElectronTypes(browserWindow, remoteVersion), + ).resolves.toBe(undefined); + expect(fetch).toHaveBeenCalled(); + }); + + it('does not crash if fetch() does not find the package', async () => { + mocked(fetch).mockResolvedValue( + new Response('Cannot find package', { + status: 404, + statusText: 'Not Found', + }), + ); + await expect( + electronTypes.getElectronTypes(browserWindow, remoteVersion), + ).resolves.toBe(undefined); + expect(fetch).toHaveBeenCalled(); + }); }); describe('getNodeTypes', () => { @@ -114,4 +269,33 @@ describe('ElectronTypes', () => { expect(fetch).not.toHaveBeenCalled(); }); }); + + describe('uncache', () => { + beforeEach(async () => { + // setup: fetch and cache some types + expect(fs.existsSync(cacheFile)).toBe(false); + const types = 'here are the types'; + mocked(fetch).mockImplementation( + () => + new Response(JSON.stringify({ files: types }), { + status: 200, + statusText: 'OK', + }), + ); + await electronTypes.getElectronTypes(browserWindow, remoteVersion); + }); + + it('uncaches fetched types', () => { + expect(fs.existsSync(cacheFile)).toBe(true); + electronTypes.uncache(remoteVersion); + expect(fs.existsSync(cacheFile)).toBe(false); + }); + + it('does not touch local builds', () => { + expect(fs.existsSync(cacheFile)).toBe(true); + const version = { ...remoteVersion, source: VersionSource.local }; + electronTypes.uncache(version); + expect(fs.existsSync(cacheFile)).toBe(true); + }); + }); }); diff --git a/tests/mocks/electron-fiddle.ts b/tests/mocks/electron-fiddle.ts index b3b3481222..6143769fa8 100644 --- a/tests/mocks/electron-fiddle.ts +++ b/tests/mocks/electron-fiddle.ts @@ -14,6 +14,7 @@ export class ElectronFiddleMock { public createThemeFile = jest.fn(); public fetchVersions = jest.fn(); public getAvailableThemes = jest.fn(); + public getElectronTypes = jest.fn(); public getIsPackageManagerInstalled = jest.fn(); public getLatestStable = jest.fn(); public getLocalVersionState = jest.fn(); @@ -45,4 +46,6 @@ export class ElectronFiddleMock { public taskDone = jest.fn(); public readThemeFile = jest.fn(); public themePath = '~/.electron-fiddle/themes'; + public uncacheTypes = jest.fn(); + public unwatchElectronTypes = jest.fn(); } diff --git a/tests/renderer/electron-types-spec.ts b/tests/renderer/electron-types-spec.ts index 24231e30b7..aed90455d1 100644 --- a/tests/renderer/electron-types-spec.ts +++ b/tests/renderer/electron-types-spec.ts @@ -1,8 +1,4 @@ -import * as path from 'node:path'; - -import * as fs from 'fs-extra'; import { mocked } from 'jest-mock'; -import * as tmp from 'tmp'; import { InstallState, @@ -12,35 +8,19 @@ import { } from '../../src/interfaces'; import { ElectronTypes } from '../../src/renderer/electron-types'; import { MonacoMock, NodeTypesMock } from '../mocks/mocks'; -import { waitFor } from '../utils'; - -jest.unmock('fs-extra'); +import { emitEvent, waitFor } from '../utils'; describe('ElectronTypes', () => { const version = '10.11.12'; let addExtraLib: ReturnType; - let cacheFile: string; - let localFile: string; let localVersion: RunnableVersion; let monaco: MonacoMock; let remoteVersion: RunnableVersion; - let tmpdir: tmp.DirResult; let electronTypes: ElectronTypes; let nodeTypesData: NodeTypesMock[]; let disposable: { dispose: typeof jest.fn }; beforeEach(() => { - tmpdir = tmp.dirSync({ - template: 'electron-fiddle-typedefs-XXXXXX', - unsafeCleanup: true, - }); - - const electronCacheDir = path.join(tmpdir.name, 'electron-cache'); - const localDir = path.join(tmpdir.name, 'local'); - - fs.ensureDirSync(electronCacheDir); - fs.ensureDirSync(localDir); - monaco = new MonacoMock(); ({ addExtraLib } = monaco.languages.typescript.javascriptDefaults); disposable = { dispose: jest.fn() }; @@ -51,36 +31,22 @@ describe('ElectronTypes', () => { state: InstallState.installed, source: VersionSource.remote, } as const; - cacheFile = path.join( - electronCacheDir, - remoteVersion.version, - 'electron.d.ts', - ); localVersion = { version, - localPath: localDir, + localPath: '/foo/bar/', state: InstallState.installed, source: VersionSource.local, } as const; - localFile = path.join(localDir, 'gen/electron/tsc/typings/electron.d.ts'); - electronTypes = new ElectronTypes(monaco as any, electronCacheDir); + electronTypes = new ElectronTypes(monaco as any); nodeTypesData = require('../fixtures/node-types.json'); }); afterEach(async () => { await electronTypes.setVersion(); - tmpdir.removeCallback(); }); - function makeFetchSpy(text: string) { - return jest.spyOn(global, 'fetch').mockResolvedValue({ - text: async () => text, - json: async () => ({}), - } as Response); - } - describe('setVersion({ source: local })', () => { const missingLocalVersion = { version, @@ -89,20 +55,17 @@ describe('ElectronTypes', () => { source: VersionSource.local, } as const; - function saveTypesFile(content: string) { - fs.outputFileSync(localFile, content); - return content; - } - it('gives types to monaco', async () => { - const types = saveTypesFile('some types'); + const types = 'some types'; + mocked(window.ElectronFiddle.getElectronTypes).mockResolvedValue(types); await electronTypes.setVersion(localVersion); expect(addExtraLib).toHaveBeenCalledWith(types); }); it('disposes the previous monaco content', async () => { // setup: call setVersion once to get some content into monaco - const types = saveTypesFile('some types'); + const types = 'some types'; + mocked(window.ElectronFiddle.getElectronTypes).mockResolvedValue(types); await electronTypes.setVersion(localVersion); expect(addExtraLib).toHaveBeenCalledWith(types); expect(disposable.dispose).not.toHaveBeenCalled(); @@ -113,12 +76,16 @@ describe('ElectronTypes', () => { }); it('watches for the types file to be updated', async () => { - const oldTypes = saveTypesFile('some types'); + const oldTypes = 'some types'; + mocked(window.ElectronFiddle.getElectronTypes).mockResolvedValue( + oldTypes, + ); await electronTypes.setVersion(localVersion); expect(addExtraLib).toHaveBeenCalledWith(oldTypes); expect(disposable.dispose).not.toHaveBeenCalled(); - const newTypes = saveTypesFile('some changed types'); + const newTypes = 'some changed types'; + emitEvent('electron-types-changed', newTypes); expect(newTypes).not.toEqual(oldTypes); await waitFor(() => addExtraLib.mock.calls.length > 1); expect(addExtraLib).toHaveBeenCalledWith(newTypes); @@ -127,7 +94,8 @@ describe('ElectronTypes', () => { it('stops watching old types files when the version changes', async () => { // set to version A - let types = saveTypesFile('some types'); + const types = 'some types'; + mocked(window.ElectronFiddle.getElectronTypes).mockResolvedValue(types); await electronTypes.setVersion(localVersion); expect(addExtraLib).toHaveBeenCalledWith(types); @@ -137,7 +105,6 @@ describe('ElectronTypes', () => { // test that updating the now-unobserved version A triggers no actions addExtraLib.mockReset(); - types = saveTypesFile('some changed types'); try { await waitFor(() => addExtraLib.mock.calls.length > 0); } catch (err) { @@ -157,10 +124,10 @@ describe('ElectronTypes', () => { addExtraLib.mockReturnValue({ dispose: jest.fn() }); }); - it('fetches types', async () => { + it('gets types', async () => { const version = { ...remoteVersion, version: '15.0.0-nightly.20210628' }; const types = 'here are the types'; - const fetchSpy = makeFetchSpy(types); + mocked(window.ElectronFiddle.getElectronTypes).mockResolvedValue(types); mocked(window.ElectronFiddle.getNodeTypes).mockResolvedValue({ version: version.version, types: nodeTypesData @@ -170,9 +137,8 @@ describe('ElectronTypes', () => { await electronTypes.setVersion(version); - expect(fetchSpy).toHaveBeenCalledTimes(1); - expect(fetchSpy).toHaveBeenCalledWith( - expect.stringMatching('electron-nightly'), + expect(window.ElectronFiddle.getElectronTypes).toHaveBeenCalledWith( + version, ); expect(window.ElectronFiddle.getNodeTypes).toHaveBeenCalledWith( version.version, @@ -182,42 +148,6 @@ describe('ElectronTypes', () => { expect(addExtraLib).toHaveBeenCalledWith(types); }); - it('caches fetched types', async () => { - expect(fs.existsSync(cacheFile)).toBe(false); - - // setup: fetch and cache a .d.ts that we did not have - const types = 'here are the types'; - const fetchSpy = makeFetchSpy(types); - await electronTypes.setVersion(remoteVersion); - expect(addExtraLib).toHaveBeenCalledTimes(1); - expect(addExtraLib).toHaveBeenLastCalledWith(types); - expect(fetchSpy).toHaveBeenCalledTimes(1); - expect(fs.existsSync(cacheFile)).toBe(true); - - // test re-using the same version does not trigger another fetch - // (i.e. the types are cached locally) - await electronTypes.setVersion(remoteVersion); - expect(addExtraLib).toHaveBeenCalledTimes(2); - expect(addExtraLib).toHaveBeenLastCalledWith(types); - expect(fetchSpy).toHaveBeenCalledTimes(1); - }); - - it('does not crash if fetch() rejects', async () => { - const spy = jest - .spyOn(global, 'fetch') - .mockRejectedValue(new Error('💩')); - await electronTypes.setVersion(remoteVersion); - expect(spy).toHaveBeenCalledTimes(1); - expect(addExtraLib).not.toHaveBeenCalled(); - }); - - it('does not crash if fetch() does not find the package', async () => { - const spy = makeFetchSpy('Cannot find package'); - await electronTypes.setVersion(remoteVersion); - expect(spy).toHaveBeenCalledTimes(1); - expect(addExtraLib).not.toHaveBeenCalled(); - }); - it('does not crash if types are not found', async () => { mocked(window.ElectronFiddle.getNodeTypes).mockResolvedValue(undefined); await electronTypes.setVersion(remoteVersion); @@ -227,25 +157,15 @@ describe('ElectronTypes', () => { }); describe('uncache()', () => { - beforeEach(async () => { - // setup: fetch and cache some types - expect(fs.existsSync(cacheFile)).toBe(false); - const types = 'here are the types'; - makeFetchSpy(types); - await electronTypes.setVersion(remoteVersion); - }); - - it('uncaches fetched types', () => { - expect(fs.existsSync(cacheFile)).toBe(true); - electronTypes.uncache(remoteVersion); - expect(fs.existsSync(cacheFile)).toBe(false); + it('uncaches remote versions', async () => { + await electronTypes.uncache(remoteVersion); + expect(window.ElectronFiddle.uncacheTypes).toHaveBeenCalled(); }); - it('does not touch local builds', () => { - expect(fs.existsSync(cacheFile)).toBe(true); + it('does not uncache local versions', async () => { const version = { ...remoteVersion, source: VersionSource.local }; - electronTypes.uncache(version); - expect(fs.existsSync(cacheFile)).toBe(true); + await electronTypes.uncache(version); + expect(window.ElectronFiddle.uncacheTypes).not.toHaveBeenCalled(); }); }); });