Skip to content

Commit

Permalink
Feature: toBePartial matcher (#423)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
YoeriNijs authored Apr 2, 2021
1 parent 4300a56 commit ac40000
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 43 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'});
```
Expand Down
2 changes: 2 additions & 0 deletions projects/spectator/jest/src/lib/matchers-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ declare namespace jest {

toBeEmpty(): boolean;

toBePartial(partial: object): boolean;

toBeHidden(): boolean;

toBeSelected(): boolean;
Expand Down
24 changes: 23 additions & 1 deletion projects/spectator/jest/test/matchers/matchers.spec.ts
Original file line number Diff line number Diff line change
@@ -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: `
<div id="display-none" style="display:none">Hidden with display none</div>
Expand Down Expand Up @@ -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' });
});
});
});
2 changes: 2 additions & 0 deletions projects/spectator/src/lib/matchers-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ declare namespace jasmine {

toBeEmpty(): boolean;

toBePartial(partial: object): boolean;

toBeHidden(): boolean;

toBeSelected(): boolean;
Expand Down
30 changes: 30 additions & 0 deletions projects/spectator/src/lib/matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Spectator, createComponentFactory } from '@ngneat/spectator';

import { MatcherEnhancementsComponent } from './matcher-enhancements.component';

describe('Matcher enhancements', () => {
let spectator: Spectator<MatcherEnhancementsComponent>;
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 });
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { Component } from '@angular/core';

export interface Dummy {
label: string;
active: boolean;
}

@Component({
selector: 'matcher-enhancements',
template: `
Expand All @@ -13,4 +18,6 @@ import { Component } from '@angular/core';
<img src="http://localhost:8080/assets/myimg.jpg" />
`
})
export class MatcherEnhancementsComponent {}
export class MatcherEnhancementsComponent {
public dummyValue: Dummy = { label: 'this is a dummy value', active: true };
}

0 comments on commit ac40000

Please sign in to comment.