Welcome to mirror list, hosted at ThFree Co, Russian Federation.

appActions.js « e2e - github.com/nasa/openmct.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 3064ecf07940b5e1ad003730c4c4ff778f86fe44 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
/*****************************************************************************
 * Open MCT, Copyright (c) 2014-2022, 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.
 *****************************************************************************/

/**
 * The fixtures in this file are to be used to consolidate common actions performed by the
 * various test suites. The goal is only to avoid duplication of code across test suites and not to abstract
 * away the underlying functionality of the application. For more about the App Action pattern, see /e2e/README.md)
 *
 * For example, if two functions are nearly identical in
 * timer.e2e.spec.js and notebook.e2e.spec.js, that function should be generalized and moved into this file.
 */

/**
 * Defines parameters to be used in the creation of a domain object.
 * @typedef {Object} CreateObjectOptions
 * @property {string} type the type of domain object to create (e.g.: "Sine Wave Generator").
 * @property {string} [name] the desired name of the created domain object.
 * @property {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the Identifier or uuid of the parent object.
 */

/**
 * Contains information about the newly created domain object.
 * @typedef {Object} CreatedObjectInfo
 * @property {string} name the name of the created object
 * @property {string} uuid the uuid of the created object
 * @property {string} url the relative url to the object (for use with `page.goto()`)
 */

const Buffer = require('buffer').Buffer;

/**
 * This common function creates a domain object with the default options. It is the preferred way of creating objects
 * in the e2e suite when uninterested in properties of the objects themselves.
 *
 * @param {import('@playwright/test').Page} page
 * @param {CreateObjectOptions} options
 * @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
 */
async function createDomainObjectWithDefaults(page, { type, name, parent = 'mine' }) {
    const parentUrl = await getHashUrlToDomainObject(page, parent);

    // Navigate to the parent object. This is necessary to create the object
    // in the correct location, such as a folder, layout, or plot.
    await page.goto(`${parentUrl}?hideTree=true`);
    await page.waitForLoadState('networkidle');

    //Click the Create button
    await page.click('button:has-text("Create")');

    // Click the object specified by 'type'
    await page.click(`li:text("${type}")`);

    // Modify the name input field of the domain object to accept 'name'
    if (name) {
        const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
        await nameInput.fill("");
        await nameInput.fill(name);
    }

    // Click OK button and wait for Navigate event
    await Promise.all([
        page.waitForLoadState(),
        page.click('[aria-label="Save"]'),
        // Wait for Save Banner to appear
        page.waitForSelector('.c-message-banner__message')
    ]);

    // Wait until the URL is updated
    await page.waitForURL(`**/${parent}/*`);
    const uuid = await getFocusedObjectUuid(page);
    const objectUrl = await getHashUrlToDomainObject(page, uuid);

    if (await _isInEditMode(page, uuid)) {
        // Save (exit edit mode)
        await page.locator('button[title="Save"]').click();
        await page.locator('li[title="Save and Finish Editing"]').click();
    }

    return {
        name: name || `Unnamed ${type}`,
        uuid: uuid,
        url: objectUrl
    };
}

/**
 * @param {import('@playwright/test').Page} page
 * @param {string} name
 */
async function expandTreePaneItemByName(page, name) {
    const treePane = page.locator('#tree-pane');
    const treeItem = treePane.locator(`role=treeitem[expanded=false][name=/${name}/]`);
    const expandTriangle = treeItem.locator('.c-disclosure-triangle');
    await expandTriangle.click();
}

/**
 * Create a Plan object from JSON with the provided options.
 * @param {import('@playwright/test').Page} page
 * @param {*} options
 * @returns {Promise<CreatedObjectInfo>} An object containing information about the newly created domain object.
 */
async function createPlanFromJSON(page, { name, json, parent = 'mine' }) {
    const parentUrl = await getHashUrlToDomainObject(page, parent);

    // Navigate to the parent object. This is necessary to create the object
    // in the correct location, such as a folder, layout, or plot.
    await page.goto(`${parentUrl}?hideTree=true`);

    //Click the Create button
    await page.click('button:has-text("Create")');

    // Click 'Plan' menu option
    await page.click(`li:text("Plan")`);

    // Modify the name input field of the domain object to accept 'name'
    if (name) {
        const nameInput = page.locator('form[name="mctForm"] .first input[type="text"]');
        await nameInput.fill("");
        await nameInput.fill(name);
    }

    // Upload buffer from memory
    await page.locator('input#fileElem').setInputFiles({
        name: 'plan.txt',
        mimeType: 'text/plain',
        buffer: Buffer.from(JSON.stringify(json))
    });

    // Click OK button and wait for Navigate event
    await Promise.all([
        page.waitForLoadState(),
        page.click('[aria-label="Save"]'),
        // Wait for Save Banner to appear
        page.waitForSelector('.c-message-banner__message')
    ]);

    // Wait until the URL is updated
    await page.waitForURL(`**/mine/*`);
    const uuid = await getFocusedObjectUuid(page);
    const objectUrl = await getHashUrlToDomainObject(page, uuid);

    return {
        uuid,
        name,
        url: objectUrl
    };
}

