From ac40000dfa6665468e24e22607351e90c3805501 Mon Sep 17 00:00:00 2001 From: Yoeri Nijs Date: Fri, 2 Apr 2021 13:57:48 +0200 Subject: [PATCH] Feature: toBePartial matcher (#423) * feature: toBePartial matcher * Added matcher to changelog as unreleased * Added some method documentation * Use spread operater to be sure that object is a copy * Refactored toBePartial matcher a bit, added jasmine support and updated changelog * Review comment: object instead of any --- README.md | 1 + .../spectator/jest/src/lib/matchers-types.ts | 2 + .../jest/test/matchers/matchers.spec.ts | 24 ++++++- projects/spectator/src/lib/matchers-types.ts | 2 + projects/spectator/src/lib/matchers.ts | 30 ++++++++ .../matcher-enhancements.component.spec.ts | 41 ----------- .../matcher-enhancements.component.spec.ts | 68 +++++++++++++++++++ .../matcher-enhancements.component.ts | 9 ++- 8 files changed, 134 insertions(+), 43 deletions(-) delete mode 100644 projects/spectator/test/matcher-enhancements/matcher-enhancements.component.spec.ts create mode 100644 projects/spectator/test/matchers/matcher-enhancements.component.spec.ts rename projects/spectator/test/{matcher-enhancements => matchers}/matcher-enhancements.component.ts (73%) diff --git a/README.md b/README.md index 6f8833c4..b1b6a137 100644 --- a/README.md +++ b/README.md @@ -1111,6 +1111,7 @@ expect('element').toBeSelected(); expect('element').toBeVisible(); expect('input').toBeFocused(); expect('div').toBeMatchedBy('.js-something'); +expect(spectator.component.object).toBePartial({ aProperty: 'aValue' }); expect('div').toHaveDescendant('.child'); expect('div').toHaveDescendantWithText({selector: '.child', text: 'text'}); ``` diff --git a/projects/spectator/jest/src/lib/matchers-types.ts b/projects/spectator/jest/src/lib/matchers-types.ts index f55d21ce..1e467d37 100644 --- a/projects/spectator/jest/src/lib/matchers-types.ts +++ b/projects/spectator/jest/src/lib/matchers-types.ts @@ -34,6 +34,8 @@ declare namespace jest { toBeEmpty(): boolean; + toBePartial(partial: object): boolean; + toBeHidden(): boolean; toBeSelected(): boolean; diff --git a/projects/spectator/jest/test/matchers/matchers.spec.ts b/projects/spectator/jest/test/matchers/matchers.spec.ts index 44da9e26..f7f5e8c9 100644 --- a/projects/spectator/jest/test/matchers/matchers.spec.ts +++ b/projects/spectator/jest/test/matchers/matchers.spec.ts @@ -1,7 +1,12 @@ -import { toBeVisible } from '@ngneat/spectator'; +import { toBeVisible, toBePartial } from '@ngneat/spectator'; import { createComponentFactory, Spectator } from '@ngneat/spectator/jest'; import { Component } from '@angular/core'; +interface Dummy { + lorem: string; + ipsum: string; +} + @Component({ template: ` @@ -82,4 +87,21 @@ describe('Matchers', () => { expect('#parent-visibility-hidden').toBeHidden(); }); }); + + describe('toBePartial', () => { + it('should return true when expected is partial of actual', () => { + const actual: Dummy = { lorem: 'first', ipsum: 'second' }; + expect(actual).toBePartial({ lorem: 'first' }); + }); + + it('should return true when expected is same as actual', () => { + const actual: Dummy = { lorem: 'first', ipsum: 'second' }; + expect(actual).toBePartial({...actual}); + }); + + it('should return false when expected is not partial of actual', () => { + const actual: Dummy = { lorem: 'first', ipsum: 'second' }; + expect(actual).not.toBePartial({ lorem: 'second' }); + }); + }); }); diff --git a/projects/spectator/src/lib/matchers-types.ts b/projects/spectator/src/lib/matchers-types.ts index d42a4cd2..8ce6c4a8 100644 --- a/projects/spectator/src/lib/matchers-types.ts +++ b/projects/spectator/src/lib/matchers-types.ts @@ -34,6 +34,8 @@ declare namespace jasmine { toBeEmpty(): boolean; + toBePartial(partial: object): boolean; + toBeHidden(): boolean; toBeSelected(): boolean; diff --git a/projects/spectator/src/lib/matchers.ts b/projects/spectator/src/lib/matchers.ts index 5ce5b8a2..68d16a57 100644 --- a/projects/spectator/src/lib/matchers.ts +++ b/projects/spectator/src/lib/matchers.ts @@ -339,6 +339,36 @@ export const toBeEmpty = comparator(el => { return { pass, message }; }); +/** + * Verify if an object has some expected properties. + * + * const actual = { lorem: 'first', ipsum: 'second' }; + * expect(actual).toBePartial({ lorem: 'first' }); + */ +export const toBePartial = comparator((actual, expected) => { + const mapToPropsAndValues = (values: any[], properties: any[]) => { + return properties.map(prop => { + return { + name: prop, + value: values[prop], + type: typeof values[prop] + }; + }); + }; + const actualProps = Object.getOwnPropertyNames(actual); + const actualPropsAndValues = mapToPropsAndValues(actual, actualProps); + + const expectedProps = Object.getOwnPropertyNames(expected); + const expectedPropsAndValues = mapToPropsAndValues(expected, expectedProps); + + const pass = expectedProps.every(expectedProp => actual[expectedProp] === expected[expectedProp]); + const message = () => + `Expected element${pass ? ' not' : ''} to contain properties: ${JSON.stringify(expectedPropsAndValues)}.` + .concat(` Actual properties: ${JSON.stringify(actualPropsAndValues)}`); + + return { pass, message }; +}); + /** * Hidden elements are elements that have: * 1. Display property set to "none" diff --git a/projects/spectator/test/matcher-enhancements/matcher-enhancements.component.spec.ts b/projects/spectator/test/matcher-enhancements/matcher-enhancements.component.spec.ts deleted file mode 100644 index 330010dd..00000000 --- a/projects/spectator/test/matcher-enhancements/matcher-enhancements.component.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Spectator, createComponentFactory } from '@ngneat/spectator'; - -import { MatcherEnhancementsComponent } from './matcher-enhancements.component'; - -describe('Matcher Enchancements Test', () => { - let spectator: Spectator; - const createComponent = createComponentFactory({ - component: MatcherEnhancementsComponent - }); - - beforeEach(() => (spectator = createComponent())); - - it('should match mulitple elements with different text', () => { - const el = spectator.query('.text-check'); - expect(el).toHaveText(['It should', 'different text']); - expect(el).toContainText(['It should', 'different text']); - expect(el).toHaveExactText(['It should have', 'Some different text']); - }); - - it('should match multiple inputs with different values', () => { - const inputs = spectator.queryAll('input.sample'); - expect(inputs).toHaveValue(['test1', 'test2']); - expect(inputs).toContainValue(['test1', 'test2']); - }); - - it('should match multiple classes on an element', () => { - expect(spectator.query('#multi-class')).toHaveClass(['one-class', 'two-class']); - }); - - it('should match attributes with object syntax', () => { - expect(spectator.query('#attr-check')).toHaveAttribute({ label: 'test label' }); - }); - - it('should match properties with object syntax', () => { - expect(spectator.query('.checkbox')).toHaveProperty({ checked: true }); - }); - - it('should match partial properties with object syntax', () => { - expect(spectator.query('img')).toContainProperty({ src: 'assets/myimg.jpg' }); - }); -}); diff --git a/projects/spectator/test/matchers/matcher-enhancements.component.spec.ts b/projects/spectator/test/matchers/matcher-enhancements.component.spec.ts new file mode 100644 index 00000000..21d0d5c8 --- /dev/null +++ b/projects/spectator/test/matchers/matcher-enhancements.component.spec.ts @@ -0,0 +1,68 @@ +import { Spectator, createComponentFactory } from '@ngneat/spectator'; + +import { MatcherEnhancementsComponent } from './matcher-enhancements.component'; + +describe('Matcher enhancements', () => { + let spectator: Spectator; + const createComponent = createComponentFactory({ + component: MatcherEnhancementsComponent + }); + + beforeEach(() => spectator = createComponent()); + + describe('Text', () => { + it('should match mulitple elements with different text', () => { + const el = spectator.query('.text-check'); + expect(el).toHaveText(['It should', 'different text']); + expect(el).toContainText(['It should', 'different text']); + expect(el).toHaveExactText(['It should have', 'Some different text']); + }); + }); + + describe('Value', () => { + it('should match multiple inputs with different values', () => { + const inputs = spectator.queryAll('input.sample'); + expect(inputs).toHaveValue(['test1', 'test2']); + expect(inputs).toContainValue(['test1', 'test2']); + }); + }); + + describe('Class', () => { + it('should match multiple classes on an element', () => { + expect('#multi-class').toHaveClass(['one-class', 'two-class']); + }); + }); + + describe('Attribute', () => { + it('should match attributes with object syntax', () => { + expect('#attr-check').toHaveAttribute({ label: 'test label' }); + }); + }); + + describe('Property', () => { + it('should match properties with object syntax', () => { + expect(spectator.query('.checkbox')).toHaveProperty({ checked: true }); + }); + + it('should match partial properties with object syntax', () => { + expect('img').toContainProperty({ src: 'assets/myimg.jpg' }); + }); + }); + + describe('Partial', () => { + it('should return true when expected is partial of actual', () => { + expect(spectator.component.dummyValue).toBePartial({ label: 'this is a dummy value' }); + expect(spectator.component.dummyValue).toBePartial({ active: true }); + }); + + it('should return true when expected is same as actual', () => { + expect(spectator.component.dummyValue).toBePartial({...spectator.component.dummyValue}); + }); + + it('should return false when expected is not partial of actual', () => { + expect(spectator.component.dummyValue).not.toBePartial({ unknown: 'property' }); + expect(spectator.component.dummyValue).not.toBePartial({ label: 'this is another dummy value' }); + expect(spectator.component.dummyValue).not.toBePartial({ active: false }); + }); + }); +}); diff --git a/projects/spectator/test/matcher-enhancements/matcher-enhancements.component.ts b/projects/spectator/test/matchers/matcher-enhancements.component.ts similarity index 73% rename from projects/spectator/test/matcher-enhancements/matcher-enhancements.component.ts rename to projects/spectator/test/matchers/matcher-enhancements.component.ts index b91c9be5..87390b15 100644 --- a/projects/spectator/test/matcher-enhancements/matcher-enhancements.component.ts +++ b/projects/spectator/test/matchers/matcher-enhancements.component.ts @@ -1,5 +1,10 @@ import { Component } from '@angular/core'; +export interface Dummy { + label: string; + active: boolean; +} + @Component({ selector: 'matcher-enhancements', template: ` @@ -13,4 +18,6 @@ import { Component } from '@angular/core'; ` }) -export class MatcherEnhancementsComponent {} +export class MatcherEnhancementsComponent { + public dummyValue: Dummy = { label: 'this is a dummy value', active: true }; +}