diff --git a/cypress/integration/basics.js b/cypress/integration/basics.js index 4e1e575..8456740 100644 --- a/cypress/integration/basics.js +++ b/cypress/integration/basics.js @@ -1,267 +1,335 @@ /** * Copyright (C) 2020 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ /* * Tests the basic features of the application. */ "use strict"; describe('JSON Generation', function() { beforeEach(function() { /* Clear the session storage, as it is used to restore field data; * and we don't want a test to load data from the previous test. */ cy.window().then((win) => { win.sessionStorage.clear() }) cy.visit('./index.html'); }); it('works just from the software name', function() { cy.get('#name').type('My Test Software'); cy.get('#generateCodemetaV2').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "name": "My Test Software", }); }); it('works just from all main fields when using only one license', function() { cy.get('#name').type('My Test Software'); cy.get('#description').type('This is a\ngreat piece of software'); cy.get('#dateCreated').type('2019-10-02'); cy.get('#datePublished').type('2020-01-01'); cy.get('#license').type('AGPL-3.0'); cy.get("#license").type('{enter}'); cy.get('#generateCodemetaV2').click(); cy.get("#license").should('have.value', ''); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "license": "https://spdx.org/licenses/AGPL-3.0", "dateCreated": "2019-10-02", "datePublished": "2020-01-01", "name": "My Test Software", "description": "This is a\ngreat piece of software", }); }); it('works just from all main fields when using multiple licenses', function() { cy.get('#name').type('My Test Software'); cy.get('#description').type('This is a\ngreat piece of software'); cy.get('#dateCreated').type('2019-10-02'); cy.get('#datePublished').type('2020-01-01'); cy.get('#license').type('AGPL-3.0'); cy.get("#license").type('{enter}'); cy.get('#license').type('MIT'); cy.get("#license").type('{enter}'); cy.get('#generateCodemetaV2').click(); cy.get("#license").should('have.value', ''); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "license": ["https://spdx.org/licenses/AGPL-3.0", "https://spdx.org/licenses/MIT"], "dateCreated": "2019-10-02", "datePublished": "2020-01-01", "name": "My Test Software", "description": "This is a\ngreat piece of software", }); }); it('works when choosing licenses without the keyboard', function() { cy.get('#name').type('My Test Software'); cy.get('#description').type('This is a\ngreat piece of software'); cy.get('#dateCreated').type('2019-10-02'); cy.get('#datePublished').type('2020-01-01'); cy.get('#license').type('AGPL-3.0'); // no cy.get("#license").type('{enter}'); here cy.get('#generateCodemetaV2').click(); cy.get("#license").should('have.value', ''); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "license": "https://spdx.org/licenses/AGPL-3.0", "dateCreated": "2019-10-02", "datePublished": "2020-01-01", "name": "My Test Software", "description": "This is a\ngreat piece of software", }); }); it('works for new codemeta terms in both versions', function() { cy.get('#name').type('My Test Software'); cy.get('#contIntegration').type('https://test-ci.org/my-software'); cy.get('#isSourceCodeOf').type('Bigger Application'); cy.get('#reviewAspect').type('Some software aspect'); cy.get('#reviewBody').type('Some review'); cy.get('#generateCodemetaV2').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "name": "My Test Software", "contIntegration": "https://test-ci.org/my-software", "codemeta:continuousIntegration": { "id": "https://test-ci.org/my-software" }, "codemeta:isSourceCodeOf": { "id": "Bigger Application" }, "schema:review": { "type": "schema:Review", "schema:reviewAspect": "Some software aspect", "schema:reviewBody": "Some review" } }); cy.get('#generateCodemetaV3').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://w3id.org/codemeta/3.0", "type": "SoftwareSourceCode", "name": "My Test Software", "continuousIntegration": "https://test-ci.org/my-software", "codemeta:contIntegration": { "id": "https://test-ci.org/my-software" }, "isSourceCodeOf": "Bigger Application", "review": { "type": "Review", "reviewAspect": "Some software aspect", "reviewBody": "Some review" } }); }); }); describe('JSON Import', function() { it('works just from the software name', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "name": "My Test Software", })) ); cy.get('#importCodemeta').click(); cy.get('#name').should('have.value', 'My Test Software'); }); it('works just from all main fields when using license as string', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "license": "https://spdx.org/licenses/AGPL-3.0", "dateCreated": "2019-10-02", "datePublished": "2020-01-01", "name": "My Test Software", "description": "This is a\ngreat piece of software", })) ); cy.get('#importCodemeta').click(); cy.get('#name').should('have.value', 'My Test Software'); cy.get('#description').should('have.value', 'This is a\ngreat piece of software'); cy.get('#dateCreated').should('have.value', '2019-10-02'); cy.get('#datePublished').should('have.value', '2020-01-01'); cy.get('#license').should('have.value', ''); cy.get("#selected-licenses").children().should('have.length', 1); cy.get("#selected-licenses").children().first().children().first().should('have.text', 'AGPL-3.0'); }); it('works just from all main fields when using license as array', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "license": ["https://spdx.org/licenses/AGPL-3.0", "https://spdx.org/licenses/MIT"], "dateCreated": "2019-10-02", "datePublished": "2020-01-01", "name": "My Test Software", "description": "This is a\ngreat piece of software", })) ); cy.get('#importCodemeta').click(); cy.get('#name').should('have.value', 'My Test Software'); cy.get('#description').should('have.value', 'This is a\ngreat piece of software'); cy.get('#dateCreated').should('have.value', '2019-10-02'); cy.get('#datePublished').should('have.value', '2020-01-01'); cy.get('#license').should('have.value', ''); cy.get("#selected-licenses").children().should('have.length', 2); cy.get("#selected-licenses").children().eq(0).children().first().should('have.text', 'AGPL-3.0'); cy.get("#selected-licenses").children().eq(1).children().first().should('have.text', 'MIT'); }); it('works with expanded document version', function () { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "http://schema.org/name": [ { "@value": "My Test Software" } ], "@type": [ "http://schema.org/SoftwareSourceCode" ] })) ); cy.get('#importCodemeta').click(); cy.get('#name').should('have.value', 'My Test Software'); }); it('errors on invalid type', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "foo", "name": "My Test Software", })) ); cy.get('#importCodemeta').click(); // Should still be imported as much as possible cy.get('#name').should('have.value', 'My Test Software'); // But must display an error cy.get('#errorMessage').should('have.text', 'Wrong document type: must be "SoftwareSourceCode"/"SoftwareApplication", not "foo"'); }); it('allows singleton array as context', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": ["https://doi.org/10.5063/schema/codemeta-2.0"], "@type": "SoftwareSourceCode", "name": "My Test Software", })) ); cy.get('#importCodemeta').click(); cy.get('#name').should('have.value', 'My Test Software'); }); + it('works for new codemeta v3.0 terms', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://w3id.org/codemeta/3.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "continuousIntegration": "https://test-ci.org/my-software", + "isSourceCodeOf": "Bigger Application", + "review": { + "type": "Review", + "reviewAspect": "Some software aspect", + "reviewBody": "Some review" + } + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#contIntegration').should('have.value', 'https://test-ci.org/my-software'); + cy.get('#isSourceCodeOf').should('have.value', 'Bigger Application'); + cy.get('#reviewAspect').should('have.value', 'Some software aspect'); + cy.get('#reviewBody').should('have.value', 'Some review'); + }); + + it('works for codemeta v2.0 terms in v3.0 version', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://w3id.org/codemeta/3.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "continuousIntegration": "https://test-ci.org/my-software", + "codemeta:contIntegration": { + "id": "https://test-ci.org/my-software" + }, + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#contIntegration').should('have.value', 'https://test-ci.org/my-software'); + }); + + it('works for codemeta v3.0 terms in v2.0 version, and does not work for new terms', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "contIntegration": "https://test-ci.org/my-software", + "codemeta:continuousIntegration": { + "id": "https://test-ci.org/my-software" + }, + "codemeta:isSourceCodeOf": { + "id": "Bigger Application" + }, + "schema:review": { + "type": "schema:Review", + "schema:reviewAspect": "Some software aspect", + "schema:reviewBody": "Some review" + } + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#contIntegration').should('have.value', 'https://test-ci.org/my-software'); + cy.get('#isSourceCodeOf').should('have.value', ''); + cy.get('#reviewAspect').should('have.value', ''); + cy.get('#reviewBody').should('have.value', ''); + }); }); diff --git a/cypress/integration/persons.js b/cypress/integration/persons.js index 38734e7..163e201 100644 --- a/cypress/integration/persons.js +++ b/cypress/integration/persons.js @@ -1,658 +1,801 @@ /** * Copyright (C) 2020 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ /* * Tests the author/contributor dynamic fieldsets */ "use strict"; describe('Zero author', function() { it('can be exported', function() { cy.get('#name').type('My Test Software'); cy.get('#generateCodemetaV2').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "name": "My Test Software", }); }); it('can be imported from no list', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "name": "My Test Software", })) ); cy.get('#importCodemeta').click(); cy.get('#author_nb').should('have.value', '0'); cy.get('#author_0').should('not.exist'); cy.get('#author_1').should('not.exist'); }); it('can be imported from empty list', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "name": "My Test Software", "author": [], })) ); cy.get('#importCodemeta').click(); cy.get('#author_nb').should('have.value', '0'); cy.get('#author_0').should('not.exist'); cy.get('#author_1').should('not.exist'); }); }); describe('One full author', function() { it('can be exported', function() { cy.get('#name').type('My Test Software'); cy.get('#author_nb').should('have.value', '0'); cy.get('#author_0').should('not.exist'); cy.get('#author_1').should('not.exist'); cy.get('#author_1_givenName').should('not.exist'); cy.get('#author_add').click(); cy.get('#author_nb').should('have.value', '1'); cy.get('#author_0').should('not.exist'); cy.get('#author_1').should('exist'); cy.get('#author_2').should('not.exist'); cy.get('#author_1_givenName').should('have.value', ''); cy.get('#author_1_familyName').should('have.value', ''); cy.get('#author_1_email').should('have.value', ''); cy.get('#author_1_id').should('have.value', ''); cy.get('#author_1_affiliation').should('have.value', ''); cy.get('#author_1_givenName').type('Jane'); cy.get('#author_1_familyName').type('Doe'); cy.get('#author_1_email').type('jdoe@example.org'); cy.get('#author_1_id').type('http://example.org/~jdoe'); cy.get('#author_1_affiliation').type('http://example.org/'); cy.get('#generateCodemetaV2').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "name": "My Test Software", "author": [ { "type": "Person", "id": "http://example.org/~jdoe", "givenName": "Jane", "familyName": "Doe", "email": "jdoe@example.org", "affiliation": { "type": "Organization", "id": "http://example.org/", } } ], }); }); it('can be imported', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "name": "My Test Software", "author": [ { "@type": "Person", "@id": "http://example.org/~jdoe", "givenName": "Jane", "familyName": "Doe", "email": "jdoe@example.org", "affiliation": { "@type": "Organization", "@id": "http://example.org/", } } ], })) ); cy.get('#importCodemeta').click(); cy.get('#author_nb').should('have.value', '1'); cy.get('#author_0').should('not.exist'); cy.get('#author_1').should('exist'); cy.get('#author_2').should('not.exist'); cy.get('#author_1_givenName').should('have.value', 'Jane'); cy.get('#author_1_familyName').should('have.value', 'Doe'); cy.get('#author_1_email').should('have.value', 'jdoe@example.org'); cy.get('#author_1_id').should('have.value', 'http://example.org/~jdoe'); cy.get('#author_1_affiliation').should('have.value', 'http://example.org/'); }); }); describe('Affiliation id', function() { it('can be exported', function() { cy.get('#name').type('My Test Software'); cy.get('#author_add').click(); cy.get('#author_1_givenName').type('Jane'); cy.get('#author_1_affiliation').type('http://example.org/'); cy.get('#generateCodemetaV2').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "name": "My Test Software", "author": [ { "type": "Person", "givenName": "Jane", "affiliation": { "type": "Organization", "id": "http://example.org/", } } ], }); }); it('can be imported', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "name": "My Test Software", "author": [ { "@type": "Person", "@id": "http://example.org/~jdoe", "givenName": "Jane", "familyName": "Doe", "email": "jdoe@example.org", "affiliation": { "@type": "Organization", "@id": "http://example.org/", } } ], })) ); cy.get('#importCodemeta').click(); cy.get('#author_nb').should('have.value', '1'); cy.get('#author_0').should('not.exist'); cy.get('#author_1').should('exist'); cy.get('#author_2').should('not.exist'); cy.get('#author_1_affiliation').should('have.value', 'http://example.org/'); }); }); describe('Affiliation name', function() { it('can be exported', function() { cy.get('#name').type('My Test Software'); cy.get('#author_add').click(); cy.get('#author_1_givenName').type('Jane'); cy.get('#author_1_affiliation').type('Example Org'); cy.get('#generateCodemetaV2').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "name": "My Test Software", "author": [ { "type": "Person", "givenName": "Jane", "affiliation": { "type": "Organization", "name": "Example Org", } } ], }); }); it('can be imported', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "name": "My Test Software", "author": [ { "@type": "Person", "@id": "http://example.org/~jdoe", "givenName": "Jane", "familyName": "Doe", "email": "jdoe@example.org", "affiliation": { "@type": "Organization", "name": "Example Org", } } ], })) ); cy.get('#importCodemeta').click(); cy.get('#author_nb').should('have.value', '1'); cy.get('#author_0').should('not.exist'); cy.get('#author_1').should('exist'); cy.get('#author_2').should('not.exist'); cy.get('#author_1_affiliation').should('have.value', 'Example Org'); }); }); describe('Author order change', function() { it('is a noop with a single author', function() { cy.get('#name').type('My Test Software'); cy.get('#author_add').click(); cy.get('#author_1_givenName').type('Jane'); cy.get('#author_1_affiliation').type('Example Org'); cy.get('#author_1_moveToRight').click(); cy.get('#author_1_givenName').should('have.value', 'Jane'); cy.get('#author_1_affiliation').should('have.value', 'Example Org'); cy.get('#author_1_moveToLeft').click(); cy.get('#author_1_givenName').should('have.value', 'Jane'); cy.get('#author_1_affiliation').should('have.value', 'Example Org'); }); it('flips two authors', function() { cy.get('#name').type('My Test Software'); cy.get('#author_add').click(); cy.get('#author_add').click(); cy.get('#author_add').click(); cy.get('#author_1_givenName').type('Jane'); cy.get('#author_1_affiliation').type('Example Org'); cy.get('#author_2_givenName').type('John'); cy.get('#author_2_familyName').type('Doe'); cy.get('#author_3_givenName').type('Alex'); cy.get('#author_1_moveToRight').click(); cy.get('#author_1_givenName').should('have.value', 'John'); cy.get('#author_1_familyName').should('have.value', 'Doe'); cy.get('#author_1_affiliation').should('have.value', ''); cy.get('#author_2_givenName').should('have.value', 'Jane'); cy.get('#author_2_familyName').should('have.value', ''); cy.get('#author_2_affiliation').should('have.value', 'Example Org'); cy.get('#author_3_givenName').should('have.value', 'Alex'); cy.get('#author_3_familyName').should('have.value', ''); cy.get('#author_3_affiliation').should('have.value', ''); }); it('updates generated Codemeta', function() { cy.get('#name').type('My Test Software'); cy.get('#author_add').click(); cy.get('#author_add').click(); cy.get('#author_1_givenName').type('Jane'); cy.get('#author_1_affiliation').type('Example Org'); cy.get('#author_2_givenName').type('John'); cy.get('#author_2_familyName').type('Doe'); cy.get('#generateCodemetaV2').click(); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "name": "My Test Software", "author": [ { "type": "Person", "givenName": "Jane", "affiliation": { "type": "Organization", "name": "Example Org", } }, { "type": "Person", "givenName": "John", "familyName": "Doe", }, ], }); cy.get('#author_1_moveToRight').click(); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "name": "My Test Software", "author": [ { "type": "Person", "givenName": "John", "familyName": "Doe", }, { "type": "Person", "givenName": "Jane", "affiliation": { "type": "Organization", "name": "Example Org", } }, ], }); }); it('wraps around to the right', function() { cy.get('#name').type('My Test Software'); cy.get('#author_add').click(); cy.get('#author_add').click(); cy.get('#author_add').click(); cy.get('#author_1_givenName').type('Jane'); cy.get('#author_1_affiliation').type('Example Org'); cy.get('#author_2_givenName').type('John'); cy.get('#author_2_familyName').type('Doe'); cy.get('#author_3_givenName').type('Alex'); cy.get('#author_1_moveToLeft').click() cy.get('#author_1_givenName').should('have.value', 'Alex'); cy.get('#author_1_familyName').should('have.value', ''); cy.get('#author_1_affiliation').should('have.value', ''); cy.get('#author_2_givenName').should('have.value', 'John'); cy.get('#author_2_familyName').should('have.value', 'Doe'); cy.get('#author_2_affiliation').should('have.value', ''); cy.get('#author_3_givenName').should('have.value', 'Jane'); cy.get('#author_3_familyName').should('have.value', ''); cy.get('#author_3_affiliation').should('have.value', 'Example Org'); }); it('wraps around to the left', function() { cy.get('#name').type('My Test Software'); cy.get('#author_add').click(); cy.get('#author_add').click(); cy.get('#author_add').click(); cy.get('#author_1_givenName').type('Jane'); cy.get('#author_1_affiliation').type('Example Org'); cy.get('#author_2_givenName').type('John'); cy.get('#author_2_familyName').type('Doe'); cy.get('#author_3_givenName').type('Alex'); cy.get('#author_3_moveToRight').click() cy.get('#author_1_givenName').should('have.value', 'Alex'); cy.get('#author_1_familyName').should('have.value', ''); cy.get('#author_1_affiliation').should('have.value', ''); cy.get('#author_2_givenName').should('have.value', 'John'); cy.get('#author_2_familyName').should('have.value', 'Doe'); cy.get('#author_2_affiliation').should('have.value', ''); cy.get('#author_3_givenName').should('have.value', 'Jane'); cy.get('#author_3_familyName').should('have.value', ''); cy.get('#author_3_affiliation').should('have.value', 'Example Org'); }); }); describe('One role', function () { it('can be exported in both codemeta v2.0 and v3.0 versions', function () { cy.get('#name').type('My Test Software'); cy.get('#author_add').click(); cy.get('#author_1_givenName').type('Jane'); cy.get('#author_1_role_add').click(); cy.get('#author_1_roleName_0').type('Developer'); cy.get('#author_1_startDate_0').type('2024-03-04'); cy.get('#author_1_endDate_0').type('2024-04-03'); cy.get('#generateCodemetaV2').click(); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "type": "SoftwareSourceCode", "name": "My Test Software", "author": [ { "type": "schema:Role", "schema:author": { "type": "Person", "givenName": "Jane" }, "schema:roleName": "Developer", "schema:startDate": "2024-03-04", "schema:endDate": "2024-04-03" } ] }); cy.get('#generateCodemetaV3').click(); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://w3id.org/codemeta/3.0", "type": "SoftwareSourceCode", "name": "My Test Software", "author": [ { "type": "Role", "schema:author": { "type": "Person", "givenName": "Jane" }, "roleName": "Developer", "startDate": "2024-03-04", "endDate": "2024-04-03" } ] }); }); it('and a second one can be exported', function () { cy.get('#name').type('My Test Software'); cy.get('#author_add').click(); cy.get('#author_1_givenName').type('Jane'); cy.get('#author_1_role_add').click(); cy.get('#author_1_roleName_0').type('Developer'); cy.get('#author_1_startDate_0').type('2024-03-04'); cy.get('#author_1_endDate_0').type('2024-04-03'); cy.get('#author_1_role_add').click(); cy.get('#author_1_roleName_1').type('Maintainer'); cy.get('#author_1_startDate_1').type('2024-04-04'); cy.get('#author_1_endDate_1').type('2024-05-05'); cy.get('#generateCodemetaV3').click(); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://w3id.org/codemeta/3.0", "type": "SoftwareSourceCode", "name": "My Test Software", "author": [ { "type": "Role", "schema:author": { "type": "Person", "givenName": "Jane" }, "roleName": "Maintainer", "startDate": "2024-04-04", "endDate": "2024-05-05" }, { "type": "Role", "schema:author": { "type": "Person", "givenName": "Jane" }, "roleName": "Developer", "startDate": "2024-03-04", "endDate": "2024-04-03" } ] }); }); it('can be deleted then added again', function () { cy.get('#name').type('My Test Software'); cy.get('#author_add').click(); cy.get('#author_1_givenName').type('Jane'); cy.get('#author_1_role_add').click(); cy.get('#author_1_roleName_0').type('Developer'); cy.get('#author_1_startDate_0').type('2024-03-04'); cy.get('#author_1_endDate_0').type('2024-04-03'); cy.get('#author_1_role_remove_0').click(); cy.get('#author_1_role_add').click(); cy.get('#author_1_roleName_1').type('Maintainer'); cy.get('#author_1_startDate_1').type('2024-04-04'); cy.get('#author_1_endDate_1').type('2024-05-05'); cy.get('#generateCodemetaV3').click(); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://w3id.org/codemeta/3.0", "type": "SoftwareSourceCode", "name": "My Test Software", "author": [ { "type": "Role", "schema:author": { "type": "Person", "givenName": "Jane" }, "roleName": "Maintainer", "startDate": "2024-04-04", "endDate": "2024-05-05" } ] }); }); it('can be imported', function () { - // TODO + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://w3id.org/codemeta/3.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "author": [ + { + "type": "Role", + "schema:author": { + "type": "Person", + "givenName": "Jane" + }, + "roleName": "Developer", + "startDate": "2024-03-04", + "endDate": "2024-04-03" + } + ] + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#author_1_givenName').should('have.value', 'Jane'); + cy.get('#author_1_roleName_0').should('have.value', 'Developer'); + cy.get('#author_1_startDate_0').should('have.value', '2024-03-04'); + cy.get('#author_1_endDate_0').should('have.value', '2024-04-03'); + }); + + it('and second one for the same author can be imported (and they are merged)', function () { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://w3id.org/codemeta/3.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "author": [ + { + "type": "Role", + "schema:author": { + "type": "Person", + "givenName": "Jane" + }, + "roleName": "Maintainer", + "startDate": "2024-04-04", + "endDate": "2024-05-05" + }, + { + "type": "Role", + "schema:author": { + "type": "Person", + "givenName": "Jane" + }, + "roleName": "Developer", + "startDate": "2024-03-04", + "endDate": "2024-04-03" + } + ] + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#author_nb').should('have.value', '1'); + cy.get('#author_1_givenName').should('have.value', 'Jane'); + cy.get('#author_1_roleName_0').should('have.value', 'Maintainer'); + cy.get('#author_1_startDate_0').should('have.value', '2024-04-04'); + cy.get('#author_1_endDate_0').should('have.value', '2024-05-05'); + cy.get('#author_1_roleName_1').should('have.value', 'Developer'); + cy.get('#author_1_startDate_1').should('have.value', '2024-03-04'); + cy.get('#author_1_endDate_1').should('have.value', '2024-04-03'); }); }); describe('Multiple authors', function () { it('who both have roles can be exported', function () { cy.get('#name').type('My Test Software'); cy.get('#author_add').click(); cy.get('#author_1_givenName').type('Jane'); cy.get('#author_add').click(); cy.get('#author_2_givenName').type('Joe'); cy.get('#author_1_role_add').click(); cy.get('#author_1_roleName_0').type('Developer'); cy.get('#author_1_startDate_0').type('2024-03-04'); cy.get('#author_1_endDate_0').type('2024-04-03'); cy.get('#author_2_role_add').click(); cy.get('#author_2_roleName_0').type('Maintainer'); cy.get('#author_2_startDate_0').type('2024-04-04'); cy.get('#author_2_endDate_0').type('2024-05-05'); cy.get('#generateCodemetaV3').click(); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://w3id.org/codemeta/3.0", "type": "SoftwareSourceCode", "name": "My Test Software", "author": [ { "type": "Role", "schema:author": { "type": "Person", "givenName": "Jane" }, "roleName": "Developer", "startDate": "2024-03-04", "endDate": "2024-04-03" }, { "type": "Role", "schema:author": { "type": "Person", "givenName": "Joe" }, "roleName": "Maintainer", "startDate": "2024-04-04", "endDate": "2024-05-05" } ] }); }); it('whose one has a role and the other not can be exported', function () { cy.get('#name').type('My Test Software'); cy.get('#author_add').click(); cy.get('#author_1_givenName').type('Jane'); cy.get('#author_add').click(); cy.get('#author_2_givenName').type('Joe'); cy.get('#author_1_role_add').click(); cy.get('#author_1_roleName_0').type('Developer'); cy.get('#author_1_startDate_0').type('2024-03-04'); cy.get('#author_1_endDate_0').type('2024-04-03'); cy.get('#generateCodemetaV3').click(); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://w3id.org/codemeta/3.0", "type": "SoftwareSourceCode", "name": "My Test Software", "author": [ { "type": "Role", "schema:author": { "type": "Person", "givenName": "Jane" }, "roleName": "Developer", "startDate": "2024-03-04", "endDate": "2024-04-03" }, { "type": "Person", "givenName": "Joe" } ] }); }); + + it('who both have roles can be imported', function () { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://w3id.org/codemeta/3.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "author": [ + { + "type": "Role", + "schema:author": { + "type": "Person", + "givenName": "Jane" + }, + "roleName": "Developer", + "startDate": "2024-03-04", + "endDate": "2024-04-03" + }, + { + "type": "Role", + "schema:author": { + "type": "Person", + "givenName": "Joe" + }, + "roleName": "Maintainer", + "startDate": "2024-04-04", + "endDate": "2024-05-05" + } + ] + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#author_nb').should('have.value', '2'); + cy.get('#author_1_givenName').should('have.value', 'Jane'); + cy.get('#author_1_roleName_0').should('have.value', 'Developer'); + cy.get('#author_1_startDate_0').should('have.value', '2024-03-04'); + cy.get('#author_1_endDate_0').should('have.value', '2024-04-03'); + cy.get('#author_2_givenName').should('have.value', 'Joe'); + cy.get('#author_2_roleName_0').should('have.value', 'Maintainer'); + cy.get('#author_2_startDate_0').should('have.value', '2024-04-04'); + cy.get('#author_2_endDate_0').should('have.value', '2024-05-05'); + }); + + it('whose one has a role and the other not can be imported', function () { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://w3id.org/codemeta/3.0", + "type": "SoftwareSourceCode", + "name": "My Test Software", + "author": [ + { + "type": "Role", + "schema:author": { + "type": "Person", + "givenName": "Jane" + }, + "roleName": "Developer", + "startDate": "2024-03-04", + "endDate": "2024-04-03" + }, + { + "type": "Person", + "givenName": "Joe" + } + ] + })) + ); + cy.get('#importCodemeta').click(); + + cy.get('#author_nb').should('have.value', '2'); + cy.get('#author_1_givenName').should('have.value', 'Joe'); + cy.get('#author_2_givenName').should('have.value', 'Jane'); + cy.get('#author_2_roleName_0').should('have.value', 'Developer'); + cy.get('#author_2_startDate_0').should('have.value', '2024-03-04'); + cy.get('#author_2_endDate_0').should('have.value', '2024-04-03'); + }); }); diff --git a/js/codemeta_generation.js b/js/codemeta_generation.js index 3680f43..31bf498 100644 --- a/js/codemeta_generation.js +++ b/js/codemeta_generation.js @@ -1,378 +1,430 @@ /** * Copyright (C) 2019-2020 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ "use strict"; const LOCAL_CONTEXT_PATH = "./data/contexts/codemeta-local.jsonld"; const LOCAL_CONTEXT_URL = "local"; const CODEMETA_CONTEXTS = { "2.0": { path: "./data/contexts/codemeta-2.0.jsonld", url: "https://doi.org/10.5063/schema/codemeta-2.0" }, "3.0": { path: "./data/contexts/codemeta-3.0.jsonld", url: "https://w3id.org/codemeta/3.0" } } const SPDX_PREFIX = 'https://spdx.org/licenses/'; const loadContextData = async () => { const [contextLocal, contextV2, contextV3] = await Promise.all([ fetch(LOCAL_CONTEXT_PATH).then(response => response.json()), fetch(CODEMETA_CONTEXTS["2.0"].path).then(response => response.json()), fetch(CODEMETA_CONTEXTS["3.0"].path).then(response => response.json()) ]); return { [LOCAL_CONTEXT_URL]: contextLocal, [CODEMETA_CONTEXTS["2.0"].url]: contextV2, [CODEMETA_CONTEXTS["3.0"].url]: contextV3 } } const getJsonldCustomLoader = contexts => { return url => { const xhrDocumentLoader = jsonld.documentLoaders.xhr(); if (url in contexts) { return { contextUrl: null, document: contexts[url], documentUrl: url }; } return xhrDocumentLoader(url); } }; const initJsonldLoader = contexts => { jsonld.documentLoader = getJsonldCustomLoader(contexts); }; function emptyToUndefined(v) { if (v == null || v == "") return undefined; else return v; } function getIfSet(query) { return emptyToUndefined(document.querySelector(query).value); } function setIfDefined(query, value) { if (value !== undefined) { document.querySelector(query).value = value; } } function getLicenses() { let selectedLicenses = Array.from(document.getElementById("selected-licenses").children); return selectedLicenses.map(licenseDiv => SPDX_PREFIX + licenseDiv.children[0].innerText); } // Names of codemeta properties with a matching HTML field name const directCodemetaFields = [ 'codeRepository', 'contIntegration', 'dateCreated', 'datePublished', 'dateModified', 'downloadUrl', 'issueTracker', 'name', 'version', 'identifier', 'description', 'applicationCategory', 'releaseNotes', 'funding', 'developmentStatus', 'isSourceCodeOf', 'isPartOf', 'referencePublication' ]; const splittedCodemetaFields = [ ['keywords', ','], ['programmingLanguage', ','], ['runtimePlatform', ','], ['operatingSystem', ','], ['softwareRequirements', '\n'], ['relatedLink', '\n'], ] // Names of codemeta properties with a matching HTML field name, // in a Person object const directPersonCodemetaFields = [ 'givenName', 'familyName', 'email', 'affiliation', ]; const directRoleCodemetaFields = [ 'roleName', 'startDate', 'endDate', ]; const directReviewCodemetaFields = [ 'reviewAspect', 'reviewBody' ]; -const crossedCodemetaFields = { +const crossCodemetaFields = { "contIntegration": ["contIntegration", "continuousIntegration"], - "embargoDate": ["embargoDate", "embargoEndDate"], + // "embargoDate": ["embargoDate", "embargoEndDate"], Not present in the form yet TODO ? }; function generateShortOrg(fieldName) { var affiliation = getIfSet(fieldName); if (affiliation !== undefined) { if (isUrl(affiliation)) { return { "@type": "Organization", "@id": affiliation, }; } else { return { "@type": "Organization", "name": affiliation, }; } } else { return undefined; } } function generatePerson(idPrefix) { var doc = { "@type": "Person", } var id = getIfSet(`#${idPrefix}_id`); if (id !== undefined) { doc["@id"] = id; } directPersonCodemetaFields.forEach(function (item, index) { doc[item] = getIfSet(`#${idPrefix}_${item}`); }); doc["affiliation"] = generateShortOrg(`#${idPrefix}_affiliation`); return doc; } function generateRole(id) { const doc = { "@type": "Role" }; directRoleCodemetaFields.forEach(function (item, index) { doc[item] = getIfSet(`#${id} .${item}`); }); return doc; } function generateRoles(idPrefix, person) { const roles = []; const roleNodes = document.querySelectorAll(`ul[id^=${idPrefix}_role_`); roleNodes.forEach(roleNode => { const role = generateRole(roleNode.id); role["schema:author"] = person; // Prefix with "schema:" to prevent it from expanding into a list roles.push(role); }); return roles; } function generatePersons(prefix) { var persons = []; var nbPersons = getNbPersons(prefix); for (let personId = 1; personId <= nbPersons; personId++) { const idPrefix = `${prefix}_${personId}`; const person = generatePerson(idPrefix); const roles = generateRoles(idPrefix, person); if (roles.length > 0) { persons = persons.concat(roles); } else { persons.push(person); } } return persons; } function generateReview() { const doc = { "@type": "Review" }; directReviewCodemetaFields.forEach(function (item, index) { doc[item] = getIfSet(`#${item}`); }); return doc; } async function buildExpandedJson() { var doc = { "@context": LOCAL_CONTEXT_URL, "@type": "SoftwareSourceCode", }; let licenses = getLicenses(); if (licenses.length > 0) { doc["license"] = licenses; } // Generate most fields directCodemetaFields.forEach(function (item, index) { doc[item] = getIfSet('#' + item) }); doc["funder"] = generateShortOrg('#funder', doc["affiliation"]); const review = generateReview(); if (review["reviewAspect"] || review["reviewBody"]) { doc["review"] = generateReview(); } // Generate simple fields parsed simply by splitting splittedCodemetaFields.forEach(function (item, index) { const id = item[0]; const separator = item[1]; const value = getIfSet('#' + id); if (value !== undefined) { doc[id] = value.split(separator).map(trimSpaces); } }); // Generate dynamic fields var authors = generatePersons('author'); if (authors.length > 0) { doc["author"] = authors; } var contributors = generatePersons('contributor'); if (contributors.length > 0) { doc["contributor"] = contributors; } - for (const [key, values] of Object.entries(crossedCodemetaFields)) { - values.forEach(value => { - doc[value] = doc[key]; + for (const [key, items] of Object.entries(crossCodemetaFields)) { + items.forEach(item => { + doc[item] = doc[key]; }); } return await jsonld.expand(doc); } // v2.0 is still default version for generation, for now async function generateCodemeta(codemetaVersion = "2.0") { var inputForm = document.querySelector('#inputForm'); var codemetaText, errorHTML; if (inputForm.checkValidity()) { const expanded = await buildExpandedJson(); const compacted = await jsonld.compact(expanded, CODEMETA_CONTEXTS[codemetaVersion].url); codemetaText = JSON.stringify(compacted, null, 4); errorHTML = ""; } else { codemetaText = ""; errorHTML = "invalid input (see error above)"; inputForm.reportValidity(); } document.querySelector('#codemetaText').innerText = codemetaText; setError(errorHTML); // Run validator on the exported value, for extra validation. // If this finds a validation, it means there is a bug in our code (either // generation or validation), and the generation MUST NOT generate an // invalid codemeta file, regardless of user input. if (codemetaText && !validateDocument(JSON.parse(codemetaText))) { alert('Bug detected! The data you wrote is correct; but for some reason, it seems we generated an invalid codemeta.json. Please report this bug at https://github.com/codemeta/codemeta-generator/issues/new and copy-paste the generated codemeta.json file. Thanks!'); } if (codemetaText) { // For restoring the form state on page reload sessionStorage.setItem('codemetaText', codemetaText); } } // Imports a single field (name or @id) from an Organization. function importShortOrg(fieldName, doc) { if (doc !== undefined) { // Use @id if set, else use name setIfDefined(fieldName, doc["name"]); setIfDefined(fieldName, getDocumentId(doc)); } } +function importReview(doc) { + if (doc !== undefined) { + directReviewCodemetaFields.forEach(item => { + setIfDefined('#' + item, doc[item]); + }); + } +} + +function authorsEqual(author1, author2) { + // TODO should test more properties for equality? + return author1.givenName === author2.givenName + && author1.email === author2.email; +} + +function getSingleAuthorsFromRoles(docs) { + return docs.filter(doc => getDocumentType(doc) === "Role") + .map(doc => doc["schema:author"]) + .reduce((authorSet, currentAuthor) => { + const foundAuthor = authorSet.find(author => authorsEqual(author, currentAuthor)); + if (!foundAuthor) { + return authorSet.concat([currentAuthor]); + } else { + return authorSet; + } + }, []); +} + +function importRoles(personPrefix, roles) { + roles.forEach(role => { + const roleId = addRole(`${personPrefix}`); + directRoleCodemetaFields.forEach(item => { + setIfDefined(`#${personPrefix}_${item}_${roleId}`, role[item]); + }); + }); +} + function importPersons(prefix, legend, docs) { if (docs === undefined) { return; } - docs.forEach(function (doc, index) { + let allAuthorDocs = docs.filter(doc => getDocumentType(doc) === "Person"); + allAuthorDocs = allAuthorDocs.concat(getSingleAuthorsFromRoles(docs)); + + allAuthorDocs.forEach(function (doc, index) { var personId = addPerson(prefix, legend); setIfDefined(`#${prefix}_${personId}_id`, getDocumentId(doc)); directPersonCodemetaFields.forEach(function (item, index) { setIfDefined(`#${prefix}_${personId}_${item}`, doc[item]); }); - importShortOrg(`#${prefix}_${personId}_affiliation`, doc['affiliation']) - }) + importShortOrg(`#${prefix}_${personId}_affiliation`, doc['affiliation']); + + const roles = docs.filter(currentDoc => getDocumentType(currentDoc) === "Role") + .filter(currentDoc => authorsEqual(currentDoc["schema:author"], doc)); + importRoles(`${prefix}_${personId}`, roles); + }); } async function importCodemeta() { var inputForm = document.querySelector('#inputForm'); var doc = await parseAndValidateCodemeta(false); resetForm(); if (doc['license'] !== undefined) { if (typeof doc['license'] === 'string') { doc['license'] = [doc['license']]; } doc['license'].forEach(l => { if (l.indexOf(SPDX_PREFIX) !== 0) { return; } let licenseId = l.substring(SPDX_PREFIX.length); insertLicenseElement(licenseId); }); } directCodemetaFields.forEach(function (item, index) { setIfDefined('#' + item, doc[item]); }); importShortOrg('#funder', doc["funder"]); + importReview(doc["review"]); // Import simple fields by joining on their separator splittedCodemetaFields.forEach(function (item, index) { const id = item[0]; const separator = item[1]; let value = doc[id]; if (value !== undefined) { if (Array.isArray(value)) { value = value.join(separator); } setIfDefined('#' + id, value); } }); + for (const [key, items] of Object.entries(crossCodemetaFields)) { + let value = ""; + items.forEach(item => { + value = doc[item] || value; + }); + setIfDefined(`#${key}`, value); + } + importPersons('author', 'Author', doc['author']) importPersons('contributor', 'Contributor', doc['contributor']) } function loadStateFromStorage() { var codemetaText = sessionStorage.getItem('codemetaText') if (codemetaText) { document.querySelector('#codemetaText').innerText = codemetaText; importCodemeta(); } } diff --git a/js/dynamic_form.js b/js/dynamic_form.js index 7f43bd2..f15aba8 100644 --- a/js/dynamic_form.js +++ b/js/dynamic_form.js @@ -1,230 +1,232 @@ /** * Copyright (C) 2019 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ "use strict"; // List of all HTML fields in a Person fieldset. const personFields = [ 'givenName', 'familyName', 'email', 'id', 'affiliation', ]; function createPersonFieldset(personPrefix, legend) { // Creates a fieldset containing inputs for informations about a person var fieldset = document.createElement("fieldset") var moveButtons; fieldset.classList.add("person"); fieldset.classList.add("leafFieldset"); fieldset.id = personPrefix; fieldset.innerHTML = ` ${legend}

`; return fieldset; } function addPersonWithId(container, prefix, legend, id) { var personPrefix = `${prefix}_${id}`; var fieldset = createPersonFieldset(personPrefix, `${legend} #${id}`); container.appendChild(fieldset); document.querySelector(`#${personPrefix}_moveToLeft`) .addEventListener('click', () => movePerson(prefix, id, "left")); document.querySelector(`#${personPrefix}_moveToRight`) .addEventListener('click', () => movePerson(prefix, id, "right")); document.querySelector(`#${personPrefix}_role_add`) .addEventListener('click', () => addRole(personPrefix)); } function movePerson(prefix, id1, direction) { var nbPersons = getNbPersons(prefix); var id2; // Computer id2, the id of the person to flip id1 with (wraps around the // end of the list of persons) if (direction == "left") { id2 = id1 - 1; if (id2 <= 0) { id2 = nbPersons; } } else { id2 = id1 + 1; if (id2 > nbPersons) { id2 = 1; } } // Flip the field values, one by one personFields.forEach((fieldName) => { var field1 = document.querySelector(`#${prefix}_${id1}_${fieldName}`); var field2 = document.querySelector(`#${prefix}_${id2}_${fieldName}`); var value1 = field1.value; var value2 = field2.value; field2.value = value1; field1.value = value2; }); // Form was changed; regenerate generateCodemeta(); } function addPerson(prefix, legend) { var container = document.querySelector(`#${prefix}_container`); var personId = getNbPersons(prefix) + 1; addPersonWithId(container, prefix, legend, personId); setNbPersons(prefix, personId); return personId; } function removePerson(prefix) { var personId = getNbPersons(prefix); document.querySelector(`#${prefix}_${personId}`).remove(); setNbPersons(prefix, personId - 1); } // Initialize a group of persons (authors, contributors) on page load. // Useful if the page is reloaded. function initPersons(prefix, legend) { var nbPersons = getNbPersons(prefix); var personContainer = document.querySelector(`#${prefix}_container`) for (let personId = 1; personId <= nbPersons; personId++) { addPersonWithId(personContainer, prefix, legend, personId); } } function removePersons(prefix) { var nbPersons = getNbPersons(prefix); var personContainer = document.querySelector(`#${prefix}_container`) for (let personId = 1; personId <= nbPersons; personId++) { removePerson(prefix) } } function addRole(personPrefix) { const roleButtonGroup = document.querySelector(`#${personPrefix}_role_add`); const roleIndexNode = document.querySelector(`#${personPrefix}_role_index`); const roleIndex = parseInt(roleIndexNode.value, 10); const ul = document.createElement("ul") ul.classList.add("role"); ul.id = `${personPrefix}_role_${roleIndex}`; ul.innerHTML = `
  • `; roleButtonGroup.after(ul); document.querySelector(`#${personPrefix}_role_remove_${roleIndex}`) .addEventListener('click', () => removeRole(personPrefix, roleIndex)); roleIndexNode.value = roleIndex + 1; + + return roleIndex; } function removeRole(personPrefix, roleIndex) { document.querySelector(`#${personPrefix}_role_${roleIndex}`).remove(); } function resetForm() { removePersons('author'); removePersons('contributor'); // Reset the list of selected licenses document.getElementById("selected-licenses").innerHTML = ''; // Reset the form after deleting elements, so nbPersons doesn't get // reset before it's read. document.querySelector('#inputForm').reset(); } function fieldToLower(event) { event.target.value = event.target.value.toLowerCase(); } function initCallbacks() { document.querySelector('#license') .addEventListener('change', validateLicense); document.querySelector('#generateCodemetaV2').disabled = false; document.querySelector('#generateCodemetaV2') .addEventListener('click', () => generateCodemeta("2.0")); document.querySelector('#generateCodemetaV3').disabled = false; document.querySelector('#generateCodemetaV3') .addEventListener('click', () => generateCodemeta("3.0")); document.querySelector('#resetForm') .addEventListener('click', resetForm); document.querySelector('#validateCodemeta').disabled = false; document.querySelector('#validateCodemeta') .addEventListener('click', () => parseAndValidateCodemeta(true)); document.querySelector('#importCodemeta').disabled = false; document.querySelector('#importCodemeta') .addEventListener('click', importCodemeta); document.querySelector('#inputForm') .addEventListener('change', () => generateCodemeta()); document.querySelector('#developmentStatus') .addEventListener('change', fieldToLower); initPersons('author', 'Author'); initPersons('contributor', 'Contributor'); } diff --git a/js/validation/index.js b/js/validation/index.js index e6c9ff1..3aa9bef 100644 --- a/js/validation/index.js +++ b/js/validation/index.js @@ -1,94 +1,99 @@ /** * Copyright (C) 2020 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ /* * Reads a Codemeta file and shows human-friendly errors on it. * * This validator intentionaly does not use a schema, in order to show errors * that are easy to understand for users with no understanding of JSON-LD. */ function validateDocument(doc) { if (!Array.isArray(doc) && typeof doc != 'object') { setError("Document must be an object (starting and ending with { and }), not ${typeof doc}.") return false; } // TODO: validate id/@id + if (doc["@context"] === undefined) { + setError("Missing context (required to determine import version).") + return false; + } + // TODO: check there is either type or @type but not both var type = getDocumentType(doc); if (type === undefined) { setError("Missing type (must be SoftwareSourceCode or SoftwareApplication).") return false; } else if (!isCompactTypeEqual(type, "SoftwareSourceCode") && !isCompactTypeEqual(type, "SoftwareApplication")) { // Check this before other fields, as a wrong type error is more // understandable than "invalid field". setError(`Wrong document type: must be "SoftwareSourceCode"/"SoftwareApplication", not ${JSON.stringify(type)}`) return false; } else { return Object.entries(doc).every((entry) => { var fieldName = entry[0]; var subdoc = entry[1]; if (fieldName == "@context") { // Was checked before return true; } else if (fieldName == "type" || fieldName == "@type") { // Was checked before return true; } else if (fieldName.startsWith("codemeta:") || fieldName.startsWith("schema:")) { // Do not check fields from other versions FIXME ? return true; } else { var validator = softwareFieldValidators[fieldName]; if (validator === undefined) { // TODO: find if it's a field that belongs to another type, // and suggest that to the user setError(`Unknown field "${fieldName}".`) return false; } else { return validator(fieldName, subdoc); } } }); } } async function parseAndValidateCodemeta(showPopup) { var codemetaText = document.querySelector('#codemetaText').innerText; let parsed, doc; try { parsed = JSON.parse(codemetaText); } catch (e) { setError(`Could not read codemeta document because it is not valid JSON (${e}). Check for missing or extra quote, colon, or bracket characters.`); return; } setError(""); var isValid = validateDocument(parsed); if (showPopup) { if (isValid) { alert('Document is valid!') } else { alert('Document is invalid.'); } } - doc = await jsonld.compact(parsed, CODEMETA_CONTEXTS["2.0"].url); // Only import codemeta v2.0 for now + doc = await jsonld.compact(parsed, parsed["@context"]); return doc; }