diff --git a/e2e/helper/addInitExampleUser.js b/e2e/helper/addInitExampleUser.js new file mode 100644 index 0000000000..7d8efbee5c --- /dev/null +++ b/e2e/helper/addInitExampleUser.js @@ -0,0 +1,27 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +// This should be used to install the Example User +document.addEventListener('DOMContentLoaded', () => { + const openmct = window.openmct; + openmct.install(openmct.plugins.example.ExampleUser()); +}); diff --git a/e2e/helper/addInitOperatorStatus.js b/e2e/helper/addInitOperatorStatus.js new file mode 100644 index 0000000000..cae93daf1d --- /dev/null +++ b/e2e/helper/addInitOperatorStatus.js @@ -0,0 +1,27 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +// This should be used to install the Operator Status +document.addEventListener('DOMContentLoaded', () => { + const openmct = window.openmct; + openmct.install(openmct.plugins.OperatorStatus()); +}); diff --git a/e2e/tests/functional/plugins/operatorStatus/operatorStatus.e2e.spec.js b/e2e/tests/functional/plugins/operatorStatus/operatorStatus.e2e.spec.js new file mode 100644 index 0000000000..4b6f199a87 --- /dev/null +++ b/e2e/tests/functional/plugins/operatorStatus/operatorStatus.e2e.spec.js @@ -0,0 +1,156 @@ +/***************************************************************************** + * Open MCT, Copyright (c) 2014-2023, United States Government + * as represented by the Administrator of the National Aeronautics and Space + * Administration. All rights reserved. + * + * Open MCT is licensed under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + * Open MCT includes source code licensed under additional open source + * licenses. See the Open Source Licenses file (LICENSES.md) included with + * this source code distribution or the Licensing information page available + * at runtime from the About dialog for additional information. + *****************************************************************************/ + +/* +* This test suite is dedicated to testing the operator status plugin. +*/ + +const path = require('path'); +const { test, expect } = require('../../../../pluginFixtures'); + +/* + +Precondition: Inject Example User, Operator Status Plugins +Verify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation) + +Clear Role Status of single user test +STUB (test.fixme) Rolling through each + +*/ + +test.describe('Operator Status', () => { + test.beforeEach(async ({ page }) => { + // FIXME: determine if plugins will be added to index.html or need to be injected + // eslint-disable-next-line no-undef + await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitExampleUser.js')}); + // eslint-disable-next-line no-undef + await page.addInitScript({ path: path.join(__dirname, '../../../../helper/', 'addInitOperatorStatus.js')}); + await page.goto('./', { waitUntil: 'networkidle' }); + }); + + // verify that operator status is visible + test('operator status is visible and expands when clicked', async ({ page }) => { + await expect(page.locator('div[title="Set my operator status"]')).toBeVisible(); + await page.locator('div[title="Set my operator status"]').click(); + + // expect default status to be 'GO' + await expect(page.locator('.c-status-poll-panel')).toBeVisible(); + }); + + test('poll question indicator remains when blank poll set', async ({ page }) => { + await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible(); + await page.locator('div[title="Set the current poll question"]').click(); + // set to blank + await page.getByRole('button', { name: 'Update' }).click(); + + // should still be visible + await expect(page.locator('div[title="Set the current poll question"]')).toBeVisible(); + }); + + // Verify that user 1 sees updates from user/role 2 (Not possible without openmct-yamcs implementation) + test('operator status table reflects answered values', async ({ page }) => { + // user navigates to operator status poll + const statusPollIndicator = page.locator('div[title="Set my operator status"]'); + await statusPollIndicator.click(); + + // get user role value + const userRole = page.locator('.c-status-poll-panel__user-role'); + const userRoleText = await userRole.innerText(); + + // get selected status value + const selectStatus = page.locator('select[name="setStatus"]'); + await selectStatus.selectOption({ index: 1}); + const initialStatusValue = await selectStatus.inputValue(); + + // open manage status poll + const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]'); + await manageStatusPollIndicator.click(); + // parse the table row values + const row = page.locator(`tr:has-text("${userRoleText}")`); + const rowValues = await row.innerText(); + const rowValuesArr = rowValues.split('\t'); + const COLUMN_STATUS_INDEX = 1; + // check initial set value matches status table + expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()) + .toEqual(initialStatusValue.toLowerCase()); + + // change user status + await statusPollIndicator.click(); + // FIXME: might want to grab a dynamic option instead of arbitrary + await page.locator('select[name="setStatus"]').selectOption({ index: 2}); + const updatedStatusValue = await selectStatus.inputValue(); + // verify user status is reflected in table + await manageStatusPollIndicator.click(); + + const updatedRow = page.locator(`tr:has-text("${userRoleText}")`); + const updatedRowValues = await updatedRow.innerText(); + const updatedRowValuesArr = updatedRowValues.split('\t'); + + expect(updatedRowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()) + .toEqual(updatedStatusValue.toLowerCase()); + + }); + + test('clear poll button removes poll responses', async ({ page }) => { + // user navigates to operator status poll + const statusPollIndicator = page.locator('div[title="Set my operator status"]'); + await statusPollIndicator.click(); + + // get user role value + const userRole = page.locator('.c-status-poll-panel__user-role'); + const userRoleText = await userRole.innerText(); + + // get selected status value + const selectStatus = page.locator('select[name="setStatus"]'); + // FIXME: might want to grab a dynamic option instead of arbitrary + await selectStatus.selectOption({ index: 1}); + const initialStatusValue = await selectStatus.inputValue(); + + // open manage status poll + const manageStatusPollIndicator = page.locator('div[title="Set the current poll question"]'); + await manageStatusPollIndicator.click(); + // parse the table row values + const row = page.locator(`tr:has-text("${userRoleText}")`); + const rowValues = await row.innerText(); + const rowValuesArr = rowValues.split('\t'); + const COLUMN_STATUS_INDEX = 1; + // check initial set value matches status table + expect(rowValuesArr[COLUMN_STATUS_INDEX].toLowerCase()) + .toEqual(initialStatusValue.toLowerCase()); + + // clear the poll + await page.locator('button[title="Clear the previous poll question"]').click(); + + const updatedRow = page.locator(`tr:has-text("${userRoleText}")`); + const updatedRowValues = await updatedRow.innerText(); + const updatedRowValuesArr = updatedRowValues.split('\t'); + const UNSET_VALUE_LABEL = 'Not set'; + expect(updatedRowValuesArr[COLUMN_STATUS_INDEX]) + .toEqual(UNSET_VALUE_LABEL); + + }); + + test.fixme('iterate through all possible response values', async ({ page }) => { + // test all possible respone values for the poll + }); + +}); diff --git a/example/exampleUser/ExampleUserProvider.js b/example/exampleUser/ExampleUserProvider.js index 8fdd029234..43ab47c40e 100644 --- a/example/exampleUser/ExampleUserProvider.js +++ b/example/exampleUser/ExampleUserProvider.js @@ -65,7 +65,7 @@ export default class ExampleUserProvider extends EventEmitter { this.user = undefined; this.loggedIn = false; this.autoLoginUser = undefined; - this.status = STATUSES[1]; + this.status = STATUSES[0]; this.pollQuestion = undefined; this.defaultStatusRole = defaultStatusRole; @@ -124,6 +124,7 @@ export default class ExampleUserProvider extends EventEmitter { } setStatusForRole(role, status) { + status.timestamp = Date.now(); this.status = status; this.emit('statusChange', { role, @@ -133,14 +134,23 @@ export default class ExampleUserProvider extends EventEmitter { return true; } - getPollQuestion() { - return Promise.resolve({ - question: 'Set "GO" if your position is ready for a boarding action on the Klingon cruiser', - timestamp: Date.now() - }); + // eslint-disable-next-line require-await + async getPollQuestion() { + if (this.pollQuestion) { + return this.pollQuestion; + } else { + return undefined; + } } setPollQuestion(pollQuestion) { + if (!pollQuestion) { + // If the poll question is undefined, set it to a blank string. + // This behavior better reflects how other telemetry systems + // deal with undefined poll questions. + pollQuestion = ''; + } + this.pollQuestion = { question: pollQuestion, timestamp: Date.now() diff --git a/src/api/user/StatusAPI.js b/src/api/user/StatusAPI.js index 9c1318c380..90cdf6fe02 100644 --- a/src/api/user/StatusAPI.js +++ b/src/api/user/StatusAPI.js @@ -291,5 +291,6 @@ export default class StatusAPI extends EventEmitter { * The Status type * @typedef {Object} Status * @property {String} key - A unique identifier for this status - * @property {Number} label - A human readable label for this status + * @property {String} label - A human readable label for this status + * @property {Number} timestamp - The time that the status was set. */ diff --git a/src/plugins/operatorStatus/operator-status.scss b/src/plugins/operatorStatus/operator-status.scss index 9482c650fa..8e51063140 100644 --- a/src/plugins/operatorStatus/operator-status.scss +++ b/src/plugins/operatorStatus/operator-status.scss @@ -88,6 +88,14 @@ padding: 3px 0; } + [class*='__label'] { + padding: 3px 0; + } + + [class*='__poll-table'] { + grid-column: span 2; + } + [class*='new-question'] { align-items: center; display: flex; @@ -123,6 +131,12 @@ opacity: 0.6; } } + &__actions { + display:flex; + flex: auto; + flex-direction: row; + justify-content: flex-end; + } } .c-indicator { diff --git a/src/plugins/operatorStatus/pollQuestion/PollQuestion.vue b/src/plugins/operatorStatus/pollQuestion/PollQuestion.vue index ff8443cf9a..6c57a9bd92 100644 --- a/src/plugins/operatorStatus/pollQuestion/PollQuestion.vue +++ b/src/plugins/operatorStatus/pollQuestion/PollQuestion.vue @@ -58,6 +58,13 @@ {{ entry.roleCount }} +
| + Position + | ++ Status + | ++ Age + | +
|---|---|---|
| + {{ statusForRole.role }} + | ++ {{ statusForRole.status.label }} + | ++ {{ statusForRole.age }} + | +