diff --git a/__mocks__/comments.mock.ts b/__mocks__/comments.mock.ts index 9bdd69b3..3110047b 100644 --- a/__mocks__/comments.mock.ts +++ b/__mocks__/comments.mock.ts @@ -10,5 +10,12 @@ export const MOCK_ANNOTATION = { text: 'any_text', createdAt: new Date().toISOString(), }, + { + uuid: 'any_uuid 2', + username: 'any_username 2', + avatar: 'any_avatar 2', + text: 'any_text 2', + createdAt: new Date().toISOString(), + }, ], }; diff --git a/src/components/comments/index.test.ts b/src/components/comments/index.test.ts index 7ada921b..c6ea51a3 100644 --- a/src/components/comments/index.test.ts +++ b/src/components/comments/index.test.ts @@ -2,14 +2,40 @@ import { MOCK_CONFIG } from '../../../__mocks__/config.mock'; import { EVENT_BUS_MOCK } from '../../../__mocks__/event-bus.mock'; import { MOCK_GROUP, MOCK_LOCAL_PARTICIPANT } from '../../../__mocks__/participants.mock'; import { ABLY_REALTIME_MOCK } from '../../../__mocks__/realtime.mock'; +import ApiService from '../../services/api'; import { CommentsComponent } from './index'; +jest.mock('../../services/api', () => ({ + fetchAnnotation: jest.fn().mockImplementation((): any => []), + createAnnotations: jest.fn().mockImplementation(() => []), + createComment: jest.fn().mockImplementation(() => []), + resolveAnnotation: jest.fn().mockImplementation(() => []), +})); + describe('CommentsComponent', () => { let commentsComponent: CommentsComponent; beforeEach(() => { + jest.clearAllMocks(); commentsComponent = new CommentsComponent(); + + commentsComponent.attach({ + realtime: ABLY_REALTIME_MOCK, + localParticipant: MOCK_LOCAL_PARTICIPANT, + group: MOCK_GROUP, + config: { + ...MOCK_CONFIG, + apiUrl: 'https://dev.nodeapi.superviz.com', + }, + eventBus: EVENT_BUS_MOCK, + }); + + commentsComponent['element'].addAnnotation = jest.fn().mockImplementation(() => []); + }); + + afterEach(() => { + commentsComponent.detach(); }); it('should create a new instance of CommentsComponent', () => { @@ -25,45 +51,77 @@ describe('CommentsComponent', () => { }); it('should have an element property', () => { - expect(commentsComponent['element']).toBeUndefined(); + expect(commentsComponent['element']).toBeDefined(); }); it('should create a new element when start() is called', () => { - commentsComponent.attach({ - realtime: ABLY_REALTIME_MOCK, - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, - config: MOCK_CONFIG, - eventBus: EVENT_BUS_MOCK, - }); commentsComponent.detach(); expect(commentsComponent['element']).toBeUndefined(); }); it('should add the element to the document body when start() is called', () => { - commentsComponent.attach({ - realtime: ABLY_REALTIME_MOCK, - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, - config: MOCK_CONFIG, - eventBus: EVENT_BUS_MOCK, - }); - expect(document.body.contains(commentsComponent['element'])).toBe(true); }); it('should remove the element from the document body when destroy() is called', async () => { - commentsComponent.attach({ - realtime: ABLY_REALTIME_MOCK, - localParticipant: MOCK_LOCAL_PARTICIPANT, - group: MOCK_GROUP, - config: MOCK_CONFIG, - eventBus: EVENT_BUS_MOCK, - }); expect(commentsComponent['element']).toBeDefined(); commentsComponent.detach(); expect(document.body.contains(commentsComponent['element'])).toBe(false); }); + + it('should call apiService when fetch annotation', async () => { + const result = jest.spyOn(ApiService, 'fetchAnnotation'); + + expect(result).toHaveBeenCalledWith( + expect.any(String), + expect.any(String), + { + roomId: expect.any(String), + url: expect.any(String), + }, + ); + }); + + it('should call apiService when create annotation', async () => { + const result = jest.spyOn(ApiService, 'createAnnotations'); + + commentsComponent['element'].dispatchEvent(new CustomEvent('create-annotation', { + detail: { + text: 'test', + position: { + x: 0, + y: 0, + }, + }, + })); + + expect(result).toHaveBeenCalledWith( + expect.any(String), + expect.any(String), + { + roomId: expect.any(String), + url: expect.any(String), + userId: expect.any(String), + position: expect.any(String), + }, + ); + }); + + it('should call apiService when resolve annotation', async () => { + const result = jest.spyOn(ApiService, 'resolveAnnotation'); + + commentsComponent['element'].dispatchEvent(new CustomEvent('resolve-annotation', { + detail: { + uuid: 'test', + }, + })); + + expect(result).toHaveBeenCalledWith( + expect.any(String), + expect.any(String), + 'test', + ); + }); }); diff --git a/src/components/comments/index.ts b/src/components/comments/index.ts index 628414ef..a54ec120 100644 --- a/src/components/comments/index.ts +++ b/src/components/comments/index.ts @@ -54,6 +54,7 @@ export class CommentsComponent extends BaseComponent { */ private addListeners(): void { this.element.addEventListener('create-annotation', this.createAnnotation); + this.element.addEventListener('resolve-annotation', this.resolveAnnotation); } /** @@ -63,6 +64,7 @@ export class CommentsComponent extends BaseComponent { */ private destroyListeners(): void { this.element.removeEventListener('create-annotation', this.createAnnotation); + this.element.removeEventListener('resolve-annotation', this.createAnnotation); } /** @@ -72,22 +74,27 @@ export class CommentsComponent extends BaseComponent { * @returns {Promise} */ private createAnnotation = async (e: CustomEvent): Promise => { - const { text, position } = e.detail; - const { url } = this; - - const annotation: Annotation = await ApiService.createAnnotations(config.get('apiUrl'), config.get('apiKey'), { - roomId: config.get('roomId'), - position: JSON.stringify(position), - url, - userId: this.localParticipant.id, - }); - - const comment = await this.createComment(annotation.uuid, text); - - this.addAnnotation([{ - ...annotation, - comments: [comment], - }]); + try { + const { text, position } = e.detail; + const { url } = this; + + const annotation: Annotation = await ApiService.createAnnotations(config.get('apiUrl'), config.get('apiKey'), { + roomId: config.get('roomId'), + position: JSON.stringify(position), + url, + userId: this.localParticipant.id, + }); + + const comment = await this.createComment(annotation.uuid, text); + + this.addAnnotation([{ + ...annotation, + comments: [comment], + }]); + } catch (error) { + this.logger.log('error when creating annotation', error); + throw error; + } }; /** @@ -98,11 +105,16 @@ export class CommentsComponent extends BaseComponent { * @returns {Promise} - A promise that resolves with the created comment object */ private async createComment(annotationId: string, text: string): Promise { - return ApiService.createComment(config.get('apiUrl'), config.get('apiKey'), { - annotationId, - userId: this.localParticipant.id, - text, - }); + try { + return await ApiService.createComment(config.get('apiUrl'), config.get('apiKey'), { + annotationId, + userId: this.localParticipant.id, + text, + }); + } catch (error) { + this.logger.log('error when creating comment', error); + throw error; + } } /** @@ -135,6 +147,27 @@ export class CommentsComponent extends BaseComponent { this.addAnnotation(annotations); } catch (error) { this.logger.log('error when fetching annotations', error); + throw error; + } + } + + /** + * @function resolveAnnotation + * @description Resolves an annotation by UUID using the API + * @param {CustomEvent} e - The custom event containing the UUID of the annotation to resolve + * @returns {Promise} + */ + private async resolveAnnotation(e: CustomEvent): Promise { + try { + const { uuid } = e.detail; + await ApiService.resolveAnnotation( + config.get('apiUrl'), + config.get('apiKey'), + uuid, + ); + } catch (error) { + this.logger.log('error when fetching annotations', error); + throw error; } } } diff --git a/src/components/comments/types.ts b/src/components/comments/types.ts index 8a4c4e1c..3ea8863b 100644 --- a/src/components/comments/types.ts +++ b/src/components/comments/types.ts @@ -11,4 +11,7 @@ export type Comment = { avatar: string; text: string; createdAt: string; + + resolvable?: boolean; + resolved?: boolean; }; diff --git a/src/services/api/index.test.ts b/src/services/api/index.test.ts index 269800c4..26da01d9 100644 --- a/src/services/api/index.test.ts +++ b/src/services/api/index.test.ts @@ -44,6 +44,10 @@ jest.mock('../../common/utils', () => { if (url.includes('/comments') && method === 'POST') { return Promise.resolve({}); } + + if (url.includes('/annotations/resolve/any_annotation_id') && method === 'POST') { + return Promise.resolve({}); + } }), }; }); @@ -128,5 +132,16 @@ describe('ApiService', () => { expect(response).toEqual([]); }); + + test('should resolve an annotation', async () => { + const baseUrl = 'https://dev.nodeapi.superviz.com'; + const response = await ApiService.resolveAnnotation( + baseUrl, + VALID_API_KEY, + 'any_annotation_id', + ); + + expect(response).toEqual({}); + }); }); }); diff --git a/src/services/api/index.ts b/src/services/api/index.ts index 187f58b4..85fc35b4 100644 --- a/src/services/api/index.ts +++ b/src/services/api/index.ts @@ -49,4 +49,11 @@ export default class ApiService { }); return doRequest(url, 'GET', undefined, { apikey: apiKey }); } + + static async resolveAnnotation(baseUrl: string, apiKey: string, annotationId: string) { + const path = `/annotations/resolve/${annotationId}`; + const url = this.createUrl(baseUrl, path); + + return doRequest(url, 'POST', {}, { apikey: apiKey }); + } } diff --git a/src/web-components/base/index.ts b/src/web-components/base/index.ts index db502930..0f1c359a 100644 --- a/src/web-components/base/index.ts +++ b/src/web-components/base/index.ts @@ -1,4 +1,4 @@ -import { LitElement, css } from 'lit'; +import { LitElement } from 'lit'; import { variableStyle, typography, svHr } from './styles'; import { Constructor, WebComponentsBaseInterface } from './types'; diff --git a/src/web-components/comments/comments.ts b/src/web-components/comments/comments.ts index d329c78d..7f71237d 100644 --- a/src/web-components/comments/comments.ts +++ b/src/web-components/comments/comments.ts @@ -33,6 +33,10 @@ export class Comments extends WebComponentsBaseElement { ]; } + updateAnnotations(data: Annotation[]) { + this.annotations = data; + } + toggle() { this.open = !this.open; } diff --git a/src/web-components/comments/components/annotation-item.ts b/src/web-components/comments/components/annotation-item.ts index c11cd70e..67f945ee 100644 --- a/src/web-components/comments/components/annotation-item.ts +++ b/src/web-components/comments/components/annotation-item.ts @@ -5,17 +5,17 @@ import { Annotation, Comment } from '../../../components/comments/types'; import { WebComponentsBase } from '../../base'; import { annotationItemStyle } from '../css'; +import { AnnotationOptions } from './types'; + const WebComponentsBaseElement = WebComponentsBase(LitElement); -const styles: CSSResultGroup[] = [ - WebComponentsBaseElement.styles, - annotationItemStyle, -]; +const styles: CSSResultGroup[] = [WebComponentsBaseElement.styles, annotationItemStyle]; @customElement('superviz-comments-annotation-item') export class CommentsAnnotationItem extends WebComponentsBaseElement { declare annotation: Annotation; declare expandComments: boolean; declare selected: string; + declare options: AnnotationOptions; static styles = styles; @@ -23,8 +23,16 @@ export class CommentsAnnotationItem extends WebComponentsBaseElement { annotation: { type: Object }, expandComments: { type: Boolean }, selected: { type: String, reflect: true }, + options: { type: Object }, }; + protected firstUpdated(): void { + this.options = { + resolvable: true, + resolved: this.annotation.resolved, + }; + } + updated(changedProperties: Map) { if (changedProperties.has('selected')) { const isSelected = this.selected === this.annotation.uuid; @@ -32,6 +40,11 @@ export class CommentsAnnotationItem extends WebComponentsBaseElement { } } + selectAnnotation = () => { + const { uuid } = this.annotation; + this.emitEvent('select-annotation', { uuid }); + }; + protected render() { const replies = this.annotation.comments.length; @@ -39,16 +52,12 @@ export class CommentsAnnotationItem extends WebComponentsBaseElement { const annotationClasses: string = [ 'annotation-item', - isSelected ? 'annotation-item--selected' : 'annotation-item', + isSelected ? 'annotation-item--selected' : '', ].join(' '); - const shouldExpandAvatarComments = () => { - return !this.expandComments && replies > 1 ? 'comment-avatar--expand' : 'hidden'; - }; + const shouldExpandAvatarComments = !this.expandComments && replies > 1 ? 'comment-avatar--expand' : 'hidden'; - const shouldExpandComments = () => { - return isSelected && this.expandComments ? 'comment-item--expand' : 'hidden'; - }; + const shouldExpandComments = isSelected && this.expandComments ? 'comment-item--expand' : 'hidden'; const avatarComments = (comment: Comment, index: number) => { if (index === 1) return html``; @@ -75,21 +84,31 @@ export class CommentsAnnotationItem extends WebComponentsBaseElement { `; }; - const selectAnnotation = () => { - this.emitEvent('selectAnnotation', { uuid: this.annotation.uuid }); + const resolveAnnotation = (e: CustomEvent) => { + const { uuid } = this.annotation; + const { resolved } = e.detail; + + this.emitEvent('resolve-annotation', { + uuid, + resolved, + }); + + this.options.resolved = resolved; }; return html` -
selectAnnotation()}> +
this.selectAnnotation()}>
-
+
${this.annotation.comments.map(avatarComments)}
${replies} replies
@@ -97,7 +116,7 @@ export class CommentsAnnotationItem extends WebComponentsBaseElement {
-
+
${this.annotation.comments.map(expandedComments)} this.shadowRoot!.getElementById('comment-input--textarea') as HTMLTextAreaElement; + private getCommentInputContainer = () => this.shadowRoot!.getElementById('comment-input--container') as HTMLDivElement; + private getSendBtn = () => this.shadowRoot!.querySelector('.comment-input--send-btn') as HTMLButtonElement; - if (this.text) { - const commentsInput = this.shadowRoot?.getElementById('comment-input--textarea') as HTMLTextAreaElement; + updated(changedProperties: Map) { + if (changedProperties.has('text') && this.text.length > 0) { + const commentsInput = this.getCommentInput(); commentsInput.value = this.text; + this.updateHeight(); + } + + if (changedProperties.has('btnActive')) { + const btnSend = this.getSendBtn(); + btnSend.disabled = !this.btnActive; } } private updateHeight() { - const commentsInput = this.shadowRoot?.getElementById('comment-input--textarea') as HTMLTextAreaElement; - const commentsInputContainer = this.shadowRoot?.getElementById('comment-input--container') as HTMLDivElement; + const commentsInput = this.getCommentInput(); + const commentsInputContainer = this.getCommentInputContainer(); commentsInput.style.height = '0px'; commentsInputContainer.style.height = '0px'; @@ -51,15 +59,15 @@ export class CommentsCommentInput extends WebComponentsBaseElement { commentsInput.style.height = `${textareaHeight}px`; commentsInputContainer.style.height = `${textareaContainerHeight}px`; - const btnSend = this.shadowRoot?.querySelector('.comment-input--send-btn') as HTMLButtonElement; + const btnSend = this.getSendBtn(); btnSend.disabled = !(commentsInput.value.length > 0); } private send(e: Event) { e.preventDefault(); - const input = this.shadowRoot?.getElementById('comment-input--textarea') as HTMLTextAreaElement; - const sendBtn = this.shadowRoot?.querySelector('.comment-input--send-btn') as HTMLButtonElement; + const input = this.getCommentInput(); + const sendBtn = this.getSendBtn(); const text = input.value; this.emitEvent(this.eventType, { text }); diff --git a/src/web-components/comments/components/comment-item.ts b/src/web-components/comments/components/comment-item.ts index 23c97e57..63b4ab4a 100644 --- a/src/web-components/comments/components/comment-item.ts +++ b/src/web-components/comments/components/comment-item.ts @@ -1,15 +1,22 @@ -import { CSSResultGroup, LitElement, html } from 'lit'; +import { CSSResultGroup, LitElement, PropertyValueMap, html } from 'lit'; import { customElement } from 'lit/decorators.js'; import { DateTime } from 'luxon'; import { WebComponentsBase } from '../../base'; import { commentItemStyle } from '../css'; +import { AnnotationOptions } from './types'; + const WebComponentsBaseElement = WebComponentsBase(LitElement); const styles: CSSResultGroup[] = [WebComponentsBaseElement.styles, commentItemStyle]; @customElement('superviz-comments-comment-item') export class CommentsCommentItem extends WebComponentsBaseElement { + constructor() { + super(); + this.resolved = false; + } + static styles = styles; declare avatar: string; @@ -17,6 +24,7 @@ export class CommentsCommentItem extends WebComponentsBaseElement { declare text: string; declare resolved: boolean; declare createdAt: string; + declare options: AnnotationOptions; static properties = { avatar: { type: String }, @@ -24,6 +32,13 @@ export class CommentsCommentItem extends WebComponentsBaseElement { text: { type: String }, resolved: { type: Boolean }, createdAt: { type: String }, + options: { type: Object }, + }; + + updated = (changedProperties: PropertyValueMap) => { + if (changedProperties.has('options')) { + this.resolved = this.options?.resolved; + } }; protected render() { @@ -31,17 +46,33 @@ export class CommentsCommentItem extends WebComponentsBaseElement { return DateTime.fromISO(date).toFormat('yyyy-dd-MM'); }; + const isResolvable = this.options?.resolvable ? 'comment-item__resolve' : 'hidden'; + + const iconResolve = this.resolved ? 'resolved' : 'unresolved'; + + const resolveAnnotation = () => { + this.resolved = !this.resolved; + + this.emitEvent('resolve-annotation', { + resolved: this.resolved, + }, { composed: false, bubbles: false }); + }; + return html`
-
- +
+
+ +
+ ${this.username} + ${humanizeDate(this.createdAt)} +
+
+
- ${this.username} - ${humanizeDate(this.createdAt)} - - -
diff --git a/src/web-components/comments/components/content.ts b/src/web-components/comments/components/content.ts index d67b6367..1da0ce8c 100644 --- a/src/web-components/comments/components/content.ts +++ b/src/web-components/comments/components/content.ts @@ -30,7 +30,8 @@ export class CommentsContent extends WebComponentsBaseElement { return this.annotations.length === index + 1 ? 'hidden' : ''; }; - const selectAnnotation = (uuid: string) => { + const selectAnnotation = (e: CustomEvent) => { + const { uuid } = e.detail; this.selectedAnnotation = uuid; }; @@ -45,7 +46,7 @@ export class CommentsContent extends WebComponentsBaseElement { selectAnnotation(e.detail.uuid)} + @select-annotation=${selectAnnotation} > diff --git a/src/web-components/comments/components/types.ts b/src/web-components/comments/components/types.ts new file mode 100644 index 00000000..e26f2b97 --- /dev/null +++ b/src/web-components/comments/components/types.ts @@ -0,0 +1,4 @@ +export type AnnotationOptions = { + resolvable?: boolean; + resolved?: boolean; +} diff --git a/src/web-components/comments/css/comment-item.style.ts b/src/web-components/comments/css/comment-item.style.ts index b1cf21a1..d68cc622 100644 --- a/src/web-components/comments/css/comment-item.style.ts +++ b/src/web-components/comments/css/comment-item.style.ts @@ -5,6 +5,7 @@ export const commentItemStyle = css` display: flex; flex-direction: column; align-items: flex-start; + justify-content: center; padding: 8px; gap: 4px; } @@ -12,11 +13,19 @@ export const commentItemStyle = css` .comment-item__user { display: flex; flex-direction: row; + justify-content: space-between; + width: 100%; align-items: center; - gap: 8px; color: rgb(var(--sv-gray-500)); } + .comment-item__user-details { + display: flex; + width: 100%; + gap: 8px; + align-items: center; + } + .comment-item__avatar { width: 24px; height: 24px; @@ -33,4 +42,8 @@ export const commentItemStyle = css` .comment-item__content__body { color: rgb(var(--sv-gray-700)); } + + .hidden { + display: none; + } `; diff --git a/src/web-components/comments/tests/comments.test.ts b/src/web-components/comments/tests/comments.test.ts index 22a9e76d..ef0683f8 100644 --- a/src/web-components/comments/tests/comments.test.ts +++ b/src/web-components/comments/tests/comments.test.ts @@ -1,4 +1,5 @@ import '..'; +import { MOCK_ANNOTATION } from '../../../../__mocks__/comments.mock'; import sleep from '../../../common/utils/sleep'; let element: HTMLElement; @@ -22,7 +23,7 @@ describe('comments', () => { test('should close superviz comments', async () => { const renderedElement = document.getElementsByTagName('superviz-comments')[0]; - const app = renderedElement.shadowRoot?.getElementById('superviz-comments'); + const app = renderedElement.shadowRoot!.getElementById('superviz-comments'); renderedElement.setAttributeNode(document.createAttribute('open')); renderedElement.removeAttribute('open'); @@ -33,7 +34,7 @@ describe('comments', () => { test('should open superviz comments', async () => { const renderedElement = document.getElementsByTagName('superviz-comments')[0]; - const app = renderedElement.shadowRoot?.getElementById('superviz-comments'); + const app = renderedElement.shadowRoot!.getElementById('superviz-comments'); renderedElement.setAttributeNode(document.createAttribute('open')); @@ -72,4 +73,22 @@ describe('comments', () => { expect(element['open']).toEqual(!isOpen); }); + + test('should update annotation', async () => { + const annotation = { + ...MOCK_ANNOTATION, + position: 'any_position', + }; + + element['addAnnotation']([annotation]); + + const annotationUpdated = { + ...MOCK_ANNOTATION, + position: 'any_position_updated', + }; + + element['updateAnnotations']([annotationUpdated]); + + expect(element['annotations']).toEqual([annotationUpdated]); + }); }); diff --git a/src/web-components/comments/tests/components/annotation-item.test.ts b/src/web-components/comments/tests/components/annotation-item.test.ts new file mode 100644 index 00000000..994363fd --- /dev/null +++ b/src/web-components/comments/tests/components/annotation-item.test.ts @@ -0,0 +1,79 @@ +import { MOCK_ANNOTATION } from '../../../../../__mocks__/comments.mock'; +import sleep from '../../../../common/utils/sleep'; +import { Annotation } from '../../../../components/comments/types'; +import '../../components'; + +let element: HTMLElement; + +const createElement = async (annotation: Annotation) => { + const element = document.createElement('superviz-comments-annotation-item'); + element.setAttribute('annotation', JSON.stringify(annotation)); + document.body.appendChild(element); + await sleep(); + return element; +}; + +describe('CommentsAnnotationItem', () => { + afterEach(() => { + document.body.removeChild(element); + }); + + test('renders the annotation', async () => { + element = await createElement(MOCK_ANNOTATION); + + expect(element).toBeDefined(); + }); + + test('expands the comments when the annotation is selected', async () => { + element = await createElement(MOCK_ANNOTATION); + const annotationItem = element.shadowRoot!.querySelector('.annotation-item') as HTMLElement; + const commentsContainer = element.shadowRoot!.querySelector('.comments-container') as HTMLElement; + + expect(annotationItem.classList.contains('annotation-item--selected')).toBe(false); + expect(commentsContainer.classList.contains('hidden')).toBe(true); + expect(commentsContainer.classList.contains('comment-item--expand')).toBe(false); + + annotationItem!.addEventListener('select-annotation', () => { + element.setAttribute('selected', MOCK_ANNOTATION.uuid); + }); + + annotationItem.click(); + + annotationItem!.dispatchEvent(new CustomEvent('select-annotation', { detail: { resolved: 'true' } })); + + await sleep(); + + const expectedAnnotation = element.shadowRoot!.querySelector('.annotation-item') as HTMLElement; + const expectedCommentsContainer = element.shadowRoot!.querySelector('.comments-container') as HTMLElement; + + expect(expectedAnnotation.classList.contains('annotation-item--selected')).toBe(true); + expect(expectedCommentsContainer.classList.contains('comment-item--expand')).toBe(true); + expect(expectedCommentsContainer.classList.contains('hidden')).toBe(false); + }); + + test('resolves the annotation when the comment is unresolved', async () => { + element = await createElement(MOCK_ANNOTATION); + const commentItem = element.shadowRoot!.querySelector('superviz-comments-comment-item'); + + commentItem!.dispatchEvent(new CustomEvent('resolve-annotation', { detail: { resolved: 'true' } })); + + await sleep(); + + expect(element['options']['resolved']).toBe('true'); + }); + + test('resolves the annotation when the comment is resolved', async () => { + element = await createElement({ + ...MOCK_ANNOTATION, + resolved: true, + }); + expect(element['options']['resolved']).toBe(true); + const commentItem = element.shadowRoot!.querySelector('superviz-comments-comment-item'); + + commentItem!.dispatchEvent(new CustomEvent('resolve-annotation', { detail: { resolved: 'false' } })); + + await sleep(); + + expect(element['options']['resolved']).toBe('false'); + }); +}); diff --git a/src/web-components/comments/tests/components/annotations.test.ts b/src/web-components/comments/tests/components/annotations.test.ts index da020eb7..1a0e0917 100644 --- a/src/web-components/comments/tests/components/annotations.test.ts +++ b/src/web-components/comments/tests/components/annotations.test.ts @@ -1,12 +1,42 @@ +import sleep from '../../../../common/utils/sleep'; import '../../components'; -const element = document.createElement('superviz-comments-annotations'); -document.body.appendChild(element); +let element: HTMLElement; describe('CommentsAnnotations', () => { + beforeEach(async () => { + element = document.createElement('superviz-comments-annotations'); + document.body.appendChild(element); + await sleep(); + }); + + afterEach(() => { + document.body.removeChild(element); + }); + test('renders the add comment button', async () => { const button = element.shadowRoot!.querySelector('.add-comment-btn') as HTMLSpanElement; expect(button).toBeDefined(); expect(button.textContent).toEqual('Click anywhere to add a comment'); }); + + test('renders the comment input', async () => { + const commentInput = element.shadowRoot!.querySelector('superviz-comments-comment-input') as HTMLElement; + expect(commentInput).toBeDefined(); + }); + + test('emits an event when the comment input is submitted', async () => { + const commentInput = element.shadowRoot!.querySelector('superviz-comments-comment-input') as HTMLElement; + const eventSpy = jest.fn(); + element.addEventListener('create-annotation', eventSpy); + commentInput.dispatchEvent(new CustomEvent('create-comment', { + detail: { + text: 'test', + }, + })); + + expect(eventSpy).toHaveBeenCalledTimes(1); + + element.removeEventListener('create-annotation', eventSpy); + }); }); diff --git a/src/web-components/comments/tests/components/comment-input.test.ts b/src/web-components/comments/tests/components/comment-input.test.ts index 7acf69c8..8f897fe0 100644 --- a/src/web-components/comments/tests/components/comment-input.test.ts +++ b/src/web-components/comments/tests/components/comment-input.test.ts @@ -1,31 +1,36 @@ +import sleep from '../../../../common/utils/sleep'; import '../../components'; -const element = document.createElement('superviz-comments-comment-input'); -document.body.appendChild(element); +let element: HTMLElement; describe('CommentsCommentInput', () => { + beforeEach(async () => { + element = document.createElement('superviz-comments-comment-input'); + document.body.appendChild(element); + await sleep(); + }); + + afterEach(() => { + document.body.removeChild(element); + }); + test('renders a textarea and a send button', async () => { - const renderedElement = document.getElementsByTagName('superviz-comments-comment-input')[0]; - const textarea = renderedElement.shadowRoot?.querySelector('#comment-input--textarea') as HTMLTextAreaElement; - const sendButton = renderedElement.shadowRoot?.querySelector('.comment-input--send-btn') as HTMLButtonElement; + const textarea = element.querySelector('#comment-input--textarea') as HTMLTextAreaElement; + const sendButton = element.shadowRoot!.querySelector('.comment-input--send-btn') as HTMLButtonElement; expect(textarea).toBeDefined(); expect(sendButton).toBeDefined(); }); test('disables the send button when the textarea is empty', async () => { - const renderedElement = document.getElementsByTagName('superviz-comments-comment-input')[0]; - - const sendButton = renderedElement.shadowRoot?.querySelector('.comment-input--send-btn') as HTMLButtonElement; + const sendButton = element.shadowRoot!.querySelector('.comment-input--send-btn') as HTMLButtonElement; expect(sendButton.disabled).toBe(true); }); test('enables the send button when the textarea has text', async () => { - const renderedElement = document.getElementsByTagName('superviz-comments-comment-input')[0]; - - const textarea = renderedElement.shadowRoot?.querySelector('#comment-input--textarea') as HTMLTextAreaElement; - const sendButton = renderedElement.shadowRoot?.querySelector('.comment-input--send-btn') as HTMLButtonElement; + const textarea = element.shadowRoot!.querySelector('#comment-input--textarea') as HTMLTextAreaElement; + const sendButton = element.shadowRoot!.querySelector('.comment-input--send-btn') as HTMLButtonElement; textarea.value = 'test'; textarea.dispatchEvent(new Event('input')); @@ -34,14 +39,13 @@ describe('CommentsCommentInput', () => { }); test('emits an event when the send button is clicked', async () => { - const renderedElement = document.getElementsByTagName('superviz-comments-comment-input')[0]; - const textarea = renderedElement.shadowRoot?.querySelector('#comment-input--textarea') as HTMLTextAreaElement; - const sendButton = renderedElement.shadowRoot?.querySelector('.comment-input--send-btn') as HTMLButtonElement; + const textarea = element.shadowRoot!.querySelector('#comment-input--textarea') as HTMLTextAreaElement; + const sendButton = element.shadowRoot!.querySelector('.comment-input--send-btn') as HTMLButtonElement; const eventSpy = jest.fn(); - renderedElement.setAttribute('eventType', 'send'); - renderedElement.addEventListener('send', eventSpy); + element.setAttribute('eventType', 'send'); + element.addEventListener('send', eventSpy); textarea.value = 'test'; textarea.dispatchEvent(new Event('input')); @@ -50,4 +54,28 @@ describe('CommentsCommentInput', () => { expect(eventSpy).toHaveBeenCalledTimes(1); }); + + test('test custom props with text', async () => { + element.setAttribute('text', 'test'); + + await sleep(); + + const textarea = element.shadowRoot!.querySelector('#comment-input--textarea') as HTMLTextAreaElement; + const btnSend = element.shadowRoot!.querySelector('.comment-input--send-btn') as HTMLButtonElement; + + expect(textarea.value).toBe('test'); + expect(btnSend.disabled).toBe(false); + }); + + test('test custom props with btnActive', async () => { + element.setAttributeNode(document.createAttribute('btnActive')); + + await sleep(); + + const textarea = element.shadowRoot!.querySelector('#comment-input--textarea') as HTMLTextAreaElement; + const btnSend = element.shadowRoot!.querySelector('.comment-input--send-btn') as HTMLButtonElement; + + expect(textarea.value).toBe(''); + expect(btnSend.disabled).toBe(false); + }); }); diff --git a/src/web-components/comments/tests/components/comment-item.test.ts b/src/web-components/comments/tests/components/comment-item.test.ts index e78ac32b..da8716a6 100644 --- a/src/web-components/comments/tests/components/comment-item.test.ts +++ b/src/web-components/comments/tests/components/comment-item.test.ts @@ -1,6 +1,9 @@ -import '../../components'; import { DateTime } from 'luxon'; +import sleep from '../../../../common/utils/sleep'; + +import '../../components'; + const element = document.createElement('superviz-comments-comment-item'); document.body.appendChild(element); @@ -8,6 +11,7 @@ element.setAttribute('avatar', 'https://example.com/avatar.png'); element.setAttribute('username', 'John Doe'); element.setAttribute('text', 'This is a comment'); element.setAttribute('createdAt', DateTime.now().toISO() as string); +element.setAttribute('options', JSON.stringify({ resolved: false, resolvable: true })); describe('CommentsCommentItem', () => { test('renders the comment item with correct properties', async () => { @@ -25,4 +29,26 @@ describe('CommentsCommentItem', () => { const text = renderedElement.shadowRoot!.querySelector('.comment-item__content .text-big') as HTMLSpanElement; expect(text.textContent).toEqual('This is a comment'); }); + + test('resolves the annotation when the comment is unresolved', async () => { + await element['updateComplete']; + + const renderedElement = document.getElementsByTagName('superviz-comments-comment-item')[0]; + const resolveButton = renderedElement.shadowRoot!.querySelector('.comment-item__resolve > button') as HTMLButtonElement; + + element.dispatchEvent = jest.fn(); + + resolveButton.click(); + + await sleep(); + + expect(element.dispatchEvent) + .toHaveBeenCalledWith( + new CustomEvent( + 'resolve-annotation', + { detail: { resolved: true } }, + ), + ); + expect(element['resolved']).toEqual(true); + }); }); diff --git a/src/web-components/comments/tests/components/content.test.ts b/src/web-components/comments/tests/components/content.test.ts index 88c15ec0..356b395d 100644 --- a/src/web-components/comments/tests/components/content.test.ts +++ b/src/web-components/comments/tests/components/content.test.ts @@ -27,9 +27,9 @@ describe('CommentsContent', () => { await sleep(); - const annotationItem = element?.shadowRoot?.querySelectorAll('superviz-comments-annotation-item')[0]; + const annotationItem = element.shadowRoot!.querySelectorAll('superviz-comments-annotation-item')[0]; - annotationItem?.dispatchEvent(new CustomEvent('selectAnnotation', { + annotationItem?.dispatchEvent(new CustomEvent('select-annotation', { detail: { uuid: 'any_uuid', }, @@ -48,7 +48,7 @@ describe('CommentsContent', () => { await sleep(); - const hr = element?.shadowRoot?.querySelectorAll('.sv-hr')[0]; + const hr = element.shadowRoot!.querySelectorAll('.sv-hr')[0]; expect(hr?.classList.contains('hidden')).toBe(true); }); @@ -61,7 +61,7 @@ describe('CommentsContent', () => { await sleep(); - const hr = element?.shadowRoot?.querySelectorAll('.sv-hr')[0]; + const hr = element.shadowRoot!.querySelectorAll('.sv-hr')[0]; expect(hr?.classList.contains('hidden')).toBe(false); }); diff --git a/src/web-components/comments/tests/components/topbar.test.ts b/src/web-components/comments/tests/components/topbar.test.ts index 2e408d6a..f97dbfca 100644 --- a/src/web-components/comments/tests/components/topbar.test.ts +++ b/src/web-components/comments/tests/components/topbar.test.ts @@ -11,13 +11,13 @@ describe('CommentsTopbar', () => { test('renders the title', async () => { const renderedElement = document.getElementsByTagName('superviz-comments-topbar')[0]; - const title = renderedElement.shadowRoot?.querySelector('.text-bold'); + const title = renderedElement.shadowRoot!.querySelector('.text-bold'); expect(title?.textContent).toEqual('COMMENTS'); }); test('dispatches a close event when the close button is clicked', async () => { const renderedElement = document.getElementsByTagName('superviz-comments-topbar')[0]; - const closeButton = renderedElement.shadowRoot?.querySelector('span:last-child'); + const closeButton = renderedElement.shadowRoot!.querySelector('span:last-child'); const eventSpy = jest.fn(); renderedElement.addEventListener('close', eventSpy);