fix: Various editor / diagnostic improvements (#2363)

- Closes: #2360 
 - Closes: #2328
This commit is contained in:
Dermot Duffy
2026-02-21 09:31:10 -08:00
committed by GitHub
parent 6d5a76ac0a
commit df3614f793
13 changed files with 313 additions and 58 deletions
@@ -1,13 +1,14 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { afterEach, assert, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { CardElementManager } from '../../src/card-controller/card-element-manager';
import { StateWatcher } from '../../src/card-controller/hass/state-watcher';
import { QueryResults } from '../../src/view/query-results';
import { View } from '../../src/view/view';
import {
callStateWatcherCallback,
createCardAPI,
createCardHTMLElement,
createConfig,
createLitElement,
createStateEntity,
createView,
TestViewMedia,
@@ -20,7 +21,7 @@ describe('CardElementManager', () => {
});
it('should get element', () => {
const element = createLitElement();
const element = createCardHTMLElement();
const manager = new CardElementManager(
createCardAPI(),
element,
@@ -35,7 +36,7 @@ describe('CardElementManager', () => {
const callback = vi.fn();
const manager = new CardElementManager(
createCardAPI(),
createLitElement(),
createCardHTMLElement(),
callback,
() => undefined,
);
@@ -49,7 +50,7 @@ describe('CardElementManager', () => {
const callback = vi.fn();
const manager = new CardElementManager(
createCardAPI(),
createLitElement(),
createCardHTMLElement(),
() => undefined,
callback,
);
@@ -60,7 +61,7 @@ describe('CardElementManager', () => {
});
it('should update', () => {
const element = createLitElement();
const element = createCardHTMLElement();
const manager = new CardElementManager(
createCardAPI(),
element,
@@ -73,7 +74,7 @@ describe('CardElementManager', () => {
});
it('should get hasUpdated', () => {
const element = createLitElement();
const element = createCardHTMLElement();
element.hasUpdated = true;
const manager = new CardElementManager(
createCardAPI(),
@@ -89,7 +90,7 @@ describe('CardElementManager', () => {
const windowAddEventListener = vi.spyOn(global.window, 'addEventListener');
const addEventListener = vi.fn();
const element = createLitElement();
const element = createCardHTMLElement();
element.addEventListener = addEventListener;
const api = createCardAPI();
@@ -132,6 +133,10 @@ describe('CardElementManager', () => {
);
expect(windowAddEventListener).toBeCalledWith('location-changed', expect.anything());
expect(windowAddEventListener).toBeCalledWith('popstate', expect.anything());
expect(windowAddEventListener).toBeCalledWith(
'advanced-camera-card:editor:diagnostics',
expect.anything(),
);
expect(api.getInteractionManager().initialize).toBeCalled();
expect(api.getFullscreenManager().initialize).toBeCalled();
@@ -143,7 +148,7 @@ describe('CardElementManager', () => {
it('should disconnect', () => {
const windowRemoveEventListener = vi.spyOn(global.window, 'removeEventListener');
const element = createLitElement();
const element = createCardHTMLElement();
element.setAttribute('panel', '');
element.setAttribute('casted', '');
@@ -195,6 +200,10 @@ describe('CardElementManager', () => {
expect.anything(),
);
expect(windowRemoveEventListener).toBeCalledWith('popstate', expect.anything());
expect(windowRemoveEventListener).toBeCalledWith(
'advanced-camera-card:editor:diagnostics',
expect.anything(),
);
expect(api.getMediaLoadedInfoManager().clear).toBeCalled();
expect(api.getFullscreenManager().disconnect).toBeCalled();
@@ -217,7 +226,7 @@ describe('CardElementManager', () => {
const stateWatcher = mock<StateWatcher>();
vi.mocked(api.getHASSManager().getStateWatcher).mockReturnValue(stateWatcher);
const element = createLitElement();
const element = createCardHTMLElement();
const manager = new CardElementManager(
api,
element,
@@ -245,7 +254,7 @@ describe('CardElementManager', () => {
const stateWatcher = mock<StateWatcher>();
vi.mocked(api.getHASSManager().getStateWatcher).mockReturnValue(stateWatcher);
const element = createLitElement();
const element = createCardHTMLElement();
const manager = new CardElementManager(
api,
element,
@@ -275,7 +284,7 @@ describe('CardElementManager', () => {
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
const element = createLitElement();
const element = createCardHTMLElement();
const manager = new CardElementManager(
api,
element,
@@ -310,7 +319,7 @@ describe('CardElementManager', () => {
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
const element = createLitElement();
const element = createCardHTMLElement();
const manager = new CardElementManager(
api,
element,
@@ -334,4 +343,95 @@ describe('CardElementManager', () => {
expect(element.requestUpdate).not.toBeCalled();
});
});
describe('should handle diagnostics', () => {
afterEach(() => {
vi.clearAllMocks();
});
const createDialogWithCard = (element: HTMLElement) => {
const dialog = document.createElement('hui-dialog-edit-card');
dialog.attachShadow({ mode: 'open' });
assert(dialog.shadowRoot);
dialog.shadowRoot.append(element);
return dialog;
};
const fireFromDialog = (dialog: HTMLElement) => {
const editorDiv = document.createElement('div');
assert(dialog.shadowRoot);
dialog.shadowRoot.append(editorDiv);
editorDiv.dispatchEvent(
new CustomEvent('advanced-camera-card:editor:diagnostics', {
bubbles: true,
composed: true,
}),
);
};
it('sets view to diagnostics if card is in editor', () => {
const api = createCardAPI();
const element = createCardHTMLElement();
const manager = new CardElementManager(
api,
element,
() => undefined,
() => undefined,
);
const dialog = createDialogWithCard(element);
document.body.append(dialog);
manager.elementConnected();
fireFromDialog(dialog);
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
params: { view: 'diagnostics' },
});
});
it('resets to default view if already in diagnostics view', () => {
const api = createCardAPI();
const element = createCardHTMLElement();
const manager = new CardElementManager(
api,
element,
() => undefined,
() => undefined,
);
vi.mocked(api.getViewManager().getView).mockReturnValue(
new View({ view: 'diagnostics' }),
);
const dialog = createDialogWithCard(element);
document.body.append(dialog);
manager.elementConnected();
fireFromDialog(dialog);
expect(api.getViewManager().setViewDefault).toBeCalled();
});
it('does not set view to diagnostics if card is not in editor', () => {
const api = createCardAPI();
const element = createCardHTMLElement();
const manager = new CardElementManager(
api,
element,
() => undefined,
() => undefined,
);
manager.elementConnected();
// Event fired from a different dialog that does not contain the card
const otherDialog = document.createElement('hui-dialog-edit-card');
otherDialog.attachShadow({ mode: 'open' });
document.body.append(otherDialog);
fireFromDialog(otherDialog);
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
});
});
});
+11 -1
View File
@@ -24,7 +24,10 @@ import {
import { ActionsManager } from '../src/card-controller/actions/actions-manager';
import { AutomationsManager } from '../src/card-controller/automations-manager';
import { CameraURLManager } from '../src/card-controller/camera-url-manager';
import { CardElementManager } from '../src/card-controller/card-element-manager';
import {
CardElementManager,
CardHTMLElement,
} from '../src/card-controller/card-element-manager';
import { ConfigManager } from '../src/card-controller/config/config-manager';
import { CardController } from '../src/card-controller/controller';
import { DefaultManager } from '../src/card-controller/default-manager';
@@ -626,6 +629,13 @@ export const createParent = (options?: { children?: HTMLElement[] }): HTMLElemen
return parent;
};
export const createCardHTMLElement = (): CardHTMLElement => {
const element = createLitElement() as CardHTMLElement;
element.getCardSize = vi.fn();
element.setConfig = vi.fn();
return element;
};
export const createLitElement = (): LitElement => {
const element = document.createElement('div') as unknown as LitElement;
element.addController = vi.fn();
+91
View File
@@ -0,0 +1,91 @@
import { assert, describe, expect, it } from 'vitest';
import { isAncestorInEventPath } from '../../src/utils/event-ancestor';
// @vitest-environment jsdom
describe('isAncestorInEventPath', () => {
const dispatch = (source: EventTarget, listener: (ev: Event) => void) => {
window.addEventListener('test', listener, { once: true });
source.dispatchEvent(new CustomEvent('test', { bubbles: true, composed: true }));
};
it('returns true when element shares the ancestor with the event source', () => {
const element = document.createElement('div');
const ancestor = document.createElement('hui-dialog-edit-card');
ancestor.attachShadow({ mode: 'open' });
assert(ancestor.shadowRoot);
ancestor.shadowRoot.append(element);
document.body.append(ancestor);
const eventSource = document.createElement('div');
ancestor.shadowRoot.append(eventSource);
let result: boolean | undefined;
dispatch(eventSource, (ev) => {
result = isAncestorInEventPath(element, ev, 'hui-dialog-edit-card');
});
expect(result).toBe(true);
});
it('returns false when element and event source are in different ancestors with the same tag', () => {
const element = document.createElement('div');
const ancestor1 = document.createElement('hui-dialog-edit-card');
ancestor1.attachShadow({ mode: 'open' });
assert(ancestor1.shadowRoot);
ancestor1.shadowRoot.append(element);
document.body.append(ancestor1);
const ancestor2 = document.createElement('hui-dialog-edit-card');
ancestor2.attachShadow({ mode: 'open' });
assert(ancestor2.shadowRoot);
const eventSource = document.createElement('div');
ancestor2.shadowRoot.append(eventSource);
document.body.append(ancestor2);
let result: boolean | undefined;
dispatch(eventSource, (ev) => {
result = isAncestorInEventPath(element, ev, 'hui-dialog-edit-card');
});
expect(result).toBe(false);
});
it('returns false when element has no ancestor with the given tag', () => {
const element = document.createElement('div');
document.body.append(element);
const eventSource = document.createElement('div');
document.body.append(eventSource);
let result: boolean | undefined;
dispatch(eventSource, (ev) => {
result = isAncestorInEventPath(element, ev, 'hui-dialog-edit-card');
});
expect(result).toBe(false);
});
it('traverses shadow boundaries in the element ancestor chain', () => {
const element = document.createElement('div');
const inner = document.createElement('hui-card');
inner.attachShadow({ mode: 'open' });
assert(inner.shadowRoot);
inner.shadowRoot.append(element);
const ancestor = document.createElement('hui-dialog-edit-card');
ancestor.attachShadow({ mode: 'open' });
assert(ancestor.shadowRoot);
ancestor.shadowRoot.append(inner);
document.body.append(ancestor);
const eventSource = document.createElement('div');
ancestor.shadowRoot.append(eventSource);
let result: boolean | undefined;
dispatch(eventSource, (ev) => {
result = isAncestorInEventPath(element, ev, 'hui-dialog-edit-card');
});
expect(result).toBe(true);
});
});