diff --git a/main/export.ts b/main/export.ts index f32f5298..6a5bd437 100644 --- a/main/export.ts +++ b/main/export.ts @@ -60,6 +60,11 @@ export default class Export extends (EventEmitter as new () => TypedEventEmitter try { await action; + + if (this.context.isCanceled) { + return; + } + this.status = ExportStatus.completed; this.text = 'Export completed'; this.emit('updated', this.data); diff --git a/package.json b/package.json index ee5148b8..ec338202 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,8 @@ "xo": "^0.38.2" }, "_moduleAliases": { - "electron": "test/mocks/electron.ts" + "electron": "test/mocks/electron.ts", + "electron-store": "test/mocks/electron-store.ts" }, "ava": { "files": [ diff --git a/test/export-repro.ts b/test/export-repro.ts new file mode 100644 index 00000000..d9cd61ba --- /dev/null +++ b/test/export-repro.ts @@ -0,0 +1,61 @@ +import test from 'ava'; +import delay from 'delay'; +import {ExportStatus, Format} from '../main/common/types'; +import Export from '../main/export'; +import Video from './mocks/video'; + +test('Export status should be canceled if canceled during action', async t => { + const mockVideo = new Video() as any; + mockVideo.title = 'test-video'; + mockVideo.generatePreviewImage = () => {}; + + let resolveAction: (value?: unknown) => void = () => {}; + const actionPromise = new Promise(resolve => { + resolveAction = resolve; + }); + + const mockService = { + title: 'test-service', + formats: [Format.mp4], + action: async () => actionPromise + }; + + const mockPlugin = { + name: 'test-plugin', + shareServices: [mockService] + }; + + const exportInstance = new Export( + mockVideo, + Format.mp4, + {width: 100, height: 100, fps: 30} as any, + { + plugin: mockPlugin as any, + service: mockService as any, + extras: {} + } + ); + + // Start export + exportInstance.on('updated', state => { + console.log('Updated state:', state.status); + }); + const startPromise = exportInstance.start(); + + // Wait a bit to ensure it started + await delay(10); + + // Cancel it + exportInstance.cancel(); + + // Resolve the action *after* cancel + resolveAction(); + + // Wait for startPromise to settle + await startPromise; + + await delay(10); + + // Assert status + t.is(exportInstance.status, ExportStatus.canceled, 'Export status should be canceled'); +}); diff --git a/test/mocks/electron-store.ts b/test/mocks/electron-store.ts index c1ff308b..d8a82c8b 100644 --- a/test/mocks/electron-store.ts +++ b/test/mocks/electron-store.ts @@ -30,8 +30,30 @@ const clearMock = sinon.fake( export default class Store { get = getMock; set = setMock; + has = sinon.fake((key: string) => key in mocks || key in store); delete = deleteMock; - clear = clearMock; + + constructor(private readonly options?: any) { + this.initDefaults(); + } + + private initDefaults() { + if (this.options?.schema) { + for (const [key, value] of Object.entries(this.options.schema)) { + if ((value as any).default !== undefined) { + store[key] = (value as any).default; + } + } + } + } + + clear = () => { + for (const key of Object.keys(store)) { + delete store[key]; + } + + this.initDefaults(); + }; get store() { return { diff --git a/test/mocks/electron.ts b/test/mocks/electron.ts index 5a85bc62..4644fe41 100644 --- a/test/mocks/electron.ts +++ b/test/mocks/electron.ts @@ -10,7 +10,7 @@ process.env.TZ = 'America/New_York'; export const app = { getPath: (name: string) => path.resolve(temporaryDir, name), isPackaged: false, - getVersion: '' + getVersion: () => '3.0.0' }; export const shell = { @@ -22,3 +22,11 @@ export const clipboard = { }; export const remote = {}; + +export const Menu = { + getApplicationMenu: () => ({ + getMenuItemById: () => ({ + enabled: false + }) + }) +};