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 @@