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
1 change: 1 addition & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@
"buildTarget": "igniteui-angular-elements:build:development,test",
"tsConfig": "projects/igniteui-angular-elements/tsconfig.spec.json",
"browsers": ["chromium"],
"isolate": true,
"coverageInclude": [
"projects/igniteui-angular-elements/src/**/*.ts",
"projects/igniteui-angular-elements/src/**/*.html"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Component, Input } from '@angular/core';
import {
FilteringExpressionsTree,
FilteringLogic,
IgxBooleanFilteringOperand,
IgxDateFilteringOperand,
IgxDateTimeFilteringOperand,
IgxGridComponent,
IgxNumberFilteringOperand,
IgxStringFilteringOperand,
IgxTimeFilteringOperand
} from 'igniteui-angular';
import { ComponentConfig } from './component-config';
import { createIgxCustomElement, withRegister } from './create-custom-element';
import { ComponentRefKey, IgcNgElement } from './custom-strategy';
import { injector } from '../utils/injector-ref';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';

@Component({
selector: 'igx-custom-element-test',
template: `{{ value }}`,
standalone: true
})
class CustomElementTestComponent {
@Input()
public value: string;

@Input()
public showUI: boolean;

public counter = 0;
public readonlyProp = 'readonly';
public writableProp = 'writable';

public increment(by: number) {
this.counter += by;
return this.counter;
}
}

const TEST_TAG = 'igc-custom-element-test';

const createConfig = (overrides: Partial<ComponentConfig> = {}): ComponentConfig[] => [{
component: CustomElementTestComponent,
selector: TEST_TAG,
parents: [],
contentQueries: [],
additionalProperties: [
{ name: 'readonlyProp' },
{ name: 'writableProp', writable: true }
],
methods: ['increment'],
...overrides
}];

describe('createIgxCustomElement', () => {
const elementCtor = createIgxCustomElement(CustomElementTestComponent, { injector, registerConfig: createConfig() });
if (!customElements.get(TEST_TAG)) {
customElements.define(TEST_TAG, elementCtor);
}

let element: IgcNgElement & CustomElementTestComponent;

beforeEach(() => {
element = document.createElement(TEST_TAG) as IgcNgElement & CustomElementTestComponent;
document.body.appendChild(element);
});

afterEach(() => {
element.remove();
vi.restoreAllMocks();
});

const getInstance = async () =>
(await element.ngElementStrategy[ComponentRefKey]).instance as CustomElementTestComponent;

it('should expose the configured selector as static tagName', () => {
expect(elementCtor.tagName).toBe(TEST_TAG);
expect(Object.getOwnPropertyDescriptor(elementCtor, 'tagName').writable).toBe(false);
});

it('should proxy configured methods to the component instance and schedule change detection', async () => {
const instance = await getInstance();
const notifySpy = vi.spyOn(element.ngElementStrategy, 'notifyChanges');

expect(element.increment(2)).toBe(2);
expect(element.increment(3)).toBe(5);

expect(instance.counter).toBe(5);
expect(notifySpy).toHaveBeenCalledTimes(2);
});

it('should expose additional properties as getters on the element', async () => {
const instance = await getInstance();

expect(element.readonlyProp).toBe('readonly');
instance.readonlyProp = 'changed';
expect(element.readonlyProp).toBe('changed');
});

it('should only allow setting writable additional properties', async () => {
const instance = await getInstance();
const setInputSpy = vi.spyOn(element.ngElementStrategy, 'setInputValue').mockImplementation(() => {});

element.writableProp = 'updated';
expect(setInputSpy).toHaveBeenCalledExactlyOnceWith('writableProp', 'updated');

expect(() => (element as any).readonlyProp = 'nope').toThrow(TypeError);
expect(instance.readonlyProp).toBe('readonly');
});

it('should throw for additional properties that clash with the element prototype', () => {
const config = createConfig({ additionalProperties: [{ name: 'connectedCallback' }] });

expect(() => createIgxCustomElement(CustomElementTestComponent, { injector, registerConfig: config }))
.toThrowError(`Potentially illegal property name connectedCallback defined for ${CustomElementTestComponent.name}`);
});

it('should add the deprecated getFilterFactory to IgxGridComponent elements', () => {
const config: ComponentConfig[] = [{
component: IgxGridComponent,
selector: 'igc-filter-factory-test-grid',
parents: [],
contentQueries: [],
additionalProperties: [],
methods: []
}];
const gridCtor = createIgxCustomElement(IgxGridComponent, { injector, registerConfig: config });
const otherCtor = createIgxCustomElement(CustomElementTestComponent, { injector, registerConfig: createConfig() });

expect(otherCtor.prototype.getFilterFactory).toBeUndefined();

const factory = gridCtor.prototype.getFilterFactory();
expect(factory.stringFilteringOperand).toBe(IgxStringFilteringOperand.instance());
expect(factory.numberFilteringOperand).toBe(IgxNumberFilteringOperand.instance());
expect(factory.timeFilteringOperand).toBe(IgxTimeFilteringOperand.instance());
expect(factory.dateTimeFilteringOperand).toBe(IgxDateTimeFilteringOperand.instance());
expect(factory.dateFilteringOperand).toBe(IgxDateFilteringOperand.instance());
expect(factory.booleanFilteringOperand).toBe(IgxBooleanFilteringOperand.instance());

const tree = factory.createFilteringExpressionTree(FilteringLogic.Or, 'Name');
expect(tree).toBeInstanceOf(FilteringExpressionsTree);
expect(tree.operator).toBe(FilteringLogic.Or);
expect(tree.fieldName).toBe('Name');
});
});

