Skip to content

Commit d714799

Browse files
authored
Merge branch 'master' into dependabot/github_actions/actions/checkout-4
2 parents 03ccddb + bc0c0d6 commit d714799

32 files changed

+1321
-998
lines changed

API.md

+97-24
Original file line numberDiff line numberDiff line change
@@ -590,35 +590,108 @@ MinMax queries are issued by plots, and may be issued by other types as well. T
590590
#### Telemetry Formats
591591

592592
Telemetry format objects define how to interpret and display telemetry data.
593-
They have a simple structure:
594-
595-
- `key`: A `string` that uniquely identifies this formatter.
596-
- `format`: A `function` that takes a raw telemetry value, and returns a
597-
human-readable `string` representation of that value. It has one required
598-
argument, and three optional arguments that provide context and can be used
599-
for returning scaled representations of a value. An example of this is
600-
representing time values in a scale such as the time conductor scale. There
601-
are multiple ways of representing a point in time, and by providing a minimum
602-
scale value, maximum scale value, and a count, it's possible to provide more
603-
useful representations of time given the provided limitations.
604-
- `value`: The raw telemetry value in its native type.
605-
- `minValue`: An **optional** argument specifying the minimum displayed
606-
value.
607-
- `maxValue`: An **optional** argument specifying the maximum displayed
608-
value.
609-
- `count`: An **optional** argument specifying the number of displayed
610-
values.
611-
- `parse`: A `function` that takes a `string` representation of a telemetry
612-
value, and returns the value in its native type. **Note** parse might receive an already-parsed value. This function should be idempotent.
613-
- `validate`: A `function` that takes a `string` representation of a telemetry
614-
value, and returns a `boolean` value indicating whether the provided string
615-
can be parsed.
593+
They have a simple structure, provided here as a TypeScript interface:
594+
595+
```ts
596+
interface Formatter {
597+
key: string; // A string that uniquely identifies this formatter.
598+
599+
format: (
600+
value: any, // The raw telemetry value in its native type.
601+
minValue?: number, // An optional argument specifying the minimum displayed value.
602+
maxValue?: number, // An optional argument specifying the maximum displayed value.
603+
count?: number // An optional argument specifying the number of displayed values.
604+
) => string; // Returns a human-readable string representation of the provided value.
605+
606+
parse: (
607+
value: string | any // A string representation of a telemetry value or an already-parsed value.
608+
) => any; // Returns the value in its native type. This function should be idempotent.
609+
610+
validate: (value: string) => boolean; // Takes a string representation of a telemetry value and returns a boolean indicating whether the provided string can be parsed.
611+
}
612+
```
613+
614+
##### Built-in Formats
615+
616+
Open MCT on its own defines a handful of built-in formats:
617+
618+
###### **Number Format (default):**
619+
620+
Applied to data with `format: 'number'`
621+
```js
622+
valueMetadata = {
623+
format: 'number'
624+
// ...
625+
};
626+
```
627+
628+
```ts
629+
interface NumberFormatter extends Formatter {
630+
parse: (x: any) => number;
631+
format: (x: number) => string;
632+
validate: (value: any) => boolean;
633+
}
634+
```
635+
###### **String Format**:
636+
637+
Applied to data with `format: 'string'`
638+
```js
639+
valueMetadata = {
640+
format: 'string'
641+
// ...
642+
};
643+
```
644+
```ts
645+
interface StringFormatter extends Formatter {
646+
parse: (value: any) => string;
647+
format: (value: string) => string;
648+
validate: (value: any) => boolean;
649+
}
650+
```
651+
652+
###### **Enum Format**:
653+
Applied to data with `format: 'enum'`
654+
```js
655+
valueMetadata = {
656+
format: 'enum',
657+
enumerations: [
658+
{
659+
value: 1,
660+
string: 'APPLE'
661+
},
662+
{
663+
value: 2,
664+
string: 'PEAR',
665+
},
666+
{
667+
value: 3,
668+
string: 'ORANGE'
669+
}]
670+
// ...
671+
};
672+
```
673+
674+
Creates a two-way mapping between enum string and value to be used in the `parse` and `format` methods.
675+
Ex:
676+
- `formatter.parse('APPLE') === 1;`
677+
- `formatter.format(1) === 'APPLE';`
678+
679+
```ts
680+
interface EnumFormatter extends Formatter {
681+
parse: (value: string) => string;
682+
format: (value: number) => string;
683+
validate: (value: any) => boolean;
684+
}
685+
```
616686

617687
##### Registering Formats
618688

689+
Formats implement the following interface (provided here as TypeScript for simplicity):
690+
691+
619692
Formats are registered with the Telemetry API using the `addFormat` function. eg.
620693

