Skip to content

Commit

Permalink
feat(a16): supporting required inputs help-me-mom#5350
Browse files Browse the repository at this point in the history
  • Loading branch information
satanTime committed Apr 15, 2023
1 parent 23cc0a9 commit f956242
Show file tree
Hide file tree
Showing 27 changed files with 2,467 additions and 10,199 deletions.
842 changes: 456 additions & 386 deletions e2e/a16/package-lock.json

Large diffs are not rendered by default.

24 changes: 12 additions & 12 deletions e2e/a16/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,22 @@
"ng-mocks": "*"
},
"dependencies": {
"@angular/animations": "16.0.0-next.4",
"@angular/common": "16.0.0-next.4",
"@angular/compiler": "16.0.0-next.4",
"@angular/core": "16.0.0-next.4",
"@angular/forms": "16.0.0-next.4",
"@angular/platform-browser": "16.0.0-next.4",
"@angular/platform-browser-dynamic": "16.0.0-next.4",
"@angular/router": "16.0.0-next.4",
"@angular/animations": "16.0.0-rc.1",
"@angular/common": "16.0.0-rc.1",
"@angular/compiler": "16.0.0-rc.1",
"@angular/core": "16.0.0-rc.1",
"@angular/forms": "16.0.0-rc.1",
"@angular/platform-browser": "16.0.0-rc.1",
"@angular/platform-browser-dynamic": "16.0.0-rc.1",
"@angular/router": "16.0.0-rc.1",
"rxjs": "7.8.0",
"tslib": "2.5.0",
"zone.js": "0.13.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "16.0.0-next.6",
"@angular/cli": "16.0.0-next.6",
"@angular/compiler-cli": "16.0.0-next.4",
"@angular-devkit/build-angular": "16.0.0-rc.0",
"@angular/cli": "16.0.0-rc.0",
"@angular/compiler-cli": "16.0.0-rc.1",
"@types/jasmine": "4.3.1",
"@types/jest": "29.5.0",
"jasmine-core": "4.5.0",
Expand All @@ -48,7 +48,7 @@
"ng-packagr": "16.0.0-rc.0",
"puppeteer": "19.9.0",
"ts-node": "10.9.1",
"typescript": "4.9.5"
"typescript": "5.0.4"
},
"engines": {
"npm": "9.6.4"
Expand Down
15 changes: 15 additions & 0 deletions libs/ng-mocks/src/lib/common/core.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,21 @@ export type AnyType<T> = Type<T> | AbstractType<T>;
*/
export type AnyDeclaration<T> = AnyType<T> | InjectionToken<T> | string;

/**
* Normalized Input / Output type.
* It should be A16 structure.
*
* @internal
*/
export type DirectiveIoParsed = { name: string; alias?: string; required?: boolean };

/**
* Possible Input / Output type.
*
* @internal
*/
export type DirectiveIo = string | DirectiveIoParsed;

/**
* DebugNodeSelector describes supported types of selectors
* to search elements and instances in fixtures.
Expand Down
14 changes: 8 additions & 6 deletions libs/ng-mocks/src/lib/common/decorate.inputs.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { Input } from '@angular/core';

import { AnyType } from './core.types';
import { AnyType, DirectiveIo } from './core.types';
import funcDirectiveIoBuild from './func.directive-io-build';
import funcDirectiveIoParse from './func.directive-io-parse';

// Looks like an A9 bug, that queries from @Component are not processed.
// Also we have to pass prototype, not the class.
// Also, we have to pass prototype, not the class.
// The same issue happens with outputs, but time to time
// (when I restart tests with refreshing browser manually).
// https://github.com/help-me-mom/ng-mocks/issues/109
export default (cls: AnyType<any>, inputs?: string[], exclude?: string[]) => {
export default (cls: AnyType<any>, inputs?: Array<DirectiveIo>, exclude?: string[]) => {
// istanbul ignore else
if (inputs) {
for (const input of inputs) {
const [key, alias] = input.split(': ');
if (exclude && exclude.indexOf(key) !== -1) {
const { name, alias, required } = funcDirectiveIoParse(input);
if (exclude && exclude.indexOf(name) !== -1) {
continue;
}
Input(alias)(cls.prototype, key);
Input(funcDirectiveIoBuild({ name, alias, required }, true) as never)(cls.prototype, name);
}
}
};
12 changes: 7 additions & 5 deletions libs/ng-mocks/src/lib/common/decorate.outputs.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { Output } from '@angular/core';

import { AnyType } from './core.types';
import { AnyType, DirectiveIo } from './core.types';
import funcDirectiveIoBuild from './func.directive-io-build';
import funcDirectiveIoParse from './func.directive-io-parse';

// Looks like an A9 bug, that queries from @Component are not processed.
// Also we have to pass prototype, not the class.
// Also, we have to pass prototype, not the class.
// The same issue happens with outputs, but time to time
// (when I restart tests with refreshing browser manually).
// https://github.com/help-me-mom/ng-mocks/issues/109
export default (cls: AnyType<any>, outputs?: string[]) => {
export default (cls: AnyType<any>, outputs?: Array<DirectiveIo>) => {
// istanbul ignore else
if (outputs) {
for (const output of outputs) {
const [key, alias] = output.split(': ');
Output(alias)(cls.prototype, key);
const { name, alias, required } = funcDirectiveIoParse(output);
Output(funcDirectiveIoBuild({ name, alias, required }, true) as never)(cls.prototype, name);
}
}
};
12 changes: 12 additions & 0 deletions libs/ng-mocks/src/lib/common/func.directive-io-build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { DirectiveIo, DirectiveIoParsed } from './core.types';

export default function ({ name, alias, required }: DirectiveIoParsed, skipName = false): DirectiveIo {
if (required) {
return { name, alias, required };
}
if (!alias || name === alias) {
return skipName ? '' : name;
}

return skipName ? alias : `${name}:${alias}`;
}
15 changes: 15 additions & 0 deletions libs/ng-mocks/src/lib/common/func.directive-io-parse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { DirectiveIo, DirectiveIoParsed } from './core.types';

export default function (param: DirectiveIo): DirectiveIoParsed {
if (typeof param === 'string') {
const [name, alias] = param.split(':').map(v => v.trim());

if (name === alias || !alias) {
return { name };
}

return { name, alias };
}

return param;
}
7 changes: 4 additions & 3 deletions libs/ng-mocks/src/lib/common/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import helperMockService from '../mock-service/helper.mock-service';
import coreDefineProperty from './core.define-property';
import coreForm from './core.form';
import { mapValues } from './core.helpers';
import { AnyType } from './core.types';
import { AnyType, DirectiveIo } from './core.types';
import funcDirectiveIoParse from './func.directive-io-parse';
import funcIsMock from './func.is-mock';
import { MockControlValueAccessorProxy } from './mock-control-value-accessor-proxy';
import ngMocksUniverse from './ng-mocks-universe';
Expand Down Expand Up @@ -66,7 +67,7 @@ const applyNgValueAccessor = (instance: any, ngControl: any) => {
const applyOutputs = (instance: MockConfig & Record<keyof any, any>) => {
const mockOutputs = [];
for (const output of instance.__ngMocksConfig.outputs || []) {
mockOutputs.push(output.split(':')[0]);
mockOutputs.push(funcDirectiveIoParse(output).name);
}

for (const output of mockOutputs) {
Expand Down Expand Up @@ -113,7 +114,7 @@ export type ngMocksMockConfig = {
init?: (instance: any) => void;
isControlValueAccessor?: boolean;
isValidator?: boolean;
outputs?: string[];
outputs?: Array<DirectiveIo>;
queryScanKeys?: string[];
setControlValueAccessor?: boolean;
transform?: PipeTransform['transform'];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ export default (ngModule: NgMeta): void => {
// but we still would like to patch resolveComponentFactory in order to provide mocks.
// ɵmod is added only if Ivy has been enabled.
entryComponents: (IvyModule as any).ɵmod ? [] : /* istanbul ignore next */ entryComponents,
})(entryModule);
} as never)(entryModule);
ngModule.imports.push(entryModule);
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class BaseSimpleComponent {
}

