Commit c7096684 authored by Stefan Probst's avatar Stefan Probst
Browse files

perf: avoid form rerenders

parent 3c0a37fe
Pipeline #289225 passed with stages
in 10 minutes and 18 seconds
......@@ -23,8 +23,8 @@ describe('Create dataset page', () => {
window.localStorage.setItem('__sshoc_token__', token)
}
})
cy.visit('/dataset/new')
})
cy.visit('/dataset/new')
})
it('should set document title', () => {
......@@ -35,6 +35,11 @@ describe('Create dataset page', () => {
cy.get('h1').contains('Create Dataset')
})
it('should redirect to accoun screen on cancel', () => {
cy.findByRole('button', { name: 'Cancel' }).realClick()
cy.location('pathname').should('equal', '/account')
})
it('should not submit form when required fields are empty on submit', () => {
cy.findByRole('button', { name: 'Submit' }).realClick()
cy.findAllByText('Field must not be empty.').should('have.length', 2)
......@@ -49,54 +54,6 @@ describe('Create dataset page', () => {
cy.location('pathname').should('equal', '/success')
})
it('should display field error message when "accessible at" field is empty', () => {
cy.get('input[name="accessibleAt[0]"').focus().blur()
cy.findByText('Field must not be empty.').should('be.visible')
cy.get('input[name="accessibleAt[0]"]').should('have.attr', 'aria-invalid', 'true')
})
it('should display field error message when "accessible at" field has invalid url', () => {
cy.get('input[name="accessibleAt[0]"').focus().type('invalid-url').blur()
cy.findByText('Please provide a valid URL.').should('be.visible')
cy.get('input[name="accessibleAt[0]"]').should('have.attr', 'aria-invalid', 'true')
})
it('should display field error message when "external id" fields are empty', () => {
cy.findByLabelText('ID Service', { selector: 'button' }).focus().blur()
cy.findByLabelText('Identifier').focus().blur()
cy.findAllByText('Field must not be empty.').should('have.length', 2)
// cy.get('select[name="externalIds[0].identifierService.code"]').should(
// 'have.attr',
// 'aria-invalid',
// 'true',
// )
cy.get('input[name="externalIds[0].identifier"]').should('have.attr', 'aria-invalid', 'true')
})
it('should display field error message when "contributor" fields are empty', () => {
cy.findByLabelText('Role', { selector: 'button' }).focus().blur()
cy.get('input[name="contributors[0].actor.id"]').focus().blur()
cy.findAllByText('Field must not be empty.').should('have.length', 2)
// cy.get('select[name="contributors[0].role.code"]').should('have.attr', 'aria-invalid', 'true')
cy.get('input[name="contributors[0].actor.id"]').should('have.attr', 'aria-invalid', 'true')
})
it('should display field error message when "related item" fields are empty', () => {
cy.findByLabelText('Relation type', { selector: 'button' }).focus().blur()
cy.get('input[name="relatedItems[0].persistentId"]').focus().blur()
cy.findAllByText('Field must not be empty.').should('have.length', 2)
// cy.get('select[name="relatedItems[0].relation.code"]').should(
// 'have.attr',
// 'aria-invalid',
// 'true',
// )
cy.get('input[name="relatedItems[0].persistentId"]').should(
'have.attr',
'aria-invalid',
'true',
)
})
it('should dispatch filled out form as payload on submit', () => {
cy.get('input[name="label"]').type('The label')
cy.get('input[name="version"]').type('2.0')
......@@ -111,12 +68,8 @@ describe('Create dataset page', () => {
cy.findByRole('button', { name: 'ID Service Please select an option' }).click()
cy.findByRole('option', { name: 'GitHub' }).click({ force: true })
cy.get('input[name="externalIds[1].identifier"]').type('123')
cy.get('input[name="dateCreated"]').then((el) => {
return el.val('2022-01-01')
})
cy.get('input[name="dateLastUpdated"]').then((el) => {
return el.val('2022-02-02')
})
cy.get('input[name="dateCreated"]').type('2022-01-01')
cy.get('input[name="dateLastUpdated"]').type('2022-02-02')
cy.findByRole('button', { name: 'Role Please select an option' }).click()
cy.findByRole('option', { name: 'Contributor' }).click({ force: true })
cy.findByRole('combobox', { name: 'Name' }).type('{downarrow}{downarrow}{enter}')
......@@ -137,9 +90,7 @@ describe('Create dataset page', () => {
cy.get('input[name="properties[5].concept.uri"]').type('{downarrow}{downarrow}{enter}')
cy.get('input[name="properties[6].value"]').type('2022')
cy.intercept({ url: 'http://localhost:8080/api/datasets', method: 'POST' }).as(
'create-dataset',
)
cy.intercept({ pathname: '/api/datasets', method: 'POST' }).as('create-dataset')
cy.findByRole('button', { name: 'Submit' }).realClick()
cy.findByRole('status').contains('Successfully suggested new Dataset.')
......@@ -192,17 +143,219 @@ describe('Create dataset page', () => {
{ identifier: '123', identifierService: { code: 'GitHub' } },
],
relatedItems: [],
dateCreated: '2022-01-01T00:00:00.000Z',
dateLastUpdated: '2022-02-02T00:00:00.000Z',
})
})
})
it('should dispatch filled out form as draft payload when saving as draft', () => {
cy.get('input[name="label"]').type('The label')
cy.get('input[name="version"]').type('2.0')
cy.get('textarea[name="description"]').type('The description')
cy.get('input[name="accessibleAt[0]"]').type('https://first.com')
cy.findByRole('button', { name: 'Add Accessible at URL' }).click()
cy.get('input[name="accessibleAt[1]"]').type('https://second.com')
// FIXME: backend throws 500 when updating external ids
// @see https://gitlab.gwdg.de/sshoc/sshoc-marketplace-backend/-/issues/166
// cy.findByRole('button', { name: 'ID Service Please select an option' }).click()
// cy.findByRole('option', { name: 'Wikidata' }).click({ force: true })
// cy.get('input[name="externalIds[0].identifier"]').type('abcdef')
// cy.findByRole('button', { name: 'Add External ID' }).click()
// cy.findByRole('button', { name: 'ID Service Please select an option' }).click()
// cy.findByRole('option', { name: 'GitHub' }).click({ force: true })
// cy.get('input[name="externalIds[1].identifier"]').type('123')
cy.get('input[name="dateCreated"]').type('2022-01-01')
cy.get('input[name="dateLastUpdated"]').type('2022-02-02')
cy.findByRole('button', { name: 'Role Please select an option' }).click()
cy.findByRole('option', { name: 'Contributor' }).click({ force: true })
cy.findByRole('combobox', { name: 'Name' }).type('{downarrow}{downarrow}{enter}')
cy.findByRole('button', { name: 'Add Actor' }).click()
cy.findByRole('button', { name: 'Role Please select an option' }).click()
cy.findByRole('option', { name: 'Contact' }).click({ force: true })
cy.findAllByRole('combobox', { name: 'Name' })
.last()
.type('{downarrow}{downarrow}{downarrow}{enter}')
// cy.findAllByRole('combobox', { name: 'Concept' })
// .first()
// .type('{downarrow}{downarrow}{enter}')
cy.get('input[name="properties[0].concept.uri"]').type('{downarrow}{downarrow}{enter}')
cy.get('input[name="properties[1].value"]').type('123')
cy.get('input[name="properties[2].concept.uri"]').type('{downarrow}{downarrow}{enter}')
cy.get('input[name="properties[3].concept.uri"]').type('{downarrow}{downarrow}{enter}')
cy.get('input[name="properties[4].value"]').type('http://see-also.com')
cy.get('input[name="properties[5].concept.uri"]').type('{downarrow}{downarrow}{enter}')
cy.get('input[name="properties[6].value"]').type('2022')
cy.intercept({ pathname: '/api/datasets', query: { draft: 'true' }, method: 'POST' }).as(
'create-draft-dataset',
)
cy.findByRole('button', { name: 'Save as draft' }).realClick()
cy.findByRole('status').contains('Successfully saved Dataset draft.')
cy.location('pathname').should('equal', '/dataset/new')
cy.wait('@create-draft-dataset').then((interception) => {
assert.deepEqual(interception.request.body, {
label: 'The label',
version: '2.0',
description: 'The description',
accessibleAt: ['https://first.com', 'https://second.com'],
contributors: [
{ actor: { id: 1 }, role: { code: 'contributor' } },
{ actor: { id: 2 }, role: { code: 'contact' } },
],
properties: [
{
type: { code: 'activity', type: 'concept' },
concept: {
uri: 'http://dcu.gr/ontologies/scholarlyontology/instances/ActivityType-Collecting',
},
},
{
type: { code: 'keyword', type: 'string' },
value: '123',
},
{
type: { code: 'language', type: 'concept' },
concept: { uri: 'http://iso639-3.sil.org/code/eng' },
},
{
type: { code: 'object-format', type: 'concept' },
concept: { uri: 'http://www.iana.org/assignments/media-types/image/tiff' },
},
{
type: { code: 'see-also', type: 'url' },
value: 'http://see-also.com',
},
{
type: { code: 'license', type: 'concept' },
concept: { uri: 'http://spdx.org/licenses/Entessa' },
},
{
type: { code: 'year', type: 'int' },
value: '2022',
},
],
// FIXME: backend throws 500 when updating external ids:
// @see https://gitlab.gwdg.de/sshoc/sshoc-marketplace-backend/-/issues/166
externalIds: [],
// externalIds: [
// { identifier: 'abcdef', identifierService: { code: 'Wikidata' } },
// { identifier: '123', identifierService: { code: 'GitHub' } },
// ],
relatedItems: [],
dateCreated: '2022-01-01T00:00:00.000Z',
dateLastUpdated: '2022-02-02T00:00:00.000Z',
})
})
cy.intercept({ pathname: '/api/datasets/*', query: { draft: 'true' }, method: 'PUT' }).as(
'update-draft-dataset',
)
cy.get('input[name="label"]').clear().type('The updated label')
cy.findByRole('button', { name: 'Save as draft' }).realClick()
cy.findAllByRole('status').last().contains('Successfully saved Dataset draft.')
cy.location('pathname').should('equal', '/dataset/new')
cy.wait('@update-draft-dataset').then((interception) => {
assert.equal(interception.request.body.label, 'The updated label')
})
cy.findAllByRole('button', { name: 'Remove Property' }).first().realClick()
cy.findAllByRole('button', { name: 'Add Property' }).realClick()
cy.findByRole('button', { name: 'Property type Please select an option' }).click()
cy.findByRole('option', { name: 'Activity' }).click({ force: true })
cy.get('input[name="properties[6].concept.uri"]').type('{downarrow}{downarrow}{enter}')
cy.intercept({ pathname: '/api/datasets/*/commit', method: 'POST' }).as(
'commit-draft-dataset',
)
cy.findByRole('button', { name: 'Submit' }).realClick()
cy.findAllByRole('status').last().contains('Successfully suggested new Dataset.')
cy.location('pathname').should('equal', '/success')
cy.wait('@update-draft-dataset').then((interception) => {
assert.deepEqual(interception.request.body, {
label: 'The updated label',
version: '2.0',
description: 'The description',
accessibleAt: ['https://first.com', 'https://second.com'],
contributors: [
{ actor: { id: 1 }, role: { code: 'contributor' } },
{ actor: { id: 2 }, role: { code: 'contact' } },
],
properties: [
{
type: { code: 'keyword', type: 'string' },
value: '123',
},
{
type: { code: 'language', type: 'concept' },
concept: { uri: 'http://iso639-3.sil.org/code/eng' },
},
{
type: { code: 'object-format', type: 'concept' },
concept: { uri: 'http://www.iana.org/assignments/media-types/image/tiff' },
},
{
type: { code: 'see-also', type: 'url' },
value: 'http://see-also.com',
},
{
type: { code: 'license', type: 'concept' },
concept: { uri: 'http://spdx.org/licenses/Entessa' },
},
{
type: { code: 'year', type: 'int' },
value: '2022',
},
{
type: { code: 'activity', type: 'concept' },
concept: {
uri: 'http://dcu.gr/ontologies/scholarlyontology/instances/ActivityType-Collecting',
},
},
],
// FIXME: backend throws 500 when updating external ids:
// @see https://gitlab.gwdg.de/sshoc/sshoc-marketplace-backend/-/issues/166
externalIds: [],
// externalIds: [
// { identifier: 'abcdef', identifierService: { code: 'Wikidata' } },
// { identifier: '123', identifierService: { code: 'GitHub' } },
// ],
relatedItems: [],
dateCreated: '2022-01-01T00:00:00.000Z',
dateLastUpdated: '2022-02-02T00:00:00.000Z',
})
})
cy.wait('@commit-draft-dataset')
})
})
describe('when authenticated as administrator', () => {
beforeEach(() => {
cy.request('POST', 'http://localhost:8080/api/auth/sign-in', {
username: 'Administrator',
password: 'q1w2e3r4t5',
}).then((response) => {
cy.window().then((window) => {
const token = response.headers['authorization']
if (typeof token === 'string') {
window.localStorage.setItem('__sshoc_token__', token)
}
})
})
cy.visit('/dataset/new')
})
it.only('should display field error message when property value has invalid type', () => {
cy.findByRole('button', { name: 'Add Property' }).click()
// "processed-at" date
// "deprecated-at-source" boolean
// "see-also" url
// "year" int
// "model-version" float
it('should submit form when required fields are not empty on submit, and redirect to item detail page', () => {
cy.get('input[name="label"').focus().type('The label').blur()
cy.get('textarea[name="description"]').focus().type('The description').blur()
cy.findByRole('button', { name: 'Publish' }).realClick()
cy.findByRole('status').contains('Successfully created new Dataset.')
cy.location('pathname').should('have.string', '/dataset')
})
})
})
export {}
describe('Form field validation error messages', () => {
beforeEach(() => {
cy.request('POST', 'http://localhost:8080/api/auth/sign-in', {
username: 'Administrator',
password: 'q1w2e3r4t5',
}).then((response) => {
cy.window().then((window) => {
const token = response.headers['authorization']
if (typeof token === 'string') {
window.localStorage.setItem('__sshoc_token__', token)
}
})
})
cy.visit('/dataset/new')
})
it('should display field error message when "accessible at" field is empty', () => {
cy.get('input[name="accessibleAt[0]"]').focus().blur()
cy.findByText('Field must not be empty.').should('be.visible')
cy.get('input[name="accessibleAt[0]"]').should('have.attr', 'aria-invalid', 'true')
})
it('should display field error message when "accessible at" field has invalid url', () => {
cy.get('input[name="accessibleAt[0]"]').focus().type('invalid-url').blur()
cy.findByText('Please provide a valid URL.').should('be.visible')
cy.get('input[name="accessibleAt[0]"]').should('have.attr', 'aria-invalid', 'true')
})
it('should display field error message when "external id" fields are empty', () => {
cy.findByLabelText('ID Service', { selector: 'button' }).focus().blur()
cy.findByLabelText('Identifier').focus().blur()
cy.findAllByText('Field must not be empty.').should('have.length', 2)
// cy.get('select[name="externalIds[0].identifierService.code"]').should(
// 'have.attr',
// 'aria-invalid',
// 'true',
// )
cy.get('input[name="externalIds[0].identifier"]').should('have.attr', 'aria-invalid', 'true')
})
it('should display field error message when "contributor" fields are empty', () => {
cy.findByLabelText('Role', { selector: 'button' }).focus().blur()
cy.get('input[name="contributors[0].actor.id"]').focus().blur()
cy.findAllByText('Field must not be empty.').should('have.length', 2)
// cy.get('select[name="contributors[0].role.code"]').should('have.attr', 'aria-invalid', 'true')
cy.get('input[name="contributors[0].actor.id"]').should('have.attr', 'aria-invalid', 'true')
})
it('should display field error message when "related item" fields are empty', () => {
cy.findByLabelText('Relation type', { selector: 'button' }).focus().blur()
cy.get('input[name="relatedItems[0].persistentId"]').focus().blur()
cy.findAllByText('Field must not be empty.').should('have.length', 2)
// cy.get('select[name="relatedItems[0].relation.code"]').should(
// 'have.attr',
// 'aria-invalid',
// 'true',
// )
cy.get('input[name="relatedItems[0].persistentId"]').should('have.attr', 'aria-invalid', 'true')
})
it('should display form submit errors', () => {
// client-side we e.g. don't check for duplicate external id entries, so this will produce
// a validation error server-side.
cy.get('input[name="label"]').type('The label')
cy.get('textarea[name="description"]').type('The description')
cy.findByRole('button', { name: 'ID Service Please select an option' }).click()
cy.findByRole('option', { name: 'GitHub' }).click({ force: true })
cy.get('input[name="externalIds[0].identifier"]').type('123')
cy.findByRole('button', { name: 'Add External ID' }).click()
cy.findByRole('button', { name: 'ID Service Please select an option' }).click()
cy.findByRole('option', { name: 'GitHub' }).click({ force: true })
cy.get('input[name="externalIds[1].identifier"]').type('123')
cy.findByRole('button', { name: 'Publish' }).realClick()
cy.contains("Last submission failed: Duplicate item's external id: 123 (from: GitHub)")
})
it('should display field error message when property value has invalid type', () => {
cy.window().then((window) => {
cy.request({
method: 'POST',
url: 'http://localhost:8080/api/property-types',
headers: { authorization: window.localStorage.getItem('__sshoc_token__') },
body: {
label: '_Test int',
code: '_test-int',
type: 'int',
},
failOnStatusCode: false,
})
})
cy.window().then((window) => {
cy.request({
method: 'POST',
url: 'http://localhost:8080/api/property-types',
headers: { authorization: window.localStorage.getItem('__sshoc_token__') },
body: {
label: '_Test float',
code: '_test-float',
type: 'float',
},
failOnStatusCode: false,
})
})
cy.window().then((window) => {
cy.request({
method: 'POST',
url: 'http://localhost:8080/api/property-types',
headers: { authorization: window.localStorage.getItem('__sshoc_token__') },
body: {
label: '_Test date',
code: '_test-date',
type: 'date',
},
failOnStatusCode: false,
})
})
cy.window().then((window) => {
cy.request({
method: 'POST',
url: 'http://localhost:8080/api/property-types',
headers: { authorization: window.localStorage.getItem('__sshoc_token__') },
body: {
label: '_Test boolean',
code: '_test-boolean',
type: 'boolean',
},
failOnStatusCode: false,
})
})
cy.window().then((window) => {
cy.request({
method: 'POST',
url: 'http://localhost:8080/api/property-types',
headers: { authorization: window.localStorage.getItem('__sshoc_token__') },
body: {
label: '_Test url',
code: '_test-url',
type: 'url',
},
failOnStatusCode: false,
})
})
cy.findByRole('button', { name: 'Add Property' }).realClick()
cy.findByRole('button', { name: 'Property type Please select an option' }).click()
cy.findByRole('option', { name: '_Test int' }).click({ force: true })
cy.get('input[name="properties[7].value"]').type('abc')
cy.findByRole('button', { name: 'Add Property' }).realClick()
cy.findByRole('button', { name: 'Property type Please select an option' }).click()
cy.findByRole('option', { name: '_Test float' }).click({ force: true })
cy.get('input[name="properties[8].value"]').type('abc')
cy.findByRole('button', { name: 'Add Property' }).realClick()
cy.findByRole('button', { name: 'Property type Please select an option' }).click()
cy.findByRole('option', { name: '_Test date' }).click({ force: true })
cy.get('input[name="properties[9].value"]').type('abc')
cy.findByRole('button', { name: 'Add Property' }).realClick()
cy.findByRole('button', { name: 'Property type Please select an option' }).click()
cy.findByRole('option', { name: '_Test boolean' }).click({ force: true })
cy.get('input[name="properties[10].value"]').type('123')
cy.findByRole('button', { name: 'Add Property' }).realClick()
cy.findByRole('button', { name: 'Property type Please select an option' }).click()
cy.findByRole('option', { name: '_Test url' }).click({ force: true })
cy.get('input[name="properties[11].value"]').type('123')
cy.findByRole('button', { name: 'Add Property' }).realClick()
cy.get('[data-validation-state="invalid"]').contains('Invalid value. Expected: Number.')
cy.get('[data-validation-state="invalid"]').contains('Invalid value. Expected: Date.')
cy.get('[data-validation-state="invalid"]').contains('Invalid value. Expected: true or false.')
cy.get('[data-validation-state="invalid"]').contains('Invalid value. Expected: URL.')
cy.get('input[name="properties[7].value"]').clear().type('123')
cy.get('input[name="properties[8].value"]').clear().type('123')
cy.get('input[name="properties[9].value"]').clear().type('2022-01-01')
cy.get('input[name="properties[10].value"]').clear().type('TRUE')
cy.get('input[name="properties[11].value"]').clear().type('http://example.com')
cy.get('input[name="label"]').type('The label')
cy.get('textarea[name="description"]').type('The description')
cy.findByRole('button', { name: 'Publish' }).realClick()
cy.findByRole('status').contains('Successfully created new Dataset.')
cy.location('pathname').should('have.string', '/dataset')
})
})
This diff is collapsed.
import '@testing-library/cypress/add-commands'
import 'cypress-real-events/support'
// Cypress.Commands.add('attached', { prevSubject: 'element' }, (subject) => {
// cy.wrap(subject).should((el) => {
// expect(Cypress.dom.isDetached(el)).to.be.false
// // el.click()
// return el
// })
// })
......@@ -11,6 +11,7 @@ import { useDatasetFormFields } from '@/components/item-form/useDatasetFormField
import { useDatasetFormRecommendedFields } from '@/components/item-form/useDatasetFormRecommendedFields'
import { useDatasetValidationSchema } from '@/components/item-form/useDatasetValidationSchema'
import type { DatasetInput } from '@/data/sshoc/api/dataset'
import { getApiErrorMessage } from '@/data/sshoc/utils/get-api-error-message'
import { routes } from '@/lib/core/navigation/routes'
export type CreateDatasetFormValues = ItemFormValues<DatasetInput>
......@@ -56,7 +57,10 @@ export function DatasetCreateForm(): JSX.Element {
done?.()
},
onError(error) {
done?.({ [FORM_ERROR]: String(error) })