describe('withRegister', () => {
it('should attach a non-writable static register function', () => {
const ctor = createIgxCustomElement(CustomElementTestComponent, { injector, registerConfig: createConfig() });
const register = vi.fn();

const component = withRegister(ctor, register);
component.register();

expect(component).toBe(ctor);
expect(register).toHaveBeenCalledTimes(1);
expect(Object.getOwnPropertyDescriptor(component, 'register').writable).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { firstValueFrom, fromEvent } from 'rxjs';
import { ComponentRefKey, IgcNgElement } from './custom-strategy';
import { IgcColumnComponent, IgcTreeGridComponent } from './components';
import { defineComponents } from '../utils/register';
import { IgxTreeGridComponent } from '../lib/grids/tree-grid.component';
import { describe, it, expect } from 'vitest';

describe('Elements: deferred parent definition', () => {
it('should wait for the parent element to be defined before attaching to it', async () => {
// Relies on per-file isolation (`isolate: true` in angular.json) for a clean custom elements registry,
// otherwise other spec files sharing the same browser page may have already defined `igc-tree-grid`
expect(customElements.get('igc-tree-grid')).toBeUndefined();
defineComponents(IgcColumnComponent);

const container = document.createElement('div');
document.body.appendChild(container);
try {
container.innerHTML = `
<igc-tree-grid id="treeGrid">
<igc-column field="ID"></igc-column>
<igc-column field="Name"></igc-column>
</igc-tree-grid>`;
const treeGridEl = container.querySelector('#treeGrid');
const columnEls = Array.from(container.querySelectorAll<IgcNgElement>('igc-column'));

// columns connect first, but have to wait for their parent to be defined
let columnsResolved = false;
const columnRefs = Promise.all(columnEls.map(c => c.ngElementStrategy[ComponentRefKey]))
.then(refs => {
columnsResolved = true;
return refs;
});
await Promise.resolve();
expect(columnsResolved).toBe(false);

const childrenResolved = firstValueFrom(fromEvent(treeGridEl, 'childrenResolved'));
defineComponents(IgcTreeGridComponent);

const treeGrid = (await (treeGridEl as IgcNgElement).ngElementStrategy[ComponentRefKey]).instance as IgxTreeGridComponent;
const columns = (await columnRefs).map(ref => ref.instance);
await childrenResolved;

expect(treeGrid.columnList.toArray()).toEqual(columns);
expect(treeGrid.columns.map(c => c.field)).toEqual(['ID', 'Name']);
} finally {
container.remove();
}
});
});
Loading
Loading