621-
``` javascript
694+
```javascript
622695
openmct.telemetry.addFormat({
623696
key: 'number-to-string',
624697
format: function (number) {

e2e/README.md

+23-1
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ Current list of test tags:
193193
|`@ipad` | Test case or test suite is compatible with Playwright's iPad support and Open MCT's read-only mobile view (i.e. no create button).|
194194
|`@gds` | Denotes a GDS Test Case used in the VIPER Mission.|
195195
|`@addInit` | Initializes the browser with an injected and artificial state. Useful for loading non-default plugins. Likely will not work outside of `npm start`.|
196-
|`@localStorage` | Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB).|
196+
|`@localStorage` | Captures or generates session storage to manipulate browser state. Useful for excluding in tests which require a persistent backend (i.e. CouchDB). See [note](#utilizing-localstorage)|
197197
|`@snapshot` | Uses Playwright's snapshot functionality to record a copy of the DOM for direct comparison. Must be run inside of the playwright container.|
198198
|`@unstable` | A new test or test which is known to be flaky.|
199199
|`@2p` | Indicates that multiple users are involved, or multiple tabs/pages are used. Useful for testing multi-user interactivity.|
@@ -352,6 +352,28 @@ By adhering to this principle, we can create tests that are both robust and refl
352352
1. Avoid repeated setup to test a single assertion. Write longer tests with multiple soft assertions.
353353
This ensures that your changes will be picked up with large refactors.
354354
355+
##### Utilizing LocalStorage
356+
1. In order to save test runtime in the case of tests that require a decent amount of initial setup (such as in the case of testing complex displays), you may use [Playwright's `storageState` feature](https://playwright.dev/docs/api/class-browsercontext#browser-context-storage-state) to generate and load localStorage states.
357+
1. To generate a localStorage state to be used in a test:
358+
- Add an e2e test to our generateLocalStorageData suite which sets the initial state (creating/configuring objects, etc.), saving it in the `test-data` folder:
359+
```js
360+
// Save localStorage for future test execution
361+
await context.storageState({
362+
path: path.join(__dirname, '../../../e2e/test-data/display_layout_with_child_layouts.json')
363+
});
364+
```
365+
- Load the state from file at the beginning of the desired test suite (within the `test.describe()`). (NOTE: the storage state will be used for each test in the suite, so you may need to create a new suite):
366+
```js
367+
const LOCALSTORAGE_PATH = path.resolve(
368+
__dirname,
369+
'../../../../test-data/display_layout_with_child_layouts.json'
370+
);
371+
test.use({
372+
storageState: path.resolve(__dirname, LOCALSTORAGE_PATH)
373+
});
374+
```
375+
376+
355377
### How to write a great test
356378
357379
- Avoid using css locators to find elements to the page. Use modern web accessible locators like `getByRole`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"cookies": [],
3+
"origins": [
4+
{
5+
"origin": "http://localhost:8080",
6+
"localStorage": [
7+
{
8+
"name": "mct",
9+
"value": "{\"mine\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602020,\"created\":1732413601160,\"persisted\":1732413602020},\"764a490f-4a83-4874-a062-e38c112f69c7\":{\"identifier\":{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":30,\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"9acce141-5291-427d-8785-847faa2707e6\"},{\"width\":32,\"height\":18,\"x\":30,\"y\":1,\"identifier\":{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"9f89d6f6-fb7f-4af3-9cc3-4c5d0864e908\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413605120,\"location\":\"mine\",\"created\":1732413602020,\"persisted\":1732413605120},\"801d3a35-91ac-43ae-b175-bc1be65f3587\":{\"name\":\"Child Layout 1\",\"type\":\"layout\",\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413603140,\"location\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"created\":1732413603140,\"persisted\":1732413603140},\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\":{\"name\":\"Child Layout 2\",\"type\":\"layout\",\"identifier\":{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"},\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604240,\"location\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"created\":1732413604240,\"persisted\":1732413604240}}"
10+
},
11+
{
12+
"name": "mct-recent-objects",
13+
"value": "[{\"objectPath\":[{\"identifier\":{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"9acce141-5291-427d-8785-847faa2707e6\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604240,\"location\":\"mine\",\"created\":1732413602020,\"persisted\":1732413604240},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602020,\"created\":1732413601160,\"persisted\":1732413602020},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/764a490f-4a83-4874-a062-e38c112f69c7\",\"domainObject\":{\"identifier\":{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"9acce141-5291-427d-8785-847faa2707e6\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604240,\"location\":\"mine\",\"created\":1732413602020,\"persisted\":1732413604240}},{\"objectPath\":[{\"identifier\":{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"},\"name\":\"Child Layout 2\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604240,\"location\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"created\":1732413604240,\"persisted\":1732413604240},{\"identifier\":{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"9acce141-5291-427d-8785-847faa2707e6\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604240,\"location\":\"mine\",\"created\":1732413602020,\"persisted\":1732413604240},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602020,\"created\":1732413601160,\"persisted\":1732413602020},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/764a490f-4a83-4874-a062-e38c112f69c7/d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"domainObject\":{\"identifier\":{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"},\"name\":\"Child Layout 2\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604240,\"location\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"created\":1732413604240,\"persisted\":1732413604240}},{\"objectPath\":[{\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"name\":\"Child Layout 1\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413603140,\"location\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"created\":1732413603140,\"persisted\":1732413603140},{\"identifier\":{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"},\"name\":\"Parent Display Layout\",\"type\":\"layout\",\"composition\":[{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},{\"key\":\"d70f3dfc-99c6-47f6-87c7-db8faed8598b\",\"namespace\":\"\"}],\"configuration\":{\"items\":[{\"width\":32,\"height\":18,\"x\":1,\"y\":1,\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"hasFrame\":true,\"fontSize\":\"default\",\"font\":\"default\",\"type\":\"subobject-view\",\"id\":\"9acce141-5291-427d-8785-847faa2707e6\"}],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413604240,\"location\":\"mine\",\"created\":1732413602020,\"persisted\":1732413604240},{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602020,\"created\":1732413601160,\"persisted\":1732413602020},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine/764a490f-4a83-4874-a062-e38c112f69c7/801d3a35-91ac-43ae-b175-bc1be65f3587\",\"domainObject\":{\"identifier\":{\"key\":\"801d3a35-91ac-43ae-b175-bc1be65f3587\",\"namespace\":\"\"},\"name\":\"Child Layout 1\",\"type\":\"layout\",\"composition\":[],\"configuration\":{\"items\":[],\"layoutGrid\":[10,10]},\"notes\":\"framework/generateLocalStorageData.e2e.spec.js\\nGenerate Visual Test Data @localStorage @generatedata\\nGenerate display layout with 2 child display layouts\\nchrome\",\"modified\":1732413603140,\"location\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"created\":1732413603140,\"persisted\":1732413603140}},{\"objectPath\":[{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602020,\"created\":1732413601160,\"persisted\":1732413602020},{\"identifier\":{\"key\":\"ROOT\",\"namespace\":\"\"},\"name\":\"Open MCT\",\"type\":\"root\",\"composition\":[{\"key\":\"mine\",\"namespace\":\"\"}]}],\"navigationPath\":\"/browse/mine\",\"domainObject\":{\"identifier\":{\"key\":\"mine\",\"namespace\":\"\"},\"name\":\"My Items\",\"type\":\"folder\",\"composition\":[{\"key\":\"764a490f-4a83-4874-a062-e38c112f69c7\",\"namespace\":\"\"}],\"location\":\"ROOT\",\"modified\":1732413602020,\"created\":1732413601160,\"persisted\":1732413602020}}]"
14+
},
15+
{
16+
"name": "mct-tree-expanded",
17+
"value": "[]"
18+
}
19+
]
20+
}
21+
]
22+
}

