diff --git a/.github/workflows/e2e-couchdb.yml b/.github/workflows/e2e-couchdb.yml index 264cae4528..286190f755 100644 --- a/.github/workflows/e2e-couchdb.yml +++ b/.github/workflows/e2e-couchdb.yml @@ -49,6 +49,7 @@ jobs: - name: Run CouchDB Tests and publish to deploysentinel env: DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }} + COMMIT_INFO_SHA: ${{github.event.pull_request.head.sha }} run: npm run test:e2e:couchdb - name: Publish Results to Codecov.io diff --git a/e2e/tests/framework/baseFixtures.e2e.spec.js b/e2e/tests/framework/baseFixtures.e2e.spec.js index 81b1697538..ee8dc5a893 100644 --- a/e2e/tests/framework/baseFixtures.e2e.spec.js +++ b/e2e/tests/framework/baseFixtures.e2e.spec.js @@ -29,7 +29,8 @@ relates to how we've extended it (i.e. ./e2e/baseFixtures.js) and assumptions ma const { test } = require('../../baseFixtures.js'); test.describe('baseFixtures tests', () => { - test('Verify that tests fail if console.error is thrown', async ({ page }) => { + //Skip this test for now https://github.com/nasa/openmct/issues/6785 + test.fixme('Verify that tests fail if console.error is thrown', async ({ page }) => { test.fail(); //Go to baseURL await page.goto('./', { waitUntil: 'domcontentloaded' }); diff --git a/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js b/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js index c2e46566d4..8d83e62c39 100644 --- a/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js +++ b/e2e/tests/functional/plugins/plot/stackedPlot.e2e.spec.js @@ -26,7 +26,11 @@ necessarily be used for reference when writing new tests in this area. */ const { test, expect } = require('../../../../pluginFixtures'); -const { createDomainObjectWithDefaults, selectInspectorTab } = require('../../../../appActions'); +const { + createDomainObjectWithDefaults, + selectInspectorTab, + waitForPlotsToRender +} = require('../../../../appActions'); test.describe('Stacked Plot', () => { let stackedPlot; @@ -227,4 +231,45 @@ test.describe('Stacked Plot', () => { page.locator('[aria-label="Plot Series Properties"] .c-object-label') ).toContainText(swgC.name); }); + + test('the legend toggles between aggregate and per child', async ({ page }) => { + await page.goto(stackedPlot.url); + + // Go into edit mode + await page.click('button[title="Edit"]'); + + await selectInspectorTab(page, 'Config'); + + let legendProperties = await page.locator('[aria-label="Legend Properties"]'); + await legendProperties.locator('[title="Display legends per sub plot."]~div input').uncheck(); + + await assertAggregateLegendIsVisible(page); + + // Save (exit edit mode) + await page.locator('button[title="Save"]').click(); + await page.locator('li[title="Save and Finish Editing"]').click(); + + await assertAggregateLegendIsVisible(page); + + await page.reload(); + + await assertAggregateLegendIsVisible(page); + }); }); + +/** + * Asserts that aggregate stacked plot legend is visible + * @param {import('@playwright/test').Page} page + */ +async function assertAggregateLegendIsVisible(page) { + // Wait for plot series data to load + await waitForPlotsToRender(page); + // Wait for plot legend to be shown + await page.waitForSelector('.js-stacked-plot-legend', { state: 'attached' }); + // There should be 3 legend items + expect( + await page + .locator('.js-stacked-plot-legend .c-plot-legend__wrapper div.plot-legend-item') + .count() + ).toBe(3); +} diff --git a/src/plugins/persistence/couch/CouchObjectProvider.js b/src/plugins/persistence/couch/CouchObjectProvider.js index 8982d97bd8..ada7c80485 100644 --- a/src/plugins/persistence/couch/CouchObjectProvider.js +++ b/src/plugins/persistence/couch/CouchObjectProvider.js @@ -24,6 +24,7 @@ import CouchDocument from './CouchDocument'; import CouchObjectQueue from './CouchObjectQueue'; import { PENDING, CONNECTED, DISCONNECTED, UNKNOWN } from './CouchStatusIndicator'; import { isNotebookOrAnnotationType } from '../../notebook/notebook-constants.js'; +import _ from 'lodash'; const REV = '_rev'; const ID = '_id'; @@ -42,6 +43,8 @@ class CouchObjectProvider { this.batchIds = []; this.onEventMessage = this.onEventMessage.bind(this); this.onEventError = this.onEventError.bind(this); + this.flushPersistenceQueue = _.debounce(this.flushPersistenceQueue.bind(this)); + this.persistenceQueue = []; } /** @@ -668,9 +671,12 @@ class CouchObjectProvider { if (!this.objectQueue[key].pending) { this.objectQueue[key].pending = true; const queued = this.objectQueue[key].dequeue(); - let document = new CouchDocument(key, queued.model); - document.metadata.created = Date.now(); - this.request(key, 'PUT', document) + let couchDocument = new CouchDocument(key, queued.model); + couchDocument.metadata.created = Date.now(); + this.#enqueueForPersistence({ + key, + document: couchDocument + }) .then((response) => { this.#checkResponse(response, queued.intermediateResponse, key); }) @@ -683,6 +689,42 @@ class CouchObjectProvider { return intermediateResponse.promise; } + #enqueueForPersistence({ key, document }) { + return new Promise((resolve, reject) => { + this.persistenceQueue.push({ + key, + document, + resolve, + reject + }); + this.flushPersistenceQueue(); + }); + } + + async flushPersistenceQueue() { + if (this.persistenceQueue.length > 1) { + const batch = { + docs: this.persistenceQueue.map((queued) => queued.document) + }; + const response = await this.request('_bulk_docs', 'POST', batch); + response.forEach((responseMetadatum) => { + const queued = this.persistenceQueue.find( + (queuedMetadatum) => queuedMetadatum.key === responseMetadatum.id + ); + if (responseMetadatum.ok) { + queued.resolve(responseMetadatum); + } else { + queued.reject(responseMetadatum); + } + }); + } else if (this.persistenceQueue.length === 1) { + const { key, document, resolve, reject } = this.persistenceQueue[0]; + + this.request(key, 'PUT', document).then(resolve).catch(reject); + } + this.persistenceQueue = []; + } + /** * @private */ diff --git a/src/plugins/persistence/couch/pluginSpec.js b/src/plugins/persistence/couch/pluginSpec.js index cd522f71fd..39893372f4 100644 --- a/src/plugins/persistence/couch/pluginSpec.js +++ b/src/plugins/persistence/couch/pluginSpec.js @@ -243,6 +243,135 @@ describe('the plugin', () => { expect(requestMethod).toEqual('GET'); }); }); + describe('batches persistence', () => { + let successfulMockPromise; + let partialFailureMockPromise; + let objectsToPersist; + + beforeEach(() => { + successfulMockPromise = Promise.resolve({ + json: () => { + return [ + { + id: 'object-1', + ok: true + }, + { + id: 'object-2', + ok: true + }, + { + id: 'object-3', + ok: true + } + ]; + } + }); + + partialFailureMockPromise = Promise.resolve({ + json: () => { + return [ + { + id: 'object-1', + ok: true + }, + { + id: 'object-2', + ok: false + }, + { + id: 'object-3', + ok: true + } + ]; + } + }); + + objectsToPersist = [ + { + identifier: { + namespace: '', + key: 'object-1' + }, + name: 'object-1', + type: 'folder', + modified: 0 + }, + { + identifier: { + namespace: '', + key: 'object-2' + }, + name: 'object-2', + type: 'folder', + modified: 0 + }, + { + identifier: { + namespace: '', + key: 'object-3' + }, + name: 'object-3', + type: 'folder', + modified: 0 + } + ]; + }); + it('for multiple simultaneous successful saves', async () => { + fetch.and.returnValue(successfulMockPromise); + + await Promise.all( + objectsToPersist.map((objectToPersist) => openmct.objects.save(objectToPersist)) + ); + + const requestUrl = fetch.calls.mostRecent().args[0]; + const requestMethod = fetch.calls.mostRecent().args[1].method; + const requestBody = JSON.parse(fetch.calls.mostRecent().args[1].body); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(requestUrl.includes('_bulk_docs')).toBeTrue(); + expect(requestMethod).toEqual('POST'); + expect( + objectsToPersist.every( + (object, index) => object.identifier.key === requestBody.docs[index]._id + ) + ).toBeTrue(); + }); + it('for multiple simultaneous saves with partial failure', async () => { + fetch.and.returnValue(partialFailureMockPromise); + + let saveResults = await Promise.all( + objectsToPersist.map((objectToPersist) => + openmct.objects + .save(objectToPersist) + .then(() => true) + .catch(() => false) + ) + ); + expect(saveResults[0]).toBeTrue(); + expect(saveResults[1]).toBeFalse(); + expect(saveResults[2]).toBeTrue(); + }); + it('except for a single save', async () => { + fetch.and.returnValue({ + json: () => { + return { + id: 'object-1', + ok: true + }; + } + }); + await openmct.objects.save(objectsToPersist[0]); + + const requestUrl = fetch.calls.mostRecent().args[0]; + const requestMethod = fetch.calls.mostRecent().args[1].method; + + expect(fetch).toHaveBeenCalledTimes(1); + expect(requestUrl.includes('_bulk_docs')).toBeFalse(); + expect(requestUrl.endsWith('object-1')).toBeTrue(); + expect(requestMethod).toEqual('PUT'); + }); + }); describe('implements server-side search', () => { let mockPromise; beforeEach(() => { diff --git a/src/plugins/plot/MctPlot.vue b/src/plugins/plot/MctPlot.vue index 899af22c72..cb23e0c505 100644 --- a/src/plugins/plot/MctPlot.vue +++ b/src/plugins/plot/MctPlot.vue @@ -77,6 +77,7 @@

    Legend

    +
  • +
    + Show legend per plot +
    +
    {{ showLegendsForChildren ? 'Yes' : 'No' }}
    +
  • -
      +

        Legend

      diff --git a/src/plugins/plot/inspector/forms/LegendForm.vue b/src/plugins/plot/inspector/forms/LegendForm.vue index 5c72b1a382..d30a501832 100644 --- a/src/plugins/plot/inspector/forms/LegendForm.vue +++ b/src/plugins/plot/inspector/forms/LegendForm.vue @@ -21,6 +21,16 @@ -->