diff --git a/cypress/integration/basics.js b/cypress/integration/basics.js index 3dafb40..4a20a2d 100644 --- a/cypress/integration/basics.js +++ b/cypress/integration/basics.js @@ -1,243 +1,243 @@ /** * 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('#generateCodemeta').click(); + 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('#generateCodemeta').click(); + 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('#generateCodemeta').click(); + 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('#generateCodemeta').click(); + 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", }); }); }); 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('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('errors on invalid context URL', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-100000", "@type": "SoftwareSourceCode", "name": "My Test Software", })) ); cy.get('#importCodemeta').click(); - cy.get('#errorMessage').should('have.text', '@context must be "https://doi.org/10.5063/schema/codemeta-2.0", not "https://doi.org/10.5063/schema/codemeta-100000"'); + cy.get('#errorMessage').should('have.text', '@context must be one of "https://doi.org/10.5063/schema/codemeta-2.0", "https://w3id.org/codemeta/3.0", not "https://doi.org/10.5063/schema/codemeta-100000"'); }); it('errors on invalid context URL in array', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": ["https://doi.org/10.5063/schema/codemeta-100000"], "@type": "SoftwareSourceCode", "name": "My Test Software", })) ); cy.get('#importCodemeta').click(); - cy.get('#errorMessage').should('have.text', '@context must be "https://doi.org/10.5063/schema/codemeta-2.0", not ["https://doi.org/10.5063/schema/codemeta-100000"]'); + cy.get('#errorMessage').should('have.text', '@context must be one of "https://doi.org/10.5063/schema/codemeta-2.0", "https://w3id.org/codemeta/3.0", not ["https://doi.org/10.5063/schema/codemeta-100000"]'); }); it('errors nicely when there are other contexts', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": [ "https://doi.org/10.5063/schema/codemeta-2.0", "https://schema.org/", ], "@type": "SoftwareSourceCode", "name": "My Test Software", })) ); cy.get('#importCodemeta').click(); cy.get('#errorMessage').should('have.text', 'Multiple values in @context are not supported (@context should be "https://doi.org/10.5063/schema/codemeta-2.0", not ["https://doi.org/10.5063/schema/codemeta-2.0","https://schema.org/"])'); }); }); diff --git a/cypress/integration/persons.js b/cypress/integration/persons.js index e4e0182..14abd9d 100644 --- a/cypress/integration/persons.js +++ b/cypress/integration/persons.js @@ -1,424 +1,424 @@ /** * 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('#generateCodemeta').click(); + 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('#generateCodemeta').click(); + 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('#generateCodemeta').click(); + 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('#generateCodemeta').click(); + 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('#generateCodemeta').click(); + 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'); }); }) diff --git a/cypress/integration/special_fields.js b/cypress/integration/special_fields.js index 62eb48d..d770e5f 100644 --- a/cypress/integration/special_fields.js +++ b/cypress/integration/special_fields.js @@ -1,90 +1,90 @@ /** * 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('Funder id', function() { it('can be exported', function() { cy.get('#name').type('My Test Software'); cy.get('#funder').type('http://example.org/'); - cy.get('#generateCodemeta').click(); + 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", "funder": { "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", "funder": { "@type": "Organization", "@id": "http://example.org/", }, })) ); cy.get('#importCodemeta').click(); cy.get('#funder').should('have.value', 'http://example.org/'); }); }); describe('Funder name', function() { it('can be exported', function() { cy.get('#name').type('My Test Software'); cy.get('#funder').type('Example Org'); - cy.get('#generateCodemeta').click(); + 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", "funder": { "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", "funder": { "@type": "Organization", "name": "Example Org", } })) ); cy.get('#importCodemeta').click(); cy.get('#funder').should('have.value', 'Example Org'); }); }); diff --git a/cypress/integration/validation.js b/cypress/integration/validation.js index eaff87e..2161406 100644 --- a/cypress/integration/validation.js +++ b/cypress/integration/validation.js @@ -1,1026 +1,1026 @@ /** * Copyright (C) 2020-2021 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('Document validation', function() { it('accepts empty document', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", })) ); cy.get('#validateCodemeta').click(); cy.get('#name').should('have.value', ''); cy.get('#errorMessage').should('have.text', ''); }); it('accepts all main fields', 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('#validateCodemeta').click(); cy.get('#name').should('have.value', ''); cy.get('#errorMessage').should('have.text', ''); }); it('accepts anything in non-validated fields', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "programmingLanguage": "foo", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "programmingLanguage": 21, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "programmingLanguage": {}, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); 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('#validateCodemeta').click(); cy.get('#name').should('have.value', ''); cy.get('#errorMessage').should('have.text', 'Wrong document type: must be "SoftwareSourceCode"/"SoftwareApplication", not "foo"'); }); it('errors on invalid field name', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "foobar": "baz", })) ); cy.get('#validateCodemeta').click(); cy.get('#name').should('have.value', ''); cy.get('#errorMessage').should('have.text', 'Unknown field "foobar".'); }); }); describe('URLs validation', function() { it('accepts valid URL', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": "http://example.org/", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts empty list of URLs', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": [], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts list of valid URLs', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": ["http://example.org/", "http://example.com/"], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on invalid URL', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": "foo", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', 'Invalid URL in field "codeRepository": "foo"'); }); it('errors on non-string instead of URL', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": {}, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"codeRepository" must be an URL (or a list of URLs), not: {}'); }); it('errors on list with an invalid URL at the end', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": ["http://example.org/", "foo"], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', 'Invalid URL in field "codeRepository": "foo"'); }); it('errors on list with an invalid URL at the beginning', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": ["http://example.org/", "foo"], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', 'Invalid URL in field "codeRepository": "foo"'); }); it('errors on non-string in URL list', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": ["http://example.org/", {}], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"codeRepository" must be a list of URLs (or a single URL), but it contains: {}'); }); }); describe('Things or URLs validation', function() { it('accepts valid Thing', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": { "@type": "SoftwareApplication", "name": "Example Soft", } })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts valid URL', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": "http://example.org/", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts empty list of Things', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": [], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts list of Things', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": [ { "@type": "SoftwareApplication", "name": "Example Soft", }, { "@type": "SoftwareApplication", "name": "Test Soft", } ] })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on non-URL string', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "license": "Copyright 2021 Myself", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"license" must be an URL or a CreativeWork/SoftwareSourceCode/SoftwareApplication object, not: "Copyright 2021 Myself"'); }); it('errors on wrong type', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": 42, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"isPartOf" must be a CreativeWork/SoftwareSourceCode/SoftwareApplication object or URI, not 42'); }); it('errors on non-Thing object', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": {}, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"isPartOf" must be a (list of) CreativeWork/SoftwareSourceCode/SoftwareApplication object(s) or an URI, but is missing a type/@type.'); }); it('errors on list with an invalid Thing at the beginning', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": [ {}, { "@type": "SoftwareApplication", "name": "Example Soft", } ], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"isPartOf" must be a (list of) CreativeWork/SoftwareSourceCode/SoftwareApplication object(s) or an URI, but is missing a type/@type.'); }); it('errors on list with an invalid Thing at the end', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": [ { "@type": "SoftwareApplication", "name": "Example Soft", }, {} ], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"isPartOf" must be a (list of) CreativeWork/SoftwareSourceCode/SoftwareApplication object(s) or an URI, but is missing a type/@type.'); }); }); describe('Texts or URLs validation', function() { it('accepts valid Text', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "applicationCategory": "foo", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts valid URL', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "applicationCategory": "http://example.org/", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts empty list of Texts', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "applicationCategory": [], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts list of Texts', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "applicationCategory": ["foo", "bar"], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on non-string instead of Text', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "applicationCategory": {}, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"applicationCategory" must be a text/URL (or a list of texts/URLs), not: {}'); }); it('errors on list with an invalid Text at the beginning', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "applicationCategory": [{}, "foo"], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"applicationCategory" must be a list of texts/URLs (or a single text/URL), but it contains: {}'); }); it('errors on list with an invalid Text at the end', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "applicationCategory": ["foo", {}], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"applicationCategory" must be a list of texts/URLs (or a single text/URL), but it contains: {}'); }); }); describe('Text validation', function() { it('accepts valid Text', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "description": "foo", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on empty list of Texts', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "description": [], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"description" must be text, not []'); }); it('errors on list of Texts', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "description": ["foo", "bar"], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"description" must be text, not ["foo","bar"]'); }); it('errors on non-string instead of Text', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "description": {}, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"description" must be text, not {}'); }); }); describe('Date validation', function() { it('accepts valid Date', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "dateCreated": "2020-03-18", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on empty list of Dates', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "dateCreated": [], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"dateCreated" must be a date, not []'); }); it('errors on list of Dates', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "dateCreated": ["2020-03-18", "2020-03-19"], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"dateCreated" must be a date, not ["2020-03-18","2020-03-19"]'); }); it('errors on non-string instead of Date', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "dateCreated": {}, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"dateCreated" must be a date, not {}'); }); it('errors on non-Date string', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "dateCreated": "foo", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"dateCreated" must be a date in the format YYYY-MM-DD, not "foo"'); }); }); describe('Person validation', function() { it('accepts URI', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": "http://example.org/~jdoe", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts valid complete Person', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": { "@type": "Person", "@id": "http://example.org/~jdoe", "url": "http://example.org/~jdoe", "name": "Jane Doe", "givenName": "Jane", "familyName": "Doe", "email": "jdoe@example.org", "affiliation": { "@type": "Organization", "@id": "http://example.org/", } } })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on Person with missing type', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": { }, })) ); cy.get('#validateCodemeta').click(); - cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Person/Organization object(s) or an URI, but is missing a type/@type.'); + cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Role/Person/Organization object(s) or an URI, but is missing a type/@type.'); }); it('errors on Person with wrong type', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": { "type": "SoftwareSourceCode", }, })) ); cy.get('#validateCodemeta').click(); - cy.get('#errorMessage').should('have.text', '"author" type must be a (list of) Person/Organization object(s), not "SoftwareSourceCode"'); + cy.get('#errorMessage').should('have.text', '"author" type must be a (list of) Role/Person/Organization object(s), not "SoftwareSourceCode"'); }); it('errors on Person with unknown field', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": { "type": "Person", "foo": "bar", }, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', 'Unknown field "foo" in "author".'); }); it('errors on Person with invalid field', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": { "type": "Person", "email": 32, }, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"email" must be text, not 32'); }); it('accepts URI in list', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": [ "http://example.org/~jadoe", { "@type": "Person", "@id": "http://example.org/~jodoe", "givenName": "John", "familyName": "Doe", }, ] })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts list of valid Person', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": [ { "@type": "Person", "@id": "http://example.org/~jadoe", "givenName": "Jane", "familyName": "Doe", }, { "@type": "Person", "@id": "http://example.org/~jodoe", "givenName": "John", "familyName": "Doe", }, ] })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts Person with multiple affiliations', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": { "@type": "Person", "@id": "http://example.org/~jdoe", "name": "Jane Doe", "affiliation": [ { "@type": "Organization", "@id": "http://example.org/", }, { "@type": "Organization", "@id": "http://example.com/", }, ] } })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on list with invalid Person at the beginning', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": [ { "givenName": "Jane", "familyName": "Doe", }, { "@type": "Person", "@id": "http://example.org/~jodoe", "name": "John Doe", "givenName": "John", "familyName": "Doe", }, ] })) ); cy.get('#validateCodemeta').click(); - cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Person/Organization object(s) or an URI, but is missing a type/@type.'); + cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Role/Person/Organization object(s) or an URI, but is missing a type/@type.'); }); it('errors on list with invalid Person at the end', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": [ { "@type": "Person", "@id": "http://example.org/~jadoe", "givenName": "Jane", "familyName": "Doe", }, { "givenName": "John", "familyName": "Doe", }, ] })) ); cy.get('#validateCodemeta').click(); - cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Person/Organization object(s) or an URI, but is missing a type/@type.'); + cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Role/Person/Organization object(s) or an URI, but is missing a type/@type.'); }); }); describe('Organization validation', function() { it('accepts valid complete Organization', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": { "@type": "Organization", "@id": "http://example.org/", "url": "https://example.org/", "name": "Example Org", "identifier": "http://example.org/", "address": "Nowhere", } })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on Organization with missing type', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": { }, })) ); cy.get('#validateCodemeta').click(); - cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Person/Organization object(s) or an URI, but is missing a type/@type.'); + cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Role/Person/Organization object(s) or an URI, but is missing a type/@type.'); }); it('errors on Organization with wrong type', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": { "type": "SoftwareSourceCode", }, })) ); cy.get('#validateCodemeta').click(); - cy.get('#errorMessage').should('have.text', '"author" type must be a (list of) Person/Organization object(s), not "SoftwareSourceCode"'); + cy.get('#errorMessage').should('have.text', '"author" type must be a (list of) Role/Person/Organization object(s), not "SoftwareSourceCode"'); }); it('errors on Organization with unknown field', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": { "type": "Organization", "foo": "bar", }, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', 'Unknown field "foo" in "author".'); }); it('errors on Organization with invalid field', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": { "type": "Organization", "email": 32, }, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"email" must be text, not 32'); }); it('accepts list of valid Organization', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": [ { "@type": "Organization", "@id": "http://example.org/", "name": "Example Org", }, { "@type": "Organization", "@id": "http://example.org/~jodoe", "name": "Example Org", }, ] })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on list with invalid Organization at the beginning', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": [ { "name": "Example Org", }, { "@type": "Organization", "name": "Example Org", }, ] })) ); cy.get('#validateCodemeta').click(); - cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Person/Organization object(s) or an URI, but is missing a type/@type.'); + cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Role/Person/Organization object(s) or an URI, but is missing a type/@type.'); }); it('errors on list with invalid Organization at the end', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": [ { "@type": "Organization", "name": "Example Org", }, { "name": "Example Org", }, ] })) ); cy.get('#validateCodemeta').click(); - cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Person/Organization object(s) or an URI, but is missing a type/@type.'); + cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Role/Person/Organization object(s) or an URI, but is missing a type/@type.'); }); }); describe('CreativeWork validation', function() { it('accepts valid CreativeWork', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "name": "Small Software", "isPartOf": { "type": "CreativeWork", "name": "Big Creative Work", "author": "http://example.org/~jdoe", "keywords": ["foo", "bar"], } })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on CreativeWork with missing type', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": { } })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"isPartOf" must be a (list of) CreativeWork/SoftwareSourceCode/SoftwareApplication object(s) or an URI, but is missing a type/@type.'); }); it('errors on CreativeWork with wrong type', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": { "type": "Person", }, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"isPartOf" type must be a (list of) CreativeWork/SoftwareSourceCode/SoftwareApplication object(s), not "Person"'); }); it('errors on CreativeWork with unknown field', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": { "type": "CreativeWork", "foo": "bar", }, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', 'Unknown field "foo" in "isPartOf".'); }); it('errors on CreativeWork with invalid field', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": { "type": "CreativeWork", "url": 32, }, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"url" must be an URL (or a list of URLs), not: 32'); }); }); diff --git a/data/contexts/codemeta-local.jsonld b/data/contexts/codemeta-3.0.jsonld similarity index 83% copy from data/contexts/codemeta-local.jsonld copy to data/contexts/codemeta-3.0.jsonld index 5e19122..2db7a78 100644 --- a/data/contexts/codemeta-local.jsonld +++ b/data/contexts/codemeta-3.0.jsonld @@ -1,80 +1,88 @@ { "@context": { "type": "@type", "id": "@id", "schema":"http://schema.org/", "codemeta": "https://codemeta.github.io/terms/", "Organization": {"@id": "schema:Organization"}, "Person": {"@id": "schema:Person"}, + "Review": {"@id": "schema:Review"}, + "Role": {"@id": "schema:Role"}, "SoftwareSourceCode": {"@id": "schema:SoftwareSourceCode"}, "SoftwareApplication": {"@id": "schema:SoftwareApplication"}, "Text": {"@id": "schema:Text"}, "URL": {"@id": "schema:URL"}, "address": { "@id": "schema:address"}, "affiliation": { "@id": "schema:affiliation"}, "applicationCategory": { "@id": "schema:applicationCategory", "@type": "@id"}, "applicationSubCategory": { "@id": "schema:applicationSubCategory", "@type": "@id"}, "citation": { "@id": "schema:citation"}, "codeRepository": { "@id": "schema:codeRepository", "@type": "@id"}, "contributor": { "@id": "schema:contributor"}, "copyrightHolder": { "@id": "schema:copyrightHolder"}, "copyrightYear": { "@id": "schema:copyrightYear"}, - "creator": { "@id": "schema:creator"}, "dateCreated": {"@id": "schema:dateCreated", "@type": "schema:Date" }, "dateModified": {"@id": "schema:dateModified", "@type": "schema:Date" }, "datePublished": {"@id": "schema:datePublished", "@type": "schema:Date" }, "description": { "@id": "schema:description"}, "downloadUrl": { "@id": "schema:downloadUrl", "@type": "@id"}, "email": { "@id": "schema:email"}, "editor": { "@id": "schema:editor"}, "encoding": { "@id": "schema:encoding"}, + "endDate": { "@id": "schema:endDate"}, "familyName": { "@id": "schema:familyName"}, "fileFormat": { "@id": "schema:fileFormat", "@type": "@id"}, "fileSize": { "@id": "schema:fileSize"}, "funder": { "@id": "schema:funder"}, "givenName": { "@id": "schema:givenName"}, "hasPart": { "@id": "schema:hasPart" }, "identifier": { "@id": "schema:identifier", "@type": "@id"}, "installUrl": { "@id": "schema:installUrl", "@type": "@id"}, "isAccessibleForFree": { "@id": "schema:isAccessibleForFree"}, "isPartOf": { "@id": "schema:isPartOf"}, "keywords": { "@id": "schema:keywords"}, "license": { "@id": "schema:license", "@type": "@id"}, "memoryRequirements": { "@id": "schema:memoryRequirements", "@type": "@id"}, "name": { "@id": "schema:name"}, "operatingSystem": { "@id": "schema:operatingSystem"}, "permissions": { "@id": "schema:permissions"}, "position": { "@id": "schema:position"}, "processorRequirements": { "@id": "schema:processorRequirements"}, "producer": { "@id": "schema:producer"}, "programmingLanguage": { "@id": "schema:programmingLanguage"}, "provider": { "@id": "schema:provider"}, "publisher": { "@id": "schema:publisher"}, "relatedLink": { "@id": "schema:relatedLink", "@type": "@id"}, - "releaseNotes": { "@id": "schema:releaseNotes", "@type": "@id"}, + "review": { "@id": "schema:review", "@type": "@id" }, + "reviewAspect": { "@id": "schema:reviewAspect" }, + "reviewBody": { "@id": "schema:reviewBody" }, + "releaseNotes": { "@id": "schema:releaseNotes"}, + "roleName": { "@id": "schema:roleName"}, "runtimePlatform": { "@id": "schema:runtimePlatform"}, "sameAs": { "@id": "schema:sameAs", "@type": "@id"}, "softwareHelp": { "@id": "schema:softwareHelp"}, "softwareRequirements": { "@id": "schema:softwareRequirements", "@type": "@id"}, "softwareVersion": { "@id": "schema:softwareVersion"}, "sponsor": { "@id": "schema:sponsor"}, + "startDate": { "@id": "schema:startDate"}, "storageRequirements": { "@id": "schema:storageRequirements", "@type": "@id"}, "supportingData": { "@id": "schema:supportingData"}, "targetProduct": { "@id": "schema:targetProduct"}, "url": { "@id": "schema:url", "@type": "@id"}, "version": { "@id": "schema:version"}, - "author": { "@id": "schema:author", "@container": "@list" }, "softwareSuggestions": { "@id": "codemeta:softwareSuggestions", "@type": "@id"}, - "contIntegration": { "@id": "codemeta:contIntegration", "@type": "@id"}, + "continuousIntegration": { "@id": "codemeta:continuousIntegration", "@type": "@id"}, "buildInstructions": { "@id": "codemeta:buildInstructions", "@type": "@id"}, "developmentStatus": { "@id": "codemeta:developmentStatus", "@type": "@id"}, - "embargoDate": { "@id":"codemeta:embargoDate", "@type": "schema:Date" }, + "embargoEndDate": { "@id":"codemeta:embargoEndDate", "@type": "schema:Date" }, "funding": { "@id": "codemeta:funding" }, "readme": { "@id":"codemeta:readme", "@type": "@id" }, "issueTracker": { "@id":"codemeta:issueTracker", "@type": "@id" }, "referencePublication": { "@id": "codemeta:referencePublication", "@type": "@id"}, - "maintainer": { "@id": "codemeta:maintainer" } + "maintainer": { "@id": "codemeta:maintainer" }, + "hasSourceCode": { "@id": "codemeta:hasSourceCode", "@type": "@id"}, + "isSourceCodeOf": { "@id": "codemeta:isSourceCodeOf", "@type": "@id"} } } diff --git a/data/contexts/codemeta-3.0.txt b/data/contexts/codemeta-3.0.txt new file mode 100644 index 0000000..c7a6ac3 --- /dev/null +++ b/data/contexts/codemeta-3.0.txt @@ -0,0 +1 @@ +Matthew B. Jones, Carl Boettiger, Abby Cabunoc Mayes, Arfon Smith, Morane Gruenpeter, Valentin Lorentz, Thomas Morrell, Daniel Garijo, Peter Slaughter, Kyle Niemeyer, Yolanda Gil, Martin Fenner, Krzysztof Nowak, Mark Hahnel, Luke Coy, Alice Allen, Mercè Crosas, Ashley Sands, Neil Chue Hong, Patricia Cruse, Daniel S. Katz, Carole Goble, Bryce Mecum, Alejandra Gonzalez-Beltran, Noam Ross. 2023. CodeMeta: an exchange schema for software metadata. Version 3.0. https://w3id.org/codemeta/v3.0 diff --git a/data/contexts/codemeta-local.jsonld b/data/contexts/codemeta-local.jsonld index 5e19122..e6a30f8 100644 --- a/data/contexts/codemeta-local.jsonld +++ b/data/contexts/codemeta-local.jsonld @@ -1,80 +1,92 @@ { "@context": { "type": "@type", "id": "@id", "schema":"http://schema.org/", "codemeta": "https://codemeta.github.io/terms/", "Organization": {"@id": "schema:Organization"}, "Person": {"@id": "schema:Person"}, + "Review": {"@id": "schema:Review"}, + "Role": {"@id": "schema:Role"}, "SoftwareSourceCode": {"@id": "schema:SoftwareSourceCode"}, "SoftwareApplication": {"@id": "schema:SoftwareApplication"}, "Text": {"@id": "schema:Text"}, "URL": {"@id": "schema:URL"}, "address": { "@id": "schema:address"}, "affiliation": { "@id": "schema:affiliation"}, "applicationCategory": { "@id": "schema:applicationCategory", "@type": "@id"}, "applicationSubCategory": { "@id": "schema:applicationSubCategory", "@type": "@id"}, "citation": { "@id": "schema:citation"}, "codeRepository": { "@id": "schema:codeRepository", "@type": "@id"}, "contributor": { "@id": "schema:contributor"}, "copyrightHolder": { "@id": "schema:copyrightHolder"}, "copyrightYear": { "@id": "schema:copyrightYear"}, "creator": { "@id": "schema:creator"}, "dateCreated": {"@id": "schema:dateCreated", "@type": "schema:Date" }, "dateModified": {"@id": "schema:dateModified", "@type": "schema:Date" }, "datePublished": {"@id": "schema:datePublished", "@type": "schema:Date" }, "description": { "@id": "schema:description"}, "downloadUrl": { "@id": "schema:downloadUrl", "@type": "@id"}, "email": { "@id": "schema:email"}, "editor": { "@id": "schema:editor"}, "encoding": { "@id": "schema:encoding"}, + "endDate": { "@id": "schema:endDate"}, "familyName": { "@id": "schema:familyName"}, "fileFormat": { "@id": "schema:fileFormat", "@type": "@id"}, "fileSize": { "@id": "schema:fileSize"}, "funder": { "@id": "schema:funder"}, "givenName": { "@id": "schema:givenName"}, "hasPart": { "@id": "schema:hasPart" }, "identifier": { "@id": "schema:identifier", "@type": "@id"}, "installUrl": { "@id": "schema:installUrl", "@type": "@id"}, "isAccessibleForFree": { "@id": "schema:isAccessibleForFree"}, "isPartOf": { "@id": "schema:isPartOf"}, "keywords": { "@id": "schema:keywords"}, "license": { "@id": "schema:license", "@type": "@id"}, "memoryRequirements": { "@id": "schema:memoryRequirements", "@type": "@id"}, "name": { "@id": "schema:name"}, "operatingSystem": { "@id": "schema:operatingSystem"}, "permissions": { "@id": "schema:permissions"}, "position": { "@id": "schema:position"}, "processorRequirements": { "@id": "schema:processorRequirements"}, "producer": { "@id": "schema:producer"}, "programmingLanguage": { "@id": "schema:programmingLanguage"}, "provider": { "@id": "schema:provider"}, "publisher": { "@id": "schema:publisher"}, "relatedLink": { "@id": "schema:relatedLink", "@type": "@id"}, - "releaseNotes": { "@id": "schema:releaseNotes", "@type": "@id"}, + "review": { "@id": "schema:review", "@type": "@id" }, + "reviewAspect": { "@id": "schema:reviewAspect" }, + "reviewBody": { "@id": "schema:reviewBody" }, + "releaseNotes": { "@id": "schema:releaseNotes"}, + "roleName": { "@id": "schema:roleName"}, "runtimePlatform": { "@id": "schema:runtimePlatform"}, "sameAs": { "@id": "schema:sameAs", "@type": "@id"}, "softwareHelp": { "@id": "schema:softwareHelp"}, "softwareRequirements": { "@id": "schema:softwareRequirements", "@type": "@id"}, "softwareVersion": { "@id": "schema:softwareVersion"}, "sponsor": { "@id": "schema:sponsor"}, + "startDate": { "@id": "schema:startDate"}, "storageRequirements": { "@id": "schema:storageRequirements", "@type": "@id"}, "supportingData": { "@id": "schema:supportingData"}, "targetProduct": { "@id": "schema:targetProduct"}, "url": { "@id": "schema:url", "@type": "@id"}, "version": { "@id": "schema:version"}, "author": { "@id": "schema:author", "@container": "@list" }, "softwareSuggestions": { "@id": "codemeta:softwareSuggestions", "@type": "@id"}, "contIntegration": { "@id": "codemeta:contIntegration", "@type": "@id"}, + "continuousIntegration": { "@id": "codemeta:continuousIntegration", "@type": "@id"}, "buildInstructions": { "@id": "codemeta:buildInstructions", "@type": "@id"}, "developmentStatus": { "@id": "codemeta:developmentStatus", "@type": "@id"}, "embargoDate": { "@id":"codemeta:embargoDate", "@type": "schema:Date" }, + "embargoEndDate": { "@id":"codemeta:embargoEndDate", "@type": "schema:Date" }, "funding": { "@id": "codemeta:funding" }, "readme": { "@id":"codemeta:readme", "@type": "@id" }, "issueTracker": { "@id":"codemeta:issueTracker", "@type": "@id" }, "referencePublication": { "@id": "codemeta:referencePublication", "@type": "@id"}, - "maintainer": { "@id": "codemeta:maintainer" } + "maintainer": { "@id": "codemeta:maintainer" }, + "hasSourceCode": { "@id": "codemeta:hasSourceCode", "@type": "@id"}, + "isSourceCodeOf": { "@id": "codemeta:isSourceCodeOf", "@type": "@id"} } } diff --git a/index.html b/index.html index 8be7617..3af9815 100644 --- a/index.html +++ b/index.html @@ -1,367 +1,394 @@ CodeMeta generator
-

CodeMeta generator

+

CodeMeta generator v3.0

Most fields are optional. Mandatory fields will be highlighted when generating Codemeta.

The software itself

the software title


from SPDX licence list

Discoverability and citation


such as ISBNs, GTIN codes, UUIDs etc.. http://schema.org/identifier


grant funding software development


organization funding software development

Authors and contributors can be added below
Development community / tools


Run-time environment


Current version of the software


-
- Additional Info +
+ Editorial review

+

+ + +

+ +

+ + +

+
+ +
+ Additional Info +


see www.repostatus.org for details

+

+ + +

+

Authors
Contributors

Order of contributors does not matter.

- + +

codemeta.json:


   
diff --git a/js/codemeta_generation.js b/js/codemeta_generation.js index e60863b..4213830 100644 --- a/js/codemeta_generation.js +++ b/js/codemeta_generation.js @@ -1,300 +1,378 @@ /** * 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 CODEMETA_LOCAL_CONTEXT_PATH = "./data/contexts/codemeta-local.jsonld"; -const CODEMETA_V2_CONTEXT_PATH = './data/contexts/codemeta-2.0.jsonld'; -const CODEMETA_LOCAL_CONTEXT_URL = 'local'; -const CODEMETA_V2_CONTEXT_URL = 'https://doi.org/10.5063/schema/codemeta-2.0'; +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] = + const [contextLocal, contextV2, contextV3] = await Promise.all([ - fetch(CODEMETA_LOCAL_CONTEXT_PATH).then(response => response.json()), - fetch(CODEMETA_V2_CONTEXT_PATH).then(response => response.json()), + 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 { - [CODEMETA_LOCAL_CONTEXT_URL]: contextLocal, - [CODEMETA_V2_CONTEXT_URL]: contextV2, + [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 = { + "contIntegration": ["contIntegration", "continuousIntegration"], + "embargoDate": ["embargoDate", "embargoEndDate"], +}; + 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`) + 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++) { - persons.push(generatePerson(`${prefix}_${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": CODEMETA_LOCAL_CONTEXT_URL, + "@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]; + }); + } return await jsonld.expand(doc); } -async function generateCodemeta() { +// 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_V2_CONTEXT_URL); + 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, doc["@id"]); } } function importPersons(prefix, legend, docs) { if (docs === undefined) { return; } docs.forEach(function (doc, index) { var personId = addPerson(prefix, legend); setIfDefined(`#${prefix}_${personId}_id`, doc['@id']); directPersonCodemetaFields.forEach(function (item, index) { setIfDefined(`#${prefix}_${personId}_${item}`, doc[item]); }); importShortOrg(`#${prefix}_${personId}_affiliation`, doc['affiliation']) }) } function importCodemeta() { var inputForm = document.querySelector('#inputForm'); var doc = 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"]); // 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); } }); 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 849f50a..7f43bd2 100644 --- a/js/dynamic_form.js +++ b/js/dynamic_form.js @@ -1,191 +1,230 @@ /** * 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; +} + +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('#generateCodemeta').disabled = false; - document.querySelector('#generateCodemeta') - .addEventListener('click', generateCodemeta); + 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); + .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 546fab0..ab12b1c 100644 --- a/js/validation/index.js +++ b/js/validation/index.js @@ -1,104 +1,110 @@ /** * 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 context = doc["@context"]; - if (context == "https://doi.org/10.5063/schema/codemeta-2.0") { + const contextUrls = Object.entries(CODEMETA_CONTEXTS) + .map(([version, value]) => value.url); + if (contextUrls.includes(context)) { // Correct } else if (Array.isArray(context) && context.includes("https://doi.org/10.5063/schema/codemeta-2.0")) { if (context.length !== 1) { setError(`Multiple values in @context are not supported (@context should be "https://doi.org/10.5063/schema/codemeta-2.0", not ${JSON.stringify(context)})`); return false; } } else { - setError(`@context must be "https://doi.org/10.5063/schema/codemeta-2.0", not ${JSON.stringify(context)}`); + setError(`@context must be one of "${contextUrls.join('", "')}", not ${JSON.stringify(context)}`); 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); } } }); } } function parseAndValidateCodemeta(showPopup) { var codemetaText = document.querySelector('#codemetaText').innerText; var doc; try { doc = 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(doc); if (showPopup) { if (isValid) { alert('Document is valid!') } else { alert('Document is invalid.'); } } return doc; } diff --git a/js/validation/things.js b/js/validation/things.js index 496cd01..1906dd2 100644 --- a/js/validation/things.js +++ b/js/validation/things.js @@ -1,309 +1,335 @@ /** * Copyright (C) 2020-2021 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 */ /* * Validators for codemeta objects derived from http://schema.org/Thing. */ function getDocumentType(doc) { // TODO: check there is at most one. // FIXME: is the last variant allowed? return doc["type"] || doc["@type"] || doc["codemeta:type"] } function isCompactTypeEqual(type, compactedType) { // FIXME: are all variants allowed? return (type == `${compactedType}` || type == `schema:${compactedType}` || type == `codemeta:${compactedType}` || type == `http://schema.org/${compactedType}` ); } function noValidation(fieldName, doc) { return true; } // Validates subtypes of Thing, or URIs // // typeFieldValidators is a map: {type => {fieldName => fieldValidator}} function validateThingOrId(parentFieldName, typeFieldValidators, doc) { var acceptedTypesString = Object.keys(typeFieldValidators).join('/'); if (typeof doc == 'string') { if (!isUrl(doc)) { setError(`"${parentFieldName}" must be an URL or a ${acceptedTypesString} object, not: ${JSON.stringify(doc)}`); return false; } else { return true; } } else if (!Array.isArray(doc) && typeof doc == 'object') { return validateThing(parentFieldName, typeFieldValidators, doc); } else { setError(`"${parentFieldName}" must be a ${acceptedTypesString} object or URI, not ${JSON.stringify(doc)}`); return false; } } // Validates subtypes of Thing // // typeFieldValidators is a map: {type => {fieldName => fieldValidator}} function validateThing(parentFieldName, typeFieldValidators, doc) { // TODO: check there is either id or @id but not both // TODO: check there is either type or @type but not both var acceptedTypesString = Object.keys(typeFieldValidators).join('/'); var documentType = getDocumentType(doc); var id = doc["id"] || doc["@id"]; if (id !== undefined && !isUrl(id)) { setError(`"${fieldName}" has an invalid URI as id: ${JSON.stringify(id)}"`); return false; } if (documentType === undefined) { if (id === undefined) { setError(`"${parentFieldName}" must be a (list of) ${acceptedTypesString} object(s) or an URI, but is missing a type/@type.`); return false; } else { // FIXME: we have an @id but no @type, what should we do? return true; } } for (expectedType in typeFieldValidators) { if (isCompactTypeEqual(documentType, expectedType)) { var fieldValidators = typeFieldValidators[expectedType]; return Object.entries(doc).every((entry) => { var fieldName = entry[0]; var subdoc = entry[1]; 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 = fieldValidators[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}" in "${parentFieldName}".`) return false; } else { return validator(fieldName, subdoc); } } }); } } setError(`"${parentFieldName}" type must be a (list of) ${acceptedTypesString} object(s), not ${JSON.stringify(documentType)}`); return false; } // Helper function to validate a field is either X or a list of X. function validateListOrSingle(fieldName, doc, validator) { if (Array.isArray(doc)) { return doc.every((subdoc) => validator(subdoc, true)); } else { return validator(doc, false); } } // Validates a CreativeWork or an array of CreativeWork function validateCreativeWorks(fieldName, doc) { return validateListOrSingle(fieldName, doc, (subdoc, inList) => { return validateCreativeWork(fieldName, subdoc); }); } // Validates a single CreativeWork function validateCreativeWork(fieldName, doc) { return validateThingOrId(fieldName, { "CreativeWork": creativeWorkFieldValidators, "SoftwareSourceCode": softwareFieldValidators, "SoftwareApplication": softwareFieldValidators, }, doc); } // Validates a Person, Organization or an array of these function validateActors(fieldName, doc) { return validateListOrSingle(fieldName, doc, (subdoc, inList) => { return validateActor(fieldName, subdoc); }); } // Validates a Person or an array of Person function validatePersons(fieldName, doc) { return validateListOrSingle(fieldName, doc, (subdoc, inList) => { return validatePerson(fieldName, subdoc); }); } // Validates an Organization or an array of Organization function validateOrganizations(fieldName, doc) { return validateListOrSingle(fieldName, doc, (subdoc, inList) => { return validateOrganization(fieldName, subdoc); }); } // Validates a single Person or Organization function validateActor(fieldName, doc) { return validateThingOrId(fieldName, { + "Role": roleFieldValidators, "Person": personFieldValidators, "Organization": organizationFieldValidators, }, doc); } // Validates a single Person object function validatePerson(fieldName, doc) { return validateThingOrId(fieldName, {"Person": personFieldValidators}, doc); } // Validates a single Organization object function validateOrganization(fieldName, doc) { return validateThingOrId(fieldName, {"Organization": organizationFieldValidators}, doc); } +function validateReview(fieldName, doc) { + return validateThingOrId(fieldName, {"Review": reviewFieldValidators}, doc); +} + var softwareFieldValidators = { "@id": validateUrl, "id": validateUrl, "codeRepository": validateUrls, "programmingLanguage": noValidation, "runtimePlatform": validateTexts, "targetProduct": noValidation, // TODO: validate SoftwareApplication "applicationCategory": validateTextsOrUrls, "applicationSubCategory": validateTextsOrUrls, "downloadUrl": validateUrls, "fileSize": validateText, // TODO "installUrl": validateUrls, "memoryRequirements": validateTextsOrUrls, "operatingSystem": validateTexts, "permissions": validateTexts, "processorRequirements": validateTexts, "releaseNotes": validateTextsOrUrls, "softwareHelp": validateCreativeWorks, "softwareRequirements": noValidation, // TODO: validate SoftwareSourceCode "softwareVersion": validateText, // TODO? "storageRequirements": validateTextsOrUrls, "supportingData": noValidation, // TODO "author": validateActors, "citation": validateCreativeWorks, // TODO "contributor": validateActors, "copyrightHolder": validateActors, "copyrightYear": validateNumbers, "creator": validateActors, // TODO: still in codemeta 2.0, but removed from master "dateCreated": validateDate, "dateModified": validateDate, "datePublished": validateDate, "editor": validatePersons, "encoding": noValidation, "fileFormat": validateTextsOrUrls, "funder": validateActors, // TODO: may be other types "keywords": validateTexts, "license": validateCreativeWorks, "producer": validateActors, "provider": validateActors, "publisher": validateActors, "sponsor": validateActors, "version": validateNumberOrText, "isAccessibleForFree": validateBoolean, + "isSourceCodeOf": validateTextsOrUrls, "isPartOf": validateCreativeWorks, "hasPart": validateCreativeWorks, "position": noValidation, "identifier": noValidation, // TODO "description": validateText, "name": validateText, "sameAs": validateUrls, "url": validateUrls, "relatedLink": validateUrls, + "review": validateReview, "softwareSuggestions": noValidation, // TODO: validate SoftwareSourceCode "maintainer": validateActors, "contIntegration": validateUrls, + "continuousIntegration": validateUrls, "buildInstructions": validateUrls, "developmentStatus": validateText, // TODO: use only repostatus strings? "embargoDate": validateDate, + "embargoEndDate": validateDate, "funding": validateText, "issueTracker": validateUrls, "referencePublication": noValidation, // TODO? "readme": validateUrls, }; var creativeWorkFieldValidators = { "@id": validateUrl, "id": validateUrl, "author": validateActors, "citation": validateCreativeWorks, // TODO "contributor": validateActors, "copyrightHolder": validateActors, "copyrightYear": validateNumbers, "creator": validateActors, // TODO: still in codemeta 2.0, but removed from master "dateCreated": validateDate, "dateModified": validateDate, "datePublished": validateDate, "editor": validatePersons, "encoding": noValidation, "funder": validateActors, // TODO: may be other types "keywords": validateTexts, "license": validateCreativeWorks, "producer": validateActors, "provider": validateActors, "publisher": validateActors, "sponsor": validateActors, "version": validateNumberOrText, "isAccessibleForFree": validateBoolean, "isPartOf": validateCreativeWorks, "hasPart": validateCreativeWorks, "position": noValidation, "identifier": noValidation, // TODO "description": validateText, "name": validateText, "sameAs": validateUrls, "url": validateUrls, }; +var roleFieldValidators = { + "roleName": validateText, + "startDate": validateDate, + "endDate": validateDate, + + "schema:author": validateActor +}; + var personFieldValidators = { "@id": validateUrl, "id": validateUrl, "givenName": validateText, "familyName": validateText, "email": validateText, "affiliation": validateOrganizations, "identifier": validateUrls, "name": validateText, // TODO: this is technically valid, but should be allowed here? "url": validateUrls, }; var organizationFieldValidators = { "@id": validateUrl, "id": validateUrl, "email": validateText, "identifier": validateUrls, "name": validateText, "address": validateText, "sponsor": validateActors, "funder": validateActors, // TODO: may be other types "isPartOf": validateOrganizations, "url": validateUrls, // TODO: add more? }; + +const reviewFieldValidators = { + "reviewAspect": validateText, + "reviewBody": validateText, +} diff --git a/main.css b/main.css index 6204b47..e4cfaab 100644 --- a/main.css +++ b/main.css @@ -1,82 +1,91 @@ /** * 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 */ /* This file contains the main CSS to make the form/application usable, * without being especially pretty. */ #noscriptError { color: red; } .person { display: inline-block; } +.role { + display: flex; + flex-wrap: wrap; + gap: 8px; + list-style-type: none; + padding: 0; +} + #inputForm { max-width: 100%; display: flex; flex-wrap: wrap; } /* A fieldset that contains only label/input pairs */ .leafFieldset { flex: auto; + vertical-align: top; } p input, p textarea { width: 100%; box-sizing: border-box; } .dynamicFields { width: 100%; } .dynamicFields .moveButtons { width: 100%; display: flex; justify-content: space-between; } #license { /* License names are long */ min-width: 20em; } #funding { /* Funding names are long */ min-width: 20em; } input[type=URL] { /* URLs are longer than the other fields */ min-width: 20em; } .field-description { color : rgb(100, 104, 103); font-size: small; } #codemetaText { width: 100%; min-height: 10em; border: 1px solid black; } #errorMessage { color: red; } input:invalid { color: red; } .selected-license { margin: 2px; }