igniteui-angular-sovn
Version:
Ignite UI for Angular is a dependency-free Angular toolkit for building modern web apps
785 lines (632 loc) • 27.3 kB
text/typescript
import { configureTestSuite } from '../../test-utils/configure-suite';
import { TestBed, ComponentFixture, fakeAsync } from '@angular/core/testing';
import { IgxGridComponent } from './grid.component';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { IgxStringFilteringOperand } from '../../data-operations/filtering-condition';
import { cloneArray, resolveNestedPath } from '../../core/utils';
import { Component, DebugElement, ViewChild } from '@angular/core';
import { UIInteractions } from '../../test-utils/ui-interactions.spec';
import { GridFunctions } from '../../test-utils/grid-functions.spec';
import { IgxComboComponent } from '../../combo/public_api';
import { IGridEditEventArgs } from '../common/events';
import { SortingDirection } from '../../data-operations/sorting-strategy';
import { IgxColumnComponent } from '../public_api';
import { IgxCellEditorTemplateDirective, IgxCellTemplateDirective } from '../columns/templates.directive';
import { FormsModule } from '@angular/forms';
const first = <T>(array: T[]): T => array[0];
const DATA = [
{
id: 0,
user: {
name: {
first: 'John',
last: 'Doe'
},
email: 'johndoe@mail.com',
age: 30,
address: {
zip: 1000,
country: 'USA'
}
},
active: true
},
{
id: 1,
user: {
name: {
first: 'Jane',
last: 'Doe'
},
email: 'jane@gmail.com',
age: 23,
address: {
zip: 2000,
country: 'England'
}
},
active: true
},
{
id: 2,
user: {
name: {
first: 'Ivan',
last: 'Ivanov'
},
email: 'ivanko@po6ta.bg',
age: 33,
address: {
zip: 1700,
country: 'Bulgaria'
}
},
active: false
},
{
id: 3,
user: {
name: {
first: 'Bianka',
last: 'Bartosik'
},
email: 'bbb@gmail.pl',
age: 21,
address: {
zip: 6000,
country: 'Poland'
}
},
active: true
}
];
const LOCATIONS = [
{
id: 0,
shop: 'My Cool Market'
},
{
id: 1,
shop: 'MarMaMarket'
},
{
id: 2,
shop: 'Fun-Tasty Co.'
},
{
id: 3,
shop: 'MarMaMarket'
}
];
const DATA2 = [
{
id: 0,
productName: 'Chai',
locations: [{ ...LOCATIONS[2] }]
},
{
id: 1,
productName: 'Chang',
locations: [{ ...LOCATIONS[0] }, { ...LOCATIONS[1] }]
},
{
id: 2,
productName: 'Aniseed Syrup',
locations: [{ ...LOCATIONS[0] }, { ...LOCATIONS[1] }, { ...LOCATIONS[2] }]
},
{
id: 3,
productName: 'Uncle Bobs Organic Dried Pears',
locations: [{ ...LOCATIONS[2] }, { ...LOCATIONS[3] }]
},
];
@Component({
template: `<igx-grid></igx-grid>`,
standalone: true,
imports: [IgxGridComponent]
})
class NestedPropertiesGridComponent {
@ViewChild(IgxGridComponent, { read: IgxGridComponent })
public grid: IgxGridComponent;
}
@Component({
template: `<igx-grid #grid [autoGenerate]='false'>
<igx-column field='id' header='ID' dataType='number'></igx-column>
<igx-column field='user.name.first' header='First Name' editable='true' dataType='string'></igx-column>
<igx-column field='user.name.last' header='Last Name' editable='true' dataType='string'></igx-column>
<igx-column field='user.email' header='E-Mail' editable='true' dataType='string'></igx-column>
<igx-column field='user.age' header='Age' [sortable]="true" editable='true' dataType='number'></igx-column>
<igx-column field='user.address.zip' header='ZIP' editable='true' dataType='number'></igx-column>
<igx-column field='user.address.country' header='Country' editable='true' dataType='string'></igx-column>
<igx-column field='active' header='Active' editable='true' dataType='boolean'></igx-column>
</igx-grid>`,
standalone: true,
imports: [IgxGridComponent, IgxColumnComponent]
})
class NestedPropertiesGrid2Component {
@ViewChild('grid', { static: true, read: IgxGridComponent })
public grid: IgxGridComponent;
}
@Component({
template: `<igx-grid #grid [autoGenerate]="false">
<igx-column field='id' header='ID' dataType='number'></igx-column>
<igx-column field='productName' header='Product Name' [editable]="true" dataType='string'></igx-column>
<igx-column field='locations' header='Available At' [editable]="true" width='220px'>
<ng-template igxCell let-cell='cell'>
{{ parseArray(cell.value) }}
</ng-template>
<ng-template igxCellEditor let-cell='cell'>
<igx-combo type='line' [(ngModel)]='cell.editValue' [data]='locations' [displayKey]="'shop'" width='220px'></igx-combo>
</ng-template>
</igx-column>
</igx-grid>`,
standalone: true,
imports: [IgxGridComponent, IgxColumnComponent, IgxCellTemplateDirective, IgxCellEditorTemplateDirective, IgxComboComponent, FormsModule]
})
class NestedPropertyGridComponent {
@ViewChild('grid', { static: true, read: IgxGridComponent })
public grid: IgxGridComponent;
@ViewChild(IgxComboComponent, { read: IgxComboComponent })
public combo: IgxComboComponent;
public locations = LOCATIONS;
public parseArray(arr: { id: number; shop: string }[]): string {
return (arr || []).map((e) => e.shop).join(', ');
}
}
describe('Grid - nested data source properties #grid', () => {
const NAMES = 'John Jane Ivan Bianka'.split(' ');
const AGES = [30, 23, 33, 21];
describe('API', () => {
it('should correctly resolve key paths in nested data', () => {
expect(
DATA.map(record => resolveNestedPath(record, 'user.name.first'))
).toEqual(NAMES);
expect(
DATA.map(record => resolveNestedPath(record, 'user.age'))
).toEqual(AGES);
});
});
describe('Grid base cases', () => {
let fixture: ComponentFixture<NestedPropertiesGridComponent>;
let grid: IgxGridComponent;
const setupData = (data: Array<any>) => {
grid.autoGenerate = true;
grid.shouldGenerate = true;
grid.data = data;
fixture.detectChanges();
};
configureTestSuite((() => {
return TestBed.configureTestingModule({
imports: [NoopAnimationsModule, NestedPropertiesGridComponent]
});
}));
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(NestedPropertiesGridComponent);
fixture.detectChanges();
grid = fixture.componentInstance.grid;
}));
it('should support column API with complex field', () => {
setupData(DATA);
const column = grid.getColumnByName('user');
column.field = 'user.name.first';
fixture.detectChanges();
expect(grid.getColumnByName('user.name.first')).toBe(column);
});
it('should render the passed properties path', () => {
setupData(DATA);
const column = grid.getColumnByName('user');
column.field = 'user.name.first';
fixture.detectChanges();
expect(column.cells.map(cell => cell.value)).toEqual(NAMES);
});
it('should work with sorting', () => {
setupData(DATA);
const key = 'user.age';
const column = grid.getColumnByName('user');
column.field = key;
fixture.detectChanges();
grid.sort({ fieldName: key, dir: SortingDirection.Asc });
fixture.detectChanges();
expect(first(column.cells.map(cell => cell.value))).toEqual(21);
grid.sort({ fieldName: key, dir: SortingDirection.Desc });
fixture.detectChanges();
expect(first(column.cells.map(cell => cell.value))).toEqual(33);
});
it('should work with filtering', () => {
setupData(DATA);
const key = 'user.name.first';
const operand = IgxStringFilteringOperand.instance().condition('equals');
const column = grid.getColumnByName('user');
column.field = key;
fixture.detectChanges();
grid.filter(key, 'Jane', operand);
fixture.detectChanges();
expect(grid.dataView.length).toEqual(1);
expect(first(column.cells).value).toEqual('Jane');
});
it('should support copy/paste operations', () => {
setupData(DATA);
grid.getColumnByName('user').field = 'user.name.first';
fixture.detectChanges();
grid.setSelection({ columnStart: 'user.name.first', columnEnd: 'user.name.first', rowStart: 0, rowEnd: 0 });
fixture.detectChanges();
const selected = grid.getSelectedData();
expect(selected.length).toEqual(1);
expect(first(selected)['user.name.first']).toMatch('John');
});
it('should work with editing (cell)', () => {
const copiedData = cloneArray(DATA, true);
setupData(copiedData);
const key = 'user.name.first';
const column = grid.getColumnByName('user');
column.field = key;
grid.primaryKey = 'id';
fixture.detectChanges();
grid.updateCell('Anonymous', 0, key);
fixture.detectChanges();
expect(first(copiedData).user.name.first).toMatch('Anonymous');
});
it('should work with editing (row)', () => {
const copiedData = cloneArray(DATA, true);
setupData(copiedData);
grid.primaryKey = 'id';
grid.getColumnByName('user').field = 'user.name.first';
fixture.detectChanges();
grid.updateRow({ user: { name: { first: 'Updated!' } } }, 0);
fixture.detectChanges();
expect(first(copiedData).user.name.first).toMatch('Updated!');
});
});
});
// related to fix of issue #8343
describe('Grid nested data advanced editing #grid', () => {
let fixture: ComponentFixture<NestedPropertiesGrid2Component>;
let grid: IgxGridComponent;
let gridContent: DebugElement;
const setupData = (data: Array<any>, rowEditable = false) => {
grid.data = data;
if (rowEditable) {
grid.primaryKey = 'id';
grid.rowEditable = true;
}
fixture.detectChanges();
};
configureTestSuite((() => {
return TestBed.configureTestingModule({
imports: [NoopAnimationsModule, NestedPropertiesGrid2Component]
});
}));
beforeEach(() => {
fixture = TestBed.createComponent(NestedPropertiesGrid2Component);
fixture.detectChanges();
grid = fixture.componentInstance.grid;
gridContent = GridFunctions.getGridContent(fixture);
});
it('canceling the row editing should revert the uncommitted cell values', () => {
const copiedData = cloneArray(DATA, true);
setupData(copiedData, true);
const cell1 = grid.getCellByColumn(0, 'user.name.first');
const cell2 = grid.getCellByColumn(0, 'user.name.last');
const cell3 = grid.getCellByColumn(0, 'user.email');
UIInteractions.simulateDoubleClickAndSelectEvent(grid.gridAPI.get_cell_by_index(0, 'user.name.first'));
fixture.detectChanges();
expect(cell1.editMode).toBe(true);
cell1.editValue = 'Petar';
UIInteractions.triggerEventHandlerKeyDown('tab', gridContent);
fixture.detectChanges();
expect(cell1.editMode).toBe(false);
expect(cell2.editMode).toBe(true);
UIInteractions.triggerEventHandlerKeyDown('tab', gridContent);
fixture.detectChanges();
expect(cell2.editMode).toBe(false);
expect(cell3.editMode).toBe(true);
UIInteractions.triggerEventHandlerKeyDown('escape', gridContent);
fixture.detectChanges();
expect(cell3.editMode).toBe(false);
expect(cell1.value).toBeDefined(true);
expect(cell2.value).toBeDefined(true);
expect(cell3.value).toBeDefined(true);
// related to issue #0000, comment out the below line after fixing the issue
expect(first(copiedData).user.name.first).toMatch('John');
expect(first(copiedData).user.name.last).toMatch('Doe');
});
it('after updating a cell value the value in the previous cell should persist', () => {
const copiedData = cloneArray(DATA, true);
setupData(copiedData, true);
const cell1 = grid.getCellByColumn(0, 'user.name.first');
const cell2 = grid.getCellByColumn(0, 'user.name.last');
const cell3 = grid.getCellByColumn(0, 'user.email');
UIInteractions.simulateDoubleClickAndSelectEvent(grid.gridAPI.get_cell_by_index(0, 'user.name.last'));
fixture.detectChanges();
expect(cell2.editMode).toBe(true);
cell2.editValue = 'Petrov';
UIInteractions.triggerEventHandlerKeyDown('tab', gridContent);
fixture.detectChanges();
expect(cell2.editMode).toBe(false);
expect(cell3.editMode).toBe(true);
expect(cell1.value).toBeDefined(true);
});
it('updating values of multiple cells in a row should update the data correctly', () => {
const copiedData = cloneArray(DATA, true);
setupData(copiedData, true);
const cell1 = grid.getCellByColumn(0, 'user.name.first');
const cell2 = grid.getCellByColumn(0, 'user.name.last');
const cell3 = grid.getCellByColumn(0, 'user.email');
UIInteractions.simulateDoubleClickAndSelectEvent(grid.gridAPI.get_cell_by_index(0, 'user.name.first'));
fixture.detectChanges();
expect(cell1.editMode).toBe(true);
cell1.editValue = 'Petar';
UIInteractions.triggerEventHandlerKeyDown('tab', gridContent);
fixture.detectChanges();
expect(cell1.editMode).toBe(false);
expect(cell2.editMode).toBe(true);
cell2.editValue = 'Petrov';
UIInteractions.triggerEventHandlerKeyDown('tab', gridContent);
fixture.detectChanges();
expect(cell2.editMode).toBe(false);
expect(cell3.editMode).toBe(true);
cell3.editValue = 'ppetrov@email.com';
UIInteractions.triggerEventHandlerKeyDown('enter', gridContent);
fixture.detectChanges();
expect(cell1.value).toBeDefined(true);
expect(cell2.value).toBeDefined(true);
expect(cell3.value).toBeDefined(true);
expect(first(copiedData).user.name.last).toMatch('Petrov');
});
it('sorting the grid and modifying a cell within an unsorted column should not change the rows order', async () => {
const copiedData = cloneArray(DATA, true);
setupData(copiedData);
const header = GridFunctions.getColumnHeader('user.age', fixture);
UIInteractions.simulateClickAndSelectEvent(header);
expect(grid.headerGroupsList[0].isFiltered).toBeFalsy();
GridFunctions.verifyHeaderSortIndicator(header, false, false);
GridFunctions.clickHeaderSortIcon(header);
GridFunctions.clickHeaderSortIcon(header);
fixture.detectChanges();
GridFunctions.verifyHeaderSortIndicator(header, false, true);
const cell1 = grid.gridAPI.get_cell_by_index(0, 'user.address.zip');
expect(cell1.value).toEqual(1700);
UIInteractions.simulateDoubleClickAndSelectEvent(cell1);
fixture.detectChanges();
expect(cell1.editMode).toBe(true);
cell1.editValue = 1618;
UIInteractions.triggerEventHandlerKeyDown('enter', gridContent);
fixture.detectChanges();
await fixture.whenStable();
expect(grid.getRowByIndex(0).data.user.address.zip).toMatch('1618');
expect(copiedData[2].user.address.zip).toMatch('1618');
});
});
// related to issue #8343
describe('Edit cell with data of type Array #grid', () => {
let fixture: ComponentFixture<NestedPropertyGridComponent>;
let grid: IgxGridComponent;
let combo: IgxComboComponent;
let gridContent: DebugElement;
const setupData = (data: Array<any>, rowEditable = false) => {
grid.data = data;
if (rowEditable) {
grid.primaryKey = 'id';
grid.rowEditable = true;
}
fixture.detectChanges();
};
configureTestSuite((() => {
return TestBed.configureTestingModule({
imports: [NoopAnimationsModule, NestedPropertyGridComponent]
});
}));
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(NestedPropertyGridComponent);
fixture.detectChanges();
grid = fixture.componentInstance.grid;
gridContent = GridFunctions.getGridContent(fixture);
}));
it('igxGrid should emit the correct args when cell editing is cancelled', async () => {
const copiedData = cloneArray(DATA2, true);
setupData(copiedData);
spyOn(grid.cellEditEnter, 'emit').and.callThrough();
spyOn(grid.cellEditExit, 'emit').and.callThrough();
const cell = grid.getCellByColumn(2, 'locations');
let initialRowData = { ...cell.row.data };
UIInteractions.simulateDoubleClickAndSelectEvent(grid.gridAPI.get_cell_by_index(2, 'locations'));
fixture.detectChanges();
combo = fixture.componentInstance.combo;
fixture.detectChanges();
await fixture.whenStable();
const cellArgs: IGridEditEventArgs = {
rowID: cell.row.key,
primaryKey: cell.row.key,
cellID: cell.id,
rowData: initialRowData,
oldValue: initialRowData.locations,
cancel: false,
column: cell.column,
owner: grid,
event: jasmine.anything() as any,
valid: true
};
expect(grid.cellEditEnter.emit).toHaveBeenCalledTimes(1);
expect(grid.cellEditEnter.emit).toHaveBeenCalledWith(cellArgs);
expect(cell.editMode).toBeTruthy();
expect(combo.selection.length).toEqual(3);
combo.deselect([cell.editValue[0], cell.editValue[1]]);
fixture.detectChanges();
await fixture.whenStable();
expect(grid.data[2].locations.length).toEqual(3);
expect(cell.editValue.length).toEqual(1);
cellArgs.newValue = [cell.editValue[0]];
UIInteractions.triggerEventHandlerKeyDown('escape', gridContent);
fixture.detectChanges();
await fixture.whenStable();
expect(cell.editMode).toBeFalsy();
initialRowData = { ...cell.row.data };
cellArgs.rowData = initialRowData;
expect(cellArgs.newValue.length).toEqual(1);
expect(cellArgs.oldValue.length).toEqual(3);
delete cellArgs.cancel;
expect(grid.cellEditExit.emit).toHaveBeenCalledTimes(1);
expect(grid.cellEditExit.emit).toHaveBeenCalledWith(cellArgs);
expect(copiedData[2].locations.length).toEqual(3);
});
it('igxGrid should emit the correct args when submitting the changes', async () => {
const copiedData = cloneArray(DATA2, true);
setupData(copiedData);
spyOn(grid.cellEditEnter, 'emit').and.callThrough();
spyOn(grid.cellEdit, 'emit').and.callThrough();
spyOn(grid.cellEditDone, 'emit').and.callThrough();
spyOn(grid.cellEditExit, 'emit').and.callThrough();
const cell = grid.getCellByColumn(2, 'locations');
let initialRowData = { ...cell.row.data };
UIInteractions.simulateDoubleClickAndSelectEvent(grid.gridAPI.get_cell_by_index(2, 'locations'));
fixture.detectChanges();
combo = fixture.componentInstance.combo;
fixture.detectChanges();
await fixture.whenStable();
const cellArgs: IGridEditEventArgs = {
rowID: cell.row.key,
primaryKey: cell.row.key,
cellID: cell.id,
rowData: initialRowData,
oldValue: initialRowData.locations,
cancel: false,
column: cell.column,
owner: grid,
event: jasmine.anything() as any,
valid: true
};
expect(grid.cellEditEnter.emit).toHaveBeenCalledTimes(1);
expect(grid.cellEditEnter.emit).toHaveBeenCalledWith(cellArgs);
expect(cell.editMode).toBeTruthy();
expect(combo.selection.length).toEqual(3);
combo.deselect([cell.editValue[0], cell.editValue[1]]);
fixture.detectChanges();
await fixture.whenStable();
expect(grid.data[2].locations.length).toEqual(3);
expect(cell.editValue.length).toEqual(1);
UIInteractions.triggerEventHandlerKeyDown('enter', gridContent);
fixture.detectChanges();
await fixture.whenStable();
expect(cell.editMode).toBeFalsy();
initialRowData = { ...cell.row.data };
cellArgs.rowData = initialRowData;
cellArgs.newValue = initialRowData.locations;
expect(cellArgs.newValue.length).toEqual(1);
expect(cellArgs.oldValue.length).toEqual(3);
expect(grid.cellEdit.emit).toHaveBeenCalledTimes(1);
expect(grid.cellEdit.emit).toHaveBeenCalledWith(cellArgs);
delete cellArgs.cancel;
expect(grid.cellEditDone.emit).toHaveBeenCalledTimes(1);
expect(grid.cellEditDone.emit).toHaveBeenCalledWith(cellArgs);
expect(grid.cellEditExit.emit).toHaveBeenCalledTimes(1);
expect(grid.cellEditExit.emit).toHaveBeenCalledWith(cellArgs);
expect(copiedData[2].locations.length).toEqual(1);
});
it('igxGrid should emit the correct args when row editing is cancelled', async () => {
const copiedData = cloneArray(DATA2, true);
setupData(copiedData, true);
spyOn(grid.rowEditEnter, 'emit').and.callThrough();
spyOn(grid.rowEditExit, 'emit').and.callThrough();
const cell = grid.getCellByColumn(2, 'locations');
const row = grid.gridAPI.get_row_by_index(2);
let initialRowData = { ...cell.row.data };
UIInteractions.simulateDoubleClickAndSelectEvent(grid.gridAPI.get_cell_by_index(2, 'locations'));
fixture.detectChanges();
combo = fixture.componentInstance.combo;
fixture.detectChanges();
await fixture.whenStable();
// TODO ROW addRow
const rowArgs: IGridEditEventArgs = {
rowID: row.key,
primaryKey: cell.row.key,
rowData: initialRowData,
oldValue: row.data,
owner: grid,
isAddRow: row.addRowUI,
cancel: false,
event: jasmine.anything() as any,
valid: true
};
expect(grid.rowEditEnter.emit).toHaveBeenCalledTimes(1);
expect(grid.rowEditEnter.emit).toHaveBeenCalledWith(rowArgs);
expect(row.inEditMode).toBeTruthy();
expect(cell.editMode).toBeTruthy();
expect(combo.selection.length).toEqual(3);
combo.deselect([cell.editValue[0], cell.editValue[1]]);
fixture.detectChanges();
await fixture.whenStable();
expect(grid.data[2].locations.length).toEqual(3);
expect(cell.editValue.length).toEqual(1);
grid.gridAPI.crudService.endEdit(false);
fixture.detectChanges();
await fixture.whenStable();
rowArgs.event = undefined;
expect(row.inEditMode).toBeFalsy();
expect(cell.editMode).toBeFalsy();
initialRowData = { ...cell.row.data };
rowArgs.newValue = initialRowData;
delete rowArgs.cancel;
expect(rowArgs.newValue.locations.length).toEqual(3);
expect(rowArgs.oldValue.locations.length).toEqual(3);
expect(grid.rowEditExit.emit).toHaveBeenCalledTimes(1);
expect(grid.rowEditExit.emit).toHaveBeenCalledWith(rowArgs);
expect(copiedData[2].locations.length).toEqual(3);
});
it('igxGrid should emit the correct args when submitting the row changes', async () => {
const copiedData = cloneArray(DATA2, true);
setupData(copiedData, true);
spyOn(grid.rowEditEnter, 'emit').and.callThrough();
spyOn(grid.rowEdit, 'emit').and.callThrough();
spyOn(grid.rowEditDone, 'emit').and.callThrough();
spyOn(grid.rowEditExit, 'emit').and.callThrough();
const cell = grid.getCellByColumn(2, 'locations');
const row = grid.gridAPI.get_row_by_index(2);
let initialRowData = { ...cell.row.data };
UIInteractions.simulateDoubleClickAndSelectEvent(grid.gridAPI.get_cell_by_index(2, 'locations'));
fixture.detectChanges();
combo = fixture.componentInstance.combo;
fixture.detectChanges();
await fixture.whenStable();
// TODO ROW addRow
const rowArgs: IGridEditEventArgs = {
rowID: row.key,
primaryKey: cell.row.key,
rowData: initialRowData,
oldValue: row.data,
owner: grid,
isAddRow: row.addRowUI,
cancel: false,
event: jasmine.anything() as any,
valid: true
};
expect(grid.rowEditEnter.emit).toHaveBeenCalledTimes(1);
expect(grid.rowEditEnter.emit).toHaveBeenCalledWith(rowArgs);
expect(row.inEditMode).toBeTruthy();
expect(cell.editMode).toBeTruthy();
expect(combo.selection.length).toEqual(3);
combo.deselect([cell.editValue[0], cell.editValue[1]]);
fixture.detectChanges();
await fixture.whenStable();
expect(grid.data[2].locations.length).toEqual(3);
expect(cell.editValue.length).toEqual(1);
grid.gridAPI.crudService.endEdit(true);
fixture.detectChanges();
await fixture.whenStable();
rowArgs.event = undefined;
expect(row.inEditMode).toBeFalsy();
expect(cell.editMode).toBeFalsy();
initialRowData = { ...cell.row.data };
rowArgs.newValue = initialRowData;
expect(rowArgs.newValue.locations.length).toEqual(1);
expect(rowArgs.oldValue.locations.length).toEqual(3);
expect(grid.rowEdit.emit).toHaveBeenCalledTimes(1);
expect(grid.rowEdit.emit).toHaveBeenCalledWith(rowArgs);
delete rowArgs.cancel;
rowArgs.rowData = initialRowData;
expect(grid.rowEditDone.emit).toHaveBeenCalledTimes(1);
expect(grid.rowEditDone.emit).toHaveBeenCalledWith(rowArgs);
expect(grid.rowEditExit.emit).toHaveBeenCalledTimes(1);
expect(grid.rowEditExit.emit).toHaveBeenCalledWith(rowArgs);
expect(copiedData[2].locations.length).toEqual(1);
});
});