diff --git a/plugins/love-resources/src/components/ControlExt.svelte b/plugins/love-resources/src/components/ControlExt.svelte index ae5c1d3e8a..376c430180 100644 --- a/plugins/love-resources/src/components/ControlExt.svelte +++ b/plugins/love-resources/src/components/ControlExt.svelte @@ -19,30 +19,36 @@ import { Floor, Invite, + isOffice, JoinRequest, + loveId, Office, ParticipantInfo, RequestStatus, Room, - RoomType, - isOffice, - loveId + RoomType } from '@hcengineering/love' import { getEmbeddedLabel } from '@hcengineering/platform' - import { MessageBox, createQuery, getClient } from '@hcengineering/presentation' + import { createQuery, getClient, MessageBox } from '@hcengineering/presentation' import { - Location, - PopupResult, closePopup, eventToHTMLElement, + Location, location, + PopupResult, showPopup, tooltip } from '@hcengineering/ui' import view from '@hcengineering/view' import { onDestroy } from 'svelte' import workbench from '@hcengineering/workbench' - import { closeWidget, openWidget, sidebarStore } from '@hcengineering/workbench-resources' + import { + closeWidget, + minimizeSidebar, + openWidget, + sidebarStore, + SidebarVariant + } from '@hcengineering/workbench-resources' import love from '../plugin' import { @@ -283,8 +289,20 @@ showPopup(CamSettingPopup, {}, eventToHTMLElement(e)) } + $: isVideoWidgetOpened = $sidebarStore.widgetsState.has(love.ids.VideoWidget) + + $: if ( + isVideoWidgetOpened && + $sidebarStore.widget === undefined && + $location.path[2] !== loveId && + $sidebarStore.widgetsState.get(love.ids.VideoWidget)?.closedByUser !== true + ) { + sidebarStore.update((s) => ({ ...s, widget: love.ids.VideoWidget, variant: SidebarVariant.EXPANDED })) + } + function checkActiveVideo (loc: Location, video: boolean, room: Ref | undefined): void { - const isOpened = $sidebarStore.widgetsState.get(love.ids.VideoWidget) + const isOpened = $sidebarStore.widgetsState.has(love.ids.VideoWidget) + if (room === undefined) { if (isOpened) { closeWidget(love.ids.VideoWidget) @@ -292,13 +310,22 @@ return } - if (loc.path[2] !== loveId && video) { - if (isOpened) return - const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: love.ids.VideoWidget })[0] - if (widget === undefined) return - openWidget(widget, { - room - }) + if (video) { + if (!isOpened) { + const widget = client.getModel().findAllSync(workbench.class.Widget, { _id: love.ids.VideoWidget })[0] + if (widget === undefined) return + openWidget( + widget, + { + room + }, + loc.path[2] !== loveId + ) + } + + if (loc.path[2] === loveId && $sidebarStore.widget === love.ids.VideoWidget) { + minimizeSidebar() + } } else { closeWidget(love.ids.VideoWidget) } diff --git a/plugins/love-resources/src/components/LoveWidget.svelte b/plugins/love-resources/src/components/LoveWidget.svelte index c0bcb5dc73..1d96d51c6f 100644 --- a/plugins/love-resources/src/components/LoveWidget.svelte +++ b/plugins/love-resources/src/components/LoveWidget.svelte @@ -79,7 +79,7 @@ {/if} {#if $floors.length > 1} -
+
{/if} @@ -92,7 +92,6 @@ flex-direction: column; justify-content: space-between; height: 100%; - padding-right: 0.5rem; padding-bottom: 1rem; } diff --git a/plugins/love-resources/src/components/Room.svelte b/plugins/love-resources/src/components/Room.svelte index 3f0bb1e38a..e1d2bd92d4 100644 --- a/plugins/love-resources/src/components/Room.svelte +++ b/plugins/love-resources/src/components/Room.svelte @@ -379,7 +379,7 @@ diff --git a/plugins/workbench-resources/src/components/sidebar/widgets/WidgetsBar.svelte b/plugins/workbench-resources/src/components/sidebar/widgets/WidgetsBar.svelte index 8635252380..5f5717b4db 100644 --- a/plugins/workbench-resources/src/components/sidebar/widgets/WidgetsBar.svelte +++ b/plugins/workbench-resources/src/components/sidebar/widgets/WidgetsBar.svelte @@ -19,7 +19,7 @@ import WidgetPresenter from './/WidgetPresenter.svelte' import AddWidgetsPopup from './AddWidgetsPopup.svelte' - import { openWidget, sidebarStore, SidebarVariant } from '../../../sidebar' + import { minimizeSidebar, openWidget, sidebarStore } from '../../../sidebar' export let widgets: Widget[] = [] export let preferences: WidgetPreference[] = [] @@ -31,9 +31,9 @@ function handleSelectWidget (widget: Widget): void { if (selected === widget._id) { - sidebarStore.update((state) => ({ ...state, widget: undefined, variant: SidebarVariant.MINI })) + minimizeSidebar(true) } else { - openWidget(widget) + openWidget(widget, $sidebarStore.widgetsState.get(widget._id)?.data, true) } } diff --git a/plugins/workbench-resources/src/sidebar.ts b/plugins/workbench-resources/src/sidebar.ts index 067896011a..4510ca91e6 100644 --- a/plugins/workbench-resources/src/sidebar.ts +++ b/plugins/workbench-resources/src/sidebar.ts @@ -30,6 +30,7 @@ export interface WidgetState { data?: Record tabs: WidgetTab[] tab?: string + closedByUser?: boolean } export interface SidebarState { @@ -95,7 +96,7 @@ function setSidebarStateToLocalStorage (state: SidebarState): void { ) } -export function openWidget (widget: Widget, data?: Record): void { +export function openWidget (widget: Widget, data?: Record, active = true): void { const state = get(sidebarStore) const { widgetsState } = state const widgetState = widgetsState.get(widget._id) @@ -106,7 +107,7 @@ export function openWidget (widget: Widget, data?: Record): void { ...state, widgetsState, variant: SidebarVariant.EXPANDED, - widget: widget._id + widget: active ? widget._id : state.widget }) } @@ -114,6 +115,10 @@ export function closeWidget (widget: Ref): void { const state = get(sidebarStore) const { widgetsState } = state + if (!widgetsState.has(widget) && state.widget !== widget && state.variant === SidebarVariant.MINI) { + return + } + widgetsState.delete(widget) if (state.widget === widget) { @@ -292,3 +297,15 @@ export function isElementFromSidebar (element: HTMLElement): boolean { return isDescendant(sidebarElement, element) } + +export function minimizeSidebar (closedByUser = false): void { + const state = get(sidebarStore) + const { widget, widgetsState } = state + const widgetState = widget == null ? undefined : widgetsState.get(widget) + + if (widget !== undefined && widgetState !== undefined && closedByUser) { + widgetsState.set(widget, { ...widgetState, closedByUser }) + } + + sidebarStore.set({ ...state, ...widgetsState, widget: undefined, variant: SidebarVariant.MINI }) +} diff --git a/tests/sanity/tests/documents/documents-content.spec.ts b/tests/sanity/tests/documents/documents-content.spec.ts index bff9023129..316deeeae3 100644 --- a/tests/sanity/tests/documents/documents-content.spec.ts +++ b/tests/sanity/tests/documents/documents-content.spec.ts @@ -17,6 +17,7 @@ import { PlanningNavigationMenuPage } from '../model/planning/planning-navigatio import { PlanningPage } from '../model/planning/planning-page' import { SignUpData } from '../model/common-types' import { TestData } from '../chat/types' +import { faker } from '@faker-js/faker' const retryOptions = { intervals: [1000, 1500, 2500], timeout: 60000 } @@ -148,4 +149,150 @@ test.describe('Content in the Documents tests', () => { await documentContentSecondPage.proseTableCell(1, 1).fill('Center') await expect(documentContentPage.proseTableCell(1, 1)).toContainText('Center', { timeout: 5000 }) }) + + test.describe('Image in the document', () => { + test('Check Image alignment setting', async ({ page }) => { + await documentContentPage.addImageToDocument(page) + + await test.step('Align image to right', async () => { + await documentContentPage.clickImageAlignButton('right') + await documentContentPage.checkImageAlign('right') + }) + + await test.step('Align image to left', async () => { + await documentContentPage.clickImageAlignButton('left') + await documentContentPage.checkImageAlign('left') + }) + + await test.step('Align image to center', async () => { + await documentContentPage.clickImageAlignButton('center') + await documentContentPage.checkImageAlign('center') + }) + }) + + test('Check Image view and size actions', async ({ page }) => { + await documentContentPage.addImageToDocument(page) + const imageSrc = await documentContentPage.firstImageInDocument().getAttribute('src') + + await test.step('Set size of image to the 25%', async () => { + await documentContentPage.clickImageSizeButton('25%') + await documentContentPage.checkImageSize('25%') + }) + + await test.step('Set size of image to the 50%', async () => { + await documentContentPage.clickImageSizeButton('50%') + await documentContentPage.checkImageSize('50%') + }) + + await test.step('Set size of image to the 100%', async () => { + await documentContentPage.clickImageSizeButton('100%') + await documentContentPage.checkImageSize('100%') + }) + + await test.step('Set size of image to the unset', async () => { + const IMAGE_ORIGINAL_SIZE = 199 + await documentContentPage.clickImageSizeButton('Unset') + await documentContentPage.checkImageSize(IMAGE_ORIGINAL_SIZE) + }) + + await test.step('User can open image in fullscreen on current page', async () => { + await documentContentPage.clickImageFullscreenButton() + await expect(documentContentPage.fullscreenImage()).toBeVisible() + await documentContentPage.page.keyboard.press('Escape') + await expect(documentContentPage.fullscreenImage()).toBeHidden() + }) + + await test.step('User can open image original in the new tab', async () => { + const [newPage] = await Promise.all([ + page.waitForEvent('popup'), + documentContentPage.clickImageOriginalButton() + ]) + + await newPage.waitForLoadState('domcontentloaded') + expect(newPage.url()).toBe(imageSrc) + await newPage.close() + }) + }) + + test('Remove image with Backspace', async ({ page }) => { + await documentContentPage.addImageToDocument(page) + await documentContentPage.selectedFirstImageInDocument() + await documentContentPage.page.keyboard.press('Backspace') + await expect(documentContentPage.firstImageInDocument()).toBeHidden() + }) + + test('Check Table of Content', async ({ page }) => { + const HEADER_1_CONTENT = 'Header 1' + const HEADER_2_CONTENT = 'Header 2' + const HEADER_3_CONTENT = 'Header 3' + const contentParts = [ + `# ${HEADER_1_CONTENT}\n\n${faker.lorem.paragraph(20)}\n`, + `## ${HEADER_2_CONTENT}\n\n${faker.lorem.paragraph(20)}\n`, + `### ${HEADER_3_CONTENT}\n\n${faker.lorem.paragraph(20)}` + ] + + await test.step('Fill in the document and check the appearance of the ToC items', async () => { + await documentContentPage.inputContentParapraph().click() + + let partIndex = 0 + for (const contentPart of contentParts) { + await documentContentPage.page.keyboard.type(contentPart) + await expect(documentContentPage.tocItems()).toHaveCount(++partIndex) + } + }) + + await test.step('Check if ToC element is visible', async () => { + await expect(documentContentPage.page.locator('.toc-container .toc-item')).toHaveCount(3) + }) + + await test.step('User go to first header by ToC', async () => { + await documentContentPage.tocItems().first().click() + await documentContentPage.buttonTocPopupHeader(HEADER_1_CONTENT).click() + await expect(documentContentPage.headerElementInDocument('h1', HEADER_1_CONTENT)).toBeInViewport() + }) + + await test.step('User go to last header by ToC', async () => { + await documentContentPage.tocItems().first().click() + await documentContentPage.buttonTocPopupHeader(HEADER_3_CONTENT).click() + await expect(documentContentPage.headerElementInDocument('h3', HEADER_3_CONTENT)).toBeInViewport() + }) + }) + }) + + test('Check a slash typing handling', async ({ page }) => { + await test.step('User can open the popup if types "/" in empty document', async () => { + await documentContentPage.inputContentParapraph().click() + await documentContentPage.page.keyboard.type('/') + await expect(documentContentPage.slashActionItemsPopup()).toBeVisible() + await documentContentPage.page.keyboard.press('Escape') + }) + + await test.step('User can open the popup if types "/" after some content', async () => { + await documentContentPage.inputContentParapraph().click() + await documentContentPage.page.keyboard.type('First paragraph\n\n') + await documentContentPage.page.keyboard.type('/') + await expect(documentContentPage.slashActionItemsPopup()).toBeVisible() + await documentContentPage.page.keyboard.press('Escape') + }) + + await test.step('User cannot open a popup if he types "/" inside code block', async () => { + await documentContentPage.page.keyboard.press('Enter') + await documentContentPage.page.keyboard.type('/') + await documentContentPage.menuPopupItemButton('Code block').click() + await documentContentPage.page.keyboard.type('/') + await expect(documentContentPage.slashActionItemsPopup()).toBeHidden() + await documentContentPage.page.keyboard.press('ArrowDown') + await documentContentPage.page.keyboard.press('ArrowDown') + }) + + await test.step('User can create table by slash and open a popup if he types "/" inside a table', async () => { + await documentContentPage.page.keyboard.type('/') + await documentContentPage.menuPopupItemButton('Table').click() + await documentContentPage.menuPopupItemButton('1x2').first().click() + await documentContentPage.proseTableCell(0, 1).click() + await documentContentPage.page.keyboard.type('/') + await expect(documentContentPage.slashActionItemsPopup()).toBeVisible() + await documentContentPage.page.keyboard.press('Escape') + }) + }) }) diff --git a/tests/sanity/tests/documents/documents.spec.ts b/tests/sanity/tests/documents/documents.spec.ts index e3fdf34271..d8d837c391 100644 --- a/tests/sanity/tests/documents/documents.spec.ts +++ b/tests/sanity/tests/documents/documents.spec.ts @@ -87,6 +87,43 @@ test.describe('Documents tests', () => { await documentContentPage.checkDocumentTitle(moveDocument.title) }) + test('Create a document inside another document', async () => { + const contentFirst = 'Text first line' + const parentTeamspace: NewTeamspace = { + title: `Parent Teamspace-${generateId()}`, + description: 'Parent Teamspace description', + private: false + } + const parentDocument: NewDocument = { + title: `Parent Document Title-${generateId()}`, + space: parentTeamspace.title + } + const childDocument: NewDocument = { + title: `Child Document Title-${generateId()}`, + space: parentTeamspace.title + } + + await test.step('Create a parent document by button "+" in left menu documents list', async () => { + await leftSideMenuPage.clickDocuments() + await documentsPage.checkTeamspaceNotExist(parentTeamspace.title) + await documentsPage.createNewTeamspace(parentTeamspace) + await documentsPage.checkTeamspaceExist(parentTeamspace.title) + await documentsPage.clickOnButtonCreateDocument() + await documentsPage.createDocument(parentDocument) + }) + + await test.step('Create a child document', async () => { + await documentsPage.clickAddDocumentIntoDocument(parentDocument.title) + await documentContentPage.updateDocumentTitle(childDocument.title) + const content = await documentContentPage.addContentToTheNewLine(contentFirst) + await documentContentPage.checkContent(content) + }) + + await test.step('Check nesting of documents', async () => { + await documentsPage.checkIfParentDocumentIsExistInBreadcrumbs(parentDocument.title) + }) + }) + test('Collaborative edit document content', async ({ page, browser }) => { let content = '' const contentFirstUser = 'First first!!! This string comes from the first user' diff --git a/tests/sanity/tests/model/documents/document-content-page.ts b/tests/sanity/tests/model/documents/document-content-page.ts index b18660fc43..df91df49bf 100644 --- a/tests/sanity/tests/model/documents/document-content-page.ts +++ b/tests/sanity/tests/model/documents/document-content-page.ts @@ -1,5 +1,6 @@ import { type Locator, type Page, expect } from '@playwright/test' import { CommonPage } from '../common-page' +import { uploadFile } from '../../utils' export class DocumentContentPage extends CommonPage { readonly page: Page @@ -11,11 +12,17 @@ export class DocumentContentPage extends CommonPage { readonly buttonDocumentTitle = (): Locator => this.page.locator('div[class*="main-content"] div.title input') readonly inputContent = (): Locator => this.page.locator('div.textInput div.tiptap') + readonly selectContent = (): Locator => this.page.locator('div.textInput .select-text') readonly inputContentParapraph = (): Locator => this.page.locator('div.textInput div.tiptap > p') readonly leftMenu = (): Locator => this.page.locator('div.tiptap-left-menu') readonly proseTableCell = (row: number, col: number): Locator => this.page.locator('table.proseTable').locator('tr').nth(row).locator('td').nth(col).locator('p') + readonly firstImageInDocument = (): Locator => this.page.locator('.textInput .text-editor-image-container img') + readonly tooltipImageTools = (): Locator => this.page.locator('.tippy-box') + + readonly fullscreenImage = (): Locator => this.page.locator('.popup.fullsize img') + readonly proseTableColumnHandle = (col: number): Locator => this.page.locator('table.proseTable').locator('tr').first().locator('td').nth(col).locator('div.table-col-handle') @@ -45,6 +52,15 @@ export class DocumentContentPage extends CommonPage { readonly assigneeToDo = (hasText: string): Locator => this.rowToDo(hasText).locator('div.assignee') readonly checkboxToDo = (hasText: string): Locator => this.rowToDo(hasText).locator('input.chBox') + readonly tocItems = (): Locator => this.page.locator('.toc-container .toc-item') + readonly buttonTocPopupHeader = (headerText: string): Locator => + this.page.locator(`.popup button:has-text("${headerText}")`) + + readonly headerElementInDocument = (headerType: 'h1' | 'h2' | 'h3' = 'h1', text: string): Locator => + this.page.locator(`.textInput ${headerType}:has-text("${text}")`) + + readonly slashActionItemsPopup = (): Locator => this.page.locator('.selectPopup') + async checkDocumentTitle (title: string): Promise { await expect(this.buttonDocumentTitle()).toHaveValue(title) } @@ -65,6 +81,113 @@ export class DocumentContentPage extends CommonPage { await expect(this.inputContent()).toHaveText(content) } + async checkUserAddedImage (): Promise { + await expect(this.firstImageInDocument()).toBeVisible() + } + + async checkIfImageToolsIsVisible (): Promise { + await expect(this.tooltipImageTools()).toBeVisible() + } + + async clickImageToolsButton (dataId: string): Promise { + await this.tooltipImageTools().locator(`[data-id$="${dataId}"]`).click() + } + + async selectedFirstImageInDocument (): Promise { + await this.firstImageInDocument().click() + } + + async checkIfImageHasAttribute (attribute: string, value: string): Promise { + await expect(this.firstImageInDocument()).toHaveAttribute(attribute, value) + } + + async clickImageAlignButton (align: 'left' | 'center' | 'right'): Promise { + await this.selectedFirstImageInDocument() + await this.checkIfImageToolsIsVisible() + + switch (align) { + case 'left': + await this.clickImageToolsButton('btnAlignLeft') + break + case 'right': + await this.clickImageToolsButton('btnAlignRight') + break + case 'center': + await this.clickImageToolsButton('btnAlignCenter') + break + } + } + + async clickImageSizeButton (size: string | number): Promise { + await this.selectedFirstImageInDocument() + await this.checkIfImageToolsIsVisible() + await this.clickImageToolsButton('btnMoreActions') + await this.page.locator(`.popup button:has-text("${size}")`).click() + } + + async clickImageFullscreenButton (): Promise { + await this.selectedFirstImageInDocument() + await this.checkIfImageToolsIsVisible() + await this.clickImageToolsButton('btnViewImage') + } + + async clickImageOriginalButton (): Promise { + await this.selectedFirstImageInDocument() + await this.checkIfImageToolsIsVisible() + await this.clickImageToolsButton('btnViewOriginal') + } + + async checkImageAlign (side: 'left' | 'right' | 'center' = 'left'): Promise { + const imageBox = await this.firstImageInDocument().boundingBox() + const parentBox = await this.selectContent().boundingBox() + + if (!(imageBox !== null && parentBox !== null)) { + throw new Error('Image or parent box is not found') + } + + const elementLeftEdge = imageBox.x + const parentLeftEdge = parentBox.x + const elementRightEdge = imageBox.x + imageBox.width + const parentRightEdge = parentBox.x + parentBox.width + + switch (side) { + case 'right': + expect(elementRightEdge).toEqual(parentRightEdge) + break + case 'left': + expect(elementLeftEdge).toEqual(parentLeftEdge) + break + case 'center': + expect(elementLeftEdge - parentLeftEdge).toBeGreaterThan(0) + expect(elementLeftEdge - parentLeftEdge).toEqual(parentRightEdge - elementRightEdge) + break + } + } + + async checkImageSize (size: '25%' | '50%' | '100%' | number): Promise { + const imageBox = await this.firstImageInDocument().boundingBox() + const parentBox = await this.selectContent().boundingBox() + + if (!(imageBox !== null && parentBox !== null)) { + throw new Error('Image or parent box is not found') + } + + switch (size) { + case '25%': + expect(imageBox.width).toEqual(parentBox.width / 4) + break + case '50%': + expect(imageBox.width).toEqual(parentBox.width / 2) + break + case '100%': + expect(imageBox.width).toEqual(parentBox.width) + break + default: + expect(imageBox.width).toEqual(size) + break + } + } + async updateDocumentTitle (title: string): Promise { await this.buttonDocumentTitle().fill(title) await this.buttonDocumentTitle().blur() @@ -110,4 +233,11 @@ export class DocumentContentPage extends CommonPage { await this.rowToDo(text).hover() await expect(this.checkboxToDo(text)).toBeChecked({ checked, timeout: 5000 }) } + + async addImageToDocument (page: Page): Promise { + await this.inputContentParapraph().click() + await this.leftMenu().click() + await uploadFile(page, 'cat3.jpeg', 'Image') + await this.checkUserAddedImage() + } } diff --git a/tests/sanity/tests/model/documents/documents-page.ts b/tests/sanity/tests/model/documents/documents-page.ts index ba1bc17955..56cf3819dd 100644 --- a/tests/sanity/tests/model/documents/documents-page.ts +++ b/tests/sanity/tests/model/documents/documents-page.ts @@ -20,8 +20,16 @@ export class DocumentsPage extends CommonPage { readonly buttonCreateDocument = (): Locator => this.page.locator('div[data-float="navigator"] button[id="new-document"]') - readonly buttonDocument = (name: string): Locator => - this.page.locator('button.hulyNavItem-container > span[class*="label"]', { hasText: name }) + readonly buttonDocumentWrapper = (name: string): Locator => + this.page.locator(`button.hulyNavItem-container:has-text("${name}")`) + + readonly buttonDocument = (name: string): Locator => this.buttonDocumentWrapper(name).locator('span[class*="label"]') + + readonly buttonAddDocumentToDocument = (name: string): Locator => + this.buttonDocumentWrapper(name).getByTestId('document:string:CreateDocument') + + readonly breadcrumbsByDocumentParent = (parentDocumentTitle: string): Locator => + this.page.locator(`.hulyHeader-titleGroup:has-text("${parentDocumentTitle}")`) readonly buttonDocumentsApp = (): Locator => this.page.locator('button[id$="document:string:DocumentApplication"]') readonly divTeamspacesParent = (): Locator => @@ -123,6 +131,11 @@ export class DocumentsPage extends CommonPage { await this.selectFromDropdown(this.page, popupItem) } + async clickAddDocumentIntoDocument (documentTitle: string): Promise { + await this.buttonDocumentWrapper(documentTitle).hover() + await this.buttonAddDocumentToDocument(documentTitle).click() + } + async openDocumentForTeamspace (spaceName: string, documentName: string): Promise { await this.page .locator('button.hulyNavGroup-header span[class*="label"]', { hasText: spaceName }) @@ -176,4 +189,8 @@ export class DocumentsPage extends CommonPage { await expect(this.rowTeamspace(name)).toBeVisible() await this.buttonJoinTeamspace(name).click() } + + async checkIfParentDocumentIsExistInBreadcrumbs (parentDocumentTitle: string): Promise { + await expect(this.breadcrumbsByDocumentParent(parentDocumentTitle)).toBeVisible() + } } diff --git a/tests/sanity/tests/playwright.config.ts b/tests/sanity/tests/playwright.config.ts index 1e1cb379c1..a036ed6e3f 100644 --- a/tests/sanity/tests/playwright.config.ts +++ b/tests/sanity/tests/playwright.config.ts @@ -12,6 +12,7 @@ const config: PlaywrightTestConfig = { { name: 'Platform', use: { + testIdAttribute: 'data-id', permissions: ['clipboard-read', 'clipboard-write'], ...devices['Desktop Chrome'], screenshot: 'only-on-failure',