From 5eb4360db4dccc1fcd6501d82a67e690ce837d9f Mon Sep 17 00:00:00 2001
From: arnobl <1052622+arnobl@users.noreply.github.com>
Date: Sun, 19 Apr 2026 21:29:13 +0200
Subject: [PATCH 1/4] feat(command): memento decorator to reduce boilerplate
code
---
eslint.config.mjs | 1 +
src/api/command/Memento.ts | 93 ++++++++++++++++
src/api/command/ModifiableCommand.ts | 2 +-
src/api/command/Selective.ts | 2 +-
src/api/history/LinearHistory.ts | 12 ++-
src/api/history/LinearHistoryBase.ts | 6 +-
src/api/history/TreeHistory.ts | 8 +-
src/api/history/Undoable.ts | 4 +-
src/impl/binding/BindingImpl.ts | 3 +-
src/impl/command/CommandBase.ts | 5 +-
src/impl/command/UndoableCommand.ts | 9 +-
src/impl/command/library/Redo.ts | 4 +-
src/impl/command/library/RedoNTimes.ts | 10 +-
src/impl/command/library/SetProperties.ts | 7 +-
src/impl/command/library/SetProperty.ts | 6 +-
src/impl/command/library/TransferArrayItem.ts | 4 +-
src/impl/command/library/Undo.ts | 4 +-
src/impl/command/library/UndoNTimes.ts | 10 +-
src/impl/history/LinearHistoryImpl.ts | 41 ++++---
src/impl/history/TreeHistoryImpl.ts | 39 ++++---
src/interacto.ts | 1 +
test/binding/UndoRedoBindings.test.ts | 2 +-
test/command/CommandAsync.test.ts | 1 -
test/command/StubCmd.ts | 4 -
test/command/library/SetProperty.test.ts | 6 +-
test/command/memento.test.ts | 85 +++++++++++++++
test/command/selective.test.ts | 10 +-
test/history/LinearHistory.test.ts | 93 +++++++++-------
test/history/TreeHistory.test.ts | 101 ++++++++++--------
29 files changed, 411 insertions(+), 162 deletions(-)
create mode 100644 src/api/command/Memento.ts
create mode 100644 test/command/memento.test.ts
diff --git a/eslint.config.mjs b/eslint.config.mjs
index ffeb313b..474847db 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -261,6 +261,7 @@ export default [...[].concat(
},
rules: {
...jest.configs['flat/all'].rules,
+ "no-void": "off",
"no-new": "off",
"no-underscore-dangle": "off",
"no-duplicate-imports": "off",
diff --git a/src/api/command/Memento.ts b/src/api/command/Memento.ts
new file mode 100644
index 00000000..5a601ed7
--- /dev/null
+++ b/src/api/command/Memento.ts
@@ -0,0 +1,93 @@
+/*
+ * This file is part of Interacto.
+ * Interacto is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * Interacto is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with Interacto. If not, see .
+ */
+
+import {CommandBase} from "../../impl/command/CommandBase";
+
+const INTERACTO_MEMENTO: unique symbol = Symbol("interacto-cmd-memento");
+
+interface MementoMetadata {
+ [INTERACTO_MEMENTO]?: Map;
+}
+
+/**
+ * The Interacto decorator to mark one command's property as a property from which a memento must be created.
+ * Cannot check the type of the property here (requires reflect-metadata we do not want to install).
+ * @param target - The targeted command.
+ * @param propertyName - The name of the property the decorator targets.
+ */
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export function Memento(target: unknown, propertyName: string): void {
+ if (!(target instanceof CommandBase)) {
+ // eslint-disable-next-line no-console
+ console.error("The @Memento decorator currently operates on Interacto commands only");
+ return;
+ }
+
+ // Getting the object cache related to the modifiable symbol
+ const symbolValues: unknown = (target.constructor as MementoMetadata)[INTERACTO_MEMENTO];
+ const map = symbolValues instanceof Map ? symbolValues as Map : new Map();
+
+ // Adding the property in this cache
+ const tpropertyName = propertyName as keyof CommandBase;
+ map.set(propertyName, target[tpropertyName]);
+ (target.constructor as MementoMetadata)[INTERACTO_MEMENTO] = map;
+}
+
+/**
+ * Retrieves the subset of attributes from an {@link Undoable} instance that are
+ * considered being the memento of the object (i.e., annotated with `@Memento`)
+ * @param obj - The object from which to extract memento properties.
+ * @returns Partial - An object (extracted from `obj`) containing only the properties that are
+ * declared as being the memento.
+ */
+function getMementoProperties(obj: T): Partial {
+ const modifiableAttributes: Partial = {};
+ const mementoProps: unknown = (obj.constructor as MementoMetadata)[INTERACTO_MEMENTO];
+
+ if (mementoProps instanceof Map) {
+ for (const [key, value] of mementoProps.entries()) {
+ const tkey = key as keyof T;
+ modifiableAttributes[tkey] = value as T[keyof T];
+ }
+ }
+ return modifiableAttributes;
+}
+
+/**
+ * Retrieves the subset of attributes from an {@link Undoable} instance that are
+ * considered being the memento of the object (i.e., annotated with `@Memento`)
+ * @param obj - The object from which to restore the values using the stored memento properties.
+ */
+export function restoreMementoProperties(obj: T): void {
+ for (const [propName, propMementoValue] of Object.entries(getMementoProperties(obj))) {
+ const typedPropName = propName as keyof T;
+ obj[typedPropName] = propMementoValue as T[keyof T];
+ }
+}
+
+/**
+ * Retrieves the subset of attributes from an {@link Undoable} instance that are
+ * considered being the memento of the object (i.e., annotated with `@Memento`)
+ * @param obj - The object from which to restore the values using the stored memento properties.
+ */
+export function createMementoProperties(obj: T): void {
+ const mementoProps: unknown = (obj.constructor as MementoMetadata)[INTERACTO_MEMENTO];
+
+ if (mementoProps instanceof Map) {
+ for (const propName of mementoProps.keys()) {
+ const typedPropName = propName as keyof T;
+ mementoProps.set(propName, obj[typedPropName]);
+ }
+ }
+}
diff --git a/src/api/command/ModifiableCommand.ts b/src/api/command/ModifiableCommand.ts
index 45850385..c9001c67 100644
--- a/src/api/command/ModifiableCommand.ts
+++ b/src/api/command/ModifiableCommand.ts
@@ -108,7 +108,7 @@ export function getModifiableCmdAttributes(obj: T): Partial<
const tkey = key as keyof T;
const type = typeof obj[tkey];
- if (type === "string" || type === "number" || type === "boolean") {
+ if (type === "string" || type === "number" || type === "boolean" || type === "bigint") {
modifiableAttributes[tkey] = obj[tkey];
} else {
// eslint-disable-next-line no-console
diff --git a/src/api/command/Selective.ts b/src/api/command/Selective.ts
index 744316dc..a2afed93 100644
--- a/src/api/command/Selective.ts
+++ b/src/api/command/Selective.ts
@@ -91,7 +91,7 @@ export function isCmdSelective(obj: T): boolean {
* done using the === operator.
* @returns True if the given object is selective and its selective property has as value the given `value`
*/
-export function hasSelectiveValue(
+export function hasSelectiveValue(
obj: object, value: V, eqFn: ((v1: V, v2: V) => boolean) = (v1, v2) => v1 === v2): boolean {
const res = getSelectiveValue(obj);
diff --git a/src/api/history/LinearHistory.ts b/src/api/history/LinearHistory.ts
index bcc56d8b..513a4bf2 100644
--- a/src/api/history/LinearHistory.ts
+++ b/src/api/history/LinearHistory.ts
@@ -27,9 +27,9 @@ export abstract class LinearHistory implements LinearHistoryBase {
public abstract clear(): void;
- public abstract undo(): void;
+ public abstract undo(): Promise | void;
- public abstract redo(): void;
+ public abstract redo(): Promise | void;
/**
* @returns The stack of saved undoable objects.
@@ -95,7 +95,7 @@ export abstract class LinearHistory implements LinearHistoryBase {
* The second one contains the matching commands in the redo stack.
* Each undoable is associated with its index in its undo/redo stack.
*/
- public abstract getSelectiveOf (key: T, eqFn?: (v1: T, v2: T) => boolean):
+ public abstract getSelectiveOf (key: T, eqFn?: (v1: T, v2: T) => boolean):
[undos: Array<[undoable: Undoable, index: number]>, redos: Array<[undoable: Undoable, index: number]>];
/**
@@ -106,8 +106,9 @@ export abstract class LinearHistory implements LinearHistoryBase {
* Index size()[0] is the most recent undoable elements (corresponds to getLastUndo())
* @param index - The index in the undo stack to undo up to it.
* If not valid, the method does nothing.
+ * @returns nothing or a promise (a chain of promises in fact) if the undo process is async.
*/
- public abstract undoUpTo(index: number): void;
+ public abstract undoUpTo(index: number): Promise | void;
/**
* Redoes the last undoable objects up to go to the provided index.
@@ -117,8 +118,9 @@ export abstract class LinearHistory implements LinearHistoryBase {
* Index size()[0] is the most recent undoable elements (corresponds to getLastRedo())
* @param index - The index in the redo stack to redo up to it.
* If not valid, the method does nothing.
+ * @returns nothing or a promise (a chain of promises in fact) if the undo process is async.
*/
- public abstract redoUpTo(index: number): void;
+ public abstract redoUpTo(index: number): Promise | void;
/**
* Gets all the unique selective objects of this history.
diff --git a/src/api/history/LinearHistoryBase.ts b/src/api/history/LinearHistoryBase.ts
index 27f179e9..2bd47cc6 100644
--- a/src/api/history/LinearHistoryBase.ts
+++ b/src/api/history/LinearHistoryBase.ts
@@ -22,13 +22,15 @@ import type {Observable} from "rxjs";
export interface LinearHistoryBase {
/**
* Undoes the last undoable object.
+ * @returns nothing or a promise if the undo process is async.
*/
- undo(): void;
+ undo(): Promise | void;
/**
* Redoes the last undoable object.
+ * @returns nothing or a promise if the undo process is async.
*/
- redo(): void;
+ redo(): Promise | void;
/**
* Removes all the undoable objects of the collector.
diff --git a/src/api/history/TreeHistory.ts b/src/api/history/TreeHistory.ts
index 3d27fbee..70ef4508 100644
--- a/src/api/history/TreeHistory.ts
+++ b/src/api/history/TreeHistory.ts
@@ -54,12 +54,12 @@ export interface TreeHistoryNode {
/**
* Undoes the undoable object of this node.
*/
- undo(): void;
+ undo(): Promise | void;
/**
* Redoes the undoable object of this node.
*/
- redo(): void;
+ redo(): Promise | void;
}
/**
@@ -215,11 +215,11 @@ export abstract class TreeHistory implements LinearHistoryBase {
public abstract getLastUndoMessage(): string | undefined;
- public abstract redo(): void;
+ public abstract redo(): Promise | void;
public abstract redosObservable(): Observable;
- public abstract undo(): void;
+ public abstract undo(): Promise | void;
public abstract undosObservable(): Observable;
diff --git a/src/api/history/Undoable.ts b/src/api/history/Undoable.ts
index eed88de3..4078836b 100644
--- a/src/api/history/Undoable.ts
+++ b/src/api/history/Undoable.ts
@@ -32,12 +32,12 @@ export interface Undoable {
/**
* Cancels the command.
*/
- undo(): void;
+ undo(): Promise | void;
/**
* Redoes the canceled command.
*/
- redo(): void;
+ redo(): Promise | void;
/**
* @returns The name of the history command.
diff --git a/src/impl/binding/BindingImpl.ts b/src/impl/binding/BindingImpl.ts
index 82a4e4e9..e698b30e 100644
--- a/src/impl/binding/BindingImpl.ts
+++ b/src/impl/binding/BindingImpl.ts
@@ -317,7 +317,8 @@ implements Binding {
private cancelContinuousWithEffectsCmd(cmd: C): void {
if (isUndoableType(cmd)) {
- cmd.undo();
+ // eslint-disable-next-line no-void
+ void cmd.undo();
if (this.logCmd) {
this.logger.logCmdMsg("Command undone", cmd.constructor.name);
}
diff --git a/src/impl/command/CommandBase.ts b/src/impl/command/CommandBase.ts
index 4dc18dcb..8e1c2c1d 100644
--- a/src/impl/command/CommandBase.ts
+++ b/src/impl/command/CommandBase.ts
@@ -13,6 +13,7 @@
*/
import type {Command, CmdStatus} from "../../api/command/Command";
+import {createMementoProperties} from "../../api/command/Memento";
/**
* The base implementation class for coding UI commands.
@@ -44,7 +45,9 @@ export abstract class CommandBase implements Command {
* This is the goal of the operation that should be overridden.
* This operator is called a single time before the first execution of the command.
*/
- protected createMemento(): void {}
+ protected createMemento(): void {
+ createMementoProperties(this);
+ }
public execute(): Promise | boolean {
let ok: boolean;
diff --git a/src/impl/command/UndoableCommand.ts b/src/impl/command/UndoableCommand.ts
index 990fa917..76892a7b 100644
--- a/src/impl/command/UndoableCommand.ts
+++ b/src/impl/command/UndoableCommand.ts
@@ -14,6 +14,7 @@
import {CommandBase} from "./CommandBase";
import type {Undoable, UndoableSnapshot} from "../../api/history/Undoable";
+import {restoreMementoProperties} from "../../api/command/Memento";
/**
* The base class for undoable UI commands.
@@ -68,7 +69,11 @@ export abstract class UndoableCommand ex
return this === undoable;
}
- public abstract redo(): void;
+ public redo(): Promise | void {
+ return this.execution();
+ }
- public abstract undo(): void;
+ public undo(): Promise | void {
+ restoreMementoProperties(this);
+ }
}
diff --git a/src/impl/command/library/Redo.ts b/src/impl/command/library/Redo.ts
index f9700a68..3b063620 100644
--- a/src/impl/command/library/Redo.ts
+++ b/src/impl/command/library/Redo.ts
@@ -31,7 +31,7 @@ export class Redo extends CommandBase {
return this.history.getLastRedo() !== undefined;
}
- protected execution(): void {
- this.history.redo();
+ protected execution(): Promise | void {
+ return this.history.redo();
}
}
diff --git a/src/impl/command/library/RedoNTimes.ts b/src/impl/command/library/RedoNTimes.ts
index 542dbc70..c34c85f7 100644
--- a/src/impl/command/library/RedoNTimes.ts
+++ b/src/impl/command/library/RedoNTimes.ts
@@ -34,9 +34,15 @@ export class RedoNTimes extends CommandBase {
return this.history.getRedo().length >= this.numberOfRedos;
}
- protected execution(): void {
+ protected execution(): Promise | void {
+ let chain = Promise.resolve();
for (let i = 0; i < this.numberOfRedos; i++) {
- this.history.redo();
+ const res = this.history.redo();
+ if (res instanceof Promise) {
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
+ chain = chain.then(() => res);
+ }
}
+ return chain;
}
}
diff --git a/src/impl/command/library/SetProperties.ts b/src/impl/command/library/SetProperties.ts
index c7016398..762b0710 100644
--- a/src/impl/command/library/SetProperties.ts
+++ b/src/impl/command/library/SetProperties.ts
@@ -59,13 +59,14 @@ export class SetProperties extends UndoableCommand {
protected execution(): void {}
- public redo(): void {
+ public override redo(): void {
for (const cmd of this.compositeCmds) {
- cmd.redo();
+ // eslint-disable-next-line no-void
+ void cmd.redo();
}
}
- public undo(): void {
+ public override undo(): void {
for (const cmd of this.compositeCmds) {
cmd.undo();
}
diff --git a/src/impl/command/library/SetProperty.ts b/src/impl/command/library/SetProperty.ts
index d907a279..0f4ff588 100644
--- a/src/impl/command/library/SetProperty.ts
+++ b/src/impl/command/library/SetProperty.ts
@@ -45,11 +45,7 @@ export class SetProperty extends UndoableCommand {
this.obj[this.prop] = this.newvalue;
}
- public redo(): void {
- this.execution();
- }
-
- public undo(): void {
+ public override undo(): void {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this.obj[this.prop] = this.mementoValue!;
}
diff --git a/src/impl/command/library/TransferArrayItem.ts b/src/impl/command/library/TransferArrayItem.ts
index 44ef541f..cb74f726 100644
--- a/src/impl/command/library/TransferArrayItem.ts
+++ b/src/impl/command/library/TransferArrayItem.ts
@@ -78,7 +78,7 @@ export class TransferArrayItem extends UndoableCommand {
return this.cmdName;
}
- public redo(): void {
+ public override redo(): void {
const elt = this._srcArray[this._srcIndex];
if (elt !== undefined) {
this._srcArray.splice(this._srcIndex, 1);
@@ -86,7 +86,7 @@ export class TransferArrayItem extends UndoableCommand {
}
}
- public undo(): void {
+ public override undo(): void {
const elt = this._tgtArray[this._tgtIndex];
if (elt !== undefined) {
this._tgtArray.splice(this._tgtIndex, 1);
diff --git a/src/impl/command/library/Undo.ts b/src/impl/command/library/Undo.ts
index d8cdf0e8..ad8e7f1d 100644
--- a/src/impl/command/library/Undo.ts
+++ b/src/impl/command/library/Undo.ts
@@ -31,7 +31,7 @@ export class Undo extends CommandBase {
return this.history.getLastUndo() !== undefined;
}
- protected execution(): void {
- this.history.undo();
+ protected execution(): Promise | void {
+ return this.history.undo();
}
}
diff --git a/src/impl/command/library/UndoNTimes.ts b/src/impl/command/library/UndoNTimes.ts
index 73eca7a8..dfff1aa2 100644
--- a/src/impl/command/library/UndoNTimes.ts
+++ b/src/impl/command/library/UndoNTimes.ts
@@ -34,9 +34,15 @@ export class UndoNTimes extends CommandBase {
return this.history.getUndo().length >= this.numberOfUndos;
}
- protected execution(): void {
+ protected execution(): Promise | void {
+ let chain = Promise.resolve();
for (let i = 0; i < this.numberOfUndos; i++) {
- this.history.undo();
+ const res = this.history.undo();
+ if (res instanceof Promise) {
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
+ chain = chain.then(() => res);
+ }
}
+ return chain;
}
}
diff --git a/src/impl/history/LinearHistoryImpl.ts b/src/impl/history/LinearHistoryImpl.ts
index 2d305edf..6263f70b 100644
--- a/src/impl/history/LinearHistoryImpl.ts
+++ b/src/impl/history/LinearHistoryImpl.ts
@@ -98,7 +98,8 @@ export class LinearHistoryImpl extends LinearHistory {
const lastRedo = this.redos.at(-1);
if (this.considersEqualCmds && lastRedo !== undefined && lastRedo.equals(undoable)) {
- this.redo();
+ // eslint-disable-next-line no-void
+ void this.redo();
} else {
this.undos.push(undoable);
this.undoPublisher.next(undoable);
@@ -108,12 +109,12 @@ export class LinearHistoryImpl extends LinearHistory {
}
}
- public undo(): void {
+ public undo(): Promise | void {
const undoable = this.undos.pop();
if (undoable !== undefined) {
try {
- undoable.undo();
+ return undoable.undo();
} finally {
this.redos.push(undoable);
this.undoPublisher.next(this.getLastUndo());
@@ -121,14 +122,15 @@ export class LinearHistoryImpl extends LinearHistory {
this.publishSize();
}
}
+ return undefined;
}
- public redo(): void {
+ public redo(): Promise | void {
const undoable = this.redos.pop();
if (undoable !== undefined) {
try {
- undoable.redo();
+ return undoable.redo();
} finally {
this.undos.push(undoable);
this.undoPublisher.next(undoable);
@@ -136,6 +138,7 @@ export class LinearHistoryImpl extends LinearHistory {
this.publishSize();
}
}
+ return undefined;
}
public getLastUndoMessage(): string | undefined {
@@ -201,7 +204,7 @@ export class LinearHistoryImpl extends LinearHistory {
this.sizePublisher.next(this.size());
}
- public getSelectiveOf (obj: T, eqFn?: (v1: T, v2: T) => boolean):
+ public getSelectiveOf (obj: T, eqFn?: (v1: T, v2: T) => boolean):
[undos: Array<[undoable: Undoable, index: number]>, redos: Array<[undoable: Undoable, index: number]>] {
const fnFilter = (undoable: Undoable): boolean => hasSelectiveValue(undoable, obj, eqFn);
const fnMap = (undoable: Undoable, index: number): [undoable: Undoable, index: number] => [undoable, index];
@@ -215,28 +218,42 @@ export class LinearHistoryImpl extends LinearHistory {
];
}
- public undoUpTo(index: number): void {
+ public undoUpTo(index: number): Promise | void {
if (index < 0 || index >= this.undos.length) {
- return;
+ return undefined;
}
const count = this.undos.length - index;
+ let chain: Promise = Promise.resolve();
for (let i = 0; i < count; i++) {
- this.undo();
+ const res = this.undo();
+ if (res instanceof Promise) {
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
+ chain = chain.then(() => res);
+ }
}
+
+ return chain;
}
- public redoUpTo(index: number): void {
+ public redoUpTo(index: number): Promise | void {
if (index < 0 || index >= this.redos.length) {
- return;
+ return undefined;
}
const count = this.redos.length - index;
+ let chain: Promise = Promise.resolve();
for (let i = 0; i < count; i++) {
- this.redo();
+ const res = this.redo();
+ if (res instanceof Promise) {
+ // eslint-disable-next-line @typescript-eslint/promise-function-async
+ chain = chain.then(() => res);
+ }
}
+
+ return chain;
}
public getAllSelectiveObjects(): ReadonlySet {
diff --git a/src/impl/history/TreeHistoryImpl.ts b/src/impl/history/TreeHistoryImpl.ts
index 73eb3112..41ab0dee 100644
--- a/src/impl/history/TreeHistoryImpl.ts
+++ b/src/impl/history/TreeHistoryImpl.ts
@@ -51,15 +51,15 @@ class TreeHistoryNodeImpl implements TreeHistoryNode {
this.cacheVisualSnap = undoable.getVisualSnapshot();
}
- public undo(): void {
+ public undo(): Promise | void {
if (this.parent !== undefined) {
this.parent.lastChildUndone = this;
}
- this.undoable.undo();
+ return this.undoable.undo();
}
- public redo(): void {
- this.undoable.redo();
+ public redo(): Promise | void {
+ return this.undoable.redo();
}
public get visualSnapshot(): UndoableSnapshot {
@@ -333,7 +333,8 @@ export class TreeHistoryImpl extends TreeHistory {
*/
private proceedModifiedNode(node: TreeHistoryNode): void {
// Executing the cloned undoable object
- node.undoable.redo();
+ // eslint-disable-next-line no-void
+ void node.undoable.redo();
// Must refresh the visual snapshot cache
if (node.undoable instanceof UndoableCommand) {
node.undoable.refreshCache();
@@ -350,7 +351,8 @@ export class TreeHistoryImpl extends TreeHistory {
private addClonedSubtree(node: TreeHistoryNode): void {
for (const child of node.children) {
this.proceedModifiedNode(child);
- this.undo();
+ // eslint-disable-next-line no-void
+ void this.undo();
this.addClonedSubtree(child);
}
}
@@ -390,7 +392,8 @@ export class TreeHistoryImpl extends TreeHistory {
private goToFromRoot(id: number): void {
const undoables = this.gatherToRoot(this.undoableNodes[id]);
for (const undoable of undoables) {
- undoable.redo();
+ // eslint-disable-next-line no-void
+ void undoable.redo();
}
}
@@ -415,39 +418,47 @@ export class TreeHistoryImpl extends TreeHistory {
i++;
}
+ // Ignoring async for the moment
for (let j = pathSrc.length - 1; j > i; j--) {
- pathSrc[j]?.undo();
+ // eslint-disable-next-line no-void
+ void pathSrc[j]?.undo();
}
if (i < pathSrc.length) {
- pathSrc[i]?.undo();
+ // eslint-disable-next-line no-void
+ void pathSrc[i]?.undo();
}
// to then redo the target path to the targeted node
for (let j = i; j < pathTo.length; j++) {
- pathTo[j]?.redo();
+ // eslint-disable-next-line no-void
+ void pathTo[j]?.redo();
}
}
- public redo(): void {
+ public redo(): Promise | void {
const node = this.currentNode.lastChildUndone;
if (node !== undefined) {
- node.undoable.redo();
+ const res = node.undoable.redo();
this._currentNode = node;
this.addToPath();
this.undoPublisher.next(node.undoable);
this.redoPublisher.next(this.getLastRedo());
+ return res;
}
+ return undefined;
}
- public undo(): void {
+ public undo(): Promise | void {
if (this.currentNode !== this.root) {
const currentUndoable = this.currentNode.undoable;
- this.currentNode.undo();
+ const res = this.currentNode.undo();
this._currentNode = this.currentNode.parent ?? this.root;
this.addToPath();
this.undoPublisher.next(this.getLastUndo());
this.redoPublisher.next(currentUndoable);
+ return res;
}
+ return undefined;
}
public getPositions(): Map {
diff --git a/src/interacto.ts b/src/interacto.ts
index b1f368b9..fdad306a 100644
--- a/src/interacto.ts
+++ b/src/interacto.ts
@@ -29,6 +29,7 @@ export * from "./api/binding/BindingsObserver";
export * from "./api/binding/VisitorBinding";
export * from "./api/checker/Checker";
export * from "./api/command/Command";
+export * from "./api/command/Memento";
export * from "./api/command/ModifiableCommand";
export * from "./api/command/Selective";
export * from "./api/fsm/ConcurrentFSM";
diff --git a/test/binding/UndoRedoBindings.test.ts b/test/binding/UndoRedoBindings.test.ts
index 126a95d2..492f0723 100644
--- a/test/binding/UndoRedoBindings.test.ts
+++ b/test/binding/UndoRedoBindings.test.ts
@@ -93,7 +93,7 @@ describe("test history redo bindings", () => {
throw new Error("err");
});
bindings.cmdhistory.add(undoable);
- bindings.cmdhistory.undo();
+ void bindings.cmdhistory.undo();
});
test("undo/redo: redo crash caught in binding", () => {
diff --git a/test/command/CommandAsync.test.ts b/test/command/CommandAsync.test.ts
index 3f9befbb..81f62cf4 100644
--- a/test/command/CommandAsync.test.ts
+++ b/test/command/CommandAsync.test.ts
@@ -91,7 +91,6 @@ describe("testing async commands and bindings", () => {
describe("async command alone", () => {
test("when promise not ended command not ended", () => {
jest.useFakeTimers();
- // eslint-disable-next-line no-void
void cmd.execute();
expect(cmd.getStatus()).toBe("created");
diff --git a/test/command/StubCmd.ts b/test/command/StubCmd.ts
index 5b52855f..6c968f0b 100644
--- a/test/command/StubCmd.ts
+++ b/test/command/StubCmd.ts
@@ -18,10 +18,6 @@ export class ExampleUndoableCmd extends UndoableCommand {
protected execution(): Promise | void {
return undefined;
}
-
- public redo(): void {}
-
- public undo(): void {}
}
export class StubCmd extends CommandBase {
diff --git a/test/command/library/SetProperty.test.ts b/test/command/library/SetProperty.test.ts
index 3327bb31..866aafdc 100644
--- a/test/command/library/SetProperty.test.ts
+++ b/test/command/library/SetProperty.test.ts
@@ -70,7 +70,7 @@ describe("using a set property command", () => {
obj.foo = 2;
await cmd.execute();
cmd.undo();
- cmd.redo();
+ void cmd.redo();
expect(obj.foo).toBe(3);
});
@@ -116,7 +116,7 @@ describe("using a set property command", () => {
obj.foo2 = "fooo22";
await cmd.execute();
cmd.undo();
- cmd.redo();
+ void cmd.redo();
expect(obj.foo2).toBe("yolo");
});
@@ -159,7 +159,7 @@ describe("using a set property command", () => {
obj.bar = obj3;
await cmd.execute();
cmd.undo();
- cmd.redo();
+ void cmd.redo();
expect(obj.bar).toBe(obj2);
});
diff --git a/test/command/memento.test.ts b/test/command/memento.test.ts
new file mode 100644
index 00000000..c236bc8c
--- /dev/null
+++ b/test/command/memento.test.ts
@@ -0,0 +1,85 @@
+/*
+ * This file is part of Interacto.
+ * Interacto is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ * Interacto is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ * You should have received a copy of the GNU General Public License
+ * along with Interacto. If not, see .
+ */
+
+import {ExampleUndoableCmd} from "./StubCmd";
+import {Memento} from "../../src/interacto";
+import {afterEach, beforeEach, describe, expect, jest, test} from "@jest/globals";
+
+class MementoCmd1 extends ExampleUndoableCmd {
+ @Memento
+ public x: number;
+
+ @Memento
+ public y: number;
+
+ public newX: number;
+
+ public constructor(x: number, y: number) {
+ super();
+ this.x = x;
+ this.y = y;
+ this.newX = 0;
+ }
+
+ public override execution(): void {
+ this.x = 10;
+ this.y = 20;
+ }
+}
+
+describe("using a command have memento decorators", () => {
+ let cmd1: MementoCmd1;
+
+ beforeEach(() => {
+ cmd1 = new MementoCmd1(1, 2);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ test("command execution works as expected", () => {
+ void cmd1.execute();
+ expect(cmd1.x).toBe(10);
+ expect(cmd1.y).toBe(20);
+ expect(cmd1.newX).toBe(0);
+ });
+
+ test("command undo uses automated memento to restore values", () => {
+ void cmd1.execute();
+ void cmd1.undo();
+ expect(cmd1.x).toBe(1);
+ expect(cmd1.y).toBe(2);
+ expect(cmd1.newX).toBe(0);
+ });
+
+ test("command undo/redo works as expected", () => {
+ void cmd1.execute();
+ void cmd1.undo();
+ void cmd1.redo();
+ expect(cmd1.x).toBe(10);
+ expect(cmd1.y).toBe(20);
+ expect(cmd1.newX).toBe(0);
+ });
+
+ test("command undo/redo/undo uses automated memento to restore values", () => {
+ void cmd1.execute();
+ void cmd1.undo();
+ void cmd1.redo();
+ void cmd1.undo();
+ expect(cmd1.x).toBe(1);
+ expect(cmd1.y).toBe(2);
+ expect(cmd1.newX).toBe(0);
+ });
+});
diff --git a/test/command/selective.test.ts b/test/command/selective.test.ts
index c8cf5162..381370b1 100644
--- a/test/command/selective.test.ts
+++ b/test/command/selective.test.ts
@@ -178,8 +178,8 @@ describe("using a selective command", () => {
history.add(cmd1);
history.add(new ExampleUndoableCmd());
history.add(cmd3);
- history.undo();
- history.undo();
+ void history.undo();
+ void history.undo();
const res = history.getSelectiveOf(key);
expect(res).toHaveLength(2);
expect(res[0]).toHaveLength(2);
@@ -191,8 +191,8 @@ describe("using a selective command", () => {
test("works when full undo", () => {
history.add(cmd2);
history.add(cmd1);
- history.undo();
- history.undo();
+ void history.undo();
+ void history.undo();
const res = history.getSelectiveOf(key);
expect(res).toHaveLength(2);
expect(res[0]).toHaveLength(0);
@@ -242,7 +242,7 @@ describe("using a selective command", () => {
cmd2 = new CmdSelective2("foo");
history.add(cmd2);
history.add(cmd1);
- history.undo();
+ void history.undo();
const res = [...history.getAllSelectiveObjects()];
expect(res).toHaveLength(2);
expect(res).toContain(cmd1.key);
diff --git a/test/history/LinearHistory.test.ts b/test/history/LinearHistory.test.ts
index 946ecc40..9c9b4b85 100644
--- a/test/history/LinearHistory.test.ts
+++ b/test/history/LinearHistory.test.ts
@@ -46,36 +46,36 @@ describe("using an linear history", () => {
test("history called", () => {
history.add(undoable);
- history.undo();
+ void history.undo();
expect(undoable.undo).toHaveBeenCalledTimes(1);
});
test("undoUpTo does nothing if negative index", () => {
history.add(undoable);
- history.undoUpTo(-1);
+ void history.undoUpTo(-1);
expect(history.getLastUndo()).toBe(undoable);
expect(undoable.undo).not.toHaveBeenCalled();
});
test("redoUpTo does nothing if negative index", () => {
history.add(undoable);
- history.undo();
- history.redoUpTo(-1);
+ void history.undo();
+ void history.redoUpTo(-1);
expect(history.getLastRedo()).toBe(undoable);
expect(undoable.redo).not.toHaveBeenCalled();
});
test("undoUpTo does nothing if too high index", () => {
history.add(undoable);
- history.undoUpTo(1);
+ void history.undoUpTo(1);
expect(history.getLastUndo()).toBe(undoable);
expect(undoable.undo).not.toHaveBeenCalled();
});
test("redoUpTo does nothing if too high index", () => {
history.add(undoable);
- history.undo();
- history.redoUpTo(1);
+ void history.undo();
+ void history.redoUpTo(1);
expect(history.getLastRedo()).toBe(undoable);
expect(undoable.redo).not.toHaveBeenCalled();
});
@@ -83,7 +83,7 @@ describe("using an linear history", () => {
test("undoUpTo undoes correctly with index 0", () => {
history.add(undoable);
history.add(undoable2);
- history.undoUpTo(0);
+ void history.undoUpTo(0);
expect(history.getLastUndo()).toBeUndefined();
expect(undoable.undo).toHaveBeenCalledTimes(1);
expect(undoable2.undo).toHaveBeenCalledTimes(1);
@@ -92,9 +92,9 @@ describe("using an linear history", () => {
test("redoUpTo undoes correctly with index 0", () => {
history.add(undoable);
history.add(undoable2);
- history.undo();
- history.undo();
- history.redoUpTo(0);
+ void history.undo();
+ void history.undo();
+ void history.redoUpTo(0);
expect(history.getLastRedo()).toBeUndefined();
expect(undoable.redo).toHaveBeenCalledTimes(1);
expect(undoable2.redo).toHaveBeenCalledTimes(1);
@@ -103,7 +103,7 @@ describe("using an linear history", () => {
test("undoUpTo undoes correctly", () => {
history.add(undoable);
history.add(undoable2);
- history.undoUpTo(1);
+ void history.undoUpTo(1);
expect(history.getLastUndo()).toBe(undoable);
expect(undoable.undo).not.toHaveBeenCalled();
expect(undoable2.undo).toHaveBeenCalledTimes(1);
@@ -112,9 +112,9 @@ describe("using an linear history", () => {
test("redoUpTo undoes correctly", () => {
history.add(undoable);
history.add(undoable2);
- history.undo();
- history.undo();
- history.redoUpTo(1);
+ void history.undo();
+ void history.undo();
+ void history.redoUpTo(1);
expect(history.getLastRedo()).toBe(undoable2);
expect(undoable2.redo).not.toHaveBeenCalled();
expect(undoable.redo).toHaveBeenCalledTimes(1);
@@ -122,14 +122,14 @@ describe("using an linear history", () => {
test("size history", () => {
history.add(undoable);
- history.undo();
+ void history.undo();
expect(history.size()).toStrictEqual([0, 1]);
});
test("history and redo called", () => {
history.add(undoable);
- history.undo();
- history.redo();
+ void history.undo();
+ void history.redo();
expect(undoable.undo).toHaveBeenCalledTimes(1);
expect(undoable.redo).toHaveBeenCalledTimes(1);
expect(history.size()).toStrictEqual([1, 0]);
@@ -137,20 +137,20 @@ describe("using an linear history", () => {
test("redo not called", () => {
history.add(undoable);
- history.redo();
+ void history.redo();
expect(undoable.redo).not.toHaveBeenCalled();
expect(history.size()).toStrictEqual([1, 0]);
});
test("history ok when empty", () => {
- history.undo();
+ void history.undo();
expect(history.getLastUndo()).toBeUndefined();
});
test("get last history ok on add", () => {
history.add(undoable);
- history.undo();
- history.redo();
+ void history.undo();
+ void history.redo();
expect(undoable.redo).toHaveBeenCalledTimes(1);
expect(history.getLastUndo()).toBe(undoable);
});
@@ -270,7 +270,7 @@ describe("using an linear history", () => {
test("getLastRedoOKOnRedo", () => {
history.add(undoable);
- history.undo();
+ void history.undo();
expect(history.getLastRedo()).toStrictEqual(undoable);
});
@@ -311,20 +311,20 @@ describe("using an linear history", () => {
test("getLastRedoMessageOK", () => {
history.add(undoable);
- history.undo();
+ void history.undo();
expect(history.getLastRedoMessage()).toBe("undoredomsg");
});
test("lastOrEmptyRedoMessage OK", () => {
history.add(undoable);
- history.undo();
+ void history.undo();
expect(history.getLastOrEmptyRedoMessage()).toBe("undoredomsg");
});
test("clear", () => {
history.add(undoable);
history.add(undoable2);
- history.undo();
+ void history.undo();
history.clear();
expect(history.getLastRedo()).toBeUndefined();
expect(history.getLastUndo()).toBeUndefined();
@@ -357,7 +357,12 @@ describe("using an linear history", () => {
test("one add one history", () => {
testScheduler.run(helpers => {
const {cold, expectObservable} = helpers;
- cold("-a-b", {"a": () => history.add(undoable), "b": () => history.undo()}).subscribe(v => {
+ cold("-a-b", {
+ "a": () => history.add(undoable),
+ "b": () => {
+ void history.undo();
+ }
+ }).subscribe(v => {
v();
});
@@ -375,8 +380,12 @@ describe("using an linear history", () => {
cold("-a-b-c-d-e", {
"a": () => history.add(undoable),
"b": () => history.add(undoable2),
- "c": () => history.undo(),
- "d": () => history.redo(),
+ "c": () => {
+ void history.undo();
+ },
+ "d": () => {
+ void history.redo();
+ },
"e": () => history.clear()
}).subscribe(v => v());
@@ -413,7 +422,7 @@ describe("using an linear history", () => {
history.setSizeMax(10);
history.add(undoable);
history.add(undoable2);
- history.undo();
+ void history.undo();
testScheduler.run(helpers => {
const {cold, expectObservable} = helpers;
@@ -433,7 +442,7 @@ describe("using an linear history", () => {
history.setSizeMax(10);
history.add(undoable);
history.add(undoable2);
- history.undo();
+ void history.undo();
testScheduler.run(helpers => {
const {cold, expectObservable} = helpers;
@@ -455,8 +464,8 @@ describe("using an linear history", () => {
history.add(undoable2);
history.add(mock());
history.add(mock());
- history.undo();
- history.undo();
+ void history.undo();
+ void history.undo();
history.setSizeMax(1);
expect(history.size()).toStrictEqual([1, 0]);
@@ -469,7 +478,9 @@ describe("using an linear history", () => {
history.add(undoable);
history.add(undoable2);
- expect(() => history.undo()).toThrow(new Error("err"));
+ expect(() => {
+ void history.undo();
+ }).toThrow(new Error("err"));
expect(history.getUndo()).toHaveLength(1);
expect(history.getRedo()).toHaveLength(1);
});
@@ -481,8 +492,10 @@ describe("using an linear history", () => {
history.add(undoable);
history.add(undoable2);
- history.undo();
- expect(() => history.redo()).toThrow(new Error("err2"));
+ void history.undo();
+ expect(() => {
+ void history.redo();
+ }).toThrow(new Error("err2"));
expect(history.getUndo()).toHaveLength(2);
expect(history.getRedo()).toHaveLength(0);
});
@@ -514,8 +527,8 @@ describe("using an linear history", () => {
test("does a redo if equal command", () => {
// A *B C D
- history.undo();
- history.undo();
+ void history.undo();
+ void history.undo();
undoableC.equals = jest.fn(() => true);
history.add(undoableE);
@@ -525,8 +538,8 @@ describe("using an linear history", () => {
test("clears redos if not equal command", () => {
// A *B C D
- history.undo();
- history.undo();
+ void history.undo();
+ void history.undo();
undoableC.equals = jest.fn(() => false);
history.add(undoableE);
diff --git a/test/history/TreeHistory.test.ts b/test/history/TreeHistory.test.ts
index 4442598f..85b06235 100644
--- a/test/history/TreeHistory.test.ts
+++ b/test/history/TreeHistory.test.ts
@@ -64,7 +64,7 @@ describe("using a tree-based history", () => {
});
test("history does nothing", () => {
- history.undo();
+ void history.undo();
expect(history.size()).toBe(0);
expect(history.currentNode).toBe(history.root);
});
@@ -175,7 +175,12 @@ describe("using a tree-based history", () => {
test("one add one history", () => {
testScheduler.run(helpers => {
const {cold, expectObservable} = helpers;
- cold("-a-b", {"a": () => history.add(undoable0), "b": () => history.undo()}).subscribe(v => {
+ cold("-a-b", {
+ "a": () => history.add(undoable0),
+ "b": () => {
+ void history.undo();
+ }
+ }).subscribe(v => {
v();
});
@@ -192,8 +197,12 @@ describe("using a tree-based history", () => {
const {cold, expectObservable} = helpers;
cold("-a-b-c", {
"a": () => history.add(undoable0),
- "b": () => history.undo(),
- "c": () => history.redo()
+ "b": () => {
+ void history.undo();
+ },
+ "c": () => {
+ void history.redo();
+ }
}).subscribe(v => {
v();
});
@@ -232,7 +241,9 @@ describe("using a tree-based history", () => {
cold("-a-b-c-d", {
"a": () => history.add(undoable0),
"b": () => history.add(undoable1),
- "c": () => history.undo(),
+ "c": () => {
+ void history.undo();
+ },
"d": () => history.add(undoable2)
}).subscribe(v => {
v();
@@ -314,7 +325,7 @@ describe("using a tree-based history", () => {
});
test("history works", () => {
- history.undo();
+ void history.undo();
expect(history.size()).toBe(1);
expect(history.undoableNodes[0]).toBeDefined();
expect(history.currentNode).toBe(history.root);
@@ -325,7 +336,7 @@ describe("using a tree-based history", () => {
const toRedos = new Array();
const redosStream = history.redosObservable().subscribe((e: Undoable | undefined) => toRedos.push(e));
- history.undo();
+ void history.undo();
redosStream.unsubscribe();
expect(toRedos).toHaveLength(1);
@@ -350,10 +361,10 @@ describe("using a tree-based history", () => {
const redosStream = history.redosObservable().subscribe((e: Undoable | undefined) => redos.push(e));
history.add(undoable1);
- history.undo();
- history.undo();
- history.redo();
- history.redo();
+ void history.undo();
+ void history.undo();
+ void history.redo();
+ void history.redo();
undosStream.unsubscribe();
redosStream.unsubscribe();
@@ -372,32 +383,32 @@ describe("using a tree-based history", () => {
});
test("redo does nothing", () => {
- history.redo();
+ void history.redo();
expect(history.undoableNodes).toHaveLength(1);
expect(history.undoableNodes[0]).toBeDefined();
expect(undoable0.redo).not.toHaveBeenCalledTimes(1);
});
test("get last redoable when one element and has a redo", () => {
- history.undo();
+ void history.undo();
expect(history.getLastRedo()).toBe(undoable0);
});
test("get last redoable message when one element and ahs a redo", () => {
undoable0.getUndoName.mockReturnValue("fooo");
- history.undo();
+ void history.undo();
expect(history.getLastRedoMessage()).toBe("fooo");
});
test("get last redoable message or empty when one element and ahs a redo", () => {
undoable0.getUndoName.mockReturnValue("barr");
- history.undo();
+ void history.undo();
expect(history.getLastOrEmptyRedoMessage()).toBe("barr");
});
test("history redo works", () => {
- history.undo();
- history.redo();
+ void history.undo();
+ void history.redo();
expect(history.undoableNodes).toHaveLength(1);
expect(history.undoableNodes[0]).toBeDefined();
expect(history.undoableNodes[0]?.undoable).toBe(undoable0);
@@ -406,7 +417,7 @@ describe("using a tree-based history", () => {
});
test("history new command, creates a branch", () => {
- history.undo();
+ void history.undo();
history.add(undoable1);
expect(history.undoableNodes).toHaveLength(2);
expect(history.undoableNodes[0]?.undoable).toBe(undoable0);
@@ -443,7 +454,7 @@ describe("using a tree-based history", () => {
});
test("go to undoable1 from root", () => {
- history.undo();
+ void history.undo();
history.goTo(0);
expect(history.currentNode.undoable).toBe(undoable0);
@@ -468,7 +479,7 @@ describe("using a tree-based history", () => {
});
test("go to undoable1 from undoable2", () => {
- history.undo();
+ void history.undo();
history.add(undoable1);
history.goTo(0);
@@ -631,10 +642,10 @@ describe("using a tree-based history", () => {
// 3 4
history.add(undoable0);
history.add(undoable1);
- history.undo();
+ void history.undo();
history.add(undoable2);
history.add(undoable3);
- history.undo();
+ void history.undo();
history.add(undoable4);
});
@@ -743,7 +754,7 @@ describe("using a tree-based history", () => {
test("get last redoable when moving to 4 and history", () => {
history.goTo(4);
- history.undo();
+ void history.undo();
expect(history.getLastRedo()).toBe(undoable4);
});
@@ -761,7 +772,7 @@ describe("using a tree-based history", () => {
test("get last redoable when moving to 4 and history and delete 4", () => {
history.goTo(4);
- history.undo();
+ void history.undo();
history.deleteFrom(4);
expect(history.getLastRedo()).toBe(undoable3);
expect(history.size()).toBe(4);
@@ -770,14 +781,14 @@ describe("using a tree-based history", () => {
test("get last redoable message when moving to 2 and history", () => {
undoable2.getUndoName.mockReturnValue("fooo2");
history.goTo(2);
- history.undo();
+ void history.undo();
expect(history.getLastRedoMessage()).toBe("fooo2");
});
test("get last redoable or empty message when moving to 3 and history", () => {
undoable3.getUndoName = (): string => "fooo4";
history.goTo(3);
- history.undo();
+ void history.undo();
expect(history.getLastRedoMessage()).toBe("fooo4");
});
@@ -905,28 +916,28 @@ describe("using a tree-based history", () => {
history.add(undoable1);
history.add(undoable2);
history.add(undoable3);
- history.undo();
+ void history.undo();
history.add(undoable4);
- history.undo();
- history.undo();
+ void history.undo();
+ void history.undo();
history.add(undoable5);
- history.undo();
+ void history.undo();
history.add(undoable6);
history.add(undoable7);
- history.undo();
- history.undo();
- history.undo();
+ void history.undo();
+ void history.undo();
+ void history.undo();
history.add(undoable8);
history.add(undoable9);
- history.undo();
+ void history.undo();
history.add(undoable10);
- history.undo();
+ void history.undo();
history.add(undoable11);
- history.undo();
+ void history.undo();
history.add(undoable12);
history.goTo(5);
history.add(undoable13);
- history.undo();
+ void history.undo();
history.add(undoable14);
});
@@ -1042,12 +1053,12 @@ describe("using a tree-based history", () => {
test("does not create a new branch", () => {
history.add(undoableC);
history.add(undoableD);
- history.undo();
- history.undo();
+ void history.undo();
+ void history.undo();
history.add(undoableF);
// A *B - C D
// \ F
- history.undo();
+ void history.undo();
undoableC.equals = jest.fn(() => true);
history.add(undoableE);
@@ -1058,13 +1069,13 @@ describe("using a tree-based history", () => {
test("does not create a new branch, other config", () => {
history.add(undoableF);
- history.undo();
+ void history.undo();
history.add(undoableC);
history.add(undoableD);
// A *B - F
// \ C D
- history.undo();
- history.undo();
+ void history.undo();
+ void history.undo();
undoableC.equals = jest.fn(() => true);
history.add(undoableE);
@@ -1140,7 +1151,7 @@ describe("using a tree-based history", () => {
test("creates a branch with correct undoable objects", () => {
history.add(new CmdModifiableDouble());
- history.undo();
+ void history.undo();
history.add(new CmdModifiableDouble3());
// A - B - C|D*
history.deleteNode(1);
@@ -1266,7 +1277,7 @@ describe("using a tree-based history", () => {
test("can modify the lastest modifiable command and re-execute the tree undoables that follow", () => {
history.add(new StubUndoableCmd());
- history.undo();
+ void history.undo();
history.add(new CmdModifiableDouble());
// A - B - C
// \ D*
From 245befeaf5e427e167b41bc0cf0fef5c04d2054a Mon Sep 17 00:00:00 2001
From: arnobl <1052622+arnobl@users.noreply.github.com>
Date: Sun, 19 Apr 2026 22:36:40 +0200
Subject: [PATCH 2/4] feat(command): memento decorator to reduce boilerplate
code
---
src/api/command/Memento.ts | 5 ++-
test/command/memento.test.ts | 60 ++++++++++++++++++++++++++++++++----
2 files changed, 56 insertions(+), 9 deletions(-)
diff --git a/src/api/command/Memento.ts b/src/api/command/Memento.ts
index 5a601ed7..5b84778f 100644
--- a/src/api/command/Memento.ts
+++ b/src/api/command/Memento.ts
@@ -38,9 +38,8 @@ export function Memento(target: unknown, propertyName: string): void {
const symbolValues: unknown = (target.constructor as MementoMetadata)[INTERACTO_MEMENTO];
const map = symbolValues instanceof Map ? symbolValues as Map : new Map();
- // Adding the property in this cache
- const tpropertyName = propertyName as keyof CommandBase;
- map.set(propertyName, target[tpropertyName]);
+ // Adding the property in this cache (setting its value to undefined for the moment)
+ map.set(propertyName, undefined);
(target.constructor as MementoMetadata)[INTERACTO_MEMENTO] = map;
}
diff --git a/test/command/memento.test.ts b/test/command/memento.test.ts
index c236bc8c..1db65239 100644
--- a/test/command/memento.test.ts
+++ b/test/command/memento.test.ts
@@ -14,7 +14,11 @@
import {ExampleUndoableCmd} from "./StubCmd";
import {Memento} from "../../src/interacto";
-import {afterEach, beforeEach, describe, expect, jest, test} from "@jest/globals";
+import {beforeEach, describe, expect, test} from "@jest/globals";
+
+interface Data {
+ text: string;
+}
class MementoCmd1 extends ExampleUndoableCmd {
@Memento
@@ -38,17 +42,40 @@ class MementoCmd1 extends ExampleUndoableCmd {
}
}
-describe("using a command have memento decorators", () => {
+class MementoCmd2 extends ExampleUndoableCmd {
+ public data: Data;
+
+ public newTxt: string;
+
+ public constructor(data: Data, newTxt: string) {
+ super();
+ this.data = data;
+ this.newTxt = newTxt;
+ console.log("1data value", this.data);
+ }
+
+ @Memento
+ public get txt(): string {
+ console.log("data value", this.data);
+ return this.data.text;
+ }
+
+ public set txt(value: string) {
+ this.data.text = value;
+ }
+
+ public override execution(): void {
+ this.txt = this.newTxt;
+ }
+}
+
+describe("using a command that has memento decorators", () => {
let cmd1: MementoCmd1;
beforeEach(() => {
cmd1 = new MementoCmd1(1, 2);
});
- afterEach(() => {
- jest.clearAllMocks();
- });
-
test("command execution works as expected", () => {
void cmd1.execute();
expect(cmd1.x).toBe(10);
@@ -83,3 +110,24 @@ describe("using a command have memento decorators", () => {
expect(cmd1.newX).toBe(0);
});
});
+
+describe("using a command that has a memento decorator on a getter", () => {
+ let cmd1: MementoCmd2;
+
+ beforeEach(() => {
+ cmd1 = new MementoCmd2({
+ text: "foo"
+ }, "bar");
+ });
+
+ test("command execution works as expected", () => {
+ void cmd1.execute();
+ expect(cmd1.data.text).toBe("bar");
+ });
+
+ test("command undo uses automated memento to restore values", () => {
+ void cmd1.execute();
+ void cmd1.undo();
+ expect(cmd1.data.text).toBe("foo");
+ });
+});
From a58561ac02bf11ac2069ffbbde20f0942b587961 Mon Sep 17 00:00:00 2001
From: arnobl <1052622+arnobl@users.noreply.github.com>
Date: Sun, 19 Apr 2026 22:38:19 +0200
Subject: [PATCH 3/4] feat(command): memento decorator to reduce boilerplate
code
---
test/command/memento.test.ts | 2 --
1 file changed, 2 deletions(-)
diff --git a/test/command/memento.test.ts b/test/command/memento.test.ts
index 1db65239..22700769 100644
--- a/test/command/memento.test.ts
+++ b/test/command/memento.test.ts
@@ -51,12 +51,10 @@ class MementoCmd2 extends ExampleUndoableCmd {
super();
this.data = data;
this.newTxt = newTxt;
- console.log("1data value", this.data);
}
@Memento
public get txt(): string {
- console.log("data value", this.data);
return this.data.text;
}
From 3c59e8a709e91c4383997f2807bc01f7af7be5d1 Mon Sep 17 00:00:00 2001
From: arnobl <1052622+arnobl@users.noreply.github.com>
Date: Sun, 19 Apr 2026 23:02:47 +0200
Subject: [PATCH 4/4] feat(command): memento decorator to reduce boilerplate
code
---
src/api/command/Memento.ts | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/src/api/command/Memento.ts b/src/api/command/Memento.ts
index 5b84778f..cb9edfc4 100644
--- a/src/api/command/Memento.ts
+++ b/src/api/command/Memento.ts
@@ -12,8 +12,6 @@
* along with Interacto. If not, see .
*/
-import {CommandBase} from "../../impl/command/CommandBase";
-
const INTERACTO_MEMENTO: unique symbol = Symbol("interacto-cmd-memento");
interface MementoMetadata {
@@ -28,9 +26,9 @@ interface MementoMetadata {
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export function Memento(target: unknown, propertyName: string): void {
- if (!(target instanceof CommandBase)) {
+ if (!(target instanceof Object)) {
// eslint-disable-next-line no-console
- console.error("The @Memento decorator currently operates on Interacto commands only");
+ console.error("The @Memento decorator currently operates on objects only");
return;
}