mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge pull request #1513 from tdonohue/more_e2e_tests
Enhance e2e test infrastructure. Add initial tests for authenticated actions like submission, mydspace, etc.
This commit is contained in:
32
README.md
32
README.md
@@ -208,7 +208,7 @@ After you have installed all dependencies you can now run the app. Run `yarn run
|
||||
|
||||
When building for production we're using Ahead of Time (AoT) compilation. With AoT, the browser downloads a pre-compiled version of the application, so it can render the application immediately, without waiting to compile the app first. The compiler is roughly half the size of Angular itself, so omitting it dramatically reduces the application payload.
|
||||
|
||||
To build the app for production and start the server run:
|
||||
To build the app for production and start the server (in one command) run:
|
||||
|
||||
```bash
|
||||
yarn start
|
||||
@@ -222,6 +222,10 @@ yarn run build:prod
|
||||
```
|
||||
This will build the application and put the result in the `dist` folder. You can copy this folder to wherever you need it for your application server. If you will be using the built-in Express server, you'll also need a copy of the `node_modules` folder tucked inside your copy of `dist`.
|
||||
|
||||
After building the app for production, it can be started by running:
|
||||
```bash
|
||||
yarn run serve:ssr
|
||||
```
|
||||
|
||||
### Running the application with Docker
|
||||
NOTE: At this time, we do not have production-ready Docker images for DSpace.
|
||||
@@ -283,11 +287,29 @@ E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Con
|
||||
|
||||
The test files can be found in the `./cypress/integration/` folder.
|
||||
|
||||
Before you can run e2e tests, two things are required:
|
||||
1. You MUST have a running backend (i.e. REST API). By default, the e2e tests look for this at http://localhost:8080/server/ or whatever `rest` backend is defined in your `config.prod.yml` or `config.yml`. You may override this using env variables, see [Configuring](#configuring).
|
||||
2. Your backend MUST include our Entities Test Data set. Some tests run against a (currently hardcoded) Community/Collection/Item UUID. These UUIDs are all valid for our Entities Test Data set. The Entities Test Data set may be installed easily via Docker, see https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker-compose#ingest-option-2-ingest-entities-test-data
|
||||
Before you can run e2e tests, two things are REQUIRED:
|
||||
1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo REST API (https://api7.dspace.org/server/), as that server is uncontrolled and may have content added/removed at any time.
|
||||
* After starting up your backend on localhost, make sure either your `config.prod.yml` or `config.dev.yml` has its `rest` settings defined to use that localhost backend.
|
||||
* If you'd prefer, you may instead use environment variables as described at [Configuring](#configuring). For example:
|
||||
```
|
||||
DSPACE_REST_SSL = false
|
||||
DSPACE_REST_HOST = localhost
|
||||
DSPACE_REST_PORT = 8080
|
||||
```
|
||||
2. Your backend MUST include our [Entities Test Data set](https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data). Some tests run against a specific Community/Collection/Item UUID. These UUIDs are all valid for our Entities Test Data set.
|
||||
* (Recommended) The Entities Test Data set may be installed easily via Docker, see https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker-compose#ingest-option-2-ingest-entities-test-data
|
||||
* Alternatively, the Entities Test Data set may be installed via a simple SQL import (e. g. `psql -U dspace < dspace7-entities-data.sql`). See instructions in link above.
|
||||
|
||||
Run `ng e2e` to kick off the tests. This will start Cypress and allow you to select the browser you wish to use, as well as whether you wish to run all tests or an individual test file. Once you click run on test(s), this opens the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) to run your test(s) and show you the results.
|
||||
After performing the above setup, you can run the e2e tests using
|
||||
```
|
||||
ng e2e
|
||||
````
|
||||
NOTE: By default these tests will run against the REST API backend configured via environment variables or in `config.prod.yml`. If you'd rather it use `config.dev.yml`, just set the NODE_ENV environment variable like this:
|
||||
```
|
||||
NODE_ENV=development ng e2e
|
||||
```
|
||||
|
||||
The `ng e2e` command will start Cypress and allow you to select the browser you wish to use, as well as whether you wish to run all tests or an individual test file. Once you click run on test(s), this opens the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) to run your test(s) and show you the results.
|
||||
|
||||
#### Writing E2E Tests
|
||||
|
||||
|
17
cypress.json
17
cypress.json
@@ -6,5 +6,20 @@
|
||||
"pluginsFile": "cypress/plugins/index.ts",
|
||||
"fixturesFolder": "cypress/fixtures",
|
||||
"baseUrl": "http://localhost:4000",
|
||||
"retries": 2
|
||||
"retries": {
|
||||
"runMode": 2,
|
||||
"openMode": 0
|
||||
},
|
||||
"env": {
|
||||
"DSPACE_TEST_ADMIN_USER": "dspacedemo+admin@gmail.com",
|
||||
"DSPACE_TEST_ADMIN_PASSWORD": "dspace",
|
||||
"DSPACE_TEST_COMMUNITY": "0958c910-2037-42a9-81c7-dca80e3892b4",
|
||||
"DSPACE_TEST_COLLECTION": "282164f5-d325-4740-8dd1-fa4d6d3e7200",
|
||||
"DSPACE_TEST_ENTITY_PUBLICATION": "e98b0f27-5c19-49a0-960d-eb6ad5287067",
|
||||
"DSPACE_TEST_SEARCH_TERM": "test",
|
||||
"DSPACE_TEST_SUBMIT_COLLECTION_NAME": "Sample Collection",
|
||||
"DSPACE_TEST_SUBMIT_COLLECTION_UUID": "9d8334e9-25d3-4a67-9cea-3dffdef80144",
|
||||
"DSPACE_TEST_SUBMIT_USER": "dspacedemo+submit@gmail.com",
|
||||
"DSPACE_TEST_SUBMIT_USER_PASSWORD": "dspace"
|
||||
}
|
||||
}
|
@@ -16,8 +16,8 @@ describe('Homepage', () => {
|
||||
|
||||
it('should have a working search box', () => {
|
||||
const queryString = 'test';
|
||||
cy.get('ds-search-form input[name="query"]').type(queryString);
|
||||
cy.get('ds-search-form button.search-button').click();
|
||||
cy.get('[data-test="search-box"]').type(queryString);
|
||||
cy.get('[data-test="search-button"]').click();
|
||||
cy.url().should('include', '/search');
|
||||
cy.url().should('include', 'query=' + encodeURI(queryString));
|
||||
});
|
||||
|
126
cypress/integration/login-modal.spec.ts
Normal file
126
cypress/integration/login-modal.spec.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support';
|
||||
|
||||
const page = {
|
||||
openLoginMenu() {
|
||||
// Click the "Log In" dropdown menu in header
|
||||
cy.get('ds-themed-navbar [data-test="login-menu"]').click();
|
||||
},
|
||||
openUserMenu() {
|
||||
// Once logged in, click the User menu in header
|
||||
cy.get('ds-themed-navbar [data-test="user-menu"]').click();
|
||||
},
|
||||
submitLoginAndPasswordByPressingButton(email, password) {
|
||||
// Enter email
|
||||
cy.get('ds-themed-navbar [data-test="email"]').type(email);
|
||||
// Enter password
|
||||
cy.get('ds-themed-navbar [data-test="password"]').type(password);
|
||||
// Click login button
|
||||
cy.get('ds-themed-navbar [data-test="login-button"]').click();
|
||||
},
|
||||
submitLoginAndPasswordByPressingEnter(email, password) {
|
||||
// In opened Login modal, fill out email & password, then click Enter
|
||||
cy.get('ds-themed-navbar [data-test="email"]').type(email);
|
||||
cy.get('ds-themed-navbar [data-test="password"]').type(password);
|
||||
cy.get('ds-themed-navbar [data-test="password"]').type('{enter}');
|
||||
},
|
||||
submitLogoutByPressingButton() {
|
||||
// This is the POST command that will actually log us out
|
||||
cy.intercept('POST', '/server/api/authn/logout').as('logout');
|
||||
// Click logout button
|
||||
cy.get('ds-themed-navbar [data-test="logout-button"]').click();
|
||||
// Wait until above POST command responds before continuing
|
||||
// (This ensures next action waits until logout completes)
|
||||
cy.wait('@logout');
|
||||
}
|
||||
};
|
||||
|
||||
describe('Login Modal', () => {
|
||||
it('should login when clicking button & stay on same page', () => {
|
||||
const ENTITYPAGE = '/entities/publication/' + TEST_ENTITY_PUBLICATION;
|
||||
cy.visit(ENTITYPAGE);
|
||||
|
||||
// Login menu should exist
|
||||
cy.get('ds-log-in').should('exist');
|
||||
|
||||
// Login, and the <ds-log-in> tag should no longer exist
|
||||
page.openLoginMenu();
|
||||
cy.get('.form-login').should('be.visible');
|
||||
|
||||
page.submitLoginAndPasswordByPressingButton(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD);
|
||||
cy.get('ds-log-in').should('not.exist');
|
||||
|
||||
// Verify we are still on the same page
|
||||
cy.url().should('include', ENTITYPAGE);
|
||||
|
||||
// Open user menu, verify user menu & logout button now available
|
||||
page.openUserMenu();
|
||||
cy.get('ds-user-menu').should('be.visible');
|
||||
cy.get('ds-log-out').should('be.visible');
|
||||
});
|
||||
|
||||
it('should login when clicking enter key & stay on same page', () => {
|
||||
cy.visit('/home');
|
||||
|
||||
// Open login menu in header & verify <ds-log-in> tag is visible
|
||||
page.openLoginMenu();
|
||||
cy.get('.form-login').should('be.visible');
|
||||
|
||||
// Login, and the <ds-log-in> tag should no longer exist
|
||||
page.submitLoginAndPasswordByPressingEnter(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD);
|
||||
cy.get('.form-login').should('not.exist');
|
||||
|
||||
// Verify we are still on homepage
|
||||
cy.url().should('include', '/home');
|
||||
|
||||
// Open user menu, verify user menu & logout button now available
|
||||
page.openUserMenu();
|
||||
cy.get('ds-user-menu').should('be.visible');
|
||||
cy.get('ds-log-out').should('be.visible');
|
||||
});
|
||||
|
||||
it('should support logout', () => {
|
||||
// First authenticate & access homepage
|
||||
cy.login(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD);
|
||||
cy.visit('/');
|
||||
|
||||
// Verify ds-log-in tag doesn't exist, but ds-log-out tag does exist
|
||||
cy.get('ds-log-in').should('not.exist');
|
||||
cy.get('ds-log-out').should('exist');
|
||||
|
||||
// Click logout button
|
||||
page.openUserMenu();
|
||||
page.submitLogoutByPressingButton();
|
||||
|
||||
// Verify ds-log-in tag now exists
|
||||
cy.get('ds-log-in').should('exist');
|
||||
cy.get('ds-log-out').should('not.exist');
|
||||
});
|
||||
|
||||
it('should allow new user registration', () => {
|
||||
cy.visit('/');
|
||||
|
||||
page.openLoginMenu();
|
||||
|
||||
// Registration link should be visible
|
||||
cy.get('ds-themed-navbar [data-test="register"]').should('be.visible');
|
||||
|
||||
// Click registration link & you should go to registration page
|
||||
cy.get('ds-themed-navbar [data-test="register"]').click();
|
||||
cy.location('pathname').should('eq', '/register');
|
||||
cy.get('ds-register-email').should('exist');
|
||||
});
|
||||
|
||||
it('should allow forgot password', () => {
|
||||
cy.visit('/');
|
||||
|
||||
page.openLoginMenu();
|
||||
|
||||
// Forgot password link should be visible
|
||||
cy.get('ds-themed-navbar [data-test="forgot"]').should('be.visible');
|
||||
|
||||
// Click link & you should go to Forgot Password page
|
||||
cy.get('ds-themed-navbar [data-test="forgot"]').click();
|
||||
cy.location('pathname').should('eq', '/forgot');
|
||||
cy.get('ds-forgot-email').should('exist');
|
||||
});
|
||||
});
|
149
cypress/integration/my-dspace.spec.ts
Normal file
149
cypress/integration/my-dspace.spec.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { Options } from 'cypress-axe';
|
||||
import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME } from 'cypress/support';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('My DSpace page', () => {
|
||||
it('should display recent submissions and pass accessibility tests', () => {
|
||||
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||
|
||||
cy.visit('/mydspace');
|
||||
|
||||
cy.get('ds-my-dspace-page').should('exist');
|
||||
|
||||
// At least one recent submission should be displayed
|
||||
cy.get('[data-test="list-object"]').should('be.visible');
|
||||
|
||||
// Click each filter toggle to open *every* filter
|
||||
// (As we want to scan filter section for accessibility issues as well)
|
||||
cy.get('.filter-toggle').click({ multiple: true });
|
||||
|
||||
// Analyze <ds-my-dspace-page> for accessibility issues
|
||||
testA11y(
|
||||
{
|
||||
include: ['ds-my-dspace-page'],
|
||||
exclude: [
|
||||
['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175
|
||||
],
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
// Search filters fail these two "moderate" impact rules
|
||||
'heading-order': { enabled: false },
|
||||
'landmark-unique': { enabled: false }
|
||||
}
|
||||
} as Options
|
||||
);
|
||||
});
|
||||
|
||||
it('should have a working detailed view that passes accessibility tests', () => {
|
||||
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||
|
||||
cy.visit('/mydspace');
|
||||
|
||||
cy.get('ds-my-dspace-page').should('exist');
|
||||
|
||||
// Click button in sidebar to display detailed view
|
||||
cy.get('ds-search-sidebar [data-test="detail-view"]').click();
|
||||
|
||||
cy.get('ds-object-detail').should('exist');
|
||||
|
||||
// Analyze <ds-search-page> for accessibility issues
|
||||
testA11y('ds-my-dspace-page',
|
||||
{
|
||||
rules: {
|
||||
// Search filters fail these two "moderate" impact rules
|
||||
'heading-order': { enabled: false },
|
||||
'landmark-unique': { enabled: false }
|
||||
}
|
||||
} as Options
|
||||
);
|
||||
});
|
||||
|
||||
// NOTE: Deleting existing submissions is exercised by submission.spec.ts
|
||||
it('should let you start a new submission & edit in-progress submissions', () => {
|
||||
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||
cy.visit('/mydspace');
|
||||
|
||||
// Open the New Submission dropdown
|
||||
cy.get('#dropdownSubmission').click();
|
||||
// Click on the "Item" type in that dropdown
|
||||
cy.get('#entityControlsDropdownMenu button[title="none"]').click();
|
||||
|
||||
// This should display the <ds-create-item-parent-selector> (popup window)
|
||||
cy.get('ds-create-item-parent-selector').should('be.visible');
|
||||
|
||||
// Type in a known Collection name in the search box
|
||||
cy.get('ds-authorized-collection-selector input[type="search"]').type(TEST_SUBMIT_COLLECTION_NAME);
|
||||
|
||||
// Click on the button matching that known Collection name
|
||||
cy.get('ds-authorized-collection-selector button[title="' + TEST_SUBMIT_COLLECTION_NAME + '"]').click();
|
||||
|
||||
// New URL should include /workspaceitems, as we've started a new submission
|
||||
cy.url().should('include', '/workspaceitems');
|
||||
|
||||
// The Submission edit form tag should be visible
|
||||
cy.get('ds-submission-edit').should('be.visible');
|
||||
|
||||
// A Collection menu button should exist & its value should be the selected collection
|
||||
cy.get('#collectionControlsMenuButton span').should('have.text', TEST_SUBMIT_COLLECTION_NAME);
|
||||
|
||||
// Now that we've created a submission, we'll test that we can go back and Edit it.
|
||||
// Get our Submission URL, to parse out the ID of this new submission
|
||||
cy.location().then(fullUrl => {
|
||||
// This will be the full path (/workspaceitems/[id]/edit)
|
||||
const path = fullUrl.pathname;
|
||||
// Split on the slashes
|
||||
const subpaths = path.split('/');
|
||||
// Part 2 will be the [id] of the submission
|
||||
const id = subpaths[2];
|
||||
|
||||
// Click the "Save for Later" button to save this submission
|
||||
cy.get('button#saveForLater').click();
|
||||
|
||||
// "Save for Later" should send us to MyDSpace
|
||||
cy.url().should('include', '/mydspace');
|
||||
|
||||
// Close any open notifications, to make sure they don't get in the way of next steps
|
||||
cy.get('[data-dismiss="alert"]').click({multiple: true});
|
||||
|
||||
// This is the GET command that will actually run the search
|
||||
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
||||
// On MyDSpace, find the submission we just created via its ID
|
||||
cy.get('[data-test="search-box"]').type(id);
|
||||
cy.get('[data-test="search-button"]').click();
|
||||
|
||||
// Wait for search results to come back from the above GET command
|
||||
cy.wait('@search-results');
|
||||
|
||||
// Click the Edit button for this in-progress submission
|
||||
cy.get('#edit_' + id).click();
|
||||
|
||||
// Should send us back to the submission form
|
||||
cy.url().should('include', '/workspaceitems/' + id + '/edit');
|
||||
|
||||
// Discard our new submission by clicking Discard in Submission form & confirming
|
||||
cy.get('button#discard').click();
|
||||
cy.get('button#discard_submit').click();
|
||||
|
||||
// Discarding should send us back to MyDSpace
|
||||
cy.url().should('include', '/mydspace');
|
||||
});
|
||||
});
|
||||
|
||||
it('should let you import from external sources', () => {
|
||||
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||
cy.visit('/mydspace');
|
||||
|
||||
// Open the New Import dropdown
|
||||
cy.get('#dropdownImport').click();
|
||||
// Click on the "Item" type in that dropdown
|
||||
cy.get('#importControlsDropdownMenu button[title="none"]').click();
|
||||
|
||||
// New URL should include /import-external, as we've moved to the import page
|
||||
cy.url().should('include', '/import-external');
|
||||
|
||||
// The external import searchbox should be visible
|
||||
cy.get('ds-submission-import-external-searchbar').should('be.visible');
|
||||
});
|
||||
|
||||
});
|
@@ -1,49 +1,66 @@
|
||||
import { TEST_SEARCH_TERM } from 'cypress/support';
|
||||
|
||||
const page = {
|
||||
fillOutQueryInNavBar(query) {
|
||||
// Click the magnifying glass
|
||||
cy.get('.navbar-container #search-navbar-container form a').click();
|
||||
cy.get('ds-themed-navbar [data-test="header-search-icon"]').click();
|
||||
// Fill out a query in input that appears
|
||||
cy.get('.navbar-container #search-navbar-container form input[name = "query"]').type(query);
|
||||
cy.get('ds-themed-navbar [data-test="header-search-box"]').type(query);
|
||||
},
|
||||
submitQueryByPressingEnter() {
|
||||
cy.get('.navbar-container #search-navbar-container form input[name = "query"]').type('{enter}');
|
||||
cy.get('ds-themed-navbar [data-test="header-search-box"]').type('{enter}');
|
||||
},
|
||||
submitQueryByPressingIcon() {
|
||||
cy.get('.navbar-container #search-navbar-container form .submit-icon').click();
|
||||
cy.get('ds-themed-navbar [data-test="header-search-icon"]').click();
|
||||
}
|
||||
};
|
||||
|
||||
describe('Search from Navigation Bar', () => {
|
||||
// NOTE: these tests currently assume this query will return results!
|
||||
const query = 'test';
|
||||
const query = TEST_SEARCH_TERM;
|
||||
|
||||
it('should go to search page with correct query if submitted (from home)', () => {
|
||||
cy.visit('/');
|
||||
// This is the GET command that will actually run the search
|
||||
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
||||
// Run the search
|
||||
page.fillOutQueryInNavBar(query);
|
||||
page.submitQueryByPressingEnter();
|
||||
// New URL should include query param
|
||||
cy.url().should('include', 'query=' + query);
|
||||
// Wait for search results to come back from the above GET command
|
||||
cy.wait('@search-results');
|
||||
// At least one search result should be displayed
|
||||
cy.get('ds-item-search-result-list-element').should('be.visible');
|
||||
cy.get('[data-test="list-object"]').should('be.visible');
|
||||
});
|
||||
|
||||
it('should go to search page with correct query if submitted (from search)', () => {
|
||||
cy.visit('/search');
|
||||
// This is the GET command that will actually run the search
|
||||
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
||||
// Run the search
|
||||
page.fillOutQueryInNavBar(query);
|
||||
page.submitQueryByPressingEnter();
|
||||
// New URL should include query param
|
||||
cy.url().should('include', 'query=' + query);
|
||||
// Wait for search results to come back from the above GET command
|
||||
cy.wait('@search-results');
|
||||
// At least one search result should be displayed
|
||||
cy.get('ds-item-search-result-list-element').should('be.visible');
|
||||
cy.get('[data-test="list-object"]').should('be.visible');
|
||||
});
|
||||
|
||||
it('should allow user to also submit query by clicking icon', () => {
|
||||
cy.visit('/');
|
||||
// This is the GET command that will actually run the search
|
||||
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
||||
// Run the search
|
||||
page.fillOutQueryInNavBar(query);
|
||||
page.submitQueryByPressingIcon();
|
||||
// New URL should include query param
|
||||
cy.url().should('include', 'query=' + query);
|
||||
// Wait for search results to come back from the above GET command
|
||||
cy.wait('@search-results');
|
||||
// At least one search result should be displayed
|
||||
cy.get('ds-item-search-result-list-element').should('be.visible');
|
||||
cy.get('[data-test="list-object"]').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
@@ -1,31 +1,27 @@
|
||||
import { Options } from 'cypress-axe';
|
||||
import { TEST_SEARCH_TERM } from 'cypress/support';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Search Page', () => {
|
||||
// unique ID of the search form (for selecting specific elements below)
|
||||
const SEARCHFORM_ID = '#search-form';
|
||||
|
||||
it('should contain query value when navigating to page with query parameter', () => {
|
||||
const queryString = 'test query';
|
||||
cy.visit('/search?query=' + queryString);
|
||||
cy.get(SEARCHFORM_ID + ' input[name="query"]').should('have.value', queryString);
|
||||
});
|
||||
|
||||
it('should redirect to the correct url when query was set and submit button was triggered', () => {
|
||||
const queryString = 'Another interesting query string';
|
||||
cy.visit('/search');
|
||||
// Type query in searchbox & click search button
|
||||
cy.get(SEARCHFORM_ID + ' input[name="query"]').type(queryString);
|
||||
cy.get(SEARCHFORM_ID + ' button.search-button').click();
|
||||
cy.get('[data-test="search-box"]').type(queryString);
|
||||
cy.get('[data-test="search-button"]').click();
|
||||
cy.url().should('include', 'query=' + encodeURI(queryString));
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/search');
|
||||
it('should load results and pass accessibility tests', () => {
|
||||
cy.visit('/search?query=' + TEST_SEARCH_TERM);
|
||||
cy.get('[data-test="search-box"]').should('have.value', TEST_SEARCH_TERM);
|
||||
|
||||
// <ds-search-page> tag must be loaded
|
||||
cy.get('ds-search-page').should('exist');
|
||||
|
||||
// At least one search result should be displayed
|
||||
cy.get('[data-test="list-object"]').should('be.visible');
|
||||
|
||||
// Click each filter toggle to open *every* filter
|
||||
// (As we want to scan filter section for accessibility issues as well)
|
||||
cy.get('.filter-toggle').click({ multiple: true });
|
||||
@@ -48,16 +44,18 @@ describe('Search Page', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass accessibility tests in Grid view', () => {
|
||||
cy.visit('/search');
|
||||
it('should have a working grid view that passes accessibility tests', () => {
|
||||
cy.visit('/search?query=' + TEST_SEARCH_TERM);
|
||||
|
||||
// Click to display grid view
|
||||
// TODO: These buttons should likely have an easier way to uniquely select
|
||||
cy.get('#search-sidebar-content > ds-view-mode-switch > .btn-group > [href="/search?view=grid"] > .fas').click();
|
||||
// Click button in sidebar to display grid view
|
||||
cy.get('ds-search-sidebar [data-test="grid-view"]').click();
|
||||
|
||||
// <ds-search-page> tag must be loaded
|
||||
cy.get('ds-search-page').should('exist');
|
||||
|
||||
// At least one grid object (card) should be displayed
|
||||
cy.get('[data-test="grid-object"]').should('be.visible');
|
||||
|
||||
// Analyze <ds-search-page> for accessibility issues
|
||||
testA11y('ds-search-page',
|
||||
{
|
||||
|
134
cypress/integration/submission.spec.ts
Normal file
134
cypress/integration/submission.spec.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { Options } from 'cypress-axe';
|
||||
import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME, TEST_SUBMIT_COLLECTION_UUID } from 'cypress/support';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('New Submission page', () => {
|
||||
// NOTE: We already test that new submissions can be started from MyDSpace in my-dspace.spec.ts
|
||||
|
||||
it('should create a new submission when using /submit path & pass accessibility', () => {
|
||||
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||
|
||||
// Test that calling /submit with collection & entityType will create a new submission
|
||||
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
|
||||
|
||||
// Should redirect to /workspaceitems, as we've started a new submission
|
||||
cy.url().should('include', '/workspaceitems');
|
||||
|
||||
// The Submission edit form tag should be visible
|
||||
cy.get('ds-submission-edit').should('be.visible');
|
||||
|
||||
// A Collection menu button should exist & it's value should be the selected collection
|
||||
cy.get('#collectionControlsMenuButton span').should('have.text', TEST_SUBMIT_COLLECTION_NAME);
|
||||
|
||||
// 4 sections should be visible by default
|
||||
cy.get('div#section_traditionalpageone').should('be.visible');
|
||||
cy.get('div#section_traditionalpagetwo').should('be.visible');
|
||||
cy.get('div#section_upload').should('be.visible');
|
||||
cy.get('div#section_license').should('be.visible');
|
||||
|
||||
// Discard button should work
|
||||
// Clicking it will display a confirmation, which we will confirm with another click
|
||||
cy.get('button#discard').click();
|
||||
cy.get('button#discard_submit').click();
|
||||
});
|
||||
|
||||
it('should block submission & show errors if required fields are missing', () => {
|
||||
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||
|
||||
// Create a new submission
|
||||
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
|
||||
|
||||
// Attempt an immediate deposit without filling out any fields
|
||||
cy.get('button#deposit').click();
|
||||
|
||||
// A warning alert should display.
|
||||
cy.get('ds-notification div.alert-warning').should('be.visible');
|
||||
|
||||
// First section should have an exclamation error in the header
|
||||
// (as it has required fields)
|
||||
cy.get('div#traditionalpageone-header i.fa-exclamation-circle').should('be.visible');
|
||||
|
||||
// Title field should have class "is-invalid" applied, as it's required
|
||||
cy.get('input#dc_title').should('have.class', 'is-invalid');
|
||||
|
||||
// Date Year field should also have "is-valid" class
|
||||
cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid');
|
||||
|
||||
// FINALLY, cleanup after ourselves. This also exercises the MyDSpace delete button.
|
||||
// Get our Submission URL, to parse out the ID of this submission
|
||||
cy.location().then(fullUrl => {
|
||||
// This will be the full path (/workspaceitems/[id]/edit)
|
||||
const path = fullUrl.pathname;
|
||||
// Split on the slashes
|
||||
const subpaths = path.split('/');
|
||||
// Part 2 will be the [id] of the submission
|
||||
const id = subpaths[2];
|
||||
|
||||
// Even though form is incomplete, the "Save for Later" button should still work
|
||||
cy.get('button#saveForLater').click();
|
||||
|
||||
// "Save for Later" should send us to MyDSpace
|
||||
cy.url().should('include', '/mydspace');
|
||||
|
||||
// A success alert should be visible
|
||||
cy.get('ds-notification div.alert-success').should('be.visible');
|
||||
// Now, dismiss any open alert boxes (may be multiple, as tests run quickly)
|
||||
cy.get('[data-dismiss="alert"]').click({multiple: true});
|
||||
|
||||
// This is the GET command that will actually run the search
|
||||
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
||||
// On MyDSpace, find the submission we just saved via its ID
|
||||
cy.get('[data-test="search-box"]').type(id);
|
||||
cy.get('[data-test="search-button"]').click();
|
||||
|
||||
// Wait for search results to come back from the above GET command
|
||||
cy.wait('@search-results');
|
||||
|
||||
// Delete our created submission & confirm deletion
|
||||
cy.get('button#delete_' + id).click();
|
||||
cy.get('button#delete_confirm').click();
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow for deposit if all required fields completed & file uploaded', () => {
|
||||
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||
|
||||
// Create a new submission
|
||||
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
|
||||
|
||||
// Fill out all required fields (Title, Date)
|
||||
cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests');
|
||||
cy.get('input#dc_date_issued_year').type('2022');
|
||||
|
||||
// Confirm the required license by checking checkbox
|
||||
// (NOTE: requires "force:true" cause Cypress claims this checkbox is covered by its own <span>)
|
||||
cy.get('input#granted').check( {force: true} );
|
||||
|
||||
// Before using Cypress drag & drop, we have to manually trigger the "dragover" event.
|
||||
// This ensures our UI displays the dropzone that covers the entire submission page.
|
||||
// (For some reason Cypress drag & drop doesn't trigger this even itself & upload won't work without this trigger)
|
||||
cy.get('ds-uploader').trigger('dragover');
|
||||
|
||||
// This is the POST command that will upload the file
|
||||
cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload');
|
||||
|
||||
// Upload our DSpace logo via drag & drop onto submission form
|
||||
// cy.get('div#section_upload')
|
||||
cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.png', {
|
||||
action: 'drag-drop'
|
||||
});
|
||||
|
||||
// Wait for upload to complete before proceeding
|
||||
cy.wait('@upload');
|
||||
// Close the upload success notice
|
||||
cy.get('[data-dismiss="alert"]').click({multiple: true});
|
||||
|
||||
// Wait for deposit button to not be disabled & click it.
|
||||
cy.get('button#deposit').should('not.be.disabled').click();
|
||||
|
||||
// No warnings should exist. Instead, just successful deposit alert is displayed
|
||||
cy.get('ds-notification div.alert-warning').should('not.exist');
|
||||
cy.get('ds-notification div.alert-success').should('be.visible');
|
||||
});
|
||||
|
||||
});
|
@@ -1,15 +1,34 @@
|
||||
const fs = require('fs');
|
||||
|
||||
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
|
||||
// For more info, visit https://on.cypress.io/plugins-api
|
||||
module.exports = (on, config) => {
|
||||
// Define "log" and "table" tasks, used for logging accessibility errors during CI
|
||||
// Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file
|
||||
on('task', {
|
||||
// Define "log" and "table" tasks, used for logging accessibility errors during CI
|
||||
// Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file
|
||||
log(message: string) {
|
||||
console.log(message);
|
||||
return null;
|
||||
},
|
||||
table(message: string) {
|
||||
console.table(message);
|
||||
return null;
|
||||
},
|
||||
// Cypress doesn't have access to the running application in Node.js.
|
||||
// So, it's not possible to inject or load the AppConfig or environment of the Angular UI.
|
||||
// Instead, we'll read our running application's config.json, which contains the configs &
|
||||
// is regenerated at runtime each time the Angular UI application starts up.
|
||||
readUIConfig() {
|
||||
// Check if we have a config.json in the src/assets. If so, use that.
|
||||
// This is where it's written when running "ng e2e" or "yarn serve"
|
||||
if (fs.existsSync('./src/assets/config.json')) {
|
||||
return fs.readFileSync('./src/assets/config.json', 'utf8');
|
||||
// Otherwise, check the dist/browser/assets
|
||||
// This is where it's written when running "serve:ssr", which is what CI uses to start the frontend
|
||||
} else if (fs.existsSync('./dist/browser/assets/config.json')) {
|
||||
return fs.readFileSync('./dist/browser/assets/config.json', 'utf8');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
@@ -1,43 +1,83 @@
|
||||
// ***********************************************
|
||||
// This example namespace declaration will help
|
||||
// with Intellisense and code completion in your
|
||||
// IDE or Text Editor.
|
||||
// This File is for Custom Cypress commands.
|
||||
// See docs at https://docs.cypress.io/api/cypress-api/custom-commands
|
||||
// ***********************************************
|
||||
// declare namespace Cypress {
|
||||
// interface Chainable<Subject = any> {
|
||||
// customCommand(param: any): typeof customCommand;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// function customCommand(param: any): void {
|
||||
// console.warn(param);
|
||||
// }
|
||||
//
|
||||
// NOTE: You can use it like so:
|
||||
// Cypress.Commands.add('customCommand', customCommand);
|
||||
//
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
|
||||
import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model';
|
||||
import { FALLBACK_TEST_REST_BASE_URL } from '.';
|
||||
|
||||
// Declare Cypress namespace to help with Intellisense & code completion in IDEs
|
||||
// ALL custom commands MUST be listed here for code completion to work
|
||||
// tslint:disable-next-line:no-namespace
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable<Subject = any> {
|
||||
/**
|
||||
* Login to backend before accessing the next page. Ensures that the next
|
||||
* call to "cy.visit()" will be authenticated as this user.
|
||||
* @param email email to login as
|
||||
* @param password password to login as
|
||||
*/
|
||||
login(email: string, password: string): typeof login;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login user via REST API directly, and pass authentication token to UI via
|
||||
* the UI's dsAuthInfo cookie.
|
||||
* @param email email to login as
|
||||
* @param password password to login as
|
||||
*/
|
||||
function login(email: string, password: string): void {
|
||||
// Cypress doesn't have access to the running application in Node.js.
|
||||
// So, it's not possible to inject or load the AppConfig or environment of the Angular UI.
|
||||
// Instead, we'll read our running application's config.json, which contains the configs &
|
||||
// is regenerated at runtime each time the Angular UI application starts up.
|
||||
cy.task('readUIConfig').then((str: string) => {
|
||||
// Parse config into a JSON object
|
||||
const config = JSON.parse(str);
|
||||
|
||||
// Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found.
|
||||
let baseRestUrl = FALLBACK_TEST_REST_BASE_URL;
|
||||
if (!config.rest.baseUrl) {
|
||||
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
|
||||
} else {
|
||||
console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: " + config.rest.baseUrl);
|
||||
baseRestUrl = config.rest.baseUrl;
|
||||
}
|
||||
|
||||
// To login via REST, first we have to do a GET to obtain a valid CSRF token
|
||||
cy.request( baseRestUrl + '/api/authn/status' )
|
||||
.then((response) => {
|
||||
// We should receive a CSRF token returned in a response header
|
||||
expect(response.headers).to.have.property('dspace-xsrf-token');
|
||||
const csrfToken = response.headers['dspace-xsrf-token'];
|
||||
|
||||
// Now, send login POST request including that CSRF token
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: baseRestUrl + '/api/authn/login',
|
||||
headers: { 'X-XSRF-TOKEN' : csrfToken},
|
||||
form: true, // indicates the body should be form urlencoded
|
||||
body: { user: email, password: password }
|
||||
}).then((resp) => {
|
||||
// We expect a successful login
|
||||
expect(resp.status).to.eq(200);
|
||||
// We expect to have a valid authorization header returned (with our auth token)
|
||||
expect(resp.headers).to.have.property('authorization');
|
||||
|
||||
// Initialize our AuthTokenInfo object from the authorization header.
|
||||
const authheader = resp.headers.authorization as string;
|
||||
const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader);
|
||||
|
||||
// Save our AuthTokenInfo object to our dsAuthInfo UI cookie
|
||||
// This ensures the UI will recognize we are logged in on next "visit()"
|
||||
cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
// Add as a Cypress command (i.e. assign to 'cy.login')
|
||||
Cypress.Commands.add('login', login);
|
||||
|
@@ -13,14 +13,51 @@
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// When a command from ./commands is ready to use, import with `import './commands'` syntax
|
||||
// import './commands';
|
||||
// Import all custom Commands (from commands.ts) for all tests
|
||||
import './commands';
|
||||
|
||||
// Import Cypress Axe tools for all tests
|
||||
// https://github.com/component-driven/cypress-axe
|
||||
import 'cypress-axe';
|
||||
|
||||
// Runs once before the first test in each "block"
|
||||
before(() => {
|
||||
// Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie
|
||||
// This just ensures it doesn't get in the way of matching other objects in the page.
|
||||
cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true}');
|
||||
});
|
||||
|
||||
// For better stability between tests, we visit "about:blank" (i.e. blank page) after each test.
|
||||
// This ensures any remaining/outstanding XHR requests are killed, so they don't affect the next test.
|
||||
// Borrowed from: https://glebbahmutov.com/blog/visit-blank-page-between-tests/
|
||||
afterEach(() => {
|
||||
cy.window().then((win) => {
|
||||
win.location.href = 'about:blank';
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Global constants used in tests
|
||||
export const TEST_COLLECTION = '282164f5-d325-4740-8dd1-fa4d6d3e7200';
|
||||
export const TEST_COMMUNITY = '0958c910-2037-42a9-81c7-dca80e3892b4';
|
||||
export const TEST_ENTITY_PUBLICATION = 'e98b0f27-5c19-49a0-960d-eb6ad5287067';
|
||||
// May be overridden in our cypress.json config file using specified environment variables.
|
||||
// Default values listed here are all valid for the Demo Entities Data set available at
|
||||
// https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||
// (This is the data set used in our CI environment)
|
||||
|
||||
// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL
|
||||
// from the Angular UI's config.json. See 'getBaseRESTUrl()' in commands.ts
|
||||
export const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server';
|
||||
|
||||
// Admin account used for administrative tests
|
||||
export const TEST_ADMIN_USER = Cypress.env('DSPACE_TEST_ADMIN_USER') || 'dspacedemo+admin@gmail.com';
|
||||
export const TEST_ADMIN_PASSWORD = Cypress.env('DSPACE_TEST_ADMIN_PASSWORD') || 'dspace';
|
||||
// Community/collection/publication used for view/edit tests
|
||||
export const TEST_COLLECTION = Cypress.env('DSPACE_TEST_COLLECTION') || '282164f5-d325-4740-8dd1-fa4d6d3e7200';
|
||||
export const TEST_COMMUNITY = Cypress.env('DSPACE_TEST_COMMUNITY') || '0958c910-2037-42a9-81c7-dca80e3892b4';
|
||||
export const TEST_ENTITY_PUBLICATION = Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION') || 'e98b0f27-5c19-49a0-960d-eb6ad5287067';
|
||||
// Search term (should return results) used in search tests
|
||||
export const TEST_SEARCH_TERM = Cypress.env('DSPACE_TEST_SEARCH_TERM') || 'test';
|
||||
// Collection used for submission tests
|
||||
export const TEST_SUBMIT_COLLECTION_NAME = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME') || 'Sample Collection';
|
||||
export const TEST_SUBMIT_COLLECTION_UUID = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID') || '9d8334e9-25d3-4a67-9cea-3dffdef80144';
|
||||
export const TEST_SUBMIT_USER = Cypress.env('DSPACE_TEST_SUBMIT_USER') || 'dspacedemo+submit@gmail.com';
|
||||
export const TEST_SUBMIT_USER_PASSWORD = Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD') || 'dspace';
|
||||
|
@@ -35,6 +35,6 @@ services:
|
||||
tar xvfz /tmp/assetstore.tar.gz
|
||||
fi
|
||||
|
||||
/dspace/bin/dspace index-discovery
|
||||
/dspace/bin/dspace index-discovery -b
|
||||
/dspace/bin/dspace oai import
|
||||
/dspace/bin/dspace oai clean-cache
|
||||
|
@@ -20,7 +20,7 @@ services:
|
||||
environment:
|
||||
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace
|
||||
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||
- LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-2021-04-14.sql
|
||||
- LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
|
||||
dspace:
|
||||
### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' ####
|
||||
# Ensure that the database is ready BEFORE starting tomcat
|
||||
|
@@ -63,7 +63,7 @@ services:
|
||||
# This LOADSQL should be kept in sync with the LOADSQL in
|
||||
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
|
||||
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||
LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-2021-04-14.sql
|
||||
LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
|
||||
PGDATA: /pgdata
|
||||
image: dspace/dspace-postgres-pgcrypto:loadsql
|
||||
networks:
|
||||
|
@@ -147,7 +147,7 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "3.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"cypress": "8.6.0",
|
||||
"cypress": "9.5.1",
|
||||
"cypress-axe": "^0.13.0",
|
||||
"debug-loader": "^0.0.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<a *ngIf="linkType != linkTypes.None"
|
||||
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||
class="card-img-top full-width">
|
||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
|
||||
<div>
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
@@ -37,7 +37,7 @@
|
||||
<div *ngIf="linkType != linkTypes.None" class="text-center">
|
||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||
class="lead btn btn-primary viewButton">View</a>
|
||||
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ds-truncatable>
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<a *ngIf="linkType != linkTypes.None"
|
||||
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||
class="card-img-top full-width">
|
||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
|
||||
<div>
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
@@ -37,7 +37,7 @@
|
||||
<div *ngIf="linkType != linkTypes.None" class="text-center">
|
||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||
class="lead btn btn-primary viewButton">View</a>
|
||||
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ds-truncatable>
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<a *ngIf="linkType != linkTypes.None"
|
||||
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||
class="card-img-top full-width">
|
||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
|
||||
<div>
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
@@ -41,7 +41,7 @@
|
||||
<div *ngIf="linkType != linkTypes.None" class="text-center">
|
||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||
class="lead btn btn-primary viewButton">View</a>
|
||||
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ds-truncatable>
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<a *ngIf="linkType != linkTypes.None"
|
||||
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||
class="card-img-top full-width">
|
||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
|
||||
<div>
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
@@ -43,7 +43,7 @@
|
||||
<div *ngIf="linkType != linkTypes.None" class="text-center">
|
||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||
class="lead btn btn-primary viewButton">View</a>
|
||||
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ds-truncatable>
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<a *ngIf="linkType != linkTypes.None"
|
||||
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||
class="card-img-top full-width">
|
||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
|
||||
<div>
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
@@ -37,7 +37,7 @@
|
||||
<div *ngIf="linkType != linkTypes.None" class="text-center">
|
||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||
class="lead btn btn-primary viewButton">View</a>
|
||||
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ds-truncatable>
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<a *ngIf="linkType != linkTypes.None"
|
||||
[target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||
class="card-img-top full-width">
|
||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
|
||||
<div>
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
@@ -31,7 +31,7 @@
|
||||
<div *ngIf="linkType != linkTypes.None" class="text-center">
|
||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'"
|
||||
rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||
class="lead btn btn-primary viewButton">View</a>
|
||||
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ds-truncatable>
|
||||
|
@@ -8,7 +8,7 @@
|
||||
<div class="add w-100" display="dynamic" placement="bottom-right"
|
||||
ngbDropdown
|
||||
*ngIf="(moreThanOne$ | async)">
|
||||
<button class="btn btn-lg btn-outline-primary mt-1 ml-2" id="dropdownSubmission" ngbDropdownToggle
|
||||
<button class="btn btn-lg btn-outline-primary mt-1 ml-2" id="dropdownImport" ngbDropdownToggle
|
||||
type="button" [disabled]="!(initialized$|async)"
|
||||
attr.aria-label="{{'mydspace.new-submission-external' | translate}}"
|
||||
title="{{'mydspace.new-submission-external' | translate}}">
|
||||
@@ -17,8 +17,8 @@
|
||||
</button>
|
||||
<div ngbDropdownMenu
|
||||
class="dropdown-menu"
|
||||
id="entityControlsDropdownMenu"
|
||||
aria-labelledby="dropdownSubmission">
|
||||
id="importControlsDropdownMenu"
|
||||
aria-labelledby="dropdownImport">
|
||||
<ds-entity-dropdown [isSubmission]="false" (selectionChange)="openPage($event)"></ds-entity-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -10,7 +10,7 @@
|
||||
flex: initial;
|
||||
}
|
||||
|
||||
#entityControlsDropdownMenu {
|
||||
#importControlsDropdownMenu {
|
||||
min-width: 18rem;
|
||||
box-shadow: $btn-focus-box-shadow;
|
||||
}
|
||||
|
@@ -7,7 +7,9 @@
|
||||
ngbDropdown
|
||||
*ngIf="(moreThanOne$ | async)">
|
||||
<button class="btn btn-lg btn-primary mt-1 ml-2" id="dropdownSubmission" ngbDropdownToggle
|
||||
type="button" [disabled]="!(initialized$|async)">
|
||||
type="button" [disabled]="!(initialized$|async)"
|
||||
attr.aria-label="{{'mydspace.new-submission' | translate}}"
|
||||
title="{{'mydspace.new-submission' | translate}}">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i>
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
|
@@ -3,8 +3,8 @@
|
||||
<form [formGroup]="searchForm" (ngSubmit)="onSubmit(searchForm.value)" autocomplete="on">
|
||||
<input #searchInput [@toggleAnimation]="isExpanded" [attr.aria-label]="('nav.search' | translate)" name="query"
|
||||
formControlName="query" type="text" placeholder="{{searchExpanded ? ('nav.search' | translate) : ''}}"
|
||||
class="d-inline-block bg-transparent position-absolute form-control dropdown-menu-right p-1">
|
||||
<a class="submit-icon" [routerLink]="" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()">
|
||||
class="d-inline-block bg-transparent position-absolute form-control dropdown-menu-right p-1" data-test="header-search-box">
|
||||
<a class="submit-icon" [routerLink]="" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()" data-test="header-search-icon">
|
||||
<em class="fas fa-search fa-lg fa-fw"></em>
|
||||
</a>
|
||||
</form>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<ul class="navbar-nav" [ngClass]="{'mr-auto': (isXsOrSm$ | async)}">
|
||||
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item"
|
||||
(click)="$event.stopPropagation();">
|
||||
<div ngbDropdown #loginDrop display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||
<div ngbDropdown #loginDrop display="dynamic" placement="bottom-right" class="d-inline-block" data-test="login-menu" @fadeInOut>
|
||||
<a href="javascript:void(0);" class="dropdownLogin px-1 " [attr.aria-label]="'nav.login' |translate" (click)="$event.preventDefault()" ngbDropdownToggle>
|
||||
{{ 'nav.login' | translate }}
|
||||
</a>
|
||||
@@ -17,7 +17,7 @@
|
||||
{{ 'nav.login' | translate }}<span class="sr-only">(current)</span>
|
||||
</a>
|
||||
</li>
|
||||
<li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item">
|
||||
<li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item" data-test="user-menu">
|
||||
<div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
|
||||
<a href="javascript:void(0);" role="button" [attr.aria-label]="'nav.logout' |translate" (click)="$event.preventDefault()" [title]="'nav.logout' | translate" class="px-1" ngbDropdownToggle>
|
||||
<i class="fas fa-user-circle fa-lg fa-fw"></i></a>
|
||||
|
@@ -5,6 +5,7 @@
|
||||
</legend>
|
||||
<ds-number-picker
|
||||
tabindex="1"
|
||||
[id]="model.id + '_year'"
|
||||
[disabled]="model.disabled"
|
||||
[min]="minYear"
|
||||
[max]="maxYear"
|
||||
@@ -21,6 +22,7 @@
|
||||
|
||||
<ds-number-picker
|
||||
tabindex="2"
|
||||
[id]="model.id + '_month'"
|
||||
[min]="minMonth"
|
||||
[max]="maxMonth"
|
||||
[name]="'month'"
|
||||
@@ -36,6 +38,7 @@
|
||||
|
||||
<ds-number-picker
|
||||
tabindex="3"
|
||||
[id]="model.id + '_day'"
|
||||
[min]="minDay"
|
||||
[max]="maxDay"
|
||||
[name]="'day'"
|
||||
|
@@ -8,6 +8,6 @@
|
||||
</ng-container>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" *ngIf="canRegister$ | async" [routerLink]="[getRegisterRoute()]">{{"login.form.new-user" | translate}}</a>
|
||||
<a class="dropdown-item" [routerLink]="[getForgotRoute()]">{{"login.form.forgot-password" | translate}}</a>
|
||||
<a class="dropdown-item" *ngIf="canRegister$ | async" [routerLink]="[getRegisterRoute()]" data-test="register">{{"login.form.new-user" | translate}}</a>
|
||||
<a class="dropdown-item" [routerLink]="[getForgotRoute()]" data-test="forgot">{{"login.form.forgot-password" | translate}}</a>
|
||||
</div>
|
||||
|
@@ -9,7 +9,8 @@
|
||||
formControlName="email"
|
||||
placeholder="{{'login.form.email' | translate}}"
|
||||
required
|
||||
type="email">
|
||||
type="email"
|
||||
data-test="email">
|
||||
<label class="sr-only">{{"login.form.password" | translate}}</label>
|
||||
<input [attr.aria-label]="'login.form.password' |translate"
|
||||
autocomplete="off"
|
||||
@@ -17,12 +18,13 @@
|
||||
placeholder="{{'login.form.password' | translate}}"
|
||||
formControlName="password"
|
||||
required
|
||||
type="password">
|
||||
type="password"
|
||||
data-test="password">
|
||||
<div *ngIf="(error | async) && hasError" class="alert alert-danger" role="alert"
|
||||
@fadeOut>{{ (error | async) | translate }}</div>
|
||||
<div *ngIf="(message | async) && hasMessage" class="alert alert-info" role="alert"
|
||||
@fadeOut>{{ (message | async) | translate }}</div>
|
||||
|
||||
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit"
|
||||
<button class="btn btn-lg btn-primary btn-block mt-3" type="submit" data-test="login-button"
|
||||
[disabled]="!form.valid"><i class="fas fa-sign-in-alt"></i> {{"login.form.submit" | translate}}</button>
|
||||
</form>
|
||||
|
@@ -2,5 +2,5 @@
|
||||
|
||||
<div *ngIf="(error | async)" class="alert alert-danger" role="alert" @fadeOut>{{ error | async }}</div>
|
||||
|
||||
<button class="btn btn-lg btn-primary btn-block mt-3" (click)="logOut()"><i class="fas fa-sign-out-alt"></i> {{"logout.form.submit" | translate}}</button>
|
||||
<button class="btn btn-lg btn-primary btn-block mt-3" (click)="logOut()" data-test="logout-button"><i class="fas fa-sign-out-alt"></i> {{"logout.form.submit" | translate}}</button>
|
||||
</div>
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<a class="btn btn-primary mt-1 mb-3"
|
||||
id="{{'edit_' + object.id}}"
|
||||
ngbTooltip="{{'submission.workflow.generic.edit-help' | translate}}"
|
||||
[routerLink]="['/workspaceitems/' + object.id + '/edit']"
|
||||
role="button">
|
||||
@@ -6,6 +7,7 @@
|
||||
</a>
|
||||
|
||||
<button type="button"
|
||||
id="{{'delete_' + object.id}}"
|
||||
class="btn btn-danger mt-1 mb-3"
|
||||
ngbTooltip="{{'submission.workflow.generic.delete-help' | translate}}"
|
||||
(click)="$event.preventDefault();confirmDiscard(content)">
|
||||
@@ -16,7 +18,7 @@
|
||||
<ng-template #content let-c="close" let-d="dismiss">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title text-danger">{{'submission.general.discard.confirm.title' | translate}}</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="d('cancel')">
|
||||
<button type="button" id="delete_close" class="close" aria-label="Close" (click)="d('cancel')">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -24,7 +26,7 @@
|
||||
<p>{{'submission.general.discard.confirm.info' | translate}}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="c('cancel')">{{'submission.general.discard.confirm.cancel' | translate}}</button>
|
||||
<button type="button" class="btn btn-danger" (click)="c('ok')">{{'submission.general.discard.confirm.submit' | translate}}</button>
|
||||
<button type="button" id="delete_cancel" class="btn btn-secondary" (click)="c('cancel')">{{'submission.general.discard.confirm.cancel' | translate}}</button>
|
||||
<button type="button" id="delete_confirm"class="btn btn-danger" (click)="c('ok')">{{'submission.general.discard.confirm.submit' | translate}}</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@@ -9,6 +9,7 @@
|
||||
<span class="sr-only">Increment</span>
|
||||
</button>
|
||||
<input
|
||||
id="{{id}}"
|
||||
type="text"
|
||||
class="form-control d-inline-block text-center"
|
||||
maxlength="{{size}}"
|
||||
@@ -22,7 +23,7 @@
|
||||
[readonly]="disabled"
|
||||
[disabled]="disabled"
|
||||
[ngClass]="{'is-invalid': invalid}"
|
||||
aria-label="name"
|
||||
title="{{placeholder}}"
|
||||
>
|
||||
<button
|
||||
class="btn btn-link-focus"
|
||||
|
@@ -48,6 +48,7 @@ describe('NumberPickerComponent test suite', () => {
|
||||
[disabled]="disabled"
|
||||
[min]="min"
|
||||
[max]="max"
|
||||
[id]="'ds_test_field'"
|
||||
[name]="'test'"
|
||||
[size]="size"
|
||||
[(ngModel)]="initValue"
|
||||
|
@@ -12,7 +12,7 @@ import { isEmpty } from '../empty.util';
|
||||
})
|
||||
|
||||
export class NumberPickerComponent implements OnInit, ControlValueAccessor {
|
||||
|
||||
@Input() id: string;
|
||||
@Input() step: number;
|
||||
@Input() min: number;
|
||||
@Input() max: number;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="card">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', object.id]" class="card-img-top">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', object.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate">
|
||||
<ds-thumbnail [thumbnail]="(object.logo | async)?.payload" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
</a>
|
||||
@@ -11,7 +11,7 @@
|
||||
<h4 class="card-title">{{object.name}}</h4>
|
||||
<p *ngIf="object.shortDescription" class="card-text">{{object.shortDescription}}</p>
|
||||
<div *ngIf="linkType != linkTypes.None" class="text-center">
|
||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', object.id]" class="lead btn btn-primary viewButton">View</a>
|
||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', object.id]" class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -4,6 +4,7 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { LinkService } from '../../../core/cache/builders/link.service';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
let collectionGridElementComponent: CollectionGridElementComponent;
|
||||
let fixture: ComponentFixture<CollectionGridElementComponent>;
|
||||
@@ -37,6 +38,9 @@ const linkService = jasmine.createSpyObj('linkService', {
|
||||
describe('CollectionGridElementComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [CollectionGridElementComponent],
|
||||
providers: [
|
||||
{ provide: 'objectElementProvider', useValue: (mockCollectionWithAbstract) },
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="card">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', object.id]" class="card-img-top">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', object.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate">
|
||||
<ds-thumbnail [thumbnail]="(object.logo | async)?.payload" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
</a>
|
||||
@@ -11,7 +11,7 @@
|
||||
<h4 class="card-title">{{object.name}}</h4>
|
||||
<p *ngIf="object.shortDescription" class="card-text">{{object.shortDescription}}</p>
|
||||
<div *ngIf="linkType != linkTypes.None" class="text-center">
|
||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', object.id]" class="lead btn btn-primary viewButton">View</a>
|
||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', object.id]" class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -4,6 +4,7 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Community } from '../../../core/shared/community.model';
|
||||
import { LinkService } from '../../../core/cache/builders/link.service';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
let communityGridElementComponent: CommunityGridElementComponent;
|
||||
let fixture: ComponentFixture<CommunityGridElementComponent>;
|
||||
@@ -37,6 +38,9 @@ const linkService = jasmine.createSpyObj('linkService', {
|
||||
describe('CommunityGridElementComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [CommunityGridElementComponent],
|
||||
providers: [
|
||||
{ provide: 'objectElementProvider', useValue: (mockCommunityWithAbstract) },
|
||||
|
@@ -13,7 +13,7 @@
|
||||
(paginationChange)="onPaginationChange($event)">
|
||||
<div class="card-columns row" *ngIf="objects?.hasSucceeded">
|
||||
<div class="card-column col col-sm-6 col-lg-4" *ngFor="let column of (columns$ | async)" @fadeIn>
|
||||
<div class="card-element" *ngFor="let object of column">
|
||||
<div class="card-element" *ngFor="let object of column" data-test="grid-object">
|
||||
<ds-listable-object-component-loader [object]="object" [viewMode]="viewMode" [context]="context" [linkType]="linkType"></ds-listable-object-component-loader>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="card">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', dso.id]" class="card-img-top">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', dso.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate">
|
||||
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
</a>
|
||||
@@ -12,7 +12,7 @@
|
||||
<h4 class="card-title">{{dso.name}}</h4>
|
||||
<p *ngIf="dso.shortDescription" class="card-text">{{dso.shortDescription}}</p>
|
||||
<div *ngIf="linkType != linkTypes.None" class="text-center">
|
||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', dso.id]" class="lead btn btn-primary viewButton">View</a>
|
||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/collections/', dso.id]" class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
|
@@ -20,6 +20,7 @@ import { TruncatePipe } from '../../../utils/truncate.pipe';
|
||||
import { CollectionSearchResultGridElementComponent } from './collection-search-result-grid-element.component';
|
||||
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
|
||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
let collectionSearchResultGridElementComponent: CollectionSearchResultGridElementComponent;
|
||||
let fixture: ComponentFixture<CollectionSearchResultGridElementComponent>;
|
||||
@@ -60,6 +61,9 @@ const linkService = jasmine.createSpyObj('linkService', {
|
||||
describe('CollectionSearchResultGridElementComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [CollectionSearchResultGridElementComponent, TruncatePipe],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: truncatableServiceStub },
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="card">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', dso.id]" class="card-img-top">
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', dso.id]" class="card-img-top" [attr.title]="'search.results.view-result' | translate">
|
||||
<ds-thumbnail [thumbnail]="(dso.logo | async)?.payload" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
</a>
|
||||
@@ -12,7 +12,7 @@
|
||||
<h4 class="card-title">{{dso.name}}</h4>
|
||||
<p *ngIf="dso.shortDescription" class="card-text">{{dso.shortDescription}}</p>
|
||||
<div *ngIf="linkType != linkTypes.None" class="text-center">
|
||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', dso.id]" class="lead btn btn-primary viewButton">View</a>
|
||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/communities/', dso.id]" class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
|
@@ -20,6 +20,7 @@ import { TruncatePipe } from '../../../utils/truncate.pipe';
|
||||
import { CommunitySearchResultGridElementComponent } from './community-search-result-grid-element.component';
|
||||
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
|
||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
let communitySearchResultGridElementComponent: CommunitySearchResultGridElementComponent;
|
||||
let fixture: ComponentFixture<CommunitySearchResultGridElementComponent>;
|
||||
@@ -60,6 +61,9 @@ const linkService = jasmine.createSpyObj('linkService', {
|
||||
describe('CommunitySearchResultGridElementComponent', () => {
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [CommunitySearchResultGridElementComponent, TruncatePipe],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: truncatableServiceStub },
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
<a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||
class="card-img-top full-width">
|
||||
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate">
|
||||
<div>
|
||||
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
|
||||
</ds-thumbnail>
|
||||
@@ -37,7 +37,7 @@
|
||||
</p>
|
||||
<div *ngIf="linkType != linkTypes.None" class="text-center">
|
||||
<a [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="[itemPageRoute]"
|
||||
class="lead btn btn-primary viewButton">View</a>
|
||||
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</ds-truncatable>
|
||||
|
@@ -4,6 +4,7 @@ import { TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../../../../../core/cache/object-cache.service';
|
||||
@@ -99,7 +100,10 @@ export function getEntityGridElementTestComponent(component, searchResultWithMet
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [NoopAnimationsModule],
|
||||
imports: [
|
||||
NoopAnimationsModule,
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
declarations: [component, TruncatePipe],
|
||||
providers: [
|
||||
{ provide: TruncatableService, useValue: truncatableServiceStub },
|
||||
|
@@ -12,7 +12,7 @@
|
||||
(sortFieldChange)="onSortFieldChange($event)"
|
||||
(paginationChange)="onPaginationChange($event)">
|
||||
<ul *ngIf="objects?.hasSucceeded" class="list-unstyled" [ngClass]="{'ml-4': selectable}">
|
||||
<li *ngFor="let object of objects?.payload?.page; let i = index; let last = last" class="mt-4 mb-4 d-flex" [class.border-bottom]="hasBorder && !last">
|
||||
<li *ngFor="let object of objects?.payload?.page; let i = index; let last = last" class="mt-4 mb-4 d-flex" [class.border-bottom]="hasBorder && !last" data-test="list-object">
|
||||
<ds-selectable-list-item-control *ngIf="selectable" [index]="i"
|
||||
[object]="object"
|
||||
[selectionConfig]="selectionConfig"
|
||||
|
@@ -4,10 +4,10 @@
|
||||
<div *ngIf="showScopeSelector === true" class="input-group-prepend">
|
||||
<button class="scope-button btn btn-outline-secondary text-truncate" [ngbTooltip]="(selectedScope | async)?.name" type="button" (click)="openScopeModal()">{{(selectedScope | async)?.name || ('search.form.scope.all' | translate)}}</button>
|
||||
</div>
|
||||
<input type="text" [(ngModel)]="query" name="query" class="form-control" attr.aria-label="{{ searchPlaceholder }}"
|
||||
<input type="text" [(ngModel)]="query" name="query" class="form-control" attr.aria-label="{{ searchPlaceholder }}" data-test="search-box"
|
||||
[placeholder]="searchPlaceholder">
|
||||
<span class="input-group-append">
|
||||
<button type="submit" class="search-button btn btn-{{brandColor}}"><i class="fas fa-search"></i> {{ ('search.form.search' | translate) }}</button>
|
||||
<button type="submit" class="search-button btn btn-{{brandColor}}" data-test="search-button"><i class="fas fa-search"></i> {{ ('search.form.search' | translate) }}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -6,7 +6,8 @@
|
||||
(click)="switchViewTo(viewModeEnum.ListElement)"
|
||||
routerLinkActive="active"
|
||||
[class.active]="currentMode === viewModeEnum.ListElement"
|
||||
class="btn btn-secondary">
|
||||
class="btn btn-secondary"
|
||||
data-test="list-view">
|
||||
<i class="fas fa-list" title="{{'search.view-switch.show-list' | translate}}"></i>
|
||||
</a>
|
||||
<a *ngIf="isToShow(viewModeEnum.GridElement)"
|
||||
@@ -16,7 +17,8 @@
|
||||
(click)="switchViewTo(viewModeEnum.GridElement)"
|
||||
routerLinkActive="active"
|
||||
[class.active]="currentMode === viewModeEnum.GridElement"
|
||||
class="btn btn-secondary">
|
||||
class="btn btn-secondary"
|
||||
data-test="grid-view">
|
||||
<i class="fas fa-th-large" title="{{'search.view-switch.show-grid' | translate}}"></i>
|
||||
</a>
|
||||
<a *ngIf="isToShow(viewModeEnum.DetailedListElement)"
|
||||
@@ -26,7 +28,8 @@
|
||||
(click)="switchViewTo(viewModeEnum.DetailedListElement)"
|
||||
routerLinkActive="active"
|
||||
[class.active]="currentMode === viewModeEnum.DetailedListElement"
|
||||
class="btn btn-secondary">
|
||||
class="btn btn-secondary"
|
||||
data-test="detail-view">
|
||||
<i class="far fa-square" title="{{'search.view-switch.show-detail' | translate}}"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -2,6 +2,7 @@
|
||||
<div class="col">
|
||||
<button *ngIf="(showDepositAndDiscard | async)"
|
||||
type="button"
|
||||
id="discard"
|
||||
class="btn btn-danger"
|
||||
[disabled]="(processingSaveStatus | async) || (processingDepositStatus | async)"
|
||||
(click)="$event.preventDefault();confirmDiscard(content)">
|
||||
@@ -40,6 +41,7 @@
|
||||
</button>
|
||||
<button *ngIf="(showDepositAndDiscard | async)"
|
||||
type="button"
|
||||
id="deposit"
|
||||
class="btn btn-success"
|
||||
[disabled]="(processingSaveStatus | async) || (processingDepositStatus | async)"
|
||||
(click)="deposit($event)">
|
||||
@@ -60,7 +62,7 @@
|
||||
<p>{{'submission.general.discard.confirm.info' | translate}}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" (click)="c('cancel')">{{'submission.general.discard.confirm.cancel' | translate}}</button>
|
||||
<button type="button" class="btn btn-danger" (click)="c('ok')">{{'submission.general.discard.confirm.submit' | translate}}</button>
|
||||
<button type="button" id="discard_cancel" class="btn btn-secondary" (click)="c('cancel')">{{'submission.general.discard.confirm.cancel' | translate}}</button>
|
||||
<button type="button" id="discard_submit" class="btn btn-danger" (click)="c('ok')">{{'submission.general.discard.confirm.submit' | translate}}</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
@@ -3408,6 +3408,8 @@
|
||||
|
||||
"search.results.empty": "Your search returned no results.",
|
||||
|
||||
"search.results.view-result": "View",
|
||||
|
||||
|
||||
"default.search.results.head": "Search Results",
|
||||
|
||||
|
46
yarn.lock
46
yarn.lock
@@ -1451,7 +1451,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@csstools/convert-colors/-/convert-colors-1.4.0.tgz#ad495dc41b12e75d588c6db8b9834f08fa131eb7"
|
||||
integrity sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==
|
||||
|
||||
"@cypress/request@^2.88.6":
|
||||
"@cypress/request@^2.88.10":
|
||||
version "2.88.10"
|
||||
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.10.tgz#b66d76b07f860d3a4b8d7a0604d020c662752cce"
|
||||
integrity sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==
|
||||
@@ -2190,10 +2190,10 @@
|
||||
"@types/mime" "^1"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/sinonjs__fake-timers@^6.0.2":
|
||||
version "6.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.4.tgz#0ecc1b9259b76598ef01942f547904ce61a6a77d"
|
||||
integrity sha512-IFQTJARgMUBF+xVd2b+hIgXWrZEjND3vJtRCvIelcFB5SIXfjV4bOHbHJ0eXKh+0COrBRc8MqteKAz/j88rE0A==
|
||||
"@types/sinonjs__fake-timers@8.1.1":
|
||||
version "8.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3"
|
||||
integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==
|
||||
|
||||
"@types/sizzle@^2.3.2":
|
||||
version "2.3.3"
|
||||
@@ -3456,7 +3456,7 @@ buffer@^4.3.0:
|
||||
ieee754 "^1.1.4"
|
||||
isarray "^1.0.0"
|
||||
|
||||
buffer@^5.5.0:
|
||||
buffer@^5.5.0, buffer@^5.6.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
|
||||
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
|
||||
@@ -3883,15 +3883,14 @@ cli-spinners@^2.5.0:
|
||||
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d"
|
||||
integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==
|
||||
|
||||
cli-table3@~0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee"
|
||||
integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==
|
||||
cli-table3@~0.6.1:
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.1.tgz#36ce9b7af4847f288d3cdd081fbd09bf7bd237b8"
|
||||
integrity sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==
|
||||
dependencies:
|
||||
object-assign "^4.1.0"
|
||||
string-width "^4.2.0"
|
||||
optionalDependencies:
|
||||
colors "^1.1.2"
|
||||
colors "1.4.0"
|
||||
|
||||
cli-truncate@^2.1.0:
|
||||
version "2.1.0"
|
||||
@@ -4951,24 +4950,25 @@ cypress-axe@^0.13.0:
|
||||
resolved "https://registry.yarnpkg.com/cypress-axe/-/cypress-axe-0.13.0.tgz#3234e1a79a27701f2451fcf2f333eb74204c7966"
|
||||
integrity sha512-fCIy7RiDCm7t30U3C99gGwQrUO307EYE1QqXNaf9ToK4DVqW8y5on+0a/kUHMrHdlls2rENF6TN9ZPpPpwLrnw==
|
||||
|
||||
cypress@8.6.0:
|
||||
version "8.6.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.6.0.tgz#8d02fa58878b37cfc45bbfce393aa974fa8a8e22"
|
||||
integrity sha512-F7qEK/6Go5FsqTueR+0wEw2vOVKNgk5847Mys8vsWkzPoEKdxs+7N9Y1dit+zhaZCLtMPyrMwjfA53ZFy+lSww==
|
||||
cypress@9.5.1:
|
||||
version "9.5.1"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-9.5.1.tgz#51162f3688cedf5ffce311b914ef49a7c1ece076"
|
||||
integrity sha512-H7lUWB3Svr44gz1rNnj941xmdsCljXoJa2cDneAltjI9leKLMQLm30x6jLlpQ730tiVtIbW5HdUmBzPzwzfUQg==
|
||||
dependencies:
|
||||
"@cypress/request" "^2.88.6"
|
||||
"@cypress/request" "^2.88.10"
|
||||
"@cypress/xvfb" "^1.2.4"
|
||||
"@types/node" "^14.14.31"
|
||||
"@types/sinonjs__fake-timers" "^6.0.2"
|
||||
"@types/sinonjs__fake-timers" "8.1.1"
|
||||
"@types/sizzle" "^2.3.2"
|
||||
arch "^2.2.0"
|
||||
blob-util "^2.0.2"
|
||||
bluebird "^3.7.2"
|
||||
buffer "^5.6.0"
|
||||
cachedir "^2.3.0"
|
||||
chalk "^4.1.0"
|
||||
check-more-types "^2.24.0"
|
||||
cli-cursor "^3.1.0"
|
||||
cli-table3 "~0.6.0"
|
||||
cli-table3 "~0.6.1"
|
||||
commander "^5.1.0"
|
||||
common-tags "^1.8.0"
|
||||
dayjs "^1.10.4"
|
||||
@@ -4991,12 +4991,11 @@ cypress@8.6.0:
|
||||
ospath "^1.2.2"
|
||||
pretty-bytes "^5.6.0"
|
||||
proxy-from-env "1.0.0"
|
||||
ramda "~0.27.1"
|
||||
request-progress "^3.0.0"
|
||||
semver "^7.3.2"
|
||||
supports-color "^8.1.1"
|
||||
tmp "~0.2.1"
|
||||
untildify "^4.0.0"
|
||||
url "^0.11.0"
|
||||
yauzl "^2.10.0"
|
||||
|
||||
d@1, d@^1.0.1:
|
||||
@@ -11611,11 +11610,6 @@ raf-schd@^4.0.2:
|
||||
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a"
|
||||
integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==
|
||||
|
||||
ramda@~0.27.1:
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9"
|
||||
integrity sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw==
|
||||
|
||||
randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a"
|
||||
|
Reference in New Issue
Block a user