Skip to content

Commit

Permalink
feat: add e2e tests and setResetState
Browse files Browse the repository at this point in the history
  • Loading branch information
rainerhahnekamp committed Jan 6, 2025
1 parent 17cb61e commit ac86fb1
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 7 deletions.
30 changes: 30 additions & 0 deletions apps/demo/e2e/reset.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { test, expect } from '@playwright/test';

test('has title', async ({ page }) => {
await page.goto('');
await page.getByRole('link', { name: 'reset' }).click();
await page
.getByRole('row', { name: 'Go for a walk' })
.getByRole('checkbox')
.click();
await page
.getByRole('row', { name: 'Exercise' })
.getByRole('checkbox')
.click();

await expect(
page.getByRole('row', { name: 'Go for a walk' }).getByRole('checkbox')
).toBeChecked();
await expect(
page.getByRole('row', { name: 'Exercise' }).getByRole('checkbox')
).toBeChecked();

await page.getByRole('button', { name: 'Reset State' }).click();

await expect(
page.getByRole('row', { name: 'Go for a walk' }).getByRole('checkbox')
).not.toBeChecked();
await expect(
page.getByRole('row', { name: 'Exercise' }).getByRole('checkbox')
).not.toBeChecked();
});
1 change: 1 addition & 0 deletions apps/demo/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
>Redux Connector</a
>
<a mat-list-item routerLink="/todo-storage-sync">withStorageSync</a>
<a mat-list-item routerLink="/reset">withReset</a>
</mat-nav-list>
</mat-drawer>
<mat-drawer-content>
Expand Down
5 changes: 5 additions & 0 deletions apps/demo/src/app/lazy-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,9 @@ export const lazyRoutes: Route[] = [
providers: [provideFlightStore()],
component: FlightSearchReducConnectorComponent,
},
{
path: 'reset',
loadComponent: () =>
import('./reset/todo.component').then((m) => m.TodoComponent),
},
];
84 changes: 84 additions & 0 deletions apps/demo/src/app/reset/todo-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
getState,
patchState,
signalStore,
withHooks,
withMethods,
withState,
} from '@ngrx/signals';
import { addEntity, updateEntity, withEntities } from '@ngrx/signals/entities';
import { setResetState, withReset } from '@angular-architects/ngrx-toolkit';

export interface Todo {
id: number;
name: string;
finished: boolean;
description?: string;
deadline?: Date;
}

export type AddTodo = Omit<Todo, 'id'>;

export const TodoStore = signalStore(
{ providedIn: 'root' },
withReset(),
withEntities<Todo>(),
withState({
selectedIds: [] as number[],
}),
withMethods((store) => {
let currentId = 0;
return {
_add(todo: AddTodo) {
patchState(store, addEntity({ ...todo, id: ++currentId }));
},
toggleFinished(id: number) {
const todo = store.entityMap()[id];
patchState(
store,
updateEntity({ id, changes: { finished: !todo.finished } })
);
},
};
}),
withHooks({
onInit: (store) => {
store._add({
name: 'Go for a Walk',
finished: false,
description:
'Go for a walk in the park to relax and enjoy nature. Walking is a great way to clear your mind and get some exercise. It can help reduce stress and improve your mood. Make sure to wear comfortable shoes and bring a bottle of water. Enjoy the fresh air and take in the scenery around you.',
});

store._add({
name: 'Read a Book',
finished: false,
description:
'Spend some time reading a book. It can be a novel, a non-fiction book, or any other genre you enjoy. Reading can help you relax and learn new things.',
});

store._add({
name: 'Write a Journal',
finished: false,
description:
'Take some time to write in your journal. Reflect on your day, your thoughts, and your feelings. Journaling can be a great way to process emotions and document your life.',
});

store._add({
name: 'Exercise',
finished: false,
description:
'Do some physical exercise. It can be a workout, a run, or any other form of exercise you enjoy. Exercise is important for maintaining physical and mental health.',
});

store._add({
name: 'Cook a Meal',
finished: false,
description:
'Prepare a meal for yourself or your family. Cooking can be a fun and rewarding activity. Try out a new recipe or make one of your favorite dishes.',
});

setResetState(store, getState(store));
},
})
);
70 changes: 70 additions & 0 deletions apps/demo/src/app/reset/todo.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Component, effect, inject } from '@angular/core';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatIconModule } from '@angular/material/icon';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { SelectionModel } from '@angular/cdk/collections';
import { Todo, TodoStore } from './todo-store';
import { MatButton } from '@angular/material/button';

