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;
}