/**
* Open the given `domainObject`'s context menu from the object tree.
* Expands the path to the object and scrolls to it if necessary.
*
* @param {import('@playwright/test').Page} page
* @param {string} url the url to the object
*/
async function openObjectTreeContextMenu(page, url) {
    await page.goto(url);
    await page.click('button[title="Show selected item in tree"]');
    await page.locator('.is-navigated-object').click({
        button: 'right'
    });
}

/**
 * Gets the UUID of the currently focused object by parsing the current URL
 * and returning the last UUID in the path.
 * @param {import('@playwright/test').Page} page
 * @returns {Promise<string>} the uuid of the focused object
 */
async function getFocusedObjectUuid(page) {
    const UUIDv4Regexp = /[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
    const focusedObjectUuid = await page.evaluate((regexp) => {
        return window.location.href.split('?')[0].match(regexp).at(-1);
    }, UUIDv4Regexp);

    return focusedObjectUuid;
}

/**
 * Returns the hashUrl to the domainObject given its uuid.
 * Useful for directly navigating to the given domainObject.
 *
 * URLs returned will be of the form `'./browse/#/mine/<uuid0>/<uuid1>/...'`
 *
 * @param {import('@playwright/test').Page} page
 * @param {string} uuid the uuid of the object to get the url for
 * @returns {Promise<string>} the url of the object
 */
async function getHashUrlToDomainObject(page, uuid) {
    const hashUrl = await page.evaluate(async (objectUuid) => {
        const path = await window.openmct.objects.getOriginalPath(objectUuid);
        let url = './#/browse/' + [...path].reverse()
            .map((object) => window.openmct.objects.makeKeyString(object.identifier))
            .join('/');

        // Drop the vestigial '/ROOT' if it exists
        if (url.includes('/ROOT')) {
            url = url.split('/ROOT').join('');
        }

        return url;
    }, uuid);

    return hashUrl;
}

/**
 * Utilizes the OpenMCT API to detect if the given object has an active transaction (is in Edit mode).
 * @private
 * @param {import('@playwright/test').Page} page
 * @param {string | import('../src/api/objects/ObjectAPI').Identifier} identifier
 * @return {Promise<boolean>} true if the object has an active transaction, false otherwise
 */
async function _isInEditMode(page, identifier) {
    // eslint-disable-next-line no-return-await
    return await page.evaluate((objectIdentifier) => window.openmct.objects.isTransactionActive(objectIdentifier), identifier);
}

/**
 * Set the time conductor mode to either fixed timespan or realtime mode.
 * @param {import('@playwright/test').Page} page
 * @param {boolean} [isFixedTimespan=true] true for fixed timespan mode, false for realtime mode; default is true
 */
async function setTimeConductorMode(page, isFixedTimespan = true) {
    // Click 'mode' button
    await page.locator('.c-mode-button').click();

    // Switch time conductor mode
    if (isFixedTimespan) {
        await page.locator('data-testid=conductor-modeOption-fixed').click();
    } else {
        await page.locator('data-testid=conductor-modeOption-realtime').click();
    }
}

/**
 * Set the time conductor to fixed timespan mode
 * @param {import('@playwright/test').Page} page
 */
async function setFixedTimeMode(page) {
    await setTimeConductorMode(page, true);
}

/**
 * Set the time conductor to realtime mode
 * @param {import('@playwright/test').Page} page
 */
async function setRealTimeMode(page) {
    await setTimeConductorMode(page, false);
}

/**
 * @typedef {Object} OffsetValues
 * @property {string | undefined} hours
 * @property {string | undefined} mins
 * @property {string | undefined} secs
 */

/**
 * Set the values (hours, mins, secs) for the TimeConductor offsets when in realtime mode
 * @param {import('@playwright/test').Page} page
 * @param {OffsetValues} offset
 * @param {import('@playwright/test').Locator} offsetButton
 */
async function setTimeConductorOffset(page, {hours, mins, secs}, offsetButton) {
    await offsetButton.click();

    if (hours) {
        await page.fill('.pr-time-controls__hrs', hours);
    }

    if (mins) {
        await page.fill('.pr-time-controls__mins', mins);
    }

    if (secs) {
        await page.fill('.pr-time-controls__secs', secs);
    }

    // Click the check button
    await page.locator('.pr-time__buttons .icon-check').click();
}

/**
 * Set the values (hours, mins, secs) for the start time offset when in realtime mode
 * @param {import('@playwright/test').Page} page
 * @param {OffsetValues} offset
 */
async function setStartOffset(page, offset) {
    const startOffsetButton = page.locator('data-testid=conductor-start-offset-button');
    await setTimeConductorOffset(page, offset, startOffsetButton);
}

/**
 * Set the values (hours, mins, secs) for the end time offset when in realtime mode
 * @param {import('@playwright/test').Page} page
 * @param {OffsetValues} offset
 */
async function setEndOffset(page, offset) {
    const endOffsetButton = page.locator('data-testid=conductor-end-offset-button');
    await setTimeConductorOffset(page, offset, endOffsetButton);
}

// eslint-disable-next-line no-undef
module.exports = {
    createDomainObjectWithDefaults,
    expandTreePaneItemByName,
    createPlanFromJSON,
    openObjectTreeContextMenu,
    getHashUrlToDomainObject,
    getFocusedObjectUuid,
    setFixedTimeMode,
    setRealTimeMode,
    setStartOffset,
    setEndOffset
};