@Component({
exportAs: 'seeimple',
exportAs: 'simple',
selector: 'simple-component',
template: 'some template',
})
Expand Down
4 changes: 2 additions & 2 deletions libs/ng-mocks/src/lib/mock-component/mock-component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import { MockedComponent } from './types';
</simple-component>
<simple-component
[someInput]="'hi again'"
#f="seeimple"
#f="simple"
></simple-component>
<empty-component></empty-component>
<custom-form-control
Expand Down Expand Up @@ -96,7 +96,7 @@ describe('MockComponent', () => {
await TestBed.configureTestingModule({
declarations: [
ExampleContainerComponent,
MockComponents(
...MockComponents(
EmptyComponent,
GetterSetterComponent,
SimpleComponent,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import funcDirectiveIoParse from '../../common/func.directive-io-parse';
import { MockedDebugNode } from '../../mock-render/types';

import funcGetPublicProviderKeys from './func.get-public-provider-keys';
Expand All @@ -7,11 +8,11 @@ const detectInClassic = (node: MockedDebugNode, attribute: string, value: any):
for (const key of funcGetPublicProviderKeys(node)) {
const [inputs, expectedAttributes, nodeIndex] = funcParseInputsAndRequiresAttributes(node, key);
for (const input of inputs) {
const [prop, alias] = input.split(': ');
if (attribute !== (alias || prop) || expectedAttributes.indexOf(prop) === -1) {
const { name, alias } = funcDirectiveIoParse(input);
if (attribute !== (alias || name) || expectedAttributes.indexOf(name) === -1) {
continue;
}
if (value === (node.injector as any).view.nodes[nodeIndex].instance[prop]) {
if (value === (node.injector as any).view.nodes[nodeIndex].instance[name]) {
return true;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import funcDirectiveIoParse from '../../common/func.directive-io-parse';
import { MockedDebugNode } from '../../mock-render/types';
import funcParseProviderTokensDirectives from '../func.parse-provider-tokens-directives';

Expand All @@ -23,9 +24,9 @@ const collectAttributesClassic = (node: MockedDebugNode): string[] => {
for (const key of funcGetPublicProviderKeys(node)) {
const [inputs, expectedAttributes] = funcParseInputsAndRequiresAttributes(node, key);
for (const input of inputs) {
const [prop, alias] = input.split(': ');
const attr = alias || prop;
if (expectedAttributes.indexOf(prop) !== -1 && result.indexOf(attr) === -1) {
const { name, alias } = funcDirectiveIoParse(input);
const attr = alias || name;
if (expectedAttributes.indexOf(name) !== -1 && result.indexOf(attr) === -1) {
result.push(attr);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { DirectiveIo } from '../../common/core.types';
import { MockedDebugNode } from '../../mock-render/types';
import funcParseProviderTokensDirectives from '../func.parse-provider-tokens-directives';

export default (node: MockedDebugNode, key: string): [string[], string[], number] => {
export default (node: MockedDebugNode, key: string): [Array<DirectiveIo>, string[], number] => {
const config = (node.injector as any).elDef.element.publicProviders[key];
const token = config.provider.value;
if (!token) {
Expand Down
6 changes: 3 additions & 3 deletions libs/ng-mocks/src/lib/mock-helper/func.get-from-node-scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const detectGatherFlag = (gather: boolean, el: DebugNode | null, node: any): boo
return false;
};

const isNotObject = (node: any): boolean => !node || typeof node !== 'object';
const isNotObject = <T>(node: T): boolean => !node || typeof node !== 'object';

const shouldBeScanned = (scanned: any[], node: any): boolean => scanned.indexOf(node) === -1 && Array.isArray(node);

Expand All @@ -51,7 +51,7 @@ const scan = <T>(
}: {
el: DebugNode | null;
nodes: any[];
normalize: (item: any) => any;
normalize: (item: T) => T;
proto: AnyType<T>;
result: T[];
},
Expand All @@ -72,7 +72,7 @@ const scan = <T>(
continue;
}

if (shouldBeScanned(scanned, node)) {
if (shouldBeScanned(scanned, node) && Array.isArray(node)) {
scan({ result, el, nodes: node, normalize, proto }, gather, scanned);
}

Expand Down
10 changes: 6 additions & 4 deletions libs/ng-mocks/src/lib/mock-helper/mock-helper.attributes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { DirectiveIo } from '../common/core.types';
import funcDirectiveIoParse from '../common/func.directive-io-parse';
import { MockedDebugElement } from '../mock-render/types';

import mockHelperFind from './find/mock-helper.find';
Expand All @@ -13,11 +15,11 @@ const parseArgs = (args: any[]): [MockedDebugElement | null | undefined, string,
args.length === 3 ? args[2] : defaultNotFoundValue,
];

const attrMatches = (attribute: string, selector: string): string | undefined => {
const [prop, alias = ''] = attribute.split(':', 2).map(v => v.trim());
const attrMatches = (attribute: DirectiveIo, selector: string): string | undefined => {
const { name, alias = '' } = funcDirectiveIoParse(attribute);

if ((!alias && prop === selector) || (!!alias && alias === selector)) {
return prop;
if ((!alias && name === selector) || (!!alias && alias === selector)) {
return name;
}

return undefined;
Expand Down
10 changes: 6 additions & 4 deletions libs/ng-mocks/src/lib/mock-render/func.generate-template.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import coreReflectPipeResolve from '../common/core.reflect.pipe-resolve';
import { DirectiveIo } from '../common/core.types';
import funcDirectiveIoParse from '../common/func.directive-io-parse';
import { isNgDef } from '../common/func.is-ng-def';

const generateTemplateAttrWrap = (prop: string, type: 'i' | 'o') => (type === 'i' ? `[${prop}]` : `(${prop})`);
Expand All @@ -11,7 +13,7 @@ const generateTemplateAttrWithParams = (prop: string, type: 'i' | 'o'): string =
return tpl;
};

const generateTemplateAttr = (bindings: null | undefined | any[], attr: any, type: 'i' | 'o') => {
const generateTemplateAttr = (bindings: null | undefined | any[], attr: Array<DirectiveIo>, type: 'i' | 'o') => {
// unprovided params for inputs should render empty placeholders
if (!bindings && type === 'o') {
return '';
Expand All @@ -20,9 +22,9 @@ const generateTemplateAttr = (bindings: null | undefined | any[], attr: any, typ
let mockTemplate = '';
const keys = bindings ?? attr;
for (const definition of attr) {
const [property, alias] = definition.split(': ');
mockTemplate +=
keys.indexOf(alias || property) === -1 ? '' : generateTemplateAttrWithParams(alias || property, type);
const { name, alias } = funcDirectiveIoParse(definition);

mockTemplate += keys.indexOf(alias || name) === -1 ? '' : generateTemplateAttrWithParams(alias || name, type);
}

return mockTemplate;
Expand Down
6 changes: 3 additions & 3 deletions libs/ng-mocks/src/lib/mock/decorate-declaration.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, Directive, NgModule, ViewChild } from '@angular/core';

import CoreDefStack from '../common/core.def-stack';
import { AnyType } from '../common/core.types';
import { AnyType, DirectiveIo } from '../common/core.types';
import decorateInputs from '../common/decorate.inputs';
import decorateMock from '../common/decorate.mock';
import decorateOutputs from '../common/decorate.outputs';
Expand All @@ -17,8 +17,8 @@ import toExistingProvider from './to-existing-provider';
const buildConfig = (
source: AnyType<any>,
meta: {
inputs?: string[];
outputs?: string[];
inputs?: Array<DirectiveIo>;
outputs?: Array<DirectiveIo>;
providers?: NgModule['providers'];
queries?: Record<string, ViewChild>;
},
Expand Down
12 changes: 9 additions & 3 deletions libs/ng-mocks/src/lib/resolve/collect-declarations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,18 +177,24 @@ describe('collect-declarations', () => {
propDecorators: {
prop: [
{
args: [],
args: [{ alias: 'alias', required: true }],
type: { prototype: { ngMetadataName: 'Input' } },
},
{
args: [],
args: [{ alias: 'alias', required: true }],
type: { prototype: { ngMetadataName: 'Input' } },
},
],
},
});

expect(actual.inputs).toEqual(['prop']);
expect(actual.inputs).toEqual([
{
name: 'prop',
alias: 'alias',
required: true,
},
]);
});

it('skips output duplicates', () => {
Expand Down
Loading

0 comments on commit f956242

Please sign in to comment.