Merge remote-tracking branch 'artlowel/reducer-tests' into w2p-38248_PageNotFound-component

This commit is contained in:
Lotte Hofstede
2017-01-23 11:26:39 +01:00
28 changed files with 1181 additions and 71 deletions

1
.gitignore vendored
View File

@@ -6,6 +6,7 @@
npm-debug.log
/dist/
/coverage/
.idea
*.ngfactory.ts

View File

@@ -35,6 +35,7 @@ Then go to [http://localhost:3000](http://localhost:3000) in your browser
* [Running the app](#running-the-app)
* [Running in production mode](#running-in-production-mode)
* [Cleaning](#cleaning)
* [Testing](#testing)
* [Other commands](#other-commands)
* [Recommended Editors/IDEs](#recommended-editorsides)
* [Collaborating](#collaborating)
@@ -85,6 +86,49 @@ npm run clean:prod
npm run clean:dist
```
## Testing
### Unit Test
Unit tests use Karma. You can find the configuration file at the same level of this README file:
`./karma.conf.js`
If you are going to use a remote test enviroment you need to edit the './karma.conf.js'. Follow the instructions you will find inside it.
To executing tests whenever any file changes you can modify the 'autoWatch' option to 'true' and 'singleRun' option to 'false'.
A coverage report is also available at:
http://localhost:9876/
after you run:
`npm run coverage`.
To correctly run the tests you need to run the build once with:
`npm run build`.
The default browser is Google Chrome.
Place your tests in the same location of the application source code files that they test.
and run:
`npm run test`
### E2E test
E2E tests use Protractor + Selenium server + browsers. You can find the configuration file at the same level of this README file:
`./protractor.conf.js`
Protractor is installed as 'local' as a dev dependency.
If you are going to execute tests locally you need to run (once time only):
`npm run webdriver:update`.
If you are going to use a remote test enviroment you need to edit the './protractor.conf.js'. Follow the instructions you will find inside it.
The default browser is Google Chrome.
Protractor needs a functional instance of the DSpace interface to run the E2E tests, so you need to run:
`npm run watch:dev`
or any command that bring up the DSpace interface.
Place your tests at the following path:
`./e2e`
and run:
`npm run e2e`
## Other commands
There are many more commands in the `scripts` section of `package.json`. Most of these are executed by one of the commands mentioned above.
A command with a name that starts with `pre` or `post` will be executed automatically before or after the script with the matching name. e.g. if you type `npm run start` the `prestart` script will run first, then the `start` script will trigger.
@@ -111,8 +155,12 @@ See [the guide on the wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+-
dspace-angular
├── README.md * This document
├── app.json * Application manifest file
├── e2e * Folder for e2e test files
├── karma.conf.js * Unit Test configuration file
├── nodemon.json * Nodemon (https://nodemon.io/) configuration
├── package.json * This file describes the npm package for this project, its dependencies, scripts, etc.
├── postcss.config.json * PostCSS (http://postcss.org/) configuration file
├── protractor.conf.js * E2E tests configuration file
├── resources * Folder for static resources
│   ├── i18n * Folder for i18n translations
│   └── images * Folder for images
@@ -138,12 +186,13 @@ dspace-angular
├── tsconfig.json * TypeScript config for development build
├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
├── webpack.config.ts * Webpack (https://webpack.github.io/) config for development builds
├── webpack.test.config.ts * Webpack (https://webpack.github.io/) config for testing
└── webpack.prod.config.ts * Webpack (https://webpack.github.io/) config for production builds
```
## 3rd Party Library Installation
Install your library via `npm install lib-name --save` and import it in your code. `--save` will add it to `package.json`.
Install your library via `npm install lib-name --save` and import it in your code. `--save` will add it to `package.json`.
If the library does not include typings, you can install them using npm:
@@ -179,7 +228,7 @@ import * as _ from 'lodash';
## Frequently asked questions
* Why is my service, aka provider, is not injecting a parameter correctly?
* Please use `@Injectable()` for your service for typescript to correctly attach the metadata
* Please use `@Injectable()` for your service for typescript to correctly attach the metadata
* Where do I write my tests?
* You can write your tests next to your component files. e.g. for `src/app/home/home.component.ts` call it `src/app/home/home.component.spec.ts`
* How do I start the app when I get `EACCES` and `EADDRINUSE` errors?

19
e2e/app.e2e-spec.ts Normal file
View File

@@ -0,0 +1,19 @@
import { ProtractorPage } from './app.po';
describe('protractor App', function() {
let page: ProtractorPage;
beforeEach(() => {
page = new ProtractorPage();
});
it('should display title "DSpace"', () => {
page.navigateTo();
expect(page.getPageTitleText()).toEqual('DSpace');
});
it('should display title "Hello, World!"', () => {
page.navigateTo();
expect(page.getFirstPText()).toEqual('Hello, World!');
});
});

15
e2e/app.po.ts Normal file
View File

@@ -0,0 +1,15 @@
import { browser, element, by } from 'protractor';
export class ProtractorPage {
navigateTo() {
return browser.get('/');
}
getPageTitleText() {
return browser.getTitle();
}
getFirstPText() {
return element(by.xpath('//p[1]')).getText();
}
}

16
e2e/tsconfig.json Normal file
View File

@@ -0,0 +1,16 @@
{
"compileOnSave": false,
"compilerOptions": {
"declaration": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"module": "commonjs",
"moduleResolution": "node",
"outDir": "../dist/out-tsc-e2e",
"sourceMap": true,
"target": "es5",
"typeRoots": [
"../node_modules/@types"
]
}
}

24
helpers.js Normal file
View File

@@ -0,0 +1,24 @@
/**
* @author: @AngularClass
*/
var path = require('path');
// Helper functions
var ROOT = path.resolve(__dirname, '.');
function hasProcessFlag(flag) {
return process.argv.join('').indexOf(flag) > -1;
}
function isWebpackDevServer() {
return process.argv[1] && !! (/webpack-dev-server/.exec(process.argv[1]));
}
function root(args) {
args = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [ROOT].concat(args));
}
exports.hasProcessFlag = hasProcessFlag;
exports.isWebpackDevServer = isWebpackDevServer;
exports.root = root;

149
karma.conf.js Normal file
View File

@@ -0,0 +1,149 @@
/**
* @author: @AngularClass
*/
module.exports = function(config) {
var testWebpackConfig = require('./webpack.test.config.js')({env: 'test'});
// Uncomment and change to run tests on a remote Selenium server
var webdriverConfig = {
hostname: 'localhost',
port: 4444
};
var configuration = {
// base path that will be used to resolve all patterns (e.g. files, exclude)
basePath: '.',
/*
* Frameworks to use
*
* available frameworks: https://npmjs.org/browse/keyword/karma-adapter
*/
frameworks: ['jasmine'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-phantomjs-launcher'),
require('karma-webdriver-launcher'),
require('karma-coverage'),
require('karma-mocha-reporter'),
require('karma-remap-istanbul'),
require('karma-sourcemap-loader'),
require('karma-webpack')
],
// list of files to exclude
exclude: [ ],
/*
* list of files / patterns to load in the browser
*
* we are building the test environment in ./spec-bundle.js
*/
files: [ { pattern: './spec-bundle.js', watched: false } ],
/*
* preprocess matching files before serving them to the browser
* available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
*/
preprocessors: { './spec-bundle.js': ['coverage', 'webpack', 'sourcemap'] },
// Webpack Config at ./webpack.test.js
webpack: testWebpackConfig,
coverageReporter: {
reporters:[
{type: 'in-memory'},
{type: 'json', subdir: '.', file: 'coverage-final.json'},
{type: 'html', dir : 'coverage/'}
]
},
remapCoverageReporter: {
'text-summary': null,
json: './coverage/coverage.json',
html: './coverage/html'
},
remapIstanbulReporter: {
reports: {
html: 'coverage'
}
},
// Webpack please don't spam the console when running in karma!
webpackMiddleware: { stats: 'errors-only'},
/*
* test results reporter to use
*
* possible values: 'dots', 'progress'
* available reporters: https://npmjs.org/browse/keyword/karma-reporter
*/
reporters: [ 'mocha', 'coverage', 'karma-remap-istanbul' ],
// Karma web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
/*
* level of logging
* possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
*/
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
//autoWatch: true,
/*
* start these browsers
* available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
*/
browsers: [
'Chrome'
//'ChromeTravisCi',
//'SeleniumChrome',
//'SeleniumFirefox'
],
customLaunchers: {
// Continuous integraation with Chrome - launcher
'ChromeTravisCi': {
base: 'Chrome',
flags: ['--no-sandbox']
},
// Remote Selenium Server with Chrome - launcher
'SeleniumChrome': {
base: 'WebDriver',
config: webdriverConfig,
browserName: 'chrome'
},
// Remote Selenium Server with Firefox - launcher
'SeleniumFirefox': {
base: 'WebDriver',
config: webdriverConfig,
browserName: 'firefox'
}
},
/*
* Continuous Integration mode
* if true, Karma captures browsers, runs the tests and exits
*/
//singleRun: true
};
if (process.env.TRAVIS){
configuration.browsers = [
'ChromeTravisCi'
];
}
config.set(configuration);
};

View File

@@ -12,22 +12,25 @@
"clean:node": "rimraf node_modules/*",
"clean:ngc": "rimraf **/*.ngfactory.ts",
"clean:json": "rimraf *.records.json",
"clean:css": "rimraf **/*.css",
"clean:css:ts": "rimraf **/*.css.ts",
"clean:scss:ts": "rimraf **/*.scss.ts",
"clean:css:shim:ts": "rimraf **/*.css.shim.ts",
"clean:scss:shim:ts": "rimraf **/*.scss.shim.ts",
"clean:css": "rimraf src/**/*.css",
"clean:css:ts": "rimraf src/**/*.css.ts",
"clean:scss:ts": "rimraf src/**/*.scss.ts",
"clean:css:shim:ts": "rimraf src/**/*.css.shim.ts",
"clean:scss:shim:ts": "rimraf src/**/*.scss.shim.ts",
"clean:coverage": "rimraf coverage",
"clean:prod": "npm run clean:ngc && npm run clean:json && npm run clean:css && npm run clean:css:ts && npm run clean:scss:ts && npm run clean:css:shim:ts && npm run clean:scss:shim:ts && npm run clean:dist",
"clean": "npm run clean:log && npm run clean:prod && npm run clean:node",
"clean": "npm run clean:log && npm run clean:prod && npm run clean:coverage && npm run clean:node",
"sass": "node-sass src -o src --include-path node_modules --output-style compressed -q",
"sass:watch": "node-sass -w src -o src --include-path node_modules --output-style compressed -q",
"postcss": "node_modules/postcss-cli/bin/postcss -c postcss.config.json",
"style": "npm run sass && npm run postcss",
"style:watch": "nodemon -e scss -w src -x \"npm run style\"",
"rollup": "rollup -c rollup-server.js && rollup -c rollup-client.js",
"prebuild": "npm run clean:dist && npm run sass",
"prebuild": "npm run clean:dist && npm run style",
"build": "webpack --progress",
"build:prod": "webpack --config webpack.prod.config.ts",
"build:prod:rollup": "npm run build:prod && npm run rollup",
"build:prod:ngc": "npm run clean:prod && npm run sass && npm run ngc && npm run build:prod:rollup",
"build:prod:ngc:json": "npm run clean:prod && npm run sass && npm run ngc && npm run build:prod:json:rollup",
"build:prod:ngc": "npm run clean:prod && npm run style && npm run ngc && npm run build:prod:rollup",
"build:prod:ngc:json": "npm run clean:prod && npm run style && npm run ngc && npm run build:prod:json:rollup",
"build:prod:json": "webpack --config webpack.prod.config.ts --json | webpack-bundle-size-analyzer",
"build:prod:json:rollup": "npm run build:prod:json && npm run rollup",
"ngc": "ngc -p tsconfig.aot.json",
@@ -36,7 +39,7 @@
"server:dev": "nodemon --debug dist/server/index.js",
"start": "npm run server",
"start:dev": "npm run clean:prod && npm run build && npm run server",
"watch": "webpack -w & npm run sass:watch",
"watch": "webpack -w & npm run style:watch",
"watch:dev:server": "concurrently \"npm run server:dev\" \"npm run watch\"",
"watch:dev": "npm run clean:prod && npm run build && npm run watch:dev:server",
"watch:prod:server": "concurrently \"npm run server\" \"npm run watch\"",
@@ -49,7 +52,14 @@
"debug:build:prod": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js --config webpack.prod.config.ts",
"docs": "typedoc --options typedoc.json ./src",
"lint": "tslint \"src/**/*.ts\" || true",
"global": "npm install -g angular-cli nodemon npm-check-updates rimraf ts-node typedoc typescript webpack webpack-bundle-size-analyzer rollup marked node-gyp"
"global": "npm install -g angular-cli nodemon npm-check-updates rimraf ts-node typedoc typescript webpack webpack-bundle-size-analyzer rollup marked node-gyp",
"protractor": "node node_modules/protractor/bin/protractor",
"e2e": "npm run protractor",
"test": "karma start --single-run",
"test:watch": "karma start --no-single-run --auto-watch",
"coverage": "http-server -c-1 -o -p 9875 ./coverage",
"webdriver:start": "node node_modules/protractor/bin/webdriver-manager start --seleniumPort 4444",
"webdriver:update": "node node_modules/protractor/bin/webdriver-manager update --standalone"
},
"dependencies": {
"@angular/common": "2.2.3",
@@ -75,11 +85,13 @@
"angular2-platform-node": "2.1.0-rc.1",
"angular2-universal": "2.1.0-rc.1",
"angular2-universal-polyfills": "2.1.0-rc.1",
"autoprefixer": "^6.5.4",
"body-parser": "1.15.2",
"bootstrap": "4.0.0-alpha.5",
"compression": "1.6.2",
"express": "4.14.0",
"font-awesome": "4.7.0",
"http-server": "^0.9.0",
"js.clone": "0.0.3",
"methods": "1.1.2",
"morgan": "1.7.0",
@@ -94,15 +106,19 @@
"@types/body-parser": "0.0.33",
"@types/compression": "0.0.33",
"@types/cookie-parser": "1.3.30",
"@types/deep-freeze": "0.0.29",
"@types/express": "4.0.34",
"@types/express-serve-static-core": "4.0.39",
"@types/hammerjs": "2.0.33",
"@types/jasmine": "^2.2.34",
"@types/memory-cache": "0.0.29",
"@types/mime": "0.0.29",
"@types/morgan": "1.7.32",
"@types/node": "6.0.52",
"@types/serve-static": "1.7.31",
"@types/webfontloader": "1.6.27",
"ajv": "4.2.0",
"ajv-keywords": "1.1.1",
"angular2-template-loader": "0.6.0",
"autoprefixer": "6.5.4",
"awesome-typescript-loader": "2.2.4",
@@ -110,10 +126,30 @@
"concurrently": "3.1.0",
"cookie-parser": "1.4.3",
"copy-webpack-plugin": "4.0.1",
"css-loader": "^0.26.0",
"deep-freeze": "0.0.1",
"html-webpack-plugin": "^2.21.0",
"imports-loader": "0.7.0",
"istanbul-instrumenter-loader": "^0.2.0",
"jasmine-core": "~2.5.2",
"jasmine-spec-reporter": "~2.7.0",
"json-loader": "0.5.4",
"karma": "^1.2.0",
"karma-chrome-launcher": "^2.0.0",
"karma-cli": "^1.0.1",
"karma-coverage": "^1.1.1",
"karma-jasmine": "^1.0.2",
"karma-mocha-reporter": "^2.0.0",
"karma-phantomjs-launcher": "^1.0.2",
"karma-remap-istanbul": "^0.2.1",
"karma-sourcemap-loader": "^0.3.7",
"karma-webdriver-launcher": "^1.0.4",
"karma-webpack": "1.8.0",
"node-sass": "4.0.0",
"nodemon": "1.11.0",
"postcss-cli": "^2.6.0",
"protractor": "~4.0.14",
"protractor-istanbul-plugin": "~2.0.0",
"raw-loader": "0.5.1",
"reflect-metadata": "0.1.8",
"rimraf": "2.5.4",
@@ -122,7 +158,9 @@
"rollup-plugin-node-globals": "1.1.0",
"rollup-plugin-node-resolve": "2.0.0",
"rollup-plugin-uglify": "1.0.1",
"source-map-loader": "^0.1.5",
"string-replace-loader": "1.0.5",
"to-string-loader": "^1.1.4",
"ts-helpers": "1.1.2",
"ts-node": "1.7.2",
"tslint": "4.0.2",

9
postcss.config.json Normal file
View File

@@ -0,0 +1,9 @@
{
"use": ["autoprefixer"],
"input": "src/**/*.css",
"replace": true,
"local-plugins": true,
"autoprefixer": {
"browsers": "last 2 versions"
}
}

85
protractor.conf.js Normal file
View File

@@ -0,0 +1,85 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/docs/referenceConf.js
/*global jasmine */
var SpecReporter = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
// -----------------------------------------------------------------
// Uncomment to run tests using a remote Selenium server
//seleniumAddress: 'http://selenium.address:4444/wd/hub',
// Change to 'false' to run tests using a remote Selenium server
directConnect: true,
// Change if the website to test is not on the localhost
baseUrl: 'http://localhost:3000/',
// -----------------------------------------------------------------
specs: [
'./e2e/**/*.e2e-spec.ts'
],
// -----------------------------------------------------------------
// Browser and Capabilities: PhantomJS
// -----------------------------------------------------------------
// capabilities: {
// 'browserName': 'phantomjs',
// 'version': '',
// 'platform': 'ANY'
// },
// -----------------------------------------------------------------
// Browser and Capabilities: Chrome
// -----------------------------------------------------------------
capabilities: {
'browserName': 'chrome',
'version': '',
'platform': 'ANY'
},
// -----------------------------------------------------------------
// Browser and Capabilities: Firefox
// -----------------------------------------------------------------
// capabilities: {
// 'browserName': 'firefox',
// 'version': '',
// 'platform': 'ANY'
// },
// -----------------------------------------------------------------
// Browser and Capabilities: MultiCapabilities
// -----------------------------------------------------------------
//multiCapabilities: [
// {
// 'browserName': 'phantomjs',
// 'version': '',
// 'platform': 'ANY'
// },
// {
// 'browserName': 'chrome',
// 'version': '',
// 'platform': 'ANY'
// }
// {
// 'browserName': 'firefox',
// 'version': '',
// 'platform': 'ANY'
// }
//],
plugins : [{
path: 'node_modules/protractor-istanbul-plugin'
}],
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
useAllAngular2AppRoots: true,
beforeLaunch: function() {
require('ts-node').register({
project: 'e2e'
});
},
onPrepare: function() {
jasmine.getEnv().addReporter(new SpecReporter());
}
};

62
spec-bundle.js Normal file
View File

@@ -0,0 +1,62 @@
/**
* @author: @AngularClass
*/
/*
* When testing with webpack and ES6, we have to do some extra
* things to get testing to work right. Because we are gonna write tests
* in ES6 too, we have to compile those as well. That's handled in
* karma.conf.js with the karma-webpack plugin. This is the entry
* file for webpack test. Just like webpack will create a bundle.js
* file for our client, when we run test, it will compile and bundle them
* all here! Crazy huh. So we need to do some setup
*/
Error.stackTraceLimit = Infinity;
require('core-js/es6');
require('core-js/es7/reflect');
// Typescript emit helpers polyfill
require('ts-helpers');
require('zone.js/dist/zone');
require('zone.js/dist/long-stack-trace-zone');
require('zone.js/dist/proxy'); // since zone.js 0.6.15
require('zone.js/dist/sync-test');
require('zone.js/dist/jasmine-patch'); // put here since zone.js 0.6.14
require('zone.js/dist/async-test');
require('zone.js/dist/fake-async-test');
// RxJS
require('rxjs/Rx');
var testing = require('@angular/core/testing');
var browser = require('@angular/platform-browser-dynamic/testing');
testing.TestBed.initTestEnvironment(
browser.BrowserDynamicTestingModule,
browser.platformBrowserDynamicTesting()
);
/*
* Ok, this is kinda crazy. We can use the context method on
* require that webpack created in order to tell webpack
* what files we actually want to require or import.
* Below, context will be a function/object with file names as keys.
* Using that regex we are saying look in ../src then find
* any file that ends with spec.ts and get its path. By passing in true
* we say do this recursively
*/
var testContext = require.context('./src', true, /\.spec\.ts/);
/*
* get all the files, for each file, call the context function
* that will require the file and load it up here. Context will
* loop and require those spec files here
*/
function requireAll(requireContext) {
return requireContext.keys().map(requireContext);
}
// requires and returns all modules that match
var modules = requireAll(testContext);

View File

@@ -0,0 +1,81 @@
// ... test imports
import {
async,
ComponentFixture,
inject,
TestBed
} from '@angular/core/testing';
import {
CUSTOM_ELEMENTS_SCHEMA,
DebugElement
} from "@angular/core";
import { By } from '@angular/platform-browser';
import { TranslateModule, TranslateLoader } from "ng2-translate";
import { Store, StoreModule } from "@ngrx/store";
// Load the implementations that should be tested
import { AppComponent } from './app.component';
import { CommonModule } from '@angular/common';
import { HostWindowState } from "./shared/host-window.reducer";
import { HostWindowResizeAction } from "./shared/host-window.actions";
import { MockTranslateLoader } from "./shared/testing/mock-translate-loader";
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let de: DebugElement;
let el: HTMLElement;
describe('App component', () => {
// async beforeEach
beforeEach(async(() => {
return TestBed.configureTestingModule({
imports: [CommonModule, StoreModule.provideStore({}), TranslateModule.forRoot({
provide: TranslateLoader,
useClass: MockTranslateLoader
})],
declarations: [AppComponent], // declare the test component
providers: [
AppComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
}));
// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance; // component test instance
// query for the title <p> by CSS element selector
de = fixture.debugElement.query(By.css('p'));
el = de.nativeElement;
});
it('should create component', inject([AppComponent], (app: AppComponent) => {
// Perform test using fixture and service
expect(app).toBeTruthy();
}));
describe("when the window is resized", () => {
let width: number;
let height: number;
let store: Store<HostWindowState>;
beforeEach(() => {
store = fixture.debugElement.injector.get(Store);
spyOn(store, 'dispatch');
window.dispatchEvent(new Event('resize'));
width = window.innerWidth;
height = window.innerHeight;
});
it("should dispatch a HostWindowResizeAction with the width and height of the window as its payload", () => {
expect(store.dispatch).toHaveBeenCalledWith(new HostWindowResizeAction(width, height));
});
});
});

View File

@@ -8,7 +8,7 @@ import {
import { TranslateService } from "ng2-translate";
import { HostWindowState } from "./shared/host-window.reducer";
import { Store } from "@ngrx/store";
import { HostWindowActions } from "./shared/host-window.actions";
import { HostWindowResizeAction } from "./shared/host-window.actions";
@Component({
changeDetection: ChangeDetectionStrategy.Default,
@@ -52,7 +52,7 @@ export class AppComponent implements OnDestroy, OnInit {
@HostListener('window:resize', ['$event'])
private onResize(event): void {
this.store.dispatch(
HostWindowActions.resize(event.target.innerWidth, event.target.innerHeight)
new HostWindowResizeAction(event.target.innerWidth, event.target.innerHeight)
);
}

View File

@@ -1,24 +1,43 @@
import { Action } from "@ngrx/store";
import { type } from "../shared/ngrx/type";
export class HeaderActions {
static COLLAPSE = 'dspace/header/COLLAPSE';
static collapse(): Action {
return {
type: HeaderActions.COLLAPSE
}
}
/**
* For each action type in an action group, make a simple
* enum object for all of this group's action types.
*
* The 'type' utility function coerces strings into string
* literal types and runs a simple check to guarantee all
* action types in the application are unique.
*/
export const HeaderActionTypes = {
COLLAPSE: type('dspace/header/COLLAPSE'),
EXPAND: type('dspace/header/EXPAND'),
TOGGLE: type('dspace/header/TOGGLE')
};
static EXPAND = 'dspace/header/EXPAND';
static expand(): Action {
return {
type: HeaderActions.EXPAND
}
}
export class HeaderCollapseAction implements Action {
type = HeaderActionTypes.COLLAPSE;
static TOGGLE = 'dspace/header/TOGGLE';
static toggle(): Action {
return {
type: HeaderActions.TOGGLE
}
}
constructor() {}
}
export class HeaderExpandAction implements Action {
type = HeaderActionTypes.EXPAND;
constructor() {}
}
export class HeaderToggleAction implements Action {
type = HeaderActionTypes.TOGGLE;
constructor() {}
}
/**
* Export a type alias of all actions in this action group
* so that reducers can easily compose action types
*/
export type HeaderAction
= HeaderCollapseAction
| HeaderExpandAction
| HeaderToggleAction

View File

@@ -0,0 +1,81 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { HeaderComponent } from "./header.component";
import { Store, StoreModule } from "@ngrx/store";
import { HeaderState } from "./header.reducer";
import Spy = jasmine.Spy;
import { HeaderToggleAction } from "./header.actions";
import { TranslateModule } from "ng2-translate";
import { NgbCollapseModule } from "@ng-bootstrap/ng-bootstrap";
import { Observable } from "rxjs";
let comp: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
let store: Store<HeaderState>;
describe('HeaderComponent', () => {
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ StoreModule.provideStore({}), TranslateModule.forRoot(), NgbCollapseModule.forRoot() ],
declarations: [ HeaderComponent ]
})
.compileComponents(); // compile template and css
}));
// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(HeaderComponent);
comp = fixture.componentInstance;
store = fixture.debugElement.injector.get(Store);
spyOn(store, 'dispatch');
});
describe('when the toggle button is clicked', () => {
beforeEach(() => {
let navbarToggler = fixture.debugElement.query(By.css('.navbar-toggler'));
navbarToggler.triggerEventHandler('click', null);
});
it("should dispatch a HeaderToggleAction", () => {
expect(store.dispatch).toHaveBeenCalledWith(new HeaderToggleAction());
});
});
describe("when navCollapsed in the store is true", () => {
let menu: HTMLElement;
beforeEach(() => {
menu = fixture.debugElement.query(By.css('#collapsingNav')).nativeElement;
spyOn(store, 'select').and.returnValue(Observable.of({ navCollapsed: true }));
fixture.detectChanges();
});
it("should close the menu", () => {
expect(menu.classList).not.toContain('in');
});
});
describe("when navCollapsed in the store is false", () => {
let menu: HTMLElement;
beforeEach(() => {
menu = fixture.debugElement.query(By.css('#collapsingNav')).nativeElement;
spyOn(store, 'select').and.returnValue(Observable.of({ navCollapsed: false }));
fixture.detectChanges();
});
it("should open the menu", () => {
expect(menu.classList).toContain('in');
});
});
});

View File

@@ -1,9 +1,8 @@
import { Component, OnInit } from "@angular/core";
import { Store } from "@ngrx/store";
import { HeaderState } from "./header.reducer";
import { HeaderActions } from "./header.actions";
import { Observable } from "rxjs";
import 'rxjs/add/operator/filter';
import { HeaderToggleAction } from "./header.actions";
@Component({
selector: 'ds-header',
@@ -24,16 +23,8 @@ export class HeaderComponent implements OnInit {
.map(({ navCollapsed }: HeaderState) => navCollapsed);
}
private collapse(): void {
this.store.dispatch(HeaderActions.collapse());
}
private expand(): void {
this.store.dispatch(HeaderActions.expand());
}
public toggle(): void {
this.store.dispatch(HeaderActions.toggle());
this.store.dispatch(new HeaderToggleAction());
}
}

View File

@@ -0,0 +1,53 @@
import { TestBed, inject } from "@angular/core/testing";
import { EffectsTestingModule, EffectsRunner } from '@ngrx/effects/testing';
import { HeaderEffects } from "./header.effects";
import { HeaderCollapseAction } from "./header.actions";
import { HostWindowResizeAction } from "../shared/host-window.actions";
import { routerActions } from "@ngrx/router-store";
describe('HeaderEffects', () => {
beforeEach(() => TestBed.configureTestingModule({
imports: [
EffectsTestingModule
],
providers: [
HeaderEffects
]
}));
let runner: EffectsRunner;
let headerEffects: HeaderEffects;
beforeEach(inject([
EffectsRunner, HeaderEffects
],
(_runner, _headerEffects) => {
runner = _runner;
headerEffects = _headerEffects;
}
));
describe('resize$', () => {
it('should return a COLLAPSE action in response to a RESIZE action', () => {
runner.queue(new HostWindowResizeAction(800,600));
headerEffects.resize$.subscribe(result => {
expect(result).toEqual(new HeaderCollapseAction());
});
});
});
describe('routeChange$', () => {
it('should return a COLLAPSE action in response to an UPDATE_LOCATION action', () => {
runner.queue({ type: routerActions.UPDATE_LOCATION });
headerEffects.resize$.subscribe(result => {
expect(result).toEqual(new HeaderCollapseAction());
});
});
});
});

View File

@@ -1,8 +1,8 @@
import { Injectable } from "@angular/core";
import { Effect, Actions } from '@ngrx/effects'
import { HeaderActions } from "./header.actions";
import { HostWindowActions } from "../shared/host-window.actions";
import { HostWindowActionTypes } from "../shared/host-window.actions";
import { routerActions } from "@ngrx/router-store";
import { HeaderCollapseAction } from "./header.actions";
@Injectable()
export class HeaderEffects {
@@ -12,10 +12,10 @@ export class HeaderEffects {
) { }
@Effect() resize$ = this.actions$
.ofType(HostWindowActions.RESIZE)
.map(() => HeaderActions.collapse());
.ofType(HostWindowActionTypes.RESIZE)
.map(() => new HeaderCollapseAction());
@Effect() routeChange$ = this.actions$
.ofType(routerActions.UPDATE_LOCATION)
.map(() => HeaderActions.collapse());
.map(() => new HeaderCollapseAction());
}

View File

@@ -0,0 +1,89 @@
import * as deepFreeze from "deep-freeze";
import { headerReducer } from "./header.reducer";
import {
HeaderCollapseAction,
HeaderExpandAction,
HeaderToggleAction
} from "./header.actions";
class NullAction extends HeaderCollapseAction {
type = null;
constructor() {
super();
}
}
describe("headerReducer", () => {
it("should return the current state when no valid actions have been made", () => {
const state = { navCollapsed: false };
const action = new NullAction();
const newState = headerReducer(state, action);
expect(newState).toEqual(state);
});
it("should start with navCollapsed = true", () => {
const action = new NullAction();
const initialState = headerReducer(undefined, action);
// The navigation starts collapsed
expect(initialState.navCollapsed).toEqual(true);
});
it("should set navCollapsed to true in response to the COLLAPSE action", () => {
const state = { navCollapsed: false };
const action = new HeaderCollapseAction();
const newState = headerReducer(state, action);
expect(newState.navCollapsed).toEqual(true);
});
it("should perform the COLLAPSE action without affecting the previous state", () => {
const state = { navCollapsed: false };
deepFreeze(state);
const action = new HeaderCollapseAction();
headerReducer(state, action);
//no expect required, deepFreeze will ensure an exception is thrown if the state
//is mutated, and any uncaught exception will cause the test to fail
});
it("should set navCollapsed to false in response to the EXPAND action", () => {
const state = { navCollapsed: true };
const action = new HeaderExpandAction();
const newState = headerReducer(state, action);
expect(newState.navCollapsed).toEqual(false);
});
it("should perform the EXPAND action without affecting the previous state", () => {
const state = { navCollapsed: true };
deepFreeze(state);
const action = new HeaderExpandAction();
headerReducer(state, action);
});
it("should flip the value of navCollapsed in response to the TOGGLE action", () => {
const state1 = { navCollapsed: true };
const action = new HeaderToggleAction();
const state2 = headerReducer(state1, action);
const state3 = headerReducer(state2, action);
expect(state2.navCollapsed).toEqual(false);
expect(state3.navCollapsed).toEqual(true);
});
it("should perform the TOGGLE action without affecting the previous state", () => {
const state = { navCollapsed: true };
deepFreeze(state);
const action = new HeaderToggleAction();
headerReducer(state, action);
});
});

View File

@@ -1,5 +1,4 @@
import { Action } from "@ngrx/store";
import { HeaderActions } from "./header.actions";
import { HeaderAction, HeaderActionTypes } from "./header.actions";
export interface HeaderState {
navCollapsed: boolean;
@@ -9,23 +8,23 @@ const initialState: HeaderState = {
navCollapsed: true
};
export const headerReducer = (state = initialState, action: Action): HeaderState => {
export const headerReducer = (state = initialState, action: HeaderAction): HeaderState => {
switch (action.type) {
case HeaderActions.COLLAPSE: {
case HeaderActionTypes.COLLAPSE: {
return Object.assign({}, state, {
navCollapsed: true
});
}
case HeaderActions.EXPAND: {
case HeaderActionTypes.EXPAND: {
return Object.assign({}, state, {
navCollapsed: false
});
}
case HeaderActions.TOGGLE: {
case HeaderActionTypes.TOGGLE: {
return Object.assign({}, state, {
navCollapsed: !state.navCollapsed
});

View File

@@ -1,14 +1,21 @@
import { Action } from "@ngrx/store";
import { type } from "./ngrx/type";
export class HostWindowActions {
static RESIZE = 'dspace/host-window/RESIZE';
static resize(newWidth: number, newHeight: number): Action {
return {
type: HostWindowActions.RESIZE,
payload: {
width: newWidth,
height: newHeight
}
}
export const HostWindowActionTypes = {
RESIZE: type('dspace/host-window/RESIZE')
};
export class HostWindowResizeAction implements Action {
type = HostWindowActionTypes.RESIZE;
payload: {
width: number;
height: number;
};
constructor(width: number, height: number) {
this.payload = { width, height }
}
}
export type HostWindowAction
= HostWindowResizeAction;

View File

@@ -0,0 +1,48 @@
import * as deepFreeze from "deep-freeze";
import { hostWindowReducer } from "./host-window.reducer";
import { HostWindowResizeAction } from "./host-window.actions";
class NullAction extends HostWindowResizeAction {
type = null;
constructor() {
super(0,0);
}
}
describe('hostWindowReducer', () => {
it("should return the current state when no valid actions have been made", () => {
const state = { width: 800, height: 600 };
const action = new NullAction();
const newState = hostWindowReducer(state, action);
expect(newState).toEqual(state);
});
it("should start with width = null and height = null", () => {
const action = new NullAction();
const initialState = hostWindowReducer(undefined, action);
expect(initialState.width).toEqual(null);
expect(initialState.height).toEqual(null);
});
it("should update the width and height in the state in response to a RESIZE action", () => {
const state = { width: 800, height: 600 };
const action = new HostWindowResizeAction(1024, 768);
const newState = hostWindowReducer(state, action);
expect(newState.width).toEqual(1024);
expect(newState.height).toEqual(768);
});
it("should perform the RESIZE action without affecting the previous state", () => {
const state = { width: 800, height: 600 };
deepFreeze(state);
const action = new HostWindowResizeAction(1024, 768);
hostWindowReducer(state, action);
});
});

View File

@@ -1,5 +1,4 @@
import { Action } from "@ngrx/store";
import { HostWindowActions } from "./host-window.actions";
import { HostWindowAction, HostWindowActionTypes } from "./host-window.actions";
export interface HostWindowState {
width: number;
@@ -11,10 +10,10 @@ const initialState: HostWindowState = {
height: null
};
export const hostWindowReducer = (state = initialState, action: Action): HostWindowState => {
export const hostWindowReducer = (state = initialState, action: HostWindowAction): HostWindowState => {
switch (action.type) {
case HostWindowActions.RESIZE: {
case HostWindowActionTypes.RESIZE: {
return Object.assign({}, state, action.payload);
}

View File

@@ -0,0 +1,24 @@
/**
* Based on
* https://github.com/ngrx/example-app/blob/master/src/app/util.ts
*
* This function coerces a string into a string literal type.
* Using tagged union types in TypeScript 2.0, this enables
* powerful typechecking of our reducers.
*
* Since every action label passes through this function it
* is a good place to ensure all of our action labels
* are unique.
*/
let typeCache: { [label: string]: boolean } = {};
export function type<T>(label: T | ''): T {
if (typeCache[<string>label]) {
throw new Error(`Action type "${label}" is not unique"`);
}
typeCache[<string>label] = true;
return <T>label;
}

View File

@@ -0,0 +1,8 @@
import { TranslateLoader } from "ng2-translate";
import { Observable } from "rxjs";
export class MockTranslateLoader implements TranslateLoader {
getTranslation(lang: string): Observable<any> {
return Observable.of({});
}
}

View File

@@ -7,6 +7,7 @@
"removeComments": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"noEmitHelpers": true,

View File

@@ -67,6 +67,7 @@ export var commonConfig = {
}
],
},
plugins: [
// Use commonPlugins.
]

242
webpack.test.config.js Normal file
View File

@@ -0,0 +1,242 @@
/**
* @author: @AngularClass
*/
const helpers = require('./helpers');
const path = require('path');
/**
* Webpack Plugins
*/
const ProvidePlugin = require('webpack/lib/ProvidePlugin');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin');
const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin');
/**
* Webpack Constants
*/
const ENV = process.env.ENV = process.env.NODE_ENV = 'test';
/**
* Webpack configuration
*
* See: http://webpack.github.io/docs/configuration.html#cli
*/
module.exports = function (options) {
return {
/**
* Source map for Karma from the help of karma-sourcemap-loader & karma-webpack
*
* Do not change, leave as is or it wont work.
* See: https://github.com/webpack/karma-webpack#source-maps
*/
devtool: 'inline-source-map',
/**
* Options affecting the resolving of modules.
*
* See: http://webpack.github.io/docs/configuration.html#resolve
*/
resolve: {
/**
* An array of extensions that should be used to resolve modules.
*
* See: http://webpack.github.io/docs/configuration.html#resolve-extensions
*/
extensions: ['.ts', '.js'],
/**
* Make sure root is src
*/
modules: [ path.resolve(__dirname, 'src'), 'node_modules' ]
},
/**
* Options affecting the normal modules.
*
* See: http://webpack.github.io/docs/configuration.html#module
*
* 'use:' revered back to 'loader:' as a temp. workaround for #1188
* See: https://github.com/AngularClass/angular2-webpack-starter/issues/1188#issuecomment-262872034
*/
module: {
rules: [
/**
* Source map loader support for *.js files
* Extracts SourceMaps for source files that as added as sourceMappingURL comment.
*
* See: https://github.com/webpack/source-map-loader
*/
{
enforce: 'pre',
test: /\.js$/,
loader: 'source-map-loader',
exclude: [
// these packages have problems with their sourcemaps
helpers.root('node_modules/rxjs'),
helpers.root('node_modules/@angular')
]
},
/**
* Typescript loader support for .ts and Angular 2 async routes via .async.ts
*
* See: https://github.com/s-panferov/awesome-typescript-loader
*/
{
test: /\.ts$/,
loaders: [
{
loader: 'awesome-typescript-loader',
query: {
// use inline sourcemaps for "karma-remap-coverage" reporter
sourceMap: false,
inlineSourceMap: true,
sourceRoot: false,
compilerOptions: {
// Remove TypeScript helpers to be injected
// below by DefinePlugin
removeComments: true
}
}
},
'angular2-template-loader'
],
exclude: [/\.e2e\.ts$/]
},
/**
* Json loader support for *.json files.
*
* See: https://github.com/webpack/json-loader
*/
{
test: /\.json$/,
loader: 'json-loader',
exclude: [helpers.root('src/index.html')]
},
/**
* Raw loader support for *.css files
* Returns file content as string
*
* See: https://github.com/webpack/raw-loader
*/
{
test: /\.css$/,
loader: ['to-string-loader', 'css-loader'],
exclude: [helpers.root('src/index.html')]
},
/**
* Raw loader support for *.html
* Returns file content as string
*
* See: https://github.com/webpack/raw-loader
*/
{
test: /\.html$/,
loader: 'raw-loader',
exclude: [helpers.root('src/index.html')]
},
/**
* Instruments JS files with Istanbul for subsequent code coverage reporting.
* Instrument only testing sources.
*
* See: https://github.com/deepsweet/istanbul-instrumenter-loader
*/
{
enforce: 'post',
test: /\.(js|ts)$/,
loader: 'istanbul-instrumenter-loader',
include: helpers.root('src'),
exclude: [
/\.(e2e|spec)\.ts$/,
/node_modules/
]
}
]
},
/**
* Add additional plugins to the compiler.
*
* See: http://webpack.github.io/docs/configuration.html#plugins
*/
plugins: [
/**
* Plugin: DefinePlugin
* Description: Define free variables.
* Useful for having development builds with debug logging or adding global constants.
*
* Environment helpers
*
* See: https://webpack.github.io/docs/list-of-plugins.html#defineplugin
*/
// NOTE: when adding more properties make sure you include them in custom-typings.d.ts
new DefinePlugin({
'ENV': JSON.stringify(ENV),
'HMR': false,
'process.env': {
'ENV': JSON.stringify(ENV),
'NODE_ENV': JSON.stringify(ENV),
'HMR': false,
}
}),
/**
* Plugin: ContextReplacementPlugin
* Description: Provides context to Angular's use of System.import
*
* See: https://webpack.github.io/docs/list-of-plugins.html#contextreplacementplugin
* See: https://github.com/angular/angular/issues/11580
*/
new ContextReplacementPlugin(
// The (\\|\/) piece accounts for path separators in *nix and Windows
/angular(\\|\/)core(\\|\/)(esm(\\|\/)src|src)(\\|\/)linker/,
helpers.root('src'), // location of your src
{
// your Angular Async Route paths relative to this root directory
}
),
/**
* Plugin LoaderOptionsPlugin (experimental)
*
* See: https://gist.github.com/sokra/27b24881210b56bbaff7
*/
new LoaderOptionsPlugin({
debug: true,
options: {
}
}),
],
/**
* Include polyfills or mocks for various node stuff
* Description: Node configuration
*
* See: https://webpack.github.io/docs/configuration.html#node
*/
node: {
global: true,
process: false,
crypto: 'empty',
module: false,
clearImmediate: false,
setImmediate: false
}
};
}