Skip to content

Commit

Permalink
V2 (#1)
Browse files Browse the repository at this point in the history
* feat: emit focus event before checking "blur" emission

* feat: misc enhancements
  • Loading branch information
dmitry-stepanenko committed Sep 3, 2022
1 parent 00953c7 commit 3417a64
Show file tree
Hide file tree
Showing 47 changed files with 49,787 additions and 19,544 deletions.
9 changes: 5 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: on-pull-request

on:
pull_request:
push:
branches:
- 'master'
Expand All @@ -19,11 +20,11 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Check Formatting
run: npm run format:check
- name: Integration Test
run: git fetch origin master && npm run format:check -- --base=origin/master
- name: Integration Test (Jest)
run: npm run test
- name: Test Package
run: npm run test-package
- name: Integration Test (Karma)
run: npx nx run integration:test-karma
- name: Build App
run: npm run build -- --prod
- name: Build Package
Expand Down
14 changes: 7 additions & 7 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"recommendations": [
"angular.ng-template",
"nrwl.angular-console",
"esbenp.prettier-vscode",
"firsttris.vscode-jest-runner",
"dbaeumer.vscode-eslint"
]
"recommendations": [
"angular.ng-template",
"nrwl.angular-console",
"esbenp.prettier-vscode",
"firsttris.vscode-jest-runner",
"dbaeumer.vscode-eslint"
]
}
80 changes: 73 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,19 @@
<img src="https://img.shields.io/npm/v/ngx-cva-test-suite.svg?logo=npm&logoColor=fff&label=NPM+package&color=limegreen" alt="NPM package" />
</a>

`ngx-cva-test-suite` provides extensive set of test cases, ensuring your custom controls behave as intended.
`ngx-cva-test-suite` provides an extensive set of test cases, ensuring your custom controls behave as intended. Package is designed and tested to work properly with both **Jest** and **Jasmine** test runners.

It provides various configurations, that allows even the most non-standard components to be properly tested.

Among the main features:

