diff --git a/src/app/components/event-tab/event-tab.component.html b/src/app/components/event-tab/event-tab.component.html index b45f9634..eb16893f 100644 --- a/src/app/components/event-tab/event-tab.component.html +++ b/src/app/components/event-tab/event-tab.component.html @@ -24,7 +24,7 @@ @if (isTraceView()) {

Trace

} - @if (traceData()) { + @if (traceData().length > 0) { Events Trace diff --git a/src/app/components/event-tab/event-tab.component.spec.ts b/src/app/components/event-tab/event-tab.component.spec.ts index b8b5e2ad..65cfaa3d 100644 --- a/src/app/components/event-tab/event-tab.component.spec.ts +++ b/src/app/components/event-tab/event-tab.component.spec.ts @@ -15,31 +15,234 @@ * limitations under the License. */ +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; import {ComponentFixture, TestBed} from '@angular/core/testing'; -import {MatDialogModule, MatDialogRef} from '@angular/material/dialog'; +import {MatButtonToggleHarness} from '@angular/material/button-toggle/testing'; +import {MatDialog} from '@angular/material/dialog'; +import {MatListHarness} from '@angular/material/list/testing'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {Span} from '../../core/models/Trace'; import {EventTabComponent} from './event-tab.component'; +import {TraceChartComponent} from './trace-chart/trace-chart.component'; + +const MOCK_TRACE_DATA: Span[] = [ + { + name: 'agent.act', + start_time: 1733084700000000000, + end_time: 1733084760000000000, + span_id: 'span-1', + trace_id: 'trace-1', + attributes: { + 'event_id': 1, + 'gcp.vertex.agent.invocation_id': '21332-322222', + 'gcp.vertex.agent.llm_request': + '{"contents":[{"role":"user","parts":[{"text":"Hello"}]},{"role":"agent","parts":[{"text":"Hi. What can I help you with?"}]},{"role":"user","parts":[{"text":"I need help with my project."}]}]}', + }, + }, + { + name: 'tool.invoke', + start_time: 1733084705000000000, + end_time: 1733084755000000000, + span_id: 'span-2', + parent_span_id: 'span-1', + trace_id: 'trace-1', + attributes: { + 'tool_name': 'project_helper', + }, + children: [ + { + name: 'sub-tool-1.invoke', + start_time: 1733084710000000000, + end_time: 1733084750000000000, + span_id: 'span-3', + parent_span_id: 'span-2', + trace_id: 'trace-1', + attributes: { + 'sub_tool_name': 'sub_project_helper_1', + }, + children: [ + { + name: 'sub-tool-2.invoke', + start_time: 1733084715000000000, + end_time: 1733084745000000000, + span_id: 'span-4', + parent_span_id: 'span-3', + trace_id: 'trace-1', + attributes: { + 'sub_tool_name': 'sub_project_helper_2', + }, + children: [ + { + name: 'sub-tool-3.invoke', + start_time: 1733084720000000000, + end_time: 1733084740000000000, + span_id: 'span-5', + parent_span_id: 'span-4', + trace_id: 'trace-1', + attributes: { + 'sub_tool_name': 'sub_project_helper_3', + }, + children: [], + }, + ], + }, + ], + }, + ], + } +] as Span[]; + +const MOCK_EVENTS_MAP = new Map([ + ['event1', {title: 'Event 1 Title'}], + ['event2', {title: 'Event 2 Title'}], +]); describe('EventTabComponent', () => { let component: EventTabComponent; let fixture: ComponentFixture; - const mockDialogRef = { - close: jasmine.createSpy('close'), - }; + let loader: HarnessLoader; + const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']); beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [MatDialogModule, EventTabComponent], - providers: [{ provide: MatDialogRef, useValue: mockDialogRef }], -}).compileComponents(); + await TestBed + .configureTestingModule({ + imports: [EventTabComponent, NoopAnimationsModule], + providers: [{provide: MatDialog, useValue: matDialogSpy}], + }) + .compileComponents(); fixture = TestBed.createComponent(EventTabComponent); component = fixture.componentInstance; + loader = TestbedHarnessEnvironment.loader(fixture); + matDialogSpy.open.calls.reset(); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should display "No conversations" if eventsMap is empty', () => { + expect(fixture.nativeElement.textContent).toContain('No conversations'); + }); + + describe('with events', () => { + beforeEach(async () => { + fixture.componentRef.setInput('eventsMap', MOCK_EVENTS_MAP); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('should display events list by default', async () => { + const list = await loader.getHarness(MatListHarness); + const items = await list.getItems(); + expect(items.length).toBe(2); + expect(await items[0].getFullText()).toContain('Event 1 Title'); + expect(await items[1].getFullText()).toContain('Event 2 Title'); + }); + + it('should emit selectedEvent on event click', async () => { + spyOn(component.selectedEvent, 'emit'); + const list = await loader.getHarness(MatListHarness); + const items = await list.getItems(); + await (await items[0].host()).click(); + expect(component.selectedEvent.emit).toHaveBeenCalledWith('event1'); + }); + + it('should not show toggle if traceData is empty', async () => { + const hasToggleGroup = fixture.nativeElement.querySelector( + 'mat-button-toggle-group', + ); + expect(hasToggleGroup).toBeNull(); + }); + }); + + describe('with trace data', () => { + beforeEach(async () => { + fixture.componentRef.setInput('eventsMap', MOCK_EVENTS_MAP); + fixture.componentRef.setInput('traceData', MOCK_TRACE_DATA); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('should show toggle buttons', async () => { + const toggles = await loader.getAllHarnesses(MatButtonToggleHarness); + expect(toggles.length).toBe(2); + expect(await toggles[0].getText()).toBe('Events'); + expect(await toggles[1].getText()).toBe('Trace'); + }); + + it('should switch to trace view and display traces', async () => { + const traceToggle = await loader.getHarness( + MatButtonToggleHarness.with({text: 'Trace'}), + ); + await traceToggle.check(); + fixture.detectChanges(); + + const list = await loader.getHarness(MatListHarness); + const items = await list.getItems(); + expect(items.length).toBe(1); + expect(await items[0].getFullText()).toContain('Invocation 21332-322222'); + }); + + it('should open dialog when trace item is clicked', async () => { + const traceToggle = await loader.getHarness( + MatButtonToggleHarness.with({text: 'Trace'}), + ); + await traceToggle.check(); + fixture.detectChanges(); + + const list = await loader.getHarness(MatListHarness); + const items = await list.getItems(); + await (await items[0].host()).click(); + + expect(matDialogSpy.open).toHaveBeenCalledWith(TraceChartComponent, { + width: 'auto', + maxWidth: '90vw', + data: { + spans: component.spansByTraceId().get('trace-1'), + invocId: '21332-322222', + }, + }); + }); + + it('should display multiple traces if present', async () => { + const MOCK_TRACE_DATA_WITH_MULTIPLE_TRACES: Span[] = [ + ...MOCK_TRACE_DATA, + { + name: 'agent.act-2', + start_time: 1733084700000000000, + end_time: 1733084760000000000, + span_id: 'span-10', + trace_id: 'trace-2', + attributes: { + 'event_id': 10, + 'gcp.vertex.agent.invocation_id': 'invoc-2', + 'gcp.vertex.agent.llm_request': '{}', + }, + }, + ]; + fixture.componentRef.setInput( + 'traceData', + MOCK_TRACE_DATA_WITH_MULTIPLE_TRACES, + ); + fixture.detectChanges(); + await fixture.whenStable(); + + const traceToggle = await loader.getHarness( + MatButtonToggleHarness.with({text: 'Trace'}), + ); + await traceToggle.check(); + fixture.detectChanges(); + + const list = await loader.getHarness(MatListHarness); + const items = await list.getItems(); + expect(items.length).toBe(2); + expect(await items[0].getFullText()).toContain('Invocation 21332-322222'); + expect(await items[1].getFullText()).toContain('Invocation invoc-2'); + }); + }); }); diff --git a/src/app/components/event-tab/event-tab.component.ts b/src/app/components/event-tab/event-tab.component.ts index 9ddaa7c9..f52a4825 100644 --- a/src/app/components/event-tab/event-tab.component.ts +++ b/src/app/components/event-tab/event-tab.component.ts @@ -49,7 +49,7 @@ export class EventTabComponent { readonly view = signal('events'); readonly isTraceView = computed(() => this.view() === 'trace'); readonly spansByTraceId = computed(() => { - if (!this.traceData || this.traceData.length == 0) { + if (!this.traceData() || this.traceData().length == 0) { return new Map(); } return this.traceData().reduce((map, span) => { @@ -66,12 +66,6 @@ export class EventTabComponent { }, new Map()); }); - showJson: boolean[] = Array(this.eventsMap().size).fill(false); - - toggleJson(index: number) { - this.showJson[index] = !this.showJson[index]; - } - selectEvent(key: string) { this.selectedEvent.emit(key); } diff --git a/src/app/components/session-tab/session-tab.component.ts b/src/app/components/session-tab/session-tab.component.ts index e396ff5a..203fe6a2 100644 --- a/src/app/components/session-tab/session-tab.component.ts +++ b/src/app/components/session-tab/session-tab.component.ts @@ -15,13 +15,14 @@ * limitations under the License. */ -import {Component, EventEmitter, Input, OnInit, Output, Inject} from '@angular/core'; +import {NgClass} from '@angular/common'; +import {ChangeDetectorRef, Component, EventEmitter, Inject, inject, Input, OnInit, Output} from '@angular/core'; import {MatDialog} from '@angular/material/dialog'; import {Subject} from 'rxjs'; import {switchMap} from 'rxjs/operators'; + import {Session} from '../../core/models/Session'; -import {SessionService, SESSION_SERVICE} from '../../core/services/session.service'; -import { NgClass } from '@angular/common'; +import {SESSION_SERVICE, SessionService} from '../../core/services/session.service'; @Component({ selector: 'app-session-tab', @@ -40,6 +41,7 @@ export class SessionTabComponent implements OnInit { sessionList: any[] = []; private refreshSessionsSubject = new Subject(); + private readonly changeDetectorRef = inject(ChangeDetectorRef); constructor( @Inject(SESSION_SERVICE) private sessionService: SessionService, @@ -58,6 +60,7 @@ export class SessionTabComponent implements OnInit { Number(b.lastUpdateTime) - Number(a.lastUpdateTime), ); this.sessionList = res; + this.changeDetectorRef.detectChanges(); }); } diff --git a/src/app/components/trace-tab/trace-tab.component.spec.ts b/src/app/components/trace-tab/trace-tab.component.spec.ts index 086d4ada..23ee90e5 100644 --- a/src/app/components/trace-tab/trace-tab.component.spec.ts +++ b/src/app/components/trace-tab/trace-tab.component.spec.ts @@ -14,27 +14,136 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import {HarnessLoader} from '@angular/cdk/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatExpansionPanelHarness} from '@angular/material/expansion/testing'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {Span} from '../../core/models/Trace'; +import {TRACE_SERVICE} from '../../core/services/trace.service'; +import {MockTraceService} from './../../core/services/testing/mock-trace.service'; import {TraceTabComponent} from './trace-tab.component'; +const MOCK_TRACE_DATA: Span[] = [ + { + name: 'agent.act', + start_time: 1733084700000000000, + end_time: 1733084760000000000, + span_id: 'span-1', + trace_id: 'trace-1', + attributes: { + 'event_id': 1, + 'gcp.vertex.agent.invocation_id': '21332-322222', + 'gcp.vertex.agent.llm_request': + '{"contents":[{"role":"user","parts":[{"text":"Hello"}]},{"role":"agent","parts":[{"text":"Hi. What can I help you with?"}]},{"role":"user","parts":[{"text":"I need help with my project."}]}]}', + }, + }, + { + name: 'tool.invoke', + start_time: 1733084705000000000, + end_time: 1733084755000000000, + span_id: 'span-2', + parent_span_id: 'span-1', + trace_id: 'trace-1', + attributes: { + 'tool_name': 'project_helper', + }, + }, +]; + describe('TraceTabComponent', () => { let component: TraceTabComponent; let fixture: ComponentFixture; + let loader: HarnessLoader; beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [TraceTabComponent] -}) + await TestBed + .configureTestingModule({ + imports: [TraceTabComponent, NoopAnimationsModule], + providers: [ + {provide: TRACE_SERVICE, useClass: MockTraceService}, + ], + }) .compileComponents(); fixture = TestBed.createComponent(TraceTabComponent); component = fixture.componentInstance; + loader = TestbedHarnessEnvironment.loader(fixture); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should display no invocations if traceData is empty', async () => { + const expansionPanels = await loader.getAllHarnesses( + MatExpansionPanelHarness, + ); + expect(expansionPanels.length).toBe(0); + }); + + describe('with trace data', () => { + const MOCK_TRACE_DATA_WITH_MULTIPLE_TRACES: Span[] = [ + ...MOCK_TRACE_DATA, + { + name: 'agent.act-2', + start_time: 1733084700000000000, + end_time: 1733084760000000000, + span_id: 'span-10', + trace_id: 'trace-2', + attributes: { + 'event_id': 10, + 'gcp.vertex.agent.invocation_id': 'invoc-2', + 'gcp.vertex.agent.llm_request': + '{"contents":[{"role":"user","parts":[{"text":"Another user message"}]}]}', + }, + }, + ]; + + beforeEach(async () => { + fixture.componentRef.setInput( + 'traceData', + MOCK_TRACE_DATA_WITH_MULTIPLE_TRACES, + ); + fixture.detectChanges(); + await fixture.whenStable(); + }); + + it('should group traces by trace_id and display as expansion panels', + async () => { + const expansionPanels = await loader.getAllHarnesses( + MatExpansionPanelHarness, + ); + expect(expansionPanels.length).toBe(2); + }); + + it('should display user message as panel title', async () => { + const expansionPanels = await loader.getAllHarnesses( + MatExpansionPanelHarness, + ); + expect(await expansionPanels[0].getTitle()) + .toBe( + 'I need help with my project.', + ); + expect(await expansionPanels[1].getTitle()).toBe('Another user message'); + }); + + it('should pass correct data to trace-tree component', async () => { + spyOn(component, 'findInvocIdFromTraceId').and.callThrough(); + const expansionPanels = await loader.getAllHarnesses( + MatExpansionPanelHarness, + ); + await expansionPanels[0].expand(); + fixture.detectChanges(); + + expect(component.findInvocIdFromTraceId).toHaveBeenCalledWith('trace-1'); + const traceTree = fixture.nativeElement.querySelector('app-trace-tree'); + expect(traceTree).toBeTruthy(); + // Further inspection of trace-tree inputs would require a harness or + // mocking TraceTreeComponent + }); + }); }); diff --git a/src/app/components/trace-tab/trace-tab.component.ts b/src/app/components/trace-tab/trace-tab.component.ts index 4b463b1d..f3650e2e 100644 --- a/src/app/components/trace-tab/trace-tab.component.ts +++ b/src/app/components/trace-tab/trace-tab.component.ts @@ -70,6 +70,11 @@ export class TraceTabComponent implements OnInit, OnChanges { const eventItem = group?.find( item => item.attributes !== undefined && 'gcp.vertex.agent.invocation_id' in item.attributes) + + if (!eventItem) { + return '[no invocation id found]'; + } + const requestJson = JSON.parse(eventItem.attributes['gcp.vertex.agent.llm_request']) as LlmRequest const userContent = diff --git a/src/app/core/models/AgentRunRequest.ts b/src/app/core/models/AgentRunRequest.ts index 4917fd44..3018bf07 100644 --- a/src/app/core/models/AgentRunRequest.ts +++ b/src/app/core/models/AgentRunRequest.ts @@ -19,7 +19,13 @@ export interface AgentRunRequest { appName: string; userId: string; sessionId: string; - newMessage: any; + newMessage: { + parts: Array<{ + text?: string, + 'function_response'?: {id?: string; name?: string; response?: any} + }>, + role: string, + }; functionCallEventId?: string; streaming?: boolean; stateDelta?: any; diff --git a/src/app/core/services/agent.service.spec.ts b/src/app/core/services/agent.service.spec.ts index f8ebeda3..6002f532 100644 --- a/src/app/core/services/agent.service.spec.ts +++ b/src/app/core/services/agent.service.spec.ts @@ -42,7 +42,7 @@ const RUN_SSE_PAYLOAD = { sessionId: SESSION_ID, appName: TEST_APP_NAME, userId: USER_ID, - newMessage: NEW_MESSAGE, + newMessage: {parts: [{text: NEW_MESSAGE}], role: 'user'}, }; describe('AgentService', () => { diff --git a/src/app/directives/resizable-drawer.directive.spec.ts b/src/app/directives/resizable-drawer.directive.spec.ts new file mode 100644 index 00000000..e30327d4 --- /dev/null +++ b/src/app/directives/resizable-drawer.directive.spec.ts @@ -0,0 +1,171 @@ +/** + * @license + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {Component} from '@angular/core'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; + +import {ResizableDrawerDirective} from './resizable-drawer.directive'; + +// Directive constants +const SIDE_DRAWER_WIDTH_VAR = '--side-drawer-width'; +const INITIAL_WIDTH = 570; +const MIN_WIDTH = 310; + +// Test constants +const MOCKED_WINDOW_WIDTH = 2000; +const MAX_WIDTH = MOCKED_WINDOW_WIDTH / 2; // 1000 + +@Component({ + template: ` +
Drawer
+
+ `, + standalone: true, + imports: [ResizableDrawerDirective], +}) +class TestHostComponent { +} + +describe('ResizableDrawerDirective', () => { + let fixture: ComponentFixture; + let directiveElement: HTMLElement; + let resizeHandle: HTMLElement; + let body: HTMLElement; + let innerWidthSpy: jasmine.Spy; + + /** + * Reads the drawer width from the CSS variable on the root element. + */ + function getDrawerWidth(): number { + const widthStr = getComputedStyle(document.documentElement) + .getPropertyValue(SIDE_DRAWER_WIDTH_VAR); + return parseFloat(widthStr); + } + + /** + * Dispatches a mouse event with a given clientX position. + */ + function dispatchMouseEvent( + element: EventTarget, type: string, clientX: number) { + element.dispatchEvent(new MouseEvent(type, { + bubbles: true, + clientX, + })); + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TestHostComponent], + }); + innerWidthSpy = spyOnProperty(window, 'innerWidth', 'get'); + innerWidthSpy.and.returnValue(MOCKED_WINDOW_WIDTH); + fixture = TestBed.createComponent(TestHostComponent); + directiveElement = + fixture.debugElement.query(By.directive(ResizableDrawerDirective)) + .nativeElement; + resizeHandle = + fixture.debugElement.query(By.css('.resize-handler')).nativeElement; + body = document.body; + fixture.detectChanges(); // This calls ngAfterViewInit + window.dispatchEvent(new Event('resize')); + }); + + afterEach(() => { + document.documentElement.style.removeProperty(SIDE_DRAWER_WIDTH_VAR); + body.classList.remove('resizing'); + }); + + it('should set initial width to 570px after view init', () => { + // Assert + expect(directiveElement.style.width).toBe('var(--side-drawer-width)'); + expect(getDrawerWidth()).toBe(INITIAL_WIDTH); + }); + + it('should resize drawer on mouse drag', () => { + // Arrange + const startClientX = 600; + const moveClientX = 650; // Drag 50px to the right + const expectedWidth = INITIAL_WIDTH + (moveClientX - startClientX); + + // Act + dispatchMouseEvent(resizeHandle, 'mousedown', startClientX); + dispatchMouseEvent(document, 'mousemove', moveClientX); + fixture.detectChanges(); + + // Assert + expect(getDrawerWidth()).toBe(expectedWidth); + expect(body.classList).toContain('resizing'); + + // Act: Release mouse + dispatchMouseEvent(document, 'mouseup', moveClientX); + fixture.detectChanges(); + + // Assert: Resizing class is removed + expect(body.classList).not.toContain('resizing'); + }); + + it('should not resize below min width', () => { + // Arrange + const startClientX = 600; + const moveClientX = 100; // Attempt to drag far left (-500px) + + // Act + dispatchMouseEvent(resizeHandle, 'mousedown', startClientX); + dispatchMouseEvent(document, 'mousemove', moveClientX); + fixture.detectChanges(); + + // Assert + expect(getDrawerWidth()).toBe(MIN_WIDTH); + + // Cleanup + dispatchMouseEvent(document, 'mouseup', moveClientX); + }); + + it('should not resize above max width', () => { + // Arrange + const startClientX = 100; + const moveClientX = 9000; // Attempt to drag far right + + // Act + dispatchMouseEvent(resizeHandle, 'mousedown', startClientX); + dispatchMouseEvent(document, 'mousemove', moveClientX); + fixture.detectChanges(); + + // Assert + expect(getDrawerWidth()).toBe(MAX_WIDTH); + + // Cleanup + dispatchMouseEvent(document, 'mouseup', moveClientX); + }); + + it('should re-clamp width on window resize if current width exceeds new max width', + () => { + // Arrange + expect(getDrawerWidth()).toBe(INITIAL_WIDTH); // 570 + const smallWindowWidth = 800; + const expectedClampedWidth = smallWindowWidth / 2; // 400 + + // Act + innerWidthSpy.and.returnValue(smallWindowWidth); + window.dispatchEvent(new Event('resize')); + fixture.detectChanges(); + + // Assert + expect(getDrawerWidth()).toBe(expectedClampedWidth); + }); +}); diff --git a/src/app/directives/resizable-drawer.directive.ts b/src/app/directives/resizable-drawer.directive.ts index b46322c6..31689de0 100644 --- a/src/app/directives/resizable-drawer.directive.ts +++ b/src/app/directives/resizable-drawer.directive.ts @@ -26,7 +26,7 @@ interface ResizingEvent { @Directive({ selector: '[appResizableDrawer]', }) export class ResizableDrawerDirective implements AfterViewInit { private readonly sideDrawerMinWidth = 310; - private sideDrawerMaxWidth; + private sideDrawerMaxWidth = window.innerWidth / 2; private resizeHandle: HTMLElement|null = null; private resizingEvent: ResizingEvent = { @@ -35,16 +35,17 @@ export class ResizableDrawerDirective implements AfterViewInit { startingWidth: 0, }; - constructor(private el: ElementRef, private renderer: Renderer2) { - this.sideDrawerMaxWidth = window.innerWidth / 2; - } + constructor(private el: ElementRef, private renderer: Renderer2) {} ngAfterViewInit() { + this.sideDrawerMaxWidth = window.innerWidth / 2; this.resizeHandle = document.getElementsByClassName('resize-handler')[0] as HTMLElement; - this.renderer.listen( - this.resizeHandle, 'mousedown', - (event) => this.onResizeHandleMouseDown(event)); + if (this.resizeHandle) { + this.renderer.listen( + this.resizeHandle, 'mousedown', + (event) => this.onResizeHandleMouseDown(event)); + } document.documentElement.style.setProperty('--side-drawer-width', '570px'); this.renderer.setStyle( @@ -87,13 +88,14 @@ export class ResizableDrawerDirective implements AfterViewInit { private set sideDrawerWidth(width: number) { const clampedWidth = Math.min( Math.max(width, this.sideDrawerMinWidth), this.sideDrawerMaxWidth); - document.body.style.setProperty('--side-drawer-width', `${clampedWidth}px`); + document.documentElement.style.setProperty( + '--side-drawer-width', `${clampedWidth}px`); } private get sideDrawerWidth(): number { - const widthString = - getComputedStyle(document.body).getPropertyValue('--side-drawer-width'); - const parsedWidth = parseInt(widthString, 10); + const widthString = getComputedStyle(document.documentElement) + .getPropertyValue('--side-drawer-width'); + const parsedWidth = parseFloat(widthString); return isNaN(parsedWidth) ? 500 : parsedWidth; }