Skip to content
Open
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
27 changes: 27 additions & 0 deletions zeppelin-web-angular/e2e/models/notebook-assistant-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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 { Locator, Page } from '@playwright/test';
import { BasePage } from './base-page';

export class NotebookAssistantPage extends BasePage {
readonly toggleButton: Locator;
readonly panel: Locator;
readonly closeButton: Locator;

constructor(page: Page) {
super(page);
this.toggleButton = page.getByRole('button', { name: 'Toggle AI Assistant' });
this.panel = page.getByRole('complementary', { name: 'AI Assistant workspace' });
this.closeButton = page.getByRole('button', { name: 'Close AI Assistant' });
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* 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 { expect, test } from '@playwright/test';
import { NotebookAssistantPage } from '../../../models/notebook-assistant-page';
import { addPageAnnotationBeforeEach, createTestNotebook, PAGES, waitForZeppelinReady } from '../../../utils';

test.describe('Notebook AI Assistant shell', () => {
addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK_ASSISTANT);

let assistant: NotebookAssistantPage;

test.beforeEach(async ({ page }) => {
assistant = new NotebookAssistantPage(page);
});

test('opens and closes from the notebook sidebar', async ({ page }) => {
await page.goto('/#/');
await waitForZeppelinReady(page);
const { noteId } = await createTestNotebook(page);
try {
await test.step('Given a notebook with the Assistant enabled', async () => {
await page.goto(`/#/notebook/${noteId}?reactAssistant=true`);
await waitForZeppelinReady(page);
await expect(assistant.toggleButton).toBeVisible();
await expect(assistant.panel).toBeHidden();
});

await test.step('When I open the Assistant', async () => {
await assistant.toggleButton.click();
});

await test.step('Then the panel appears', async () => {
await expect(assistant.toggleButton).toHaveAttribute('aria-pressed', 'true');
await expect(assistant.panel).toBeVisible();
});

await test.step('When I close the Assistant', async () => {
await assistant.closeButton.click();
});

await test.step('Then the notebook returns to its closed state', async () => {
await expect(assistant.panel).toBeHidden();
await expect(assistant.toggleButton).toHaveAttribute('aria-pressed', 'false');
});
} finally {
const response = await page.request.delete(`/api/notebook/${noteId}`);
expect(response.ok(), 'Delete the disposable notebook').toBe(true);
}
});
});
1 change: 1 addition & 0 deletions zeppelin-web-angular/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const PAGES = {
NOTEBOOK: 'src/app/pages/workspace/notebook/notebook.component',
NOTEBOOK_ACTION_BAR: 'src/app/pages/workspace/notebook/action-bar/action-bar.component',
NOTEBOOK_ADD_PARAGRAPH: 'src/app/pages/workspace/notebook/add-paragraph/add-paragraph.component',
NOTEBOOK_ASSISTANT: 'src/app/pages/workspace/notebook/assistant/assistant-host.component',
NOTEBOOK_INTERPRETER_BINDING: 'src/app/pages/workspace/notebook/interpreter-binding/interpreter-binding.component',
NOTEBOOK_NOTE_FORM: 'src/app/pages/workspace/notebook/note-form-block/note-form-block.component',
NOTEBOOK_PERMISSIONS: 'src/app/pages/workspace/notebook/permissions/permissions.component',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* 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.
*/

.assistant-shell-nav {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 32px;
padding: 0;
border: 0;
background: transparent;
cursor: pointer;
}

.assistant-shell-nav-icon {
display: inline-grid;
place-items: center;
width: 20px;
height: 16px;
border: 1px solid rgba(48, 71, 91, 0.24);
border-radius: 3px;
background: #fff;
color: #203447;
font:
700 10px/1 Arial,
sans-serif;
}

.assistant-shell-nav[aria-pressed='true'] .assistant-shell-nav-icon {
border-color: var(--assistant-nav-active-color);
}

.assistant-shell-panel {
position: sticky;
z-index: 25;
top: 0;
box-sizing: border-box;
width: calc(var(--assistant-sidebar-width) - 40px);
height: calc(100dvh - var(--assistant-panel-top, 0px));
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
border-right: 1px solid;
box-shadow: 4px 0 2px 0 rgba(0, 0, 0, 0.06);
padding-top: 8px;
}

.assistant-shell-header {
display: flex;
align-items: center;
min-height: 36px;
padding: 4px 8px 4px 12px;
border-bottom: 1px solid;
}

.assistant-shell-header h2 {
margin: 0;
font-size: inherit;
font-weight: 600;
}

.assistant-shell-close {
position: absolute;
z-index: 1;
top: 3px;
right: 3px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 50px;
padding: 0;
border: 0;
background: transparent;
color: var(--assistant-close-color);
cursor: pointer;
font-size: 20px;
transition: color 0.2s ease;
}

.assistant-shell-close:hover {
color: #3071a9;
}

.assistant-shell-resize {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 5px;
cursor: col-resize;
touch-action: none;
}

@media (max-width: 640px) {
.assistant-shell-panel {
width: min(var(--assistant-sidebar-width), 100vw);
margin-right: calc(-1 * min(var(--assistant-sidebar-width), 100vw));
margin-left: -40px;
}

.assistant-shell-resize {
display: none;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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 { act } from 'react';
import { fireEvent } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import type { AssistantShellProps } from '@zeppelin/sdk';
import { mount } from './AssistantWorkspace';

describe('AssistantWorkspace remote', () => {
it('portals the navigation and panel, coordinates close, and shares the sidebar width', async () => {
const root = document.createElement('div');
const navigation = document.createElement('div');
const panel = document.createElement('div');
const onPanelVisibilityChange = vi.fn();
const onPanelWidthChange = vi.fn();
let requestClose = () => {};
const props: AssistantShellProps = {
noteId: 'note-a',
slots: [
{ element: navigation, kind: 'navigation' },
{ element: panel, kind: 'panel' }
],
panelWidth: 370,
onPanelVisibilityChange,
onPanelWidthChange,
subscribePanelClose: listener => {
requestClose = listener;
return () => {
requestClose = () => {};
};
}
};
let handle: ReturnType<typeof mount>;

await act(async () => {
handle = mount(root, props);
});
expect(navigation.querySelector('button')).not.toBeNull();
expect(panel.textContent).toBe('');

await act(async () => navigation.querySelector('button')?.click());
expect(panel.querySelector('aside')).not.toBeNull();
expect(onPanelVisibilityChange).toHaveBeenLastCalledWith(true);
expect(panel.style.getPropertyValue('--assistant-sidebar-width')).toBe('370px');

await act(async () => {
panel
.querySelector<HTMLElement>('[role="separator"]')
?.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
});
expect(onPanelWidthChange).toHaveBeenCalledWith(386);

onPanelWidthChange.mockClear();
const resizeHandle = panel.querySelector<HTMLElement>('[role="separator"]')!;
await act(async () => fireEvent.pointerDown(resizeHandle, { button: 0, buttons: 1, clientX: 100 }));
await act(async () => fireEvent.pointerMove(resizeHandle, { buttons: 1, clientX: 140 }));
expect(panel.style.getPropertyValue('--assistant-sidebar-width')).toBe('426px');
expect(resizeHandle.getAttribute('aria-valuenow')).toBe('426');
expect(onPanelWidthChange).not.toHaveBeenCalled();
await act(async () => fireEvent.pointerUp(resizeHandle, { button: 0, clientX: 140 }));
expect(onPanelWidthChange).toHaveBeenCalledExactlyOnceWith(426);

onPanelWidthChange.mockClear();
await act(async () => fireEvent.pointerDown(resizeHandle, { button: 0, buttons: 1, clientX: 100 }));
await act(async () => fireEvent.pointerMove(resizeHandle, { buttons: 1, clientX: 120 }));
await act(async () => fireEvent.pointerMove(resizeHandle, { buttons: 0, clientX: 120 }));
expect(onPanelWidthChange).toHaveBeenCalledExactlyOnceWith(446);

await act(async () => requestClose());
expect(panel.textContent).toBe('');
expect(onPanelVisibilityChange).toHaveBeenLastCalledWith(false);

await act(async () => navigation.querySelector('button')?.click());
await act(async () => handle.update({ ...props, noteId: 'note-b' }));
expect(panel.textContent).toBe('');

await act(async () => {
handle.unmount();
});
expect(navigation.innerHTML).toBe('');
});
});
Loading
Loading