From f9488ab350421be771f356b1775559a8e0d8e0c0 Mon Sep 17 00:00:00 2001 From: Ghislain B Date: Sat, 18 Jul 2020 13:44:02 -0400 Subject: [PATCH] feat(core): add `collectionAsync` option for both the Editors & Filters (#16) * feat: add `collectionAsync` option for both the Editors & Filters --- README.md | 4 +- package.json | 1 + .../__tests__/autoCompleteFilter.spec.ts | 111 +++++++++- .../filters/__tests__/selectFilter.spec.ts | 193 +++++++++--------- .../common/src/filters/autoCompleteFilter.ts | 53 ++++- packages/common/src/filters/selectFilter.ts | 56 ++++- packages/vanilla-bundle/package.json | 3 +- .../slick-vanilla-grid-constructor.spec.ts | 179 ++++++++-------- .../components/slick-vanilla-grid-bundle.ts | 79 +++++-- .../src/examples/example02.ts | 7 +- test/httpClientStub.ts | 50 +++++ test/jest-pretest.ts | 1 + yarn.lock | 23 ++- 13 files changed, 546 insertions(+), 214 deletions(-) create mode 100644 test/httpClientStub.ts diff --git a/README.md b/README.md index 9905f3f4f..2c10a4a0d 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,9 @@ npm run test:watch - [x] Dynamically Add Columns - [x] Tree Data - [ ] Translations Support - - [ ] add missing `collectionAsync` for Editors (maybe Filter too?) + - [x] add missing `collectionAsync` for Editors + - [x] add missing `collectionAsync` for Filters + - [x] requires updating each Filters supporting `collectionAsync` (autoCompleteFilter, selectFilter) - [x] Grid Service should use SlickGrid transactions `beginUpdate`, `endUpdate` for performance reason whenever possible #### Other Todos diff --git a/package.json b/package.json index f61054ee9..b939d9a0b 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ }, "workspaces": { "packages": [ + "examples/*", "packages/*" ], "nohoist": [ diff --git a/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts b/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts index 0f236b070..d165f54f8 100644 --- a/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts +++ b/packages/common/src/filters/__tests__/autoCompleteFilter.spec.ts @@ -3,6 +3,7 @@ import { AutoCompleteFilter } from '../autoCompleteFilter'; import { FieldType, OperatorType } from '../../enums/index'; import { AutocompleteOption, Column, FilterArguments, GridOption, SlickGrid } from '../../interfaces/index'; import { CollectionService } from '../../services/collection.service'; +import { HttpStub } from '../../../../../test/httpClientStub'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; const containerId = 'demo-container'; @@ -30,6 +31,7 @@ describe('AutoCompleteFilter', () => { let spyGetHeaderRow; let mockColumn: Column; let collectionService: CollectionService; + const http = new HttpStub(); beforeEach(() => { translaterService = new TranslateServiceStub(); @@ -69,7 +71,7 @@ describe('AutoCompleteFilter', () => { mockColumn.filter.collection = undefined; filter.init(filterArguments); } catch (e) { - expect(e.toString()).toContain(`[Slickgrid-Universal] You need to pass a "collection" for the AutoComplete Filter to work correctly.`); + expect(e.toString()).toContain(`[Slickgrid-Universal] You need to pass a "collection" (or "collectionAsync") for the AutoComplete Filter to work correctly.`); done(); } }); @@ -85,6 +87,14 @@ describe('AutoCompleteFilter', () => { } }); + it('should throw an error when "collectionAsync" Promise does not return a valid array', (done) => { + mockColumn.filter.collectionAsync = Promise.resolve({ hello: 'world' }); + filter.init(filterArguments).catch((e) => { + expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the AutoComplete Filter, the collection is not a valid array.`); + done(); + }); + }); + it('should initialize the filter', () => { mockColumn.filter.collection = [{ value: 'male', label: 'male' }, { value: 'female', label: 'female' }]; filter.init(filterArguments); @@ -241,6 +251,84 @@ describe('AutoCompleteFilter', () => { expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, clearFilterTriggered: true, shouldTriggerQuery: false }); }); + it('should create the filter with a default search term when using "collectionAsync" as a Promise', (done) => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + const mockCollection = ['male', 'female']; + mockColumn.filter.collectionAsync = Promise.resolve(mockCollection); + + filterArguments.searchTerms = ['female']; + filter.init(filterArguments); + + setTimeout(() => { + const filterElm = divContainer.querySelector('input.filter-gender'); + const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete'); + filter.setValues('male'); + + filterElm.focus(); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); + + expect(autocompleteUlElms.length).toBe(1); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true }); + done(); + }); + }); + + it('should create the filter with a default search term when using "collectionAsync" as a Promise with content to simulate http-client', (done) => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + const mockCollection = ['male', 'female']; + mockColumn.filter.collectionAsync = Promise.resolve({ content: mockCollection }); + + filterArguments.searchTerms = ['female']; + filter.init(filterArguments); + + setTimeout(() => { + const filterElm = divContainer.querySelector('input.filter-gender'); + const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete'); + filter.setValues('male'); + + filterElm.focus(); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); + + expect(autocompleteUlElms.length).toBe(1); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true }); + done(); + }); + }); + + it('should create the filter with a default search term when using "collectionAsync" is a Fetch Promise', (done) => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + const mockCollection = ['male', 'female']; + + http.status = 200; + http.object = mockCollection; + http.returnKey = 'date'; + http.returnValue = '6/24/1984'; + http.responseHeaders = { accept: 'json' }; + mockColumn.filter.collectionAsync = http.fetch('/api', { method: 'GET' }); + + filterArguments.searchTerms = ['female']; + filter.init(filterArguments); + + setTimeout(() => { + const filterElm = divContainer.querySelector('input.filter-gender'); + const autocompleteUlElms = document.body.querySelectorAll('ul.ui-autocomplete'); + filter.setValues('male'); + + filterElm.focus(); + filterElm.dispatchEvent(new (window.window as any).KeyboardEvent('keyup', { keyCode: 97, bubbles: true, cancelable: true })); + const filterFilledElms = divContainer.querySelectorAll('input.filter-gender.filled'); + + expect(autocompleteUlElms.length).toBe(1); + expect(filterFilledElms.length).toBe(1); + expect(spyCallback).toHaveBeenCalledWith(expect.anything(), { columnDef: mockColumn, operator: 'EQ', searchTerms: ['male'], shouldTriggerQuery: true }); + done(); + }); + }); + it('should create the filter and filter the string collection when "collectionFilterBy" is set', () => { mockColumn.filter = { collection: ['other', 'male', 'female'], @@ -307,6 +395,27 @@ describe('AutoCompleteFilter', () => { expect(filterCollection[2]).toEqual({ value: 'female', description: 'female' }); }); + it('should create the filter with a value/label pair collectionAsync that is inside an object when "collectionInsideObjectProperty" is defined with a dot notation', (done) => { + const mockCollection = { deep: { myCollection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }] } }; + mockColumn.filter = { + collectionAsync: Promise.resolve(mockCollection), + collectionOptions: { collectionInsideObjectProperty: 'deep.myCollection' }, + customStructure: { value: 'value', label: 'description', }, + }; + + filter.init(filterArguments); + + setTimeout(() => { + const filterCollection = filter.collection; + + expect(filterCollection.length).toBe(3); + expect(filterCollection[0]).toEqual({ value: 'other', description: 'other' }); + expect(filterCollection[1]).toEqual({ value: 'male', description: 'male' }); + expect(filterCollection[2]).toEqual({ value: 'female', description: 'female' }); + done(); + }, 2); + }); + it('should create the filter and sort the string collection when "collectionSortBy" is set', () => { mockColumn.filter = { collection: ['other', 'male', 'female'], diff --git a/packages/common/src/filters/__tests__/selectFilter.spec.ts b/packages/common/src/filters/__tests__/selectFilter.spec.ts index 4ef03c9b5..c49eb66d2 100644 --- a/packages/common/src/filters/__tests__/selectFilter.spec.ts +++ b/packages/common/src/filters/__tests__/selectFilter.spec.ts @@ -6,6 +6,7 @@ import { Column, FilterArguments, GridOption, SlickGrid } from '../../interfaces import { CollectionService } from '../../services/collection.service'; import { Filters } from '..'; import { SelectFilter } from '../selectFilter'; +import { HttpStub } from '../../../../../test/httpClientStub'; import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; jest.useFakeTimers(); @@ -35,6 +36,7 @@ describe('SelectFilter', () => { let spyGetHeaderRow; let mockColumn: Column; let collectionService: CollectionService; + const http = new HttpStub(); beforeEach(() => { translateService = new TranslateServiceStub(); @@ -75,7 +77,7 @@ describe('SelectFilter', () => { try { filter.init(filterArguments); } catch (e) { - expect(e.message).toContain(`[Slickgrid-Universal] You need to pass a "collection" for the MultipleSelect/SingleSelect Filter to work correctly.`); + expect(e.message).toContain(`[Slickgrid-Universal] You need to pass a "collection" (or "collectionAsync") for the MultipleSelect/SingleSelect Filter to work correctly.`); done(); } }); @@ -427,71 +429,6 @@ describe('SelectFilter', () => { expect(filterListElm[2].textContent).toBe('female'); }); - // it('should create the multi-select filter with a value/label pair collectionAsync that is inside an object when "collectionInsideObjectProperty" is defined with a dot notation', () => { - // const mockDataResponse = { deep: { myCollection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }] } }; - // mockColumn.filter = { - // collectionAsync: new Promise((resolve) => setTimeout(() => resolve(mockDataResponse), 1)), - // collectionOptions: { collectionInsideObjectProperty: 'deep.myCollection' }, - // customStructure: { value: 'value', label: 'description', }, - // }; - - // filter.init(filterArguments); - // jest.runAllTimers(); // fast-forward timer - - // const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); - // const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li span`); - // filterBtnElm.click(); - - // expect(filterListElm.length).toBe(3); - // expect(filterListElm[0].textContent).toBe('other'); - // expect(filterListElm[1].textContent).toBe('male'); - // expect(filterListElm[2].textContent).toBe('female'); - // }); - - // it('should create the multi-select filter with a default search term when using "collectionAsync" as a Promise', () => { - // const spyCallback = jest.spyOn(filterArguments, 'callback'); - // const mockCollection = ['male', 'female']; - // mockColumn.filter.collectionAsync = new Promise((resolve) => setTimeout(() => resolve(mockCollection), 0)); - - // filterArguments.searchTerms = ['female']; - // filter.init(filterArguments); - // jest.runAllTimers(); // fast-forward timer - - // const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); - // const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`); - // const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); - // const filterOkElm = divContainer.querySelector(`[name=filter-gender].ms-drop .ms-ok-button`); - // filterBtnElm.click(); - // filterOkElm.click(); - - // expect(filterListElm.length).toBe(2); - // expect(filterFilledElms.length).toBe(1); - // expect(filterListElm[1].checked).toBe(true); - // expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true }); - // }); - - // it('should create the multi-select filter with a default search term when using "collectionAsync" as a Promise with content to simulate http-client', () => { - // const spyCallback = jest.spyOn(filterArguments, 'callback'); - // const mockCollection = ['male', 'female']; - // mockColumn.filter.collectionAsync = new Promise((resolve) => setTimeout(() => resolve({ content: mockCollection }), 0)); - - // filterArguments.searchTerms = ['female']; - // filter.init(filterArguments); - // jest.runAllTimers(); // fast-forward timer - - // const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); - // const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`); - // const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); - // const filterOkElm = divContainer.querySelector(`[name=filter-gender].ms-drop .ms-ok-button`); - // filterBtnElm.click(); - // filterOkElm.click(); - - // expect(filterListElm.length).toBe(2); - // expect(filterFilledElms.length).toBe(1); - // expect(filterListElm[1].checked).toBe(true); - // expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true }); - // }); - it('should create the multi-select filter with a default search term and have the HTML rendered when "enableRenderHtml" is set', () => { mockColumn.filter = { enableRenderHtml: true, @@ -646,40 +583,106 @@ describe('SelectFilter', () => { expect(filterParentElm.textContent).toBe('2 de 3 sélectionnés'); }); - // it('should trigger a re-render of the DOM element when collection is replaced by new collection', async () => { - // const renderSpy = jest.spyOn(filter, 'renderDomElement'); - // const newCollection = [{ value: 'val1', label: 'label1' }, { value: 'val2', label: 'label2' }]; - // const mockDataResponse = [{ value: 'female', label: 'Female' }, { value: 'male', label: 'Male' }]; + it('should create the multi-select filter with a default search term when using "collectionAsync" as a Promise', async () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + const mockCollection = ['male', 'female']; + mockColumn.filter.collection = undefined; + mockColumn.filter.collectionAsync = Promise.resolve(mockCollection); + + filterArguments.searchTerms = ['female']; + await filter.init(filterArguments); + + const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); + const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`); + const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); + const filterOkElm = divContainer.querySelector(`[name=filter-gender].ms-drop .ms-ok-button`); + filterBtnElm.click(); + filterOkElm.click(); + + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(filterListElm[1].checked).toBe(true); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true }); + }); + + it('should create the multi-select filter with a default search term when using "collectionAsync" as a Promise with content to simulate http-client', async () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + const mockCollection = ['male', 'female']; + mockColumn.filter.collection = undefined; + mockColumn.filter.collectionAsync = Promise.resolve({ content: mockCollection }); + + filterArguments.searchTerms = ['female']; + await filter.init(filterArguments); + + const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); + const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`); + const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); + const filterOkElm = divContainer.querySelector(`[name=filter-gender].ms-drop .ms-ok-button`); + filterBtnElm.click(); + filterOkElm.click(); + + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(filterListElm[1].checked).toBe(true); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true }); + }); + + it('should create the multi-select filter with a default search term when using "collectionAsync" is a Fetch Promise', async () => { + const spyCallback = jest.spyOn(filterArguments, 'callback'); + const mockCollection = ['male', 'female']; + + http.status = 200; + http.object = mockCollection; + http.returnKey = 'date'; + http.returnValue = '6/24/1984'; + http.responseHeaders = { accept: 'json' }; + mockColumn.filter.collectionAsync = http.fetch('/api', { method: 'GET' }); + + filterArguments.searchTerms = ['female']; + await filter.init(filterArguments); - // mockColumn.filter = { - // collection: [], - // collectionAsync: new Promise((resolve) => resolve(mockDataResponse)), - // enableCollectionWatch: true, - // }; + const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); + const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li input[type=checkbox]`); + const filterFilledElms = divContainer.querySelectorAll('.ms-parent.ms-filter.search-filter.filter-gender.filled'); + const filterOkElm = divContainer.querySelector(`[name=filter-gender].ms-drop .ms-ok-button`); + filterBtnElm.click(); + filterOkElm.click(); + + expect(filterListElm.length).toBe(2); + expect(filterFilledElms.length).toBe(1); + expect(filterListElm[1].checked).toBe(true); + expect(spyCallback).toHaveBeenCalledWith(undefined, { columnDef: mockColumn, operator: 'IN', searchTerms: ['female'], shouldTriggerQuery: true }); + }); - // await filter.init(filterArguments); - // mockColumn.filter.collection = newCollection; - // jest.runAllTimers(); // fast-forward timer + it('should create the multi-select filter with a value/label pair collectionAsync that is inside an object when "collectionInsideObjectProperty" is defined with a dot notation', async () => { + const mockDataResponse = { deep: { myCollection: [{ value: 'other', description: 'other' }, { value: 'male', description: 'male' }, { value: 'female', description: 'female' }] } }; + mockColumn.filter = { + collectionAsync: Promise.resolve(mockDataResponse), + collectionOptions: { collectionInsideObjectProperty: 'deep.myCollection' }, + customStructure: { value: 'value', label: 'description', }, + }; - // expect(renderSpy).toHaveBeenCalledTimes(2); - // expect(renderSpy).toHaveBeenCalledWith(newCollection); - // }); + await filter.init(filterArguments); - // it('should trigger a re-render of the DOM element when collection changes', async () => { - // const renderSpy = jest.spyOn(filter, 'renderDomElement'); - // const mockDataResponse = [{ value: 'female', label: 'Female' }, { value: 'male', label: 'Male' }]; + const filterBtnElm = divContainer.querySelector('.ms-parent.ms-filter.search-filter.filter-gender button.ms-choice'); + const filterListElm = divContainer.querySelectorAll(`[name=filter-gender].ms-drop ul>li span`); + filterBtnElm.click(); - // mockColumn.filter = { - // collection: [], - // collectionAsync: new Promise((resolve) => resolve(mockDataResponse)), - // enableCollectionWatch: true, - // }; + expect(filterListElm.length).toBe(3); + expect(filterListElm[0].textContent).toBe('other'); + expect(filterListElm[1].textContent).toBe('male'); + expect(filterListElm[2].textContent).toBe('female'); + }); - // await filter.init(filterArguments); - // mockColumn.filter.collection.push({ value: 'other', label: 'other' }); - // jest.runAllTimers(); // fast-forward timer + it('should throw an error when "collectionAsync" Promise does not return a valid array', async (done) => { + const promise = Promise.resolve({ hello: 'world' }); + mockColumn.filter.collectionAsync = promise; - // expect(renderSpy).toHaveBeenCalledTimes(2); - // expect(renderSpy).toHaveBeenCalledWith(mockColumn.filter.collection); - // }); + try { + await filter.init(filterArguments); + } catch (e) { + expect(e.toString()).toContain(`Something went wrong while trying to pull the collection from the "collectionAsync" call in the Select Filter, the collection is not a valid array.`); + done(); + } + }); }); diff --git a/packages/common/src/filters/autoCompleteFilter.ts b/packages/common/src/filters/autoCompleteFilter.ts index f7bd61d4e..b29d05427 100644 --- a/packages/common/src/filters/autoCompleteFilter.ts +++ b/packages/common/src/filters/autoCompleteFilter.ts @@ -111,8 +111,8 @@ export class AutoCompleteFilter implements Filter { this.columnDef = args.columnDef; this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; - if (!this.grid || !this.columnDef || !this.columnFilter || (!this.columnFilter.collection && !this.columnFilter.filterOptions)) { - throw new Error(`[Slickgrid-Universal] You need to pass a "collection" for the AutoComplete Filter to work correctly. Also each option should include a value/label pair (or value/labelKey when using Locale). For example:: { filter: model: Filters.autoComplete, collection: [{ value: true, label: 'True' }, { value: false, label: 'False'}] }`); + if (!this.grid || !this.columnDef || !this.columnFilter || (!this.columnFilter.collection && !this.columnFilter.collectionAsync && !this.columnFilter.filterOptions)) { + throw new Error(`[Slickgrid-Universal] You need to pass a "collection" (or "collectionAsync") for the AutoComplete Filter to work correctly. Also each option should include a value/label pair (or value/labelKey when using Locale). For example:: { filter: model: Filters.autoComplete, collection: [{ value: true, label: 'True' }, { value: false, label: 'False'}] }`); } this.enableTranslateLabel = this.columnFilter && this.columnFilter.enableTranslateLabel || false; @@ -123,6 +123,17 @@ export class AutoCompleteFilter implements Filter { const newCollection = this.columnFilter.collection || []; this._collection = newCollection; this.renderDomElement(newCollection); + + return new Promise(resolve => { + const collectionAsync = this.columnFilter.collectionAsync; + if (collectionAsync && !this.columnFilter.collection) { + // only read the collectionAsync once (on the 1st load), + // we do this because Http Fetch will throw an error saying body was already read and is streaming is locked + resolve(this.renderOptionsAsync(collectionAsync)); + } else { + resolve(newCollection); + } + }); } /** @@ -197,7 +208,7 @@ export class AutoCompleteFilter implements Filter { } renderDomElement(collection: any[]) { - if (!Array.isArray(collection) && this.collectionOptions && (this.collectionOptions.collectionInsideObjectProperty)) { + if (!Array.isArray(collection) && this.collectionOptions?.collectionInsideObjectProperty) { const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty; collection = getDescendantProperty(collection, collectionInsideObjectProperty || ''); } @@ -334,4 +345,40 @@ export class AutoCompleteFilter implements Filter { } return false; } + + protected async renderOptionsAsync(collectionAsync: Promise): Promise { + let awaitedCollection: any = null; + + if (collectionAsync) { + // wait for the "collectionAsync", once resolved we will save it into the "collection" + const response: any | any[] = await collectionAsync; + + if (Array.isArray(response)) { + awaitedCollection = response; // from Promise + } else if (response instanceof Response && typeof response['json'] === 'function') { + awaitedCollection = await response['json'](); // from Fetch + } else if (response && response['content']) { + awaitedCollection = response['content']; // from http-client + } + + if (!Array.isArray(awaitedCollection) && this.collectionOptions?.collectionInsideObjectProperty) { + const collection = awaitedCollection || response; + const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty; + awaitedCollection = getDescendantProperty(collection, collectionInsideObjectProperty || ''); + } + + if (!Array.isArray(awaitedCollection)) { + throw new Error('Something went wrong while trying to pull the collection from the "collectionAsync" call in the AutoComplete Filter, the collection is not a valid array.'); + } + + // copy over the array received from the async call to the "collection" as the new collection to use + // this has to be BEFORE the `collectionObserver().subscribe` to avoid going into an infinite loop + this.columnFilter.collection = awaitedCollection; + + // recreate Multiple Select after getting async collection + this.renderDomElement(awaitedCollection); + } + + return awaitedCollection; + } } diff --git a/packages/common/src/filters/selectFilter.ts b/packages/common/src/filters/selectFilter.ts index 1f3617604..1f858e70c 100644 --- a/packages/common/src/filters/selectFilter.ts +++ b/packages/common/src/filters/selectFilter.ts @@ -107,8 +107,8 @@ export class SelectFilter implements Filter { this.columnDef = args.columnDef; this.searchTerms = (args.hasOwnProperty('searchTerms') ? args.searchTerms : []) || []; - if (!this.grid || !this.columnDef || !this.columnFilter || !this.columnFilter.collection) { - throw new Error(`[Slickgrid-Universal] You need to pass a "collection" for the MultipleSelect/SingleSelect Filter to work correctly. Also each option should include a value/label pair (or value/labelKey when using Locale). For example:: { filter: model: Filters.multipleSelect, collection: [{ value: true, label: 'True' }, { value: false, label: 'False'}] }`); + if (!this.grid || !this.columnDef || !this.columnFilter || (!this.columnFilter.collection && !this.columnFilter.collectionAsync)) { + throw new Error(`[Slickgrid-Universal] You need to pass a "collection" (or "collectionAsync") for the MultipleSelect/SingleSelect Filter to work correctly. Also each option should include a value/label pair (or value/labelKey when using Locale). For example:: { filter: model: Filters.multipleSelect, collection: [{ value: true, label: 'True' }, { value: false, label: 'False'}] }`); } this.enableTranslateLabel = this.columnFilter && this.columnFilter.enableTranslateLabel || false; @@ -140,7 +140,19 @@ export class SelectFilter implements Filter { const newCollection = this.columnFilter.collection || []; this.renderDomElement(newCollection); - return new Promise(resolve => resolve(newCollection)); + // return new Promise(resolve => resolve(newCollection)); + + return new Promise(async resolve => { + const collectionAsync = this.columnFilter.collectionAsync; + + if (collectionAsync && !this.columnFilter.collection) { + // only read the collectionAsync once (on the 1st load), + // we do this because Http Fetch will throw an error saying body was already read and its streaming is locked + resolve(this.renderOptionsAsync(collectionAsync)); + } else { + resolve(newCollection); + } + }); } /** @@ -232,7 +244,7 @@ export class SelectFilter implements Filter { } renderDomElement(collection: any[]) { - if (!Array.isArray(collection) && this.collectionOptions && (this.collectionOptions.collectionInsideObjectProperty)) { + if (!Array.isArray(collection) && this.collectionOptions?.collectionInsideObjectProperty) { const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty; collection = getDescendantProperty(collection, collectionInsideObjectProperty || ''); } @@ -440,4 +452,40 @@ export class SelectFilter implements Filter { // reset flag for next use this._shouldTriggerQuery = true; } + + protected async renderOptionsAsync(collectionAsync: Promise): Promise { + let awaitedCollection: any = null; + + if (collectionAsync) { + // wait for the "collectionAsync", once resolved we will save it into the "collection" + const response: any | any[] = await collectionAsync; + + if (Array.isArray(response)) { + awaitedCollection = response; // from Promise + } else if (response instanceof Response && typeof response['json'] === 'function') { + awaitedCollection = await response['json'](); // from Fetch + } else if (response && response['content']) { + awaitedCollection = response['content']; // from http-client + } + + if (!Array.isArray(awaitedCollection) && this.collectionOptions?.collectionInsideObjectProperty) { + const collection = awaitedCollection || response; + const collectionInsideObjectProperty = this.collectionOptions.collectionInsideObjectProperty; + awaitedCollection = getDescendantProperty(collection, collectionInsideObjectProperty || ''); + } + + if (!Array.isArray(awaitedCollection)) { + throw new Error('Something went wrong while trying to pull the collection from the "collectionAsync" call in the Select Filter, the collection is not a valid array.'); + } + + // copy over the array received from the async call to the "collection" as the new collection to use + // this has to be BEFORE the `collectionObserver().subscribe` to avoid going into an infinite loop + this.columnFilter.collection = awaitedCollection; + + // recreate Multiple Select after getting async collection + this.renderDomElement(awaitedCollection); + } + + return awaitedCollection; + } } diff --git a/packages/vanilla-bundle/package.json b/packages/vanilla-bundle/package.json index d9ed98e38..55ee48cba 100644 --- a/packages/vanilla-bundle/package.json +++ b/packages/vanilla-bundle/package.json @@ -47,7 +47,8 @@ "@slickgrid-universal/common": "*", "@slickgrid-universal/excel-export": "*", "@slickgrid-universal/file-export": "*", - "dompurify": "^2.0.12" + "dompurify": "^2.0.12", + "isomorphic-fetch": "^2.2.1" }, "devDependencies": { "@types/webpack": "^4.41.21", diff --git a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid-constructor.spec.ts b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid-constructor.spec.ts index d13b7fa30..4519e6702 100644 --- a/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid-constructor.spec.ts +++ b/packages/vanilla-bundle/src/components/__tests__/slick-vanilla-grid-constructor.spec.ts @@ -29,10 +29,11 @@ import { import { GraphqlService, GraphqlPaginatedResult, GraphqlServiceApi, GraphqlServiceOption } from '@slickgrid-universal/graphql'; import * as utilities from '@slickgrid-universal/common/dist/commonjs/services/backend-utilities'; -import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; import { SlickVanillaGridBundle } from '../slick-vanilla-grid-bundle'; import { EventPubSubService } from '../../services/eventPubSub.service'; import { TranslateService } from '../../services'; +import { TranslateServiceStub } from '../../../../../test/translateServiceStub'; +import { HttpStub } from '../../../../../test/httpClientStub'; const mockExecuteBackendProcess = jest.fn(); const mockRefreshBackendDataset = jest.fn(); @@ -263,6 +264,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () let sharedService: SharedService; let eventPubSubService: EventPubSubService; let translateService: TranslateServiceStub; + const http = new HttpStub(); const template = `
@@ -395,87 +397,86 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () }); }); - // describe('with editors', () => { - // it('should be able to load async editors with a regular Promise', (done) => { - // const mockCollection = ['male', 'female']; - // const promise = new Promise((resolve) => resolve(mockCollection)); - // const mockColDefs = [{ id: 'gender', field: 'gender', editor: { model: Editors.text, collectionAsync: promise } }] as Column[]; - // const getColSpy = jest.spyOn(mockGrid, 'getColumns').mockReturnValue(mockColDefs); - - // component.columnDefinitions = mockColDefs; - // component.initialization(divContainer); - - // setTimeout(() => { - // expect(getColSpy).toHaveBeenCalled(); - // expect(component.columnDefinitions[0].editor.collection).toEqual(mockCollection); - // expect(component.columnDefinitions[0].internalColumnEditor.collection).toEqual(mockCollection); - // expect(component.columnDefinitions[0].internalColumnEditor.model).toEqual(Editors.text); - // done(); - // }); - // }); - - // it('should be able to load async editors with as a Promise with content to simulate http-client', (done) => { - // const mockCollection = ['male', 'female']; - // const promise = new Promise((resolve) => resolve({ content: mockCollection })); - // const mockColDefs = [{ id: 'gender', field: 'gender', editor: { model: Editors.text, collectionAsync: promise } }] as Column[]; - // const getColSpy = jest.spyOn(mockGrid, 'getColumns').mockReturnValue(mockColDefs); - - // component.columnDefinitions = mockColDefs; - // component.initialization(divContainer); - - // setTimeout(() => { - // expect(getColSpy).toHaveBeenCalled(); - // expect(component.columnDefinitions[0].editor.collection).toEqual(mockCollection); - // expect(component.columnDefinitions[0].internalColumnEditor.collection).toEqual(mockCollection); - // expect(component.columnDefinitions[0].internalColumnEditor.model).toEqual(Editors.text); - // done(); - // }); - // }); - - // it('should be able to load async editors with a Fetch Promise', (done) => { - // const mockCollection = ['male', 'female']; - // http.status = 200; - // http.object = mockCollection; - // http.returnKey = 'date'; - // http.returnValue = '6/24/1984'; - // http.responseHeaders = { accept: 'json' }; - // const collectionAsync = http.fetch('/api', { method: 'GET' }); - // const mockColDefs = [{ id: 'gender', field: 'gender', editor: { model: Editors.text, collectionAsync } }] as Column[]; - // const getColSpy = jest.spyOn(mockGrid, 'getColumns').mockReturnValue(mockColDefs); - - // component.columnDefinitions = mockColDefs; - // component.initialization(divContainer); - - // setTimeout(() => { - // expect(getColSpy).toHaveBeenCalled(); - // expect(component.columnDefinitions[0].editor.collection).toEqual(mockCollection); - // expect(component.columnDefinitions[0].internalColumnEditor.collection).toEqual(mockCollection); - // expect(component.columnDefinitions[0].internalColumnEditor.model).toEqual(Editors.text); - // done(); - // }); - // }); - - // it('should throw an error when Fetch Promise response bodyUsed is true', (done) => { - // const consoleSpy = jest.spyOn(global.console, 'warn').mockReturnValue(); - // const mockCollection = ['male', 'female']; - // http.status = 200; - // http.object = mockCollection; - // http.returnKey = 'date'; - // http.returnValue = '6/24/1984'; - // http.responseHeaders = { accept: 'json' }; - // const collectionAsync = http.fetch('invalid-url', { method: 'GET' }); - // const mockColDefs = [{ id: 'gender', field: 'gender', editor: { model: Editors.text, collectionAsync } }] as Column[]; - // jest.spyOn(mockGrid, 'getColumns').mockReturnValue(mockColDefs); - // component.columnDefinitions = mockColDefs; - - // component.initialization(divContainer); - - // setTimeout(() => { - // expect(consoleSpy).toHaveBeenCalledWith(expect.toInclude('[SlickGrid-Universal] The response body passed to collectionAsync was already read.')); - // done(); - // }); - // }); - // }); + describe('with editors', () => { + it('should be able to load async editors with a regular Promise', (done) => { + const mockCollection = ['male', 'female']; + const promise = new Promise(resolve => resolve(mockCollection)); + const mockColDefs = [{ id: 'gender', field: 'gender', editor: { model: Editors.text, collectionAsync: promise } }] as Column[]; + const getColSpy = jest.spyOn(mockGrid, 'getColumns').mockReturnValue(mockColDefs); + + component.columnDefinitions = mockColDefs; + // component.initialization(divContainer); + + setTimeout(() => { + expect(getColSpy).toHaveBeenCalled(); + expect(component.columnDefinitions[0].editor).toBeTruthy(); + expect(component.columnDefinitions[0].editor.collection).toEqual(mockCollection); + expect(component.columnDefinitions[0].internalColumnEditor.collection).toEqual(mockCollection); + expect(component.columnDefinitions[0].internalColumnEditor.model).toEqual(Editors.text); + done(); + }); + }); + + it('should be able to load async editors with as a Promise with content to simulate http-client', (done) => { + const mockCollection = ['male', 'female']; + const promise = new Promise(resolve => resolve({ content: mockCollection })); + const mockColDefs = [{ id: 'gender', field: 'gender', editor: { model: Editors.text, collectionAsync: promise } }] as Column[]; + const getColSpy = jest.spyOn(mockGrid, 'getColumns').mockReturnValue(mockColDefs); + + component.columnDefinitions = mockColDefs; + + setTimeout(() => { + expect(getColSpy).toHaveBeenCalled(); + expect(component.columnDefinitions[0].editor.collection).toEqual(mockCollection); + expect(component.columnDefinitions[0].internalColumnEditor.collection).toEqual(mockCollection); + expect(component.columnDefinitions[0].internalColumnEditor.model).toEqual(Editors.text); + done(); + }); + }); + + it('should be able to load async editors with a Fetch Promise', (done) => { + const mockCollection = ['male', 'female']; + http.status = 200; + http.object = mockCollection; + http.returnKey = 'date'; + http.returnValue = '6/24/1984'; + http.responseHeaders = { accept: 'json' }; + const collectionAsync = http.fetch('/api', { method: 'GET' }); + const mockColDefs = [{ id: 'gender', field: 'gender', editor: { model: Editors.text, collectionAsync } }] as Column[]; + const getColSpy = jest.spyOn(mockGrid, 'getColumns').mockReturnValue(mockColDefs); + + component.columnDefinitions = mockColDefs; + + setTimeout(() => { + expect(getColSpy).toHaveBeenCalled(); + expect(component.columnDefinitions[0].editor.collection).toEqual(mockCollection); + expect(component.columnDefinitions[0].internalColumnEditor.collection).toEqual(mockCollection); + expect(component.columnDefinitions[0].internalColumnEditor.model).toEqual(Editors.text); + done(); + }); + }); + + it('should throw an error when Fetch Promise response bodyUsed is true', (done) => { + const consoleSpy = jest.spyOn(global.console, 'warn').mockReturnValue(); + const mockCollection = ['male', 'female']; + http.status = 200; + http.object = mockCollection; + http.returnKey = 'date'; + http.returnValue = '6/24/1984'; + http.responseHeaders = { accept: 'json' }; + const collectionAsync = http.fetch('invalid-url', { method: 'GET' }); + const mockColDefs = [{ id: 'gender', field: 'gender', editor: { model: Editors.text, collectionAsync } }] as Column[]; + jest.spyOn(mockGrid, 'getColumns').mockReturnValue(mockColDefs); + component.columnDefinitions = mockColDefs; + + component.initialization(divContainer); + + setTimeout(() => { + expect(consoleSpy).toHaveBeenCalledWith(expect.toInclude('[SlickGrid-Universal] The response body passed to collectionAsync was already read.')); + done(); + }); + }); + }); describe('use grouping', () => { it('should load groupItemMetaProvider to the DataView when using "draggableGrouping" feature', () => { @@ -898,7 +899,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () data: { users: { nodes: [] }, pageInfo: { hasNextPage: true }, totalCount: 0 }, metrics: { startTime: now, endTime: now, executionTime: 0, totalItemCount: 0 } }; - const promise = new Promise((resolve) => setTimeout(() => resolve(processResult), 1)); + const promise = new Promise(resolve => setTimeout(() => resolve(processResult), 1)); const processSpy = jest.spyOn(component.gridOptions.backendServiceApi, 'process').mockReturnValue(promise); jest.spyOn(component.gridOptions.backendServiceApi.service, 'buildQuery').mockReturnValue(query); @@ -920,7 +921,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () data: { users: [] }, metrics: { startTime: now, endTime: now, executionTime: 0, totalItemCount: 0 } }; - const promise = new Promise((resolve) => setTimeout(() => resolve(processResult), 1)); + const promise = new Promise(resolve => setTimeout(() => resolve(processResult), 1)); const processSpy = jest.spyOn(component.gridOptions.backendServiceApi, 'process').mockReturnValue(promise); jest.spyOn(component.gridOptions.backendServiceApi.service, 'buildQuery').mockReturnValue(query); @@ -1091,7 +1092,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () service: mockGraphqlService2, options: mockGraphqlOptions, preProcess: () => jest.fn(), - process: () => new Promise((resolve) => resolve({ data: { users: { nodes: [], totalCount: 100 } } })), + process: () => new Promise(resolve => resolve({ data: { users: { nodes: [], totalCount: 100 } } })), } as GraphqlServiceApi, pagination: mockPagination, } as GridOption; @@ -1109,7 +1110,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () backendServiceApi: { service: mockGraphqlService, preProcess: () => jest.fn(), - process: () => new Promise((resolve) => resolve('process resolved')), + process: () => new Promise(resolve => resolve('process resolved')), } } as GridOption; component.initialization(divContainer); @@ -1126,7 +1127,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () service: mockGraphqlService, useLocalSorting: true, preProcess: () => jest.fn(), - process: () => new Promise((resolve) => resolve('process resolved')), + process: () => new Promise(resolve => resolve('process resolved')), } } as GridOption; component.initialization(divContainer); @@ -1156,7 +1157,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () service: mockGraphqlService, useLocalFiltering: true, preProcess: () => jest.fn(), - process: () => new Promise((resolve) => resolve('process resolved')), + process: () => new Promise(resolve => resolve('process resolved')), } } as GridOption; component.initialization(divContainer); @@ -1174,7 +1175,7 @@ describe('Slick-Vanilla-Grid-Bundle Component instantiated via Constructor', () backendServiceApi: { service: mockGraphqlService, preProcess: () => jest.fn(), - process: () => new Promise((resolve) => resolve('process resolved')), + process: () => new Promise(resolve => resolve('process resolved')), } } as GridOption; component.initialization(divContainer); diff --git a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts index 95dcd2d14..815ab2f17 100644 --- a/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts +++ b/packages/vanilla-bundle/src/components/slick-vanilla-grid-bundle.ts @@ -992,24 +992,6 @@ export class SlickVanillaGridBundle { return paginationOptions; } - /** - * For convenience to the user, we provide the property "editor" as an Slickgrid-Universal editor complex object - * however "editor" is used internally by SlickGrid for it's own Editor Factory - * so in our lib we will swap "editor" and copy it into a new property called "internalColumnEditor" - * then take back "editor.model" and make it the new "editor" so that SlickGrid Editor Factory still works - */ - swapInternalEditorToSlickGridFactoryEditor(columnDefinitions: Column[]) { - return columnDefinitions.map((column: Column) => { - // on every Editor that have a "collectionAsync", resolve the data and assign it to the "collection" property - // if (column.editor && column.editor.collectionAsync) { - // this.loadEditorCollectionAsync(column); - // } - const columnEditor = column.editor as ColumnEditor; - - return { ...column, editor: columnEditor?.model, internalColumnEditor: { ...columnEditor } }; - }); - } - /** Initialize the Pagination Service once */ private initializePaginationService(paginationOptions: Pagination) { if (this.gridOptions) { @@ -1039,6 +1021,31 @@ export class SlickVanillaGridBundle { } } + /** Load the Editor Collection asynchronously and replace the "collection" property when Promise resolves */ + private loadEditorCollectionAsync(column: Column) { + const collectionAsync = column && column.editor && (column.editor as ColumnEditor).collectionAsync; + if (collectionAsync) { + // wait for the "collectionAsync", once resolved we will save it into the "collection" + // the collectionAsync can be of 3 types HttpClient, HttpFetch or a Promise + collectionAsync.then((response: any | any[]) => { + if (Array.isArray(response)) { + this.updateEditorCollection(column, response); // from Promise + } else if (response instanceof Response && typeof response.json === 'function') { + if (response.bodyUsed) { + console.warn(`[SlickGrid-Universal] The response body passed to collectionAsync was already read.` + + `Either pass the dataset from the Response or clone the response first using response.clone()`); + } else { + // from Fetch + (response as Response).json().then(data => this.updateEditorCollection(column, data)); + } + } else if (response && response['content']) { + this.updateEditorCollection(column, response['content']); // from http-client + } + }); + } + } + + /** Load any possible Grid Presets (columns, filters) */ private loadPresetsWhenDatasetInitialized() { if (this.gridOptions && !this.customDataView) { // if user entered some Filter "presets", we need to reflect them all in the DOM @@ -1161,6 +1168,24 @@ export class SlickVanillaGridBundle { } } + /** + * For convenience to the user, we provide the property "editor" as an Slickgrid-Universal editor complex object + * however "editor" is used internally by SlickGrid for it's own Editor Factory + * so in our lib we will swap "editor" and copy it into a new property called "internalColumnEditor" + * then take back "editor.model" and make it the new "editor" so that SlickGrid Editor Factory still works + */ + private swapInternalEditorToSlickGridFactoryEditor(columnDefinitions: Column[]) { + return columnDefinitions.map((column: Column) => { + // on every Editor that have a "collectionAsync", resolve the data and assign it to the "collection" property + if (column.editor?.collectionAsync) { + this.loadEditorCollectionAsync(column); + } + const columnEditor = column.editor as ColumnEditor; + + return { ...column, editor: columnEditor?.model, internalColumnEditor: { ...columnEditor } }; + }); + } + private treeDataSortComparer(flatDataset: any[]): any[] { const dataViewIdIdentifier = this._gridOptions?.datasetIdPropertyName ?? 'id'; const treeDataOpt: TreeDataOption = this._gridOptions?.treeDataOptions ?? { columnId: '' }; @@ -1191,4 +1216,22 @@ export class SlickVanillaGridBundle { private translateColumnGroupKeys() { this.extensionUtility.translateItems(this.sharedService.allColumns, 'columnGroupKey', 'columnGroup'); } + + /** + * Update the "internalColumnEditor.collection" property. + * Since this is called after the async call resolves, the pointer will not be the same as the "column" argument passed. + * Once we found the new pointer, we will reassign the "editor" and "collection" to the "internalColumnEditor" so it has newest collection + */ + private updateEditorCollection(column: Column, newCollection: T[]) { + (column.editor as ColumnEditor).collection = newCollection; + + // find the new column reference pointer & reassign the new editor to the internalColumnEditor + const columns = this.grid.getColumns(); + if (Array.isArray(columns)) { + const columnRef = columns.find((col: Column) => col.id === column.id); + if (columnRef) { + columnRef.internalColumnEditor = column.editor as ColumnEditor; + } + } + } } diff --git a/packages/web-demo-vanilla-bundle/src/examples/example02.ts b/packages/web-demo-vanilla-bundle/src/examples/example02.ts index 89c88639e..25b3e1bad 100644 --- a/packages/web-demo-vanilla-bundle/src/examples/example02.ts +++ b/packages/web-demo-vanilla-bundle/src/examples/example02.ts @@ -120,8 +120,13 @@ export class Example2 { sortable: true, filterable: true, filter: { + model: Filters.singleSelect, collection: [{ value: '', label: '' }, { value: true, label: 'True' }, { value: false, label: 'False' }], - model: Filters.singleSelect + + // Select Filters also support collection that are async, it could be a Promise (shown below) or Fetch result + // collectionAsync: new Promise(resolve => setTimeout(() => { + // resolve([{ value: '', label: '' }, { value: true, label: 'True' }, { value: false, label: 'False' }]); + // }, 250)), } } ]; diff --git a/test/httpClientStub.ts b/test/httpClientStub.ts new file mode 100644 index 000000000..edad3185c --- /dev/null +++ b/test/httpClientStub.ts @@ -0,0 +1,50 @@ +export class HttpStub { + status: number; + statusText: string; + object: any = {}; + returnKey: string; + returnValue: any; + responseHeaders: any; + + fetch(input, init) { + let request; + const responseInit: any = {}; + responseInit.headers = new Headers(); + + for (const name in this.responseHeaders || {}) { + if (name) { + responseInit.headers.set(name, this.responseHeaders[name]); + } + } + + responseInit.status = this.status || 200; + + if (Request.prototype.isPrototypeOf(input)) { + request = input; + } else { + request = new Request(input, init || {}); + } + if (request.body && request.body.type) { + request.headers.set('Content-Type', request.body.type); + } + + const promise = Promise.resolve().then(() => { + if (request.headers.get('Content-Type') === 'application/json' && request.method !== 'GET') { + return request.json().then((object) => { + object[this.returnKey] = this.returnValue; + const data = JSON.stringify(object); + const response = new Response(data, responseInit); + return this.status >= 200 && this.status < 300 ? Promise.resolve(response) : Promise.reject(response); + }); + } else { + const data = JSON.stringify(this.object); + const response = new Response(data, responseInit); + if (input === 'invalid-url') { + Object.defineProperty(response, 'bodyUsed', { writable: true, configurable: true, value: true }); + } + return this.status >= 200 && this.status < 300 ? Promise.resolve(response) : Promise.reject(response); + } + }); + return promise; + } +} diff --git a/test/jest-pretest.ts b/test/jest-pretest.ts index 18169bd48..c4781a5fd 100644 --- a/test/jest-pretest.ts +++ b/test/jest-pretest.ts @@ -1,4 +1,5 @@ import 'jsdom-global/register'; +import 'isomorphic-fetch'; import * as jQuery from 'jquery'; (global as any).$ = (global as any).jQuery = jQuery; diff --git a/yarn.lock b/yarn.lock index f6910e2e2..18e27bb4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6719,7 +6719,7 @@ is-ssh@^1.3.0: dependencies: protocols "^1.1.0" -is-stream@^1.1.0: +is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= @@ -6815,6 +6815,14 @@ isobject@^4.0.0: resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0" integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA== +isomorphic-fetch@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk= + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" @@ -8606,6 +8614,14 @@ node-fetch-npm@^2.0.2: json-parse-better-errors "^1.0.0" safe-buffer "^5.1.1" +node-fetch@^1.0.1: + version "1.7.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" + integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + node-fetch@^2.3.0, node-fetch@^2.5.0: version "2.6.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" @@ -12417,6 +12433,11 @@ whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" +whatwg-fetch@>=0.10.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.2.0.tgz#8e134f701f0a4ab5fda82626f113e2b647fd16dc" + integrity sha512-SdGPoQMMnzVYThUbSrEvqTlkvC1Ux27NehaJ/GUHBfNrh5Mjg+1/uRyFMwVnxO2MrikMWvWAqUGgQOfVU4hT7w== + whatwg-mimetype@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"