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
Original file line number Diff line number Diff line change
Expand Up @@ -3988,6 +3988,43 @@ rawData
sortOrder: 30,
});

export const EMPTY_PAGE_DOCUMENT_TYPE_ID = 'e1a7c3f4-9b62-4d18-8a5c-0f3e6d2b7c91';

// A property-less document type used to build a deep and wide document tree, see page-tree.data.ts.
rawData.push({
allowedTemplates: [],
defaultTemplate: null,
id: EMPTY_PAGE_DOCUMENT_TYPE_ID,
alias: 'emptyPage',
name: 'Empty Page',
description: null,
icon: 'icon-document',
allowedAsRoot: true,
variesByCulture: false,
variesBySegment: false,
isElement: false,
hasChildren: false,
parent: null,
isFolder: false,
properties: [],
containers: [],
allowedDocumentTypes: [
{
documentType: {
id: EMPTY_PAGE_DOCUMENT_TYPE_ID,
},
sortOrder: 0,
},
],
compositions: [],
cleanup: {
preventCleanup: false,
keepAllVersionsNewerThanDays: null,
keepLatestVersionPerDayForDays: null,
},
flags: [],
});

export const data: Array<UmbMockDocumentTypeModel> = rawData.map((dt) => ({
...dt,
compositions: dt.compositions.map((c) => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { UmbMockDocumentModel } from '../../mock-data-set.types.js';
import { data as pageTree } from './page-tree.data.js';
import type { DocumentVariantResponseModel } from '@umbraco-cms/backoffice/external/backend-api';

type UmbDocumentVariantState = DocumentVariantResponseModel['state'];
Expand Down Expand Up @@ -2609,4 +2610,5 @@ export const data: Array<UmbMockDocumentModel> = [
values: allDataTypesValues,
flags: [],
},
...pageTree,
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { UmbMockDocumentModel } from '../../mock-data-set.types.js';
import { EMPTY_PAGE_DOCUMENT_TYPE_ID } from './document-type.data.js';
import type { DocumentVariantResponseModel } from '@umbraco-cms/backoffice/external/backend-api';

type UmbDocumentVariantState = DocumentVariantResponseModel['state'];

// A large document tree for exercising tree pagination and deep expansion:
// "Page 1" sits in the root with 1000 children, and along the first branch (Page 1 > Page 1.1 >
// Page 1.1.1 > ...) the ten first children of each page get 500 children of their own, down to
// level 5. Names follow the position in the tree: Page 1.2.10.
const ROOT_CHILD_COUNT = 1000;
const BRANCH_CHILD_COUNT = 500;
const BRANCHING_SIBLING_COUNT = 10;
const MAX_DEPTH = 5;

const DATE = '2026-01-05 09:00:00';

const documents: Array<UmbMockDocumentModel> = [];

const pageId = (path: Array<number>) => `empty-page-${path.join('-')}`;

const addPage = (path: Array<number>, ancestorIds: Array<string>, hasChildren: boolean) => {
const id = pageId(path);
const name = `Page ${path.join('.')}`;
const parentId = ancestorIds[ancestorIds.length - 1];

documents.push({
ancestors: ancestorIds.map((ancestorId) => ({ id: ancestorId })),
template: null,
id,
createDate: DATE,
parent: parentId ? { id: parentId } : null,
documentType: {
id: EMPTY_PAGE_DOCUMENT_TYPE_ID,
icon: 'icon-document',
},
hasChildren,
noAccess: false,
isProtected: false,
isTrashed: false,
variants: [
{
state: 'Published' as UmbDocumentVariantState,
publishDate: DATE,
culture: null,
segment: null,
name,
createDate: DATE,
updateDate: DATE,
id,
flags: [],
},
],
values: [],
flags: [],
});
};

const addChildren = (parentPath: Array<number>, parentAncestorIds: Array<string>, count: number) => {
const ancestorIds = [...parentAncestorIds, pageId(parentPath)];
const depth = parentPath.length + 1;
// Only the first branch keeps nesting, so a page has children of its own when its parent is on
// that branch, it is one of the first siblings, and we are not at the deepest level yet.
const parentIsOnFirstBranch = parentPath.every((segment) => segment === 1);
const childrenBranch = parentIsOnFirstBranch && depth < MAX_DEPTH;

for (let index = 1; index <= count; index++) {
const path = [...parentPath, index];
const hasChildren = childrenBranch && index <= BRANCHING_SIBLING_COUNT;

addPage(path, ancestorIds, hasChildren);

if (hasChildren) {
addChildren(path, ancestorIds, BRANCH_CHILD_COUNT);
}
}
};

addPage([1], [], true);
addChildren([1], [], ROOT_CHILD_COUNT);

export const data: Array<UmbMockDocumentModel> = documents;
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { expect } from '@open-wc/testing';
import { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import { aTimeout, expect } from '@open-wc/testing';
import { firstValueFrom, Observable, of } from '@umbraco-cms/backoffice/external/rxjs';
import { customElement } from '@umbraco-cms/backoffice/external/lit';
import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
import { UmbTreeItemActiveManager } from './tree-active-manager';

@customElement('test-my-controller-host')
Expand All @@ -17,10 +18,82 @@
manager = new UmbTreeItemActiveManager(hostElement);
});

describe('isCurrentLocation', () => {
const currentPath = () => window.location.pathname;

it('is true while the browser location points at the path', async () => {

Check failure on line 24 in src/Umbraco.Web.UI.Client/src/packages/core/tree/active-manager/tree-active-manager.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add at least one assertion to this test case.

See more on https://sonarcloud.io/project/issues?id=umbraco_Umbraco-CMS&issues=AZ_bKJjHHDXuILNlfTPX&open=AZ_bKJjHHDXuILNlfTPX&pullRequest=23585
const isActive = await firstValueFrom(manager.isCurrentLocation(of(currentPath())));
expect(isActive).to.be.true;
});

it('is false for a path the browser is not on', async () => {

Check failure on line 29 in src/Umbraco.Web.UI.Client/src/packages/core/tree/active-manager/tree-active-manager.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add at least one assertion to this test case.

See more on https://sonarcloud.io/project/issues?id=umbraco_Umbraco-CMS&issues=AZ_bKJjHHDXuILNlfTPY&open=AZ_bKJjHHDXuILNlfTPY&pullRequest=23585
const isActive = await firstValueFrom(manager.isCurrentLocation(of('/some/other/path')));
expect(isActive).to.be.false;
});

it('is false for an empty path, so an unresolved item never matches', async () => {

Check failure on line 34 in src/Umbraco.Web.UI.Client/src/packages/core/tree/active-manager/tree-active-manager.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add at least one assertion to this test case.

See more on https://sonarcloud.io/project/issues?id=umbraco_Umbraco-CMS&issues=AZ_bKJjHHDXuILNlfTPZ&open=AZ_bKJjHHDXuILNlfTPZ&pullRequest=23585
const isActive = await firstValueFrom(manager.isCurrentLocation(of('')));
expect(isActive).to.be.false;
});

it('does not match a sibling path that shares a prefix', async () => {

Check failure on line 39 in src/Umbraco.Web.UI.Client/src/packages/core/tree/active-manager/tree-active-manager.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add at least one assertion to this test case.

See more on https://sonarcloud.io/project/issues?id=umbraco_Umbraco-CMS&issues=AZ_bKJjHHDXuILNlfTPa&open=AZ_bKJjHHDXuILNlfTPa&pullRequest=23585
const isActive = await firstValueFrom(manager.isCurrentLocation(of(currentPath() + '-2')));
expect(isActive).to.be.false;
});

it('re-evaluates when the path changes', async () => {
const path = new UmbStringState('/some/other/path');
const values: Array<boolean> = [];
const subscription = manager.isCurrentLocation(path.asObservable()).subscribe((v) => values.push(v));

path.setValue(currentPath());
subscription.unsubscribe();

expect(values).to.deep.equal([false, true]);
});
});

describe('navigationend listener', () => {
// This manager owns a listener on a global object, so it has to be bound to the host lifecycle.
// A leak here is invisible until the tab has thousands of stale listeners.
it('is added while the host is connected and removed again when it disconnects', async () => {
let added = 0;
let removed = 0;
const originalAdd = window.addEventListener;
const originalRemove = window.removeEventListener;
window.addEventListener = function (this: Window, type: string, listener: any, options?: any) {
if (type === 'navigationend') added++;
return originalAdd.call(this, type, listener, options);
} as typeof window.addEventListener;
window.removeEventListener = function (this: Window, type: string, listener: any, options?: any) {
if (type === 'navigationend') removed++;
return originalRemove.call(this, type, listener, options);
} as typeof window.removeEventListener;

try {
const hostElement = new UmbTestControllerHostElement();
document.body.appendChild(hostElement);
new UmbTreeItemActiveManager(hostElement);
await aTimeout(0);

expect(added).to.equal(1);
expect(removed).to.equal(0);

hostElement.remove();
await aTimeout(0);

expect(removed).to.equal(1);
} finally {
window.addEventListener = originalAdd;
window.removeEventListener = originalRemove;
}
});

Check warning on line 90 in src/Umbraco.Web.UI.Client/src/packages/core/tree/active-manager/tree-active-manager.test.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (v17/dev)

❌ New issue: Excess Number of Function Arguments

'is added while the host is connected and removed again when it disconnects' has 8 arguments, max arguments = 4 This function has too many arguments, indicating a lack of encapsulation. Avoid adding more arguments.
});

describe('Public API', () => {
describe('properties', () => {
it('has an active property', () => {
expect(manager).to.have.property('active').to.be.an.instanceOf(Observable);
it('has an activeTrail property', () => {
expect(manager).to.have.property('activeTrail').to.be.an.instanceOf(Observable);
});
});

Expand All @@ -33,7 +106,7 @@

describe('isActive', () => {
it('checks if an item is active', (done) => {
manager.setActive([item]);
manager.setActiveTrail([item]);
const isActive = manager.isActive(item);
expect(isActive).to.be.an.instanceOf(Observable);
manager.isActive(item).subscribe((value) => {
Expand All @@ -43,26 +116,52 @@
});
});

describe('setActive & getActive', () => {
it('sets and gets the active state', () => {
const active = [item];
manager.setActive(active);
expect(manager.getActive()).to.deep.equal(active);
describe('setActiveTrail & getActiveTrail', () => {
it('sets and gets the active trail', () => {
const trail = [item];
manager.setActiveTrail(trail);
expect(manager.getActiveTrail()).to.deep.equal(trail);
});
});

describe('removeActiveIfMatch', () => {
it('removes the active state', () => {
const active = [item];
manager.setActive(active);
manager.removeActiveIfMatch(active);
expect(manager.getActive()).to.deep.equal([]);
});
it('does not remove the active state if it does not match', () => {
const active = [item];
manager.setActive(active);
manager.removeActiveIfMatch([item2]);
describe('removeActiveTrailIfMatch', () => {
it('removes the active trail', () => {
const trail = [item];
manager.setActiveTrail(trail);
manager.removeActiveTrailIfMatch(trail);
expect(manager.getActiveTrail()).to.deep.equal([]);
});
it('does not remove the active trail if it does not match', () => {
const trail = [item];
manager.setActiveTrail(trail);
manager.removeActiveTrailIfMatch([item2]);
expect(manager.getActiveTrail()).to.deep.equal([item]);
});
});

describe('deprecated members', () => {
it('setActive delegates to setActiveTrail', () => {
manager.setActive([item]);
expect(manager.getActiveTrail()).to.deep.equal([item]);
});

it('getActive delegates to getActiveTrail', () => {
manager.setActiveTrail([item]);
expect(manager.getActive()).to.deep.equal([item]);
});

it('removeActiveIfMatch delegates to removeActiveTrailIfMatch', () => {
manager.setActiveTrail([item]);
manager.removeActiveIfMatch([item]);
expect(manager.getActiveTrail()).to.deep.equal([]);
});

it('active emits the same values as activeTrail', (done) => {
manager.setActiveTrail([item]);
manager.active.subscribe((value) => {
expect(value).to.deep.equal([item]);
done();
});
});
});
});
Loading
Loading