e2e/tests/framework/generateLocalStorageData.e2e.spec.js

+36
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,42 @@ test.describe('Generate Visual Test Data @localStorage @generatedata', () => {
5555
await page.goto('./', { waitUntil: 'domcontentloaded' });
5656
});
5757

58+
test('Generate display layout with 2 child display layouts', async ({ page, context }) => {
59+
// Create Display Layout
60+
const parent = await createDomainObjectWithDefaults(page, {
61+
type: 'Display Layout',
62+
name: 'Parent Display Layout'
63+
});
64+
const child1 = await createDomainObjectWithDefaults(page, {
65+
type: 'Display Layout',
66+
name: 'Child Layout 1',
67+
parent: parent.uuid
68+
});
69+
const child2 = await createDomainObjectWithDefaults(page, {
70+
type: 'Display Layout',
71+
name: 'Child Layout 2',
72+
parent: parent.uuid
73+
});
74+
75+
await page.goto(parent.url);
76+
await page.getByLabel('Edit').click();
77+
await page.getByLabel(`${child2.name} Layout Grid`).hover();
78+
await page.getByLabel('Move Sub-object Frame').nth(1).click();
79+
await page.getByLabel('X:').fill('30');
80+
81+
await page.getByLabel(`${child1.name} Layout Grid`).hover();
82+
await page.getByLabel('Move Sub-object Frame').first().click();
83+
await page.getByLabel('Y:').fill('30');
84+
85+
await page.getByLabel('Save').click();
86+
await page.getByRole('listitem', { name: 'Save and Finish Editing' }).click();
87+
88+
//Save localStorage for future test execution
89+
await context.storageState({
90+
path: path.join(__dirname, '../../../e2e/test-data/display_layout_with_child_layouts.json')
91+
});
92+
});
93+
5894
// TODO: Visual test for the generated object here
5995
// - Move to using appActions to create the overlay plot
6096
// and embedded standard telemetry object

0 commit comments

Comments
 (0)