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
78 changes: 78 additions & 0 deletions examples/ag-ui/angular/e2e/app-mode-promo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,84 @@ test('switching between popup and sidebar preserves App mode', async ({ page })
await expect(page.locator('popup-mode')).toBeVisible();
});

test('sidebar App mode keeps the map framed when the chat rail closes and reopens', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await openDemo(page, '/sidebar?appmode=on');

const sidebar = page.locator('chat-sidebar');
const map = page.locator('sidebar-mode app-map-canvas');
const panel = page.locator('.chat-sidebar__panel');

await expect(sidebar).toHaveAttribute('data-open', 'true');
await expect(panel).toHaveAttribute('data-open', 'true');
await expect(map).toBeVisible();

async function layoutState(): Promise<{
contentWidth: number;
contentRight: number;
mapWidth: number;
mapRight: number;
marginRight: string;
rootClaim: string | null;
occupyRight: string;
}> {
return page.evaluate(() => {
const contentEl = document.querySelector('.chat-sidebar__content');
const mapEl = document.querySelector('sidebar-mode app-map-canvas');
if (!contentEl || !mapEl) throw new Error('App-mode map layout elements missing');
const contentRect = contentEl.getBoundingClientRect();
const mapRect = mapEl.getBoundingClientRect();
const contentStyle = getComputedStyle(contentEl);
const root = document.documentElement;
const rootStyle = getComputedStyle(root);
return {
contentWidth: contentRect.width,
contentRight: contentRect.right,
mapWidth: mapRect.width,
mapRight: mapRect.right,
marginRight: contentStyle.marginRight,
rootClaim: root.getAttribute('data-threadplane-chat-sidebar'),
occupyRight: rootStyle.getPropertyValue('--tplane-chat-occupy-right').trim(),
};
});
}

await expect.poll(async () => parseFloat((await layoutState()).marginRight)).toBeGreaterThan(300);
const openLayout = await layoutState();
expect(openLayout.rootClaim).toBe('open');
expect(parseFloat(openLayout.marginRight)).toBeGreaterThan(300);
expect(openLayout.contentWidth).toBeGreaterThan(500);
expect(openLayout.mapWidth).toBeCloseTo(openLayout.contentWidth, 0);
expect(openLayout.mapRight).toBeCloseTo(openLayout.contentRight, 0);
Comment on lines +98 to +104

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The > 300 (margin-right) and > 500 (content width) thresholds are implicit assumptions about the sidebar panel width at 1280 × 800. A short comment — e.g. // sidebar panel is ~350px wide — makes the intent clear and makes future threshold adjustments obvious if the default panel width ever changes.

Comment on lines +98 to +104

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The > 300 (margin-right) and > 500 (content width) thresholds are implicit assumptions about the sidebar panel width at a 1280 × 800 viewport. They'll silently regress if the default panel width changes. A short comment stating the expected relationship — e.g. "sidebar panel is ~350px wide" — makes the intent clear and makes future threshold adjustments obvious.


await page.getByRole('button', { name: 'Close chat' }).click();
await expect(sidebar).toHaveAttribute('data-open', 'false');
await expect(panel).toHaveAttribute('data-open', 'false');
await expect(page.getByRole('button', { name: 'Open chat' })).toBeVisible();
await expect.poll(async () => parseFloat((await layoutState()).marginRight)).toBe(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: after expect.poll() resolves on this line, layoutState() is called again immediately on line 112. There's a tiny window where layout could still be transitioning. In practice the poll acts as a synchronisation barrier, so this is unlikely to flake — just worth knowing if this test is occasionally unstable in CI.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: parseFloat((await layoutState()).marginRight).toBe(0) is consistent with the rest of the test, but once the sidebar is closed the computed marginRight will be "0px"parseFloat0, so this works. The same approach as line 98 / 123 is fine.

One thing worth noting: after expect.poll() resolves on line 110, the test calls layoutState() again on line 112. There's a tiny window where the layout could still be transitioning. In practice the poll settling acts as a synchronisation barrier, so this is unlikely to flake — just worth knowing if this test is occasionally unstable in CI.


const closedLayout = await layoutState();
expect(closedLayout.rootClaim).toBeNull();
expect(closedLayout.occupyRight).toBe('0px');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential brittleness: asserts the CSS variable is exactly '0px'. If the library ever sets --tplane-chat-occupy-right to '0' (no unit suffix) on close, this fails incorrectly. Using parseFloat would be more robust and consistent with how marginRight is checked elsewhere.

Suggested change
expect(closedLayout.occupyRight).toBe('0px');
expect(parseFloat(closedLayout.occupyRight)).toBe(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential brittleness: this asserts the CSS variable is the string '0px' exactly. If the library ever sets --tplane-chat-occupy-right to '0' (no unit) when the sidebar closes, this assertion fails incorrectly. Using parseFloat(closedLayout.occupyRight)toBe(0) would be more robust and consistent with how marginRight is checked elsewhere.

Suggested change
expect(closedLayout.occupyRight).toBe('0px');
expect(parseFloat(closedLayout.occupyRight)).toBe(0);

expect(parseFloat(closedLayout.marginRight)).toBe(0);
expect(closedLayout.contentWidth).toBeGreaterThan(openLayout.contentWidth);
expect(closedLayout.mapWidth).toBeCloseTo(closedLayout.contentWidth, 0);
expect(closedLayout.mapRight).toBeCloseTo(closedLayout.contentRight, 0);

await page.getByRole('button', { name: 'Open chat' }).click();
await expect(sidebar).toHaveAttribute('data-open', 'true');
await expect(panel).toHaveAttribute('data-open', 'true');
await expect.poll(async () => parseFloat((await layoutState()).marginRight)).toBeGreaterThan(300);

const reopenedLayout = await layoutState();
expect(reopenedLayout.rootClaim).toBe('open');
expect(parseFloat(reopenedLayout.marginRight)).toBeGreaterThan(300);
expect(reopenedLayout.contentWidth).toBeGreaterThan(500);
expect(reopenedLayout.contentWidth).toBeLessThan(closedLayout.contentWidth);
expect(reopenedLayout.mapWidth).toBeCloseTo(reopenedLayout.contentWidth, 0);
expect(reopenedLayout.mapRight).toBeCloseTo(reopenedLayout.contentRight, 0);
});

// Embed is full-chat with no background, so it can't host App mode: choosing
// Embed from App mode turns App mode off.
test('choosing embed from App mode turns App mode off and shows the full chat', async ({ page }) => {
Expand Down
10 changes: 4 additions & 6 deletions examples/ag-ui/angular/src/app/shell/ag-ui-shell.component.css
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,11 @@
* viewport minus the toolbar. */
height: calc(100% - var(--demo-toolbar-height));
}
/* chat-sidebar panel renders top-aligned with the page, NOT under the
* toolbar — so the panel's close button sits at the same viewport-y as
* the hamburger inside the toolbar (both at surface-top + 8 padding).
* The panel's z-index is below the toolbar's so the toolbar still
* renders above it where they overlap on the right edge. */
/* Keep the sidebar panel below the toolbar. The toolbar has the higher z-index
* for portaled select menus, so a top-aligned panel leaves its close button
* visually present but intercepted by toolbar controls. */
.ag-ui-shell ::ng-deep .chat-sidebar__panel {
top: 0;
top: var(--demo-toolbar-height);
}
.ag-ui-shell__theme-toggle--toolbar { margin-left: auto; }

Expand Down
Loading