@Component({
template: `
<div class="button">
<button mat-raised-button (click)="resetState()">Reset State</button>
</div>
<div>
<mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<!-- Checkbox Column -->
<ng-container matColumnDef="finished">
<mat-header-cell *matHeaderCellDef></mat-header-cell>
<mat-cell *matCellDef="let row" class="actions">
<mat-checkbox
(click)="$event.stopPropagation()"
(change)="toggleFinished(row)"
[checked]="row.finished"
>
</mat-checkbox>
</mat-cell>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
<mat-cell *matCellDef="let element">{{ element.name }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row
*matRowDef="let row; columns: displayedColumns"
(click)="selection.toggle(row)"
></mat-row>
</mat-table>
</div>
`,
styles: `.button {
margin-bottom: 1em;
}
`,
imports: [MatCheckboxModule, MatIconModule, MatTableModule, MatButton],
})
export class TodoComponent {
todoStore = inject(TodoStore);

displayedColumns: string[] = ['finished', 'name'];
dataSource = new MatTableDataSource<Todo>([]);
selection = new SelectionModel<Todo>(true, []);

constructor() {
effect(() => {
this.dataSource.data = this.todoStore.entities();
});
}

toggleFinished(todo: Todo) {
this.todoStore.toggleFinished(todo.id);
}

resetState() {
this.todoStore.resetState();
}
}
16 changes: 16 additions & 0 deletions docs/docs/with-reset.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,19 @@ console.log(store.user()); // { id: 2, name: 'John' }
store.resetState();
console.log(store.user()); // { id: 1, name: 'Konrad' }
```

## `setResetState()`

If you want to set a custom reset state, you can use the `setResetState()` method.

Example:

```typescript
// continue from the previous example

setResetState(store, { user: { id: 3, name: 'Jane' }, address: { city: 'Berlin', zip: '10115' } });
store.changeUser(4, 'Alice');

store.resetState();
console.log(store.user()); // { id: 3, name: 'Jane' }
```
2 changes: 1 addition & 1 deletion libs/ngrx-toolkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ export * from './lib/with-undo-redo';
export * from './lib/with-data-service';
export { withStorageSync, SyncConfig } from './lib/with-storage-sync';
export * from './lib/with-pagination';
export { withReset } from './lib/with-reset';
export { withReset, setResetState } from './lib/with-reset';
31 changes: 30 additions & 1 deletion libs/ngrx-toolkit/src/lib/with-reset.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
withMethods,
withState,
} from '@ngrx/signals';
import { withReset } from './with-reset';
import { setResetState, withReset } from './with-reset';
import { TestBed } from '@angular/core/testing';
import { effect } from '@angular/core';

Expand All @@ -26,6 +26,9 @@ describe('withReset', () => {
changeUserName(name: string) {
patchState(store, (value) => ({ user: { ...value.user, name } }));
},
changeAddress(city: string, zip: string) {
patchState(store, { address: { city, zip } });
},
}))
);

Expand Down Expand Up @@ -80,4 +83,30 @@ describe('withReset', () => {
TestBed.flushEffects();
expect(effectCounter).toBe(1);
});

it('should be possible to change the reset state', () => {
const { store } = setup();

setResetState(store, {
user: { id: 2, name: 'Max' },
address: { city: 'London', zip: 'SW1' },
});

store.changeUser(3, 'Ludwig');
store.changeAddress('Paris', '75001');

store.resetState();
expect(getState(store)).toEqual({
user: { id: 2, name: 'Max' },
address: { city: 'London', zip: 'SW1' },
});
});

it('should throw on setResetState if store is not configured with withReset()', () => {
const Store = signalStore({ providedIn: 'root' }, withState({}));
const store = TestBed.inject(Store);
expect(() => setResetState(store, {})).toThrowError(
'Cannot set reset state, since store is not configured with withReset()'
);
});
});
48 changes: 43 additions & 5 deletions libs/ngrx-toolkit/src/lib/with-reset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,61 @@ import {
getState,
patchState,
signalStoreFeature,
StateSource,
withHooks,
withMethods,
withProps,
} from '@ngrx/signals';

export type PublicMethods = {
resetState(): void;
};

/**
* Adds a `resetState` method to the store, which resets the state
* to the initial state.
*
* If you want to set a custom initial state, you can use {@link setResetState}.
*/
export function withReset() {
return signalStoreFeature(
withProps(() => ({ _resetState: { value: {} } })),
withMethods((store) => ({
resetState() {
patchState(store, store._resetState.value);
},
})),
withMethods((store): PublicMethods => {
// workaround to TS excessive property check
const methods = {
resetState() {
patchState(store, store._resetState.value);
},
__setResetState__(state: object) {
store._resetState.value = state;
},
};

return methods;
}),
withHooks((store) => ({
onInit() {
store._resetState.value = getState(store);
},
}))
);
}

/**
* Sets the reset state of the store to the given state.
*
* Throws an error if the store is not configured with {@link withReset}.
* @param store the instance of a SignalStore
* @param state the state to set as the reset state
*/
export function setResetState<State extends object>(
store: StateSource<State>,
state: State
): void {
if (!('__setResetState__' in store)) {
throw new Error(
'Cannot set reset state, since store is not configured with withReset()'
);
}
(store.__setResetState__ as (state: State) => void)(state);
}

0 comments on commit ac86fb1

Please sign in to comment.