| import './index.js'; |
| import { $, $$ } from 'common-sk/modules/dom'; |
| import { deepCopy } from 'common-sk/modules/object'; |
| import { fetchMock } from 'fetch-mock'; |
| import { trstatus } from './test_data'; |
| |
| describe('corpus-selector-sk', () => { |
| // Component under test. |
| let corpusSelectorSk; |
| |
| // Creates a new corpus-selector-sk instance with the given options and |
| // attaches it to the DOM. Variable corpusSelectorSk is set to the new |
| // instance. |
| function newCorpusSelectorSk( |
| {updateFreqSeconds, corpusRendererFn, selectedCorpus}={}) { |
| corpusSelectorSk = document.createElement('corpus-selector-sk'); |
| if (updateFreqSeconds) |
| corpusSelectorSk.setAttribute('update-freq-seconds', updateFreqSeconds); |
| if (corpusRendererFn) corpusSelectorSk.corpusRendererFn = corpusRendererFn; |
| if (selectedCorpus) corpusSelectorSk.selectedCorpus = selectedCorpus; |
| document.body.appendChild(corpusSelectorSk); |
| } |
| |
| // Same as newCorpusSelectorSk, except it returns a promise that resolves when |
| // the corpora is loaded. |
| function loadCorpusSelectorSk(options) { |
| const loaded = eventPromise('corpus-selector-sk-loaded'); |
| newCorpusSelectorSk(options); |
| return loaded; |
| } |
| |
| const corporaLiText = |
| (el) => $("li", el).map((li) => li.innerText); |
| |
| const selectedCorpusLiText = (el) => { |
| const li = $$('li.selected', el); |
| return li ? li.innerText : null; |
| }; |
| |
| let clock; |
| |
| beforeEach(() => { |
| clock = sinon.useFakeTimers(); |
| }); |
| |
| afterEach(() => { |
| // Remove the stale instance under test. |
| if (corpusSelectorSk) { |
| document.body.removeChild(corpusSelectorSk); |
| corpusSelectorSk = null; |
| } |
| |
| fetchMock.reset(); |
| clock.restore(); |
| }); |
| |
| it('shows loading indicator', () => { |
| fetchMock.get('/json/trstatus', trstatus); |
| newCorpusSelectorSk(); // Don't wait for the corpora to load. |
| expect(corpusSelectorSk.innerText).to.equal('Loading corpora details...'); |
| }); |
| |
| it('renders corpora with unspecified default corpus', async () => { |
| fetchMock.get('/json/trstatus', trstatus); |
| await loadCorpusSelectorSk(); |
| expect(corporaLiText(corpusSelectorSk)).to.deep.equal( |
| ['canvaskit', 'colorImage', 'gm', 'image', 'pathkit', 'skp', 'svg']); |
| expect(corpusSelectorSk.selectedCorpus).to.be.undefined; |
| expect(selectedCorpusLiText(corpusSelectorSk)).to.be.null; |
| }); |
| |
| it('renders corpora with default corpus', async () => { |
| fetchMock.get('/json/trstatus', trstatus); |
| await loadCorpusSelectorSk({selectedCorpus: 'gm'}); |
| expect(corporaLiText(corpusSelectorSk)).to.deep.equal( |
| ['canvaskit', 'colorImage', 'gm', 'image', 'pathkit', 'skp', 'svg']); |
| expect(corpusSelectorSk.selectedCorpus).to.equal('gm'); |
| expect(selectedCorpusLiText(corpusSelectorSk)).to.equal('gm'); |
| }); |
| |
| it('renders corpora with custom function', async () => { |
| fetchMock.get('/json/trstatus', trstatus); |
| await loadCorpusSelectorSk({ |
| corpusRendererFn: |
| (c) => `${c.name} : ${c.untriagedCount} / ${c.negativeCount}` |
| }); |
| expect(corporaLiText(corpusSelectorSk)).to.deep.equal([ |
| 'canvaskit : 2 / 2', |
| 'colorImage : 0 / 1', |
| 'gm : 61 / 1494', |
| 'image : 22 / 35', |
| 'pathkit : 0 / 0', |
| 'skp : 0 / 1', |
| 'svg : 19 / 21']); |
| }); |
| |
| it('selects corpus and emits "corpus-selected" event when clicked', |
| async () => { |
| fetchMock.get('/json/trstatus', trstatus); |
| await loadCorpusSelectorSk({selectedCorpus: 'gm'}); |
| expect(corpusSelectorSk.selectedCorpus).to.equal('gm'); |
| expect(selectedCorpusLiText(corpusSelectorSk)).to.equal('gm'); |
| |
| // Click on 'svg' corpus. |
| const corpusSelected = eventPromise('corpus-selected'); |
| $$('li[title="svg"]', corpusSelectorSk).click(); |
| const ev = await corpusSelected; |
| |
| // Assert that selected corpus changed. |
| expect(ev.detail.corpus).to.equal('svg'); |
| expect(corpusSelectorSk.selectedCorpus).to.equal('svg'); |
| expect(selectedCorpusLiText(corpusSelectorSk)).to.equal('svg'); |
| }); |
| |
| it('can set the selected corpus programmatically', async () => { |
| fetchMock.get('/json/trstatus', trstatus); |
| await loadCorpusSelectorSk({selectedCorpus: 'gm'}); |
| expect(corpusSelectorSk.selectedCorpus).to.equal('gm'); |
| expect(selectedCorpusLiText(corpusSelectorSk)).to.equal('gm'); |
| |
| // Select corpus 'svg' programmatically. |
| const corpusSelected = eventPromise('corpus-selected'); |
| corpusSelectorSk.selectedCorpus = 'svg'; |
| const ev = await corpusSelected; |
| |
| // Assert that selected corpus changed. |
| expect(ev.detail.corpus).to.equal('svg'); |
| expect(corpusSelectorSk.selectedCorpus).to.equal('svg'); |
| expect(selectedCorpusLiText(corpusSelectorSk)).to.equal('svg'); |
| }); |
| |
| it('does not trigger corpus change event if selected corpus is clicked', |
| async () => { |
| fetchMock.get('/json/trstatus', trstatus); |
| await loadCorpusSelectorSk({selectedCorpus: 'gm'}); |
| expect(corpusSelectorSk.selectedCorpus).to.equal('gm'); |
| expect(selectedCorpusLiText(corpusSelectorSk)).to.equal('gm'); |
| |
| // Click on 'gm' corpus. |
| corpusSelectorSk.dispatchEvent = sinon.fake(); |
| $$('li[title="gm"]', corpusSelectorSk).click(); |
| |
| // Assert that selected corpus didn't change and that no event was emitted. |
| expect(corpusSelectorSk.dispatchEvent.callCount).to.equal(0); |
| expect(corpusSelectorSk.selectedCorpus).to.equal('gm'); |
| expect(selectedCorpusLiText(corpusSelectorSk)).to.equal('gm'); |
| }); |
| |
| it('updates automatically with the specified frequency', async () => { |
| // Mock /json/trstatus such that the negativeCounts will increase by 1000 |
| // after each call. |
| let updatedStatus = deepCopy(trstatus); |
| const fakeRpcEndpoint = sinon.fake(() => { |
| const retval = deepCopy(updatedStatus); |
| updatedStatus.corpStatus.forEach((corp) => corp.negativeCount += 1000); |
| return retval; |
| }); |
| fetchMock.get('/json/trstatus', fakeRpcEndpoint); |
| |
| // Initial load. |
| await loadCorpusSelectorSk({ |
| corpusRendererFn: |
| (c) => `${c.name} : ${c.untriagedCount} / ${c.negativeCount}`, |
| updateFreqSeconds: 10, |
| }); |
| expect(fakeRpcEndpoint.callCount).to.equal(1); |
| expect(corporaLiText(corpusSelectorSk)).to.deep.equal([ |
| 'canvaskit : 2 / 2', |
| 'colorImage : 0 / 1', |
| 'gm : 61 / 1494', |
| 'image : 22 / 35', |
| 'pathkit : 0 / 0', |
| 'skp : 0 / 1', |
| 'svg : 19 / 21']); |
| |
| // First update. |
| let updated = eventPromise('corpus-selector-sk-loaded', 0); |
| clock.tick(10000); |
| expect(fakeRpcEndpoint.callCount).to.equal(2); |
| await updated; |
| expect(corporaLiText(corpusSelectorSk)).to.deep.equal([ |
| 'canvaskit : 2 / 1002', |
| 'colorImage : 0 / 1001', |
| 'gm : 61 / 2494', |
| 'image : 22 / 1035', |
| 'pathkit : 0 / 1000', |
| 'skp : 0 / 1001', |
| 'svg : 19 / 1021']); |
| |
| // Second update. |
| updated = eventPromise('corpus-selector-sk-loaded', 0); |
| clock.tick(10000); |
| expect(fakeRpcEndpoint.callCount).to.equal(3); |
| await updated; |
| expect(corporaLiText(corpusSelectorSk)).to.deep.equal([ |
| 'canvaskit : 2 / 2002', |
| 'colorImage : 0 / 2001', |
| 'gm : 61 / 3494', |
| 'image : 22 / 2035', |
| 'pathkit : 0 / 2000', |
| 'skp : 0 / 2001', |
| 'svg : 19 / 2021']); |
| }); |
| |
| it('does not update if update frequency is not specified', async () => { |
| const fakeRpcEndpoint = sinon.fake.returns(trstatus); |
| fetchMock.get('/json/trstatus', fakeRpcEndpoint); |
| |
| // RPC end-point called once on creation. |
| await loadCorpusSelectorSk(); |
| expect(fakeRpcEndpoint.callCount).to.equal(1); |
| |
| // No further RPC calls after waiting a long time. |
| clock.tick(Number.MAX_SAFE_INTEGER); |
| expect(fakeRpcEndpoint.callCount).to.equal(1); |
| }); |
| |
| it('stops pinging server for updates after detached from DOM', async () => { |
| const fakeRpcEndpoint = sinon.fake.returns(trstatus); |
| fetchMock.get('/json/trstatus', fakeRpcEndpoint); |
| |
| // RPC end-point called once on creation. |
| await loadCorpusSelectorSk({updateFreqSeconds: 10}); |
| expect(fakeRpcEndpoint.callCount).to.equal(1); |
| |
| // Does update. |
| clock.tick(20000); |
| expect(fakeRpcEndpoint.callCount).to.equal(3); |
| |
| // Detach component from DOM. |
| document.body.removeChild(corpusSelectorSk); |
| |
| // No further RPC calls. |
| clock.tick(20000); |
| expect(fakeRpcEndpoint.callCount).to.equal(3); |
| |
| // Reattach component, otherwise afterEach() will try to remove it and fail. |
| document.body.appendChild(corpusSelectorSk); |
| }); |
| |
| |
| it('should emit event "fetch-error" on RPC failure', async () => { |
| fetchMock.get('/json/trstatus', 500); |
| |
| const fetchError = eventPromise('fetch-error'); |
| newCorpusSelectorSk(); |
| await fetchError; |
| |
| expect(corporaLiText(corpusSelectorSk)).to.be.empty; |
| }) |
| }); |
| |
| // TODO(lovisolo): Move to test_util.js. |
| // Returns a promise that will resolve when the given event is caught at the |
| // document's body element, or reject if the event isn't caught within the given |
| // amount of time. |
| // |
| // Set timeoutMillis = 0 to skip call to setTimeout(). This is necessary on |
| // tests that simulate the passing of time with sinon.useFakeTimers(), which |
| // could trigger the timeout before the promise has a chance to catch the event. |
| // |
| // Sample usage: |
| // |
| // // Code under test. |
| // function doSomethingThatTriggersCustomEvent() { |
| // ... |
| // this.dispatchEvent( |
| // new CustomEvent('my-event', {detail: {foo: 'bar'}, bubbles: true}); |
| // } |
| // |
| // // Test. |
| // it('should trigger a custom event', async () => { |
| // const myEvent = eventPromise('my-event'); |
| // doSomethingThatTriggersCustomEvent(); |
| // const ev = await myEvent; |
| // expect(ev.detail.foo).to.equal('bar'); |
| // }); |
| function eventPromise(event, timeoutMillis = 5000) { |
| // The executor function passed as a constructor argument to the Promise |
| // object is executed immediately. This guarantees that the event handler |
| // is added to document.body before returning. |
| return new Promise((resolve, reject) => { |
| let timeout; |
| const handler = (e) => { |
| document.body.removeEventListener(event, handler); |
| clearTimeout(timeout); |
| resolve(e); |
| }; |
| // Skip setTimeout() call with timeoutMillis = 0. Useful when faking time in |
| // tests with sinon.useFakeTimers(). See |
| // https://sinonjs.org/releases/v7.5.0/fake-timers/. |
| if (timeoutMillis !== 0) { |
| timeout = setTimeout(() => { |
| document.body.removeEventListener(event, handler); |
| reject(new Error(`timed out after ${timeoutMillis} ms ` + |
| `while waiting to catch event "${event}"`)); |
| }, timeoutMillis); |
| } |
| document.body.addEventListener(event, handler); |
| }); |
| } |