Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[OS-9314] Create a single view and change the css class of the thread view #17

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,20 @@ urlpatterns = [
]
```

Then to render the widget for an object, include the CSS and JS file while adding a `div.comment-viewer` element to your DOM:
Then to render the widget for an object, include the CSS and JS file while adding a :
- `div.comment-viewer` element to your DOM if you want to display one comment (simple view).
- `div.comment-thread-viewer` element to your DOM if you want to display a paginated list of all comments (thread view).

```html
{% block style %}
<link href="{% static 'osis_comment/osis-comment.css' %}" rel="stylesheet"/>
{% endblock style %}

{% block content %}
<!-- Simple view -->
<div class="comment-viewer" data-url="{% url 'some-test' object.uuid %}"></div>
<!-- Thread view -->
<div class="comment-thread-viewer" data-url="{% url 'some-test' object.uuid %}"></div>
{% endblock %}

{% block script %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,24 @@
*
*/

import CommentThread from './CommentThread.vue';
import CommentViewer from './CommentViewer.vue';
import fetchMock from 'fetch-mock';
import type {Meta, StoryFn} from "@storybook/vue3";
import type {EntriesResponse} from "./interfaces";

export default {
title: 'CommentThread',
component: CommentThread,
title: 'CommentViewer',
component: CommentViewer,
} as Meta;

const Template: StoryFn<typeof CommentThread> = (args) => ({
components: {CommentThread},
const Template: StoryFn<typeof CommentViewer> = (args) => ({
components: {CommentViewer},
setup() {
fetchMock.restore().mock('path:/api', mockEntries);
return {args};
},
template: `
<CommentThread v-bind="args" url="/api" />
<CommentViewer v-bind="args" url="/api" :single-mode="false" />
`,
});

Expand Down Expand Up @@ -101,48 +101,57 @@ Default.args = {
export const NoComments = () => {
fetchMock.restore().mock('path:/api', {...mockEntries, results: []});
return {
components: {CommentThread},
template: '<CommentThread url="/api"/>',
components: {CommentViewer},
template: '<CommentViewer url="/api" :single-mode="false" />',
};
};

export const HttpError = () => {
fetchMock.restore().mock('path:/api', 404);
return {
components: {CommentThread},
template: '<CommentThread url="/api"/>',
components: {CommentViewer},
template: '<CommentViewer url="/api" :single-mode="false" />',
};
};

export const Exception = () => {
fetchMock.restore().mock('path:/api', {throws: new Error('Some network error')});
return {
components: {CommentThread},
template: '<CommentThread url="/api"/>',
components: {CommentViewer},
template: '<CommentViewer url="/api" :single-mode="false" />',
};
};

export const WithoutAdding = () => {
fetchMock.restore().mock('path:/api', {...mockEntries, create: {error: "Nope"}});
return {
components: {CommentThread},
template: '<CommentThread url="/api"/>',
components: {CommentViewer},
template: '<CommentViewer url="/api" :single-mode="false" />',
};
};

export const Paginated = () => {
fetchMock.restore().mock('path:/api', {...mockEntries, next: "/api?next", previous: "/api?previous"});
return {
components: {CommentThread},
template: '<CommentThread url="/api" />',
components: {CommentViewer},
template: '<CommentViewer url="/api" :single-mode="false" />',
};
};


export const Ckeditor = () => {
fetchMock.restore().mock('path:/api', {...mockEntries, results: []});
return {
components: {CommentThread},
template: '<CommentThread url="/api" :rich-text-config="{}" />',
components: {CommentViewer},
template: '<CommentViewer url="/api" :single-mode="false" :rich-text-config="{}" />',
};
};


export const SingleView = () => {
fetchMock.restore().mock('path:/api', {...mockEntries, results: [mockEntries.results[0]]});
return {
components: {CommentViewer},
template: '<CommentViewer url="/api" :single-mode="true" />',
};
};
148 changes: 126 additions & 22 deletions frontend/CommentThread.test.ts → frontend/CommentViewer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

import {beforeEach, expect, test} from 'vitest';
import {flushPromises, mount} from '@vue/test-utils';
import CommentThread from "./CommentThread.vue";
import CommentViewer from "./CommentViewer.vue";
import type {EntriesResponse} from "./interfaces";
import fetchMock from "fetch-mock";
import CommentEditor from "./components/CommentEditor.vue";
Expand Down Expand Up @@ -90,19 +90,19 @@ beforeEach(() => {
fetchMock.reset().mock('path:' + apiUrl, mockEntries);
});

test('thread wihout token', () => {
test('thread without token', () => {
(document.getElementById('csrf-token') as Element).remove();

expect(() => mount(CommentThread, {
props: {url: apiUrl},
expect(() => mount(CommentViewer, {
props: {url: apiUrl, singleMode: false},
})).toThrowError('Please include {% csrf_token %} in your page.');
});

test('thread', async () => {
expect(CommentThread).toBeTruthy();
expect(CommentViewer).toBeTruthy();

const wrapper = mount(CommentThread, {
props: {url: apiUrl},
const wrapper = mount(CommentViewer, {
props: {url: apiUrl, singleMode: false},
});
await flushPromises();
expect(wrapper.html()).toMatchSnapshot();
Expand All @@ -112,7 +112,7 @@ test('thread', async () => {
test('handle 404', async () => {
fetchMock.reset().mock('path:' + apiUrl, 404);

const wrapper = mount(CommentThread, {
const wrapper = mount(CommentViewer, {
props: {url: apiUrl},
});

Expand All @@ -124,8 +124,8 @@ test('handle 404', async () => {
test('handle network error', async () => {
fetchMock.reset().mock('path:' + apiUrl, {throws: new Error('Some network error')});

const wrapper = mount(CommentThread, {
props: {url: apiUrl},
const wrapper = mount(CommentViewer, {
props: {url: apiUrl, singleMode: false},
});

await flushPromises();
Expand All @@ -134,8 +134,8 @@ test('handle network error', async () => {
});

test('change sort', async () => {
const wrapper = mount(CommentThread, {
props: {url: apiUrl},
const wrapper = mount(CommentViewer, {
props: {url: apiUrl, singleMode: false},
});

await flushPromises();
Expand All @@ -153,6 +153,110 @@ test('change sort', async () => {
expect(sortButton.get('i').classes()).toContain('fa-sort-down');
});

test('single mode', async () => {
const previousUrl = '/api/dac97c6d-ddb9-47cf-bf72-913fa0ebbbfd/?limit=10&sort=-modified_at&page=1';
const nextUrl = '/api/dac97c6d-ddb9-47cf-bf72-913fa0ebbbfd/?limit=10&sort=-modified_at&page=3';

const sortButton = '.panel-heading .btn';
const previousButton = '.btn-default .fa-chevron-left';
const nextButton = '.btn-default .fa-chevron-right';
const createButton = '.btn-primary .fa-plus';

fetchMock.reset().mock('path:' + apiUrl, {
...mockEntries,
previous: previousUrl,
next: nextUrl,
});

const singleWrapperWithEntry = mount(CommentViewer, {
props: {url: apiUrl, singleMode: true},
});

await flushPromises();
expect(fetchMock.lastUrl()).toBe('/api/dac97c6d-ddb9-47cf-bf72-913fa0ebbbfd/?last=1');

[sortButton, previousButton, nextButton, createButton].forEach((selector) => {
expect(singleWrapperWithEntry.find(selector).exists()).toBe(false);
});

expect(singleWrapperWithEntry.find('.panel-heading').text()).toBe('thread.title');

expect(
singleWrapperWithEntry.find('.comment-authoring').text()).toBe(
'entry.last_update_by John Doe entry.authored_date',
);

fetchMock.reset().mock('path:' + apiUrl, {
...mockEntries,
results: [],
});

const singleWrapperWithoutEntry = mount(CommentViewer, {
props: {url: apiUrl, singleMode: true},
});

await flushPromises();

[sortButton, previousButton, nextButton].forEach((selector) => {
expect(singleWrapperWithoutEntry.find(selector).exists()).toBe(false);
});

expect(singleWrapperWithoutEntry.find(createButton).exists()).toBe(true);
});

test('thread mode', async () => {
const previousUrl = '/api/dac97c6d-ddb9-47cf-bf72-913fa0ebbbfd/?limit=10&sort=-modified_at&page=1';
const nextUrl = '/api/dac97c6d-ddb9-47cf-bf72-913fa0ebbbfd/?limit=10&sort=-modified_at&page=3';

const sortButton = '.panel-heading .btn';
const previousButton = '.btn-default .fa-chevron-left';
const nextButton = '.btn-default .fa-chevron-right';
const createButton = '.btn-primary .fa-plus';

fetchMock.reset().mock('path:' + apiUrl, {
...mockEntries,
previous: previousUrl,
next: nextUrl,
});

const threadWrapperWithEntries = mount(CommentViewer, {
props: {url: apiUrl, singleMode: false},
});

await flushPromises();
expect(fetchMock.lastUrl()).toBe('/api/dac97c6d-ddb9-47cf-bf72-913fa0ebbbfd/?limit=10&sort=-modified_at');

[sortButton, previousButton, nextButton, createButton].forEach((selector) => {
expect(threadWrapperWithEntries.find(selector).exists()).toBe(true);
});

expect(threadWrapperWithEntries.find('.panel-heading').text()).toBe('thread.title (3) thread.sort');

expect(
threadWrapperWithEntries.find('.comment-authoring').text()).toBe(
'John Doe entry.authored_date',
);

fetchMock.reset().mock('path:' + apiUrl, {
...mockEntries,
results: [],
});

const threadWrapperWithoutEntries = mount(CommentViewer, {
props: {url: apiUrl, singleMode: false},
});

await flushPromises();

[previousButton, nextButton].forEach((selector) => {
expect(threadWrapperWithoutEntries.find(selector).exists()).toBe(false);
});

[sortButton, createButton].forEach((selector) => {
expect(threadWrapperWithoutEntries.find(selector).exists()).toBe(true);
});
});

test('handle pagination', async () => {
const previousUrl = '/api/dac97c6d-ddb9-47cf-bf72-913fa0ebbbfd/?limit=10&sort=-modified_at&page=1';
const nextUrl = '/api/dac97c6d-ddb9-47cf-bf72-913fa0ebbbfd/?limit=10&sort=-modified_at&page=3';
Expand All @@ -162,8 +266,8 @@ test('handle pagination', async () => {
next: nextUrl,
});

const wrapper = mount(CommentThread, {
props: {url: apiUrl},
const wrapper = mount(CommentViewer, {
props: {url: apiUrl, singleMode: false},
});

await flushPromises();
Expand All @@ -176,8 +280,8 @@ test('handle pagination', async () => {
});

test('add comment', async () => {
const wrapper = mount(CommentThread, {
props: {url: apiUrl, tags: ['foo']},
const wrapper = mount(CommentViewer, {
props: {url: apiUrl, tags: ['foo'], singleMode: false},
});

await flushPromises();
Expand All @@ -198,8 +302,8 @@ test('add comment', async () => {
});

test('cancel adding comment', async () => {
const wrapper = mount(CommentThread, {
props: {url: apiUrl},
const wrapper = mount(CommentViewer, {
props: {url: apiUrl, singleMode: false},
});

await flushPromises();
Expand All @@ -212,8 +316,8 @@ test('cancel adding comment', async () => {
});

test('edit comment', async () => {
const wrapper = mount(CommentThread, {
props: {url: apiUrl},
const wrapper = mount(CommentViewer, {
props: {url: apiUrl, singleMode: false},
});

await flushPromises();
Expand Down Expand Up @@ -243,8 +347,8 @@ test('edit comment', async () => {

test('delete comment', async () => {

const wrapper = mount(CommentThread, {
props: {url: apiUrl},
const wrapper = mount(CommentViewer, {
props: {url: apiUrl, singleMode: false},
});

await flushPromises();
Expand Down
Loading
Loading