Fix OS switching not working on tree view UI

This commit resolves a rendering bug in the tree view component.
Previously, updating the tree collection prior to node updates led to
rendering errors due to the presence of non-existent nodes in the new
collection.

Changes:

- Implement manual control over the rendering process in tree view. This
  includes clearing the rendering queue and currently rendered nodes
  before updates, aligning the rendering process with the updated
  collection.
- Add Cypress E2E tests to test switching between all operating systems
  and script views, ensuring no uncaught errors and preventing
  regression.
- Replace hardcoded operating system lists in the download URL list view
  with a unified `getSupportedOsList()` method from the application,
  reducing duplication and simplifying future updates.
- Rename `initial-nodes` to `nodes` in `TreeView.vue` to reflect their
  mutable nature.
- Centralize the function for getting operating system names into
  `OperatingSystemNames.ts`, improving reusability in E2E tests.
This commit is contained in:
undergroundwires
2023-12-14 09:51:42 +01:00
parent fe3de498c8
commit 3457fe18cf
13 changed files with 285 additions and 76 deletions

View File

@@ -0,0 +1,24 @@
import {
describe, it, expect,
} from 'vitest';
import { ApplicationFactory } from '@/application/ApplicationFactory';
import { getOperatingSystemDisplayName } from '@/presentation/components/Shared/OperatingSystemNames';
import { OperatingSystem } from '@/domain/OperatingSystem';
describe('OperatingSystemNames', () => {
describe('getOperatingSystemDisplayName', () => {
describe('retrieving display names for supported operating systems', async () => {
// arrange
const application = await ApplicationFactory.Current.getApp();
const supportedOperatingSystems = application.getSupportedOsList();
supportedOperatingSystems.forEach((supportedOperatingSystem) => {
it(`should return a non-empty name for ${OperatingSystem[supportedOperatingSystem]}`, () => {
// act
const displayName = getOperatingSystemDisplayName(supportedOperatingSystem);
// assert
expect(displayName).to.have.length.greaterThanOrEqual(1);
});
});
});
});
});

View File

@@ -9,23 +9,43 @@ import { provideDependencies } from '@/presentation/bootstrapping/DependencyProv
import { ApplicationContextStub } from '@tests/unit/shared/Stubs/ApplicationContextStub';
describe('TreeView', () => {
it('should render all provided root nodes correctly', async () => {
it('renders all provided root nodes correctly', async () => {
// arrange
const nodes = createSampleNodes();
const wrapper = createTreeViewWrapper(nodes);
// act
await waitForStableDom(wrapper.element);
// assert
const expectedTotalRootNodes = nodes.length;
expect(wrapper.findAll('.node').length).to.equal(expectedTotalRootNodes, wrapper.html());
const rootNodeTexts = nodes.map((node) => node.data.label);
const rootNodeTexts = nodes.map((node) => (node.data as TreeInputMetadata).label);
rootNodeTexts.forEach((label) => {
expect(wrapper.text()).to.include(label);
});
});
// Regression test for a bug where updating the nodes prop caused uncaught exceptions.
it('updates nodes correctly when props change', async () => {
// arrange
const firstNodeLabel = 'Node 1';
const secondNodeLabel = 'Node 2';
const initialNodes: TreeInputNodeDataWithMetadata[] = [{ id: 'node1', data: { label: firstNodeLabel } }];
const updatedNodes: TreeInputNodeDataWithMetadata[] = [{ id: 'node2', data: { label: secondNodeLabel } }];
const wrapper = createTreeViewWrapper(initialNodes);
// act
await wrapper.setProps({ nodes: updatedNodes });
await waitForStableDom(wrapper.element);
// assert
expect(wrapper.text()).toContain(secondNodeLabel);
expect(wrapper.text()).not.toContain(firstNodeLabel);
});
});
function createTreeViewWrapper(initialNodeData: readonly TreeInputNodeData[]) {
function createTreeViewWrapper(initialNodeData: readonly TreeInputNodeDataWithMetadata[]) {
return mount(defineComponent({
components: {
TreeView,
@@ -33,26 +53,34 @@ function createTreeViewWrapper(initialNodeData: readonly TreeInputNodeData[]) {
setup() {
provideDependencies(new ApplicationContextStub());
const initialNodes = shallowRef(initialNodeData);
const nodes = shallowRef(initialNodeData);
const selectedLeafNodeIds = shallowRef<readonly string[]>([]);
return {
initialNodes,
nodes,
selectedLeafNodeIds,
};
},
template: `
<TreeView
:initialNodes="initialNodes"
:selectedLeafNodeIds="selectedLeafNodeIds"
>
<template v-slot:node-content="{ nodeMetadata }">
{{ nodeMetadata.label }}
</template>
</TreeView>`,
<TreeView
:nodes="nodes"
:selectedLeafNodeIds="selectedLeafNodeIds"
>
<template v-slot:node-content="{ nodeMetadata }">
{{ nodeMetadata.label }}
</template>
</TreeView>
`,
}));
}
function createSampleNodes() {
interface TreeInputMetadata {
readonly label: string;
}
type TreeInputNodeDataWithMetadata = TreeInputNodeData & { readonly data?: TreeInputMetadata };
function createSampleNodes(): TreeInputNodeDataWithMetadata[] {
return [
{
id: 'root1',
@@ -93,7 +121,7 @@ function createSampleNodes() {
function waitForStableDom(rootElement, timeout = 3000, interval = 200): Promise<void> {
return new Promise((resolve, reject) => {
let lastTimeoutId;
let lastTimeoutId: ReturnType<typeof setTimeout>;
const observer = new MutationObserver(() => {
if (lastTimeoutId) {
clearTimeout(lastTimeoutId);