Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/data/spec/selectors/entity-selectors$.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe('EntitySelectors$', () => {

// listen for changes to the hero collection
store
.select<HeroCollection>(ENTITY_CACHE_NAME as any, 'Hero')
.select((state: any) => state[ENTITY_CACHE_NAME]['Hero'])
.subscribe((c: HeroCollection) => (collection = c));
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type Options = ESLintUtils.InferOptionsTypeFromRule<typeof rule>;

const valid: () => (string | ValidTestCase<Options>)[] = () => [
`export const selectFeature: MemoizedSelector<any, any> = (state: AppState) => state.feature`,
`export const selectFeature: MemoizedSelectorWithProps<any, any> = ({ feature }) => feature`,
`export const selectFeature: Selector<any, any> = ({ feature }) => feature`,
`export const selectFeature = createSelector((state: AppState) => state.feature)`,
`export const selectFeature = createFeatureSelector<FeatureState>(featureKey)`,
`export const selectFeature = createFeatureSelector<AppState, FeatureState>(featureKey)`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,7 @@ export default createRule<Options, MessageIds>({

const hasSelectorType =
typeName !== null &&
[
'MemoizedSelector',
'MemoizedSelectorWithProps',
'Selector',
'SelectorWithProps',
].includes(typeName);
['MemoizedSelector', 'Selector'].includes(typeName);

const isSelectorCall =
init?.type === 'CallExpression' && isSelectorFactoryCall(init);
Expand Down
5 changes: 4 additions & 1 deletion modules/router-store/spec/router_store_module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ describe('Router Store Module', () => {
new Promise<void>((done) => {
let logs: any[] = [];
store
.pipe(select(customStateKey), withLatestFrom(store))
.pipe(
select((state: State) => state[customStateKey]),
withLatestFrom(store)
)
.subscribe(([routerStoreState, storeState]) => {
logs.push([routerStoreState, storeState]);
});
Expand Down
7 changes: 6 additions & 1 deletion modules/router-store/src/store_router_connecting.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,13 @@ export class StoreRouterConnectingService {
}

private setUpStoreStateListener(): void {
const selector: (state: any) => RouterReducerState =
typeof this.stateKey === 'string'
? (state) => state[this.stateKey as string]
: this.stateKey;

this.store
.pipe(select(this.stateKey as any), withLatestFrom(this.store))
.pipe(select(selector), withLatestFrom(this.store))
.subscribe(([routerStoreState, storeState]) => {
this.navigateIfNeeded(routerStoreState, storeState);
});
Expand Down
296 changes: 296 additions & 0 deletions modules/store/migrations/21_0_0/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
import {
SchematicTestRunner,
UnitTestTree,
} from '@angular-devkit/schematics/testing';
import { createWorkspace } from '@ngrx/schematics-core/testing';
import * as path from 'path';
import { tags } from '@angular-devkit/core';
import { logging } from '@angular-devkit/core';

describe('Store Migration to 21.0.0', () => {
const collectionPath = path.join(
process.cwd(),
'dist/modules/store/migrations/migration.json'
);
const schematicRunner = new SchematicTestRunner('schematics', collectionPath);

let appTree: UnitTestTree;

beforeEach(async () => {
appTree = await createWorkspace(schematicRunner, appTree);
});

const verifySchematic = async (input: string, output: string) => {
appTree.create('main.ts', input);

const logs: logging.LogEntry[] = [];
schematicRunner.logger.subscribe((e) => logs.push(e));

const tree = await schematicRunner.runSchematic(
'ngrx-store-migration-21',
{},
appTree
);

const actual = tree.readContent('main.ts');
expect(actual).toBe(output);

return logs;
};

describe('removing SelectorWithProps and MemoizedSelectorWithProps imports', () => {
it('should remove SelectorWithProps from import', async () => {
const input = tags.stripIndent`
import { createSelector, SelectorWithProps } from '@ngrx/store';

const selector = createSelector(
(state: AppState) => state.items,
(items) => items.length
);
`;
const output = tags.stripIndent`
import { createSelector } from '@ngrx/store';

const selector = createSelector(
(state: AppState) => state.items,
(items) => items.length
);
`;

await verifySchematic(input, output);
});

it('should remove MemoizedSelectorWithProps from import', async () => {
const input = tags.stripIndent`
import { Store, MemoizedSelectorWithProps } from '@ngrx/store';

class MyComponent {
constructor(private store: Store) {}
}
`;
const output = tags.stripIndent`
import { Store } from '@ngrx/store';

class MyComponent {
constructor(private store: Store) {}
}
`;

await verifySchematic(input, output);
});

it('should remove both SelectorWithProps and MemoizedSelectorWithProps from import', async () => {
const input = tags.stripIndent`
import { createSelector, SelectorWithProps, MemoizedSelectorWithProps } from '@ngrx/store';

const selector = createSelector(
(state: AppState) => state.items,
(items) => items.length
);
`;
const output = tags.stripIndent`
import { createSelector } from '@ngrx/store';

const selector = createSelector(
(state: AppState) => state.items,
(items) => items.length
);
`;

await verifySchematic(input, output);
});

it('should remove entire import if only removed types are imported', async () => {
const input = tags.stripIndent`
import { SelectorWithProps } from '@ngrx/store';
import { Component } from '@angular/core';
`;
const output = tags.stripIndent`
import { Component } from '@angular/core';
`;

await verifySchematic(input, output);
});

it('should not modify files without removed types', async () => {
const input = tags.stripIndent`
import { createSelector, Store } from '@ngrx/store';

const selector = createSelector(
(state: AppState) => state.items,
(items) => items.length
);
`;

await verifySchematic(input, input);
});

it('should handle double-quote imports', async () => {
const input = tags.stripIndent`
import { createSelector, SelectorWithProps } from "@ngrx/store";

const selector = createSelector(
(state: AppState) => state.items,
(items) => items.length
);
`;
const output = tags.stripIndent`
import { createSelector } from "@ngrx/store";

const selector = createSelector(
(state: AppState) => state.items,
(items) => items.length
);
`;

await verifySchematic(input, output);
});
});

describe('migrating string-key select calls', () => {
it('should migrate store.select with a string key', async () => {
const input = tags.stripIndent`
import { Store } from '@ngrx/store';

class MyComponent {
data$ = this.store.select('featureName');
constructor(private store: Store) {}
}
`;
const output = tags.stripIndent`
import { Store } from '@ngrx/store';

class MyComponent {
data$ = this.store.select((state: any) => state['featureName']);
constructor(private store: Store) {}
}
`;

await verifySchematic(input, output);
});

it('should migrate select operator with a string key', async () => {
const input = tags.stripIndent`
import { Store, select } from '@ngrx/store';

class MyComponent {
data$ = this.store.pipe(select('featureName'));
constructor(private store: Store) {}
}
`;
const output = tags.stripIndent`
import { Store, select } from '@ngrx/store';

class MyComponent {
data$ = this.store.pipe(select((state: any) => state['featureName']));
constructor(private store: Store) {}
}
`;

await verifySchematic(input, output);
});

it('should migrate nested string-key select', async () => {
const input = tags.stripIndent`
import { Store, select } from '@ngrx/store';

class MyComponent {
data$ = this.store.pipe(select('feature', 'nested', 'prop'));
constructor(private store: Store) {}
}
`;
const output = tags.stripIndent`
import { Store, select } from '@ngrx/store';

class MyComponent {
data$ = this.store.pipe(select((state: any) => state['feature']['nested']['prop']));
constructor(private store: Store) {}
}
`;

await verifySchematic(input, output);
});

it('should not modify select calls with function selectors', async () => {
const input = tags.stripIndent`
import { Store, select } from '@ngrx/store';

const selectItems = (state: AppState) => state.items;

class MyComponent {
data$ = this.store.pipe(select(selectItems));
constructor(private store: Store) {}
}
`;

await verifySchematic(input, input);
});
});

describe('warning about select with props', () => {
it('should add TODO comment and warn about select(selector, props) calls', async () => {
const input = tags.stripIndent`
import { Store, select } from '@ngrx/store';

class MyComponent {
data$ = this.store.pipe(select(mySelector, { id: 1 }));
constructor(private store: Store) {}
}
`;
const output = tags.stripIndent`
import { Store, select } from '@ngrx/store';

class MyComponent {
// TODO: @ngrx/store v21 migration - convert to a factory selector. See https://ngrx.io/guide/migration/v21
data$ = this.store.pipe(select(mySelector, { id: 1 }));
constructor(private store: Store) {}
}
`;

const logs = await verifySchematic(input, output);
const warnings = logs.filter((l) => l.level === 'warn');
expect(warnings.length).toBe(1);
expect(warnings[0].message).toContain('requires manual migration');
});

it('should add TODO comment and warn about store.select(selector, props) calls', async () => {
const input = tags.stripIndent`
import { Store } from '@ngrx/store';

class MyComponent {
data$ = this.store.select(mySelector, { id: 1 });
constructor(private store: Store) {}
}
`;
const output = tags.stripIndent`
import { Store } from '@ngrx/store';

class MyComponent {
// TODO: @ngrx/store v21 migration - convert to a factory selector. See https://ngrx.io/guide/migration/v21
data$ = this.store.select(mySelector, { id: 1 });
constructor(private store: Store) {}
}
`;

const logs = await verifySchematic(input, output);
const warnings = logs.filter((l) => l.level === 'warn');
expect(warnings.length).toBe(1);
expect(warnings[0].message).toContain('requires manual migration');
});
});

describe('files without @ngrx/store import', () => {
it('should not modify files without @ngrx/store import', async () => {
const input = tags.stripIndent`
import { Component } from '@angular/core';

class MyComponent {
select(key: string) { return key; }
data = this.select('featureName');
}
`;

await verifySchematic(input, input);
});
});
});
Loading
Loading