-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: update history when a state property is set to undefined #14
Conversation
if (state.value.count === 1) { | ||
state.value.foo = undefined; | ||
} else { | ||
state.value.foo = 'bar'; | ||
} | ||
state.value.count++; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not certain what this test is trying to prove.. all changes within the same execution scope will cause a single history change as far as I know. This includes increasing the counter.
Can you provide a simpler test that sets a value from defined to undefined.
You can also use the delete
keyword to remove a property on the object as well
I would be interested to see both scenarios. And if they work or not without the changes made to the library
Ensure to add an await before the expectation is confirmed to simulate a change in execution context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I simplified the test case and I added another test case to check the behavior with delete
.
@@ -230,7 +233,7 @@ export function proxyWithHistory<V>( | |||
if (proxyObject.canUndo()) { | |||
proxyObject.history.wip = proxyObject.clone( | |||
proxyObject.history.nodes[--proxyObject.history.index]?.snapshot | |||
); | |||
) ?? EmptyWIP |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm unsure what the expectation is. The issue says that the value is set to undefined and no history change is reflected.. but this is setting the value to an empty object when null or undefined is found.
Seems like a data integrity issue.
Why not just set the value externally from the lib to an empty object if that's what's required in that use case?
What is the expectation here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I'm understandin correctly, the method shouldSaveHistory
returns true
when the value changed and when the new value is different than the history wip.
Since by default (when empty) the history wip is undefined
, when setting a value to undefined
the above method returns false
and the history isn't updated. By setting the default history wip value to an empty object, when the value has changed and the history wip is empty the above method would return true
, so setting a value to undefined
would work as expected.
Using an empty object ensures that it is equal (===
) only to itself, so even setting the new value to an empty object would work as expected, but that is not the use case I was trying to address.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still don't really get the point..if the value is undefined and you try to set it to undefined then there should be no history change..
But if it's set then that means it's not undefined.
And if it sees undefined then undefined will not be equal to wip..
So what am I missing?
In theory there is no benefit to changing it to an empty object.
Do your test cases pass without these changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not the value that is undefined
but the history wip
property.
The first test case fails, the second one (delete
) succeeds. If you run the first test case without the changes, you can see that when invoking setValueToUndefined()
a state value is set from "bar"
to undefined
and the history is not updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks.
I'll check it in detail later
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it('should update history when property is deleted', async () => {
const state = proxyWithHistory<
Partial<{ prop0: number; prop1: number; prop2: number; prop3: number }>
>({ prop0: 0, prop1: 1, prop2: 2, prop3: 3 });
await Promise.resolve();
expect(state.value.prop0).toEqual(0);
expect(state.value.prop1).toEqual(1);
expect(state.value.prop2).toEqual(2);
expect(state.value.prop3).toEqual(3);
expect(state.history.nodes.length).toEqual(1);
delete state.value.prop0;
await Promise.resolve();
expect(state.value.prop0).toEqual(undefined);
expect(state.value.prop1).toEqual(1);
expect(state.value.prop2).toEqual(2);
expect(state.value.prop3).toEqual(3);
expect(state.history.nodes.length).toEqual(2);
});
it('should update history when property is set to undefined', async () => {
const state = proxyWithHistory<
Partial<{ prop0: number; prop1: number; prop2: number; prop3: number }>
>({ prop0: 0, prop1: 1, prop2: 2, prop3: 3 });
await Promise.resolve();
expect(state.value.prop0).toEqual(0);
expect(state.value.prop1).toEqual(1);
expect(state.value.prop2).toEqual(2);
expect(state.value.prop3).toEqual(3);
expect(state.history.nodes.length).toEqual(1);
state.value.prop0 = undefined;
await Promise.resolve();
expect(state.value.prop0).toEqual(undefined);
expect(state.value.prop1).toEqual(1);
expect(state.value.prop2).toEqual(2);
expect(state.value.prop3).toEqual(3);
expect(state.history.nodes.length).toEqual(2);
});
i used these test cases without your changes to understand the issue a little better in isolation of any other property changes.
first test case passes and the second one doesnt.
so thanks i have a clearer understanding
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, I added your your test cases to the vanilla suite and removed mine from react suite.
await screen.findByText('count: 0'); | ||
|
||
fireEvent.click(screen.getByText('inc')); | ||
await screen.findByText('count: 1'); | ||
|
||
fireEvent.click(screen.getByText('inc')); | ||
await screen.findByText('count: 2'); | ||
|
||
fireEvent.click(screen.getByText('inc')); | ||
await screen.findByText('count: 3'); | ||
|
||
fireEvent.click(screen.getByText('undo')); | ||
await screen.findByText('count: 2'); | ||
|
||
fireEvent.click(screen.getByText('redo')); | ||
await screen.findByText('count: 3'); | ||
|
||
fireEvent.click(screen.getByText('undo')); | ||
await screen.findByText('count: 2'); | ||
|
||
fireEvent.click(screen.getByText('undo')); | ||
await screen.findByText('count: 1'); | ||
|
||
fireEvent.click(screen.getByText('undo')); | ||
await screen.findByText('count: 0'); | ||
|
||
fireEvent.click(screen.getByText('inc')); | ||
await screen.findByText('count: 1'); | ||
|
||
fireEvent.click(screen.getByText('undo')); | ||
await screen.findByText('count: 0'); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
test title is : should update history when a state property is set to undefined
but there is no confirmation of what its trying to test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added more meaningful expectations.
@@ -230,7 +233,7 @@ export function proxyWithHistory<V>( | |||
if (proxyObject.canUndo()) { | |||
proxyObject.history.wip = proxyObject.clone( | |||
proxyObject.history.nodes[--proxyObject.history.index]?.snapshot | |||
); | |||
) ?? EmptyWIP |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it('should update history when property is deleted', async () => {
const state = proxyWithHistory<
Partial<{ prop0: number; prop1: number; prop2: number; prop3: number }>
>({ prop0: 0, prop1: 1, prop2: 2, prop3: 3 });
await Promise.resolve();
expect(state.value.prop0).toEqual(0);
expect(state.value.prop1).toEqual(1);
expect(state.value.prop2).toEqual(2);
expect(state.value.prop3).toEqual(3);
expect(state.history.nodes.length).toEqual(1);
delete state.value.prop0;
await Promise.resolve();
expect(state.value.prop0).toEqual(undefined);
expect(state.value.prop1).toEqual(1);
expect(state.value.prop2).toEqual(2);
expect(state.value.prop3).toEqual(3);
expect(state.history.nodes.length).toEqual(2);
});
it('should update history when property is set to undefined', async () => {
const state = proxyWithHistory<
Partial<{ prop0: number; prop1: number; prop2: number; prop3: number }>
>({ prop0: 0, prop1: 1, prop2: 2, prop3: 3 });
await Promise.resolve();
expect(state.value.prop0).toEqual(0);
expect(state.value.prop1).toEqual(1);
expect(state.value.prop2).toEqual(2);
expect(state.value.prop3).toEqual(3);
expect(state.history.nodes.length).toEqual(1);
state.value.prop0 = undefined;
await Promise.resolve();
expect(state.value.prop0).toEqual(undefined);
expect(state.value.prop1).toEqual(1);
expect(state.value.prop2).toEqual(2);
expect(state.value.prop3).toEqual(3);
expect(state.history.nodes.length).toEqual(2);
});
i used these test cases without your changes to understand the issue a little better in isolation of any other property changes.
first test case passes and the second one doesnt.
so thanks i have a clearer understanding
type EmptyWIP = Record<string, never>; | ||
const EmptyWIP: EmptyWIP = {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i still have a problem with the semantics here so how about we use a Symbol and more standard naming convention
type EmptyWIP = Record<string, never>; | |
const EmptyWIP: EmptyWIP = {}; | |
const EMPTY_WIP = Symbol('valtio-history-wip-empty'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I fixed the naming case and used a Symbol.
export type History<T> = { | ||
/** | ||
* field for holding sandbox changes; used to avoid infinite loops | ||
*/ | ||
wip?: Snapshot<T>; | ||
wip: Snapshot<T> | EmptyWIP; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
given we use this once
wip: Snapshot<T> | EmptyWIP; | |
wip: Snapshot<T> | typeof EMPTY_WIP; |
@@ -134,7 +137,7 @@ export function proxyWithHistory<V>( | |||
* - history.wip: field for holding sandbox changes; used to avoid infinite loops<br> | |||
*/ | |||
history: ref<History<V>>({ | |||
wip: undefined, // to avoid infinite loop | |||
wip: EmptyWIP, // to avoid infinite loop |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also ensure the others are renamed
wip: EmptyWIP, // to avoid infinite loop | |
wip: EMPTY_WIP, // to avoid infinite loop |
@@ -1,6 +1,6 @@ | |||
{ | |||
"name": "valtio-history", | |||
"version": "0.3.2", | |||
"version": "0.3.3", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you dont need to adjust this.. its automated when releasing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, rolled back
you will need to run
otherwise looks good |
Formatting applied 😊 |
Thanks Fabio for finding and fixing this issue I'll release it later |
release has been made |
Fixes #13