2023-06-16 19:45:59 +00:00
/ * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
2024-01-09 21:31:51 +00:00
* Open MCT , Copyright ( c ) 2014 - 2024 , United States Government
2023-06-16 19:45:59 +00:00
* 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 .
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * /
2024-01-08 23:14:33 +00:00
import fs from 'fs' ;
2023-06-16 19:45:59 +00:00
2024-01-02 15:24:22 +00:00
import { createDomainObjectWithDefaults , createPlanFromJSON } from '../../../appActions.js' ;
2024-01-08 23:14:33 +00:00
import { getEarliestStartTime } from '../../../helper/planningUtils' ;
2024-01-02 15:24:22 +00:00
import { expect , test } from '../../../pluginFixtures.js' ;
2023-06-16 19:45:59 +00:00
2024-01-08 23:14:33 +00:00
const examplePlanSmall3 = JSON . parse (
fs . readFileSync (
new URL ( '../../../test-data/examplePlans/ExamplePlan_Small3.json' , import . meta . url )
)
) ;
2024-01-28 23:46:10 +00:00
const examplePlanSmall1 = JSON . parse (
fs . readFileSync (
new URL ( '../../../test-data/examplePlans/ExamplePlan_Small1.json' , import . meta . url )
)
) ;
2024-01-08 23:14:33 +00:00
// eslint-disable-next-line no-unused-vars
const START _TIME _COLUMN = 0 ;
// eslint-disable-next-line no-unused-vars
const END _TIME _COLUMN = 1 ;
const TIME _TO _FROM _COLUMN = 2 ;
// eslint-disable-next-line no-unused-vars
const ACTIVITY _COLUMN = 3 ;
const HEADER _ROW = 0 ;
2024-01-31 00:30:57 +00:00
const NUM _COLUMNS = 5 ;
2024-01-08 23:14:33 +00:00
2023-06-16 19:45:59 +00:00
test . describe ( 'Time List' , ( ) => {
2024-01-28 23:46:10 +00:00
test ( "Create a Time List, add a single Plan to it, verify all the activities are displayed with no milliseconds and selecting an activity shows it's properties" , async ( {
2023-06-16 19:45:59 +00:00
page
} ) => {
// Goto baseURL
await page . goto ( './' , { waitUntil : 'domcontentloaded' } ) ;
const timelist = await test . step ( 'Create a Time List' , async ( ) => {
const createdTimeList = await createDomainObjectWithDefaults ( page , { type : 'Time List' } ) ;
const objectName = await page . locator ( '.l-browse-bar__object-name' ) . innerText ( ) ;
expect ( objectName ) . toBe ( createdTimeList . name ) ;
return createdTimeList ;
} ) ;
await test . step ( 'Create a Plan and add it to the timelist' , async ( ) => {
2024-01-08 23:14:33 +00:00
await createPlanFromJSON ( page , {
2023-06-16 19:45:59 +00:00
name : 'Test Plan' ,
2024-01-28 23:46:10 +00:00
json : examplePlanSmall1 ,
2024-01-08 23:14:33 +00:00
parent : timelist . uuid
2023-06-16 19:45:59 +00:00
} ) ;
2024-01-28 23:46:10 +00:00
const groups = Object . keys ( examplePlanSmall1 ) ;
const firstGroupKey = groups [ 0 ] ;
const firstGroupItems = examplePlanSmall1 [ firstGroupKey ] ;
const firstActivity = firstGroupItems [ 0 ] ;
const lastActivity = firstGroupItems [ firstGroupItems . length - 1 ] ;
const startBound = firstActivity . start ;
const endBound = lastActivity . end ;
2023-06-16 19:45:59 +00:00
// Switch to fixed time mode with all plan events within the bounds
await page . goto (
` ${ timelist . url } ?tc.mode=fixed&tc.startBound= ${ startBound } &tc.endBound= ${ endBound } &tc.timeSystem=utc&view=timelist.view `
) ;
// Verify all events are displayed
2024-01-08 23:14:33 +00:00
const eventCount = await page . getByRole ( 'row' ) . count ( ) ;
// subtracting one for the header
2024-01-28 23:46:10 +00:00
await expect ( eventCount - 1 ) . toEqual ( firstGroupItems . length ) ;
2023-06-16 19:45:59 +00:00
} ) ;
await test . step ( 'Does not show milliseconds in times' , async ( ) => {
2024-01-08 23:14:33 +00:00
// Get an activity
const row = page . getByRole ( 'row' ) . nth ( 2 ) ;
2023-06-16 19:45:59 +00:00
// Verify that none fo the times have milliseconds displayed.
// Example: 2024-11-17T16:00:00Z is correct and 2024-11-17T16:00:00.000Z is wrong
await expect ( row . locator ( '.--start' ) ) . not . toContainText ( '.' ) ;
await expect ( row . locator ( '.--end' ) ) . not . toContainText ( '.' ) ;
await expect ( row . locator ( '.--duration' ) ) . not . toContainText ( '.' ) ;
} ) ;
2024-01-28 23:46:10 +00:00
await test . step ( 'Shows activity properties when a row is selected' , async ( ) => {
await page . getByRole ( 'row' ) . nth ( 2 ) . click ( ) ;
// Find the activity state section in the inspector
await page . getByRole ( 'tab' , { name : 'Activity' } ) . click ( ) ;
// Check that activity state label is displayed in the inspector.
await expect ( page . getByLabel ( 'Activity Status' ) . locator ( "[aria-selected='true']" ) ) . toHaveText (
'Not started'
) ;
} ) ;
2024-01-31 00:30:57 +00:00
} ) ;
} ) ;
test ( "View a timelist in expanded view, verify all the activities are displayed and selecting an activity shows it's properties" , async ( {
page
} ) => {
// Goto baseURL
await page . goto ( './' , { waitUntil : 'domcontentloaded' } ) ;
const timelist = await test . step ( 'Create a Time List' , async ( ) => {
const createdTimeList = await createDomainObjectWithDefaults ( page , { type : 'Time List' } ) ;
const objectName = await page . locator ( '.l-browse-bar__object-name' ) . innerText ( ) ;
expect ( objectName ) . toBe ( createdTimeList . name ) ;
return createdTimeList ;
} ) ;
await test . step ( 'Create a Plan and add it to the timelist' , async ( ) => {
await createPlanFromJSON ( page , {
name : 'Test Plan' ,
json : examplePlanSmall1 ,
parent : timelist . uuid
} ) ;
// Ensure that all activities are shown in the expanded view
const groups = Object . keys ( examplePlanSmall1 ) ;
const firstGroupKey = groups [ 0 ] ;
const firstGroupItems = examplePlanSmall1 [ firstGroupKey ] ;
const firstActivity = firstGroupItems [ 0 ] ;
const lastActivity = firstGroupItems [ firstGroupItems . length - 1 ] ;
const startBound = firstActivity . start ;
const endBound = lastActivity . end ;
// Switch to fixed time mode with all plan events within the bounds
await page . goto (
` ${ timelist . url } ?tc.mode=fixed&tc.startBound= ${ startBound } &tc.endBound= ${ endBound } &tc.timeSystem=utc&view=timelist.view `
) ;
// Change the object to edit mode
await page . getByRole ( 'button' , { name : 'Edit Object' } ) . click ( ) ;
// Find the display properties section in the inspector
await page . getByRole ( 'tab' , { name : 'View Properties' } ) . click ( ) ;
// Switch to expanded view and save the setting
await page . getByLabel ( 'Display Style' ) . selectOption ( { label : 'Expanded' } ) ;
// Click on the "Save" button
await page . getByRole ( 'button' , { name : 'Save' } ) . click ( ) ;
await page . getByRole ( 'listitem' , { name : 'Save and Finish Editing' } ) . click ( ) ;
// Verify all events are displayed
const eventCount = await page . getByRole ( 'row' ) . count ( ) ;
await expect ( eventCount ) . toEqual ( firstGroupItems . length ) ;
} ) ;
await test . step ( 'Shows activity properties when a row is selected' , async ( ) => {
await page . getByRole ( 'row' ) . nth ( 2 ) . click ( ) ;
// Find the activity state section in the inspector
await page . getByRole ( 'tab' , { name : 'Activity' } ) . click ( ) ;
// Check that activity state label is displayed in the inspector.
await expect ( page . getByLabel ( 'Activity Status' ) . locator ( "[aria-selected='true']" ) ) . toHaveText (
'Not started'
) ;
2023-06-16 19:45:59 +00:00
} ) ;
} ) ;
2024-01-08 23:14:33 +00:00
/ * *
* The regular expression used to parse the countdown string .
* Some examples of valid Countdown strings :
* ` ` `
* '35D 02:03:04'
* '-1D 01:02:03'
* '01:02:03'
* '-05:06:07'
* ` ` `
* /
const COUNTDOWN _REGEXP = /(-)?(\d+D\s)?(\d{2}):(\d{2}):(\d{2})/ ;
/ * *
2024-02-02 08:37:37 +00:00
* @ typedef { Object } CountdownOrUpObject
* @ property { string } sign - The sign of the countdown ( '-' if the countdown is negative , '+' otherwise ) .
2024-01-08 23:14:33 +00:00
* @ property { string } days - The number of days in the countdown ( undefined if there are no days ) .
* @ property { string } hours - The number of hours in the countdown .
* @ property { string } minutes - The number of minutes in the countdown .
* @ property { string } seconds - The number of seconds in the countdown .
* @ property { string } toString - The countdown string .
* /
/ * *
* Object representing the indices of the capture groups in a countdown regex match .
*
* @ typedef { { SIGN : number , DAYS : number , HOURS : number , MINUTES : number , SECONDS : number , REGEXP : RegExp } }
* @ property { number } SIGN - The index for the sign capture group ( 1 if a '-' sign is present , otherwise undefined ) .
* @ property { number } DAYS - The index for the days capture group ( 2 for the number of days , otherwise undefined ) .
* @ property { number } HOURS - The index for the hours capture group ( 3 for the hour part of the time ) .
* @ property { number } MINUTES - The index for the minutes capture group ( 4 for the minute part of the time ) .
* @ property { number } SECONDS - The index for the seconds capture group ( 5 for the second part of the time ) .
* /
const COUNTDOWN = Object . freeze ( {
SIGN : 1 ,
DAYS : 2 ,
HOURS : 3 ,
MINUTES : 4 ,
SECONDS : 5
} ) ;
test . describe ( 'Time List with controlled clock' , ( ) => {
test . use ( {
clockOptions : {
now : getEarliestStartTime ( examplePlanSmall3 ) ,
shouldAdvanceTime : true
}
} ) ;
test . beforeEach ( async ( { page } ) => {
await page . goto ( './' , { waitUntil : 'domcontentloaded' } ) ;
} ) ;
test ( 'Time List shows current events and counts down correctly in real-time mode' , async ( {
page
} ) => {
await test . step ( 'Create a Time List, add a Plan to it, and switch to real-time mode' , async ( ) => {
// Create Time List
const timelist = await createDomainObjectWithDefaults ( page , {
type : 'Time List'
} ) ;
// Create a Plan with events that count down and up.
// Add it as a child to the Time List.
await createPlanFromJSON ( page , {
json : examplePlanSmall3 ,
parent : timelist . uuid
} ) ;
// Navigate to the Time List in real-time mode
await page . goto (
` ${ timelist . url } ?tc.mode=local&tc.startDelta=900000&tc.endDelta=1800000&tc.timeSystem=utc&view=grid `
) ;
} ) ;
const countUpCells = [
getCellByIndex ( page , 1 , TIME _TO _FROM _COLUMN ) ,
getCellByIndex ( page , 2 , TIME _TO _FROM _COLUMN )
] ;
const countdownCells = [
getCellByIndex ( page , 3 , TIME _TO _FROM _COLUMN ) ,
getCellByIndex ( page , 4 , TIME _TO _FROM _COLUMN )
] ;
// Verify that the countdown cells are counting down
for ( let i = 0 ; i < countdownCells . length ; i ++ ) {
await test . step ( ` Countdown cell ${ i + 1 } counts down ` , async ( ) => {
const countdownCell = countdownCells [ i ] ;
// Get the initial countdown timestamp object
2024-02-02 08:37:37 +00:00
const beforeCountdown = await getAndAssertCountdownOrUpObject ( page , i + 3 ) ;
// should not have a '-' sign
await expect ( countdownCell ) . not . toHaveText ( '-' ) ;
2024-01-08 23:14:33 +00:00
// Wait until it changes
await expect ( countdownCell ) . not . toHaveText ( beforeCountdown . toString ( ) ) ;
// Get the new countdown timestamp object
2024-02-02 08:37:37 +00:00
const afterCountdown = await getAndAssertCountdownOrUpObject ( page , i + 3 ) ;
2024-01-08 23:14:33 +00:00
// Verify that the new countdown timestamp object is less than the old one
expect ( Number ( afterCountdown . seconds ) ) . toBeLessThan ( Number ( beforeCountdown . seconds ) ) ;
} ) ;
}
// Verify that the count-up cells are counting up
for ( let i = 0 ; i < countUpCells . length ; i ++ ) {
await test . step ( ` Count-up cell ${ i + 1 } counts up ` , async ( ) => {
2024-02-02 08:37:37 +00:00
const countUpCell = countUpCells [ i ] ;
2024-01-08 23:14:33 +00:00
// Get the initial count-up timestamp object
2024-02-02 08:37:37 +00:00
const beforeCountUp = await getAndAssertCountdownOrUpObject ( page , i + 1 ) ;
// should not have a '+' sign
await expect ( countUpCell ) . not . toHaveText ( '+' ) ;
2024-01-08 23:14:33 +00:00
// Wait until it changes
2024-02-02 08:37:37 +00:00
await expect ( countUpCell ) . not . toHaveText ( beforeCountUp . toString ( ) ) ;
2024-01-08 23:14:33 +00:00
// Get the new count-up timestamp object
2024-02-02 08:37:37 +00:00
const afterCountUp = await getAndAssertCountdownOrUpObject ( page , i + 1 ) ;
2024-01-08 23:14:33 +00:00
// Verify that the new count-up timestamp object is greater than the old one
2024-02-02 08:37:37 +00:00
expect ( Number ( afterCountUp . seconds ) ) . toBeGreaterThan ( Number ( beforeCountUp . seconds ) ) ;
2024-01-08 23:14:33 +00:00
} ) ;
}
} ) ;
} ) ;
/ * *
* Get the cell at the given row and column indices .
* @ param { import ( '@playwright/test' ) . Page } page
* @ param { number } rowIndex
* @ param { number } columnIndex
* @ returns { import ( '@playwright/test' ) . Locator } cell
* /
function getCellByIndex ( page , rowIndex , columnIndex ) {
return page . getByRole ( 'cell' ) . nth ( rowIndex * NUM _COLUMNS + columnIndex ) ;
}
/ * *
* Return the innerText of the cell at the given row and column indices .
* @ param { import ( '@playwright/test' ) . Page } page
* @ param { number } rowIndex
* @ param { number } columnIndex
* @ returns { Promise < string > } text
* /
async function getCellTextByIndex ( page , rowIndex , columnIndex ) {
const text = await getCellByIndex ( page , rowIndex , columnIndex ) . innerText ( ) ;
return text ;
}
/ * *
2024-02-02 08:37:37 +00:00
* Get the text from the countdown ( or countup ) cell in the given row , assert that it matches the countdown / countup
2024-01-08 23:14:33 +00:00
* regex , and return an object representing the countdown .
* @ param { import ( '@playwright/test' ) . Page } page
* @ param { number } rowIndex the row index
2024-02-02 08:37:37 +00:00
* @ returns { Promise < CountdownOrUpObject > } The countdown ( or countup ) object
2024-01-08 23:14:33 +00:00
* /
2024-02-02 08:37:37 +00:00
async function getAndAssertCountdownOrUpObject ( page , rowIndex ) {
2024-01-08 23:14:33 +00:00
const timeToFrom = await getCellTextByIndex ( page , HEADER _ROW + rowIndex , TIME _TO _FROM _COLUMN ) ;
expect ( timeToFrom ) . toMatch ( COUNTDOWN _REGEXP ) ;
const match = timeToFrom . match ( COUNTDOWN _REGEXP ) ;
return {
sign : match [ COUNTDOWN . SIGN ] ,
days : match [ COUNTDOWN . DAYS ] ,
hours : match [ COUNTDOWN . HOURS ] ,
minutes : match [ COUNTDOWN . MINUTES ] ,
seconds : match [ COUNTDOWN . SECONDS ] ,
toString : ( ) => timeToFrom
} ;
}