+ {{ entity.description }} +
+ +No recent items available
++ {{ entity.description }} +
+No results found for "{{ query }}"
+Nanopub 1 content
', + contributor: 'http://example.org/user1' + }, + { + '@id': 'http://example.org/nanopub2', + body: 'Nanopub 2 content
', + contributor: 'http://example.org/user2' + } + ]; + + const mockUser = { + uri: 'http://example.org/user1', + admin: false + }; + + beforeEach(() => { + nanopubModule.listNanopubs.mockResolvedValue(mockNanopubs); + nanopubModule.describeNanopub.mockResolvedValue({}); + nanopubModule.postNewNanopub.mockResolvedValue({}); + nanopubModule.deleteNanopub.mockResolvedValue({}); + }); + + afterEach(() => { + jest.clearAllMocks(); + if (wrapper) { + wrapper.destroy(); + } + }); + + it('renders without crashing', () => { + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: mockUser + } + }); + expect(wrapper.exists()).toBe(true); + }); + + it('loads nanopubs on mount', async () => { + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: mockUser + } + }); + + await wrapper.vm.$nextTick(); + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(nanopubModule.listNanopubs).toHaveBeenCalledWith('http://example.org/resource1'); + expect(wrapper.vm.nanopubs).toEqual(mockNanopubs); + }); + + it('shows loading state while loading', () => { + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: mockUser + } + }); + + wrapper.setData({ loading: true }); + expect(wrapper.find('.loading').exists()).toBe(true); + }); + + it('shows error state on load failure', async () => { + const error = new Error('Load failed'); + nanopubModule.listNanopubs.mockRejectedValue(error); + + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: mockUser + } + }); + + await wrapper.vm.$nextTick(); + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(wrapper.find('.error').exists()).toBe(true); + expect(wrapper.vm.error).toContain('Failed to load'); + }); + + it('determines if user can edit nanopub', () => { + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: mockUser + } + }); + + const ownNanopub = { contributor: 'http://example.org/user1' }; + const otherNanopub = { contributor: 'http://example.org/user2' }; + + expect(wrapper.vm.canEdit(ownNanopub)).toBe(true); + expect(wrapper.vm.canEdit(otherNanopub)).toBe(false); + }); + + it('allows admin to edit any nanopub', () => { + const adminUser = { uri: 'http://example.org/admin', admin: 'True' }; + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: adminUser + } + }); + + const anyNanopub = { contributor: 'http://example.org/user2' }; + expect(wrapper.vm.canEdit(anyNanopub)).toBe(true); + }); + + it('disallows edit when user has no uri', () => { + const noUriUser = { admin: false }; + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: noUriUser + } + }); + + const nanopub = { contributor: 'http://example.org/user1' }; + expect(wrapper.vm.canEdit(nanopub)).toBe(false); + }); + + it('enters edit mode for nanopub', async () => { + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: mockUser + } + }); + + const nanopub = { '@id': 'http://example.org/nanopub1', editing: false }; + await wrapper.vm.editNanopub(nanopub); + + expect(nanopubModule.describeNanopub).toHaveBeenCalledWith('http://example.org/nanopub1'); + expect(nanopub.editing).toBe(true); + }); + + it('handles save nanopub', async () => { + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: mockUser + } + }); + + const nanopub = { '@id': 'http://example.org/nanopub1', resource: {} }; + await wrapper.vm.handleSaveNanopub(nanopub); + + expect(nanopubModule.postNewNanopub).toHaveBeenCalledWith(nanopub.resource, nanopub['@context']); + expect(nanopubModule.listNanopubs).toHaveBeenCalled(); + }); + + it('handles create nanopub', async () => { + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: mockUser + } + }); + + const nanopub = { '@id': null, resource: {} }; + await wrapper.vm.handleCreateNanopub(nanopub); + + expect(nanopubModule.postNewNanopub).toHaveBeenCalledWith(nanopub.resource, nanopub['@context']); + expect(nanopubModule.listNanopubs).toHaveBeenCalled(); + }); + + it('shows delete confirmation modal', async () => { + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: mockUser + } + }); + + const nanopub = { '@id': 'http://example.org/nanopub1' }; + wrapper.vm.deleteNanopub(nanopub); + await wrapper.vm.$nextTick(); + + expect(wrapper.vm.toDelete).toBe(nanopub); + }); + + it('can cancel delete', async () => { + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: mockUser + } + }); + + wrapper.setData({ toDelete: { '@id': 'http://example.org/nanopub1' } }); + await wrapper.vm.$nextTick(); + wrapper.vm.cancelDelete(); + + expect(wrapper.vm.toDelete).toBe(null); + }); + + it('confirms and deletes nanopub', async () => { + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: mockUser + } + }); + + const nanopub = { '@id': 'http://example.org/nanopub1' }; + wrapper.setData({ toDelete: nanopub }); + + await wrapper.vm.confirmDelete(); + + expect(nanopubModule.deleteNanopub).toHaveBeenCalledWith('http://example.org/nanopub1'); + expect(wrapper.vm.toDelete).toBe(null); + expect(nanopubModule.listNanopubs).toHaveBeenCalled(); + }); + + it('hides new nanopub form when disabled', async () => { + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: mockUser, + disableNanopubing: true + } + }); + + await wrapper.vm.$nextTick(); + await new Promise(resolve => setTimeout(resolve, 0)); + expect(wrapper.vm.disableNanopubing).toBe(true); + }); + + it('shows new nanopub form when not disabled', async () => { + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: mockUser, + disableNanopubing: false + } + }); + + await wrapper.vm.$nextTick(); + await new Promise(resolve => setTimeout(resolve, 0)); + expect(wrapper.vm.disableNanopubing).toBe(false); + }); + + it('trusts HTML content', () => { + wrapper = shallowMount(Nanopubs, { + localVue, + propsData: { + resource: 'http://example.org/resource1', + currentUser: mockUser + } + }); + + const html = 'Test content
'; + expect(wrapper.vm.trustHtml(html)).toBe(html); + }); +}); diff --git a/whyis/static/tests/components/new-nanopub.spec.js b/whyis/static/tests/components/new-nanopub.spec.js new file mode 100644 index 00000000..29975c74 --- /dev/null +++ b/whyis/static/tests/components/new-nanopub.spec.js @@ -0,0 +1,374 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import NewNanopub from '@/components/new-nanopub.vue'; +import * as formatsModule from '@/utilities/formats'; + +const localVue = createLocalVue(); + +jest.mock('@/utilities/formats'); + +describe('NewNanopub Component', () => { + let wrapper; + const mockFormats = [ + { extension: 'ttl', label: 'Turtle', mimetype: 'text/turtle' }, + { extension: 'rdf', label: 'RDF/XML', mimetype: 'application/rdf+xml' }, + { extension: 'jsonld', label: 'JSON-LD', mimetype: 'application/ld+json' } + ]; + + beforeEach(() => { + formatsModule.getFormatByExtension.mockImplementation((ext) => { + return mockFormats.find(f => f.extension === ext); + }); + formatsModule.getFormatFromFilename.mockImplementation((filename) => { + const ext = filename.split('.').pop(); + return mockFormats.find(f => f.extension === ext); + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + if (wrapper) { + wrapper.destroy(); + } + }); + + it('renders without crashing', () => { + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { + nanopub: { resource: {} } + } + }); + expect(wrapper.exists()).toBe(true); + }); + + it('initializes with default graph and formats', () => { + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { + nanopub: { resource: {} } + } + }); + + expect(wrapper.vm.currentGraph).toBe('assertion'); + expect(wrapper.vm.graphs).toEqual(['assertion', 'provenance', 'pubinfo']); + expect(wrapper.vm.formatOptions.length).toBeGreaterThan(0); + }); + + it('displays correct verb prop', () => { + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { + nanopub: { resource: {} }, + verb: 'Create' + } + }); + + const button = wrapper.find('.btn-primary'); + expect(button.text()).toBe('Create'); + }); + + it('uses default verb when not provided', () => { + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { + nanopub: { resource: {} } + } + }); + + const button = wrapper.find('.btn-primary'); + expect(button.text()).toBe('Save'); + }); + + it('shows cancel button when editing', () => { + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { + nanopub: { resource: {} }, + editing: true + } + }); + + expect(wrapper.findAll('.btn-secondary').length).toBe(1); + }); + + it('hides cancel button when not editing', () => { + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { + nanopub: { resource: {} }, + editing: false + } + }); + + expect(wrapper.findAll('.btn-secondary').length).toBe(0); + }); + + it('enables save button when content is present', async () => { + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { + nanopub: { resource: {} } + } + }); + + wrapper.setData({ graphContent: 'Some content' }); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.canSave).toBe(true); + }); + + it('disables save button when content is empty', async () => { + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { + nanopub: { resource: { assertion: '', provenance: '', pubinfo: '' } } + } + }); + + await wrapper.vm.$nextTick(); + // Component initializes with empty content from nanopub + expect(wrapper.vm.graphContent).toBe(''); + expect(wrapper.vm.canSave).toBe(false); + }); + + it('disables save button when content is whitespace', () => { + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { + nanopub: { resource: {} } + } + }); + + wrapper.setData({ graphContent: ' \n\t ' }); + expect(wrapper.vm.canSave).toBe(false); + }); + + it('switches between graphs', async () => { + const nanopub = { + resource: { + assertion: 'assertion content', + provenance: 'provenance content', + pubinfo: 'pubinfo content' + } + }; + + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { nanopub } + }); + + wrapper.setData({ currentGraph: 'provenance' }); + await wrapper.vm.$nextTick(); + expect(wrapper.vm.graphContent).toContain('provenance'); + }); + + it('emits save event with nanopub', () => { + const nanopub = { resource: {} }; + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { nanopub } + }); + + wrapper.setData({ graphContent: 'Test content' }); + wrapper.vm.handleSave(); + + expect(wrapper.emitted('save')).toBeTruthy(); + expect(wrapper.emitted('save')[0][0]).toBe(nanopub); + }); + + it('updates nanopub resource with graph content on save', () => { + const nanopub = { resource: {} }; + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { nanopub } + }); + + wrapper.setData({ + currentGraph: 'assertion', + graphContent: 'Test assertion content' + }); + wrapper.vm.handleSave(); + + expect(nanopub.resource.assertion).toBe('Test assertion content'); + }); + + it('clears content after save when not editing', () => { + const nanopub = { resource: {} }; + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { + nanopub, + editing: false + } + }); + + wrapper.setData({ graphContent: 'Test content' }); + wrapper.vm.handleSave(); + + expect(wrapper.vm.graphContent).toBe(''); + }); + + it('keeps content after save when editing', () => { + const nanopub = { resource: {} }; + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { + nanopub, + editing: true + } + }); + + wrapper.setData({ graphContent: 'Test content' }); + wrapper.vm.handleSave(); + + expect(wrapper.vm.graphContent).toBe('Test content'); + }); + + it('emits cancel event on cancel', () => { + const nanopub = { resource: {}, editing: true }; + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { + nanopub, + editing: true + } + }); + + wrapper.vm.handleCancel(); + + expect(wrapper.emitted('cancel')).toBeTruthy(); + expect(nanopub.editing).toBe(false); + }); + + it('handles file upload', async () => { + wrapper = shallowMount(NewNanopub, { + localVue, + propsData: { + nanopub: { resource: {} } + } + }); + + const fileContent = '@prefix ex:++ +{{def.value(ns.prov.value)}}
+
{{note}}
+ {% endfor %} +{{render_reference(ex)}}
{% endif %} + {% endfor %} +Super class definitions also apply to this class.
+{{superClass.value(ns.skos.definition)}}
+ {% endif %} +{{subClass.value(ns.skos.definition)}}
+ {% endif %} +