diff --git a/e2e/tests/functional/forms.e2e.spec.js b/e2e/tests/functional/forms.e2e.spec.js
index 793c859ed7..8296ff3ce1 100644
--- a/e2e/tests/functional/forms.e2e.spec.js
+++ b/e2e/tests/functional/forms.e2e.spec.js
@@ -192,8 +192,12 @@ test.describe('Persistence operations @couchdb', () => {
]);
//Slow down the test a bit
- await expect(page.getByRole('treeitem', { name: ` ${myItemsFolderName}` })).toBeVisible();
- await expect(page2.getByRole('treeitem', { name: ` ${myItemsFolderName}` })).toBeVisible();
+ await expect(
+ page.getByRole('button', { name: `Expand ${myItemsFolderName} folder` })
+ ).toBeVisible();
+ await expect(
+ page2.getByRole('button', { name: `Expand ${myItemsFolderName} folder` })
+ ).toBeVisible();
// Both pages: Click the Create button
await Promise.all([
diff --git a/e2e/tests/functional/tree.e2e.spec.js b/e2e/tests/functional/tree.e2e.spec.js
index efdd70b6d7..8eea8d227c 100644
--- a/e2e/tests/functional/tree.e2e.spec.js
+++ b/e2e/tests/functional/tree.e2e.spec.js
@@ -174,6 +174,42 @@ test.describe('Main Tree', () => {
]);
});
});
+ test('Opening and closing an item before the request has been fulfilled will abort the request @couchdb', async ({
+ page,
+ openmctConfig
+ }) => {
+ const { myItemsFolderName } = openmctConfig;
+ let requestWasAborted = false;
+
+ page.on('requestfailed', (request) => {
+ // check if the request was aborted
+ if (request.failure().errorText === 'net::ERR_ABORTED') {
+ requestWasAborted = true;
+ }
+ });
+
+ await createDomainObjectWithDefaults(page, {
+ type: 'Folder',
+ name: 'Foo'
+ });
+
+ // Intercept and delay request
+ const delayInMs = 500;
+
+ await page.route('**', async (route, request) => {
+ await new Promise((resolve) => setTimeout(resolve, delayInMs));
+ route.continue();
+ });
+
+ // Quickly Expand/close the root folder
+ await page
+ .getByRole('button', {
+ name: `Expand ${myItemsFolderName} folder`
+ })
+ .dblclick({ delay: 400 });
+
+ expect(requestWasAborted).toBe(true);
+ });
});
/**
diff --git a/src/api/objects/ObjectAPI.js b/src/api/objects/ObjectAPI.js
index 6b5c8fe22b..ebc7767371 100644
--- a/src/api/objects/ObjectAPI.js
+++ b/src/api/objects/ObjectAPI.js
@@ -242,11 +242,16 @@ export default class ObjectAPI {
return domainObject;
})
.catch((error) => {
- console.warn(`Failed to retrieve ${keystring}:`, error);
delete this.cache[keystring];
- const result = this.applyGetInterceptors(identifier);
- return result;
+ // suppress abort errors
+ if (error.name === 'AbortError') {
+ return;
+ }
+
+ console.warn(`Failed to retrieve ${keystring}:`, error);
+
+ return this.applyGetInterceptors(identifier);
});
this.cache[keystring] = objectPromise;
diff --git a/src/api/objects/ObjectAPISpec.js b/src/api/objects/ObjectAPISpec.js
index 9456440adf..1d910ae6b5 100644
--- a/src/api/objects/ObjectAPISpec.js
+++ b/src/api/objects/ObjectAPISpec.js
@@ -248,10 +248,17 @@ describe('The Object API', () => {
});
it('displays a notification in the event of an error', () => {
- mockProvider.get.and.returnValue(Promise.reject());
+ openmct.notifications.warn = jasmine.createSpy('warn');
+ mockProvider.get.and.returnValue(
+ Promise.reject({
+ name: 'Error',
+ status: 404,
+ statusText: 'Not Found'
+ })
+ );
return objectAPI.get(mockDomainObject.identifier).catch(() => {
- expect(openmct.notifications.error).toHaveBeenCalledWith(
+ expect(openmct.notifications.warn).toHaveBeenCalledWith(
`Failed to retrieve object ${TEST_NAMESPACE}:${TEST_KEY}`
);
});
diff --git a/src/plugins/persistence/couch/CouchObjectProvider.js b/src/plugins/persistence/couch/CouchObjectProvider.js
index ada7c80485..b7d1a62ba0 100644
--- a/src/plugins/persistence/couch/CouchObjectProvider.js
+++ b/src/plugins/persistence/couch/CouchObjectProvider.js
@@ -223,10 +223,16 @@ class CouchObjectProvider {
return json;
} catch (error) {
+ // abort errors are expected
+ if (error.name === 'AbortError') {
+ return;
+ }
+
// Network error, CouchDB unreachable.
if (response === null) {
this.indicator.setIndicatorToState(DISCONNECTED);
console.error(error.message);
+
throw new Error(`CouchDB Error - No response"`);
} else {
if (body?.model && isNotebookOrAnnotationType(body.model)) {
diff --git a/src/ui/components/viewControl.vue b/src/ui/components/viewControl.vue
index d7ee3924c3..0b3bc54284 100644
--- a/src/ui/components/viewControl.vue
+++ b/src/ui/components/viewControl.vue
@@ -22,7 +22,12 @@
@@ -42,6 +47,18 @@ export default {
controlClass: {
type: String,
default: 'c-disclosure-triangle'
+ },
+ domainObject: {
+ type: Object,
+ default: () => {}
+ }
+ },
+ computed: {
+ ariaLabelValue() {
+ const name = this.domainObject.name ? ` ${this.domainObject.name}` : '';
+ const type = this.domainObject.type ? ` ${this.domainObject.type}` : '';
+
+ return `${this.value ? 'Collapse' : 'Expand'}${name}${type}`;
}
},
methods: {
diff --git a/src/ui/layout/mct-tree.vue b/src/ui/layout/mct-tree.vue
index ff161788f2..1b5cc04c3b 100644
--- a/src/ui/layout/mct-tree.vue
+++ b/src/ui/layout/mct-tree.vue
@@ -326,12 +326,13 @@ export default {
},
async openTreeItem(parentItem) {
const parentPath = parentItem.navigationPath;
+ const abortSignal = this.startItemLoad(parentPath);
- this.startItemLoad(parentPath);
// pass in abort signal when functional
const childrenItems = await this.loadAndBuildTreeItemsFor(
parentItem.object.identifier,
- parentItem.objectPath
+ parentItem.objectPath,
+ abortSignal
);
const parentIndex = this.treeItems.indexOf(parentItem);
diff --git a/src/ui/layout/tree-item.vue b/src/ui/layout/tree-item.vue
index 3e7e2d5760..f26a2871ba 100644
--- a/src/ui/layout/tree-item.vue
+++ b/src/ui/layout/tree-item.vue
@@ -44,6 +44,7 @@