Tree item abort (#6757)
* adding abortSignal back to composition load * suppress AbortError console.errors from couch, delay requests for test to trigger abort --------- Co-authored-by: John Hill <john.c.hill@nasa.gov> Co-authored-by: Andrew Henry <akhenry@gmail.com>
This commit is contained in:
@@ -192,8 +192,12 @@ test.describe('Persistence operations @couchdb', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
//Slow down the test a bit
|
//Slow down the test a bit
|
||||||
await expect(page.getByRole('treeitem', { name: ` ${myItemsFolderName}` })).toBeVisible();
|
await expect(
|
||||||
await expect(page2.getByRole('treeitem', { name: ` ${myItemsFolderName}` })).toBeVisible();
|
page.getByRole('button', { name: `Expand ${myItemsFolderName} folder` })
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page2.getByRole('button', { name: `Expand ${myItemsFolderName} folder` })
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
// Both pages: Click the Create button
|
// Both pages: Click the Create button
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -242,11 +242,16 @@ export default class ObjectAPI {
|
|||||||
return domainObject;
|
return domainObject;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.warn(`Failed to retrieve ${keystring}:`, error);
|
|
||||||
delete this.cache[keystring];
|
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;
|
this.cache[keystring] = objectPromise;
|
||||||
|
|||||||
@@ -248,10 +248,17 @@ describe('The Object API', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('displays a notification in the event of an error', () => {
|
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(() => {
|
return objectAPI.get(mockDomainObject.identifier).catch(() => {
|
||||||
expect(openmct.notifications.error).toHaveBeenCalledWith(
|
expect(openmct.notifications.warn).toHaveBeenCalledWith(
|
||||||
`Failed to retrieve object ${TEST_NAMESPACE}:${TEST_KEY}`
|
`Failed to retrieve object ${TEST_NAMESPACE}:${TEST_KEY}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -223,10 +223,16 @@ class CouchObjectProvider {
|
|||||||
|
|
||||||
return json;
|
return json;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// abort errors are expected
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Network error, CouchDB unreachable.
|
// Network error, CouchDB unreachable.
|
||||||
if (response === null) {
|
if (response === null) {
|
||||||
this.indicator.setIndicatorToState(DISCONNECTED);
|
this.indicator.setIndicatorToState(DISCONNECTED);
|
||||||
console.error(error.message);
|
console.error(error.message);
|
||||||
|
|
||||||
throw new Error(`CouchDB Error - No response"`);
|
throw new Error(`CouchDB Error - No response"`);
|
||||||
} else {
|
} else {
|
||||||
if (body?.model && isNotebookOrAnnotationType(body.model)) {
|
if (body?.model && isNotebookOrAnnotationType(body.model)) {
|
||||||
|
|||||||
@@ -22,7 +22,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<span
|
<span
|
||||||
:class="[controlClass, { 'c-disclosure-triangle--expanded': value }, { 'is-enabled': enabled }]"
|
:class="[controlClass, { 'c-disclosure-triangle--expanded': value }, { 'is-enabled': enabled }]"
|
||||||
|
tabindex="0"
|
||||||
|
role="button"
|
||||||
|
:aria-label="ariaLabelValue"
|
||||||
|
:aria-expanded="value ? 'true' : 'false'"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
|
@keydown.enter="handleClick"
|
||||||
></span>
|
></span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -42,6 +47,18 @@ export default {
|
|||||||
controlClass: {
|
controlClass: {
|
||||||
type: String,
|
type: String,
|
||||||
default: 'c-disclosure-triangle'
|
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: {
|
methods: {
|
||||||
|
|||||||
@@ -326,12 +326,13 @@ export default {
|
|||||||
},
|
},
|
||||||
async openTreeItem(parentItem) {
|
async openTreeItem(parentItem) {
|
||||||
const parentPath = parentItem.navigationPath;
|
const parentPath = parentItem.navigationPath;
|
||||||
|
const abortSignal = this.startItemLoad(parentPath);
|
||||||
|
|
||||||
this.startItemLoad(parentPath);
|
|
||||||
// pass in abort signal when functional
|
// pass in abort signal when functional
|
||||||
const childrenItems = await this.loadAndBuildTreeItemsFor(
|
const childrenItems = await this.loadAndBuildTreeItemsFor(
|
||||||
parentItem.object.identifier,
|
parentItem.object.identifier,
|
||||||
parentItem.objectPath
|
parentItem.objectPath,
|
||||||
|
abortSignal
|
||||||
);
|
);
|
||||||
const parentIndex = this.treeItems.indexOf(parentItem);
|
const parentIndex = this.treeItems.indexOf(parentItem);
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
<view-control
|
<view-control
|
||||||
ref="action"
|
ref="action"
|
||||||
class="c-tree__item__view-control"
|
class="c-tree__item__view-control"
|
||||||
|
:domain-object="node.object"
|
||||||
:value="isOpen || isLoading"
|
:value="isOpen || isLoading"
|
||||||
:enabled="!activeSearch && hasComposition"
|
:enabled="!activeSearch && hasComposition"
|
||||||
@input="itemAction()"
|
@input="itemAction()"
|
||||||
|
|||||||
Reference in New Issue
Block a user