- ensures the correct amount of calls for the `onChange` function _(incorrect usage may result in extra emissions of `valueChanges` of formControl)_
- ensures correct triggering of `onTouched` function _(is needed for `touched` state of the control and `updateOn: 'blur'` [strategy](https://angular.io/api/forms/AbstractControl#updateOn) to function properly)_
- ensures that no extra emissions are present when control is disabled
- checks for control to be resettable using `AbstractControl.reset()`

In the repository you can also [find few simple CVA components](apps/integration/src/app/controls), that are configured properly along with `ngx-cva-test-suite` setup for them.

## Installation

```
Expand Down Expand Up @@ -37,6 +46,53 @@ runValueAccessorTests({
});
```

## Using host template

This type of configuration might become handy, if your CVA component relies on projected content or specific layout to function correctly. A good example of such would be a select component, that gets it's options as projected content.

```typescript
import { runValueAccessorTests } from 'ngx-cva-test-suite';
import { Component, ViewChild } from '@angular/core';

import { CustomCheckboxControlValueAccessor } from './support/standard-value-accessors-directives';

@Component({
template: `
<app-select>
<app-select-option [value]="1">Opt 1</app-select-option>
<app-select-option [value]="2">Opt 2</app-select-option>
<app-select-option [value]="3">Opt 3</app-select-option>
</app-select>
`,
})
export class SelectWrapperComponent {
@ViewChild(AppSelectComponent) ctrl: AppSelectComponent;
}

runValueAccessorTests<AppSelectComponent, SelectWrapperComponent>({
// <= if host template is used, it should be marked explicitly as a type
component: AppSelectComponent, // <= using actual AppSelectComponent as a test target
testModuleMetadata: {
declarations: [SelectWrapperComponent],
imports: [AppSelectModule], // <= importing the module for app-select
},
hostTemplate: {
// specify that "AppSelectComponent" should not be tested directly
hostComponent: SelectWrapperComponent,
// specify the way to access "AppSelectComponent" from the host template
getTestingComponent: (fixture) => fixture.componentInstance.ctrl,
},
supportsOnBlur: false,
internalValueChangeSetter: (fixture, value) => {
// "setValue" is a function that is being called
// when user selects any "app-select-option"
fixture.componentInstance.ctrl.setValue(value, true);
},
getComponentValue: (fixture) => fixture.componentInstance.ctrl.value,
getValues: () => [1, 2, 3], // <= setting the same values as select options in host template
});
```

## Config

### interface CVATestConfig<T extends CVAComponentType, H = T>
Expand Down Expand Up @@ -164,6 +220,14 @@ If set to true, `ControlValueAccessor.setDisabledState()` function will not be c
Test suite will automatically detect whether it's Jest or Jasmine environment. If needed, this can be overriden
#### excludeSteps?: CVATestSteps[];
List of steps to be excluded from execution. Cannot be specified along with `includeSteps`
#### includeSteps?: CVATestSteps[];
List of steps to be included in execution. Cannot be specified along with `excludeSteps`
### interface HostTemplate<T, H>
#### hostComponent: Type<any>
Expand All @@ -174,19 +238,21 @@ Wrapper, that hosts testing component. For example, to test `app-select-componen
@Component({
selector: 'app-test-component-wrapper',
template: `
<app-select label="Label Value">
<app-select-item [value]="1" label="Opt 1"></app-select-item>
<app-select-item [value]="2" label="Opt 2"></app-select-item>
<app-select-item [value]="3" label="Opt 3"></app-select-item>
<app-select label="Label Value" #ctrl>
<app-select-option [value]="1" label="Opt 1"></app-select-option>
<app-select-option [value]="2" label="Opt 2"></app-select-option>
<app-select-option [value]="3" label="Opt 3"></app-select-option>
</app-select>
`,
})
class TestWrapperComponent {}
class TestWrapperComponent {
@ViewChild('ctrl') ctrl: AppSelectComponent;
}
```
#### getTestingComponent: (fixture: ComponentFixture<H>) => T
Getter for the actual component that is being tested
Using the hostComponent above, the following function should be used:
`(fixture) => fixture.debugElement.children[0].componentInstance;`
`(fixture) => fixture.componentInstance.ctrl;`
9 changes: 9 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@
"jestConfig": "apps/integration/jest.config.js",
"passWithNoTests": true
}
},
"test-karma": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/integration/src/karma-test.ts",
"tsConfig": "apps/integration/tsconfig.spec-karma.json",
"karmaConfig": "apps/integration/karma.conf.js",
"polyfills": "apps/integration/src/polyfills.ts"
}
}
}
},
Expand Down
30 changes: 15 additions & 15 deletions apps/integration-e2e/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
{
"extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["src/plugins/index.js"],
"rules": {
"@typescript-eslint/no-var-requires": "off",
"no-undef": "off"
}
}
]
"extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["src/plugins/index.js"],
"rules": {
"@typescript-eslint/no-var-requires": "off",
"no-undef": "off"
}
}
]
}
20 changes: 10 additions & 10 deletions apps/integration-e2e/cypress.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"fileServerFolder": ".",
"fixturesFolder": "./src/fixtures",
"integrationFolder": "./src/integration",
"modifyObstructiveCode": false,
"supportFile": "./src/support/index.ts",
"pluginsFile": false,
"video": true,
"videosFolder": "../../dist/cypress/apps/integration-e2e/videos",
"screenshotsFolder": "../../dist/cypress/apps/integration-e2e/screenshots",
"chromeWebSecurity": false
"fileServerFolder": ".",
"fixturesFolder": "./src/fixtures",
"integrationFolder": "./src/integration",
"modifyObstructiveCode": false,
"supportFile": "./src/support/index.ts",
"pluginsFile": false,
"video": true,
"videosFolder": "../../dist/cypress/apps/integration-e2e/videos",
"screenshotsFolder": "../../dist/cypress/apps/integration-e2e/screenshots",
"chromeWebSecurity": false
}
4 changes: 2 additions & 2 deletions apps/integration-e2e/src/fixtures/example.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io"
"name": "Using fixtures to represent data",
"email": "hello@cypress.io"
}
34 changes: 17 additions & 17 deletions apps/integration-e2e/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"sourceMap": false,
"outDir": "../../dist/out-tsc",
"allowJs": true,
"types": ["cypress", "node"],
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.js"],
"angularCompilerOptions": {
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"sourceMap": false,
"outDir": "../../dist/out-tsc",
"allowJs": true,
"types": ["cypress", "node"],
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.js"],
"angularCompilerOptions": {
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
63 changes: 32 additions & 31 deletions apps/integration/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts"],
"extends": ["plugin:@nrwl/nx/angular", "plugin:@angular-eslint/template/process-inline-templates"],
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "lib",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "lib",
"style": "kebab-case"
}
]
}
},
{
"files": ["*.html"],
"extends": ["plugin:@nrwl/nx/angular-template"],
"rules": {}
}
]
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts"],
"extends": ["plugin:@nrwl/nx/angular", "plugin:@angular-eslint/template/process-inline-templates"],
"rules": {
"@typescript-eslint/no-non-null-assertion": "off",
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "lib",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "lib",
"style": "kebab-case"
}
]
}
},
{
"files": ["*.html"],
"extends": ["plugin:@nrwl/nx/angular-template"],
"rules": {}
}
]
}
16 changes: 16 additions & 0 deletions apps/integration/karma.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html

const { join } = require('path');
const getBaseKarmaConfig = require('../../karma.conf');

module.exports = function (config) {
const baseConfig = getBaseKarmaConfig();
config.set({
...baseConfig,
coverageIstanbulReporter: {
...baseConfig.coverageIstanbulReporter,
dir: join(__dirname, '../../coverage/apps/docs/'),
},
});
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
input {
padding: 0.2em 0.5em;
padding: 0.2em 0.5em;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export class CounterControlComponent implements ControlValueAccessor {
disabled = false;
value = 0;

private onTouched: () => void;
private onChange: (value: number) => void;
protected onTouched: () => void;
protected onChange: (value: number) => void;

up() {
this.setValue(this.value + 1, true);
Expand All @@ -48,7 +48,7 @@ export class CounterControlComponent implements ControlValueAccessor {
this.setValue(value, false);
}

private setValue(value: number, emitEvent: boolean) {
protected setValue(value: number, emitEvent: boolean) {
const parsed = parseInt(value as any);
this.value = isNaN(parsed) ? 0 : parsed;
if (emitEvent && this.onChange) {
Expand Down
Loading

0 comments on commit 3417a64

Please sign in to comment.