From 39c9244cbf2d9aa17db646d449604a431edfd1be Mon Sep 17 00:00:00 2001 From: wpxp123456 <2677556700@qq.com> Date: Tue, 27 Jan 2026 19:23:27 +0800 Subject: [PATCH] fix(api): fix api FFormula.calculationResultApplied and FFormula.onCalculationResultApplied (#6522) --- .../engine-formula/src/facade/f-formula.ts | 156 +++++------------- .../data-sync/data-sync-primary.controller.ts | 2 +- .../data-sync/data-sync-replica.controller.ts | 2 +- .../remote-instance.service.ts | 16 +- .../sheets-formula/src/facade/f-formula.ts | 147 ++++++++++++++++- .../calculate-result-apply.controller.ts | 12 +- .../src/__testing__/test-formula-move.ts | 12 +- .../test-remove-rows-of-filter-rows.ts | 4 +- 8 files changed, 210 insertions(+), 141 deletions(-) diff --git a/packages/engine-formula/src/facade/f-formula.ts b/packages/engine-formula/src/facade/f-formula.ts index dd408e0d84..5f120e8685 100644 --- a/packages/engine-formula/src/facade/f-formula.ts +++ b/packages/engine-formula/src/facade/f-formula.ts @@ -15,10 +15,48 @@ */ import type { ICommandInfo, IDisposable, IUnitRange } from '@univerjs/core'; -import type { FormulaExecutedStateType, IExecutionInProgressParams, IExprTreeNode, IFormulaDependencyTreeFullJson, IFormulaDependencyTreeJson, IFormulaDependentsAndInRangeResults, IFormulaExecuteResultMap, IFormulaStringMap, ISequenceNode, ISetCellFormulaDependencyCalculationResultMutation, ISetFormulaCalculationNotificationMutation, ISetFormulaCalculationResultMutation, ISetFormulaCalculationStartMutation, ISetFormulaDependencyCalculationResultMutation, ISetFormulaStringBatchCalculationResultMutation, ISetQueryFormulaDependencyAllResultMutation } from '@univerjs/engine-formula'; +import type { + FormulaExecutedStateType, + IExecutionInProgressParams, + IExprTreeNode, + IFormulaDependencyTreeFullJson, + IFormulaDependencyTreeJson, + IFormulaDependentsAndInRangeResults, + IFormulaExecuteResultMap, + IFormulaStringMap, + ISequenceNode, + ISetCellFormulaDependencyCalculationResultMutation, + ISetFormulaCalculationNotificationMutation, + ISetFormulaCalculationStartMutation, + ISetFormulaDependencyCalculationResultMutation, + ISetFormulaStringBatchCalculationResultMutation, + ISetQueryFormulaDependencyAllResultMutation, +} from '@univerjs/engine-formula'; import { ICommandService, IConfigService, Inject, Injector } from '@univerjs/core'; import { FBase } from '@univerjs/core/facade'; -import { ENGINE_FORMULA_CYCLE_REFERENCE_COUNT, ENGINE_FORMULA_RETURN_DEPENDENCY_TREE, GlobalComputingStatusService, IDefinedNamesService, IFunctionService, ISuperTableService, LexerTreeBuilder, SetCellFormulaDependencyCalculationMutation, SetCellFormulaDependencyCalculationResultMutation, SetFormulaCalculationNotificationMutation, SetFormulaCalculationResultMutation, SetFormulaCalculationStartMutation, SetFormulaCalculationStopMutation, SetFormulaDependencyCalculationMutation, SetFormulaDependencyCalculationResultMutation, SetFormulaStringBatchCalculationMutation, SetFormulaStringBatchCalculationResultMutation, SetQueryFormulaDependencyAllMutation, SetQueryFormulaDependencyAllResultMutation, SetQueryFormulaDependencyMutation, SetQueryFormulaDependencyResultMutation, SetTriggerFormulaCalculationStartMutation } from '@univerjs/engine-formula'; +import { + ENGINE_FORMULA_CYCLE_REFERENCE_COUNT, + ENGINE_FORMULA_RETURN_DEPENDENCY_TREE, + GlobalComputingStatusService, + IDefinedNamesService, + IFunctionService, + ISuperTableService, + LexerTreeBuilder, + SetCellFormulaDependencyCalculationMutation, + SetCellFormulaDependencyCalculationResultMutation, + SetFormulaCalculationNotificationMutation, + SetFormulaCalculationStartMutation, + SetFormulaCalculationStopMutation, + SetFormulaDependencyCalculationMutation, + SetFormulaDependencyCalculationResultMutation, + SetFormulaStringBatchCalculationMutation, + SetFormulaStringBatchCalculationResultMutation, + SetQueryFormulaDependencyAllMutation, + SetQueryFormulaDependencyAllResultMutation, + SetQueryFormulaDependencyMutation, + SetQueryFormulaDependencyResultMutation, + SetTriggerFormulaCalculationStartMutation, +} from '@univerjs/engine-formula'; import { filter, firstValueFrom, map, race, timer } from 'rxjs'; /** @@ -240,120 +278,6 @@ export class FFormula extends FBase { this._configService.setConfig(ENGINE_FORMULA_CYCLE_REFERENCE_COUNT, maxIteration); } - /** - * Listens for the moment when formula-calculation results are applied. - * - * This event fires after the engine completes a calculation cycle and - * dispatches a `SetFormulaCalculationResultMutation`. - * The callback is invoked during an idle frame to avoid blocking UI updates. - * - * @param {Function} callback - A function called with the calculation result payload - * once the result-application mutation is emitted. - * @returns {IDisposable} A disposable used to unsubscribe from the event. - * - * @example - * ```ts - * const formulaEngine = univerAPI.getFormula(); - * - * const dispose = formulaEngine.calculationResultApplied((result) => { - * console.log('Calculation results applied:', result); - * }); - * - * // Later… - * dispose.dispose(); - * ``` - */ - calculationResultApplied(callback: (result: ISetFormulaCalculationResultMutation) => void): IDisposable { - return this._commandService.onCommandExecuted((command: ICommandInfo) => { - if (command.id !== SetFormulaCalculationResultMutation.id) { - return; - } - - const params = command.params as ISetFormulaCalculationResultMutation; - - if (params !== undefined) { - requestIdleCallback(() => { - callback(params); - }); - } - }); - } - - /** - * Waits for formula-calculation results to be applied. - * - * This method resolves under three conditions: - * 1. A real calculation runs and the engine emits a "calculation started" signal, - * followed by a "calculation result applied" signal. - * 2. No calculation actually starts within 500 ms — the method assumes there is - * nothing to wait for and resolves automatically. - * 3. A global 30 s timeout triggers, in which case the promise rejects. - * - * The API internally listens to both “calculation in progress” events and - * “calculation result applied” events, ensuring it behaves correctly whether - * formulas are recalculated or skipped due to cache/state. - * - * @returns {Promise} A promise that resolves when calculation results are applied - * or when no calculation occurs within the start-detection window. - * - * @example - * ```ts - * const formulaEngine = univerAPI.getFormula(); - * - * // Wait for formula updates to apply before reading values. - * await formulaEngine.onCalculationResultApplied(); - * - * const value = sheet.getRange("C24").getValue(); - * console.log("Updated value:", value); - * ``` - */ - onCalculationResultApplied(): Promise { - return new Promise((resolve, reject) => { - let started = false; - let finished = false; - - // Global timeout: reject if the whole calculation hangs - const mainTimer = setTimeout(() => { - cleanup(); - reject(new Error('Calculation end timeout')); - }, 30_000); - - // Watchdog: if no "calculation started" signal is received within 500ms, - // assume there is no real calculation running and resolve immediately. - const startWatchdog = setTimeout(() => { - if (!started) { - cleanup(); - resolve(); - } - }, 500); - - // Listen for "calculation in progress" signal (stageInfo) - const processingDisposable = this.calculationProcessing(() => { - if (started) return; - started = true; - - // A start signal is received → no need for the watchdog anymore - clearTimeout(startWatchdog); - }); - - // Listen for the "calculation completed" signal - const endDisposable = this.calculationResultApplied(() => { - if (finished) return; - finished = true; - - cleanup(); - resolve(); - }); - - function cleanup(): void { - clearTimeout(mainTimer); - clearTimeout(startWatchdog); - processingDisposable.dispose(); - endDisposable.dispose(); - } - }); - } - /** * Execute a batch of formulas asynchronously and receive computed results. * diff --git a/packages/rpc/src/controllers/data-sync/data-sync-primary.controller.ts b/packages/rpc/src/controllers/data-sync/data-sync-primary.controller.ts index f0b7e72733..7e06621b5a 100644 --- a/packages/rpc/src/controllers/data-sync/data-sync-primary.controller.ts +++ b/packages/rpc/src/controllers/data-sync/data-sync-primary.controller.ts @@ -120,7 +120,7 @@ export class DataSyncPrimaryController extends RxDisposable { // do not sync mutations those are not meant to be synced this._syncingMutations.has(id) ) { - this._remoteInstanceService.syncMutation({ mutationInfo: commandInfo as IMutationInfo }); + this._remoteInstanceService.syncMutation({ mutationInfo: commandInfo as IMutationInfo }, options); } })); } diff --git a/packages/rpc/src/controllers/data-sync/data-sync-replica.controller.ts b/packages/rpc/src/controllers/data-sync/data-sync-replica.controller.ts index 69d45edd4c..d5808baab5 100644 --- a/packages/rpc/src/controllers/data-sync/data-sync-replica.controller.ts +++ b/packages/rpc/src/controllers/data-sync/data-sync-replica.controller.ts @@ -62,7 +62,7 @@ export class DataSyncReplicaController extends Disposable { if (commandInfo.type === CommandType.MUTATION && !(options as IRemoteSyncMutationOptions)?.fromSync) { this._remoteSyncService.syncMutation({ mutationInfo: commandInfo as IMutationInfo, - }); + }, options); } }) ); diff --git a/packages/rpc/src/services/remote-instance/remote-instance.service.ts b/packages/rpc/src/services/remote-instance/remote-instance.service.ts index b91c8a99fa..ddd86b2c81 100644 --- a/packages/rpc/src/services/remote-instance/remote-instance.service.ts +++ b/packages/rpc/src/services/remote-instance/remote-instance.service.ts @@ -30,15 +30,17 @@ export const RemoteSyncServiceName = 'rpc.remote-sync.service'; */ export const IRemoteSyncService = createIdentifier(RemoteSyncServiceName); export interface IRemoteSyncService { - syncMutation(params: { mutationInfo: IMutationInfo }): Promise; + syncMutation(params: { mutationInfo: IMutationInfo }, options?: IExecutionOptions): Promise; } export class RemoteSyncPrimaryService implements IRemoteSyncService { constructor(@ICommandService private readonly _commandService: ICommandService) { // empty } - async syncMutation(params: { mutationInfo: IMutationInfo }): Promise { + async syncMutation(params: { mutationInfo: IMutationInfo }, options?: IExecutionOptions): Promise { + const { fromCollab, ...restOptions } = options || {}; return this._commandService.syncExecuteCommand(params.mutationInfo.id, params.mutationInfo.params, { + ...restOptions, onlyLocal: true, fromSync: true, }); @@ -60,7 +62,7 @@ export interface IRemoteInstanceService { createInstance(params: { unitID: string; type: UniverInstanceType; snapshot: IWorkbookData }): Promise; disposeInstance(params: { unitID: string }): Promise; - syncMutation(params: { mutationInfo: IMutationInfo }): Promise; + syncMutation(params: { mutationInfo: IMutationInfo }, options?: IExecutionOptions): Promise; } export class WebWorkerRemoteInstanceService implements IRemoteInstanceService { @@ -76,8 +78,8 @@ export class WebWorkerRemoteInstanceService implements IRemoteInstanceService { return Promise.resolve(true); } - async syncMutation(params: { mutationInfo: IMutationInfo }): Promise { - return this._applyMutation(params.mutationInfo); + async syncMutation(params: { mutationInfo: IMutationInfo }, options?: IExecutionOptions): Promise { + return this._applyMutation(params.mutationInfo, options); } async createInstance(params: { @@ -111,9 +113,11 @@ export class WebWorkerRemoteInstanceService implements IRemoteInstanceService { return this._univerInstanceService.disposeUnit(params.unitID); } - protected _applyMutation(mutationInfo: IMutationInfo): boolean { + protected _applyMutation(mutationInfo: IMutationInfo, options?: IExecutionOptions): boolean { const { id, params: mutationParams } = mutationInfo; + const { fromCollab, ...restOptions } = options || {}; return this._commandService.syncExecuteCommand(id, mutationParams, { + ...restOptions, onlyLocal: true, fromSync: true, }); diff --git a/packages/sheets-formula/src/facade/f-formula.ts b/packages/sheets-formula/src/facade/f-formula.ts index 2b3a98bccc..d3858c6601 100644 --- a/packages/sheets-formula/src/facade/f-formula.ts +++ b/packages/sheets-formula/src/facade/f-formula.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import type { IDisposable, ILocales } from '@univerjs/core'; - -import type { IFunctionInfo } from '@univerjs/engine-formula'; +import type { ICommandInfo, IDisposable, ILocales } from '@univerjs/core'; +import type { IFunctionInfo, ISetFormulaCalculationResultMutation } from '@univerjs/engine-formula'; import type { CalculationMode, IRegisterAsyncFunction, IRegisterFunction, ISingleFunctionRegisterParams, IUniverSheetsFormulaBaseConfig } from '@univerjs/sheets-formula'; import { debounce, IConfigService, ILogService, LifecycleService, LifecycleStages } from '@univerjs/core'; -import { SetTriggerFormulaCalculationStartMutation } from '@univerjs/engine-formula'; +import { FormulaExecuteStageType, SetFormulaCalculationResultMutation, SetTriggerFormulaCalculationStartMutation } from '@univerjs/engine-formula'; import { FFormula } from '@univerjs/engine-formula/facade'; +import { SetRangeValuesMutation } from '@univerjs/sheets'; import { IRegisterFunctionService, PLUGIN_CONFIG_KEY_BASE, RegisterFunctionService } from '@univerjs/sheets-formula'; /** @@ -271,6 +271,61 @@ export interface IFFormulaSheetsMixin { * ``` */ registerAsyncFunction(name: string, func: IRegisterAsyncFunction, { locales, description }: { locales?: ILocales; description?: string | IFunctionInfo }): IDisposable; + + /** + * Listens for the moment when formula-calculation results are applied. + * + * This event fires after the engine completes a calculation cycle and + * dispatches a `SetFormulaCalculationResultMutation`. + * The callback is invoked during an idle frame to avoid blocking UI updates. + * + * @param {Function} callback - A function called with the calculation result payload + * once the result-application mutation is emitted. + * @returns {IDisposable} A disposable used to unsubscribe from the event. + * + * @example + * ```ts + * const formulaEngine = univerAPI.getFormula(); + * + * const dispose = formulaEngine.calculationResultApplied((result) => { + * console.log('Calculation results applied:', result); + * }); + * + * // Later… + * dispose.dispose(); + * ``` + */ + calculationResultApplied(callback: (result: ISetFormulaCalculationResultMutation) => void): IDisposable; + + /** + * Waits for formula-calculation results to be applied. + * + * This method resolves under three conditions: + * 1. A real calculation runs and the engine emits a "calculation started" signal, + * followed by a "calculation result applied" signal. + * 2. No calculation actually starts within 500 ms — the method assumes there is + * nothing to wait for and resolves automatically. + * 3. A global 30 s timeout triggers, in which case the promise rejects. + * + * The API internally listens to both “calculation in progress” events and + * “calculation result applied” events, ensuring it behaves correctly whether + * formulas are recalculated or skipped due to cache/state. + * + * @returns {Promise} A promise that resolves when calculation results are applied + * or when no calculation occurs within the start-detection window. + * + * @example + * ```ts + * const formulaEngine = univerAPI.getFormula(); + * + * // Wait for formula updates to apply before reading values. + * await formulaEngine.onCalculationResultApplied(); + * + * const value = sheet.getRange("C24").getValue(); + * console.log("Updated value:", value); + * ``` + */ + onCalculationResultApplied(): Promise; } export class FFormulaSheetsMixin extends FFormula implements IFFormulaSheetsMixin { @@ -370,6 +425,90 @@ export class FFormulaSheetsMixin extends FFormula implements IFFormulaSheetsMixi this._debouncedFormulaCalculation(); return functionsDisposable; } + + override calculationResultApplied(callback: (result: ISetFormulaCalculationResultMutation) => void): IDisposable { + let setFormulaCalculationResult = false; + let applyFormulaCalculationResult = false; + let result: ISetFormulaCalculationResultMutation | null = null; + + return this._commandService.onCommandExecuted((command: ICommandInfo, options) => { + if (command.id !== SetFormulaCalculationResultMutation.id && command.id !== SetRangeValuesMutation.id) { + return; + } + + if (command.id === SetFormulaCalculationResultMutation.id) { + setFormulaCalculationResult = true; + result = command.params as ISetFormulaCalculationResultMutation; + } + + if (command.id === SetRangeValuesMutation.id && options?.applyFormulaCalculationResult) { + applyFormulaCalculationResult = true; + } + + if (!setFormulaCalculationResult || !applyFormulaCalculationResult) { + return; + } + + requestIdleCallback(() => { + callback(result!); + }); + }); + } + + override onCalculationResultApplied(): Promise { + return new Promise((resolve, reject) => { + let started = false; + let finished = false; + + // Global timeout: reject if the whole calculation hangs + const mainTimer = setTimeout(() => { + cleanup(); + reject(new Error('Calculation end timeout')); + }, 60_000); + + // Watchdog: if no "calculation started" signal is received within 500ms, + // assume there is no real calculation running and resolve immediately. + const startWatchdog = setTimeout(() => { + if (!started) { + cleanup(); + resolve(); + } + }, 500); + + // Listen for "calculation in progress" signal (stageInfo) + const processingDisposable = this.calculationProcessing((stageInfo) => { + // If no formulas to calculate, resolve immediately + const { stage, totalArrayFormulasToCalculate, totalFormulasToCalculate } = stageInfo; + if (stage === FormulaExecuteStageType.START_CALCULATION && totalArrayFormulasToCalculate + totalFormulasToCalculate === 0) { + cleanup(); + resolve(); + return; + } + + if (started) return; + started = true; + + // A start signal is received → no need for the watchdog anymore + clearTimeout(startWatchdog); + }); + + // Listen for the "calculation completed" signal + const endDisposable = this.calculationResultApplied(() => { + if (finished) return; + finished = true; + + cleanup(); + resolve(); + }); + + function cleanup(): void { + clearTimeout(mainTimer); + clearTimeout(startWatchdog); + processingDisposable.dispose(); + endDisposable.dispose(); + } + }); + } } FFormula.extend(FFormulaSheetsMixin); diff --git a/packages/sheets/src/controllers/calculate-result-apply.controller.ts b/packages/sheets/src/controllers/calculate-result-apply.controller.ts index 1cb807685a..094dc3158d 100644 --- a/packages/sheets/src/controllers/calculate-result-apply.controller.ts +++ b/packages/sheets/src/controllers/calculate-result-apply.controller.ts @@ -16,9 +16,8 @@ import type { ICellData, ICommandInfo, IObjectMatrixPrimitiveType, Nullable } from '@univerjs/core'; import type { ISetFormulaCalculationResultMutation } from '@univerjs/engine-formula'; -import { Disposable, ICommandService, Inject, IUniverInstanceService, ObjectMatrix } from '@univerjs/core'; +import { Disposable, ICommandService, Inject, IUniverInstanceService, ObjectMatrix, sequenceExecute } from '@univerjs/core'; import { handleNumfmtInCell, SetFormulaCalculationResultMutation } from '@univerjs/engine-formula'; - import { SetRangeValuesMutation } from '../commands/mutations/set-range-values.mutation'; export class CalculateResultApplyController extends Disposable { @@ -80,11 +79,14 @@ export class CalculateResultApplyController extends Disposable { } } - const result = redoMutationsInfo.every((m) => - this._commandService.executeCommand(m.id, m.params, { + const result = sequenceExecute( + redoMutationsInfo, + this._commandService, + { onlyLocal: true, fromFormula: true, - }) + applyFormulaCalculationResult: true, + } ); return result; }) diff --git a/tests/formula-integration/src/__testing__/test-formula-move.ts b/tests/formula-integration/src/__testing__/test-formula-move.ts index 9386ce16c7..b57ccc9b86 100644 --- a/tests/formula-integration/src/__testing__/test-formula-move.ts +++ b/tests/formula-integration/src/__testing__/test-formula-move.ts @@ -66,12 +66,13 @@ export async function expectMoveFormulaRowsResultMatchesSnapshot() { univerInstanceService.focusUnit(workbook.getId()); const worksheet = workbook.getActiveSheet(); + await testBed.api.getFormula().onCalculationResultApplied(); + // move row 3 to before row 5 const rowSpec = worksheet.getRange('3:3'); worksheet.moveRows(rowSpec, 4); await testBed.api.getFormula().onCalculationResultApplied(); - await new Promise((resolve) => setTimeout(resolve, 500)); const resultSnapshot = workbook.save(); const snapshotFilePath = path.resolve(snapshotRootDir, `${getTestFilePath()}-result.json`); @@ -88,7 +89,6 @@ export async function expectMoveFormulaRowsResultMatchesSnapshot() { // perform undo operation await testBed.api.undo(); await testBed.api.getFormula().onCalculationResultApplied(); - await new Promise((resolve) => setTimeout(resolve, 500)); // compare the result with the snapshot const resultSnapshot_undo = workbook.save(); @@ -121,12 +121,13 @@ export async function expectMoveFormulaSiRowsResultMatchesSnapshot() { univerInstanceService.focusUnit(workbook.getId()); const worksheet = workbook.getActiveSheet(); + await testBed.api.getFormula().onCalculationResultApplied(); + // move row 3 to before row 5 const rowSpec = worksheet.getRange('3:3'); worksheet.moveRows(rowSpec, 4); await testBed.api.getFormula().onCalculationResultApplied(); - await new Promise((resolve) => setTimeout(resolve, 500)); const resultSnapshot = workbook.save(); const snapshotFilePath = path.resolve(snapshotRootDir, `${getTestFilePath()}-result.json`); @@ -143,7 +144,6 @@ export async function expectMoveFormulaSiRowsResultMatchesSnapshot() { // perform undo operation await testBed.api.undo(); await testBed.api.getFormula().onCalculationResultApplied(); - await new Promise((resolve) => setTimeout(resolve, 500)); // compare the result with the snapshot const resultSnapshot_undo = workbook.save(); @@ -176,6 +176,8 @@ export async function expectMoveFormulaCellResultMatchesSnapshot() { univerInstanceService.focusUnit(workbook.getId()); const worksheet = workbook.getActiveSheet(); + await testBed.api.getFormula().onCalculationResultApplied(); + // move D4:D5 to G13:G14 const fromRange = worksheet.getRange('D4:D5').getRange(); const toRange = worksheet.getRange('G13:G14').getRange(); @@ -185,7 +187,6 @@ export async function expectMoveFormulaCellResultMatchesSnapshot() { }); await testBed.api.getFormula().onCalculationResultApplied(); - await new Promise((resolve) => setTimeout(resolve, 500)); const resultSnapshot = workbook.save(); const snapshotFilePath = path.resolve(snapshotRootDir, `${getTestFilePath()}-result.json`); @@ -202,7 +203,6 @@ export async function expectMoveFormulaCellResultMatchesSnapshot() { // perform undo operation await testBed.api.undo(); await testBed.api.getFormula().onCalculationResultApplied(); - await new Promise((resolve) => setTimeout(resolve, 500)); // compare the result with the snapshot const resultSnapshot_undo = workbook.save(); diff --git a/tests/formula-integration/src/__testing__/test-remove-rows-of-filter-rows.ts b/tests/formula-integration/src/__testing__/test-remove-rows-of-filter-rows.ts index 261851afaf..4947eca55a 100644 --- a/tests/formula-integration/src/__testing__/test-remove-rows-of-filter-rows.ts +++ b/tests/formula-integration/src/__testing__/test-remove-rows-of-filter-rows.ts @@ -66,11 +66,12 @@ export async function expectRemoveRowsOfFilterRowsResultMatchesSnapshot() { univerInstanceService.focusUnit(workbook.getId()); const worksheet = workbook.getActiveSheet(); + await testBed.api.getFormula().onCalculationResultApplied(); + // remove rows 2 to 5, where the 3 to 4 rows are filtered rows worksheet.deleteRows(1, 4); await testBed.api.getFormula().onCalculationResultApplied(); - await new Promise((resolve) => setTimeout(resolve, 500)); const resultSnapshot = workbook.save(); const snapshotFilePath = path.resolve(snapshotRootDir, `${getTestFilePath()}-result.json`); @@ -87,7 +88,6 @@ export async function expectRemoveRowsOfFilterRowsResultMatchesSnapshot() { // perform undo operation await testBed.api.undo(); await testBed.api.getFormula().onCalculationResultApplied(); - await new Promise((resolve) => setTimeout(resolve, 500)); // compare the result with the snapshot const resultSnapshot_undo = workbook.save();