Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
codecov:
require_ci_to_pass: false #This setting will update the bot regardless of whether or not tests pass

# Disabling annotations for now. They are incorrectly labelling lines as lacking coverage when they are in fact covered by tests.
github_checks:
annotations: false

coverage:
status:
project:
Expand Down
1 change: 0 additions & 1 deletion e2e/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
module.exports = {
extends: ['plugin:playwright/recommended'],
rules: {
'playwright/max-nested-describe': ['error', { max: 1 }],
'playwright/expect-expect': 'off'
},
overrides: [
Expand Down
25 changes: 20 additions & 5 deletions e2e/baseFixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,25 +103,40 @@ const extendedTest = test.extend({
* Default: `true`
*/
failOnConsoleError: [true, { option: true }],
ignore404s: [[], { option: true }],
/**
* Extends the base page class to enable console log error detection.
* @see {@link https://github.com/microsoft/playwright/discussions/11690 Github Discussion}
*/
page: async ({ page, failOnConsoleError }, use) => {
page: async ({ page, failOnConsoleError, ignore404s }, use) => {
// Capture any console errors during test execution
const messages = [];
let messages = [];
page.on('console', (msg) => messages.push(msg));

await use(page);

if (ignore404s.length > 0) {
messages = messages.filter((msg) => {
let keep = true;

if (msg.text().match(/404 \((Object )?Not Found\)/) !== null) {
keep = ignore404s.every((ignoreRule) => {
return msg.location().url.match(ignoreRule) === null;
});
}

return keep;
});
}

// Assert against console errors during teardown
if (failOnConsoleError) {
messages.forEach((msg) =>
messages.forEach((msg) => {
// eslint-disable-next-line playwright/no-standalone-expect
expect
.soft(msg.type(), `Console error detected: ${_consoleMessageToString(msg)}`)
.not.toEqual('error')
);
.not.toEqual('error');
});
}
}
});
Expand Down
97 changes: 93 additions & 4 deletions e2e/tests/functional/search.e2e.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import { expect, test } from '../../pluginFixtures.js';
test.describe('Grand Search', () => {
let grandSearchInput;

test.use({ ignore404s: [/_design\/object_names\/_view\/object_names$/] });

test.beforeEach(async ({ page }) => {
grandSearchInput = page
.getByLabel('OpenMCT Search')
Expand Down Expand Up @@ -191,19 +193,106 @@ test.describe('Grand Search', () => {
await expect(searchResults).toContainText(folderName);
});

test.describe('Search will test for the presence of the object_names index, and', () => {
test('use index if available @couchdb @network', async ({ page }) => {
await createObjectsForSearch(page);

let isObjectNamesViewAvailable = false;
let isObjectNamesUsedForSearch = false;

page.on('request', async (request) => {
const isObjectNamesRequest = request.url().endsWith('_view/object_names');
const isHeadRequest = request.method().toLowerCase() === 'head';

if (isObjectNamesRequest && isHeadRequest) {
const response = await request.response();
isObjectNamesViewAvailable = response.status() === 200;
}
});

page.on('request', (request) => {
const isObjectNamesRequest = request.url().endsWith('_view/object_names');
const isPostRequest = request.method().toLowerCase() === 'post';

if (isObjectNamesRequest && isPostRequest) {
isObjectNamesUsedForSearch = true;
}
});

// Full search for object
await grandSearchInput.pressSequentially('Clock', { delay: 100 });

// Wait for search to finish
await waitForSearchCompletion(page);

expect(isObjectNamesViewAvailable).toBe(true);
expect(isObjectNamesUsedForSearch).toBe(true);
});

test('fall-back on base index if index not available @couchdb @network', async ({ page }) => {
await page.route('**/_view/object_names', (route) => {
route.fulfill({
status: 404
});
});
await createObjectsForSearch(page);

let isObjectNamesViewAvailable = false;
let isFindUsedForSearch = false;

page.on('request', async (request) => {
const isObjectNamesRequest = request.url().endsWith('_view/object_names');
const isHeadRequest = request.method().toLowerCase() === 'head';

if (isObjectNamesRequest && isHeadRequest) {
const response = await request.response();
isObjectNamesViewAvailable = response.status() === 200;
}
});

page.on('request', (request) => {
const isFindRequest = request.url().endsWith('_find');
const isPostRequest = request.method().toLowerCase() === 'post';

if (isFindRequest && isPostRequest) {
isFindUsedForSearch = true;
}
});

// Full search for object
await grandSearchInput.pressSequentially('Clock', { delay: 100 });

// Wait for search to finish
await waitForSearchCompletion(page);
console.info(
`isObjectNamesViewAvailable: ${isObjectNamesViewAvailable} | isFindUsedForSearch: ${isFindUsedForSearch}`
);
expect(isObjectNamesViewAvailable).toBe(false);
expect(isFindUsedForSearch).toBe(true);
});
});

test('Search results are debounced @couchdb @network', async ({ page }) => {
// Unfortunately 404s are always logged to the JavaScript console and can't be suppressed
// A 404 is now thrown when we test for the presence of the object names view used by search.
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/nasa/openmct/issues/6179'
});
await createObjectsForSearch(page);

let networkRequests = [];

page.on('request', (request) => {
const searchRequest =
request.url().endsWith('_find') || request.url().includes('by_keystring');
const fetchRequest = request.resourceType() === 'fetch';
if (searchRequest && fetchRequest) {
const isSearchRequest =
request.url().endsWith('object_names') ||
request.url().endsWith('_find') ||
request.url().includes('by_keystring');
const isFetchRequest = request.resourceType() === 'fetch';
// CouchDB search results in a one-time head request to test for the presence of an index.
const isHeadRequest = request.method().toLowerCase() === 'head';

if (isSearchRequest && isFetchRequest && !isHeadRequest) {
networkRequests.push(request);
}
});
Expand Down
7 changes: 6 additions & 1 deletion e2e/tests/performance/memory/navigation.memory.perf.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,6 @@ test.describe('Navigation memory leak is not detected in', () => {
page,
'example-imagery-memory-leak-test'
);

// If we got here without timing out, then the root view object was garbage collected and no memory leak was detected.
expect(result).toBe(true);
});
Expand Down Expand Up @@ -317,6 +316,12 @@ test.describe('Navigation memory leak is not detected in', () => {

// Manually invoke the garbage collector once all references are removed.
window.gc();
window.gc();
window.gc();

setTimeout(() => {
window.gc();
}, 1000);

return gcPromise;
});
Expand Down
12 changes: 10 additions & 2 deletions src/api/composition/CompositionCollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/

import { isIdentifier } from '../objects/object-utils';

/**
* @typedef {import('openmct').DomainObject} DomainObject
*/
Expand Down Expand Up @@ -209,9 +211,15 @@ export default class CompositionCollection {
this.#cleanUpMutables();
const children = await this.#provider.load(this.domainObject);
const childObjects = await Promise.all(
children.map((c) => this.#publicAPI.objects.get(c, abortSignal))
children.map((child) => {
if (isIdentifier(child)) {
return this.#publicAPI.objects.get(child, abortSignal);
} else {
return Promise.resolve(child);
}
})
);
childObjects.forEach((c) => this.add(c, true));
childObjects.forEach((child) => this.add(child, true));
this.#emit('load');

return childObjects;
Expand Down
5 changes: 3 additions & 2 deletions src/api/composition/CompositionProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ export default class CompositionProvider {
* object.
* @param {DomainObject} domainObject the domain object
* for which to load composition
* @returns {Promise<Identifier[]>} a promise for
* the Identifiers in this composition
* @returns {Promise<Identifier[] | DomainObject[]>} a promise for
* the Identifiers or Domain Objects in this composition. If Identifiers are returned,
* they will be automatically resolved to domain objects by the API.
*/
load(domainObject) {
throw new Error('This method must be implemented by a subclass.');
Expand Down
8 changes: 6 additions & 2 deletions src/api/composition/DefaultCompositionProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*****************************************************************************/
import { toRaw } from 'vue';

import { makeKeyString } from '../objects/object-utils.js';
import { makeKeyString, parseKeyString } from '../objects/object-utils.js';
import CompositionProvider from './CompositionProvider.js';

/**
Expand Down Expand Up @@ -75,7 +75,11 @@ export default class DefaultCompositionProvider extends CompositionProvider {
* the Identifiers in this composition
*/
load(domainObject) {
return Promise.all(domainObject.composition);
const identifiers = domainObject.composition
.filter((idOrKeystring) => idOrKeystring !== null && idOrKeystring !== undefined)
.map((idOrKeystring) => parseKeyString(idOrKeystring));

return Promise.all(identifiers);
}
/**
* Attach listeners for changes to the composition of a given domain object.
Expand Down
13 changes: 11 additions & 2 deletions src/api/objects/ObjectAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import ConflictError from './ConflictError.js';
import InMemorySearchProvider from './InMemorySearchProvider.js';
import InterceptorRegistry from './InterceptorRegistry.js';
import MutableDomainObject from './MutableDomainObject.js';
import { isIdentifier, isKeyString } from './object-utils.js';
import RootObjectProvider from './RootObjectProvider.js';
import RootRegistry from './RootRegistry.js';
import Transaction from './Transaction.js';
Expand Down Expand Up @@ -742,11 +743,19 @@ export default class ObjectAPI {
* @param {AbortSignal} abortSignal (optional) signal to abort fetch requests
* @returns {Promise<Array<DomainObject>>} a promise containing an array of domain objects
*/
async getOriginalPath(identifier, path = [], abortSignal = null) {
const domainObject = await this.get(identifier, abortSignal);
async getOriginalPath(identifierOrObject, path = [], abortSignal = null) {
let domainObject;

if (isKeyString(identifierOrObject) || isIdentifier(identifierOrObject)) {
domainObject = await this.get(identifierOrObject, abortSignal);
} else {
domainObject = identifierOrObject;
}

if (!domainObject) {
return [];
}

path.push(domainObject);
const { location } = domainObject;
if (location && !this.#pathContainsDomainObject(location, path)) {
Expand Down
17 changes: 14 additions & 3 deletions src/plugins/CouchDBSearchFolder/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export default function (folderName, couchPlugin, searchFilter) {
location: 'ROOT'
});
}
},
search() {
return Promise.resolve([]);
}
});

Expand All @@ -35,9 +38,17 @@ export default function (folderName, couchPlugin, searchFilter) {
);
},
load() {
return couchProvider.getObjectsByFilter(searchFilter).then((objects) => {
return objects.map((object) => object.identifier);
});
let searchResults;

if (searchFilter.viewName !== undefined) {
// Use a view to search, instead of an _all_docs find
searchResults = couchProvider.getObjectsByView(searchFilter);
} else {
// Use the _find endpoint to search _all_docs
searchResults = couchProvider.getObjectsByFilter(searchFilter);
}

return searchResults;
}
});
};
Expand Down
Loading
Loading